Skip to content

feat(provider): add suport for deSEC #496

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 30, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ Light container updating DNS A and/or AAAA records periodically for multiple DNS
- Cloudflare
- DD24
- DDNSS.de
- deSEC
- DigitalOcean
- DonDominio
- DNSOMatic
Expand Down Expand Up @@ -164,6 +165,7 @@ Check the documentation for your DNS provider:
- [Aliyun](https://github.com/qdm12/ddns-updater/blob/master/docs/aliyun.md)
- [Cloudflare](https://github.com/qdm12/ddns-updater/blob/master/docs/cloudflare.md)
- [DDNSS.de](https://github.com/qdm12/ddns-updater/blob/master/docs/ddnss.de.md)
- [deSEC](https://github.com/qdm12/ddns-updater/blob/master/docs/desec.md)
- [DigitalOcean](https://github.com/qdm12/ddns-updater/blob/master/docs/digitalocean.md)
- [DD24](https://github.com/qdm12/ddns-updater/blob/master/docs/domaindiscount24.md)
- [DonDominio](https://github.com/qdm12/ddns-updater/blob/master/docs/dondominio.md)
Expand Down
35 changes: 35 additions & 0 deletions docs/desec.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# deSEC

## Configuration

### Example

```json
{
"settings": [
{
"provider": "desec",
"domain": "dedyn.io",
"host": "host",
"token": "token",
"ip_version": "ipv4",
"provider_ip": false
}
]
}
```

### Compulsory parameters

- `"domain"`
- `"host"`
- `"token"` is your token that you can create [here](https://desec.io/tokens)

### Optional parameters

- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
- `"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.

## Domain setup

[desec.io/domains](https://desec.io/domains)
2 changes: 2 additions & 0 deletions internal/provider/constants/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const (
Cloudflare models.Provider = "cloudflare"
Dd24 models.Provider = "dd24"
DdnssDe models.Provider = "ddnss"
DeSEC models.Provider = "desec"
DigitalOcean models.Provider = "digitalocean"
DNSOMatic models.Provider = "dnsomatic"
DNSPod models.Provider = "dnspod"
Expand Down Expand Up @@ -52,6 +53,7 @@ func ProviderChoices() []models.Provider {
Cloudflare,
Dd24,
DdnssDe,
DeSEC,
DigitalOcean,
DNSOMatic,
DNSPod,
Expand Down
3 changes: 3 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/qdm12/ddns-updater/internal/provider/providers/cloudflare"
"github.com/qdm12/ddns-updater/internal/provider/providers/dd24"
"github.com/qdm12/ddns-updater/internal/provider/providers/ddnss"
"github.com/qdm12/ddns-updater/internal/provider/providers/desec"
"github.com/qdm12/ddns-updater/internal/provider/providers/digitalocean"
"github.com/qdm12/ddns-updater/internal/provider/providers/dnsomatic"
"github.com/qdm12/ddns-updater/internal/provider/providers/dnspod"
Expand Down Expand Up @@ -79,6 +80,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, host string
return dd24.New(data, domain, host, ipVersion)
case constants.DdnssDe:
return ddnss.New(data, domain, host, ipVersion)
case constants.DeSEC:
return desec.New(data, domain, host, ipVersion)
case constants.DigitalOcean:
return digitalocean.New(data, domain, host, ipVersion)
case constants.DNSOMatic:
Expand Down
149 changes: 149 additions & 0 deletions internal/provider/providers/desec/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package desec

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/netip"
"net/url"
"strings"

"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/provider/constants"
"github.com/qdm12/ddns-updater/internal/provider/errors"
"github.com/qdm12/ddns-updater/internal/provider/headers"
"github.com/qdm12/ddns-updater/internal/provider/utils"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
)

type Provider struct {
domain string
host string
ipVersion ipversion.IPVersion
token string
useProviderIP bool
}

func New(data json.RawMessage, domain, host string,
ipVersion ipversion.IPVersion) (p *Provider, err error) {
extraSettings := struct {
Token string `json:"token"`
UseProviderIP bool `json:"provider_ip"`
}{}
err = json.Unmarshal(data, &extraSettings)
if err != nil {
return nil, err
}
p = &Provider{
domain: domain,
host: host,
ipVersion: ipVersion,
token: extraSettings.Token,
useProviderIP: extraSettings.UseProviderIP,
}
err = p.isValid()
if err != nil {
return nil, err
}
return p, nil
}

func (p *Provider) isValid() error {
switch {
case p.token == "":
return fmt.Errorf("%w", errors.ErrTokenNotSet)
case p.host == "*":
return fmt.Errorf("%w", errors.ErrHostWildcard)
}
return nil
}

func (p *Provider) String() string {
return fmt.Sprintf("[domain: %s | host: %s | provider: deSEC]", p.domain, p.host)
}

func (p *Provider) Domain() string {
return p.domain
}

func (p *Provider) Host() string {
return p.host
}

func (p *Provider) IPVersion() ipversion.IPVersion {
return p.ipVersion
}

func (p *Provider) Proxied() bool {
return false
}

func (p *Provider) BuildDomainName() string {
return utils.BuildDomainName(p.host, p.domain)
}

func (p *Provider) HTML() models.HTMLRow {
return models.HTMLRow{
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
Host: p.Host(),
Provider: "<a href=\"https://desec.io/\">deSEC</a>",
IPVersion: p.ipVersion.String(),
}
}

func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
User: url.UserPassword(p.BuildDomainName(), p.token),
Host: "update.dedyn.io",
Path: "/nic/update",
}
values := url.Values{}
values.Set("hostname", utils.BuildURLQueryHostname(p.host, p.domain))
if !p.useProviderIP {
values.Set("myip", ip.String())
}
u.RawQuery = values.Encode()

request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
}
headers.SetUserAgent(request)

response, err := client.Do(request)
if err != nil {
return netip.Addr{}, err
}
defer response.Body.Close()

b, err := io.ReadAll(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
}
s := string(b)

switch response.StatusCode {
case http.StatusOK:
case http.StatusUnauthorized:
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrAuth, utils.ToSingleLine(s))
case http.StatusNotFound:
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrHostnameNotExists, utils.ToSingleLine(s))
default:
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid,
response.StatusCode, utils.ToSingleLine(s))
}

switch {
case strings.HasPrefix(s, constants.Notfqdn):
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case strings.HasPrefix(s, "badrequest"):
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
case strings.HasPrefix(s, "good"):
return ip, nil
default:
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))
}
}