From 03154c35f8dff814b26bd01a042eb75cbc4a0291 Mon Sep 17 00:00:00 2001 From: Brian Shea <165865819+brianshea2@users.noreply.github.com> Date: Tue, 24 Dec 2024 03:51:51 -0500 Subject: [PATCH] feat(providers): add `myaddr.tools` (#885) --- README.md | 2 + docs/myaddr.md | 33 +++++ internal/params/json.go | 11 +- internal/provider/constants/providers.go | 2 + internal/provider/provider.go | 3 + .../provider/providers/myaddr/provider.go | 131 ++++++++++++++++++ 6 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 docs/myaddr.md create mode 100644 internal/provider/providers/myaddr/provider.go diff --git a/README.md b/README.md index 46e6f930d..544fc2e09 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ This readme and the [docs/](docs/) directory are **versioned** to match the prog - Linode - Loopia - LuaDNS + - Myaddr - Name.com - Namecheap - Netcup @@ -243,6 +244,7 @@ Check the documentation for your DNS provider: - [Linode](docs/linode.md) - [Loopia](docs/loopia.md) - [LuaDNS](docs/luadns.md) +- [Myaddr](docs/myaddr.md) - [Name.com](docs/name.com.md) - [Namecheap](docs/namecheap.md) - [Netcup](docs/netcup.md) diff --git a/docs/myaddr.md b/docs/myaddr.md new file mode 100644 index 000000000..df35bb0f4 --- /dev/null +++ b/docs/myaddr.md @@ -0,0 +1,33 @@ +# [Myaddr](https://myaddr.tools/) + +## Configuration + +### Example + +```json +{ + "settings": [ + { + "provider": "myaddr", + "domain": "your-name.myaddr.tools", + "key": "key", + "ip_version": "ipv4", + "ipv6_suffix": "" + } + ] +} +``` + +### Compulsory parameters + +- `"domain"` - the **single** domain to update; note the `key` below updates all records and subdomains for this domain. It should be `your-name*.myaddr.tools`. +- `"key"` - the private key corresponding to the domain to update + +### Optional parameters + +- `"ip_version"` can be `ipv4` (A records), or `ipv6` (AAAA records) or `ipv4 or ipv6` (update one of the two, depending on the public ip found). It defaults to `ipv4 or ipv6`. +- `"ipv6_suffix"` is the IPv6 interface identifier suffix to use. It can be for example `0:0:0:0:72ad:8fbb:a54e:bedd/64`. If left empty, it defaults to no suffix and the raw public IPv6 address obtained is used in the record updating. + +## Domain setup + +Claim a subdomain at [myaddr.tools](https://myaddr.tools/) diff --git a/internal/params/json.go b/internal/params/json.go index 937943b9c..341c4bb1d 100644 --- a/internal/params/json.go +++ b/internal/params/json.go @@ -12,6 +12,7 @@ import ( "github.com/qdm12/ddns-updater/internal/models" "github.com/qdm12/ddns-updater/internal/provider" + "github.com/qdm12/ddns-updater/internal/provider/constants" "github.com/qdm12/ddns-updater/internal/provider/utils" "github.com/qdm12/ddns-updater/pkg/publicip/ipversion" "golang.org/x/net/publicsuffix" @@ -145,7 +146,10 @@ func extractAllSettings(jsonBytes []byte) ( return allProviders, warnings, nil } -var ErrProviderNoLongerSupported = errors.New("provider no longer supported") +var ( + ErrProviderNoLongerSupported = errors.New("provider no longer supported") + ErrProviderMultipleDomains = errors.New("provider does not support multiple domains") +) func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage, retroGlobalIPv6Suffix netip.Prefix) ( @@ -178,6 +182,11 @@ func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage, } } + if common.Provider == string(constants.Myaddr) && len(owners) > 1 { + return nil, nil, fmt.Errorf("%w: %s for parent domain %q", + ErrProviderMultipleDomains, common.Provider, domain) + } + if common.IPVersion == "" { common.IPVersion = ipversion.IP4or6.String() } diff --git a/internal/provider/constants/providers.go b/internal/provider/constants/providers.go index 30ce24a50..482560cf9 100644 --- a/internal/provider/constants/providers.go +++ b/internal/provider/constants/providers.go @@ -37,6 +37,7 @@ const ( Linode models.Provider = "linode" Loopia models.Provider = "loopia" LuaDNS models.Provider = "luadns" + Myaddr models.Provider = "myaddr" Namecheap models.Provider = "namecheap" NameCom models.Provider = "name.com" Netcup models.Provider = "netcup" @@ -90,6 +91,7 @@ func ProviderChoices() []models.Provider { Linode, Loopia, LuaDNS, + Myaddr, Namecheap, NameCom, Njalla, diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 9807679b8..13962ea46 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -43,6 +43,7 @@ import ( "github.com/qdm12/ddns-updater/internal/provider/providers/linode" "github.com/qdm12/ddns-updater/internal/provider/providers/loopia" "github.com/qdm12/ddns-updater/internal/provider/providers/luadns" + "github.com/qdm12/ddns-updater/internal/provider/providers/myaddr" "github.com/qdm12/ddns-updater/internal/provider/providers/namecheap" "github.com/qdm12/ddns-updater/internal/provider/providers/namecom" "github.com/qdm12/ddns-updater/internal/provider/providers/netcup" @@ -148,6 +149,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin return loopia.New(data, domain, owner, ipVersion, ipv6Suffix) case constants.LuaDNS: return luadns.New(data, domain, owner, ipVersion, ipv6Suffix) + case constants.Myaddr: + return myaddr.New(data, domain, owner, ipVersion, ipv6Suffix) case constants.Namecheap: return namecheap.New(data, domain, owner) case constants.NameCom: diff --git a/internal/provider/providers/myaddr/provider.go b/internal/provider/providers/myaddr/provider.go new file mode 100644 index 000000000..ecbc62d97 --- /dev/null +++ b/internal/provider/providers/myaddr/provider.go @@ -0,0 +1,131 @@ +package myaddr + +import ( + "context" + "encoding/json" + "fmt" + "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 + owner string + ipVersion ipversion.IPVersion + ipv6Suffix netip.Prefix + key string +} + +func New(data json.RawMessage, domain, owner string, + ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix, +) (*Provider, error) { + var providerSpecificSettings struct { + Key string `json:"key"` + } + err := json.Unmarshal(data, &providerSpecificSettings) + if err != nil { + return nil, fmt.Errorf("json decoding provider specific settings: %w", err) + } + err = validateSettings(domain, providerSpecificSettings.Key) + if err != nil { + return nil, fmt.Errorf("validating provider specific settings: %w", err) + } + return &Provider{ + domain: domain, + owner: owner, + ipVersion: ipVersion, + ipv6Suffix: ipv6Suffix, + key: providerSpecificSettings.Key, + }, nil +} + +func validateSettings(domain, key string) (err error) { + err = utils.CheckDomain(domain) + if err != nil { + return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err) + } + if key == "" { + return fmt.Errorf("%w", errors.ErrKeyNotSet) + } + return nil +} + +func (p *Provider) String() string { + return utils.ToString(p.Domain(), p.Owner(), constants.Myaddr, p.IPVersion()) +} + +func (p *Provider) Domain() string { + return p.domain +} + +func (p *Provider) Owner() string { + return p.owner +} + +func (p *Provider) BuildDomainName() string { + return utils.BuildDomainName(p.owner, p.domain) +} + +func (p *Provider) HTML() models.HTMLRow { + return models.HTMLRow{ + Domain: fmt.Sprintf("%s", p.BuildDomainName(), p.BuildDomainName()), + Owner: p.Owner(), + Provider: "myaddr", + IPVersion: p.IPVersion().String(), + } +} + +func (p *Provider) Proxied() bool { + return false +} + +func (p *Provider) IPVersion() ipversion.IPVersion { + return p.ipVersion +} + +func (p *Provider) IPv6Suffix() netip.Prefix { + return p.ipv6Suffix +} + +func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (netip.Addr, error) { + u := &url.URL{ + Scheme: "https", + Host: "myaddr.tools", + Path: "/update", + } + v := url.Values{} + v.Set("key", p.key) + v.Set("ip", ip.String()) + buffer := strings.NewReader(v.Encode()) + request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer) + if err != nil { + return netip.Addr{}, fmt.Errorf("creating http request: %w", err) + } + headers.SetContentType(request, "application/x-www-form-urlencoded") + headers.SetUserAgent(request) + response, err := client.Do(request) + if err != nil { + return netip.Addr{}, err + } + defer response.Body.Close() + switch response.StatusCode { + case http.StatusOK: + return ip, nil + case http.StatusBadRequest: + return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrBadRequest, utils.BodyToSingleLine(response.Body)) + case http.StatusNotFound: + return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrKeyNotValid, utils.BodyToSingleLine(response.Body)) + default: + return netip.Addr{}, fmt.Errorf("%w: %d: %s", + errors.ErrHTTPStatusNotValid, response.StatusCode, utils.BodyToSingleLine(response.Body)) + } +}