|
# retoor <retoor@molodetz.nl>
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import xmlrpc.client
|
|
from typing import Any
|
|
|
|
from devplacepy import stealth
|
|
from devplacepy.config import INTERNAL_BASE_URL
|
|
from .registry import Method
|
|
|
|
logger = logging.getLogger("xmlrpc.bridge")
|
|
|
|
FAULT_INVALID_PARAMS = -32602
|
|
FAULT_MISSING_PARAM = -32001
|
|
FAULT_TRANSPORT = -32300
|
|
|
|
WRITE_METHODS = {"POST", "PUT", "DELETE", "PATCH"}
|
|
TIMEOUT_SECONDS = 60.0
|
|
|
|
|
|
def _coerce_value(value: Any) -> str:
|
|
if isinstance(value, bool):
|
|
return "1" if value else "0"
|
|
return str(value)
|
|
|
|
|
|
def _build_path(method: Method, params: dict[str, Any]) -> str:
|
|
path = method.path
|
|
for param in method.path_params:
|
|
if param.name not in params:
|
|
raise xmlrpc.client.Fault(
|
|
FAULT_MISSING_PARAM,
|
|
f"Missing required path parameter '{param.name}' for {method.name}.",
|
|
)
|
|
path = path.replace("{" + param.name + "}", _coerce_value(params[param.name]))
|
|
return path
|
|
|
|
|
|
def _split_params(
|
|
method: Method, params: dict[str, Any]
|
|
) -> tuple[dict[str, str], dict[str, str]]:
|
|
declared = {p.name: p for p in method.params}
|
|
path_names = {p.name for p in method.path_params}
|
|
query: dict[str, str] = {}
|
|
form: dict[str, str] = {}
|
|
for key, value in params.items():
|
|
if key in path_names:
|
|
continue
|
|
spec = declared.get(key)
|
|
location = spec.location if spec else None
|
|
if location == "query":
|
|
query[key] = _coerce_value(value)
|
|
elif location == "form":
|
|
form[key] = _coerce_value(value)
|
|
elif method.http_method in WRITE_METHODS:
|
|
form[key] = _coerce_value(value)
|
|
else:
|
|
query[key] = _coerce_value(value)
|
|
return query, form
|
|
|
|
|
|
def _check_required(method: Method, params: dict[str, Any]) -> None:
|
|
for spec in method.params:
|
|
if spec.required and spec.name not in params:
|
|
raise xmlrpc.client.Fault(
|
|
FAULT_MISSING_PARAM,
|
|
f"Missing required parameter '{spec.name}' for {method.name}.",
|
|
)
|
|
|
|
|
|
def _decode(response: Any) -> Any:
|
|
content_type = response.headers.get("content-type", "")
|
|
if "application/json" in content_type:
|
|
try:
|
|
return response.json()
|
|
except ValueError:
|
|
return response.text
|
|
return response.text
|
|
|
|
|
|
def call(method: Method, params: dict[str, Any], auth: dict[str, str]) -> Any:
|
|
if not isinstance(params, dict):
|
|
raise xmlrpc.client.Fault(
|
|
FAULT_INVALID_PARAMS,
|
|
f"{method.name} expects a single struct of named parameters.",
|
|
)
|
|
params = dict(params)
|
|
api_key = str(params.pop("api_key", "") or auth.get("api_key", ""))
|
|
_check_required(method, params)
|
|
path = _build_path(method, params)
|
|
query, form = _split_params(method, params)
|
|
|
|
headers = {
|
|
"Accept": "application/json",
|
|
"X-Requested-With": "fetch",
|
|
}
|
|
if api_key:
|
|
headers["Authorization"] = f"Bearer {api_key}"
|
|
headers["X-API-KEY"] = api_key
|
|
elif auth.get("authorization"):
|
|
headers["Authorization"] = auth["authorization"]
|
|
if auth.get("real_ip"):
|
|
headers["X-Real-IP"] = auth["real_ip"]
|
|
if auth.get("forwarded_for"):
|
|
headers["X-Forwarded-For"] = auth["forwarded_for"]
|
|
|
|
logger.debug("Bridge %s -> %s %s", method.name, method.http_method, path)
|
|
try:
|
|
with stealth.stealth_sync_client(
|
|
base_url=INTERNAL_BASE_URL,
|
|
timeout=TIMEOUT_SECONDS,
|
|
follow_redirects=True,
|
|
headers=headers,
|
|
) as client:
|
|
response = client.request(
|
|
method.http_method,
|
|
path,
|
|
params=query or None,
|
|
data=form or None,
|
|
)
|
|
except Exception as exc:
|
|
logger.warning("Bridge transport error for %s: %s", method.name, exc)
|
|
raise xmlrpc.client.Fault(
|
|
FAULT_TRANSPORT, f"Transport error calling {method.name}: {exc}"
|
|
)
|
|
|
|
body = _decode(response)
|
|
if response.status_code >= 400:
|
|
message = body
|
|
if isinstance(body, dict):
|
|
error = body.get("error")
|
|
if isinstance(error, dict):
|
|
message = error.get("message", body)
|
|
elif error:
|
|
message = error
|
|
else:
|
|
message = body.get("detail", body)
|
|
raise xmlrpc.client.Fault(response.status_code, _stringify(message))
|
|
return body
|
|
|
|
|
|
def _stringify(message: Any) -> str:
|
|
if isinstance(message, str):
|
|
return message
|
|
return str(message)
|