Skip to content

Commit 2fbbe12

Browse files
committed
feat: support zoneedit
1 parent 07cdce1 commit 2fbbe12

File tree

5 files changed

+198
-0
lines changed

5 files changed

+198
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Light container updating DNS A and/or AAAA records periodically for multiple DNS
6969
- Spdyn
7070
- Strato.de
7171
- Variomedia.de
72+
- Zoneedit
7273
- **Want more?** [Create an issue for it](https://github.com/qdm12/ddns-updater/issues/new/choose)!
7374
- Web User interface
7475

@@ -195,6 +196,7 @@ Check the documentation for your DNS provider:
195196
- [Spdyn](https://github.com/qdm12/ddns-updater/blob/master/docs/spdyn.md)
196197
- [Strato.de](https://github.com/qdm12/ddns-updater/blob/master/docs/strato.md)
197198
- [Variomedia.de](https://github.com/qdm12/ddns-updater/blob/master/docs/variomedia.md)
199+
- [Zoneedit](https://github.com/qdm12/ddns-updater/blob/master/docs/zoneedit.md)
198200

199201
Note that:
200202

docs/zoneedit.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Zoneedit
2+
3+
## Configuration
4+
5+
### Example
6+
7+
```json
8+
{
9+
"settings": [
10+
{
11+
"provider": "zoneedit",
12+
"domain": "domain.com",
13+
"host": "@",
14+
"username": "username",
15+
"token": "token",
16+
"ip_version": "ipv4",
17+
"provider_ip": true
18+
}
19+
]
20+
}
21+
```
22+
23+
### Compulsory parameters
24+
25+
- `"domain"`
26+
- `"host"` is your host and can be a subdomain or `"@"` or `"*"`
27+
- `"username"`
28+
- `"token"`
29+
30+
### Optional parameters
31+
32+
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
33+
- `"provider_ip"` can be set to `true` to let your DNS provider determine your IPv4 address (and/or IPv6 address) automatically when you send an update request, without sending the new IP address detected by the program in the request.
34+
35+
## Domain setup
36+
37+
[support.zoneedit.com/en/knowledgebase/article/dynamic-dns](https://support.zoneedit.com/en/knowledgebase/article/dynamic-dns)

internal/provider/constants/providers.go

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ const (
4141
Spdyn models.Provider = "spdyn"
4242
Strato models.Provider = "strato"
4343
Variomedia models.Provider = "variomedia"
44+
Zoneedit models.Provider = "zoneedit"
4445
)
4546

4647
func ProviderChoices() []models.Provider {
@@ -80,5 +81,6 @@ func ProviderChoices() []models.Provider {
8081
Spdyn,
8182
Strato,
8283
Variomedia,
84+
Zoneedit,
8385
}
8486
}

internal/provider/provider.go

+3
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ import (
4747
"github.com/qdm12/ddns-updater/internal/provider/providers/spdyn"
4848
"github.com/qdm12/ddns-updater/internal/provider/providers/strato"
4949
"github.com/qdm12/ddns-updater/internal/provider/providers/variomedia"
50+
"github.com/qdm12/ddns-updater/internal/provider/providers/zoneedit"
5051
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
5152
)
5253

@@ -141,6 +142,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, host string
141142
return strato.New(data, domain, host, ipVersion)
142143
case constants.Variomedia:
143144
return variomedia.New(data, domain, host, ipVersion)
145+
case constants.Zoneedit:
146+
return zoneedit.New(data, domain, host, ipVersion)
144147
default:
145148
return nil, fmt.Errorf("%w: %s", ErrProviderUnknown, providerName)
146149
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package zoneedit
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/netip"
10+
"net/url"
11+
"strings"
12+
13+
"github.com/qdm12/ddns-updater/internal/models"
14+
"github.com/qdm12/ddns-updater/internal/settings/constants"
15+
"github.com/qdm12/ddns-updater/internal/settings/errors"
16+
"github.com/qdm12/ddns-updater/internal/settings/headers"
17+
"github.com/qdm12/ddns-updater/internal/settings/utils"
18+
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
19+
)
20+
21+
type Provider struct {
22+
domain string
23+
host string
24+
ipVersion ipversion.IPVersion
25+
username string
26+
token string
27+
useProviderIP bool
28+
}
29+
30+
func New(data json.RawMessage, domain, host string,
31+
ipVersion ipversion.IPVersion) (p *Provider, err error) {
32+
extraSettings := struct {
33+
Username string `json:"username"`
34+
Token string `json:"token"`
35+
UseProviderIP bool `json:"provider_ip"`
36+
}{}
37+
err = json.Unmarshal(data, &extraSettings)
38+
if err != nil {
39+
return nil, err
40+
}
41+
p = &Provider{
42+
domain: domain,
43+
host: host,
44+
ipVersion: ipVersion,
45+
username: extraSettings.Username,
46+
token: extraSettings.Token,
47+
useProviderIP: extraSettings.UseProviderIP,
48+
}
49+
err = p.isValid()
50+
if err != nil {
51+
return nil, err
52+
}
53+
return p, nil
54+
}
55+
56+
func (p *Provider) isValid() error {
57+
switch {
58+
case p.username == "":
59+
return fmt.Errorf("%w", errors.ErrEmptyUsername)
60+
case p.token == "":
61+
return fmt.Errorf("%w", errors.ErrEmptyToken)
62+
}
63+
return nil
64+
}
65+
66+
func (p *Provider) String() string {
67+
return utils.ToString(p.domain, p.host, constants.Zoneedit, p.ipVersion)
68+
}
69+
70+
func (p *Provider) Domain() string {
71+
return p.domain
72+
}
73+
74+
func (p *Provider) Host() string {
75+
return p.host
76+
}
77+
78+
func (p *Provider) IPVersion() ipversion.IPVersion {
79+
return p.ipVersion
80+
}
81+
82+
func (p *Provider) Proxied() bool {
83+
return false
84+
}
85+
86+
func (p *Provider) BuildDomainName() string {
87+
return utils.BuildDomainName(p.host, p.domain)
88+
}
89+
90+
func (p *Provider) HTML() models.HTMLRow {
91+
return models.HTMLRow{
92+
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName())),
93+
Host: models.HTML(p.Host()),
94+
Provider: "<a href=\"https://www.zoneedit.com/\">Zoneedit</a>",
95+
IPVersion: models.HTML(p.ipVersion.String()),
96+
}
97+
}
98+
99+
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (
100+
newIP netip.Addr, err error) {
101+
u := url.URL{
102+
Scheme: "https",
103+
Host: "api.cp.zoneedit.com",
104+
Path: "dyn/generic.php",
105+
User: url.UserPassword(p.username, p.token),
106+
}
107+
values := url.Values{}
108+
values.Set("hostname", utils.BuildURLQueryHostname(p.host, p.domain))
109+
if !p.useProviderIP {
110+
values.Set("myip", ip.String())
111+
}
112+
if p.host == "*" {
113+
values.Set("wildcard", "ON")
114+
}
115+
u.RawQuery = values.Encode()
116+
117+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
118+
if err != nil {
119+
return netip.Addr{}, err
120+
}
121+
headers.SetUserAgent(request)
122+
123+
response, err := client.Do(request)
124+
if err != nil {
125+
return netip.Addr{}, err
126+
}
127+
defer response.Body.Close()
128+
129+
b, err := io.ReadAll(response.Body)
130+
if err != nil {
131+
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
132+
}
133+
s := string(b)
134+
135+
if response.StatusCode != http.StatusOK {
136+
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus,
137+
response.StatusCode, utils.ToSingleLine(s))
138+
}
139+
140+
switch {
141+
case s == "":
142+
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoResultReceived)
143+
case strings.Contains(s, "NO_SERVICE"):
144+
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoService)
145+
case strings.Contains(s, "NO_ACCESS"):
146+
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
147+
case strings.Contains(s, "ILLEGAL_INPUT"), strings.Contains(s, "TOO_SOON"):
148+
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAbuse)
149+
case strings.Contains(s, "NO_ERROR"), strings.Contains(s, "OK"):
150+
return ip, nil
151+
default:
152+
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))
153+
}
154+
}

0 commit comments

Comments
 (0)