forked from retoor/devplacepy
- 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
125 lines
3.9 KiB
Python
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()
|