chore(all): migrate from net.IP* to net/netip

This commit is contained in:
Quentin McGaw
2023-06-12 09:48:43 +00:00
parent 3e6fb5ead4
commit 9a4a268926
68 changed files with 775 additions and 712 deletions

View File

@@ -243,7 +243,7 @@ func _main(ctx context.Context, settingsSource SettingsSource, args []string, lo
updater := update.NewUpdater(db, client, notify, logger)
runner := update.NewRunner(db, updater, ipGetter, config.Update.Period,
config.IPv6.Mask, config.Update.Cooldown, logger, resolver, timeNow)
config.IPv6.MaskBits, config.Update.Cooldown, logger, resolver, timeNow)
runnerHandler, runnerCtx, runnerDone := goshutdown.NewGoRoutineHandler("runner")
go runner.Run(runnerCtx, runnerDone)

View File

@@ -1,27 +1,38 @@
package settings
import (
"net"
"errors"
"fmt"
"net/netip"
"github.com/qdm12/gosettings"
)
type IPv6 struct {
Mask net.IPMask
// MaskBits is the IPv6 mask in bits, for example 128 for /128
MaskBits uint8
}
func (i *IPv6) setDefaults() {
const ipv6Bits = 8 * net.IPv6len
if i.Mask == nil {
i.Mask = net.CIDRMask(ipv6Bits, ipv6Bits)
}
i.MaskBits = gosettings.DefaultNumber(i.MaskBits,
uint8(netip.IPv6Unspecified().BitLen()))
}
func (i IPv6) mergeWith(other IPv6) (merged IPv6) {
merged.Mask = gosettings.MergeWithSlice(i.Mask, other.Mask)
merged.MaskBits = gosettings.MergeWithNumber(i.MaskBits, other.MaskBits)
return merged
}
var (
ErrMaskBitsTooHigh = errors.New("mask bits is too high")
)
func (i IPv6) Validate() (err error) {
const maxMaskBits = 128
if i.MaskBits > maxMaskBits {
return fmt.Errorf("%w: %d must be equal or below to %d",
ErrMaskBitsTooHigh, i.MaskBits, maxMaskBits)
}
return nil
}

View File

