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/bnetserver | |
| 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/bnetserver')
| -rw-r--r-- | src/server/bnetserver/Main.cpp | 20 | ||||
| -rw-r--r-- | src/server/bnetserver/REST/LoginHttpSession.cpp | 118 | ||||
| -rw-r--r-- | src/server/bnetserver/REST/LoginHttpSession.h | 43 | ||||
| -rw-r--r-- | src/server/bnetserver/REST/LoginRESTService.cpp | 491 | ||||
| -rw-r--r-- | src/server/bnetserver/REST/LoginRESTService.h | 91 | ||||
| -rw-r--r-- | src/server/bnetserver/Server/Session.cpp | 4 | ||||
| -rw-r--r-- | src/server/bnetserver/Server/Session.h | 9 |
7 files changed, 367 insertions, 409 deletions
diff --git a/src/server/bnetserver/Main.cpp b/src/server/bnetserver/Main.cpp index e1168a1a90b..6d667cd48b1 100644 --- a/src/server/bnetserver/Main.cpp +++ b/src/server/bnetserver/Main.cpp @@ -200,21 +200,29 @@ int main(int argc, char** argv) Trinity::Net::ScanLocalNetworks(); - // Start the listening port (acceptor) for auth connections - int32 bnport = sConfigMgr->GetIntDefault("BattlenetPort", 1119); - if (bnport < 0 || bnport > 0xFFFF) + std::string httpBindIp = sConfigMgr->GetStringDefault("BindIP", "0.0.0.0"); + int32 httpPort = sConfigMgr->GetIntDefault("LoginREST.Port", 8081); + if (httpPort <= 0 || httpPort > 0xFFFF) { - TC_LOG_ERROR("server.bnetserver", "Specified battle.net port ({}) out of allowed range (1-65535)", bnport); + TC_LOG_ERROR("server.bnetserver", "Specified login service port ({}) out of allowed range (1-65535)", httpPort); return 1; } - if (!sLoginService.Start(ioContext.get())) + if (!sLoginService.StartNetwork(*ioContext, httpBindIp, httpPort)) { TC_LOG_ERROR("server.bnetserver", "Failed to initialize login service"); return 1; } - std::shared_ptr<void> sLoginServiceHandle(nullptr, [](void*) { sLoginService.Stop(); }); + std::shared_ptr<void> sLoginServiceHandle(nullptr, [](void*) { sLoginService.StopNetwork(); }); + + // Start the listening port (acceptor) for auth connections + int32 bnport = sConfigMgr->GetIntDefault("BattlenetPort", 1119); + if (bnport <= 0 || bnport > 0xFFFF) + { + TC_LOG_ERROR("server.bnetserver", "Specified battle.net port ({}) out of allowed range (1-65535)", bnport); + return 1; + } // Get the list of realms for the server sRealmList->Initialize(*ioContext, sConfigMgr->GetIntDefault("RealmsStateUpdateDelay", 10)); diff --git a/src/server/bnetserver/REST/LoginHttpSession.cpp b/src/server/bnetserver/REST/LoginHttpSession.cpp new file mode 100644 index 00000000000..95112cb8836 --- /dev/null +++ b/src/server/bnetserver/REST/LoginHttpSession.cpp @@ -0,0 +1,118 @@ +/* + * 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 "LoginHttpSession.h" +#include "DatabaseEnv.h" +#include "LoginRESTService.h" +#include "SslContext.h" +#include "Util.h" + +namespace Battlenet +{ +LoginHttpSession::LoginHttpSession(boost::asio::ip::tcp::socket&& socket) + : SslSocket(std::move(socket), SslContext::instance()) +{ +} + +LoginHttpSession::~LoginHttpSession() = default; + +void LoginHttpSession::Start() +{ + std::string ip_address = GetRemoteIpAddress().to_string(); + TC_LOG_TRACE("server.http.session", "{} Accepted connection", GetClientInfo()); + + // Verify that this IP is not in the ip_banned table + LoginDatabase.Execute(LoginDatabase.GetPreparedStatement(LOGIN_DEL_EXPIRED_IP_BANS)); + + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_IP_INFO); + stmt->setString(0, ip_address); + + _queryProcessor.AddCallback(LoginDatabase.AsyncQuery(stmt) + .WithPreparedCallback([sess = shared_from_this()](PreparedQueryResult result) { sess->CheckIpCallback(std::move(result)); })); +} + +void LoginHttpSession::CheckIpCallback(PreparedQueryResult result) +{ + if (result) + { + bool banned = false; + do + { + Field* fields = result->Fetch(); + if (fields[0].GetUInt64() != 0) + banned = true; + + } while (result->NextRow()); + + if (banned) + { + TC_LOG_DEBUG("server.http.session", "{} tries to log in using banned IP!", GetClientInfo()); + CloseSocket(); + return; + } + } + + AsyncHandshake(); +} + +Trinity::Net::Http::RequestHandlerResult LoginHttpSession::RequestHandler(Trinity::Net::Http::RequestContext& context) +{ + return sLoginService.HandleRequest(shared_from_this(), context); +} + +std::shared_ptr<Trinity::Net::Http::SessionState> LoginHttpSession::ObtainSessionState(Trinity::Net::Http::RequestContext& context) const +{ + using namespace std::string_literals; + + std::shared_ptr<Trinity::Net::Http::SessionState> state; + + auto cookieItr = context.request.find(boost::beast::http::field::cookie); + if (cookieItr != context.request.end()) + { + std::vector<std::string_view> cookies = Trinity::Tokenize(Trinity::Net::Http::ToStdStringView(cookieItr->value()), ';', false); + std::size_t eq = 0; + auto sessionIdItr = std::find_if(cookies.begin(), cookies.end(), [&](std::string_view cookie) + { + std::string_view name = cookie; + eq = cookie.find('='); + if (eq != std::string_view::npos) + name = cookie.substr(0, eq); + + return name == SESSION_ID_COOKIE; + }); + if (sessionIdItr != cookies.end()) + { + std::string_view value = sessionIdItr->substr(eq + 1); + state = sLoginService.FindAndRefreshSessionState(value, GetRemoteIpAddress()); + } + } + + if (!state) + { + state = sLoginService.CreateNewSessionState(GetRemoteIpAddress()); + + std::string_view host = Trinity::Net::Http::ToStdStringView(context.request[boost::beast::http::field::host]); + if (std::size_t port = host.find(':'); port != std::string_view::npos) + host.remove_suffix(host.length() - port); + + context.response.insert(boost::beast::http::field::set_cookie, Trinity::StringFormat("{}={}; Path=/bnetserver; Domain={}; Secure; HttpOnly; SameSite=None", + SESSION_ID_COOKIE, boost::uuids::to_string(state->Id), host)); + } + + return state; +} +} diff --git a/src/server/bnetserver/REST/LoginHttpSession.h b/src/server/bnetserver/REST/LoginHttpSession.h new file mode 100644 index 00000000000..17c94b55bda --- /dev/null +++ b/src/server/bnetserver/REST/LoginHttpSession.h @@ -0,0 +1,43 @@ +/* + * 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_LOGIN_HTTP_SESSION_H +#define TRINITYCORE_LOGIN_HTTP_SESSION_H + +#include "HttpSslSocket.h" + +namespace Battlenet +{ +class LoginHttpSession : public Trinity::Net::Http::SslSocket<LoginHttpSession> +{ +public: + static constexpr std::string_view SESSION_ID_COOKIE = "JSESSIONID"; + + explicit LoginHttpSession(boost::asio::ip::tcp::socket&& socket); + ~LoginHttpSession(); + + void Start() override; + + void CheckIpCallback(PreparedQueryResult result); + + Trinity::Net::Http::RequestHandlerResult RequestHandler(Trinity::Net::Http::RequestContext& context) override; + +protected: + std::shared_ptr<Trinity::Net::Http::SessionState> ObtainSessionState(Trinity::Net::Http::RequestContext& context) const override; +}; +} +#endif // TRINITYCORE_LOGIN_HTTP_SESSION_H diff --git a/src/server/bnetserver/REST/LoginRESTService.cpp b/src/server/bnetserver/REST/LoginRESTService.cpp index 6141dc702f9..602a38dceed 100644 --- a/src/server/bnetserver/REST/LoginRESTService.cpp +++ b/src/server/bnetserver/REST/LoginRESTService.cpp @@ -16,71 +16,63 @@ */ #include "LoginRESTService.h" +#include "Base64.h" #include "Configuration/Config.h" #include "CryptoHash.h" #include "CryptoRandom.h" #include "DatabaseEnv.h" -#include "Errors.h" #include "IpNetwork.h" +#include "IteratorPair.h" #include "ProtobufJSON.h" #include "Resolver.h" -#include "SslContext.h" #include "Util.h" -#include "httpget.h" -#include "httppost.h" -#include "soapH.h" +#include <boost/uuid/string_generator.hpp> +#include <fmt/chrono.h> -int ns1__executeCommand(soap*, char*, char**) { return SOAP_OK; } +namespace Battlenet +{ +LoginRESTService& LoginRESTService::Instance() +{ + static LoginRESTService instance; + return instance; +} -class AsyncRequest +bool LoginRESTService::StartNetwork(Trinity::Asio::IoContext& ioContext, std::string const& bindIp, uint16 port, int32 threadCount) { -public: - AsyncRequest(soap const& server) : _client(server), _responseStatus(0) { } + if (!HttpService::StartNetwork(ioContext, bindIp, port, threadCount)) + return false; - AsyncRequest(AsyncRequest const&) = delete; - AsyncRequest& operator=(AsyncRequest const&) = delete; - AsyncRequest(AsyncRequest&&) = default; - AsyncRequest& operator=(AsyncRequest&&) = default; + using Trinity::Net::Http::RequestHandlerFlag; - bool InvokeIfReady() + RegisterHandler(boost::beast::http::verb::get, "/bnetserver/login/", [this](std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) { - ASSERT(_callback); - return _callback->InvokeIfReady(); - } - - soap* GetClient() { return &_client; } - void SetCallback(std::unique_ptr<QueryCallback> callback) { _callback = std::move(callback); } - int32 GetResponseStatus() const { return _responseStatus; } - void SetResponseStatus(int32 responseStatus) { _responseStatus = responseStatus; } + return HandleGetForm(std::move(session), context); + }); -private: - soap _client; - std::unique_ptr<QueryCallback> _callback; - int32 _responseStatus; -}; + RegisterHandler(boost::beast::http::verb::get, "/bnetserver/gameAccounts/", [this](std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) + { + return HandleGetGameAccounts(std::move(session), context); + }); -int32 handle_get_plugin(soap* soapClient) -{ - return sLoginService.HandleHttpRequest(soapClient, "GET", sLoginService._getHandlers); -} + RegisterHandler(boost::beast::http::verb::get, "/bnetserver/portal/", [this](std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) + { + return HandleGetPortal(std::move(session), context); + }); -int32 handle_post_plugin(soap* soapClient) -{ - return sLoginService.HandleHttpRequest(soapClient, "POST", sLoginService._postHandlers); -} + RegisterHandler(boost::beast::http::verb::post, "/bnetserver/login/", [this](std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) + { + return HandlePostLogin(std::move(session), context); + }, RequestHandlerFlag::DoNotLogRequestContent); -bool LoginRESTService::Start(Trinity::Asio::IoContext* ioContext) -{ - _ioContext = ioContext; - _bindIP = sConfigMgr->GetStringDefault("BindIP", "0.0.0.0"); - _port = sConfigMgr->GetIntDefault("LoginREST.Port", 8081); - if (_port < 0 || _port > 0xFFFF) + RegisterHandler(boost::beast::http::verb::post, "/bnetserver/refreshLoginTicket/", [this](std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) { - TC_LOG_ERROR("server.rest", "Specified login service port ({}) out of allowed range (1-65535), defaulting to 8081", _port); - _port = 8081; - } + return HandlePostRefreshLoginTicket(std::move(session), context); + }); - Trinity::Asio::Resolver resolver(*ioContext); + _bindIP = bindIp; + _port = port; + + Trinity::Asio::Resolver resolver(ioContext); _hostnames[0] = sConfigMgr->GetStringDefault("LoginREST.ExternalAddress", "127.0.0.1"); Optional<boost::asio::ip::tcp::endpoint> externalAddress = resolver.Resolve(boost::asio::ip::tcp::v4(), _hostnames[0], std::to_string(_port)); @@ -103,8 +95,8 @@ bool LoginRESTService::Start(Trinity::Asio::IoContext* ioContext) _addresses[1] = localAddress->address(); // set up form inputs - Battlenet::JSON::Login::FormInput* input; - _formInputs.set_type(Battlenet::JSON::Login::LOGIN_FORM); + JSON::Login::FormInput* input; + _formInputs.set_type(JSON::Login::LOGIN_FORM); input = _formInputs.add_inputs(); input->set_input_id("account_name"); input->set_type("text"); @@ -124,16 +116,10 @@ bool LoginRESTService::Start(Trinity::Asio::IoContext* ioContext) _loginTicketDuration = sConfigMgr->GetIntDefault("LoginREST.TicketDuration", 3600); - _thread = std::thread(&LoginRESTService::Run, this); + _acceptor->AsyncAcceptWithCallback<&LoginRESTService::OnSocketAccept>(); return true; } -void LoginRESTService::Stop() -{ - _stopped = true; - _thread.join(); -} - std::string const& LoginRESTService::GetHostnameForClient(boost::asio::ip::address const& address) const { if (auto addressIndex = Trinity::Net::SelectAddressForClient(address, _addresses)) @@ -145,117 +131,53 @@ std::string const& LoginRESTService::GetHostnameForClient(boost::asio::ip::addre return _hostnames[0]; } -void LoginRESTService::Run() +std::string LoginRESTService::ExtractAuthorization(HttpRequest const& request) { - soap soapServer(SOAP_C_UTFSTRING, SOAP_C_UTFSTRING); - - // check every 3 seconds if world ended - soapServer.accept_timeout = 3; - soapServer.recv_timeout = 5; - soapServer.send_timeout = 5; - if (!soap_valid_socket(soap_bind(&soapServer, _bindIP.c_str(), _port, 100))) - { - TC_LOG_ERROR("server.rest", "Couldn't bind to {}:{}", _bindIP, _port); - return; - } - - TC_LOG_INFO("server.rest", "Login service bound to http://{}:{}", _bindIP, _port); - - http_post_handlers handlers[] = - { - { "application/json;charset=utf-8", handle_post_plugin }, - { "application/json", handle_post_plugin }, - { nullptr, nullptr } - }; + using namespace std::string_view_literals; - _getHandlers["/bnetserver/login/"] = &LoginRESTService::HandleGetForm; - _getHandlers["/bnetserver/gameAccounts/"] = &LoginRESTService::HandleGetGameAccounts; - _getHandlers["/bnetserver/portal/"] = &LoginRESTService::HandleGetPortal; + std::string ticket; + auto itr = request.find(boost::beast::http::field::authorization); + if (itr == request.end()) + return ticket; - _postHandlers["/bnetserver/login/"] = &LoginRESTService::HandlePostLogin; - _postHandlers["/bnetserver/refreshLoginTicket/"] = &LoginRESTService::HandlePostRefreshLoginTicket; + std::string_view authorization = Trinity::Net::Http::ToStdStringView(itr->value()); + constexpr std::string_view BASIC_PREFIX = "Basic "sv; - soap_register_plugin_arg(&soapServer, &http_get, (void*)&handle_get_plugin); - soap_register_plugin_arg(&soapServer, &http_post, handlers); - soap_register_plugin_arg(&soapServer, &ContentTypePlugin::Init, (void*)"application/json;charset=utf-8"); - soap_register_plugin_arg(&soapServer, &ResponseCodePlugin::Init, nullptr); + if (authorization.starts_with(BASIC_PREFIX)) + authorization.remove_prefix(BASIC_PREFIX.length()); - // Use our already ready ssl context - soapServer.ctx = Battlenet::SslContext::instance().native_handle(); - soapServer.ssl_flags = SOAP_SSL_RSA; + Optional<std::vector<uint8>> decoded = Trinity::Encoding::Base64::Decode(authorization); + if (!decoded) + return ticket; - while (!_stopped) - { - if (!soap_valid_socket(soap_accept(&soapServer))) - continue; // ran into an accept timeout - - std::shared_ptr<AsyncRequest> soapClient = std::make_shared<AsyncRequest>(soapServer); - if (soap_ssl_accept(soapClient->GetClient()) != SOAP_OK) - { - TC_LOG_DEBUG("server.rest", "Failed SSL handshake from IP={}", boost::asio::ip::address_v4(soapClient->GetClient()->ip).to_string()); - continue; - } - - TC_LOG_DEBUG("server.rest", "Accepted connection from IP={}", boost::asio::ip::address_v4(soapClient->GetClient()->ip).to_string()); - - Trinity::Asio::post(*_ioContext, [soapClient]() - { - soapClient->GetClient()->user = (void*)&soapClient; // this allows us to make a copy of pointer inside GET/POST handlers to increment reference count - soap_begin(soapClient->GetClient()); - soap_begin_recv(soapClient->GetClient()); - }); - } - - // and release the context handle here - soap does not own it so it should not free it on exit - soapServer.ctx = nullptr; - - TC_LOG_INFO("server.rest", "Login service exiting..."); -} - -int32 LoginRESTService::HandleHttpRequest(soap* soapClient, char const* method, HttpMethodHandlerMap const& handlers) -{ - TC_LOG_DEBUG("server.rest", "[{}:{}] Handling {} request path=\"{}\"", - boost::asio::ip::address_v4(soapClient->ip).to_string(), soapClient->port, method, soapClient->path); - - size_t pathLength = strlen(soapClient->path); - if (char const* queryPart = strchr(soapClient->path, '?')) - pathLength = queryPart - soapClient->path; + std::string_view decodedHeader(reinterpret_cast<char const*>(decoded->data()), decoded->size()); - auto handler = handlers.find(std::string{ soapClient->path, pathLength }); - if (handler != handlers.end()) - { - int32 status = (this->*handler->second)(*reinterpret_cast<std::shared_ptr<AsyncRequest>*>(soapClient->user)); - if (status != SOAP_OK) - { - ResponseCodePlugin::GetForClient(soapClient)->ErrorCode = status; - return SendResponse(soapClient, Battlenet::JSON::Login::ErrorResponse()); - } + if (std::size_t ticketEnd = decodedHeader.find(':'); ticketEnd != std::string_view::npos) + decodedHeader.remove_suffix(decodedHeader.length() - ticketEnd); - return SOAP_OK; - } - - ResponseCodePlugin::GetForClient(soapClient)->ErrorCode = 404; - return SendResponse(soapClient, Battlenet::JSON::Login::ErrorResponse()); + ticket = decodedHeader; + return ticket; } -int32 LoginRESTService::HandleGetForm(std::shared_ptr<AsyncRequest> request) +LoginRESTService::RequestHandlerResult LoginRESTService::HandleGetForm(std::shared_ptr<LoginHttpSession> /*session*/, HttpRequestContext& context) { - return SendResponse(request->GetClient(), _formInputs); + context.response.set(boost::beast::http::field::content_type, "application/json;charset=utf-8"); + context.response.body() = ::JSON::Serialize(_formInputs); + return RequestHandlerResult::Handled; } -int32 LoginRESTService::HandleGetGameAccounts(std::shared_ptr<AsyncRequest> request) +LoginRESTService::RequestHandlerResult LoginRESTService::HandleGetGameAccounts(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) { - if (!request->GetClient()->userid) - return 401; - - request->SetCallback(std::make_unique<QueryCallback>(LoginDatabase.AsyncQuery([&] { - LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_GAME_ACCOUNT_LIST); - stmt->setString(0, request->GetClient()->userid); - return stmt; - }()) - .WithPreparedCallback([this, request](PreparedQueryResult result) + std::string ticket = ExtractAuthorization(context.request); + if (ticket.empty()) + return HandleUnauthorized(std::move(session), context); + + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_GAME_ACCOUNT_LIST); + stmt->setString(0, ticket); + session->QueueQuery(LoginDatabase.AsyncQuery(stmt) + .WithPreparedCallback([session, context = std::move(context)](PreparedQueryResult result) mutable { - Battlenet::JSON::Login::GameAccountList response; + JSON::Login::GameAccountList gameAccounts; if (result) { auto formatDisplayName = [](char const* name) -> std::string @@ -270,7 +192,7 @@ int32 LoginRESTService::HandleGetGameAccounts(std::shared_ptr<AsyncRequest> requ do { Field* fields = result->Fetch(); - Battlenet::JSON::Login::GameAccountInfo* gameAccount = response.add_game_accounts(); + JSON::Login::GameAccountInfo* gameAccount = gameAccounts.add_game_accounts(); gameAccount->set_display_name(formatDisplayName(fields[0].GetCString())); gameAccount->set_expansion(fields[1].GetUInt8()); if (!fields[2].IsNull()) @@ -285,40 +207,37 @@ int32 LoginRESTService::HandleGetGameAccounts(std::shared_ptr<AsyncRequest> requ } while (result->NextRow()); } - SendResponse(request->GetClient(), response); - }))); - - Trinity::Asio::post(*_ioContext, [this, request]() { HandleAsyncRequest(request); }); + context.response.set(boost::beast::http::field::content_type, "application/json;charset=utf-8"); + context.response.body() = ::JSON::Serialize(gameAccounts); + session->SendResponse(context); + })); - return SOAP_OK; + return RequestHandlerResult::Async; } -int32 LoginRESTService::HandleGetPortal(std::shared_ptr<AsyncRequest> request) +LoginRESTService::RequestHandlerResult LoginRESTService::HandleGetPortal(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) { - std::string const& hostname = GetHostnameForClient(boost::asio::ip::address_v4(request->GetClient()->ip)); - std::string response = Trinity::StringFormat("{}:{}", hostname, sConfigMgr->GetIntDefault("BattlenetPort", 1119)); - - soap_response(request->GetClient(), SOAP_FILE); - soap_send_raw(request->GetClient(), response.c_str(), response.length()); - return soap_end_send(request->GetClient()); + context.response.set(boost::beast::http::field::content_type, "text/plain"); + context.response.body() = Trinity::StringFormat("{}:{}", GetHostnameForClient(session->GetRemoteIpAddress()), sConfigMgr->GetIntDefault("BattlenetPort", 1119)); + return RequestHandlerResult::Handled; } -int32 LoginRESTService::HandlePostLogin(std::shared_ptr<AsyncRequest> request) +LoginRESTService::RequestHandlerResult LoginRESTService::HandlePostLogin(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) { - char* buf = nullptr; - size_t len = 0; - soap_http_body(request->GetClient(), &buf, &len); - - Battlenet::JSON::Login::LoginForm loginForm; - if (!buf || !JSON::Deserialize(buf, &loginForm)) + JSON::Login::LoginForm loginForm; + if (!::JSON::Deserialize(context.request.body(), &loginForm)) { - ResponseCodePlugin::GetForClient(request->GetClient())->ErrorCode = 400; - - Battlenet::JSON::Login::LoginResult loginResult; - loginResult.set_authentication_state(Battlenet::JSON::Login::LOGIN); + JSON::Login::LoginResult loginResult; + loginResult.set_authentication_state(JSON::Login::LOGIN); loginResult.set_error_code("UNABLE_TO_DECODE"); loginResult.set_error_message("There was an internal error while connecting to Battle.net. Please try again later."); - return SendResponse(request->GetClient(), loginResult); + + context.response.result(boost::beast::http::status::bad_request); + context.response.set(boost::beast::http::field::content_type, "application/json;charset=utf-8"); + context.response.body() = ::JSON::Serialize(loginResult); + session->SendResponse(context); + + return RequestHandlerResult::Handled; } std::string login; @@ -340,44 +259,32 @@ int32 LoginRESTService::HandlePostLogin(std::shared_ptr<AsyncRequest> request) std::string sentPasswordHash = CalculateShaPassHash(login, password); - request->SetCallback(std::make_unique<QueryCallback>(LoginDatabase.AsyncQuery(stmt) - .WithChainingPreparedCallback([request, login, sentPasswordHash, this](QueryCallback& callback, PreparedQueryResult result) + session->QueueQuery(LoginDatabase.AsyncQuery(stmt) + .WithChainingPreparedCallback([this, session, context = std::move(context), login = std::move(login), sentPasswordHash = std::move(sentPasswordHash)](QueryCallback& callback, PreparedQueryResult result) mutable { - if (result) + if (!result) { - Field* fields = result->Fetch(); - uint32 accountId = fields[0].GetUInt32(); - std::string pass_hash = fields[1].GetString(); - uint32 failedLogins = fields[2].GetUInt32(); - std::string loginTicket = fields[3].GetString(); - uint32 loginTicketExpiry = fields[4].GetUInt32(); - bool isBanned = fields[5].GetUInt64() != 0; - - if (sentPasswordHash == pass_hash) - { - if (loginTicket.empty() || loginTicketExpiry < time(nullptr)) - { - std::array<uint8, 20> ticket = Trinity::Crypto::GetRandomBytes<20>(); + JSON::Login::LoginResult loginResult; + loginResult.set_authentication_state(JSON::Login::DONE); + context.response.set(boost::beast::http::field::content_type, "application/json;charset=utf-8"); + context.response.body() = ::JSON::Serialize(loginResult); + session->SendResponse(context); + return; + } - loginTicket = "TC-" + ByteArrayToHexStr(ticket); - } + Field* fields = result->Fetch(); + uint32 accountId = fields[0].GetUInt32(); + std::string pass_hash = fields[1].GetString(); + uint32 failedLogins = fields[2].GetUInt32(); + std::string loginTicket = fields[3].GetString(); + uint32 loginTicketExpiry = fields[4].GetUInt32(); + bool isBanned = fields[5].GetUInt64() != 0; - LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_AUTHENTICATION); - stmt->setString(0, loginTicket); - stmt->setUInt32(1, time(nullptr) + _loginTicketDuration); - stmt->setUInt32(2, accountId); - callback.WithPreparedCallback([request, loginTicket](PreparedQueryResult) - { - Battlenet::JSON::Login::LoginResult loginResult; - loginResult.set_authentication_state(Battlenet::JSON::Login::DONE); - loginResult.set_login_ticket(loginTicket); - sLoginService.SendResponse(request->GetClient(), loginResult); - }).SetNextQuery(LoginDatabase.AsyncQuery(stmt)); - return; - } - else if (!isBanned) + if (sentPasswordHash != pass_hash) + { + if (!isBanned) { - std::string ip_address = boost::asio::ip::address_v4(request->GetClient()->ip).to_string(); + std::string ip_address = session->GetRemoteIpAddress().to_string(); uint32 maxWrongPassword = uint32(sConfigMgr->GetIntDefault("WrongPass.MaxCount", 0)); if (sConfigMgr->GetBoolDefault("WrongPass.Logging", false)) @@ -421,31 +328,50 @@ int32 LoginRESTService::HandlePostLogin(std::shared_ptr<AsyncRequest> request) LoginDatabase.CommitTransaction(trans); } } + + JSON::Login::LoginResult loginResult; + loginResult.set_authentication_state(JSON::Login::DONE); + + context.response.set(boost::beast::http::field::content_type, "application/json;charset=utf-8"); + context.response.body() = ::JSON::Serialize(loginResult); + session->SendResponse(context); + return; } - Battlenet::JSON::Login::LoginResult loginResult; - loginResult.set_authentication_state(Battlenet::JSON::Login::DONE); - sLoginService.SendResponse(request->GetClient(), loginResult); - }))); + if (loginTicket.empty() || loginTicketExpiry < time(nullptr)) + loginTicket = "TC-" + ByteArrayToHexStr(Trinity::Crypto::GetRandomBytes<20>()); - Trinity::Asio::post(*_ioContext, [this, request]() { HandleAsyncRequest(request); }); + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_AUTHENTICATION); + stmt->setString(0, loginTicket); + stmt->setUInt32(1, time(nullptr) + _loginTicketDuration); + stmt->setUInt32(2, accountId); + callback.WithPreparedCallback([session, context = std::move(context), loginTicket = std::move(loginTicket)](PreparedQueryResult) mutable + { + JSON::Login::LoginResult loginResult; + loginResult.set_authentication_state(JSON::Login::DONE); + loginResult.set_login_ticket(loginTicket); + + context.response.set(boost::beast::http::field::content_type, "application/json;charset=utf-8"); + context.response.body() = ::JSON::Serialize(loginResult); + session->SendResponse(context); + }).SetNextQuery(LoginDatabase.AsyncQuery(stmt)); + })); - return SOAP_OK; + return RequestHandlerResult::Async; } -int32 LoginRESTService::HandlePostRefreshLoginTicket(std::shared_ptr<AsyncRequest> request) +LoginRESTService::RequestHandlerResult LoginRESTService::HandlePostRefreshLoginTicket(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context) { - if (!request->GetClient()->userid) - return 401; - - request->SetCallback(std::make_unique<QueryCallback>(LoginDatabase.AsyncQuery([&] { - LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_EXISTING_AUTHENTICATION); - stmt->setString(0, request->GetClient()->userid); - return stmt; - }()) - .WithPreparedCallback([this, request](PreparedQueryResult result) + std::string ticket = ExtractAuthorization(context.request); + if (ticket.empty()) + return HandleUnauthorized(std::move(session), context); + + LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_EXISTING_AUTHENTICATION); + stmt->setString(0, ticket); + session->QueueQuery(LoginDatabase.AsyncQuery(stmt) + .WithPreparedCallback([this, session, context = std::move(context), ticket = std::move(ticket)](PreparedQueryResult result) mutable { - Battlenet::JSON::Login::LoginRefreshResult loginRefreshResult; + JSON::Login::LoginRefreshResult loginRefreshResult; if (result) { uint32 loginTicketExpiry = (*result)[0].GetUInt32(); @@ -456,7 +382,7 @@ int32 LoginRESTService::HandlePostRefreshLoginTicket(std::shared_ptr<AsyncReques LoginDatabasePreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_EXISTING_AUTHENTICATION); stmt->setUInt32(0, uint32(now + _loginTicketDuration)); - stmt->setString(1, request->GetClient()->userid); + stmt->setString(1, ticket); LoginDatabase.Execute(stmt); } else @@ -465,34 +391,12 @@ int32 LoginRESTService::HandlePostRefreshLoginTicket(std::shared_ptr<AsyncReques else loginRefreshResult.set_is_expired(true); - SendResponse(request->GetClient(), loginRefreshResult); - }))); - - Trinity::Asio::post(*_ioContext, [this, request]() { HandleAsyncRequest(request); }); - - return SOAP_OK; -} - -int32 LoginRESTService::SendResponse(soap* soapClient, google::protobuf::Message const& response) -{ - std::string jsonResponse = JSON::Serialize(response); - - soap_response(soapClient, SOAP_FILE); - soap_send_raw(soapClient, jsonResponse.c_str(), jsonResponse.length()); - return soap_end_send(soapClient); -} + context.response.set(boost::beast::http::field::content_type, "application/json;charset=utf-8"); + context.response.body() = ::JSON::Serialize(loginRefreshResult); + session->SendResponse(context); + })); -void LoginRESTService::HandleAsyncRequest(std::shared_ptr<AsyncRequest> request) -{ - if (!request->InvokeIfReady()) - { - Trinity::Asio::post(*_ioContext, [this, request]() { HandleAsyncRequest(request); }); - } - else if (request->GetResponseStatus()) - { - ResponseCodePlugin::GetForClient(request->GetClient())->ErrorCode = request->GetResponseStatus(); - SendResponse(request->GetClient(), Battlenet::JSON::Login::ErrorResponse()); - } + return RequestHandlerResult::Async; } std::string LoginRESTService::CalculateShaPassHash(std::string const& name, std::string const& password) @@ -510,85 +414,8 @@ std::string LoginRESTService::CalculateShaPassHash(std::string const& name, std: return ByteArrayToHexStr(sha.GetDigest(), true); } -Namespace namespaces[] = -{ - { nullptr, nullptr, nullptr, nullptr } -}; - -LoginRESTService& LoginRESTService::Instance() -{ - static LoginRESTService instance; - return instance; -} - -char const* const LoginRESTService::ResponseCodePlugin::PluginId = "bnet-error-code"; - -int32 LoginRESTService::ResponseCodePlugin::Init(soap* s, soap_plugin* p, void* /*arg*/) -{ - ResponseCodePlugin* data = new ResponseCodePlugin(); - data->fresponse = s->fresponse; - - p->id = PluginId; - p->fcopy = &Copy; - p->fdelete = &Destroy; - p->data = data; - - s->fresponse = &ChangeResponse; - return SOAP_OK; -} - -int32 LoginRESTService::ResponseCodePlugin::Copy(soap* /*s*/, soap_plugin* dst, soap_plugin* src) -{ - dst->data = new ResponseCodePlugin(*reinterpret_cast<ResponseCodePlugin*>(src->data)); - return SOAP_OK; -} - -void LoginRESTService::ResponseCodePlugin::Destroy(soap* s, soap_plugin* p) -{ - ResponseCodePlugin* data = reinterpret_cast<ResponseCodePlugin*>(p->data); - s->fresponse = data->fresponse; - delete data; -} - -int32 LoginRESTService::ResponseCodePlugin::ChangeResponse(soap* s, int32 originalResponse, uint64 contentLength) -{ - ResponseCodePlugin* self = reinterpret_cast<ResponseCodePlugin*>(soap_lookup_plugin(s, PluginId)); - return self->fresponse(s, self->ErrorCode && originalResponse == SOAP_FILE ? self->ErrorCode : originalResponse, contentLength); -} - -LoginRESTService::ResponseCodePlugin* LoginRESTService::ResponseCodePlugin::GetForClient(soap* s) -{ - return ASSERT_NOTNULL(reinterpret_cast<ResponseCodePlugin*>(soap_lookup_plugin(s, PluginId))); -} - -char const* const LoginRESTService::ContentTypePlugin::PluginId = "bnet-content-type"; - -int32 LoginRESTService::ContentTypePlugin::Init(soap* s, soap_plugin* p, void* arg) -{ - ContentTypePlugin* data = new ContentTypePlugin(); - data->fposthdr = s->fposthdr; - data->ContentType = reinterpret_cast<char const*>(arg); - - p->id = PluginId; - p->fdelete = &Destroy; - p->data = data; - - s->fposthdr = &OnSetHeader; - return SOAP_OK; -} - -void LoginRESTService::ContentTypePlugin::Destroy(soap* s, soap_plugin* p) +void LoginRESTService::OnSocketAccept(boost::asio::ip::tcp::socket&& sock, uint32 threadIndex) { - ContentTypePlugin* data = reinterpret_cast<ContentTypePlugin*>(p->data); - s->fposthdr = data->fposthdr; - delete data; + sLoginService.OnSocketOpen(std::move(sock), threadIndex); } - -int32 LoginRESTService::ContentTypePlugin::OnSetHeader(soap* s, char const* key, char const* value) -{ - ContentTypePlugin* self = reinterpret_cast<ContentTypePlugin*>(soap_lookup_plugin(s, PluginId)); - if (key && !strcmp("Content-Type", key)) - value = self->ContentType; - - return self->fposthdr(s, key, value); } diff --git a/src/server/bnetserver/REST/LoginRESTService.h b/src/server/bnetserver/REST/LoginRESTService.h index f783fea243a..1313493e023 100644 --- a/src/server/bnetserver/REST/LoginRESTService.h +++ b/src/server/bnetserver/REST/LoginRESTService.h @@ -18,98 +18,59 @@ #ifndef LoginRESTService_h__ #define LoginRESTService_h__ -#include "Define.h" -#include "IoContext.h" +#include "HttpService.h" #include "Login.pb.h" -#include "Session.h" -#include <boost/asio/ip/tcp.hpp> -#include <atomic> -#include <thread> - -class AsyncRequest; -struct soap; -struct soap_plugin; +#include "LoginHttpSession.h" +namespace Battlenet +{ enum class BanMode { BAN_IP = 0, BAN_ACCOUNT = 1 }; -class LoginRESTService +class LoginRESTService : public Trinity::Net::Http::HttpService<LoginHttpSession> { public: - LoginRESTService() : _ioContext(nullptr), _stopped(false), _port(0), _loginTicketDuration(0) { } + using RequestHandlerResult = Trinity::Net::Http::RequestHandlerResult; + using HttpRequest = Trinity::Net::Http::Request; + using HttpResponse = Trinity::Net::Http::Response; + using HttpRequestContext = Trinity::Net::Http::RequestContext; + using HttpSessionState = Trinity::Net::Http::SessionState; + + LoginRESTService() : HttpService("login"), _port(0), _loginTicketDuration(0) { } static LoginRESTService& Instance(); - bool Start(Trinity::Asio::IoContext* ioContext); - void Stop(); + bool StartNetwork(Trinity::Asio::IoContext& ioContext, std::string const& bindIp, uint16 port, int32 threadCount = 1) override; std::string const& GetHostnameForClient(boost::asio::ip::address const& address) const; - int32 GetPort() const { return _port; } + uint16 GetPort() const { return _port; } private: - void Run(); - - friend int32 handle_get_plugin(soap* soapClient); - friend int32 handle_post_plugin(soap* soapClient); - - using HttpMethodHandlerMap = std::unordered_map<std::string, int32(LoginRESTService::*)(std::shared_ptr<AsyncRequest>)>; - int32 HandleHttpRequest(soap* soapClient, char const* method, HttpMethodHandlerMap const& handlers); - - int32 HandleGetForm(std::shared_ptr<AsyncRequest> request); - int32 HandleGetGameAccounts(std::shared_ptr<AsyncRequest> request); - int32 HandleGetPortal(std::shared_ptr<AsyncRequest> request); + static void OnSocketAccept(boost::asio::ip::tcp::socket&& sock, uint32 threadIndex); - int32 HandlePostLogin(std::shared_ptr<AsyncRequest> request); - int32 HandlePostRefreshLoginTicket(std::shared_ptr<AsyncRequest> request); + static std::string ExtractAuthorization(HttpRequest const& request); - int32 SendResponse(soap* soapClient, google::protobuf::Message const& response); + RequestHandlerResult HandleGetForm(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); + RequestHandlerResult HandleGetGameAccounts(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); + RequestHandlerResult HandleGetPortal(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); - void HandleAsyncRequest(std::shared_ptr<AsyncRequest> request); + RequestHandlerResult HandlePostLogin(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); + RequestHandlerResult HandlePostRefreshLoginTicket(std::shared_ptr<LoginHttpSession> session, HttpRequestContext& context); - std::string CalculateShaPassHash(std::string const& name, std::string const& password); + static std::string CalculateShaPassHash(std::string const& name, std::string const& password); - struct ResponseCodePlugin - { - static char const* const PluginId; - static int32 Init(soap* s, soap_plugin*, void*); - static int32 Copy(soap* s, soap_plugin* dst, soap_plugin* src); - static void Destroy(soap* s, soap_plugin* p); - static int32 ChangeResponse(soap* s, int32 originalResponse, uint64 contentLength); - - static ResponseCodePlugin* GetForClient(soap* s); - - int32(*fresponse)(soap* s, int32 status, uint64 length); - int32 ErrorCode; - }; - - struct ContentTypePlugin - { - static char const* const PluginId; - static int32 Init(soap* s, soap_plugin* p, void*); - static void Destroy(soap* s, soap_plugin* p); - static int32 OnSetHeader(soap* s, char const* key, char const* value); - - int32(*fposthdr)(soap* s, char const* key, char const* value); - char const* ContentType; - }; - - Trinity::Asio::IoContext* _ioContext; - std::thread _thread; - std::atomic<bool> _stopped; - Battlenet::JSON::Login::FormInputs _formInputs; + JSON::Login::FormInputs _formInputs; std::string _bindIP; - int32 _port; + uint16 _port; std::array<std::string, 2> _hostnames; std::array<boost::asio::ip::address, 2> _addresses; uint32 _loginTicketDuration; - - HttpMethodHandlerMap _getHandlers; - HttpMethodHandlerMap _postHandlers; }; +} -#define sLoginService LoginRESTService::Instance() +#define sLoginService Battlenet::LoginRESTService::Instance() #endif // LoginRESTService_h__ diff --git a/src/server/bnetserver/Server/Session.cpp b/src/server/bnetserver/Server/Session.cpp index ea5256ed635..b450df14e1d 100644 --- a/src/server/bnetserver/Server/Session.cpp +++ b/src/server/bnetserver/Server/Session.cpp @@ -30,6 +30,7 @@ #include "RealmList.h" #include "RealmList.pb.h" #include "ServiceDispatcher.h" +#include "SslContext.h" #include "Timezone.h" #include <rapidjson/document.h> #include <zlib.h> @@ -73,7 +74,8 @@ void Battlenet::Session::GameAccountInfo::LoadResult(Field const* fields) DisplayName = Name; } -Battlenet::Session::Session(boost::asio::ip::tcp::socket&& socket) : BattlenetSocket(std::move(socket)), _accountInfo(new AccountInfo()), _gameAccountInfo(nullptr), _locale(), +Battlenet::Session::Session(boost::asio::ip::tcp::socket&& socket) : BattlenetSocket(std::move(socket), SslContext::instance()), + _accountInfo(new AccountInfo()), _gameAccountInfo(nullptr), _locale(), _os(), _build(0), _timezoneOffset(0min), _ipCountry(), _clientSecret(), _authed(false), _requestToken(0) { _headerLengthBuffer.Resize(2); diff --git a/src/server/bnetserver/Server/Session.h b/src/server/bnetserver/Server/Session.h index 6d22c202615..33a058f940c 100644 --- a/src/server/bnetserver/Server/Session.h +++ b/src/server/bnetserver/Server/Session.h @@ -20,11 +20,10 @@ #include "AsyncCallbackProcessor.h" #include "Duration.h" +#include "QueryResult.h" #include "Realm.h" -#include "SslContext.h" -#include "SslSocket.h" #include "Socket.h" -#include "QueryResult.h" +#include "SslSocket.h" #include <boost/asio/ip/tcp.hpp> #include <google/protobuf/message.h> #include <memory> @@ -65,9 +64,9 @@ using namespace bgs::protocol; namespace Battlenet { - class Session : public Socket<Session, SslSocket<SslContext>> + class Session : public Socket<Session, SslSocket<>> { - typedef Socket<Session, SslSocket<SslContext>> BattlenetSocket; + typedef Socket<Session, SslSocket<>> BattlenetSocket; public: struct LastPlayedCharacterInfo |
