commit 7801a9af98c3fea51f24942586ce8d43f36afb69 Author: sotrali Date: Tue Apr 14 23:12:26 2026 -0500 hello world diff --git a/README.md b/README.md new file mode 100644 index 0000000..9d05bf3 --- /dev/null +++ b/README.md @@ -0,0 +1,13 @@ +# Dynamic DNS Client for Porkbun API + +To use, put binary in a folder with a file named ".env". +The contents of the .env file should be JSON in the following format: + ```{ + "secretapikey": "XXXXXXXXXXXXX", + "apikey": "XXXXXXXXXXXXX", + "domains": [ + "example.com", + "example.net", + "example.io" + ] +}``` diff --git a/dynamic-ddns b/dynamic-ddns new file mode 100755 index 0000000..d3e3106 Binary files /dev/null and b/dynamic-ddns differ diff --git a/main.go b/main.go new file mode 100644 index 0000000..f91c9a6 --- /dev/null +++ b/main.go @@ -0,0 +1,130 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + "net/http" + "encoding/json" + "strings" +) + + +const API_DOMAIN string = "https://api.porkbun.com" +const ENDPOINT_GET string = "/api/json/v3/dns/retrieve/" +const ENDPOINT_SET string = "/api/json/v3/dns/edit/" + +// error helper +func checkE(e error) { + if e != nil { + log.Fatal(e) + panic(e) + } +} + +// http request helper +func createBody(bodyMap map[string]string) io.Reader { + bodyData, err := json.Marshal(bodyMap) + checkE(err) + body := string(bodyData) + return strings.NewReader(body) +} + +// http response helper +func readBody(res *http.Response) []byte { + body, err := io.ReadAll(res.Body) + res.Body.Close() + if res.StatusCode > 299 { + log.Fatalf("Response failed with status code: %d and\nbody: %s\n", res.StatusCode, body) + } + checkE(err) + return body +} + +func syncDns(domain string, apiSecret string, apiKey string, serverIp string) bool { + // construct endpoint url + url := API_DOMAIN + ENDPOINT_GET + domain + + // use api keys to construct request body for porkbun + body := createBody(map[string]string{ + "secretapikey" : apiSecret, + "apikey" : apiKey, + }) + + // submit request for domain's DNS records + res, err := http.Post(url, "application/json", body) + checkE(err) + + // parse records out from response body + var bodyJson map[string]interface{} + err = json.Unmarshal(readBody(res), &bodyJson) + checkE(err) + dnsRecords := bodyJson["records"].([]interface{}) + + // identify outdated A records + aRecordsToUpdate := make(map[string]string) + for i := range len(dnsRecords) { + record := dnsRecords[i].(map[string]interface{}) + if record["type"].(string) != "A" { continue } + if record["content"].(string) == serverIp { continue } + aRecordsToUpdate[record["name"].(string)] = record["id"].(string) + } + + // we are potentially done now + if len(aRecordsToUpdate) == 0 { + fmt.Println(domain + " is up to date") + return false + } + + // update each outdated A record + for recordName, recordId := range aRecordsToUpdate { + url = API_DOMAIN + ENDPOINT_SET + domain + "/" + recordId + subdomain := strings.Split(recordName, ".")[0] + body = createBody(map[string]string{ + "secretapikey" : apiSecret, + "apikey" : apiKey, + "content" : serverIp, + "name" : subdomain, + "type": "A", + "ttl": "600", + }) + + // make request to update A record + res, err = http.Post(url, "application/json", body) + checkE(err) + fmt.Println("UPDATED " + recordName) + } + + return true +} + +func main() { + // open + deserialize env JSON + envJson, err := os.ReadFile(".env") + checkE(err) + var env map[string]interface{} + err = json.Unmarshal(envJson, &env) + checkE(err) + + // parse/organize api keys + domains to update + apiSecret := env["secretapikey"].(string) + apiKey := env["apikey"].(string) + domains := env["domains"].([]interface{}) + + // determine server's current ip + res, err := http.Get("https://api.ipify.org") + checkE(err) + ip := string(readBody(res)) + + fmt.Println("SERVER IP: " + ip) + + // loop through all domains + update DNS records + var domain string + for i := range len(domains) { + domain = domains[i].(string) + fmt.Println("checking " + domain) + syncDns(domain, apiSecret, apiKey, ip) + } +} +