407 lines
12 KiB
C++
407 lines
12 KiB
C++
|
|
#include <spdlog/spdlog.h>
|
|
|
|
#include <boost/beast/core/flat_buffer.hpp>
|
|
#include <boost/beast/http/read.hpp>
|
|
#include <boost/beast/http/write.hpp>
|
|
#include <boost/beast/websocket/rfc6455.hpp>
|
|
#include <boost/url.hpp>
|
|
#include <boost/url/parse.hpp>
|
|
#include <http/proxy_address.hpp>
|
|
#include <http/r3_helpers.hpp>
|
|
#include <http/server.hpp>
|
|
#include <http/utils.hpp>
|
|
#include <r3.hpp>
|
|
|
|
namespace base::http {
|
|
|
|
namespace detail {
|
|
|
|
// method list for the below functions
|
|
#define ML() \
|
|
C(get, METHOD_GET) \
|
|
C(post, METHOD_POST) \
|
|
C(put, METHOD_PUT) \
|
|
C(delete_, METHOD_DELETE) \
|
|
C(patch, METHOD_PATCH) \
|
|
C(head, METHOD_HEAD) \
|
|
C(options, METHOD_OPTIONS)
|
|
|
|
constexpr auto BeastToR3(beast::http::verb verb) {
|
|
using enum beast::http::verb;
|
|
switch(verb) {
|
|
#define C(v, r) \
|
|
case v: return r;
|
|
ML()
|
|
#undef C
|
|
default: return 0;
|
|
}
|
|
}
|
|
|
|
constexpr beast::http::verb R3ToBeast(int method) {
|
|
using enum beast::http::verb;
|
|
switch(method) {
|
|
#define C(v, r) \
|
|
case r: return v;
|
|
ML()
|
|
#undef C
|
|
default: BASE_CHECK(false, "should not reach this :)");
|
|
}
|
|
}
|
|
|
|
#undef ML
|
|
|
|
} // namespace detail
|
|
|
|
// make this a global helper?
|
|
constexpr auto stlify(boost::core::string_view sv) {
|
|
return std::string_view { sv.data(), sv.length() };
|
|
}
|
|
|
|
inline bool IsTcp(GenericProtocol p) {
|
|
const auto family = p.family();
|
|
const auto protocol = p.protocol();
|
|
|
|
if(family != AF_INET && family != AF_INET6)
|
|
return false;
|
|
|
|
// IPPROTO_IP is also used for remote TCP connections. Thanks BSD
|
|
return protocol == IPPROTO_TCP || protocol == IPPROTO_IP;
|
|
}
|
|
|
|
struct Server::Impl {
|
|
struct Session {
|
|
Session(Impl& impl, Server::StreamType&& stream)
|
|
: impl(impl), stream(std::move(stream)) {}
|
|
|
|
void Close() {
|
|
if(stream.socket().is_open()) {
|
|
bsys::error_code ec;
|
|
stream.socket().shutdown(asio::socket_base::shutdown_send, ec);
|
|
}
|
|
}
|
|
|
|
Awaitable<void> DoSession() {
|
|
beast::flat_buffer buffer {};
|
|
try {
|
|
while(true) {
|
|
curReq = {};
|
|
|
|
// Set the timeout.
|
|
stream.expires_after(std::chrono::seconds(30));
|
|
|
|
co_await beast::http::async_read(stream, buffer, curReq, asio::deferred);
|
|
|
|
auto addr = GetProxyAddress(GetAddress(), curReq);
|
|
|
|
auto res = co_await impl.Route(curReq, addr, *this);
|
|
|
|
// This usually means that some higher level stuff (e.g: WebSockets)
|
|
// did the thing
|
|
if(!res.has_value())
|
|
co_return;
|
|
|
|
co_await WriteResponse(std::move(*res));
|
|
|
|
if(!stream.socket().is_open())
|
|
break;
|
|
}
|
|
} catch(bsys::system_error& ec) {
|
|
// if(ec.code() != beast::http::error::end_of_stream)
|
|
// spdlog::error("Error in http::BasicServer::DoSession(): {}", ec.what());
|
|
}
|
|
|
|
// todo: standalone close() function
|
|
Close();
|
|
co_return;
|
|
}
|
|
|
|
asio::ip::address GetAddress() const {
|
|
const auto protocol = stream.socket().remote_endpoint().protocol();
|
|
|
|
if(IsTcp(protocol)) {
|
|
// ugghhhhhhhhnhhnhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhhh
|
|
auto* sa = stream.socket().remote_endpoint().data();
|
|
|
|
switch(sa->sa_family) {
|
|
case AF_INET: {
|
|
auto* sain = std::bit_cast<sockaddr_in*>(sa);
|
|
return asio::ip::address_v4 { htonl(sain->sin_addr.s_addr) };
|
|
} break;
|
|
|
|
case AF_INET6: {
|
|
auto* sain6 = std::bit_cast<sockaddr_in6*>(sa);
|
|
|
|
// This is really annoying
|
|
// N.B: The array here isn't zerofilled because we copy into its whole capacity
|
|
// anyways so it's not really like it even matters
|
|
asio::ip::address_v6::bytes_type addr;
|
|
memcpy(&addr[0], sain6->sin6_addr.s6_addr, 16);
|
|
|
|
return asio::ip::address_v6 { addr, sain6->sin6_scope_id };
|
|
} break;
|
|
}
|
|
}
|
|
|
|
// This is one of the reasons we should probably switch to a variant for this
|
|
// so we can expose unix support while not being wonky and broken, but we
|
|
// currently don't use unix support in the codebase or production so it's not really like it matters
|
|
// all that much
|
|
return asio::ip::make_address("1.2.3.4");
|
|
}
|
|
|
|
Awaitable<void> WriteResponse(Response res) {
|
|
if(!stream.socket().is_open())
|
|
co_return;
|
|
|
|
auto addr = GetProxyAddress(GetAddress(), curReq);
|
|
|
|
auto responseBytesWritten = co_await beast::async_write(stream, res.Release(), asio::deferred);
|
|
|
|
#ifdef BASE_HTTP_REQUEST_LOGGING
|
|
impl.logger.Info("{} - {} {} HTTP/{}.{} ({})", addr.to_string(), stlify(curReq.method_string()), stlify(curReq.target()),
|
|
curReq.version() / 10, curReq.version() % 10, responseBytesWritten);
|
|
#else
|
|
static_cast<void>(addr);
|
|
static_cast<void>(responseBytesWritten);
|
|
#endif
|
|
|
|
// Handle `Connection: close` semantics
|
|
if(!curReq.keep_alive())
|
|
Close();
|
|
co_return;
|
|
}
|
|
|
|
// private:
|
|
Impl& impl;
|
|
Server::StreamType stream;
|
|
Request::BeastRequest curReq {};
|
|
};
|
|
|
|
using RouteVariant = std::variant<Server::RouteHandler, Server::WebSocketRouteHandler>;
|
|
|
|
template <class ProtocolEndpoint>
|
|
Impl(asio::any_io_executor ioc, ProtocolEndpoint ep)
|
|
: exec(ioc), ep(ep), acceptor(ioc) {}
|
|
|
|
void On(beast::http::verb verb, std::string_view path, Server::RouteHandler handler) {
|
|
handlerStorage.emplace_back(handler);
|
|
char* errstr { nullptr };
|
|
|
|
// We use the "data" user pointer to instead index into our handler storage.
|
|
// It's messy and awful, but it does work!
|
|
tree.insert_routel(detail::BeastToR3(verb), path.data(), path.length(), std::bit_cast<void*>(handlerStorage.size() - 1), &errstr);
|
|
|
|
if(errstr) {
|
|
// error inserting into tree
|
|
spdlog::error("Error inserting route \"{}\" into tree: {}", path, errstr);
|
|
free(errstr);
|
|
return;
|
|
}
|
|
}
|
|
|
|
void OnWebSocket(std::string_view path, Server::WebSocketRouteHandler handler) {
|
|
handlerStorage.emplace_back(handler);
|
|
char* errstr { nullptr };
|
|
tree.insert_routel(detail::BeastToR3(beast::http::verb::get), path.data(), path.length(), std::bit_cast<void*>(handlerStorage.size() - 1),
|
|
&errstr);
|
|
if(errstr) {
|
|
// error inserting into tree
|
|
spdlog::error("Error inserting route \"{}\" into tree: {}", path, errstr);
|
|
free(errstr);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Call this after creating all required routes with On().
|
|
bool CompileTree() {
|
|
char* errstr { nullptr };
|
|
tree.compile(&errstr);
|
|
if(errstr) {
|
|
spdlog::error("Error compiling route tree: {}", errstr);
|
|
free(errstr);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
Awaitable<std::optional<Response>> Route(Request::BeastRequest& req, asio::ip::address srcAddress, Session& session) {
|
|
auto const noSuchRouteError = [&]() -> beast::http::message_generator {
|
|
beast::http::response<beast::http::string_body> resp { beast::http::status::bad_request, req.version() };
|
|
SetCommonResponseFields(resp);
|
|
resp.set(beast::http::field::content_type, "text/plain");
|
|
resp.keep_alive(false);
|
|
resp.body() = std::format("No such route \"{} {}\"", std::string_view { req.method_string().data(), req.method_string().length() },
|
|
std::string_view { req.target().data(), req.target().length() });
|
|
resp.prepare_payload();
|
|
return resp;
|
|
};
|
|
|
|
auto const internalServerError = [&]() -> beast::http::message_generator {
|
|
beast::http::response<beast::http::string_body> resp { beast::http::status::internal_server_error, req.version() };
|
|
SetCommonResponseFields(resp);
|
|
resp.set(beast::http::field::content_type, "text/plain");
|
|
resp.keep_alive(false);
|
|
resp.body() = "HTTP/1.1 500 Internal Server Error";
|
|
resp.prepare_payload();
|
|
return resp;
|
|
};
|
|
|
|
auto const wsRequired = [&]() -> beast::http::message_generator {
|
|
beast::http::response<beast::http::string_body> resp { beast::http::status::bad_request, req.version() };
|
|
SetCommonResponseFields(resp);
|
|
resp.set(beast::http::field::content_type, "text/plain");
|
|
resp.keep_alive(false);
|
|
resp.body() = "Invalid HTTP request to WebSockets endpoint";
|
|
resp.prepare_payload();
|
|
return resp;
|
|
};
|
|
|
|
auto url = burl::parse_origin_form(req.target());
|
|
if(url.has_error()) {
|
|
// If we were unable to parse the URL (somehow), then gracefully handle that
|
|
co_return Response { internalServerError() };
|
|
}
|
|
|
|
auto path = url->path();
|
|
|
|
r3::MatchEntry me { path.data(), static_cast<i32>(path.length()) };
|
|
me.set_request_method(detail::BeastToR3(req.method()));
|
|
|
|
auto route = tree.match_route(me);
|
|
if(auto rp = route.get()) {
|
|
// this is a mess but it's a mess which works :)
|
|
auto handlerIndex = std::bit_cast<usize>(rp->data);
|
|
auto& handler = handlerStorage[handlerIndex];
|
|
|
|
auto routed_request = Request { req, url.value(), me };
|
|
routed_request.src = srcAddress;
|
|
|
|
// Dispatch to the given handler variant
|
|
if(auto* httpHandler = std::get_if<Server::RouteHandler>(&handler); httpHandler) {
|
|
co_return co_await (*httpHandler)(routed_request);
|
|
} else if(auto* wsHandler = std::get_if<Server::WebSocketRouteHandler>(&handler); wsHandler) {
|
|
if(!beast::websocket::is_upgrade(routed_request.Native())) {
|
|
auto res = wsRequired();
|
|
co_return Response { std::move(res) };
|
|
}
|
|
|
|
auto ws = std::make_shared<WebSocketClient>(std::move(session.stream), routed_request);
|
|
|
|
if(!co_await ws->Handshake()) {
|
|
co_return std::nullopt;
|
|
}
|
|
|
|
co_await (*wsHandler)(routed_request, std::move(ws));
|
|
co_return std::nullopt;
|
|
}
|
|
} else {
|
|
// TODO: maybe a catch-all handler at some point. for now this is fine enough
|
|
|
|
// Send a bad request detailing that a route is not available for the given
|
|
// URL path.
|
|
co_return Response { noSuchRouteError() };
|
|
}
|
|
co_return std::nullopt;
|
|
}
|
|
|
|
Awaitable<void> Listener() {
|
|
try {
|
|
// Setup the acceptor
|
|
acceptor.open(ep.protocol());
|
|
acceptor.set_option(asio::socket_base::reuse_address(true));
|
|
|
|
if(IsTcp(ep.protocol())) {
|
|
// set SO_REUSEPORT using a custom type. This is flaky but we pin boost
|
|
// so this will be ok I suppose
|
|
using reuse_port = asio::detail::socket_option::boolean<SOL_SOCKET, SO_REUSEPORT>;
|
|
acceptor.set_option(reuse_port(true));
|
|
|
|
spdlog::info("Able to enable SO_NODELAY; doing so");
|
|
acceptor.set_option(asio::ip::tcp::no_delay { true });
|
|
}
|
|
|
|
acceptor.bind(ep);
|
|
acceptor.listen(asio::socket_base::max_listen_connections);
|
|
|
|
listening = true;
|
|
|
|
// Now do the thing..
|
|
while(true) {
|
|
auto socket = co_await acceptor.async_accept(asio::deferred);
|
|
asio::co_spawn(acceptor.get_executor(), RunSession(StreamType { std::move(socket) }), asio::detached);
|
|
}
|
|
} catch(bsys::system_error& ec) {
|
|
// Filter out "operation cancelled", because that's not really an error we need to know about.
|
|
if(ec.code() != asio::error::operation_aborted)
|
|
spdlog::error("Error in http::BasicServer::Listener(): {}", ec.what());
|
|
}
|
|
|
|
listening = false;
|
|
}
|
|
|
|
// TODO: Maybe we should actually store all sessions or something, and use shared_ptrs to refer to them
|
|
// That way on server shutdown we can cleanly shut them all down
|
|
|
|
/// Runs a single HTTP client session.
|
|
Awaitable<void> RunSession(StreamType stream) {
|
|
// std::shared_ptr<Session> session = std::make_shared<Session>(*this, std::move(stream));
|
|
Session session { *this, std::move(stream) };
|
|
co_await session.DoSession();
|
|
co_return;
|
|
}
|
|
|
|
void Start() {
|
|
if(!CompileTree()) {
|
|
BASE_CHECK(false, "Route tree compilation failure, cannot start HTTP server");
|
|
}
|
|
|
|
asio::co_spawn(exec, Listener(), asio::detached);
|
|
}
|
|
|
|
void Stop() {
|
|
bsys::error_code ec;
|
|
acceptor.close(ec);
|
|
}
|
|
|
|
// Router tree.
|
|
r3::Tree tree { 16 };
|
|
|
|
std::vector<RouteVariant> handlerStorage;
|
|
|
|
asio::any_io_executor exec;
|
|
EndpointType ep;
|
|
bool listening { false };
|
|
|
|
asio::basic_socket_acceptor<GenericProtocol> acceptor;
|
|
};
|
|
|
|
Server::Server(asio::any_io_executor ioc, asio::ip::tcp::endpoint ep) {
|
|
impl = std::make_unique<Impl>(ioc, ep);
|
|
}
|
|
|
|
Server::~Server() = default;
|
|
|
|
bool Server::Listening() const {
|
|
return impl->listening;
|
|
}
|
|
|
|
void Server::On(beast::http::verb verb, std::string_view path, RouteHandler handler) {
|
|
impl->On(verb, path, handler);
|
|
}
|
|
|
|
void Server::GetWebSockets(std::string_view path, WebSocketRouteHandler wsRoute) {
|
|
impl->OnWebSocket(path, wsRoute);
|
|
}
|
|
|
|
void Server::Start() {
|
|
impl->Start();
|
|
}
|
|
|
|
void Server::Stop() {
|
|
if(impl->listening)
|
|
impl->Stop();
|
|
}
|
|
|
|
} // namespace base::http
|