aboutsummaryrefslogtreecommitdiff
path: root/src/server/bnetserver/REST/LoginRESTService.cpp
diff options
context:
space:
mode:
authorShauren <shauren.trinity@gmail.com>2016-03-28 17:12:57 +0200
committerShauren <shauren.trinity@gmail.com>2016-03-28 17:12:57 +0200
commitdde620c402daf4ea8d132fb72a77eabc22f7a6d0 (patch)
tree7c12161d7a22915736b0c9a106de896eeb283399 /src/server/bnetserver/REST/LoginRESTService.cpp
parent619669c6209441fc2fb5b483d553badee8c30ad5 (diff)
Core: Updated to 6.2.4
* Rewrite bnetserver for new authentication protocol
Diffstat (limited to 'src/server/bnetserver/REST/LoginRESTService.cpp')
-rw-r--r--src/server/bnetserver/REST/LoginRESTService.cpp433
1 files changed, 433 insertions, 0 deletions
diff --git a/src/server/bnetserver/REST/LoginRESTService.cpp b/src/server/bnetserver/REST/LoginRESTService.cpp
new file mode 100644
index 00000000000..9ce21be9459
--- /dev/null
+++ b/src/server/bnetserver/REST/LoginRESTService.cpp
@@ -0,0 +1,433 @@
+/*
+ * Copyright (C) 2008-2016 TrinityCore <http://www.trinitycore.org/>
+ *
+ * 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 "LoginRESTService.h"
+#include "Configuration/Config.h"
+#include "DatabaseEnv.h"
+#include "ProtobufJSON.h"
+#include "Realm.h"
+#include "SessionManager.h"
+#include "SHA1.h"
+#include "SHA256.h"
+#include "SslContext.h"
+#include "Util.h"
+#include "httpget.h"
+#include "httppost.h"
+#include "soapH.h"
+
+int ns1__executeCommand(soap*, char*, char**) { return SOAP_OK; }
+
+int32 handle_get_plugin(soap* soapClient)
+{
+ return sLoginService.HandleGet(soapClient);
+}
+
+int32 handle_post_plugin(soap* soapClient)
+{
+ return sLoginService.HandlePost(soapClient);
+}
+
+bool LoginRESTService::Start(boost::asio::io_service& ioService)
+{
+ _bindIP = sConfigMgr->GetStringDefault("BindIP", "0.0.0.0");
+ _port = sConfigMgr->GetIntDefault("LoginREST.Port", 8081);
+ if (_port < 0 || _port > 0xFFFF)
+ {
+ TC_LOG_ERROR("server.rest", "Specified login service port (%d) out of allowed range (1-65535), defaulting to 8081", _port);
+ _port = 8081;
+ }
+
+ boost::system::error_code ec;
+ 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");
+ boost::asio::ip::tcp::resolver::query externalAddressQuery(boost::asio::ip::tcp::v4(), configuredAddress, std::to_string(_port));
+ boost::asio::ip::tcp::resolver::iterator endPoint = resolver.resolve(externalAddressQuery, ec);
+ if (endPoint == end || ec)
+ {
+ TC_LOG_ERROR("server.rest", "Could not resolve LoginREST.ExternalAddress %s", configuredAddress.c_str());
+ return false;
+ }
+
+ _externalAddress = endPoint->endpoint();
+
+ configuredAddress = sConfigMgr->GetStringDefault("LoginREST.LocalAddress", "127.0.0.1");
+ boost::asio::ip::tcp::resolver::query localAddressQuery(boost::asio::ip::tcp::v4(), configuredAddress, std::to_string(_port));
+ endPoint = resolver.resolve(localAddressQuery, ec);
+ if (endPoint == end || ec)
+ {
+ TC_LOG_ERROR("server.rest", "Could not resolve LoginREST.ExternalAddress %s", configuredAddress.c_str());
+ return false;
+ }
+
+ _localAddress = endPoint->endpoint();
+
+ // set up form inputs
+ Battlenet::JSON::Login::FormInput* input;
+ _formInputs.set_type(Battlenet::JSON::Login::LOGIN_FORM);
+ input = _formInputs.add_inputs();
+ input->set_input_id("account_name");
+ input->set_type("text");
+ input->set_label("E-mail");
+ input->set_max_length(320);
+
+ input = _formInputs.add_inputs();
+ input->set_input_id("password");
+ input->set_type("password");
+ input->set_label("Password");
+ input->set_max_length(16);
+
+ input = _formInputs.add_inputs();
+ input->set_input_id("log_in_submit");
+ input->set_type("submit");
+ input->set_label("Log In");
+
+ _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));
+
+ _thread = std::thread(std::bind(&LoginRESTService::Run, this));
+ return true;
+}
+
+void LoginRESTService::Stop()
+{
+ _stopped = true;
+ _loginTicketCleanupTimer->cancel();
+ _thread.join();
+}
+
+boost::asio::ip::tcp::endpoint const& LoginRESTService::GetAddressForClient(boost::asio::ip::address const& address) const
+{
+ if (address.is_loopback())
+ return _localAddress;
+
+ if (boost::asio::ip::address_v4::netmask(_localAddress.address().to_v4()).to_ulong() & address.to_v4().to_ulong())
+ return _localAddress;
+
+ return _externalAddress;
+}
+
+void LoginRESTService::Run()
+{
+ 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 %s:%d", _bindIP.c_str(), _port);
+ return;
+ }
+
+ TC_LOG_INFO("server.rest", "Login service bound to http://%s:%d", _bindIP.c_str(), _port);
+
+ http_post_handlers handlers[] =
+ {
+ { "application/json;charset=utf-8", handle_post_plugin },
+ { "application/json", handle_post_plugin },
+ { NULL }
+ };
+
+ soap_register_plugin_arg(&soapServer, &http_get, handle_get_plugin);
+ soap_register_plugin_arg(&soapServer, &http_post, handlers);
+ soap_register_plugin_arg(&soapServer, &ContentTypePlugin::Init, "application/json;charset=utf-8");
+
+ // Use our already ready ssl context
+ soapServer.ctx = Battlenet::SslContext::instance().native_handle();
+ soapServer.ssl_flags = SOAP_SSL_RSA;
+
+ while (!_stopped)
+ {
+ if (!soap_valid_socket(soap_accept(&soapServer)))
+ continue; // ran into an accept timeout
+
+ std::unique_ptr<soap> soapClient = Trinity::make_unique<soap>(soapServer);
+ boost::asio::ip::address_v4 address(soapClient->ip);
+ if (soap_ssl_accept(soapClient.get()) != SOAP_OK)
+ {
+ TC_LOG_DEBUG("server.rest", "Failed SSL handshake from IP=%s", address.to_string().c_str());
+ continue;
+ }
+
+ TC_LOG_DEBUG("server.rest", "Accepted connection from IP=%s", address.to_string().c_str());
+
+ std::thread([soapClient{ std::move(soapClient) }]
+ {
+ soap_serve(soapClient.get());
+ }).detach();
+ }
+
+ // 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::HandleGet(soap* soapClient)
+{
+ boost::asio::ip::address_v4 address(soapClient->ip);
+ std::string ip_address = address.to_string();
+
+ TC_LOG_DEBUG("server.rest", "[%s:%d] Handling GET request path=\"%s\"", ip_address.c_str(), soapClient->port, soapClient->path);
+
+ static std::string const expectedPath = "/bnetserver/login/";
+ if (strstr(soapClient->path, expectedPath.c_str()) != &soapClient->path[0])
+ return 404;
+
+ return SendResponse(soapClient, _formInputs);
+}
+
+int32 LoginRESTService::HandlePost(soap* soapClient)
+{
+ boost::asio::ip::address_v4 address(soapClient->ip);
+ std::string ip_address = address.to_string();
+
+ TC_LOG_DEBUG("server.rest", "[%s:%d] Handling POST request path=\"%s\"", ip_address.c_str(), soapClient->port, soapClient->path);
+
+ static std::string const expectedPath = "/bnetserver/login/";
+ if (strstr(soapClient->path, expectedPath.c_str()) != &soapClient->path[0])
+ return 404;
+
+ char *buf;
+ size_t len;
+ 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)
+ return 500;
+
+ ResponseCodePlugin* responseCode = reinterpret_cast<ResponseCodePlugin*>(soap_lookup_plugin(soapClient, ResponseCodePlugin::PluginId));
+ ASSERT(responseCode);
+
+ responseCode->ErrorCode = 400;
+
+ 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.");
+ return SendResponse(soapClient, loginResult);
+ }
+
+ std::string login;
+ std::string password;
+
+ for (int32 i = 0; i < loginForm.inputs_size(); ++i)
+ {
+ if (loginForm.inputs(i).input_id() == "account_name")
+ login = loginForm.inputs(i).value();
+ else if (loginForm.inputs(i).input_id() == "password")
+ password = loginForm.inputs(i).value();
+ }
+
+ Utf8ToUpperOnlyLatin(login);
+ Utf8ToUpperOnlyLatin(password);
+
+ PreparedStatement* stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_ACCOUNT_INFO);
+ stmt->setString(0, login);
+ stmt->setString(1, CalculateShaPassHash(login, std::move(password)));
+ if (PreparedQueryResult result = LoginDatabase.Query(stmt))
+ {
+ std::unique_ptr<Battlenet::Session::AccountInfo> accountInfo = Trinity::make_unique<Battlenet::Session::AccountInfo>();
+ accountInfo->LoadResult(result);
+
+ stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_CHARACTER_COUNTS_BY_BNET_ID);
+ stmt->setUInt32(0, accountInfo->Id);
+ if (PreparedQueryResult characterCountsResult = LoginDatabase.Query(stmt))
+ {
+ 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());
+ }
+
+ stmt = LoginDatabase.GetPreparedStatement(LOGIN_SEL_BNET_LAST_PLAYER_CHARACTERS);
+ stmt->setUInt32(0, accountInfo->Id);
+ if (PreparedQueryResult lastPlayerCharactersResult = LoginDatabase.Query(stmt))
+ {
+ 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()]
+ .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));
+ }
+
+ loginResult.set_authentication_state(Battlenet::JSON::Login::DONE);
+ return SendResponse(soapClient, loginResult);
+}
+
+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);
+}
+
+std::string LoginRESTService::CalculateShaPassHash(std::string const& name, std::string const& password)
+{
+ SHA256Hash email;
+ email.UpdateData(name);
+ email.Finalize();
+
+ SHA256Hash sha;
+ sha.UpdateData(ByteArrayToHexStr(email.GetDigest(), email.GetLength()));
+ sha.UpdateData(":");
+ sha.UpdateData(password);
+ sha.Finalize();
+
+ return ByteArrayToHexStr(sha.GetDigest(), sha.GetLength(), true);
+}
+
+std::unique_ptr<Battlenet::Session::AccountInfo> LoginRESTService::VerifyLoginTicket(std::string const& id)
+{
+ std::unique_lock<std::mutex> lock(_loginTicketMutex);
+
+ auto itr = _validLoginTickets.find(id);
+ if (itr != _validLoginTickets.end())
+ {
+ if (itr->second.ExpiryTime > time(nullptr))
+ {
+ std::unique_ptr<Battlenet::Session::AccountInfo> accountInfo = std::move(itr->second.Account);
+ _validLoginTickets.erase(itr);
+ return accountInfo;
+ }
+ }
+
+ return std::unique_ptr<Battlenet::Session::AccountInfo>();
+}
+
+void LoginRESTService::AddLoginTicket(std::string const& id, std::unique_ptr<Battlenet::Session::AccountInfo> accountInfo)
+{
+ std::unique_lock<std::mutex> lock(_loginTicketMutex);
+
+ _validLoginTickets[id] = { id, std::move(accountInfo), time(nullptr) + 10 };
+}
+
+void LoginRESTService::CleanupLoginTickets(boost::system::error_code const& error)
+{
+ if (error)
+ return;
+
+ time_t now = time(nullptr);
+
+ {
+ std::unique_lock<std::mutex> lock(_loginTicketMutex);
+ for (auto itr = _validLoginTickets.begin(); itr != _validLoginTickets.end();)
+ {
+ if (itr->second.ExpiryTime < now)
+ itr = _validLoginTickets.erase(itr);
+ else
+ ++itr;
+ }
+ }
+
+ _loginTicketCleanupTimer->expires_from_now(boost::posix_time::seconds(10));
+ _loginTicketCleanupTimer->async_wait(std::bind(&LoginRESTService::CleanupLoginTickets, this, std::placeholders::_1));
+}
+
+Namespace namespaces[] =
+{
+ { NULL, NULL, NULL, NULL }
+};
+
+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->fdelete = &Destroy;
+ p->data = data;
+
+ s->fresponse = &ChangeResponse;
+ 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, size_t contentLength)
+{
+ ResponseCodePlugin* self = reinterpret_cast<ResponseCodePlugin*>(soap_lookup_plugin(s, PluginId));
+ return self->fresponse(s, self->ErrorCode && originalResponse == SOAP_FILE ? self->ErrorCode : originalResponse, contentLength);
+}
+
+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)
+{
+ ContentTypePlugin* data = reinterpret_cast<ContentTypePlugin*>(p->data);
+ s->fposthdr = data->fposthdr;
+ delete data;
+}
+
+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);
+}