mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-04-21 00:22:35 -04:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e95816ab46 | ||
|
|
75191c2876 | ||
|
|
4b65908a88 | ||
|
|
f910ac9cfc | ||
|
|
5a56464b38 | ||
|
|
8b9ca1204e | ||
|
|
0b747f8323 | ||
|
|
7f6ccdb4fb | ||
|
|
7b8505cff4 | ||
|
|
40e4da4e93 | ||
|
|
981bed1e13 | ||
|
|
d73be0a9e5 | ||
|
|
4c7f17e816 | ||
|
|
5ea1537d59 | ||
|
|
9220585a98 |
@@ -20,7 +20,7 @@
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"` is the domain to update. For example, for the owner/host `sub`, it would be `sub.duckdns.org`. The [eTLD+1](https://developer.mozilla.org/en-US/docs/Glossary/eTLD) must be `duckdns.org`.
|
||||
- `"domain"` is the domain to update. For example, for the owner/host `sub`, it would be `sub.duckdns.org`. The [eTLD](https://developer.mozilla.org/en-US/docs/Glossary/eTLD) must be `duckdns.org`.
|
||||
- `"token"`
|
||||
|
||||
### Optional parameters
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"settings": [
|
||||
{
|
||||
"provider": "goip",
|
||||
"domain": "mysubdomain.goip.de",
|
||||
"domain": "mydomain.goip.de",
|
||||
"username": "username",
|
||||
"password": "password",
|
||||
"ip_version": "",
|
||||
@@ -22,12 +22,11 @@
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"` is the domain to update. For example, for the owner/host `sub`, it would be `sub.goip.de`. The [eTLD+1](https://developer.mozilla.org/en-US/docs/Glossary/eTLD) must be `goip.de` or `goip.it`.
|
||||
- `"domain"` is the domain to update. For example, for the owner/host `sub`, it would be `sub.goip.de`. The [eTLD](https://developer.mozilla.org/en-US/docs/Glossary/eTLD) must be `goip.de` or `goip.it`.
|
||||
- `"username"` is your goip.de username listed under "Routers"
|
||||
- `"password"` is your router account password
|
||||
|
||||
### Optional parameters
|
||||
|
||||
- `"domain"` is the domain name which can be `goip.de` or `goip.it`, and defaults to `goip.de` if left unset. This is automatically disabled for an IPv6 public address since it is not supported.
|
||||
- `"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`.
|
||||
- `"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.
|
||||
|
||||
@@ -26,7 +26,7 @@ Also keep in mind, that TTL, Expire, Retry and Refresh values of the given Domai
|
||||
|
||||
### Compulsory parameters
|
||||
|
||||
- `"domain"` is the domain to update. It can be `example.com` (root domain) or `sub.example.com` (subdomain of `example.com`).
|
||||
- `"domain"` is the domain to update. It can be `example.com` (root domain) or `sub.example.com` (subdomain of `example.com`) or the wildcard `*.example.com`.
|
||||
- `"api_key"` is your api key (generated in the [customercontrolpanel](https://www.customercontrolpanel.de))
|
||||
- `"password"` is your api password (generated in the [customercontrolpanel](https://www.customercontrolpanel.de)). Netcup only allows one ApiPassword. This is not the account password. This password is used for all api keys.
|
||||
- `"customer_number"` is your customer number (viewable in the [customercontrolpanel](https://www.customercontrolpanel.de) next to your name). As seen in the example above, provide the number as string value.
|
||||
|
||||
@@ -41,5 +41,12 @@
|
||||
## Record creation
|
||||
|
||||
In case you don't have an A or AAAA record for your host and domain combination, it will be created by DDNS-Updater.
|
||||
However, to do so, the corresponding ALIAS record, that is automatically created by Porkbun, is automatically deleted to allow this.
|
||||
|
||||
Porkbun creates default DNS entries for new domains, which can conflict with creating a root or wildcard A/AAAA record. Therefore, ddns-updater automatically removes any conflicting default record before creating records, as described in the table below:
|
||||
|
||||
| Record type | Owner | Record value | Situation requiring a removal |
|
||||
| --- | --- | --- | --- |
|
||||
| `ALIAS` | `@` | pixie.porkbun.com | Creating A or AAAA record for the root domain **or** wildcard domain |
|
||||
| `CNAME` | `*` | pixie.porkbun.com | Creating A or AAAA record for the wildcard domain |
|
||||
|
||||
More details is in [this comment by @everydaycombat](https://github.com/qdm12/ddns-updater/issues/546#issuecomment-1773960193).
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
A = "A"
|
||||
AAAA = "AAAA"
|
||||
)
|
||||
8
internal/provider/constants/recordtypes.go
Normal file
8
internal/provider/constants/recordtypes.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package constants
|
||||
|
||||
const (
|
||||
A = "A"
|
||||
AAAA = "AAAA"
|
||||
CNAME = "CNAME"
|
||||
ALIAS = "ALIAS"
|
||||
)
|
||||
@@ -57,7 +57,7 @@ func (p *Provider) getRecordID(ctx context.Context, client *http.Client,
|
||||
if err != nil || data.Code != "InvalidDomainName.NoExist" {
|
||||
return "", fmt.Errorf("%w: %d: %s",
|
||||
errors.ErrHTTPStatusNotValid, response.StatusCode,
|
||||
utils.BodyToSingleLine(response.Body))
|
||||
utils.ToSingleLine(string(bodyBytes)))
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w", errors.ErrRecordNotFound)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -137,11 +136,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid,
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -125,19 +124,16 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.ToSingleLine(s))
|
||||
}
|
||||
|
||||
s = strings.ToLower(s)
|
||||
|
||||
switch {
|
||||
case strings.Contains(s, "authorization failed"):
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -140,11 +139,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
@@ -158,7 +156,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
|
||||
case strings.Contains(s, constants.Notfqdn):
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
|
||||
case strings.Contains(s, "Updated 1 hostname"):
|
||||
case strings.Contains(s, "updated 1 hostname"):
|
||||
return ip, nil
|
||||
default:
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -127,11 +126,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusOK:
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -140,11 +139,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
|
||||
@@ -120,7 +120,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
RawQuery: url.Values{
|
||||
"user": {p.username},
|
||||
"apikey": {p.key},
|
||||
"host": {p.BuildDomainName()},
|
||||
"host": {utils.BuildURLQueryHostname(p.owner, p.domain)},
|
||||
"ip": {ip.String()},
|
||||
"lang": {"en"},
|
||||
}.Encode(),
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -161,11 +160,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
@@ -176,9 +174,9 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
switch {
|
||||
case len(s) < minChars:
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrResponseTooShort, s)
|
||||
case s[0:minChars] == "KO":
|
||||
case s[0:minChars] == "ko":
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
|
||||
case s[0:minChars] == "OK":
|
||||
case s[0:minChars] == "ok":
|
||||
var ips []netip.Addr
|
||||
if ip.Is6() {
|
||||
ips = ipextract.IPv6(s)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -137,11 +136,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -146,11 +145,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -134,11 +133,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid,
|
||||
@@ -148,13 +146,13 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
switch {
|
||||
case s == "":
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrReceivedNoResult)
|
||||
case strings.Contains(s, "NO_SERVICE"):
|
||||
case strings.Contains(s, "no_service"):
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoService)
|
||||
case strings.Contains(s, "NO_ACCESS"):
|
||||
case strings.Contains(s, "no_access"):
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
|
||||
case strings.Contains(s, "ILLEGAL_INPUT"), strings.Contains(s, "TOO_SOON"):
|
||||
case strings.Contains(s, "illegal_input"), strings.Contains(s, "too_soon"):
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBannedAbuse)
|
||||
case strings.Contains(s, "NO_ERROR"), strings.Contains(s, "OK"):
|
||||
case strings.Contains(s, "no_error"), strings.Contains(s, "ok"):
|
||||
return ip, nil
|
||||
default:
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -154,11 +153,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
|
||||
// TODO handle the encoding of the response body properly. Often it can be JSON,
|
||||
// see other provider code for examples on how to decode JSON.
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
// TODO handle every possible status codes from the provider API.
|
||||
// If undocumented, try them out by sending bogus HTTP requests to see
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -124,22 +123,20 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)
|
||||
}
|
||||
|
||||
loweredResponse := strings.ToLower(s)
|
||||
switch {
|
||||
case loweredResponse == "":
|
||||
case s == "":
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrReceivedNoResult)
|
||||
case strings.HasPrefix(loweredResponse, "no ip change detected"),
|
||||
strings.HasPrefix(loweredResponse, "updated "):
|
||||
case strings.HasPrefix(s, "no ip change detected"),
|
||||
strings.HasPrefix(s, "updated "):
|
||||
return ip, nil
|
||||
default:
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))
|
||||
|
||||
@@ -110,6 +110,7 @@ func (p *Provider) HTML() models.HTMLRow {
|
||||
}
|
||||
}
|
||||
|
||||
// See https://api.gandi.net/docs/livedns/#v5-livedns-domains-fqdn-records
|
||||
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
|
||||
recordType := constants.A
|
||||
if ip.Is6() {
|
||||
@@ -118,23 +119,18 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "https",
|
||||
Host: "dns.api.gandi.net",
|
||||
Path: fmt.Sprintf("/api/v5/domains/%s/records/%s/%s", p.domain, p.owner, recordType),
|
||||
Host: "api.gandi.net",
|
||||
Path: fmt.Sprintf("v5/livedns/domains/%s/records/%s/%s", p.domain, p.owner, recordType),
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(nil)
|
||||
encoder := json.NewEncoder(buffer)
|
||||
const defaultTTL uint32 = 3600
|
||||
ttl := defaultTTL
|
||||
if p.ttl != 0 {
|
||||
ttl = p.ttl
|
||||
}
|
||||
requestData := struct {
|
||||
Values [1]string `json:"rrset_values"`
|
||||
TTL uint32 `json:"rrset_ttl"`
|
||||
TTL uint32 `json:"rrset_ttl,omitempty"`
|
||||
}{
|
||||
Values: [1]string{ip.Unmap().String()},
|
||||
TTL: ttl,
|
||||
TTL: p.ttl,
|
||||
}
|
||||
err = encoder.Encode(requestData)
|
||||
if err != nil {
|
||||
|
||||
@@ -34,13 +34,6 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
}
|
||||
|
||||
for _, rrdata := range recordResourceSet.Rrdatas {
|
||||
if rrdata == ip.String() {
|
||||
// already up to date
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
if !rrSetFound {
|
||||
err = p.createRRSet(ctx, client, fqdn, recordType, ip)
|
||||
if err != nil {
|
||||
@@ -49,6 +42,13 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
for _, rrdata := range recordResourceSet.Rrdatas {
|
||||
if rrdata == ip.String() {
|
||||
// already up to date
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
|
||||
err = p.patchRRSet(ctx, client, fqdn, recordType, ip)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("updating record: %w", err)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -170,15 +169,15 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
utils.BodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(s, p.BuildDomainName()+" ("+ip.String()+")"):
|
||||
return ip, nil
|
||||
case strings.HasPrefix(strings.ToLower(s), "zugriff verweigert"):
|
||||
case strings.HasPrefix(s, "zugriff verweigert"):
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
|
||||
default:
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -126,11 +125,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
switch s {
|
||||
case "":
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -132,11 +131,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusOK:
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -135,11 +134,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)
|
||||
|
||||
@@ -37,7 +37,7 @@ func New(data json.RawMessage, domain, owner string,
|
||||
return nil, fmt.Errorf("JSON decoding provider specific settings: %w", err)
|
||||
}
|
||||
|
||||
err = validateSettings(domain, owner, extraSettings.CustomerNumber,
|
||||
err = validateSettings(domain, extraSettings.CustomerNumber,
|
||||
extraSettings.APIKey, extraSettings.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("validating provider specific settings: %w", err)
|
||||
@@ -54,15 +54,13 @@ func New(data json.RawMessage, domain, owner string,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func validateSettings(domain, owner, customerNumber, apiKey, password string) (err error) {
|
||||
func validateSettings(domain, customerNumber, apiKey, password 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.ErrOwnerWildcard)
|
||||
case customerNumber == "":
|
||||
return fmt.Errorf("%w", errors.ErrCustomerNumberNotSet)
|
||||
case apiKey == "":
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -139,14 +138,9 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
|
||||
switch s {
|
||||
@@ -166,7 +160,9 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
|
||||
}
|
||||
|
||||
if !strings.Contains(s, "nochg") && !strings.Contains(s, "good") {
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)
|
||||
} else if !strings.Contains(s, "nochg") && !strings.Contains(s, "good") {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -128,11 +127,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
switch response.StatusCode {
|
||||
case http.StatusOK:
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -132,11 +131,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)
|
||||
|
||||
@@ -10,17 +10,17 @@ import (
|
||||
)
|
||||
|
||||
func extractAPIError(response *http.Response) (err error) {
|
||||
decoder := json.NewDecoder(response.Body)
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
_ = response.Body.Close()
|
||||
return fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
|
||||
var apiError struct {
|
||||
Message string `json:"Message"`
|
||||
}
|
||||
err = decoder.Decode(&apiError)
|
||||
err = json.Unmarshal(b, &apiError)
|
||||
if err != nil {
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
_ = response.Body.Close()
|
||||
return fmt.Errorf("reading response body: %w", err)
|
||||
}
|
||||
apiError.Message = string(b)
|
||||
}
|
||||
queryID := response.Header.Get("X-Ovh-QueryID")
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -171,11 +170,10 @@ func (p *Provider) updateWithDynHost(ctx context.Context, client *http.Client,
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, s)
|
||||
|
||||
@@ -11,13 +11,23 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/provider/errors"
|
||||
)
|
||||
|
||||
type dnsRecord struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
Content string `json:"content"`
|
||||
TTL string `json:"ttl"`
|
||||
Priority string `json:"prio"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
// See https://porkbun.com/api/json/v3/documentation#DNS%20Retrieve%20Records%20by%20Domain,%20Subdomain%20and%20Type
|
||||
func (p *Provider) getRecordIDs(ctx context.Context, client *http.Client, recordType string) (
|
||||
recordIDs []string, err error) {
|
||||
url := "https://porkbun.com/api/json/v3/dns/retrieveByNameType/" + p.domain + "/" + recordType + "/"
|
||||
if p.owner != "@" {
|
||||
func (p *Provider) getRecords(ctx context.Context, client *http.Client, recordType, owner string) (
|
||||
records []dnsRecord, err error) {
|
||||
url := "https://api.porkbun.com/api/json/v3/dns/retrieveByNameType/" + p.domain + "/" + recordType + "/"
|
||||
if owner != "@" {
|
||||
// Note Porkbun requires we send the unescaped '*' character.
|
||||
url += p.owner
|
||||
url += owner
|
||||
}
|
||||
|
||||
postRecordsParams := struct {
|
||||
@@ -29,29 +39,22 @@ func (p *Provider) getRecordIDs(ctx context.Context, client *http.Client, record
|
||||
}
|
||||
|
||||
type jsonResponseData struct {
|
||||
Records []struct {
|
||||
ID string `json:"id"`
|
||||
} `json:"records"`
|
||||
Records []dnsRecord `json:"records"`
|
||||
}
|
||||
const decodeBody = true
|
||||
responseData, err := httpPost[jsonResponseData](ctx, client, url, postRecordsParams, decodeBody)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("for record type %s: %w",
|
||||
recordType, err)
|
||||
return nil, fmt.Errorf("for record type %s and record owner %s: %w",
|
||||
recordType, owner, err)
|
||||
}
|
||||
|
||||
recordIDs = make([]string, len(responseData.Records))
|
||||
for i := range responseData.Records {
|
||||
recordIDs[i] = responseData.Records[i].ID
|
||||
}
|
||||
|
||||
return recordIDs, nil
|
||||
return responseData.Records, nil
|
||||
}
|
||||
|
||||
// See https://porkbun.com/api/json/v3/documentation#DNS%20Create%20Record
|
||||
func (p *Provider) createRecord(ctx context.Context, client *http.Client,
|
||||
recordType, ipStr string) (err error) {
|
||||
url := "https://porkbun.com/api/json/v3/dns/create/" + p.domain
|
||||
recordType, owner, ipStr string) (err error) {
|
||||
url := "https://api.porkbun.com/api/json/v3/dns/create/" + p.domain
|
||||
postRecordsParams := struct {
|
||||
SecretAPIKey string `json:"secretapikey"`
|
||||
APIKey string `json:"apikey"`
|
||||
@@ -64,14 +67,14 @@ func (p *Provider) createRecord(ctx context.Context, client *http.Client,
|
||||
APIKey: p.apiKey,
|
||||
Content: ipStr,
|
||||
Type: recordType,
|
||||
Name: p.owner,
|
||||
Name: owner,
|
||||
TTL: strconv.FormatUint(uint64(p.ttl), 10),
|
||||
}
|
||||
const decodeBody = false
|
||||
_, err = httpPost[struct{}](ctx, client, url, postRecordsParams, decodeBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("for record type %s: %w",
|
||||
recordType, err)
|
||||
return fmt.Errorf("for record type %s and record owner %s: %w",
|
||||
recordType, owner, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -79,8 +82,8 @@ func (p *Provider) createRecord(ctx context.Context, client *http.Client,
|
||||
|
||||
// See https://porkbun.com/api/json/v3/documentation#DNS%20Edit%20Record%20by%20Domain%20and%20ID
|
||||
func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
|
||||
recordType, ipStr, recordID string) (err error) {
|
||||
url := "https://porkbun.com/api/json/v3/dns/edit/" + p.domain + "/" + recordID
|
||||
recordType, owner, ipStr, recordID string) (err error) {
|
||||
url := "https://api.porkbun.com/api/json/v3/dns/edit/" + p.domain + "/" + recordID
|
||||
postRecordsParams := struct {
|
||||
SecretAPIKey string `json:"secretapikey"`
|
||||
APIKey string `json:"apikey"`
|
||||
@@ -94,24 +97,24 @@ func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
|
||||
Content: ipStr,
|
||||
Type: recordType,
|
||||
TTL: strconv.FormatUint(uint64(p.ttl), 10),
|
||||
Name: p.owner,
|
||||
Name: owner,
|
||||
}
|
||||
const decodeBody = false
|
||||
_, err = httpPost[struct{}](ctx, client, url, postRecordsParams, decodeBody)
|
||||
if err != nil {
|
||||
return fmt.Errorf("for record type %s and record id %s: %w",
|
||||
recordType, recordID, err)
|
||||
return fmt.Errorf("for record type %s, record owner %s and record id %s: %w",
|
||||
recordType, owner, recordID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// See https://porkbun.com/api/json/v3/documentation#DNS%20Delete%20Records%20by%20Domain,%20Subdomain%20and%20Type
|
||||
func (p *Provider) deleteAliasRecord(ctx context.Context, client *http.Client) (err error) {
|
||||
url := "https://porkbun.com/api/json/v3/dns/deleteByNameType/" + p.domain + "/ALIAS/"
|
||||
if p.owner != "@" {
|
||||
func (p *Provider) deleteRecord(ctx context.Context, client *http.Client, recordType, owner string) (err error) {
|
||||
url := "https://api.porkbun.com/api/json/v3/dns/deleteByNameType/" + p.domain + "/" + recordType + "/"
|
||||
if owner != "@" {
|
||||
// Note Porkbun requires we send the unescaped '*' character.
|
||||
url += p.owner
|
||||
url += owner
|
||||
}
|
||||
postRecordsParams := struct {
|
||||
SecretAPIKey string `json:"secretapikey"`
|
||||
@@ -124,7 +127,8 @@ func (p *Provider) deleteAliasRecord(ctx context.Context, client *http.Client) (
|
||||
const decodeBody = false
|
||||
_, err = httpPost[struct{}](ctx, client, url, postRecordsParams, decodeBody)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("for record type %s and record owner %s: %w",
|
||||
recordType, owner, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,6 +3,7 @@ package porkbun
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
@@ -119,46 +120,85 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
recordType = constants.AAAA
|
||||
}
|
||||
ipStr := ip.String()
|
||||
recordIDs, err := p.getRecordIDs(ctx, client, recordType)
|
||||
records, err := p.getRecords(ctx, client, recordType, p.owner)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("getting record IDs: %w", err)
|
||||
}
|
||||
|
||||
if len(recordIDs) == 0 {
|
||||
// ALIAS record needs to be deleted to allow creating an A record.
|
||||
err = p.deleteALIASRecordIfNeeded(ctx, client)
|
||||
if len(records) == 0 {
|
||||
err = p.deleteDefaultConflictingRecordsIfNeeded(ctx, client)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("deleting ALIAS record if needed: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("deleting default conflicting records: %w", err)
|
||||
}
|
||||
|
||||
err = p.createRecord(ctx, client, recordType, ipStr)
|
||||
err = p.createRecord(ctx, client, recordType, p.owner, ipStr)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("creating record: %w", err)
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
for _, recordID := range recordIDs {
|
||||
err = p.updateRecord(ctx, client, recordType, ipStr, recordID)
|
||||
for _, record := range records {
|
||||
err = p.updateRecord(ctx, client, recordType, p.owner, ipStr, record.ID)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("updating record: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
func (p *Provider) deleteALIASRecordIfNeeded(ctx context.Context, client *http.Client) (err error) {
|
||||
aliasRecordIDs, err := p.getRecordIDs(ctx, client, "ALIAS")
|
||||
if err != nil {
|
||||
return fmt.Errorf("getting ALIAS record IDs: %w", err)
|
||||
} else if len(aliasRecordIDs) == 0 {
|
||||
// deleteDefaultConflictingRecordsIfNeeded deletes any default records that would conflict with a new record,
|
||||
// see https://github.com/qdm12/ddns-updater/blob/master/docs/porkbun.md#record-creation
|
||||
func (p *Provider) deleteDefaultConflictingRecordsIfNeeded(ctx context.Context, client *http.Client) (err error) {
|
||||
const porkbunParkedDomain = "pixie.porkbun.com"
|
||||
switch p.owner {
|
||||
case "@":
|
||||
err = p.deleteSingleMatchingRecord(ctx, client, constants.ALIAS, "@", porkbunParkedDomain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting default ALIAS @ parked domain record: %w", err)
|
||||
}
|
||||
return nil
|
||||
case "*":
|
||||
err = p.deleteSingleMatchingRecord(ctx, client, constants.CNAME, "*", porkbunParkedDomain)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting default CNAME * parked domain record: %w", err)
|
||||
}
|
||||
|
||||
err = p.deleteSingleMatchingRecord(ctx, client, constants.ALIAS, "@", porkbunParkedDomain)
|
||||
if err == nil || stderrors.Is(err, errors.ErrConflictingRecord) {
|
||||
// allow conflict ALIAS records to be set to something besides the parked domain
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("deleting default ALIAS @ parked domain record: %w", err)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
err = p.deleteAliasRecord(ctx, client)
|
||||
// deleteSingleMatchingRecord deletes an eventually present record matching a specific record type if the content
|
||||
// matches the expected content value.
|
||||
// It returns an error if multiple records are found or if one record is found with an unexpected value.
|
||||
func (p *Provider) deleteSingleMatchingRecord(ctx context.Context, client *http.Client,
|
||||
recordType, owner, expectedContent string) (err error) {
|
||||
records, err := p.getRecords(ctx, client, recordType, owner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting ALIAS record: %w", err)
|
||||
return fmt.Errorf("getting records: %w", err)
|
||||
}
|
||||
|
||||
switch {
|
||||
case len(records) == 0:
|
||||
return nil
|
||||
case len(records) > 1:
|
||||
return fmt.Errorf("%w: %d %s records are already set", errors.ErrConflictingRecord, len(records), recordType)
|
||||
case records[0].Content != expectedContent:
|
||||
return fmt.Errorf("%w: %s record has content %q mismatching expected content %q",
|
||||
errors.ErrConflictingRecord, recordType, records[0].Content, expectedContent)
|
||||
}
|
||||
|
||||
// Single record with content matching expected content.
|
||||
err = p.deleteRecord(ctx, client, recordType, owner)
|
||||
if err != nil {
|
||||
return fmt.Errorf("deleting record: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -154,11 +153,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
errors.ErrHTTPStatusNotValid, response.StatusCode, utils.BodyToSingleLine(response.Body))
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(s, constants.Notfqdn):
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -148,11 +147,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
bodyString, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
bodyString := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -127,11 +126,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
str, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
str := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid, response.StatusCode, str)
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -137,11 +136,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
"net/url"
|
||||
@@ -135,18 +134,16 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Add
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
s, err := utils.ReadAndCleanBody(response.Body)
|
||||
if err != nil {
|
||||
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
|
||||
return netip.Addr{}, fmt.Errorf("reading response: %w", err)
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrHTTPStatusNotValid,
|
||||
response.StatusCode, utils.ToSingleLine(s))
|
||||
}
|
||||
|
||||
s = strings.ToLower(s)
|
||||
switch {
|
||||
case strings.Contains(s, `success_code="200"`):
|
||||
ips := ipextract.IPv4(s)
|
||||
|
||||
26
internal/provider/utils/body.go
Normal file
26
internal/provider/utils/body.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ReadAndCleanBody reads the body, closes it, trims spaces from the body data
|
||||
// and converts it to lowercase.
|
||||
func ReadAndCleanBody(body io.ReadCloser) (cleanedBody string, err error) {
|
||||
b, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("reading body: %w", err)
|
||||
}
|
||||
err = body.Close()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("closing body: %w", err)
|
||||
}
|
||||
|
||||
cleanedBody = string(b)
|
||||
cleanedBody = strings.TrimSpace(cleanedBody)
|
||||
cleanedBody = strings.ToLower(cleanedBody)
|
||||
|
||||
return cleanedBody, nil
|
||||
}
|
||||
@@ -32,14 +32,22 @@ func (s *Service) logInfoNoLookupUpdate(hostname, ipKind string, lastIP, ip neti
|
||||
ipKind, hostname, lastIP, ipKind, ip))
|
||||
}
|
||||
|
||||
func (s *Service) logDebugLookupSkip(hostname, ipKind string, recordIP, ip netip.Addr) {
|
||||
func (s *Service) logDebugLookupSkip(hostname, ipKind string, recordIPs []netip.Addr, ip netip.Addr) {
|
||||
s.logger.Debug(fmt.Sprintf("%s address of %s is %s and your %s address"+
|
||||
" is %s, skipping update", ipKind, hostname, recordIP, ipKind, ip))
|
||||
" is %s, skipping update", ipKind, hostname, ipsToString(recordIPs), ipKind, ip))
|
||||
}
|
||||
|
||||
func (s *Service) logInfoLookupUpdate(hostname, ipKind string, recordIP, ip netip.Addr) {
|
||||
s.logger.Info(fmt.Sprintf("%s address of %s is %s and your %s address is %s",
|
||||
ipKind, hostname, recordIP, ipKind, ip))
|
||||
func (s *Service) logInfoLookupUpdate(hostname, ipKind string, recordIPs []netip.Addr, ip netip.Addr) {
|
||||
s.logger.Info(fmt.Sprintf("%s address of %s is %s and your %s address is %s",
|
||||
ipKind, hostname, ipsToString(recordIPs), ipKind, ip))
|
||||
}
|
||||
|
||||
func ipsToString(ips []netip.Addr) string {
|
||||
ipStrings := make([]string, len(ips))
|
||||
for i, ip := range ips {
|
||||
ipStrings[i] = ip.String()
|
||||
}
|
||||
return strings.Join(ipStrings, ", ")
|
||||
}
|
||||
|
||||
type joinedErrors struct { //nolint:errname
|
||||
|
||||
@@ -3,8 +3,10 @@ package update
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/netip"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
@@ -34,7 +36,8 @@ type Service struct {
|
||||
|
||||
func NewService(db Database, updater UpdaterInterface, ipGetter PublicIPFetcher,
|
||||
period time.Duration, cooldown time.Duration, logger Logger, resolver LookupIPer,
|
||||
timeNow func() time.Time, hioClient HealthchecksIOClient) *Service {
|
||||
timeNow func() time.Time, hioClient HealthchecksIOClient,
|
||||
) *Service {
|
||||
return &Service{
|
||||
period: period,
|
||||
db: db,
|
||||
@@ -50,42 +53,59 @@ func NewService(db Database, updater UpdaterInterface, ipGetter PublicIPFetcher,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) lookupIPsResilient(ctx context.Context, hostname string, tries int) (
|
||||
ipv4 netip.Addr, ipv6 netip.Addr, err error) {
|
||||
for i := 0; i < tries; i++ {
|
||||
ipv4, ipv6, err = s.lookupIPs(ctx, hostname)
|
||||
if err == nil {
|
||||
return ipv4, ipv6, nil
|
||||
}
|
||||
func (s *Service) lookupIPsResilient(ctx context.Context, hostname string, tries uint) (
|
||||
ipv4, ipv6 []netip.Addr, err error,
|
||||
) {
|
||||
type result struct {
|
||||
network string
|
||||
ips []net.IP
|
||||
err error
|
||||
}
|
||||
return netip.Addr{}, netip.Addr{}, err
|
||||
}
|
||||
|
||||
func (s *Service) lookupIPs(ctx context.Context, hostname string) (
|
||||
ipv4 netip.Addr, ipv6 netip.Addr, err error) {
|
||||
netIPs, err := s.resolver.LookupIP(ctx, "ip", hostname)
|
||||
if err != nil {
|
||||
return netip.Addr{}, netip.Addr{}, err
|
||||
}
|
||||
ips := make([]netip.Addr, len(netIPs))
|
||||
for i, netIP := range netIPs {
|
||||
switch {
|
||||
case netIP == nil:
|
||||
case netIP.To4() != nil:
|
||||
ips[i] = netip.AddrFrom4([4]byte(netIP.To4()))
|
||||
default: // IPv6
|
||||
ips[i] = netip.AddrFrom16([16]byte(netIP.To16()))
|
||||
}
|
||||
results := make(chan result)
|
||||
networks := []string{"ip4", "ip6"}
|
||||
lookupCtx, cancel := context.WithCancel(ctx)
|
||||
for _, network := range networks {
|
||||
go func(ctx context.Context, network string, results chan<- result) {
|
||||
for range tries {
|
||||
ips, err := s.resolver.LookupIP(ctx, network, hostname)
|
||||
if err != nil {
|
||||
if strings.HasSuffix(err.Error(), "no such host") {
|
||||
results <- result{network: network} // no IP address for this network
|
||||
return
|
||||
}
|
||||
continue // retry
|
||||
}
|
||||
results <- result{network: network, ips: ips, err: err}
|
||||
return
|
||||
}
|
||||
}(lookupCtx, network, results)
|
||||
}
|
||||
|
||||
for _, ip := range ips {
|
||||
if ip.Is6() {
|
||||
ipv6 = ip
|
||||
} else {
|
||||
ipv4 = ip
|
||||
for range networks {
|
||||
result := <-results
|
||||
if result.err != nil {
|
||||
if err == nil {
|
||||
cancel()
|
||||
err = fmt.Errorf("looking up %s addresses: %w", result.network, result.err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
switch result.network {
|
||||
case "ip4":
|
||||
ipv4 = make([]netip.Addr, len(result.ips))
|
||||
for i, ip := range result.ips {
|
||||
ipv4[i] = netip.AddrFrom4([4]byte(ip))
|
||||
}
|
||||
case "ip6":
|
||||
ipv6 = make([]netip.Addr, len(result.ips))
|
||||
for i, ip := range result.ips {
|
||||
ipv6[i] = netip.AddrFrom16([16]byte(ip))
|
||||
}
|
||||
}
|
||||
}
|
||||
return ipv4, ipv6, nil
|
||||
cancel()
|
||||
|
||||
return ipv4, ipv6, err
|
||||
}
|
||||
|
||||
func doIPVersion(records []librecords.Record) (doIP, doIPv4, doIPv6 bool) {
|
||||
@@ -106,7 +126,8 @@ func doIPVersion(records []librecords.Record) (doIP, doIPv4, doIPv6 bool) {
|
||||
}
|
||||
|
||||
func (s *Service) getNewIPs(ctx context.Context, doIP, doIPv4, doIPv6 bool) (
|
||||
ip, ipv4, ipv6 netip.Addr, errors []error) {
|
||||
ip, ipv4, ipv6 netip.Addr, errors []error,
|
||||
) {
|
||||
var err error
|
||||
if doIP {
|
||||
ip, err = tryAndRepeatGettingIP(ctx, s.ipGetter.IP, s.logger, ipversion.IP4or6)
|
||||
@@ -130,7 +151,8 @@ func (s *Service) getNewIPs(ctx context.Context, doIP, doIPv4, doIPv6 bool) (
|
||||
}
|
||||
|
||||
func (s *Service) getRecordIDsToUpdate(ctx context.Context, records []librecords.Record,
|
||||
ip, ipv4, ipv6 netip.Addr) (recordIDs map[uint]struct{}) {
|
||||
ip, ipv4, ipv6 netip.Addr,
|
||||
) (recordIDs map[uint]struct{}) {
|
||||
recordIDs = make(map[uint]struct{})
|
||||
for i, record := range records {
|
||||
shouldUpdate := s.shouldUpdateRecord(ctx, record, ip, ipv4, ipv6)
|
||||
@@ -143,7 +165,8 @@ func (s *Service) getRecordIDsToUpdate(ctx context.Context, records []librecords
|
||||
}
|
||||
|
||||
func (s *Service) shouldUpdateRecord(ctx context.Context, record librecords.Record,
|
||||
ip, ipv4, ipv6 netip.Addr) (update bool) {
|
||||
ip, ipv4, ipv6 netip.Addr,
|
||||
) (update bool) {
|
||||
now := s.timeNow()
|
||||
|
||||
isWithinCooldown := now.Sub(record.History.GetSuccessTime()) < s.cooldown
|
||||
@@ -183,7 +206,8 @@ func (s *Service) shouldUpdateRecord(ctx context.Context, record librecords.Reco
|
||||
}
|
||||
|
||||
func (s *Service) shouldUpdateRecordNoLookup(hostname string, ipVersion ipversion.IPVersion,
|
||||
lastIP, publicIP netip.Addr) (update bool) {
|
||||
lastIP, publicIP netip.Addr,
|
||||
) (update bool) {
|
||||
ipKind := ipVersionToIPKind(ipVersion)
|
||||
if publicIP.IsValid() && publicIP.Compare(lastIP) != 0 {
|
||||
s.logInfoNoLookupUpdate(hostname, ipKind, lastIP, publicIP)
|
||||
@@ -194,9 +218,10 @@ func (s *Service) shouldUpdateRecordNoLookup(hostname string, ipVersion ipversio
|
||||
}
|
||||
|
||||
func (s *Service) shouldUpdateRecordWithLookup(ctx context.Context, hostname string,
|
||||
ipVersion ipversion.IPVersion, publicIP netip.Addr) (update bool) {
|
||||
ipVersion ipversion.IPVersion, publicIP netip.Addr,
|
||||
) (update bool) {
|
||||
const tries = 5
|
||||
recordIPv4, recordIPv6, err := s.lookupIPsResilient(ctx, hostname, tries)
|
||||
recordIPv4s, recordIPv6s, err := s.lookupIPsResilient(ctx, hostname, tries)
|
||||
if err != nil {
|
||||
ctxErr := ctx.Err()
|
||||
if ctxErr != nil {
|
||||
@@ -208,18 +233,27 @@ func (s *Service) shouldUpdateRecordWithLookup(ctx context.Context, hostname str
|
||||
}
|
||||
|
||||
ipKind := ipVersionToIPKind(ipVersion)
|
||||
recordIP := recordIPv4
|
||||
recordIPs := recordIPv4s
|
||||
if publicIP.Is6() {
|
||||
recordIP = recordIPv6
|
||||
recordIPs = recordIPv6s
|
||||
}
|
||||
recordIP = getIPMatchingVersion(recordIP, recordIPv4, recordIPv6, ipVersion)
|
||||
recordIPs = getIPsMatchingVersion(recordIPs, recordIPv4s, recordIPv6s, ipVersion)
|
||||
|
||||
if publicIP.IsValid() && publicIP.Compare(recordIP) != 0 {
|
||||
if publicIP.IsValid() && !ipsContainsIP(recordIPs, publicIP) {
|
||||
// Note if the recordIP is not valid (not found), we want to update.
|
||||
s.logInfoLookupUpdate(hostname, ipKind, recordIP, publicIP)
|
||||
s.logInfoLookupUpdate(hostname, ipKind, recordIPs, publicIP)
|
||||
return true
|
||||
}
|
||||
s.logDebugLookupSkip(hostname, ipKind, recordIP, publicIP)
|
||||
s.logDebugLookupSkip(hostname, ipKind, recordIPs, publicIP)
|
||||
return false
|
||||
}
|
||||
|
||||
func ipsContainsIP(ips []netip.Addr, ip netip.Addr) bool {
|
||||
for _, ip2 := range ips {
|
||||
if ip.Compare(ip2) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -231,8 +265,22 @@ func getIPMatchingVersion(ip, ipv4, ipv6 netip.Addr, ipVersion ipversion.IPVersi
|
||||
return ipv4
|
||||
case ipversion.IP6:
|
||||
return ipv6
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid IP version %s", ipVersion))
|
||||
}
|
||||
}
|
||||
|
||||
func getIPsMatchingVersion(ip, ipv4, ipv6 []netip.Addr, ipVersion ipversion.IPVersion) []netip.Addr {
|
||||
switch ipVersion {
|
||||
case ipversion.IP4or6:
|
||||
return ip
|
||||
case ipversion.IP4:
|
||||
return ipv4
|
||||
case ipversion.IP6:
|
||||
return ipv6
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid IP version %s", ipVersion))
|
||||
}
|
||||
return netip.Addr{}
|
||||
}
|
||||
|
||||
func setInitialUpToDateStatus(db Database, id uint, updateIP netip.Addr, now time.Time) error {
|
||||
@@ -357,7 +405,8 @@ func (s *Service) Start(ctx context.Context) (runError <-chan error, startErr er
|
||||
}
|
||||
|
||||
func (s *Service) run(ctx context.Context, ready chan<- struct{},
|
||||
done chan<- struct{}) {
|
||||
done chan<- struct{},
|
||||
) {
|
||||
defer close(done)
|
||||
ticker := time.NewTicker(s.period)
|
||||
close(ready)
|
||||
|
||||
Reference in New Issue
Block a user