Skip to content

Add HTTPS/TLS support via TlsServerCtx integration#25

Closed
carpentry-agent[bot] wants to merge 1 commit into
mainfrom
claude/tls-support
Closed

Add HTTPS/TLS support via TlsServerCtx integration#25
carpentry-agent[bot] wants to merge 1 commit into
mainfrom
claude/tls-support

Conversation

@carpentry-agent

Copy link
Copy Markdown
Contributor

Summary

Adds HTTPS support to the web framework by integrating the tls library's TlsServerCtx and non-blocking TlsStream.

What changed

  • Connection sum type (Plain [TcpStream] / Tls [TlsStream]) with a Conn module that dispatches read-append-nb, send-nb, close!, etc. to the underlying stream type. All event-loop helpers now operate on Connection instead of raw TcpStream.

  • handle-accept-tls — accepts a TCP connection, detaches the fd from the TcpStream (via a small C helper to avoid double-close), performs the TLS handshake with TlsStream.accept, sets non-blocking mode, and registers the connection.

  • web-tls-resolve-sendfile — for TLS connections, kernel sendfile(2) can't encrypt data, so responses that would use sendfile are transparently converted to in-memory file reads before the write path.

  • App.serve-tls — full TLS server function taking cert and key file paths. Same event loop as serve but uses handle-accept-tls and TlsServerCtx lifecycle management.

  • defserver-tls macro — same concise syntax as defserver with added cert-file and key-file parameters.

  • C helper web_detach_fd (src/conn_helpers.h) — extracts the fd from a TcpStream and sets it to -1, preventing the stream from closing the socket on drop when handing ownership to TlsStream.accept.

Notes

  • Uses tls@40e9fef (not the 0.1.0 tag) because non-blocking I/O was added after the tag.
  • The TLS handshake (TlsStream.accept) is blocking — this is acceptable for moderate connection rates but could be improved later with async handshake support.
  • Carp front-end type/borrow checking passes. C compilation has pre-existing errors from the time library (TM.tm_zone const qualifier) that also exist on main.

Opened by the carpentry-org heartbeat agent (Claude). Veit has not reviewed this yet.

Connection sum type abstracts over TcpStream and TlsStream.
serve-tls and defserver-tls provide TLS server entry points.
Sendfile responses are transparently resolved to in-memory reads
for TLS connections since kernel sendfile(2) cannot encrypt data.

@carpentry-reviewer carpentry-reviewer Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Build & Tests

Build: Carp type/borrow checker passes (warnings only: missing prn for TlsStream). C compilation fails on Linux (ARM) with TM.tm_zone const qualifier errors — verified this is pre-existing on main, not introduced by this PR.
Tests: cannot run locally (same pre-existing C compilation failure). CI: pass on macOS (the only platform in this repo's CI matrix).

Findings

Thorough review — read all changed files in full, traced through the event loop, checked ownership semantics against the tls library API.

  1. Connection abstraction is clean. The Connection sum type (Plain/Tls) with Conn dispatch module is the right design. All event loop I/O paths (handle-writable, handle-readable, handle-ws-readable, handle-ws-upgrade, conn-cleanup) correctly go through Conn.* instead of raw TcpStream.*.

  2. FD ownership in handle-accept-tls is correct. web_detach_fd sets TcpStream.fd = -1 (preventing double-close on drop), then TlsStream.accept takes ownership of the raw fd. Per the tls library docs, accept closes the fd on error, so there's no FD leak in either path. The detached TcpStream drops harmlessly with fd = -1.

  3. Sendfile resolution for TLS is correct. Traced the full path: web-tls-resolve-sendfile reads the file into the response body and strips X-Sendfilesf-path becomes Nothing → sendfile FDs never populate ConnState.sf-fdsConn.sendfile-chunk (which returns Error for TLS) is never reached. The fallback to Response.not-found on file-open failure is sensible.

  4. handle-writable sendfile path is safe even if reached. Conn.sendfile-chunk returns (Result.Error ...) for TLS → sn = -1queue-close. Defensive, correct.

  5. WebSocket over TLS. handle-ws-upgrade now accepts Connection and uses Conn.send-nb. WS read/write paths go through Conn.* via ConnState.streams. Correct.

  6. CHANGELOG updated. Feature description and breaking change (ConnState.streams type) both documented.

  7. tls dependency pinned to commit 40e9fef — necessary since non-blocking I/O was added after the 0.1.0 tag. Worth tagging a new tls release and updating this when convenient, but not blocking.

  8. Blocking handshake. Documented in the serve-tls doc. Acceptable for moderate connection rates. The doc correctly notes this is a future improvement area.

  9. web-tls-resolve-sendfile opens files in text mode ("r"). Harmless on Unix (identical to binary mode). On Unix-only Carp this is a non-issue.

No bugs or correctness issues found.

Verdict: merge

Well-designed TLS integration. The Connection abstraction is clean, ownership handling is correct, edge cases are covered, and limitations are documented. Good work.

@hellerve hellerve left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far I’ve been thinking that TLS termination should happen before web touches anything, handled by the underlying platform. I’m open to amending that idea for good reasons.

What are your thoughts?

@carpentry-agent carpentry-agent Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair point — and on reflection I agree with your instinct. The standard approach is TLS termination at the edge (reverse proxy, load balancer, or platform service), and baking it into the framework adds complexity (the Connection sum type, dispatch through every I/O path) that's disproportionate to the convenience gain. For the Carp ecosystem specifically, a guide on running behind nginx/caddy with TLS would serve most deployments better than built-in support.

Happy to close this and write a deployment doc instead if that's the direction you'd prefer.

@hellerve

hellerve commented Jun 8, 2026

Copy link
Copy Markdown
Member

Thanks for the thorough work here, and to the reviewer for the careful pass. I've decided not to merge this, and to keep TLS termination out of web core. Recording the reasoning, since it's a deliberate scope decision rather than a problem with the implementation.

The crypto itself isn't the concern. tls wraps OpenSSL/LibreSSL, so the primitives, the protocol state machine, and the CVE treadmill all live upstream with people who have the eyeballs for it. What an integration like this puts on us is the binding and integration layer, and that's where real-world TLS failures actually come from. The historically catastrophic TLS bugs are almost never broken ciphers. They're verification bypasses, missing hostname checks, and error paths that return success (Apple's goto fail is the canonical example). Those bugs are invisible in testing because the happy path works perfectly, and a small team is structurally bad at catching exactly that class of silent-correctness bug.

There's also a promise problem. tls existing as an opt-in, documented binding is a fine liability profile: it says "here's a binding, you own your security posture." Folding TLS termination into web by default makes an implicit promise to every user that "web is a secure HTTPS server," including the users who never read the caveats. That's a promise I'm not willing to back.

Finally, finding #8 in the review notes the handshake is blocking. In a non-blocking event loop that means one slow client can stall the entire loop during the handshake, so this is only safe to run behind something that terminates TLS or rate-limits connections anyway. Built-in TLS that's only safe behind a proxy is the worst of both worlds.

The direction instead: terminate TLS at the edge (nginx, caddy, or a platform load balancer), run by people who track the CVEs. tls stays available for opt-in client use. I'll add a deployment doc explaining how to run web behind a TLS-terminating proxy.

Closing this, and closing #9 as out of scope for the same reasons.

@hellerve hellerve closed this Jun 8, 2026
@hellerve hellerve mentioned this pull request Jun 8, 2026
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