Skip to content

Latest commit

 

History

History
82 lines (53 loc) · 5.31 KB

File metadata and controls

82 lines (53 loc) · 5.31 KB

CLAUDE.md

Guidance for Claude Code when working in this repository.

Overview

A demonstration XML-RPC client/server in Python with two additions on top of stock xmlrpc: keyword-argument support, and exact transport of integers/floats that XML-RPC's native types cannot represent. Four files carry all the logic — server.py, client.py, utils.py, plus the tests.

Commands

pip install -r requirements.txt

python server.py both                  # HTTP :8000 + HTTPS :8443
python server.py http --port 9000      # single protocol; --host/--port
python server.py https                 # generates server.crt/.key on first run

python client.py add 2 2               # add/subtract/multiply/divide
python client.py list-methods --url https://localhost:8443
python client.py test                  # connection check

pytest tests/                          # unit tests always run; e2e tests
                                       # skip unless servers are listening
LOG_LEVEL=DEBUG python client.py add 1 1   # both log sinks honour LOG_LEVEL

Running the full e2e suite requires python server.py both in another terminal.

Architecture

The Data envelope

XML-RPC has no keyword arguments and no way to express a "response object", so every non-system call is wrapped in a Data (utils.py).

Request path:

  1. DataServerProxy._Method.__call__ (client.py) packs the call's *args/**kwargs into a Data and sends it as the single XML-RPC parameter.
  2. ThreadedXMLRPCServer._dispatch (server.py) recognises the dict by its _is_data_object marker, unpacks and decodes it, and calls the target function.
  3. The return value is wrapped in another Data carrying response_code (200/400/404), result and error.
  4. XMLRPCClient._return_result decodes it, raising RPCError on a server-reported error.

Data is marshalled by making it look like a dict: __dict__ is overridden as a property returning _get_dict_data(), which is what xmlrpc.client's dump_instance reads. That property override is load-bearing — do not replace it with a plain attribute. _get_dict_data() also strips None values, so error/result keys may be absent; always read them with .get().

Wire markers (utils.py)

XML-RPC's <int> is 32-bit and <double> is a lossy float, so numbers travel as tagged strings:

Python value On the wire
42 "__INT__42"
3.14 "__FLOAT__3.14"
"__INT__42" (a real string) "__STR____INT__42"
True / False unchanged (checked before int, since bool subclasses int)

convert_value_for_xmlrpc encodes, convert_value_from_xmlrpc decodes; both recurse through lists, tuples and dicts.

The __STR__ escape is required for correctness. Without it a user string that happens to start with __INT__ decodes into an integer on the far side. Any change to the marker set must keep encode/decode symmetric — tests/test_conversion.py asserts the round trip.

Threading and TLS

ThreadedXMLRPCServer is a ThreadingMixIn server, so each request is handled on its own thread.

The HTTPS path deliberately does not wrap the listening socket. Doing so makes SSLSocket.accept() run the TLS handshake inside the single accept loop, where one client that connects and stays silent blocks every other client — a one-line denial of service. Instead the context is passed to the server and get_request() wraps each accepted connection with do_handshake_on_connect=False, deferring the handshake to the first read on the worker thread. BoundedXMLRPCRequestHandler.timeout then bounds how long a silent client can hold that thread. Preserve this arrangement.

Invariants

  • Never dispatch underscore-prefixed method names. SimpleXMLRPCServer blocks them in resolve_dotted_attribute, but the custom _dispatch bypasses that path; the explicit guard in _dispatch is what stops __dir__, __repr__ and friends from being remotely callable. register_instance exposes every public attribute of MathFunctions, so this check is the only boundary.
  • Server errors become Data(response_code=400), not XML-RPC faults. _dispatch catches everything. Callers must check response_code, and CLI paths must map failures to a non-zero exit.
  • Keep encode/decode symmetric (see wire markers above).
  • The private key is written 0600 via _write_secret. Don't switch it back to a plain open().

Testing

  • tests/test_conversion.py — pure unit tests for conversion and Data.
  • tests/test_client.py — end-to-end; the session-scoped http_url/https_url fixtures skip when no server is listening. Do not reintroduce pytest.exit here: it aborts the entire run, taking the server-independent tests with it.
  • conftest.py at the repo root puts the root on sys.path; without it plain pytest tests/ fails with ModuleNotFoundError.

Known limitations

  • Data validates arguments with json.dumps, but the transport is XML-RPC. That rejects bytes and datetime, which XML-RPC handles natively with use_builtin_types=True. Intentional conservatism, but it is stricter than the wire format requires.
  • No authentication, and the client disables certificate verification by default. Local demo only — see the security notes in README.md.
  • logs/, server.crt and server.key are generated at runtime and gitignored.