@@ -3,7 +3,7 @@ package env
import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"github.com/qdm12/ddns-updater/internal/config/settings"
@@ -15,7 +15,7 @@ func (s *Source) readIPv6() (settings settings.IPv6, err error) {
return settings, nil
}
settings.Mask, err = ipv6DecimalPrefixToMask(maskStr)
settings.MaskBits, err = ipv6DecimalPrefixToBits(maskStr)
if err != nil {
return settings, fmt.Errorf("%w: for environment variable IPV6_PREFIX", err)
}
@@ -25,33 +25,20 @@ func (s *Source) readIPv6() (settings settings.IPv6, err error) {
var ErrParsePrefix = errors.New("cannot parse IP prefix")
func ipv6DecimalPrefixToMask(prefixDecimal string) (ipMask net.IPMask, err error) {
if prefixDecimal == "" {
return nil, fmt.Errorf("%w: empty prefix", ErrParsePrefix)
}
func ipv6DecimalPrefixToBits(prefixDecimal string) (maskBits uint8, err error) {
prefixDecimal = strings.TrimPrefix(prefixDecimal, "/")
const bits = 8 * net.IPv6len
ones, consumed, ok := decimalToInteger(prefixDecimal)
if !ok || consumed != len(prefixDecimal) || ones < 0 || ones > bits {
return nil, fmt.Errorf("%w: %s", ErrParsePrefix, prefixDecimal)
const base, bits = 10, 8
maskBitsUint64, err := strconv.ParseUint(prefixDecimal, base, bits)
if err != nil {
return 0, fmt.Errorf("parsing prefix decimal as uint8: %w", err)
}
return net.CIDRMask(ones, bits), nil
}
func decimalToInteger(s string) (ones int, i int, ok bool) {
const big = 0xFFFFFF // Bigger than we need, not too big to worry about overflow
const ten = 10
for i = 0; i < len(s) && '0' <= s[i] && s[i] <= '9'; i++ {
ones = ones*ten + int(s[i]-'0')
if ones >= big {
return big, i, false
}
const maxBits = 128
if bits > maxBits {
return 0, fmt.Errorf("%w: %d bits cannot be larger than %d",
ErrParsePrefix, bits, maxBits)
}
return ones, i, true
return uint8(maskBitsUint64), nil
}

View File

@@ -2,43 +2,43 @@ package env
import (
"fmt"
"net"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_ipv6DecimalPrefixToMask(t *testing.T) {
func Test_ipv6DecimalPrefixToBits(t *testing.T) {
t.Parallel()
testCases := map[string]struct {
prefixDecimal string
ipMask net.IPMask
maskBits uint8
err error
}{
"empty": {
err: fmt.Errorf("cannot parse IP prefix: empty prefix"),
err: fmt.Errorf(`parsing prefix decimal as uint8: ` +
`strconv.ParseUint: parsing "": invalid syntax`),
},
"malformed": {
prefixDecimal: "malformed",
err: fmt.Errorf("cannot parse IP prefix: malformed"),
err: fmt.Errorf(`parsing prefix decimal as uint8: ` +
`strconv.ParseUint: parsing "malformed": invalid syntax`),
},
"with leading slash": {
prefixDecimal: "/78",
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 0, 0, 0, 0, 0, 0},
maskBits: 78,
},
"without leading slash": {
prefixDecimal: "78",
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 252, 0, 0, 0, 0, 0, 0},
maskBits: 78,
},
"full IPv6 mask": {
prefixDecimal: "/128",
ipMask: net.IPMask{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255},
maskBits: 128,
},
"zero IPv6 mask": {
prefixDecimal: "/0",
ipMask: net.IPMask{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
},
}
@@ -47,7 +47,7 @@ func Test_ipv6DecimalPrefixToMask(t *testing.T) {
t.Run(name, func(t *testing.T) {
t.Parallel()
ipMask, err := ipv6DecimalPrefixToMask(testCase.prefixDecimal)
ipMask, err := ipv6DecimalPrefixToBits(testCase.prefixDecimal)
if testCase.err != nil {
require.Error(t, err)
@@ -56,7 +56,7 @@ func Test_ipv6DecimalPrefixToMask(t *testing.T) {
assert.NoError(t, err)
}
assert.Equal(t, testCase.ipMask, ipMask)
assert.Equal(t, testCase.maskBits, ipMask)
})
}
}

View File

@@ -1,7 +1,7 @@
package data
import (
"net"
"net/netip"
"time"
"github.com/qdm12/ddns-updater/internal/models"
@@ -9,6 +9,6 @@ import (
type PersistentDatabase interface {
Close() error
StoreNewIP(domain, host string, ip net.IP, t time.Time) (err error)
StoreNewIP(domain, host string, ip netip.Addr, t time.Time) (err error)
GetEvents(domain, host string) (events []models.HistoryEvent, err error)
}

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"net/netip"
"strings"
"github.com/qdm12/ddns-updater/internal/constants"
@@ -30,23 +31,35 @@ func isHealthy(db AllSelecter, resolver LookupIPer) (err error) {
} else if record.Settings.Proxied() {
continue
}
hostname := record.Settings.BuildDomainName()
lookedUpIPs, err := resolver.LookupIP(context.Background(), "ip", hostname)
currentIP := record.History.GetCurrentIP()
if !currentIP.IsValid() {
return fmt.Errorf("%w: for hostname %s", ErrRecordIPNotSet, hostname)
}
lookedUpNetIPs, err := resolver.LookupIP(context.Background(), "ip", hostname)
if err != nil {
return err
}
currentIP := record.History.GetCurrentIP()
if currentIP == nil {
return fmt.Errorf("%w: for hostname %s", ErrRecordIPNotSet, hostname)
}
found := false
lookedUpIPsString := make([]string, len(lookedUpIPs))
for i, lookedUpIP := range lookedUpIPs {
lookedUpIPsString[i] = lookedUpIP.String()
if lookedUpIP.Equal(currentIP) {
lookedUpIPsString := make([]string, len(lookedUpNetIPs))
for i, netIP := range lookedUpNetIPs {
var ip netip.Addr
switch {
case netIP == nil:
case netIP.To4() != nil:
ip = netip.AddrFrom4([4]byte(netIP.To4()))
default: // IPv6
ip = netip.AddrFrom16([16]byte(netIP.To16()))
}
if ip.Compare(currentIP) == 0 {
found = true
break
}
lookedUpIPsString[i] = ip.String()
}
if !found {
return fmt.Errorf("%w: %s instead of %s for %s",

View File

@@ -2,7 +2,7 @@ package models
import (
"fmt"
"net"
"net/netip"
"strings"
"time"
)
@@ -12,17 +12,17 @@ import (
type History []HistoryEvent // current and previous ips
type HistoryEvent struct { // current and previous ips
IP net.IP `json:"ip"`
Time time.Time `json:"time"`
IP netip.Addr `json:"ip"`
Time time.Time `json:"time"`
}
// GetPreviousIPs returns an antichronological list of previous
// IP addresses if there is any.
func (h History) GetPreviousIPs() []net.IP {
func (h History) GetPreviousIPs() []netip.Addr {
if len(h) <= 1 {
return nil
}
IPs := make([]net.IP, len(h)-1)
IPs := make([]netip.Addr, len(h)-1)
const two = 2
for i := len(h) - two; i >= 0; i-- {
IPs[i] = h[i].IP
@@ -31,9 +31,9 @@ func (h History) GetPreviousIPs() []net.IP {
}
// GetCurrentIP returns the current IP address (latest in history).
func (h History) GetCurrentIP() net.IP {
func (h History) GetCurrentIP() netip.Addr {
if len(h) < 1 {
return nil
return netip.Addr{}
}
return h[len(h)-1].IP
}
@@ -66,7 +66,7 @@ func (h History) GetDurationSinceSuccess(now time.Time) string {
func (h History) String() (s string) {
currentIP := h.GetCurrentIP()
if currentIP == nil {
if !currentIP.IsValid() {
return ""
}
successTime := h[len(h)-1].Time

View File

@@ -96,7 +96,7 @@ func checkData(data dataModel) error {
}
t = event.Time
switch {
case event.IP == nil:
case !event.IP.IsValid():
return fmt.Errorf("%w: IP %d of %d for record %s",
ErrIPEmpty, i+1, len(record.Events), record)
case event.Time.IsZero():

View File

@@ -1,14 +1,14 @@
package json
import (
"net"
"net/netip"
"time"
"github.com/qdm12/ddns-updater/internal/models"
)
// StoreNewIP stores a new IP address for a certain domain and host.
func (db *Database) StoreNewIP(domain, host string, ip net.IP, t time.Time) (err error) {
func (db *Database) StoreNewIP(domain, host string, ip netip.Addr, t time.Time) (err error) {
db.Lock()
defer db.Unlock()
for i, record := range db.data.Records {

View File

@@ -28,7 +28,7 @@ func (r *Record) HTML(now time.Time) models.HTMLRow {
time.Since(r.Time).Round(time.Second).String()+" ago"))
}
currentIP := r.History.GetCurrentIP()
if currentIP != nil {
if currentIP.IsValid() {
row.CurrentIP = models.HTML(`<a href="https://ipinfo.io/"` + currentIP.String() + `\>` + currentIP.String() + "</a>")
} else {
row.CurrentIP = NotAvailable

View File

@@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"github.com/qdm12/ddns-updater/internal/settings/constants"
@@ -14,9 +14,9 @@ import (
)
func (p *Provider) createRecord(ctx context.Context,
client *http.Client, ip net.IP) (recordID string, err error) {
client *http.Client, ip netip.Addr) (recordID string, err error) {
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
stderrors "errors"
"fmt"
"net"
"net/http"
"net/netip"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/settings/constants"
@@ -96,10 +96,10 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
// Documentation at https://api.aliyun.com/
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
@@ -107,15 +107,15 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
if stderrors.Is(err, errors.ErrRecordNotFound) {
recordID, err = p.createRecord(ctx, client, ip)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
return newIP, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
}
} else if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrGetRecordID, err)
return newIP, fmt.Errorf("%w: %w", errors.ErrGetRecordID, err)
}
err = p.updateRecord(ctx, client, recordID, ip)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUpdateRecord, err)
return newIP, fmt.Errorf("%w: %w", errors.ErrUpdateRecord, err)
}
return ip, nil

View File

@@ -3,8 +3,8 @@ package aliyun
import (
"context"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"github.com/qdm12/ddns-updater/internal/settings/constants"
@@ -13,9 +13,9 @@ import (
)
func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
recordID string, ip net.IP) (err error) {
recordID string, ip netip.Addr) (err error) {
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -98,7 +98,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "dyndns.kasserver.com",
@@ -108,7 +108,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
values := url.Values{}
values.Set("host", utils.BuildURLQueryHostname(p.host, p.domain))
if !p.useProviderIP {
if ip.To4() == nil { // ipv6
if ip.Is6() { // ipv6
values.Set("myip6", ip.String())
} else {
values.Set("myip", ip.String())
@@ -118,59 +118,60 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnsuccessfulResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnsuccessfulResponse, err)
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, utils.ToSingleLine(s))
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, utils.ToSingleLine(s))
}
switch s {
case "":
return nil, fmt.Errorf("%w", errors.ErrNoResultReceived)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoResultReceived)
case constants.Nineoneone:
return nil, fmt.Errorf("%w", errors.ErrDNSServerSide)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrDNSServerSide)
case constants.Abuse:
return nil, fmt.Errorf("%w", errors.ErrAbuse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAbuse)
case "!donator":
return nil, fmt.Errorf("%w", errors.ErrFeatureUnavailable)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrFeatureUnavailable)
case constants.Badagent:
return nil, fmt.Errorf("%w", errors.ErrBannedUserAgent)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBannedUserAgent)
case constants.Badauth:
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case constants.Nohost:
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
}
if !strings.Contains(s, "nochg") && !strings.Contains(s, "good") {
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
var ips []net.IP
if ip.To4() != nil {
var ips []netip.Addr
if ip.Is4() {
ips = utils.FindIPv4Addresses(s)
} else {
ips = utils.FindIPv6Addresses(s)
}
if len(ips) == 0 {
return nil, fmt.Errorf("%w", errors.ErrNoIPInResponse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoIPInResponse)
}
newIP = ips[0]
if !p.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
if !p.useProviderIP && ip.Compare(newIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
}

View File

@@ -6,8 +6,8 @@ import (
"encoding/json"
stderrors "errors"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"regexp"
"strings"
@@ -146,10 +146,10 @@ func (p *Provider) setHeaders(request *http.Request) {
// Obtain domain ID.
// See https://api.cloudflare.com/#dns-records-for-a-zone-list-dns-records.
func (p *Provider) getRecordID(ctx context.Context, client *http.Client, newIP net.IP) (
func (p *Provider) getRecordID(ctx context.Context, client *http.Client, newIP netip.Addr) (
identifier string, upToDate bool, err error) {
recordType := constants.A
if newIP.To4() == nil {
if newIP.Is6() {
recordType = constants.AAAA
}
@@ -214,10 +214,10 @@ func (p *Provider) getRecordID(ctx context.Context, client *http.Client, newIP n
return listRecordsResponse.Result[0].ID, false, nil
}
func (p *Provider) CreateRecord(ctx context.Context, client *http.Client, ip net.IP) (recordID string, err error) {
func (p *Provider) CreateRecord(ctx context.Context, client *http.Client, ip netip.Addr) (recordID string, err error) {
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
@@ -294,9 +294,9 @@ func (p *Provider) CreateRecord(ctx context.Context, client *http.Client, ip net
return parsedJSON.Result.ID, nil
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
@@ -306,10 +306,10 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
case stderrors.Is(err, errors.ErrNoResultReceived):
identifier, err = p.CreateRecord(ctx, client, ip)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
}
case err != nil:
return nil, fmt.Errorf("%w: %w", errors.ErrGetRecordID, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrGetRecordID, err)
case upToDate:
return ip, nil
}
@@ -338,24 +338,24 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
encoder := json.NewEncoder(buffer)
err = encoder.Encode(requestData)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrRequestEncode, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrRequestEncode, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
if err != nil {
return nil, err
return netip.Addr{}, err
}
p.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
if response.StatusCode > http.StatusUnsupportedMediaType {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
}
@@ -372,7 +372,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
err = decoder.Decode(&parsedJSON)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
if !parsedJSON.Success {
@@ -380,14 +380,15 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
for _, e := range parsedJSON.Errors {
errStr += fmt.Sprintf("error %d: %s; ", e.Code, e.Message)
}
return nil, fmt.Errorf("%w: %s", errors.ErrUnsuccessfulResponse, errStr)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnsuccessfulResponse, errStr)
}
newIP = net.ParseIP(parsedJSON.Result.Content)
if newIP == nil {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMalformed, parsedJSON.Result.Content)
} else if !newIP.Equal(ip) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
newIP, err = netip.ParseAddr(parsedJSON.Result.Content)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err)
} else if newIP.Compare(ip) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -91,7 +91,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
// see https://www.domaindiscount24.com/faq/en/dynamic-dns
u := url.URL{
Scheme: "https",
@@ -110,24 +110,24 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.ToSingleLine(s))
}
@@ -135,11 +135,11 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
switch {
case strings.Contains(s, "authorization failed"):
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case s == "", strings.Contains(s, "success"):
return ip, nil
// TODO missing cases
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -101,7 +101,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "www.ddnss.de",
@@ -113,7 +113,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
values.Set("host", utils.BuildURLQueryHostname(p.host, p.domain))
if !p.useProviderIP {
ipKey := "ip"
if p.dualStack && ip.To4() == nil { // ipv6 update for dual stack
if p.dualStack && ip.Is6() { // ipv6 update for dual stack
ipKey = "ip6"
}
values.Set(ipKey, ip.String())
@@ -122,37 +122,37 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.ToSingleLine(s))
}
switch {
case strings.Contains(s, "badysys"):
return nil, fmt.Errorf("%w", errors.ErrInvalidSystemParam)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrInvalidSystemParam)
case strings.Contains(s, constants.Badauth):
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case strings.Contains(s, constants.Notfqdn):
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case strings.Contains(s, "Updated 1 hostname"):
return ip, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
}

View File

@@ -5,8 +5,8 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"github.com/qdm12/ddns-updater/internal/models"
@@ -142,15 +142,15 @@ func (p *Provider) getRecordID(ctx context.Context, recordType string, client *h
return result.DomainRecords[0].ID, nil
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
recordType := constants.A
if ip.To4() == nil { // IPv6
if ip.Is6() {
recordType = constants.AAAA
}
recordID, err := p.getRecordID(ctx, recordType, client)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrGetRecordID, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrGetRecordID, err)
}
u := url.URL{
@@ -172,23 +172,23 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
err = encoder.Encode(requestData)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrRequestEncode, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrRequestEncode, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
if err != nil {
return nil, err
return netip.Addr{}, err
}
p.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
}
@@ -200,14 +200,14 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
err = decoder.Decode(&responseData)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
newIP = net.ParseIP(responseData.DomainRecord.Data)
if newIP == nil {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMalformed, responseData.DomainRecord.Data)
} else if !newIP.Equal(ip) {
return nil, fmt.Errorf("%w: sent %s but received %s ",
newIP, err = netip.ParseAddr(responseData.DomainRecord.Data)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err)
} else if newIP.Compare(ip) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"regexp"
"strings"
@@ -99,7 +99,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
// Multiple hosts can be updated in one query, see https://www.dnsomatic.com/docs/api
u := url.URL{
Scheme: "https",
@@ -124,58 +124,59 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, s)
}
switch s {
case constants.Nohost, constants.Notfqdn:
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case constants.Badauth:
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case constants.Badagent:
return nil, fmt.Errorf("%w", errors.ErrBannedUserAgent)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBannedUserAgent)
case constants.Abuse:
return nil, fmt.Errorf("%w", errors.ErrAbuse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAbuse)
case "dnserr", constants.Nineoneone:
return nil, fmt.Errorf("%w: %s", errors.ErrDNSServerSide, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrDNSServerSide, s)
}
if !strings.Contains(s, "nochg") && !strings.Contains(s, "good") {
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
var ips []net.IP
if ip.To4() != nil {
var ips []netip.Addr
if ip.Is4() {
ips = utils.FindIPv4Addresses(s)
} else {
ips = utils.FindIPv6Addresses(s)
}
if len(ips) == 0 {
return nil, fmt.Errorf("%w", errors.ErrNoIPInResponse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoIPInResponse)
}
newIP = ips[0]
if !p.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
if !p.useProviderIP && ip.Compare(newIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
}

View File

@@ -6,8 +6,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"github.com/qdm12/ddns-updater/internal/models"
@@ -93,9 +93,9 @@ func (p *Provider) setHeaders(request *http.Request) {
headers.SetUserAgent(request)
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
u := url.URL{
@@ -116,18 +116,18 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
if err != nil {
return nil, err
return netip.Addr{}, err
}
p.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
}
@@ -143,14 +143,14 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
err = decoder.Decode(&recordResp)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
var recordID, recordLine string
for _, record := range recordResp.Records {
if record.Type == recordType && record.Name == p.host {
receivedIP := net.ParseIP(record.Value)
if ip.Equal(receivedIP) {
receivedIP, err := netip.ParseAddr(record.Value)
if err == nil && ip.Compare(receivedIP) == 0 {
return ip, nil
}
recordID = record.ID
@@ -159,7 +159,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
}
if recordID == "" {
return nil, fmt.Errorf("%w", errors.ErrNotFound)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNotFound)
}
u.Path = "/Record.Ddns"
@@ -176,24 +176,24 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err = http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
if err != nil {
return nil, err
return netip.Addr{}, err
}
p.setHeaders(request)
response, err = client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
}
data, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
return netip.Addr{}, fmt.Errorf("reading response body: %w", err)
}
var ddnsResp struct {
@@ -205,15 +205,16 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
err = json.Unmarshal(data, &ddnsResp)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
ipStr := ddnsResp.Record.Value
receivedIP := net.ParseIP(ipStr)
if receivedIP == nil {
return nil, fmt.Errorf("%w: %s from JSON data: %s", errors.ErrIPReceivedMalformed, ipStr, data)
} else if !ip.Equal(receivedIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, receivedIP.String())
receivedIP, err := netip.ParseAddr(ipStr)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err)
} else if ip.Compare(receivedIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return ip, nil
}

View File

@@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -108,7 +108,7 @@ func (p *Provider) setHeaders(request *http.Request) {
headers.SetAccept(request, "application/json")
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "simple-api.dondominio.net",
@@ -118,7 +118,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
values.Set("apipasswd", p.password)
values.Set("domain", p.domain)
values.Set("name", p.name)
isIPv4 := ip.To4() != nil
isIPv4 := ip.Is4()
if isIPv4 {
values.Set("ipv4", ip.String())
} else {
@@ -129,18 +129,18 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
if err != nil {
return nil, err
return netip.Addr{}, err
}
p.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
}
@@ -158,22 +158,23 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
err = decoder.Decode(&responseData)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
if !responseData.Success {
return nil, fmt.Errorf("%w: %s (error code %d)",
return netip.Addr{}, fmt.Errorf("%w: %s (error code %d)",
errors.ErrUnsuccessfulResponse, responseData.ErrorCodeMessage, responseData.ErrorCode)
}
ipString := responseData.ResponseData.GlueRecords[0].IPv4
if !isIPv4 {
ipString = responseData.ResponseData.GlueRecords[0].IPv6
}
newIP = net.ParseIP(ipString)
if newIP == nil {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMalformed, ipString)
} else if !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
newIP, err = netip.ParseAddr(ipString)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err)
} else if ip.Compare(newIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
}

View File

@@ -6,8 +6,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"regexp"
@@ -93,25 +93,25 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
records, err := p.getRecords(ctx, client)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrListRecords, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrListRecords, err)
}
var oldIP net.IP
var oldIP netip.Addr
for _, data := range records.Data {
if data.Type == recordType && data.Record == utils.BuildURLQueryHostname(p.host, p.domain) {
if data.Editable == "0" {
return nil, fmt.Errorf("%w", errors.ErrRecordNotEditable)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrRecordNotEditable)
}
oldIP = net.ParseIP(data.Value)
if ip.Equal(oldIP) { // constants.Success, nothing to change
oldIP, err = netip.ParseAddr(data.Value)
if err == nil && ip.Compare(oldIP) == 0 { // constants.Success, nothing to change
return ip, nil
}
break
@@ -121,13 +121,13 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
// Create the record with the new IP before removing the old one if it exists.
err = p.createRecord(ctx, client, ip)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
}
if oldIP != nil { // Found editable record with a different IP address, so remove it
if oldIP.IsValid() { // Found editable record with a different IP address, so remove it
err = p.removeRecord(ctx, client, oldIP)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrRemoveRecord, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrRemoveRecord, err)
}
}
@@ -203,9 +203,9 @@ func (p *Provider) getRecords(ctx context.Context, client *http.Client) (
return records, nil
}
func (p *Provider) removeRecord(ctx context.Context, client *http.Client, ip net.IP) error { //nolint:dupl
func (p *Provider) removeRecord(ctx context.Context, client *http.Client, ip netip.Addr) error { //nolint:dupl
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
@@ -251,9 +251,9 @@ func (p *Provider) removeRecord(ctx context.Context, client *http.Client, ip net
return nil
}
func (p *Provider) createRecord(ctx context.Context, client *http.Client, ip net.IP) error { //nolint:dupl
func (p *Provider) createRecord(ctx context.Context, client *http.Client, ip netip.Addr) error { //nolint:dupl
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"regexp"
@@ -94,7 +94,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "www.duckdns.org",
@@ -106,7 +106,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
values.Set("token", p.token)
u.RawQuery = values.Encode()
if !p.useProviderIP {
if ip.To4() == nil {
if ip.Is6() {
values.Set("ip6", ip.String())
} else {
values.Set("ip", ip.String())
@@ -115,44 +115,45 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.ToSingleLine(s))
}
const minChars = 2
switch {
case len(s) < minChars:
return nil, fmt.Errorf("%w: response %q is too short", errors.ErrUnmarshalResponse, s)
return netip.Addr{}, fmt.Errorf("%w: response %q is too short", errors.ErrUnmarshalResponse, s)
case s[0:minChars] == "KO":
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case s[0:minChars] == "OK":
ips := utils.FindIPv4Addresses(s)
if len(ips) == 0 {
return nil, fmt.Errorf("%w", errors.ErrNoIPInResponse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoIPInResponse)
}
newIP = ips[0]
if !p.useProviderIP && !newIP.Equal(ip) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
if !p.useProviderIP && newIP.Compare(ip) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -105,7 +105,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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.clientKey),
@@ -121,35 +121,35 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.ToSingleLine(s))
}
switch {
case strings.HasPrefix(s, constants.Notfqdn):
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case strings.HasPrefix(s, "badrequest"):
return nil, fmt.Errorf("%w", errors.ErrBadRequest)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
case strings.HasPrefix(s, "good"):
return ip, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -106,7 +106,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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.dynu.com",
@@ -119,7 +119,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
hostname := utils.BuildDomainName(p.host, p.domain)
values.Set("hostname", hostname)
if !p.useProviderIP {
if ip.To4() == nil {
if ip.Is6() {
values.Set("myipv6", ip.String())
} else {
values.Set("myip", ip.String())
@@ -129,39 +129,39 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnsuccessfulResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnsuccessfulResponse, err)
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.ToSingleLine(s))
}
switch {
case strings.Contains(s, constants.Badauth):
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case strings.Contains(s, constants.Notfqdn):
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case strings.Contains(s, constants.Abuse):
return nil, fmt.Errorf("%w", errors.ErrAbuse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAbuse)
case strings.Contains(s, "good"):
return ip, nil
case strings.Contains(s, "nochg"): // Updated but not changed
return ip, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))
}
}

View File

@@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"github.com/qdm12/ddns-updater/internal/models"
@@ -90,8 +90,8 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
isIPv4 := ip.To4() != nil
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
isIPv4 := ip.Is4()
host := "dynv6.com"
if isIPv4 {
host = "ipv4." + host
@@ -117,19 +117,19 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
if response.StatusCode == http.StatusOK {
return ip, nil
}
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -87,9 +87,9 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
var hostPrefix string
if ip.To4() == nil {
if ip.Is6() {
hostPrefix = "v6."
}
@@ -101,34 +101,34 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
}
loweredResponse := strings.ToLower(s)
switch {
case loweredResponse == "":
return nil, fmt.Errorf("%w", errors.ErrNoResultReceived)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoResultReceived)
case strings.HasPrefix(loweredResponse, "no ip change detected"),
strings.HasPrefix(loweredResponse, "updated "):
return ip, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, utils.ToSingleLine(s))
}
}

View File

@@ -5,8 +5,8 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"github.com/qdm12/ddns-updater/internal/models"
@@ -96,14 +96,10 @@ func (p *Provider) setHeaders(request *http.Request) {
request.Header.Set("X-Api-Key", p.key)
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
recordType := constants.A
var ipStr string
if ip.To4() == nil { // IPv6
if ip.Is6() {
recordType = constants.AAAA
ipStr = ip.To16().String()
} else {
ipStr = ip.To4().String()
}
u := url.URL{
@@ -123,28 +119,28 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
Values [1]string `json:"rrset_values"`
TTL int `json:"rrset_ttl"`
}{
Values: [1]string{ipStr},
Values: [1]string{ip.Unmap().String()},
TTL: ttl,
}
err = encoder.Encode(requestData)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrRequestEncode, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrRequestEncode, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
if err != nil {
return nil, err
return netip.Addr{}, err
}
p.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusCreated {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
}

View File

@@ -4,8 +4,8 @@ import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"github.com/qdm12/ddns-updater/internal/settings/constants"
ddnserrors "github.com/qdm12/ddns-updater/internal/settings/errors"
@@ -14,9 +14,9 @@ import (
"google.golang.org/api/option"
)
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
@@ -24,7 +24,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
option.WithCredentialsJSON(p.credentials),
option.WithHTTPClient(client))
if err != nil {
return nil, fmt.Errorf("creating GCP DDNS service: %w", err)
return netip.Addr{}, fmt.Errorf("creating GCP DDNS service: %w", err)
}
rrSetsService := clouddns.NewResourceRecordSetsService(ddnsService)
@@ -36,7 +36,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
if errors.Is(err, ddnserrors.ErrNotFound) {
rrSetFound = false // not finding the record is fine
} else {
return nil, fmt.Errorf("getting record resource set: %w", err)
return netip.Addr{}, fmt.Errorf("getting record resource set: %w", err)
}
}
@@ -50,14 +50,14 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
if !rrSetFound {
err = p.createRecord(rrSetsService, fqdn, recordType, ip)
if err != nil {
return nil, fmt.Errorf("creating record: %w", err)
return netip.Addr{}, fmt.Errorf("creating record: %w", err)
}
return ip, nil
}
err = p.updateRecord(rrSetsService, fqdn, recordType, ip)
if err != nil {
return nil, fmt.Errorf("updating record: %w", err)
return netip.Addr{}, fmt.Errorf("updating record: %w", err)
}
return ip, nil
@@ -79,7 +79,7 @@ func (p *Provider) getResourceRecordSet(rrSetsService *clouddns.ResourceRecordSe
}
func (p *Provider) createRecord(rrSetsService *clouddns.ResourceRecordSetsService,
fqdn, recordType string, ip net.IP) (err error) {
fqdn, recordType string, ip netip.Addr) (err error) {
rrSet := &clouddns.ResourceRecordSet{
Name: fqdn,
Rrdatas: []string{ip.String()},
@@ -91,7 +91,7 @@ func (p *Provider) createRecord(rrSetsService *clouddns.ResourceRecordSetsServic
}
func (p *Provider) updateRecord(rrSetsService *clouddns.ResourceRecordSetsService,
fqdn, recordType string, ip net.IP) (err error) {
fqdn, recordType string, ip netip.Addr) (err error) {
rrSet := &clouddns.ResourceRecordSet{
Name: fqdn,
Rrdatas: []string{ip.String()},

View File

@@ -6,8 +6,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"regexp"
@@ -103,9 +103,9 @@ func (p *Provider) setHeaders(request *http.Request) {
headers.SetAccept(request, "application/json")
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
type goDaddyPutBody struct {
@@ -124,18 +124,18 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
err = encoder.Encode(requestData)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrRequestEncode, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrRequestEncode, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPut, u.String(), buffer)
if err != nil {
return nil, err
return netip.Addr{}, err
}
p.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
@@ -145,7 +145,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
err = fmt.Errorf("%w: %d", errors.ErrBadHTTPStatus, response.StatusCode)
@@ -154,7 +154,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
jsonErr := json.Unmarshal(b, &parsedJSON)
if jsonErr != nil || parsedJSON.Message == "" {
return nil, fmt.Errorf("%w: %s", err, utils.ToSingleLine(string(b)))
return netip.Addr{}, fmt.Errorf("%w: %s", err, utils.ToSingleLine(string(b)))
}
return nil, fmt.Errorf("%w: %s", err, parsedJSON.Message)
return netip.Addr{}, fmt.Errorf("%w: %s", err, parsedJSON.Message)
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -96,7 +96,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "domains.google.com",
@@ -113,57 +113,58 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
switch s {
case "":
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
case constants.Nohost, constants.Notfqdn:
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case constants.Badauth:
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case constants.Badagent:
return nil, fmt.Errorf("%w", errors.ErrBannedUserAgent)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBannedUserAgent)
case constants.Abuse:
return nil, fmt.Errorf("%w", errors.ErrAbuse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAbuse)
case constants.Nineoneone:
return nil, fmt.Errorf("%w", errors.ErrDNSServerSide)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrDNSServerSide)
case "conflict constants.A", "conflict constants.AAAA":
return nil, fmt.Errorf("%w", errors.ErrConflictingRecord)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrConflictingRecord)
}
if !strings.Contains(s, "nochg") && !strings.Contains(s, "good") {
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
var ips []net.IP
if ip.To4() != nil {
var ips []netip.Addr
if ip.Is4() {
ips = utils.FindIPv4Addresses(s)
} else {
ips = utils.FindIPv6Addresses(s)
}
if len(ips) == 0 {
return nil, fmt.Errorf("%w", errors.ErrNoIPInResponse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoIPInResponse)
}
newIP = ips[0]
if !p.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
if !p.useProviderIP && ip.Compare(newIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -90,7 +90,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
fqdn := p.BuildDomainName()
u := url.URL{
Scheme: "https",
@@ -107,47 +107,48 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
switch s {
case "":
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
case constants.Badauth:
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
}
if !strings.Contains(s, "nochg") && !strings.Contains(s, "good") {
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
var ips []net.IP
if ip.To4() != nil {
var ips []netip.Addr
if ip.Is4() {
ips = utils.FindIPv4Addresses(s)
} else {
ips = utils.FindIPv6Addresses(s)
}
if len(ips) == 0 {
return nil, fmt.Errorf("%w", errors.ErrNoIPInResponse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoIPInResponse)
}
newIP = ips[0]
if !p.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
if !p.useProviderIP && ip.Compare(newIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -98,7 +98,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "infomaniak.com",
@@ -114,19 +114,19 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
@@ -134,34 +134,36 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
case http.StatusOK:
switch {
case strings.HasPrefix(s, "good "):
newIP = net.ParseIP(s[5:])
if newIP == nil {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMalformed, s)
} else if !p.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP)
newIP, err = netip.ParseAddr(s[5:])
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err)
} else if !p.useProviderIP && ip.Compare(newIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
case strings.HasPrefix(s, "nochg "):
newIP = net.ParseIP(s[6:])
if newIP == nil {
return nil, fmt.Errorf("%w: in response %q", errors.ErrNoResultReceived, s)
} else if !p.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP)
newIP, err = netip.ParseAddr(s[6:])
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: in response %q", errors.ErrNoResultReceived, s)
} else if !p.useProviderIP && ip.Compare(newIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
case http.StatusBadRequest:
switch s {
case constants.Nohost:
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case constants.Badauth:
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
default:
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
}
default:
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
}
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -94,7 +94,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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),
@@ -103,7 +103,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
values := url.Values{}
values.Set("hostname", utils.BuildURLQueryHostname(p.host, p.domain))
if ip.To4() != nil {
if ip.Is4() {
values.Set("myip", ip.String())
} else {
values.Set("myipv6", ip.String())
@@ -113,28 +113,28 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, fmt.Errorf("creating http request: %w", err)
return netip.Addr{}, fmt.Errorf("creating http request: %w", err)
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, fmt.Errorf("doing http request: %w", err)
return netip.Addr{}, fmt.Errorf("doing http request: %w", err)
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
}
if !strings.HasPrefix(s, "good") || !strings.HasPrefix(s, "nochg") {
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
return ip, nil

View File

@@ -7,8 +7,8 @@ import (
goerrors "errors"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strconv"
@@ -90,14 +90,14 @@ func (p *Provider) HTML() models.HTMLRow {
}
// Using https://www.linode.com/docs/api/domains/
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
domainID, err := p.getDomainID(ctx, client)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrGetDomainID, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrGetDomainID, err)
}
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
@@ -105,16 +105,16 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
if goerrors.Is(err, errors.ErrNotFound) {
err := p.createRecord(ctx, client, domainID, recordType, ip)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
}
return ip, nil
} else if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrGetRecordID, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrGetRecordID, err)
}
err = p.updateRecord(ctx, client, domainID, recordID, ip)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUpdateRecord, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUpdateRecord, err)
}
return ip, nil
@@ -242,7 +242,7 @@ func (p *Provider) getRecordID(ctx context.Context, client *http.Client,
}
func (p *Provider) createRecord(ctx context.Context, client *http.Client,
domainID int, recordType string, ip net.IP) (err error) {
domainID int, recordType string, ip netip.Addr) (err error) {
u := url.URL{
Scheme: "https",
Host: "api.linode.com",
@@ -292,18 +292,19 @@ func (p *Provider) createRecord(ctx context.Context, client *http.Client,
return fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
newIP := net.ParseIP(responseData.IP)
if newIP == nil {
return fmt.Errorf("%w: %s", errors.ErrIPReceivedMalformed, responseData.IP)
} else if !newIP.Equal(ip) {
return fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
newIP, err := netip.ParseAddr(responseData.IP)
if err != nil {
return fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err)
} else if newIP.Compare(ip) != 0 {
return fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return nil
}
func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
domainID, recordID int, ip net.IP) (err error) {
domainID, recordID int, ip netip.Addr) (err error) {
u := url.URL{
Scheme: "https",
Host: "api.linode.com",
@@ -347,11 +348,12 @@ func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
return fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
newIP := net.ParseIP(data.IP)
if newIP == nil {
return fmt.Errorf("%w: %s", errors.ErrIPReceivedMalformed, data.IP)
} else if !newIP.Equal(ip) {
return fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
newIP, err := netip.ParseAddr(data.IP)
if err != nil {
return fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err)
} else if newIP.Compare(ip) != 0 {
return fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return nil

View File

@@ -6,8 +6,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"github.com/qdm12/ddns-updater/internal/models"
@@ -99,22 +99,22 @@ func (p *Provider) setHeaders(request *http.Request) {
}
// Using https://www.luadns.com/api.html
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
zoneID, err := p.getZoneID(ctx, client)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrGetZoneID, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrGetZoneID, err)
}
record, err := p.getRecord(ctx, client, zoneID, ip)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrGetRecordInZone, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrGetRecordInZone, err)
}
newRecord := record
newRecord.Content = ip.String()
err = p.updateRecord(ctx, client, zoneID, newRecord)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUpdateRecord, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUpdateRecord, err)
}
return ip, nil
}
@@ -184,7 +184,7 @@ func (p *Provider) getZoneID(ctx context.Context, client *http.Client) (zoneID i
return 0, fmt.Errorf("%w", errors.ErrZoneNotFound)
}
func (p *Provider) getRecord(ctx context.Context, client *http.Client, zoneID int, ip net.IP) (
func (p *Provider) getRecord(ctx context.Context, client *http.Client, zoneID int, ip netip.Addr) (
record luaDNSRecord, err error) {
u := url.URL{
Scheme: "https",
@@ -226,7 +226,7 @@ func (p *Provider) getRecord(ctx context.Context, client *http.Client, zoneID in
return record, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
recordName := utils.BuildURLQueryHostname(p.host, p.domain) + "."
@@ -288,7 +288,8 @@ func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
}
if updatedRecord.Content != newRecord.Content {
return fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, updatedRecord.Content)
return fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, newRecord.Content, updatedRecord.Content)
}
return nil
}

View File

@@ -6,8 +6,8 @@ import (
"encoding/xml"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"regexp"
@@ -101,7 +101,7 @@ func (p *Provider) setHeaders(request *http.Request) {
headers.SetAccept(request, "application/xml")
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "dynamicdns.park-your-domain.com",
@@ -118,18 +118,18 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
p.setHeaders(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
}
@@ -146,11 +146,11 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
err = decoder.Decode(&parsedXML)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
if parsedXML.Errors.Error != "" {
return nil, fmt.Errorf("%w: %s", errors.ErrUnsuccessfulResponse, parsedXML.Errors.Error)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnsuccessfulResponse, parsedXML.Errors.Error)
}
if parsedXML.IP == "" {
@@ -159,12 +159,12 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
return newIP, nil
}
newIP = net.ParseIP(parsedXML.IP)
if newIP == nil {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMalformed, parsedXML.IP)
}
if !p.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
newIP, err = netip.ParseAddr(parsedXML.IP)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err)
} else if !p.useProviderIP && ip.Compare(newIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
}

View File

@@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"github.com/qdm12/ddns-updater/internal/models"
@@ -88,7 +88,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "njal.la",
@@ -97,7 +97,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
values := url.Values{}
values.Set("h", utils.BuildURLQueryHostname(p.host, p.domain))
values.Set("k", p.key)
updatingIP6 := ip.To4() == nil
updatingIP6 := ip.Is6()
if p.useProviderIP {
values.Set("auto", "")
} else {
@@ -111,13 +111,13 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
@@ -131,30 +131,31 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
}
err = decoder.Decode(&respBody)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
switch response.StatusCode {
case http.StatusOK:
if respBody.Message != "record updated" {
return nil, fmt.Errorf("%w: message received: %s", errors.ErrUnknownResponse, respBody.Message)
return netip.Addr{}, fmt.Errorf("%w: message received: %s", errors.ErrUnknownResponse, respBody.Message)
}
ipString := respBody.Value.A
if updatingIP6 {
ipString = respBody.Value.AAAA
}
newIP = net.ParseIP(ipString)
if newIP == nil {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMalformed, ipString)
} else if !p.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
newIP, err = netip.ParseAddr(ipString)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err)
} else if !p.useProviderIP && ip.Compare(newIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
case http.StatusUnauthorized:
return nil, fmt.Errorf("%w: %s", errors.ErrAuth, respBody.Message)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrAuth, respBody.Message)
case http.StatusInternalServerError:
return nil, fmt.Errorf("%w: %s", errors.ErrBadRequest, respBody.Message)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrBadRequest, respBody.Message)
}
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, respBody.Message)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, respBody.Message)
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -101,7 +101,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
u := url.URL{
Scheme: "https",
Host: "dynupdate.no-ip.com",
@@ -111,7 +111,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
values := url.Values{}
values.Set("hostname", utils.BuildURLQueryHostname(p.host, p.domain))
if !p.useProviderIP {
if ip.To4() == nil {
if ip.Is6() {
values.Set("myipv6", ip.String())
} else {
values.Set("myip", ip.String())
@@ -121,61 +121,62 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
}
switch s {
case "":
return nil, fmt.Errorf("%w", errors.ErrNoResultReceived)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoResultReceived)
case constants.Nineoneone:
return nil, fmt.Errorf("%w", errors.ErrDNSServerSide)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrDNSServerSide)
case constants.Abuse:
return nil, fmt.Errorf("%w", errors.ErrAbuse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAbuse)
case "!donator":
return nil, fmt.Errorf("%w", errors.ErrFeatureUnavailable)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrFeatureUnavailable)
case constants.Badagent:
return nil, fmt.Errorf("%w", errors.ErrBannedUserAgent)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBannedUserAgent)
case constants.Badauth:
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case constants.Nohost:
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
}
if !strings.Contains(s, "nochg") && !strings.Contains(s, "good") {
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
var ips []net.IP
if ip.To4() != nil {
var ips []netip.Addr
if ip.Is4() {
ips = utils.FindIPv4Addresses(s)
} else {
ips = utils.FindIPv6Addresses(s)
}
if len(ips) == 0 {
return nil, fmt.Errorf("%w", errors.ErrNoIPInResponse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrNoIPInResponse)
}
newIP = ips[0]
if !p.useProviderIP && !ip.Equal(newIP) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP.String())
if !p.useProviderIP && ip.Compare(newIP) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return newIP, nil
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -97,7 +97,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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),
@@ -113,35 +113,36 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
}
if !strings.HasPrefix(s, "good ") {
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
responseIPString := strings.TrimPrefix(s, "good ")
newIP = net.ParseIP(responseIPString)
if newIP == nil {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMalformed, responseIPString)
} else if !p.useProviderIP && !newIP.Equal(ip) {
return nil, fmt.Errorf("%w: %s", errors.ErrIPReceivedMismatch, newIP)
newIP, err = netip.ParseAddr(responseIPString)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrIPReceivedMalformed, err)
} else if !p.useProviderIP && newIP.Compare(ip) != 0 {
return netip.Addr{}, fmt.Errorf("%w: sent ip %s to update but received %s",
errors.ErrIPReceivedMismatch, ip, newIP)
}
return ip, nil
}

