client.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  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 dnsPayload 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 CreateDNSRecord(ctx context.Context, apiKey, zoneID string, rec DNSRecord) (DNSRecord, error) {
  52. url := fmt.Sprintf("%s/zones/%s/dns_records", baseURL, zoneID)
  53. payload := dnsPayload{
  54. Type: rec.Type,
  55. Name: rec.Name,
  56. Content: rec.Content,
  57. Proxied: rec.Proxied,
  58. TTL: 1, // auto
  59. }
  60. data, err := json.Marshal(payload)
  61. if err != nil {
  62. return DNSRecord{}, fmt.Errorf("marshal payload: %w", err)
  63. }
  64. body, err := doRequest(ctx, http.MethodPost, url, apiKey, bytes.NewReader(data))
  65. if err != nil {
  66. return DNSRecord{}, err
  67. }
  68. var created DNSRecord
  69. if err := json.Unmarshal(body, &created); err != nil {
  70. return DNSRecord{}, fmt.Errorf("unmarshal created record: %w", err)
  71. }
  72. return created, nil
  73. }
  74. func UpdateDNSRecord(ctx context.Context, apiKey, zoneID, recordID string, rec DNSRecord) error {
  75. url := fmt.Sprintf("%s/zones/%s/dns_records/%s", baseURL, zoneID, recordID)
  76. payload := dnsPayload{
  77. Type: rec.Type,
  78. Name: rec.Name,
  79. Content: rec.Content,
  80. Proxied: rec.Proxied,
  81. TTL: rec.TTL,
  82. }
  83. data, err := json.Marshal(payload)
  84. if err != nil {
  85. return fmt.Errorf("marshal payload: %w", err)
  86. }
  87. _, err = doRequest(ctx, http.MethodPut, url, apiKey, bytes.NewReader(data))
  88. return err
  89. }
  90. func doRequest(ctx context.Context, method, rawURL, apiKey string, body io.Reader) (json.RawMessage, error) {
  91. parsed, _ := url.Parse(rawURL)
  92. path := parsed.Path
  93. if parsed.RawQuery != "" {
  94. path = path + "?" + parsed.RawQuery
  95. }
  96. start := time.Now()
  97. slog.Default().Debug("cloudflare request", "method", method, "path", path)
  98. req, err := http.NewRequestWithContext(ctx, method, rawURL, body)
  99. if err != nil {
  100. err = fmt.Errorf("create request: %w", err)
  101. slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
  102. return nil, err
  103. }
  104. req.Header.Set("Authorization", "Bearer "+apiKey)
  105. req.Header.Set("Content-Type", "application/json")
  106. resp, err := httpClient.Do(req)
  107. if err != nil {
  108. err = fmt.Errorf("do request: %w", err)
  109. slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
  110. return nil, err
  111. }
  112. defer resp.Body.Close()
  113. respBody, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
  114. if err != nil {
  115. err = fmt.Errorf("read response: %w", err)
  116. slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
  117. return nil, err
  118. }
  119. var cfResp cfResponse
  120. if err := json.Unmarshal(respBody, &cfResp); err != nil {
  121. err = fmt.Errorf("unmarshal response (status %d): %w", resp.StatusCode, err)
  122. slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
  123. return nil, err
  124. }
  125. if !cfResp.Success {
  126. var err error
  127. if len(cfResp.Errors) > 0 {
  128. err = fmt.Errorf("cloudflare API error: %s (code %d)", cfResp.Errors[0].Message, cfResp.Errors[0].Code)
  129. } else {
  130. err = fmt.Errorf("cloudflare API error: status %d", resp.StatusCode)
  131. }
  132. slog.Default().Debug("cloudflare request failed", "method", method, "path", path, "error", err)
  133. return nil, err
  134. }
  135. slog.Default().Debug("cloudflare request done", "method", method, "path", path, "status", resp.StatusCode, "duration_ms", time.Since(start).Milliseconds())
  136. return cfResp.Result, nil
  137. }