HTTP Server

Tutorial

Nov 11, 2023 | 3 min read

Minimal Setup Example

Lets start with the following example:

Most Minimal Example
 1#include <boost/asio/any_io_executor.hpp>
 2#include <boost/asio/thread_pool.hpp>
 3
 4#include <roar/server.hpp>
 5#include <roar/utility/scope_exit.hpp>
 6
 7#include <iostream>
 8
 9int main()
10{
11    boost::asio::thread_pool pool{4};
12
13    // Create server.
14    Roar::Server server{{.executor = pool.executor()}};
15
16    // SSL Server (you can provide your own SSL Context for more options.)
17    //
18    // auto sslContext = SslServerContext{
19    //      .certificate = "certificate string here", // or filesystem::path to cert
20    //      .privateKey = "private key here", // or filesystem::path to key
21    //      .password = "passwordHereIfApplicable",
22    // };
23    // initializeServerSslContext(sslContext);
24    //
25    // Roar::Server server{{
26    //     .executor = pool.executor()
27    //     .sslContext = std::move(sslContext),
28    // }};
29
30    // stop the thread_pool on scope exit to guarantee that all asynchronous tasks are finished before the server is
31    // destroyed.
32    const auto shutdownPool = Roar::ScopeExit{[&pool]() {
33        pool.stop();
34        pool.join();
35    }};
36
37    // Start server and bind on port "port".
38    server.start(8081);
39
40    // Prevent exit somehow:
41    std::cin.get();
42}

In this example we are creating an asio thread pool for the asynchronous actions of our server, the server itself and then start the server by binding and accepting on port 8081.

Warning

Important: The Roar::ScopeExit construct ensures that the threads are shutdown before the server or request listeners are destroyed. This way we do not need to ensure their survival in asynchronous handlers and do not need to use shared pointers.

This example is not very useful though, because any requests would be greeted by 404. This is where the RequestListener concept comes into play.

The Request Listener

Lets define one:

basic_request_listener.hpp
 1#pragma once
 2
 3#include <roar/routing/request_listener.hpp>
 4
 5#include <boost/describe/class.hpp>
 6
 7class MyRequestListener
 8{
 9  private:
10    ROAR_MAKE_LISTENER(MyRequestListener);
11
12    // Use the ROAR_GET, ... macro to define a route. Routes are basically mappings of paths to handlers.
13    ROAR_GET(index)("/");
14
15  private:
16    // This makes routes visible to the library. For as long as we have to wait for native reflection...
17    BOOST_DESCRIBE_CLASS(MyRequestListener, (), (), (), (roar_index, roar_upload))
18};
basic_request_listener.cpp
 1#include "basic_request_listener.hpp"
 2
 3using namespace boost::beast::http;
 4
 5void RequestListener::index(Roar::Session& session, Roar::EmptyBodyRequest&& request)
 6{
 7    session
 8        .send<string_body>(request)
 9        ->status(status::ok)
10        .contentType("text/plain")
11        .body("Hi")
12        .commit();
13}

This is our first request listener. It is a class that is able to accept http requests for predefined paths. In this case the “/” path will get routed to the index member function that can now handle this request. If the function is empty, the Roar::Session will run out of scope and will be closed and destroyed.

ROAR_GET, ROAR_PUT, …, ROAR_ROUTE is a macro that create some boilerplate in the class. More about them is in the RequestListener section. Doxygen about all defined macros: ROAR_ROUTE.

Now if we can add this listener to our server:

Adding the request listener to our
 1// Other includes as before
 2#include "basic_request_listener.hpp"
 3
 4int main()
 5{
 6    boost::asio::thread_pool pool{4};
 7
 8    // Create server.
 9    Roar::Server server{{.executor = pool.executor()}};
10
11    // stop the thread_pool on scope exit to guarantee that all asynchronous tasks are finished before the server is
12    // destroyed.
13    const auto shutdownPool = Roar::ScopeExit{[&pool]() {
14        pool.stop();
15        pool.join();
16    }};
17
18    server.installRequestListener<RequestListener>();
19
20    // Start server and bind on port "port".
21    server.start(8081);
22
23    // Prevent exit somehow:
24    std::cin.get();
25}