View File

@@ -6,8 +6,8 @@ import (
stderrors "errors"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
"time"
@@ -135,7 +135,8 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) updateWithDynHost(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) updateWithDynHost(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),
@@ -152,37 +153,37 @@ func (p *Provider) updateWithDynHost(ctx context.Context, client *http.Client, i
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrBadRequest, err)
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, s)
}
switch {
case strings.HasPrefix(s, constants.Notfqdn):
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case strings.HasPrefix(s, "badrequest"):
return nil, fmt.Errorf("%w", errors.ErrBadRequest)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
case strings.HasPrefix(s, "nochg"):
return ip, nil
case strings.HasPrefix(s, "good"):
return ip, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
}
@@ -191,15 +192,12 @@ var (
ErrRefresh = stderrors.New("cannot refresh records")
)
func (p *Provider) updateWithZoneDNS(ctx context.Context, client *http.Client, ip net.IP) (
newIP net.IP, err error) {
func (p *Provider) updateWithZoneDNS(ctx context.Context, client *http.Client, ip netip.Addr) (
newIP netip.Addr, err error) {
ipStr := ip.Unmap().String()
recordType := constants.A
var ipStr string
if ip.To4() == nil { // IPv6
if ip.Is6() {
recordType = constants.AAAA
ipStr = ip.To16().String()
} else {
ipStr = ip.To4().String()
}
// subDomain filter of the ovh api expect an empty string to get @ record
subDomain := p.host
@@ -209,37 +207,37 @@ func (p *Provider) updateWithZoneDNS(ctx context.Context, client *http.Client, i
timestamp, err := p.getAdjustedUnixTimestamp(ctx, client)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrGetAdjustedTime, err)
return netip.Addr{}, fmt.Errorf("%w: %w", ErrGetAdjustedTime, err)
}
recordIDs, err := p.getRecords(ctx, client, recordType, subDomain, timestamp)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrListRecords, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrListRecords, err)
}
if len(recordIDs) == 0 {
err = p.createRecord(ctx, client, recordType, subDomain, ipStr, timestamp)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrCreateRecord, err)
}
} else {
for _, recordID := range recordIDs {
err = p.updateRecord(ctx, client, recordID, ipStr, timestamp)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUpdateRecord, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUpdateRecord, err)
}
}
}
err = p.refresh(ctx, client, timestamp)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrRefresh, err)
return netip.Addr{}, fmt.Errorf("%w: %w", ErrRefresh, err)
}
return ip, nil
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
if p.mode != "api" {
return p.updateWithDynHost(ctx, client, ip)
}

