Skip to content

Commit 456e913

Browse files
niklasknoellqdm12
niklasknoell
authored andcommitted
add Loopia
1 parent 5e4bd16 commit 456e913

File tree

5 files changed

+210
-0
lines changed

5 files changed

+210
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ This readme and the [docs/](docs/) directory are **versioned** to match the prog
7676
- INWX
7777
- Ionos
7878
- Linode
79+
- Loopia
7980
- LuaDNS
8081
- Name.com
8182
- Namecheap
@@ -239,6 +240,7 @@ Check the documentation for your DNS provider:
239240
- [INWX](docs/inwx.md)
240241
- [Ionos](docs/ionos.md)
241242
- [Linode](docs/linode.md)
243+
- [Loopia](docs/loopia.md)
242244
- [LuaDNS](docs/luadns.md)
243245
- [Name.com](docs/name.com.md)
244246
- [Namecheap](docs/namecheap.md)

docs/loopia.md

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Loopia
2+
3+
## Configuration
4+
5+
### Example
6+
7+
```json
8+
{
9+
"settings": [
10+
{
11+
"provider": "loopia",
12+
"domain": "domain.com",
13+
"username": "username",
14+
"password": "password",
15+
"ip_version": "ipv4",
16+
"ipv6_suffix": ""
17+
}
18+
]
19+
}
20+
```
21+
22+
### Compulsory parameters
23+
24+
- `"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.
25+
- `"username"`
26+
- `"password"`
27+
28+
### Optional parameters
29+
30+
- `"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`.
31+
- `"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.

internal/provider/constants/providers.go

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const (
3535
INWX models.Provider = "inwx"
3636
Ionos models.Provider = "ionos"
3737
Linode models.Provider = "linode"
38+
Loopia models.Provider = "loopia"
3839
LuaDNS models.Provider = "luadns"
3940
Namecheap models.Provider = "namecheap"
4041
NameCom models.Provider = "name.com"
@@ -86,6 +87,7 @@ func ProviderChoices() []models.Provider {
8687
INWX,
8788
Ionos,
8889
Linode,
90+
Loopia,
8991
LuaDNS,
9092
Namecheap,
9193
NameCom,

internal/provider/provider.go

+2
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ func New(providerName models.Provider, data json.RawMessage, domain, owner strin
141141
return ionos.New(data, domain, owner, ipVersion, ipv6Suffix)
142142
case constants.Linode:
143143
return linode.New(data, domain, owner, ipVersion, ipv6Suffix)
144+
case constants.Loopia:
145+
return loopia.New(data, domain, owner, ipVersion, ipv6Suffix)
144146
case constants.LuaDNS:
145147
return luadns.New(data, domain, owner, ipVersion, ipv6Suffix)
146148
case constants.Namecheap:
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
package loopia
2+
3+
import (
4+
"context"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"net/netip"
10+
"net/url"
11+
"strings"
12+
13+
"github.com/qdm12/ddns-updater/internal/models"
14+
"github.com/qdm12/ddns-updater/internal/provider/constants"
15+
"github.com/qdm12/ddns-updater/internal/provider/errors"
16+
"github.com/qdm12/ddns-updater/internal/provider/headers"
17+
"github.com/qdm12/ddns-updater/internal/provider/utils"
18+
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
19+
)
20+
21+
type Provider struct {
22+
domain string
23+
owner string
24+
ipVersion ipversion.IPVersion
25+
ipv6Suffix netip.Prefix
26+
username string
27+
password string
28+
}
29+
30+
func New(data json.RawMessage, domain, owner string,
31+
ipVersion ipversion.IPVersion, ipv6Suffix netip.Prefix) (
32+
provider *Provider, err error) {
33+
var providerSpecificSettings struct {
34+
Username string `json:"username"`
35+
Password string `json:"password"`
36+
}
37+
err = json.Unmarshal(data, &providerSpecificSettings)
38+
if err != nil {
39+
return nil, fmt.Errorf("json decoding provider specific settings: %w", err)
40+
}
41+
42+
err = validateSettings(domain, owner,
43+
providerSpecificSettings.Username, providerSpecificSettings.Password)
44+
if err != nil {
45+
return nil, fmt.Errorf("validating provider specific settings: %w", err)
46+
}
47+
48+
return &Provider{
49+
domain: domain,
50+
owner: owner,
51+
ipVersion: ipVersion,
52+
ipv6Suffix: ipv6Suffix,
53+
username: providerSpecificSettings.Username,
54+
password: providerSpecificSettings.Password,
55+
}, nil
56+
}
57+
58+
func validateSettings(domain, owner, username, password string) (err error) {
59+
err = utils.CheckDomain(domain)
60+
if err != nil {
61+
return fmt.Errorf("%w: %w", errors.ErrDomainNotValid, err)
62+
}
63+
64+
switch {
65+
// Loopia says it supports wildcards but cannot get it to work
66+
case owner == "*":
67+
return fmt.Errorf("%w", errors.ErrOwnerWildcard)
68+
case username == "":
69+
return fmt.Errorf("%w", errors.ErrUsernameNotSet)
70+
case password == "":
71+
return fmt.Errorf("%w", errors.ErrPasswordNotSet)
72+
}
73+
return nil
74+
}
75+
76+
func (p *Provider) String() string {
77+
return utils.ToString(p.domain, p.owner, constants.Dyn, p.ipVersion)
78+
}
79+
80+
func (p *Provider) Domain() string {
81+
return p.domain
82+
}
83+
84+
func (p *Provider) Owner() string {
85+
return p.owner
86+
}
87+
88+
func (p *Provider) IPVersion() ipversion.IPVersion {
89+
return p.ipVersion
90+
}
91+
92+
func (p *Provider) IPv6Suffix() netip.Prefix {
93+
return p.ipv6Suffix
94+
}
95+
96+
func (p *Provider) Proxied() bool {
97+
return false
98+
}
99+
100+
func (p *Provider) BuildDomainName() string {
101+
return utils.BuildDomainName(p.owner, p.domain)
102+
}
103+
104+
func (p *Provider) HTML() models.HTMLRow {
105+
return models.HTMLRow{
106+
Domain: fmt.Sprintf("<a href=\"http://%s\">%s</a>", p.BuildDomainName(), p.BuildDomainName()),
107+
Owner: p.Owner(),
108+
Provider: "<a href=\"https://www.loopia.se/\">Loopia</a>",
109+
IPVersion: p.ipVersion.String(),
110+
}
111+
}
112+
113+
// Unfortunately api description only available in swedish
114+
// https://support.loopia.se/wiki/om-dyndns-stodet/
115+
// Bit of explanation for curl scripting in english is available at
116+
// https://support.loopia.com/wiki/curl/
117+
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
118+
u := url.URL{
119+
Scheme: "https",
120+
User: url.UserPassword(p.username, p.password),
121+
Host: "dyndns.loopia.se",
122+
Path: "/",
123+
}
124+
values := url.Values{}
125+
values.Set("hostname", utils.BuildURLQueryHostname(p.owner, p.domain))
126+
values.Set("myip", ip.String())
127+
u.RawQuery = values.Encode()
128+
129+
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
130+
if err != nil {
131+
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
132+
}
133+
headers.SetUserAgent(request)
134+
135+
response, err := client.Do(request)
136+
if err != nil {
137+
return netip.Addr{}, err
138+
}
139+
defer response.Body.Close()
140+
141+
// response is simply plain text
142+
b, err := io.ReadAll(response.Body)
143+
if err != nil {
144+
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
145+
}
146+
s := string(b)
147+
148+
// have only found 200 returned
149+
if response.StatusCode != http.StatusOK {
150+
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
151+
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
152+
}
153+
154+
switch {
155+
case strings.HasPrefix(s, "nohost"):
156+
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
157+
case strings.HasPrefix(s, "badrequest"):
158+
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
159+
case strings.HasPrefix(s, "911"):
160+
// returned for multiple issues, bad ip, bad request formats, etc
161+
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
162+
case strings.HasPrefix(s, "badauth"):
163+
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
164+
case strings.HasPrefix(s, constants.Notfqdn):
165+
return netip.Addr{}, fmt.Errorf("%w", errors.Notfqdn)
166+
case strings.HasPrefix(s, "good"):
167+
return ip, nil
168+
case strings.HasPrefix(s, "nochg"):
169+
return ip, nil
170+
default:
171+
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
172+
}
173+
}

0 commit comments

Comments
 (0)