Previous: File Router Home: Next: Sanitizing URLs

Router

This example defines a router for URL paths. If the specified route matches one of the existing routes, the example executes the underlying callback function.

/*
    This example defines a router for URL paths.
    Each path is associated with a callback
    function.
*/

#ifndef BOOST_URL_SOURCE
#define BOOST_URL_SOURCE
#endif

#include "router.hpp"

#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/config.hpp>

#include <iostream>
#include <functional>

namespace urls = boost::urls;
namespace core = boost::core;
namespace asio = boost::asio;
namespace beast = boost::beast;
namespace http = beast::http;
using string_view = core::string_view;
using request_t = http::request<http::string_body>;
struct connection;
using handler = std::function<void(connection&, urls::matches)>;

int
serve(
    urls::router<handler> const& r,
    asio::ip::address const& a,
    unsigned short port,
    std::string const& doc_root);

struct connection
{
    connection(asio::io_context& ioc)
        : socket(ioc) {}

    void
    string_reply(core::string_view msg);

    void
    file_reply(core::string_view path);

    void
    error_reply(http::status, core::string_view msg);

    beast::error_code ec;
    asio::ip::tcp::socket socket;
    std::string doc_root;
    request_t req;
};

int
main(int argc, char **argv)
{
    /*
     * Parse cmd-line params
     */
    if (argc != 4)
    {
        core::string_view exec = argv[0];
        auto file_pos = exec.find_last_of("/\\");
        if (file_pos != core::string_view::npos)
            exec = exec.substr(file_pos + 1);
        std::cerr
            << "Usage: " << exec
            << " <address> <port> <doc_root>\n"
               "Example: " << exec << " 0.0.0.0 8080 .\n"
                       "Default values:\n"
                       "- address: 0.0.0.0\n"
                       "- port: 8080\n"
                       "- doc_root: ./\n";
    }
    auto const address = asio::ip::make_address(argc > 1 ? argv[1] : "0.0.0.0");
    auto const port = static_cast<unsigned short>(argc > 2 ? std::atoi(argv[2]) : 8080);
    auto const doc_root = std::string(argc > 3 ? argv[3] : ".");

    /*
     * Create router
     */
    urls::router<handler> r;

    r.insert("/", [&](connection& c, urls::matches const&) {
        c.string_reply("Hello!");
    });

    r.insert("/user/{name}", [&](connection& c, urls::matches const& m) {
        std::string msg = "Hello, ";
        urls::pct_string_view(m[0]).decode({}, urls::string_token::append_to(msg));
        msg += "!";
        c.string_reply(msg);
    });

    r.insert("/user", [&](connection& c, urls::matches const&) {
        std::string msg = "Users: ";
        auto names = {"johndoe", "maria", "alice"};
        for (auto name: names) {
            msg += "<a href=\"/user/";
            msg += name;
            msg += "\">";
            msg += name;
            msg += "</a> ";
        }
        c.string_reply(msg);
    });

    r.insert("/public/{path+}", [&](connection& c, urls::matches m) {
        c.file_reply(m["path"]);
    });

    return serve(r, address, port, doc_root);
}

#define ROUTER_CHECK(cond)       if(!(cond)) { break; }
#define ROUTER_CHECK_EC(ec, cat) if(ec.failed()) { std::cerr << #cat << ": " << ec.message() << "\n"; break; }

int
serve(
    urls::router<handler> const& r,
    asio::ip::address const& address,
    unsigned short port,
    std::string const& doc_root)
{
    /*
     * Serve the routes with a simple synchronous
     * server. This is an implementation detail
     * in the context of this example.
     */
    std::cout << "Listening on http://" << address << ":" << port << "\n";
    asio::io_context ioc(1);
    asio::ip::tcp::acceptor acceptor(ioc, {address, port});
    urls::matches m;
    for(;;)
    {
        connection c(ioc);
        c.doc_root = doc_root;
        acceptor.accept(c.socket);
        beast::flat_buffer buffer;
        for(;;)
        {
            // Read a request
            http::read(c.socket, buffer, c.req, c.ec);
            ROUTER_CHECK(c.ec != http::error::end_of_stream)
            ROUTER_CHECK_EC(c.ec, read)
            // Handle request
            auto rpath = urls::parse_path(c.req.target());
            if (c.req.method() != http::verb::get &&
                c.req.method() != http::verb::head)
                c.error_reply(
                    http::status::bad_request,
                    std::string("Unknown HTTP-method: ") +
                        std::string(c.req.method_string()));
            else if (!rpath)
                c.error_reply(http::status::bad_request, "Illegal request-target");
            else if (auto h = r.find(*rpath, m))
                (*h)(c, m);
            else
                c.error_reply(
                    http::status::not_found,
                    "The resource '" +
                        std::string(rpath->buffer()) +
                        "' was not found.");
            ROUTER_CHECK_EC(c.ec, write)
            ROUTER_CHECK(c.req.keep_alive())
        }
        c.socket.shutdown(asio::ip::tcp::socket::shutdown_send, c.ec);
    }
    return EXIT_SUCCESS;
}

