a simple multithreaded webserver
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
webserver/webserver.cc

271 lines
7.7 KiB

3 years ago
// this file is derived from a boost::beast sample
3 years ago
#include <boost/beast/core.hpp>
#include <boost/filesystem.hpp>
3 years ago
#include <cstdlib>
#include <iostream>
#include <memory>
#include <string>
3 years ago
namespace fs = boost::filesystem;
3 years ago
3 years ago
#include "webserver.hh"
3 years ago
namespace pEp {
Webserver::Webserver(net::ip::address addr, unsigned short port, const std::string& doc_root)
: _ioc{1}
, _acceptor{_ioc, {addr, port}, false}
, _doc_root{doc_root}
, _generic_handler{}
, _port{port}
, _running{false}
{ }
3 years ago
beast::string_view Webserver::mime_type(beast::string_view path)
3 years ago
{
using beast::iequals;
auto const ext = [&path]
{
auto const pos = path.rfind(".");
if(pos == beast::string_view::npos)
return beast::string_view{};
return path.substr(pos);
}();
if(iequals(ext, ".htm")) return "text/html; charset=utf-8";
if(iequals(ext, ".html")) return "text/html; charset=utf-8";
if(iequals(ext, ".css")) return "text/css; charset=utf-8";
if(iequals(ext, ".txt")) return "text/plain; charset=utf-8";
if(iequals(ext, ".js")) return "application/javascript; charset=utf-8";
if(iequals(ext, ".json")) return "application/json; charset=utf-8";
if(iequals(ext, ".xml")) return "application/xml; charset=utf-8";
3 years ago
if(iequals(ext, ".png")) return "image/png";
if(iequals(ext, ".jpeg")) return "image/jpeg";
if(iequals(ext, ".jpg")) return "image/jpeg";
if(iequals(ext, ".gif")) return "image/gif";
if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon";
if(iequals(ext, ".tiff")) return "image/tiff";
if(iequals(ext, ".tif")) return "image/tiff";
if(iequals(ext, ".svg")) return "image/svg+xml; charset=utf-8";
3 years ago
if(iequals(ext, ".svgz")) return "image/svg+xml";
3 years ago
return "application/octet-stream";
3 years ago
}
void Webserver::add_url_handler(const std::string& url_regex, handler_t handler)
3 years ago
{
std::lock_guard< std::mutex > lock(_mtx);
_urls.emplace(url_regex, Handling{boost::regex(url_regex), handler});
3 years ago
}
void Webserver::remove_url_handler(const std::string& url_regex)
{
3 years ago
std::lock_guard< std::mutex > lock(_mtx);
_urls.erase(url_regex);
}
void Webserver::add_generic_url_handler(handler_t handler)
{
_generic_handler = handler;
}
void Webserver::remove_generic_url_handler()
{
_generic_handler = nullptr;
}
Webserver::response Webserver::create_status_response(const request& req, http::status status)
3 years ago
{
3 years ago
http::response< http::string_body > res{status, req.version()};
3 years ago
res.set(http::field::content_type, "text/html; charset=utf-8");
3 years ago
res.keep_alive(req.keep_alive());
std::stringstream s;
s << "<html><body>" << int(status) << " " << status << "</body></html>";
res.body() = s.str();
3 years ago
res.prepare_payload();
3 years ago
if (status != http::status::internal_server_error)
res.keep_alive(req.keep_alive());
return res;
}
Webserver* Webserver::probing_port_range(net::ip::address addr, unsigned short
start, unsigned short end, unsigned short& port, const std::string&
doc_root)
{
pEp::Webserver *web = nullptr;
for (port = start; port <= end; ++port) {
try {
web = new pEp::Webserver{addr, port, doc_root};
break;
}
catch (boost::system::system_error& err) {
}
}
return web;
}
void Webserver::deliver_status(tcp::socket *socket, const request& req, http::status status)
{
const response res { create_status_response(req, status) };
3 years ago
beast::error_code ec;
http::write(*socket, res, ec);
3 years ago
}
void Webserver::deliver_file(tcp::socket *socket, const request& req)
3 years ago
{
static boost::regex file{"/([\\w\\d]{1,100}\\.[\\w\\d]{1,4})"};
3 years ago
boost::cmatch m;
3 years ago
std::string d{req.target().data(), req.target().length()};
if (boost::regex_match(d.c_str(), m, file)) {
3 years ago
fs::path p{_doc_root};
p /= std::string(m[1]);
3 years ago
beast::error_code ec;
http::file_body::value_type body;
body.open(p.string().c_str(), beast::file_mode::scan, ec);
3 years ago
if (ec == beast::errc::no_such_file_or_directory) {
deliver_status(socket, req, http::status::not_found);
}
else if (ec) {
deliver_status(socket, req, http::status::internal_server_error);
}
else {
auto const size = body.size();
http::response<http::file_body> res{
std::piecewise_construct,
std::make_tuple(std::move(body)),
std::make_tuple(http::status::ok, req.version())};
res.set(http::field::content_type, mime_type(p.string().c_str()));
3 years ago
res.content_length(size);
res.keep_alive(req.keep_alive());
http::response_serializer<http::file_body> res_ser{res};
http::write(*socket, res_ser, ec);
3 years ago
}
3 years ago
}
else {
3 years ago
deliver_status(socket, req, http::status::not_found);
3 years ago
}
3 years ago
}
Webserver::handler_t Webserver::find_handler(const request& req, boost::cmatch& m)
3 years ago
{
3 years ago
std::lock_guard< std::mutex > lock(_mtx);
3 years ago
for (auto it=_urls.begin(); it!=_urls.end(); ++it) {
3 years ago
std::string d{req.target().data(), req.target().length()};
if (boost::regex_match(d.c_str(), m, it->second.regex))
3 years ago
return it->second.handler;
}
3 years ago
return _generic_handler; // might be empty std::function!
3 years ago
}
3 years ago
3 years ago
void Webserver::do_session(tcp::socket *socket)
{
beast::error_code ec;
beast::flat_buffer buffer;
3 years ago
3 years ago
while (_running)
3 years ago
{
http::request_parser<http::string_body> parser;
parser.body_limit((std::numeric_limits<std::int32_t>::max)());
http::read(*socket, buffer, parser, ec);
if (ec) {
#ifndef NDEBUG
// This will print stuff like ...
// - end of stream
// - An established connection was aborted by the software in your host machine
// ... so don't be alarmed.
std::cerr << "pEpWebserver: " << ec.message() << "\n";
#endif
goto the_end;
}
http::request<http::string_body> req = parser.get();
3 years ago
const auto method = req.method();
switch (method)
{
case http::verb::post: // fall through
case http::verb::get:
{
3 years ago
boost::cmatch m;
Webserver::handler_t handler = find_handler(req, m);
if (handler) {
try{
const Webserver::response res = handler(m, req);
http::write(*socket, res, ec);
3 years ago
}
catch(...){
deliver_status(socket, req, http::status::internal_server_error);
3 years ago
}
}
else {
if(method == http::verb::get && !_doc_root.empty())
{
deliver_file(socket, req);
}else{
deliver_status(socket, req, http::status::not_found);
}
3 years ago
}
break;
3 years ago
}
3 years ago
default:
3 years ago
deliver_status(socket, req, http::status::method_not_allowed);
3 years ago
};
3 years ago
if (ec)
break;
3 years ago
}
3 years ago
the_end:
3 years ago
socket->shutdown(tcp::socket::shutdown_send, ec);
delete socket;
3 years ago
}
void Webserver::runner(Webserver *me)
3 years ago
{
while (me->_running)
3 years ago
{
tcp::socket* socket = new tcp::socket{me->_ioc};
me->_acceptor.accept(*socket);
3 years ago
std::function< void() > tf = [=]()
{
me->thread_init();
me->do_session(socket);
me->thread_done();
};
3 years ago
std::thread{tf}.detach();
3 years ago
}
}
void Webserver::run()
{
_running = true;
_runner = std::thread(runner, this);
}
3 years ago
void Webserver::shutdown()
{
_running = false;
}
3 years ago
};