# retoor <retoor@molodetz.nl>
import logging
import httpx
from fastapi import APIRouter, Request
from starlette.responses import PlainTextResponse, Response
from devplacepy.config import XMLRPC_BIND, XMLRPC_PORT
logger = logging.getLogger(__name__)
router = APIRouter()
CONNECT_HOST = XMLRPC_BIND if XMLRPC_BIND not in ("0.0.0.0", "::") else "127.0.0.1"
TARGET_URL = f"http://{CONNECT_HOST}:{XMLRPC_PORT}/"
TIMEOUT_SECONDS = 65.0
FORWARD_HEADERS = (
"authorization",
"x-api-key",
"x-real-ip",
"x-forwarded-for",
"content-type",
)
USAGE = (
"DevPlace XML-RPC bridge\n\n"
"POST XML-RPC method calls to this endpoint. Every documented REST endpoint is\n"
"exposed as a method named after its id with dots (for example posts.create,\n"
"feed.index, profile.update). Each method takes ONE struct of named parameters;\n"
"authenticate with 'api_key' inside the struct or an X-API-KEY / Bearer header.\n\n"
"Introspection: system.listMethods, system.methodHelp, system.methodSignature.\n"
"Batching: system.multicall.\n\n"
"Python example:\n"
" import xmlrpc.client\n"
" proxy = xmlrpc.client.ServerProxy('https://your-host/xmlrpc', allow_none=True)\n"
" print(proxy.system.listMethods())\n"
" proxy.posts.create({'title': 'Hi', 'body': '...', 'api_key': 'YOUR_KEY'})\n"
)
@router.get("")
@router.get("/")
@router.get("/{path:path}")
async def info(request: Request, path: str = "") -> PlainTextResponse:
return PlainTextResponse(USAGE)
@router.post("")
@router.post("/")
@router.post("/{path:path}")
async def proxy(request: Request, path: str = "") -> Response:
body = await request.body()
headers = {
key: value
for key, value in request.headers.items()
if key.lower() in FORWARD_HEADERS
}
headers.setdefault("content-type", "text/xml")
try:
async with httpx.AsyncClient(timeout=TIMEOUT_SECONDS) as client:
upstream = await client.post(TARGET_URL, content=body, headers=headers)
except httpx.HTTPError as exc:
logger.warning("XML-RPC bridge unavailable: %s", exc)
return PlainTextResponse(
"XML-RPC bridge is not available.", status_code=502
)
return Response(
content=upstream.content,
status_code=upstream.status_code,
media_type=upstream.headers.get("content-type", "text/xml"),
)