After this, making requests to the server on “/” will yield a response:

Curl
 1$ curl -v localhost:8081/
 2*   Trying 127.0.0.1:8081...
 3* Connected to localhost (127.0.0.1) port 8081 (#0)
 4> GET / HTTP/1.1
 5> Host: localhost:8081
 6> User-Agent: curl/7.83.0
 7> Accept: */*
 8>
 9* Mark bundle as not supporting multiuse
10< HTTP/1.1 200 OK
11< Server: Roar+Boost.Beast/330
12< Content-Type: text/plain
13< Content-Length: 2
14<
15Hi* Connection #0 to host localhost left intact

RequestListener

A request listener is a class that maps paths to request handlers. The following snippet shows a more detailed example.

More sophisticated request listener.
 1#pragma once
 2
 3#include <roar/routing/request_listener.hpp>
 4
 5#include <boost/describe/class.hpp>
 6
 7class MyRequestListener
 8{
 9  private:
10    ROAR_MAKE_LISTENER(MyRequestListener);
11
12    // This is an abbreviation, where only the path is specified.
13    ROAR_GET(index)("/");
14
15    // This here sets more options:
16    ROAR_GET(images)({
17        .path = "/img/(.+)",
18        .pathType = Roar::RoutePathType::Regex,  // Path is a regular expression
19        .routeOptions = {
20            .allowUnsecure = true, // Allow HTTP request here, even if HTTPS server
21            .expectUpgrade = false, // Expect Websocket upgrade request here?
22            .cors = Roar::makePermissiveCorsSettings("get"),
23        }
24    })
25
26  private:
27    // This makes routes visible to the library. For as long as we have to wait for native reflection...
28    BOOST_DESCRIBE_CLASS(MyRequestListener, (), (), (), (roar_index, roar_images))
29};

Available options for routes are documented here: RouteInfo.

Head Requests

Head requests are not automatically possible on get requests. Because the library cannot formulate responses without running user code. If you want to support head requests, you have to specify them explicitely.

The BOOST_DESCRIBE_CLASS macro is used to make generated decorations for the handlers visible to the roar library. Add all routes you have to the last list and prefix them with “roar_”. Forgetting them here will result in them not being found.

Routing

Routing is the process of taking a path and finding the appropriate handler for it. Roar supports two kinds: simple string matches of paths and regex paths.

String Paths

String paths take precedence over regex paths.

Regex Paths

Regex routes are taken when a path of a request matches with the given regex. These matches can be obtained in the handler like so:

More sophisticated request listener.
 1#include "basic_request_listener.hpp"
 2
 3using namespace boost::beast::http;
 4
 5void RequestListener::image(Roar::Session& session, Roar::EmptyBodyRequest&& request)
 6{
 7    // Does not return the full path match.
 8    auto const& matches = request.pathMatches();
 9
10    // Suppose the regex was "/img/(.+)/([a-z])" and the path was "/img/bla/b".
11    // then: matches[0] => "bla",
12    //       matches[1] => "b"
13}

Sending Responses

Here is an example on how to send a response. The session is kept alive over the course of the send operation.

Write Example
 1void RequestListener::index(Roar::Session& session, Roar::EmptyBodyRequest&& request)
 2{
 3    session
 4        .send<string_body>(request)
 5        ->status(status::ok)
 6        .contentType("text/plain")
 7        .body("Hi")
 8        .commit() // Actually performs the send operation.
 9        // Javascript-Promise like continuation:
10        .then([](auto& session, auto const& req){
11            // Send complete.
12        })
13        .fail([](Roar::Error const&) {
14            // Send failed.
15        })
16    ;
17}

Reading a Body

Here is an example on how to read a body. The session is kept alive over the course of the read operation.

Write Example
 1void RequestListener::upload(Roar::Session& session, Roar::EmptyBodyRequest&& request)
 2{
 3    namespace http = boost::beast::http;
 4
 5    boost::beast::error_code ec;
 6    http::file_body::value_type body;
 7    body.open("/tmp/bla.txt", boost::beast::file_mode::write, ec);
 8    // if (ec) reply with error.
 9
10    session.template read<http::file_body>(std::move(req), std::move(body))
11        ->noBodyLimit()
12        .commit()
13        .then([](Session& session, Request<http::file_body> const& req) {
14            session
15                .send<string_body>(request)
16                ->status(status::ok)
17                .contentType("text/plain")
18                .body("Thank you!")
19                .commit();
20        });
21}

Rate Limiting

Download/Upload speeds can be set on a session using “readLimit” and “writeLimit”. Session class.

Serving Directories

Serving directories for file download, upload and deletion can be done in the following way: (Also see the serve_directory example for a more).

A class that gives access to the filesystem
 1class FileServer
 2{
 3  private:
 4    ROAR_MAKE_LISTENER(FileServer);
 5
 6    ROAR_SERVE(serveMyDirectory)
 7    ({
 8        // RouteOptions, that are also present in normal routes.
 9        .path = "/",
10        .routeOptions = {.allowUnsecure = false},
11
12        // ServeOptions, these can also be set on a per session basis. Provide defaults here:
13        .serveOptions = {
14            // Allow GET requests for file downloads?
15            .allowDownload = true,
16
17            // Allow PUT requests for file uploads? Defaults to false.
18            .allowUpload = false,
19
20            // Allow PUT requests to overwrite existing files? Defaults to false.
21            .allowOverwrite = false,
22
23            // Allow DELETE requests to delete existing files (and empty directories)? Defaults to false.
24            .allowDelete = false,
25
26            // Allow DELETE requests to recursively delete directories? Defaults to false.
27            .allowDeleteOfNonEmptyDirectories = false,
28
29            // If this option is set, requests to directories do not try to serve an index.html,
30            // but give a table with all existing files instead. Defaults to true.
31            .allowListing = true,
32
33            // A path to the filesystem. Special magic prefix values are replaced.
34            .pathProvider = "~/roar",
35            // Alternatives:
36            // .pathProvider = &FileServer::servePath_
37            // .pathProvider = &FileServer::servePath
38            // .pathProvider = &FileServer::servePath2
39
40            // Optional! CSS Style to style the directory listing. I recommend using the default, its pretty ;)
41            .customListingStyle = "",
42        },
43    });
44
45  private:
46    std::filesystem::path servePath_;
47    std::filesystem::path servePath() const {
48        return servePath_;
49    }
50    std::filesystem::path servePath2() {
51        return servePath_;
52    }
53
54    BOOST_DESCRIBE_CLASS(FileServer, (), (), (), (roar_serveMyDirectory))
55};

This class defines a route that gives access to the filesystem using ROAR_SERVE. Filesystem paths can be prefixed by one of the following values:

  • ~ Linux: home. Windows: home

  • %userprofile% Linux: home. Windows: home

  • %appdata% Linux: home. Windows: CSIDL_APPDATA

  • %localappdata% Linux: home. Windows: CSIDL_APPDATA_LOCAL

  • %temp% Linux: /tmp. Windows: %userprofile%\AppData\Local\Temp

A ROAR_SERVE macro also generates a function that you need to define. This function can be used to fine-control permissions.

Control permissions.
 1Roar::ServeDecision FileServer::serveMyDirectory(
 2    Roar::Session& session,
 3    Roar::EmptyBodyRequest const& request,
 4    Roar::FileAndStatus const& fileAndStatus,
 5    Roar::ServeOptions<FileServer>& options)
 6{
 7    session.readLimit(100'000); // 100kb/s
 8    session.writeLimit(100'000); // 100kb/s
 9
10    std::cout << fileAndStatus.file << "\n";
11    std::cout << static_cast<int>(fileAndStatus.status.type()) << "\n";
12
13    // You can do something like:
14    /**
15     * if (user has permissions)
16     *   options.allowUpload = true;
17     */
18    options.allowListing = true;
19    return Roar::ServeDecision::Continue;
20    // return Roar::ServeDecision::Deny;
21    // return Roar::ServeDecision::Handled;
22}

The altered options in the above example are only set for the individual session and do not carry over to other sessions. The Return values have the following meaning:

  • Continue: continue and handle the request based on the given permissions.

  • Deny: Do not continue and return a forbidden response.

  • Handled: You have handled the request here, the server will only let the session run out of scope (close by reset, if not kept alive by you).