| """
|
| Cloudflare API Client
|
| Handles authentication and base HTTP operations for Cloudflare services
|
| """
|
|
|
| import asyncio
|
| import json
|
| from typing import Any, Dict, Optional, Union
|
|
|
| import aiohttp
|
|
|
| from app.logger import logger
|
|
|
|
|
| class CloudflareClient:
|
| """Base client for Cloudflare API operations"""
|
|
|
| def __init__(
|
| self,
|
| api_token: str,
|
| account_id: str,
|
| worker_url: Optional[str] = None,
|
| timeout: int = 30,
|
| ):
|
| self.api_token = api_token
|
| self.account_id = account_id
|
| self.worker_url = worker_url
|
| self.timeout = timeout
|
| self.base_url = "https://api.cloudflare.com/client/v4"
|
|
|
|
|
| self.headers = {
|
| "Authorization": f"Bearer {api_token}",
|
| "Content-Type": "application/json",
|
| }
|
|
|
| async def _make_request(
|
| self,
|
| method: str,
|
| url: str,
|
| data: Optional[Dict[str, Any]] = None,
|
| headers: Optional[Dict[str, str]] = None,
|
| use_worker: bool = False,
|
| ) -> Dict[str, Any]:
|
| """Make HTTP request to Cloudflare API or Worker"""
|
|
|
|
|
| if use_worker and self.worker_url:
|
| full_url = f"{self.worker_url.rstrip('/')}/{url.lstrip('/')}"
|
| else:
|
| full_url = f"{self.base_url}/{url.lstrip('/')}"
|
|
|
| request_headers = self.headers.copy()
|
| if headers:
|
| request_headers.update(headers)
|
|
|
| timeout = aiohttp.ClientTimeout(total=self.timeout)
|
|
|
| try:
|
| async with aiohttp.ClientSession(timeout=timeout) as session:
|
| async with session.request(
|
| method=method.upper(),
|
| url=full_url,
|
| headers=request_headers,
|
| json=data if data else None,
|
| ) as response:
|
| response_text = await response.text()
|
|
|
| try:
|
| response_data = (
|
| json.loads(response_text) if response_text else {}
|
| )
|
| except json.JSONDecodeError:
|
| response_data = {"raw_response": response_text}
|
|
|
| if not response.ok:
|
| logger.error(
|
| f"Cloudflare API error: {response.status} - {response_text}"
|
| )
|
| raise CloudflareError(
|
| f"HTTP {response.status}: {response_text}",
|
| response.status,
|
| response_data,
|
| )
|
|
|
| return response_data
|
|
|
| except asyncio.TimeoutError:
|
| logger.error(f"Timeout making request to {full_url}")
|
| raise CloudflareError(f"Request timeout after {self.timeout}s")
|
| except aiohttp.ClientError as e:
|
| logger.error(f"HTTP client error: {e}")
|
| raise CloudflareError(f"Client error: {e}")
|
|
|
| async def get(
|
| self,
|
| url: str,
|
| headers: Optional[Dict[str, str]] = None,
|
| use_worker: bool = False,
|
| ) -> Dict[str, Any]:
|
| """Make GET request"""
|
| return await self._make_request(
|
| "GET", url, headers=headers, use_worker=use_worker
|
| )
|
|
|
| async def post(
|
| self,
|
| url: str,
|
| data: Optional[Dict[str, Any]] = None,
|
| headers: Optional[Dict[str, str]] = None,
|
| use_worker: bool = False,
|
| ) -> Dict[str, Any]:
|
| """Make POST request"""
|
| return await self._make_request(
|
| "POST", url, data=data, headers=headers, use_worker=use_worker
|
| )
|
|
|
| async def put(
|
| self,
|
| url: str,
|
| data: Optional[Dict[str, Any]] = None,
|
| headers: Optional[Dict[str, str]] = None,
|
| use_worker: bool = False,
|
| ) -> Dict[str, Any]:
|
| """Make PUT request"""
|
| return await self._make_request(
|
| "PUT", url, data=data, headers=headers, use_worker=use_worker
|
| )
|
|
|
| async def delete(
|
| self,
|
| url: str,
|
| headers: Optional[Dict[str, str]] = None,
|
| use_worker: bool = False,
|
| ) -> Dict[str, Any]:
|
| """Make DELETE request"""
|
| return await self._make_request(
|
| "DELETE", url, headers=headers, use_worker=use_worker
|
| )
|
|
|
| async def upload_file(
|
| self,
|
| url: str,
|
| file_data: bytes,
|
| content_type: str = "application/octet-stream",
|
| headers: Optional[Dict[str, str]] = None,
|
| use_worker: bool = False,
|
| ) -> Dict[str, Any]:
|
| """Upload file data"""
|
|
|
|
|
| if use_worker and self.worker_url:
|
| full_url = f"{self.worker_url.rstrip('/')}/{url.lstrip('/')}"
|
| else:
|
| full_url = f"{self.base_url}/{url.lstrip('/')}"
|
|
|
| upload_headers = {
|
| "Authorization": f"Bearer {self.api_token}",
|
| "Content-Type": content_type,
|
| }
|
| if headers:
|
| upload_headers.update(headers)
|
|
|
| timeout = aiohttp.ClientTimeout(
|
| total=self.timeout * 2
|
| )
|
|
|
| try:
|
| async with aiohttp.ClientSession(timeout=timeout) as session:
|
| async with session.put(
|
| url=full_url, headers=upload_headers, data=file_data
|
| ) as response:
|
| response_text = await response.text()
|
|
|
| try:
|
| response_data = (
|
| json.loads(response_text) if response_text else {}
|
| )
|
| except json.JSONDecodeError:
|
| response_data = {"raw_response": response_text}
|
|
|
| if not response.ok:
|
| logger.error(
|
| f"File upload error: {response.status} - {response_text}"
|
| )
|
| raise CloudflareError(
|
| f"Upload failed: HTTP {response.status}",
|
| response.status,
|
| response_data,
|
| )
|
|
|
| return response_data
|
|
|
| except asyncio.TimeoutError:
|
| logger.error(f"Timeout uploading file to {full_url}")
|
| raise CloudflareError(f"Upload timeout after {self.timeout * 2}s")
|
| except aiohttp.ClientError as e:
|
| logger.error(f"Upload client error: {e}")
|
| raise CloudflareError(f"Upload error: {e}")
|
|
|
| def get_account_url(self, endpoint: str) -> str:
|
| """Get URL for account-scoped endpoint"""
|
| return f"accounts/{self.account_id}/{endpoint}"
|
|
|
| def get_worker_url(self, endpoint: str) -> str:
|
| """Get URL for worker endpoint"""
|
| if not self.worker_url:
|
| raise CloudflareError("Worker URL not configured")
|
| return endpoint
|
|
|
|
|
| class CloudflareError(Exception):
|
| """Cloudflare API error"""
|
|
|
| def __init__(
|
| self,
|
| message: str,
|
| status_code: Optional[int] = None,
|
| response_data: Optional[Dict[str, Any]] = None,
|
| ):
|
| super().__init__(message)
|
| self.status_code = status_code
|
| self.response_data = response_data or {}
|
|
|
| def __str__(self) -> str:
|
| if self.status_code:
|
| return f"CloudflareError({self.status_code}): {super().__str__()}"
|
| return f"CloudflareError: {super().__str__()}"
|
|
|