View File

@@ -5,8 +5,8 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"github.com/qdm12/ddns-updater/internal/models"
@@ -257,20 +257,20 @@ func (p *Provider) updateRecord(ctx context.Context, client *http.Client,
return nil
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
recordType := constants.A
if ip.To4() == nil { // IPv6
if ip.Is6() {
recordType = constants.AAAA
}
ipStr := ip.String()
recordIDs, err := p.getRecordIDs(ctx, client, recordType)
if err != nil {
return nil, err
return netip.Addr{}, err
}
if len(recordIDs) == 0 {
err = p.createRecord(ctx, client, recordType, ipStr)
if err != nil {
return nil, err
return netip.Addr{}, err
}
return ip, nil
}
@@ -278,7 +278,7 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
for _, recordID := range recordIDs {
err = p.updateRecord(ctx, client, recordType, ipStr, recordID)
if err != nil {
return nil, err
return netip.Addr{}, err
}
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -98,7 +98,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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),
@@ -114,13 +114,13 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
@@ -130,38 +130,38 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
case http.StatusNoContent: // no change
return ip, nil
case http.StatusUnauthorized:
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case http.StatusConflict:
return nil, fmt.Errorf("%w", errors.ErrZoneNotFound)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrZoneNotFound)
case http.StatusGone:
return nil, fmt.Errorf("%w", errors.ErrAccountInactive)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAccountInactive)
case http.StatusLengthRequired:
return nil, fmt.Errorf("%w: %s", errors.ErrMalformedIPSent, ip)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrMalformedIPSent, ip)
case http.StatusPreconditionFailed:
return nil, fmt.Errorf("%w: %s", errors.ErrPrivateIPSent, ip)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrPrivateIPSent, ip)
case http.StatusServiceUnavailable:
return nil, fmt.Errorf("%w", errors.ErrDNSServerSide)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrDNSServerSide)
default:
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
}
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
switch {
case strings.HasPrefix(s, constants.Notfqdn):
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case strings.HasPrefix(s, "abuse"):
return nil, fmt.Errorf("%w", errors.ErrAbuse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAbuse)
case strings.HasPrefix(s, "badrequest"):
return nil, fmt.Errorf("%w", errors.ErrBadRequest)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
case strings.HasPrefix(s, "good"), strings.HasPrefix(s, "nochg"):
return ip, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
}

