Skip to main content

HTTPdis

A small synchronous HTTP dispatcher for Python services: named and regex routes, JSON/form payloads, responses, static files and Basic authentication. Used by dwho, Covenant and Auton. HTTPdis is a reference to the TARDIS in Doctor Who, matching the naming of Sonicprobe, dwho and Auton. This is independent software, not an official product.

When to use it

HTTPdis is useful for maintaining and extending services already built around its request handlers and lifecycle hooks. It uses the Python standard HTTP server and sonicprobe workers; it is not an ASGI server. For an unrelated new application, compare the maintenance cost of this custom stack with a maintained web framework.

Installation

python -m pip install httpdis

The declared range remains Python 2.7 and Python 3.5+. Legacy installations require compatible dependency versions. On Python 3.13+, legacy-cgi and legacycrypt replace removed standard-library APIs while preserving multipart and password-file interfaces. Static MIME detection requires libmagic. See the CI matrix for actual tested interpreters and the tests for the covered behaviors.

Minimal JSON service

from httpdis.ext import httpdis_json as server

def health(request):
    return {'status': 'ok'}

server.register(health, 'GET', name='health')
options = {'listen_addr': '127.0.0.1', 'listen_port': 8080, 'max_workers': 2}
server.init(options)
server.run(options)
curl http://127.0.0.1:8080/health

Handlers receive a request with get_path(), query_params(), payload_params(), get_headers() and get_server_vars(). They may return data or an HttpResponse / HttpResponseJson to control status and headers. register supports method lists, compiled regular expressions and safe_init, at_start, at_stop hooks. Routes and options are process-global; use a separate process for independent apps.

Request body limits

Since HTTPdis 0.6.28, register(..., max_body_size=...) sets the body limit for a named or regex route. It applies to POST, PUT and PATCH before reading or parsing the body; oversized requests return HTTP 413.

server.register(action_handler, 'POST', name='v1/actions', max_body_size=1024)
server.register(notification_handler, 'POST', name='v1/notifications',
                max_body_size=64 * 1024)

Define your handlers before registering them. The value is a non-negative integer number of bytes. 0 allows only an empty body; None or an omitted argument uses the current global max_body_size option (1 MiB by default). A route value replaces the global default and may be smaller or larger. It does not change other routes or the global option. Negative values, booleans, floats and strings are rejected during registration.

Existing registrations and positional arguments remain compatible. JSON services using httpdis.ext.httpdis_json.register share the same option. GET/HEAD/DELETE continue to use the query handler; this option does not change their behavior.

Authentication and deployment

Configure auth_basic_file with a trusted htpasswd file and auth_basic with the realm. Set to_auth=True on protected routes, or use a username list such as to_auth=['alice']. A protected route without configured authentication now returns 401 rather than allowing the request. Invalid credentials are rejected; request credentials are isolated between threads. Existing {SHA} and system crypt hashes remain readable for compatibility; legacy SHA-1/DES hashes are not recommendations for new password storage. Basic credentials require HTTPS in transit.

Bind to loopback or a private interface behind a reverse proxy providing TLS, request timeouts, connection limits and access controls. This release does not claim complete HTTP parser hardening for hostile public traffic. Configure max_body_size to limit request bodies. Chunked request bodies are not supported. Internal exceptions are logged server-side and return a generic 500 to clients.

Static files

A static route uses static=True and root='/srv/public'. Regex routes can use replacement to map URLs to filenames. Resolved symlinks outside the root are rejected. Treat the static directory as administrator-controlled; these checks are not a filesystem sandbox against concurrent hostile filesystem modifications. Invalid If-Modified-Since values are ignored. Files are buffered in memory, so serve large downloads through the reverse proxy.

Compatibility fixes

  • Correct UTF-8 response byte lengths and duplicate Content-Type/Length headers.
  • Correct Basic authentication on Python 3 and username-list registration.
  • Keep credentials isolated across request threads and reject malformed headers.
  • Reject missing authentication on protected routes and truncated request bodies.
  • Resolve static paths before checking containment; tolerate invalid dates.
  • Keep removed CGI/crypt functionality available through compatibility packages.

