mirror of
https://github.com/qdm12/ddns-updater.git
synced 2026-03-31 06:24:00 -04:00
Feature: public IP package to work over HTTPs and DNS (#158)
This commit is contained in:
@@ -34,6 +34,9 @@ issues:
|
||||
linters:
|
||||
- maligned
|
||||
- dupl
|
||||
- path: pkg/publicip/.*_test.go
|
||||
linters:
|
||||
- dupl
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
|
||||
@@ -15,6 +15,7 @@ WORKDIR /tmp/gobuild
|
||||
# Copy repository code and install Go dependencies
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY pkg/ ./pkg/
|
||||
COPY cmd/ ./cmd/
|
||||
COPY internal/ ./internal/
|
||||
|
||||
@@ -78,9 +79,9 @@ ENV \
|
||||
CONFIG= \
|
||||
PERIOD=5m \
|
||||
UPDATE_COOLDOWN_PERIOD=5m \
|
||||
IP_METHOD=cycle \
|
||||
IPV4_METHOD=cycle \
|
||||
IPV6_METHOD=cycle \
|
||||
IP_METHOD=all \
|
||||
IPV4_METHOD=all \
|
||||
IPV6_METHOD=all \
|
||||
HTTP_TIMEOUT=10s \
|
||||
|
||||
# Web UI
|
||||
|
||||
18
README.md
18
README.md
@@ -162,9 +162,9 @@ Note that:
|
||||
| --- | --- | --- |
|
||||
| `CONFIG` | | One line JSON object containing the entire config (takes precendence over config.json file) if specified |
|
||||
| `PERIOD` | `5m` | Default period of IP address check, following [this format](https://golang.org/pkg/time/#ParseDuration) |
|
||||
| `IP_METHOD` | `cycle` | Method to obtain the public IP address (ipv4 or ipv6). See the [IP Methods section](#IP-methods) |
|
||||
| `IPV4_METHOD` | `cycle` | Method to obtain the public IPv4 address only. See the [IP Methods section](#IP-methods) |
|
||||
| `IPV6_METHOD` | `cycle` | Method to obtain the public IPv6 address only. See the [IP Methods section](#IP-methods) |
|
||||
| `IP_METHOD` | `all` | Comma separated methods to obtain the public IP address (ipv4 or ipv6). See the [IP Methods section](#IP-methods) |
|
||||
| `IPV4_METHOD` | `all` | Comma separated methods to obtain the public IPv4 address only. See the [IP Methods section](#IP-methods) |
|
||||
| `IPV6_METHOD` | `all` | Comma separated methods to obtain the public IPv6 address only. See the [IP Methods section](#IP-methods) |
|
||||
| `UPDATE_COOLDOWN_PERIOD` | `5m` | Duration to cooldown between updates for each record. This is useful to avoid being rate limited or banned. |
|
||||
| `HTTP_TIMEOUT` | `10s` | Timeout for all HTTP requests |
|
||||
| `LISTENING_PORT` | `8000` | Internal TCP listening port for the web UI |
|
||||
@@ -179,7 +179,7 @@ Note that:
|
||||
|
||||
#### IP methods
|
||||
|
||||
By default, all ip methods are cycled through between all ip methods available for the specified ip version, if any. This allows you not to be blocked for making too many requests. You can otherwise pick one of the following.
|
||||
By default, all ip methods are specified. The program will cycle between each. This allows you not to be blocked for making too many requests. You can otherwise pick one or more of the following, for each ip version:
|
||||
|
||||
- IPv4 or IPv6 (for most cases)
|
||||
- `opendns` using [https://diagnostic.opendns.com/myip](https://diagnostic.opendns.com/myip)
|
||||
@@ -190,14 +190,12 @@ By default, all ip methods are cycled through between all ip methods available f
|
||||
- `google` using [https://domains.google.com/checkip](https://domains.google.com/checkip)
|
||||
- IPv4 only (useful for updating both ipv4 and ipv6)
|
||||
- `ipify` using [https://api.ipify.org](https://api.ipify.org)
|
||||
- `noip4` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
|
||||
- `noip8245_4` using [http://ip1.dynupdate.no-ip.com:8245](http://ip1.dynupdate.no-ip.com:8245)
|
||||
- `noip` using [http://ip1.dynupdate.no-ip.com](http://ip1.dynupdate.no-ip.com)
|
||||
- IPv6 only
|
||||
- `ipify6` using [https://api6.ipify.org](https://api6.ipify.org)
|
||||
- `noip6` using [http://ip1.dynupdate6.no-ip.com](http://ip1.dynupdate6.no-ip.com)
|
||||
- `noip8245_6` using [http://ip1.dynupdate6.no-ip.com:8245](http://ip1.dynupdate6.no-ip.com:8245)
|
||||
- `ipify` using [https://api6.ipify.org](https://api6.ipify.org)
|
||||
- `noip` using [http://ip1.dynupdate6.no-ip.com](http://ip1.dynupdate6.no-ip.com)
|
||||
|
||||
You can also specify an HTTPS URL to obtain your public IP address (i.e. `-e IPV6_METHOD=https://ipinfo.io/ip`)
|
||||
You can also specify one or more HTTPS URL to obtain your public IP address (i.e. `-e IPV6_METHOD=https://ipinfo.io/ip`).
|
||||
|
||||
### Host firewall
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/server"
|
||||
"github.com/qdm12/ddns-updater/internal/splash"
|
||||
"github.com/qdm12/ddns-updater/internal/update"
|
||||
pubiphttp "github.com/qdm12/ddns-updater/pkg/publicip/http"
|
||||
"github.com/qdm12/golibs/admin"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/network/connectivity"
|
||||
@@ -44,9 +45,7 @@ func main() {
|
||||
type allParams struct {
|
||||
period time.Duration
|
||||
cooldown time.Duration
|
||||
ipMethod models.IPMethod
|
||||
ipv4Method models.IPMethod
|
||||
ipv6Method models.IPMethod
|
||||
httpIPOptions []pubiphttp.Option
|
||||
dir string
|
||||
dataDir string
|
||||
listeningPort uint16
|
||||
@@ -148,8 +147,13 @@ func _main(ctx context.Context, timeNow func() time.Time) int {
|
||||
wg := &sync.WaitGroup{}
|
||||
defer wg.Wait()
|
||||
|
||||
ipGetter, err := pubiphttp.New(client, p.httpIPOptions...)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return 1
|
||||
}
|
||||
|
||||
updater := update.NewUpdater(db, client, notify, logger)
|
||||
ipGetter := update.NewIPGetter(client, p.ipMethod, p.ipv4Method, p.ipv6Method)
|
||||
runner := update.NewRunner(db, updater, ipGetter, p.cooldown, logger, timeNow)
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
@@ -230,18 +234,25 @@ func getParams(paramsReader params.Reader, logger logging.Logger) (p allParams,
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.ipMethod, err = paramsReader.IPMethod()
|
||||
|
||||
httpIPProviders, err := paramsReader.IPMethod()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.ipv4Method, err = paramsReader.IPv4Method()
|
||||
httpIP4Providers, err := paramsReader.IPv4Method()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.ipv6Method, err = paramsReader.IPv6Method()
|
||||
httpIP6Providers, err := paramsReader.IPv6Method()
|
||||
if err != nil {
|
||||
return p, err
|
||||
}
|
||||
p.httpIPOptions = []pubiphttp.Option{
|
||||
pubiphttp.SetProvidersIP(httpIPProviders[0], httpIPProviders[1:]...),
|
||||
pubiphttp.SetProvidersIP4(httpIP4Providers[0], httpIP4Providers[1:]...),
|
||||
pubiphttp.SetProvidersIP6(httpIP6Providers[0], httpIP6Providers[1:]...),
|
||||
}
|
||||
|
||||
p.dir, err = paramsReader.ExeDir()
|
||||
if err != nil {
|
||||
return p, err
|
||||
|
||||
@@ -12,9 +12,9 @@ services:
|
||||
- CONFIG=
|
||||
- PERIOD=5m
|
||||
- UPDATE_COOLDOWN_PERIOD=5m
|
||||
- IP_METHOD=cycle
|
||||
- IPV4_METHOD=cycle
|
||||
- IPV6_METHOD=cycle
|
||||
- IP_METHOD=all
|
||||
- IPV4_METHOD=all
|
||||
- IPV6_METHOD=all
|
||||
- HTTP_TIMEOUT=10s
|
||||
|
||||
# Web UI
|
||||
|
||||
2
go.mod
2
go.mod
@@ -4,7 +4,9 @@ go 1.16
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi v1.5.1
|
||||
github.com/golang/mock v1.4.3
|
||||
github.com/kyokomi/emoji v2.2.4+incompatible
|
||||
github.com/miekg/dns v1.1.40
|
||||
github.com/ovh/go-ovh v1.1.0
|
||||
github.com/qdm12/golibs v0.0.0-20210215133151-c711ebd3e56a
|
||||
github.com/stretchr/testify v1.6.1
|
||||
|
||||
21
go.sum
21
go.sum
@@ -7,6 +7,7 @@ github.com/asaskevich/govalidator v0.0.0-20180720115003-f9ffefc3facf/go.mod h1:l
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk=
|
||||
github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s=
|
||||
github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU=
|
||||
@@ -39,6 +40,7 @@ github.com/go-openapi/validate v0.17.0 h1:pqoViQz3YLOGIhAmD0N4Lt6pa/3Gnj3ymKqQwq
|
||||
github.com/go-openapi/validate v0.17.0/go.mod h1:Uh4HdOzKt19xGIGm1qHf/ofbX1YQ4Y+MYsct2VUrAJ4=
|
||||
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible h1:K/R+8tc58AaqLkqG2Ol3Qk+DR/TlNuhuh457pBFPtt0=
|
||||
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
|
||||
github.com/google/uuid v1.0.0 h1:b4Gk+7WdP/d3HZH8EJsZpvV7EtDOgaZLtnaNGIu1adA=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
@@ -57,6 +59,8 @@ github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVc
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM=
|
||||
github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE=
|
||||
github.com/miekg/dns v1.1.40 h1:pyyPFfGMnciYUk/mXpKkVmeMQjfXqt3FAJ2hy7tPiLA=
|
||||
github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
||||
github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mr-tron/base58 v1.1.3 h1:v+sk57XuaCKGXpWtVBX8YJzO7hMGx4Aajh4TQbdEFdc=
|
||||
@@ -75,21 +79,30 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykE
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIKYqbNC9s=
|
||||
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad h1:Jh8cai0fqIK+f6nG0UgPW5wFk8wmiMhM3AyciDBdtQg=
|
||||
golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478 h1:l5EDrHhldLYb3ZRHDUhXF7Om7MvYXnkV9/iQNo1lX6g=
|
||||
golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -97,6 +110,10 @@ golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425 h1:VvQyQJN0tSuecqgcIxMWnnfG5kSmgy9KZR9sW3W5QeA=
|
||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.57.0 h1:9unxIsFcTt4I55uWluz+UmL95q4kdJ0buvQ1ZIqVQww=
|
||||
@@ -106,5 +123,7 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
rsc.io/quote/v3 v3.1.0 h1:9JKUTTIUgS6kzR9mK1YuGKv6Nl+DijDNIc0ghT58FaY=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
package constants
|
||||
|
||||
import "github.com/qdm12/ddns-updater/internal/models"
|
||||
|
||||
const (
|
||||
IPv4 models.IPVersion = "ipv4"
|
||||
IPv6 models.IPVersion = "ipv6"
|
||||
IPv4OrIPv6 models.IPVersion = "ipv4 or ipv6"
|
||||
)
|
||||
@@ -1,67 +0,0 @@
|
||||
package constants
|
||||
|
||||
import (
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
func IPMethods() []models.IPMethod {
|
||||
return []models.IPMethod{
|
||||
{
|
||||
Name: "cycle",
|
||||
},
|
||||
{
|
||||
Name: "opendns",
|
||||
URL: "https://diagnostic.opendns.com/myip",
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "ifconfig",
|
||||
URL: "https://ifconfig.io/ip",
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "ipinfo",
|
||||
URL: "https://ipinfo.io/ip",
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "ipify",
|
||||
URL: "https://api.ipify.org",
|
||||
IPv4: true,
|
||||
},
|
||||
{
|
||||
Name: "ipify6",
|
||||
URL: "https://api6.ipify.org",
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "google",
|
||||
URL: "https://domains.google.com/checkip",
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "noip4",
|
||||
URL: "http://ip1.dynupdate.no-ip.com",
|
||||
IPv4: true,
|
||||
},
|
||||
{
|
||||
Name: "noip6",
|
||||
URL: "http://ip1.dynupdate6.no-ip.com",
|
||||
IPv6: true,
|
||||
},
|
||||
{
|
||||
Name: "noip8245_4",
|
||||
URL: "http://ip1.dynupdate.no-ip.com:8245",
|
||||
IPv4: true,
|
||||
},
|
||||
{
|
||||
Name: "noip8245_6",
|
||||
URL: "http://ip1.dynupdate6.no-ip.com:8245",
|
||||
IPv6: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,4 @@ type (
|
||||
Status string
|
||||
// HTML is for constants HTML strings.
|
||||
HTML string
|
||||
// IPVersion is ipv4 or ipv6.
|
||||
IPVersion string
|
||||
)
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
// GetPublicIP downloads a webpage and extracts the IP address from it.
|
||||
func GetPublicIP(ctx context.Context, client *http.Client, url string,
|
||||
ipVersion models.IPVersion) (ip net.IP, err error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get public %s address: %w", ipVersion, err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("cannot get public %s address from %s: HTTP status code %d",
|
||||
ipVersion, url, response.StatusCode)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s := string(b)
|
||||
|
||||
switch ipVersion {
|
||||
case constants.IPv4:
|
||||
return searchIP(constants.IPv4, s)
|
||||
case constants.IPv6:
|
||||
return searchIP(constants.IPv6, s)
|
||||
case constants.IPv4OrIPv6:
|
||||
var ipv4Err, ipv6Err error
|
||||
ip, ipv4Err = searchIP(constants.IPv4, s)
|
||||
if ipv4Err != nil {
|
||||
ip, ipv6Err = searchIP(constants.IPv6, s)
|
||||
}
|
||||
if ipv6Err != nil {
|
||||
return nil, fmt.Errorf("%s, %s", ipv4Err, ipv6Err)
|
||||
}
|
||||
return ip, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("ip version %q not supported", ipVersion)
|
||||
}
|
||||
}
|
||||
|
||||
func searchIP(version models.IPVersion, s string) (ip net.IP, err error) {
|
||||
verifier := verification.NewVerifier()
|
||||
var regexSearch func(s string) []string
|
||||
switch version {
|
||||
case constants.IPv4:
|
||||
regexSearch = verifier.SearchIPv4
|
||||
case constants.IPv6:
|
||||
regexSearch = verifier.SearchIPv6
|
||||
default:
|
||||
return nil, fmt.Errorf("ip version %q is not supported for regex search", version)
|
||||
}
|
||||
ips := regexSearch(s)
|
||||
if ips == nil {
|
||||
return nil, fmt.Errorf("no public %s address found", version)
|
||||
}
|
||||
uniqueIPs := make(map[string]struct{})
|
||||
for _, ipString := range ips {
|
||||
uniqueIPs[ipString] = struct{}{}
|
||||
}
|
||||
netIPs := []net.IP{}
|
||||
for ipString := range uniqueIPs {
|
||||
netIP := net.ParseIP(ipString)
|
||||
if netIP == nil || netIPIsPrivate(netIP) {
|
||||
// in case the regex is not restrictive enough
|
||||
// or the IP address is private
|
||||
continue
|
||||
}
|
||||
netIPs = append(netIPs, netIP)
|
||||
}
|
||||
switch len(netIPs) {
|
||||
case 0:
|
||||
return nil, fmt.Errorf("no public %s address found", version)
|
||||
case 1:
|
||||
return netIPs[0], nil
|
||||
default:
|
||||
sort.Slice(netIPs, func(i, j int) bool {
|
||||
return bytes.Compare(netIPs[i], netIPs[j]) < 0
|
||||
})
|
||||
ips = make([]string, len(netIPs))
|
||||
for i := range netIPs {
|
||||
ips[i] = netIPs[i].String()
|
||||
}
|
||||
return nil, fmt.Errorf("multiple public %s addresses found: %s", version, strings.Join(ips, " "))
|
||||
}
|
||||
}
|
||||
|
||||
func netIPIsPrivate(netIP net.IP) bool {
|
||||
for _, privateCIDRBlock := range [8]string{
|
||||
"127.0.0.1/8", // localhost
|
||||
"10.0.0.0/8", // 24-bit block
|
||||
"172.16.0.0/12", // 20-bit block
|
||||
"192.168.0.0/16", // 16-bit block
|
||||
"169.254.0.0/16", // link local address
|
||||
"::1/128", // localhost IPv6
|
||||
"fc00::/7", // unique local address IPv6
|
||||
"fe80::/10", // link local address IPv6
|
||||
} {
|
||||
_, CIDR, _ := net.ParseCIDR(privateCIDRBlock)
|
||||
if CIDR.Contains(netIP) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,167 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_GetPublicIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
IPVersion models.IPVersion
|
||||
mockContent []byte
|
||||
mockStatus int
|
||||
mockErr error
|
||||
ip net.IP
|
||||
err error
|
||||
}{
|
||||
"network error": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockErr: fmt.Errorf("error"),
|
||||
err: fmt.Errorf(`cannot get public ipv4 address: Get "https://getmyip.com": error`),
|
||||
},
|
||||
"bad status": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockStatus: http.StatusUnauthorized,
|
||||
err: fmt.Errorf("cannot get public ipv4 address from https://getmyip.com: HTTP status code 401"),
|
||||
},
|
||||
"ipv4 address": {
|
||||
IPVersion: constants.IPv4,
|
||||
mockContent: []byte("55.55.55.55"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{55, 55, 55, 55},
|
||||
},
|
||||
"ipv6 address": {
|
||||
IPVersion: constants.IPv6,
|
||||
mockContent: []byte("ad07:e846:51ac:6cd0:0000:0000:0000:0000"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
"ipv4 or ipv6 found ipv4": {
|
||||
IPVersion: constants.IPv4OrIPv6,
|
||||
mockContent: []byte("55.55.55.55"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{55, 55, 55, 55},
|
||||
},
|
||||
"ipv4 or ipv6 found ipv6": {
|
||||
IPVersion: constants.IPv4OrIPv6,
|
||||
mockContent: []byte("ad07:e846:51ac:6cd0:0000:0000:0000:0000"),
|
||||
mockStatus: http.StatusOK,
|
||||
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
"ipv4 or ipv6 not found": {
|
||||
IPVersion: constants.IPv4OrIPv6,
|
||||
mockContent: []byte("abc"),
|
||||
mockStatus: http.StatusOK,
|
||||
err: fmt.Errorf("no public ipv4 address found, no public ipv6 address found"),
|
||||
},
|
||||
"unsupported ip version": {
|
||||
IPVersion: models.IPVersion("x"),
|
||||
mockStatus: http.StatusOK,
|
||||
err: fmt.Errorf("ip version \"x\" not supported"),
|
||||
},
|
||||
}
|
||||
const URL = "https://getmyip.com"
|
||||
ctx := context.Background()
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, URL, r.URL.String())
|
||||
if tc.mockErr != nil {
|
||||
return nil, tc.mockErr
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: tc.mockStatus,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(tc.mockContent)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
ip, err := GetPublicIP(ctx, client, URL, tc.IPVersion)
|
||||
if tc.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tc.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_searchIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
IPVersion models.IPVersion
|
||||
s string
|
||||
ip net.IP
|
||||
err error
|
||||
}{
|
||||
"unsupported ip version": {
|
||||
IPVersion: constants.IPv4OrIPv6,
|
||||
err: fmt.Errorf("ip version \"ipv4 or ipv6\" is not supported for regex search"),
|
||||
},
|
||||
"no content": {
|
||||
IPVersion: constants.IPv4,
|
||||
err: fmt.Errorf("no public ipv4 address found"),
|
||||
},
|
||||
"single ipv4 address": {
|
||||
IPVersion: constants.IPv4,
|
||||
s: "abcd 55.55.55.55 abcd",
|
||||
ip: net.IP{55, 55, 55, 55},
|
||||
},
|
||||
"single ipv6 address": {
|
||||
IPVersion: constants.IPv6,
|
||||
s: "abcd bd07:e846:51ac:6cd0:0000:0000:0000:0000 abcd",
|
||||
ip: net.IP{0xbd, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
"single private ipv4 address": {
|
||||
IPVersion: constants.IPv4,
|
||||
s: "abcd 10.0.0.3 abcd",
|
||||
err: fmt.Errorf("no public ipv4 address found"),
|
||||
},
|
||||
"single private ipv6 address": {
|
||||
IPVersion: constants.IPv6,
|
||||
s: "abcd ::1 abcd",
|
||||
err: fmt.Errorf("no public ipv6 address found"),
|
||||
},
|
||||
"2 ipv4 addresses": {
|
||||
IPVersion: constants.IPv4,
|
||||
s: "55.55.55.55 56.56.56.56",
|
||||
err: fmt.Errorf("multiple public ipv4 addresses found: 55.55.55.55 56.56.56.56"),
|
||||
},
|
||||
"2 ipv6 addresses": {
|
||||
IPVersion: constants.IPv6,
|
||||
s: "bd07:e846:51ac:6cd0:0000:0000:0000:0000 ad07:e846:51ac:6cd0:0000:0000:0000:0000",
|
||||
err: fmt.Errorf("multiple public ipv6 addresses found: ad07:e846:51ac:6cd0:: bd07:e846:51ac:6cd0::"), //nolint:golint,lll
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ip, err := searchIP(tc.IPVersion, tc.s)
|
||||
if tc.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tc.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type roundTripFunc func(r *http.Request) (*http.Response, error)
|
||||
|
||||
func (s roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
|
||||
return s(r)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/internal/settings"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type commonSettings struct {
|
||||
@@ -102,13 +103,15 @@ func makeSettingsFromObject(common commonSettings, rawSettings json.RawMessage,
|
||||
}
|
||||
}
|
||||
hosts := strings.Split(common.Host, ",")
|
||||
ipVersion := models.IPVersion(common.IPVersion)
|
||||
if len(ipVersion) == 0 {
|
||||
ipVersion = constants.IPv4OrIPv6 // default
|
||||
|
||||
if len(common.IPVersion) == 0 {
|
||||
common.IPVersion = ipversion.IP4or6.String()
|
||||
}
|
||||
if ipVersion != constants.IPv4OrIPv6 && ipVersion != constants.IPv4 && ipVersion != constants.IPv6 {
|
||||
return nil, warnings, fmt.Errorf("ip version %q is not valid", ipVersion)
|
||||
ipVersion, err := ipversion.Parse(common.IPVersion)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
var settingsConstructor settings.Constructor
|
||||
switch provider {
|
||||
case constants.CLOUDFLARE:
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/settings"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/http"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
"github.com/qdm12/golibs/params"
|
||||
)
|
||||
|
||||
const https = "https"
|
||||
|
||||
type Reader interface {
|
||||
// JSON
|
||||
JSONSettings(filePath string) (allSettings []settings.Settings, warnings []string, err error)
|
||||
|
||||
// Core
|
||||
Period() (period time.Duration, warnings []string, err error)
|
||||
IPMethod() (method models.IPMethod, err error)
|
||||
IPv4Method() (method models.IPMethod, err error)
|
||||
IPv6Method() (method models.IPMethod, err error)
|
||||
IPMethod() (providers []http.Provider, err error)
|
||||
IPv4Method() (providers []http.Provider, err error)
|
||||
IPv6Method() (providers []http.Provider, err error)
|
||||
HTTPTimeout() (duration time.Duration, err error)
|
||||
CooldownPeriod() (duration time.Duration, err error)
|
||||
|
||||
@@ -119,74 +119,75 @@ func (r *reader) Period() (period time.Duration, warnings []string, err error) {
|
||||
return period, nil, err
|
||||
}
|
||||
|
||||
func (r *reader) IPMethod() (method models.IPMethod, err error) {
|
||||
s, err := r.env.Get("IP_METHOD", params.Default("cycle"))
|
||||
if err != nil {
|
||||
return method, err
|
||||
}
|
||||
for _, choice := range constants.IPMethods() {
|
||||
if choice.Name == s {
|
||||
return choice, nil
|
||||
}
|
||||
}
|
||||
url, err := url.Parse(s)
|
||||
if err != nil || url == nil || url.Scheme != https {
|
||||
return method, fmt.Errorf("ip method %q is not valid", s)
|
||||
}
|
||||
return models.IPMethod{
|
||||
Name: s,
|
||||
URL: s,
|
||||
IPv4: true,
|
||||
IPv6: true,
|
||||
}, nil
|
||||
var (
|
||||
ErrIPMethodInvalid = errors.New("ip method is not valid")
|
||||
ErrIPMethodVersion = errors.New("ip method not valid for IP version")
|
||||
)
|
||||
|
||||
// IPMethod obtains the HTTP method for IP v4 or v6 to obtain your public IP address.
|
||||
func (r *reader) IPMethod() (providers []http.Provider, err error) {
|
||||
return r.httpIPMethod("IP_METHOD", ipversion.IP4or6)
|
||||
}
|
||||
|
||||
func (r *reader) IPv4Method() (method models.IPMethod, err error) {
|
||||
s, err := r.env.Get("IPV4_METHOD", params.Default("cycle"))
|
||||
if err != nil {
|
||||
return method, err
|
||||
}
|
||||
for _, choice := range constants.IPMethods() {
|
||||
if choice.Name == s {
|
||||
if s != "cycle" && !choice.IPv4 {
|
||||
return method, fmt.Errorf("ip method %s does not support IPv4", s)
|
||||
}
|
||||
return choice, nil
|
||||
}
|
||||
}
|
||||
url, err := url.Parse(s)
|
||||
if err != nil || url == nil || url.Scheme != https {
|
||||
return method, fmt.Errorf("ipv4 method %q is not valid", s)
|
||||
}
|
||||
return models.IPMethod{
|
||||
Name: s,
|
||||
URL: s,
|
||||
IPv4: true,
|
||||
}, nil
|
||||
// IPMethod obtains the HTTP method for IP v4 to obtain your public IP address.
|
||||
func (r *reader) IPv4Method() (providers []http.Provider, err error) {
|
||||
return r.httpIPMethod("IPV4_METHOD", ipversion.IP4)
|
||||
}
|
||||
|
||||
func (r *reader) IPv6Method() (method models.IPMethod, err error) {
|
||||
s, err := r.env.Get("IPV6_METHOD", params.Default("cycle"))
|
||||
// IPMethod obtains the HTTP method for IP v6 to obtain your public IP address.
|
||||
func (r *reader) IPv6Method() (providers []http.Provider, err error) {
|
||||
return r.httpIPMethod("IPV6_METHOD", ipversion.IP6)
|
||||
}
|
||||
|
||||
func (r *reader) httpIPMethod(envKey string, version ipversion.IPVersion) (
|
||||
providers []http.Provider, err error) {
|
||||
s, err := r.env.Get(envKey, params.Default("cycle"))
|
||||
if err != nil {
|
||||
return method, err
|
||||
return nil, err
|
||||
}
|
||||
for _, choice := range constants.IPMethods() {
|
||||
if choice.Name == s {
|
||||
if s != "cycle" && !choice.IPv6 {
|
||||
return method, fmt.Errorf("ip method %s does not support IPv6", s)
|
||||
}
|
||||
return choice, nil
|
||||
|
||||
availableProviders := http.ListProvidersForVersion(version)
|
||||
choices := make(map[http.Provider]struct{}, len(availableProviders))
|
||||
for _, provider := range availableProviders {
|
||||
choices[provider] = struct{}{}
|
||||
}
|
||||
|
||||
fields := strings.Split(s, ",")
|
||||
|
||||
for _, field := range fields {
|
||||
// Retro-compatibility.
|
||||
switch field {
|
||||
case "ipify6":
|
||||
field = "ipify"
|
||||
case "noip4", "noip6", "noip8245_4", "noip8245_6":
|
||||
field = "noip"
|
||||
case "cycle":
|
||||
field = "all"
|
||||
}
|
||||
|
||||
if field == "all" {
|
||||
return availableProviders, nil
|
||||
}
|
||||
|
||||
// Custom URL check
|
||||
url, err := url.Parse(field)
|
||||
if err == nil && url != nil && url.Scheme == "https" {
|
||||
providers = append(providers, http.CustomProvider(url))
|
||||
continue
|
||||
}
|
||||
|
||||
provider := http.Provider(field)
|
||||
if _, ok := choices[provider]; !ok {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPMethodInvalid, provider)
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
url, err := url.Parse(s)
|
||||
if err != nil || url == nil || url.Scheme != https {
|
||||
return method, fmt.Errorf("ipv6 method %q is not valid", s)
|
||||
|
||||
if len(providers) == 0 {
|
||||
return nil, fmt.Errorf("%w: %s", ErrIPMethodVersion, version)
|
||||
}
|
||||
return models.IPMethod{
|
||||
Name: s,
|
||||
URL: s,
|
||||
IPv6: true,
|
||||
}, nil
|
||||
|
||||
return providers, nil
|
||||
}
|
||||
|
||||
func (r *reader) ExeDir() (dir string, err error) {
|
||||
|
||||
@@ -13,13 +13,14 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type cloudflare struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
key string
|
||||
token string
|
||||
email string
|
||||
@@ -30,7 +31,7 @@ type cloudflare struct {
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewCloudflare(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewCloudflare(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Key string `json:"key"`
|
||||
@@ -99,7 +100,7 @@ func (c *cloudflare) Host() string {
|
||||
return c.host
|
||||
}
|
||||
|
||||
func (c *cloudflare) IPVersion() models.IPVersion {
|
||||
func (c *cloudflare) IPVersion() ipversion.IPVersion {
|
||||
return c.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,18 +13,19 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type ddnss struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewDdnss(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewDdnss(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
@@ -72,7 +73,7 @@ func (d *ddnss) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *ddnss) IPVersion() models.IPVersion {
|
||||
func (d *ddnss) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,16 +12,17 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type digitalOcean struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
token string
|
||||
}
|
||||
|
||||
func NewDigitalOcean(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewDigitalOcean(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
@@ -60,7 +61,7 @@ func (d *digitalOcean) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *digitalOcean) IPVersion() models.IPVersion {
|
||||
func (d *digitalOcean) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,20 +13,21 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type dnsomatic struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewDNSOMatic(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewDNSOMatic(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
@@ -77,7 +78,7 @@ func (d *dnsomatic) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dnsomatic) IPVersion() models.IPVersion {
|
||||
func (d *dnsomatic) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,16 +12,17 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type dnspod struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
token string
|
||||
}
|
||||
|
||||
func NewDNSPod(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewDNSPod(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
@@ -60,7 +61,7 @@ func (d *dnspod) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dnspod) IPVersion() models.IPVersion {
|
||||
func (d *dnspod) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,18 +12,19 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type donDominio struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
name string
|
||||
}
|
||||
|
||||
func NewDonDominio(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewDonDominio(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
@@ -76,7 +77,7 @@ func (d *donDominio) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *donDominio) IPVersion() models.IPVersion {
|
||||
func (d *donDominio) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,17 +13,18 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type dreamhost struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
key string
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewDreamhost(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewDreamhost(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Key string `json:"key"`
|
||||
@@ -69,7 +70,7 @@ func (d *dreamhost) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dreamhost) IPVersion() models.IPVersion {
|
||||
func (d *dreamhost) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,18 +12,19 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type duckdns struct {
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
token string
|
||||
useProviderIP bool
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewDuckdns(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewDuckdns(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
@@ -68,7 +69,7 @@ func (d *duckdns) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *duckdns) IPVersion() models.IPVersion {
|
||||
func (d *duckdns) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,18 +12,19 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type dyn struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewDyn(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewDyn(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
@@ -71,7 +72,7 @@ func (d *dyn) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dyn) IPVersion() models.IPVersion {
|
||||
func (d *dyn) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -10,17 +10,18 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type dynV6 struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
token string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewDynV6(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewDynV6(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
@@ -64,7 +65,7 @@ func (d *dynV6) Host() string {
|
||||
return d.host
|
||||
}
|
||||
|
||||
func (d *dynV6) IPVersion() models.IPVersion {
|
||||
func (d *dynV6) IPVersion() ipversion.IPVersion {
|
||||
return d.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,16 +13,17 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type freedns struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
token string
|
||||
}
|
||||
|
||||
func NewFreedns(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewFreedns(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
@@ -65,7 +66,7 @@ func (f *freedns) Proxied() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (f *freedns) IPVersion() models.IPVersion {
|
||||
func (f *freedns) IPVersion() ipversion.IPVersion {
|
||||
return f.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,17 +12,18 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type gandi struct {
|
||||
domain string
|
||||
host string
|
||||
ttl int
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
key string
|
||||
}
|
||||
|
||||
func NewGandi(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewGandi(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Key string `json:"key"`
|
||||
@@ -63,7 +64,7 @@ func (g *gandi) Host() string {
|
||||
return g.host
|
||||
}
|
||||
|
||||
func (g *gandi) IPVersion() models.IPVersion {
|
||||
func (g *gandi) IPVersion() ipversion.IPVersion {
|
||||
return g.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,18 +13,19 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type godaddy struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
key string
|
||||
secret string
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewGodaddy(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewGodaddy(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
matcher regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Key string `json:"key"`
|
||||
@@ -69,7 +70,7 @@ func (g *godaddy) Host() string {
|
||||
return g.host
|
||||
}
|
||||
|
||||
func (g *godaddy) IPVersion() models.IPVersion {
|
||||
func (g *godaddy) IPVersion() ipversion.IPVersion {
|
||||
return g.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,19 +13,20 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type google struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewGoogle(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewGoogle(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
@@ -71,7 +72,7 @@ func (g *google) Host() string {
|
||||
return g.host
|
||||
}
|
||||
|
||||
func (g *google) IPVersion() models.IPVersion {
|
||||
func (g *google) IPVersion() ipversion.IPVersion {
|
||||
return g.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,18 +13,19 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type he struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewHe(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewHe(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Password string `json:"password"`
|
||||
@@ -65,7 +66,7 @@ func (h *he) Host() string {
|
||||
return h.host
|
||||
}
|
||||
|
||||
func (h *he) IPVersion() models.IPVersion {
|
||||
func (h *he) IPVersion() ipversion.IPVersion {
|
||||
return h.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,18 +13,19 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type infomaniak struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewInfomaniak(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewInfomaniak(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
@@ -72,7 +73,7 @@ func (i *infomaniak) Host() string {
|
||||
return i.host
|
||||
}
|
||||
|
||||
func (i *infomaniak) IPVersion() models.IPVersion {
|
||||
func (i *infomaniak) IPVersion() ipversion.IPVersion {
|
||||
return i.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -16,16 +16,17 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type linode struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
token string
|
||||
}
|
||||
|
||||
func NewLinode(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewLinode(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Token string `json:"token"`
|
||||
@@ -64,7 +65,7 @@ func (l *linode) Host() string {
|
||||
return l.host
|
||||
}
|
||||
|
||||
func (l *linode) IPVersion() models.IPVersion {
|
||||
func (l *linode) IPVersion() ipversion.IPVersion {
|
||||
return l.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,18 +13,19 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type luaDNS struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
email string
|
||||
token string
|
||||
}
|
||||
|
||||
func NewLuaDNS(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewLuaDNS(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Email string `json:"email"`
|
||||
@@ -68,7 +69,7 @@ func (l *luaDNS) Host() string {
|
||||
return l.host
|
||||
}
|
||||
|
||||
func (l *luaDNS) IPVersion() models.IPVersion {
|
||||
func (l *luaDNS) IPVersion() ipversion.IPVersion {
|
||||
return l.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,20 +12,21 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type namecheap struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
password string
|
||||
useProviderIP bool
|
||||
matcher regex.Matcher
|
||||
}
|
||||
|
||||
func NewNamecheap(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewNamecheap(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
matcher regex.Matcher) (s Settings, err error) {
|
||||
if ipVersion == constants.IPv6 {
|
||||
if ipVersion == ipversion.IP6 {
|
||||
return s, ErrIPv6NotSupported
|
||||
}
|
||||
extraSettings := struct {
|
||||
@@ -68,7 +69,7 @@ func (n *namecheap) Host() string {
|
||||
return n.host
|
||||
}
|
||||
|
||||
func (n *namecheap) IPVersion() models.IPVersion {
|
||||
func (n *namecheap) IPVersion() ipversion.IPVersion {
|
||||
return n.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,19 +13,20 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/verification"
|
||||
)
|
||||
|
||||
type noip struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewNoip(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewNoip(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
@@ -76,7 +77,7 @@ func (n *noip) Host() string {
|
||||
return n.host
|
||||
}
|
||||
|
||||
func (n *noip) IPVersion() models.IPVersion {
|
||||
func (n *noip) IPVersion() ipversion.IPVersion {
|
||||
return n.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,18 +12,19 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type opendns struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewOpendns(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewOpendns(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
@@ -71,7 +72,7 @@ func (o *opendns) Host() string {
|
||||
return o.host
|
||||
}
|
||||
|
||||
func (o *opendns) IPVersion() models.IPVersion {
|
||||
func (o *opendns) IPVersion() ipversion.IPVersion {
|
||||
return o.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -13,12 +13,13 @@ import (
|
||||
ovhClient "github.com/ovh/go-ovh/ovh"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type ovh struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
@@ -29,7 +30,7 @@ type ovh struct {
|
||||
consumerKey string
|
||||
}
|
||||
|
||||
func NewOVH(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewOVH(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
@@ -98,7 +99,7 @@ func (o *ovh) Host() string {
|
||||
return o.host
|
||||
}
|
||||
|
||||
func (o *ovh) IPVersion() models.IPVersion {
|
||||
func (o *ovh) IPVersion() ipversion.IPVersion {
|
||||
return o.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,18 +12,19 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type selfhostde struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
username string
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewSelfhostde(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewSelfhostde(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Username string `json:"username"`
|
||||
@@ -71,7 +72,7 @@ func (sd *selfhostde) Host() string {
|
||||
return sd.host
|
||||
}
|
||||
|
||||
func (sd *selfhostde) IPVersion() models.IPVersion {
|
||||
func (sd *selfhostde) IPVersion() ipversion.IPVersion {
|
||||
return sd.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type Settings interface {
|
||||
@@ -21,11 +22,11 @@ type Settings interface {
|
||||
BuildDomainName() string
|
||||
HTML() models.HTMLRow
|
||||
Proxied() bool
|
||||
IPVersion() models.IPVersion
|
||||
IPVersion() ipversion.IPVersion
|
||||
Update(ctx context.Context, client *http.Client, ip net.IP) (newIP net.IP, err error)
|
||||
}
|
||||
|
||||
type Constructor func(data json.RawMessage, domain string, host string, ipVersion models.IPVersion,
|
||||
type Constructor func(data json.RawMessage, domain string, host string, ipVersion ipversion.IPVersion,
|
||||
matcher regex.Matcher) (s Settings, err error)
|
||||
|
||||
func buildDomainName(host, domain string) string {
|
||||
@@ -39,7 +40,7 @@ func buildDomainName(host, domain string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func toString(domain, host string, provider models.Provider, ipVersion models.IPVersion) string {
|
||||
func toString(domain, host string, provider models.Provider, ipVersion ipversion.IPVersion) string {
|
||||
return fmt.Sprintf("[domain: %s | host: %s | provider: %s | ip: %s]", domain, host, provider, ipVersion)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,17 +12,18 @@ import (
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/regex"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type strato struct {
|
||||
domain string
|
||||
host string
|
||||
ipVersion models.IPVersion
|
||||
ipVersion ipversion.IPVersion
|
||||
password string
|
||||
useProviderIP bool
|
||||
}
|
||||
|
||||
func NewStrato(data json.RawMessage, domain, host string, ipVersion models.IPVersion,
|
||||
func NewStrato(data json.RawMessage, domain, host string, ipVersion ipversion.IPVersion,
|
||||
_ regex.Matcher) (s Settings, err error) {
|
||||
extraSettings := struct {
|
||||
Password string `json:"password"`
|
||||
@@ -66,7 +67,7 @@ func (s *strato) Host() string {
|
||||
return s.host
|
||||
}
|
||||
|
||||
func (s *strato) IPVersion() models.IPVersion {
|
||||
func (s *strato) IPVersion() ipversion.IPVersion {
|
||||
return s.ipVersion
|
||||
}
|
||||
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
)
|
||||
|
||||
type cycler interface {
|
||||
next() models.IPMethod
|
||||
}
|
||||
|
||||
type cyclerImpl struct {
|
||||
sync.Mutex
|
||||
counter int
|
||||
methods []models.IPMethod
|
||||
}
|
||||
|
||||
func newCycler(methods []models.IPMethod) cycler {
|
||||
return &cyclerImpl{
|
||||
methods: methods,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *cyclerImpl) next() models.IPMethod {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
method := c.methods[c.counter]
|
||||
c.counter++
|
||||
if c.counter == len(c.methods) {
|
||||
c.counter = 0
|
||||
}
|
||||
return method
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_newCycler(t *testing.T) {
|
||||
t.Parallel()
|
||||
ipMethods := []models.IPMethod{
|
||||
{Name: "a"}, {Name: "b"},
|
||||
}
|
||||
c := newCycler(ipMethods)
|
||||
require.NotNil(t, c)
|
||||
ipMethod := c.next()
|
||||
assert.Equal(t, ipMethod, models.IPMethod{Name: "a"})
|
||||
}
|
||||
|
||||
func Test_next(t *testing.T) {
|
||||
t.Parallel()
|
||||
c := &cyclerImpl{
|
||||
methods: []models.IPMethod{
|
||||
{Name: "a"}, {Name: "b"},
|
||||
},
|
||||
}
|
||||
var m models.IPMethod
|
||||
m = c.next()
|
||||
assert.Equal(t, m, models.IPMethod{Name: "a"})
|
||||
m = c.next()
|
||||
assert.Equal(t, m, models.IPMethod{Name: "b"})
|
||||
m = c.next()
|
||||
assert.Equal(t, m, models.IPMethod{Name: "a"})
|
||||
}
|
||||
|
||||
func Test_next_RaceCondition(t *testing.T) {
|
||||
// Run with -race flag
|
||||
t.Parallel()
|
||||
const workers = 5
|
||||
const loopSize = 101
|
||||
c := &cyclerImpl{
|
||||
methods: []models.IPMethod{
|
||||
{Name: "a"}, {Name: "b"}, {Name: "c"}, {Name: "d"},
|
||||
},
|
||||
}
|
||||
ready := make(chan struct{})
|
||||
wg := &sync.WaitGroup{}
|
||||
for i := 0; i < workers; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
<-ready
|
||||
for i := 0; i < loopSize; i++ {
|
||||
c.next()
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
close(ready)
|
||||
wg.Wait()
|
||||
assert.Equal(t, (workers*loopSize)%len(c.methods), c.counter)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/constants"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/qdm12/ddns-updater/internal/network"
|
||||
)
|
||||
|
||||
const cycle = "cycle"
|
||||
|
||||
type IPGetter interface {
|
||||
IP(ctx context.Context) (ip net.IP, err error)
|
||||
IPv4(ctx context.Context) (ip net.IP, err error)
|
||||
IPv6(ctx context.Context) (ip net.IP, err error)
|
||||
}
|
||||
|
||||
type ipGetter struct {
|
||||
client *http.Client
|
||||
ipMethod models.IPMethod
|
||||
ipv4Method models.IPMethod
|
||||
ipv6Method models.IPMethod
|
||||
cyclerIP cycler
|
||||
cyclerIPv4 cycler
|
||||
cyclerIPv6 cycler
|
||||
}
|
||||
|
||||
func NewIPGetter(client *http.Client, ipMethod, ipv4Method, ipv6Method models.IPMethod) IPGetter {
|
||||
ipMethods := []models.IPMethod{}
|
||||
ipv4Methods := []models.IPMethod{}
|
||||
ipv6Methods := []models.IPMethod{}
|
||||
for _, method := range constants.IPMethods() {
|
||||
switch {
|
||||
case method.IPv4 && method.IPv6:
|
||||
ipMethods = append(ipMethods, method)
|
||||
case method.IPv4:
|
||||
ipv4Methods = append(ipv4Methods, method)
|
||||
case method.IPv6:
|
||||
ipv6Methods = append(ipv6Methods, method)
|
||||
}
|
||||
}
|
||||
return &ipGetter{
|
||||
client: client,
|
||||
ipMethod: ipMethod,
|
||||
ipv4Method: ipv4Method,
|
||||
ipv6Method: ipv6Method,
|
||||
cyclerIP: newCycler(ipMethods),
|
||||
cyclerIPv4: newCycler(ipv4Methods),
|
||||
cyclerIPv6: newCycler(ipv6Methods),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *ipGetter) IP(ctx context.Context) (ip net.IP, err error) {
|
||||
method := i.ipMethod
|
||||
if method.Name == cycle {
|
||||
method = i.cyclerIP.next()
|
||||
}
|
||||
return network.GetPublicIP(ctx, i.client, method.URL, constants.IPv4OrIPv6)
|
||||
}
|
||||
|
||||
func (i *ipGetter) IPv4(ctx context.Context) (ip net.IP, err error) {
|
||||
method := i.ipv4Method
|
||||
if method.Name == cycle {
|
||||
method = i.cyclerIPv4.next()
|
||||
}
|
||||
return network.GetPublicIP(ctx, i.client, method.URL, constants.IPv4)
|
||||
}
|
||||
|
||||
func (i *ipGetter) IPv6(ctx context.Context) (ip net.IP, err error) {
|
||||
method := i.ipv6Method
|
||||
if method.Name == cycle {
|
||||
method = i.cyclerIPv6.next()
|
||||
}
|
||||
return network.GetPublicIP(ctx, i.client, method.URL, constants.IPv6)
|
||||
}
|
||||
@@ -1,169 +0,0 @@
|
||||
package update
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_NewIPGetter(t *testing.T) {
|
||||
t.Parallel()
|
||||
client := &http.Client{}
|
||||
ipMethod := models.IPMethod{Name: "ip"}
|
||||
ipv4Method := models.IPMethod{Name: "ipv4"}
|
||||
ipv6Method := models.IPMethod{Name: "ipv6"}
|
||||
ipGetter := NewIPGetter(client, ipMethod, ipv4Method, ipv6Method)
|
||||
assert.NotNil(t, ipGetter)
|
||||
}
|
||||
|
||||
func Test_IP(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
ipMethod models.IPMethod
|
||||
mockContent []byte
|
||||
ip net.IP
|
||||
}{
|
||||
"url ipv4": {
|
||||
ipMethod: models.IPMethod{URL: "https://opendns.com/ip"},
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
"url ipv6": {
|
||||
ipMethod: models.IPMethod{URL: "https://opendns.com/ip"},
|
||||
mockContent: []byte("blabla ad07:e846:51ac:6cd0:0000:0000:0000:0000 sds"),
|
||||
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
"cycle": {
|
||||
ipMethod: models.IPMethod{Name: cycle},
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
url := tc.ipMethod.URL
|
||||
if tc.ipMethod.Name == cycle {
|
||||
url = "https://diagnostic.opendns.com/myip"
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, url, r.URL.String())
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(tc.mockContent)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
ig := NewIPGetter(client, tc.ipMethod, models.IPMethod{}, models.IPMethod{})
|
||||
ip, err := ig.IP(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IPv4(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
ipMethod models.IPMethod
|
||||
mockContent []byte
|
||||
ip net.IP
|
||||
}{
|
||||
"url": {
|
||||
ipMethod: models.IPMethod{URL: "https://opendns.com/ip"},
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
"cycle": {
|
||||
ipMethod: models.IPMethod{Name: cycle},
|
||||
mockContent: []byte("blabla 58.67.201.151.25 sds"),
|
||||
ip: net.IP{58, 67, 201, 151},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
url := tc.ipMethod.URL
|
||||
if tc.ipMethod.Name == cycle {
|
||||
url = "https://api.ipify.org"
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, url, r.URL.String())
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(tc.mockContent)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
ig := NewIPGetter(client, models.IPMethod{}, tc.ipMethod, models.IPMethod{})
|
||||
ip, err := ig.IPv4(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_IPv6(t *testing.T) {
|
||||
t.Parallel()
|
||||
tests := map[string]struct {
|
||||
ipMethod models.IPMethod
|
||||
mockContent []byte
|
||||
ip net.IP
|
||||
}{
|
||||
"url": {
|
||||
ipMethod: models.IPMethod{URL: "https://ip6.ddnss.de/meineip.php"},
|
||||
mockContent: []byte("blabla ad07:e846:51ac:6cd0:0000:0000:0000:0000 sds"),
|
||||
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
"cycle": {
|
||||
ipMethod: models.IPMethod{Name: cycle},
|
||||
mockContent: []byte("blabla ad07:e846:51ac:6cd0:0000:0000:0000:0000 sds"),
|
||||
ip: net.IP{0xad, 0x7, 0xe8, 0x46, 0x51, 0xac, 0x6c, 0xd0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0},
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
url := tc.ipMethod.URL
|
||||
if tc.ipMethod.Name == cycle {
|
||||
url = "https://api6.ipify.org"
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, url, r.URL.String())
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(tc.mockContent)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
ig := NewIPGetter(client, models.IPMethod{}, models.IPMethod{}, tc.ipMethod)
|
||||
ip, err := ig.IPv6(ctx)
|
||||
assert.Nil(t, err)
|
||||
assert.True(t, tc.ip.Equal(ip))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"github.com/qdm12/ddns-updater/internal/data"
|
||||
"github.com/qdm12/ddns-updater/internal/models"
|
||||
librecords "github.com/qdm12/ddns-updater/internal/records"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/qdm12/golibs/logging"
|
||||
)
|
||||
|
||||
@@ -24,12 +26,12 @@ type runner struct {
|
||||
forceResult chan []error
|
||||
cooldown time.Duration
|
||||
netLookupIP func(hostname string) ([]net.IP, error)
|
||||
ipGetter IPGetter
|
||||
ipGetter publicip.Fetcher
|
||||
logger logging.Logger
|
||||
timeNow func() time.Time
|
||||
}
|
||||
|
||||
func NewRunner(db data.Database, updater Updater, ipGetter IPGetter,
|
||||
func NewRunner(db data.Database, updater Updater, ipGetter publicip.Fetcher,
|
||||
cooldown time.Duration, logger logging.Logger, timeNow func() time.Time) Runner {
|
||||
return &runner{
|
||||
db: db,
|
||||
@@ -72,11 +74,11 @@ func (r *runner) lookupIPs(hostname string) (ipv4 net.IP, ipv6 net.IP, err error
|
||||
func doIPVersion(records []librecords.Record) (doIP, doIPv4, doIPv6 bool) {
|
||||
for _, record := range records {
|
||||
switch record.Settings.IPVersion() {
|
||||
case constants.IPv4OrIPv6:
|
||||
case ipversion.IP4or6:
|
||||
doIP = true
|
||||
case constants.IPv4:
|
||||
case ipversion.IP4:
|
||||
doIPv4 = true
|
||||
case constants.IPv6:
|
||||
case ipversion.IP6:
|
||||
doIPv6 = true
|
||||
}
|
||||
if doIP && doIPv4 && doIPv6 {
|
||||
@@ -95,13 +97,13 @@ func (r *runner) getNewIPs(ctx context.Context, doIP, doIPv4, doIPv6 bool) (ip,
|
||||
}
|
||||
}
|
||||
if doIPv4 {
|
||||
ipv4, err = r.ipGetter.IPv4(ctx)
|
||||
ipv4, err = r.ipGetter.IP4(ctx)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
}
|
||||
if doIPv6 {
|
||||
ipv6, err = r.ipGetter.IPv6(ctx)
|
||||
ipv6, err = r.ipGetter.IP6(ctx)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
@@ -138,23 +140,23 @@ func (r *runner) shouldUpdateRecord(record librecords.Record, ip, ipv4, ipv6 net
|
||||
return r.shouldUpdateRecordWithLookup(hostname, ipVersion, ip, ipv4, ipv6)
|
||||
}
|
||||
|
||||
func (r *runner) shouldUpdateRecordNoLookup(hostname string, ipVersion models.IPVersion,
|
||||
func (r *runner) shouldUpdateRecordNoLookup(hostname string, ipVersion ipversion.IPVersion,
|
||||
lastIP, ip, ipv4, ipv6 net.IP) (update bool) {
|
||||
switch ipVersion {
|
||||
case constants.IPv4OrIPv6:
|
||||
case ipversion.IP4or6:
|
||||
if ip != nil && !ip.Equal(lastIP) {
|
||||
r.logger.Info("Last IP address stored for %s is %s and your IP address is %s", hostname, lastIP, ip)
|
||||
return true
|
||||
}
|
||||
r.logger.Debug("Last IP address stored for %s is %s and your IP address is %s, skipping update", hostname, lastIP, ip)
|
||||
case constants.IPv4:
|
||||
case ipversion.IP4:
|
||||
if ipv4 != nil && !ipv4.Equal(lastIP) {
|
||||
r.logger.Info("Last IPv4 address stored for %s is %s and your IPv4 address is %s", hostname, lastIP, ip)
|
||||
return true
|
||||
}
|
||||
r.logger.Debug("Last IPv4 address stored for %s is %s and your IP address is %s, skipping update",
|
||||
hostname, lastIP, ip)
|
||||
case constants.IPv6:
|
||||
case ipversion.IP6:
|
||||
if ipv6 != nil && !ipv6.Equal(lastIP) {
|
||||
r.logger.Info("Last IPv6 address stored for %s is %s and your IPv6 address is %s", hostname, lastIP, ip)
|
||||
return true
|
||||
@@ -165,7 +167,7 @@ func (r *runner) shouldUpdateRecordNoLookup(hostname string, ipVersion models.IP
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *runner) shouldUpdateRecordWithLookup(hostname string, ipVersion models.IPVersion,
|
||||
func (r *runner) shouldUpdateRecordWithLookup(hostname string, ipVersion ipversion.IPVersion,
|
||||
ip, ipv4, ipv6 net.IP) (update bool) {
|
||||
const tries = 5
|
||||
recordIPv4, recordIPv6, err := r.lookupIPsResilient(hostname, tries)
|
||||
@@ -173,7 +175,7 @@ func (r *runner) shouldUpdateRecordWithLookup(hostname string, ipVersion models.
|
||||
r.logger.Warn("cannot DNS resolve %s after %d tries: %s", hostname, tries, err) // update anyway
|
||||
}
|
||||
switch ipVersion {
|
||||
case constants.IPv4OrIPv6:
|
||||
case ipversion.IP4or6:
|
||||
recordIP := recordIPv4
|
||||
if ip.To4() == nil {
|
||||
recordIP = recordIPv6
|
||||
@@ -183,13 +185,13 @@ func (r *runner) shouldUpdateRecordWithLookup(hostname string, ipVersion models.
|
||||
return true
|
||||
}
|
||||
r.logger.Debug("IP address of %s is %s and your IP address is %s, skipping update", hostname, recordIP, ip)
|
||||
case constants.IPv4:
|
||||
case ipversion.IP4:
|
||||
if ipv4 != nil && !ipv4.Equal(recordIPv4) {
|
||||
r.logger.Info("IPv4 address of %s is %s and your IPv4 address is %s", hostname, recordIPv4, ipv4)
|
||||
return true
|
||||
}
|
||||
r.logger.Debug("IPv4 address of %s is %s and your IPv4 address is %s, skipping update", hostname, recordIPv4, ipv4)
|
||||
case constants.IPv6:
|
||||
case ipversion.IP6:
|
||||
if ipv6 != nil && !ipv6.Equal(recordIPv6) {
|
||||
r.logger.Info("IPv6 address of %s is %s and your IPv6 address is %s", hostname, recordIPv6, ipv6)
|
||||
return true
|
||||
@@ -199,13 +201,13 @@ func (r *runner) shouldUpdateRecordWithLookup(hostname string, ipVersion models.
|
||||
return false
|
||||
}
|
||||
|
||||
func getIPMatchingVersion(ip, ipv4, ipv6 net.IP, ipVersion models.IPVersion) net.IP {
|
||||
func getIPMatchingVersion(ip, ipv4, ipv6 net.IP, ipVersion ipversion.IPVersion) net.IP {
|
||||
switch ipVersion {
|
||||
case constants.IPv4OrIPv6:
|
||||
case ipversion.IP4or6:
|
||||
return ip
|
||||
case constants.IPv4:
|
||||
case ipversion.IP4:
|
||||
return ipv4
|
||||
case constants.IPv6:
|
||||
case ipversion.IP6:
|
||||
return ipv6
|
||||
}
|
||||
return nil
|
||||
|
||||
16
pkg/publicip/dns/client.go
Normal file
16
pkg/publicip/dns/client.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
//go:generate mockgen -destination=mock_$GOPACKAGE/$GOFILE . Client
|
||||
|
||||
// Client is an interface for the DNS client used in the implementation in this package.
|
||||
// You SHOULD NOT use this interface anywhere as it is implementation specific.
|
||||
type Client interface {
|
||||
ExchangeContext(ctx context.Context, m *dns.Msg, a string) (r *dns.Msg, rtt time.Duration, err error)
|
||||
}
|
||||
59
pkg/publicip/dns/dns.go
Normal file
59
pkg/publicip/dns/dns.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type Fetcher interface {
|
||||
IP(ctx context.Context) (publicIP net.IP, err error)
|
||||
IP4(ctx context.Context) (publicIP net.IP, err error)
|
||||
IP6(ctx context.Context) (publicIP net.IP, err error)
|
||||
}
|
||||
|
||||
type fetcher struct {
|
||||
ring ring
|
||||
client Client
|
||||
client4 Client
|
||||
client6 Client
|
||||
}
|
||||
|
||||
type ring struct {
|
||||
// counter is used to get an index in the providers slice
|
||||
counter *uint32 // uint32 for 32 bit systems atomic operations
|
||||
providers []Provider
|
||||
}
|
||||
|
||||
func New(options ...Option) (f Fetcher, err error) {
|
||||
settings := newDefaultSettings()
|
||||
for _, option := range options {
|
||||
if err := option(&settings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
dialer := &net.Dialer{
|
||||
Timeout: settings.timeout,
|
||||
}
|
||||
|
||||
return &fetcher{
|
||||
ring: ring{
|
||||
counter: new(uint32),
|
||||
providers: settings.providers,
|
||||
},
|
||||
client: &dns.Client{
|
||||
Net: "udp",
|
||||
Dialer: dialer,
|
||||
},
|
||||
client4: &dns.Client{
|
||||
Net: "udp4",
|
||||
Dialer: dialer,
|
||||
},
|
||||
client6: &dns.Client{
|
||||
Net: "udp6",
|
||||
Dialer: dialer,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
25
pkg/publicip/dns/dns_test.go
Normal file
25
pkg/publicip/dns/dns_test.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
intf, err := New(SetTimeout(time.Hour))
|
||||
require.NoError(t, err)
|
||||
|
||||
impl, ok := intf.(*fetcher)
|
||||
require.True(t, ok)
|
||||
|
||||
assert.NotNil(t, impl.ring.counter)
|
||||
assert.NotEmpty(t, impl.ring.providers)
|
||||
assert.NotNil(t, impl.client)
|
||||
assert.NotNil(t, impl.client4)
|
||||
assert.NotNil(t, impl.client6)
|
||||
}
|
||||
68
pkg/publicip/dns/fetch.go
Normal file
68
pkg/publicip/dns/fetch.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoTXTRecordFound = errors.New("no TXT record found")
|
||||
ErrTooManyAnswers = errors.New("too many answers")
|
||||
ErrInvalidAnswerType = errors.New("invalid answer type")
|
||||
ErrTooManyTXTRecords = errors.New("too many TXT records")
|
||||
ErrIPMalformed = errors.New("IP address malformed")
|
||||
)
|
||||
|
||||
func fetch(ctx context.Context, client Client, providerData providerData) (
|
||||
publicIP net.IP, err error) {
|
||||
message := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: providerData.fqdn,
|
||||
Qtype: dns.TypeTXT,
|
||||
Qclass: uint16(providerData.class),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
r, _, err := client.ExchangeContext(ctx, message, providerData.nameserver)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
L := len(r.Answer)
|
||||
if L == 0 {
|
||||
return nil, ErrNoTXTRecordFound
|
||||
} else if L > 1 {
|
||||
return nil, 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",
|
||||
ErrInvalidAnswerType, answer)
|
||||
}
|
||||
|
||||
L = len(txt.Txt)
|
||||
if L == 0 {
|
||||
return nil, ErrNoTXTRecordFound
|
||||
} else if L > 1 {
|
||||
return nil, 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)
|
||||
}
|
||||
|
||||
return publicIP, nil
|
||||
}
|
||||
126
pkg/publicip/dns/fetch_test.go
Normal file
126
pkg/publicip/dns/fetch_test.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/miekg/dns"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/dns/mock_dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_fetch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
providerData := providerData{
|
||||
nameserver: "nameserver",
|
||||
fqdn: "record",
|
||||
class: dns.ClassNONE,
|
||||
}
|
||||
|
||||
expectedMessage := &dns.Msg{
|
||||
MsgHdr: dns.MsgHdr{
|
||||
Opcode: dns.OpcodeQuery,
|
||||
},
|
||||
Question: []dns.Question{
|
||||
{
|
||||
Name: providerData.fqdn,
|
||||
Qtype: dns.TypeTXT,
|
||||
Qclass: uint16(providerData.class),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
response *dns.Msg
|
||||
exchangeErr error
|
||||
publicIP net.IP
|
||||
err error
|
||||
}{
|
||||
"success": {
|
||||
response: &dns.Msg{
|
||||
Answer: []dns.RR{
|
||||
&dns.TXT{
|
||||
Txt: []string{"55.55.55.55"},
|
||||
},
|
||||
},
|
||||
},
|
||||
publicIP: net.IP{55, 55, 55, 55},
|
||||
},
|
||||
"exchange error": {
|
||||
exchangeErr: errors.New("dummy"),
|
||||
err: errors.New("dummy"),
|
||||
},
|
||||
"no answer": {
|
||||
response: &dns.Msg{},
|
||||
err: ErrNoTXTRecordFound,
|
||||
},
|
||||
"too many answers": {
|
||||
response: &dns.Msg{
|
||||
Answer: []dns.RR{&dns.TXT{}, &dns.TXT{}},
|
||||
},
|
||||
err: errors.New("too many answers: 2 instead of 1"),
|
||||
},
|
||||
"wrong answer type": {
|
||||
response: &dns.Msg{
|
||||
Answer: []dns.RR{&dns.A{}},
|
||||
},
|
||||
err: errors.New("invalid answer type: *dns.A instead of *dns.TXT"),
|
||||
},
|
||||
"no TXT record": {
|
||||
response: &dns.Msg{
|
||||
Answer: []dns.RR{&dns.TXT{}},
|
||||
},
|
||||
err: errors.New("no TXT record found"),
|
||||
},
|
||||
"too many TXT record": {
|
||||
response: &dns.Msg{
|
||||
Answer: []dns.RR{&dns.TXT{
|
||||
Txt: []string{"a", "b"},
|
||||
}},
|
||||
},
|
||||
err: errors.New("too many TXT records: 2 instead of 1"),
|
||||
},
|
||||
"invalid IP address": {
|
||||
response: &dns.Msg{
|
||||
Answer: []dns.RR{&dns.TXT{
|
||||
Txt: []string{"invalid"},
|
||||
}},
|
||||
},
|
||||
err: errors.New(`IP address malformed: "invalid"`),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctrl := gomock.NewController(t)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
client := mock_dns.NewMockClient(ctrl)
|
||||
client.EXPECT().
|
||||
ExchangeContext(ctx, expectedMessage, providerData.nameserver).
|
||||
Return(testCase.response, time.Millisecond, testCase.exchangeErr)
|
||||
|
||||
publicIP, err := fetch(ctx, client, providerData)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if !testCase.publicIP.Equal(publicIP) {
|
||||
t.Errorf("IP address mismatch: expected %s and got %s", testCase.publicIP, publicIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
32
pkg/publicip/dns/integration_test.go
Normal file
32
pkg/publicip/dns/integration_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// +build integration
|
||||
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_integration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fetcher, err := New(SetProviders(Google, Cloudflare))
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
publicIP1, err := fetcher.IP4(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, publicIP1)
|
||||
|
||||
publicIP2, err := fetcher.IP4(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, publicIP2)
|
||||
|
||||
assert.Equal(t, publicIP1, publicIP2)
|
||||
|
||||
t.Logf("Public IP is %s", publicIP1)
|
||||
}
|
||||
26
pkg/publicip/dns/ip.go
Normal file
26
pkg/publicip/dns/ip.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
func (f *fetcher) IP(ctx context.Context) (publicIP net.IP, err error) {
|
||||
return f.ip(ctx, f.client)
|
||||
}
|
||||
|
||||
func (f *fetcher) IP4(ctx context.Context) (publicIP net.IP, err error) {
|
||||
return f.ip(ctx, f.client4)
|
||||
}
|
||||
|
||||
func (f *fetcher) IP6(ctx context.Context) (publicIP net.IP, err error) {
|
||||
return f.ip(ctx, f.client6)
|
||||
}
|
||||
|
||||
func (f *fetcher) ip(ctx context.Context, client Client) (
|
||||
publicIP net.IP, 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())
|
||||
}
|
||||
52
pkg/publicip/dns/mock_dns/client.go
Normal file
52
pkg/publicip/dns/mock_dns/client.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/qdm12/ddns-updater/pkg/publicip/dns (interfaces: Client)
|
||||
|
||||
// Package mock_dns is a generated GoMock package.
|
||||
package mock_dns
|
||||
|
||||
import (
|
||||
context "context"
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
dns "github.com/miekg/dns"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
)
|
||||
|
||||
// MockClient is a mock of Client interface
|
||||
type MockClient struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockClientMockRecorder
|
||||
}
|
||||
|
||||
// MockClientMockRecorder is the mock recorder for MockClient
|
||||
type MockClientMockRecorder struct {
|
||||
mock *MockClient
|
||||
}
|
||||
|
||||
// NewMockClient creates a new mock instance
|
||||
func NewMockClient(ctrl *gomock.Controller) *MockClient {
|
||||
mock := &MockClient{ctrl: ctrl}
|
||||
mock.recorder = &MockClientMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockClient) EXPECT() *MockClientMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// ExchangeContext mocks base method
|
||||
func (m *MockClient) ExchangeContext(arg0 context.Context, arg1 *dns.Msg, arg2 string) (*dns.Msg, time.Duration, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "ExchangeContext", arg0, arg1, arg2)
|
||||
ret0, _ := ret[0].(*dns.Msg)
|
||||
ret1, _ := ret[1].(time.Duration)
|
||||
ret2, _ := ret[2].(error)
|
||||
return ret0, ret1, ret2
|
||||
}
|
||||
|
||||
// ExchangeContext indicates an expected call of ExchangeContext
|
||||
func (mr *MockClientMockRecorder) ExchangeContext(arg0, arg1, arg2 interface{}) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExchangeContext", reflect.TypeOf((*MockClient)(nil).ExchangeContext), arg0, arg1, arg2)
|
||||
}
|
||||
37
pkg/publicip/dns/options.go
Normal file
37
pkg/publicip/dns/options.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package dns
|
||||
|
||||
import "time"
|
||||
|
||||
type settings struct {
|
||||
providers []Provider
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func newDefaultSettings() settings {
|
||||
return settings{
|
||||
providers: ListProviders(),
|
||||
timeout: time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
type Option func(s *settings) error
|
||||
|
||||
func SetProviders(first Provider, providers ...Provider) Option {
|
||||
return func(s *settings) error {
|
||||
providers = append(providers, first)
|
||||
for _, provider := range providers {
|
||||
if err := ValidateProvider(provider); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.providers = providers
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetTimeout(timeout time.Duration) Option {
|
||||
return func(s *settings) error {
|
||||
s.timeout = timeout
|
||||
return nil
|
||||
}
|
||||
}
|
||||
95
pkg/publicip/dns/options_test.go
Normal file
95
pkg/publicip/dns/options_test.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_newDefaultSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := newDefaultSettings()
|
||||
|
||||
assert.NotEmpty(t, settings.providers)
|
||||
assert.GreaterOrEqual(t, int(settings.timeout), int(time.Millisecond))
|
||||
}
|
||||
|
||||
func Test_SetProviders(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
initialSettings settings
|
||||
providers []Provider
|
||||
expectedSettings settings
|
||||
err error
|
||||
}{
|
||||
"Google": {
|
||||
initialSettings: settings{
|
||||
providers: []Provider{Cloudflare},
|
||||
},
|
||||
providers: []Provider{Google},
|
||||
expectedSettings: settings{
|
||||
providers: []Provider{Google},
|
||||
},
|
||||
},
|
||||
"Google and Cloudflare": {
|
||||
initialSettings: settings{
|
||||
providers: []Provider{Cloudflare},
|
||||
},
|
||||
providers: []Provider{Google, Cloudflare},
|
||||
expectedSettings: settings{
|
||||
providers: []Provider{Cloudflare, Google},
|
||||
},
|
||||
},
|
||||
"invalid provider": {
|
||||
initialSettings: settings{
|
||||
providers: []Provider{Cloudflare},
|
||||
},
|
||||
providers: []Provider{Provider("invalid")},
|
||||
expectedSettings: settings{
|
||||
providers: []Provider{Cloudflare},
|
||||
},
|
||||
err: errors.New("unknown provider: invalid"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := testCase.initialSettings
|
||||
|
||||
option := SetProviders(testCase.providers[0], testCase.providers[1:]...)
|
||||
err := option(&settings)
|
||||
|
||||
assert.Equal(t, testCase.expectedSettings, settings)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SetTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
initialSettings := settings{}
|
||||
expectedSettings := settings{
|
||||
timeout: time.Hour,
|
||||
}
|
||||
|
||||
option := SetTimeout(time.Hour)
|
||||
err := option(&initialSettings)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedSettings, initialSettings)
|
||||
}
|
||||
57
pkg/publicip/dns/providers.go
Normal file
57
pkg/publicip/dns/providers.go
Normal file
@@ -0,0 +1,57 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
)
|
||||
|
||||
type Provider string
|
||||
|
||||
const (
|
||||
Cloudflare Provider = "cloudflare"
|
||||
Google Provider = "google"
|
||||
)
|
||||
|
||||
func ListProviders() []Provider {
|
||||
return []Provider{
|
||||
Cloudflare,
|
||||
Google,
|
||||
}
|
||||
}
|
||||
|
||||
var ErrUnknownProvider = errors.New("unknown provider")
|
||||
|
||||
func ValidateProvider(provider Provider) error {
|
||||
for _, possible := range ListProviders() {
|
||||
if provider == possible {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%w: %s", ErrUnknownProvider, provider)
|
||||
}
|
||||
|
||||
type providerData struct {
|
||||
nameserver string
|
||||
fqdn string
|
||||
class dns.Class
|
||||
}
|
||||
|
||||
func (provider Provider) data() providerData {
|
||||
switch provider {
|
||||
case Google:
|
||||
return providerData{
|
||||
nameserver: "ns1.google.com:53",
|
||||
fqdn: "o-o.myaddr.l.google.com.",
|
||||
class: dns.ClassINET,
|
||||
}
|
||||
case Cloudflare:
|
||||
return providerData{
|
||||
nameserver: "one.one.one.one:53",
|
||||
fqdn: "whoami.cloudflare.",
|
||||
class: dns.ClassCHAOS,
|
||||
}
|
||||
}
|
||||
panic(`provider unknown: "` + string(provider) + `"`)
|
||||
}
|
||||
89
pkg/publicip/dns/providers_test.go
Normal file
89
pkg/publicip/dns/providers_test.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package dns
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/miekg/dns"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ValidateProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
provider Provider
|
||||
err error
|
||||
}{
|
||||
"valid provider": {
|
||||
provider: Google,
|
||||
},
|
||||
"invalid provider": {
|
||||
provider: Provider("invalid"),
|
||||
err: errors.New("unknown provider: invalid"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := ValidateProvider(testCase.provider)
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_data(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
provider Provider
|
||||
data providerData
|
||||
panicMessage string
|
||||
}{
|
||||
"google": {
|
||||
provider: Google,
|
||||
data: providerData{
|
||||
nameserver: "ns1.google.com:53",
|
||||
fqdn: "o-o.myaddr.l.google.com.",
|
||||
class: dns.ClassINET,
|
||||
},
|
||||
},
|
||||
"cloudflare": {
|
||||
provider: Cloudflare,
|
||||
data: providerData{
|
||||
nameserver: "one.one.one.one:53",
|
||||
fqdn: "whoami.cloudflare.",
|
||||
class: dns.ClassCHAOS,
|
||||
},
|
||||
},
|
||||
"invalid provider": {
|
||||
provider: Provider("invalid"),
|
||||
panicMessage: `provider unknown: "invalid"`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if len(testCase.panicMessage) > 0 {
|
||||
assert.PanicsWithValue(t, testCase.panicMessage, func() {
|
||||
testCase.provider.data()
|
||||
})
|
||||
return
|
||||
}
|
||||
data := testCase.provider.data()
|
||||
assert.Equal(t, testCase.data, data)
|
||||
})
|
||||
}
|
||||
}
|
||||
98
pkg/publicip/http/fetch.go
Normal file
98
pkg/publicip/http/fetch.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrNoIPFound = errors.New("no IP address found")
|
||||
ErrTooManyIPs = errors.New("too many IP addresses")
|
||||
ErrIPMalformed = errors.New("IP address malformed")
|
||||
)
|
||||
|
||||
var (
|
||||
ipv4Regex = regexp.MustCompile(`(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])`) //nolint:lll
|
||||
ipv6Regex = regexp.MustCompile(`(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`) //nolint:lll
|
||||
)
|
||||
|
||||
func fetch(ctx context.Context, client *http.Client, url string, version ipversion.IPVersion) (
|
||||
publicIP net.IP, err error) {
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response, err := client.Do(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
b, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := response.Body.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s := string(b)
|
||||
|
||||
ipv4Strings := ipv4Regex.FindAllString(s, -1)
|
||||
ipv6Strings := ipv6Regex.FindAllString(s, -1)
|
||||
|
||||
var ipString string
|
||||
switch version {
|
||||
case ipversion.IP4or6:
|
||||
switch {
|
||||
case len(ipv4Strings) == 1: // priority to IPv4
|
||||
ipString = ipv4Strings[0]
|
||||
case len(ipv6Strings) == 1:
|
||||
ipString = ipv6Strings[0]
|
||||
case len(ipv4Strings) > 1:
|
||||
return nil, 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",
|
||||
ErrTooManyIPs, len(ipv6Strings))
|
||||
default:
|
||||
return nil, 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)
|
||||
case 1:
|
||||
ipString = ipv4Strings[0]
|
||||
default:
|
||||
return nil, 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)
|
||||
case 1:
|
||||
ipString = ipv6Strings[0]
|
||||
default:
|
||||
return nil, 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)
|
||||
}
|
||||
|
||||
return publicIP, nil
|
||||
}
|
||||
178
pkg/publicip/http/fetch_test.go
Normal file
178
pkg/publicip/http/fetch_test.go
Normal file
@@ -0,0 +1,178 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_fetch(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
errDummy := errors.New("dummy")
|
||||
|
||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
tests := map[string]struct {
|
||||
ctx context.Context
|
||||
url string
|
||||
version ipversion.IPVersion
|
||||
httpContent []byte
|
||||
httpErr error
|
||||
publicIP net.IP
|
||||
err error
|
||||
}{
|
||||
"canceled context": {
|
||||
ctx: canceledCtx,
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP4or6,
|
||||
err: errors.New(`Get "https://opendns.com/ip": context canceled`),
|
||||
},
|
||||
"http error": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP4or6,
|
||||
httpErr: errDummy,
|
||||
err: errors.New(`Get "https://opendns.com/ip": dummy`),
|
||||
},
|
||||
"empty response": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP4or6,
|
||||
err: errors.New(`no IP address found: from "https://opendns.com/ip"`),
|
||||
},
|
||||
"no IP for IP4or6": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP4or6,
|
||||
httpContent: []byte(`abc def`),
|
||||
err: errors.New(`no IP address found: from "https://opendns.com/ip"`),
|
||||
},
|
||||
"single IPv4 for IP4or6": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP4or6,
|
||||
httpContent: []byte(`1.67.201.251`),
|
||||
publicIP: net.IP{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,
|
||||
},
|
||||
},
|
||||
"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},
|
||||
},
|
||||
"too many IPv4s for IP4or6": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP4or6,
|
||||
httpContent: []byte(`1.67.201.251 1.67.201.251`),
|
||||
err: errors.New("too many IP addresses: found 2 IPv4 addresses instead of 1"),
|
||||
},
|
||||
"too many IPv6s for IP4or6": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP4or6,
|
||||
httpContent: []byte(`::1 ::1`),
|
||||
err: errors.New("too many IP addresses: found 2 IPv6 addresses instead of 1"),
|
||||
},
|
||||
"no IP for IP4": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP4,
|
||||
httpContent: []byte(`abc def`),
|
||||
err: errors.New(`no IP address found: from "https://opendns.com/ip" for version ipv4`),
|
||||
},
|
||||
"single IPv4 for IP4": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP4,
|
||||
httpContent: []byte(`1.67.201.251`),
|
||||
publicIP: net.IP{1, 67, 201, 251},
|
||||
},
|
||||
"too many IPv4s for IP4": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP4,
|
||||
httpContent: []byte(`1.67.201.251 1.67.201.251`),
|
||||
err: errors.New("too many IP addresses: found 2 IPv4 addresses instead of 1"),
|
||||
},
|
||||
"no IP for IP6": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP6,
|
||||
httpContent: []byte(`abc def`),
|
||||
err: errors.New(`no IP address found: from "https://opendns.com/ip" for version ipv6`),
|
||||
},
|
||||
"single IPv6 for IP6": {
|
||||
ctx: context.Background(),
|
||||
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,
|
||||
},
|
||||
},
|
||||
"too many IPv6s for IP6": {
|
||||
ctx: context.Background(),
|
||||
url: "https://opendns.com/ip",
|
||||
version: ipversion.IP6,
|
||||
httpContent: []byte(`::1 ::1`),
|
||||
err: errors.New("too many IP addresses: found 2 IPv6 addresses instead of 1"),
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, tc.url, r.URL.String())
|
||||
if err := r.Context().Err(); err != nil {
|
||||
return nil, err
|
||||
} else if tc.httpErr != nil {
|
||||
return nil, tc.httpErr
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(tc.httpContent)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
publicIP, err := fetch(tc.ctx, client, tc.url, tc.version)
|
||||
|
||||
if tc.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, tc.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if !tc.publicIP.Equal(publicIP) {
|
||||
t.Errorf("IP address mismatch: expected %s and got %s", tc.publicIP, publicIP)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
63
pkg/publicip/http/http.go
Normal file
63
pkg/publicip/http/http.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type Fetcher interface {
|
||||
IP(ctx context.Context) (publicIP net.IP, err error)
|
||||
IP4(ctx context.Context) (publicIP net.IP, err error)
|
||||
IP6(ctx context.Context) (publicIP net.IP, err error)
|
||||
}
|
||||
|
||||
type fetcher struct {
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
ip4or6 urlsRing // URLs to get ipv4 or ipv6
|
||||
ip4 urlsRing // URLs to get ipv4 only
|
||||
ip6 urlsRing // URLs to get ipv6 only
|
||||
}
|
||||
|
||||
type urlsRing struct {
|
||||
counter *uint32
|
||||
urls []string
|
||||
}
|
||||
|
||||
func New(client *http.Client, options ...Option) (f Fetcher, err error) {
|
||||
settings := newDefaultSettings()
|
||||
for _, option := range options {
|
||||
if err := option(&settings); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
fetcher := &fetcher{
|
||||
client: client,
|
||||
timeout: settings.timeout,
|
||||
}
|
||||
|
||||
fetcher.ip4or6.counter = new(uint32)
|
||||
for _, provider := range settings.providersIP {
|
||||
url, _ := provider.url(ipversion.IP4or6)
|
||||
fetcher.ip4or6.urls = append(fetcher.ip4or6.urls, url)
|
||||
}
|
||||
|
||||
fetcher.ip4.counter = new(uint32)
|
||||
for _, provider := range settings.providersIP4 {
|
||||
url, _ := provider.url(ipversion.IP4)
|
||||
fetcher.ip4.urls = append(fetcher.ip4.urls, url)
|
||||
}
|
||||
|
||||
fetcher.ip6.counter = new(uint32)
|
||||
for _, provider := range settings.providersIP6 {
|
||||
url, _ := provider.url(ipversion.IP6)
|
||||
fetcher.ip6.urls = append(fetcher.ip6.urls, url)
|
||||
}
|
||||
|
||||
return fetcher, nil
|
||||
}
|
||||
92
pkg/publicip/http/http_test.go
Normal file
92
pkg/publicip/http/http_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_New(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &http.Client{Timeout: time.Second}
|
||||
|
||||
testCases := map[string]struct {
|
||||
options []Option
|
||||
fetcher *fetcher
|
||||
err error
|
||||
}{
|
||||
"no options": {
|
||||
fetcher: &fetcher{
|
||||
client: client,
|
||||
timeout: 5 * time.Second,
|
||||
ip4or6: urlsRing{
|
||||
counter: new(uint32),
|
||||
urls: []string{"https://domains.google.com/checkip"},
|
||||
},
|
||||
ip4: urlsRing{
|
||||
counter: new(uint32),
|
||||
urls: []string{"http://ip1.dynupdate.no-ip.com"},
|
||||
},
|
||||
ip6: urlsRing{
|
||||
counter: new(uint32),
|
||||
urls: []string{"http://ip1.dynupdate6.no-ip.com"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"with options": {
|
||||
options: []Option{
|
||||
SetProvidersIP(Opendns),
|
||||
SetProvidersIP4(Ipify),
|
||||
SetProvidersIP6(Ipify),
|
||||
SetTimeout(time.Second),
|
||||
},
|
||||
fetcher: &fetcher{
|
||||
client: client,
|
||||
timeout: time.Second,
|
||||
ip4or6: urlsRing{
|
||||
counter: new(uint32),
|
||||
urls: []string{"https://diagnostic.opendns.com/myip"},
|
||||
},
|
||||
ip4: urlsRing{
|
||||
counter: new(uint32),
|
||||
urls: []string{"https://api.ipify.org"},
|
||||
},
|
||||
ip6: urlsRing{
|
||||
counter: new(uint32),
|
||||
urls: []string{"https://api6.ipify.org"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"bad option": {
|
||||
options: []Option{
|
||||
SetProvidersIP(Provider("invalid")),
|
||||
},
|
||||
err: errors.New("unknown provider: invalid"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
f, err := New(client, testCase.options...)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
assert.Nil(t, f)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
implementation, ok := f.(*fetcher)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, testCase.fetcher, implementation)
|
||||
})
|
||||
}
|
||||
}
|
||||
35
pkg/publicip/http/integration_test.go
Normal file
35
pkg/publicip/http/integration_test.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// +build integration
|
||||
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_integration(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
client := &http.Client{}
|
||||
|
||||
fetcher, err := New(client, SetProvidersIP(Opendns))
|
||||
require.NoError(t, err)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
publicIP1, err := fetcher.IP4(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, publicIP1)
|
||||
|
||||
publicIP2, err := fetcher.IP4(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, publicIP2)
|
||||
|
||||
assert.Equal(t, publicIP1, publicIP2)
|
||||
|
||||
t.Logf("Public IP is %s", publicIP1)
|
||||
}
|
||||
32
pkg/publicip/http/ip.go
Normal file
32
pkg/publicip/http/ip.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
func (f *fetcher) IP(ctx context.Context) (publicIP net.IP, err error) {
|
||||
return f.ip(ctx, f.ip4or6, ipversion.IP4or6)
|
||||
}
|
||||
|
||||
func (f *fetcher) IP4(ctx context.Context) (publicIP net.IP, err error) {
|
||||
return f.ip(ctx, f.ip4, ipversion.IP4)
|
||||
}
|
||||
|
||||
func (f *fetcher) IP6(ctx context.Context) (publicIP net.IP, 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) {
|
||||
index := int(atomic.AddUint32(ring.counter, 1)) % len(ring.urls)
|
||||
url := ring.urls[index]
|
||||
|
||||
ctx, cancel := context.WithTimeout(ctx, f.timeout)
|
||||
defer cancel()
|
||||
|
||||
return fetch(ctx, f.client, url, version)
|
||||
}
|
||||
305
pkg/publicip/http/ip_test.go
Normal file
305
pkg/publicip/http/ip_test.go
Normal file
@@ -0,0 +1,305 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func uint32Ptr(n uint32) *uint32 { return &n }
|
||||
|
||||
func Test_fetcher_IP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
const url = "c"
|
||||
httpBytes := []byte(`55.55.55.55`)
|
||||
expectedPublicIP := net.IP{55, 55, 55, 55}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, url, r.URL.String())
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(httpBytes)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
initialFetcher := &fetcher{
|
||||
client: client,
|
||||
timeout: time.Hour,
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(1),
|
||||
urls: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
expectedFetcher := &fetcher{
|
||||
client: client,
|
||||
timeout: time.Hour,
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(2),
|
||||
urls: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
publicIP, err := initialFetcher.IP(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
if !expectedPublicIP.Equal(publicIP) {
|
||||
t.Errorf("IP address mismatch: expected %s and got %s", expectedPublicIP, publicIP)
|
||||
}
|
||||
assert.Equal(t, expectedFetcher, initialFetcher)
|
||||
}
|
||||
|
||||
func Test_fetcher_IP4(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ctx := context.Background()
|
||||
const url = "c"
|
||||
httpBytes := []byte(`55.55.55.55`)
|
||||
expectedPublicIP := net.IP{55, 55, 55, 55}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, url, r.URL.String())
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(httpBytes)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
initialFetcher := &fetcher{
|
||||
client: client,
|
||||
timeout: time.Hour,
|
||||
ip4: urlsRing{
|
||||
counter: uint32Ptr(1),
|
||||
urls: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
expectedFetcher := &fetcher{
|
||||
client: client,
|
||||
timeout: time.Hour,
|
||||
ip4: urlsRing{
|
||||
counter: uint32Ptr(2),
|
||||
urls: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
publicIP, err := initialFetcher.IP4(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
if !expectedPublicIP.Equal(publicIP) {
|
||||
t.Errorf("IP address mismatch: expected %s and got %s", expectedPublicIP, publicIP)
|
||||
}
|
||||
assert.Equal(t, expectedFetcher, initialFetcher)
|
||||
}
|
||||
|
||||
func Test_fetcher_IP6(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
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,
|
||||
}
|
||||
|
||||
client := &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, url, r.URL.String())
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(httpBytes)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
initialFetcher := &fetcher{
|
||||
client: client,
|
||||
timeout: time.Hour,
|
||||
ip6: urlsRing{
|
||||
counter: uint32Ptr(1),
|
||||
urls: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
expectedFetcher := &fetcher{
|
||||
client: client,
|
||||
timeout: time.Hour,
|
||||
ip6: urlsRing{
|
||||
counter: uint32Ptr(2),
|
||||
urls: []string{"a", "b", "c"},
|
||||
},
|
||||
}
|
||||
|
||||
publicIP, err := initialFetcher.IP6(ctx)
|
||||
|
||||
assert.NoError(t, err)
|
||||
if !expectedPublicIP.Equal(publicIP) {
|
||||
t.Errorf("IP address mismatch: expected %s and got %s", expectedPublicIP, publicIP)
|
||||
}
|
||||
assert.Equal(t, expectedFetcher, initialFetcher)
|
||||
}
|
||||
|
||||
func Test_fetcher_ip(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
canceledCtx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
newTestClient := func(expectedURL string, httpBytes []byte, httpErr error) *http.Client {
|
||||
return &http.Client{
|
||||
Transport: roundTripFunc(func(r *http.Request) (*http.Response, error) {
|
||||
assert.Equal(t, expectedURL, r.URL.String())
|
||||
if err := r.Context().Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpErr != nil {
|
||||
return nil, httpErr
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: ioutil.NopCloser(bytes.NewReader(httpBytes)),
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
testCases := map[string]struct {
|
||||
initialFetcher *fetcher
|
||||
ctx context.Context
|
||||
publicIP net.IP
|
||||
err error
|
||||
finalFetcher *fetcher // client is ignored when comparing the two
|
||||
}{
|
||||
"first run": {
|
||||
ctx: context.Background(),
|
||||
initialFetcher: &fetcher{
|
||||
timeout: time.Hour,
|
||||
client: newTestClient("b", []byte(`55.55.55.55`), nil),
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(0),
|
||||
urls: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
publicIP: net.IP{55, 55, 55, 55},
|
||||
finalFetcher: &fetcher{
|
||||
timeout: time.Hour,
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(1),
|
||||
urls: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"second run": {
|
||||
ctx: context.Background(),
|
||||
initialFetcher: &fetcher{
|
||||
timeout: time.Hour,
|
||||
client: newTestClient("a", []byte(`55.55.55.55`), nil),
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(1),
|
||||
urls: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
publicIP: net.IP{55, 55, 55, 55},
|
||||
finalFetcher: &fetcher{
|
||||
timeout: time.Hour,
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(2),
|
||||
urls: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"max uint32": {
|
||||
ctx: context.Background(),
|
||||
initialFetcher: &fetcher{
|
||||
timeout: time.Hour,
|
||||
client: newTestClient("a", []byte(`55.55.55.55`), nil),
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(^uint32(0)),
|
||||
urls: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
publicIP: net.IP{55, 55, 55, 55},
|
||||
finalFetcher: &fetcher{
|
||||
timeout: time.Hour,
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(0),
|
||||
urls: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"zero timeout": {
|
||||
ctx: context.Background(),
|
||||
initialFetcher: &fetcher{
|
||||
client: newTestClient("a", nil, nil),
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(1),
|
||||
urls: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
finalFetcher: &fetcher{
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(2),
|
||||
urls: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
err: errors.New(`Get "a": context deadline exceeded`),
|
||||
},
|
||||
"canceled context": {
|
||||
ctx: canceledCtx,
|
||||
initialFetcher: &fetcher{
|
||||
timeout: time.Hour,
|
||||
client: newTestClient("a", nil, nil),
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(1),
|
||||
urls: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
finalFetcher: &fetcher{
|
||||
timeout: time.Hour,
|
||||
ip4or6: urlsRing{
|
||||
counter: uint32Ptr(2),
|
||||
urls: []string{"a", "b"},
|
||||
},
|
||||
},
|
||||
err: errors.New(`Get "a": context canceled`),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
urlRing := testCase.initialFetcher.ip4or6
|
||||
|
||||
publicIP, err := testCase.initialFetcher.ip(testCase.ctx, urlRing, ipversion.IP4or6)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
if !testCase.publicIP.Equal(publicIP) {
|
||||
t.Errorf("IP address mismatch: expected %s and got %s", testCase.publicIP, publicIP)
|
||||
}
|
||||
|
||||
testCase.initialFetcher.client = nil
|
||||
assert.Equal(t, testCase.finalFetcher, testCase.initialFetcher)
|
||||
})
|
||||
}
|
||||
}
|
||||
72
pkg/publicip/http/options.go
Normal file
72
pkg/publicip/http/options.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type settings struct {
|
||||
providersIP []Provider
|
||||
providersIP4 []Provider
|
||||
providersIP6 []Provider
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
func newDefaultSettings() settings {
|
||||
const defaultTimeout = 5 * time.Second
|
||||
return settings{
|
||||
providersIP: []Provider{Google},
|
||||
providersIP4: []Provider{Noip},
|
||||
providersIP6: []Provider{Noip},
|
||||
timeout: defaultTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
type Option func(s *settings) error
|
||||
|
||||
func SetProvidersIP(first Provider, providers ...Provider) Option {
|
||||
providers = append(providers, first)
|
||||
return func(s *settings) error {
|
||||
for _, provider := range providers {
|
||||
if err := ValidateProvider(provider, ipversion.IP4or6); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.providersIP = providers
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetProvidersIP4(first Provider, providers ...Provider) Option {
|
||||
providers = append(providers, first)
|
||||
return func(s *settings) error {
|
||||
for _, provider := range providers {
|
||||
if err := ValidateProvider(provider, ipversion.IP4); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.providersIP4 = providers
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetProvidersIP6(first Provider, providers ...Provider) Option {
|
||||
providers = append(providers, first)
|
||||
return func(s *settings) error {
|
||||
for _, provider := range providers {
|
||||
if err := ValidateProvider(provider, ipversion.IP6); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.providersIP6 = providers
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func SetTimeout(timeout time.Duration) Option {
|
||||
return func(s *settings) error {
|
||||
s.timeout = timeout
|
||||
return nil
|
||||
}
|
||||
}
|
||||
192
pkg/publicip/http/options_test.go
Normal file
192
pkg/publicip/http/options_test.go
Normal file
@@ -0,0 +1,192 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_newDefaultSettings(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := newDefaultSettings()
|
||||
|
||||
assert.NotEmpty(t, settings.providersIP)
|
||||
assert.NotEmpty(t, settings.providersIP4)
|
||||
assert.NotEmpty(t, settings.providersIP6)
|
||||
assert.GreaterOrEqual(t, int(settings.timeout), int(time.Millisecond))
|
||||
}
|
||||
|
||||
func Test_SetProvidersIP(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
initialSettings settings
|
||||
providers []Provider
|
||||
expectedSettings settings
|
||||
err error
|
||||
}{
|
||||
"Google": {
|
||||
initialSettings: settings{
|
||||
providersIP: []Provider{Opendns},
|
||||
},
|
||||
providers: []Provider{Google},
|
||||
expectedSettings: settings{
|
||||
providersIP: []Provider{Google},
|
||||
},
|
||||
},
|
||||
"bad provider for IP version": {
|
||||
initialSettings: settings{
|
||||
providersIP: []Provider{Opendns},
|
||||
},
|
||||
providers: []Provider{Noip},
|
||||
expectedSettings: settings{
|
||||
providersIP: []Provider{Opendns},
|
||||
},
|
||||
err: errors.New(`provider does not support IP version: "noip" for version ipv4 or ipv6`),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := testCase.initialSettings
|
||||
|
||||
option := SetProvidersIP(testCase.providers[0], testCase.providers[1:]...)
|
||||
err := option(&settings)
|
||||
|
||||
assert.Equal(t, testCase.expectedSettings, settings)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SetProvidersIP4(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
initialSettings settings
|
||||
providers []Provider
|
||||
expectedSettings settings
|
||||
err error
|
||||
}{
|
||||
"NoIP": {
|
||||
initialSettings: settings{
|
||||
providersIP4: []Provider{Ipify},
|
||||
},
|
||||
providers: []Provider{Noip},
|
||||
expectedSettings: settings{
|
||||
providersIP4: []Provider{Noip},
|
||||
},
|
||||
},
|
||||
"bad provider for IP version": {
|
||||
initialSettings: settings{
|
||||
providersIP4: []Provider{Ipify},
|
||||
},
|
||||
providers: []Provider{Opendns},
|
||||
expectedSettings: settings{
|
||||
providersIP4: []Provider{Ipify},
|
||||
},
|
||||
err: errors.New(`provider does not support IP version: "opendns" for version ipv4`),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := testCase.initialSettings
|
||||
|
||||
option := SetProvidersIP4(testCase.providers[0], testCase.providers[1:]...)
|
||||
err := option(&settings)
|
||||
|
||||
assert.Equal(t, testCase.expectedSettings, settings)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SetProvidersIP6(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
initialSettings settings
|
||||
providers []Provider
|
||||
expectedSettings settings
|
||||
err error
|
||||
}{
|
||||
"NoIP": {
|
||||
initialSettings: settings{
|
||||
providersIP6: []Provider{Ipify},
|
||||
},
|
||||
providers: []Provider{Noip},
|
||||
expectedSettings: settings{
|
||||
providersIP6: []Provider{Noip},
|
||||
},
|
||||
},
|
||||
"bad provider for IP version": {
|
||||
initialSettings: settings{
|
||||
providersIP6: []Provider{Ipify},
|
||||
},
|
||||
providers: []Provider{Opendns},
|
||||
expectedSettings: settings{
|
||||
providersIP6: []Provider{Ipify},
|
||||
},
|
||||
err: errors.New(`provider does not support IP version: "opendns" for version ipv6`),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
settings := testCase.initialSettings
|
||||
|
||||
option := SetProvidersIP6(testCase.providers[0], testCase.providers[1:]...)
|
||||
err := option(&settings)
|
||||
|
||||
assert.Equal(t, testCase.expectedSettings, settings)
|
||||
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SetTimeout(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
initialSettings := settings{}
|
||||
expectedSettings := settings{
|
||||
timeout: time.Hour,
|
||||
}
|
||||
|
||||
option := SetTimeout(time.Hour)
|
||||
err := option(&initialSettings)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedSettings, initialSettings)
|
||||
}
|
||||
117
pkg/publicip/http/providers.go
Normal file
117
pkg/publicip/http/providers.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
)
|
||||
|
||||
type Provider string
|
||||
|
||||
const (
|
||||
Google Provider = "google"
|
||||
Ifconfig Provider = "ifconfig"
|
||||
Ipify Provider = "ipify"
|
||||
Ipinfo Provider = "ipinfo"
|
||||
Noip Provider = "noip"
|
||||
Opendns Provider = "opendns"
|
||||
)
|
||||
|
||||
func ListProviders() []Provider {
|
||||
return []Provider{
|
||||
Google,
|
||||
Ifconfig,
|
||||
Ipify,
|
||||
Ipinfo,
|
||||
Noip,
|
||||
Opendns,
|
||||
}
|
||||
}
|
||||
|
||||
func ListProvidersForVersion(version ipversion.IPVersion) (providers []Provider) {
|
||||
allProviders := ListProviders()
|
||||
for _, provider := range allProviders {
|
||||
if provider.SupportsVersion(version) {
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
var (
|
||||
ErrUnknownProvider = errors.New("unknown provider")
|
||||
ErrProviderIPVersion = errors.New("provider does not support IP version")
|
||||
)
|
||||
|
||||
func ValidateProvider(provider Provider, version ipversion.IPVersion) error {
|
||||
for _, possible := range ListProviders() {
|
||||
if provider == possible {
|
||||
_, ok := provider.url(version)
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: %q for version %s",
|
||||
ErrProviderIPVersion, provider, version.String())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("%w: %s", ErrUnknownProvider, provider)
|
||||
}
|
||||
|
||||
func (provider Provider) url(version ipversion.IPVersion) (url string, ok bool) {
|
||||
switch version {
|
||||
case ipversion.IP4:
|
||||
switch provider { //nolint:exhaustive
|
||||
case Ipify:
|
||||
url = "https://api.ipify.org"
|
||||
case Noip:
|
||||
url = "http://ip1.dynupdate.no-ip.com"
|
||||
}
|
||||
|
||||
case ipversion.IP6:
|
||||
switch provider { //nolint:exhaustive
|
||||
case Ipify:
|
||||
url = "https://api6.ipify.org"
|
||||
case Noip:
|
||||
url = "http://ip1.dynupdate6.no-ip.com"
|
||||
}
|
||||
|
||||
case ipversion.IP4or6:
|
||||
switch provider { //nolint:exhaustive
|
||||
case Google:
|
||||
url = "https://domains.google.com/checkip"
|
||||
case Ifconfig:
|
||||
url = "https://ifconfig.io/ip"
|
||||
case Ipinfo:
|
||||
url = "https://ipinfo.io/ip"
|
||||
case Opendns:
|
||||
url = "https://diagnostic.opendns.com/myip"
|
||||
}
|
||||
}
|
||||
|
||||
// Custom URL?
|
||||
if s := string(provider); strings.HasPrefix(s, "url:") {
|
||||
url = strings.TrimPrefix(s, "url:")
|
||||
}
|
||||
|
||||
if len(url) == 0 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return url, true
|
||||
}
|
||||
|
||||
func (provider Provider) SupportsVersion(version ipversion.IPVersion) bool {
|
||||
_, ok := provider.url(version)
|
||||
return ok
|
||||
}
|
||||
|
||||
// CustomProvider creates a provider with a custom HTTP(s) URL.
|
||||
// It is the responsibility of the caller to make sure it is a valid URL
|
||||
// and that it supports the desired IP version(s) as no further check is
|
||||
// done on it.
|
||||
func CustomProvider(httpsURL *url.URL) Provider { //nolint:interfacer
|
||||
return Provider("url:" + httpsURL.String())
|
||||
}
|
||||
93
pkg/publicip/http/providers_test.go
Normal file
93
pkg/publicip/http/providers_test.go
Normal file
@@ -0,0 +1,93 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/ipversion"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_ListProvidersForVersion(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
version ipversion.IPVersion
|
||||
providers []Provider
|
||||
}{
|
||||
"ip4or6": {
|
||||
version: ipversion.IP4or6,
|
||||
providers: []Provider{Google, Ifconfig, Ipinfo, Opendns},
|
||||
},
|
||||
"ip4": {
|
||||
version: ipversion.IP4,
|
||||
providers: []Provider{Ipify, Noip},
|
||||
},
|
||||
"ip6": {
|
||||
version: ipversion.IP6,
|
||||
providers: []Provider{Ipify, Noip},
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
providers := ListProvidersForVersion(testCase.version)
|
||||
assert.Equal(t, testCase.providers, providers)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_ValidateProvider(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := map[string]struct {
|
||||
provider Provider
|
||||
version ipversion.IPVersion
|
||||
err error
|
||||
}{
|
||||
"valid": {
|
||||
provider: Google,
|
||||
version: ipversion.IP4or6,
|
||||
},
|
||||
"invalid for ip version": {
|
||||
provider: Google,
|
||||
version: ipversion.IP4,
|
||||
err: errors.New(`provider does not support IP version: "google" for version ipv4`),
|
||||
},
|
||||
"unknown": {
|
||||
provider: Provider("unknown"),
|
||||
version: ipversion.IP4,
|
||||
err: errors.New("unknown provider: unknown"),
|
||||
},
|
||||
}
|
||||
|
||||
for name, testCase := range testCases {
|
||||
testCase := testCase
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := ValidateProvider(testCase.provider, testCase.version)
|
||||
if testCase.err != nil {
|
||||
require.Error(t, err)
|
||||
assert.Equal(t, testCase.err.Error(), err.Error())
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_customurl(t *testing.T) {
|
||||
t.Parallel()
|
||||
url := &url.URL{
|
||||
Scheme: "https",
|
||||
Host: "abc",
|
||||
}
|
||||
customProvider := CustomProvider(url)
|
||||
s, ok := customProvider.url(ipversion.IP4or6)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "https://abc", s)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package update
|
||||
package http
|
||||
|
||||
import "net/http"
|
||||
|
||||
43
pkg/publicip/ipversion/ipversion.go
Normal file
43
pkg/publicip/ipversion/ipversion.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package ipversion
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type IPVersion uint8
|
||||
|
||||
const (
|
||||
IP4or6 IPVersion = iota
|
||||
IP4
|
||||
IP6
|
||||
)
|
||||
|
||||
func (v IPVersion) String() string {
|
||||
switch v {
|
||||
case IP4or6:
|
||||
return "ipv4 or ipv6"
|
||||
case IP4:
|
||||
return "ipv4"
|
||||
case IP6:
|
||||
return "ipv6"
|
||||
default:
|
||||
return "ip?"
|
||||
}
|
||||
}
|
||||
|
||||
var ErrInvalidIPVersion = errors.New("invalid IP version")
|
||||
|
||||
func Parse(s string) (version IPVersion, err error) {
|
||||
switch strings.ToLower(s) {
|
||||
case "ipv4 or ipv6":
|
||||
return IP4or6, nil
|
||||
case "ipv4":
|
||||
return IP4, nil
|
||||
case "ipv6":
|
||||
return IP6, nil
|
||||
default:
|
||||
return IP4or6, fmt.Errorf("%w: %q", ErrInvalidIPVersion, s)
|
||||
}
|
||||
}
|
||||
73
pkg/publicip/publicip.go
Normal file
73
pkg/publicip/publicip.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/http"
|
||||
)
|
||||
|
||||
type Fetcher 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)
|
||||
}
|
||||
|
||||
type fetcher struct {
|
||||
settings settings
|
||||
dns Fetcher
|
||||
http Fetcher
|
||||
// Cycling effect if both are enabled
|
||||
counter *uint32 // 32 bit for 32 bit systems
|
||||
fetchTypes []FetchType
|
||||
}
|
||||
|
||||
var ErrNoFetchTypeSpecified = errors.New("at least one fetcher type must be specified")
|
||||
|
||||
func NewFetcher(dnsSettings DNSSettings, httpSettings HTTPSettings) (f Fetcher, err error) {
|
||||
settings := settings{
|
||||
dns: dnsSettings,
|
||||
http: httpSettings,
|
||||
}
|
||||
|
||||
fetcher := &fetcher{
|
||||
settings: settings,
|
||||
counter: new(uint32),
|
||||
}
|
||||
|
||||
if settings.dns.Enabled {
|
||||
fetcher.dns, err = dns.New(settings.dns.Options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fetcher.fetchTypes = append(fetcher.fetchTypes, DNS)
|
||||
}
|
||||
|
||||
if settings.http.Enabled {
|
||||
fetcher.http, err = http.New(settings.http.Client, settings.http.Options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fetcher.fetchTypes = append(fetcher.fetchTypes, HTTP)
|
||||
}
|
||||
|
||||
if len(fetcher.fetchTypes) == 0 {
|
||||
return nil, ErrNoFetchTypeSpecified
|
||||
}
|
||||
|
||||
return fetcher, nil
|
||||
}
|
||||
|
||||
func (f *fetcher) IP(ctx context.Context) (ip net.IP, err error) {
|
||||
return f.getSubFetcher().IP(ctx)
|
||||
}
|
||||
|
||||
func (f *fetcher) IP4(ctx context.Context) (ipv4 net.IP, err error) {
|
||||
return f.getSubFetcher().IP4(ctx)
|
||||
}
|
||||
|
||||
func (f *fetcher) IP6(ctx context.Context) (ipv6 net.IP, err error) {
|
||||
return f.getSubFetcher().IP6(ctx)
|
||||
}
|
||||
25
pkg/publicip/settings.go
Normal file
25
pkg/publicip/settings.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/qdm12/ddns-updater/pkg/publicip/dns"
|
||||
iphttp "github.com/qdm12/ddns-updater/pkg/publicip/http"
|
||||
)
|
||||
|
||||
type settings struct {
|
||||
// If both dns and http are enabled it will cycle between both of them.
|
||||
dns DNSSettings
|
||||
http HTTPSettings
|
||||
}
|
||||
|
||||
type DNSSettings struct {
|
||||
Enabled bool
|
||||
Options []dns.Option
|
||||
}
|
||||
|
||||
type HTTPSettings struct {
|
||||
Enabled bool
|
||||
Client *http.Client
|
||||
Options []iphttp.Option
|
||||
}
|
||||
26
pkg/publicip/subfetcher.go
Normal file
26
pkg/publicip/subfetcher.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package publicip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var ErrFetcherUndefined = errors.New("fetcher type undefined")
|
||||
|
||||
func (f *fetcher) getSubFetcher() Fetcher {
|
||||
fetcherType := f.fetchTypes[0]
|
||||
if len(f.fetchTypes) > 1 { // cycling effect
|
||||
index := int(atomic.AddUint32(f.counter, 1)) % len(f.fetchTypes)
|
||||
fetcherType = f.fetchTypes[index]
|
||||
}
|
||||
|
||||
switch fetcherType {
|
||||
case DNS:
|
||||
return f.dns
|
||||
case HTTP:
|
||||
return f.http
|
||||
default:
|
||||
panic(fmt.Sprintf("fetcher type undefined: %d", fetcherType))
|
||||
}
|
||||
}
|
||||
8
pkg/publicip/types.go
Normal file
8
pkg/publicip/types.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package publicip
|
||||
|
||||
type FetchType uint8
|
||||
|
||||
const (
|
||||
DNS FetchType = iota
|
||||
HTTP
|
||||
)
|
||||
Reference in New Issue
Block a user