Ecosyste.ms: Awesome
An open API service indexing awesome lists of open source software.
https://github.com/kamchatka-volcano/asyncgi
An asynchronous FastCGI web microframework for C++
https://github.com/kamchatka-volcano/asyncgi
asio async asynchronous cpp17 fastcgi microframework webframework
Last synced: about 1 month ago
JSON representation
An asynchronous FastCGI web microframework for C++
- Host: GitHub
- URL: https://github.com/kamchatka-volcano/asyncgi
- Owner: kamchatka-volcano
- License: ms-pl
- Created: 2022-05-21T11:52:14.000Z (over 2 years ago)
- Default Branch: master
- Last Pushed: 2024-08-24T14:12:17.000Z (4 months ago)
- Last Synced: 2024-10-30T02:03:28.906Z (about 2 months ago)
- Topics: asio, async, asynchronous, cpp17, fastcgi, microframework, webframework
- Language: C++
- Homepage:
- Size: 270 KB
- Stars: 36
- Watchers: 3
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE.md
Awesome Lists containing this project
README
[![build & test (clang, gcc, MSVC)](https://github.com/kamchatka-volcano/asyncgi/actions/workflows/build_and_test.yml/badge.svg?branch=master)](https://github.com/kamchatka-volcano/asyncgi/actions/workflows/build_and_test.yml)
**asyncgi** - is a C++17 asynchronous microframework for creating web applications interfacing with any HTTP server
supporting [FastCGI](https://en.wikipedia.org/wiki/FastCGI) protocol. It aims to provide a modern way of
using [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface), with a custom performant FastCGI implementation in
C++, multithreading support and a clean and simple API:```c++
#includenamespace 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();
}
```## Table of Contents
* [Usage](#usage)
* [Connection](#connection)
* [Request processor](#request-processor)
* [Router](#router)
* [Route parameters](#route-parameters)
* [Route context](#route-context)
* [Route matchers](#route-matchers)
* [Complete guest book example](#complete-guest-book-example)
* [Timer](#timer)
* [Client](#client)
* [Executing an asio task](#executing-an-asio-task)
* [Showcase](#showcase)
* [Development status](#development-status)
* [Installation](#installation)
* [Building examples](#building-examples)
* [Running functional tests](#running-functional-tests)
* [License](#license)## Usage
### Connection
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.
### Request processor
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
```c++
///examples/example_request_processor.cpp
///
#includenamespace http = asyncgi::http;
http::Response guestBookPage(const asyncgi::Request& request)
{
if (request.path() == "/")
return {R"(
Guest book
No messages
)"};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:```c++
void guestBookPage(const asyncgi::Request& request, asyncgi::Responder& responder)
{
if (request.path() == "/")
responder.send(R"(
Guest book
No messages
)");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.### Router
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
```c++
///examples/example_router.cpp
///
#include
#includenamespace http = asyncgi::http;
using namespace std::string_literals;class GuestBookState {
public:
std::vector 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 messages_;
std::mutex mutex_;
};class GuestBookPage {
public:
GuestBookPage(GuestBookState& state)
: state_(&state)
{
}http::Response operator()(const asyncgi::Request&)
{
auto messages = state_->messages();
auto page = "Guest book
"s;
if (messages.empty())
page += "No messages
";
else
for (const auto& msg : messages)
page += "" + msg + "
";page += "
";
page += ""
"Message:"
""
""
"";
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(state);
router.route("/", http::RequestMethod::Post).process(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();
}
```### Route parameters
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
```c++
///examples/example_route_params.cpp
///
#include
#includeusing namespace asyncgi;
using namespace std::string_literals;class GuestBookState {
public:
std::vector 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(messages_.size()))
return;
messages_.erase(std::next(messages_.begin(), index));
}private:
std::vector messages_;
std::mutex mutex_;
};std::string makeMessage(int index, const std::string& msg)
{
return msg + R"( )";
}class GuestBookPage {
public:
explicit GuestBookPage(GuestBookState& state)
: state_{&state}
{
}http::Response operator()(const asyncgi::Request&)
{
auto messages = state_->messages();
auto page = "Guest book
"s;
if (messages.empty())
page += "No messages
";
else
for (auto i = 0; i < static_cast(messages.size()); ++i)
page += "" + makeMessage(i, messages.at(i)) + "
";page += "
";
page += ""
"Message:"
""
""
"";
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(state);
router.route("/", http::RequestMethod::Post).process(state);
router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post).process(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
```c++
///examples/example_route_params_user_defined_types.cpp
///
#include
#includeusing namespace asyncgi;
using namespace std::string_literals;struct MessageNumber {
int value;
};template<>
struct asyncgi::config::StringConverter {
static std::optional fromString(const std::string& data)
{
return MessageNumber{std::stoi(data)};
}
};class GuestBookState {
public:
std::vector 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(messages_.size()))
return;
messages_.erase(std::next(messages_.begin(), index));
}private:
std::vector messages_;
std::mutex mutex_;
};std::string makeMessage(int index, const std::string& msg)
{
return msg + R"( )";
}class GuestBookPage {
public:
explicit GuestBookPage(GuestBookState& state)
: state_{&state}
{
}http::Response operator()(const asyncgi::Request&)
{
auto messages = state_->messages();
auto page = "Guest book
"s;
if (messages.empty())
page += "No messages
";
else
for (auto i = 0; i < static_cast(messages.size()); ++i)
page += "" + makeMessage(i, messages.at(i)) + "
";page += "
";
page += ""
"Message:"
""
""
"";
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(state);
router.route("/", http::RequestMethod::Post).process(state);
router.route(asyncgi::rx{"/delete/(.+)"}, http::RequestMethod::Post).process(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();
}
```### Route context
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` 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
```c++
///examples/example_route_context.cpp
///
#include
#include
#includenamespace http = asyncgi::http;
using namespace std::string_literals;enum class AccessRole {
Admin,
Guest
};struct RouteContext {
AccessRole role = AccessRole::Guest;
};struct AdminAuthorizer {
std::optional 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"(
Login:
Password:
)"};
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{io};
router.route(asyncgi::rx{".*"}).process();
router.route("/").process(
[](const asyncgi::Request&, asyncgi::Responder& response, RouteContext& context)
{
if (context.role == AccessRole::Admin)
response.send("Hello admin
");
else
response.send(R"(Hello guest
login)");
});router.route("/login", http::RequestMethod::Get).process();
router.route("/login", http::RequestMethod::Post).process();
router.route().set(http::ResponseStatus::_404_Not_Found, "Page not found");auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi.sock");
io.run();
}
```### Route matchers
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
```c++
///examples/example_route_matcher.cpp
///
#include
#include
#includenamespace http = asyncgi::http;
using namespace std::string_literals;enum class AccessRole {
Admin,
Guest
};struct RouteContext {
AccessRole role = AccessRole::Guest;
};struct AdminAuthorizer {
std::optional 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"(
Login:
Password:
)"};
}
};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 {
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{io};
router.route(asyncgi::rx{".*"}).process();
router.route("/").process(
[](const asyncgi::Request&, RouteContext& context) -> http::Response
{
if (context.role == AccessRole::Admin)
return {"Hello admin
"};
else
return {R"(Hello guest
login)"};
});router.route("/login", http::RequestMethod::Get, AccessRole::Guest).process();
router.route("/login", http::RequestMethod::Post, AccessRole::Guest).process();
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();
}
```### Complete guest book example
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
```c++
///examples/example_guestbook.cpp
///
#include
#include
#include
#includenamespace http = asyncgi::http;
using namespace std::string_literals;enum class AccessRole {
Admin,
Guest
};struct RouteContext {
AccessRole role = AccessRole::Guest;
};template<>
struct asyncgi::config::RouteMatcher {
bool operator()(AccessRole value, const asyncgi::Request&, const RouteContext& context) const
{
return value == context.role;
}
};std::optional 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"(
Login:
Password:
)"};
}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 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(messages_.size()))
return;
messages_.erase(std::next(messages_.begin(), index));
}private:
std::vector messages_;
std::mutex mutex_;
};std::string makeMessagesDiv(const std::vector& messages, AccessRole role)
{
if (messages.empty())
return "No messages";auto messagesDiv = std::string{};
for (auto i = 0; i < static_cast(messages.size()); ++i) {
messagesDiv += "" + messages.at(i).name + " says:
)";" + messages.at(i).text + "";
if (role == AccessRole::Admin)
messagesDiv += R"(
}
return messagesDiv;
}std::string makeLinksDiv(AccessRole role)
{
return (role == AccessRole::Admin ? R"(logout )"s
: R"(login )"s) +
R"(source)"s;
}auto showGuestBookPage(GuestBookState& state)
{
return [&state](const asyncgi::Request& request, RouteContext& context) -> http::Response
{
auto page = R"(
%LINKS%
)"s;
asyncgi guest book
%MESSAGES%
%ERROR_MSG%
Name:
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%"}, "Messages can't contain urls");
if (request.query("error") == "empty_msg")
page = std::regex_replace(page, std::regex{"%ERROR_MSG%"}, "Messages can't be empty");
}
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(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{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](https://asyncgi-guestbook.eelnet.org).
### Timer
A timer object `asyncgi::Timer` can be created to change or check some state periodically.
Example
```c++
///examples/example_timer.cpp
///
#includenamespace 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(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
```c++
///examples/response_wait_future.cpp
///
#include
#includeusing 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();
}
```### Client
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
```c++
///examples/example_client.cpp
///
#include
#includeusing 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& 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
```c++
///examples/example_client_in_processor.cpp
///
#includenamespace 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& 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();
router.route().set(http::ResponseStatus::_404_Not_Found);
auto server = asyncgi::Server{io, router};
server.listen("/tmp/fcgi_client.sock");
io.run();
}
```### Executing an asio task
`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
```c++
///examples/example_asio_dispatcher.cpp
///
#include
#include
#includeint main()
{
auto io = asyncgi::IO{};
auto disp = asyncgi::AsioDispatcher{io};
disp.postTask(
[&io](const asyncgi::TaskContext& ctx) mutable
{
auto timer = std::make_shared(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
```c++
///examples/example_response_dispatching_asio_task.cpp
///
#include
#includenamespace 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(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();
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 .
## Showcase
* [`stone_skipper`](https://github.com/kamchatka-volcano/stone_skipper)
## Development status
`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](https://github.com/chriskohlhoff/asio) - used for establishing connections, sending and receiving data.
* [fcgi_responder](https://github.com/kamchatka-volcano/fcgi_responder/) - implementation of the `FastCGI` protocol.
* [whaleroute](https://github.com/kamchatka-volcano/whaleroute/) - implementation of the request router.
* [hot_teacup](https://github.com/kamchatka-volcano/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.## Installation
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](https://vcpkg.io/en/getting-started.html) by running the build with this command:```
cmake -S . -B build -DASYNCGI_USE_BOOST_ASIO=ON -DCMAKE_TOOLCHAIN_FILE=/scripts/buildsystems/vcpkg.cmake
```## Building examples
```
cd asyncgi
cmake -S . -B build -DENABLE_EXAMPLES=ON
cmake --build build
cd build/examples
```## Running functional tests
Download [`lunchtoast`](https://github.com/kamchatka-volcano/lunchtoast/releases) 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`:```bat
@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 %*
```## License
**asyncgi** is licensed under the [MS-PL license](/LICENSE.md)