Initial server interface, minor logging changes.
This commit is contained in:
parent
b0ed20369f
commit
eab689909d
2
BUILD
2
BUILD
@ -5,7 +5,7 @@ load("@hedron_compile_commands//:refresh_compile_commands.bzl",
|
||||
refresh_compile_commands(
|
||||
name = "refresh_compile_commands",
|
||||
targets = {
|
||||
"//source:parselinklog": "",
|
||||
"//source:*": "",
|
||||
"//tests/...": "",
|
||||
},
|
||||
)
|
||||
|
||||
408
beastref.cpp
Normal file
408
beastref.cpp
Normal file
@ -0,0 +1,408 @@
|
||||
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/config.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
namespace beast = boost::beast; // from <boost/beast.hpp>
|
||||
namespace http = beast::http; // from <boost/beast/http.hpp>
|
||||
namespace net = boost::asio; // from <boost/asio.hpp>
|
||||
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
|
||||
|
||||
// Return a reasonable mime type based on the extension of a file.
|
||||
beast::string_view
|
||||
mime_type(beast::string_view path)
|
||||
{
|
||||
using beast::iequals;
|
||||
auto const ext = [&path]
|
||||
{
|
||||
auto const pos = path.rfind(".");
|
||||
if(pos == beast::string_view::npos)
|
||||
return beast::string_view{};
|
||||
return path.substr(pos);
|
||||
}();
|
||||
if(iequals(ext, ".htm")) return "text/html";
|
||||
if(iequals(ext, ".html")) return "text/html";
|
||||
if(iequals(ext, ".php")) return "text/html";
|
||||
if(iequals(ext, ".css")) return "text/css";
|
||||
if(iequals(ext, ".txt")) return "text/plain";
|
||||
if(iequals(ext, ".js")) return "application/javascript";
|
||||
if(iequals(ext, ".json")) return "application/json";
|
||||
if(iequals(ext, ".xml")) return "application/xml";
|
||||
if(iequals(ext, ".swf")) return "application/x-shockwave-flash";
|
||||
if(iequals(ext, ".flv")) return "video/x-flv";
|
||||
if(iequals(ext, ".png")) return "image/png";
|
||||
if(iequals(ext, ".jpe")) return "image/jpeg";
|
||||
if(iequals(ext, ".jpeg")) return "image/jpeg";
|
||||
if(iequals(ext, ".jpg")) return "image/jpeg";
|
||||
if(iequals(ext, ".gif")) return "image/gif";
|
||||
if(iequals(ext, ".bmp")) return "image/bmp";
|
||||
if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon";
|
||||
if(iequals(ext, ".tiff")) return "image/tiff";
|
||||
if(iequals(ext, ".tif")) return "image/tiff";
|
||||
if(iequals(ext, ".svg")) return "image/svg+xml";
|
||||
if(iequals(ext, ".svgz")) return "image/svg+xml";
|
||||
return "application/text";
|
||||
}
|
||||
|
||||
// Append an HTTP rel-path to a local filesystem path.
|
||||
// The returned path is normalized for the platform.
|
||||
std::string
|
||||
path_cat(
|
||||
beast::string_view base,
|
||||
beast::string_view path)
|
||||
{
|
||||
if(base.empty())
|
||||
return std::string(path);
|
||||
std::string result(base);
|
||||
#ifdef BOOST_MSVC
|
||||
char constexpr path_separator = '\\';
|
||||
if(result.back() == path_separator)
|
||||
result.resize(result.size() - 1);
|
||||
result.append(path.data(), path.size());
|
||||
for(auto& c : result)
|
||||
if(c == '/')
|
||||
c = path_separator;
|
||||
#else
|
||||
char constexpr path_separator = '/';
|
||||
if(result.back() == path_separator)
|
||||
result.resize(result.size() - 1);
|
||||
result.append(path.data(), path.size());
|
||||
#endif
|
||||
return result;
|
||||
}
|
||||
|
||||
// Return a response for the given request.
|
||||
//
|
||||
// The concrete type of the response message (which depends on the
|
||||
// request), is type-erased in message_generator.
|
||||
template <class Body, class Allocator>
|
||||
http::message_generator
|
||||
handle_request(
|
||||
beast::string_view doc_root,
|
||||
http::request<Body, http::basic_fields<Allocator>>&& req)
|
||||
{
|
||||
// Returns a bad request response
|
||||
auto const bad_request =
|
||||
[&req](beast::string_view why)
|
||||
{
|
||||
http::response<http::string_body> res{http::status::bad_request, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, "text/html");
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = std::string(why);
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
};
|
||||
|
||||
// Returns a not found response
|
||||
auto const not_found =
|
||||
[&req](beast::string_view target)
|
||||
{
|
||||
http::response<http::string_body> res{http::status::not_found, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, "text/html");
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = "The resource '" + std::string(target) + "' was not found.";
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
};
|
||||
|
||||
// Returns a server error response
|
||||
auto const server_error =
|
||||
[&req](beast::string_view what)
|
||||
{
|
||||
http::response<http::string_body> res{http::status::internal_server_error, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, "text/html");
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = "An error occurred: '" + std::string(what) + "'";
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
};
|
||||
|
||||
// Make sure we can handle the method
|
||||
if( req.method() != http::verb::get &&
|
||||
req.method() != http::verb::head)
|
||||
return bad_request("Unknown HTTP-method");
|
||||
|
||||
// Request path must be absolute and not contain "..".
|
||||
if( req.target().empty() ||
|
||||
req.target()[0] != '/' ||
|
||||
req.target().find("..") != beast::string_view::npos)
|
||||
return bad_request("Illegal request-target");
|
||||
|
||||
// Build the path to the requested file
|
||||
std::string path = path_cat(doc_root, req.target());
|
||||
if(req.target().back() == '/')
|
||||
path.append("index.html");
|
||||
|
||||
// Attempt to open the file
|
||||
beast::error_code ec;
|
||||
http::file_body::value_type body;
|
||||
body.open(path.c_str(), beast::file_mode::scan, ec);
|
||||
|
||||
// Handle the case where the file doesn't exist
|
||||
if(ec == beast::errc::no_such_file_or_directory)
|
||||
return not_found(req.target());
|
||||
|
||||
// Handle an unknown error
|
||||
if(ec)
|
||||
return server_error(ec.message());
|
||||
|
||||
// Cache the size since we need it after the move
|
||||
auto const size = body.size();
|
||||
|
||||
// Respond to HEAD request
|
||||
if(req.method() == http::verb::head)
|
||||
{
|
||||
http::response<http::empty_body> res{http::status::ok, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, mime_type(path));
|
||||
res.content_length(size);
|
||||
res.keep_alive(req.keep_alive());
|
||||
return res;
|
||||
}
|
||||
|
||||
// Respond to GET request
|
||||
http::response<http::file_body> res{
|
||||
std::piecewise_construct,
|
||||
std::make_tuple(std::move(body)),
|
||||
std::make_tuple(http::status::ok, req.version())};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, mime_type(path));
|
||||
res.content_length(size);
|
||||
res.keep_alive(req.keep_alive());
|
||||
return res;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Report a failure
|
||||
void
|
||||
fail(beast::error_code ec, char const* what)
|
||||
{
|
||||
std::cerr << what << ": " << ec.message() << "\n";
|
||||
}
|
||||
|
||||
// Handles an HTTP server connection
|
||||
class session : public std::enable_shared_from_this<session>
|
||||
{
|
||||
beast::tcp_stream stream_;
|
||||
beast::flat_buffer buffer_;
|
||||
std::shared_ptr<std::string const> doc_root_;
|
||||
http::request<http::string_body> req_;
|
||||
|
||||
public:
|
||||
// Take ownership of the stream
|
||||
session(
|
||||
tcp::socket&& socket,
|
||||
std::shared_ptr<std::string const> const& doc_root)
|
||||
: stream_(std::move(socket))
|
||||
, doc_root_(doc_root)
|
||||
{
|
||||
}
|
||||
|
||||
// Start the asynchronous operation
|
||||
void
|
||||
run()
|
||||
{
|
||||
// We need to be executing within a strand to perform async operations
|
||||
// on the I/O objects in this session. Although not strictly necessary
|
||||
// for single-threaded contexts, this example code is written to be
|
||||
// thread-safe by default.
|
||||
net::dispatch(stream_.get_executor(),
|
||||
beast::bind_front_handler(
|
||||
&session::do_read,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
do_read()
|
||||
{
|
||||
// Make the request empty before reading,
|
||||
// otherwise the operation behavior is undefined.
|
||||
req_ = {};
|
||||
|
||||
// Set the timeout.
|
||||
stream_.expires_after(std::chrono::seconds(30));
|
||||
|
||||
// Read a request
|
||||
http::async_read(stream_, buffer_, req_,
|
||||
beast::bind_front_handler(
|
||||
&session::on_read,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
on_read(
|
||||
beast::error_code ec,
|
||||
std::size_t bytes_transferred)
|
||||
{
|
||||
boost::ignore_unused(bytes_transferred);
|
||||
|
||||
// This means they closed the connection
|
||||
if(ec == http::error::end_of_stream)
|
||||
return do_close();
|
||||
|
||||
if(ec)
|
||||
return fail(ec, "read");
|
||||
|
||||
// Send the response
|
||||
send_response(
|
||||
handle_request(*doc_root_, std::move(req_)));
|
||||
}
|
||||
|
||||
void
|
||||
send_response(http::message_generator&& msg)
|
||||
{
|
||||
bool keep_alive = msg.keep_alive();
|
||||
|
||||
// Write the response
|
||||
beast::async_write(
|
||||
stream_,
|
||||
std::move(msg),
|
||||
beast::bind_front_handler(
|
||||
&session::on_write, shared_from_this(), keep_alive));
|
||||
}
|
||||
|
||||
void
|
||||
on_write(
|
||||
bool keep_alive,
|
||||
beast::error_code ec,
|
||||
std::size_t bytes_transferred)
|
||||
{
|
||||
boost::ignore_unused(bytes_transferred);
|
||||
|
||||
if(ec)
|
||||
return fail(ec, "write");
|
||||
|
||||
if(! keep_alive)
|
||||
{
|
||||
// This means we should close the connection, usually because
|
||||
// the response indicated the "Connection: close" semantic.
|
||||
return do_close();
|
||||
}
|
||||
|
||||
// Read another request
|
||||
do_read();
|
||||
}
|
||||
|
||||
void
|
||||
do_close()
|
||||
{
|
||||
// Send a TCP shutdown
|
||||
beast::error_code ec;
|
||||
stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
|
||||
|
||||
// At this point the connection is closed gracefully
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Accepts incoming connections and launches the sessions
|
||||
class listener : public std::enable_shared_from_this<listener>
|
||||
{
|
||||
net::io_context& ioc_;
|
||||
tcp::acceptor acceptor_;
|
||||
std::shared_ptr<std::string const> doc_root_;
|
||||
|
||||
public:
|
||||
listener(
|
||||
net::io_context& ioc,
|
||||
tcp::endpoint endpoint,
|
||||
std::shared_ptr<std::string const> const& doc_root)
|
||||
: ioc_(ioc)
|
||||
, acceptor_(net::make_strand(ioc))
|
||||
, doc_root_(doc_root)
|
||||
{
|
||||
beast::error_code ec;
|
||||
|
||||
// Open the acceptor
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if(ec)
|
||||
{
|
||||
fail(ec, "open");
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow address reuse
|
||||
acceptor_.set_option(net::socket_base::reuse_address(true), ec);
|
||||
if(ec)
|
||||
{
|
||||
fail(ec, "set_option");
|
||||
return;
|
||||
}
|
||||
|
||||
// Bind to the server address
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if(ec)
|
||||
{
|
||||
fail(ec, "bind");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start listening for connections
|
||||
acceptor_.listen(
|
||||
net::socket_base::max_listen_connections, ec);
|
||||
if(ec)
|
||||
{
|
||||
fail(ec, "listen");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Start accepting incoming connections
|
||||
void
|
||||
run()
|
||||
{
|
||||
do_accept();
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
do_accept()
|
||||
{
|
||||
// The new connection gets its own strand
|
||||
acceptor_.async_accept(
|
||||
net::make_strand(ioc_),
|
||||
beast::bind_front_handler(
|
||||
&listener::on_accept,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
on_accept(beast::error_code ec, tcp::socket socket)
|
||||
{
|
||||
if(ec)
|
||||
{
|
||||
fail(ec, "accept");
|
||||
return; // To avoid infinite loop
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create the session and run it
|
||||
std::make_shared<session>(
|
||||
std::move(socket),
|
||||
doc_root_)->run();
|
||||
}
|
||||
|
||||
// Accept another connection
|
||||
do_accept();
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
|
||||
22
source/BUILD
22
source/BUILD
@ -1,15 +1,21 @@
|
||||
cc_library(
|
||||
name = "headers",
|
||||
hdrs = [
|
||||
"include/parselink/utility/argparse.h",
|
||||
"include/parselink/server.h",
|
||||
],
|
||||
strip_include_prefix = "include/parselink",
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "parselinkd",
|
||||
srcs = ["main.cpp"],
|
||||
deps = [
|
||||
"@fmt",
|
||||
"@boost//:beast",
|
||||
srcs = [
|
||||
"main.cpp",
|
||||
"server.cpp",
|
||||
],
|
||||
)
|
||||
cc_binary(
|
||||
name = "parselinklog",
|
||||
srcs = ["log.cpp"],
|
||||
deps = [
|
||||
"headers",
|
||||
"@boost//:beast",
|
||||
"//source/common"
|
||||
],
|
||||
)
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
# parselink
|
||||
|
||||
cc_library(
|
||||
name = "lib",
|
||||
name = "common",
|
||||
srcs = [
|
||||
"source/logging.cpp",
|
||||
],
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
// Author: Kurt Sassenrath
|
||||
@ -34,7 +34,7 @@ namespace logging {
|
||||
// Any log levels higher (lower severity) than static_threshold cannot not be
|
||||
// enabled in the library, but the compiler should be able to optimize away
|
||||
// some/most of the calls.
|
||||
constexpr inline auto static_threshold = level::verbose;
|
||||
constexpr inline auto static_threshold = level::trace;
|
||||
constexpr inline auto default_threshold = level::info;
|
||||
|
||||
// Structure for holding a message. Note: message is a view over some buffer,
|
||||
@ -80,6 +80,22 @@ public:
|
||||
requires (Level > static_threshold)
|
||||
[[gnu::flatten]] void log(fmt::format_string<Args...>, Args&&...) const {}
|
||||
|
||||
#define LOG_API(lvl) \
|
||||
template <typename... Args> \
|
||||
[[gnu::always_inline]] void lvl(fmt::format_string<Args...>&& format, Args&&... args) const { \
|
||||
log<level::lvl>(std::forward<decltype(format)>(format), std::forward<Args>(args)...); \
|
||||
}
|
||||
|
||||
LOG_API(critical);
|
||||
LOG_API(error);
|
||||
LOG_API(warning);
|
||||
LOG_API(info);
|
||||
LOG_API(verbose);
|
||||
LOG_API(debug);
|
||||
LOG_API(trace);
|
||||
|
||||
#undef LOG_API
|
||||
|
||||
void set_threshold(level new_threshold) noexcept;
|
||||
|
||||
private:
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
// Author: Kurt Sassenrath
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
// Author: Kurt Sassenrath
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
// Author: Kurt Sassenrath
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
// Author: Kurt Sassenrath
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
// Author: Kurt Sassenrath
|
||||
@ -28,8 +28,6 @@ struct console_endpoint : public endpoint {
|
||||
static constexpr std::string_view format_string =
|
||||
"{:%Y-%m-%d %H:%M:%S}.{:03} [{:<8}] {:>20} | {}\n";
|
||||
|
||||
std::span<char> buffer() noexcept override { return buffer_; }
|
||||
|
||||
bool colored() const noexcept override { return true; }
|
||||
|
||||
void write(message const& msg) override {
|
||||
@ -38,8 +36,6 @@ struct console_endpoint : public endpoint {
|
||||
(msg.time.time_since_epoch() % 1000ms) / 1ms,
|
||||
themed_arg{enum_name_only{msg.lvl}}, msg.name, msg.message);
|
||||
}
|
||||
|
||||
std::array<char, 4096> buffer_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
37
source/include/parselink/server.h
Normal file
37
source/include/parselink/server.h
Normal file
@ -0,0 +1,37 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// ___ __ _ _
|
||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
// Author: Kurt Sassenrath
|
||||
// Module: Server
|
||||
//
|
||||
// Server interface.
|
||||
//
|
||||
// Copyright (c) 2023 Kurt Sassenrath.
|
||||
//
|
||||
// License TBD.
|
||||
//-----------------------------------------------------------------------------
|
||||
#ifndef server_5b46f075be3caa00
|
||||
#define server_5b46f075be3caa00
|
||||
|
||||
#include <memory>
|
||||
#include <cstdint>
|
||||
|
||||
namespace parselink {
|
||||
|
||||
class server {
|
||||
public:
|
||||
virtual ~server() = default;
|
||||
virtual std::error_code run() noexcept = 0;
|
||||
};
|
||||
|
||||
std::unique_ptr<server> make_server(std::string_view address,
|
||||
std::uint16_t user_port, std::uint16_t websocket_port);
|
||||
|
||||
} // namespace parselink
|
||||
|
||||
#endif // server_5b46f075be3caa00
|
||||
356
source/include/parselink/utility/argparse.h
Normal file
356
source/include/parselink/utility/argparse.h
Normal file
@ -0,0 +1,356 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// ___ __ _ _
|
||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
// Author: Kurt Sassenrath
|
||||
// Module: Utility
|
||||
//
|
||||
// Command line argument parser. Pretty rough-n-ready, but gets the job done
|
||||
// for starting the server.
|
||||
//
|
||||
// Copyright (c) 2023 Kurt Sassenrath.
|
||||
//
|
||||
// License TBD.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#ifndef argparse_d2ddac0dab0d7b88
|
||||
#define argparse_d2ddac0dab0d7b88
|
||||
|
||||
#include <chrono>
|
||||
#include <charconv>
|
||||
#include <initializer_list>
|
||||
#include <optional>
|
||||
#include <map>
|
||||
#include <span>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <variant>
|
||||
#include <vector>
|
||||
|
||||
// Simple command line parser for testing executables.
|
||||
|
||||
namespace argparse {
|
||||
|
||||
namespace custom {
|
||||
template <typename T>
|
||||
struct argument_parser {};
|
||||
|
||||
template <typename T>
|
||||
concept has_parser = requires {
|
||||
{ argument_parser<std::decay_t<T>>::parse(std::string_view{}) }
|
||||
-> std::same_as<T*>;
|
||||
};
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
struct argument_parser {};
|
||||
|
||||
template <>
|
||||
struct argument_parser<bool> {
|
||||
static bool* parse(std::string_view value) noexcept {
|
||||
if (value == "1" || value == "true") {
|
||||
return new bool{true};
|
||||
} else if (value == "0" || value == "false") {
|
||||
return new bool{false};
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
inline constexpr std::initializer_list<
|
||||
std::tuple<char const*, std::chrono::nanoseconds>> unit_map = {
|
||||
{"ns", std::chrono::nanoseconds{1}},
|
||||
{"us", std::chrono::microseconds{1}},
|
||||
{"ms", std::chrono::milliseconds{1}},
|
||||
{"s", std::chrono::seconds{1}},
|
||||
{"m", std::chrono::minutes{1}},
|
||||
{"h", std::chrono::hours{1}},
|
||||
};
|
||||
|
||||
template <typename rep, typename period>
|
||||
struct argument_parser<std::chrono::duration<rep, period>> {
|
||||
using duration = std::chrono::duration<rep, period>;
|
||||
|
||||
static duration* parse(std::string_view value) noexcept {
|
||||
rep result;
|
||||
auto err = std::from_chars(value.begin(), value.end(), result);
|
||||
if (err.ec == std::errc{}) {
|
||||
auto this_unit = std::string_view{err.ptr, value.end()};
|
||||
for (auto const& [unit, dura] : unit_map) {
|
||||
if (std::string_view{unit} == this_unit) {
|
||||
auto v =
|
||||
std::chrono::duration_cast<duration>(result * dura);
|
||||
return new duration{v};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
requires requires (T& t) {
|
||||
std::from_chars(nullptr, nullptr, t);
|
||||
}
|
||||
struct argument_parser<T> {
|
||||
static T* parse(std::string_view value) noexcept {
|
||||
T result;
|
||||
auto err = std::from_chars(value.begin(), value.end(), result);
|
||||
return err.ec == std::errc{} ? new T{result} : nullptr;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
struct argument_parser<std::string> {
|
||||
static std::string* parse(std::string_view value) noexcept {
|
||||
return new std::string{value};
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
concept has_parser = requires {
|
||||
{ argument_parser<std::decay_t<T>>::parse(std::string_view{}) }
|
||||
-> std::same_as<T*>;
|
||||
};
|
||||
|
||||
static_assert(has_parser<int>);
|
||||
|
||||
namespace detail {
|
||||
|
||||
// This wrapper acts similar to std::any, but also provides a method to
|
||||
// parse a string_view for the value as well. Parsers can be implemented
|
||||
// by creating an argparse::custom::argument_parser template specialization
|
||||
// for a given type.
|
||||
struct any_arg {
|
||||
|
||||
constexpr any_arg() = default;
|
||||
|
||||
template <typename T>
|
||||
any_arg(T&& value) : iface(&dispatcher<std::decay_t<T>>::table) {
|
||||
dispatcher<std::decay_t<T>>::create(*this, std::forward<T>(value));
|
||||
}
|
||||
|
||||
any_arg(any_arg const& other) : iface(other.iface) {
|
||||
if (other.iface) {
|
||||
data = other.iface->copy(other.data);
|
||||
}
|
||||
}
|
||||
|
||||
any_arg(any_arg&& other) : data(std::move(other.data)), iface(other.iface) {
|
||||
other.iface = nullptr;
|
||||
}
|
||||
|
||||
any_arg& operator=(any_arg const& other) {
|
||||
if (has_value()) {
|
||||
iface->destroy(data);
|
||||
}
|
||||
iface = other.iface;
|
||||
if (other.iface) {
|
||||
data = other.iface->copy(other.data);
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
~any_arg() {
|
||||
if (has_value()) {
|
||||
iface->destroy(data);
|
||||
}
|
||||
}
|
||||
|
||||
bool parse(std::string_view sv) {
|
||||
return iface->parse(*this, sv);
|
||||
}
|
||||
|
||||
bool has_value() const noexcept {
|
||||
return static_cast<bool>(iface);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
bool holds() const noexcept {
|
||||
return iface == &dispatcher<std::decay_t<T>>::table;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
friend T const* arg_cast(any_arg const*) noexcept;
|
||||
|
||||
void* data = nullptr;
|
||||
struct interface {
|
||||
void (*destroy)(void*);
|
||||
void *(*copy)(void*);
|
||||
bool (*parse)(any_arg&, std::string_view);
|
||||
};
|
||||
|
||||
interface const* iface = nullptr;
|
||||
|
||||
template <typename T>
|
||||
struct dispatcher {
|
||||
template <typename... Args>
|
||||
static void create(any_arg& self, Args&&... args) {
|
||||
self.data = new T{std::forward<Args>(args)...};
|
||||
}
|
||||
|
||||
static void* copy(void *ptr) {
|
||||
return new T{*static_cast<T*>(ptr)};
|
||||
}
|
||||
|
||||
static bool parse(any_arg& self, std::string_view sv) {
|
||||
if constexpr (custom::has_parser<T>) {
|
||||
self.data = custom::argument_parser<T>::parse(sv);
|
||||
return static_cast<bool>(self.data);
|
||||
} else if constexpr (has_parser<T>) {
|
||||
self.data = argument_parser<T>::parse(sv);
|
||||
return static_cast<bool>(self.data);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static void destroy(void* ptr) {
|
||||
delete static_cast<T*>(ptr);
|
||||
}
|
||||
|
||||
static T const* cast(void* ptr) {
|
||||
return static_cast<T const*>(ptr);
|
||||
}
|
||||
|
||||
static constexpr struct interface table {
|
||||
&dispatcher::destroy, &dispatcher::copy, &dispatcher::parse };
|
||||
};
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
T const* arg_cast(any_arg const* ar) noexcept {
|
||||
if (ar->holds<T>()) {
|
||||
return any_arg::dispatcher<std::decay_t<T>>::cast(ar->data);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
class command_line_parser {
|
||||
public:
|
||||
|
||||
using argument = detail::any_arg;
|
||||
constexpr static char delimiter = '=';
|
||||
using opt_list = std::initializer_list<
|
||||
std::tuple<std::string_view, argument>>;
|
||||
|
||||
struct result {
|
||||
enum class code {
|
||||
no_error,
|
||||
unknown_option,
|
||||
bad_option_value,
|
||||
};
|
||||
|
||||
code ec = code::no_error;
|
||||
std::string error_value;
|
||||
std::map<std::string, argument, std::less<>> opts;
|
||||
std::vector<std::string> arguments;
|
||||
|
||||
explicit constexpr operator bool() const noexcept {
|
||||
return ec == code::no_error;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T const* maybe_opt(std::string_view name) const noexcept {
|
||||
auto entry = opts.find(name);
|
||||
return entry != opts.end() ?
|
||||
detail::arg_cast<T>(&entry->second) : nullptr;
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T const& opt(std::string_view opt_name) const {
|
||||
auto const* v = maybe_opt<T>(opt_name);
|
||||
if (!v) {
|
||||
throw std::bad_cast{};
|
||||
}
|
||||
return *v;
|
||||
}
|
||||
|
||||
std::string_view operator[](std::size_t index) const {
|
||||
return arguments[index];
|
||||
}
|
||||
};
|
||||
|
||||
command_line_parser() noexcept = default;
|
||||
|
||||
explicit command_line_parser(opt_list opts) {
|
||||
for (auto const& [name, value_type] : opts) {
|
||||
add_option(name, value_type);
|
||||
}
|
||||
}
|
||||
|
||||
bool add_option(std::string_view name, argument value) {
|
||||
options_[std::string{name}] = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
result parse(std::span<std::string_view> arglist) {
|
||||
return parse_inner(arglist);
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
T const* get_option(std::string_view name) {
|
||||
auto entry = options_.find(name);
|
||||
if (entry != options_.end()) {
|
||||
return detail::arg_cast<T>(&entry->second);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
private:
|
||||
|
||||
static auto split_option(std::string_view kv) {
|
||||
auto delim = kv.find(delimiter);
|
||||
if (delim != kv.npos) {
|
||||
return std::make_tuple(kv.substr(0, delim), kv.substr(delim + 1));
|
||||
} else {
|
||||
return std::make_tuple(kv, std::string_view{});
|
||||
}
|
||||
}
|
||||
|
||||
result parse_inner(std::span<std::string_view> arglist) {
|
||||
result res;
|
||||
|
||||
for (auto const& [key, value] : options_) {
|
||||
res.opts[key] = value;
|
||||
}
|
||||
|
||||
for (auto ar : arglist) {
|
||||
if (ar.empty()) continue; // ???
|
||||
if (ar.substr(0, 2) == "--") {
|
||||
auto [key, value] = split_option(ar.substr(2));
|
||||
auto opt_entry = res.opts.find(key);
|
||||
if (opt_entry == res.opts.end()) {
|
||||
res.ec = result::code::unknown_option;
|
||||
res.error_value = std::string{ar};
|
||||
return res;
|
||||
}
|
||||
|
||||
if (!opt_entry->second.parse(value)) {
|
||||
res.ec = result::code::bad_option_value;
|
||||
res.error_value = std::string{value};
|
||||
return res;
|
||||
}
|
||||
} else {
|
||||
res.arguments.emplace_back(ar);
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
std::map<std::string, argument, std::less<>> options_;
|
||||
std::vector<std::string> arguments_;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
#endif // argparse_d2ddac0dab0d7b88
|
||||
@ -1,13 +0,0 @@
|
||||
#include <logging.h>
|
||||
|
||||
using namespace parselink;
|
||||
using level = parselink::logging::level;
|
||||
|
||||
namespace {
|
||||
logging::logger logger("parselog");
|
||||
}
|
||||
|
||||
int main(int argc, char**) {
|
||||
logger.log<level::critical>("Found {} arguments", argc);
|
||||
return 0;
|
||||
}
|
||||
478
source/main.cpp
478
source/main.cpp
@ -1,444 +1,54 @@
|
||||
#include <boost/beast/core.hpp>
|
||||
#include <boost/beast/http.hpp>
|
||||
#include <boost/beast/version.hpp>
|
||||
#include <boost/asio/dispatch.hpp>
|
||||
#include <boost/asio/strand.hpp>
|
||||
#include <boost/config.hpp>
|
||||
#include <algorithm>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
#include <logging.h>
|
||||
#include <utility/argparse.h>
|
||||
#include <server.h>
|
||||
|
||||
namespace beast = boost::beast; // from <boost/beast.hpp>
|
||||
namespace http = beast::http; // from <boost/beast/http.hpp>
|
||||
namespace net = boost::asio; // from <boost/asio.hpp>
|
||||
using tcp = boost::asio::ip::tcp; // from <boost/asio/ip/tcp.hpp>
|
||||
|
||||
// Return a reasonable mime type based on the extension of a file.
|
||||
beast::string_view
|
||||
mime_type(beast::string_view path)
|
||||
{
|
||||
using beast::iequals;
|
||||
auto const ext = [&path]
|
||||
{
|
||||
auto const pos = path.rfind(".");
|
||||
if(pos == beast::string_view::npos)
|
||||
return beast::string_view{};
|
||||
return path.substr(pos);
|
||||
}();
|
||||
if(iequals(ext, ".htm")) return "text/html";
|
||||
if(iequals(ext, ".html")) return "text/html";
|
||||
if(iequals(ext, ".php")) return "text/html";
|
||||
if(iequals(ext, ".css")) return "text/css";
|
||||
if(iequals(ext, ".txt")) return "text/plain";
|
||||
if(iequals(ext, ".js")) return "application/javascript";
|
||||
if(iequals(ext, ".json")) return "application/json";
|
||||
if(iequals(ext, ".xml")) return "application/xml";
|
||||
if(iequals(ext, ".swf")) return "application/x-shockwave-flash";
|
||||
if(iequals(ext, ".flv")) return "video/x-flv";
|
||||
if(iequals(ext, ".png")) return "image/png";
|
||||
if(iequals(ext, ".jpe")) return "image/jpeg";
|
||||
if(iequals(ext, ".jpeg")) return "image/jpeg";
|
||||
if(iequals(ext, ".jpg")) return "image/jpeg";
|
||||
if(iequals(ext, ".gif")) return "image/gif";
|
||||
if(iequals(ext, ".bmp")) return "image/bmp";
|
||||
if(iequals(ext, ".ico")) return "image/vnd.microsoft.icon";
|
||||
if(iequals(ext, ".tiff")) return "image/tiff";
|
||||
if(iequals(ext, ".tif")) return "image/tiff";
|
||||
if(iequals(ext, ".svg")) return "image/svg+xml";
|
||||
if(iequals(ext, ".svgz")) return "image/svg+xml";
|
||||
return "application/text";
|
||||
namespace {
|
||||
parselink::logging::logger logger("main");
|
||||
}
|
||||
|
||||
// Append an HTTP rel-path to a local filesystem path.
|
||||
// The returned path is normalized for the platform.
|
||||
std::string
|
||||
path_cat(
|
||||
beast::string_view base,
|
||||
beast::string_view path)
|
||||
{
|
||||
if(base.empty())
|
||||
return std::string(path);
|
||||
std::string result(base);
|
||||
#ifdef BOOST_MSVC
|
||||
char constexpr path_separator = '\\';
|
||||
if(result.back() == path_separator)
|
||||
result.resize(result.size() - 1);
|
||||
result.append(path.data(), path.size());
|
||||
for(auto& c : result)
|
||||
if(c == '/')
|
||||
c = path_separator;
|
||||
#else
|
||||
char constexpr path_separator = '/';
|
||||
if(result.back() == path_separator)
|
||||
result.resize(result.size() - 1);
|
||||
result.append(path.data(), path.size());
|
||||
#endif
|
||||
return result;
|
||||
using level = parselink::logging::level;
|
||||
|
||||
int run(std::span<std::string_view> arg_list) {
|
||||
argparse::command_line_parser parser({
|
||||
{"address", {std::string{"0.0.0.0"}}},
|
||||
{"user_port", {std::uint16_t{9001}}},
|
||||
{"websocket_port", {std::uint16_t{10501}}},
|
||||
{"verbose", {false}},
|
||||
});
|
||||
|
||||
auto args = parser.parse(arg_list);
|
||||
|
||||
if (args.ec != argparse::command_line_parser::result::code::no_error) {
|
||||
logger.error("Failed to parse arguments: {} ({})", args.ec,
|
||||
args.error_value);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (args.opt<bool>("verbose")) {
|
||||
logger.set_threshold(level::trace);
|
||||
}
|
||||
|
||||
|
||||
|
||||
auto server = parselink::make_server(args.opt<std::string>("address"),
|
||||
args.opt<std::uint16_t>("user_port"),
|
||||
args.opt<std::uint16_t>("websocket_port"));
|
||||
|
||||
if (server) {
|
||||
server->run();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Return a response for the given request.
|
||||
//
|
||||
// The concrete type of the response message (which depends on the
|
||||
// request), is type-erased in message_generator.
|
||||
template <class Body, class Allocator>
|
||||
http::message_generator
|
||||
handle_request(
|
||||
beast::string_view doc_root,
|
||||
http::request<Body, http::basic_fields<Allocator>>&& req)
|
||||
{
|
||||
// Returns a bad request response
|
||||
auto const bad_request =
|
||||
[&req](beast::string_view why)
|
||||
{
|
||||
http::response<http::string_body> res{http::status::bad_request, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, "text/html");
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = std::string(why);
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
};
|
||||
|
||||
// Returns a not found response
|
||||
auto const not_found =
|
||||
[&req](beast::string_view target)
|
||||
{
|
||||
http::response<http::string_body> res{http::status::not_found, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, "text/html");
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = "The resource '" + std::string(target) + "' was not found.";
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
};
|
||||
|
||||
// Returns a server error response
|
||||
auto const server_error =
|
||||
[&req](beast::string_view what)
|
||||
{
|
||||
http::response<http::string_body> res{http::status::internal_server_error, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, "text/html");
|
||||
res.keep_alive(req.keep_alive());
|
||||
res.body() = "An error occurred: '" + std::string(what) + "'";
|
||||
res.prepare_payload();
|
||||
return res;
|
||||
};
|
||||
|
||||
// Make sure we can handle the method
|
||||
if( req.method() != http::verb::get &&
|
||||
req.method() != http::verb::head)
|
||||
return bad_request("Unknown HTTP-method");
|
||||
|
||||
// Request path must be absolute and not contain "..".
|
||||
if( req.target().empty() ||
|
||||
req.target()[0] != '/' ||
|
||||
req.target().find("..") != beast::string_view::npos)
|
||||
return bad_request("Illegal request-target");
|
||||
|
||||
// Build the path to the requested file
|
||||
std::string path = path_cat(doc_root, req.target());
|
||||
if(req.target().back() == '/')
|
||||
path.append("index.html");
|
||||
|
||||
// Attempt to open the file
|
||||
beast::error_code ec;
|
||||
http::file_body::value_type body;
|
||||
body.open(path.c_str(), beast::file_mode::scan, ec);
|
||||
|
||||
// Handle the case where the file doesn't exist
|
||||
if(ec == beast::errc::no_such_file_or_directory)
|
||||
return not_found(req.target());
|
||||
|
||||
// Handle an unknown error
|
||||
if(ec)
|
||||
return server_error(ec.message());
|
||||
|
||||
// Cache the size since we need it after the move
|
||||
auto const size = body.size();
|
||||
|
||||
// Respond to HEAD request
|
||||
if(req.method() == http::verb::head)
|
||||
{
|
||||
http::response<http::empty_body> res{http::status::ok, req.version()};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, mime_type(path));
|
||||
res.content_length(size);
|
||||
res.keep_alive(req.keep_alive());
|
||||
return res;
|
||||
}
|
||||
|
||||
// Respond to GET request
|
||||
http::response<http::file_body> res{
|
||||
std::piecewise_construct,
|
||||
std::make_tuple(std::move(body)),
|
||||
std::make_tuple(http::status::ok, req.version())};
|
||||
res.set(http::field::server, BOOST_BEAST_VERSION_STRING);
|
||||
res.set(http::field::content_type, mime_type(path));
|
||||
res.content_length(size);
|
||||
res.keep_alive(req.keep_alive());
|
||||
return res;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Report a failure
|
||||
void
|
||||
fail(beast::error_code ec, char const* what)
|
||||
{
|
||||
std::cerr << what << ": " << ec.message() << "\n";
|
||||
}
|
||||
|
||||
// Handles an HTTP server connection
|
||||
class session : public std::enable_shared_from_this<session>
|
||||
{
|
||||
beast::tcp_stream stream_;
|
||||
beast::flat_buffer buffer_;
|
||||
std::shared_ptr<std::string const> doc_root_;
|
||||
http::request<http::string_body> req_;
|
||||
|
||||
public:
|
||||
// Take ownership of the stream
|
||||
session(
|
||||
tcp::socket&& socket,
|
||||
std::shared_ptr<std::string const> const& doc_root)
|
||||
: stream_(std::move(socket))
|
||||
, doc_root_(doc_root)
|
||||
{
|
||||
}
|
||||
|
||||
// Start the asynchronous operation
|
||||
void
|
||||
run()
|
||||
{
|
||||
// We need to be executing within a strand to perform async operations
|
||||
// on the I/O objects in this session. Although not strictly necessary
|
||||
// for single-threaded contexts, this example code is written to be
|
||||
// thread-safe by default.
|
||||
net::dispatch(stream_.get_executor(),
|
||||
beast::bind_front_handler(
|
||||
&session::do_read,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
do_read()
|
||||
{
|
||||
// Make the request empty before reading,
|
||||
// otherwise the operation behavior is undefined.
|
||||
req_ = {};
|
||||
|
||||
// Set the timeout.
|
||||
stream_.expires_after(std::chrono::seconds(30));
|
||||
|
||||
// Read a request
|
||||
http::async_read(stream_, buffer_, req_,
|
||||
beast::bind_front_handler(
|
||||
&session::on_read,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
on_read(
|
||||
beast::error_code ec,
|
||||
std::size_t bytes_transferred)
|
||||
{
|
||||
boost::ignore_unused(bytes_transferred);
|
||||
|
||||
// This means they closed the connection
|
||||
if(ec == http::error::end_of_stream)
|
||||
return do_close();
|
||||
|
||||
if(ec)
|
||||
return fail(ec, "read");
|
||||
|
||||
// Send the response
|
||||
send_response(
|
||||
handle_request(*doc_root_, std::move(req_)));
|
||||
}
|
||||
|
||||
void
|
||||
send_response(http::message_generator&& msg)
|
||||
{
|
||||
bool keep_alive = msg.keep_alive();
|
||||
|
||||
// Write the response
|
||||
beast::async_write(
|
||||
stream_,
|
||||
std::move(msg),
|
||||
beast::bind_front_handler(
|
||||
&session::on_write, shared_from_this(), keep_alive));
|
||||
}
|
||||
|
||||
void
|
||||
on_write(
|
||||
bool keep_alive,
|
||||
beast::error_code ec,
|
||||
std::size_t bytes_transferred)
|
||||
{
|
||||
boost::ignore_unused(bytes_transferred);
|
||||
|
||||
if(ec)
|
||||
return fail(ec, "write");
|
||||
|
||||
if(! keep_alive)
|
||||
{
|
||||
// This means we should close the connection, usually because
|
||||
// the response indicated the "Connection: close" semantic.
|
||||
return do_close();
|
||||
}
|
||||
|
||||
// Read another request
|
||||
do_read();
|
||||
}
|
||||
|
||||
void
|
||||
do_close()
|
||||
{
|
||||
// Send a TCP shutdown
|
||||
beast::error_code ec;
|
||||
stream_.socket().shutdown(tcp::socket::shutdown_send, ec);
|
||||
|
||||
// At this point the connection is closed gracefully
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Accepts incoming connections and launches the sessions
|
||||
class listener : public std::enable_shared_from_this<listener>
|
||||
{
|
||||
net::io_context& ioc_;
|
||||
tcp::acceptor acceptor_;
|
||||
std::shared_ptr<std::string const> doc_root_;
|
||||
|
||||
public:
|
||||
listener(
|
||||
net::io_context& ioc,
|
||||
tcp::endpoint endpoint,
|
||||
std::shared_ptr<std::string const> const& doc_root)
|
||||
: ioc_(ioc)
|
||||
, acceptor_(net::make_strand(ioc))
|
||||
, doc_root_(doc_root)
|
||||
{
|
||||
beast::error_code ec;
|
||||
|
||||
// Open the acceptor
|
||||
acceptor_.open(endpoint.protocol(), ec);
|
||||
if(ec)
|
||||
{
|
||||
fail(ec, "open");
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow address reuse
|
||||
acceptor_.set_option(net::socket_base::reuse_address(true), ec);
|
||||
if(ec)
|
||||
{
|
||||
fail(ec, "set_option");
|
||||
return;
|
||||
}
|
||||
|
||||
// Bind to the server address
|
||||
acceptor_.bind(endpoint, ec);
|
||||
if(ec)
|
||||
{
|
||||
fail(ec, "bind");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start listening for connections
|
||||
acceptor_.listen(
|
||||
net::socket_base::max_listen_connections, ec);
|
||||
if(ec)
|
||||
{
|
||||
fail(ec, "listen");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Start accepting incoming connections
|
||||
void
|
||||
run()
|
||||
{
|
||||
do_accept();
|
||||
}
|
||||
|
||||
private:
|
||||
void
|
||||
do_accept()
|
||||
{
|
||||
// The new connection gets its own strand
|
||||
acceptor_.async_accept(
|
||||
net::make_strand(ioc_),
|
||||
beast::bind_front_handler(
|
||||
&listener::on_accept,
|
||||
shared_from_this()));
|
||||
}
|
||||
|
||||
void
|
||||
on_accept(beast::error_code ec, tcp::socket socket)
|
||||
{
|
||||
if(ec)
|
||||
{
|
||||
fail(ec, "accept");
|
||||
return; // To avoid infinite loop
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create the session and run it
|
||||
std::make_shared<session>(
|
||||
std::move(socket),
|
||||
doc_root_)->run();
|
||||
}
|
||||
|
||||
// Accept another connection
|
||||
do_accept();
|
||||
}
|
||||
};
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
// Check command line arguments.
|
||||
if (argc != 5)
|
||||
{
|
||||
std::cerr <<
|
||||
"Usage: http-server-async <address> <port> <doc_root> <threads>\n" <<
|
||||
"Example:\n" <<
|
||||
" http-server-async 0.0.0.0 8080 . 1\n";
|
||||
return EXIT_FAILURE;
|
||||
// TODO(ksassenrath): Add configuration file to the mix.
|
||||
|
||||
std::vector<std::string_view> args;
|
||||
for (int i = 1; i < argc; ++i) {
|
||||
args.emplace_back(argv[i]);
|
||||
}
|
||||
auto const address = net::ip::make_address(argv[1]);
|
||||
auto const port = static_cast<unsigned short>(std::atoi(argv[2]));
|
||||
auto const doc_root = std::make_shared<std::string>(argv[3]);
|
||||
auto const threads = std::max<int>(1, std::atoi(argv[4]));
|
||||
|
||||
// The io_context is required for all I/O
|
||||
net::io_context ioc{threads};
|
||||
|
||||
// Create and launch a listening port
|
||||
std::make_shared<listener>(
|
||||
ioc,
|
||||
tcp::endpoint{address, port},
|
||||
doc_root)->run();
|
||||
|
||||
// Run the I/O service on the requested number of threads
|
||||
std::vector<std::thread> v;
|
||||
v.reserve(threads - 1);
|
||||
for(auto i = threads - 1; i > 0; --i)
|
||||
v.emplace_back(
|
||||
[&ioc]
|
||||
{
|
||||
ioc.run();
|
||||
});
|
||||
ioc.run();
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
return run(args);
|
||||
}
|
||||
|
||||
79
source/server.cpp
Normal file
79
source/server.cpp
Normal file
@ -0,0 +1,79 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// ___ __ _ _
|
||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
// Author: Kurt Sassenrath
|
||||
// Module: Server
|
||||
//
|
||||
// Server implementation. Currently, a monolithic server which:
|
||||
// * Communicates with users via TCP (msgpack).
|
||||
// * Runs the websocket server for overlays to read.
|
||||
//
|
||||
// Copyright (c) 2023 Kurt Sassenrath.
|
||||
//
|
||||
// License TBD.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <logging.h>
|
||||
#include <server.h>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
#include <boost/asio/ip/address.hpp>
|
||||
#include <boost/asio/ip/tcp.hpp>
|
||||
#include <boost/asio/signal_set.hpp>
|
||||
|
||||
namespace net = boost::asio;
|
||||
using namespace parselink;
|
||||
|
||||
namespace {
|
||||
logging::logger logger("server");
|
||||
}
|
||||
|
||||
|
||||
class monolithic_server : public server {
|
||||
public:
|
||||
monolithic_server(std::string_view address, std::uint16_t user_port,
|
||||
std::uint16_t websocket_port);
|
||||
|
||||
std::error_code run() noexcept override;
|
||||
|
||||
private:
|
||||
net::io_context io_context_;
|
||||
net::ip::address addr_;
|
||||
net::ip::tcp::acceptor user_acceptor_;
|
||||
};
|
||||
|
||||
|
||||
monolithic_server::monolithic_server(std::string_view address,
|
||||
std::uint16_t user_port, std::uint16_t websocket_port)
|
||||
: io_context_{1}
|
||||
, addr_(net::ip::address::from_string(std::string{address}))
|
||||
, user_acceptor_{io_context_, {addr_, user_port}} {
|
||||
logger.debug("Creating monolithic_server with"
|
||||
"\n\taddress {},\n\tuser_port {},\n\twebsocket_port {}",
|
||||
address, user_port, websocket_port);
|
||||
}
|
||||
|
||||
std::error_code monolithic_server::run() noexcept {
|
||||
logger.info("Starting server.");
|
||||
|
||||
net::signal_set signals(io_context_, SIGINT, SIGTERM);
|
||||
signals.async_wait([&](auto, auto){
|
||||
logger.info("Received signal... Shutting down.");
|
||||
io_context_.stop();
|
||||
});
|
||||
|
||||
io_context_.run();
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::unique_ptr<server> parselink::make_server(std::string_view address,
|
||||
std::uint16_t user_port, std::uint16_t websocket_port) {
|
||||
using impl = monolithic_server;
|
||||
return std::make_unique<impl>(address, user_port, websocket_port);
|
||||
}
|
||||
@ -2,7 +2,7 @@ cc_test(
|
||||
name = "logging",
|
||||
srcs = ["logging.cpp"],
|
||||
deps = [
|
||||
"//source/common:lib",
|
||||
"//source/common",
|
||||
"@ut",
|
||||
],
|
||||
)
|
||||
|
||||
@ -1,3 +1,21 @@
|
||||
//-----------------------------------------------------------------------------
|
||||
// ___ __ _ _
|
||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||
//
|
||||
//-----------------------------------------------------------------------------
|
||||
// Author: Kurt Sassenrath
|
||||
// Module: Tests
|
||||
//
|
||||
// Logging tests.
|
||||
//
|
||||
// Copyright (c) 2023 Kurt Sassenrath.
|
||||
//
|
||||
// License TBD.
|
||||
//-----------------------------------------------------------------------------
|
||||
|
||||
#include <logging.h>
|
||||
|
||||
#include <boost/ut.hpp>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user