diff options
| author | Shauren <shauren.trinity@gmail.com> | 2023-12-17 23:21:10 +0100 |
|---|---|---|
| committer | Shauren <shauren.trinity@gmail.com> | 2023-12-17 23:21:10 +0100 |
| commit | acb5fbd48b5bd911dd0da6016a3d86d4c64724b6 (patch) | |
| tree | 0840b5d5a8157407373891a36743b755dafc5687 /src/server/shared/Networking | |
| parent | 5f00ac4b2bf2d47ea24a93c362737fe904456d2e (diff) | |
Core/Bnet: Rewrite LoginRESTService using boost::beast instead of gsoap as http backend and extract generic http code to be reusable elsewhere
Diffstat (limited to 'src/server/shared/Networking')
| -rw-r--r-- | src/server/shared/Networking/Http/BaseHttpSocket.cpp | 115 | ||||
| -rw-r--r-- | src/server/shared/Networking/Http/BaseHttpSocket.h | 191 | ||||
| -rw-r--r-- | src/server/shared/Networking/Http/HttpCommon.h | 55 | ||||
| -rw-r--r-- | src/server/shared/Networking/Http/HttpService.cpp | 258 | ||||
| -rw-r--r-- | src/server/shared/Networking/Http/HttpService.h | 188 | ||||
| -rw-r--r-- | src/server/shared/Networking/Http/HttpSessionState.h | 35 | ||||
| -rw-r--r-- | src/server/shared/Networking/Http/HttpSocket.h | 75 | ||||
| -rw-r--r-- | src/server/shared/Networking/Http/HttpSslSocket.h | 97 | ||||
| -rw-r--r-- | src/server/shared/Networking/NetworkThread.h | 2 | ||||
| -rw-r--r-- | src/server/shared/Networking/Socket.h | 18 | ||||
| -rw-r--r-- | src/server/shared/Networking/SslSocket.h | 4 |
11 files changed, 1029 insertions, 9 deletions
diff --git a/src/server/shared/Networking/Http/BaseHttpSocket.cpp b/src/server/shared/Networking/Http/BaseHttpSocket.cpp new file mode 100644 index 00000000000..ca92c442aa2 --- /dev/null +++ b/src/server/shared/Networking/Http/BaseHttpSocket.cpp @@ -0,0 +1,115 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "BaseHttpSocket.h" +#include <boost/beast/http/serializer.hpp> + +namespace Trinity::Net::Http +{ +using RequestSerializer = boost::beast::http::request_serializer<ResponseBody>; +using ResponseSerializer = boost::beast::http::response_serializer<ResponseBody>; + +bool AbstractSocket::ParseRequest(MessageBuffer& packet, RequestParser& parser) +{ + if (!parser.is_done()) + { + // need more data in the payload + boost::system::error_code ec = {}; + std::size_t readDataSize = parser.put(boost::asio::const_buffer(packet.GetReadPointer(), packet.GetActiveSize()), ec); + packet.ReadCompleted(readDataSize); + } + + return parser.is_done(); +} + +std::string AbstractSocket::SerializeRequest(Request const& request) +{ + RequestSerializer serializer(request); + + std::string buffer; + while (!serializer.is_done()) + { + size_t totalBytes = 0; + boost::system::error_code ec = {}; + serializer.next(ec, [&]<typename ConstBufferSequence>(boost::system::error_code const&, ConstBufferSequence const& buffers) + { + size_t totalBytesInBuffers = boost::asio::buffer_size(buffers); + + buffer.reserve(buffer.size() + totalBytes); + + auto begin = boost::asio::buffers_begin(buffers); + auto end = boost::asio::buffers_end(buffers); + + std::copy(begin, end, std::back_inserter(buffer)); + totalBytes += totalBytesInBuffers; + }); + + serializer.consume(totalBytes); + } + + return buffer; +} + +MessageBuffer AbstractSocket::SerializeResponse(Request const& request, Response& response) +{ + response.prepare_payload(); + + ResponseSerializer serializer(response); + bool (*serializerIsDone)(ResponseSerializer&); + if (request.method() != boost::beast::http::verb::head) + { + serializerIsDone = [](ResponseSerializer& s) { return s.is_done(); }; + } + else + { + serializerIsDone = [](ResponseSerializer& s) { return s.is_header_done(); }; + serializer.split(true); + } + + MessageBuffer buffer; + while (!serializerIsDone(serializer)) + { + serializer.limit(buffer.GetRemainingSpace()); + + size_t totalBytes = 0; + boost::system::error_code ec = {}; + serializer.next(ec, [&]<typename ConstBufferSequence>(boost::system::error_code& currentError, ConstBufferSequence const& buffers) + { + size_t totalBytesInBuffers = boost::asio::buffer_size(buffers); + if (totalBytesInBuffers > buffer.GetRemainingSpace()) + { + currentError = boost::beast::http::error::need_more; + return; + } + + auto begin = boost::asio::buffers_begin(buffers); + auto end = boost::asio::buffers_end(buffers); + + std::copy(begin, end, buffer.GetWritePointer()); + buffer.WriteCompleted(totalBytesInBuffers); + totalBytes += totalBytesInBuffers; + }); + + serializer.consume(totalBytes); + + if (ec == boost::beast::http::error::need_more) + buffer.Resize(buffer.GetBufferSize() + 4096); + } + + return buffer; +} +} diff --git a/src/server/shared/Networking/Http/BaseHttpSocket.h b/src/server/shared/Networking/Http/BaseHttpSocket.h new file mode 100644 index 00000000000..330287252d2 --- /dev/null +++ b/src/server/shared/Networking/Http/BaseHttpSocket.h @@ -0,0 +1,191 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef TRINITYCORE_BASE_HTTP_SOCKET_H +#define TRINITYCORE_BASE_HTTP_SOCKET_H + +#include "AsyncCallbackProcessor.h" +#include "DatabaseEnvFwd.h" +#include "HttpCommon.h" +#include "HttpSessionState.h" +#include "Optional.h" +#include "QueryCallback.h" +#include "Socket.h" +#include <boost/asio/buffers_iterator.hpp> +#include <boost/beast/http/parser.hpp> +#include <boost/beast/http/string_body.hpp> +#include <boost/uuid/uuid_io.hpp> + +namespace Trinity::Net::Http +{ +using RequestParser = boost::beast::http::request_parser<RequestBody>; + +class TC_SHARED_API AbstractSocket +{ +public: + AbstractSocket() = default; + AbstractSocket(AbstractSocket const& other) = default; + AbstractSocket(AbstractSocket&& other) = default; + AbstractSocket& operator=(AbstractSocket const& other) = default; + AbstractSocket& operator=(AbstractSocket&& other) = default; + virtual ~AbstractSocket() = default; + + static bool ParseRequest(MessageBuffer& packet, RequestParser& parser); + + static std::string SerializeRequest(Request const& request); + static MessageBuffer SerializeResponse(Request const& request, Response& response); + + virtual void SendResponse(RequestContext& context) = 0; + + virtual void QueueQuery(QueryCallback&& queryCallback) = 0; + + virtual std::string GetClientInfo() const = 0; + + virtual Optional<boost::uuids::uuid> GetSessionId() const = 0; +}; + +template<typename Derived, typename Stream> +class BaseSocket : public ::Socket<Derived, Stream>, public AbstractSocket +{ + using Base = ::Socket<Derived, Stream>; + +public: + template<typename... Args> + explicit BaseSocket(boost::asio::ip::tcp::socket&& socket, Args&&... args) + : Base(std::move(socket), std::forward<Args>(args)...) { } + + BaseSocket(BaseSocket const& other) = delete; + BaseSocket(BaseSocket&& other) = delete; + BaseSocket& operator=(BaseSocket const& other) = delete; + BaseSocket& operator=(BaseSocket&& other) = delete; + + ~BaseSocket() = default; + + void ReadHandler() override + { + if (!this->IsOpen()) + return; + + MessageBuffer& packet = this->GetReadBuffer(); + while (packet.GetActiveSize() > 0) + { + if (!ParseRequest(packet, *_httpParser)) + { + // Couldn't receive the whole data this time. + break; + } + + if (!HandleMessage(_httpParser->get())) + { + this->CloseSocket(); + break; + } + + this->ResetHttpParser(); + } + + this->AsyncRead(); + } + + bool HandleMessage(Request& request) + { + RequestContext context { .request = std::move(request) }; + + if (!_state) + _state = this->ObtainSessionState(context); + + RequestHandlerResult status = this->RequestHandler(context); + + if (status != RequestHandlerResult::Async) + this->SendResponse(context); + + return status != RequestHandlerResult::Error; + } + + virtual RequestHandlerResult RequestHandler(RequestContext& context) = 0; + + void SendResponse(RequestContext& context) override + { + MessageBuffer buffer = SerializeResponse(context.request, context.response); + + TC_LOG_DEBUG("server.http", "{} Request {} {} done, status {}", this->GetClientInfo(), ToStdStringView(context.request.method_string()), + ToStdStringView(context.request.target()), context.response.result_int()); + if (sLog->ShouldLog("server.http", LOG_LEVEL_TRACE)) + { + sLog->OutMessage("server.http", LOG_LEVEL_TRACE, "{} Request: ", this->GetClientInfo(), + CanLogRequestContent(context) ? SerializeRequest(context.request) : "<REDACTED>"); + sLog->OutMessage("server.http", LOG_LEVEL_TRACE, "{} Response: ", this->GetClientInfo(), + CanLogResponseContent(context) ? std::string_view(reinterpret_cast<char const*>(buffer.GetBasePointer()), buffer.GetActiveSize()) : "<REDACTED>"); + } + + this->QueuePacket(std::move(buffer)); + + if (!context.response.keep_alive()) + this->DelayedCloseSocket(); + } + + void QueueQuery(QueryCallback&& queryCallback) override + { + this->_queryProcessor.AddCallback(std::move(queryCallback)); + } + + bool Update() override + { + if (!this->Base::Update()) + return false; + + this->_queryProcessor.ProcessReadyCallbacks(); + return true; + } + + std::string GetClientInfo() const override + { + std::string info; + info.reserve(500); + auto itr = StringFormatTo(std::back_inserter(info), "[{}:{}", this->GetRemoteIpAddress().to_string(), this->GetRemotePort()); + if (_state) + itr = StringFormatTo(itr, ", Session Id: {}", boost::uuids::to_string(_state->Id)); + + StringFormatTo(itr, "]"); + return info; + } + + Optional<boost::uuids::uuid> GetSessionId() const final + { + if (this->_state) + return this->_state->Id; + + return {}; + } + +protected: + void ResetHttpParser() + { + this->_httpParser.reset(); + this->_httpParser.emplace(); + this->_httpParser->eager(true); + } + + virtual std::shared_ptr<SessionState> ObtainSessionState(RequestContext& context) const = 0; + + QueryCallbackProcessor _queryProcessor; + Optional<RequestParser> _httpParser; + std::shared_ptr<SessionState> _state; +}; +} + +#endif // TRINITYCORE_BASE_HTTP_SOCKET_H diff --git a/src/server/shared/Networking/Http/HttpCommon.h b/src/server/shared/Networking/Http/HttpCommon.h new file mode 100644 index 00000000000..5f6ecb6c147 --- /dev/null +++ b/src/server/shared/Networking/Http/HttpCommon.h @@ -0,0 +1,55 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef TRINITYCORE_HTTP_COMMON_H +#define TRINITYCORE_HTTP_COMMON_H + +#include "Define.h" +#include <boost/beast/http/message.hpp> +#include <boost/beast/http/string_body.hpp> + +namespace Trinity::Net::Http +{ +using RequestBody = boost::beast::http::string_body; +using ResponseBody = boost::beast::http::string_body; + +using Request = boost::beast::http::request<RequestBody>; +using Response = boost::beast::http::response<ResponseBody>; + +struct RequestContext +{ + Request request; + Response response; + struct RequestHandler const* handler = nullptr; +}; + +TC_SHARED_API bool CanLogRequestContent(RequestContext const& context); +TC_SHARED_API bool CanLogResponseContent(RequestContext const& context); + +inline std::string_view ToStdStringView(boost::beast::string_view bsw) +{ + return { bsw.data(), bsw.size() }; +} + +enum class RequestHandlerResult +{ + Handled, + Error, + Async, +}; +} +#endif // TRINITYCORE_HTTP_COMMON_H diff --git a/src/server/shared/Networking/Http/HttpService.cpp b/src/server/shared/Networking/Http/HttpService.cpp new file mode 100644 index 00000000000..8995b65612a --- /dev/null +++ b/src/server/shared/Networking/Http/HttpService.cpp @@ -0,0 +1,258 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "HttpService.h" +#include "BaseHttpSocket.h" +#include "CryptoRandom.h" +#include "Timezone.h" +#include <boost/beast/version.hpp> +#include <boost/uuid/string_generator.hpp> +#include <fmt/chrono.h> + +namespace Trinity::Net::Http +{ +bool CanLogRequestContent(RequestContext const& context) +{ + return !context.handler || !context.handler->Flags.HasFlag(RequestHandlerFlag::DoNotLogRequestContent); +} + +bool CanLogResponseContent(RequestContext const& context) +{ + return !context.handler || !context.handler->Flags.HasFlag(RequestHandlerFlag::DoNotLogResponseContent); +} + +RequestHandlerResult DispatcherService::HandleRequest(std::shared_ptr<AbstractSocket> session, RequestContext& context) +{ + TC_LOG_DEBUG(_logger, "{} Starting request {} {}", session->GetClientInfo(), + ToStdStringView(context.request.method_string()), ToStdStringView(context.request.target())); + + std::string_view path = [&] + { + std::string_view path = ToStdStringView(context.request.target()); + size_t queryIndex = path.find('?'); + if (queryIndex != std::string_view::npos) + path = path.substr(0, queryIndex); + return path; + }(); + + context.handler = [&]() -> HttpMethodHandlerMap::mapped_type const* + { + switch (context.request.method()) + { + case boost::beast::http::verb::get: + case boost::beast::http::verb::head: + { + auto itr = _getHandlers.find(path); + return itr != _getHandlers.end() ? &itr->second : nullptr; + } + case boost::beast::http::verb::post: + { + auto itr = _postHandlers.find(path); + return itr != _postHandlers.end() ? &itr->second : nullptr; + } + default: + break; + } + return nullptr; + }(); + + SystemTimePoint responseDate = SystemTimePoint::clock::now(); + context.response.set(boost::beast::http::field::date, StringFormat("{:%a, %d %b %Y %T GMT}", responseDate - Timezone::GetSystemZoneOffsetAt(responseDate))); + context.response.set(boost::beast::http::field::server, BOOST_BEAST_VERSION_STRING); + context.response.keep_alive(context.response.keep_alive()); + + if (!context.handler) + return HandlePathNotFound(std::move(session), context); + + return context.handler->Func(std::move(session), context); +} + +RequestHandlerResult DispatcherService::HandlePathNotFound(std::shared_ptr<AbstractSocket> /*session*/, RequestContext& context) +{ + context.response.result(boost::beast::http::status::not_found); + return RequestHandlerResult::Handled; +} + +RequestHandlerResult DispatcherService::HandleUnauthorized(std::shared_ptr<AbstractSocket> /*session*/, RequestContext& context) +{ + context.response.result(boost::beast::http::status::unauthorized); + return RequestHandlerResult::Handled; +} + +void DispatcherService::RegisterHandler(boost::beast::http::verb method, std::string_view path, + std::function<RequestHandlerResult(std::shared_ptr<AbstractSocket> session, RequestContext& context)> handler, + RequestHandlerFlag flags) +{ + HttpMethodHandlerMap& handlerMap = [&]() -> HttpMethodHandlerMap& + { + switch (method) + { + case boost::beast::http::verb::get: + return _getHandlers; + case boost::beast::http::verb::post: + return _postHandlers; + default: + { + std::string_view methodString = ToStdStringView(boost::beast::http::to_string(method)); + ABORT_MSG("Tried to register a handler for unsupported HTTP method " STRING_VIEW_FMT, STRING_VIEW_FMT_ARG(methodString)); + } + } + }(); + + handlerMap[std::string(path)] = { .Func = std::move(handler), .Flags = flags }; + TC_LOG_INFO(_logger, "Registered new handler for {} {}", ToStdStringView(boost::beast::http::to_string(method)), path); +} + +void SessionService::InitAndStoreSessionState(std::shared_ptr<SessionState> state, boost::asio::ip::address const& address) +{ + state->RemoteAddress = address; + + // Generate session id + { + std::unique_lock lock{ _sessionsMutex }; + + while (state->Id.is_nil() || _sessions.contains(state->Id)) + std::copy_n(Trinity::Crypto::GetRandomBytes<16>().begin(), 16, state->Id.begin()); + + TC_LOG_DEBUG(_logger, "Client at {} created new session {}", address.to_string(), boost::uuids::to_string(state->Id)); + _sessions[state->Id] = std::move(state); + } +} + +void SessionService::Start(Asio::IoContext& ioContext) +{ + _inactiveSessionsKillTimer = std::make_unique<Asio::DeadlineTimer>(ioContext); + _inactiveSessionsKillTimer->expires_from_now(boost::posix_time::minutes(1)); + _inactiveSessionsKillTimer->async_wait([this](boost::system::error_code const& err) + { + if (err) + return; + + KillInactiveSessions(); + }); +} + +void SessionService::Stop() +{ + _inactiveSessionsKillTimer = nullptr; + { + std::unique_lock lock{ _sessionsMutex }; + _sessions.clear(); + } + { + std::unique_lock lock{ _inactiveSessionsMutex }; + _inactiveSessions.clear(); + } +} + +std::shared_ptr<SessionState> SessionService::FindAndRefreshSessionState(std::string_view id, boost::asio::ip::address const& address) +{ + std::shared_ptr<SessionState> state; + + { + std::shared_lock lock{ _sessionsMutex }; + auto itr = _sessions.find(boost::uuids::string_generator()(id.begin(), id.end())); + if (itr == _sessions.end()) + { + TC_LOG_DEBUG(_logger, "Client at {} attempted to use a session {} that was expired", address.to_string(), id); + return nullptr; // no session + } + + state = itr->second; + } + + if (state->RemoteAddress != address) + { + TC_LOG_ERROR(_logger, "Client at {} attempted to use a session {} that was last accessed from {}, denied access", + address.to_string(), id, state->RemoteAddress.to_string()); + return nullptr; + } + + { + std::unique_lock inactiveSessionsLock{ _inactiveSessionsMutex }; + _inactiveSessions.erase(state->Id); + } + + return state; +} + +void SessionService::MarkSessionInactive(boost::uuids::uuid const& id) +{ + { + std::unique_lock inactiveSessionsLock{ _inactiveSessionsMutex }; + _inactiveSessions.insert(id); + } + + { + auto itr = _sessions.find(id); + if (itr != _sessions.end()) + { + itr->second->InactiveTimestamp = TimePoint::clock::now() + Minutes(5); + TC_LOG_TRACE(_logger, "Session {} marked as inactive", boost::uuids::to_string(id)); + } + } +} + +void SessionService::KillInactiveSessions() +{ + std::set<boost::uuids::uuid> inactiveSessions; + + { + std::unique_lock lock{ _inactiveSessionsMutex }; + std::swap(_inactiveSessions, inactiveSessions); + } + + { + TimePoint now = TimePoint::clock::now(); + std::size_t inactiveSessionsCount = inactiveSessions.size(); + + std::unique_lock lock{ _sessionsMutex }; + for (auto itr = inactiveSessions.begin(); itr != inactiveSessions.end(); ) + { + auto sessionItr = _sessions.find(*itr); + if (sessionItr == _sessions.end() || sessionItr->second->InactiveTimestamp < now) + { + _sessions.erase(sessionItr); + itr = inactiveSessions.erase(itr); + } + else + ++itr; + } + + TC_LOG_DEBUG(_logger, "Killed {} inactive sessions", inactiveSessionsCount - inactiveSessions.size()); + } + + { + // restore sessions not killed to inactive queue + std::unique_lock lock{ _inactiveSessionsMutex }; + for (auto itr = inactiveSessions.begin(); itr != inactiveSessions.end(); ) + { + auto node = inactiveSessions.extract(itr++); + _inactiveSessions.insert(std::move(node)); + } + } + + _inactiveSessionsKillTimer->expires_from_now(boost::posix_time::minutes(1)); + _inactiveSessionsKillTimer->async_wait([this](boost::system::error_code const& err) + { + if (err) + return; + + KillInactiveSessions(); + }); +} +} diff --git a/src/server/shared/Networking/Http/HttpService.h b/src/server/shared/Networking/Http/HttpService.h new file mode 100644 index 00000000000..01c66146ae3 --- /dev/null +++ b/src/server/shared/Networking/Http/HttpService.h @@ -0,0 +1,188 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef TRINITYCORE_HTTP_SERVICE_H +#define TRINITYCORE_HTTP_SERVICE_H + +#include "AsioHacksFwd.h" +#include "Concepts.h" +#include "Define.h" +#include "EnumFlag.h" +#include "HttpCommon.h" +#include "HttpSessionState.h" +#include "Optional.h" +#include "SocketMgr.h" +#include <boost/uuid/uuid.hpp> +#include <functional> +#include <map> +#include <set> +#include <shared_mutex> + +namespace Trinity::Net::Http +{ +class AbstractSocket; + +enum class RequestHandlerFlag +{ + None = 0x0, + DoNotLogRequestContent = 0x1, + DoNotLogResponseContent = 0x2, +}; + +DEFINE_ENUM_FLAG(RequestHandlerFlag); + +struct RequestHandler +{ + std::function<RequestHandlerResult(std::shared_ptr<AbstractSocket> session, RequestContext& context)> Func; + EnumFlag<RequestHandlerFlag> Flags = RequestHandlerFlag::None; +}; + +class TC_SHARED_API DispatcherService +{ +public: + explicit DispatcherService(std::string_view loggerSuffix) : _logger("server.http.dispatcher.") + { + _logger.append(loggerSuffix); + } + + RequestHandlerResult HandleRequest(std::shared_ptr<AbstractSocket> session, RequestContext& context); + + RequestHandlerResult HandlePathNotFound(std::shared_ptr<AbstractSocket> session, RequestContext& context); + RequestHandlerResult HandleUnauthorized(std::shared_ptr<AbstractSocket> session, RequestContext& context); + +protected: + void RegisterHandler(boost::beast::http::verb method, std::string_view path, + std::function<RequestHandlerResult(std::shared_ptr<AbstractSocket> session, RequestContext& context)> handler, + RequestHandlerFlag flags = RequestHandlerFlag::None); + +private: + using HttpMethodHandlerMap = std::map<std::string, RequestHandler, std::less<>>; + + HttpMethodHandlerMap _getHandlers; + HttpMethodHandlerMap _postHandlers; + + std::string _logger; +}; + +class TC_SHARED_API SessionService +{ +public: + explicit SessionService(std::string_view loggerSuffix) : _logger("server.http.session.") + { + _logger.append(loggerSuffix); + } + + void Start(Asio::IoContext& ioContext); + void Stop(); + + std::shared_ptr<SessionState> FindAndRefreshSessionState(std::string_view id, boost::asio::ip::address const& address); + void MarkSessionInactive(boost::uuids::uuid const& id); + +protected: + void InitAndStoreSessionState(std::shared_ptr<SessionState> state, boost::asio::ip::address const& address); + + void KillInactiveSessions(); + +private: + std::shared_mutex _sessionsMutex; + std::map<boost::uuids::uuid, std::shared_ptr<SessionState>> _sessions; + + std::mutex _inactiveSessionsMutex; + std::set<boost::uuids::uuid> _inactiveSessions; + std::unique_ptr<Asio::DeadlineTimer> _inactiveSessionsKillTimer; + + std::string _logger; +}; + +template<typename Callable, typename SessionImpl> +concept HttpRequestHandler = invocable_r<Callable, RequestHandlerResult, std::shared_ptr<SessionImpl>, RequestContext&>; + +template<typename SessionImpl> +class HttpService : public SocketMgr<SessionImpl>, public DispatcherService, public SessionService +{ +public: + HttpService(std::string_view loggerSuffix) : DispatcherService(loggerSuffix), SessionService(loggerSuffix), _ioContext(nullptr), _logger("server.http.") + { + _logger.append(loggerSuffix); + } + + bool StartNetwork(Asio::IoContext& ioContext, std::string const& bindIp, uint16 port, int32 threadCount = 1) override + { + if (!SocketMgr<SessionImpl>::StartNetwork(ioContext, bindIp, port, threadCount)) + return false; + + SessionService::Start(ioContext); + return true; + } + + void StopNetwork() override + { + SessionService::Stop(); + SocketMgr<SessionImpl>::StopNetwork(); + } + + // http handling + using DispatcherService::RegisterHandler; + + template<HttpRequestHandler<SessionImpl> Callable> + void RegisterHandler(boost::beast::http::verb method, std::string_view path, Callable handler, RequestHandlerFlag flags = RequestHandlerFlag::None) + { + this->DispatcherService::RegisterHandler(method, path, [handler = std::move(handler)](std::shared_ptr<AbstractSocket> session, RequestContext& context) -> RequestHandlerResult + { + return handler(std::static_pointer_cast<SessionImpl>(std::move(session)), context); + }, flags); + } + + // session tracking + virtual std::shared_ptr<SessionState> CreateNewSessionState(boost::asio::ip::address const& address) + { + std::shared_ptr<SessionState> state = std::make_shared<SessionState>(); + InitAndStoreSessionState(state, address); + return state; + } + +protected: + class Thread : public NetworkThread<SessionImpl> + { + protected: + void SocketRemoved(std::shared_ptr<SessionImpl> session) override + { + if (Optional<boost::uuids::uuid> id = session->GetSessionId()) + _service->MarkSessionInactive(*id); + } + + private: + friend HttpService; + + SessionService* _service; + }; + + NetworkThread<SessionImpl>* CreateThreads() const override + { + Thread* threads = new Thread[this->GetNetworkThreadCount()]; + for (int32 i = 0; i < this->GetNetworkThreadCount(); ++i) + threads[i]._service = const_cast<HttpService*>(this); + return threads; + } + +private: + Asio::IoContext* _ioContext; + std::string _logger; +}; +} + +#endif // TRINITYCORE_HTTP_SERVICE_H diff --git a/src/server/shared/Networking/Http/HttpSessionState.h b/src/server/shared/Networking/Http/HttpSessionState.h new file mode 100644 index 00000000000..3012a2efc65 --- /dev/null +++ b/src/server/shared/Networking/Http/HttpSessionState.h @@ -0,0 +1,35 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef TRINITYCORE_HTTP_SESSION_STATE_H +#define TRINITYCORE_HTTP_SESSION_STATE_H + +#include "Duration.h" +#include <boost/asio/ip/address.hpp> +#include <boost/uuid/uuid.hpp> + +namespace Trinity::Net::Http +{ +struct SessionState +{ + boost::uuids::uuid Id = { }; + boost::asio::ip::address RemoteAddress; + TimePoint InactiveTimestamp = TimePoint::max(); +}; +} + +#endif // TRINITYCORE_HTTP_SESSION_STATE_H diff --git a/src/server/shared/Networking/Http/HttpSocket.h b/src/server/shared/Networking/Http/HttpSocket.h new file mode 100644 index 00000000000..2bd18efd565 --- /dev/null +++ b/src/server/shared/Networking/Http/HttpSocket.h @@ -0,0 +1,75 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef TRINITYCORE_HTTP_SOCKET_H +#define TRINITYCORE_HTTP_SOCKET_H + +#include "BaseHttpSocket.h" +#include <boost/beast/core/tcp_stream.hpp> + +namespace Trinity::Net::Http +{ +namespace Impl +{ +class BoostBeastSocketWrapper : public boost::beast::tcp_stream +{ +public: + using boost::beast::tcp_stream::tcp_stream; + + void shutdown(boost::asio::socket_base::shutdown_type what, boost::system::error_code& shutdownError) + { + socket().shutdown(what, shutdownError); + } + + void close(boost::system::error_code& /*error*/) + { + boost::beast::tcp_stream::close(); + } + + boost::asio::ip::tcp::socket::endpoint_type remote_endpoint() const + { + return socket().remote_endpoint(); + } +}; +} + +template <typename Derived> +class Socket : public BaseSocket<Derived, Impl::BoostBeastSocketWrapper> +{ + using SocketBase = BaseSocket<Derived, Impl::BoostBeastSocketWrapper>; + +public: + explicit Socket(boost::asio::ip::tcp::socket&& socket) + : SocketBase(std::move(socket)) { } + + Socket(Socket const& other) = delete; + Socket(Socket&& other) = delete; + Socket& operator=(Socket const& other) = delete; + Socket& operator=(Socket&& other) = delete; + + ~Socket() = default; + + void Start() override + { + this->ResetHttpParser(); + + this->AsyncRead(); + } +}; +} + +#endif // TRINITYCORE_HTTP_SOCKET_H diff --git a/src/server/shared/Networking/Http/HttpSslSocket.h b/src/server/shared/Networking/Http/HttpSslSocket.h new file mode 100644 index 00000000000..cdb70645e05 --- /dev/null +++ b/src/server/shared/Networking/Http/HttpSslSocket.h @@ -0,0 +1,97 @@ +/* + * This file is part of the TrinityCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef TRINITYCORE_HTTP_SSL_SOCKET_H +#define TRINITYCORE_HTTP_SSL_SOCKET_H + +#include "BaseHttpSocket.h" +#include "SslSocket.h" +#include <boost/beast/core/stream_traits.hpp> +#include <boost/beast/core/tcp_stream.hpp> +#include <boost/beast/ssl/ssl_stream.hpp> + +namespace Trinity::Net::Http +{ +namespace Impl +{ +class BoostBeastSslSocketWrapper : public ::SslSocket<boost::beast::ssl_stream<boost::beast::tcp_stream>> +{ +public: + using SslSocket::SslSocket; + + void shutdown(boost::asio::socket_base::shutdown_type what, boost::system::error_code& shutdownError) + { + _sslSocket.shutdown(shutdownError); + boost::beast::get_lowest_layer(_sslSocket).socket().shutdown(what, shutdownError); + } + + void close(boost::system::error_code& /*error*/) + { + boost::beast::get_lowest_layer(_sslSocket).close(); + } + + boost::asio::ip::tcp::socket::endpoint_type remote_endpoint() const + { + return boost::beast::get_lowest_layer(_sslSocket).socket().remote_endpoint(); + } +}; +} + +template <typename Derived> +class SslSocket : public BaseSocket<Derived, Impl::BoostBeastSslSocketWrapper> +{ + using SocketBase = BaseSocket<Derived, Impl::BoostBeastSslSocketWrapper>; + +public: + explicit SslSocket(boost::asio::ip::tcp::socket&& socket, boost::asio::ssl::context& sslContext) + : SocketBase(std::move(socket), sslContext) { } + + SslSocket(SslSocket const& other) = delete; + SslSocket(SslSocket&& other) = delete; + SslSocket& operator=(SslSocket const& other) = delete; + SslSocket& operator=(SslSocket&& other) = delete; + + ~SslSocket() = default; + + void Start() override + { + this->AsyncHandshake(); + } + + void AsyncHandshake() + { + this->underlying_stream().async_handshake(boost::asio::ssl::stream_base::server, + [self = this->shared_from_this()](boost::system::error_code const& error) { self->HandshakeHandler(error); }); + } + + void HandshakeHandler(boost::system::error_code const& error) + { + if (error) + { + TC_LOG_ERROR("server.http.session.ssl", "{} SSL Handshake failed {}", this->GetClientInfo(), error.message()); + this->CloseSocket(); + return; + } + + this->ResetHttpParser(); + + this->AsyncRead(); + } +}; +} + +#endif // TRINITYCORE_HTTP_SSL_SOCKET_H diff --git a/src/server/shared/Networking/NetworkThread.h b/src/server/shared/Networking/NetworkThread.h index 69d62403249..0195c48b9fc 100644 --- a/src/server/shared/Networking/NetworkThread.h +++ b/src/server/shared/Networking/NetworkThread.h @@ -77,7 +77,7 @@ public: return _connections; } - virtual void AddSocket(std::shared_ptr<SocketType> sock) + void AddSocket(std::shared_ptr<SocketType> sock) { std::lock_guard<std::mutex> lock(_newSocketsLock); diff --git a/src/server/shared/Networking/Socket.h b/src/server/shared/Networking/Socket.h index a996ecb2cbe..511f94ed366 100644 --- a/src/server/shared/Networking/Socket.h +++ b/src/server/shared/Networking/Socket.h @@ -18,14 +18,13 @@ #ifndef __SOCKET_H__ #define __SOCKET_H__ -#include "MessageBuffer.h" #include "Log.h" +#include "MessageBuffer.h" +#include <boost/asio/ip/tcp.hpp> #include <atomic> -#include <queue> #include <memory> -#include <functional> +#include <queue> #include <type_traits> -#include <boost/asio/ip/tcp.hpp> #define READ_BLOCK_SIZE 4096 #ifdef BOOST_ASIO_HAS_IOCP @@ -63,12 +62,19 @@ template<class T, class Stream = boost::asio::ip::tcp::socket> class Socket : public std::enable_shared_from_this<T> { public: - explicit Socket(boost::asio::ip::tcp::socket&& socket) : _socket(std::move(socket)), _remoteAddress(_socket.remote_endpoint().address()), - _remotePort(_socket.remote_endpoint().port()), _readBuffer(), _closed(false), _closing(false), _isWritingAsync(false) + template<typename... Args> + explicit Socket(boost::asio::ip::tcp::socket&& socket, Args&&... args) : _socket(std::move(socket), std::forward<Args>(args)...), + _remoteAddress(_socket.remote_endpoint().address()), _remotePort(_socket.remote_endpoint().port()), + _closed(false), _closing(false), _isWritingAsync(false) { _readBuffer.Resize(READ_BLOCK_SIZE); } + Socket(Socket const& other) = delete; + Socket(Socket&& other) = delete; + Socket& operator=(Socket const& other) = delete; + Socket& operator=(Socket&& other) = delete; + virtual ~Socket() { _closed = true; diff --git a/src/server/shared/Networking/SslSocket.h b/src/server/shared/Networking/SslSocket.h index e00b1b6b65e..c19c8612edf 100644 --- a/src/server/shared/Networking/SslSocket.h +++ b/src/server/shared/Networking/SslSocket.h @@ -24,11 +24,11 @@ namespace boostssl = boost::asio::ssl; -template<class SslContext, class Stream = boostssl::stream<boost::asio::ip::tcp::socket>> +template<class Stream = boostssl::stream<boost::asio::ip::tcp::socket>> class SslSocket { public: - explicit SslSocket(boost::asio::ip::tcp::socket&& socket) : _sslSocket(std::move(socket), SslContext::instance()) + explicit SslSocket(boost::asio::ip::tcp::socket&& socket, boost::asio::ssl::context& sslContext) : _sslSocket(std::move(socket), sslContext) { _sslSocket.set_verify_mode(boostssl::verify_none); } |
