From 8ba6bb8646817c6926ac1a75f3ee553e573097f2 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 24 Feb 2024 18:32:39 -0600 Subject: [PATCH 1/5] refactor to allow for multiple registrar accounts --- build-docker.sh | 41 +++++++-------- config.json | 1 - gandi_service.go | 131 +++++++++++++++++++++++++++++++---------------- main.go | 121 +++++++++++-------------------------------- 4 files changed, 139 insertions(+), 155 deletions(-) diff --git a/build-docker.sh b/build-docker.sh index 00256b9..3d9a138 100755 --- a/build-docker.sh +++ b/build-docker.sh @@ -1,42 +1,43 @@ #!/bin/bash -e -VERSION="0.0.1" +VERSION="0.1.0" rm -rf dockerbuild || true mkdir dockerbuild -#cp Dockerfile dockerbuild/Dockerfile-amd64 +cp Dockerfile dockerbuild/Dockerfile-amd64 cp Dockerfile dockerbuild/Dockerfile-arm #cp Dockerfile dockerbuild/Dockerfile-arm64 -#sed -E 's|FROM alpine|FROM amd64/alpine|' -i dockerbuild/Dockerfile-amd64 +sed -E 's|FROM alpine|FROM amd64/alpine|' -i dockerbuild/Dockerfile-amd64 sed -E 's|FROM alpine|FROM arm32v7/alpine|' -i dockerbuild/Dockerfile-arm #sed -E 's|FROM alpine|FROM arm64v8/alpine|' -i dockerbuild/Dockerfile-arm64 -#sed -E 's/GOARCH=/GOARCH=amd64/' -i dockerbuild/Dockerfile-amd64 +sed -E 's/GOARCH=/GOARCH=amd64/' -i dockerbuild/Dockerfile-amd64 sed -E 's/GOARCH=/GOARCH=arm/' -i dockerbuild/Dockerfile-arm #sed -E 's/GOARCH=/GOARCH=arm64/' -i dockerbuild/Dockerfile-arm64 -#docker build -f dockerbuild/Dockerfile-amd64 -t sequentialread/gandi-dns-updater:$VERSION-amd64 . -docker build -f dockerbuild/Dockerfile-arm -t sequentialread/gandi-dns-updater:$VERSION-arm . -#docker build -f dockerbuild/Dockerfile-arm64 -t sequentialread/gandi-dns-updater:$VERSION-arm64 . +docker build -f dockerbuild/Dockerfile-amd64 -t sequentialread/dns-updater:$VERSION-amd64 . +docker build -f dockerbuild/Dockerfile-arm -t sequentialread/dns-updater:$VERSION-arm . +#docker build -f dockerbuild/Dockerfile-arm64 -t sequentialread/dns-updater:$VERSION-arm64 . -#docker push sequentialread/gandi-dns-updater:$VERSION-amd64 -docker push sequentialread/gandi-dns-updater:$VERSION-arm -#docker push sequentialread/gandi-dns-updater:$VERSION-arm64 +docker push sequentialread/dns-updater:$VERSION-amd64 +docker push sequentialread/dns-updater:$VERSION-arm +#docker push sequentialread/dns-updater:$VERSION-arm64 export DOCKER_CLI_EXPERIMENTAL=enabled -# docker manifest create sequentialread/gandi-dns-updater:$VERSION \ -# sequentialread/gandi-dns-updater:$VERSION-amd64 \ -# sequentialread/gandi-dns-updater:$VERSION-arm \ -# sequentialread/gandi-dns-updater:$VERSION-arm64 +# docker manifest create sequentialread/dns-updater:$VERSION \ +# sequentialread/dns-updater:$VERSION-amd64 \ +# sequentialread/dns-updater:$VERSION-arm \ +# sequentialread/dns-updater:$VERSION-arm64 -docker manifest create sequentialread/gandi-dns-updater:$VERSION \ - sequentialread/gandi-dns-updater:$VERSION-arm \ +docker manifest create sequentialread/dns-updater:$VERSION \ + sequentialread/dns-updater:$VERSION-arm \ + sequentialread/dns-updater:$VERSION-amd64 -#docker manifest annotate --arch amd64 sequentialread/gandi-dns-updater:$VERSION sequentialread/gandi-dns-updater:$VERSION-amd64 -docker manifest annotate --arch arm sequentialread/gandi-dns-updater:$VERSION sequentialread/gandi-dns-updater:$VERSION-arm -#docker manifest annotate --arch arm64 sequentialread/gandi-dns-updater:$VERSION sequentialread/gandi-dns-updater:$VERSION-arm64 +docker manifest annotate --arch amd64 sequentialread/dns-updater:$VERSION sequentialread/dns-updater:$VERSION-amd64 +docker manifest annotate --arch arm sequentialread/dns-updater:$VERSION sequentialread/dns-updater:$VERSION-arm +#docker manifest annotate --arch arm64 sequentialread/dns-updater:$VERSION sequentialread/dns-updater:$VERSION-arm64 -docker manifest push sequentialread/gandi-dns-updater:$VERSION \ No newline at end of file +docker manifest push sequentialread/dns-updater:$VERSION \ No newline at end of file diff --git a/config.json b/config.json index 80c9136..2387035 100644 --- a/config.json +++ b/config.json @@ -1,5 +1,4 @@ { - "GandiRecordTTLSeconds": 300, "HealthPollingSeconds": 10, "ResetAfterConsecutiveFailures": 10 } \ No newline at end of file diff --git a/gandi_service.go b/gandi_service.go index 79f92c6..7ce3518 100644 --- a/gandi_service.go +++ b/gandi_service.go @@ -1,9 +1,12 @@ package main import ( + "bytes" + "encoding/json" "fmt" "io" "io/ioutil" + "log" "net/http" "time" @@ -11,9 +14,10 @@ import ( ) type GandiService struct { - Client *http.Client - APIUrl string - APIKey string + Client *http.Client + APIUrl string + APIKey string + RecordTTLSeconds int } type GandiDomainRecords struct { @@ -28,55 +32,94 @@ type GandiDomainRecord struct { TTL int `json:"rrset_ttl"` } -func NewGandiService(apiKey string) *GandiService { +func NewGandiService(apiKey string, recordTTLSeconds int) *GandiService { return &GandiService{ - Client: &http.Client{Timeout: 30 * time.Second}, - APIUrl: "https://api.gandi.net/v5", - APIKey: apiKey, + Client: &http.Client{Timeout: 30 * time.Second}, + APIUrl: "https://api.gandi.net/v5", + APIKey: apiKey, + RecordTTLSeconds: recordTTLSeconds, } } -// func (service *GandiService) UpdateFreeSubdomains(freeSubdomains map[string][]string) error { - -// requestBody := GandiDomainRecords{ -// Items: []GandiDomainRecord{}, -// } -// for subdomain, ips := range freeSubdomains { -// requestBody.Items = append( -// requestBody.Items, -// GandiDomainRecord{ -// Name: subdomain, -// Type: "A", -// Values: ips, -// TTL: 300, -// }, -// GandiDomainRecord{ -// Name: fmt.Sprintf("*.%s", subdomain), -// Type: "A", -// Values: ips, -// TTL: 300, -// }, -// ) -// } +func (service *GandiService) TryToUpdateRecordsForDomain(domain DomainConfig, currentPublicIPv4 string) bool { + path := fmt.Sprintf("/livedns/domains/%s/records", domain.Domain) + statusCode, responseBytes, err := service.GandiHTTP("GET", path, nil) + if err != nil { + // if gandi isn't responding at all, we can just give up and try again in 10 seconds. + log.Println("waiting for gandi to respond...") + return false + } + if statusCode >= 300 { + // if we got a non-200 status code from gandi, thats a fatal error. Log it. + log.Printf("gandi (GET %s) returned http %d: \n%s\n\n", path, statusCode, responseBytes) + return false + } + var gandiResourceRecords []GandiDomainRecord + err = json.Unmarshal(responseBytes, &gandiResourceRecords) + if err != nil { + log.Printf("gandi (GET %s) returned http %d, but it didn't return valid json: %s: \n%s\n\n", path, statusCode, err, responseBytes) + return false + } -// if len(requestBody.Items) == 0 { -// return nil -// } + configIndexesToAdd := map[int]bool{} + for i, _ := range domain.Records { + configIndexesToAdd[i] = true + } + newRecords := []GandiDomainRecord{} + for _, gandiRecord := range gandiResourceRecords { + addedConfigRecord := false + for i, configRecord := range domain.Records { + // log.Println(configRecord.Name == gandiRecord.Name, " AND ", configRecord.Type == gandiRecord.Type, + // " | ", configRecord.Name, "==", gandiRecord.Name, " AND ", configRecord.Type, "==", gandiRecord.Type) + if configRecord.Name == gandiRecord.Name && recordTypesMatch(configRecord.Type, gandiRecord.Type) { + if configIndexesToAdd[i] && (len(gandiRecord.Values) == 0 || gandiRecord.Values[0] != currentPublicIPv4) { + configIndexesToAdd[i] = false + addedConfigRecord = true + newRecords = append(newRecords, GandiDomainRecord{ + Name: configRecord.Name, + Type: configRecord.Type, + TTL: service.RecordTTLSeconds, + Values: []string{currentPublicIPv4}, + }) + } + } + } + if !addedConfigRecord { + newRecords = append(newRecords, gandiRecord) + } + } + for i, configRecord := range domain.Records { + if configIndexesToAdd[i] { + newRecords = append([]GandiDomainRecord{{ + Name: configRecord.Name, + Type: configRecord.Type, + TTL: service.RecordTTLSeconds, + Values: []string{currentPublicIPv4}, + }}, newRecords...) + } + } -// requestBodyBytes, err := json.Marshal(requestBody) -// if err != nil { -// return err -// } + requestBody := GandiDomainRecords{ + RemoveApexNS: false, + Items: newRecords, + } + requestBodyBytes, err := json.Marshal(requestBody) + if err != nil { + panic(err) + } -// endpoint := fmt.Sprintf("/livedns/domains/%s/records", "") -// statusCode, responseBytes, err := service.GandiHTTP("PUT", endpoint, bytes.NewBuffer(requestBodyBytes)) -// if err != nil { -// log.Printf("Failed to update free subdomains on gandi: %s \n\nResponse was: %s\n\n", err, string(responseBytes)) -// return err -// } + log.Printf("replacing all resource records for the domain %s...\n%s\n\n", domain.Domain, string(requestBodyBytes)) + endpoint := fmt.Sprintf("/livedns/domains/%s/records", domain.Domain) + statusCode, responseBytes, err = service.GandiHTTP("PUT", endpoint, bytes.NewBuffer(requestBodyBytes)) + if err != nil { + panic(err) + } + if statusCode >= 300 { + log.Printf("gandi (PUT %s) returned http %d. we PUT the data: %s\n\n and gandi responded with:\n%s\n\n", endpoint, statusCode, string(requestBodyBytes), responseBytes) + } -// return nil -// } + return true +} func (service *GandiService) GandiHTTP(method string, path string, body io.Reader) (int, []byte, error) { return service.MyHTTP( diff --git a/main.go b/main.go index 9465c5a..e1f564f 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "encoding/json" "fmt" "log" @@ -20,15 +19,22 @@ type Config struct { PingPongLog bool ResetAfterConsecutiveFailures int HealthPollingSeconds int - GandiRecordTTLSeconds int - GandiAPIKey string + RegistrarAccounts []RegistrarAccount DNSResolvers []string Domains []DomainConfig } +type RegistrarAccount struct { + Label string + Provider string + APIKey string + RecordTTLSeconds int +} + type DomainConfig struct { - Domain string - Records []RecordConfig + RegistrarAccountLabel string + Domain string + Records []RecordConfig } type RecordConfig struct { @@ -49,6 +55,10 @@ type domainState struct { failures int } +type registrarAccount interface { + TryToUpdateRecordsForDomain(domain DomainConfig, currentPublicIPv4 string) bool +} + var lastConsensusIpv4GeneratedAt time.Time var lastConsensusIpv4 string var lastConsensusIpv4Err error @@ -64,6 +74,8 @@ var dnsRecordTypesWithSameSemantics = [][]string{ var dnsResolvers map[string]*dnsresolver.DnsResolver +var registrarAccounts map[string]registrarAccount + func main() { var err error @@ -87,7 +99,16 @@ func main() { dnsResolvers[address] = dnsresolver.New(ipAddressStrings) } - gandi := NewGandiService(config.GandiAPIKey) + registrarAccounts = make(map[string]registrarAccount) + + for _, accountInfo := range config.RegistrarAccounts { + if accountInfo.Provider == "gandi" { + registrarAccounts[accountInfo.Label] = NewGandiService(accountInfo.APIKey, accountInfo.RecordTTLSeconds) + } + // if accountInfo.Provider == "porkbun" { + // registrarAccounts[accountInfo.Label] = NewPorkbunService(accountInfo.APIKey, accountInfo.RecordTTLSeconds) + // } + } // this sets lastConsensusIpv4 waitSeconds := 5 @@ -111,7 +132,7 @@ func main() { monitorChannel := make(chan bool) - go constantlyMonitorDomains(config, gandi, monitorChannel) + go constantlyMonitorDomains(config, monitorChannel) lastMonitorUpdate := time.Now() for { @@ -135,7 +156,7 @@ func main() { } -func constantlyMonitorDomains(config *Config, gandi *GandiService, monitorChannel chan bool) { +func constantlyMonitorDomains(config *Config, monitorChannel chan bool) { debug.SetPanicOnFault(true) @@ -265,7 +286,8 @@ func constantlyMonitorDomains(config *Config, gandi *GandiService, monitorChanne currentPublicIPv4, err := getConsensusPublicIpv4WithCache(config) if err == nil && state.recentlySetIPv4 != currentPublicIPv4 { - updated := tryToUpdateGandiRecordsForDomain(gandi, config, domain, currentPublicIPv4) + registrarAccount := registrarAccounts[domain.RegistrarAccountLabel] + updated := registrarAccount.TryToUpdateRecordsForDomain(domain, currentPublicIPv4) if updated { state.recentlySetIPv4 = currentPublicIPv4 } @@ -282,87 +304,6 @@ func constantlyMonitorDomains(config *Config, gandi *GandiService, monitorChanne time.Sleep(time.Second * time.Duration(config.HealthPollingSeconds)) } - -} - -func tryToUpdateGandiRecordsForDomain(gandi *GandiService, config *Config, domain DomainConfig, currentPublicIPv4 string) bool { - path := fmt.Sprintf("/livedns/domains/%s/records", domain.Domain) - statusCode, responseBytes, err := gandi.GandiHTTP("GET", path, nil) - if err != nil { - // if gandi isn't responding at all, we can just give up and try again in 10 seconds. - log.Println("waiting for gandi to respond...") - return false - } - if statusCode >= 300 { - // if we got a non-200 status code from gandi, thats a fatal error. Log it. - log.Printf("gandi (GET %s) returned http %d: \n%s\n\n", path, statusCode, responseBytes) - return false - } - var gandiResourceRecords []GandiDomainRecord - err = json.Unmarshal(responseBytes, &gandiResourceRecords) - if err != nil { - log.Printf("gandi (GET %s) returned http %d, but it didn't return valid json: %s: \n%s\n\n", path, statusCode, err, responseBytes) - return false - } - - configIndexesToAdd := map[int]bool{} - for i, _ := range domain.Records { - configIndexesToAdd[i] = true - } - newRecords := []GandiDomainRecord{} - for _, gandiRecord := range gandiResourceRecords { - addedConfigRecord := false - for i, configRecord := range domain.Records { - // log.Println(configRecord.Name == gandiRecord.Name, " AND ", configRecord.Type == gandiRecord.Type, - // " | ", configRecord.Name, "==", gandiRecord.Name, " AND ", configRecord.Type, "==", gandiRecord.Type) - if configRecord.Name == gandiRecord.Name && recordTypesMatch(configRecord.Type, gandiRecord.Type) { - if configIndexesToAdd[i] && (len(gandiRecord.Values) == 0 || gandiRecord.Values[0] != currentPublicIPv4) { - configIndexesToAdd[i] = false - addedConfigRecord = true - newRecords = append(newRecords, GandiDomainRecord{ - Name: configRecord.Name, - Type: configRecord.Type, - TTL: config.GandiRecordTTLSeconds, - Values: []string{currentPublicIPv4}, - }) - } - } - } - if !addedConfigRecord { - newRecords = append(newRecords, gandiRecord) - } - } - for i, configRecord := range domain.Records { - if configIndexesToAdd[i] { - newRecords = append([]GandiDomainRecord{{ - Name: configRecord.Name, - Type: configRecord.Type, - TTL: config.GandiRecordTTLSeconds, - Values: []string{currentPublicIPv4}, - }}, newRecords...) - } - } - - requestBody := GandiDomainRecords{ - RemoveApexNS: false, - Items: newRecords, - } - requestBodyBytes, err := json.Marshal(requestBody) - if err != nil { - panic(err) - } - - log.Printf("replacing all resource records for the domain %s...\n%s\n\n", domain.Domain, string(requestBodyBytes)) - endpoint := fmt.Sprintf("/livedns/domains/%s/records", domain.Domain) - statusCode, responseBytes, err = gandi.GandiHTTP("PUT", endpoint, bytes.NewBuffer(requestBodyBytes)) - if err != nil { - panic(err) - } - if statusCode >= 300 { - log.Printf("gandi (PUT %s) returned http %d. we PUT the data: %s\n\n and gandi responded with:\n%s\n\n", endpoint, statusCode, string(requestBodyBytes), responseBytes) - } - - return true } func getConsensusPublicIpv4WithCache(config *Config) (string, error) { -- 2.34.2 From 22ce5879a8459d3f8acfb07c2fb0f95acc784915 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 24 Feb 2024 18:43:40 -0600 Subject: [PATCH 2/5] config validation for RegistrarAccounts --- config.go | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/config.go b/config.go index 7da6292..f97b4ed 100644 --- a/config.go +++ b/config.go @@ -18,26 +18,49 @@ func GetConfig() *Config { panic(errorspkg.Wrap(err, "ReadConfiguration returned")) } - if config.GandiAPIKey == "" { - panic(errorspkg.New("GandiAPIKey is required")) + if len(config.RegistrarAccounts) < 1 { + log.Fatal("at least one RegistrarAccount is required") } if len(config.Domains) == 0 { - panic(errorspkg.New("at least one domain is required")) - } - if config.GandiRecordTTLSeconds < 300 { - panic(errorspkg.New("GandiRecordTTLSeconds must be 300 or greater")) + log.Fatal("at least one domain is required") } if config.ResetAfterConsecutiveFailures < 3 { - panic(errorspkg.New("ResetAfterConsecutiveFailures must be 3 or greater")) + log.Fatal("ResetAfterConsecutiveFailures must be 3 or greater") } if config.HealthPollingSeconds < 5 { - panic(errorspkg.New("HealthPollingSeconds must be 5 or greater")) + log.Fatal("HealthPollingSeconds must be 5 or greater") + } + + accountLabels := map[string]bool{} + + for _, info := range config.RegistrarAccounts { + if info.Provider != "gandi" && info.Provider != "porkbun" { + log.Fatalf("Provider '%s' is not supported. supported Providers: gandi, porkbun", info.Provider) + } + + if accountLabels[info.Label] { + log.Fatalf("Two accounts have the same label '%s' ", info.Label) + } + + accountLabels[info.Label] = true + + if info.RecordTTLSeconds < 300 { + log.Fatalf("RecordTTLSeconds on %s account '%s' must be 300 or greater", info.Provider, info.Label) + } + } + for _, domain := range config.Domains { + if !accountLabels[domain.RegistrarAccountLabel] { + log.Fatalf( + "Domain '%s' references RegistrarAccountLabel '%s' which is not in RegistrarAccounts", + domain.Domain, domain.RegistrarAccountLabel, + ) + } } log.Println("👳 Gandi DNS Updater starting up ✌ with config:") configToLogBytes, _ := json.MarshalIndent(config, "", " ") configToLogString := regexp.MustCompile( - `("GandiAPIKey": ")[^"]+(",)`, + `("APIKey": ")[^"]+(",)`, ).ReplaceAllString( string(configToLogBytes), "$1******$2", -- 2.34.2 From dbdaa4b3b7f8cd9a12a1ccaa22ff656fe2bfc48a Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 24 Feb 2024 22:39:44 -0600 Subject: [PATCH 3/5] Add porkbunService --- main.go | 18 +++-- porkbun_service.go | 183 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 5 deletions(-) create mode 100644 porkbun_service.go diff --git a/main.go b/main.go index e1f564f..51e9578 100644 --- a/main.go +++ b/main.go @@ -27,7 +27,8 @@ type Config struct { type RegistrarAccount struct { Label string Provider string - APIKey string + KeyId string + SecretKey string RecordTTLSeconds int } @@ -42,6 +43,13 @@ type RecordConfig struct { Type string } +func (record RecordConfig) FQDN(domain string) string { + if record.Name == "@" { + return domain + } + return strings.Join([]string{record.Name, domain}, ".") +} + type getPublicIPFunc func() (string, error) type taskResult struct { @@ -103,11 +111,11 @@ func main() { for _, accountInfo := range config.RegistrarAccounts { if accountInfo.Provider == "gandi" { - registrarAccounts[accountInfo.Label] = NewGandiService(accountInfo.APIKey, accountInfo.RecordTTLSeconds) + registrarAccounts[accountInfo.Label] = NewGandiService(accountInfo.SecretKey, accountInfo.RecordTTLSeconds) + } + if accountInfo.Provider == "porkbun" { + registrarAccounts[accountInfo.Label] = NewPorkbunService(accountInfo.KeyId, accountInfo.SecretKey, accountInfo.RecordTTLSeconds) } - // if accountInfo.Provider == "porkbun" { - // registrarAccounts[accountInfo.Label] = NewPorkbunService(accountInfo.APIKey, accountInfo.RecordTTLSeconds) - // } } // this sets lastConsensusIpv4 diff --git a/porkbun_service.go b/porkbun_service.go new file mode 100644 index 0000000..0f84d59 --- /dev/null +++ b/porkbun_service.go @@ -0,0 +1,183 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + "time" + + errors "git.sequentialread.com/forest/pkg-errors" +) + +type PorkbunService struct { + Client *http.Client + APIUrl string + RecordTTLSeconds int + Auth PorkbunAuth +} + +type PorkbunAuth struct { + KeyId string `json:"apikey"` + SecretKey string `json:"secretapikey"` +} + +type PorkbunRecordsResponse struct { + Records []PorkbunRecord `json:"records"` +} + +type PorkbunStatusResponse struct { + Status string `json:"status"` +} + +type PorkbunRecord struct { + Id string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Content string `json:"content"` + TTL string `json:"ttl"` + Priority string `json:"prio"` + Notes string `json:"notes"` +} + +type PorkbunUpdate struct { + KeyId string `json:"apikey"` + SecretKey string `json:"secretapikey"` + Name string `json:"name"` + Type string `json:"type"` + Content string `json:"content"` + TTL string `json:"ttl"` + Priority string `json:"prio"` +} + +func NewPorkbunService(keyId, secretKey string, recordTTLSeconds int) *PorkbunService { + return &PorkbunService{ + Client: &http.Client{Timeout: 30 * time.Second}, + APIUrl: "https://porkbun.com/api/json/v3", + Auth: PorkbunAuth{ + KeyId: keyId, + SecretKey: secretKey, + }, + RecordTTLSeconds: recordTTLSeconds, + } +} + +func (service *PorkbunService) TryToUpdateRecordsForDomain(domain DomainConfig, currentPublicIPv4 string) bool { + path := fmt.Sprintf("/dns/retrieve/%s", domain.Domain) + authJsonBytes, _ := json.Marshal(service.Auth) + statusCode, responseBytes, err := service.PorkbunHTTP("POST", path, bytes.NewBuffer(authJsonBytes)) + if err != nil { + // if porkbun isn't responding at all, we can just give up and try again in 10 seconds. + log.Println("waiting for porkbun to respond...") + return false + } + if statusCode >= 300 { + // if we got a non-200 status code from gandi, thats a fatal error. Log it. + log.Printf("porkbun (GET %s) returned http %d: \n%s\n\n", path, statusCode, responseBytes) + return false + } + var responseObj PorkbunRecordsResponse + err = json.Unmarshal(responseBytes, &responseObj) + if err != nil { + log.Printf("porkbun (GET %s) returned http %d, but it didn't return valid json: %s: \n%s\n\n", path, statusCode, err, responseBytes) + return false + } + + configIndexesToAdd := map[int]bool{} + for i, _ := range domain.Records { + configIndexesToAdd[i] = true + } + for _, porkbunRecord := range responseObj.Records { + for i, configRecord := range domain.Records { + if configRecord.FQDN(domain.Domain) == porkbunRecord.Name && recordTypesMatch(configRecord.Type, porkbunRecord.Type) { + configIndexesToAdd[i] = false + if porkbunRecord.Content != currentPublicIPv4 { + updateJsonBytes, _ := json.Marshal(PorkbunUpdate{ + KeyId: service.Auth.KeyId, + SecretKey: service.Auth.SecretKey, + Name: strings.ReplaceAll(configRecord.Name, "@", ""), + Type: configRecord.Type, + TTL: strconv.Itoa(service.RecordTTLSeconds), + Content: currentPublicIPv4, + }) + path := fmt.Sprintf("/dns/edit/%s/%s", domain.Domain, porkbunRecord.Id) + log.Printf("porkbun POST %s\n", path) + statusCode, responseBytes, err := service.PorkbunHTTP("POST", path, bytes.NewBuffer(updateJsonBytes)) + if err != nil { + log.Println("waiting for porkbun to respond...") + return false + } + var responseObj PorkbunStatusResponse + err = json.Unmarshal(responseBytes, &responseObj) + if err != nil { + log.Printf("porkbun (POST %s) returned http %d, but it didn't return valid json: %s: \n%s\n\n", path, statusCode, err, responseBytes) + return false + } + if responseObj.Status != "SUCCESS" { + log.Printf("porkbun (POST %s) returned http %d: \n%s\n\n", path, statusCode, responseBytes) + return false + } + } + } + } + } + + for i, configRecord := range domain.Records { + if configIndexesToAdd[i] { + createJsonBytes, _ := json.Marshal(PorkbunUpdate{ + KeyId: service.Auth.KeyId, + SecretKey: service.Auth.SecretKey, + Name: strings.ReplaceAll(configRecord.Name, "@", ""), + Type: configRecord.Type, + TTL: strconv.Itoa(service.RecordTTLSeconds), + Content: currentPublicIPv4, + }) + path := fmt.Sprintf("/dns/create/%s", domain.Domain) + log.Printf("porkbun POST %s\n", path) + statusCode, responseBytes, err := service.PorkbunHTTP("POST", path, bytes.NewBuffer(createJsonBytes)) + if err != nil { + log.Println("waiting for porkbun to respond...") + return false + } + var responseObj PorkbunStatusResponse + err = json.Unmarshal(responseBytes, &responseObj) + if err != nil { + log.Printf("porkbun (POST %s) returned http %d, but it didn't return valid json: %s: \n%s\n\n", path, statusCode, err, responseBytes) + return false + } + if responseObj.Status != "SUCCESS" { + log.Printf("porkbun (POST %s) returned http %d: \n%s\n\n", path, statusCode, responseBytes) + return false + } + } + } + + return true +} + +func (service *PorkbunService) PorkbunHTTP( + method string, + url string, + body io.Reader, +) (int, []byte, error) { + + request, err := http.NewRequest(method, url, body) + if err != nil { + return 0, nil, errors.Wrapf(err, "failed to create HTTP request calling %s %s", method, url) + } + response, err := service.Client.Do(request) + if err != nil { + return 0, nil, errors.Wrapf(err, "HTTP request error when calling %s %s", method, url) + } + bytes, err := ioutil.ReadAll(response.Body) + if err != nil { + return response.StatusCode, nil, errors.Wrapf(err, "HTTP read error when calling %s %s ", method, url) + } + + return response.StatusCode, bytes, err +} -- 2.34.2 From c21666cb3c9532279a03f2dfefb005db21879e65 Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 24 Feb 2024 22:47:32 -0600 Subject: [PATCH 4/5] udpdate example config on readme --- ReadMe.md | 12 ++++++++++-- config.go | 36 ++++++++++++++++++++++++++++++++++++ main.go | 35 ----------------------------------- 3 files changed, 46 insertions(+), 37 deletions(-) diff --git a/ReadMe.md b/ReadMe.md index cfff5d8..8e41b13 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -21,17 +21,24 @@ It uses a consensus of over 50% of the following 3rd party services in order to ``` { "PingPongLog": false, - "GandiRecordTTLSeconds": 300, "HealthPollingSeconds": 20, "ResetAfterConsecutiveFailures": 3, - "GandiAPIKey": "****************", "DNSResolvers": [ "8.8.8.8:53", "9.9.9.9:53", "1.1.1.1:53" ], + "RegistrarAccounts": [ + { + "Label": "sequentialread-gandi", + "Provider": "gandi", + "SecretKey" : "*****************", + "RecordTTLSeconds": 300 + } + ] "Domains": [ { + "RegistrarAccountLabel": "sequentialread-gandi", "Domain": "sequentialread.com", "Records": [ { @@ -45,6 +52,7 @@ It uses a consensus of over 50% of the following 3rd party services in order to ] }, { + "RegistrarAccountLabel": "sequentialread-gandi", "Domain": "server.garden", "Records": [ { diff --git a/config.go b/config.go index f97b4ed..e556133 100644 --- a/config.go +++ b/config.go @@ -5,11 +5,47 @@ import ( "log" "reflect" "regexp" + "strings" configlite "git.sequentialread.com/forest/config-lite" errorspkg "git.sequentialread.com/forest/pkg-errors" ) +type Config struct { + PingPongLog bool + ResetAfterConsecutiveFailures int + HealthPollingSeconds int + RegistrarAccounts []RegistrarAccount + DNSResolvers []string + Domains []DomainConfig +} + +type RegistrarAccount struct { + Label string + Provider string + KeyId string + SecretKey string + RecordTTLSeconds int +} + +type DomainConfig struct { + RegistrarAccountLabel string + Domain string + Records []RecordConfig +} + +type RecordConfig struct { + Name string + Type string +} + +func (record RecordConfig) FQDN(domain string) string { + if record.Name == "@" { + return domain + } + return strings.Join([]string{record.Name, domain}, ".") +} + func GetConfig() *Config { config := Config{} diff --git a/main.go b/main.go index 51e9578..84649fb 100644 --- a/main.go +++ b/main.go @@ -15,41 +15,6 @@ import ( dnsresolver "github.com/bogdanovich/dns_resolver" ) -type Config struct { - PingPongLog bool - ResetAfterConsecutiveFailures int - HealthPollingSeconds int - RegistrarAccounts []RegistrarAccount - DNSResolvers []string - Domains []DomainConfig -} - -type RegistrarAccount struct { - Label string - Provider string - KeyId string - SecretKey string - RecordTTLSeconds int -} - -type DomainConfig struct { - RegistrarAccountLabel string - Domain string - Records []RecordConfig -} - -type RecordConfig struct { - Name string - Type string -} - -func (record RecordConfig) FQDN(domain string) string { - if record.Name == "@" { - return domain - } - return strings.Join([]string{record.Name, domain}, ".") -} - type getPublicIPFunc func() (string, error) type taskResult struct { -- 2.34.2 From a7f399a336e5a2cb42cc69bf3f2503ae3ad5eada Mon Sep 17 00:00:00 2001 From: forest Date: Sat, 24 Feb 2024 22:50:01 -0600 Subject: [PATCH 5/5] docs link --- porkbun_service.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/porkbun_service.go b/porkbun_service.go index 0f84d59..81e6d74 100644 --- a/porkbun_service.go +++ b/porkbun_service.go @@ -15,6 +15,8 @@ import ( errors "git.sequentialread.com/forest/pkg-errors" ) +// https://porkbun.com/api/json/v3/documentation + type PorkbunService struct { Client *http.Client APIUrl string -- 2.34.2