The dependency loop with sonicprobe is retained because sonicprobe's historical HTTP JSON module re-exports HTTPdis. A future major release should untangle it.

Development and publication

python -m pip install -e . mock
python -m unittest discover -s tests -v
python -m pip install build twine
python -m build
python -m twine check --strict dist/*

Update VERSION, RELEASE and setup.yml together. After tests and distribution validation succeed, the master publication workflow creates vX.Y.Z and uploads via Trusted Publishing (decryptus/httpdis, pypi.yml, environment pypi). Already-tagged versions are not replaced by ordinary master commits.

License: GPL-3.0-or-later. Original authors and Wazo/Proformatique copyrights are preserved in the source files.

See the September 2026 code and architecture review (French).

Independent embedded servers

Use HttpServerContext when multiple HTTPdis servers must coexist in one process. Each context owns its routes, options, authentication object and lifecycle state:

from httpdis.ext.httpdis_json import HttpServerContext

api = HttpServerContext()
api.register(lambda request: {'status': 'ready'}, 'GET', name='health')
api.init({'listen_addr': '127.0.0.1', 'listen_port': 8666,
          'max_body_size': 1048576})
api.run()  # Blocking; another thread or the launcher may call api.stop().

httpdis.httpdis.HttpServerContext uses the plain HTTP handler; httpdis.ext.httpdis_json.HttpServerContext uses the JSON handler. The registration arguments and route callbacks match the existing module API. Callbacks can obtain the owning context through request.get_context(). Application services should still receive explicit decoded values and caller identity, not HTTP requests.

run(options=None, http_req_handler=..., http_server_class=...) binds a private handler subclass, so server version settings and mutable content-type lists do not modify the supplied handler class or another server. Omitting options passes the context's initialized options to at_start; supplying it preserves the historical callback argument behavior. context.server exposes the running transport (including the actual port when listening on port 0).

For a separately constructed stdlib-compatible HTTP server, use context.bind_handler(YourHandler) or assign server.httpdis_context = context before serving requests. The bound handler carries its context explicitly.

Register routes and initialize before serving. A context rejects concurrent run() calls and reinitialization while running. stop() affects only that context and is idempotent until the next init(). A stopped context must be initialized before restarting. Reinitialization retains registered routes, as with the historical API. This is not a hot-reconfiguration interface. Hooks still run per registered method/route entry; sharing application objects in callbacks is the caller's responsibility. Transport shutdown retains Sonicprobe's existing worker behavior; it does not forcibly cancel running application callbacks.

Signals remain process-wide. Context initialization does not install handlers unless use_sigterm_handler=True is explicitly requested. In an embedded process, let the launcher choose which contexts to stop.

Compatibility with the global API

The module-level register, init, run, stop and sigterm_handler functions remain available with their existing signatures, including the JSON wrappers. They delegate to a default context. Historical _COMMANDS, _NCMD, _RCMD, _OPTIONS, _AUTH, _HTTP_SERVER and _KILLED attributes still reflect that context, including direct reassignment. Handlers used without an explicit context continue to use this default. Global init() still installs signal handlers by default. These global calls remain a single shared server configuration; use explicit contexts for independent servers.

Routes, authentication, response formats, route/global body-limit fallback and lifecycle callback arguments remain compatible. Handler classes are now bound through a subclass rather than modified in place. Repeated stop() calls no longer repeat shutdown hooks. Startup/serving failures close the created socket, and shutdown-hook failures still trigger transport cleanup.

The default schema is defined once in httpdis.config. Both historical DEFAULT_OPTIONS import paths remain independent dictionaries with the same initial values; get_default_options() returns a fresh dictionary. Mutating an export is not a way to reconfigure an already initialized context.

Application-owned authentication providers (development)

The embedding daemon may supply auth_provider when initializing a server context. This is an additive extension, not a new default authentication backend. HTTPdis neither chooses credential storage nor opens a database for this interface.

from httpdis.authentication import AuthenticationProvider, Identity
from httpdis.ext.httpdis_json import HttpServerContext

class DaemonProvider(AuthenticationProvider):
    challenge = 'Bearer'

    def __init__(self, credential_service):
        self.credentials = credential_service

    def authenticate(self, request):
        # The supplied service verifies expiry/revocation and returns an account.
        account = self.credentials.verify(request.header('authorization'))
        if account is None:
            return None
        return Identity(account.name, 'token', account.scopes)

context = HttpServerContext()
# credential_service is constructed/configured by the embedding application.
# context.init({'auth_provider': DaemonProvider(credential_service), ...})

Use the same auth_provider option on the existing options object or module-level init() facade when needed. Independent daemons must use independent HttpServerContext instances; the historical module facade remains a singleton. DWho/application configuration should construct a provider explicitly and pass the instance. No dynamic backend import, YAML syntax, credential path or default store is introduced by HTTPdis here.

A provider implements authenticate(AuthenticationRequest) and returns an immutable Identity(principal, method, scopes). It may return None or raise AuthenticationDenied for invalid/missing/expired/revoked credentials. Backend failure, including AuthenticationUnavailable, produces a generic HTTP 503; exception details are not exposed or logged by the adapter. There is no fallback to Basic or anonymous access after provider failure. A fixed optional challenge sets WWW-Authenticate on HTTP 401. Route user allowlists still apply (HTTP 403).

The request contains the method, parsed path without query parameters, selected security headers as immutable pairs, and the direct socket peer address. Header lookup is case-insensitive; duplicate security headers are rejected. Forwarded identity headers are never trusted or passed through this contract. No HTTP handler, request body or mutable server context is passed to the provider. Do not log header values: they contain credentials and cookies. The request repr is redacted.

Successful requests expose HTTP_AUTH_USER for existing applications and HTTP_AUTH_IDENTITY for the verified method/scopes. They never expose a password. Scopes are data, not automatic authorization: applications must enforce them alongside endpoint/owner/maintenance permissions before accepting scoped tokens. Authentication applies only to routes registered with to_auth; this extension does not silently make public routes private.

Providers and their storage adapters must support concurrent calls. Their lifecycle, configuration, defaults, persistence, revocation and shutdown belong to the daemon. Two contexts may use distinct provider instances and stores without sharing state. Sharing a provider is an explicit application decision. HTTPdis does not cache provider identities or take ownership of the supplied backend.

Compatibility and remaining work

Without auth_provider, Basic, auth_basic_file, auth_basic, route allowlists and the historical API keep their behavior. Configuring both a provider and auth_basic_file is rejected rather than establishing an implicit fallback. The new contract uses syntax supported by the existing Python compatibility matrix and adds no required dependency.

The provider contract alone does not supply accounts or credential verification; the optional local service described below supplies those separately. Browser login HTTP routes and TOTP enrollment/replay protection remain pending. The contract does not configure cookies, HTTPS, CSRF or CORS automatically. Browser sessions must not be exposed until those transport controls and the backend are implemented and tested together. Existing HTTPdis CORS/OPTIONS behavior is unchanged by this compatibility-preserving addition.

Local authentication backend (development, optional)

httpdis.auth_backend.LocalAuthService supplies local account, opaque session and opaque API token behavior behind the provider contract. The daemon must supply both a store and a password adapter. No file path, database, credentials, accounts or HTTP login routes are created by importing HTTPdis.

from httpdis.auth_backend import LocalAuthService, Argon2Passwords, BearerAuthProvider

# store is selected and constructed by the application, not HTTPdis.
# auth = LocalAuthService(store=store, passwords=Argon2Passwords(), audit=audit)
# context.init({'auth_provider': BearerAuthProvider(auth), ...})

Install httpdis[auth] on Python 3.10+ to use the optional maintained argon2-cffi adapter. It creates Argon2id password hashes. The core/provider contract and injected service remain compatible with the legacy interpreter matrix, without installing that optional dependency there. No weak password implementation is supplied as a fallback. The password adapter contract is hash(password) -> encoded_hash and verify(encoded_hash, password) -> bool; backend/configuration errors must raise, not return a successful identity.

Administration and account changes

provision(principal, password, scopes) creates or replaces an account. Replacing an account rotates its revision and invalidates all existing sessions/tokens. disable(principal) disables login and invalidates existing credentials. issue_token(principal, scopes, ttl) requires a scope subset of the enabled account, an explicit TTL no longer than 30 days, and returns a one-time secret plus a credential ID for revoke_token(credential_id). These are trusted administrative service methods, not unauthenticated user operations. The embedding application must authorize any management interface it builds around them.

The service validates identity/scope structure using the provider contract. It never interprets Auton endpoint names or maintenance permissions. Consumers must apply the verified scopes to business operations. Newly provisioned passwords must have at least 12 characters and no more than 1,024 UTF-8 bytes; no password normalization or silent truncation is performed. Applications may impose stronger password/enrollment policies before provisioning. Updating Argon2 parameters can be handled by explicit credential rotation; automatic rehash migration is not implemented in this first adapter.

Sessions, tokens and bounded work

login(principal, password, peer) returns a session grant with its secret, CSRF secret, expiry and identity. authenticate_session(secret, csrf, mutation=True) checks the CSRF secret before refreshing idle expiry. Session absolute expiry is 8 hours and idle expiry 30 minutes by default, configurable downward at service construction. Every request checks the enabled account and its revision. logout(secret) revokes that session. Rotation/revocation does not undo work already authorized or cancel application jobs.

Secrets use 32 bytes of OS randomness. Only SHA-256 digests of session/token/CSRF secrets are stored; account passwords use the injected password hasher. Raw secrets are returned to the caller for delivery, never logged by the service. Grant reprs are redacted. BearerAuthProvider accepts only explicit Authorization Bearer credentials and checks the token on every protected request; there is no Basic fallback or implicit cookie/session acceptance.

Login work is bounded to two concurrent password verifications per service instance. A transactional 60-second fixed window allows 5 attempts per account, 20 per direct peer, and 100 globally per store. Successful attempts count too. Unknown accounts still perform dummy password verification after admission. Applications should also limit login traffic at their trusted ingress; this bounded local policy can reject legitimate logins during a distributed attack. Supply the actual socket peer or a separately validated trusted-proxy result, never a raw user-controlled forwarded header. Each table is capped at 4,096 records by default (configurable downward). Expired entries are cleaned opportunistically; active credentials are not evicted merely to make room. Capacity exhaustion fails closed.

An optional audit callback receives fixed event names and, where known, the principal, never passwords, hashes, tokens, headers or private backend errors. Events cover account updates/disable, login success/denial/throttling, token issue/ revocation/denial and session revocation/denial. Routine successful credential checks do not generate audit writes. Audit sink errors are logged generically and do not alter authentication outcomes; a deployment requiring transactional audit must provide a stronger integration rather than assuming this callback is durable.

Store contract and deployment limits

The store supplies transaction() yielding an adapter with get(table, key), put(table, key, value), delete(table, key) and items(table). Values are detached copies. Transactions serialize the whole block and roll back on exceptions; items returns a stable snapshot, and deleting a missing key is harmless. The namespaces are accounts, sessions, tokens, attempts. Records include account revision, scopes and Unix expiry timestamps; use a trusted clock consistent across workers. Sharing a store between service instances is an explicit choice and also shares rate-limit counters. Durable adapters must preserve atomicity across processes, protect stored password hashes and make account changes visible to all workers. HTTPdis does not open or close a caller-owned store.

MemoryAuthStore is an explicitly chosen single-process adapter for development and tests. Its data is lost on restart, including accounts, tokens and rate limits; it provides no restart durability or multi-process coordination.

Durable SQLite authentication

Install httpdis[auth] (Sonicprobe >= 0.3.55) to use the existing AnySQL SQLite adapter with automatic reconnect disabled. The embedding daemon chooses the filename, creates its private parent directory, and owns the store lifecycle:

from httpdis.auth_sqlite import SQLiteAuthStore
from httpdis.auth_backend import LocalAuthService, Argon2Passwords

store = SQLiteAuthStore('/var/lib/my-daemon/auth/auth.db', timeout=5)
try:
    service = LocalAuthService(store, Argon2Passwords())
    # Supply service to your provider and trusted administration interface.
    # Stop request workers before closing the store.
finally:
    store.close()

There is no default filename and no automatic account creation. Existing Basic and provider configurations are unaffected. The database must be a local regular file, owned by the daemon user with no group/other permissions (created 0600). The parent must be owned by that user and not writable by group/others; keep the path and its ancestors under trusted administration. Symlinks, hard links and replacement of an open store's file are rejected. Use a dedicated authentication database, independent of job history. Network filesystems are not supported.

Each transaction opens its own AnySQL connection in the calling thread, takes BEGIN IMMEDIATE, commits on success, and closes on every exit (rolling back uncommitted work). synchronous=FULL, the DELETE journal and secure deletion are selected explicitly. Lock waits are bounded by timeout (0 < seconds <= 30). Multiple threads/processes can use the same database; create one store per process after forking. Failures are surfaced without reconnect/replay or memory fallback.

Accounts, sessions, token revocations and rate-limit counters survive restart. Passwords remain hashes; session/token/CSRF secrets are not stored in plaintext. This is not encryption at rest: protect the database and backups as credentials. Stop all writers before copying the database for backup. Only schema version 1 is accepted; incompatible databases are refused, never silently reset. JSON records are bounded to 64 KiB and support the existing four namespaces. Redis and automatic schema migrations are not supplied by this adapter.

Browser integration is still pending: this backend does not expose login/logout routes, set cookies, configure HTTPS/CORS, enforce request Origin or provide TOTP. The caller must mark every cookie-authenticated mutation for CSRF validation; do not expose a cookie provider without the complete browser transport protections. Only the token-only provider is supplied in this step. Session/login APIs are neutral services ready for the subsequent transport integration.

Metadata

Release files for httpdis 0.6.30

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for httpdis 0.6.30
File Size Uploaded
httpdis-0.6.30.tar.gz 63.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for httpdis 0.6.30
File Interpreter ABI Platform
httpdis-0.6.30-py2.py3-none-any.whl Python 2, Python 3 none any Details

Total release size: 107.9 kB

Release files / httpdis-0.6.30.tar.gz

Download URL httpdis-0.6.30.tar.gz
Size 63.1 kB
Tags Source
SHA-256 checksum
How to use checksums
1d67cf1ef2fad7121642d0d6e22580e9d46cbda1fec64c3f7b35b6096ca25082
BLAKE2b-256 checksum
How to use checksums
71315f8938774705c58d4fb274502b454337f81717cb6bc5492f1071af19481d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release files / httpdis-0.6.30-py2.py3-none-any.whl

Download URL httpdis-0.6.30-py2.py3-none-any.whl
Size 44.8 kB
Tags Python 2 Python 3
SHA-256 checksum
How to use checksums
208bba1f0ee7b35f4b54a2965f093141cb1ce324e5e951cbdd333656463a4386
BLAKE2b-256 checksum
How to use checksums
b2b03582b9474d3754282fcd3f3d2265eb770684e63d8b8ed4d9d30ac202e730
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 29, 2026.

Transparency log

Release history Release notifications | RSS feed

0.6.31

2 release files

This release

0.6.30 This release

2 release files

0.6.29

2 release files

0.6.28

2 release files

0.6.27

2 release files

0.6.26

2 release files

0.6.22

2 release files

0.6.21

2 release files

0.6.20

2 release files

0.6.19

2 release files

0.6.18

2 release files

0.6.14

2 release files

0.6.13

2 release files

0.6.12

2 release files

0.6.11

2 release files

0.6.10

2 release files

0.6.9

2 release files

0.6.8

2 release files

0.6.7

2 release files

0.6.6

2 release files

0.6.5

2 release files

0.6.4

2 release files

0.6.2

2 release files

0.6.1

2 release files

0.5.17

1 release file

0.5.16

1 release file

0.5.15

1 release file

0.5.14

1 release file

0.5.13

1 release file

0.5.12

1 release file

0.5.11

1 release file

0.5.10

1 release file

0.5.9

1 release file

0.5.8

1 release file

0.5.7

1 release file

0.5.6

1 release file

0.5.5

1 release file

0.5.4

1 release file

0.5.3

1 release file

0.5.2

1 release file

0.5

1 release file

0.0.4

1 release file

0.0.3

1 release file

0.0.2

1 release file

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page