Last active
June 15, 2021 03:14
-
-
Save t2ym/dd593d48fe4cd3ea6dd7eb433222502f to your computer and use it in GitHub Desktop.
sample for nicexprs.h - based on nghttp2/examples/asio-sv.cc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* nghttp2 - HTTP/2 C Library | |
* | |
* Copyright (c) 2014 Tatsuhiro Tsujikawa | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining | |
* a copy of this software and associated documentation files (the | |
* "Software"), to deal in the Software without restriction, including | |
* without limitation the rights to use, copy, modify, merge, publish, | |
* distribute, sublicense, and/or sell copies of the Software, and to | |
* permit persons to whom the Software is furnished to do so, subject to | |
* the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be | |
* included in all copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, | |
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF | |
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND | |
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE | |
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION | |
* OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION | |
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
// We wrote this code based on the original code which has the | |
// following license: | |
// | |
// main.cpp | |
// ~~~~~~~~ | |
// | |
// Copyright (c) 2003-2013 Christopher M. Kohlhoff (chris at kohlhoff dot com) | |
// | |
// Distributed under the Boost Software License, Version 1.0. (See accompanying | |
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) | |
// | |
#include <iostream> | |
#include <string> | |
#include <future> | |
#include <locale> | |
#include <boost/stacktrace.hpp> | |
#include <boost/algorithm/string.hpp> | |
#include <nlohmann/json.hpp> | |
#include <openssl/sha.h> | |
#include <boost/archive/iterators/base64_from_binary.hpp> | |
#include <boost/archive/iterators/transform_width.hpp> | |
#include <boost/date_time/posix_time/posix_time.hpp> | |
#include <boost/xpressive/xpressive.hpp> | |
#include <nghttp2/asio_http2_server.h> | |
#include "inja.hpp" // Install https://github.com/pantor/inja by curl -L -O https://raw.githubusercontent.com/pantor/inja/master/single_include/inja/inja.hpp | |
#include "compile_time_truncation.h" // Install rearranged version of https://davidgorski.ca/posts/truncate-string-whitespace-compiletime-cpp/ by curl -L -O https://gist.githubusercontent.com/t2ym/b4349f1f8845e11499a39a562b1384ba/raw/699316c9350b9552e67d1fb4f2dc10f4622f33c0/compile_time_truncation.h | |
#define PUSH_LOG 0 | |
#define INSTANTIATION_LOG 0 | |
#define MATCHING_LOG 0 | |
#define DUMP_MIDDLEWARE_ITEM 1 | |
#define STREAMING_SUPPORT 1 // support streaming filters in nicexprs.h; With this disabled, nicexprs.h is almost the same as version 0.0.9 | |
#if STREAMING_SUPPORT | |
#define GENERATOR_STREAM 1 // enable std::iostream adaptor for generator_cb in nicexprs.h | |
#if GENERATOR_STREAM | |
#define BOOST_FILTER 0 // enable boost::iostreams::filtering_streambuf<output> adaptor in nicexprs.h | |
#define GZIP_FILTER 0 // use boost::iostreams::gzip_compressor in asio-sv.cc; not in nicexprs.h | |
#define UNICODE_UPPERCASE 0 // use libicu for uppercasing in asio-sv.cc; not in nicexprs.h | |
#if !UNICODE_UPPERCASE | |
#define BOOST_UPPERCASE 0 // use boost::algorithm::to_upper for uppercasing in asio-sv.cc; not in nicexprs.h | |
#endif | |
#endif | |
#define STREAMING_LOG 0 // enable verbose logging to std::cerr for streaming | |
#if STREAMING_LOG | |
#define STREAMING_DUMP 1 // enable dumping streaming data to std::cerr | |
#endif | |
#endif | |
#include "nicexprs.h" | |
#if UNICODE_UPPERCASE | |
// link with -licuio -licuuc -licui18n -licudata | |
#include <unicode/unistr.h> | |
#include <unicode/ustream.h> | |
#include <unicode/locid.h> | |
#endif | |
#if BOOST_FILTER | |
#include <boost/iostreams/filtering_streambuf.hpp> | |
#if GZIP_FILTER | |
// link with -lboost_iostreams -lz -lbz2 | |
#include <boost/iostreams/filter/gzip.hpp> | |
#endif | |
#endif | |
using json = nlohmann::json; | |
// ./configure --enable-app --without-libxml2 --enable-asio-lib --enable-examples CC=clang CXX=clang++ | |
//using namespace nghttp2::asio_http2; | |
//using namespace nghttp2::asio_http2::server; | |
using namespace nicexprs; | |
class CanaryObject { | |
public: | |
CanaryObject(std::string name_): name(name_) { | |
//std::cerr << "CanaryObject(" << name_ << ") constructed" << std::endl; | |
} | |
~CanaryObject() { | |
//std::cerr << "CanaryObject(" << name << ") destructed" << std::endl; | |
} | |
std::string name; | |
}; | |
generator_cb string_generator(std::string data) { | |
auto strio = std::make_shared<std::pair<std::string, size_t>>(std::move(data), | |
data.size()); | |
auto canary = std::make_shared<CanaryObject>(data); | |
return [strio, canary](uint8_t *buf, size_t len, uint32_t *data_flags) { | |
auto &data = strio->first; | |
auto &left = strio->second; | |
auto n = std::min(len, left); | |
std::copy_n(data.c_str() + data.size() - left, n, buf); | |
left -= n; | |
if (left == 0) { | |
*data_flags |= NGHTTP2_DATA_FLAG_EOF; | |
} | |
return n; | |
}; | |
} | |
std::shared_ptr<http2> server_ptr_for_signal_handler; | |
void signal_handler(int signum) { | |
std::cerr << "Interrupt signal (" << signum << ") received." << std::endl; | |
if (server_ptr_for_signal_handler) { | |
server_ptr_for_signal_handler->stop(); | |
} | |
} | |
// sample custom helper | |
// Note: The point here is how to store and manipulate data per request with a custom helper. Not the awkward url parser. | |
class custom_helper: public helper_base { | |
public: | |
std::shared_ptr<json> req_json; | |
std::shared_ptr<std::map<std::string, std::string>> search_parameters; | |
std::shared_ptr<std::map<std::string, std::string>> route_parameters; | |
json res_data; | |
std::string url_decode(const std::string &str) { // not optimized at all | |
// TODO: use a robust and tested url parser library | |
using namespace boost::xpressive; // I just want to avoid raw pointers here | |
sregex re = (s1=*(~(set='+','%'))) >> !((s4='+')|('%' >> (s2=repeat<2,2>(xdigit)))) >> !(s3 = *_); // regex compiled at build time | |
smatch what; | |
std::ostringstream oss; | |
auto it = str.cbegin(); | |
auto it_end = str.cend(); | |
while (regex_match(it, it_end, what, re)) { | |
if (what.position(1) >= 0) { | |
oss << what[1]; | |
} | |
if (what.position(2) >= 0) { | |
oss << (uint8_t)std::stoi(what[2].str(), nullptr, 16); | |
} | |
else if (what.position(4) >= 0) { | |
oss << " "; | |
} | |
if (what.position(3) >= 1) { | |
std::advance(it, what.position(3)); | |
} | |
else { | |
break; | |
} | |
} | |
return oss.str(); | |
} | |
void parse_search_parameters(request &req) { // not optimized at all | |
if (req.uri().raw_query.size() == 0) { | |
return; | |
} | |
if (!search_parameters) { | |
search_parameters = std::make_shared<std::map<std::string, std::string>>(); | |
} | |
// TODO: use a robust and tested url parser library | |
using namespace boost::xpressive; // I just want to avoid raw pointers here | |
std::string &raw = req.uri().raw_query; | |
sregex re = (s1=+(~(set='&','='))) >> +('=' >> (s2=*(~(set='&')))) >> !('&' >> (s3 = *_)); // regex compiled at build time | |
smatch what; | |
auto it = raw.cbegin(); | |
auto it_end = raw.cend(); | |
while (regex_match(it, it_end, what, re)) { | |
search_parameters->emplace(url_decode(what[1].str()), url_decode(what[2].str())); | |
if (what.position(3) < 0) { | |
break; | |
} | |
std::advance(it, what.position(3)); | |
} | |
} | |
// parse parameterized prefix /prefix/route/:param1/:param2/... and put them into route_parameters | |
// Note: No support for crafted parameters like /prefix/route/xxx-:param1-def/yyy-:param2. Only simple parameters surrounded by /: and /(, or EOL) | |
std::size_t parse_route_parameters(request &req) { | |
middleware_index current_item = req.controller->it->index; | |
middleware_index item_index = current_item; | |
std::vector<middleware_item> &request_filters = *(req.controller->request_filters); | |
middleware_index size = request_filters.size(); | |
while (request_filters[item_index].r_type != route_type::PARAMETERIZED_PREFIX) { | |
item_index = request_filters[item_index].parent_index; | |
if (item_index >= size) { | |
return 0; // no parameterized prefix found | |
} | |
} | |
const middleware_item &item = request_filters[item_index]; | |
// item.r_type == route_type::PARAMETERIZED_PREFIX | |
// TODO: use a robust and tested route parser library | |
using namespace boost::xpressive; // I just want to avoid raw pointers here | |
const std::string &prefix_path = item.path; | |
const std::string &req_path = req.uri().path; | |
if (!(req_path.size() >= item.prefix.size() + item.prefix_length && | |
req_path.compare(0, item.prefix.size(), item.prefix, 0, item.prefix.size()) == 0 && | |
req_path.compare(item.prefix.size(), item.prefix_length, prefix_path, 0, item.prefix_length) == 0)) { | |
return 0; // prefix_path does not match | |
} | |
auto it = prefix_path.cbegin(); | |
auto it_end = prefix_path.cend(); | |
std::advance(it, item.prefix_length); // point /: | |
sregex re = as_xpr('/') >> ':' >> (s1=*(~(set='/'))) >> !(s2 = ('/' >> *_)); // regex compiled at build time | |
smatch what; | |
auto it_path = req_path.cbegin(); | |
auto it_path_end = req_path.cend(); | |
std::advance(it_path, item.prefix.size() + item.prefix_length); | |
sregex re_path = as_xpr('/') >> (s1=*(~(set='/'))) >> !(s2 = ('/' >> *_)); // regex compiled at build time | |
smatch what_path; | |
std::size_t matched_parameters = 0; | |
while (regex_match(it, it_end, what, re) && regex_match(it_path, it_path_end, what_path, re_path)) { | |
matched_parameters++; | |
if (!route_parameters) { | |
route_parameters = std::make_shared<std::map<std::string, std::string>>(); | |
} | |
route_parameters->emplace(what[1].str(), what_path[1].str()); | |
if (what.position(2) < 0 || what_path.position(2) < 0) { | |
break; // no more parameters | |
} | |
std::advance(it, what.position(2)); | |
std::advance(it_path, what_path.position(2)); | |
} | |
return matched_parameters; // return the number of matched parameters | |
} | |
// pseudo-backend with 100ms delay | |
std::function<void ()> pseudo_backend_connector(request &req, response &res, std::function<void (const boost::system::error_code &ec)> cb) { | |
auto timer = std::make_shared<boost::asio::deadline_timer>( | |
res.res.io_service(), boost::posix_time::milliseconds(100)); // pseudo 100ms delay | |
auto cancelled = std::make_shared<bool>(false); | |
auto on_cancel = [timer, cancelled](){ | |
timer->cancel(); | |
*cancelled = true; | |
}; | |
timer->async_wait([req, res, cb, cancelled](const boost::system::error_code &ec){ | |
if (*cancelled) { | |
return; | |
} | |
auto helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
if (helper && helper->route_parameters) { | |
for (const auto &pair : *helper->route_parameters) { | |
helper->res_data[pair.first] = "from_backend(" + pair.second + ")"; | |
} | |
} | |
cb(ec); | |
}); | |
return on_cancel; | |
} | |
// type of next(err) callback for threaded tasks to call req.next() in their original nghttp2 worker threads | |
typedef std::function<void (uint32_t err)> threaded_task_next_cb; | |
// threaded task helper | |
template<typename threaded_task_result_type> | |
void post_threaded_task( | |
request &req, | |
response &res, | |
boost::asio::thread_pool &pool, | |
std::function<void (std::shared_ptr<std::promise<threaded_task_result_type&>> promise_ptr, | |
threaded_task_result_type &result, | |
threaded_task_next_cb next)> task, | |
threaded_task_result_type &result) { | |
auto closed = std::make_shared<std::atomic_bool>(); | |
auto err_ptr = std::make_shared<std::atomic<uint32_t>>(); | |
auto promise_ptr = std::make_shared<std::promise<threaded_task_result_type&>>(); | |
auto future_ptr = std::make_shared<std::future<threaded_task_result_type&>>(promise_ptr->get_future()); | |
closed->store(false); | |
err_ptr->store(NGHTTP2_NO_ERROR); | |
res.on_close([closed](uint32_t error_code) { | |
closed->store(true); | |
}); | |
// req_next_caller() to be called from within the nghttp2 worker thread for the request | |
auto req_next_caller = [&req, future_ptr, closed, err_ptr](){ | |
if (closed->load()) { | |
return; | |
} | |
future_ptr->get(); // stored in result | |
req.next(err_ptr->load()); | |
}; | |
// next() to be called from a task in the thread pool | |
threaded_task_next_cb next = [&res, req_next_caller, err_ptr](uint32_t err = NGHTTP2_NO_ERROR) { | |
err_ptr->store(err); | |
// post req_next_caller to be called within the nghttp2 worker thread | |
boost::asio::post(res.res.io_service(), std::move(req_next_caller)); | |
}; | |
// task for the thread pool | |
auto threaded_task = [task, &result, promise_ptr, next, closed]() { | |
if (!closed->load()) { // atomic load | |
task(promise_ptr, result, next); | |
} | |
}; | |
// post the task to the thread pool | |
boost::asio::post(pool, threaded_task); | |
} | |
const std::string &detect_mime_type(const std::string &path) { | |
static const std::map<std::string, std::string> mime_types = { | |
{ ".htm", "text/html" }, | |
{ ".html", "text/html" }, | |
{ ".php", "text/html" }, | |
{ ".css", "text/css" }, | |
{ ".txt", "text/plain" }, | |
{ ".js", "application/javascript" }, | |
{ ".json", "application/json" }, | |
{ ".xml", "application/xml" }, | |
{ ".swf", "application/x-shockwave-flash" }, | |
{ ".flv", "video/x-flv" }, | |
{ ".png", "image/png" }, | |
{ ".jpe", "image/jpeg" }, | |
{ ".jpeg", "image/jpeg" }, | |
{ ".jpg", "image/jpeg" }, | |
{ ".gif", "image/gif" }, | |
{ ".bmp", "image/bmp" }, | |
{ ".ico", "image/vnd.microsoft.icon" }, | |
{ ".tiff", "image/tiff" }, | |
{ ".tif", "image/tiff" }, | |
{ ".svg", "image/svg+xml" }, | |
{ ".svgz", "image/svg+xml" } | |
}; | |
static const std::string default_type = "text/plain"; | |
std::size_t idx = path.rfind("."); | |
if (idx != std::string::npos) { | |
auto it = mime_types.find(path.substr(idx)); | |
if (it != mime_types.cend()) { | |
return it->second; | |
} | |
} | |
return default_type; | |
} | |
}; | |
int main(int argc, char *argv[]) { | |
//std::cerr << "boost::stacktrack::stacktrace()" << std::endl << boost::stacktrace::stacktrace() << std::endl; | |
//return 0; | |
try { | |
// Check command line arguments. | |
if (argc < 4) { | |
std::cerr | |
<< "Usage: asio-sv <address> <port> <threads> [<private-key-file> " | |
<< "<cert-file>] [<docroot>]\n"; | |
return 1; | |
} | |
boost::system::error_code ec; | |
std::string addr = argv[1]; | |
std::string port = argv[2]; | |
std::size_t num_threads = std::stoi(argv[3]); | |
auto server_ptr = std::make_shared<http2>(); | |
server_ptr_for_signal_handler = server_ptr; | |
http2 &server = *server_ptr; | |
signal(SIGINT, signal_handler); | |
signal(SIGTERM, signal_handler); | |
signal(SIGKILL, signal_handler); | |
server.num_threads(num_threads); | |
// custom_helper class has moved outside of the main function to allow template member functions | |
middleware_cb raw_body = [](request &req, response &res){ | |
req.helper = std::make_shared<custom_helper>(); // initialize custom_helper object | |
auto method = req.method(); | |
if (method == "POST" || method == "PUT") { | |
auto body = std::make_shared<std::ostringstream>(); | |
req.on_data([&req, body](const uint8_t *data, std::size_t len){ | |
if (len == 0) { // EOF | |
req.body = body->str(); | |
req.next(); | |
} | |
else { | |
body->write(reinterpret_cast<const char *>(data), len); | |
} | |
}); | |
} | |
else { | |
req.next(); | |
} | |
}; | |
middleware_cb body_parser = [](request &req, response &res) { | |
if (req.helper) { | |
auto it = req.header().find("content-type"); | |
if (it != req.header().end()) { | |
if (it->second.value == "application/json") { | |
try { | |
auto helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
helper->req_json = std::make_shared<json>(std::move(json::parse(req.body))); | |
} | |
catch (std::exception &e) { | |
std::cerr << "body_parser: exception: " << e.what() << std::endl; | |
} | |
} | |
} | |
} | |
req.next(); | |
}; | |
middleware_cb url_parser = [](request &req, response &res) { | |
if (req.helper) { | |
auto helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
helper->parse_search_parameters(req); | |
} | |
req.next(); | |
}; | |
middleware_cb route_parser = [](request &req, response &res) { | |
if (req.helper) { | |
auto helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
helper->parse_route_parameters(req); | |
} | |
req.next(); | |
}; | |
middleware_cb uppercase_middleware = [](request &req, response &res){ | |
//boost::to_upper(req.body); // locale-aware feature-rich uppercasing | |
std::transform(req.body.cbegin(), req.body.cend(), req.body.begin(), ::toupper); // ASCII uppercasing | |
req.header().emplace("x-uppercase", header_value{ std::string("UPPERCASED") }); | |
res.on_push([](response &res, std::string &method, std::string &raw_path_query, header_map &header){ | |
//boost::to_upper(raw_path_query); | |
std::transform(raw_path_query.cbegin(), raw_path_query.cend(), raw_path_query.begin(), ::toupper); | |
header.emplace("x-uppercase", header_value{ std::string("UPPERCASED") }); | |
}); | |
res.on_response([](response &res){ | |
//boost::to_upper(res.body); | |
std::transform(res.body.cbegin(), res.body.cend(), res.body.begin(), ::toupper); | |
res.header().emplace("x-uppercase", header_value{ std::string("UPPERCASED") }); | |
auto it = res.header().find("content-length"); | |
auto value = std::to_string(res.body.size()); | |
if (it != res.header().end()) { | |
it->second.value = std::move(value); | |
} | |
else { | |
res.header().emplace("content-length", header_value{ std::move(value), false }); | |
} | |
res.next(); | |
}); | |
req.next(); | |
}; | |
middleware_cb hello_handler = [](request &req, response &res){ | |
res.write_head(200, {{"foo", {"bar"}}}); | |
res.end("hello, world\n"); | |
}; | |
middleware_cb post_handler = [](request &req, response &res){ | |
if (req.method() == "POST") { | |
std::stringstream response_body; | |
res.on_close([](uint32_t err){ | |
//std::cerr << "on_close" << std::endl; | |
}); | |
json res_body; | |
res_body["body-length"] = req.body.length(); | |
bool body_value_set = false; | |
if (req.helper) { | |
auto helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
if (helper->req_json) { | |
res_body["body"] = *helper->req_json; | |
body_value_set = true; | |
} | |
} | |
if (!body_value_set) { | |
res_body["body"] = std::move(json()); // null | |
} | |
response_body << res_body.dump(2); | |
auto data = response_body.str(); | |
res.write_head(200, {{"content-type", {"application/json"}}, {"content-length", { std::to_string(data.size()) }}}); | |
res.end(string_generator(data)); | |
//res.end(data); | |
} | |
else { | |
req.next(); | |
} | |
}; | |
middleware_cb get_params_handler = [](request &req, response &res){ | |
std::stringstream response_body; | |
json res_body; | |
if (req.helper) { | |
auto helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
if (helper->search_parameters) { | |
for (const auto &pair : *helper->search_parameters) { | |
res_body[pair.first] = pair.second; | |
} | |
} | |
} | |
response_body << res_body.dump(2); | |
auto data = response_body.str(); | |
res.write_head(200, {{"content-type", {"application/json"}}, {"content-length", { std::to_string(data.size()) }}}); | |
res.end(data); | |
}; | |
middleware_cb route_params_handler = [](request &req, response &res){ | |
std::stringstream response_body; | |
json res_body; | |
bool body_value_set = false; | |
if (req.helper) { | |
auto helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
if (helper->route_parameters) { | |
for (const auto &pair : *helper->route_parameters) { | |
res_body[pair.first] = pair.second; | |
} | |
} | |
} | |
response_body << res_body.dump(2); | |
auto data = response_body.str(); | |
res.write_head(200, {{"content-type", {"application/json"}}, {"content-length", { std::to_string(data.size()) }}}); | |
res.end(data); | |
}; | |
middleware_cb backend_handler = [](request &req, response &res){ | |
if (req.helper) { | |
auto helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
auto backend_cb = [&req, &res, helper](const boost::system::error_code &ec)->void { | |
if (!ec) { | |
req.next(); | |
} | |
else { | |
req.next(NGHTTP2_INTERNAL_ERROR); | |
} | |
}; | |
auto cancel_cb = helper->pseudo_backend_connector(req, res, backend_cb); | |
res.on_close([cancel_cb](uint32_t error_code) { | |
// stream closed before backend returns | |
cancel_cb(); // cancelling backend access | |
}); | |
} | |
}; | |
middleware_cb template_handler = [](request &req, response &res){ | |
std::shared_ptr<custom_helper> helper; | |
if (req.helper) { | |
helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
} | |
if (helper) { | |
std::string html; | |
std::stringstream response_body; | |
try { | |
// TODO: switch rendering pages according to parameter existence | |
constexpr auto html_template = compiletime::trimIndent(R"( | |
<html> | |
<body> | |
<h1>Hello {{ param1 }} and {{ param2 }}!</h1> | |
</body> | |
</html> | |
)"); | |
inja::render_to(response_body, (const char *)html_template, helper->res_data); | |
} | |
catch (std::exception &e) { | |
// rendering error: typically some parameters are missing in res_data | |
std::cerr << "template_handler: exception: " << e.what() << std::endl; | |
std::cerr << "template_handler: errored response_body in rendering: " << response_body.str() << std::endl; | |
response_body.str(""); | |
response_body.clear(); | |
constexpr auto internal_server_error_html = compiletime::trimIndent(R"( | |
<html> | |
<body> | |
<h1>500 Internal Server Error</h1> | |
</body> | |
</html> | |
)"); | |
response_body << internal_server_error_html; | |
html = response_body.str(); | |
res.write_head(500, {{"content-type", {"text/html"}}, {"content-length", { std::to_string(html.size()) }}}); | |
res.end(html); | |
//req.next(); // alternatively skip rendereing an error page and delegate to fallback mechanisms | |
return; | |
} | |
html = response_body.str(); | |
res.write_head(200, {{"content-type", {"text/html"}}, {"content-length", { std::to_string(html.size()) }}}); | |
res.end(html); | |
} | |
}; | |
middleware_cb fallback_handler = [](request &req, response &res){ | |
res.write_head(404); | |
res.end(); | |
}; | |
middleware_cb push_handler = [](request &req, response &res){ | |
boost::system::error_code ec; | |
res.on_close([](uint32_t err){ | |
#if PUSH_LOG | |
std::cerr << "/push2 on_close" << std::endl; | |
#endif | |
}); | |
auto push = res.push(ec, "GET", "/push2/1"); | |
if (!ec) { | |
std::string path = push->push_raw_path_query; | |
push->on_close([path /* WARNING: capturing push would leak *push object */](uint32_t err){ | |
#if PUSH_LOG | |
std::cerr << "push for " << path << " on_close" << std::endl; | |
#endif | |
}); | |
push->write_head(200, {{"content-type", {"text/html"}}}); | |
push->end("<html><body>server push FTW!<body></html>\n"); | |
} | |
res.write_head(200, {{"content-type", {"text/html"}}}); | |
res.end("<html><body>you'll receive server push!<iframe src=\"/push2/1\"></iframe></body></html>\n"); | |
}; | |
middleware_cb delay_handler = [](request &req, response &res){ | |
res.write_head(200); | |
auto timer = std::make_shared<boost::asio::deadline_timer>( | |
res.res.io_service(), boost::posix_time::seconds(3)); | |
auto closed = std::make_shared<bool>(); | |
res.on_close([timer, closed](uint32_t error_code) { | |
timer->cancel(); | |
*closed = true; | |
}); | |
timer->async_wait([&res, closed](const boost::system::error_code &ec) { | |
if (ec || *closed) { | |
return; | |
} | |
res.end("finally!\n"); | |
}); | |
}; | |
const int num_pooled_threads = 16; | |
boost::asio::thread_pool thread_pool{num_pooled_threads}; | |
middleware_cb threaded_task_handler = [&thread_pool](request &req, response &res){ | |
if (!req.helper) { | |
req.next(); // do nothing if helper does not exist | |
return; | |
} | |
// Blocking operations should not be performed in nghttp2 worker threads | |
// so that aynchronous network I/O operations can continue without blocking | |
// task for the thread pool | |
auto threaded_task = [&req, &res](std::shared_ptr<std::promise<json&>> promise_ptr, | |
json &result, | |
custom_helper::threaded_task_next_cb next) { | |
uint32_t err = NGHTTP2_NO_ERROR; | |
// do some tasks and store results into the promise object | |
result["param1"] = "1st result from a threaded task"; | |
result["param2"] = "2nd result from a threaded task"; | |
// simulate blocking operations by sleeping | |
// only 160(=16 threads / 0.1 sec) requests per second can be processed at most in the thread pool | |
// this maximum throughput should be consistent with any number of nghttp2 worker threads | |
std::this_thread::sleep_for(std::chrono::milliseconds(100)); | |
promise_ptr->set_value(result); | |
// err = NGHTTP2_CANCEL // if errored | |
next(err); | |
}; | |
auto helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
// post threaded_task to the thread pool and get its result in helper->res_data as json | |
helper->post_threaded_task<json>(req, res, thread_pool, threaded_task, helper->res_data); | |
}; | |
middleware_cb defer_handler = [](request &req, response &res){ | |
res.write_head(200); | |
auto timer = std::make_shared<boost::asio::deadline_timer>( | |
res.res.io_service(), boost::posix_time::seconds(3)); | |
auto closed = std::make_shared<bool>(); | |
auto first_sent = std::make_shared<bool>(false); | |
auto defer_sent = std::make_shared<bool>(false); | |
res.on_close([timer, closed](uint32_t error_code) { | |
//std::cerr << "on_close timer.use_count() = " << timer.use_count() << std::endl; | |
timer->cancel(); | |
*closed = true; | |
}); | |
res.end([&res, timer, closed, first_sent, defer_sent](uint8_t *buf, std::size_t len, uint32_t *data_flags){ | |
std::stringstream first("finally "); | |
std::stringstream second("done!"); | |
if (*closed) { | |
return (ssize_t)NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; | |
} | |
if (!*first_sent) { | |
ssize_t n = std::min(len, first.str().size()); | |
n = first.readsome((char *)buf, n); | |
*first_sent = true; | |
#if STREAMING_LOG | |
std::cerr << "[defer_handler end callback] first sent; bytes = " << n << std::endl; | |
#endif | |
return n; | |
} | |
else if (!*defer_sent) { | |
timer->async_wait([&res, closed](const boost::system::error_code &ec) { | |
if (ec || *closed) { | |
return; | |
} | |
#if STREAMING_LOG | |
std::cerr << "[defer_handler end callback] /defer res.resume()" << std::endl; | |
#endif | |
res.resume(); | |
}); | |
*defer_sent = true; | |
#if STREAMING_LOG | |
std::cerr << "[defer_handler end callback] defer sent" << std::endl; | |
#endif | |
return (ssize_t)NGHTTP2_ERR_DEFERRED; | |
} | |
else { | |
ssize_t n = std::min(len, second.str().size()); | |
n = second.readsome((char *)buf, n); | |
*first_sent = true; | |
*data_flags |= NGHTTP2_DATA_FLAG_EOF; | |
#if STREAMING_LOG | |
std::cerr << "[defer_handler end callback] second sent; bytes = " << n << std::endl; | |
#endif | |
return n; | |
} | |
}); | |
}; | |
middleware_cb trailer_handler = [](request &req, response &res){ | |
// send trailer part. | |
res.write_head(200, {{"trailer", {"digest"}}}); | |
std::string body = "nghttp2 FTW!\n"; | |
res.on_trailer([](response &res){ | |
auto it = res.trailer().find("digest"); | |
// recalculate digest | |
auto digest = std::string(SHA256_DIGEST_LENGTH, '\0'); | |
SHA256_CTX sha_ctx; | |
SHA256_Init(&sha_ctx); | |
SHA256_Update(&sha_ctx, res.body.data(), res.body.size()); | |
SHA256_Final((unsigned char*)digest.data(), &sha_ctx); | |
// base64 encode | |
typedef boost::archive::iterators::base64_from_binary | |
<boost::archive::iterators::transform_width<std::string::iterator, 6, 8>> base64_iterator; | |
std::ostringstream value; | |
value << "SHA-256="; | |
std::copy(base64_iterator(digest.begin()), base64_iterator(digest.end()), std::ostream_iterator<char>(value)); | |
std::size_t equals = (3 - digest.size() % 3) % 3; | |
while (equals-- > 0) { | |
value << '='; | |
} | |
// update trailer header value | |
it->second.value = value.str(); | |
}); | |
auto left = std::make_shared<size_t>(body.size()); | |
res.end([&res, body, left](uint8_t *dst, std::size_t len, | |
uint32_t *data_flags) { | |
auto n = std::min(len, *left); | |
std::copy_n(body.c_str() + (body.size() - *left), n, dst); | |
*left -= n; | |
if (*left == 0) { | |
*data_flags |= | |
NGHTTP2_DATA_FLAG_EOF | NGHTTP2_DATA_FLAG_NO_END_STREAM; | |
// RFC 3230 Instance Digests in HTTP. The digest value is | |
// SHA-256 message digest of body. | |
res.write_trailer( | |
{{"digest", {"placeholder"}}}); | |
} | |
return n; | |
}); | |
}; | |
std::string docroot; | |
if (argc >= 7 && std::string("quit").compare(argv[6]) != 0) { | |
docroot = argv[6]; | |
} | |
// ported from asio-sv2.cc; maybe incompatible with Windows | |
middleware_cb static_file_handler = [&docroot](request &req, response &res) { | |
std::shared_ptr<custom_helper> helper; | |
if (req.helper) { | |
helper = std::dynamic_pointer_cast<custom_helper>(req.helper); | |
} | |
auto prefix_length = req.controller->it->prefix.size() + req.controller->it->path.size(); | |
auto path = req.uri().path.substr(prefix_length); | |
if (!nghttp2::asio_http2::check_path(path) || docroot.size() == 0) { | |
req.next(); // fallback | |
return; | |
} | |
auto file_path = docroot + path; | |
bool ends_with_slash = false; | |
if (file_path[file_path.size() - 1] == '/') { | |
// ends with / | |
ends_with_slash = true; | |
file_path = file_path.substr(0, file_path.size() - 1); | |
} | |
struct stat stbuf; | |
int stat_result = ::stat(file_path.c_str(), &stbuf); | |
if (stat_result == 0) { | |
if (S_ISDIR(stbuf.st_mode)) { | |
if (ends_with_slash) { | |
file_path += "/index.html"; | |
path += "/index.html"; | |
stat_result = ::stat(file_path.c_str(), &stbuf); | |
} | |
else { | |
// attach slash and redirect | |
path = req.uri().path + "/"; | |
// attach query if necessary | |
if (req.uri().raw_query.size() > 0) { | |
path += "?" + req.uri().raw_query; | |
} | |
res.write_head(302, header_map{ { "location", { path }}}); | |
res.end(); | |
return; | |
} | |
} | |
else if (S_ISREG(stbuf.st_mode)) { | |
if (ends_with_slash) { | |
// / is attached to a regular file path | |
req.next(); // fallback | |
return; | |
} | |
else { | |
// regular file found | |
} | |
} | |
} | |
auto range = req.header().equal_range("accept-encoding"); | |
bool is_gzip_encoding_acceptable = true; | |
bool is_gzip_encoding_explicitly_acceptable = false; | |
bool is_accept_encoding_header_existent = false; | |
for (auto it = range.first; it != range.second; ++it) { | |
is_accept_encoding_header_existent = true; | |
if (it->second.value == "gzip" || it->second.value == "*") { | |
is_gzip_encoding_explicitly_acceptable = true; | |
break; | |
} | |
else { | |
using namespace boost::xpressive; | |
// deflate, gzip;q=1.0, *;q=0.5 | |
sregex re = (s1=+(alnum|'*')) >> !(as_xpr(';') >> 'q' >> '=' >> +(digit|'.')) >> !(as_xpr(',') >> +as_xpr(' ') >> (s2 = *_)); // regex compiled at build time | |
smatch what; | |
auto str = it->second.value; | |
auto it_re = str.cbegin(); | |
auto it_re_end = str.cend(); | |
while (regex_match(it_re, it_re_end, what, re)) { | |
if (what.position(1) >= 0) { | |
if (what[1].str() == "gzip" || what[1].str() == "*") { | |
is_gzip_encoding_explicitly_acceptable = true; | |
break; | |
} | |
} | |
if (what.position(2) >= 1) { | |
std::advance(it, what.position(2)); | |
} | |
else { | |
break; | |
} | |
} | |
if (is_gzip_encoding_explicitly_acceptable) { | |
break; | |
} | |
} | |
} | |
if (is_accept_encoding_header_existent && !is_gzip_encoding_explicitly_acceptable) { | |
is_gzip_encoding_acceptable = false; | |
} | |
bool is_gzip_found = false; | |
auto gzip_path = file_path + ".gz"; | |
if (is_gzip_encoding_acceptable && stat_result == 0) { | |
if (file_path.rfind(".gz") == file_path.size() - 3) { | |
// ends with ".gz" | |
} | |
else { | |
struct stat stbuf2; | |
auto stat_result2 = ::stat(gzip_path.c_str(), &stbuf2); | |
if (stat_result2 == 0) { | |
is_gzip_found = true; | |
stat_result = stat_result2; | |
stbuf = stbuf2; | |
} | |
} | |
} | |
bool is_not_modified = false; | |
auto it = req.header().find("if-modified-since"); | |
if (it != req.header().end()) { | |
try { | |
auto if_modified_since = it->second.value; | |
time_t if_modified_since_time_t = 0; | |
std::stringstream sstr(if_modified_since); | |
static const std::locale posix_time_locale( | |
std::locale::classic(), | |
new boost::posix_time::time_input_facet("%a, %d %b %Y %H:%M:%S GMT")); | |
sstr.imbue(posix_time_locale); | |
boost::posix_time::ptime posix_time; | |
sstr >> posix_time; | |
if (sstr) { | |
if_modified_since_time_t = boost::posix_time::to_time_t(posix_time); | |
} | |
if (stat_result == 0 && stbuf.st_mtime <= if_modified_since_time_t) { | |
is_not_modified = true; | |
} | |
} | |
catch (std::exception &e) {} | |
} | |
auto header = header_map(); | |
if (stat_result == 0) { | |
if (helper) { | |
header.emplace("content-type", | |
header_value{helper->detect_mime_type(file_path)}); | |
} | |
if (is_gzip_found) { | |
header.emplace("content-encoding", | |
header_value{"gzip"}); | |
} | |
if (!is_not_modified) { | |
header.emplace("content-length", | |
header_value{std::to_string(stbuf.st_size)}); | |
} | |
header.emplace("last-modified", | |
header_value{nghttp2::asio_http2::http_date(stbuf.st_mtime)}); | |
} | |
if (is_not_modified) { | |
// 304 not modified | |
res.write_head(304, std::move(header)); | |
res.end(); | |
return; | |
} | |
auto fd = open((is_gzip_found ? gzip_path : file_path).c_str(), O_RDONLY); | |
if (fd == -1) { | |
req.next(); // fallback | |
return; | |
} | |
res.write_head(200, std::move(header)); | |
// Note: read() is a blocking I/O operation and can lag in seeking and reading the file's underlying storage | |
// Unlike the original nghttp2 asio library, nicexprs.h 0.0.5 does not support streaming of contents | |
// while reading from their data sources | |
// This means that successive multiple requests of different non-cached very large files may stagnate | |
// further request handling | |
// TODO: Adaptive buffering in reading files | |
// - If read() returns immediately, the file is cached and can be processed without delay | |
// - If read() returns with significant delay such as 10ms (presumably via HDD or NAS), | |
// the file is not cached and the buffering can be delegated to reader threads and | |
// the nghttp2 worker thread can defer handling of the request | |
// Such reader threads must handle multiple reading operations effectively without redundant operations. | |
// - If there are no on_response callbacks, file buffering can be skipped and data can be sent to HTTP/2 | |
// layer immediately after they are read from data sources | |
res.end(nghttp2::asio_http2::file_generator_from_fd(fd)); | |
}; | |
middleware_cb hello_error_handler = [](request &req, response &res){ | |
res.write_head(200, {{"foo", {"bar"}}}); | |
auto str = std::make_shared<std::stringstream>(); | |
for (int i = 0; i < 10; i++) { | |
(*str) << "hello, error world! "; | |
(*str) << " rep = "; | |
(*str) << std::to_string(i); | |
(*str) << std::endl; | |
} | |
auto call_count = std::make_shared<int>(0); | |
res.end([&res, str, call_count](uint8_t *buf, std::size_t len, uint32_t *data_flags)->ssize_t{ | |
(*call_count)++; | |
if (*call_count == 5) { | |
#if STREAMING_LOG | |
std::cerr << "[hello error generator] ERROR" << std::endl; | |
#endif | |
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; | |
} | |
ssize_t bytes = str->readsome((char *)buf, std::min(len, (std::size_t)20)); | |
if (bytes > 0) { | |
#if STREAMING_LOG | |
std::cerr << "[hello error generator] output bytes = " << bytes << std::endl; | |
#endif | |
return bytes; | |
} | |
else { | |
#if STREAMING_LOG | |
std::cerr << "[hello error generator] EOF" << std::endl; | |
#endif | |
*data_flags |= NGHTTP2_DATA_FLAG_EOF; | |
return 0; | |
} | |
}); | |
}; | |
#if STREAMING_SUPPORT | |
middleware_cb hello_stream = [](request &req, response &res) { | |
std::ostringstream oss; | |
for (int i = 0; i < 10; i++) { | |
#if UNICODE_UPPERCASE | |
oss << "ħĕĺŀő, ŵȍȓḹɗ!"; | |
#else // !UNICODE_UPPERCASE | |
oss << "hello, world!"; | |
#endif // !UNICODE_UPPERCASE | |
oss << " rep = "; | |
oss << std::to_string(i); | |
oss << std::endl; | |
} | |
#if STREAMING_LOG | |
std::cerr << "[hello generator] oss.str() = " << oss.str() << std::endl; | |
#endif | |
auto str = std::make_shared<std::string>(oss.str()); | |
auto left = std::make_shared<std::size_t>(str->size()); | |
auto call_count = std::make_shared<int>(0); | |
auto timer = std::make_shared<boost::asio::deadline_timer>(res.res.io_service(), boost::posix_time::seconds(3));; | |
auto closed = std::make_shared<bool>(); | |
res.on_close([timer, closed](uint32_t error_code) { | |
timer->cancel(); | |
*closed = true; | |
}); | |
generator_cb generator = [&res, closed, timer, str, left, call_count](uint8_t *buf, std::size_t len, uint32_t *data_flags)->ssize_t{ | |
(*call_count)++; | |
#if STREAMING_LOG | |
std::cerr << "[hello generator] called with len = " << len << std::endl; | |
#endif | |
if (*call_count == 5) { | |
#if STREAMING_LOG | |
std::cerr << "[hello generator] return = " << "DEFER" << std::endl; | |
#endif | |
timer->async_wait([&res, closed](const boost::system::error_code &ec) { | |
if (ec || *closed) { | |
return; | |
} | |
#if STREAMING_LOG | |
std::cerr << "[hello generator] resuming deferred stream" << std::endl; | |
#endif | |
res.resume(); | |
}); | |
return NGHTTP2_ERR_DEFERRED; | |
} | |
/* | |
if (*call_count == 2) { | |
std::cerr << "[hello generator] ERROR" << std::endl; | |
return NGHTTP2_ERR_TEMPORAL_CALLBACK_FAILURE; | |
} | |
*/ | |
ssize_t bytes = std::min(*left, std::min(len, (std::size_t)20)); | |
std::copy_n((char *)&(*str)[str->size() - *left], bytes, (char *)buf); | |
*left -= bytes; | |
if (bytes > 0) { | |
#if STREAMING_LOG | |
std::cerr << "[hello generator] output bytes = " << bytes << std::endl; | |
#if STREAMING_DUMP | |
std::cerr << "[hello generator] output = \n\""; | |
std::cerr.write((char *)buf, bytes); | |
std::cerr << "\" end of output" << std::endl; | |
#endif | |
#endif | |
return bytes; | |
} | |
else { | |
#if STREAMING_LOG | |
std::cerr << "[hello generator] EOF" << std::endl; | |
#endif | |
*data_flags |= NGHTTP2_DATA_FLAG_EOF; | |
return 0; | |
} | |
}; | |
#if STREAMING_LOG | |
std::cerr << "[hello_stream]" << std::endl; | |
#endif | |
res.write_head(200, header_map{ { "content-type", { "text/plain" } } }); | |
auto rep = std::make_shared<int>(10); | |
res.end(generator); | |
}; | |
// no-operation stream handler without buffering | |
middleware_cb noop_stream_handler = [](request &req, response &res){ | |
res.on_response(nullptr, [](response &res, generator_cb src)->generator_cb { | |
return [src](uint8_t *buf, std::size_t len, uint32_t *data_flags)->ssize_t { | |
ssize_t bytes = src(buf, len, data_flags); | |
// do some filtering on [buf, buf + bytes) if bytes > 0 | |
return bytes; | |
}; | |
}); | |
req.next(); | |
}; | |
#if GENERATOR_STREAM | |
// skeleton of no-resizing stream handler with buffering | |
middleware_cb noresize_stream_handler = [](request &req, response &res){ | |
res.on_response(nullptr, [&req](response &res, generator_cb src)->generator_cb { | |
// wrap src with generator_stream | |
auto src_stream = std::make_shared<generator_stream>(src, 16384 /* buf_size */); | |
return [&req, &res, src, src_stream](uint8_t *buf, std::size_t len, uint32_t *data_flags)->ssize_t { | |
// Note: It is essential to use istream::readsome() method so that source generator can work in non-blocking | |
ssize_t bytes = src_stream->readsome((char *)buf, len); // read to output buffer directly | |
bytes = src_stream->check_status(data_flags); // check for gcount(), good(), eof(), fail() and update data_flags | |
if (bytes > 0) { // data available | |
// do some filtering on [buf, buf + bytes) | |
} | |
return bytes; | |
}; | |
}); | |
req.next(); | |
}; | |
// line-by-line stream handler with generator_stream as a base class | |
middleware_cb line_number_handler = [](request &req, response &res){ | |
res.on_response( | |
[](response &res){ | |
res.header().emplace("x-line-number-stream", header_value{ "numbered" }); | |
res.header().erase("content-length"); // content-length would become incorrect for transforms that change data lengths | |
}, | |
[&req](response &res, generator_cb src)->generator_cb { | |
class line_number_filter: public generator_stream { | |
public: | |
line_number_filter(generator_cb cb, std::streamsize buf_size = 16384): generator_stream(cb, buf_size), _M_line_number(0) {} | |
bool do_peek(std::string &buf, char *&begin, char *&end) { | |
// detect a chunk (valid unit of filtering) in [begin, end) | |
// - end can be modified to point the end of a detected chunk | |
// - backend input buffer is handed as buf | |
// seek a line | |
char *newline = (char *)::memchr(begin, '\n', end - begin); | |
if (newline) { | |
end = newline + 1; | |
_M_line_number++; | |
return true; // true if a valid chunk is found | |
} | |
else { | |
if (end - begin == buf.size()) { // buf.data() == begin | |
// buffer full | |
_M_line_number++; | |
return true; // compromised to treat as a new line | |
} | |
else { | |
// buffer not full; still a chance to detect a valid chunk after the buffer is filled | |
// do_peek() might be called multiple times for the same position of a source with updated buffer data | |
return false; | |
} | |
} | |
} | |
std::streamsize do_filter(char *in_beg, std::streamsize in_size) { | |
// optionally [in_beg, in_beg + in_size) can be used as a workspace for filtering | |
auto out_beg = in_beg; | |
auto out_size = in_size; | |
*this << _M_line_number << ": "; // prepend a line number | |
write(out_beg, out_size); | |
return out_size; // TODO: output size; not used for now | |
} | |
void do_close() {} // override do_close() method to flush filtering buffer on EOF | |
protected: | |
int _M_line_number; // any stateful filtering information can be stored in *this object | |
}; | |
auto line_number = std::make_shared<line_number_filter>(src); | |
return [line_number](uint8_t *buf, std::size_t len, uint32_t *data_flags)->ssize_t { | |
return line_number->filter(buf, len, data_flags); // filter() method baheves as generator_cb | |
}; | |
}); | |
req.next(); | |
}; | |
#endif | |
#if BOOST_FILTER | |
#if GZIP_FILTER | |
// generator_stream as a base class for boost_filtering_streambuf_adaptor | |
middleware_cb gzip_stream_handler = [](request &req, response &res){ | |
auto range = req.header().equal_range("accept-encoding"); | |
bool is_gzip_encoding_acceptable = true; | |
bool is_gzip_encoding_explicitly_acceptable = false; | |
bool is_accept_encoding_header_existent = false; | |
for (auto it = range.first; it != range.second; ++it) { | |
is_accept_encoding_header_existent = true; | |
if (it->second.value == "gzip" || it->second.value == "*") { | |
is_gzip_encoding_explicitly_acceptable = true; | |
break; | |
} | |
else { | |
using namespace boost::xpressive; | |
// deflate, gzip;q=1.0, *;q=0.5 | |
sregex re = (s1=+(alnum|'*')) >> !(as_xpr(';') >> 'q' >> '=' >> +(digit|'.')) >> !(as_xpr(',') >> +as_xpr(' ') >> (s2 = *_)); // regex compiled at build time | |
smatch what; | |
auto str = it->second.value; | |
auto it_re = str.cbegin(); | |
auto it_re_end = str.cend(); | |
while (regex_match(it_re, it_re_end, what, re)) { | |
if (what.position(1) >= 0) { | |
if (what[1].str() == "gzip" || what[1].str() == "*") { | |
is_gzip_encoding_explicitly_acceptable = true; | |
break; | |
} | |
} | |
if (what.position(2) >= 1) { | |
std::advance(it, what.position(2)); | |
} | |
else { | |
break; | |
} | |
} | |
if (is_gzip_encoding_explicitly_acceptable) { | |
break; | |
} | |
} | |
} | |
if (is_accept_encoding_header_existent && !is_gzip_encoding_explicitly_acceptable) { | |
is_gzip_encoding_acceptable = false; | |
} | |
if (is_gzip_encoding_acceptable) { | |
auto already_encoded = std::make_shared<bool>(false); | |
res.on_response( | |
[already_encoded](response &res){ | |
if (res.header().find("content-encoding") == res.header().end()) { | |
res.header().emplace("content-encoding", header_value{ "gzip" }); | |
res.header().erase("content-length"); // remove the current content-length header, which would become incorrect if it were to remain | |
} | |
else { | |
*already_encoded = true; | |
} | |
}, | |
[&req, already_encoded](response &res, generator_cb src)->generator_cb { | |
// construct adaptor for boost::iostreams::gzip_compressor | |
auto gzip = std::make_shared<boost_filtering_streambuf_adaptor>(src, boost::iostreams::gzip_compressor()); | |
return [already_encoded, src, gzip](uint8_t *buf, std::size_t len, uint32_t *data_flags)->ssize_t { | |
if (*already_encoded) { | |
return src(buf, len, data_flags); // skip filtering | |
} | |
else { | |
return gzip->filter(buf, len, data_flags); // gzip streaming body | |
} | |
}; | |
} | |
); | |
} | |
req.next(); | |
}; | |
#endif // GZIP_FILTER | |
#endif // BOOST_FILTER | |
#if GENERATOR_STREAM | |
// generator_stream as a filtering base class | |
middleware_cb uppercase_stream_handler = [](request &req, response &res){ | |
res.on_push([](response &res, std::string &method, std::string &raw_path_query, header_map &header){ | |
std::transform(raw_path_query.cbegin(), raw_path_query.cend(), raw_path_query.begin(), ::toupper); | |
header.emplace("x-uppercase", header_value{ std::string("UPPERCASED") }); | |
}); | |
res.on_response( | |
[](response &res){ | |
res.header().emplace("x-uppercase-stream", header_value{ "UPPERCASED" }); | |
}, | |
[&req](response &res, generator_cb src)->generator_cb { | |
class uppercase_filter: public generator_stream { | |
public: | |
using generator_stream::generator_stream; // uppercase_filter(generator_cb cb): generator_stream(cb) {} | |
bool do_peek(std::string &buf, char *&begin, char *&end) { | |
// find a chunk that ends with a UTF-8 character boundary | |
// Note: With this naive approach, combined characters with modifiers can be divided into 2 separate chunks | |
char *p = end - 1; | |
while (begin <= p) { | |
if (((unsigned int)(unsigned char)*p) & 0x80) { | |
// UTF-8 multibyte characters | |
if ((((unsigned int)(unsigned char)*p) & 0xc0) == 0xc0) { | |
// start of UTF-8 character sequence | |
std::streamsize s; | |
switch (((unsigned int)(unsigned char)*p) & 0x30) { | |
case 0x00: // 2 bytes | |
case 0x10: // 2 bytes | |
s = 2; | |
break; | |
case 0x20: // 3 bytes | |
s = 3; | |
break; | |
case 0x30: // 4 bytes | |
s = 4; | |
break; | |
} | |
if (p + s <= end) { | |
end = p + s; | |
} | |
else { | |
end = p; | |
} | |
return true; | |
} | |
else { | |
p--; | |
} | |
} | |
else { | |
// 1 byte | |
end = p + 1; | |
return true; | |
} | |
} | |
// give up finding a character boundary | |
return true; | |
} | |
std::streamsize do_filter(char *in_beg, std::streamsize in_size) { | |
#if UNICODE_UPPERCASE | |
// Note: Conversion in 32bit Unicode characters is definitely slow | |
icu::UnicodeString unicodeString(in_beg, in_size); | |
std::string result; | |
unicodeString.toUpper().toUTF8String(result); | |
write(result.data(), result.size()); | |
return result.size(); | |
#else // !UNICODE_UPPERCASE | |
#if BOOST_UPPERCASE | |
std::string str(in_beg, in_size); | |
boost::algorithm::to_upper(str); // locale should be set | |
*this << str; | |
return str.size(); | |
#else // !BOOST_UPPERCASE | |
std::transform(in_beg, in_beg + in_size, in_beg, ::toupper); // ASCII uppercasing; no changes in size | |
write(in_beg, in_size); | |
return in_size; | |
#endif // !BOOST_UPPERCASE | |
#endif // !UNICODE_UPPERCASE | |
} | |
}; | |
auto uppercase = std::make_shared<uppercase_filter>(src); | |
return [&req, &res, uppercase](uint8_t *buf, std::size_t len, uint32_t *data_flags)->ssize_t { | |
return uppercase->filter(buf, len, data_flags); | |
}; | |
} | |
); | |
req.next(); | |
}; | |
#endif | |
#if GENERATOR_STREAM | |
// generator_stream as read buffer | |
middleware_cb trailer_stream_handler = [](request &req, response &res){ | |
auto sha_ctx = std::make_shared<SHA256_CTX>(); | |
SHA256_Init(&*sha_ctx); | |
res.on_response( | |
[](response &res){ | |
if (res.is_push) { | |
return; // no trailers for push responses | |
} | |
auto it = res.header().find("trailer"); | |
if (it == res.header().end()) { | |
res.header().emplace("trailer", header_value{ "digest" }); | |
} | |
else { | |
it->second.value += ", digest"; | |
} | |
}, | |
[&req, sha_ctx](response &res, generator_cb src)->generator_cb { | |
auto src_stream = std::make_shared<generator_stream>(src /* , buf_size = 16384 */); | |
return [&req, &res, src, src_stream, sha_ctx](uint8_t *buf, std::size_t len, uint32_t *data_flags)->ssize_t { | |
// Note: It is essential to use istream::readsome() method so that source generator can work in non-blocking | |
#if STREAMING_LOG | |
std::cerr << "[trailer_stream_handler] generator_cb called; len = " << len << std::endl; | |
#endif | |
ssize_t bytes = src_stream->readsome((char *)buf, len); // read non-blockingly as much as possible to output buffer directly | |
#if STREAMING_LOG | |
std::cerr << "[trailer_stream_handler] readsome bytes = " << bytes << std::endl; | |
#endif | |
bytes = src_stream->check_status(data_flags); | |
#if STREAMING_LOG | |
std::cerr << "[trailer_stream_handler] check_status bytes = " << bytes << " ; dataflags = " << *data_flags << std::endl; | |
#endif | |
if (bytes > 0) { | |
SHA256_Update(&*sha_ctx, buf, bytes); // calculate digest on streaming data | |
// no conversion of body data | |
} | |
if (*data_flags & NGHTTP2_DATA_FLAG_EOF && !res.is_push) { | |
*data_flags |= NGHTTP2_DATA_FLAG_NO_END_STREAM; | |
// write trailer header | |
res.write_trailer(); | |
} | |
#if STREAMING_LOG | |
std::cerr << "[trailer_stream_handler] return bytes = " << bytes << std::endl; | |
#endif | |
return bytes; | |
}; | |
} | |
); | |
res.on_trailer([sha_ctx](response &res){ | |
// finalize digest | |
auto digest = std::string(SHA256_DIGEST_LENGTH, '\0'); | |
SHA256_Final((unsigned char*)digest.data(), &*sha_ctx); | |
// base64 encode | |
typedef boost::archive::iterators::base64_from_binary | |
<boost::archive::iterators::transform_width<std::string::iterator, 6, 8>> base64_iterator; | |
std::ostringstream value; | |
value << "SHA-256="; | |
std::copy(base64_iterator(digest.begin()), base64_iterator(digest.end()), std::ostream_iterator<char>(value)); | |
std::size_t equals = (3 - digest.size() % 3) % 3; | |
while (equals-- > 0) { | |
value << '='; | |
} | |
// add trailer header value | |
res.trailer().emplace("digest", header_value{value.str()}); | |
}); | |
req.next(); | |
}; | |
#endif | |
#endif | |
auto dummy_middleware_generator = [](std::string name){ | |
middleware_cb cb = [name](request req, response res){ | |
#if MATCHING_LOG | |
std::cerr << "middleware " << name << " invoked for " << req.uri().path | |
<< " prefix_length=" << req.key.prefix_length << " " << req.uri().path.substr(0, req.key.prefix_length) << std::endl; | |
#endif | |
res.on_response([name](response &res){ | |
#if MATCHING_LOG | |
std::cerr << "middleware " << name << ".on_response invoked" << std::endl; | |
#endif | |
res.header().emplace("x-middleware", header_value{ name, false }); | |
res.next(); | |
}); | |
res.write_head(200); | |
res.end(name); | |
//req.next(); | |
}; | |
return cb; | |
}; | |
#define STRING(str) #str | |
#define DUMMY_MIDDLEWARE(name) auto name = dummy_middleware_generator(STRING(name)) | |
DUMMY_MIDDLEWARE(url_checker); | |
DUMMY_MIDDLEWARE(proxy_handler); | |
DUMMY_MIDDLEWARE(sub1_handler); | |
DUMMY_MIDDLEWARE(sub2_handler); | |
DUMMY_MIDDLEWARE(sub3_handler); | |
DUMMY_MIDDLEWARE(path1_handler); | |
DUMMY_MIDDLEWARE(path2_handler); | |
DUMMY_MIDDLEWARE(css_handler); | |
DUMMY_MIDDLEWARE(js_handler); | |
DUMMY_MIDDLEWARE(subpath1_handler); | |
DUMMY_MIDDLEWARE(subpath2_handler); | |
DUMMY_MIDDLEWARE(view_handler); | |
DUMMY_MIDDLEWARE(spath1_handler); | |
DUMMY_MIDDLEWARE(spath2_handler); | |
DUMMY_MIDDLEWARE(subfolder2_handler); | |
DUMMY_MIDDLEWARE(subfolder3_handler); | |
web_app_ptr proxy_app = (*web_app::create()) // /subfolder;6 -> 12 | |
.use(url_checker) // /subfolder;7 -> 8 | |
.use(proxy_handler) // /subfolder;8 -> 12 | |
.ptr(); | |
web_app_ptr sub_app = (*web_app::create()) // ;12 -> 17 | |
.dispatch() // ;13 -> 14, 15, 16; copy: 34 -> 35, 36, 37 | |
.get("/sub1", sub1_handler) // ;14 -> 16; copy: 35 -> 37 | |
.get("/sub2", sub2_handler) // ;15 -> 16; copy: 36 -> 37 | |
.parent() | |
.get("/sub3", sub3_handler) // ;16 -> 17; copy: 37 -> 38 | |
.ptr(); | |
web_app_ptr app = (*web_app::create()) | |
.use(raw_body) // ;0 -> 1 | |
// with nicexprs.h 0.0.7, 1st byte reception shortened from 40sec(HDD), 6.8sec(SATA SSD) to 28ms with a 2.5GB non-cached file | |
.get("/static_file", static_file_handler) // bypass response buffering with 0.0.7 | |
#if STREAMING_SUPPORT | |
.get("/streaming") | |
.use(trailer_stream_handler) | |
#if BOOST_FILTER | |
#if GZIP_FILTER | |
.get("/gzip") | |
.use(gzip_stream_handler) | |
.get("/static", static_file_handler) | |
.use(uppercase_stream_handler) | |
.get("/push", push_handler) | |
.use(line_number_handler) | |
.get("/hello_stream", hello_stream) | |
.get("/hello", hello_handler) | |
.get("/hello_error", hello_error_handler) | |
.all(fallback_handler) | |
.parent() | |
#endif | |
#endif | |
.get("/static", static_file_handler) | |
#if GENERATOR_STREAM | |
.use(uppercase_stream_handler) | |
#endif | |
.get("/push", push_handler) | |
#if GENERATOR_STREAM | |
.use(line_number_handler) | |
#endif | |
.get("/hello_stream", hello_stream) | |
.get("/hello", hello_handler) | |
.get("/hello_error", hello_error_handler) | |
.parent() | |
#endif // STREAMING_SUPPORT | |
.use(uppercase_middleware) // ;1 -> 2 | |
#if STREAMING_SUPPORT | |
.get("/streaming") // fallback; use buffered filters; no streaming filters; streaming filters are wrapped as buffered filters | |
.get("/fallback_hello", hello_handler) | |
.get("/fallback_hello_stream", hello_stream) | |
.get("/fallback_hello_error", hello_error_handler) | |
.get("/fallback_delay", delay_handler) | |
.get("/fallback_defer", defer_handler) | |
.get("/fallback_push", push_handler) | |
.parent() | |
#endif // STREAMING_SUPPORT | |
.dispatch("/subfolder") // ;2 -> 3, 4, 5, 6, 9, 10, 11, 12 | |
.get("/hello2", hello_handler) // /subfolder;3 -> 12 | |
.post("/post", post_handler) // /subfolder;4 -> 12 | |
.get("/push2", push_handler) // /subfolder;5 -> 12 | |
.get("/proxy", proxy_app) // /subfolder;6 -> 12 | |
.get("/delay2", delay_handler) // /subfolder;9 -> 12 | |
.get("/defer2", defer_handler) // /subfolder;10 -> 12 | |
.get("/trailer2", trailer_handler) // /subfolder;11 -> 12 | |
.parent() | |
.all("/subapp", sub_app) // ;12 -> 17 | |
.all("/subapp2") // ;17 -> 18, 30 | |
.get("/path1", path1_handler) // /subapp2;18 -> 19 | |
.get("/path2", path2_handler) // /subapp2;19 -> 20 | |
.dispatch("/dispatchfolder") // /subapp2;20 -> 21, 22, 23, 26, 27 | |
.get("/css", css_handler) // /subapp2/dispatchfolder;21 -> 27 | |
.get("/js", js_handler) // /subapp2/dispatchfolder;22 -> 27 | |
.dispatch("/dynamic") // /subapp2/dispatchfolder;23 -> 24, 25, 27 | |
.get("/subpath1", subpath1_handler) // /subapp2/dispatchfolder/dynamic;24 -> 27 | |
.get("/subpath2/:param1/:param2", subpath2_handler) // /subapp2/dispatchfolder/dynamic;25 -> 27 | |
.parent() | |
.get("/view", view_handler) // /subapp2/dispatchfolder;26 -> 27 | |
.parent() | |
.all("/subsubapp1") // /subapp2;27 -> 28, 30 | |
.get("/spath1", spath1_handler) // /subapp2/subsubapp1;28 -> 29 | |
.get("/spath2", spath2_handler) // /subapp2/subsubapp1;29 -> 30 | |
.parent() | |
.parent() | |
.dispatch() // ;30 -> 31, 32, 33 | |
.get("/subfolder2", static_file_handler) // ;31 -> 33 | |
.get("/subfolder3", subfolder3_handler) // ;32 -> 33 | |
.parent() | |
.all("/subappcopy", web_app::copy(sub_app)) // ;33 -> 34, 38 | |
.all("/api") | |
.use(body_parser) | |
.use(url_parser) | |
.post("/post", post_handler) | |
.get("/params", get_params_handler) | |
.parent() | |
.dispatch("/route_params") | |
.all("/route1/:param1/:param2") | |
.use(route_parser) | |
.use(route_params_handler) | |
.parent() | |
.get("/route2/:param1/:param2") | |
.use(route_parser) | |
.use(backend_handler) | |
.use(template_handler) | |
.parent() | |
.parent() | |
.get("/threaded_task") | |
.use(threaded_task_handler) | |
.use(template_handler) | |
.parent() | |
.all(fallback_handler) // ;38 -> end | |
.mount("/", server) | |
.ptr(); // ptr() is equivalent to shared_from_this() | |
std::string quit = "quit"; | |
if (argc >= 7 && quit.compare(argv[6]) == 0) { | |
std::cerr << "quitting without listening" << std::endl; | |
} | |
else if (argc >= 6) { | |
boost::asio::ssl::context tls(boost::asio::ssl::context::sslv23); | |
tls.use_private_key_file(argv[4], boost::asio::ssl::context::pem); | |
tls.use_certificate_chain_file(argv[5]); | |
nghttp2::asio_http2::server::configure_tls_context_easy(ec, tls); | |
if (server.listen_and_serve(ec, tls, addr, port, true)) { | |
std::cerr << "error: " << ec.message() << std::endl; | |
} | |
else { | |
server.join(); | |
} | |
} else { | |
if (server.listen_and_serve(ec, addr, port, true)) { | |
std::cerr << "error: " << ec.message() << std::endl; | |
} | |
else { | |
server.join(); | |
} | |
} | |
thread_pool.join(); | |
} catch (std::exception &e) { | |
std::cerr << "exception: " << e.what() << "\n"; | |
} | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment