Files
devplacepy/devplacepy/services/xmlrpc/server.py
T
retoor 19cc85f409 fix: correct audit event names, attachment deletion, redirect safety, and add account deactivation check
- Fix audit event names in CLI prune/clear commands from `cli.seo.*` to `cli.seo_meta.*`
- Refactor `_delete_attachment_file` to accept full attachment dict instead of storage_path string, using directory and stored_name fields with ATTACHMENTS_DIR
- Add `safe_next` validation for referer header in validation error redirect and media redirect
- Add `is_active` check in login router to reject deactivated accounts with "Account is deactivated" error
- Replace raw `request.headers.get("Referer")` with `redirect_back()` utility in bookmarks, polls, reactions, and votes routers
- Move `mark_conversation_read` call from `get_conversation_messages` to `messages_page` to avoid side effects during message retrieval
- Fix poll audit link to use `option.get("label")` instead of `option.get("text")`
- Add `VOTABLE` set validation in votes router to reject invalid target types with 400 response
- Strip control characters (0x00-0x20) from URLs in `_safe_url` instead of simple strip
- Add `__getattr__` fallback in services `__init__.py` for dynamic attribute access
2026-06-21 16:46:27 +00:00

125 lines
3.9 KiB
Python

# retoor <retoor@molodetz.nl>
from __future__ import annotations
import logging
from socketserver import ForkingMixIn
from xmlrpc.server import SimpleXMLRPCDispatcher, SimpleXMLRPCRequestHandler, SimpleXMLRPCServer
from defusedxml.xmlrpc import monkey_patch as _harden_xmlrpc
from devplacepy.config import XMLRPC_BIND, XMLRPC_PORT
_harden_xmlrpc()
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):
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
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()