Skip to content

feat: add name.com provider #474

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 4 commits into from
Jun 15, 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 @@ -57,6 +57,7 @@ Light container updating DNS A and/or AAAA records periodically for multiple DNS
- INWX
- Linode
- LuaDNS
- Name.com
- Namecheap
- Netcup
- NoIP
Expand Down Expand Up @@ -184,6 +185,7 @@ Check the documentation for your DNS provider:
- [INWX](https://github.com/qdm12/ddns-updater/blob/master/docs/inwx.md)
- [Linode](https://github.com/qdm12/ddns-updater/blob/master/docs/linode.md)
- [LuaDNS](https://github.com/qdm12/ddns-updater/blob/master/docs/luadns.md)
- [Name.com](https://github.com/qdm12/ddns-updater/blob/master/docs/name.com.md)
- [Namecheap](https://github.com/qdm12/ddns-updater/blob/master/docs/namecheap.md)
- [Netcup](https://github.com/qdm12/ddns-updater/blob/master/docs/netcup.md)
- [NoIP](https://github.com/qdm12/ddns-updater/blob/master/docs/noip.md)
Expand Down
33 changes: 33 additions & 0 deletions docs/name.com.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Name.com

<a href="https://www.name.com"><img src="../readme/name.svg" alt="drawing" width="25%"/></a>

## Configuration

### Example

```json
{
"settings": [
{
"provider": "name.com",
"domain": "domain.com",
"host": "@",
"username": "username",
"token": "token"
}
]
}
```

### Compulsory parameters

- `"domain"`
- `"host"` is your host and can be a subdomain, `"@"` or `"*"` generally
- `"username"` is your account username
- `"token"` which you can obtain from [www.name.com/account/settings/api](https://www.name.com/account/settings/api)

### Optional parameters

- `"ttl"` is the time this record can be cached for in seconds. Name.com allows a minimum TTL of 300, or 5 minutes. Name.com defaults to 300 if not provided.
- `"ip_version"` can be `ipv4` (A records) or `ipv6` (AAAA records), defaults to `ipv4 or ipv6`
2 changes: 2 additions & 0 deletions internal/provider/constants/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
Linode models.Provider = "linode"
LuaDNS models.Provider = "luadns"
Namecheap models.Provider = "namecheap"
NameCom models.Provider = "name.com"
Netcup models.Provider = "netcup"
Njalla models.Provider = "njalla"
NoIP models.Provider = "noip"
Expand Down Expand Up @@ -72,6 +73,7 @@ func ProviderChoices() []models.Provider {
Linode,
LuaDNS,
Namecheap,
NameCom,
Njalla,
NoIP,
OpenDNS,
Expand Down
1 change: 1 addition & 0 deletions internal/provider/errors/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var (
ErrConflictingRecord = errors.New("conflicting record")
ErrDNSServerSide = errors.New("server side DNS error")
ErrDomainDisabled = errors.New("record disabled")
ErrDomainNotFound = errors.New("domain not found")
ErrDomainIDNotFound = errors.New("ID not found in domain record")
ErrFeatureUnavailable = errors.New("feature is not available to the user")
ErrHostnameNotExists = errors.New("hostname does not exist")
Expand Down
1 change: 1 addition & 0 deletions internal/provider/errors/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ var (
ErrEmptyAccessKeyID = errors.New("empty access key id")
ErrEmptyAccessKeySecret = errors.New("empty key secret")
ErrEmptyTTL = errors.New("TTL is not set")
ErrTTLTooLow = errors.New("TTL is too low")
ErrEmptyUsername = errors.New("empty username")
ErrEmptyZoneIdentifier = errors.New("empty zone identifier")
ErrEmptyHost = errors.New("host cannot be empty")
Expand Down
3 changes: 3 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/qdm12/ddns-updater/internal/provider/providers/linode"
"github.com/qdm12/ddns-updater/internal/provider/providers/luadns"
"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"
"github.com/qdm12/ddns-updater/internal/provider/providers/njalla"
"github.com/qdm12/ddns-updater/internal/provider/providers/noip"
Expand Down Expand Up @@ -120,6 +121,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, host string
return luadns.New(data, domain, host, ipVersion)
case constants.Namecheap:
return namecheap.New(data, domain, host, ipVersion)
case constants.NameCom:
return namecom.New(data, domain, host, ipVersion)
case constants.Netcup:
return netcup.New(data, domain, host, ipVersion)
case constants.Njalla:
Expand Down
65 changes: 65 additions & 0 deletions internal/provider/providers/namecom/createrecord.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package namecom

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

"github.com/qdm12/ddns-updater/internal/provider/constants"
"github.com/qdm12/ddns-updater/internal/provider/errors"
)

func (p *Provider) createRecord(ctx context.Context, client *http.Client,
ip netip.Addr) (err error) {
recordType := constants.A
if ip.Is6() {
recordType = constants.AAAA
}

u := &url.URL{
Scheme: "https",
Host: "api.name.com",
Path: fmt.Sprintf("/v4/domains/%s/records", p.domain),
User: url.UserPassword(p.username, p.token),
}

postRecordsParams := struct {
Host string `json:"host"`
Type string `json:"type"`
Answer string `json:"answer"`
TTL *uint32 `json:"ttl,omitempty"`
}{
Host: p.host,
Type: recordType,
Answer: ip.String(),
TTL: p.ttl,
}

bodyBytes, err := json.Marshal(postRecordsParams)
if err != nil {
return fmt.Errorf("%w: %w", errors.ErrRequestMarshal, err)
}

request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), bytes.NewBuffer(bodyBytes))
if err != nil {
return fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
}
setHeaders(request)

response, err := client.Do(request)
if err != nil {
return fmt.Errorf("doing HTTP request: %w", err)
}
defer response.Body.Close()

switch response.StatusCode {
case http.StatusOK, http.StatusCreated:
return verifySuccessResponseBody(response.Body, ip)
default:
return parseErrorResponse(response)
}
}
64 changes: 64 additions & 0 deletions internal/provider/providers/namecom/getrecord.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package namecom

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

"github.com/qdm12/ddns-updater/internal/provider/errors"
)

func (p *Provider) getRecordID(ctx context.Context, client *http.Client,
recordType string) (recordID int, err error) {
u := &url.URL{
Scheme: "https",
Host: "api.name.com",
Path: fmt.Sprintf("/v4/domains/%s/records", p.domain),
User: url.UserPassword(p.username, p.token),
}

// by default GET request will return 1000 records.
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return 0, fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
}
setHeaders(request)

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

switch response.StatusCode {
case http.StatusOK:
case http.StatusNotFound:
return 0, fmt.Errorf("%w", errors.ErrDomainNotFound)
default:
return 0, parseErrorResponse(response)
}

decoder := json.NewDecoder(response.Body)
var data struct {
Records []struct {
RecordID int `json:"id"`
Host string `json:"host"`
Type string `json:"type"`
} `json:"records"`
}
err = decoder.Decode(&data)
if err != nil {
return 0, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}

for _, record := range data.Records {
if record.Host == p.host && record.Type == recordType {
return record.RecordID, nil
}
}

return 0, fmt.Errorf("%w: in %d record(s)",
errors.ErrRecordNotFound, len(data.Records))
}
13 changes: 13 additions & 0 deletions internal/provider/providers/namecom/headers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package namecom

import (
"net/http"

"github.com/qdm12/ddns-updater/internal/provider/headers"
)

func setHeaders(request *http.Request) {
headers.SetContentType(request, "application/json")
headers.SetAccept(request, "application/json")
headers.SetUserAgent(request)
}
119 changes: 119 additions & 0 deletions internal/provider/providers/namecom/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package namecom

import (
"context"
"encoding/json"
stderrors "errors"
"fmt"
"net/http"
"net/netip"

"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/utils"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
)

type Provider struct {
domain string
host string
ipVersion ipversion.IPVersion
username string
token string
ttl *uint32
}

func New(data json.RawMessage, domain, host string,
ipVersion ipversion.IPVersion) (p *Provider, err error) {
extraSettings := struct {
Username string `json:"username"`
Token string `json:"token"`
TTL *uint32 `json:"ttl,omitempty"`
}{}
err = json.Unmarshal(data, &extraSettings)
if err != nil {
return nil, err
}

const minTTL = 300
switch {
case extraSettings.Username == "":
return nil, fmt.Errorf("%w", errors.ErrEmptyUsername)
case extraSettings.Token == "":
return nil, fmt.Errorf("%w", errors.ErrEmptyPassword)
case extraSettings.TTL != nil && *extraSettings.TTL < minTTL:
return nil, fmt.Errorf("%w: %d must be at least %d",
errors.ErrTTLTooLow, *extraSettings.TTL, minTTL)
}

return &Provider{
domain: domain,
host: host,
ipVersion: ipVersion,
username: extraSettings.Username,
token: extraSettings.Token,
ttl: extraSettings.TTL,
}, nil
}

func (p *Provider) String() string {
return utils.ToString(p.domain, p.host, constants.NameCom, p.ipVersion)
}

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: models.HTML(fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName())),
Host: models.HTML(p.Host()),
Provider: "<a href=\"https://name.com\">Name.com</a>",
IPVersion: models.HTML(p.ipVersion.String()),
}
}

func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
// Documentation at https://www.name.com/api-docs
recordType := constants.A
if ip.Is6() {
recordType = constants.AAAA
}

recordID, err := p.getRecordID(ctx, client, recordType)

if stderrors.Is(err, errors.ErrRecordNotFound) {
err = p.createRecord(ctx, client, ip)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
}

return ip, nil
} else if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrGetRecordID, err)
}

err = p.updateRecord(ctx, client, recordID, ip)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUpdateRecord, err)
}

return ip, nil
}
Loading