123 lines
3.9 KiB
Go
123 lines
3.9 KiB
Go
package cloudflare
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
const apiURL = "https://api.cloudflare.com/client/v4"
|
|
|
|
// API is the narrow Cloudflare DNS boundary used by route commands.
|
|
type API interface {
|
|
EnsureCNAME(context.Context, string, string, string) error
|
|
DeleteCNAME(context.Context, string, string, string) error
|
|
}
|
|
|
|
type dnsRecord struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Content string `json:"content"`
|
|
Proxied bool `json:"proxied"`
|
|
}
|
|
|
|
type Client struct {
|
|
baseURL string
|
|
httpClient *http.Client
|
|
token string
|
|
}
|
|
|
|
func NewClient(token string) *Client {
|
|
return &Client{baseURL: apiURL, httpClient: &http.Client{Timeout: 15 * time.Second}, token: token}
|
|
}
|
|
|
|
func (c *Client) EnsureCNAME(ctx context.Context, zoneID, hostname, tunnelID string) error {
|
|
records, err := c.cnameRecords(ctx, zoneID, hostname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
target := tunnelTarget(tunnelID)
|
|
if len(records) == 0 {
|
|
body, err := json.Marshal(struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
Content string `json:"content"`
|
|
Proxied bool `json:"proxied"`
|
|
}{Type: "CNAME", Name: hostname, Content: target, Proxied: true})
|
|
if err != nil {
|
|
return errors.New("encode Cloudflare DNS record")
|
|
}
|
|
var created dnsRecord
|
|
return c.request(ctx, http.MethodPost, "/zones/"+url.PathEscape(zoneID)+"/dns_records", body, &created)
|
|
}
|
|
if len(records) != 1 || !strings.EqualFold(records[0].Name, hostname) || !strings.EqualFold(records[0].Content, target) || !records[0].Proxied {
|
|
return errors.New("existing Cloudflare DNS record does not match the managed tunnel route")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Client) DeleteCNAME(ctx context.Context, zoneID, hostname, tunnelID string) error {
|
|
records, err := c.cnameRecords(ctx, zoneID, hostname)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(records) == 0 {
|
|
return nil
|
|
}
|
|
target := tunnelTarget(tunnelID)
|
|
if len(records) != 1 || !strings.EqualFold(records[0].Name, hostname) || !strings.EqualFold(records[0].Content, target) || !records[0].Proxied {
|
|
return errors.New("existing Cloudflare DNS record does not match the managed tunnel route")
|
|
}
|
|
var deleted dnsRecord
|
|
return c.request(ctx, http.MethodDelete, "/zones/"+url.PathEscape(zoneID)+"/dns_records/"+url.PathEscape(records[0].ID), nil, &deleted)
|
|
}
|
|
|
|
func (c *Client) cnameRecords(ctx context.Context, zoneID, hostname string) ([]dnsRecord, error) {
|
|
var records []dnsRecord
|
|
path := "/zones/" + url.PathEscape(zoneID) + "/dns_records?" + url.Values{"type": {"CNAME"}, "name": {hostname}}.Encode()
|
|
err := c.request(ctx, http.MethodGet, path, nil, &records)
|
|
return records, err
|
|
}
|
|
|
|
func (c *Client) request(ctx context.Context, method, path string, body []byte, result any) error {
|
|
request, err := http.NewRequestWithContext(ctx, method, c.baseURL+path, bytes.NewReader(body))
|
|
if err != nil {
|
|
return errors.New("create Cloudflare API request")
|
|
}
|
|
request.Header.Set("Authorization", "Bearer "+c.token)
|
|
request.Header.Set("Content-Type", "application/json")
|
|
client := c.httpClient
|
|
if client == nil {
|
|
client = &http.Client{Timeout: 15 * time.Second}
|
|
}
|
|
response, err := client.Do(request)
|
|
if err != nil {
|
|
return errors.New("call Cloudflare API")
|
|
}
|
|
defer response.Body.Close()
|
|
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
|
return fmt.Errorf("Cloudflare API returned HTTP %d", response.StatusCode)
|
|
}
|
|
var envelope struct {
|
|
Success bool `json:"success"`
|
|
Result json.RawMessage `json:"result"`
|
|
}
|
|
if err := json.NewDecoder(response.Body).Decode(&envelope); err != nil || !envelope.Success {
|
|
return errors.New("Cloudflare API returned an invalid response")
|
|
}
|
|
if err := json.Unmarshal(envelope.Result, result); err != nil {
|
|
return errors.New("Cloudflare API returned an invalid result")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func tunnelTarget(tunnelID string) string {
|
|
return tunnelID + ".cfargotunnel.com"
|
|
}
|