From 10cf806b73bfce40ef013e609474e0e79ac0adf1 Mon Sep 17 00:00:00 2001 From: David Lick Date: Sun, 12 Sep 2021 20:09:08 -0400 Subject: [PATCH] Initial commit --- digitalocean/client.go | 90 ++++++++++++++++++++++++++++++++++++++++++ digitalocean/errors.go | 9 +++++ digitalocean/models.go | 12 ++++++ go.mod | 3 ++ main.go | 54 +++++++++++++++++++++++++ 5 files changed, 168 insertions(+) create mode 100644 digitalocean/client.go create mode 100644 digitalocean/errors.go create mode 100644 digitalocean/models.go create mode 100644 go.mod create mode 100644 main.go diff --git a/digitalocean/client.go b/digitalocean/client.go new file mode 100644 index 0000000..0699dae --- /dev/null +++ b/digitalocean/client.go @@ -0,0 +1,90 @@ +package digitalocean + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" +) + +type client struct { + baseUrl string + domain string + token string + httpClient *http.Client +} + +func NewClient(baseUrl, token, domain string) *client { + return &client{ + baseUrl: baseUrl, + domain: domain, + token: token, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } +} + +func (c *client) GetARecordID() (string, error) { + req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/v2/domains/%s/records", c.baseUrl, c.domain), nil) + if err != nil { + return "", err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.token)) + + var records RecordsResponse + err = c.doRequest(req, &records) + if err != nil { + return "", err + } + + for _, record := range records.DomainRecords { + if record.Name == "vpn" && record.Type == "A" { + return strconv.Itoa(int(record.Id)), nil + } + } + + return "", ErrNotFound +} + +func (c *client) SetARecord(id, ip string) error { + body := fmt.Sprintf(`{"data":%q}`, strings.ReplaceAll(ip, "\n", "")) + req, err := http.NewRequest(http.MethodPatch, fmt.Sprintf("%s/v2/domains/%s/records/%s", c.baseUrl, c.domain, id), bytes.NewBufferString(body)) + if err != nil { + return err + } + req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", c.token)) + req.Header.Add("Content-Type", "application/json") + + var resp map[string]interface{} + err = c.doRequest(req, &resp) + return err +} + +func (c *client) doRequest(req *http.Request, output interface{}) error { + resp, err := c.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode >= http.StatusInternalServerError { + return fmt.Errorf("%w: statuscode %d", ErrServerError, resp.StatusCode) + } + + if resp.StatusCode >= http.StatusBadRequest { + return fmt.Errorf("%w: statuscode %d", ErrClientError, resp.StatusCode) + } + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + err = json.Unmarshal(body, &output) + return err +} diff --git a/digitalocean/errors.go b/digitalocean/errors.go new file mode 100644 index 0000000..f776a9c --- /dev/null +++ b/digitalocean/errors.go @@ -0,0 +1,9 @@ +package digitalocean + +import "errors" + +var ( + ErrNotFound = errors.New("not found") + ErrClientError = errors.New("client error") + ErrServerError = errors.New("server error") +) diff --git a/digitalocean/models.go b/digitalocean/models.go new file mode 100644 index 0000000..4231982 --- /dev/null +++ b/digitalocean/models.go @@ -0,0 +1,12 @@ +package digitalocean + +type DomainRecords struct { + Data string `json:"data"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` +} + +type RecordsResponse struct { + DomainRecords []DomainRecords `json:"domain_records"` +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ca4d37e --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/davidlick/digitalocean-ddns + +go 1.16 diff --git a/main.go b/main.go new file mode 100644 index 0000000..1f9657e --- /dev/null +++ b/main.go @@ -0,0 +1,54 @@ +package main + +import ( + "io/ioutil" + "log" + "net/http" + "os" + + "github.com/davidlick/digitalocean-ddns/digitalocean" +) + +var ( + client = &http.Client{} +) + +func main() { + token := os.Getenv("DIGITALOCEAN_TOKEN") + doBaseUrl := os.Getenv("DIGITALOCEAN_URL") + doDomain := os.Getenv("DIGITALOCEAN_DOMAIN") + + client := digitalocean.NewClient(doBaseUrl, token, doDomain) + + ip, err := getPublicIP() + if err != nil { + log.Fatalf("failed to get public ip: %v", err) + } + + id, err := client.GetARecordID() + if err != nil { + log.Fatalf("failed to get A record ID: %v", err) + } + + err = client.SetARecord(id, ip) + if err != nil { + log.Fatalf("failed to set A record ip: %v", err) + } + + log.Printf("successfully set DNS A record id %s to %s", id, ip) +} + +func getPublicIP() (string, error) { + resp, err := http.Get("https://icanhazip.com") + if err != nil { + return "", err + } + defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(body), nil +}