#undef ROUTER_CHECK_EC
#undef ROUTER_CHECK

void
connection::
error_reply(http::status s, core::string_view msg)
{
    // invalid route
    http::response<http::string_body> res{s, req.version()};
    res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
    res.set(http::field::content_type, "text/html");
    res.keep_alive(req.keep_alive());
    res.body() = msg;
    res.prepare_payload();
    http::write(socket, res, ec);
}


void
connection::
string_reply(core::string_view msg)
{
    http::response<http::string_body> res{http::status::ok, req.version()};
    res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
    res.set(http::field::content_type, "text/html");
    res.keep_alive(req.keep_alive());
    res.body() = msg;
    res.prepare_payload();
    http::write(socket, res, ec);
}

core::string_view
mime_type(core::string_view path);

std::string
path_cat(
    beast::string_view base,
    beast::string_view path);

void
connection::
file_reply(core::string_view path)
{
    http::file_body::value_type body;
    std::string jpath = path_cat(doc_root, path);
    body.open(jpath.c_str(), beast::file_mode::scan, ec);
    if(ec == beast::errc::no_such_file_or_directory)
    {
        error_reply(
            http::status::not_found,
            "The resource '" + std::string(path) +
                "' was not found in " + jpath);
        return;
    }
    auto const size = body.size();
    http::response<http::file_body> res{
        std::piecewise_construct,
        std::make_tuple(std::move(body)),
        std::make_tuple(http::status::ok, req.version())};
    res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
    res.set(http::field::content_type, mime_type(path));
    res.content_length(size);
    res.keep_alive(req.keep_alive());
    http::write(socket, res, ec);
}

// Append an HTTP rel-path to a local filesystem path.
// The returned path is normalized for the platform.
std::string
path_cat(
    core::string_view base,
    core::string_view path)
{
    if (base.empty())
        return std::string(path);
    std::string result(base);
#ifdef BOOST_MSVC
    char constexpr path_separator = '\\';
#else
    char constexpr path_separator = '/';
#endif
    if( result.back() == path_separator &&
        path.starts_with(path_separator))
        result.resize(result.size() - 1);
    else if (result.back() != path_separator &&
             !path.starts_with(path_separator))
    {
        result.push_back(path_separator);
    }
    result.append(path.data(), path.size());
#ifdef BOOST_MSVC
    for(auto& c : result)
        if(c == '/')
            c = path_separator;
#endif
    return result;
}

core::string_view
mime_type(core::string_view path)
{
    using beast::iequals;
    auto const ext = [&path]
    {
        auto const pos = path.rfind(".");
        if(pos == beast::string_view::npos)
            return beast::string_view{};
        return path.substr(pos);
    }();
    if(iequals(ext, ".htm"))  return "text/html";
    if(iequals(ext, ".html")) return "text/html";
    if(iequals(ext, ".php"))  return "text/html";
    if(iequals(ext, ".css"))  return "text/css";
    if(iequals(ext, ".txt"))  return "text/plain";
    if(iequals(ext, ".js"))   return "application/javascript";
    if(iequals(ext, ".json")) return "application/json";
    if(iequals(ext, ".xml"))  return "application/xml";
    if(iequals(ext, ".swf"))  return "application/x-shockwave-flash";
    if(iequals(ext, ".flv"))  return "video/x-flv";
    if(iequals(ext, ".png"))  return "image/png";
    if(iequals(ext, ".jpe"))  return "image/jpeg";
    if(iequals(ext, ".jpeg")) return "image/jpeg";
    if(iequals(ext, ".jpg"))  return "image/jpeg";
    if(iequals(ext, ".gif"))  return "image/gif";
    if(iequals(ext, ".bmp"))  return "image/bmp";
    if(iequals(ext, ".ico"))  return "image/vnd.microsoft.icon";
    if(iequals(ext, ".tiff")) return "image/tiff";
    if(iequals(ext, ".tif"))  return "image/tiff";
    if(iequals(ext, ".svg"))  return "image/svg+xml";
    if(iequals(ext, ".svgz")) return "image/svg+xml";
    return "application/text";
}