Skip to content

Commit

Permalink
feat(providers): loopia.se (#842)
Browse files Browse the repository at this point in the history
  • Loading branch information
niklasknoell authored Oct 20, 2024
1 parent 5e4bd16 commit 03f5d0b
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ This readme and the [docs/](docs/) directory are **versioned** to match the prog
- INWX
- Ionos
- Linode
- Loopia
- LuaDNS
- Name.com
- Namecheap
Expand Down Expand Up @@ -239,6 +240,7 @@ Check the documentation for your DNS provider:
- [INWX](docs/inwx.md)
- [Ionos](docs/ionos.md)
- [Linode](docs/linode.md)
- [Loopia](docs/loopia.md)
- [LuaDNS](docs/luadns.md)
- [Name.com](docs/name.com.md)
- [Namecheap](docs/namecheap.md)
Expand Down
31 changes: 31 additions & 0 deletions docs/loopia.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Loopia

## Configuration

### Example

```json
{
"settings": [
{
"provider": "loopia",
"domain": "domain.com",
"username": "username",
"password": "password",
"ip_version": "ipv4",
"ipv6_suffix": ""
}
]
}
```

### Compulsory parameters

- `"domain"` is the domain to update. It can be `example.com` (root domain), `sub.example.com` (subdomain of `example.com`). It cannot be a wildcard domain.
- `"username"`
- `"password"`

### 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 @@ -35,6 +35,7 @@ const (
INWX models.Provider = "inwx"
Ionos models.Provider = "ionos"
Linode models.Provider = "linode"
Loopia models.Provider = "loopia"
LuaDNS models.Provider = "luadns"
Namecheap models.Provider = "namecheap"
NameCom models.Provider = "name.com"
Expand Down Expand Up @@ -86,6 +87,7 @@ func ProviderChoices() []models.Provider {
INWX,
Ionos,
Linode,
Loopia,
LuaDNS,
Namecheap,
NameCom,
Expand Down
3 changes: 3 additions & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import (
"github.com/qdm12/ddns-updater/internal/provider/providers/inwx"
"github.com/qdm12/ddns-updater/internal/provider/providers/ionos"
"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/namecheap"
"github.com/qdm12/ddns-updater/internal/provider/providers/namecom"
Expand Down Expand Up @@ -141,6 +142,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin
return ionos.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.Linode:
return linode.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.Loopia:
return loopia.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.LuaDNS:
return luadns.New(data, domain, owner, ipVersion, ipv6Suffix)
case constants.Namecheap:
Expand Down
169 changes: 169 additions & 0 deletions internal/provider/providers/loopia/provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package loopia

import (
"context"
"encoding/json"
"fmt"
"io"
"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
username string
password string
}

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

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

return &Provider{
domain: domain,
owner: owner,
ipVersion: ipVersion,
ipv6Suffix: ipv6Suffix,
username: providerSpecificSettings.Username,
password: providerSpecificSettings.Password,
}, nil
}

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

switch {
// Loopia says it supports wildcards but cannot get it to work
case owner == "*":
return fmt.Errorf("%w", errors.ErrOwnerWildcard)
case username == "":
return fmt.Errorf("%w", errors.ErrUsernameNotSet)
case password == "":
return fmt.Errorf("%w", errors.ErrPasswordNotSet)
}
return nil
}

func (p *Provider) String() string {
return utils.ToString(p.domain, p.owner, constants.Dyn, 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
}

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://www.loopia.se/\">Loopia</a>",
IPVersion: p.ipVersion.String(),
}
}

// Unfortunately api description only available in swedish
// https://support.loopia.se/wiki/om-dyndns-stodet/
// Bit of explanation for curl scripting in english is available at
// https://support.loopia.com/wiki/curl/
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
User: url.UserPassword(p.username, p.password),
Host: "dyndns.loopia.se",
Path: "/",
}
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)
}
headers.SetUserAgent(request)

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

// response is simply plain text
b, err := io.ReadAll(response.Body)
if err != nil {
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
}
s := string(b)

// have only found 200 returned
if response.StatusCode != http.StatusOK {
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
}

switch {
case strings.HasPrefix(s, "nohost"),
strings.HasPrefix(s, constants.Notfqdn):
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case strings.HasPrefix(s, "badrequest"),
strings.HasPrefix(s, "911"):
// 911 is returned for multiple issues: bad ip, bad request formats, etc
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
case strings.HasPrefix(s, "badauth"):
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case strings.HasPrefix(s, "good"), strings.HasPrefix(s, "nochg"):
return ip, nil
default:
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
}

0 comments on commit 03f5d0b

Please sign in to comment.