diff --git a/README.md b/README.md index 4acf641..34bb29a 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Before run, you need configure this environment variables. - `CLOUDFLARE_ZONE` - (required) zone name with domain you want to check. See: [https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones](https://developers.cloudflare.com/fundamentals/get-started/concepts/accounts-and-zones/#zones) - `ON_CHANGE_COMMENT` - (optional) in the event that the ip address of the dns record changes, this comment will be added to the record - `CHECK_INTERVAL_SECONDS` - (optional) how often will the ip address of the records be checked (default: `300`) - - `PUBLIC_IP_RESOLVER` - (optional) public ip address resolver. For now only resolving via `https://ifconfig.me` is implemented. (default: `ifconfig.me`) + - `PUBLIC_IP_RESOLVER` - (optional) public ip address resolver. (default: `ifconfig.me`) Available: `ifconfig.me`, `v4.ident.me` ### Building from source @@ -43,6 +43,7 @@ version: "3" services: cf-dns-updater: image: mkelcik/cloudflare-ddns-update:latest + restart: unless-stopped environment: - CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com - CLOUDFLARE_API_KEY=your_cloudflare_api_key diff --git a/docker-compose.yaml b/docker-compose.yaml index 8705729..53b9571 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -2,6 +2,7 @@ version: "3" services: cf-dns-updater: image: mkelcik/cloudflare-ddns-update:latest + restart: unless-stopped environment: - CLOUDFLARE_DNS_TO_CHECK=my.testdomain.com,your.testdomain.com - CLOUDFLARE_API_KEY=your_cloudflare_api_key diff --git a/main.go b/main.go index 956eacb..fc5c241 100644 --- a/main.go +++ b/main.go @@ -17,13 +17,15 @@ type PublicIpResolver interface { ResolvePublicIp(ctx context.Context) (net.IP, error) } -func getResolver(resolverName string) PublicIpResolver { +func getResolver(resolverName string) (PublicIpResolver, string) { switch resolverName { // HERE add another resolver if needed + case public_resolvers.V4IdentMeTag: + return public_resolvers.NewV4IdentMeDefault(), public_resolvers.V4IdentMeTag case public_resolvers.IfConfigMeTag: fallthrough default: - return public_resolvers.NewDefaultIfConfigMe() + return public_resolvers.NewDefaultIfConfigMe(), public_resolvers.IfConfigMeTag } } @@ -48,14 +50,14 @@ func main() { } // public ip resolver - publicIpResolver := getResolver(config.PublicIpResolverTag) + publicIpResolver, resolverTag := getResolver(config.PublicIpResolverTag) checkFunc := func() { currentPublicIP, err := publicIpResolver.ResolvePublicIp(ctx) if err != nil { log.Fatal(err) } - log.Printf("Current public ip `%s`", currentPublicIP) + log.Printf("Current public ip `%s` (%s)", currentPublicIP, resolverTag) dns, err := allDNSRecords(ctx, api, cloudflare.ZoneIdentifier(zoneID)) if err != nil { diff --git a/public_resolvers/base_resolver.go b/public_resolvers/base_resolver.go new file mode 100644 index 0000000..f286ea1 --- /dev/null +++ b/public_resolvers/base_resolver.go @@ -0,0 +1,44 @@ +package public_resolvers + +import ( + "context" + "fmt" + "io" + "net" + "net/http" +) + +type Doer interface { + Do(*http.Request) (*http.Response, error) +} + +type baseResolver struct { + client Doer + url string +} + +func (i baseResolver) ResolvePublicIp(ctx context.Context) (net.IP, error) { + req, err := http.NewRequestWithContext(ctx, http.MethodGet, i.url, nil) + if err != nil { + return net.IP{}, fmt.Errorf("error creating ifconfig request: %w", err) + } + + resp, err := i.client.Do(req) + if err != nil { + return net.IP{}, err + } + defer func() { + _ = resp.Body.Close() + }() + + if resp.StatusCode != http.StatusOK { + return net.IP{}, fmt.Errorf("unexpected response code %d", resp.StatusCode) + } + + ipText, err := io.ReadAll(resp.Body) + if err != nil { + return net.IP{}, fmt.Errorf("error reading body: %w", err) + } + + return net.ParseIP(string(ipText)), nil +} diff --git a/public_resolvers/base_resolver_test.go b/public_resolvers/base_resolver_test.go new file mode 100644 index 0000000..bb13d66 --- /dev/null +++ b/public_resolvers/base_resolver_test.go @@ -0,0 +1,97 @@ +package public_resolvers + +import ( + "bytes" + "context" + "io" + "net" + "net/http" + "reflect" + "testing" +) + +// RoundTripFunc . +type RoundTripFunc func(req *http.Request) *http.Response + +// RoundTrip . +func (f RoundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) { + return f(req), nil +} + +// NewTestClient returns *http.Client with Transport replaced to avoid making real calls +func NewTestClient(fn RoundTripFunc) *http.Client { + return &http.Client{ + Transport: RoundTripFunc(fn), + } +} + +func Test_baseResolver_ResolvePublicIp(t *testing.T) { + + testUrl := "http://my-test-url.url" + testIp := `192.168.0.100` + + client := NewTestClient(func(req *http.Request) *http.Response { + + if req.URL.String() != testUrl { + return &http.Response{ + StatusCode: 500, + // Send response to be tested + Body: io.NopCloser(bytes.NewBufferString(`invalid url`)), + // Must be set to non-nil value or it panics + Header: make(http.Header), + } + } + + return &http.Response{ + StatusCode: 200, + // Send response to be tested + Body: io.NopCloser(bytes.NewBufferString(testIp)), + // Must be set to non-nil value or it panics + Header: make(http.Header), + } + }) + + type fields struct { + client Doer + url string + } + type args struct { + ctx context.Context + } + tests := []struct { + name string + fields fields + args args + want net.IP + wantErr bool + }{ + { + name: "check parse ip4", + fields: fields{ + client: client, + url: testUrl, + }, + args: args{ + ctx: context.Background(), + }, + want: net.ParseIP(testIp), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + i := baseResolver{ + client: tt.fields.client, + url: tt.fields.url, + } + got, err := i.ResolvePublicIp(tt.args.ctx) + if (err != nil) != tt.wantErr { + t.Errorf("ResolvePublicIp() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("ResolvePublicIp() got = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/public_resolvers/ifconfigme.go b/public_resolvers/ifconfigme.go index 97bfbeb..88c3763 100644 --- a/public_resolvers/ifconfigme.go +++ b/public_resolvers/ifconfigme.go @@ -1,28 +1,17 @@ package public_resolvers import ( - "context" - "fmt" - "io" - "net" "net/http" "time" ) const ( IfConfigMeTag = "ifconfig.me" -) - -type Doer interface { - Do(*http.Request) (*http.Response, error) -} - -var ( ifConfigMeUrl = "https://ifconfig.me" ) type IfConfigMe struct { - client Doer + baseResolver } func NewDefaultIfConfigMe() *IfConfigMe { @@ -31,32 +20,11 @@ func NewDefaultIfConfigMe() *IfConfigMe { }) } -func NewIfConfigMe(c Doer) *IfConfigMe { - return &IfConfigMe{client: c} -} - -func (i IfConfigMe) ResolvePublicIp(ctx context.Context) (net.IP, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, ifConfigMeUrl, nil) - if err != nil { - return net.IP{}, fmt.Errorf("error creating ifconfig request: %w", err) - } - - resp, err := i.client.Do(req) - if err != nil { - return net.IP{}, err - } - defer func() { - _ = resp.Body.Close() - }() - - if resp.StatusCode != http.StatusOK { - return net.IP{}, fmt.Errorf("unexpected response code %d", resp.StatusCode) - } - - ipText, err := io.ReadAll(resp.Body) - if err != nil { - return net.IP{}, fmt.Errorf("error reading body: %w", err) - } - - return net.ParseIP(string(ipText)), nil +func NewIfConfigMe(client Doer) *IfConfigMe { + return &IfConfigMe{ + baseResolver: baseResolver{ + client: client, + url: ifConfigMeUrl, + }, + } } diff --git a/public_resolvers/v4identme.go b/public_resolvers/v4identme.go new file mode 100644 index 0000000..4252f0f --- /dev/null +++ b/public_resolvers/v4identme.go @@ -0,0 +1,30 @@ +package public_resolvers + +import ( + "net/http" + "time" +) + +const ( + V4IdentMeTag = "v4.ident.me" + v4IdentMeUrl = "https://v4.ident.me/" +) + +type V4IdentMe struct { + baseResolver +} + +func NewV4IdentMeDefault() *V4IdentMe { + return NewV4IdentMe(&http.Client{ + Timeout: 10 * time.Second, + }) +} + +func NewV4IdentMe(client Doer) *V4IdentMe { + return &V4IdentMe{ + baseResolver: baseResolver{ + client: client, + url: v4IdentMeUrl, + }, + } +}