Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add DigitalOcean provider #240

Merged
merged 2 commits into from
May 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading