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/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.go b/config.go index 7da6292..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{} @@ -18,26 +54,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", 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..84649fb 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - "bytes" "encoding/json" "fmt" "log" @@ -16,26 +15,6 @@ import ( dnsresolver "github.com/bogdanovich/dns_resolver" ) -type Config struct { - PingPongLog bool - ResetAfterConsecutiveFailures int - HealthPollingSeconds int - GandiRecordTTLSeconds int - GandiAPIKey string - DNSResolvers []string - Domains []DomainConfig -} - -type DomainConfig struct { - Domain string - Records []RecordConfig -} - -type RecordConfig struct { - Name string - Type string -} - type getPublicIPFunc func() (string, error) type taskResult struct { @@ -49,6 +28,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 +47,8 @@ var dnsRecordTypesWithSameSemantics = [][]string{ var dnsResolvers map[string]*dnsresolver.DnsResolver +var registrarAccounts map[string]registrarAccount + func main() { var err error @@ -87,7 +72,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.SecretKey, accountInfo.RecordTTLSeconds) + } + if accountInfo.Provider == "porkbun" { + registrarAccounts[accountInfo.Label] = NewPorkbunService(accountInfo.KeyId, accountInfo.SecretKey, accountInfo.RecordTTLSeconds) + } + } // this sets lastConsensusIpv4 waitSeconds := 5 @@ -111,7 +105,7 @@ func main() { monitorChannel := make(chan bool) - go constantlyMonitorDomains(config, gandi, monitorChannel) + go constantlyMonitorDomains(config, monitorChannel) lastMonitorUpdate := time.Now() for { @@ -135,7 +129,7 @@ func main() { } -func constantlyMonitorDomains(config *Config, gandi *GandiService, monitorChannel chan bool) { +func constantlyMonitorDomains(config *Config, monitorChannel chan bool) { debug.SetPanicOnFault(true) @@ -265,7 +259,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 +277,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) { diff --git a/porkbun_service.go b/porkbun_service.go new file mode 100644 index 0000000..81e6d74 --- /dev/null +++ b/porkbun_service.go @@ -0,0 +1,185 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "log" + "net/http" + "strconv" + "strings" + "time" + + errors "git.sequentialread.com/forest/pkg-errors" +) + +// https://porkbun.com/api/json/v3/documentation + +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 +}