2026-06-14 07:48:10 +00:00
|
|
|
# retoor <retoor@molodetz.nl>
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import logging
|
|
|
|
|
from socketserver import ForkingMixIn
|
|
|
|
|
from xmlrpc.server import SimpleXMLRPCDispatcher, SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
|
|
|
|
|
|
2026-06-21 16:46:27 +00:00
|
|
|
from defusedxml.xmlrpc import monkey_patch as _harden_xmlrpc
|
|
|
|
|
|
2026-06-14 07:48:10 +00:00
|
|
|
from devplacepy.config import XMLRPC_BIND, XMLRPC_PORT
|
2026-06-21 16:46:27 +00:00
|
|
|
|
|
|
|
|
_harden_xmlrpc()
|
2026-06-14 07:48:10 +00:00
|
|
|
from . import bridge
|
|
|
|
|
from .registry import Method, build_index, method_help, method_signature
|
|
|
|
|
|
|
|
|
|
logging.basicConfig(
|
|
|
|
|
level=logging.INFO,
|
|
|
|
|
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
|
|
|
)
|
|
|
|
|
logger = logging.getLogger("xmlrpc.server")
|
|
|
|
|
|
|
|
|
|
RPC_PATHS = ("/", "/RPC2", "/xmlrpc")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _auth_from_headers(headers) -> dict[str, str]:
|
|
|
|
|
auth: dict[str, str] = {}
|
|
|
|
|
api_key = headers.get("X-API-KEY") or headers.get("x-api-key")
|
|
|
|
|
authorization = headers.get("Authorization") or headers.get("authorization") or ""
|
|
|
|
|
if not api_key and authorization.lower().startswith("bearer "):
|
|
|
|
|
api_key = authorization[7:].strip()
|
|
|
|
|
if authorization.lower().startswith("basic "):
|
|
|
|
|
auth["authorization"] = authorization.strip()
|
|
|
|
|
if api_key:
|
|
|
|
|
auth["api_key"] = api_key.strip()
|
|
|
|
|
real_ip = headers.get("X-Real-IP") or headers.get("x-real-ip")
|
|
|
|
|
if real_ip:
|
|
|
|
|
auth["real_ip"] = real_ip
|
|
|
|
|
forwarded = headers.get("X-Forwarded-For") or headers.get("x-forwarded-for")
|
|
|
|
|
if forwarded:
|
|
|
|
|
auth["forwarded_for"] = forwarded
|
|
|
|
|
return auth
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class _RequestHandler(SimpleXMLRPCRequestHandler):
|
|
|
|
|
rpc_paths = RPC_PATHS
|
|
|
|
|
|
|
|
|
|
def decode_request_content(self, data):
|
2026-06-21 16:46:27 +00:00
|
|
|
if b"<!DOCTYPE" in data or b"<!ENTITY" in data:
|
|
|
|
|
self.send_response(400)
|
|
|
|
|
self.send_header("Content-length", "0")
|
|
|
|
|
self.end_headers()
|
|
|
|
|
return None
|
2026-06-14 07:48:10 +00:00
|
|
|
self.server.current_auth = _auth_from_headers(self.headers)
|
|
|
|
|
return super().decode_request_content(data)
|
|
|
|
|
|
|
|
|
|
def log_message(self, *args, **kwargs):
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class BridgeDispatcher(SimpleXMLRPCDispatcher):
|
|
|
|
|
def system_methodHelp(self, method_name):
|
|
|
|
|
target = self.index.get(method_name)
|
|
|
|
|
if target is not None:
|
|
|
|
|
return method_help(target)
|
|
|
|
|
return super().system_methodHelp(method_name)
|
|
|
|
|
|
|
|
|
|
def system_methodSignature(self, method_name):
|
|
|
|
|
target = self.index.get(method_name)
|
|
|
|
|
if target is not None:
|
|
|
|
|
return method_signature(target)
|
|
|
|
|
return super().system_methodSignature(method_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ForkingXMLRPCServer(ForkingMixIn, BridgeDispatcher, SimpleXMLRPCServer):
|
|
|
|
|
allow_reuse_address = True
|
|
|
|
|
daemon_threads = True
|
|
|
|
|
|
|
|
|
|
def __init__(self, addr, index):
|
|
|
|
|
self.index = index
|
|
|
|
|
self.current_auth: dict[str, str] = {}
|
|
|
|
|
SimpleXMLRPCServer.__init__(
|
|
|
|
|
self,
|
|
|
|
|
addr,
|
|
|
|
|
requestHandler=_RequestHandler,
|
|
|
|
|
allow_none=True,
|
|
|
|
|
logRequests=False,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _make_handler(server: "ForkingXMLRPCServer", method: Method):
|
|
|
|
|
def handler(*args):
|
|
|
|
|
params = args[0] if args else {}
|
|
|
|
|
return bridge.call(method, params, server.current_auth)
|
|
|
|
|
|
|
|
|
|
handler.__name__ = method.name
|
|
|
|
|
handler.__doc__ = method_help(method)
|
|
|
|
|
return handler
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_server(host: str, port: int) -> ForkingXMLRPCServer:
|
|
|
|
|
index = build_index()
|
|
|
|
|
server = ForkingXMLRPCServer((host, port), index)
|
|
|
|
|
server.register_introspection_functions()
|
|
|
|
|
server.register_multicall_functions()
|
|
|
|
|
for name, method in index.items():
|
|
|
|
|
server.register_function(_make_handler(server, method), name)
|
|
|
|
|
logger.info("XML-RPC bridge registered %d methods", len(index))
|
|
|
|
|
return server
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
server = build_server(XMLRPC_BIND, XMLRPC_PORT)
|
|
|
|
|
logger.info("Forking XML-RPC server listening on %s:%d", XMLRPC_BIND, XMLRPC_PORT)
|
|
|
|
|
try:
|
|
|
|
|
server.serve_forever()
|
|
|
|
|
except KeyboardInterrupt:
|
|
|
|
|
logger.info("XML-RPC server interrupted")
|
|
|
|
|
finally:
|
|
|
|
|
server.server_close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|