package services import ( "encoding/json" "fmt" "io" "net/http" "time" ) const cloudflareBaseURL = "https://api.cloudflare.com/client/v4" // CloudflareClient es el cliente HTTP para la API de Cloudflare. type CloudflareClient struct { token string accountID string httpClient *http.Client } // NewCloudflareClient crea un cliente con el API Token. func NewCloudflareClient(token, accountID string) *CloudflareClient { return &CloudflareClient{ token: token, accountID: accountID, httpClient: &http.Client{ Timeout: 20 * time.Second, }, } } // get realiza una petición GET autenticada y devuelve el body. func (c *CloudflareClient) get(path string, dest interface{}) error { req, err := http.NewRequest(http.MethodGet, cloudflareBaseURL+path, nil) if err != nil { return fmt.Errorf("cloudflare: crear request: %w", err) } req.Header.Set("Authorization", "Bearer "+c.token) req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return fmt.Errorf("cloudflare: ejecutar request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("cloudflare: leer body: %w", err) } if resp.StatusCode < 200 || resp.StatusCode >= 300 { return fmt.Errorf("cloudflare: status %d – %s", resp.StatusCode, string(body)) } // Cloudflare siempre envuelve en {"result":..., "success":true} var wrapper struct { Success bool `json:"success"` Errors []CFError `json:"errors"` Result json.RawMessage `json:"result"` } if err := json.Unmarshal(body, &wrapper); err != nil { return fmt.Errorf("cloudflare: decodificar envelope: %w", err) } if !wrapper.Success { if len(wrapper.Errors) > 0 { return fmt.Errorf("cloudflare API error %d: %s", wrapper.Errors[0].Code, wrapper.Errors[0].Message) } return fmt.Errorf("cloudflare: respuesta no exitosa") } if dest != nil && wrapper.Result != nil { if err := json.Unmarshal(wrapper.Result, dest); err != nil { return fmt.Errorf("cloudflare: decodificar result: %w", err) } } return nil } // ─── Tipos de error ────────────────────────────────────────────────────────── type CFError struct { Code int `json:"code"` Message string `json:"message"` } // ─── Tipos de respuesta ────────────────────────────────────────────────────── // CFUser información del usuario autenticado. type CFUser struct { ID string `json:"id"` Email string `json:"email"` FirstName string `json:"first_name"` LastName string `json:"last_name"` Username string `json:"username"` } // CFZone representa una zona (dominio) en Cloudflare. type CFZone struct { ID string `json:"id"` Name string `json:"name"` Status string `json:"status"` Paused bool `json:"paused"` Type string `json:"type"` NameServers []string `json:"name_servers"` OriginalNS []string `json:"original_name_servers"` ModifiedOn string `json:"modified_on"` ActivatedOn string `json:"activated_on"` Plan CFPlan `json:"plan"` } // CFPlan representa el plan de una zona. type CFPlan struct { ID string `json:"id"` Name string `json:"name"` } // CFDNSRecord representa un registro DNS. type CFDNSRecord struct { ID string `json:"id"` Type string `json:"type"` Name string `json:"name"` Content string `json:"content"` Proxied bool `json:"proxied"` Proxiable bool `json:"proxiable"` TTL int `json:"ttl"` Priority int `json:"priority,omitempty"` CreatedOn string `json:"created_on"` ModifiedOn string `json:"modified_on"` } // CFSSLStatus representa el estado SSL de una zona. type CFSSLStatus struct { ID string `json:"id"` Type string `json:"type"` Status string `json:"status"` Hosts []string `json:"hosts"` PrimaryCert string `json:"primary_certificate"` ExpiresOn string `json:"expires_on"` } // CFFirewallRule representa una regla de firewall (Access Rules). type CFFirewallRule struct { ID string `json:"id"` Notes string `json:"notes"` AllowedModes []string `json:"allowed_modes"` Mode string `json:"mode"` Configuration CFFirewallCfg `json:"configuration"` Scope CFScope `json:"scope"` CreatedOn string `json:"created_on"` ModifiedOn string `json:"modified_on"` } // CFFirewallCfg configuración del target en la regla. type CFFirewallCfg struct { Target string `json:"target"` Value string `json:"value"` } // CFScope scope de la regla (zone o account). type CFScope struct { ID string `json:"id"` Name string `json:"name"` Type string `json:"type"` } // ─── Métodos de la API ─────────────────────────────────────────────────────── // GetUser obtiene la información del usuario autenticado. func (c *CloudflareClient) GetUser() (*CFUser, error) { var user CFUser if err := c.get("/user", &user); err != nil { return nil, err } return &user, nil } // GetZones lista todas las zonas de la cuenta. func (c *CloudflareClient) GetZones() ([]CFZone, error) { var zones []CFZone // pagina 1, 50 por página (máximo) if err := c.get("/zones?per_page=50&page=1", &zones); err != nil { return nil, err } return zones, nil } // GetDNSRecords devuelve los registros DNS de una zona. func (c *CloudflareClient) GetDNSRecords(zoneID string) ([]CFDNSRecord, error) { var records []CFDNSRecord path := fmt.Sprintf("/zones/%s/dns_records?per_page=100&page=1", zoneID) if err := c.get(path, &records); err != nil { return nil, err } return records, nil } // GetSSLCertificates devuelve los certificate packs de una zona. func (c *CloudflareClient) GetSSLCertificates(zoneID string) ([]CFSSLStatus, error) { var certs []CFSSLStatus path := fmt.Sprintf("/zones/%s/ssl/certificate_packs", zoneID) if err := c.get(path, &certs); err != nil { return nil, err } return certs, nil } // GetFirewallRules devuelve las reglas de acceso IP de una zona. func (c *CloudflareClient) GetFirewallRules(zoneID string) ([]CFFirewallRule, error) { var rules []CFFirewallRule path := fmt.Sprintf("/zones/%s/firewall/access_rules/rules?per_page=50&page=1", zoneID) if err := c.get(path, &rules); err != nil { return nil, err } return rules, nil }