Optional TLS/SSL using OpenSSL 3.x. TLS sits below the HTTP/WebSocket layer, transparently wrapping socket I/O. When enabled, ConnectionHandler uses SSL_read()/SSL_write() instead of raw ::read()/::write(). Without configuration, connections use raw TCP exactly as before.
#include "http/http_server.h"
#include "config/server_config.h"
ServerConfig config;
config.bind_host = "0.0.0.0";
config.bind_port = 443;
config.tls.enabled = true;
config.tls.cert_file = "/etc/ssl/server.pem";
config.tls.key_file = "/etc/ssl/server.key";
config.tls.min_version = "1.2";
HttpServer server(config);
server.Get("/", [](const HttpRequest& req, HttpResponse& res) {
res.Status(200).Text("Hello over TLS!");
});
server.Start();| Component | Header | Role |
|---|---|---|
TlsContext |
include/tls/tls_context.h |
Server-wide RAII wrapper around SSL_CTX (server mode) |
TlsClientContext |
include/tls/tls_client_context.h |
Client-mode SSL_CTX for upstream connections |
TlsConnection |
include/tls/tls_connection.h |
Per-connection RAII wrapper around SSL (server + client) |
One per server. Created by HttpServer when config.tls.enabled == true.
TlsContext(const std::string& cert_file, const std::string& key_file);- Loads certificate and private key, verifies they match
- Sets TLS 1.2 minimum by default (constructor checks return value, throws on failure)
SetMinProtocolVersion(version)— throws if OpenSSL rejects the floor (prevents silent fail-open)SetCipherList(ciphers)— configurable cipher suitesSetAlpnProtocols({"h2", "http/1.1"})— registers ALPN selection callback for HTTP/2 negotiation- Non-copyable, non-movable
- Shared ownership:
HttpServercreates viamake_shared, passes toNetServer— guarantees context outlives both regardless of destruction order
Client-mode SSL_CTX for outbound upstream connections. Created by UpstreamHostPool when upstream.tls.enabled == true, shared across all PoolPartition instances for that service.
TlsClientContext(const UpstreamTlsConfig& config);- Peer verification:
SSL_VERIFY_PEERenabled by default (verify_peer = true). Loads CA bundle fromca_file. - SNI: Sets
sni_hostnameon eachSSLobject viaSSL_set_tlsext_host_name()so virtual-hosted upstreams route correctly. - Minimum version: Same
min_versionsupport as server-mode ("1.2"or"1.3"). - Shared ownership:
shared_ptr<TlsClientContext>shared across PoolPartitions — outlives any single partition.
Key difference from TlsContext (server mode): no certificate/key loading (client doesn't present a cert), no ALPN advertisement (upstream is HTTP/1.1 only for now).
One per client connection. Created in NetServer::HandleNewConnection() when TLS context is configured.
TlsConnection(TlsContext& ctx, int fd);| Constant | Value | Meaning |
|---|---|---|
TLS_COMPLETE |
0 | Handshake complete / would_block |
TLS_WANT_READ |
1 | Needs read readiness |
TLS_WANT_WRITE |
2 | Needs write readiness |
TLS_ERROR |
-1 | Fatal error |
TLS_PEER_CLOSED |
-2 | Peer sent close_notify |
TLS_CROSS_RW |
-3 | Read needs write / Write needs read (renegotiation) |
DoHandshake()— returns TLS_COMPLETE, TLS_WANT_READ, TLS_WANT_WRITE, or TLS_ERRORRead(buf, len)— returns >0 bytes, TLS_COMPLETE (would_block), TLS_CROSS_RW, TLS_PEER_CLOSED, or TLS_ERRORWrite(buf, len)— returns >0 bytes, TLS_COMPLETE (would_block), TLS_CROSS_RW, or TLS_ERRORShutdown()— sends close_notifyIsHandshakeComplete(),GetCipherName(),GetProtocolVersion()GetAlpnProtocol()— returns the ALPN-negotiated protocol (e.g.,"h2","http/1.1", or""if not negotiated)
┌──────────────┐
│ NONE │ (no TLS, raw TCP)
│ ::read() │
│ ::send() │
└──────────────┘
┌──────────────┐ ┌──────────────┐
│ HANDSHAKE │────────▶│ READY │
│ DoHandshake()│ done │ SSL_read() │
│ │ │ SSL_write() │
└──────────────┘ └──────────────┘
▲ │
│ │
SetTlsConnection() Normal operation
(before RegisterCallbacks)
| TLS State | Read Method |
|---|---|
NONE |
::read(fd, buf, len) |
HANDSHAKE |
tls_->DoHandshake() (WANT_READ → return, WANT_WRITE → EnableWriteMode) |
READY |
tls_->Read(buf, len) |
| TLS State | Write Method |
|---|---|
NONE |
::send(fd, buf, len, SEND_FLAGS) |
HANDSHAKE |
tls_->DoHandshake() (on complete, falls through to flush pending output) |
READY |
tls_->Write(buf, len) |
Critical: TLS is injected BEFORE RegisterCallbacks() in NetServer::HandleNewConnection():
auto conn = make_shared<ConnectionHandler>(dispatcher, std::move(socket));
if (tls_ctx_) {
auto tls = make_unique<TlsConnection>(*tls_ctx_, conn->fd());
conn->SetTlsConnection(std::move(tls)); // BEFORE RegisterCallbacks
}
conn->RegisterCallbacks(); // Enables epoll — safe nowThis prevents a race where RegisterCallbacks() enables epoll read mode and Client Hello bytes arrive before OnMessage() knows about TLS.
{
"tls": {
"enabled": true,
"cert_file": "/etc/ssl/server.pem",
"key_file": "/etc/ssl/server.key",
"min_version": "1.2"
}
}| Variable | Description |
|---|---|
REACTOR_TLS_ENABLED |
true / false |
REACTOR_TLS_CERT |
Path to certificate file |
REACTOR_TLS_KEY |
Path to private key file |
ConfigLoader::Validate() checks:
- If TLS enabled, cert_file and key_file must be non-empty
- Certificate and key must match (verified by
SSL_CTX_check_private_key())
All OpenSSL configuration calls check return values. If SSL_CTX_set_min_proto_version() fails, the server throws rather than starting with a weaker TLS minimum:
if (!SSL_CTX_set_min_proto_version(ctx_, TLS1_2_VERSION)) {
throw std::runtime_error("Failed to set minimum TLS version");
}Default is TLS 1.2. Can be set to "1.2" or "1.3" via config. The SetMinProtocolVersion() method enforces this at the SSL_CTX level.
OpenSSL 3.x — system library (-lssl -lcrypto). Also provides SHA-1 and base64 for the WebSocket handshake (Sec-WebSocket-Accept header computation).
Install: sudo apt install libssl-dev (Ubuntu/Debian) or brew install openssl (macOS).