Core/Bnet: Changed login REST service to use async db queries instead of spawning a thread for each request

This commit is contained in:
Shauren
2017-07-14 19:18:42 +02:00
parent f29d0740e6
commit 274c45e548
4 changed files with 161 additions and 86 deletions
+1 -1
View File
@@ -163,7 +163,7 @@ int main(int argc, char** argv)
return 1;
}
if (!sLoginService.Start(*ioService))
if (!sLoginService.Start(ioService.get()))
{
TC_LOG_ERROR("server.bnetserver", "Failed to initialize login service");
return 1;
+150 -79
View File
@@ -18,7 +18,9 @@
#include "LoginRESTService.h"
#include "Configuration/Config.h"
#include "DatabaseEnv.h"
#include "Errors.h"
#include "ProtobufJSON.h"
#include "Optional.h"
#include "Realm.h"
#include "SessionManager.h"
#include "SHA1.h"
@@ -31,6 +33,37 @@
int ns1__executeCommand(soap*, char*, char**) { return SOAP_OK; }
class AsyncLoginRequest
{
public:
AsyncLoginRequest(std::shared_ptr<soap> client)
: _client(std::move(client)) { }
AsyncLoginRequest(AsyncLoginRequest const&) = delete;
AsyncLoginRequest& operator=(AsyncLoginRequest const&) = delete;
AsyncLoginRequest(AsyncLoginRequest&&) = default;
AsyncLoginRequest& operator=(AsyncLoginRequest&&) = default;
bool InvokeIfReady()
{
ASSERT(_callback.is_initialized());
return _callback->InvokeIfReady() == QueryCallback::Completed;
}
soap* GetClient() const { return _client.get(); }
void SetCallback(QueryCallback val) { _callback = std::move(val); }
std::unique_ptr<Battlenet::Session::AccountInfo>& GetResult() { return _result; }
void SetResult(std::unique_ptr<Battlenet::Session::AccountInfo> val) { _result = std::move(val); }
private:
std::shared_ptr<soap> _client;
Optional<QueryCallback> _callback;
std::unique_ptr<Battlenet::Session::AccountInfo> _result;
};
/* Codes 600 to 999 are user definable */
#define SOAP_CUSTOM_STATUS_ASYNC 600
int32 handle_get_plugin(soap* soapClient)
{
return sLoginService.HandleGet(soapClient);
@@ -41,8 +74,9 @@ int32 handle_post_plugin(soap* soapClient)
return sLoginService.HandlePost(soapClient);
}
bool LoginRESTService::Start(boost::asio::io_service& ioService)
bool LoginRESTService::Start(boost::asio::io_service* ioService)
{
_ioService = ioService;
_bindIP = sConfigMgr->GetStringDefault("BindIP", "0.0.0.0");
_port = sConfigMgr->GetIntDefault("LoginREST.Port", 8081);
if (_port < 0 || _port > 0xFFFF)
@@ -52,7 +86,7 @@ bool LoginRESTService::Start(boost::asio::io_service& ioService)
}
boost::system::error_code ec;
boost::asio::ip::tcp::resolver resolver(ioService);
boost::asio::ip::tcp::resolver resolver(*ioService);
boost::asio::ip::tcp::resolver::iterator end;
std::string configuredAddress = sConfigMgr->GetStringDefault("LoginREST.ExternalAddress", "127.0.0.1");
@@ -97,7 +131,7 @@ bool LoginRESTService::Start(boost::asio::io_service& ioService)
input->set_type("submit");
input->set_label("Log In");
_loginTicketCleanupTimer = new boost::asio::deadline_timer(ioService);
_loginTicketCleanupTimer = new boost::asio::deadline_timer(*ioService);
_loginTicketCleanupTimer->expires_from_now(boost::posix_time::seconds(10));
_loginTicketCleanupTimer->async_wait(std::bind(&LoginRESTService::CleanupLoginTickets, this, std::placeholders::_1));
@@ -172,10 +206,13 @@ void LoginRESTService::Run()
TC_LOG_DEBUG("server.rest", "Accepted connection from IP=%s", address.to_string().c_str());
std::thread([soapClient]
_ioService->post([soapClient]()
{
soap_serve(soapClient.get());
}).detach();
soapClient->user = (void*)&soapClient; // this allows us to make a copy of pointer inside GET/POST handlers to increment reference count
soap_begin(soapClient.get());
if (soap_begin_recv(soapClient.get()) != SOAP_CUSTOM_STATUS_ASYNC)
soap_closesock(soapClient.get());
});
}
// and release the context handle here - soap does not own it so it should not free it on exit
@@ -214,7 +251,6 @@ int32 LoginRESTService::HandlePost(soap* soapClient)
soap_http_body(soapClient, &buf, &len);
Battlenet::JSON::Login::LoginForm loginForm;
Battlenet::JSON::Login::LoginResult loginResult;
if (!JSON::Deserialize(buf, &loginForm))
{
if (soap_register_plugin_arg(soapClient, &ResponseCodePlugin::Init, nullptr) != SOAP_OK)
@@ -225,6 +261,7 @@ int32 LoginRESTService::HandlePost(soap* soapClient)
responseCode->ErrorCode = 400;
Battlenet::JSON::Login::LoginResult loginResult;
loginResult.set_authentication_state(Battlenet::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.");
@@ -248,99 +285,127 @@ int32 LoginRESTService::HandlePost(soap* soapClient)
PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_ACCOUNT_INFO);
stmt->setString(0, login);
if (PreparedQueryResult result = LoginDatabase.Query(stmt))
std::string sentPasswordHash = CalculateShaPassHash(login, password);
std::shared_ptr<AsyncLoginRequest> request = std::make_shared<AsyncLoginRequest>(*reinterpret_cast<std::shared_ptr<soap>*>(soapClient->user));
request->SetCallback(LoginDatabase.AsyncQuery(stmt)
.WithChainingPreparedCallback([request, login, sentPasswordHash](QueryCallback& callback, PreparedQueryResult result)
{
std::string pass_hash = result->Fetch()[13].GetString();
std::unique_ptr<Battlenet::Session::AccountInfo> accountInfo = Trinity::make_unique<Battlenet::Session::AccountInfo>();
accountInfo->LoadResult(result);
if (CalculateShaPassHash(login, std::move(password)) == pass_hash)
if (result)
{
stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_CHARACTER_COUNTS_BY_BNET_ID);
stmt->setUInt32(0, accountInfo->Id);
if (PreparedQueryResult characterCountsResult = LoginDatabase.Query(stmt))
std::string pass_hash = result->Fetch()[13].GetString();
request->SetResult(Trinity::make_unique<Battlenet::Session::AccountInfo>());
request->GetResult()->LoadResult(result);
if (sentPasswordHash == pass_hash)
{
do
{
Field* fields = characterCountsResult->Fetch();
accountInfo->GameAccounts[fields[0].GetUInt32()]
.CharacterCounts[Battlenet::RealmHandle{ fields[3].GetUInt8(), fields[4].GetUInt8(), fields[2].GetUInt32() }.GetAddress()] = fields[1].GetUInt8();
} while (characterCountsResult->NextRow());
PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_CHARACTER_COUNTS_BY_BNET_ID);
stmt->setUInt32(0, request->GetResult()->Id);
callback.SetNextQuery(LoginDatabase.AsyncQuery(stmt));
return;
}
else if (!request->GetResult()->IsBanned)
{
std::string ip_address = boost::asio::ip::address_v4(request->GetClient()->ip).to_string();
uint32 maxWrongPassword = uint32(sConfigMgr->GetIntDefault("WrongPass.MaxCount", 0));
stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_LAST_PLAYER_CHARACTERS);
stmt->setUInt32(0, accountInfo->Id);
if (PreparedQueryResult lastPlayerCharactersResult = LoginDatabase.Query(stmt))
if (sConfigMgr->GetBoolDefault("WrongPass.Logging", false))
TC_LOG_DEBUG("server.rest", "[%s, Account %s, Id %u] Attempted to connect with wrong password!", ip_address.c_str(), login.c_str(), request->GetResult()->Id);
if (maxWrongPassword)
{
SQLTransaction trans = LoginDatabase.BeginTransaction();
PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_FAILED_LOGINS);
stmt->setUInt32(0, request->GetResult()->Id);
trans->Append(stmt);
++request->GetResult()->FailedLogins;
TC_LOG_DEBUG("server.rest", "MaxWrongPass : %u, failed_login : %u", maxWrongPassword, request->GetResult()->Id);
if (request->GetResult()->FailedLogins >= maxWrongPassword)
{
BanMode banType = BanMode(sConfigMgr->GetIntDefault("WrongPass.BanType", uint16(BanMode::BAN_IP)));
int32 banTime = sConfigMgr->GetIntDefault("WrongPass.BanTime", 600);
if (banType == BanMode::BAN_ACCOUNT)
{
stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_BNET_ACCOUNT_AUTO_BANNED);
stmt->setUInt32(0, request->GetResult()->Id);
}
else
{
stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_IP_AUTO_BANNED);
stmt->setString(0, ip_address);
}
stmt->setUInt32(1, banTime);
trans->Append(stmt);
stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_RESET_FAILED_LOGINS);
stmt->setUInt32(0, request->GetResult()->Id);
trans->Append(stmt);
}
LoginDatabase.CommitTransaction(trans);
}
}
}
Battlenet::JSON::Login::LoginResult loginResult;
loginResult.set_authentication_state(Battlenet::JSON::Login::DONE);
sLoginService.SendResponse(request->GetClient(), loginResult);
})
.WithChainingPreparedCallback([request](QueryCallback& callback, PreparedQueryResult characterCountsResult)
{
if (characterCountsResult)
{
do
{
Field* fields = characterCountsResult->Fetch();
request->GetResult()->GameAccounts[fields[0].GetUInt32()]
.CharacterCounts[Battlenet::RealmHandle{ fields[3].GetUInt8(), fields[4].GetUInt8(), fields[2].GetUInt32() }.GetAddress()] = fields[1].GetUInt8();
} while (characterCountsResult->NextRow());
}
PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_LAST_PLAYER_CHARACTERS);
stmt->setUInt32(0, request->GetResult()->Id);
callback.SetNextQuery(LoginDatabase.AsyncQuery(stmt));
})
.WithPreparedCallback([request](PreparedQueryResult lastPlayerCharactersResult)
{
if (lastPlayerCharactersResult)
{
do
{
Field* fields = lastPlayerCharactersResult->Fetch();
Battlenet::RealmHandle realmId{ fields[1].GetUInt8(), fields[2].GetUInt8(), fields[3].GetUInt32() };
Battlenet::Session::LastPlayedCharacterInfo& lastPlayedCharacter = accountInfo->GameAccounts[fields[0].GetUInt32()]
Battlenet::Session::LastPlayedCharacterInfo& lastPlayedCharacter = request->GetResult()->GameAccounts[fields[0].GetUInt32()]
.LastPlayedCharacters[realmId.GetSubRegionAddress()];
lastPlayedCharacter.RealmId = realmId;
lastPlayedCharacter.CharacterName = fields[4].GetString();
lastPlayedCharacter.CharacterGUID = fields[5].GetUInt64();
lastPlayedCharacter.LastPlayedTime = fields[6].GetUInt32();
}
BigNumber ticket;
ticket.SetRand(20 * 8);
loginResult.set_login_ticket("TC-" + ByteArrayToHexStr(ticket.AsByteArray(20).get(), 20));
AddLoginTicket(loginResult.login_ticket(), std::move(accountInfo));
} while (lastPlayerCharactersResult->NextRow());
}
else if (!accountInfo->IsBanned)
{
uint32 maxWrongPassword = uint32(sConfigMgr->GetIntDefault("WrongPass.MaxCount", 0));
if (sConfigMgr->GetBoolDefault("WrongPass.Logging", false))
TC_LOG_DEBUG("server.rest", "[%s, Account %s, Id %u] Attempted to connect with wrong password!", ip_address.c_str(), login.c_str(), accountInfo->Id);
BigNumber ticket;
ticket.SetRand(20 * 8);
if (maxWrongPassword)
{
SQLTransaction trans = LoginDatabase.BeginTransaction();
stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_FAILED_LOGINS);
stmt->setUInt32(0, accountInfo->Id);
trans->Append(stmt);
Battlenet::JSON::Login::LoginResult loginResult;
loginResult.set_authentication_state(Battlenet::JSON::Login::DONE);
loginResult.set_login_ticket("TC-" + ByteArrayToHexStr(ticket.AsByteArray(20).get(), 20));
sLoginService.SendResponse(request->GetClient(), loginResult);
++accountInfo->FailedLogins;
sLoginService.AddLoginTicket(loginResult.login_ticket(), std::move(request->GetResult()));
}));
TC_LOG_DEBUG("server.rest", "MaxWrongPass : %u, failed_login : %u", maxWrongPassword, accountInfo->Id);
_ioService->post(std::bind(&LoginRESTService::HandleAsyncRequest, this, std::move(request)));
if (accountInfo->FailedLogins >= maxWrongPassword)
{
BanMode banType = BanMode(sConfigMgr->GetIntDefault("WrongPass.BanType", uint16(BanMode::BAN_IP)));
int32 banTime = sConfigMgr->GetIntDefault("WrongPass.BanTime", 600);
if (banType == BanMode::BAN_ACCOUNT)
{
stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_BNET_ACCOUNT_AUTO_BANNED);
stmt->setUInt32(0, accountInfo->Id);
}
else
{
stmt = LoginDatabase.GetPreparedStatement(LOGIN_INS_IP_AUTO_BANNED);
stmt->setString(0, ip_address);
}
stmt->setUInt32(1, banTime);
trans->Append(stmt);
stmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_BNET_RESET_FAILED_LOGINS);
stmt->setUInt32(0, accountInfo->Id);
trans->Append(stmt);
}
LoginDatabase.CommitTransaction(trans);
}
}
}
loginResult.set_authentication_state(Battlenet::JSON::Login::DONE);
return SendResponse(soapClient, loginResult);
return SOAP_CUSTOM_STATUS_ASYNC;
}
int32 LoginRESTService::SendResponse(soap* soapClient, google::protobuf::Message const& response)
@@ -352,6 +417,12 @@ int32 LoginRESTService::SendResponse(soap* soapClient, google::protobuf::Message
return soap_end_send(soapClient);
}
void LoginRESTService::HandleAsyncRequest(std::shared_ptr<AsyncLoginRequest> request)
{
if (!request->InvokeIfReady())
_ioService->post(std::bind(&LoginRESTService::HandleAsyncRequest, this, std::move(request)));
}
std::string LoginRESTService::CalculateShaPassHash(std::string const& name, std::string const& password)
{
SHA256Hash email;
@@ -18,8 +18,8 @@
#ifndef LoginRESTService_h__
#define LoginRESTService_h__
#include "Session.h"
#include "Define.h"
#include "Session.h"
#include "Login.pb.h"
#include <boost/asio/io_service.hpp>
#include <boost/asio/ip/tcp.hpp>
@@ -29,6 +29,7 @@
#include <mutex>
#include <thread>
class AsyncLoginRequest;
struct soap;
struct soap_plugin;
@@ -41,11 +42,11 @@ enum class BanMode
class LoginRESTService
{
public:
LoginRESTService() : _stopped(false), _port(0), _loginTicketCleanupTimer(nullptr) { }
LoginRESTService() : _ioService(nullptr), _stopped(false), _port(0), _loginTicketCleanupTimer(nullptr) { }
static LoginRESTService& Instance();
bool Start(boost::asio::io_service& ioService);
bool Start(boost::asio::io_service* ioService);
void Stop();
boost::asio::ip::tcp::endpoint const& GetAddressForClient(boost::asio::ip::address const& address) const;
@@ -63,6 +64,8 @@ private:
int32 SendResponse(soap* soapClient, google::protobuf::Message const& response);
void HandleAsyncRequest(std::shared_ptr<AsyncLoginRequest> request);
std::string CalculateShaPassHash(std::string const& name, std::string const& password);
void AddLoginTicket(std::string const& id, std::unique_ptr<Battlenet::Session::AccountInfo> accountInfo);
@@ -99,6 +102,7 @@ private:
char const* ContentType;
};
boost::asio::io_service* _ioService;
std::thread _thread;
std::atomic<bool> _stopped;
Battlenet::JSON::Login::FormInputs _formInputs;
@@ -121,12 +121,12 @@ void LoginDatabaseConnection::DoPrepareStatements()
PrepareStatement(LOGIN_SEL_BNET_ACCOUNT_INFO, "SELECT " BnetAccountInfo ", " BnetGameAccountInfo ", ba.sha_pass_hash"
" FROM battlenet_accounts ba LEFT JOIN battlenet_account_bans bab ON ba.id = bab.id LEFT JOIN account a ON ba.id = a.battlenet_account"
" LEFT JOIN account_banned ab ON a.id = ab.id AND ab.active = 1 LEFT JOIN account_access aa ON a.id = aa.id AND aa.RealmID = -1 WHERE ba.email = ? ORDER BY a.id", CONNECTION_SYNCH);
" LEFT JOIN account_banned ab ON a.id = ab.id AND ab.active = 1 LEFT JOIN account_access aa ON a.id = aa.id AND aa.RealmID = -1 WHERE ba.email = ? ORDER BY a.id", CONNECTION_ASYNC);
PrepareStatement(LOGIN_UPD_BNET_LAST_LOGIN_INFO, "UPDATE battlenet_accounts SET last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ? WHERE id = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_UPD_BNET_GAME_ACCOUNT_LOGIN_INFO, "UPDATE account SET sessionkey = ?, last_ip = ?, last_login = NOW(), locale = ?, failed_logins = 0, os = ? WHERE username = ?", CONNECTION_SYNCH);
PrepareStatement(LOGIN_SEL_BNET_CHARACTER_COUNTS_BY_ACCOUNT_ID, "SELECT rc.acctid, rc.numchars, r.id, r.Region, r.Battlegroup FROM realmcharacters rc INNER JOIN realmlist r ON rc.realmid = r.id WHERE rc.acctid = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_SEL_BNET_CHARACTER_COUNTS_BY_BNET_ID, "SELECT rc.acctid, rc.numchars, r.id, r.Region, r.Battlegroup FROM realmcharacters rc INNER JOIN realmlist r ON rc.realmid = r.id LEFT JOIN account a ON rc.acctid = a.id WHERE a.battlenet_account = ?", CONNECTION_SYNCH);
PrepareStatement(LOGIN_SEL_BNET_LAST_PLAYER_CHARACTERS, "SELECT lpc.accountId, lpc.region, lpc.battlegroup, lpc.realmId, lpc.characterName, lpc.characterGUID, lpc.lastPlayedTime FROM account_last_played_character lpc LEFT JOIN account a ON lpc.accountId = a.id WHERE a.battlenet_account = ?", CONNECTION_SYNCH);
PrepareStatement(LOGIN_SEL_BNET_CHARACTER_COUNTS_BY_BNET_ID, "SELECT rc.acctid, rc.numchars, r.id, r.Region, r.Battlegroup FROM realmcharacters rc INNER JOIN realmlist r ON rc.realmid = r.id LEFT JOIN account a ON rc.acctid = a.id WHERE a.battlenet_account = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_SEL_BNET_LAST_PLAYER_CHARACTERS, "SELECT lpc.accountId, lpc.region, lpc.battlegroup, lpc.realmId, lpc.characterName, lpc.characterGUID, lpc.lastPlayedTime FROM account_last_played_character lpc LEFT JOIN account a ON lpc.accountId = a.id WHERE a.battlenet_account = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_DEL_BNET_LAST_PLAYER_CHARACTERS, "DELETE FROM account_last_played_character WHERE accountId = ? AND region = ? AND battlegroup = ?", CONNECTION_ASYNC);
PrepareStatement(LOGIN_INS_BNET_LAST_PLAYER_CHARACTERS, "INSERT INTO account_last_played_character (accountId, region, battlegroup, realmId, characterName, characterGUID, lastPlayedTime) VALUES (?,?,?,?,?,?,?)", CONNECTION_ASYNC);
PrepareStatement(LOGIN_INS_BNET_ACCOUNT, "INSERT INTO battlenet_accounts (`email`,`sha_pass_hash`) VALUES (?, ?)", CONNECTION_SYNCH);