HTTPS plugin for ecewo.
ecewo-https adds TLS termination to an ecewo application. It runs an
HTTPS-terminating listener that performs the TLS handshake, decrypts the
traffic, and forwards plain HTTP to your existing ecewo HTTP listener on a
loopback port.
- Per-app: attach independently to multiple
ecewo_app_tinstances. - Two interchangeable TLS backends: OpenSSL or mbedTLS, selected at build time.
- Static or shared library (
.a/.so). - Opaque, ABI-stable, FFI-friendly C API matching the conventions in
ecewo.h. - Memory managed through ecewo's app arena and connection arena pool;
no plugin-owned
mallocon the connection hot path. - Cleanup wired through
ecewo_atexit— no manual teardown needed. - Transparent to upstream: once the loopback connection is up, decrypted bytes are forwarded straight through, so anything ecewo serves over plain HTTP — chunked transfer encoding, HTTP/1.1 pipelining, large bodies, HEAD, WebSocket upgrades — works over HTTPS unchanged.
wss://works. The plugin is byte-transparent on both sides of an RFC 6455 upgrade; whatever your handlers do overws://works overwss://. Verified by an end-to-end smoke test.- Advertises
http/1.1via ALPN. TCP_NODELAYset on both the inbound and the loopback sockets to keep small WebSocket frames and other interactive traffic low-latency.
- ecewo v4 (fetched automatically via CMake
FetchContent) - CMake 3.14+
- A C11 compiler
- One of:
- OpenSSL 1.1+ (3.x supported)
- mbedTLS 3.x
#include "ecewo.h"
#include "ecewo-https.h"
static void ping(ecewo_request_t *req, ecewo_response_t *res) {
ecewo_send_text(res, ECEWO_OK, "pong");
}
int main(void) {
ecewo_app_t *app = ecewo_create();
ECEWO_GET(app, "/ping", ping);
ecewo_bind(app, 8080); /* plain HTTP listener */
ecewo_https_config_t *cfg = ecewo_https_config_new();
ecewo_https_config_set_cert_file(cfg, "server.crt");
ecewo_https_config_set_key_file (cfg, "server.key");
ecewo_https_config_set_port (cfg, 8443);
ecewo_https_config_set_http_port(cfg, 8080);
ecewo_https_attach(app, cfg);
ecewo_https_config_free(cfg);
ecewo_run();
return 0;
}A single ecewo process now serves both http://...:8080 and
https://...:8443 — the same routes, the same handlers.
Add as a FetchContent dependency, then link ecewo::https:
include(FetchContent)
FetchContent_Declare(ecewo_https
GIT_REPOSITORY https://github.com/ecewo/ecewo-https.git
GIT_TAG main
)
# Pick a backend BEFORE MakeAvailable. Default is "openssl".
set(ECEWO_HTTPS_BACKEND "openssl" CACHE STRING "" FORCE)
FetchContent_MakeAvailable(ecewo_https)
add_executable(myapp main.c)
target_link_libraries(myapp PRIVATE ecewo::ecewo ecewo::https)| Option | Values | Default | Description |
|---|---|---|---|
ECEWO_HTTPS_BACKEND |
openssl, mbedtls |
openssl |
TLS backend to compile in. |
ECEWO_HTTPS_BUILD_SHARED |
ON, OFF |
OFF |
Build a shared library instead of a static one. |
ECEWO_HTTPS_BUILD_TESTS |
ON, OFF |
OFF |
Build the smoke test (top-level builds only). |
When the plugin is the top-level project, ECEWO_HTTPS_BUILD_SHARED also
controls how the bundled ecewo build is produced, so the static and shared
builds stay consistent.
# OpenSSL, static
cmake -S . -B build -DECEWO_HTTPS_BACKEND=openssl -DECEWO_HTTPS_BUILD_SHARED=OFF
cmake --build build -j
# mbedTLS, shared
cmake -S . -B build -DECEWO_HTTPS_BACKEND=mbedtls -DECEWO_HTTPS_BUILD_SHARED=ON
cmake --build build -jAll public symbols are declared in include/ecewo-https.h.
The configuration object is opaque; populate it via setters.
ecewo_https_config_t *ecewo_https_config_new(void);
void ecewo_https_config_free(ecewo_https_config_t *config);
void ecewo_https_config_set_cert_file (ecewo_https_config_t *c, const char *path);
void ecewo_https_config_set_key_file (ecewo_https_config_t *c, const char *path);
void ecewo_https_config_set_ca_file (ecewo_https_config_t *c, const char *path);
void ecewo_https_config_set_passphrase (ecewo_https_config_t *c, const char *passphrase);
void ecewo_https_config_set_port (ecewo_https_config_t *c, uint16_t port);
void ecewo_https_config_set_http_port (ecewo_https_config_t *c, uint16_t http_port);
void ecewo_https_config_set_verify_client(ecewo_https_config_t *c, bool verify);| Setter | Required | Default | Meaning |
|---|---|---|---|
cert_file |
yes | — | PEM-encoded server certificate (or chain). |
key_file |
yes | — | PEM-encoded private key matching the certificate. |
ca_file |
no | none | PEM CA bundle, used when verify_client is set. |
passphrase |
no | none | Passphrase for an encrypted private key. |
port |
no | 8443 |
Port the HTTPS listener binds on. |
http_port |
no | 8080 |
Loopback port to forward decrypted traffic to. |
verify_client |
no | false |
Require a valid client certificate (mTLS). |
ecewo_https_config_free() is safe to call as soon as ecewo_https_attach()
returns — the attach call deep-copies any state it needs.
int ecewo_https_attach (ecewo_app_t *app, const ecewo_https_config_t *config);
bool ecewo_https_is_attached(const ecewo_app_t *app);
uint16_t ecewo_https_port (const ecewo_app_t *app);
const char *ecewo_https_backend (void); /* "OpenSSL" or "mbedTLS" */ecewo_https_attach():
- must be called after
ecewo_bind(app, http_port)and beforeecewo_run()/ecewo_listen(); - starts the HTTPS listener on
portand a proxy path that forwards decrypted bytes tohttp_port; - stores its state on the app via
ecewo_set_app_data()and registers cleanup withecewo_atexit(); - returns
0on success, non-zero on error (missing cert/key, port in use, TLS context creation failed).
There is no manual cleanup function. When the app shuts down, the listener is closed, every active connection is drained, and the TLS context is freed automatically.
ecewo-https plugin
┌─────────────────────────────┐
client ─── TLS ───► port 8443 (HTTPS listener)
│ TLS handshake, decrypt │
├──── plaintext ──────────────┘
│
▼
ecewo HTTP listener on 127.0.0.1:8080
│
┌─────────────┴─────────────┐
│ routes, middleware, │
│ handlers (your code) │
└─────────────┬─────────────┘
│
◄── plaintext response ─
client ◄── TLS ───── port 8443 (encrypts and writes)
For each accepted connection:
- The plugin borrows a per-connection arena via
ecewo_arena_borrow()and allocates the connection state and a small initial plaintext buffer inside it. - The TLS engine drives the handshake against the client. ALPN is
negotiated; the plugin advertises
http/1.1. - The plugin buffers decrypted plaintext until it has seen the end of
the first request's headers (
\r\n\r\n), then opens a libuv TCP connection to127.0.0.1:<http_port>. The pre-connect buffer grows on demand from 16 KiB up to a 4 MiB cap. - Once that loopback connection is up, every subsequent decrypted byte
is forwarded straight through. The plugin does not parse HTTP
itself — framing,
Content-Length,Transfer-Encoding: chunked, pipelining, HEAD, andHTTP/1.1 101upgrades are all handled by upstream ecewo. - Responses come back as plain HTTP, are re-encrypted, and written back to the client.
- On close, the connection arena is returned to the pool via
ecewo_arena_return().
| Knob | Value | Where |
|---|---|---|
| Pre-upstream-connect plaintext buffer, initial | 16 KiB | HTTPS_DECRYPT_BUF_INIT |
| Pre-upstream-connect plaintext buffer, max | 4 MiB | HTTPS_DECRYPT_BUF_MAX |
| Per-read TLS plaintext drain | 16 KiB | HTTPS_FLUSH_BUF_SIZE |
| Listen backlog | 128 | HTTPS_LISTEN_BACKLOG |
| mbedTLS inbound ciphertext buffer, max | 1 MiB | TLS_MBEDTLS_INBUF_MAX |
| mbedTLS outbound ciphertext buffer, max | 1 MiB | TLS_MBEDTLS_OUTBUF_MAX |
| Min TLS version | TLS 1.2 | OpenSSL: SSL_CTX_set_min_proto_version; mbedTLS: mbedtls_ssl_conf_min_version |
The pre-connect buffer cap only governs how much can accumulate while the loopback connection is being established. Once the loopback is up, plaintext is forwarded directly with no per-connection buffer cap on total bytes — bodies are streamed, not buffered, by the plugin.
There is no WebSocket-specific code path in the plugin. After the HTTP
upgrade response (HTTP/1.1 101 Switching Protocols) flows through, the
plugin keeps doing what it does before: TLS-decrypt inbound bytes,
forward them to the loopback; receive bytes from the loopback,
TLS-encrypt them, send them out. RFC 6455 frames cross the proxy as
opaque bytes in both directions.
What this means in practice:
- Implement WebSocket on the upstream side via
ecewo_connection_takeover(or any plugin that does). The exact same handler works overws://andwss://. - The plugin does not parse WebSocket frames, enforce control-frame rules, or alter mask bits. It is a pure transport.
Sec-WebSocket-Acceptis computed and signed by your handler; the plugin only carries the request and response bytes.- Long-lived WS connections survive as long as both endpoints keep them open; the plugin does not impose its own idle timeout. ecewo's request-level timeouts apply to the upstream HTTP connection up to the point of takeover.
| Capability | OpenSSL | mbedTLS |
|---|---|---|
| TLS 1.2 / TLS 1.3 | yes | yes (per mbedTLS build) |
ALPN http/1.1 |
yes | yes (when MBEDTLS_SSL_ALPN is enabled) |
| Server-side session cache | yes (1024 entries) | not configured (would require app-supplied ticket-key management; out of scope here) |
| Encrypted private keys | yes (passphrase config) |
yes |
| Client certificate verification | yes (verify_client) |
yes |
- All public types are opaque; their definitions live in
src/, never ininclude/. Adding fields will not break consumer binaries. - All public functions are declared
extern "C"and exported via a generatedECEWO_HTTPS_EXPORTmacro. Only the documentedecewo_https_*symbols escape; everything else is hidden (-fvisibility=hidden). - The shared library is built with
SOVERSION = ${PROJECT_VERSION_MAJOR}. - Function signatures use only fixed-width scalars and opaque pointers,
so the API is directly usable from any FFI (Python
ctypes, Rust, Go cgo, Zig, etc.).
A smoke test that exercises HTTPS end-to-end ships in tests/. It
generates a self-signed cert at configure time, starts the smoke binary,
and drives curl -k against it. Each run covers:
- a
GET /pingover HTTPS; - a chunked POST (
Transfer-Encoding: chunked) — verifies that chunked traffic reaches upstream; - a 256 KiB Content-Length echo — round-trips a body that is well above the legacy 64 KiB hard cap to verify the dynamic buffer;
- a WebSocket upgrade handshake (
tests/wsclient.py) — verifies that aGET /wswith the rightUpgrade/Sec-WebSocket-Keyreturns a validHTTP/1.1 101with the correctSec-WebSocket-Accept; - an ALPN check — verifies that the server negotiates
http/1.1.
The WS test requires python3 (stdlib only; ssl, socket, hashlib).
cmake -S . -B build -DECEWO_HTTPS_BACKEND=openssl -DECEWO_HTTPS_BUILD_TESTS=ON
cmake --build build -j
ctest --test-dir build --output-on-failureThe same test runs against all four build matrix cells:
openssl-static, openssl-shared, mbedtls-static, mbedtls-shared.
MIT. See LICENSE.