Files
mailgoserver/internal/dnspublish/digitalocean.go
T

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
}