Initial server interface, minor logging changes.

This commit is contained in:
Kurt Sassenrath 2023-09-04 22:46:33 -07:00
parent b0ed20369f
commit eab689909d
17 changed files with 982 additions and 469 deletions

2
BUILD
View File

@ -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
View 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();
}
};
//------------------------------------------------------------------------------

View File

@ -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"
],
)

View File

@ -1,7 +1,7 @@
# parselink
cc_library(
name = "lib",
name = "common",
srcs = [
"source/logging.cpp",
],

View File

@ -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:

View File

@ -3,7 +3,7 @@
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
// / ___/ (_| | | \__ \ __/ /__| | | | | <
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
//
//-----------------------------------------------------------------------------
// Author: Kurt Sassenrath

View File

@ -3,7 +3,7 @@
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
// / ___/ (_| | | \__ \ __/ /__| | | | | <
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
//
//-----------------------------------------------------------------------------
// Author: Kurt Sassenrath

View File

@ -3,7 +3,7 @@
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
// / ___/ (_| | | \__ \ __/ /__| | | | | <
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
//
//-----------------------------------------------------------------------------
// Author: Kurt Sassenrath

View File

@ -3,7 +3,7 @@
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
// / ___/ (_| | | \__ \ __/ /__| | | | | <
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
//
//-----------------------------------------------------------------------------
// Author: Kurt Sassenrath

View File

@ -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_;
};
}

View 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

View 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

View File

@ -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;
}

View File

@ -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
View 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);
}

View File

@ -2,7 +2,7 @@ cc_test(
name = "logging",
srcs = ["logging.cpp"],
deps = [
"//source/common:lib",
"//source/common",
"@ut",
],
)

View File

@ -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>