Dracon logo: a steel dragon whose head is an RJ45 plug and whose tail ends in three stripped copper wires

Dracon

A small, fast HTTP/HTTPS server for Linux.


Dracon is written from scratch in C++23 on top of epoll and Boost.Coroutine2 — no Boost.Asio, no framework. The server itself only speaks HTTP: every request is answered by a plugin, a shared library it loads at start up.

Each connection runs inside its own coroutine, so a handler is written as plain sequential code while every read and write that would block yields back to the event loop. There is one event loop per hardware thread and the connections are spread across them, which is what lets a handful of threads carry a very large number of concurrent ones.

A plugin, in full

#include <dracon/http.h>
#include <dracon/plugin.h>

PLUGIN_EXPORT uint32_t plugin_order() { return 0; }

PLUGIN_EXPORT Dracon::HttpSession create_session(const Dracon::Request &req)
{
    if (req.url() != "/hello")
        return {}; // not ours, the server asks the next plugin

    return [](Dracon::AbstractStream &stream, Dracon::Request &req) {
        stream >> req; // read the rest of the request
        stream << Dracon::Response{200, "hello\n"};
    };
}

Drop the resulting .so into the plugins directory and it is live on the next start. The whole API is the header-only include/dracon/: requests and responses, streams and chunked output, RESTful routing, and worker threads for the slow work that must not block an event loop.

…and one with routes

For an API, restful.h does the URL matching. The handler is the session, and the resources a route captures arrive as ordinary arguments in the order it declares them — so one function can serve a whole family of routes, and the shorter ones simply hand over empty strings.

#include <dracon/plugin.h>
#include <dracon/restful.h>

Dracon::RESTfulRouterType router{"/api/v1/"};

static void customers(Dracon::AbstractStream &stream, Dracon::Request &req,
                      const std::string &customerId = {},
                      const std::string &licenseId = {})
{
    stream >> req;
    stream << Dracon::Response{200, "customer " + customerId +
                                    ", license " + licenseId + "\n"};
}

PLUGIN_EXPORT bool init_plugin(const std::string &/*confDir*/)
{
    for (const auto route : {"customers",
                             "customers/{customerId}",
                             "customers/{customerId}/licenses/{licenseId}"})
        router.createRoute(route)->addMethodHandler("GET", customers);
    return true;
}

PLUGIN_EXPORT uint32_t plugin_order() { return 0; }

PLUGIN_EXPORT Dracon::HttpSession create_session(const Dracon::Request &req)
{
    return router.createHandler(req.url(), req.method());
}

Which answers all three, filling in what each one captured:

GET /api/v1/customers
customer , license
GET /api/v1/customers/42
customer 42, license
GET /api/v1/customers/42/licenses/7
customer 42, license 7

An unknown method on a known route is answered with a 405 and the right Allow header, without the handler being involved. Take a Dracon::ParsedRoute instead of the captures when the query strings or the allowed methods are wanted too.

At a glance

Language
C++23 — GCC ≥ 13 or Clang ≥ 17
Built on
epoll, Boost.Coroutine2, OpenSSL
Platform
Linux only, and unapologetically so
Handlers
Plugins loaded with dlopen at start up
Concurrency
One coroutine per connection, one event loop per thread
License
AGPL, with an exception so plugins may use any license

Installing

Debian packages build straight out of the tree: dracon-server for the daemon, its systemd unit and the default configuration, dracon-mod-staticcontent for the plugin serving this page, and libdracon-dev for the headers you build your own against. A Certbot plugin, python3-certbot-dracon, obtains and installs the certificate:

certbot run --configurator dracon -d example.com