Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ecewo-https

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_t instances.
  • 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 malloc on 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 over ws:// works over wss://. Verified by an end-to-end smoke test.
  • Advertises http/1.1 via ALPN.
  • TCP_NODELAY set on both the inbound and the loopback sockets to keep small WebSocket frames and other interactive traffic low-latency.

Requirements

  • ecewo v4 (fetched automatically via CMake FetchContent)
  • CMake 3.14+
  • A C11 compiler
  • One of:
    • OpenSSL 1.1+ (3.x supported)
    • mbedTLS 3.x

Quick start

#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.

Building

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)

CMake options

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.

Standalone build

# 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 -j

API reference

All public symbols are declared in include/ecewo-https.h. The configuration object is opaque; populate it via setters.

Configuration

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.

Attach / introspection

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 before ecewo_run() / ecewo_listen();
  • starts the HTTPS listener on port and a proxy path that forwards decrypted bytes to http_port;
  • stores its state on the app via ecewo_set_app_data() and registers cleanup with ecewo_atexit();
  • returns 0 on 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.

How it works

                                  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:

  1. The plugin borrows a per-connection arena via ecewo_arena_borrow() and allocates the connection state and a small initial plaintext buffer inside it.
  2. The TLS engine drives the handshake against the client. ALPN is negotiated; the plugin advertises http/1.1.
  3. 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 to 127.0.0.1:<http_port>. The pre-connect buffer grows on demand from 16 KiB up to a 4 MiB cap.
  4. 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, and HTTP/1.1 101 upgrades are all handled by upstream ecewo.
  5. Responses come back as plain HTTP, are re-encrypted, and written back to the client.
  6. On close, the connection arena is returned to the pool via ecewo_arena_return().

Limits and tunables

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.

WebSocket Secure (wss://)

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 over ws:// and wss://.
  • The plugin does not parse WebSocket frames, enforce control-frame rules, or alter mask bits. It is a pure transport.
  • Sec-WebSocket-Accept is 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.

Backend differences

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

ABI / FFI notes

  • All public types are opaque; their definitions live in src/, never in include/. Adding fields will not break consumer binaries.
  • All public functions are declared extern "C" and exported via a generated ECEWO_HTTPS_EXPORT macro. Only the documented ecewo_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.).

Testing

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 /ping over 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 a GET /ws with the right Upgrade/Sec-WebSocket-Key returns a valid HTTP/1.1 101 with the correct Sec-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-failure

The same test runs against all four build matrix cells: openssl-static, openssl-shared, mbedtls-static, mbedtls-shared.

License

MIT. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages