From eab689909dd2ffc9368b22b30e2db2becdd8b7c1 Mon Sep 17 00:00:00 2001 From: Kurt Sassenrath Date: Mon, 4 Sep 2023 22:46:33 -0700 Subject: [PATCH] Initial server interface, minor logging changes. --- BUILD | 2 +- beastref.cpp | 408 +++++++++++++++++ source/BUILD | 22 +- source/common/BUILD | 2 +- source/common/include/logging.h | 20 +- source/common/include/logging/formatters.h | 2 +- source/common/include/logging/level.h | 2 +- source/common/include/logging/theme.h | 2 +- source/common/include/logging/traits.h | 2 +- source/common/source/logging.cpp | 6 +- source/include/parselink/server.h | 37 ++ source/include/parselink/utility/argparse.h | 356 +++++++++++++++ source/log.cpp | 13 - source/main.cpp | 478 ++------------------ source/server.cpp | 79 ++++ tests/common/BUILD | 2 +- tests/common/logging.cpp | 18 + 17 files changed, 982 insertions(+), 469 deletions(-) create mode 100644 beastref.cpp create mode 100644 source/include/parselink/server.h create mode 100644 source/include/parselink/utility/argparse.h delete mode 100644 source/log.cpp create mode 100644 source/server.cpp diff --git a/BUILD b/BUILD index f3b23d6..2fd7caf 100644 --- a/BUILD +++ b/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/...": "", }, ) diff --git a/beastref.cpp b/beastref.cpp new file mode 100644 index 0000000..9811ece --- /dev/null +++ b/beastref.cpp @@ -0,0 +1,408 @@ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace beast = boost::beast; // from +namespace http = beast::http; // from +namespace net = boost::asio; // from +using tcp = boost::asio::ip::tcp; // from + +// 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 +http::message_generator +handle_request( + beast::string_view doc_root, + http::request>&& req) +{ + // Returns a bad request response + auto const bad_request = + [&req](beast::string_view why) + { + http::response 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 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 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 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 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 +{ + beast::tcp_stream stream_; + beast::flat_buffer buffer_; + std::shared_ptr doc_root_; + http::request req_; + +public: + // Take ownership of the stream + session( + tcp::socket&& socket, + std::shared_ptr 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 +{ + net::io_context& ioc_; + tcp::acceptor acceptor_; + std::shared_ptr doc_root_; + +public: + listener( + net::io_context& ioc, + tcp::endpoint endpoint, + std::shared_ptr 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( + std::move(socket), + doc_root_)->run(); + } + + // Accept another connection + do_accept(); + } +}; + +//------------------------------------------------------------------------------ + + diff --git a/source/BUILD b/source/BUILD index ef17b82..237fee6 100644 --- a/source/BUILD +++ b/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" ], ) diff --git a/source/common/BUILD b/source/common/BUILD index ae97318..da0dad8 100644 --- a/source/common/BUILD +++ b/source/common/BUILD @@ -1,7 +1,7 @@ # parselink cc_library( - name = "lib", + name = "common", srcs = [ "source/logging.cpp", ], diff --git a/source/common/include/logging.h b/source/common/include/logging.h index 05aac2d..66452f9 100644 --- a/source/common/include/logging.h +++ b/source/common/include/logging.h @@ -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&&...) const {} +#define LOG_API(lvl) \ + template \ + [[gnu::always_inline]] void lvl(fmt::format_string&& format, Args&&... args) const { \ + log(std::forward(format), std::forward(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: diff --git a/source/common/include/logging/formatters.h b/source/common/include/logging/formatters.h index 9b33675..a6dcf40 100644 --- a/source/common/include/logging/formatters.h +++ b/source/common/include/logging/formatters.h @@ -3,7 +3,7 @@ // / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ // / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / // / ___/ (_| | | \__ \ __/ /__| | | | | < -// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . // //----------------------------------------------------------------------------- // Author: Kurt Sassenrath diff --git a/source/common/include/logging/level.h b/source/common/include/logging/level.h index bf75f47..8f023ac 100644 --- a/source/common/include/logging/level.h +++ b/source/common/include/logging/level.h @@ -3,7 +3,7 @@ // / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ // / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / // / ___/ (_| | | \__ \ __/ /__| | | | | < -// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . // //----------------------------------------------------------------------------- // Author: Kurt Sassenrath diff --git a/source/common/include/logging/theme.h b/source/common/include/logging/theme.h index 5657d7d..8084dd7 100644 --- a/source/common/include/logging/theme.h +++ b/source/common/include/logging/theme.h @@ -3,7 +3,7 @@ // / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ // / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / // / ___/ (_| | | \__ \ __/ /__| | | | | < -// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . // //----------------------------------------------------------------------------- // Author: Kurt Sassenrath diff --git a/source/common/include/logging/traits.h b/source/common/include/logging/traits.h index 37c145d..b05df02 100644 --- a/source/common/include/logging/traits.h +++ b/source/common/include/logging/traits.h @@ -3,7 +3,7 @@ // / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ // / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / // / ___/ (_| | | \__ \ __/ /__| | | | | < -// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . // //----------------------------------------------------------------------------- // Author: Kurt Sassenrath diff --git a/source/common/source/logging.cpp b/source/common/source/logging.cpp index dc4d4f8..5920404 100644 --- a/source/common/source/logging.cpp +++ b/source/common/source/logging.cpp @@ -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 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 buffer_; }; } diff --git a/source/include/parselink/server.h b/source/include/parselink/server.h new file mode 100644 index 0000000..137ba76 --- /dev/null +++ b/source/include/parselink/server.h @@ -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 +#include + +namespace parselink { + +class server { +public: + virtual ~server() = default; + virtual std::error_code run() noexcept = 0; +}; + +std::unique_ptr make_server(std::string_view address, + std::uint16_t user_port, std::uint16_t websocket_port); + +} // namespace parselink + +#endif // server_5b46f075be3caa00 diff --git a/source/include/parselink/utility/argparse.h b/source/include/parselink/utility/argparse.h new file mode 100644 index 0000000..a41bf86 --- /dev/null +++ b/source/include/parselink/utility/argparse.h @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Simple command line parser for testing executables. + +namespace argparse { + + namespace custom { + template + struct argument_parser {}; + + template + concept has_parser = requires { + { argument_parser>::parse(std::string_view{}) } + -> std::same_as; + }; + } + + template + struct argument_parser {}; + + template <> + struct argument_parser { + 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> 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 + struct argument_parser> { + using duration = std::chrono::duration; + + 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(result * dura); + return new duration{v}; + } + } + } + + return nullptr; + } + }; + + template + requires requires (T& t) { + std::from_chars(nullptr, nullptr, t); + } + struct argument_parser { + 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 { + static std::string* parse(std::string_view value) noexcept { + return new std::string{value}; + } + }; + + template + concept has_parser = requires { + { argument_parser>::parse(std::string_view{}) } + -> std::same_as; + }; + + static_assert(has_parser); + +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 + any_arg(T&& value) : iface(&dispatcher>::table) { + dispatcher>::create(*this, std::forward(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(iface); + } + + template + bool holds() const noexcept { + return iface == &dispatcher>::table; + } + + template + 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 + struct dispatcher { + template + static void create(any_arg& self, Args&&... args) { + self.data = new T{std::forward(args)...}; + } + + static void* copy(void *ptr) { + return new T{*static_cast(ptr)}; + } + + static bool parse(any_arg& self, std::string_view sv) { + if constexpr (custom::has_parser) { + self.data = custom::argument_parser::parse(sv); + return static_cast(self.data); + } else if constexpr (has_parser) { + self.data = argument_parser::parse(sv); + return static_cast(self.data); + } else { + return false; + } + } + + static void destroy(void* ptr) { + delete static_cast(ptr); + } + + static T const* cast(void* ptr) { + return static_cast(ptr); + } + + static constexpr struct interface table { + &dispatcher::destroy, &dispatcher::copy, &dispatcher::parse }; + }; + }; + + template + T const* arg_cast(any_arg const* ar) noexcept { + if (ar->holds()) { + return any_arg::dispatcher>::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>; + + struct result { + enum class code { + no_error, + unknown_option, + bad_option_value, + }; + + code ec = code::no_error; + std::string error_value; + std::map> opts; + std::vector arguments; + + explicit constexpr operator bool() const noexcept { + return ec == code::no_error; + } + + template + T const* maybe_opt(std::string_view name) const noexcept { + auto entry = opts.find(name); + return entry != opts.end() ? + detail::arg_cast(&entry->second) : nullptr; + } + + template + T const& opt(std::string_view opt_name) const { + auto const* v = maybe_opt(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 arglist) { + return parse_inner(arglist); + } + + template + T const* get_option(std::string_view name) { + auto entry = options_.find(name); + if (entry != options_.end()) { + return detail::arg_cast(&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 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> options_; + std::vector arguments_; +}; + +} + + +#endif // argparse_d2ddac0dab0d7b88 diff --git a/source/log.cpp b/source/log.cpp deleted file mode 100644 index 55a2291..0000000 --- a/source/log.cpp +++ /dev/null @@ -1,13 +0,0 @@ -#include - -using namespace parselink; -using level = parselink::logging::level; - -namespace { - logging::logger logger("parselog"); -} - -int main(int argc, char**) { - logger.log("Found {} arguments", argc); - return 0; -} diff --git a/source/main.cpp b/source/main.cpp index 53feb35..66b44ba 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -1,444 +1,54 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include -namespace beast = boost::beast; // from -namespace http = beast::http; // from -namespace net = boost::asio; // from -using tcp = boost::asio::ip::tcp; // from - -// 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 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("verbose")) { + logger.set_threshold(level::trace); + } + + + + auto server = parselink::make_server(args.opt("address"), + args.opt("user_port"), + args.opt("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 -http::message_generator -handle_request( - beast::string_view doc_root, - http::request>&& req) -{ - // Returns a bad request response - auto const bad_request = - [&req](beast::string_view why) - { - http::response 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 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 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 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 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 -{ - beast::tcp_stream stream_; - beast::flat_buffer buffer_; - std::shared_ptr doc_root_; - http::request req_; - -public: - // Take ownership of the stream - session( - tcp::socket&& socket, - std::shared_ptr 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 -{ - net::io_context& ioc_; - tcp::acceptor acceptor_; - std::shared_ptr doc_root_; - -public: - listener( - net::io_context& ioc, - tcp::endpoint endpoint, - std::shared_ptr 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( - 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
\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 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(std::atoi(argv[2])); - auto const doc_root = std::make_shared(argv[3]); - auto const threads = std::max(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( - ioc, - tcp::endpoint{address, port}, - doc_root)->run(); - - // Run the I/O service on the requested number of threads - std::vector 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); } diff --git a/source/server.cpp b/source/server.cpp new file mode 100644 index 0000000..1148cee --- /dev/null +++ b/source/server.cpp @@ -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 +#include + +#include +#include +#include +#include + +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 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(address, user_port, websocket_port); +} diff --git a/tests/common/BUILD b/tests/common/BUILD index abfa0b8..611ea55 100644 --- a/tests/common/BUILD +++ b/tests/common/BUILD @@ -2,7 +2,7 @@ cc_test( name = "logging", srcs = ["logging.cpp"], deps = [ - "//source/common:lib", + "//source/common", "@ut", ], ) diff --git a/tests/common/logging.cpp b/tests/common/logging.cpp index d100019..378976e 100644 --- a/tests/common/logging.cpp +++ b/tests/common/logging.cpp @@ -1,3 +1,21 @@ +//----------------------------------------------------------------------------- +// ___ __ _ _ +// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __ +// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ / +// / ___/ (_| | | \__ \ __/ /__| | | | | < +// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ . +// +//----------------------------------------------------------------------------- +// Author: Kurt Sassenrath +// Module: Tests +// +// Logging tests. +// +// Copyright (c) 2023 Kurt Sassenrath. +// +// License TBD. +//----------------------------------------------------------------------------- + #include #include