// Ryzom - MMORPG Framework // Copyright (C) 2010 Winch Gate Property Limited // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 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 Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . #include "stdpch.h" #include "nel/net/module.h" #include "nel/net/module_builder_parts.h" #include "nel/net/login_cookie.h" #include "game_share/welcome_service_itf.h" #include "game_share/utils.h" #include "server_share/mysql_wrapper.h" #include "server_share/login_service_itf.h" #include "database_mapping.h" #include "nel_database_mapping.h" #include "entity_locator.h" using namespace std; using namespace NLMISC; using namespace NLNET; using namespace MSW; using namespace RSMGR; using namespace ENTITYLOC; namespace LS { class CLoginService : public CEmptyModuleCommBehav > >, public WS::CLoginServiceSkel, // public LS::CLoginServiceSkel, public CLoginServiceWebItf, public ICharacterEventCb { /// Mysql ring database connection MSW::CConnection _RingDb; /// Mysql nel database connection MSW::CConnection _NelDb; typedef std::set TLSCLients; /// Login service client (mostly frontend) TLSCLients _LSClients; typedef uint32 TUserId; typedef map TLoggedUsers; /// list of user that are only logged (not online) and checked for timeout /// If the timeout occur, the users are put back to offline (and their cookie /// erased) TLoggedUsers _LoggedUsers; enum { /// Default logged user timout (in second) DEFAULT_LOGGED_USER_TIMEOUT = 15*60, // 15 mn (as in FE timeout for awaited users) }; uint32 _LoggedUserTimeout; public: CLoginService() { _LoggedUserTimeout = DEFAULT_LOGGED_USER_TIMEOUT; CLoginServiceSkel::init(this); } static const std::string &getInitStringHelp() { static std::string help(CModuleBase::getInitStringHelp()+"db(host= [port=] user= password= base=) web(port=) "); return help; } bool initModule(const TParsedCommandLine &pcl) { // recall base class if (!CModuleBase::initModule(pcl)) return false; const TParsedCommandLine *initRingDb = pcl.getParam("ring_db"); if (initRingDb == NULL) { nlwarning("LS : missing ring database connection information"); return false; } const TParsedCommandLine *initNelDb = pcl.getParam("nel_db"); if (initNelDb == NULL) { nlwarning("LS : missing nel database connection information"); return false; } // connect to the databases if (!_RingDb.connect(*initRingDb)) { nlwarning("Failed to connect to database using %s", initRingDb->toString().c_str()); return false; } if (!_NelDb.connect(*initNelDb)) { nlwarning("Failed to connect to database using %s", initNelDb->toString().c_str()); return false; } const TParsedCommandLine *initWeb = pcl.getParam("web"); if (initWeb == NULL) { nlwarning("LS : missing web connection information"); return false; } const TParsedCommandLine *webPort = initWeb->getParam("port"); if (webPort == NULL) { nlwarning("LS : missing web.port connection information"); return false; } // open the web interface uint16 port = 0; NLMISC::fromString(webPort->ParamValue, port); openItf(port); return true; } void onModuleUpdate() { H_AUTO(CLoginService_onModuleUpdate); // check that we are registered in the entity locator if (getSpeaker() == NULL && IEntityLocator::getInstance() != NULL) registerListener(IEntityLocator::getInstance()); try { CLoginServiceWebItf::update(); } catch (...) { nlwarning( "Recovered from exception in CLoginServiceWebItf::update()" ); } // check for logged user to put back to offline uint32 now = NLMISC::CTime::getSecondsSince1970(); TLoggedUsers::iterator first(_LoggedUsers.begin()), last(_LoggedUsers.end()); for (; first != last; ++first) { if (first->second+_LoggedUserTimeout < now) { uint32 userId = first->first; nldebug("LS : update : user %u is inactive since %u second, setting it to offline", userId, _LoggedUserTimeout); // set this user offline ! CRingUserPtr ru = CRingUser::load(_RingDb, userId, __FILE__, __LINE__); BOMB_IF(ru == NULL, "LS : updateModule : failed to load ring user "<setCurrentStatus(TCurrentStatus::cs_offline); // clear the cookie ru->setCookie(""); // update the database ru->update(_RingDb); // clear the logged user entry _LoggedUsers.erase(first); // stop the check for this update break; } } } // bool onProcessModuleMessage(IModuleProxy *senderModuleProxy, const CMessage &message) // { // if (CLoginServiceSkel::onDispatchMessage(senderModuleProxy, message)) // return true; // // // manual dispatching // // return false; // } virtual void onModuleUp(IModuleProxy *proxy) { if (proxy->getModuleClassName() == "WelcomeService") { nlinfo("LS : adding WS '%s'", proxy->getModuleName().c_str()); // this is one of our clients, store it _LSClients.insert(proxy); } } virtual void onModuleDown(IModuleProxy *proxy) { TLSCLients::iterator it(_LSClients.find(proxy)); if (it != _LSClients.end()) { nlinfo("LS : removing WS '%s'", proxy->getModuleName().c_str()); // we just lost a client _LSClients.erase(it); } } ////////////////////////////////////////////////// ///// login service from WS module interface callbacks ////////////////////////////////////////////////// virtual void pendingUserLost(NLNET::IModuleProxy *sender, const NLNET::CLoginCookie &cookie) { nldebug("LS:pendingUserLost : WS '%s' report that user %u with cookie %s did not connect in the allowed time", sender->getModuleName().c_str(), cookie.getUserId(), cookie.toString().c_str()); uint32 userId = cookie.getUserId(); CRingUserPtr ru = CRingUser::load(_RingDb, userId, __FILE__, __LINE__); BOMB_IF(ru == NULL, "LS:pendingUserLost : failed to load user "<getCurrentStatus() != TCurrentStatus::cs_logged, "LS:pendingUserLost : the user "<getCurrentStatus().toString(), return); if (ru->getCookie() != cookie.setToString()) { // ignore this message because the user have relogged and obtained another cookie nldebug("LS:pendingUserLost : message ignored because user has obtained another cookie"); return; } ru->setCurrentStatus(TCurrentStatus::cs_offline); ru->setCookie(""); ru->update(_RingDb); // remove it from the list of logged user _LoggedUsers.erase(userId); } ////////////////////////////////////////////////// ///// entity locator callbacks ////////////////////////////////////////////////// virtual void onUserConnection(NLNET::IModuleProxy *locatorHost, uint32 userId) { nldebug("LS: entity locator report user %u connection", userId); // set the user in 'online state' and remove it from the 'user to check for time out' list CRingUserPtr ru = CRingUser::load(_RingDb, userId, __FILE__, __LINE__); BOMB_IF(ru == NULL, "LS : onUserConnection : failed to load ring user "<setCurrentStatus(TCurrentStatus::cs_online); ru->update(_RingDb); // remove it of the logged checked user list _LoggedUsers.erase(userId); } virtual void onUserDisconnection(NLNET::IModuleProxy *locatorHost, uint32 userId) { nldebug("LS: entity locator report user %u disconnection", userId); // set the user in ':logged state' and put it in the 'user to check for time out' list CRingUserPtr ru = CRingUser::load(_RingDb, userId, __FILE__, __LINE__); BOMB_IF(ru == NULL, "LS : onUserConnection : failed to load ring user "<setCurrentStatus(TCurrentStatus::cs_logged); ru->update(_RingDb); // erase existing record if any _LoggedUsers.erase(userId); // insert the user in the logged user list _LoggedUsers.insert(make_pair(userId, NLMISC::CTime::getSecondsSince1970())); } virtual void onCharacterConnection(NLNET::IModuleProxy *locatorHost, uint32 charId, uint32 lastDisconnectionDate) { /* nothing */ } virtual void onCharacterDisconnection(NLNET::IModuleProxy *locatorHost, uint32 charId) { /* nothing */ } ////////////////////////////////////////////////// ///// Web interface callbacks ////////////////////////////////////////////////// /// Connection callback : a new interface client connect virtual void on_CLoginServiceWeb_Connection(NLNET::TSockId from) { } virtual void on_CLoginServiceWeb_Disconnection(NLNET::TSockId from) { // nothing to do right now } /// A user has passed the auth, web sets the user online and asks for a cookie virtual void on_login(NLNET::TSockId from, uint32 userId, const std::string &ipAddress, uint32 domainId) { nldebug("CLoginService : receive login request from %s with user %u, address %s", from->getTcpSock()->remoteAddr().asString().c_str(), userId, ipAddress.c_str()); //1 check is user already online, if so, disconnect it CRingUserPtr ru = CRingUser::load(_RingDb, userId, __FILE__, __LINE__); if (ru == NULL) { // invalid user ! nldebug("on_login : invalid ring user %u", userId); loginResult(from, userId, "", 1, "Invalid user"); return; } if (ru->getCurrentStatus() == TCurrentStatus::cs_online) { // this user seams online, we need to disconnect it from the shard // send a disconnect message to all LS client TLSCLients::iterator first(_LSClients.begin()), last(_LSClients.end()); for (; first != last; ++first) { WS::CWelcomeServiceProxy ws(*first); ws.disconnectUser(this, userId); } // check in the entity locator that the player is really online if (IEntityLocator::getInstance() && IEntityLocator::getInstance()->isUserOnline(userId)) { nldebug("LS : on_login : user %u already connected, disconnecting him and reject login", userId); loginResult(from, userId, "", 2, "User already online, please relog"); return; } // in fact, perhaps not really online, allow him to connect. // a worst, the user is online but we have asked to the WS to // disconnect him. // In fact, most of the time, this case is when the SU is stopped // with online user, so the database is not sync with the // real user status. } // Now prevent from logging-in at the same time with a free GM's player account and a GM CS account CNelUserPtr nelUser = CNelUser::load(_NelDb, userId, __FILE__, __LINE__); BOMB_IF(nelUser == NULL, "on_login : invalid nel user %u" << userId, loginResult(from, userId, "", 5, "Invalid user"); return); if (nelUser->getGMId() != 0) { uint32 otherUserId = nelUser->getGMId(); CRingUserPtr otherRu = CRingUser::load(_RingDb, otherUserId, __FILE__, __LINE__); if (otherRu == NULL) { nlwarning("on_login : Can't find ring user %u from account %u with GMId", otherUserId, userId); } //else if (otherRu->getCurrentStatus() != TCurrentStatus::cs_offline) // cs_logged and cs_online else if (otherRu->getCurrentStatus() == TCurrentStatus::cs_online) // less strict check, only avoid csr/player account logged on the same time { // DON'T check in the entity locator that the player is really online //if (IEntityLocator::getInstance() && IEntityLocator::getInstance()->isUserOnline(otherUserId)) if (IEntityLocator::getInstance() && IEntityLocator::getInstance()->isUserOnline(otherUserId)) { nldebug("LS : on_login : user %u already connected, rejecting login of %u with GMId", otherUserId, userId); loginResult(from, userId, "", 3, toString("User %u (%u's GMId) already online", otherUserId, userId)); return; } } } CSString query; query << "SELECT UId FROM user WHERE GMId = "< result = auto_ptr(_NelDb.storeResult()); for (uint32 i=0; i!=result->getNumRows(); ++i) { result->fetchRow(); uint32 otherUserId; result->getField(0, otherUserId); CRingUserPtr otherRu = CRingUser::load(_RingDb, otherUserId, __FILE__, __LINE__); if (otherRu == NULL) { nlwarning("on_login : Can't find ring user %u which GMID is account %u", otherUserId, userId); } //else if (otherRu->getCurrentStatus() != TCurrentStatus::cs_offline) // cs_logged and cs_online else if (otherRu->getCurrentStatus() == TCurrentStatus::cs_online) // less strict check, only avoid csr/player account logged on the same time { // DON'T check in the entity locator that the player is really online //if (IEntityLocator::getInstance() && IEntityLocator::getInstance()->isUserOnline(otherUserId)) if (IEntityLocator::getInstance() && IEntityLocator::getInstance()->isUserOnline(otherUserId)) { nldebug("LS : on_login : user %u already connected, rejecting login of %u which is the GMId of it", otherUserId, userId); loginResult(from, userId, "", 4, toString("GM user %u (having GMId=%u) already online", otherUserId, userId)); return; } } } NLNET::CInetAddress addr(ipAddress); //2 generate a cookie and set the player status and cookie in database NLNET::CLoginCookie cookie(addr.internalIPAddress(), userId); ru->setCookie(cookie.setToString()); ru->setCurrentStatus(TCurrentStatus::cs_logged); ru->setCurrentActivity(TCurrentActivity::ca_none); ru->setCurrentDomainId(domainId); // save the user record ru->update(_RingDb); // erase existing record if any _LoggedUsers.erase(userId); // insert the user in the user to wait table _LoggedUsers.insert(make_pair(userId, NLMISC::CTime::getSecondsSince1970())); //3 call the return method to the web loginResult(from, userId, ru->getCookie(), 0, ""); } virtual void on_logout(NLNET::TSockId from, uint32 userId) { nldebug("CLoginService : receive logout request from %s with user %u", from->getTcpSock()->remoteAddr().asString().c_str(), userId); // load the ring user CRingUserPtr ru = CRingUser::load(_RingDb, userId, __FILE__, __LINE__); if (ru == NULL) { // invalid user ! nldebug("on_logout : invalid user %u", userId); logoutResult(from, 1, "unkown user"); return; } if (ru->getCurrentStatus() == TCurrentStatus::cs_offline) { // the user is offline, could not logout logoutResult(from, 2, "user already offline"); return; } if (ru->getCurrentStatus() == TCurrentStatus::cs_online) { // the user is online (in game), ignore the disconnect but return ok logoutResult(from, 0, ""); return; } // ok, the user is logged, we can put it offline ru->setCurrentStatus( TCurrentStatus::cs_offline); ru->setCookie(""); ru->setCurrentDomainId(-1); ru->update(_RingDb); logoutResult(from, 0, ""); } NLMISC_COMMAND_HANDLER_TABLE_EXTEND_BEGIN(CLoginService, CModuleBase) NLMISC_COMMAND_HANDLER_ADD(CLoginService, openWebInterface, "Open the web interface", ""); // NLMISC_COMMAND_HANDLER_ADD(CLoginService, closeWebInterface, "Close the web interface", "no param"); NLMISC_COMMAND_HANDLER_ADD(CLoginService, LoggedUserTimeout, "get or set the logged user timeout in second", "[]"); NLMISC_COMMAND_HANDLER_TABLE_END NLMISC_CLASS_COMMAND_DECL(LoggedUserTimeout) { if (args.size() > 1) return false; if (!args.empty()) { NLMISC::fromString(args[0], _LoggedUserTimeout); } // output the value log.displayNL("LoggedUserTimeout = %u", _LoggedUserTimeout); return true; } // NLMISC_CLASS_COMMAND_DECL(closeWebInterface) // { // if (args.size() != 0) // return false; // // // } NLMISC_CLASS_COMMAND_DECL(openWebInterface) { if (args.size() != 1) return false; uint16 port; NLMISC::fromString(args[0], port); log.displayNL("Opening web interface on port %u", port); openItf(port); return true; } }; NLNET_REGISTER_MODULE_FACTORY(CLoginService, "LoginService"); } // namespace LS