#include #include #include #include #include #include #include #include #include #include #include #include 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 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) // logger.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(sa); return asio::ip::address_v4 { htonl(sain->sin_addr.s_addr) }; } break; case AF_INET6: { auto* sain6 = std::bit_cast(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 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(addr); static_cast(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; template 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(handlerStorage.size() - 1), &errstr); if(errstr) { // error inserting into tree logger.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(handlerStorage.size() - 1), &errstr); if(errstr) { // error inserting into tree logger.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) { logger.Error("Error compiling route tree: {}", errstr); free(errstr); return false; } return true; } Awaitable> Route(Request::BeastRequest& req, asio::ip::address srcAddress, Session& session) { auto const noSuchRouteError = [&]() -> beast::http::message_generator { beast::http::response 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 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 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(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(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(&handler); httpHandler) { co_return co_await (*httpHandler)(routed_request); } else if(auto* wsHandler = std::get_if(&handler); wsHandler) { if(!beast::websocket::is_upgrade(routed_request.Native())) { auto res = wsRequired(); co_return Response { std::move(res) }; } auto ws = std::make_shared(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 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; acceptor.set_option(reuse_port(true)); logger.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) logger.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 RunSession(StreamType stream) { // std::shared_ptr session = std::make_shared(*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 handlerStorage; asio::any_io_executor exec; EndpointType ep; bool listening { false }; asio::basic_socket_acceptor acceptor; Logger logger { MakeChannelId(MessageSource::Http, MessageComponentSource::Http_Server) }; }; Server::Server(asio::any_io_executor ioc, asio::ip::tcp::endpoint ep) { impl = std::make_unique(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