feat: mvp (#1)
MVP including features: * Detect IP change * Configurable/generic external IP provider * Configurable/generic DNS provider * Impl. of plain external IP provider * Impl. of Ionos DNS provider Reviewed-on: #1 Co-authored-by: Timo Behrendt <t.behrendt@t00n.de> Co-committed-by: Timo Behrendt <t.behrendt@t00n.de>
This commit was merged in pull request #1.
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -21,3 +21,4 @@
|
|||||||
# Go workspace file
|
# Go workspace file
|
||||||
go.work
|
go.work
|
||||||
|
|
||||||
|
.vscode
|
||||||
|
|||||||
21
Dockerfile
Normal file
21
Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
FROM golang:1.21-alpine
|
||||||
|
|
||||||
|
# Set the Current Working Directory inside the container
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy go mod and sum files
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
|
||||||
|
# Download all dependencies. Dependencies will be cached if the go.mod and go.sum files are not changed
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
# Copy the source from the current directory to the Working Directory inside the container
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the Go app
|
||||||
|
RUN go build -o main .
|
||||||
|
|
||||||
|
|
||||||
|
# Command to run the executable
|
||||||
|
|
||||||
|
CMD ["./main"]
|
||||||
8
Makefile
Normal file
8
Makefile
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
test:
|
||||||
|
go test ./pkg/... -coverprofile=coverage.out
|
||||||
|
|
||||||
|
build:
|
||||||
|
go build
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golint ./...
|
||||||
53
README.md
53
README.md
@@ -1,2 +1,55 @@
|
|||||||
# realDynDNS
|
# realDynDNS
|
||||||
|
RealDynDNS aims to be a replacement to "classical" dynDNS solutions that offer a subdomain. Instead realDynDns actually changes your DNS entries.
|
||||||
|
|
||||||
|
This service requires your DNS provider to expose an API that allows your DNS entries to be changed.
|
||||||
|
A service that provides your current external IP is also required.
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
The configuration is done via a YAML file called `config.yaml`. The following example shows the configuration for a domain with two subdomains.
|
||||||
|
|
||||||
|
Configuration of the IP provider and the DNS provider is mandatory.
|
||||||
|
|
||||||
|
The `check_interval` is mandatory and follows the cron syntax including seconds. It defines how often the IP is checked and the DNS entries are updated. If you don't own the external ip provider, please be nice and don't set the interval too low.
|
||||||
|
|
||||||
|
You may configure multiple top level domains with multiple subdomains. If you also want the top level domain to be updated, add it to the subdomains list with an `@`.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
ip_provider:
|
||||||
|
type: plain
|
||||||
|
config:
|
||||||
|
url: https://ifconfig.me
|
||||||
|
dns_provider:
|
||||||
|
type: ionos
|
||||||
|
config:
|
||||||
|
api_key: <your-api-key>
|
||||||
|
base_url: https://api.hosting.ionos.com/dns
|
||||||
|
domains:
|
||||||
|
- tld: example.com
|
||||||
|
subdomains:
|
||||||
|
- "@"
|
||||||
|
- www
|
||||||
|
check_interval: 0 0 0/6 * * * *
|
||||||
|
```
|
||||||
|
|
||||||
|
## DNS Providers
|
||||||
|
The DNS provider abstracts the API of your DNS provider. Currently the following providers are supported:
|
||||||
|
|
||||||
|
### IONOS
|
||||||
|
IONOS requires two configuration parameters. You can get your API key [here](https://developer.hosting.ionos.com/docs/getstarted).
|
||||||
|
```yaml
|
||||||
|
api_key: <your-api-key>
|
||||||
|
base_url: https://api.hosting.ionos.com/dns
|
||||||
|
```
|
||||||
|
|
||||||
|
## External IP Providers
|
||||||
|
The external IP provider is used to get your current external IP. Currently the following providers are supported:
|
||||||
|
|
||||||
|
### Plain
|
||||||
|
Any provider that returns your IP as plain text can be used. The following configuration is required:
|
||||||
|
```yaml
|
||||||
|
url: <your-providers-URL>
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples for providers are:
|
||||||
|
- https://ifconfig.me
|
||||||
|
- https://api.ipify.org
|
||||||
16
config.example.yaml
Normal file
16
config.example.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
ip_provider:
|
||||||
|
type: plain
|
||||||
|
config:
|
||||||
|
url: https://ifconfig.me
|
||||||
|
dns_provider:
|
||||||
|
type: ionos
|
||||||
|
config:
|
||||||
|
api_key: <your-api-key>
|
||||||
|
base_url: https://api.hosting.ionos.com/dns
|
||||||
|
domains:
|
||||||
|
- tld: example.com
|
||||||
|
subdomains:
|
||||||
|
- "@"
|
||||||
|
- www
|
||||||
|
check_interval: 0 0 0/6 * * * *
|
||||||
15
go.mod
Normal file
15
go.mod
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
module realdnydns
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/go-co-op/gocron v1.31.2 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
|
github.com/stretchr/testify v1.8.4 // indirect
|
||||||
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
40
go.sum
Normal file
40
go.sum
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
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/go-co-op/gocron v1.31.2 h1:tAUW64bxYc5QlzEy2t30TnHX2+uInNDajKXxWi4SACA=
|
||||||
|
github.com/go-co-op/gocron v1.31.2/go.mod h1:39f6KNSGVOU1LO/ZOoZfcSxwlsJDQOKSu8erN0SH48Y=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
|
||||||
|
go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
72
main.go
Normal file
72
main.go
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"realdnydns/pkg/changeDetector"
|
||||||
|
"realdnydns/pkg/config"
|
||||||
|
"realdnydns/pkg/dnsProvider"
|
||||||
|
ionos "realdnydns/pkg/dnsProvider/ionos"
|
||||||
|
"realdnydns/pkg/externalIpProvider"
|
||||||
|
plainExternalIpProvider "realdnydns/pkg/externalIpProvider/plain"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-co-op/gocron"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
configClient := config.Config{}
|
||||||
|
err := configClient.Load("config.yaml")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var externalIpProvider externalIpProvider.ExternalIpProvider
|
||||||
|
switch configClient.ExternalIPProvider.Type {
|
||||||
|
case "plain":
|
||||||
|
var plainConfig plainExternalIpProvider.PlainExternalIpProviderConfig
|
||||||
|
err := configClient.ExternalIPProvider.ProviderConfig.Decode(&plainConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
externalIpProvider, err = plainExternalIpProvider.New(plainConfig)
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown external IP provider: %s", configClient.ExternalIPProvider.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
var dnsProvider dnsProvider.DNSProvider
|
||||||
|
switch configClient.DNSProvider.Type {
|
||||||
|
case "ionos":
|
||||||
|
var ionosConfig ionos.IONOSConfig
|
||||||
|
err := configClient.DNSProvider.ProviderConfig.Decode(&ionosConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
dnsProvider, err = ionos.NewIonos(&ionosConfig)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Errorf("unknown DNS provider: %s", configClient.DNSProvider.Type))
|
||||||
|
}
|
||||||
|
|
||||||
|
changeDetector := changeDetector.New(externalIpProvider, dnsProvider, configClient.Domains)
|
||||||
|
|
||||||
|
s := gocron.NewScheduler(time.UTC)
|
||||||
|
s.SingletonMode()
|
||||||
|
job, err := s.CronWithSeconds(configClient.CheckInterval).DoWithJobDetails(func(in string, job gocron.Job) {
|
||||||
|
numberChanged, err := changeDetector.DetectAndApplyChanges()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("Number of changes: %d\n", numberChanged)
|
||||||
|
fmt.Println("Next run:", job.NextRun())
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Starting scheduler")
|
||||||
|
fmt.Println("Next run:", job.NextRun())
|
||||||
|
s.StartBlocking()
|
||||||
|
}
|
||||||
9
model/common/common.go
Normal file
9
model/common/common.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
type ARecord struct {
|
||||||
|
Domain string
|
||||||
|
IP string
|
||||||
|
TTL int
|
||||||
|
Prio int
|
||||||
|
Disabled bool
|
||||||
|
}
|
||||||
53
pkg/changeDetector/changeDetector.go
Normal file
53
pkg/changeDetector/changeDetector.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package changeDetector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"realdnydns/pkg/config"
|
||||||
|
"realdnydns/pkg/dnsProvider"
|
||||||
|
"realdnydns/pkg/externalIpProvider"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChangeDetector struct {
|
||||||
|
externalIpProvider externalIpProvider.ExternalIpProvider
|
||||||
|
dnsProvider dnsProvider.DNSProvider
|
||||||
|
domains []config.DomainConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(
|
||||||
|
externalIpProvider externalIpProvider.ExternalIpProvider,
|
||||||
|
dnsProvider dnsProvider.DNSProvider,
|
||||||
|
domains []config.DomainConfig,
|
||||||
|
) ChangeDetector {
|
||||||
|
return ChangeDetector{
|
||||||
|
externalIpProvider: externalIpProvider,
|
||||||
|
dnsProvider: dnsProvider,
|
||||||
|
domains: domains,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ChangeDetector) DetectAndApplyChanges() (int, error) {
|
||||||
|
externalIp, err := c.externalIpProvider.GetExternalIp()
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var numberUpdated int
|
||||||
|
|
||||||
|
for _, domain := range c.domains {
|
||||||
|
for _, subdomain := range domain.Subdomains {
|
||||||
|
currentRecord, err := c.dnsProvider.GetRecord(domain.TLD, subdomain)
|
||||||
|
if err != nil {
|
||||||
|
return numberUpdated, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentRecord.IP != externalIp.String() {
|
||||||
|
_, err = c.dnsProvider.UpdateRecord(domain.TLD, subdomain, externalIp, currentRecord.TTL, currentRecord.Prio, currentRecord.Disabled)
|
||||||
|
numberUpdated++
|
||||||
|
if err != nil {
|
||||||
|
return numberUpdated, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return numberUpdated, nil
|
||||||
|
}
|
||||||
105
pkg/changeDetector/changeDetector_test.go
Normal file
105
pkg/changeDetector/changeDetector_test.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package changeDetector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"realdnydns/model/common"
|
||||||
|
"realdnydns/pkg/config"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockExternalIpProvider struct {
|
||||||
|
ExternalIp string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockExternalIpProvider) GetExternalIp() (net.IP, error) {
|
||||||
|
return net.ParseIP(m.ExternalIp), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockDNSProvider interface {
|
||||||
|
UpdateRecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error)
|
||||||
|
GetRecord(tld string, subdomain string) (*common.ARecord, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockDNSProviderImpl struct {
|
||||||
|
GetRecordIpResponse string
|
||||||
|
UpdateRecordIpResponse string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDNSProviderImpl) GetRecord(tld string, subdomain string) (*common.ARecord, error) {
|
||||||
|
return &common.ARecord{
|
||||||
|
IP: m.GetRecordIpResponse,
|
||||||
|
TTL: 10,
|
||||||
|
Prio: 20,
|
||||||
|
Disabled: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockDNSProviderImpl) UpdateRecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) {
|
||||||
|
return &common.ARecord{
|
||||||
|
IP: m.UpdateRecordIpResponse,
|
||||||
|
TTL: 10,
|
||||||
|
Prio: 20,
|
||||||
|
Disabled: false,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectAndApplyChanges(t *testing.T) {
|
||||||
|
t.Run("with changes", testDetectAndApplyChangesWithChanges())
|
||||||
|
t.Run("without changes", testDetectAndApplyChangesWithoutChanges())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDetectAndApplyChangesWithChanges() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
changeDetector := New(&MockExternalIpProvider{
|
||||||
|
ExternalIp: "127.0.0.1",
|
||||||
|
}, &MockDNSProviderImpl{
|
||||||
|
GetRecordIpResponse: "127.0.0.2",
|
||||||
|
UpdateRecordIpResponse: "127.0.0.1",
|
||||||
|
}, []config.DomainConfig{
|
||||||
|
{
|
||||||
|
TLD: "example.com",
|
||||||
|
Subdomains: []string{
|
||||||
|
"test",
|
||||||
|
"@",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
numberUpdated, err := changeDetector.DetectAndApplyChanges()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if numberUpdated != 2 {
|
||||||
|
t.Errorf("expected numberUpdated to be 2, got %v", numberUpdated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testDetectAndApplyChangesWithoutChanges() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
changeDetector := New(&MockExternalIpProvider{
|
||||||
|
ExternalIp: "127.0.0.1",
|
||||||
|
}, &MockDNSProviderImpl{
|
||||||
|
GetRecordIpResponse: "127.0.0.1",
|
||||||
|
UpdateRecordIpResponse: "127.0.0.1",
|
||||||
|
}, []config.DomainConfig{
|
||||||
|
{
|
||||||
|
TLD: "example.com",
|
||||||
|
Subdomains: []string{
|
||||||
|
"test",
|
||||||
|
"@",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
numberUpdated, err := changeDetector.DetectAndApplyChanges()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if numberUpdated != 0 {
|
||||||
|
t.Errorf("expected numberUpdated to be 0, got %v", numberUpdated)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pkg/config/__mocks__/testLoadCanFindFile.yaml
Normal file
16
pkg/config/__mocks__/testLoadCanFindFile.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
ip_provider:
|
||||||
|
type: plain
|
||||||
|
config:
|
||||||
|
url: https://ifconfig.me
|
||||||
|
dns_provider:
|
||||||
|
type: ionos
|
||||||
|
config:
|
||||||
|
api_key: exampleAPIKey
|
||||||
|
base_url: https://example.com
|
||||||
|
domains:
|
||||||
|
- tld: example.com
|
||||||
|
subdomains:
|
||||||
|
- "@"
|
||||||
|
- www
|
||||||
|
check_interval: 0 0 0/6 * * * *
|
||||||
43
pkg/config/config.go
Normal file
43
pkg/config/config.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DomainConfig struct {
|
||||||
|
TLD string `yaml:"tld"`
|
||||||
|
Subdomains []string `yaml:"subdomains"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ExternalIPProvider ExternalIpProviderConfig `yaml:"ip_provider"`
|
||||||
|
DNSProvider DNSProviderConfig `yaml:"dns_provider"`
|
||||||
|
Domains []DomainConfig `yaml:"domains"`
|
||||||
|
CheckInterval string `yaml:"check_interval"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExternalIpProviderConfig struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
ProviderConfig yaml.Node `yaml:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DNSProviderConfig struct {
|
||||||
|
Type string `yaml:"type"`
|
||||||
|
ProviderConfig yaml.Node `yaml:"config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Load(filePath string) error {
|
||||||
|
err := yaml.Unmarshal([]byte(filePath), c)
|
||||||
|
if err != nil {
|
||||||
|
inputConfig, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return yaml.Unmarshal(inputConfig, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
65
pkg/config/config_test.go
Normal file
65
pkg/config/config_test.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestLoad(t *testing.T) {
|
||||||
|
t.Run("Can find file", testLoadCanFindFile())
|
||||||
|
t.Run("Cannot find file", testLoadCannotFindFile())
|
||||||
|
t.Run("Unmarshals from direct input", testLoadUnmarshalsFromDirectInput())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadCanFindFile() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
|
||||||
|
c := Config{}
|
||||||
|
err := c.Load("./__mocks__/testLoadCanFindFile.yaml")
|
||||||
|
|
||||||
|
want := c.DNSProvider.Type == "ionos" && c.ExternalIPProvider.Type == "plain"
|
||||||
|
|
||||||
|
if !want || err != nil {
|
||||||
|
t.Fatalf("DnsProviderName couldn't be properly loaded or unmarshaled, Load() = %v, want %v", err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadCannotFindFile() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
c := Config{}
|
||||||
|
err := c.Load("nonexistent.yaml")
|
||||||
|
want := err != nil
|
||||||
|
|
||||||
|
if !want {
|
||||||
|
t.Fatalf("Config didn't throw an error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testLoadUnmarshalsFromDirectInput() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
c := Config{}
|
||||||
|
err := c.Load(`---
|
||||||
|
ip_provider:
|
||||||
|
type: plain
|
||||||
|
config:
|
||||||
|
url: https://ifconfig.me
|
||||||
|
dns_provider:
|
||||||
|
type: ionos
|
||||||
|
config:
|
||||||
|
api_key: exampleAPIKey
|
||||||
|
base_url: https://example.com
|
||||||
|
domains:
|
||||||
|
- tld: example.com
|
||||||
|
subdomains:
|
||||||
|
- "@"
|
||||||
|
- www
|
||||||
|
check_interval: 0 0 0/6 * * * *`)
|
||||||
|
|
||||||
|
want := c.DNSProvider.Type == "ionos" && c.ExternalIPProvider.Type == "plain"
|
||||||
|
|
||||||
|
if !want || err != nil {
|
||||||
|
t.Fatalf("DnsProviderName couldn't be properly loaded or unmarshaled, Load() = %v, want %v", err, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pkg/dnsProvider/__mocks__/testNewCanCreateIonosProvider.yaml
Normal file
16
pkg/dnsProvider/__mocks__/testNewCanCreateIonosProvider.yaml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
ip_provider:
|
||||||
|
type: plain
|
||||||
|
config:
|
||||||
|
url: https://ifconfig.me
|
||||||
|
dns_provider:
|
||||||
|
type: ionos
|
||||||
|
config:
|
||||||
|
api_key: exampleAPIKey
|
||||||
|
base_url: https://example.com
|
||||||
|
domains:
|
||||||
|
- tld: example.com
|
||||||
|
subdomains:
|
||||||
|
- "@"
|
||||||
|
- www
|
||||||
|
check_interval: 0 0 0/6 * * * *
|
||||||
11
pkg/dnsProvider/dnsProvider.go
Normal file
11
pkg/dnsProvider/dnsProvider.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package dnsProvider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"realdnydns/model/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DNSProvider interface {
|
||||||
|
UpdateRecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error)
|
||||||
|
GetRecord(tld string, subdomain string) (*common.ARecord, error)
|
||||||
|
}
|
||||||
213
pkg/dnsProvider/ionos/api/ionosAPI.go
Normal file
213
pkg/dnsProvider/ionos/api/ionosAPI.go
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package ionosAPI
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"realdnydns/model/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Docs: https://developer.hosting.ionos.com/docs/dns
|
||||||
|
*/
|
||||||
|
type IonosAPI interface {
|
||||||
|
GetARecord(tld string, subdomain string) (*common.ARecord, error)
|
||||||
|
SetARecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error)
|
||||||
|
GetZoneId(tld string) (string, error)
|
||||||
|
GetRecordId(zoneId string, tld string, subdomain string, recordType string) (string, error)
|
||||||
|
HttpCall(method string, url string, body io.Reader) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IonosAPIImpl struct {
|
||||||
|
APIKey string
|
||||||
|
BaseURL string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZonesResponse []struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Id string `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ZoneResponse struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Records []RecordResponse `json:"records"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecordResponse struct {
|
||||||
|
Id string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
RootName string `json:"rootName"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
ChangeDate string `json:"changeDate"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
Prio int `json:"prio"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeRecord struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Content string `json:"content"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
Prio int `json:"prio"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeRecordRequest struct {
|
||||||
|
Content string `json:"content"`
|
||||||
|
TTL int `json:"ttl"`
|
||||||
|
Prio int `json:"prio"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(APIKey string, BaseURL string) IonosAPI {
|
||||||
|
return &IonosAPIImpl{
|
||||||
|
APIKey: APIKey,
|
||||||
|
BaseURL: BaseURL,
|
||||||
|
HTTPClient: &http.Client{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IonosAPIImpl) HttpCall(method string, url string, body io.Reader) (*http.Response, error) {
|
||||||
|
req, err := http.NewRequest(method, i.BaseURL+url, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("X-API-Key", i.APIKey)
|
||||||
|
|
||||||
|
return i.HTTPClient.Do(req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IonosAPIImpl) GetZoneId(tld string) (string, error) {
|
||||||
|
res, err := i.HttpCall("GET", "/v1/zones", nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody := make([]byte, res.ContentLength)
|
||||||
|
res.Body.Read(responseBody)
|
||||||
|
|
||||||
|
zones := []ZoneResponse{}
|
||||||
|
json.Unmarshal(responseBody, &zones)
|
||||||
|
|
||||||
|
for _, z := range zones {
|
||||||
|
if z.Name == tld {
|
||||||
|
return z.Id, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("zone not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IonosAPIImpl) GetRecordId(zoneId string, tld string, subdomain string, recordType string) (string, error) {
|
||||||
|
res, err := i.HttpCall("GET", "/v1/zones/"+zoneId, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody := make([]byte, res.ContentLength)
|
||||||
|
res.Body.Read(responseBody)
|
||||||
|
|
||||||
|
zone := ZoneResponse{}
|
||||||
|
json.Unmarshal(responseBody, &zone)
|
||||||
|
|
||||||
|
var domain string
|
||||||
|
if subdomain != "" {
|
||||||
|
domain = subdomain + "." + tld
|
||||||
|
} else {
|
||||||
|
domain = tld
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range zone.Records {
|
||||||
|
if record.Type == recordType && record.Name == domain {
|
||||||
|
return record.Id, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", errors.New("record not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *IonosAPIImpl) SetARecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) {
|
||||||
|
zoneId, err := i.GetZoneId(tld)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
recordId, err := i.GetRecordId(zoneId, tld, subdomain, "A")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
changeRecordRequest, err := json.Marshal(ChangeRecordRequest{
|
||||||
|
Content: ip.String(),
|
||||||
|
TTL: ttl,
|
||||||
|
Prio: prio,
|
||||||
|
Disabled: false,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := i.HttpCall("PUT", "/v1/zones/"+zoneId+"/records/"+recordId, bytes.NewReader(changeRecordRequest))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return nil, errors.New("error updating record")
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody := make([]byte, res.ContentLength)
|
||||||
|
res.Body.Read(responseBody)
|
||||||
|
|
||||||
|
changeRecord := ChangeRecord{}
|
||||||
|
json.Unmarshal(responseBody, &changeRecord)
|
||||||
|
|
||||||
|
return &common.ARecord{
|
||||||
|
Domain: changeRecord.Name,
|
||||||
|
IP: changeRecord.Content,
|
||||||
|
TTL: changeRecord.TTL,
|
||||||
|
Prio: changeRecord.Prio,
|
||||||
|
Disabled: changeRecord.Disabled,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ionos *IonosAPIImpl) GetARecord(tld string, subdomain string) (*common.ARecord, error) {
|
||||||
|
zoneId, err := ionos.GetZoneId(tld)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
recordId, err := ionos.GetRecordId(zoneId, tld, subdomain, "A")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := ionos.HttpCall("GET", "/v1/zones/"+zoneId+"/records/"+recordId, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody := make([]byte, res.ContentLength)
|
||||||
|
res.Body.Read(responseBody)
|
||||||
|
|
||||||
|
record := RecordResponse{}
|
||||||
|
json.Unmarshal(responseBody, &record)
|
||||||
|
|
||||||
|
return &common.ARecord{
|
||||||
|
Domain: record.Name,
|
||||||
|
IP: record.Content,
|
||||||
|
TTL: record.TTL,
|
||||||
|
Prio: record.Prio,
|
||||||
|
Disabled: record.Disabled,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
284
pkg/dnsProvider/ionos/api/ionosAPI_test.go
Normal file
284
pkg/dnsProvider/ionos/api/ionosAPI_test.go
Normal file
@@ -0,0 +1,284 @@
|
|||||||
|
package ionosAPI
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func utilMockServerImpl() func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
zonesResponse := []ZoneResponse{
|
||||||
|
{
|
||||||
|
Name: "example.com",
|
||||||
|
Id: "1234567890",
|
||||||
|
Type: "NATIVE",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "notTheExample.org",
|
||||||
|
Id: "0987654321",
|
||||||
|
Type: "SLAVE",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
zonesResponseJson, _ := json.Marshal(zonesResponse)
|
||||||
|
|
||||||
|
recordResponseSub := RecordResponse{
|
||||||
|
Id: "abcdefghij",
|
||||||
|
Name: "example.com",
|
||||||
|
RootName: "example.com",
|
||||||
|
Type: "A",
|
||||||
|
Content: "127.0.0.1",
|
||||||
|
ChangeDate: "2019-12-09T13:04:25.772Z",
|
||||||
|
TTL: 300,
|
||||||
|
Prio: 0,
|
||||||
|
Disabled: false,
|
||||||
|
}
|
||||||
|
recordResponseSubJson, _ := json.Marshal(recordResponseSub)
|
||||||
|
|
||||||
|
recordResponseTLD := RecordResponse{
|
||||||
|
Id: "jihgfedcba",
|
||||||
|
Name: "sub.example.com",
|
||||||
|
RootName: "example.com",
|
||||||
|
Type: "A",
|
||||||
|
Content: "127.0.0.2",
|
||||||
|
ChangeDate: "2019-12-09T13:04:25.772Z",
|
||||||
|
TTL: 300,
|
||||||
|
Prio: 0,
|
||||||
|
Disabled: false,
|
||||||
|
}
|
||||||
|
recordResponseTLDJson, _ := json.Marshal(recordResponseTLD)
|
||||||
|
|
||||||
|
zoneResponse := ZoneResponse{
|
||||||
|
Id: "1234567890",
|
||||||
|
Name: "example.com",
|
||||||
|
Type: "NATIVE",
|
||||||
|
Records: []RecordResponse{
|
||||||
|
recordResponseSub,
|
||||||
|
recordResponseTLD,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
zoneResponseJson, _ := json.Marshal(zoneResponse)
|
||||||
|
|
||||||
|
changeRecord := ChangeRecord{
|
||||||
|
Name: "sub.example.com",
|
||||||
|
Type: "A",
|
||||||
|
Content: "127.0.0.1",
|
||||||
|
TTL: 300,
|
||||||
|
Prio: 0,
|
||||||
|
Disabled: false,
|
||||||
|
}
|
||||||
|
changeRecordJson, _ := json.Marshal(changeRecord)
|
||||||
|
|
||||||
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var response []byte
|
||||||
|
|
||||||
|
if r.Method == "GET" {
|
||||||
|
if r.RequestURI == "/v1/zones" {
|
||||||
|
response = zonesResponseJson
|
||||||
|
} else if r.RequestURI == "/v1/zones/1234567890" {
|
||||||
|
response = zoneResponseJson
|
||||||
|
} else if r.RequestURI == "/v1/zones/1234567890/records/abcdefghij" {
|
||||||
|
response = recordResponseSubJson
|
||||||
|
} else if r.RequestURI == "/v1/zones/1234567890/records/jihgfedcba" {
|
||||||
|
response = recordResponseTLDJson
|
||||||
|
}
|
||||||
|
} else if r.Method == "PUT" {
|
||||||
|
response = changeRecordJson
|
||||||
|
} else {
|
||||||
|
response = []byte("404")
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Write(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHttpCall(t *testing.T) {
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(utilMockServerImpl()))
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
ionosAPI := New("dummyKey", mockServer.URL)
|
||||||
|
|
||||||
|
t.Run("returns response for GET request", testHttpCallGet(ionosAPI))
|
||||||
|
t.Run("returns response for PUT request", testHttpCallPut(ionosAPI))
|
||||||
|
t.Run("returns error for non existing endpoint", testHttpCallNonExistingEndpoint())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHttpCallGet(api IonosAPI) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
res, err := api.HttpCall("GET", "/v1/zones", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("HttpCall() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
t.Fatalf("HttpCall() returned unexpected status code: %v instead of 200", res.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHttpCallPut(api IonosAPI) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
res, err := api.HttpCall("PUT", "/v1/zones/1234567890/records/abcdefghij", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("HttpCall() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
t.Fatalf("HttpCall() returned unexpected status code: %v instead of 200", res.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHttpCallNonExistingEndpoint() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(404)
|
||||||
|
}))
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
api := New("dummyKey", mockServer.URL)
|
||||||
|
|
||||||
|
res, err := api.HttpCall("GET", "/v1/nonExistingEndpoint", nil)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("HttpCall() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 404 {
|
||||||
|
t.Fatalf("HttpCall() returned unexpected status code: %v instead of 404", res.StatusCode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetZoneId(t *testing.T) {
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(utilMockServerImpl()))
|
||||||
|
|
||||||
|
ionosAPI := New("dummyKey", mockServer.URL)
|
||||||
|
|
||||||
|
t.Run("returns zoneId for tracked domain", testGetZoneIdSuccess(ionosAPI))
|
||||||
|
t.Run("returns error for non tracked domain", testGetZoneIdNoMatch(ionosAPI))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetZoneIdSuccess(api IonosAPI) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
zoneId, err := api.GetZoneId("example.com")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetZoneId() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if zoneId != "1234567890" {
|
||||||
|
t.Fatalf("GetZoneId() returned unexpected zoneId: %v instead of 1234567890", zoneId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetZoneIdNoMatch(api IonosAPI) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
zoneId, err := api.GetZoneId("nonTrackedDomain.com")
|
||||||
|
|
||||||
|
if err == nil || zoneId != "" {
|
||||||
|
t.Fatalf("GetZoneId() did not return an error for a non tracked domain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRecordId(t *testing.T) {
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(utilMockServerImpl()))
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
ionosAPI := New("dummyKey", mockServer.URL)
|
||||||
|
|
||||||
|
t.Run("returns zoneId for top level domain", testGetRecordIdForTopLevelDomain(ionosAPI))
|
||||||
|
t.Run("returns zoneId for subdomain", testGetRecordIdForSubdomain(ionosAPI))
|
||||||
|
t.Run("returns error for untracked subdomain", testGetRecordIdWithUntrackedSubdomain(ionosAPI))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetRecordIdForTopLevelDomain(api IonosAPI) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
recordId, err := api.GetRecordId("1234567890", "example.com", "", "A")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetZoneId() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if recordId != "abcdefghij" {
|
||||||
|
t.Fatalf("GetZoneId() returned unexpected zoneId: %v instead of abcdefghij", recordId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetRecordIdForSubdomain(api IonosAPI) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
recordId, err := api.GetRecordId("1234567890", "example.com", "sub", "A")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetZoneId() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if recordId != "jihgfedcba" {
|
||||||
|
t.Fatalf("GetZoneId() returned unexpected zoneId: %v instead of jihgfedcba", recordId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetRecordIdWithUntrackedSubdomain(api IonosAPI) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
recordId, err := api.GetRecordId("1234567890", "example.com", "untrackedSub", "A")
|
||||||
|
|
||||||
|
if err == nil && recordId == "" {
|
||||||
|
t.Fatalf("GetZoneId() did not return an error for a non tracked domain")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetARecord(t *testing.T) {
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(utilMockServerImpl()))
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
ionosAPI := New("dummyKey", mockServer.URL)
|
||||||
|
|
||||||
|
t.Run("returns A record for top level domain", testGetARecordFor(ionosAPI, "example.com", ""))
|
||||||
|
t.Run("returns A record for subdomain", testGetARecordFor(ionosAPI, "example.com", "sub"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetARecordFor(api IonosAPI, tld string, subdomain string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
var domain string
|
||||||
|
if subdomain == "" {
|
||||||
|
domain = tld
|
||||||
|
} else {
|
||||||
|
domain = subdomain + "." + tld
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := api.GetARecord(tld, subdomain)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetARecord() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.Domain != domain {
|
||||||
|
t.Fatalf("GetARecord() returned unexpected record for: %v instead of %v", record.Domain, domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetARecord(t *testing.T) {
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(utilMockServerImpl()))
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
ionosAPI := New("dummyKey", mockServer.URL)
|
||||||
|
|
||||||
|
changedARecord, err := ionosAPI.SetARecord("example.com", "sub", net.ParseIP("127.0.0.1"), 300, 0, false)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("SetARecord() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if changedARecord.Domain != "sub.example.com" {
|
||||||
|
t.Fatalf("SetARecord() returned unexpected record for: %v instead of sub.example.com", changedARecord.Domain)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
pkg/dnsProvider/ionos/ionos.go
Normal file
40
pkg/dnsProvider/ionos/ionos.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package ionosDnsProvider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"realdnydns/model/common"
|
||||||
|
"realdnydns/pkg/dnsProvider"
|
||||||
|
ionosAPI "realdnydns/pkg/dnsProvider/ionos/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IONOS struct {
|
||||||
|
API ionosAPI.IonosAPI
|
||||||
|
}
|
||||||
|
|
||||||
|
type IONOSConfig struct {
|
||||||
|
APIKey string `yaml:"api_key"`
|
||||||
|
BaseURL string `yaml:"base_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIonos(config *IONOSConfig) (dnsProvider.DNSProvider, error) {
|
||||||
|
if config.APIKey == "" {
|
||||||
|
return nil, errors.New("api_key is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.BaseURL == "" {
|
||||||
|
return nil, errors.New("base_url is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &IONOS{
|
||||||
|
ionosAPI.New(config.APIKey, config.BaseURL),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ionos *IONOS) UpdateRecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) {
|
||||||
|
return ionos.API.SetARecord(tld, subdomain, ip, ttl, prio, disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ionos *IONOS) GetRecord(tld string, subdomain string) (*common.ARecord, error) {
|
||||||
|
return ionos.API.GetARecord(tld, subdomain)
|
||||||
|
}
|
||||||
225
pkg/dnsProvider/ionos/ionos_test.go
Normal file
225
pkg/dnsProvider/ionos/ionos_test.go
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
package ionosDnsProvider_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
common "realdnydns/model/common"
|
||||||
|
ionosDnsProvider "realdnydns/pkg/dnsProvider/ionos"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
//t.Run("Config cannot be decoded", testNewConfigCannotBeDecoded())
|
||||||
|
t.Run("API key is required", testNewAPIKeyIsRequired())
|
||||||
|
t.Run("Base URL is required", testNewBaseURLIsRequired())
|
||||||
|
t.Run("Valid config", testNewValidConfig())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNewAPIKeyIsRequired() func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
config := ionosDnsProvider.IONOSConfig{
|
||||||
|
BaseURL: "https://api.ionos.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ionosDnsProvider.NewIonos(&config)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNewBaseURLIsRequired() func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
config := ionosDnsProvider.IONOSConfig{
|
||||||
|
APIKey: "1234",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ionosDnsProvider.NewIonos(&config)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error, got nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNewValidConfig() func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
config := ionosDnsProvider.IONOSConfig{
|
||||||
|
APIKey: "1234",
|
||||||
|
BaseURL: "https://api.ionos.com",
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := ionosDnsProvider.NewIonos(&config)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected nil, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockIonosAPI struct {
|
||||||
|
SetARecordFunc func(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error)
|
||||||
|
GetARecordFunc func(tld string, subdomain string) (*common.ARecord, error)
|
||||||
|
GetRecordIdFunc func(zoneId string, tld string, subdomain string, recordType string) (string, error)
|
||||||
|
GetZoneIdFunc func(tld string) (string, error)
|
||||||
|
HttpCallFunc func(method string, url string, body io.Reader) (*http.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockIonosAPI) SetARecord(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) {
|
||||||
|
return m.SetARecordFunc(tld, subdomain, ip, ttl, prio, disabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockIonosAPI) GetARecord(tld string, subdomain string) (*common.ARecord, error) {
|
||||||
|
return m.GetARecordFunc(tld, subdomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockIonosAPI) GetRecordId(zoneId string, tld string, subdomain string, recordType string) (string, error) {
|
||||||
|
return m.GetRecordIdFunc(zoneId, tld, subdomain, recordType)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockIonosAPI) GetZoneId(tld string) (string, error) {
|
||||||
|
return m.GetZoneIdFunc(tld)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockIonosAPI) HttpCall(method string, url string, body io.Reader) (*http.Response, error) {
|
||||||
|
return m.HttpCallFunc(method, url, body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUpdateRecord(t *testing.T) {
|
||||||
|
t.Run("API error", testUpdateRecordAPIError())
|
||||||
|
t.Run("API success", testUpdateRecordAPISuccess())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdateRecordAPIError() func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
api := &MockIonosAPI{
|
||||||
|
SetARecordFunc: func(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) {
|
||||||
|
return nil, errors.New("API error")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := ionosDnsProvider.IONOS{
|
||||||
|
API: api,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := provider.UpdateRecord("example.com", "sub", net.ParseIP("127.0.0.1"), 60, 0, false)
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testUpdateRecordAPISuccess() func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
api := &MockIonosAPI{
|
||||||
|
SetARecordFunc: func(tld string, subdomain string, ip net.IP, ttl int, prio int, disabled bool) (*common.ARecord, error) {
|
||||||
|
return &common.ARecord{
|
||||||
|
Domain: "sub",
|
||||||
|
IP: "127.0.0.1",
|
||||||
|
TTL: 60,
|
||||||
|
Prio: 0,
|
||||||
|
Disabled: false,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := ionosDnsProvider.IONOS{
|
||||||
|
API: api,
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := provider.UpdateRecord("example.com", "sub", net.ParseIP("127.0.0.1"), 60, 0, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected nil, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.Domain != "sub" {
|
||||||
|
t.Errorf("Expected sub, got %s", record.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.IP != "127.0.0.1" {
|
||||||
|
t.Errorf("Expected 127.0.0.1, got %s", record.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.TTL != 60 {
|
||||||
|
t.Errorf("Expected 60, got %d", record.TTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.Prio != 0 {
|
||||||
|
t.Errorf("Expected 0, got %d", record.Prio)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.Disabled {
|
||||||
|
t.Error("Expected false, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetRecord(t *testing.T) {
|
||||||
|
t.Run("API error", testGetRecordAPIError())
|
||||||
|
t.Run("API success", testGetRecordAPISuccess())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetRecordAPIError() func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
api := &MockIonosAPI{
|
||||||
|
GetARecordFunc: func(tld string, subdomain string) (*common.ARecord, error) {
|
||||||
|
return nil, errors.New("API error")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := ionosDnsProvider.IONOS{
|
||||||
|
API: api,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := provider.GetRecord("example.com", "sub")
|
||||||
|
if err == nil {
|
||||||
|
t.Error("Expected error, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetRecordAPISuccess() func(*testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
api := &MockIonosAPI{
|
||||||
|
GetARecordFunc: func(tld string, subdomain string) (*common.ARecord, error) {
|
||||||
|
return &common.ARecord{
|
||||||
|
Domain: "sub",
|
||||||
|
IP: "127.0.0.1",
|
||||||
|
TTL: 60,
|
||||||
|
Prio: 0,
|
||||||
|
Disabled: false,
|
||||||
|
}, nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
provider := ionosDnsProvider.IONOS{
|
||||||
|
API: api,
|
||||||
|
}
|
||||||
|
|
||||||
|
record, err := provider.GetRecord("example.com", "sub")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected nil, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.Domain != "sub" {
|
||||||
|
t.Errorf("expected sub, got %s", record.Domain)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.IP != "127.0.0.1" {
|
||||||
|
t.Errorf("expected 127.0.0.1, got %s", record.IP)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.TTL != 60 {
|
||||||
|
t.Errorf("expected 60, got %d", record.TTL)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.Prio != 0 {
|
||||||
|
t.Errorf("expected 0, got %d", record.Prio)
|
||||||
|
}
|
||||||
|
|
||||||
|
if record.Disabled {
|
||||||
|
t.Error("expected false, got true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
pkg/externalIpProvider/externalIpProvider.go
Normal file
9
pkg/externalIpProvider/externalIpProvider.go
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
package externalIpProvider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExternalIpProvider interface {
|
||||||
|
GetExternalIp() (net.IP, error)
|
||||||
|
}
|
||||||
56
pkg/externalIpProvider/plain/plain.go
Normal file
56
pkg/externalIpProvider/plain/plain.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package externalIpProvider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
externalIpProvider "realdnydns/pkg/externalIpProvider"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ExternalIpProviderImplPlain struct {
|
||||||
|
IPProviderURL string
|
||||||
|
HTTPClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type PlainExternalIpProviderConfig struct {
|
||||||
|
Url string `yaml:"url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(config PlainExternalIpProviderConfig) (externalIpProvider.ExternalIpProvider, error) {
|
||||||
|
if config.Url == "" {
|
||||||
|
return nil, errors.New("url is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ExternalIpProviderImplPlain{
|
||||||
|
config.Url,
|
||||||
|
&http.Client{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ExternalIpProviderImplPlain) GetExternalIp() (net.IP, error) {
|
||||||
|
parsedUrl, err := url.Parse(p.IPProviderURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := p.HTTPClient.Get(parsedUrl.String())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if res.StatusCode != 200 {
|
||||||
|
return nil, errors.New("unexpected status code")
|
||||||
|
}
|
||||||
|
|
||||||
|
responseBody := make([]byte, res.ContentLength)
|
||||||
|
res.Body.Read(responseBody)
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
parsedIp := net.ParseIP(string(responseBody))
|
||||||
|
if parsedIp == nil {
|
||||||
|
return nil, errors.New("unable to parse ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsedIp, nil
|
||||||
|
}
|
||||||
150
pkg/externalIpProvider/plain/plain_test.go
Normal file
150
pkg/externalIpProvider/plain/plain_test.go
Normal file
@@ -0,0 +1,150 @@
|
|||||||
|
package externalIpProvider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
t.Run("returns external ip provider", testNew())
|
||||||
|
t.Run("returns error when url is empty", testNewEmptyUrl())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNew() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
provider, err := New(PlainExternalIpProviderConfig{
|
||||||
|
Url: "http://localhost",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPlain() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if provider == nil {
|
||||||
|
t.Fatalf("NewPlain() returned nil provider")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testNewEmptyUrl() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
_, err := New(PlainExternalIpProviderConfig{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("NewPlain() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetExternal(t *testing.T) {
|
||||||
|
t.Run("returns external ip", testGetExternalIp())
|
||||||
|
t.Run("returns error when unable to parse ip", testGetExternalIpUnparsableIp())
|
||||||
|
t.Run("returns error when unexpected status code", testGetExternalIpUnexpectedStatusCode())
|
||||||
|
t.Run("returns error when unable to parse url", testGetExternalIpUnableToParseUrl())
|
||||||
|
t.Run("returns error when unable to get", testGetExternalIpUnableToGet())
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetExternalIp() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("127.0.0.1"))
|
||||||
|
},
|
||||||
|
))
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
provider, err := New(PlainExternalIpProviderConfig{
|
||||||
|
Url: mockServer.URL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPlain() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ip, err := provider.GetExternalIp()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetExternalIp() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ip.String() != "127.0.0.1" {
|
||||||
|
t.Fatalf("GetExternalIp() returned unexpected ip: %v instead of 127.0.0.1", ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetExternalIpUnparsableIp() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Write([]byte("not an ip"))
|
||||||
|
},
|
||||||
|
))
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
provider, err := New(PlainExternalIpProviderConfig{
|
||||||
|
Url: mockServer.URL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPlain() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = provider.GetExternalIp()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetExternalIp() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetExternalIpUnexpectedStatusCode() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
mockServer := httptest.NewServer(http.HandlerFunc(
|
||||||
|
func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(500)
|
||||||
|
},
|
||||||
|
))
|
||||||
|
defer mockServer.Close()
|
||||||
|
|
||||||
|
provider, err := New(PlainExternalIpProviderConfig{
|
||||||
|
Url: mockServer.URL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPlain() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = provider.GetExternalIp()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetExternalIp() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetExternalIpUnableToParseUrl() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
provider, err := New(PlainExternalIpProviderConfig{
|
||||||
|
Url: "not a url !'§%&",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPlain() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = provider.GetExternalIp()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetExternalIp() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testGetExternalIpUnableToGet() func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
// force error by using a non-responding url, beware of side effects
|
||||||
|
provider, err := New(PlainExternalIpProviderConfig{
|
||||||
|
Url: "http://localhost:1234",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPlain() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = provider.GetExternalIp()
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("GetExternalIp() returned unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user