Skip to content
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

Add support for Dynu.com #285

Merged
merged 9 commits into from
Feb 5, 2022
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -44,6 +44,7 @@ Light container updating DNS A and/or AAAA records periodically for multiple DNS
- Dreamhost
- DuckDNS
- DynDNS
- Dynu
- FreeDNS
- Gandi
- GoDaddy
Expand Down Expand Up @@ -164,6 +165,7 @@ Check the documentation for your DNS provider:
- [Dreamhost](https://github.com/qdm12/ddns-updater/blob/master/docs/dreamhost.md)
- [DuckDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/duckdns.md)
- [DynDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/dyndns.md)
- [Dynu](https://github.com/qdm12/ddns-updater/blob/master/docs/dynu.md)
- [DynV6](https://github.com/qdm12/ddns-updater/blob/master/docs/dynv6.md)
- [FreeDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/freedns.md)
- [Gandi](https://github.com/qdm12/ddns-updater/blob/master/docs/gandi.md)
Expand Down
41 changes: 41 additions & 0 deletions docs/dynu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Dynu

## Configuration

### Example

```json
{
"settings": [
{
"provider": "dynu",
"domain": "domain.com",
"host": "host",
"alias": "subdomain",
"location": "group",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"provider_ip": true
}
]
}
```

### Compulsory parameters

- `"domain"`
- `"host"` is your host or `"@"` (@ allows to update all domains that has no group)
- `"username"`
- `"password"` could be plain text or password in MD5 or SHA256 format (There's also an option for setting a password for IP Update only)

### 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.
- `"alias"` Specify the subdomain you want to set the IP
- `"location"` Specify the Group for which you want to set the IP (domains and subdomains in the same group on dynu)

NOTE: `location` takes precedence over `alias`, so if you set `location` `alias` will do nothing

## Domain setup
2 changes: 2 additions & 0 deletions internal/settings/constants/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
Dreamhost models.Provider = "dreamhost"
DuckDNS models.Provider = "duckdns"
Dyn models.Provider = "dyn"
Dynu models.Provider = "dynu"
DynV6 models.Provider = "dynv6"
FreeDNS models.Provider = "freedns"
Gandi models.Provider = "gandi"
Expand Down Expand Up @@ -50,6 +51,7 @@ func ProviderChoices() []models.Provider {
Dreamhost,
DuckDNS,
Dyn,
Dynu,
DynV6,
FreeDNS,
Gandi,
Expand Down
1 change: 1 addition & 0 deletions internal/settings/errors/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var (
ErrHostOnlyAt = errors.New(`host can only be "@"`)
ErrHostOnlySubdomain = errors.New("host can only be a subdomain")
ErrHostWildcard = errors.New(`host cannot be a "*"`)
ErrHostWithAlias = errors.New("host cannot be @ if alias is set") // Dynu
ErrIPv6NotSupported = errors.New("IPv6 is not supported by this provider")
ErrMalformedEmail = errors.New("malformed email address")
ErrMalformedKey = errors.New("malformed key")
Expand Down
172 changes: 172 additions & 0 deletions internal/settings/providers/dynu/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package dynu

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

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

type provider struct {
domain string
host string
location string
alias string
ipVersion ipversion.IPVersion
username string
password string
useProviderIP bool
}

func New(data json.RawMessage, domain, host string,
ipVersion ipversion.IPVersion) (p *provider, err error) {
extraSettings := struct {
Username string `json:"username"`
Password string `json:"password"`
UseProviderIP bool `json:"provider_ip"`
Location string `json:"location"`
Alias string `json:"alias"`
}{}
if err := json.Unmarshal(data, &extraSettings); err != nil {
return nil, err
}
p = &provider{
msxdan marked this conversation as resolved.
Show resolved Hide resolved
domain: domain,
host: host,
ipVersion: ipVersion,
location: extraSettings.Location,
alias: extraSettings.Alias,
username: extraSettings.Username,
password: extraSettings.Password,
useProviderIP: extraSettings.UseProviderIP,
}
if err := p.isValid(); err != nil {
return nil, err
}
return p, nil
}

func (p *provider) isValid() error {
switch {
case p.username == "":
return errors.ErrEmptyUsername
case p.password == "":
return errors.ErrEmptyPassword
case p.host == "*":
return errors.ErrHostWildcard
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wildcards are not supported then right? Are custom hosts (i.e. a.example.com) supported?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I use @ or * it updates all DDNS IPs for my domains on Dynu, * doesn't update subdomains of a concrete domain, so *.domain.com updates domain.com but not subdomain.domain.com

There's an alias (subdomain) parameter that could be used to update a concrete subdomain, to update a bunch of domains and subdomains in one call there's an optional parameter called location which could be used if configured in Dynu, would be similar to * but you have to set your domains and subdomains to a group in dynu.

I'm adding more features to support all these parameters, I'll update the pull as soon as I finish.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I use @ or * it updates all DDNS IPs for my domains on Dynu, * doesn't update subdomains of a concrete domain, so *.domain.com updates domain.com but not subdomain.domain.com

I'm a bit confused here.
Correct me if I'm wrong:

  • @ for domain.com updates only domain.com but not `sub.domain.com
  • * for domain.com updates domain.com, domain2.com but not sub.domain.com

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, @ does nothing really, if you don't set the hostname or if it's not a valid hostname for your user it will update all records for your domains that don't have a group assigned.

* doesn't acts like a wildcard, you can use it in the hostname but it will update only the domain.com and not sub.domain.com

case p.alias != "" && p.host == "@":
return errors.ErrHostWithAlias
msxdan marked this conversation as resolved.
Show resolved Hide resolved
msxdan marked this conversation as resolved.
Show resolved Hide resolved
}
return nil
}

func (p *provider) String() string {
return utils.ToString(p.domain, p.Host(), constants.Dynu, p.ipVersion)
}

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

func (p *provider) Host() string {
if p.alias != "" {
return p.alias + "." + p.host
}
msxdan marked this conversation as resolved.
Show resolved Hide resolved
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: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName())),
Host: models.HTML(p.Host()),
Provider: "<a href=\"https://dynu.com/\">Dynu</a>",
IPVersion: models.HTML(p.ipVersion.String()),
}
}

func (p *provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
u := url.URL{
Scheme: "https",
Host: "api.dynu.com",
Path: "/nic/update",
}
values := url.Values{}
values.Set("username", p.username)
values.Set("password", p.password)
values.Set("location", p.location)
if p.host != "@" {
values.Set("hostname", utils.BuildURLQueryHostname(p.host, p.domain))
msxdan marked this conversation as resolved.
Show resolved Hide resolved
}
if p.alias != "" {
values.Set("alias", p.alias)
}
if !p.useProviderIP {
if ip.To4() == nil {
values.Set("myipv6", ip.String())
} else {
values.Set("myip", ip.String())
}
}
u.RawQuery = values.Encode()

request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
msxdan marked this conversation as resolved.
Show resolved Hide resolved
}
headers.SetUserAgent(request)

response, err := client.Do(request)
if err != nil {
return nil, err
msxdan marked this conversation as resolved.
Show resolved Hide resolved
}
defer response.Body.Close()

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

if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.ToSingleLine(s))
}

switch {
case strings.Contains(s, constants.Badauth):
return nil, errors.ErrAuth
case strings.Contains(s, constants.Notfqdn):
return nil, errors.ErrHostnameNotExists
case strings.Contains(s, constants.Abuse):
return nil, errors.ErrAbuse
case strings.Contains(s, "good"):
return ip, nil
case strings.Contains(s, "nochg"): // Updated but not changed
return ip, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))
}
}
3 changes: 3 additions & 0 deletions internal/settings/settings.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"github.com/qdm12/ddns-updater/internal/settings/providers/dreamhost"
"github.com/qdm12/ddns-updater/internal/settings/providers/duckdns"
"github.com/qdm12/ddns-updater/internal/settings/providers/dyn"
"github.com/qdm12/ddns-updater/internal/settings/providers/dynu"
"github.com/qdm12/ddns-updater/internal/settings/providers/dynv6"
"github.com/qdm12/ddns-updater/internal/settings/providers/freedns"
"github.com/qdm12/ddns-updater/internal/settings/providers/gandi"
Expand Down Expand Up @@ -85,6 +86,8 @@ func New(provider models.Provider, data json.RawMessage, domain, host string,
return duckdns.New(data, domain, host, ipVersion, matcher)
case constants.Dyn:
return dyn.New(data, domain, host, ipVersion)
case constants.Dynu:
return dynu.New(data, domain, host, ipVersion)
case constants.DynV6:
return dynv6.New(data, domain, host, ipVersion)
case constants.FreeDNS:
Expand Down