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))
+ }
+}