asyncgi - is a C++17 asynchronous microframework for creating web applications interfacing with any HTTP server supporting FastCGI protocol. It aims to provide a modern way of using CGI, with a custom performant FastCGI implementation in C++, multithreading support and a clean and simple API:
#include <asyncgi/asyncgi.h>
namespace http = asyncgi::http;
int main()
{
auto io = asyncgi::IO{};
auto router = asyncgi::Router{io};
router.route("/", http::RequestMethod::Get).process(
[](const asyncgi::Request&)
{
return http::Response{"Hello world"};
});
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
Web applications developed with asyncgi
require to establish a FastCGI connection with a web server handling HTTP requests. Most popular servers provide this functionality, for example NGINX
can be used with a following configuration:
server {
listen 8088;
server_name localhost;
location / {
try_files $uri @fcgi;
}
location @fcgi {
fastcgi_pass unix:/tmp/fcgi.sock;
#or using a TCP socket
#fastcgi_pass localhost:9000;
include fastcgi_params;
}
}
asyncgi
supports both UNIX domain
and TCP
sockets for opening FastCGI
connections.
In order to process requests, it's necessary to provide a function or function object that fulfills
the RequestProcessor
requirement. This means that the function must be invocable with one of the following signatures:
http::Response (const asyncgi::Request&)
void (const asyncgi::Request&, asyncgi::Responder&)
.
Example
///examples/example_request_processor.cpp
///
#include <asyncgi/asyncgi.h>
namespace http = asyncgi::http;
http::Response guestBookPage(const asyncgi::Request& request)
{
if (request.path() == "/")
return {R"(
<h1>Guest book</h1>
<p>No messages</p>
)"};
return http::ResponseStatus::_404_Not_Found;
}
int main()
{
auto io = asyncgi::IO{};
auto server = asyncgi::Server{io, guestBookPage};
//Listen for FastCGI connections on UNIX domain socket
server.listen("/tmp/fcgi.sock");
//or over TCP
//server.listen("127.0.0.1", 9088);
io.run();
return 0;
}
Here, the guestBookPage
function serves as the request processor. Another way to implement it is by accepting a
reference to the asyncgi::Responder
object, which can be used for sending responses manually:
void guestBookPage(const asyncgi::Request& request, asyncgi::Responder& responder)
{
if (request.path() == "/")
responder.send(R"(
<h1>Guest book</h1>
<p>No messages</p>
)");
return responder.send(http::ResponseStatus::_404_Not_Found);
}
This approach tends to be more verbose and error-prone, therefore it is preferable to use it only when access to asyncgi::Responder is required for initiating asynchronous operations from the request processor. These cases are covered in the later parts of this document.
Multiple request processors can be registered in the asyncgi::Router
object, where they are matched to the paths
specified in requests. The asyncgi::Router
itself satisfies the RequestProcessor
requirement.
If multiple threads are required for request processing, the desired number of workers can be passed to
the asyncgi::IO
object's constructor. In such cases, the user must ensure that any shared data in the request
processors is protected from concurrent read/write access.
Example
///examples/example_router.cpp
///
#include <asyncgi/asyncgi.h>
#include <mutex>
namespace http = asyncgi::http;
using namespace std::string_literals;
class GuestBookState {
public:
std::vector<std::string> messages()
{
auto lock = std::scoped_lock{mutex_};
return messages_;
}
void addMessage(const std::string& msg)
{
auto lock = std::scoped_lock{mutex_};
messages_.emplace_back(msg);
}
private:
std::vector<std::string> messages_;
std::mutex mutex_;
};
class GuestBookPage {
public:
GuestBookPage(GuestBookState& state)
: state_(&state)
{
}
http::Response operator()(const asyncgi::Request&)
{
auto messages = state_->messages();
auto page = "<h1>Guest book</h1>"s;
if (messages.empty())
page += "<p>No messages</p>";
else
for (const auto& msg : messages)
page += "<p>" + msg + "</p>";
page += "<hr>";
page += "<form method=\"post\" enctype=\"multipart/form-data\">"
"<label for=\"msg\">Message:</label>"
"<input id=\"msg\" name=\"msg\" value=\"\">"
"<input value=\"Submit\" data-popup=\"true\" type=\"submit\">"
"</form>";
return page;
}
private:
GuestBookState* state_;
};
class GuestBookAddMessage {
public:
GuestBookAddMessage(GuestBookState& state)
: state_(&state)
{
}
http::Response operator()(const asyncgi::Request& request)
{
state_->addMessage(std::string{request.formField("msg")});
return http::Redirect{"/"};
}
private:
GuestBookState* state_;
};
int main()
{
auto io = asyncgi::IO{4}; //4 threads processing requests
auto state = GuestBookState{};
auto router = asyncgi::Router{io};
router.route("/", http::RequestMethod::Get).process<GuestBookPage>(state);
router.route("/", http::RequestMethod::Post).process<GuestBookAddMessage>(state);
router.route().set(http::Response{http::ResponseStatus::_404_Not_Found, "Page not found"});
//Alternatively, it's possible to pass arguments for creation of http::Response object to the set() method.
//router.route().set(http::ResponseStatus::Code_404_Not_Found, "Page not found");
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
When using asyncgi::Router
with regular expressions, request processors must satisfy
the ParametrizedRequestProcessor
requirement. That means that a function object must be invocable with one of the
following signatures:
http::Response void(const TRouteParams&..., const asyncgi::Request&)
void (const TRouteParams&..., const asyncgi::Request&, asyncgi::Responder&)
The TRouteParams
represents zero or more parameters generated from the capturing groups of the regular expression. For
example, http::Response (int age, string name, const asyncgi::Request&)
signature can be used to process
requests matched by asyncgi::rx{"/person/(\\w+)/age/(\\d+)"}
.
In the following example a ParametrizedRequestProcessor
named GuestBookRemoveMessage
is added to remove the stored
guest book messages:
Example
///examples/example_route_params.cpp
///
#include <asyncgi/asyncgi.h>
#include <mutex>
using namespace asyncgi;
using namespace std::string_literals;
class GuestBookState {
public:
std::vector<std::string> messages()
{
auto lock = std::scoped_lock{mutex_};
return messages_;
}
void addMessage(const std::string& msg)
{
auto lock = std::scoped_lock{mutex_};
messages_.emplace_back(msg);
}
void removeMessage(int index)
{
auto lock = std::scoped_lock{mutex_};
if (index < 0 || index >= static_cast<int>(messages_.size()))
return;
messages_.erase(std::next(messages_.begin(), index));
}
private:
std::vector<std::string> messages_;
std::mutex mutex_;
};
std::string makeMessage(int index, const std::string& msg)
{
return msg + R"(<form action="/delete/)" + std::to_string(index) +
R"(" method="post"> <input value="Delete" type="submit"> </form></div>)";
}
class GuestBookPage {
public:
explicit GuestBookPage(GuestBookState& state)
: state_{&state}
{
}
http::Response operator()(const asyncgi::Request&)
{
auto messages = state_->messages();
auto page = "<h1>Guest book</h1>"s;
if (messages.empty())
page += "<p>No messages</p>";
else
for (auto i = 0; i < static_cast<int>(messages.size()); ++i)
page += "<p>" + makeMessage(i, messages.at(i)) + "</p>";
page += "<hr>";
page += "<form method=\"post\" enctype=\"multipart/form-data\">"
"<label for=\"msg\">Message:</label>"
"<input id=\"msg\" name=\"msg\" value=\"\">"
"<input value=\"Submit\" data-popup=\"true\" type=\"submit\">"
"</form>";
return page;
}
private:
GuestBookState* state_;
};
class GuestBookAddMessage {
public:
explicit GuestBookAddMessage(GuestBookState& state)
: state_{&state}
{
}
http::Response operator()(const asyncgi::Request& request)
{
state_->addMessage(std::string{request.formField("msg")});
return http::Redirect{"/"};
}
private:
GuestBookState* state_;
};
class GuestBookRemoveMessage {
public:
explicit GuestBookRemoveMessage(GuestBookState& state)
: state_{&state}
{
}
http::Response operator()(int index, const asyncgi::Request&)
{
state_->removeMessage(index);
return http::Redirect{"/"};
}
private:
GuestBookState* state_;
};
int main()
{
auto io = asyncgi::IO{4};
auto state = GuestBookState{};
auto router = asyncgi::Router{io};
router.route("/", http::RequestMethod::Get).process<GuestBookPage>(state);
router.route("/", http::RequestMethod::Post).process<GuestBookAddMessage>(state);
router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post).process<GuestBookRemoveMessage>(state);
router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
Regular expression capture groups are transformed into request processor arguments using std::stringstream
. In order
to support request processors with user-defined parameter types, it is necessary to provide a specialization
of asyncgi::config::StringConverter
class template. The previous example has been modified to reformat
the GuestBookRemoveMessage
request processor to use the MessageNumber
structure as a request processor argument:
Example
///examples/example_route_params_user_defined_types.cpp
///
#include <asyncgi/asyncgi.h>
#include <mutex>
using namespace asyncgi;
using namespace std::string_literals;
struct MessageNumber {
int value;
};
template<>
struct asyncgi::config::StringConverter<MessageNumber> {
static std::optional<MessageNumber> fromString(const std::string& data)
{
return MessageNumber{std::stoi(data)};
}
};
class GuestBookState {
public:
std::vector<std::string> messages()
{
auto lock = std::scoped_lock{mutex_};
return messages_;
}
void addMessage(const std::string& msg)
{
auto lock = std::scoped_lock{mutex_};
messages_.emplace_back(msg);
}
void removeMessage(int index)
{
auto lock = std::scoped_lock{mutex_};
if (index < 0 || index >= static_cast<int>(messages_.size()))
return;
messages_.erase(std::next(messages_.begin(), index));
}
private:
std::vector<std::string> messages_;
std::mutex mutex_;
};
std::string makeMessage(int index, const std::string& msg)
{
return msg + R"(<form action="/delete/)" + std::to_string(index) +
R"(" method="post"> <input value="Delete" type="submit"> </form></div>)";
}
class GuestBookPage {
public:
explicit GuestBookPage(GuestBookState& state)
: state_{&state}
{
}
http::Response operator()(const asyncgi::Request&)
{
auto messages = state_->messages();
auto page = "<h1>Guest book</h1>"s;
if (messages.empty())
page += "<p>No messages</p>";
else
for (auto i = 0; i < static_cast<int>(messages.size()); ++i)
page += "<p>" + makeMessage(i, messages.at(i)) + "</p>";
page += "<hr>";
page += "<form method=\"post\" enctype=\"multipart/form-data\">"
"<label for=\"msg\">Message:</label>"
"<input id=\"msg\" name=\"msg\" value=\"\">"
"<input value=\"Submit\" data-popup=\"true\" type=\"submit\">"
"</form>";
return page;
}
private:
GuestBookState* state_;
};
class GuestBookAddMessage {
public:
explicit GuestBookAddMessage(GuestBookState& state)
: state_{&state}
{
}
http::Response operator()(const asyncgi::Request& request)
{
state_->addMessage(std::string{request.formField("msg")});
return http::Redirect{"/"};
}
private:
GuestBookState* state_;
};
class GuestBookRemoveMessage {
public:
explicit GuestBookRemoveMessage(GuestBookState& state)
: state_{&state}
{
}
http::Response operator()(MessageNumber msgNumber, const asyncgi::Request&)
{
state_->removeMessage(msgNumber.value);
return http::Redirect{"/"};
}
private:
GuestBookState* state_;
};
int main()
{
auto io = asyncgi::IO{4};
auto state = GuestBookState{};
auto router = asyncgi::Router{io};
router.route("/", http::RequestMethod::Get).process<GuestBookPage>(state);
router.route("/", http::RequestMethod::Post).process<GuestBookAddMessage>(state);
router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post).process<GuestBookRemoveMessage>(state);
router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
When using asyncgi::Router
, it is possible to specify a template argument for a context structure type. This
structure is then passed to the ContextualRequestProcessor
functions and can be accessed and modified throughout the
request processing for multiple routes. The ContextualRequestProcessor
is a RequestProcessor
that takes an
additional argument referring to the context object.
A single request can match multiple routes, as long as all preceding request processors do not send any response. To
avoid sending responses request processors can use the std::optional<http::Response>
signature and return empty
values. This
allows using asyncgi::Router
to register middleware-like processors, which primarily modify the route context for
subsequent processors.
The next example demonstrates how a route context can be used for storing authorization information:
Example
///examples/example_route_context.cpp
///
#include <asyncgi/asyncgi.h>
#include <mutex>
#include <optional>
namespace http = asyncgi::http;
using namespace std::string_literals;
enum class AccessRole {
Admin,
Guest
};
struct RouteContext {
AccessRole role = AccessRole::Guest;
};
struct AdminAuthorizer {
std::optional<http::Response> operator()(const asyncgi::Request& request, RouteContext& context)
{
if (request.cookie("admin_id") == "ADMIN_SECRET")
context.role = AccessRole::Admin;
return std::nullopt;
}
};
struct LoginPage {
http::Response operator()(const asyncgi::Request&, RouteContext& context)
{
if (context.role == AccessRole::Guest)
return {R"(
<html>
<form method="post" enctype="multipart/form-data">
<label for="msg">Login:</label>
<input id="login" name="login" value="">
<label for="msg">Password:</label>
<input id="passwd" name="passwd" value="">
<input value="Submit" data-popup="true" type="submit">
</form></html>)"};
else //We are already logged in as the administrator
return http::Redirect{"/"};
}
};
struct LoginPageAuthorize {
http::Response operator()(const asyncgi::Request& request, RouteContext& context)
{
if (context.role == AccessRole::Guest) {
if (request.formField("login") == "admin" && request.formField("passwd") == "12345")
return {http::Redirect{"/"}, {asyncgi::http::Cookie("admin_id", "ADMIN_SECRET")}};
else
return http::Redirect{"/login"};
}
else //We are already logged in as the administrator
return http::Redirect{"/"};
}
};
int main()
{
auto io = asyncgi::IO{4}; //4 threads processing requests
auto router = asyncgi::Router<RouteContext>{io};
router.route(asyncgi::rx{".*"}).process<AdminAuthorizer>();
router.route("/").process(
[](const asyncgi::Request&, asyncgi::Responder& response, RouteContext& context)
{
if (context.role == AccessRole::Admin)
response.send("<p>Hello admin</p>");
else
response.send(R"(<p>Hello guest</p><p><a href="/login">login</a>)");
});
router.route("/login", http::RequestMethod::Get).process<LoginPage>();
router.route("/login", http::RequestMethod::Post).process<LoginPageAuthorize>();
router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
Any parameter of request or context objects can be registered for route matching
in asyncgi::Router::route()
method. To achieve this, it is required to provide a specialization of
the asyncgi::config::RouteMatcher
class template and implement a comparator bool operator() within it. Let's see how
to register the enum class Access
from the previous example as a route matcher:
Example
///examples/example_route_matcher.cpp
///
#include <asyncgi/asyncgi.h>
#include <mutex>
#include <optional>
namespace http = asyncgi::http;
using namespace std::string_literals;
enum class AccessRole {
Admin,
Guest
};
struct RouteContext {
AccessRole role = AccessRole::Guest;
};
struct AdminAuthorizer {
std::optional<http::Response> operator()(const asyncgi::Request& request, RouteContext& context)
{
if (request.cookie("admin_id") == "ADMIN_SECRET")
context.role = AccessRole::Admin;
return std::nullopt;
}
};
struct LoginPage {
http::Response operator()(const asyncgi::Request&)
{
return {R"(
<html>
<form method="post" enctype="multipart/form-data">
<label for="msg">Login:</label>
<input id="login" name="login" value="">
<label for="msg">Password:</label>
<input id="passwd" name="passwd" value="">
<input value="Submit" data-popup="true" type="submit">
</form></html>)"};
}
};
struct LoginPageAuthorize {
http::Response operator()(const asyncgi::Request& request)
{
if (request.formField("login") == "admin" && request.formField("passwd") == "12345")
return {http::Redirect{"/"}, {asyncgi::http::Cookie("admin_id", "ADMIN_SECRET")}};
return http::Redirect{"/login"};
}
};
template<>
struct asyncgi::config::RouteMatcher<AccessRole, RouteContext> {
bool operator()(AccessRole value, const asyncgi::Request&, const RouteContext& context) const
{
return value == context.role;
}
};
int main()
{
auto io = asyncgi::IO{4};
auto router = asyncgi::Router<RouteContext>{io};
router.route(asyncgi::rx{".*"}).process<AdminAuthorizer>();
router.route("/").process(
[](const asyncgi::Request&, RouteContext& context) -> http::Response
{
if (context.role == AccessRole::Admin)
return {"<p>Hello admin</p>"};
else
return {R"(<p>Hello guest</p><p><a href="/login">login</a>)"};
});
router.route("/login", http::RequestMethod::Get, AccessRole::Guest).process<LoginPage>();
router.route("/login", http::RequestMethod::Post, AccessRole::Guest).process<LoginPageAuthorize>();
router.route("/login", http::RequestMethod::Get, AccessRole::Admin).set("/", http::RedirectType::Found);
router.route("/login", http::RequestMethod::Post, AccessRole::Admin).set("/", http::RedirectType::Found);
router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
Let's combine all the previous examples to create a simple guest book.
The messages in the guest book will only persist during the application runtime, as they are stored in a std::vector
.
The admin credentials for logging in are as follows: login: admin
, password: 12345
. The admin account has the
ability to delete posts.
Example
///examples/example_guestbook.cpp
///
#include <asyncgi/asyncgi.h>
#include <mutex>
#include <optional>
#include <regex>
namespace http = asyncgi::http;
using namespace std::string_literals;
enum class AccessRole {
Admin,
Guest
};
struct RouteContext {
AccessRole role = AccessRole::Guest;
};
template<>
struct asyncgi::config::RouteMatcher<AccessRole, RouteContext> {
bool operator()(AccessRole value, const asyncgi::Request&, const RouteContext& context) const
{
return value == context.role;
}
};
std::optional<http::Response> authorizeAdmin(const asyncgi::Request& request, RouteContext& context)
{
if (request.cookie("admin_id") == "ADMIN_SECRET")
context.role = AccessRole::Admin;
return std::nullopt;
}
http::Response showLoginPage(const asyncgi::Request&)
{
return {R"(
<head><link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"></head>
<form method="post" enctype="multipart/form-data">
<label for="msg">Login:</label>
<input id="login" name="login" value="">
<label for="msg">Password:</label>
<input id="passwd" name="passwd" value="">
<input value="Submit" data-popup="true" type="submit">
</form>)"};
}
http::Response loginAdmin(const asyncgi::Request& request)
{
if (request.formField("login") == "admin" && request.formField("passwd") == "12345")
return {http::Redirect{"/"}, {http::Cookie("admin_id", "ADMIN_SECRET")}};
else
return http::Redirect{"/login"};
}
http::Response logoutAdmin(const asyncgi::Request&)
{
return {http::Redirect{"/"}, {http::Cookie("admin_id", "")}};
}
struct GuestBookMessage {
std::string name;
std::string text;
};
class GuestBookState {
public:
std::vector<GuestBookMessage> messages()
{
auto lock = std::scoped_lock{mutex_};
return messages_;
}
void addMessage(std::string name, std::string msg)
{
name = std::regex_replace(name, std::regex{"<"}, "<");
name = std::regex_replace(name, std::regex{">"}, ">");
msg = std::regex_replace(msg, std::regex{"<"}, "<");
msg = std::regex_replace(msg, std::regex{">"}, ">");
auto lock = std::scoped_lock{mutex_};
messages_.emplace_back(GuestBookMessage{name.empty() ? "Guest" : name, msg});
}
void removeMessage(int index)
{
auto lock = std::scoped_lock{mutex_};
if (index < 0 || index >= static_cast<int>(messages_.size()))
return;
messages_.erase(std::next(messages_.begin(), index));
}
private:
std::vector<GuestBookMessage> messages_;
std::mutex mutex_;
};
std::string makeMessagesDiv(const std::vector<GuestBookMessage>& messages, AccessRole role)
{
if (messages.empty())
return "<div>No messages</div>";
auto messagesDiv = std::string{};
for (auto i = 0; i < static_cast<int>(messages.size()); ++i) {
messagesDiv += "<h4>" + messages.at(i).name + " says:</hr><pre>" + messages.at(i).text + "</pre>";
if (role == AccessRole::Admin)
messagesDiv += R"(<form action="/delete/)" + std::to_string(i) +
R"(" method="post"> <input value="Delete" type="submit"></form>)";
}
return messagesDiv;
}
std::string makeLinksDiv(AccessRole role)
{
return (role == AccessRole::Admin ? R"(<a href="/logout">logout</a> )"s
: R"(<a href="/login">login</a> )"s) +
R"(<a href="https://github.com/kamchatka-volcano/asyncgi/blob/master/examples/example_guestbook.cpp">source</a></div>)"s;
}
auto showGuestBookPage(GuestBookState& state)
{
return [&state](const asyncgi::Request& request, RouteContext& context) -> http::Response
{
auto page = R"(<head><link rel="stylesheet" href="https://cdn.simplecss.org/simple.min.css"></head>
<div style="display:flex; flex-direction: row; justify-content: flex-end">%LINKS%</div>
<div style="display:flex; flex-direction: column; height: calc(100vh - 5em);">
<h1>asyncgi guest book</h1>
<div style="flex: 1; min-height: 0; display:flex; flex-direction: column;">
<div style="flex:1; overflow-y: auto; row-gap: 60px">
%MESSAGES%
</div>
</div>
<hr>
<div>
%ERROR_MSG%
<form style="display:flex; flex-direction: column; width:66%;" method="post" enctype="multipart/form-data">
<label for="name">Name:</label>
<input id="name" name="name" style="width:50%">
<textarea style="min-height:5em; resize:none;" name="msg" id="msg" maxlength="4192" autocomplete="off"></textarea>
<div style="display:flex; flex-direction: row; justify-content: flex-end">
<input value="Submit" data-popup="true" type="submit" style="width:33%"></td>
</div>
</form>
</div>
</div>)"s;
page = std::regex_replace(page, std::regex{"%MESSAGES%"}, makeMessagesDiv(state.messages(), context.role));
page = std::regex_replace(page, std::regex{"%LINKS%"}, makeLinksDiv(context.role));
if (request.hasQuery("error")) {
if (request.query("error") == "urls_in_msg")
page = std::regex_replace(page, std::regex{"%ERROR_MSG%"}, "<mark>Messages can't contain urls</mark>");
if (request.query("error") == "empty_msg")
page = std::regex_replace(page, std::regex{"%ERROR_MSG%"}, "<mark>Messages can't be empty</mark>");
}
else
page = std::regex_replace(page, std::regex{"%ERROR_MSG%"}, "");
return page;
};
}
auto addMessage(GuestBookState& state)
{
return [&state](const asyncgi::Request& request) -> http::Response
{
if (std::all_of(
request.formField("msg").begin(),
request.formField("msg").end(),
[](char ch)
{
return std::isspace(static_cast<unsigned char>(ch));
}))
return http::Redirect{"/?error=empty_msg"};
else if (
request.formField("msg").find("http://") != std::string_view::npos ||
request.formField("msg").find("https://") != std::string_view::npos)
return http::Redirect{"/?error=urls_in_msg"};
else {
state.addMessage(std::string{request.formField("name")}, std::string{request.formField("msg")});
return http::Redirect{"/"};
}
};
}
auto removeMessage(GuestBookState& state)
{
return [&state](int index, const asyncgi::Request&) -> http::Response
{
state.removeMessage(index);
return http::Redirect{"/"};
};
}
int main()
{
auto io = asyncgi::IO{4};
auto state = GuestBookState{};
auto router = asyncgi::Router<RouteContext>{io};
router.route(asyncgi::rx{".*"}).process(authorizeAdmin);
router.route("/", http::RequestMethod::Get).process(showGuestBookPage(state));
router.route("/", http::RequestMethod::Post).process(addMessage(state));
router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post, AccessRole::Admin)
.process(removeMessage(state));
router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post, AccessRole::Guest)
.set(http::ResponseStatus::_401_Unauthorized);
router.route("/login", http::RequestMethod::Get, AccessRole::Guest).process(showLoginPage);
router.route("/login", http::RequestMethod::Post, AccessRole::Guest).process(loginAdmin);
router.route("/logout").process(logoutAdmin);
router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
The live demo can be accessed here.
A timer object asyncgi::Timer
can be created to change or check some state periodically.
Example
///examples/example_timer.cpp
///
#include <asyncgi/asyncgi.h>
namespace http = asyncgi::http;
struct Greeter{
Greeter(const int& secondsCounter)
: secondsCounter_{&secondsCounter}
{
}
http::Response operator()(const asyncgi::Request&)
{
return "Hello world\n(alive for " + std::to_string(*secondsCounter_) + " seconds)";
}
private:
const int* secondsCounter_;
};
int main()
{
auto io = asyncgi::IO{};
int secondsCounter = 0;
auto timer = asyncgi::Timer{io};
timer.startPeriodic(
std::chrono::seconds(1),
[&secondsCounter]()
{
++secondsCounter;
});
auto router = asyncgi::Router{io};
router.route("/").process<Greeter>(secondsCounter);
router.route().set(http::ResponseStatus::_404_Not_Found);
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
The asyncgi::Timer::waitFuture
method can accept an std::future
object and invoke a provided callable object with
its result when the future object becomes ready. This function does not block while waiting and uses an internal timer
to periodically check the state of the future. To use it during request processing, a timer object created from
an asyncgi::Responder
reference must be used. It is important to avoid using this timer after the response has already
been sent.
Example
///examples/response_wait_future.cpp
///
#include <asyncgi/asyncgi.h>
#include <thread>
using namespace asyncgi;
struct DelayedPage{
void operator()(const asyncgi::Request&, asyncgi::Responder& responder)
{
auto timer = asyncgi::Timer{responder};
timer.waitFuture(
std::async(
std::launch::async,
[]
{
std::this_thread::sleep_for(std::chrono::seconds(3));
return "world";
}),
[responder](const std::string& result) mutable
{
responder.send(http::Response{"Hello " + result});
});
}
};
int main()
{
auto io = asyncgi::IO{};
auto router = asyncgi::Router{io};
auto delayedPage = DelayedPage{};
router.route("/", http::RequestMethod::Get).process(delayedPage);
router.route().set(http::ResponseStatus::_404_Not_Found);
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
With asyncgi::Client
it's possible to make direct requests to FastCGI
applications. This
enables multiple asyncgi
-based applications to communicate with each other without the need for other inter-process
communication solutions.
Example
///examples/example_client.cpp
///
#include <asyncgi/asyncgi.h>
#include <iostream>
using namespace asyncgi;
int main()
{
auto io = asyncgi::IO{};
auto client = asyncgi::Client{io};
client.makeRequest(
"/tmp/fcgi.sock",
http::Request{http::RequestMethod::Get, "/"},
[&io](const std::optional<http::ResponseView>& response)
{
if (response)
std::cout << response->body() << std::endl;
else
std::cout << "No response" << std::endl;
io.stop();
});
io.run();
}
To make FastCGI requests during request processing, a client object created from an asyncgi::Responder
reference must
be used. It is important to avoid using this client object after the response has already been sent.
Example
///examples/example_client_in_processor.cpp
///
#include <asyncgi/asyncgi.h>
namespace http = asyncgi::http;
struct RequestPage{
void operator()(const asyncgi::Request&, asyncgi::Responder& responder)
{
// making request to FastCgi application listening on /tmp/fcgi.sock and showing the received response
auto client = asyncgi::Client{responder};
client.makeRequest(
"/tmp/fcgi.sock",
http::Request{http::RequestMethod::Get, "/"},
[responder](const std::optional<http::ResponseView>& reqResponse) mutable
{
if (reqResponse)
responder.send(std::string{reqResponse->body()});
else
responder.send("No response");
});
}
};
int main()
{
auto io = asyncgi::IO{};
auto router = asyncgi::Router{io};
router.route("/", http::RequestMethod::Get).process<RequestPage>();
router.route().set(http::ResponseStatus::_404_Not_Found);
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi_client.sock");
io.run();
}
asyncgi
internally uses the asio
library. A dispatcher object asyncgi::AsioDispatcher
can be created to invoke
callable objects that require access to the asio::io_context
object.
Example
///examples/example_asio_dispatcher.cpp
///
#include <asyncgi/asyncgi.h>
#include <asio/steady_timer.hpp>
#include <iostream>
int main()
{
auto io = asyncgi::IO{};
auto disp = asyncgi::AsioDispatcher{io};
disp.postTask(
[&io](const asyncgi::TaskContext& ctx) mutable
{
auto timer = std::make_shared<asio::steady_timer>(ctx.io());
timer->expires_after(std::chrono::seconds{3});
timer->async_wait(
[timer, ctx, &io](auto&) mutable
{
std::cout << "Hello world" << std::endl;
io.stop();
});
});
io.run();
}
To invoke such a callable object during request processing, a dispatcher created from an asyncgi::Responder
reference
must be used. It is important to avoid using this dispatcher after the response has already been sent.
Example
///examples/example_response_dispatching_asio_task.cpp
///
#include <asyncgi/asyncgi.h>
#include <asio/steady_timer.hpp>
namespace http = asyncgi::http;
struct DelayedPage {
void operator()(const asyncgi::Request&, asyncgi::Responder& responder)
{
auto disp = asyncgi::AsioDispatcher{responder};
disp.postTask(
[responder](const asyncgi::TaskContext& ctx) mutable
{
auto timer = std::make_shared<asio::steady_timer>(ctx.io());
timer->expires_after(std::chrono::seconds{3});
timer->async_wait([timer, responder, ctx](auto&) mutable { // Note how we capture ctx object here,
responder.send("Hello world"); // it's necessary to keep it (or its copy) alive
}); // before the end of request processing
});
}
};
int main()
{
auto io = asyncgi::IO{};
auto router = asyncgi::Router{io};
router.route("/", http::RequestMethod::Get).process<DelayedPage>();
router.route().set(http::ResponseStatus::_404_Not_Found);
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
To use asyncgi
with the Boost.Asio
library, set the ASYNCGI_USE_BOOST_ASIO
CMake variable .
asyncgi
is currently in the open beta stage, with all planned features complete. Until it reaches a non-zero major
version number, there may be frequent introductions of backward-incompatible changes.
Unit tests are not included because most of the functionality in asyncgi
is derived from the following libraries,
which already have their own test coverage:
- asio - used for establishing connections, sending and receiving data.
- fcgi_responder - implementation of the
FastCGI
protocol. - whaleroute - implementation of the request router.
- hot_teacup - parsing of HTTP data received over
FastCGI
connections, forming HTTP responses.
Instead of mocking code that integrates the functionality of these libraries, asyncgi
is tested using functional
tests. These tests check the behavior of the executables found in the examples/
directory when running with the NGINX
server. You can find these tests in the functional_tests/
directory.
Download and link the library from your project's CMakeLists.txt:
cmake_minimum_required(VERSION 3.14)
include(FetchContent)
FetchContent_Declare(cmdlime
GIT_REPOSITORY "https://github.com/kamchatka-volcano/asyncgi.git"
GIT_TAG "origin/master"
)
#uncomment if you need to install cmdlime with your target
#set(INSTALL_ASYNCGI ON)
FetchContent_MakeAvailable(asyncgi)
add_executable(${PROJECT_NAME})
target_link_libraries(${PROJECT_NAME} PRIVATE asyncgi::asyncgi)
To install the library system-wide, use the following commands:
git clone https://github.com/kamchatka-volcano/asyncgi.git
cd asyncgi
cmake -S . -B build
cmake --build build
cmake --install build
After installation, you can use the find_package() command to make the installed library available inside your project:
find_package(asyncgi 0.1.0 REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE asyncgi::asyncgi)
If you want to use the Boost.Asio
library, Boost
dependencies can be resolved
using vcpkg by running the build with this command:
cmake -S . -B build -DASYNCGI_USE_BOOST_ASIO=ON -DCMAKE_TOOLCHAIN_FILE=<vcpkg path>/scripts/buildsystems/vcpkg.cmake
cd asyncgi
cmake -S . -B build -DENABLE_EXAMPLES=ON
cmake --build build
cd build/examples
Download lunchtoast
executable, build asyncgi
examples
and start NGINX with functional_tests/nginx_*.conf
config file.
Launch tests with the following command:
- Linux command:
lunchtoast functional_tests
- Windows command:
lunchtoast.exe functional_tests -shell="msys2 -c" -skip=linux
To run functional tests on Windows, it's recommended to use the bash shell from the msys2
project. After installing
it, add the following script msys2.cmd
to your system PATH
:
@echo off
setlocal
IF NOT DEFINED MSYS2_PATH_TYPE set MSYS2_PATH_TYPE=inherit
set CHERE_INVOKING=1
C:\\msys64\\usr\\bin\\bash.exe -leo pipefail %*
asyncgi is licensed under the MS-PL license