A C++11 embedded webserver framework.
Focuses on the concept of middleware functions to provide an extremely easy to use API.
The most basic example might be as simple as:
#include <cex.hpp>
int main()
{
cex::Server app;
app.use([](cex::Request* req, cex::Response* res, std::function<void()> next)
{
res->end(200);
});
app.listen("127.0.0.1", 5555, true);
return 0;
}
libcex
requires the following libraries:
- libevhtp
- OpenSSL (optional) - for HTTPS support
- zlib (optional) - for compression of response payloads
libcex
uses the cmake
build system to compile the library and testcases. To compile/install, simply do:
$ git clone https://github.com/patrickjane/libcex .
remote: Enumerating objects: 4, done.
remote: Counting objects: 100% (4/4), done.
remote: Compressing objects: 100% (3/3), done.
remote: Total 4 (delta 0), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (4/4), done.
$ mkdir build
$ cd build
$ cmake ..
$ make
If cmake cannot find your OpenSSL installation, or you've installed in a non-standard location, you might want to add -DOPENSSL_ROOT_DIR=/path/to/ssl
to the cmake call.
After successfully compiling the library, testcases can be run with ctest
:
$ ctest
Test project /Users/patrickjane/Development/libcex/build
Start 1: filesystem
1/5 Test #1: filesystem ....................... Passed 0.10 sec
Start 2: mw_security
2/5 Test #2: mw_security ...................... Passed 0.04 sec
Start 3: mw_session
3/5 Test #3: mw_session ....................... Passed 0.04 sec
Start 4: routing
4/5 Test #4: routing .......................... Passed 0.15 sec
Start 5: uploads
5/5 Test #5: uploads .......................... Passed 0.06 sec
100% tests passed, 0 tests failed out of 5
Total Test time (real) = 0.41 sec
For a full API documentation, visit the doxygen site at: https://patrickjane.github.io/libcex/index.html
The cex::Server
class provides the HTTP/HTTPS listener and actually processes the request received by clients.
A server can be created with default options (see API docs) or concrete options:
cex::Server app;
// or
cex::Server::Config cfg;
cex::Server app(&cfg);
The listener is started once the listen
method is called:
app.listen(true);
Supplying true
to the last parameter starts the listener/eventloop within the calling thread. If false
is provided, a background thread will be spawned for the listener/eventloop, and the call to listen
returns immediately.
Note: The background thread is only used for the eventloop. The actual request processing might use additional/more threads as given by the threadCount
config option (default: 4), independently from the listener thread.
The cex::Server
class also provides the interface to attach middleware functions.
Each middleware function will receive the cex::Request
and cex::Response
objects, which allow to interact with the currently receiced request as well as construct responses which will be sent back to the client.
Middleware functions can be attached for a certain HTTP method, a certain URL, or globally (without restrictions).
Example:
// global middleware matching all incoming requests
app.use([](cex::Request* req, cex::Response* res, std::function<void()> next) { ... });
// middleware only for HTTP GET requests
app.get([](cex::Request* req, cex::Response* res, std::function<void()> next) { ... });
// middleware only for HTTP GET and path /content
app.get("/content", app.use([](cex::Request* req, cex::Response* res, std::function<void()> next) { ... });
// middleware for any HTTP method and path /content
app.use("/content", app.use([](cex::Request* req, cex::Response* res, std::function<void()> next) { ... });
Middleware functions can be a function pointer, function object or a lambda.
!! Attention !!
Since, depending on the thread count, requests can be processes by a random thread, attached middleware functions must be reentrant.
However, requests will only be processed within one thread, no matter how many middlewares are attached.
For each incoming request, all attached middlewares are evaluated. If a request matches the middleware's HTTP method and URL, the middleware function is executed. The middlewares are executed in the order they were registered.
Each middleware function receives the following three parameters:
- The
cex::Request
object contaning everything about the incoming request - The
cex::Response
object which is used to create a response - A function pointer which shall be used/called to skip to the next middleware
Execution of middlewares stops once:
- the last registered middleware was executed
- the
next
method of a middleware was not called
libcex
already provides a few predefined middleware functions ready to use:
cex::filesystem
middleware for accesing static files on the filesystem (API docs ↗) (Options ↗)cex::security
middleware that sets a number of security related HTTP headers (API docs ↗) (Options ↗)cex::sessionHandler
middleware that adds/retrieves session cookies (API docs ↗) (Options ↗)cex::basicAuth
middleware that extracts HTTP basic auth information from the request (API docs ↗)
Example:
#include <cex.hpp>
#include <cex/session.hpp>
#include <cex/security.hpp>
#include <cex/filesystem.hpp>
#include <cex/basicauth.hpp>
int main()
{
cex::Server app;
// use filesystem middleware
std::shared_ptr<cex::FilesystemOptions> fsOpts(new cex::FilesystemOptions());
fsOpts.get()->rootPath= "/some/docs/folder";
app.use("/docs", cex::filesystem(fsOpts));
// use security middleware with some options set
std::shared_ptr<cex::SecurityOptions> secOpts(new cex::SecurityOptions());
secOpts.get()->xFrameAllow= cex::xfFrom;
secOpts.get()->xFrameFrom= "my.domain.de";
secOpts.get()->stsMaxAge= 183400;
secOpts.get()->ieNoOpen= cex::no;
secOpts.get()->noDNSPrefetch= cex::no;
app.use(cex::securityHeaders(secOpts));
// use session middleware
std::shared_ptr<cex::SessionOptions> sessionOpts(new cex::SessionOptions());
sessionOpts.get()->expires = 60*60*24*3;
sessionOpts.get()->maxAge= 144;
sessionOpts.get()->domain= "my.domain.de";
sessionOpts.get()->path= "/somePath";
sessionOpts.get()->name= "sessionID";
sessionOpts.get()->secure= false;
sessionOpts.get()->httpOnly=true;
sessionOpts.get()->sameSiteLax= true;
sessionOpts.get()->sameSiteStrict= true;
app.use(cex::sessionHandler(sessionOpts));
// use basic auth middleware
app.use(cex::basicAuth());
// start server
app.listen(true);
return 0;
}
The cex::Request
class provides access to the request contents (URL, headers, parameters, body, ...) as sent by the client. An instance of cex::Request
represents a single HTTP request which shall be handled by the application. In terms of HTTP communication, cex::Request
is read only, that is, it cannot be used to send a response. For this, cex::Response
is used.
Example:
app.use("/content", [](cex::Request* req, cex::Response* res, std::function<void()> next)
{
printf("Protocol: [%d], Method [%d], port [%d], host [%s], url [%s], path [%s], file [%s], user [%s], password [%s]\n",
req->getProtocol(),
req->getMethod(),
req->getPort(),
req->getHost(),
req->getUrl(),
req->getPath(),
req->getFile(),
req->properties.getString("basicUsername").c_str(),
req->properties.getString("basicPassword").c_str());
req->eachQueryParam([](const char* key, const char* value)
{
printf("PARAM: [%s] = [%s]\n", key, value);
return true;
});
const char* body= req->getBody();
// do something with body ...
});
To allow middlewares to transfer information between them, the cex::Request
class contains a property list. For example, the cex::basicAuth
middleware stored the username and password supplied by the client in the properties basicUsername
and basicPassword
.
The cex::Response
class provides the interface for sending responses back to the client. This includes the HTTP Code, payloads as well as header parameters.
The most simple response might just include the HTTP code:
app.use("/content", [](cex::Request* req, cex::Response* res, std::function<void()> next)
{
res->end(200); // HTTP 200 OK
});
The response class also allows to set headers:
res->set("Content-Type", "text/plain");
... or send a payload:
res->end("Hello world :)", 200)
libcex
allows incoming file uploads using a special form of middleware function, the cex::UploadFunction
. It is different from the usual middlewares in that it is called repeatedly for a single request, each time providing a chunk of upload data. In addition, upload functions are executed before middlewares, that is, the first middleware function is only called after all of the upload has been received.
Note that libcex
does not buffer incoming data. It is up to the application to handle uploaded data within the cex::UploadFunction
.
The cex::Server
class provides an interface to attach upload functions:
app.uploads("/uploads", [&uploadBuffer](cex::Request* req, const char* data, size_t len)
{
int fd= -1;
if (!req->properties.has("uploadFileHandle"))
{
fd= ::open("myfile", O_CREAT|O_TRUNC|O_WRONLY, S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH);
req->properties.set("uploadFileHandle", (long)fd);
}
else
{
fd= req->properties.getLong("uploadFileHandle");
}
::write(fd, data, len);
});
don't forget to close the file descriptor:
app.post([](cex::Request* req, cex::Response* res, std::function<void()> next)
{
if (req->properties.has("uploadFileHandle"))
{
int fd= req->properties.getLong("uploadFileHandle");
::close(fd);
req->properties.remove("uploadFileHandle");
}
res->end(200);
});
In case a response shall contain a large payload, using cex::Response::end
would lead to the entire response beeing kept in memory, which might be undesirable.
To solve this issue, libcex
provides a streaming API for sending responses:
app.get("/myfile", [](cex::Request* req, cex::Response* res, std::function<void()> next)
{
std::ifstream file;
file.open("myfile", std::ios_base::in|std::ios_base::binary);
res->set("Content-Type", "application/octet-stream");
res->stream(200, &file);
});
The cex::Response::stream
function accepts a std::istream
, such as a std::ifstream
.
libcex
uses the following two awesome libraries for unit tests:
- bandit - Human-friendly unit testing for C++11
- cpp-httplib - A C++11 header-only HTTP library