View File

@@ -5,8 +5,8 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -104,9 +104,9 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
recordType := constants.A
if ip.To4() == nil {
if ip.Is6() {
recordType = constants.AAAA
}
u := url.URL{
@@ -136,12 +136,12 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
encoder := json.NewEncoder(buffer)
err = encoder.Encode(requestData)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrRequestEncode, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrRequestEncode, err)
}
request, err := http.NewRequestWithContext(ctx, http.MethodPost, u.String(), buffer)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetContentType(request, "application/json")
headers.SetXAuthUsername(request, p.username)
@@ -150,12 +150,12 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
if response.StatusCode > http.StatusUnsupportedMediaType {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.BodyToSingleLine(response.Body))
}
@@ -168,11 +168,11 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
err = decoder.Decode(&parsedJSON)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
if parsedJSON.Message != "ok" {
return nil, fmt.Errorf("%w: %s", errors.ErrUnsuccessfulResponse, parsedJSON.Error)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnsuccessfulResponse, parsedJSON.Error)
}
return ip, nil

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -104,7 +104,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
// see https://wiki.securepoint.de/SPDyn/Variablen
u := url.URL{
Scheme: "https",
@@ -130,42 +130,42 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
bodyString := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.ToSingleLine(bodyString))
}
switch {
case isAny(bodyString, constants.Abuse, "numhost"):
return nil, fmt.Errorf("%w", errors.ErrAbuse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAbuse)
case isAny(bodyString, constants.Badauth, "!yours"):
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case strings.HasPrefix(bodyString, "good"):
return ip, nil
case bodyString == constants.Notfqdn:
return nil, fmt.Errorf("%w: not fqdn", errors.ErrBadRequest)
return netip.Addr{}, fmt.Errorf("%w: not fqdn", errors.ErrBadRequest)
case strings.HasPrefix(bodyString, "nochg"):
return ip, nil
case isAny(bodyString, "nohost", "fatal"):
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, bodyString)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, bodyString)
}
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -93,7 +93,7 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
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.domain, p.password),
@@ -109,38 +109,38 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
str := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, str)
return netip.Addr{}, fmt.Errorf("%w: %d: %s", errors.ErrBadHTTPStatus, response.StatusCode, str)
}
switch {
case strings.HasPrefix(str, constants.Notfqdn):
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case strings.HasPrefix(str, constants.Abuse):
return nil, fmt.Errorf("%w", errors.ErrAbuse)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAbuse)
case strings.HasPrefix(str, "badrequest"):
return nil, fmt.Errorf("%w", errors.ErrBadRequest)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
case strings.HasPrefix(str, "constants.Badauth"):
return nil, fmt.Errorf("%w", errors.ErrAuth)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrAuth)
case strings.HasPrefix(str, "good"), strings.HasPrefix(str, "nochg"):
return ip, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, str)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, str)
}
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"net/url"
"strings"
@@ -98,10 +98,10 @@ func (p *Provider) HTML() models.HTMLRow {
}
}
func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error) {
func (p *Provider) Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error) {
host := "dyndns.variomedia.de"
if p.useProviderIP {
if ip.To4() == nil {
if ip.Is6() {
host = "dyndns6.variomedia.de"
} else {
host = "dyndns4.variomedia.de"
@@ -123,35 +123,35 @@ func (p *Provider) Update(ctx context.Context, client *http.Client, ip net.IP) (
request, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
headers.SetUserAgent(request)
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
return netip.Addr{}, fmt.Errorf("%w: %w", errors.ErrUnmarshalResponse, err)
}
s := string(b)
if response.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%w: %d: %s",
return netip.Addr{}, fmt.Errorf("%w: %d: %s",
errors.ErrBadHTTPStatus, response.StatusCode, utils.ToSingleLine(s))
}
switch {
case strings.HasPrefix(s, constants.Notfqdn):
return nil, fmt.Errorf("%w", errors.ErrHostnameNotExists)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrHostnameNotExists)
case strings.HasPrefix(s, "badrequest"):
return nil, fmt.Errorf("%w", errors.ErrBadRequest)
return netip.Addr{}, fmt.Errorf("%w", errors.ErrBadRequest)
case strings.HasPrefix(s, "good"):
return ip, nil
default:
return nil, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
return netip.Addr{}, fmt.Errorf("%w: %s", errors.ErrUnknownResponse, s)
}
}

View File

@@ -5,8 +5,8 @@ import (
"encoding/json"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"github.com/qdm12/ddns-updater/internal/models"
"github.com/qdm12/ddns-updater/internal/settings/constants"
@@ -56,7 +56,7 @@ type Settings interface {
HTML() models.HTMLRow
Proxied() bool
IPVersion() ipversion.IPVersion
Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error)
Update(ctx context.Context, client *http.Client, ip netip.Addr) (newIP netip.Addr, err error)
}
var ErrProviderUnknown = errors.New("unknown provider")

View File

@@ -1,8 +1,7 @@
package utils
import (
"fmt"
"net"
"net/netip"
"regexp"
)
@@ -16,29 +15,26 @@ func MatchEmail(email string) bool {
return regexEmail.MatchString(email)
}
func FindIPv4Addresses(text string) (addresses []net.IP) {
func FindIPv4Addresses(text string) (addresses []netip.Addr) {
const n = -1
ipv4Strings := regexIPv4.FindAllString(text, n)
return mustParseIPAddresses(ipv4Strings)
}
func FindIPv6Addresses(text string) (addresses []net.IP) {
func FindIPv6Addresses(text string) (addresses []netip.Addr) {
const n = -1
ipv6Strings := regexIPv6.FindAllString(text, n)
return mustParseIPAddresses(ipv6Strings)
}
func mustParseIPAddresses(ipStrings []string) (addresses []net.IP) {
func mustParseIPAddresses(ipStrings []string) (addresses []netip.Addr) {
if len(ipStrings) == 0 {
return nil
}
addresses = make([]net.IP, len(ipStrings))
addresses = make([]netip.Addr, len(ipStrings))
for i, ipString := range ipStrings {
addresses[i] = net.ParseIP(ipString)
if addresses[i] == nil {
panic(fmt.Sprintf("invalid IP address: %q", ipString))
}
addresses[i] = netip.MustParseAddr(ipString)
}
return addresses

View File

@@ -1,7 +1,7 @@
package utils
import (
"net"
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
@@ -12,7 +12,7 @@ func Test_FindIPv4Addresses(t *testing.T) {
testCases := map[string]struct {
text string
addresses []net.IP
addresses []netip.Addr
}{
"empty_string": {},
"no_address": {
@@ -20,15 +20,15 @@ func Test_FindIPv4Addresses(t *testing.T) {
},
"single_address_exact": {
text: "192.168.1.5",
addresses: []net.IP{net.IPv4(192, 168, 1, 5)},
addresses: []netip.Addr{netip.AddrFrom4([4]byte{192, 168, 1, 5})},
},
"multiple_in_text": {
text: "sd 192.168.1.5 1.5 1.3.5.4",
addresses: []net.IP{net.IPv4(192, 168, 1, 5), net.IPv4(1, 3, 5, 4)},
addresses: []netip.Addr{netip.AddrFrom4([4]byte{192, 168, 1, 5}), netip.AddrFrom4([4]byte{1, 3, 5, 4})},
},
"longer_than_normal_ip": {
text: "0.0.0.0.0",
addresses: []net.IP{net.IPv4(0, 0, 0, 0)},
addresses: []netip.Addr{netip.AddrFrom4([4]byte{0, 0, 0, 0})},
},
}
for name, testCase := range testCases {
@@ -47,7 +47,7 @@ func Test_FindIPv6Addresses(t *testing.T) {
testCases := map[string]struct {
text string
addresses []net.IP
addresses []netip.Addr
}{
"empty_string": {},
"no_address": {
@@ -58,13 +58,13 @@ func Test_FindIPv6Addresses(t *testing.T) {
},
"single_address_exact": {
text: "::1",
addresses: []net.IP{net.ParseIP("::1")},
addresses: []netip.Addr{netip.MustParseAddr("::1")},
},
"multiple_in_text": {
text: "2001:0db8:85a3:0000:0000:8a2e:0370:7334 sdas ::1",
addresses: []net.IP{
net.ParseIP("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
net.ParseIP("::1"),
addresses: []netip.Addr{
netip.MustParseAddr("2001:0db8:85a3:0000:0000:8a2e:0370:7334"),
netip.MustParseAddr("::1"),
},
},
}

View File

@@ -2,16 +2,16 @@ package update
import (
"context"
"net"
"net/netip"
"strconv"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
)
type getIPFunc func(ctx context.Context) (ip net.IP, err error)
type getIPFunc func(ctx context.Context) (ip netip.Addr, err error)
func tryAndRepeatGettingIP(ctx context.Context, getIPFunc getIPFunc,
logger Logger, version ipversion.IPVersion) (ip net.IP, err error) {
logger Logger, version ipversion.IPVersion) (ip netip.Addr, err error) {
const tries = 3
logMessagePrefix := "obtaining " + version.String() + " address"
for try := 0; try < tries; try++ {

View File

@@ -3,19 +3,20 @@ package update
import (
"context"
"net"
"net/netip"
"time"
"github.com/qdm12/ddns-updater/internal/records"
)
type PublicIPFetcher interface {
IP(ctx context.Context) (net.IP, error)
IP4(ctx context.Context) (net.IP, error)
IP6(ctx context.Context) (net.IP, error)
IP(ctx context.Context) (netip.Addr, error)
IP4(ctx context.Context) (netip.Addr, error)
IP6(ctx context.Context) (netip.Addr, error)
}
type UpdaterInterface interface {
Update(ctx context.Context, recordID uint, ip net.IP, now time.Time) (err error)
Update(ctx context.Context, recordID uint, ip netip.Addr, now time.Time) (err error)
}
type Database interface {

15
internal/update/mask.go Normal file
View File

@@ -0,0 +1,15 @@
package update
import (
"fmt"
"net/netip"
)
func mustMaskIPv6(ipv6 netip.Addr, ipv6MaskBits uint8) (maskedIPv6 netip.Addr) {
prefix, err := ipv6.Prefix(int(ipv6MaskBits))
if err != nil {
panic(fmt.Sprintf("getting masked IPv6 prefix: %s", err))
}
maskedIPv6 = prefix.Addr()
return maskedIPv6
}

View File

@@ -0,0 +1,19 @@
package update
import (
"net/netip"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_mustMaskIPv6(t *testing.T) {
t.Parallel()
const maskBits = 24
ip := netip.AddrFrom4([4]byte{1, 2, 3, 4})
maskedIP := mustMaskIPv6(ip, maskBits)
expected := netip.AddrFrom4([4]byte{1, 2, 3, 0})
assert.Equal(t, expected, maskedIP)
}

View File

@@ -3,7 +3,7 @@ package update
import (
"context"
"fmt"
"net"
"net/netip"
"time"
"github.com/qdm12/ddns-updater/internal/constants"
@@ -13,55 +13,67 @@ import (
)
type Runner struct {
period time.Duration
db Database
updater UpdaterInterface
force chan struct{}
forceResult chan []error
ipv6Mask net.IPMask
cooldown time.Duration
resolver LookupIPer
ipGetter PublicIPFetcher
logger Logger
timeNow func() time.Time
period time.Duration
db Database
updater UpdaterInterface
force chan struct{}
forceResult chan []error
ipv6MaskBits uint8
cooldown time.Duration
resolver LookupIPer
ipGetter PublicIPFetcher
logger Logger
timeNow func() time.Time
}
func NewRunner(db Database, updater UpdaterInterface, ipGetter PublicIPFetcher,
period time.Duration, ipv6Mask net.IPMask, cooldown time.Duration,
period time.Duration, ipv6MaskBits uint8, cooldown time.Duration,
logger Logger, resolver LookupIPer, timeNow func() time.Time) *Runner {
return &Runner{
period: period,
db: db,
updater: updater,
force: make(chan struct{}),
forceResult: make(chan []error),
ipv6Mask: ipv6Mask,
cooldown: cooldown,
resolver: resolver,
ipGetter: ipGetter,
logger: logger,
timeNow: timeNow,
period: period,
db: db,
updater: updater,
force: make(chan struct{}),
forceResult: make(chan []error),
ipv6MaskBits: ipv6MaskBits,
cooldown: cooldown,
resolver: resolver,
ipGetter: ipGetter,
logger: logger,
timeNow: timeNow,
}
}
func (r *Runner) lookupIPsResilient(ctx context.Context, hostname string, tries int) (
ipv4 net.IP, ipv6 net.IP, err error) {
ipv4 netip.Addr, ipv6 netip.Addr, err error) {
for i := 0; i < tries; i++ {
ipv4, ipv6, err = r.lookupIPs(ctx, hostname)
if err == nil {
return ipv4, ipv6, nil
}
}
return nil, nil, err
return netip.Addr{}, netip.Addr{}, err
}
func (r *Runner) lookupIPs(ctx context.Context, hostname string) (ipv4 net.IP, ipv6 net.IP, err error) {
ips, err := r.resolver.LookupIP(ctx, "ip", hostname)
func (r *Runner) lookupIPs(ctx context.Context, hostname string) (
ipv4 netip.Addr, ipv6 netip.Addr, err error) {
netIPs, err := r.resolver.LookupIP(ctx, "ip", hostname)
if err != nil {
return nil, nil, err
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()))
}
}
for _, ip := range ips {
if ip.To4() == nil {
if ip.Is6() {
ipv6 = ip
} else {
ipv4 = ip
@@ -87,15 +99,15 @@ func doIPVersion(records []librecords.Record) (doIP, doIPv4, doIPv6 bool) {
return doIP, doIPv4, doIPv6
}
func (r *Runner) getNewIPs(ctx context.Context, doIP, doIPv4, doIPv6 bool, ipv6Mask net.IPMask) (
ip, ipv4, ipv6 net.IP, errors []error) {
func (r *Runner) getNewIPs(ctx context.Context, doIP, doIPv4, doIPv6 bool, ipv6MaskBits uint8) (
ip, ipv4, ipv6 netip.Addr, errors []error) {
var err error
if doIP {
ip, err = tryAndRepeatGettingIP(ctx, r.ipGetter.IP, r.logger, ipversion.IP4or6)
if err != nil {
errors = append(errors, err)
} else if ip.To4() == nil {
ip = ip.Mask(ipv6Mask)
} else if ip.Is6() {
ip = mustMaskIPv6(ip, ipv6MaskBits)
}
}
if doIPv4 {
@@ -109,17 +121,17 @@ func (r *Runner) getNewIPs(ctx context.Context, doIP, doIPv4, doIPv6 bool, ipv6M
if err != nil {
errors = append(errors, err)
} else {
ipv6 = ipv6.Mask(ipv6Mask)
ip = mustMaskIPv6(ip, ipv6MaskBits)
}
}
return ip, ipv4, ipv6, errors
}
func (r *Runner) getRecordIDsToUpdate(ctx context.Context, records []librecords.Record,
ip, ipv4, ipv6 net.IP, now time.Time, ipv6Mask net.IPMask) (recordIDs map[uint]struct{}) {
ip, ipv4, ipv6 netip.Addr, now time.Time, ipv6MaskBits uint8) (recordIDs map[uint]struct{}) {
recordIDs = make(map[uint]struct{})
for i, record := range records {
if shouldUpdate := r.shouldUpdateRecord(ctx, record, ip, ipv4, ipv6, now, ipv6Mask); shouldUpdate {
if shouldUpdate := r.shouldUpdateRecord(ctx, record, ip, ipv4, ipv6, now, ipv6MaskBits); shouldUpdate {
id := uint(i)
recordIDs[id] = struct{}{}
}
@@ -128,7 +140,7 @@ func (r *Runner) getRecordIDsToUpdate(ctx context.Context, records []librecords.
}
func (r *Runner) shouldUpdateRecord(ctx context.Context, record librecords.Record,
ip, ipv4, ipv6 net.IP, now time.Time, ipv6Mask net.IPMask) (update bool) {
ip, ipv4, ipv6 netip.Addr, now time.Time, ipv6MaskBits uint8) (update bool) {
isWithinBanPeriod := record.LastBan != nil && now.Sub(*record.LastBan) < time.Hour
isWithinCooldown := now.Sub(record.History.GetSuccessTime()) < r.cooldown
if isWithinBanPeriod || isWithinCooldown {
@@ -143,14 +155,14 @@ func (r *Runner) shouldUpdateRecord(ctx context.Context, record librecords.Recor
lastIP := record.History.GetCurrentIP() // can be nil
return r.shouldUpdateRecordNoLookup(hostname, ipVersion, lastIP, ip, ipv4, ipv6)
}
return r.shouldUpdateRecordWithLookup(ctx, hostname, ipVersion, ip, ipv4, ipv6, ipv6Mask)
return r.shouldUpdateRecordWithLookup(ctx, hostname, ipVersion, ip, ipv4, ipv6, ipv6MaskBits)
}
func (r *Runner) shouldUpdateRecordNoLookup(hostname string, ipVersion ipversion.IPVersion,
lastIP, ip, ipv4, ipv6 net.IP) (update bool) {
lastIP, ip, ipv4, ipv6 netip.Addr) (update bool) {
switch ipVersion {
case ipversion.IP4or6:
if ip != nil && !ip.Equal(lastIP) {
if ip.IsValid() && ip.Compare(lastIP) != 0 {
r.logger.Info("Last IP address stored for " + hostname +
" is " + lastIP.String() + " and your IP address is " + ip.String())
return true
@@ -158,7 +170,7 @@ func (r *Runner) shouldUpdateRecordNoLookup(hostname string, ipVersion ipversion
r.logger.Debug("Last IP address stored for " + hostname + " is " +
lastIP.String() + " and your IP address is " + ip.String() + ", skipping update")
case ipversion.IP4:
if ipv4 != nil && !ipv4.Equal(lastIP) {
if ipv4.IsValid() && ipv4.Compare(lastIP) != 0 {
r.logger.Info("Last IPv4 address stored for " + hostname +
" is " + lastIP.String() + " and your IPv4 address is " + ip.String())
return true
@@ -166,7 +178,7 @@ func (r *Runner) shouldUpdateRecordNoLookup(hostname string, ipVersion ipversion
r.logger.Debug("Last IPv4 address stored for " + hostname + " is " +
lastIP.String() + " and your IPv4 address is " + ip.String() + ", skipping update")
case ipversion.IP6:
if ipv6 != nil && !ipv6.Equal(lastIP) {
if ipv6.IsValid() && ipv6.Compare(lastIP) != 0 {
r.logger.Info("Last IPv6 address stored for " + hostname +
" is " + lastIP.String() + " and your IPv6 address is " + ip.String())
return true
@@ -178,7 +190,7 @@ func (r *Runner) shouldUpdateRecordNoLookup(hostname string, ipVersion ipversion
}
func (r *Runner) shouldUpdateRecordWithLookup(ctx context.Context, hostname string, ipVersion ipversion.IPVersion,
ip, ipv4, ipv6 net.IP, ipv6Mask net.IPMask) (update bool) {
ip, ipv4, ipv6 netip.Addr, ipv6MaskBits uint8) (update bool) {
const tries = 5
recordIPv4, recordIPv6, err := r.lookupIPsResilient(ctx, hostname, tries)
if err != nil {
@@ -191,17 +203,17 @@ func (r *Runner) shouldUpdateRecordWithLookup(ctx context.Context, hostname stri
fmt.Sprint(tries) + " tries: " + err.Error()) // update anyway
}
if recordIPv6 != nil {
recordIPv6 = recordIPv6.Mask(ipv6Mask)
if recordIPv6.IsValid() {
recordIPv6 = mustMaskIPv6(recordIPv6, ipv6MaskBits)
}
switch ipVersion {
case ipversion.IP4or6:
recordIP := recordIPv4
if ip.To4() == nil {
if ip.Is6() {
recordIP = recordIPv6
}
if ip != nil && !ip.Equal(recordIPv4) && !ip.Equal(recordIPv6) {
if ip.IsValid() && ip.Compare(recordIPv4) != 0 && ip.Compare(recordIPv6) != 0 {
r.logger.Info("IP address of " + hostname + " is " + recordIP.String() +
" and your IP address is " + ip.String())
return true
@@ -209,7 +221,7 @@ func (r *Runner) shouldUpdateRecordWithLookup(ctx context.Context, hostname stri
r.logger.Debug("IP address of " + hostname + " is " + recordIP.String() +
" and your IP address is " + ip.String() + ", skipping update")
case ipversion.IP4:
if ipv4 != nil && !ipv4.Equal(recordIPv4) {
if ipv4.IsValid() && ipv4.Compare(recordIPv4) != 0 {
r.logger.Info("IPv4 address of " + hostname + " is " + recordIPv4.String() +
" and your IPv4 address is " + ipv4.String())
return true
@@ -217,7 +229,7 @@ func (r *Runner) shouldUpdateRecordWithLookup(ctx context.Context, hostname stri
r.logger.Debug("IPv4 address of " + hostname + " is " + recordIPv4.String() +
" and your IPv4 address is " + ipv4.String() + ", skipping update")
case ipversion.IP6:
if ipv6 != nil && !ipv6.Equal(recordIPv6) {
if ipv6.IsValid() && ipv6.Compare(recordIPv6) != 0 {
r.logger.Info("IPv6 address of " + hostname + " is " + recordIPv6.String() +
" and your IPv6 address is " + ipv6.String())
return true
@@ -228,7 +240,7 @@ func (r *Runner) shouldUpdateRecordWithLookup(ctx context.Context, hostname stri
return false
}
func getIPMatchingVersion(ip, ipv4, ipv6 net.IP, ipVersion ipversion.IPVersion) net.IP {
func getIPMatchingVersion(ip, ipv4, ipv6 netip.Addr, ipVersion ipversion.IPVersion) netip.Addr {
switch ipVersion {
case ipversion.IP4or6:
return ip
@@ -237,17 +249,17 @@ func getIPMatchingVersion(ip, ipv4, ipv6 net.IP, ipVersion ipversion.IPVersion)
case ipversion.IP6:
return ipv6
}
return nil
return netip.Addr{}
}
func setInitialUpToDateStatus(db Database, id uint, updateIP net.IP, now time.Time) error {
func setInitialUpToDateStatus(db Database, id uint, updateIP netip.Addr, now time.Time) error {
record, err := db.Select(id)
if err != nil {
return err
}
record.Status = constants.UPTODATE
record.Time = now
if record.History.GetCurrentIP() == nil {
if !record.History.GetCurrentIP().IsValid() {
record.History = append(record.History, models.HistoryEvent{
IP: updateIP,
Time: now,
@@ -256,18 +268,18 @@ func setInitialUpToDateStatus(db Database, id uint, updateIP net.IP, now time.Ti
return db.Update(id, record)
}
func (r *Runner) updateNecessary(ctx context.Context, ipv6Mask net.IPMask) (errors []error) {
func (r *Runner) updateNecessary(ctx context.Context, ipv6MaskBits uint8) (errors []error) {
records := r.db.SelectAll()
doIP, doIPv4, doIPv6 := doIPVersion(records)
r.logger.Debug(fmt.Sprintf("configured to fetch IP: v4 or v6: %t, v4: %t, v6: %t", doIP, doIPv4, doIPv6))
ip, ipv4, ipv6, errors := r.getNewIPs(ctx, doIP, doIPv4, doIPv6, ipv6Mask)
ip, ipv4, ipv6, errors := r.getNewIPs(ctx, doIP, doIPv4, doIPv6, ipv6MaskBits)
r.logger.Debug(fmt.Sprintf("your public IP address are: v4 or v6: %s, v4: %s, v6: %s", ip, ipv4, ipv6))
for _, err := range errors {
r.logger.Error(err.Error())
}
now := r.timeNow()
recordIDs := r.getRecordIDsToUpdate(ctx, records, ip, ipv4, ipv6, now, ipv6Mask)
recordIDs := r.getRecordIDsToUpdate(ctx, records, ip, ipv4, ipv6, now, ipv6MaskBits)
for i, record := range records {
id := uint(i)
@@ -302,9 +314,9 @@ func (r *Runner) Run(ctx context.Context, done chan<- struct{}) {
for {
select {
case <-ticker.C:
r.updateNecessary(ctx, r.ipv6Mask)
r.updateNecessary(ctx, r.ipv6MaskBits)
case <-r.force:
r.forceResult <- r.updateNecessary(ctx, r.ipv6Mask)
r.forceResult <- r.updateNecessary(ctx, r.ipv6MaskBits)
case <-ctx.Done():
ticker.Stop()
return

View File

@@ -4,8 +4,8 @@ import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
"time"
"github.com/qdm12/ddns-updater/internal/constants"
@@ -32,7 +32,7 @@ func NewUpdater(db Database, client *http.Client, notify notifyFunc, logger Debu
}
}
func (u *Updater) Update(ctx context.Context, id uint, ip net.IP, now time.Time) (err error) {
func (u *Updater) Update(ctx context.Context, id uint, ip netip.Addr, now time.Time) (err error) {
record, err := u.db.Select(id)
if err != nil {
return err

View File

@@ -4,7 +4,7 @@ import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"github.com/miekg/dns"
)
@@ -18,7 +18,7 @@ var (
)
func fetch(ctx context.Context, client Client, providerData providerData) (
publicIP net.IP, err error) {
publicIP netip.Addr, err error) {
message := &dns.Msg{
MsgHdr: dns.MsgHdr{
Opcode: dns.OpcodeQuery,
@@ -34,34 +34,34 @@ func fetch(ctx context.Context, client Client, providerData providerData) (
r, _, err := client.ExchangeContext(ctx, message, providerData.nameserver)
if err != nil {
return nil, err
return netip.Addr{}, err
}
L := len(r.Answer)
if L == 0 {
return nil, ErrNoTXTRecordFound
return netip.Addr{}, fmt.Errorf("%w", ErrNoTXTRecordFound)
} else if L > 1 {
return nil, fmt.Errorf("%w: %d instead of 1", ErrTooManyAnswers, L)
return netip.Addr{}, fmt.Errorf("%w: %d instead of 1", ErrTooManyAnswers, L)
}
answer := r.Answer[0]
txt, ok := answer.(*dns.TXT)
if !ok {
return nil, fmt.Errorf("%w: %T instead of *dns.TXT",
return netip.Addr{}, fmt.Errorf("%w: %T instead of *dns.TXT",
ErrInvalidAnswerType, answer)
}
L = len(txt.Txt)
if L == 0 {
return nil, ErrNoTXTRecordFound
return netip.Addr{}, fmt.Errorf("%w", ErrNoTXTRecordFound)
} else if L > 1 {
return nil, fmt.Errorf("%w: %d instead of 1", ErrTooManyTXTRecords, L)
return netip.Addr{}, fmt.Errorf("%w: %d instead of 1", ErrTooManyTXTRecords, L)
}
ipString := txt.Txt[0]
publicIP = net.ParseIP(ipString)
if publicIP == nil {
return nil, fmt.Errorf("%w: %q", ErrIPMalformed, ipString)
publicIP, err = netip.ParseAddr(ipString)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", ErrIPMalformed, err)
}
return publicIP, nil

View File

@@ -3,7 +3,7 @@ package dns
import (
"context"
"errors"
"net"
"net/netip"
"testing"
"time"
@@ -39,7 +39,7 @@ func Test_fetch(t *testing.T) {
testCases := map[string]struct {
response *dns.Msg
exchangeErr error
publicIP net.IP
publicIP netip.Addr
err error
}{
"success": {
@@ -50,7 +50,7 @@ func Test_fetch(t *testing.T) {
},
},
},
publicIP: net.IP{55, 55, 55, 55},
publicIP: netip.AddrFrom4([4]byte{55, 55, 55, 55}),
},
"exchange error": {
exchangeErr: errors.New("dummy"),
@@ -92,7 +92,7 @@ func Test_fetch(t *testing.T) {
Txt: []string{"invalid"},
}},
},
err: errors.New(`IP address malformed: "invalid"`),
err: errors.New(`IP address malformed: ParseAddr("invalid"): unable to parse IP`),
},
}
@@ -118,7 +118,7 @@ func Test_fetch(t *testing.T) {
assert.NoError(t, err)
}
if !testCase.publicIP.Equal(publicIP) {
if testCase.publicIP.Compare(publicIP) != 0 {
t.Errorf("IP address mismatch: expected %s and got %s", testCase.publicIP, publicIP)
}
})

View File

@@ -2,24 +2,24 @@ package dns
import (
"context"
"net"
"net/netip"
"sync/atomic"
)
func (f *Fetcher) IP(ctx context.Context) (publicIP net.IP, err error) {
func (f *Fetcher) IP(ctx context.Context) (publicIP netip.Addr, err error) {
return f.ip(ctx, f.client)
}
func (f *Fetcher) IP4(ctx context.Context) (publicIP net.IP, err error) {
func (f *Fetcher) IP4(ctx context.Context) (publicIP netip.Addr, err error) {
return f.ip(ctx, f.client4)
}
func (f *Fetcher) IP6(ctx context.Context) (publicIP net.IP, err error) {
func (f *Fetcher) IP6(ctx context.Context) (publicIP netip.Addr, err error) {
return f.ip(ctx, f.client6)
}
func (f *Fetcher) ip(ctx context.Context, client Client) (
publicIP net.IP, err error) {
publicIP netip.Addr, err error) {
index := int(atomic.AddUint32(f.ring.counter, 1)) % len(f.ring.providers)
provider := f.ring.providers[index]
return fetch(ctx, client, provider.data())

View File

@@ -5,8 +5,8 @@ import (
"errors"
"fmt"
"io"
"net"
"net/http"
"net/netip"
"regexp"
"strings"
@@ -26,33 +26,33 @@ var (
)
func fetch(ctx context.Context, client *http.Client, url string, version ipversion.IPVersion) (
publicIP net.IP, err error) {
publicIP netip.Addr, err error) {
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
return netip.Addr{}, err
}
response, err := client.Do(request)
if err != nil {
return nil, err
return netip.Addr{}, err
}
defer response.Body.Close()
switch response.StatusCode {
case http.StatusOK:
case http.StatusForbidden, http.StatusTooManyRequests:
return nil, fmt.Errorf("%w: %d (%s)", ErrBanned,
return netip.Addr{}, fmt.Errorf("%w: %d (%s)", ErrBanned,
response.StatusCode, bodyToSingleLine(response.Body))
}
b, err := io.ReadAll(response.Body)
if err != nil {
return nil, err
return netip.Addr{}, err
}
err = response.Body.Close()
if err != nil {
return nil, err
return netip.Addr{}, err
}
s := string(b)
@@ -69,39 +69,39 @@ func fetch(ctx context.Context, client *http.Client, url string, version ipversi
case len(ipv6Strings) == 1:
ipString = ipv6Strings[0]
case len(ipv4Strings) > 1:
return nil, fmt.Errorf("%w: found %d IPv4 addresses instead of 1",
return netip.Addr{}, fmt.Errorf("%w: found %d IPv4 addresses instead of 1",
ErrTooManyIPs, len(ipv4Strings))
case len(ipv6Strings) > 1:
return nil, fmt.Errorf("%w: found %d IPv6 addresses instead of 1",
return netip.Addr{}, fmt.Errorf("%w: found %d IPv6 addresses instead of 1",
ErrTooManyIPs, len(ipv6Strings))
default:
return nil, fmt.Errorf("%w: from %q", ErrNoIPFound, url)
return netip.Addr{}, fmt.Errorf("%w: from %q", ErrNoIPFound, url)
}
case ipversion.IP4:
switch len(ipv4Strings) {
case 0:
return nil, fmt.Errorf("%w: from %q for version %s", ErrNoIPFound, url, version)
return netip.Addr{}, fmt.Errorf("%w: from %q for version %s", ErrNoIPFound, url, version)
case 1:
ipString = ipv4Strings[0]
default:
return nil, fmt.Errorf("%w: found %d IPv4 addresses instead of 1",
return netip.Addr{}, fmt.Errorf("%w: found %d IPv4 addresses instead of 1",
ErrTooManyIPs, len(ipv4Strings))
}
case ipversion.IP6:
switch len(ipv6Strings) {
case 0:
return nil, fmt.Errorf("%w: from %q for version %s", ErrNoIPFound, url, version)
return netip.Addr{}, fmt.Errorf("%w: from %q for version %s", ErrNoIPFound, url, version)
case 1:
ipString = ipv6Strings[0]
default:
return nil, fmt.Errorf("%w: found %d IPv6 addresses instead of 1",
return netip.Addr{}, fmt.Errorf("%w: found %d IPv6 addresses instead of 1",
ErrTooManyIPs, len(ipv6Strings))
}
}
publicIP = net.ParseIP(ipString)
if publicIP == nil {
return nil, fmt.Errorf("%w: %s", ErrIPMalformed, ipString)
publicIP, err = netip.ParseAddr(ipString)
if err != nil {
return netip.Addr{}, fmt.Errorf("%w: %w", ErrIPMalformed, err)
}
return publicIP, nil

View File

@@ -5,8 +5,8 @@ import (
"context"
"errors"
"io"
"net"
"net/http"
"net/netip"
"testing"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
@@ -28,7 +28,7 @@ func Test_fetch(t *testing.T) {
version ipversion.IPVersion
httpContent []byte
httpErr error
publicIP net.IP
publicIP netip.Addr
err error
}{
"canceled context": {
@@ -62,24 +62,24 @@ func Test_fetch(t *testing.T) {
url: "https://opendns.com/ip",
version: ipversion.IP4or6,
httpContent: []byte(`1.67.201.251`),
publicIP: net.IP{1, 67, 201, 251},
publicIP: netip.AddrFrom4([4]byte{1, 67, 201, 251}),
},
"single IPv6 for IP4or6": {
ctx: context.Background(),
url: "https://opendns.com/ip",
version: ipversion.IP4or6,
httpContent: []byte(`::1`),
publicIP: net.IP{
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
},
publicIP: netip.AddrFrom16([16]byte{
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1,
}),
},
"IPv4 and IPv6 for IP4or6": {
ctx: context.Background(),
url: "https://opendns.com/ip",
version: ipversion.IP4or6,
httpContent: []byte(`1.67.201.251 ::1`),
publicIP: net.IP{1, 67, 201, 251},
publicIP: netip.AddrFrom4([4]byte{1, 67, 201, 251}),
},
"too many IPv4s for IP4or6": {
ctx: context.Background(),
@@ -107,7 +107,7 @@ func Test_fetch(t *testing.T) {
url: "https://opendns.com/ip",
version: ipversion.IP4,
httpContent: []byte(`1.67.201.251`),
publicIP: net.IP{1, 67, 201, 251},
publicIP: netip.AddrFrom4([4]byte{1, 67, 201, 251}),
},
"too many IPv4s for IP4": {
ctx: context.Background(),
@@ -128,10 +128,10 @@ func Test_fetch(t *testing.T) {
url: "https://opendns.com/ip",
version: ipversion.IP6,
httpContent: []byte(`::1`),
publicIP: net.IP{
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
},
publicIP: netip.AddrFrom16([16]byte{
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1,
}),
},
"too many IPv6s for IP6": {
ctx: context.Background(),
@@ -171,7 +171,7 @@ func Test_fetch(t *testing.T) {
assert.NoError(t, err)
}
if !tc.publicIP.Equal(publicIP) {
if tc.publicIP.Compare(publicIP) != 0 {
t.Errorf("IP address mismatch: expected %s and got %s", tc.publicIP, publicIP)
}
})

View File

@@ -4,26 +4,26 @@ import (
"context"
"errors"
"fmt"
"net"
"net/netip"
"strings"
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
)
func (f *Fetcher) IP(ctx context.Context) (publicIP net.IP, err error) {
func (f *Fetcher) IP(ctx context.Context) (publicIP netip.Addr, err error) {
return f.ip(ctx, f.ip4or6, ipversion.IP4or6)
}
func (f *Fetcher) IP4(ctx context.Context) (publicIP net.IP, err error) {
func (f *Fetcher) IP4(ctx context.Context) (publicIP netip.Addr, err error) {
return f.ip(ctx, f.ip4, ipversion.IP4)
}
func (f *Fetcher) IP6(ctx context.Context) (publicIP net.IP, err error) {
func (f *Fetcher) IP6(ctx context.Context) (publicIP netip.Addr, err error) {
return f.ip(ctx, f.ip6, ipversion.IP6)
}
func (f *Fetcher) ip(ctx context.Context, ring *urlsRing, version ipversion.IPVersion) (
publicIP net.IP, err error) {
publicIP netip.Addr, err error) {
ring.mutex.Lock()
var index int
@@ -39,7 +39,7 @@ func (f *Fetcher) ip(ctx context.Context, ring *urlsRing, version ipversion.IPVe
if banned == len(ring.urls) {
banString := ring.banString()
ring.mutex.Unlock()
return nil, fmt.Errorf("%w: %s", ErrBanned, banString)
return netip.Addr{}, fmt.Errorf("%w: %s", ErrBanned, banString)
}
}
@@ -57,7 +57,7 @@ func (f *Fetcher) ip(ctx context.Context, ring *urlsRing, version ipversion.IPVe
ring.banned[index] = strings.ReplaceAll(err.Error(), ErrBanned.Error()+": ", "")
ring.mutex.Unlock()
}
return nil, err
return netip.Addr{}, err
}
return publicIP, nil
}

View File

@@ -4,8 +4,8 @@ import (
"bytes"
"context"
"io"
"net"
"net/http"
"net/netip"
"testing"
"time"
@@ -19,7 +19,7 @@ func Test_fetcher_IP(t *testing.T) {
ctx := context.Background()
const url = "c"
httpBytes := []byte(`55.55.55.55`)
expectedPublicIP := net.IP{55, 55, 55, 55}
expectedPublicIP := netip.AddrFrom4([4]byte{55, 55, 55, 55})
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
@@ -51,7 +51,7 @@ func Test_fetcher_IP(t *testing.T) {
publicIP, err := initialFetcher.IP(ctx)
assert.NoError(t, err)
if !expectedPublicIP.Equal(publicIP) {
if expectedPublicIP.Compare(publicIP) != 0 {
t.Errorf("IP address mismatch: expected %s and got %s", expectedPublicIP, publicIP)
}
assert.Equal(t, expectedFetcher, initialFetcher)
@@ -63,7 +63,7 @@ func Test_fetcher_IP4(t *testing.T) {
ctx := context.Background()
const url = "c"
httpBytes := []byte(`55.55.55.55`)
expectedPublicIP := net.IP{55, 55, 55, 55}
expectedPublicIP := netip.AddrFrom4([4]byte{55, 55, 55, 55})
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
@@ -95,7 +95,7 @@ func Test_fetcher_IP4(t *testing.T) {
publicIP, err := initialFetcher.IP4(ctx)
assert.NoError(t, err)
if !expectedPublicIP.Equal(publicIP) {
if expectedPublicIP.Compare(publicIP) != 0 {
t.Errorf("IP address mismatch: expected %s and got %s", expectedPublicIP, publicIP)
}
assert.Equal(t, expectedFetcher, initialFetcher)
@@ -107,10 +107,9 @@ func Test_fetcher_IP6(t *testing.T) {
ctx := context.Background()
const url = "c"
httpBytes := []byte(`::1`)
expectedPublicIP := net.IP{
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1,
}
expectedPublicIP := netip.AddrFrom16([16]byte{
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 1})
client := &http.Client{
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
@@ -142,7 +141,7 @@ func Test_fetcher_IP6(t *testing.T) {
publicIP, err := initialFetcher.IP6(ctx)
assert.NoError(t, err)
if !expectedPublicIP.Equal(publicIP) {
if expectedPublicIP.Compare(publicIP) != 0 {
t.Errorf("IP address mismatch: expected %s and got %s", expectedPublicIP, publicIP)
}
assert.Equal(t, expectedFetcher, initialFetcher)
@@ -176,7 +175,7 @@ func Test_fetcher_ip(t *testing.T) {
testCases := map[string]struct {
initialFetcher *Fetcher
ctx context.Context
publicIP net.IP
publicIP netip.Addr
err error
errMessage string
finalFetcher *Fetcher // client is ignored when comparing the two
@@ -191,7 +190,7 @@ func Test_fetcher_ip(t *testing.T) {
urls: []string{"a", "b"},
},
},
publicIP: net.IP{55, 55, 55, 55},
publicIP: netip.AddrFrom4([4]byte{55, 55, 55, 55}),
finalFetcher: &Fetcher{
timeout: time.Hour,
ip4or6: &urlsRing{
@@ -210,7 +209,7 @@ func Test_fetcher_ip(t *testing.T) {
urls: []string{"a", "b"},
},
},
publicIP: net.IP{55, 55, 55, 55},
publicIP: netip.AddrFrom4([4]byte{55, 55, 55, 55}),
finalFetcher: &Fetcher{
timeout: time.Hour,
ip4or6: &urlsRing{
@@ -276,7 +275,7 @@ func Test_fetcher_ip(t *testing.T) {
banned: map[int]string{1: "banned"},
},
},
publicIP: net.IP{55, 55, 55, 55},
publicIP: netip.AddrFrom4([4]byte{55, 55, 55, 55}),
},
"all banned": {
ctx: context.Background(),
@@ -335,7 +334,7 @@ func Test_fetcher_ip(t *testing.T) {
assert.EqualError(t, err, testCase.errMessage)
}
if !testCase.publicIP.Equal(publicIP) {
if testCase.publicIP.Compare(publicIP) != 0 {
t.Errorf("IP address mismatch: expected %s and got %s", testCase.publicIP, publicIP)
}

View File

@@ -5,8 +5,8 @@ import (
"errors"
"fmt"
"math/rand"
"net"
"net/http"
"net/netip"
"sync"
)
@@ -47,7 +47,7 @@ func New(client *http.Client, options ...Option) (info *Info, err error) {
// Get finds IP information for the given IP address using one of
// the ip data provider picked at random. A `nil` IP address can be
// given to signal to fetch information on the current public IP address.
func (i *Info) Get(ctx context.Context, ip net.IP) (result Result, err error) {
func (i *Info) Get(ctx context.Context, ip netip.Addr) (result Result, err error) {
if len(i.providers) == 1 {
return i.providers[0].get(ctx, ip)
}
@@ -80,7 +80,7 @@ func (i *Info) Get(ctx context.Context, ip net.IP) (result Result, err error) {
// GetMultiple finds IP information for the given IP addresses, each using
// one of the ip data provider picked at random. It returns a slice of results
// matching the order of the IP addresses given as argument.
func (i *Info) GetMultiple(ctx context.Context, ips []net.IP) (results []Result, err error) {
func (i *Info) GetMultiple(ctx context.Context, ips []netip.Addr) (results []Result, err error) {
type resultWithError struct {
index int
result Result
@@ -92,7 +92,7 @@ func (i *Info) GetMultiple(ctx context.Context, ips []net.IP) (results []Result,
channel := make(chan resultWithError)
for index, ip := range ips {
go func(ctx context.Context, index int, ip net.IP) {
go func(ctx context.Context, index int, ip netip.Addr) {
result := resultWithError{
index: index,
}

View File

@@ -4,8 +4,8 @@ import (
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"net/netip"
)
func newIpinfo(client *http.Client) *ipinfo {
@@ -18,12 +18,12 @@ type ipinfo struct {
client *http.Client
}
func (p *ipinfo) get(ctx context.Context, ip net.IP) (
func (p *ipinfo) get(ctx context.Context, ip netip.Addr) (
result Result, err error) {
result.Source = string(Ipinfo)
url := "https://ipinfo.io/"
if ip != nil {
if ip.IsValid() {
url += ip.String()
}
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
@@ -51,10 +51,10 @@ func (p *ipinfo) get(ctx context.Context, ip net.IP) (
decoder := json.NewDecoder(response.Body)
var data struct {
IP net.IP `json:"ip"`
Region string `json:"region"`
Country string `json:"country"`
City string `json:"city"`
IP netip.Addr `json:"ip"`
Region string `json:"region"`
Country string `json:"country"`
City string `json:"city"`
}
err = decoder.Decode(&data)
if err != nil {

View File

@@ -4,8 +4,8 @@ import (
"context"
"errors"
"fmt"
"net"
"net/http"
"net/netip"
)
type Provider string
@@ -32,7 +32,7 @@ func ValidateProvider(provider Provider) error {
}
type provider interface {
get(ctx context.Context, ip net.IP) (result Result, err error)
get(ctx context.Context, ip netip.Addr) (result Result, err error)
}
func newProvider(providerName Provider, client *http.Client) provider { //nolint:ireturn

View File

@@ -1,9 +1,9 @@
package info
import "net"
import "net/netip"
type Result struct {
IP net.IP
IP netip.Addr
Country *string
Region *string
City *string

View File

@@ -3,16 +3,16 @@ package publicip
import (
"context"
"errors"
"net"
"net/netip"
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
"github.com/qdm12/ddns-updater/pkg/publicip/http"
)
type ipFetcher interface {
IP(ctx context.Context) (ip net.IP, err error)
IP4(ctx context.Context) (ipv4 net.IP, err error)
IP6(ctx context.Context) (ipv6 net.IP, err error)
IP(ctx context.Context) (ip netip.Addr, err error)
IP4(ctx context.Context) (ipv4 netip.Addr, err error)
IP6(ctx context.Context) (ipv6 netip.Addr, err error)
}
type Fetcher struct {
@@ -58,14 +58,14 @@ func NewFetcher(dnsSettings DNSSettings, httpSettings HTTPSettings) (f *Fetcher,
return fetcher, nil
}
func (f *Fetcher) IP(ctx context.Context) (ip net.IP, err error) {
func (f *Fetcher) IP(ctx context.Context) (ip netip.Addr, err error) {
return f.getSubFetcher().IP(ctx)
}
func (f *Fetcher) IP4(ctx context.Context) (ipv4 net.IP, err error) {
func (f *Fetcher) IP4(ctx context.Context) (ipv4 netip.Addr, err error) {
return f.getSubFetcher().IP4(ctx)
}
func (f *Fetcher) IP6(ctx context.Context) (ipv6 net.IP, err error) {
func (f *Fetcher) IP6(ctx context.Context) (ipv6 netip.Addr, err error) {
return f.getSubFetcher().IP6(ctx)
}