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

Support for Domeneshop #810

Merged
merged 9 commits into from
Oct 9, 2024
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 @@ -56,6 +56,7 @@ This readme and the [docs/](docs/) directory are **versioned** to match the prog
- DDNSS.de
- deSEC
- DigitalOcean
- Domeneshop
- DonDominio
- DNSOMatic
- DNSPod
Expand Down Expand Up @@ -218,6 +219,7 @@ Check the documentation for your DNS provider:
- [deSEC](docs/desec.md)
- [DigitalOcean](docs/digitalocean.md)
- [DD24](docs/dd24.md)
- [Domeneshop](docs/domeneshop.md)
- [DonDominio](docs/dondominio.md)
- [DNSOMatic](docs/dnsomatic.md)
- [DNSPod](docs/dnspod.md)
Expand Down
31 changes: 31 additions & 0 deletions docs/domeneshop.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Domeneshop.no

## Configuration

### Example

```json
{
"settings": [
{
"provider": "domeneshop",
"domain": "domain.com,seconddomain.com",
"token": "token",
"secret": "secret",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```

### Compulsory parameters

- `"domain"` is the domain to update. It can be `example.com` (root domain) or `sub.example.com` (subdomain of `example.com`)
- `"token"` See [api.domeneshop.no/docs/](https://api.domeneshop.no/docs/) for instructions on how to generate credentials.
- `"secret"`

### 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.
2 changes: 2 additions & 0 deletions internal/provider/constants/providers.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const (
DigitalOcean models.Provider = "digitalocean"
DNSOMatic models.Provider = "dnsomatic"
DNSPod models.Provider = "dnspod"
Domeneshop models.Provider = "domeneshop"
DonDominio models.Provider = "dondominio"
Dreamhost models.Provider = "dreamhost"
DuckDNS models.Provider = "duckdns"
Expand Down Expand Up @@ -65,6 +66,7 @@ func ProviderChoices() []models.Provider {
DigitalOcean,
DNSOMatic,
DNSPod,
Domeneshop,
DonDominio,
Dreamhost,
DuckDNS,
Expand Down
3 changes: 3 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"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"
"github.com/qdm12/ddns-updater/internal/provider/providers/domeneshop"
"github.com/qdm12/ddns-updater/internal/provider/providers/dondominio"
"github.com/qdm12/ddns-updater/internal/provider/providers/dreamhost"
"github.com/qdm12/ddns-updater/internal/provider/providers/duckdns"
Expand Down Expand Up @@ -100,6 +101,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin
return dnsomatic.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.DNSPod:
return dnspod.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.Domeneshop:
return domeneshop.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.DonDominio:
return dondominio.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.Dreamhost:
Expand Down
155 changes: 155 additions & 0 deletions internal/provider/providers/domeneshop/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package domeneshop

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

"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
token string
secret string
ipVersion ipversion.IPVersion
ipv6Suffix netip.Prefix
}

func New(data json.RawMessage, domain, owner string,
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
provider *Provider, err error) {
var providerSpecificSettings struct {
Token string `json:"token"`
Secret string `json:"secret"`
}
err = json.Unmarshal(data, &providerSpecificSettings)
if err != nil {
return nil, fmt.Errorf("json decoding provider specific settings: %w", err)
}

err = validateSettings(domain, owner,
providerSpecificSettings.Token, providerSpecificSettings.Secret)
if err != nil {
return nil, fmt.Errorf("validating provider specific settings: %w", err)
}

return &Provider{
domain: domain,
owner: owner,
token: providerSpecificSettings.Token,
secret: providerSpecificSettings.Secret,
ipVersion: ipVersion,
ipv6Suffix: ipv6Suffix,
}, nil
}

func validateSettings(domain, owner, token, secret string) (err error) {
err = utils.CheckDomain(domain)
if err != nil {
return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err)
}

switch {
case owner == "":
return fmt.Errorf("%w", errors.ErrOwnerNotSet)
case owner == "*":
return fmt.Errorf("%w", errors.ErrOwnerWildcard)
case token == "":
return fmt.Errorf("%w", errors.ErrTokenNotSet)
case secret == "":
return fmt.Errorf("%w", errors.ErrSecretNotSet)
}
return nil
}

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

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

func (p *Provider) Owner() string {
return p.owner
}

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

func (p *Provider) IPv6Suffix() netip.Prefix {
return p.ipv6Suffix
}
jobrajac marked this conversation as resolved.
Show resolved Hide resolved

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

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

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

// Link to documentation:
// https://api.domeneshop.no/docs/#tag/ddns/paths/~1dyndns~1update/get
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "api.domeneshop.no",
Path: "/v0/dyndns/update",
}
values := url.Values{}
values.Set("hostname", utils.BuildURLQueryHostname(p.owner, p.domain))
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)
}

request.SetBasicAuth(p.token, p.secret)
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.StatusNoContent:
return ip, nil
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))
}
}