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(
|
refresh_compile_commands(
|
||||||
name = "refresh_compile_commands",
|
name = "refresh_compile_commands",
|
||||||
targets = {
|
targets = {
|
||||||
"//source:parselinklog": "",
|
"//source:*": "",
|
||||||
"//tests/...": "",
|
"//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(
|
cc_binary(
|
||||||
name = "parselinkd",
|
name = "parselinkd",
|
||||||
srcs = ["main.cpp"],
|
srcs = [
|
||||||
deps = [
|
"main.cpp",
|
||||||
"@fmt",
|
"server.cpp",
|
||||||
"@boost//:beast",
|
|
||||||
],
|
],
|
||||||
)
|
|
||||||
cc_binary(
|
|
||||||
name = "parselinklog",
|
|
||||||
srcs = ["log.cpp"],
|
|
||||||
deps = [
|
deps = [
|
||||||
|
"headers",
|
||||||
|
"@boost//:beast",
|
||||||
"//source/common"
|
"//source/common"
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
# parselink
|
# parselink
|
||||||
|
|
||||||
cc_library(
|
cc_library(
|
||||||
name = "lib",
|
name = "common",
|
||||||
srcs = [
|
srcs = [
|
||||||
"source/logging.cpp",
|
"source/logging.cpp",
|
||||||
],
|
],
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||||
//
|
//
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Author: Kurt Sassenrath
|
// Author: Kurt Sassenrath
|
||||||
@ -34,7 +34,7 @@ namespace logging {
|
|||||||
// Any log levels higher (lower severity) than static_threshold cannot not be
|
// 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
|
// enabled in the library, but the compiler should be able to optimize away
|
||||||
// some/most of the calls.
|
// 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;
|
constexpr inline auto default_threshold = level::info;
|
||||||
|
|
||||||
// Structure for holding a message. Note: message is a view over some buffer,
|
// Structure for holding a message. Note: message is a view over some buffer,
|
||||||
@ -80,6 +80,22 @@ public:
|
|||||||
requires (Level > static_threshold)
|
requires (Level > static_threshold)
|
||||||
[[gnu::flatten]] void log(fmt::format_string<Args...>, Args&&...) const {}
|
[[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;
|
void set_threshold(level new_threshold) noexcept;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||||
//
|
//
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Author: Kurt Sassenrath
|
// Author: Kurt Sassenrath
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||||
//
|
//
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Author: Kurt Sassenrath
|
// Author: Kurt Sassenrath
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||||
//
|
//
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Author: Kurt Sassenrath
|
// Author: Kurt Sassenrath
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||||
//
|
//
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Author: Kurt Sassenrath
|
// Author: Kurt Sassenrath
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||||
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||||
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||||
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\
|
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||||
//
|
//
|
||||||
//-----------------------------------------------------------------------------
|
//-----------------------------------------------------------------------------
|
||||||
// Author: Kurt Sassenrath
|
// Author: Kurt Sassenrath
|
||||||
@ -28,8 +28,6 @@ struct console_endpoint : public endpoint {
|
|||||||
static constexpr std::string_view format_string =
|
static constexpr std::string_view format_string =
|
||||||
"{:%Y-%m-%d %H:%M:%S}.{:03} [{:<8}] {:>20} | {}\n";
|
"{:%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; }
|
bool colored() const noexcept override { return true; }
|
||||||
|
|
||||||
void write(message const& msg) override {
|
void write(message const& msg) override {
|
||||||
@ -38,8 +36,6 @@ struct console_endpoint : public endpoint {
|
|||||||
(msg.time.time_since_epoch() % 1000ms) / 1ms,
|
(msg.time.time_since_epoch() % 1000ms) / 1ms,
|
||||||
themed_arg{enum_name_only{msg.lvl}}, msg.name, msg.message);
|
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 <logging.h>
|
||||||
#include <boost/beast/http.hpp>
|
#include <utility/argparse.h>
|
||||||
#include <boost/beast/version.hpp>
|
#include <server.h>
|
||||||
#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 {
|
||||||
namespace http = beast::http; // from <boost/beast/http.hpp>
|
parselink::logging::logger logger("main");
|
||||||
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.
|
using level = parselink::logging::level;
|
||||||
// The returned path is normalized for the platform.
|
|
||||||
std::string
|
int run(std::span<std::string_view> arg_list) {
|
||||||
path_cat(
|
argparse::command_line_parser parser({
|
||||||
beast::string_view base,
|
{"address", {std::string{"0.0.0.0"}}},
|
||||||
beast::string_view path)
|
{"user_port", {std::uint16_t{9001}}},
|
||||||
{
|
{"websocket_port", {std::uint16_t{10501}}},
|
||||||
if(base.empty())
|
{"verbose", {false}},
|
||||||
return std::string(path);
|
});
|
||||||
std::string result(base);
|
|
||||||
#ifdef BOOST_MSVC
|
auto args = parser.parse(arg_list);
|
||||||
char constexpr path_separator = '\\';
|
|
||||||
if(result.back() == path_separator)
|
if (args.ec != argparse::command_line_parser::result::code::no_error) {
|
||||||
result.resize(result.size() - 1);
|
logger.error("Failed to parse arguments: {} ({})", args.ec,
|
||||||
result.append(path.data(), path.size());
|
args.error_value);
|
||||||
for(auto& c : result)
|
return 1;
|
||||||
if(c == '/')
|
}
|
||||||
c = path_separator;
|
|
||||||
#else
|
if (args.opt<bool>("verbose")) {
|
||||||
char constexpr path_separator = '/';
|
logger.set_threshold(level::trace);
|
||||||
if(result.back() == path_separator)
|
}
|
||||||
result.resize(result.size() - 1);
|
|
||||||
result.append(path.data(), path.size());
|
|
||||||
#endif
|
|
||||||
return result;
|
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[])
|
int main(int argc, char* argv[])
|
||||||
{
|
{
|
||||||
// Check command line arguments.
|
// TODO(ksassenrath): Add configuration file to the mix.
|
||||||
if (argc != 5)
|
|
||||||
{
|
std::vector<std::string_view> args;
|
||||||
std::cerr <<
|
for (int i = 1; i < argc; ++i) {
|
||||||
"Usage: http-server-async <address> <port> <doc_root> <threads>\n" <<
|
args.emplace_back(argv[i]);
|
||||||
"Example:\n" <<
|
|
||||||
" http-server-async 0.0.0.0 8080 . 1\n";
|
|
||||||
return EXIT_FAILURE;
|
|
||||||
}
|
}
|
||||||
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
|
return run(args);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|||||||
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",
|
name = "logging",
|
||||||
srcs = ["logging.cpp"],
|
srcs = ["logging.cpp"],
|
||||||
deps = [
|
deps = [
|
||||||
"//source/common:lib",
|
"//source/common",
|
||||||
"@ut",
|
"@ut",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,3 +1,21 @@
|
|||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// ___ __ _ _
|
||||||
|
// / _ \__ _ _ __ ___ ___ / /(_)_ __ | | __
|
||||||
|
// / /_)/ _` | '__/ __|/ _ \/ / | | '_ \| |/ /
|
||||||
|
// / ___/ (_| | | \__ \ __/ /__| | | | | <
|
||||||
|
// \/ \__,_|_| |___/\___\____/_|_| |_|_|\_\ .
|
||||||
|
//
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
// Author: Kurt Sassenrath
|
||||||
|
// Module: Tests
|
||||||
|
//
|
||||||
|
// Logging tests.
|
||||||
|
//
|
||||||
|
// Copyright (c) 2023 Kurt Sassenrath.
|
||||||
|
//
|
||||||
|
// License TBD.
|
||||||
|
//-----------------------------------------------------------------------------
|
||||||
|
|
||||||
#include <logging.h>
|
#include <logging.h>
|
||||||
|
|
||||||
#include <boost/ut.hpp>
|
#include <boost/ut.hpp>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user