Vermell - Minimal, dependency-free C++ web framework using epoll
A minimal, zero-bloat web framework designed for modern C++ environments. Fast, structural, and strictly typed. Vermell is a web framework for modern C++ environments: one header to include, one static library to link, and nothing else. No runtime, no garbage collector, no framework-specific DSL, no vendored dependencies - what you write is C++, and what runs is C++. Under the hood it is an event-driven engine: a non-blocking epoll loop reads requests and hands work to a pool of worker threads. That split is what makes Vermell fast under load and resilient against slow clients. - Zero dependencies - only base Linux APIs (sockets, epoll, pthreads, fork/exec). - One command to build - g++ -std=c++20 server.cpp -o exe -lvermell . - Any Linux with g++ - x86_64, ARM (aarch64, armv7), Android via Termux, WSL, Raspberry Pi, containers. - Hardened by default - timeouts, request caps, connection limits and a render jail are on out of the box. - In-tree JSON DOM - strict RFC 8259 parser and serializer, typed parameters, raw bodies, multipart uploads. - C++ templates - compose() modules andrender() variables. - Fluent configuration - one configure({...}) call or chainable setters, readable at runtime. 📚 Full documentation: vermell.cc - bilingual (EN/ES) manual covering every section of this README with examples and diagrams. - Installation - Quick Start - Compile - Routing & Handlers - Server Configuration - MIME Types & File Rendering - Static Directories - Templates: compose & render - Render Security - Process & Environment - Examples - Support - Testing - Contribution - License $ git clone https://github.com/vermellcc/vermell.git $ cd Vermell $ cmake . $ cmake --build . $ make install Ready-to-use scaffold: $ npx create-vermell-static $ docker pull vermellcc/vermell Packages for amd64 , arm64 and armhf live on GitHub Pages, signed and ready to add: $ sudo install -d -m 0755 /etc/apt/keyrings $ curl -fsSL https://vermellcc.github.io/vermell/vermell-apt-key.asc | sudo gpg --dearmor --yes -o /etc/apt/keyrings/vermell.gpg $ echo "deb [signed-by=/etc/apt/keyrings/vermell.gpg] https://vermellcc.github.io/vermell stable main" | sudo tee /etc/apt/sources.list.d/vermell.list $ sudo apt-get update $ sudo apt-get install -y libvermell Key fingerprint: 022D 56AA 7A6B 2028 B005 3629 F616 54D8 8AD1 C323 A Vermell server is a Router : register a handler for a route, choose a port, and call listen() . #include int main() { Router router; router.setPort(8080); router.get("/", { [](Query &http) { http.send("Hello from Vermell"); }}); router.listen(); } The { ... } around the handler matter. The second argument ofrouter.get(...) is aMiddlewareList , so handlers are always passed as a braced list:router.get("/", { [](Query &http) { ... } }) . Compile and run: $ g++ -std=c++20 server.cpp -o exe -lvermell $ ./exe Then point your browser or curl at it: $ curl http://localhost:8080/ Hello from Vermell router.listen() blocks and serves forever. listenOne() serves a single request and returns - handy for tests and one-shot servers. A single g++ invocation compiles and links everything - no extra flags, no link order games: $ g++ -std=c++20 server.cpp -o exe -lvermell For larger projects use CMake, but a server is always one command away. Portability. Vermell has no dependencies, so anything that derives from Linux and has a C++20 g++ can build it: x86_64, ARM (aarch64, armv7), Android via Termux, WSL, Raspberry Pi, containers. macOS and Windows are not supported targets (epoll).No root? No problem. On Termux (or any system without root) you cannot make install into/usr/local . Include the header by relative path (#include "../include/vermell/vermell.h" ) and link the static library directly - copylibvermell.a next to your sources and compile with-L. -lvermell : // Termux / no-root build: header referenced by relative path #include "../include/vermell/vermell.h" int main() { Router router; router.setPort(8080); router.get("/", { [](Query &http) { http.send("hi from termux"); }}); router.listen(); } $ cp libvermell.a . # static library next to the sources $ g++ -std=c++20 server.cpp -o exe -L. -lvermell $ ./exe The router exposes one registration method per HTTP verb. Static routes dispatch in O(1) through a transparent-hash route map. router.get("/users", { [](Query &web) { web.send("list"); } }); router.post("/users", { [](Query &web) { web.send("create"); } }); router.put("/users/:id", { [](Query &web) { web.send("update"); } }); router.deleteX("/users/:id", { [](Query &web) { web.send("delete"); } }); router.patch("/users/:id", { [](Query &web) { web.send("patch"); } }); router.head("/status", { [](Query &web) { web.send("head"); } }); router.options("/ping", { [](Query &web) { web.send("options"); } }); router.link("/rel", { [](Query &web) { web.send("link"); } }); router.unlink("/unlink", { [](Query &web) { web.send("unlink"); } }); router.purge("/cache", { [](Query &web) { web.send("purge"); } }); Note the deleteX() name: delete is a C++ keyword. For larger applications, declare routes separately and mount them with router.use() : // routes.cpp - separated declaration Route_t users_routes("/users/:id", { [](Query &web) { web.json(R"({"op":"get"})"); } }, GET_TYPE); // main.cpp - mounting router.use(users_routes); router.use(admin_routes); Every handler is a C++ lambda void(Query&) . The capture list between [ and ] decides how outside state reaches it: string app_name = "vermell-demo"; int port = 8080; // [] - nothing captured: the handler only sees the Query router.get("/ping", { [](Query &web) { web.json(R"({"pong":true})"); }}); // [=] - outside values arrive BY COPY: a private snapshot router.get("/name", { [=](Query &web) { web.send(app_name); // reads a copy made at registration }}); // [&] - outside variables arrive BY REFERENCE: a live view router.get("/info", { [&](Query &web) { web.send(app_name + ":" + std::to_string(port)); }}); // named captures - only what you need: // [port] -> copy of port [&port] -> reference to port // [this] -> enclosing object [=, &port] -> all by copy, port by ref | Capture | Meaning | |---|---| [] | No capture - the handler only receives the Query . | [=] | Every used outside variable by copy (snapshot at creation). | [&] | Every used outside variable by reference (live aliases). | [x] / [&x] | Named capture: copy of x , or reference to x . | [this] | Capture the enclosing class (members by reference). | [=, &x] | Everything by copy, except x by reference. | Thread safety. Handlers run on worker threads and live for the whole server lifetime. [&] captures are references to the registering scope: fine for variables that outlivelisten() , but never capture stack locals that die earlier - that is a dangling reference. Because requests run concurrently, shared mutable state captured by reference needs a mutex; prefer[=] for immutable snapshots. Every knob of the request/response pipeline lives in vermell::Config (include/vermell/config.hpp ). Pass it whole with router.configure({...}) (defaults preserve the legacy behavior): router.configure({ // network .backlog = SOMAXCONN, // pending connections queue of listen() .reuse_port = false, // SO_REUSEPORT: OFF by default (a same-UID // process could otherwise bind the port and // intercept a share of the traffic) // request reading .read_timeout = std::chrono::seconds{30}, // inactivity between chunks .request_timeout = std::chrono::seconds{60}, // total deadline for the whole // request to arrive (slowloris cure) .write_timeout = std::chrono::seconds{10}, // inactivity while responding .max_request_size = 16UL * 1024UL * 1024UL, // bigger => 413 Payload Too Large .read_chunk = 32UL * 1024UL, // bytes read per recv() call // concurrency / epoll .threads = 4, // worker threads; 0 = auto (hardware_concurrency) .max_events = 1024, // epoll event batch size .max_queue_size = 512, // queued tasks before the dispatcher sheds load .max_connections = 1024, // hard cap on open connections; 0 = unlimited .epoll_timeout = std::chrono::milliseconds{1000}, }); Hardening defaults: read_chunk is clamped to[1, 1 MiB] ,max_events to[1, 65536] ,threads to[0, 256] and every timeout to[1ms, INT_MAX ms] - absurd values are a memory/DoS foot-gun, not a feature. Requests to HTTP/1.1 (or newer) without exactly oneHost header are rejected with 400 (RFC 9112 §3.2, proxy desync / request-smuggling vector); HTTP/1.0 legacy clients keep working.max_connections is bounded by default (1024) so a connection flood cannot exhaust memory.Slowloris is not a DoS anymore: request bytes are read on the event loop (non-blocking), so a trickling client occupies an epoll fd - bounded by max_connections and theread_timeout /request_timeout deadlines - never a worker thread. A client that sends 1 byte every few seconds for hours is dropped with 408 as soon as the whole request exceedsrequest_timeout . When the task queue is full the dispatcher sheds the connection (503) instead of stalling the accept loop. Or use the chainable setters: router.setThreads(4) .setMaxRequestSize(16UL * 1024UL * 1024UL) .setReadTimeout(std::chrono::seconds{30}); // setWriteTimeout, setRequestTimeout, setReadChunkSize, setMaxEvents, // setMaxQueueSize, setMaxConnections, setBacklog, setBufferSize, // setPort, setReusePort configure() replaces the WHOLE configuration (designated initializers recommended): settings made earlier with the setters are discarded, so pass everything in one call.configure() also applies to a running server - timeouts, limits and the thread count are picked up live by the event loop and the worker pool (RequestIO::ApplyConfig ); only the network-side knobs (port ,backlog ,reuse_port ) need a restart. The active configuration is readable at runtime with router.config() . A full annotated example lives in examples/configuration . Vermell detects the Content-Type from the final extension of a file. This means kevin.txt.html is served as text/html , and matching is case-insensitive. Query strings and
Comments
No comments yet. Start the discussion.