Skip to content

Commit

Permalink
add DigitalOcean provider (#240)
Browse files Browse the repository at this point in the history
* add DigitalOcean provider

* linting
  • Loading branch information
Sleavely authored May 2, 2024
1 parent 611ee02 commit 04207ba
Show file tree
Hide file tree
Showing 6 changed files with 424 additions and 0 deletions.
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
- [Update root domain](#update-root-domain)
- [Configuration examples](#configuration-examples)
- [Cloudflare](#cloudflare)
- [DigitalOcean](#digitalocean)
- [DNSPod](#dnspod)
- [Dreamhost](#dreamhost)
- [Dynv6](#dynv6)
Expand Down Expand Up @@ -91,6 +92,7 @@
| Provider | IPv4 support | IPv6 support | Root Domain | Subdomains |
| ------------------------------------- | :----------------: | :----------------: | :----------------: | :----------------: |
| [Cloudflare][cloudflare] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| [DigitalOcean][digitalocean] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| [Google Domains][google.domains] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
| [DNSPod][dnspod] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
| [Dynv6][dynv6] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
Expand All @@ -110,6 +112,7 @@
| [IONOS][ionos] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |

[cloudflare]: https://cloudflare.com
[digitalocean]: https://digitalocean.com
[google.domains]: https://domains.google
[dnspod]: https://www.dnspod.cn
[dynv6]: https://dynv6.com
Expand Down Expand Up @@ -322,6 +325,33 @@ For DNSPod, you need to provide your API Token(you can create it [here](https://

</details>

#### DigitalOcean

For DigitalOcean, you need to provide a API Token with the `domain` scopes (you can create it [here](https://cloud.digitalocean.com/account/api/tokens/new)), and config all the domains & subdomains.

<details>
<summary>Example</summary>

```json
{
"provider": "DigitalOcean",
"login_token": "dop_v1_00112233445566778899aabbccddeeff",
"domains": [
{
"domain_name": "example.com",
"sub_domains": ["@", "www"]
}
],
"resolver": "8.8.8.8",
"ip_urls": ["https://api.ip.sb/ip"],
"ip_type": "IPv4",
"interval": 300
}

```

</details>

#### Dreamhost

For Dreamhost, you need to provide your API Token(you can create it [here](https://panel.dreamhost.com/?tree=home.api)), and config all the domains & subdomains.
Expand Down
244 changes: 244 additions & 0 deletions internal/provider/digitalocean/digitalocean_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
package digitalocean

import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"strings"

"github.com/TimothyYe/godns/internal/settings"
"github.com/TimothyYe/godns/internal/utils"
log "github.com/sirupsen/logrus"
)

const (
// URL is the endpoint for the DigitalOcean API.
URL = "https://api.digitalocean.com/v2"
)

// DNSProvider struct definition.
type DNSProvider struct {
configuration *settings.Settings
API string
}

type DomainRecordsResponse struct {
Records []DNSRecord `json:"domain_records"`
}

// DNSRecord for DigitalOcean API.
type DNSRecord struct {
ID int32 `json:"id"`
Type string `json:"type"`
Name string `json:"name"`
IP string `json:"data"`
TTL int32 `json:"ttl"`
}

// SetIP updates DNSRecord.IP.
func (r *DNSRecord) SetIP(ip string) {
r.IP = ip
}

// Init passes DNS settings and store it to the provider instance.
func (provider *DNSProvider) Init(conf *settings.Settings) {
provider.configuration = conf
provider.API = URL
}

func (provider *DNSProvider) UpdateIP(domainName, subdomainName, ip string) error {
log.Infof("Checking IP for domain %s", domainName)

records := provider.getDNSRecords(domainName)
matched := false

// update records
for _, rec := range records {
rec := rec
if !recordTracked(provider.getCurrentDomain(domainName), &rec) {
log.Debug("Skipping record:", rec.Name)
continue
}

if strings.Contains(rec.Name, subdomainName) || rec.Name == domainName {
if rec.IP != ip {
log.Infof("IP mismatch: Current(%+v) vs DigitalOcean(%+v)", ip, rec.IP)
provider.updateRecord(domainName, rec, ip)
} else {
log.Infof("Record OK: %+v - %+v", rec.Name, rec.IP)
}

matched = true
}
}

if !matched {
log.Debugf("Record %s not found, will create it.", subdomainName)
if err := provider.createRecord(domainName, subdomainName, ip); err != nil {
return err
}
log.Infof("Record [%s] created with IP address: %s", subdomainName, ip)
}

return nil
}

func (provider *DNSProvider) getRecordType() string {
var recordType string = utils.IPTypeA
if provider.configuration.IPType == "" || strings.ToUpper(provider.configuration.IPType) == utils.IPV4 {
recordType = utils.IPTypeA
} else if strings.ToUpper(provider.configuration.IPType) == utils.IPV6 {
recordType = utils.IPTypeAAAA
}

return recordType
}

func (provider *DNSProvider) getCurrentDomain(domainName string) *settings.Domain {
for _, domain := range provider.configuration.Domains {
domain := domain
if domain.DomainName == domainName {
return &domain
}
}

return nil
}

// Check if record is present in domain conf.
func recordTracked(domain *settings.Domain, record *DNSRecord) bool {
for _, subDomain := range domain.SubDomains {
if record.Name == subDomain {
return true
}
}

return false
}

// Create a new request with auth in place and optional proxy.
func (provider *DNSProvider) newRequest(method, url string, body io.Reader) (*http.Request, *http.Client) {
client := utils.GetHTTPClient(provider.configuration)
if client == nil {
log.Info("cannot create HTTP client")
}

req, _ := http.NewRequest(method, provider.API+url, body)
req.Header.Set("Content-Type", "application/json")

if provider.configuration.Email != "" && provider.configuration.Password != "" {
req.Header.Set("X-Auth-Email", provider.configuration.Email)
req.Header.Set("X-Auth-Key", provider.configuration.Password)
} else if provider.configuration.LoginToken != "" {
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", provider.configuration.LoginToken))
}
log.Debugf("Created %+v request for %+v", string(method), string(url))

return req, client
}

// Get all DNS A(AAA) records for a zone.
func (provider *DNSProvider) getDNSRecords(domainName string) []DNSRecord {

var empty []DNSRecord
var r DomainRecordsResponse
recordType := provider.getRecordType()

log.Infof("Querying records with type: %s", recordType)
req, client := provider.newRequest("GET", fmt.Sprintf("/domains/"+domainName+"/records?type=%s&page=1&per_page=200", recordType), nil)
resp, err := client.Do(req)
if err != nil {
log.Error("Request error:", err)
return empty
}

body, _ := io.ReadAll(resp.Body)
err = json.Unmarshal(body, &r)
if err != nil {
log.Infof("Decoder error: %+v", err)
log.Debugf("Response body: %+v", string(body))
return empty
}

return r.Records
}

func (provider *DNSProvider) createRecord(domain, subDomain, ip string) error {
recordType := provider.getRecordType()

newRecord := DNSRecord{
Type: recordType,
IP: ip,
TTL: int32(provider.configuration.Interval),
}

if subDomain == utils.RootDomain {
newRecord.Name = utils.RootDomain
} else {
newRecord.Name = subDomain
}

content, err := json.Marshal(newRecord)
if err != nil {
log.Errorf("Encoder error: %+v", err)
return err
}

req, client := provider.newRequest("POST", fmt.Sprintf("/domains/%s/records", domain), bytes.NewBuffer(content))
resp, err := client.Do(req)
if err != nil {
log.Error("Request error:", err)
return err
}

defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Errorf("Failed to read request body: %+v", err)
return err
}

var r DNSRecord
err = json.Unmarshal(body, &r)
if err != nil {
log.Errorf("Response decoder error: %+v", err)
log.Debugf("Response body: %+v", string(body))
return err
}

return nil
}

// Update DNS Record with new IP.
func (provider *DNSProvider) updateRecord(domainName string, record DNSRecord, newIP string) string {

var r DNSRecord
record.SetIP(newIP)
var lastIP string

j, _ := json.Marshal(record)
req, client := provider.newRequest("PUT",
fmt.Sprintf("/domains/%s/records/%d", domainName, record.ID),
bytes.NewBuffer(j),
)
resp, err := client.Do(req)
if err != nil {
log.Error("Request error:", err)
return ""
}

defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
err = json.Unmarshal(body, &r)
if err != nil {
log.Errorf("Decoder error: %+v", err)
log.Debugf("Response body: %+v", string(body))
return ""
}
log.Infof("Record updated: %+v - %+v", record.Name, record.IP)
lastIP = record.IP

return lastIP
}
Loading

0 comments on commit 04207ba

Please sign in to comment.