|
|
"""
|
|
|
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__()}"
|
|
|
|