| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203 |
- package cloudflare
- import (
- "bytes"
- "context"
- "encoding/json"
- "fmt"
- "io"
- "log/slog"
- "net/http"
- "net/url"
- "time"
- )
- const baseURL = "https://api.cloudflare.com/client/v4"
- var httpClient = &http.Client{Timeout: 10 * time.Second}
- type DNSRecord struct {
- ID string `json:"id"`
- Type string `json:"type"`
- Name string `json:"name"`
- Content string `json:"content"`
- Proxied bool `json:"proxied"`
- TTL int `json:"ttl"`
- }
- type cfResponse struct {
- Success bool `json:"success"`
- Errors []cfError `json:"errors"`
- Result json.RawMessage `json:"result"`
- }
- type cfError struct {
- Code int `json:"code"`
- Message string `json:"message"`
- }
- func ListDNSRecords(ctx context.Context, apiKey, zoneID string) ([]DNSRecord, error) {
- url := fmt.Sprintf("%s/zones/%s/dns_records?per_page=500", baseURL, zoneID)
- body, err := doRequest(ctx, http.MethodGet, url, apiKey, nil)
- if err != nil {
- return nil, err
- }
- var records []DNSRecord
- if err := json.Unmarshal(body, &records); err != nil {
- return nil, fmt.Errorf("unmarshal dns records: %w", err)
- }
- return records, nil
- }
- type dnsPayload struct {
- Type string `json:"type"`
- Name string `json:"name"`
- Content string `json:"content"`
- Proxied bool `json:"proxied"`
- TTL int `json:"ttl"`
- }
- func CreateDNSRecord(ctx context.Context, apiKey, zoneID string, rec DNSRecord) (DNSRecord, error) {
- url := fmt.Sprintf("%s/zones/%s/dns_records", baseURL, zoneID)
- payload := dnsPayload{
- Type: rec.Type,
- Name: rec.Name,
- Content: rec.Content,
- Proxied: rec.Proxied,
- TTL: 1, // auto
- }
- data, err := json.Marshal(payload)
- if err != nil {
- return DNSRecord{}, fmt.Errorf("marshal payload: %w", err)
- }
- body, err := doRequest(ctx, http.MethodPost, url, apiKey, bytes.NewReader(data))
- if err != nil {
- return DNSRecord{}, err
- }
- var created DNSRecord
- if err := json.Unmarshal(body, &created); err != nil {
- return DNSRecord{}, fmt.Errorf("unmarshal created record: %w", err)
- }
- return created, nil
- }
- func UpdateDNSRecord(ctx context.Context, apiKey, zoneID, recordID string, rec DNSRecord) error {
- url := fmt.Sprintf("%s/zones/%s/dns_records/%s", baseURL, zoneID, recordID)
- payload := dnsPayload{
- Type: rec.Type,
- Name: rec.Name,
- Content: rec.Content,
- Proxied: rec.Proxied,
- TTL: rec.TTL,
- }
- data, err := json.Marshal(payload)
- if err != nil {
- return fmt.Errorf("marshal payload: %w", err)
- }
- _, err = doRequest(ctx, http.MethodPut, url, apiKey, bytes.NewReader(data))
- return err
- }
- type batchPutRecord struct {
- ID string `json:"id"`
- Type string `json:"type"`
- Name string `json:"name"`
- Content string `json:"content"`
- Proxied bool `json:"proxied"`
- TTL int `json:"ttl"`
- }
- type batchRequest struct {
- Puts []batchPutRecord `json:"puts"`
- }
- func BatchUpdateDNSRecords(ctx context.Context, apiKey, zoneID string, records []DNSRecord) error {
- url := fmt.Sprintf("%s/zones/%s/dns_records/batch", baseURL, zoneID)
- puts := make([]batchPutRecord, len(records))
- for i, rec := range records {
- puts[i] = batchPutRecord{
- ID: rec.ID,
- Type: rec.Type,
- Name: rec.Name,
- Content: rec.Content,
- Proxied: rec.Proxied,
- TTL: rec.TTL,
- }
- }
- data, err := json.Marshal(batchRequest{Puts: puts})
- if err != nil {
- return fmt.Errorf("marshal batch payload: %w", err)
- }
- _, err = doRequest(ctx, http.MethodPost, url, apiKey, bytes.NewReader(data))
- return err
- }
- func doRequest(ctx context.Context, method, rawURL, apiKey string, body io.Reader) (json.RawMessage, error) {
- parsed, _ := url.Parse(rawURL)
- path := parsed.Path
- if parsed.RawQuery != "" {
- path = path + "?" + parsed.RawQuery
- }
- start := time.Now()
- slog.Default().Debug("cloudflare request", "method", method, "path", path)
- req, err := http.NewRequestWithContext(ctx, method, rawURL, body)
- if err != nil {
- err = fmt.Errorf("create request: %w", err)
- slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
- return nil, err
- }
- req.Header.Set("Authorization", "Bearer "+apiKey)
- req.Header.Set("Content-Type", "application/json")
- resp, err := httpClient.Do(req)
- if err != nil {
- err = fmt.Errorf("do request: %w", err)
- slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
- return nil, err
- }
- defer resp.Body.Close()
- respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
- if err != nil {
- err = fmt.Errorf("read response: %w", err)
- slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
- return nil, err
- }
- var cfResp cfResponse
- if err := json.Unmarshal(respBody, &cfResp); err != nil {
- err = fmt.Errorf("unmarshal response (status %d): %w", resp.StatusCode, err)
- slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
- return nil, err
- }
- if !cfResp.Success {
- var err error
- if len(cfResp.Errors) > 0 {
- err = fmt.Errorf("cloudflare API error: %s (code %d)", cfResp.Errors[0].Message, cfResp.Errors[0].Code)
- } else {
- err = fmt.Errorf("cloudflare API error: status %d", resp.StatusCode)
- }
- slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
- return nil, err
- }
- slog.Default().Debug("cloudflare request done", "method", method, "path", path, "status", resp.StatusCode, "duration_ms", time.Since(start).Milliseconds())
- return cfResp.Result, nil
- }
|