Skip to content

TLS listeners, in-memory certificates, Request.secure - #1

Merged
mariusandra merged 5 commits into
masterfrom
tls-listeners
Sep 12, 2026
Merged

TLS listeners, in-memory certificates, Request.secure#1
mariusandra merged 5 commits into
masterfrom
tls-listeners

Conversation

@mariusandra

Copy link
Copy Markdown

FrameOS serves its on-device API over HTTPS from inside the runtime instead of through a Caddy process (FrameOS/frameos: docs/native-https.md). This fork gives mummy what that needs:

  • addListener(port, address, tls) / removeListener: any number of plain and TLS listeners per server, added before or while serving, from any thread (the serving thread applies them via a SelectEvent, so the selector is only ever touched by one thread). serve() with no arguments serves them all; serve(port, address) is unchanged.
  • newTlsConfig(certificateChainPem, privateKeyPem): an SSL_CTX from PEM in memory, TLS >= 1.2, partial-write mode so the loop's bytesSent bookkeeping is shared with plain sockets. Only PEM_read_bio_X509, SSL_CTX_use_certificate, SSL_CTX_use_PrivateKey and SSL_get_version are declared on top of std/openssl.
  • Per connection: SSL_accept driven by WANT_READ/WANT_WRITE, SSL_read drained until WANT_READ, SSL_write on the head outgoing buffer, SSL_shutdown + SSL_free on close. Everything TLS is under when defined(ssl).
  • Request.secure is true for requests that arrived over a TLS listener.

tests/test_tls.nim covers GET/POST, plain and TLS side by side, a 4 MB response, keep-alive, WebSocket over TLS, a client stalled mid-handshake, plain text on the TLS port, and listeners added and removed while serving. The upstream suite (test, test_http, test_http2, test_websockets, routers, multipart, request smuggling, fuzz_recv) passes with and without -d:ssl.

🤖 Generated with Claude Code

https://claude.ai/code/session_018eY8yWLDyHKssZ31wnkB54

mariusandra and others added 3 commits September 12, 2026 14:25
FrameOS serves its on-device API over HTTPS from inside the runtime instead
of through a Caddy process. This fork gives mummy what that needs:

- addListener(port, address, tls) / removeListener: any number of plain and
  TLS listeners per server, added before or while serving, from any thread
  (the serving thread applies them via a SelectEvent, so the selector is
  only ever touched by one thread). serve() with no arguments serves them
  all; serve(port, address) is unchanged.
- newTlsConfig(certificateChainPem, privateKeyPem): SSL_CTX from PEM in
  memory, TLS >= 1.2, partial-write mode so the loop's bytesSent
  bookkeeping is shared with plain sockets. Only PEM_read_bio_X509,
  SSL_CTX_use_certificate, SSL_CTX_use_PrivateKey and SSL_get_version are
  declared on top of std/openssl.
- Per connection: SSL_accept driven by WANT_READ/WANT_WRITE, SSL_read
  drained until WANT_READ (a record can hold more than one wake-up's worth),
  SSL_write on the head outgoing buffer, SSL_shutdown + SSL_free on close.
  Everything TLS is under when defined(ssl).
- Request.secure is true for requests that arrived over a TLS listener.

tests/test_tls.nim covers GET/POST, plain and TLS side by side, a 4 MB
response, keep-alive, WebSocket over TLS, a client stalled mid-handshake,
plain text on the TLS port, and listeners added and removed while serving.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eY8yWLDyHKssZ31wnkB54
A listener is created on the caller's thread and released on the serving
thread (removeListener, destroy). ORC's cycle-candidate roots are per
thread, so a cyclic-capable ref freed on another thread crashed in
unregisterCycle at shutdown. DataEntry and OutgoingBuffer are acyclic for
the same reason.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eY8yWLDyHKssZ31wnkB54
mariusandra and others added 2 commits September 12, 2026 14:54
- PEM_read_bio_X509, SSL_CTX_use_certificate, SSL_CTX_use_PrivateKey,
  SSL_get_version and X509_free are declared with the wrapper's own
  DLLSSLName / DLLUtilName patterns. Stock Nim binds std/openssl at run
  time, so a plain importc left them undefined at link time (the Ubuntu
  -d:ssl jobs); a Nim built to link -lssl still resolves them.
- Listener changes ride the responseQueued event instead of a fourth
  SelectEvent: the loop drains the ops queue on that wake-up. On Windows
  every SelectEvent is a loopback socket pair, and creating the fourth one
  raised inside newServer on the 2.2.6 job.
- newServer initialises its locks before the block that can fail: destroy()
  takes taskQueueLock, and an exception raised before initLock turned into
  a segfault there instead of a MummyError.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eY8yWLDyHKssZ31wnkB54
newServer can fail between creating its SelectEvents (on Windows each is a
loopback socket pair; the fuzzer's few thousand servers exhaust the dynamic
port range part-way through), and destroy then dereferenced a nil event
instead of surfacing the MummyError. Upstream's Windows job flakes the same
way; the fuzzer step now runs where it is meaningful, on Linux.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018eY8yWLDyHKssZ31wnkB54
@mariusandra
mariusandra merged commit 90ad798 into master Sep 12, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant