104 lines
3.1 KiB
Go
104 lines
3.1 KiB
Go
package dnspublish
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// DigitalOceanAPIBase is overridden by tests to point at a fake httptest.Server
|
|
// instead of the real DigitalOcean API.
|
|
var DigitalOceanAPIBase = "https://api.digitalocean.com/v2"
|
|
|
|
// digitaloceanRelativeName converts a full record name into one relative to the zone,
|
|
// which is what DigitalOcean's API expects (e.g. "selector._domainkey" for zone
|
|
// "example.com", record "selector._domainkey.example.com"; "@" for the zone apex
|
|
// itself). recordFQDN must be recordFQDN==zoneName or a subdomain of it.
|
|
func digitaloceanRelativeName(recordFQDN, zoneName string) (string, error) {
|
|
recordFQDN = strings.TrimSuffix(recordFQDN, ".")
|
|
zoneName = strings.TrimSuffix(zoneName, ".")
|
|
if recordFQDN == zoneName {
|
|
return "@", nil
|
|
}
|
|
suffix := "." + zoneName
|
|
if !strings.HasSuffix(recordFQDN, suffix) {
|
|
return "", fmt.Errorf("digitalocean: record %q is not under zone %q", recordFQDN, zoneName)
|
|
}
|
|
return strings.TrimSuffix(recordFQDN, suffix), nil
|
|
}
|
|
|
|
func digitaloceanDo(req *http.Request, token string, out any) (int, error) {
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
client := &http.Client{Timeout: 15 * time.Second}
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer resp.Body.Close()
|
|
if out != nil {
|
|
if err := json.NewDecoder(resp.Body).Decode(out); err != nil {
|
|
return resp.StatusCode, err
|
|
}
|
|
}
|
|
return resp.StatusCode, nil
|
|
}
|
|
|
|
func setTXTRecordDigitalOcean(creds Credentials, recordFQDN, value string) error {
|
|
name, err := digitaloceanRelativeName(recordFQDN, creds.ZoneName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var list struct {
|
|
DomainRecords []struct {
|
|
ID int64 `json:"id"`
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
} `json:"domain_records"`
|
|
}
|
|
req, err := http.NewRequest(http.MethodGet, DigitalOceanAPIBase+"/domains/"+creds.ZoneName+"/records?type=TXT&name="+name, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
status, err := digitaloceanDo(req, creds.DigitalOceanAPIToken, &list)
|
|
if err != nil {
|
|
return fmt.Errorf("digitalocean: list existing records: %w", err)
|
|
}
|
|
if status >= 300 {
|
|
return fmt.Errorf("digitalocean: list existing records: status %d", status)
|
|
}
|
|
var existingID int64
|
|
for _, r := range list.DomainRecords {
|
|
if r.Type == "TXT" && r.Name == name {
|
|
existingID = r.ID
|
|
break
|
|
}
|
|
}
|
|
|
|
body, err := json.Marshal(map[string]any{"type": "TXT", "name": name, "data": value, "ttl": txtTTL})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if existingID != 0 {
|
|
req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/domains/%s/records/%d", DigitalOceanAPIBase, creds.ZoneName, existingID), bytes.NewReader(body))
|
|
} else {
|
|
req, err = http.NewRequest(http.MethodPost, DigitalOceanAPIBase+"/domains/"+creds.ZoneName+"/records", bytes.NewReader(body))
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
status, err = digitaloceanDo(req, creds.DigitalOceanAPIToken, nil)
|
|
if err != nil {
|
|
return fmt.Errorf("digitalocean: upsert TXT record: %w", err)
|
|
}
|
|
if status >= 300 {
|
|
return fmt.Errorf("digitalocean: upsert TXT record: status %d", status)
|
|
}
|
|
return nil
|
|
}
|