Skip to content

Commit e735ef3

Browse files
authored
Feat: add servercow provider (#224)
1 parent 3ece0c9 commit e735ef3

File tree

6 files changed

+231
-0
lines changed

6 files changed

+231
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
- OVH
5959
- Porkbun
6060
- Selfhost.de
61+
- Servercow.de
6162
- Spdyn
6263
- Strato.de
6364
- Variomedia.de
@@ -174,6 +175,7 @@ Check the documentation for your DNS provider:
174175
- [OVH](https://github.com/qdm12/ddns-updater/blob/master/docs/ovh.md)
175176
- [Porkbun](https://github.com/qdm12/ddns-updater/blob/master/docs/porkbun.md)
176177
- [Selfhost.de](https://github.com/qdm12/ddns-updater/blob/master/docs/selfhost.de.md)
178+
- [Servercow.de](https://github.com/qdm12/ddns-updater/blob/master/docs/servercow.md)
177179
- [Spdyn](https://github.com/qdm12/ddns-updater/blob/master/docs/spdyn.md)
178180
- [Strato.de](https://github.com/qdm12/ddns-updater/blob/master/docs/strato.md)
179181
- [Variomedia.de](https://github.com/qdm12/ddns-updater/blob/master/docs/variomedia.md)

docs/servercow.md

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
# Servercow
2+
3+
## Configuration
4+
5+
### Example
6+
7+
```json
8+
{
9+
"settings": [
10+
{
11+
"provider": "servercow",
12+
"domain": "domain.com",
13+
"host": "",
14+
"username": "servercow_username",
15+
"password": "servercow_password",
16+
"ttl": 600,
17+
"ip_version": "ipv4"
18+
}
19+
]
20+
}
21+
```
22+
23+
### Compulsury parameters
24+
25+
- `"domain"`
26+
- `"host"` is your host and can be `""`, a subdomain or `"*"` generally
27+
- `"username"` is the username for your DNS API User
28+
- `"password"` is the password for your DNS API User
29+
30+
### Optional parameters
31+
32+
- `"ttl"` can be set to an integer value for record TTL in seconds (if not set the default is 120)
33+
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), and defaults to `ipv4 or ipv6`
34+
35+
## Domain setup
36+
37+
See [their article](https://cp.servercow.de/en/plugin/support_manager/knowledgebase/view/34/dns-api-v1/7/)

internal/settings/constants/providers.go

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const (
3030
OVH models.Provider = "ovh"
3131
Porkbun models.Provider = "porkbun"
3232
SelfhostDe models.Provider = "selfhost.de"
33+
Servercow models.Provider = "servercow"
3334
Spdyn models.Provider = "spdyn"
3435
Strato models.Provider = "strato"
3536
Variomedia models.Provider = "variomedia"

internal/settings/headers/headers.go

+8
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,11 @@ func SetOauth(request *http.Request, value string) {
2929
func SetXFilter(request *http.Request, value string) {
3030
request.Header.Set("X-Filter", value)
3131
}
32+
33+
func SetXAuthUsername(request *http.Request, value string){
34+
request.Header.Set("X-Auth-Username", value)
35+
}
36+
37+
func SetXAuthPassword(request *http.Request, value string){
38+
request.Header.Set("X-Auth-Password", value)
39+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package servercow
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"encoding/json"
7+
"fmt"
8+
"net"
9+
"net/http"
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/log"
18+
"github.com/qdm12/ddns-updater/internal/settings/utils"
19+
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
20+
)
21+
22+
type provider struct {
23+
username string
24+
host string
25+
domain string
26+
ipVersion ipversion.IPVersion
27+
password string
28+
useProviderIP bool
29+
ttl uint
30+
logger log.Logger
31+
}
32+
33+
func New(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
34+
logger log.Logger) (p *provider, err error) {
35+
extraSettings := struct {
36+
Username string `json:"username"`
37+
Password string `json:"password"`
38+
Domain string `json:"domain"`
39+
TTL uint `json:"ttl"`
40+
UseProviderIP bool `json:"provider_ip"`
41+
}{}
42+
if err := json.Unmarshal(data, &extraSettings); err != nil {
43+
return nil, err
44+
}
45+
46+
p = &provider{
47+
host: host,
48+
ipVersion: ipVersion,
49+
username: extraSettings.Username,
50+
password: extraSettings.Password,
51+
useProviderIP: extraSettings.UseProviderIP,
52+
domain: extraSettings.Domain,
53+
logger: logger,
54+
ttl: extraSettings.TTL,
55+
}
56+
if err := p.isValid(); err != nil {
57+
return nil, err
58+
}
59+
return p, nil
60+
}
61+
62+
func (p *provider) isValid() error {
63+
switch {
64+
case p.username == "":
65+
return errors.ErrEmptyUsername
66+
case p.password == "":
67+
return errors.ErrEmptyPassword
68+
}
69+
if strings.Contains(p.host, "*") {
70+
return errors.ErrHostWildcard
71+
}
72+
return nil
73+
}
74+
75+
func (p *provider) String() string {
76+
return utils.ToString("servercow.de", p.host, constants.Servercow, p.ipVersion)
77+
}
78+
79+
func (p *provider) Domain() string {
80+
return p.domain
81+
}
82+
83+
func (p *provider) Host() string {
84+
return p.host
85+
}
86+
87+
func (p *provider) IPVersion() ipversion.IPVersion {
88+
return p.ipVersion
89+
}
90+
91+
func (p *provider) Proxied() bool {
92+
return false
93+
}
94+
95+
func (p *provider) BuildDomainName() string {
96+
return utils.BuildDomainName(p.host, p.domain)
97+
}
98+
99+
func (p *provider) HTML() models.HTMLRow {
100+
return models.HTMLRow{
101+
Domain: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName())),
102+
Host: models.HTML(p.Host()),
103+
Provider: "<a href=\"https://servercow.de\">Servercow</a>",
104+
IPVersion: models.HTML(p.ipVersion.String()),
105+
}
106+
}
107+
108+
func (p *provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
109+
recordType := constants.A
110+
if ip.To4() == nil {
111+
recordType = constants.AAAA
112+
}
113+
u := url.URL{
114+
Scheme: "https",
115+
Host: "api.servercow.de",
116+
Path: "/dns/v1/domains/" + p.domain,
117+
}
118+
119+
updateHost := p.host
120+
if updateHost == "@" {
121+
updateHost = ""
122+
}
123+
124+
requestData := struct {
125+
Type string `json:"type"` // constants.A or constants.AAAA depending on ip address given
126+
Name string `json:"name"` // DNS record name (only the subdomain part)
127+
Content string `json:"content"` // ip address
128+
TTL uint `json:"ttl"`
129+
}{
130+
Type: recordType,
131+
Name: updateHost,
132+
Content: ip.String(),
133+
TTL: p.ttl,
134+
}
135+
136+
buffer := bytes.NewBuffer(nil)
137+
encoder := json.NewEncoder(buffer)
138+
if err := encoder.Encode(requestData); err != nil {
139+
return nil, fmt.Errorf("%w: %s", errors.ErrRequestEncode, err)
140+
}
141+
142+
p.logger.Debug("HTTP POST: " + u.String() + ": " + buffer.String())
143+
144+
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
145+
if err != nil {
146+
return nil, err
147+
}
148+
headers.SetContentType(request, "application/json")
149+
headers.SetXAuthUsername(request, p.username)
150+
headers.SetXAuthPassword(request, p.password)
151+
headers.SetUserAgent(request)
152+
153+
response, err := client.Do(request)
154+
if err != nil {
155+
return nil, err
156+
}
157+
defer response.Body.Close()
158+
159+
if response.StatusCode > http.StatusUnsupportedMediaType {
160+
return nil, fmt.Errorf("%w: %d: %s",
161+
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
162+
}
163+
164+
decoder := json.NewDecoder(response.Body)
165+
166+
var parsedJSON struct {
167+
Message string `json:"message"`
168+
Error string `json:"error"`
169+
}
170+
171+
if err := decoder.Decode(&parsedJSON); err != nil {
172+
return nil, fmt.Errorf("%w: %s", errors.ErrUnmarshalResponse, err)
173+
}
174+
175+
if parsedJSON.Message != "ok" {
176+
return nil, fmt.Errorf("%w: %s", errors.ErrUnsuccessfulResponse, parsedJSON.Error)
177+
}
178+
179+
return ip, nil
180+
}

internal/settings/settings.go

+3
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838
"github.com/qdm12/ddns-updater/internal/settings/providers/ovh"
3939
"github.com/qdm12/ddns-updater/internal/settings/providers/porkbun"
4040
"github.com/qdm12/ddns-updater/internal/settings/providers/selfhostde"
41+
"github.com/qdm12/ddns-updater/internal/settings/providers/servercow"
4142
"github.com/qdm12/ddns-updater/internal/settings/providers/spdyn"
4243
"github.com/qdm12/ddns-updater/internal/settings/providers/strato"
4344
"github.com/qdm12/ddns-updater/internal/settings/providers/variomedia"
@@ -113,6 +114,8 @@ func New(provider models.Provider, data json.RawMessage, domain, host string,
113114
return porkbun.New(data, domain, host, ipVersion, logger)
114115
case constants.SelfhostDe:
115116
return selfhostde.New(data, domain, host, ipVersion, logger)
117+
case constants.Servercow:
118+
return servercow.New(data, domain, host, ipVersion, logger)
116119
case constants.Spdyn:
117120
return spdyn.New(data, domain, host, ipVersion, logger)
118121
case constants.Strato:

0 commit comments

Comments
 (0)