You've already forked ddns-updater
Refactor: Moved all under internal.
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
FEAT: Directadmin provider is now working
This commit is contained in:
@@ -12,29 +12,32 @@ const (
|
||||
defaultCheckInterval = 5 * 60
|
||||
envKeyDnsToCheck = "DNS_NAMES"
|
||||
envKeyPublicIpResolverTag = "PUBLIC_IP_RESOLVER"
|
||||
envKeyDNSProviderTag = "DNS_PROVIDER"
|
||||
envKeyPublicDNSServer = "PUBLIC_DNS_SERVER"
|
||||
envKeyCloudflareApiKey = "CLOUDFLARE_API_KEY"
|
||||
envKeyCloudflareZone = "CLOUDFLARE_ZONE"
|
||||
envKeyDirectadminUser = "DA_USER"
|
||||
envKeyDirectadminKey = "DA_LOGIN_KEY"
|
||||
envKeyDirectadminUrl = "DA_LOGIN_URL"
|
||||
envKeyDirectadminKey = "DA_KEY"
|
||||
envKeyDirectadminUrl = "DA_URL"
|
||||
envKeyOnChangeComment = "ON_CHANGE_COMMENT"
|
||||
envKeyCheckIntervalSeconds = "CHECK_INTERVAL_SECONDS"
|
||||
envKeyNotifiers = "NOTIFIERS"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
DnsRecordsToCheck []string
|
||||
PublicIpResolverTag string
|
||||
DNSProviderTag string
|
||||
DirectadminUsername string
|
||||
DirectadminKey string
|
||||
DirectadminUrl string
|
||||
ApiToken string
|
||||
WebhookToken string
|
||||
CloudflareZone string
|
||||
OnChangeComment string
|
||||
Notifiers []string
|
||||
CheckInterval time.Duration
|
||||
DnsRecordsToCheck []string
|
||||
PublicIpResolverTag string
|
||||
PublicDNSServer string
|
||||
DNSProviderTag string
|
||||
DirectadminUsername string
|
||||
DirectadminKey string
|
||||
DirectadminUrl string
|
||||
ApiToken string
|
||||
WebhookToken string
|
||||
CloudflareZone string
|
||||
CloudflareOnChangeComment string
|
||||
Notifiers []string
|
||||
CheckInterval time.Duration
|
||||
}
|
||||
|
||||
func (c Config) Validate() error {
|
||||
@@ -44,21 +47,17 @@ func (c Config) Validate() error {
|
||||
return fmt.Errorf("empty api token env key %s", envKeyCloudflareApiKey)
|
||||
}
|
||||
|
||||
// if c.CloudflareZone == "" {
|
||||
// return fmt.Errorf("empty zone in env key %s", envKeyCloudflareZone)
|
||||
// }
|
||||
|
||||
case "directadmin":
|
||||
if c.DirectadminUrl == "" {
|
||||
return fmt.Errorf("empty DirectAdmin URL env key %s", envKeyDirectadminUrl)
|
||||
}
|
||||
|
||||
if c.DirectadminUsername == "" {
|
||||
return fmt.Errorf("empty Username in env key %s", envKeyDirectadminUser)
|
||||
return fmt.Errorf("empty DirectAdmin Username in env key %s", envKeyDirectadminUser)
|
||||
}
|
||||
|
||||
if c.DirectadminKey == "" {
|
||||
return fmt.Errorf("empty Login Key in env key %s", envKeyDirectadminKey)
|
||||
return fmt.Errorf("empty DirectAdmin Login Key in env key %s", envKeyDirectadminKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,16 +76,26 @@ func NewConfig() Config {
|
||||
}
|
||||
|
||||
return Config{
|
||||
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
||||
PublicIpResolverTag: os.Getenv(envKeyPublicIpResolverTag),
|
||||
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
||||
DirectadminUsername: os.Getenv(envKeyDirectadminUser),
|
||||
DirectadminKey: os.Getenv(envKeyDirectadminKey),
|
||||
DirectadminUrl: os.Getenv(envKeyDirectadminUrl),
|
||||
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
||||
OnChangeComment: os.Getenv(envKeyOnChangeComment),
|
||||
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
|
||||
CheckInterval: time.Duration(checkInterval) * time.Second,
|
||||
WebhookToken: os.Getenv("WEBHOOK_TOKEN"),
|
||||
DnsRecordsToCheck: parseCommaDelimited(os.Getenv(envKeyDnsToCheck)),
|
||||
DNSProviderTag: getEnvDefault(envKeyDNSProviderTag, "cloudflare"),
|
||||
PublicDNSServer: getEnvDefault(envKeyPublicDNSServer, "1.1.1.1"),
|
||||
PublicIpResolverTag: getEnvDefault(envKeyPublicIpResolverTag, "icanhazip"),
|
||||
ApiToken: os.Getenv(envKeyCloudflareApiKey),
|
||||
CloudflareZone: os.Getenv(envKeyCloudflareZone),
|
||||
CloudflareOnChangeComment: os.Getenv(envKeyOnChangeComment),
|
||||
DirectadminUsername: os.Getenv(envKeyDirectadminUser),
|
||||
DirectadminKey: os.Getenv(envKeyDirectadminKey),
|
||||
DirectadminUrl: os.Getenv(envKeyDirectadminUrl),
|
||||
Notifiers: parseCommaDelimited(os.Getenv(envKeyNotifiers)),
|
||||
CheckInterval: time.Duration(checkInterval) * time.Second,
|
||||
WebhookToken: os.Getenv("WEBHOOK_TOKEN"),
|
||||
}
|
||||
}
|
||||
|
||||
func getEnvDefault(key, fallback string) string {
|
||||
value, exists := os.LookupEnv(key)
|
||||
if !exists {
|
||||
value = fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
@@ -59,12 +59,12 @@ func TestConfig_Validate(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
c := Config{
|
||||
DnsRecordsToCheck: tt.fields.DnsRecordsToCheck,
|
||||
PublicIpResolverTag: tt.fields.PublicIpResolverTag,
|
||||
ApiToken: tt.fields.ApiToken,
|
||||
CloudflareZone: tt.fields.CloudflareZone,
|
||||
OnChangeComment: tt.fields.OnChangeComment,
|
||||
CheckInterval: tt.fields.CheckInterval,
|
||||
DnsRecordsToCheck: tt.fields.DnsRecordsToCheck,
|
||||
PublicIpResolverTag: tt.fields.PublicIpResolverTag,
|
||||
ApiToken: tt.fields.ApiToken,
|
||||
CloudflareZone: tt.fields.CloudflareZone,
|
||||
CloudflareOnChangeComment: tt.fields.OnChangeComment,
|
||||
CheckInterval: tt.fields.CheckInterval,
|
||||
}
|
||||
if err := c.Validate(); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
|
||||
|
||||
@@ -5,7 +5,8 @@ import (
|
||||
)
|
||||
|
||||
type DNSProvider interface {
|
||||
UpdateRecord(hostname string, ip string, old_ip string) error
|
||||
UpdateRecord(hostname string, ip string) error
|
||||
FetchRecord(hostname string) (string, error)
|
||||
}
|
||||
|
||||
type DomainParts struct {
|
||||
@@ -16,8 +17,15 @@ type DomainParts struct {
|
||||
func GetDomainParts(hostname string) *DomainParts {
|
||||
data := arrayToSlice(strings.Split(hostname, "."))
|
||||
|
||||
out := DomainParts{Name: data[0], Domain: strings.Join(data[1:], ".")}
|
||||
return &out
|
||||
if len(data) <= 2 {
|
||||
// This might be the actual root domain
|
||||
out := DomainParts{Name: strings.Join(data[0:], "."), Domain: strings.Join(data[0:], ".")}
|
||||
return &out
|
||||
} else {
|
||||
// This is a subdomain
|
||||
out := DomainParts{Name: data[0], Domain: strings.Join(data[1:], ".")}
|
||||
return &out
|
||||
}
|
||||
}
|
||||
|
||||
func arrayToSlice(array []string) []string {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"log"
|
||||
|
||||
"github.com/cloudflare/cloudflare-go"
|
||||
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
||||
"hub.cybercinch.nz/cybercinch/ddns-update/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,7 +18,7 @@ type CloudflareProvider struct {
|
||||
Config internal.Config
|
||||
}
|
||||
|
||||
func (d *CloudflareProvider) UpdateRecord(hostname string, ip string, old_ip string) error {
|
||||
func (d *CloudflareProvider) UpdateRecord(hostname string, ip string) error {
|
||||
// old_ip is not required for Cloudflare updates
|
||||
domain_parts := GetDomainParts(hostname)
|
||||
zoneId := d.FindZoneIdByName(domain_parts.Domain)
|
||||
@@ -33,8 +33,8 @@ func (d *CloudflareProvider) UpdateRecord(hostname string, ip string, old_ip str
|
||||
Content: ip,
|
||||
}
|
||||
|
||||
if d.Config.OnChangeComment != "" {
|
||||
update.Comment = &d.Config.OnChangeComment
|
||||
if d.Config.CloudflareOnChangeComment != "" {
|
||||
update.Comment = &d.Config.CloudflareOnChangeComment
|
||||
}
|
||||
|
||||
_, err = d.Client.UpdateDNSRecord(d.Context, cloudflare.ZoneIdentifier(zoneId), update)
|
||||
@@ -44,6 +44,13 @@ func (d *CloudflareProvider) UpdateRecord(hostname string, ip string, old_ip str
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *CloudflareProvider) FetchRecord(hostname string) (string, error) {
|
||||
domain_parts := GetDomainParts(hostname)
|
||||
zoneId := d.FindZoneIdByName(domain_parts.Domain)
|
||||
dnsRecord, err := d.GetDnsRecord(hostname, zoneId)
|
||||
return dnsRecord[0].Content, err
|
||||
}
|
||||
|
||||
func (d *CloudflareProvider) NewClient(api_token string) {
|
||||
api, err := cloudflare.NewWithAPIToken(api_token)
|
||||
if err != nil {
|
||||
|
||||
@@ -2,10 +2,12 @@ package dns_providers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/levelzerotechnology/directadmin-go"
|
||||
"github.com/mkelcik/cloudflare-ddns-update/internal"
|
||||
"hub.cybercinch.nz/cybercinch/ddns-update/internal"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -18,20 +20,54 @@ type Directadmin struct {
|
||||
Config internal.Config
|
||||
}
|
||||
|
||||
func (d *Directadmin) UpdateRecord(hostname string, ip string, old_ip string) error {
|
||||
type ListDNSRecordsParams struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (d *Directadmin) FetchRecord(hostname string) (string, error) {
|
||||
dnsRecord, err := d.GetDnsRecord(hostname)
|
||||
if err != nil {
|
||||
log.Fatal("unable to retrieve DNS record")
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dnsRecord[0].Value, nil
|
||||
}
|
||||
|
||||
func (d *Directadmin) UpdateRecord(hostname string, ip string) error {
|
||||
|
||||
result := GetDomainParts(hostname)
|
||||
current_record, _ := d.GetDnsRecord(hostname)
|
||||
|
||||
a := directadmin.DNSRecord{Name: result.Name, Ttl: 300, Type: "A", Value: old_ip}
|
||||
b := directadmin.DNSRecord{Name: result.Name, Ttl: 300, Type: "A", Value: ip}
|
||||
// Create an updated record for the new ip
|
||||
new_record := directadmin.DNSRecord{Name: current_record[0].Name, Ttl: current_record[0].Ttl, Type: current_record[0].Type, Value: ip}
|
||||
|
||||
err := d.Client.UpdateDNSRecord(result.Domain, a, b)
|
||||
err := d.Client.UpdateDNSRecord(result.Domain, current_record[0], new_record)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Directadmin) GetDnsRecord(hostname string) ([]directadmin.DNSRecord, error) {
|
||||
|
||||
domainParts := GetDomainParts(hostname)
|
||||
dnsRecords, err := d.Client.GetDNSRecords(domainParts.Domain)
|
||||
var slice []directadmin.DNSRecord
|
||||
|
||||
for _, dnsRecord := range dnsRecords {
|
||||
if domainParts.Name == dnsRecord.Name {
|
||||
slice = append(slice, dnsRecord)
|
||||
}
|
||||
}
|
||||
|
||||
if len(slice) == 0 {
|
||||
return nil, fmt.Errorf("unable to find DNS record for %s", hostname)
|
||||
}
|
||||
|
||||
return slice, err
|
||||
}
|
||||
|
||||
func (d *Directadmin) NewClient(server_url string, username string, key string) {
|
||||
api, err := directadmin.New(server_url, 5*time.Second, false, false)
|
||||
if err != nil {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package internal
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
func parseCommaDelimited(data string) []string {
|
||||
out := make([]string, 0, strings.Count(data, ",")+1)
|
||||
|
||||
81
internal/notifications/types.go
Normal file
81
internal/notifications/types.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
configDelimiter = "@"
|
||||
)
|
||||
|
||||
type Doer interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type Notifiers []Notifier
|
||||
|
||||
func (n Notifiers) NotifyWithLog(ctx context.Context, notification Notification) error {
|
||||
var outErr error
|
||||
for _, notifier := range n {
|
||||
if err := notifier.Notify(ctx, notification); err != nil {
|
||||
outErr = errors.Join(outErr, err)
|
||||
continue
|
||||
}
|
||||
log.Printf("Notification sent via %s\n", notifier.Tag())
|
||||
}
|
||||
return outErr
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
OldIp net.IP `json:"old_ip,omitempty"`
|
||||
NewIp net.IP `json:"new_ip"`
|
||||
CheckedAt time.Time `json:"checked_at"`
|
||||
ResolverTag string `json:"resolver_tag"`
|
||||
Domain string `json:"domain"`
|
||||
WebhookToken string `json:"token,omitempty"`
|
||||
}
|
||||
|
||||
func (n Notification) ToSlice() []string {
|
||||
return []string{n.OldIp.String(), n.NewIp.String(), n.CheckedAt.Format(time.RFC3339), n.ResolverTag, n.Domain}
|
||||
}
|
||||
|
||||
var Available = map[string]func(string) (Notifier, error){
|
||||
webhookTag: func(config string) (Notifier, error) {
|
||||
parts := strings.Split(config, configDelimiter)
|
||||
|
||||
if len(parts) < 2 {
|
||||
return nil, fmt.Errorf("wrong webhook config, missing url part")
|
||||
}
|
||||
|
||||
return NewWebhookNotification(WebhookConfig{Url: parts[1]}, &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
}), nil
|
||||
},
|
||||
}
|
||||
|
||||
type Notifier interface {
|
||||
Tag() string
|
||||
Notify(ctx context.Context, notification Notification) error
|
||||
}
|
||||
|
||||
func GetNotifiers(tags []string) Notifiers {
|
||||
out := Notifiers{}
|
||||
for _, t := range tags {
|
||||
if initFn, ok := Available[strings.Split(t, configDelimiter)[0]]; ok {
|
||||
notifier, err := initFn(t)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
continue
|
||||
}
|
||||
out = append(out, notifier)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
64
internal/notifications/webhook.go
Normal file
64
internal/notifications/webhook.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const (
|
||||
webhookTag = "webhook"
|
||||
)
|
||||
|
||||
type WebhookConfig struct {
|
||||
Url string
|
||||
}
|
||||
|
||||
type WebhookNotification struct {
|
||||
config WebhookConfig
|
||||
client Doer
|
||||
}
|
||||
|
||||
func (w WebhookNotification) Tag() string {
|
||||
return webhookTag
|
||||
}
|
||||
|
||||
func NewWebhookNotification(config WebhookConfig, client Doer) *WebhookNotification {
|
||||
return &WebhookNotification{config: config, client: client}
|
||||
}
|
||||
|
||||
func (w WebhookNotification) getRequestBody(notification Notification) (io.Reader, error) {
|
||||
out := bytes.NewBuffer(nil)
|
||||
if err := json.NewEncoder(out).Encode(notification); err != nil {
|
||||
return nil, fmt.Errorf("error encoding json notification body: %w", err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (w WebhookNotification) Notify(ctx context.Context, notification Notification) error {
|
||||
body, err := w.getRequestBody(notification)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebhookNotification::NotifyWithLog error: %w", err)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, w.config.Url, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebhookNotification::NotifyWithLog error creating request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := w.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("WebhookNotification::NotifyWithLog error while sending notification: %w", err)
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("WebhookNotification::NotifyWithLog unexpected non 2xx code %d returned", resp.StatusCode)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
55
internal/public_resolvers/base_resolver.go
Normal file
55
internal/public_resolvers/base_resolver.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
var NoIPInResponseError = errors.New("no ip found in response")
|
||||
|
||||
type Doer interface {
|
||||
Do(*http.Request) (*http.Response, error)
|
||||
}
|
||||
|
||||
type ipParserFunc func(reader io.Reader) (string, error)
|
||||
|
||||
func defaultIpParser(reader io.Reader) (string, error) {
|
||||
out, err := io.ReadAll(reader)
|
||||
return string(out), err
|
||||
}
|
||||
|
||||
type baseResolver struct {
|
||||
client Doer
|
||||
url string
|
||||
ipParser ipParserFunc
|
||||
}
|
||||
|
||||
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 := i.ipParser(resp.Body)
|
||||
if err != nil {
|
||||
return net.IP{}, fmt.Errorf("error reading body: %w", err)
|
||||
}
|
||||
|
||||
return net.ParseIP(ipText), nil
|
||||
}
|
||||
100
internal/public_resolvers/base_resolver_test.go
Normal file
100
internal/public_resolvers/base_resolver_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
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: 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
|
||||
fn ipParserFunc
|
||||
}
|
||||
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,
|
||||
fn: defaultIpParser,
|
||||
},
|
||||
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,
|
||||
ipParser: tt.fields.fn,
|
||||
}
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
49
internal/public_resolvers/cloudflare_trace.go
Normal file
49
internal/public_resolvers/cloudflare_trace.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
CloudflareTraceTag = "1.1.1.1"
|
||||
CloudflareTraceUrl = "https://1.1.1.1/cdn-cgi/trace"
|
||||
|
||||
ipPrefix = "ip="
|
||||
)
|
||||
|
||||
type CloudflareTrace struct {
|
||||
baseResolver
|
||||
}
|
||||
|
||||
func NewDefaultCloudflareTrace() *CloudflareTrace {
|
||||
return NewCloudflareTrace(&http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
func cloudflareTraceResponseParser(reader io.Reader) (string, error) {
|
||||
data, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, row := range strings.Split(string(data), "\n") {
|
||||
if strings.Index(row, ipPrefix) == 0 {
|
||||
return strings.TrimSpace(strings.ReplaceAll(row, ipPrefix, "")), nil
|
||||
}
|
||||
}
|
||||
return "", NoIPInResponseError
|
||||
}
|
||||
|
||||
func NewCloudflareTrace(client Doer) *CloudflareTrace {
|
||||
return &CloudflareTrace{
|
||||
baseResolver: baseResolver{
|
||||
client: client,
|
||||
url: CloudflareTraceUrl,
|
||||
ipParser: cloudflareTraceResponseParser,
|
||||
},
|
||||
}
|
||||
}
|
||||
78
internal/public_resolvers/cloudflare_trace_test.go
Normal file
78
internal/public_resolvers/cloudflare_trace_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_cloudflareTraceResponseParser(t *testing.T) {
|
||||
|
||||
type args struct {
|
||||
reader io.Reader
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "ok",
|
||||
args: args{
|
||||
reader: bytes.NewBuffer([]byte(`fl=31f118
|
||||
h=1.1.1.1
|
||||
ip=94.113.142.206
|
||||
ts=1683145336.383
|
||||
visit_scheme=https
|
||||
uag=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
|
||||
colo=PRG
|
||||
sliver=none
|
||||
http=http/2
|
||||
loc=CZ
|
||||
tls=TLSv1.3
|
||||
sni=off
|
||||
warp=off
|
||||
gateway=off
|
||||
rbi=off
|
||||
kex=X25519`)),
|
||||
},
|
||||
want: "94.113.142.206",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "no ip in response",
|
||||
args: args{
|
||||
reader: bytes.NewBuffer([]byte(`fl=31f118
|
||||
h=1.1.1.1
|
||||
ts=1683145336.383
|
||||
visit_scheme=https
|
||||
uag=Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Safari/537.36
|
||||
colo=PRG
|
||||
sliver=none
|
||||
http=http/2
|
||||
loc=CZ
|
||||
tls=TLSv1.3
|
||||
sni=off
|
||||
warp=off
|
||||
gateway=off
|
||||
rbi=off
|
||||
kex=X25519`)),
|
||||
},
|
||||
want: "",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := cloudflareTraceResponseParser(tt.args.reader)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("cloudflareTraceResponseParser() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("cloudflareTraceResponseParser() got = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
31
internal/public_resolvers/icanhazip.go
Normal file
31
internal/public_resolvers/icanhazip.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
IcanhazipTag = "icanhazip"
|
||||
IcanhazipUrl = "https://v4.icanhazip.com/"
|
||||
)
|
||||
|
||||
type Icanhazip struct {
|
||||
baseResolver
|
||||
}
|
||||
|
||||
func NewIcanhazipDefault() *Icanhazip {
|
||||
return NewIcanhazip(&http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
func NewIcanhazip(client Doer) *Icanhazip {
|
||||
return &Icanhazip{
|
||||
baseResolver: baseResolver{
|
||||
client: client,
|
||||
url: v4IdentMeUrl,
|
||||
ipParser: defaultIpParser,
|
||||
},
|
||||
}
|
||||
}
|
||||
31
internal/public_resolvers/ifconfigme.go
Normal file
31
internal/public_resolvers/ifconfigme.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package public_resolvers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
IfConfigMeTag = "ifconfig.me"
|
||||
ifConfigMeUrl = "https://ifconfig.me"
|
||||
)
|
||||
|
||||
type IfConfigMe struct {
|
||||
baseResolver
|
||||
}
|
||||
|
||||
func NewDefaultIfConfigMe() *IfConfigMe {
|
||||
return NewIfConfigMe(&http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
})
|
||||
}
|
||||
|
||||
func NewIfConfigMe(client Doer) *IfConfigMe {
|
||||
return &IfConfigMe{
|
||||
baseResolver: baseResolver{
|
||||
client: client,
|
||||
url: ifConfigMeUrl,
|
||||
ipParser: defaultIpParser,
|
||||
},
|
||||
}
|
||||
}
|
||||
31
internal/public_resolvers/v4identme.go
Normal file
31
internal/public_resolvers/v4identme.go
Normal file
@@ -0,0 +1,31 @@
|
||||
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,
|
||||
ipParser: defaultIpParser,
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user