Skip to content

Commit 04207ba

Browse files
authored
add DigitalOcean provider (#240)
* add DigitalOcean provider * linting
1 parent 611ee02 commit 04207ba

File tree

6 files changed

+424
-0
lines changed

6 files changed

+424
-0
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
- [Update root domain](#update-root-domain)
3838
- [Configuration examples](#configuration-examples)
3939
- [Cloudflare](#cloudflare)
40+
- [DigitalOcean](#digitalocean)
4041
- [DNSPod](#dnspod)
4142
- [Dreamhost](#dreamhost)
4243
- [Dynv6](#dynv6)
@@ -91,6 +92,7 @@
9192
| Provider | IPv4 support | IPv6 support | Root Domain | Subdomains |
9293
| ------------------------------------- | :----------------: | :----------------: | :----------------: | :----------------: |
9394
| [Cloudflare][cloudflare] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
95+
| [DigitalOcean][digitalocean] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
9496
| [Google Domains][google.domains] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
9597
| [DNSPod][dnspod] | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: |
9698
| [Dynv6][dynv6] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
@@ -110,6 +112,7 @@
110112
| [IONOS][ionos] | :white_check_mark: | :white_check_mark: | :x: | :white_check_mark: |
111113

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

323326
</details>
324327

328+
#### DigitalOcean
329+
330+
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.
331+
332+
<details>
333+
<summary>Example</summary>
334+
335+
```json
336+
{
337+
"provider": "DigitalOcean",
338+
"login_token": "dop_v1_00112233445566778899aabbccddeeff",
339+
"domains": [
340+
{
341+
"domain_name": "example.com",
342+
"sub_domains": ["@", "www"]
343+
}
344+
],
345+
"resolver": "8.8.8.8",
346+
"ip_urls": ["https://api.ip.sb/ip"],
347+
"ip_type": "IPv4",
348+
"interval": 300
349+
}
350+
351+
```
352+
353+
</details>
354+
325355
#### Dreamhost
326356

327357
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.
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
package digitalocean
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"io"
8+
"net/http"
9+
"strings"
10+
11+
"github.com/TimothyYe/godns/internal/settings"
12+
"github.com/TimothyYe/godns/internal/utils"
13+
log "github.com/sirupsen/logrus"
14+
)
15+
16+
const (
17+
// URL is the endpoint for the DigitalOcean API.
18+
URL = "https://api.digitalocean.com/v2"
19+
)
20+
21+
// DNSProvider struct definition.
22+
type DNSProvider struct {
23+
configuration *settings.Settings
24+
API string
25+
}
26+
27+
type DomainRecordsResponse struct {
28+
Records []DNSRecord `json:"domain_records"`
29+
}
30+
31+
// DNSRecord for DigitalOcean API.
32+
type DNSRecord struct {
33+
ID int32 `json:"id"`
34+
Type string `json:"type"`
35+
Name string `json:"name"`
36+
IP string `json:"data"`
37+
TTL int32 `json:"ttl"`
38+
}
39+
40+
// SetIP updates DNSRecord.IP.
41+
func (r *DNSRecord) SetIP(ip string) {
42+
r.IP = ip
43+
}
44+
45+
// Init passes DNS settings and store it to the provider instance.
46+
func (provider *DNSProvider) Init(conf *settings.Settings) {
47+
provider.configuration = conf
48+
provider.API = URL
49+
}
50+
51+
func (provider *DNSProvider) UpdateIP(domainName, subdomainName, ip string) error {
52+
log.Infof("Checking IP for domain %s", domainName)
53+
54+
records := provider.getDNSRecords(domainName)
55+
matched := false
56+
57+
// update records
58+
for _, rec := range records {
59+
rec := rec
60+
if !recordTracked(provider.getCurrentDomain(domainName), &rec) {
61+
log.Debug("Skipping record:", rec.Name)
62+
continue
63+
}
64+
65+
if strings.Contains(rec.Name, subdomainName) || rec.Name == domainName {
66+
if rec.IP != ip {
67+
log.Infof("IP mismatch: Current(%+v) vs DigitalOcean(%+v)", ip, rec.IP)
68+
provider.updateRecord(domainName, rec, ip)
69+
} else {
70+
log.Infof("Record OK: %+v - %+v", rec.Name, rec.IP)
71+
}
72+
73+
matched = true
74+
}
75+
}
76+
77+
if !matched {
78+
log.Debugf("Record %s not found, will create it.", subdomainName)
79+
if err := provider.createRecord(domainName, subdomainName, ip); err != nil {
80+
return err
81+
}
82+
log.Infof("Record [%s] created with IP address: %s", subdomainName, ip)
83+
}
84+
85+
return nil
86+
}
87+
88+
func (provider *DNSProvider) getRecordType() string {
89+
var recordType string = utils.IPTypeA
90+
if provider.configuration.IPType == "" || strings.ToUpper(provider.configuration.IPType) == utils.IPV4 {
91+
recordType = utils.IPTypeA
92+
} else if strings.ToUpper(provider.configuration.IPType) == utils.IPV6 {
93+
recordType = utils.IPTypeAAAA
94+
}
95+
96+
return recordType
97+
}
98+
99+
func (provider *DNSProvider) getCurrentDomain(domainName string) *settings.Domain {
100+
for _, domain := range provider.configuration.Domains {
101+
domain := domain
102+
if domain.DomainName == domainName {
103+
return &domain
104+
}
105+
}
106+
107+
return nil
108+
}
109+
110+
// Check if record is present in domain conf.
111+
func recordTracked(domain *settings.Domain, record *DNSRecord) bool {
112+
for _, subDomain := range domain.SubDomains {
113+
if record.Name == subDomain {
114+
return true
115+
}
116+
}
117+
118+
return false
119+
}
120+
121+
// Create a new request with auth in place and optional proxy.
122+
func (provider *DNSProvider) newRequest(method, url string, body io.Reader) (*http.Request, *http.Client) {
123+
client := utils.GetHTTPClient(provider.configuration)
124+
if client == nil {
125+
log.Info("cannot create HTTP client")
126+
}
127+
128+
req, _ := http.NewRequest(method, provider.API+url, body)
129+
req.Header.Set("Content-Type", "application/json")
130+
131+
if provider.configuration.Email != "" && provider.configuration.Password != "" {
132+
req.Header.Set("X-Auth-Email", provider.configuration.Email)
133+
req.Header.Set("X-Auth-Key", provider.configuration.Password)
134+
} else if provider.configuration.LoginToken != "" {
135+
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", provider.configuration.LoginToken))
136+
}
137+
log.Debugf("Created %+v request for %+v", string(method), string(url))
138+
139+
return req, client
140+
}
141+
142+
// Get all DNS A(AAA) records for a zone.
143+
func (provider *DNSProvider) getDNSRecords(domainName string) []DNSRecord {
144+
145+
var empty []DNSRecord
146+
var r DomainRecordsResponse
147+
recordType := provider.getRecordType()
148+
149+
log.Infof("Querying records with type: %s", recordType)
150+
req, client := provider.newRequest("GET", fmt.Sprintf("/domains/"+domainName+"/records?type=%s&page=1&per_page=200", recordType), nil)
151+
resp, err := client.Do(req)
152+
if err != nil {
153+
log.Error("Request error:", err)
154+
return empty
155+
}
156+
157+
body, _ := io.ReadAll(resp.Body)
158+
err = json.Unmarshal(body, &r)
159+
if err != nil {
160+
log.Infof("Decoder error: %+v", err)
161+
log.Debugf("Response body: %+v", string(body))
162+
return empty
163+
}
164+
165+
return r.Records
166+
}
167+
168+
func (provider *DNSProvider) createRecord(domain, subDomain, ip string) error {
169+
recordType := provider.getRecordType()
170+
171+
newRecord := DNSRecord{
172+
Type: recordType,
173+
IP: ip,
174+
TTL: int32(provider.configuration.Interval),
175+
}
176+
177+
if subDomain == utils.RootDomain {
178+
newRecord.Name = utils.RootDomain
179+
} else {
180+
newRecord.Name = subDomain
181+
}
182+
183+
content, err := json.Marshal(newRecord)
184+
if err != nil {
185+
log.Errorf("Encoder error: %+v", err)
186+
return err
187+
}
188+
189+
req, client := provider.newRequest("POST", fmt.Sprintf("/domains/%s/records", domain), bytes.NewBuffer(content))
190+
resp, err := client.Do(req)
191+
if err != nil {
192+
log.Error("Request error:", err)
193+
return err
194+
}
195+
196+
defer resp.Body.Close()
197+
body, err := io.ReadAll(resp.Body)
198+
if err != nil {
199+
log.Errorf("Failed to read request body: %+v", err)
200+
return err
201+
}
202+
203+
var r DNSRecord
204+
err = json.Unmarshal(body, &r)
205+
if err != nil {
206+
log.Errorf("Response decoder error: %+v", err)
207+
log.Debugf("Response body: %+v", string(body))
208+
return err
209+
}
210+
211+
return nil
212+
}
213+
214+
// Update DNS Record with new IP.
215+
func (provider *DNSProvider) updateRecord(domainName string, record DNSRecord, newIP string) string {
216+
217+
var r DNSRecord
218+
record.SetIP(newIP)
219+
var lastIP string
220+
221+
j, _ := json.Marshal(record)
222+
req, client := provider.newRequest("PUT",
223+
fmt.Sprintf("/domains/%s/records/%d", domainName, record.ID),
224+
bytes.NewBuffer(j),
225+
)
226+
resp, err := client.Do(req)
227+
if err != nil {
228+
log.Error("Request error:", err)
229+
return ""
230+
}
231+
232+
defer resp.Body.Close()
233+
body, _ := io.ReadAll(resp.Body)
234+
err = json.Unmarshal(body, &r)
235+
if err != nil {
236+
log.Errorf("Decoder error: %+v", err)
237+
log.Debugf("Response body: %+v", string(body))
238+
return ""
239+
}
240+
log.Infof("Record updated: %+v - %+v", record.Name, record.IP)
241+
lastIP = record.IP
242+
243+
return lastIP
244+
}

0 commit comments

Comments
 (0)