SSX3LobbyServer/lib/http/server.cpp

407 lines
12 KiB
C++
Raw Normal View History

#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