client.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. package cloudflare
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "fmt"
  7. "io"
  8. "log/slog"
  9. "net/http"
  10. "net/url"
  11. "time"
  12. )
  13. const baseURL = "https://api.cloudflare.com/client/v4"
  14. var httpClient = &http.Client{Timeout: 10 * time.Second}
  15. type DNSRecord struct {
  16. ID string `json:"id"`
  17. Type string `json:"type"`
  18. Name string `json:"name"`
  19. Content string `json:"content"`
  20. Proxied bool `json:"proxied"`
  21. TTL int `json:"ttl"`
  22. }
  23. type cfResponse struct {
  24. Success bool `json:"success"`
  25. Errors []cfError `json:"errors"`
  26. Result json.RawMessage `json:"result"`
  27. }
  28. type cfError struct {
  29. Code int `json:"code"`
  30. Message string `json:"message"`
  31. }
  32. func ListDNSRecords(ctx context.Context, apiKey, zoneID string) ([]DNSRecord, error) {
  33. url := fmt.Sprintf("%s/zones/%s/dns_records?per_page=500", baseURL, zoneID)
  34. body, err := doRequest(ctx, http.MethodGet, url, apiKey, nil)
  35. if err != nil {
  36. return nil, err
  37. }
  38. var records []DNSRecord
  39. if err := json.Unmarshal(body, &records); err != nil {
  40. return nil, fmt.Errorf("unmarshal dns records: %w", err)
  41. }
  42. return records, nil
  43. }
  44. type updatePayload struct {
  45. Type string `json:"type"`
  46. Name string `json:"name"`
  47. Content string `json:"content"`
  48. Proxied bool `json:"proxied"`
  49. TTL int `json:"ttl"`
  50. }
  51. func UpdateDNSRecord(ctx context.Context, apiKey, zoneID, recordID string, rec DNSRecord) error {
  52. url := fmt.Sprintf("%s/zones/%s/dns_records/%s", baseURL, zoneID, recordID)
  53. payload := updatePayload{
  54. Type: rec.Type,
  55. Name: rec.Name,
  56. Content: rec.Content,
  57. Proxied: rec.Proxied,
  58. TTL: rec.TTL,
  59. }
  60. data, err := json.Marshal(payload)
  61. if err != nil {
  62. return fmt.Errorf("marshal payload: %w", err)
  63. }
  64. _, err = doRequest(ctx, http.MethodPut, url, apiKey, bytes.NewReader(data))
  65. return err
  66. }
  67. func doRequest(ctx context.Context, method, rawURL, apiKey string, body io.Reader) (json.RawMessage, error) {
  68. parsed, _ := url.Parse(rawURL)
  69. path := parsed.Path
  70. if parsed.RawQuery != "" {
  71. path = path + "?" + parsed.RawQuery
  72. }
  73. start := time.Now()
  74. slog.Default().Debug("cloudflare request", "method", method, "path", path)
  75. req, err := http.NewRequestWithContext(ctx, method, rawURL, body)
  76. if err != nil {
  77. err = fmt.Errorf("create request: %w", err)
  78. slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
  79. return nil, err
  80. }
  81. req.Header.Set("Authorization", "Bearer "+apiKey)
  82. req.Header.Set("Content-Type", "application/json")
  83. resp, err := httpClient.Do(req)
  84. if err != nil {
  85. err = fmt.Errorf("do request: %w", err)
  86. slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
  87. return nil, err
  88. }
  89. defer resp.Body.Close()
  90. respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
  91. if err != nil {
  92. err = fmt.Errorf("read response: %w", err)
  93. slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
  94. return nil, err
  95. }
  96. var cfResp cfResponse
  97. if err := json.Unmarshal(respBody, &cfResp); err != nil {
  98. err = fmt.Errorf("unmarshal response (status %d): %w", resp.StatusCode, err)
  99. slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
  100. return nil, err
  101. }
  102. if !cfResp.Success {
  103. var err error
  104. if len(cfResp.Errors) > 0 {
  105. err = fmt.Errorf("cloudflare API error: %s (code %d)", cfResp.Errors[0].Message, cfResp.Errors[0].Code)
  106. } else {
  107. err = fmt.Errorf("cloudflare API error: status %d", resp.StatusCode)
  108. }
  109. slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
  110. return nil, err
  111. }
  112. slog.Default().Debug("cloudflare request done", "method", method, "path", path, "status", resp.StatusCode, "duration_ms", time.Since(start).Milliseconds())
  113. return cfResp.Result, nil
  114. }