// NeL - 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 "stdnet.h" #include "nel/net/callback_client.h" #include "nel/net/service.h" #include "nel/net/login_cookie.h" #include "nel/net/login_server.h" #include "nel/net/udp_sock.h" using namespace std; using namespace NLMISC; namespace NLNET { struct CPendingUser { CPendingUser (const CLoginCookie &cookie, const string &un, const string &up, const string &ux, uint32 instanceId, uint32 charSlot) : Cookie (cookie), UserName(un), UserPriv(up), UserExtended(ux), InstanceId(instanceId), CharSlot(charSlot) { Time = CTime::getSecondsSince1970(); } CLoginCookie Cookie; string UserName; string UserPriv; // privilege for executing commands from the clients string UserExtended; // extended data (for free use) uint32 InstanceId; // the world instance in witch the user is awaited uint32 CharSlot; // the expected character slot, any other will be denied uint32 Time; // when the cookie is inserted in pending list }; static list PendingUsers; static CCallbackServer *Server = NULL; static string ListenAddr; static bool AcceptInvalidCookie = false; static string DefaultUserPriv = ""; static TDisconnectClientCallback DisconnectClientCallback = NULL; // true=tcp false=udp static bool ModeTcp = 0; // default value is 15 minutes static uint TimeBeforeEraseCookie = 15*60; /// contains the correspondance between userid and the sockid map UserIdSockAssociations; TNewClientCallback NewClientCallback = NULL; TNewCookieCallback NewCookieCallback = NULL; ////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// ///////////// CONNECTION TO THE WELCOME SERVICE ////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// void notifyWSRemovedPendingCookie(CLoginCookie& cookie) { CMessage msgout ("RPC"); // remove pending cookie msgout.serial(cookie); CUnifiedNetwork::getInstance()->send ("WS", msgout); } void CLoginServer::refreshPendingList () { // delete too old cookie list::iterator it = PendingUsers.begin(); uint32 Time = CTime::getSecondsSince1970(); while (it != PendingUsers.end ()) { if ((*it).Time < Time - TimeBeforeEraseCookie) { nlinfo("LS: Removing cookie '%s' because too old", (*it).Cookie.toString().c_str()); notifyWSRemovedPendingCookie((*it).Cookie); it = PendingUsers.erase (it); } else { it++; } } } void cbWSChooseShard (CMessage &msgin, const std::string &/* serviceName */, TServiceId /* sid */) { // the WS call me that a new client want to come in my shard string reason, userName, userPriv, userExtended; uint32 instanceId, charSlot; CLoginCookie cookie; // refreshPendingList (); // // S08: receive "CS" message from WS and send "SCS" message to WS // msgin.serial (cookie); msgin.serial (userName, userPriv, userExtended); msgin.serial (instanceId); msgin.serial (charSlot); vector::iterator> pendingToRemove; list::iterator it; for (it = PendingUsers.begin(); it != PendingUsers.end (); it++) { const CPendingUser &pu = *it; if (pu.Cookie == cookie) { // the cookie already exists, erase it and return false nlwarning ("LS: Cookie %s is already in the pending user list", cookie.toString().c_str()); // notifyWSRemovedPendingCookie((*it).Cookie); PendingUsers.erase (it); reason = "cookie already exists"; // iterator is invalid, set it to end it = PendingUsers.end(); break; } else if (pu.Cookie.getUserId() == cookie.getUserId()) { // we already have a cookie for this user, remove the old one pendingToRemove.push_back(it); } } // remove any useless cookie while (!pendingToRemove.empty()) { PendingUsers.erase(pendingToRemove.back()); pendingToRemove.pop_back(); } if (it == PendingUsers.end ()) { // add it to the awaiting client nlinfo ("LS: New cookie %s (name '%s' priv '%s' extended '%s' instance %u slot %u) inserted in the pending user list (awaiting new client)", cookie.toString().c_str(), userName.c_str(), userPriv.c_str(), userExtended.c_str(), instanceId, charSlot); PendingUsers.push_back (CPendingUser (cookie, userName, userPriv, userExtended, instanceId, charSlot)); reason = ""; // callback if needed if (NewCookieCallback != NULL) { NewCookieCallback(cookie); } } CMessage msgout ("SCS"); msgout.serial (reason); msgout.serial (cookie); msgout.serial (ListenAddr); uint32 nbPending = (uint32)PendingUsers.size(); msgout.serial (nbPending); CUnifiedNetwork::getInstance()->send ("WS", msgout); } void cbWSDisconnectClient (CMessage &msgin, const std::string &serviceName, TServiceId /* sid */) { // the WS tells me that i have to disconnect a client uint32 userid; msgin.serial (userid); if (ModeTcp) { map::iterator it = UserIdSockAssociations.find (userid); if (it == UserIdSockAssociations.end ()) { nlwarning ("LS: Can't disconnect the user %d, he is not found", userid); } else { nlinfo ("LS: Disconnect the user %d", userid); Server->disconnect ((*it).second); } } if (DisconnectClientCallback != NULL) { DisconnectClientCallback (userid, serviceName); } } static TUnifiedCallbackItem WSCallbackArray[] = { { "CS", cbWSChooseShard }, { "DC", cbWSDisconnectClient }, }; ////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// ///////////// CONNECTION TO THE CLIENT /////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// void cbShardValidation (CMessage &msgin, TSockId from, CCallbackNetBase &netbase) { // // S13: receive "SV" message from the client // // the client send me a cookie CLoginCookie cookie; string reason; msgin.serial (cookie); string userName, userPriv, userExtended; uint32 instanceId, charSlot; // verify that the user was pending reason = CLoginServer::isValidCookie (cookie, userName, userPriv, userExtended, instanceId, charSlot); // if the cookie is not valid and we accept them, clear the error if(AcceptInvalidCookie && !reason.empty()) { reason = ""; cookie.set (rand(), rand(), rand()); } CMessage msgout2 ("SV"); msgout2.serial (reason); netbase.send (msgout2, from); if (!reason.empty()) { nlwarning ("LS: User (%s) is not in the pending user list (cookie:%s)", netbase.hostAddress(from).asString().c_str(), cookie.toString().c_str()); // disconnect him netbase.disconnect (from); } else { // add the user association uint32 userid = cookie.getUserId(); if (ModeTcp) UserIdSockAssociations.insert (make_pair(userid, from)); // identification OK, let's call the user callback if (NewClientCallback != NULL) NewClientCallback (from, cookie); // ok, now, he can call all callback Server->authorizeOnly (NULL, from); } } void ClientConnection (TSockId from, void * /* arg */) { nldebug("LS: new client connection: %s", from->asString ().c_str ()); // the client could only call "SV" message Server->authorizeOnly ("SV", from); } static const TCallbackItem ClientCallbackArray[] = { { "SV", cbShardValidation }, }; void CLoginServer::setListenAddress(const string &la) { // if the var is empty or not found, take it from the listenAddress() if (la.empty() && ModeTcp && Server != NULL) { ListenAddr = Server->listenAddress ().asIPString(); } else { ListenAddr = la; } // check that listen address is valid if (ListenAddr.empty()) { nlerror("FATAL : listen address in invalid, it should be either set via ListenAddress variable or with -D argument"); nlstop; } nlinfo("LS: Listen Address that will be sent to the client is now '%s'", ListenAddr.c_str()); } uint32 CLoginServer::getNbPendingUsers() { return (uint32)PendingUsers.size(); } void cfcbListenAddress (CConfigFile::CVar &var) { CLoginServer::setListenAddress (var.asString()); } void cfcbDefaultUserPriv(CConfigFile::CVar &var) { // set the new ListenAddr DefaultUserPriv = var.asString(); nlinfo("LS: The default user priv is '%s'", DefaultUserPriv.c_str()); } void cfcbAcceptInvalidCookie(CConfigFile::CVar &var) { // set the new ListenAddr AcceptInvalidCookie = var.asInt() == 1; nlinfo("LS: This service %saccept invalid cookie", AcceptInvalidCookie?"":"doesn't "); } void cfcbTimeBeforeEraseCookie(CConfigFile::CVar &var) { // set the new ListenAddr TimeBeforeEraseCookie = var.asInt(); nlinfo("LS: This service will remove cookie after %d seconds", TimeBeforeEraseCookie); } ////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// ///////////// CONNECTION TO THE WELCOME SERVICE ////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////////////////////// // common init void CLoginServer::init (const string &listenAddress) { // connect to the welcome service connectToWS (); try { cfcbDefaultUserPriv(IService::getInstance()->ConfigFile.getVar("DefaultUserPriv")); IService::getInstance()->ConfigFile.setCallback("DefaultUserPriv", cfcbDefaultUserPriv); } catch(Exception &) { } try { cfcbAcceptInvalidCookie (IService::getInstance()->ConfigFile.getVar("AcceptInvalidCookie")); IService::getInstance()->ConfigFile.setCallback("AcceptInvalidCookie", cfcbAcceptInvalidCookie); } catch(Exception &) { } try { cfcbTimeBeforeEraseCookie (IService::getInstance()->ConfigFile.getVar("TimeBeforeEraseCookie")); IService::getInstance()->ConfigFile.setCallback("TimeBeforeEraseCookie", cfcbTimeBeforeEraseCookie); } catch(Exception &) { } // setup the listen address string la; if (IService::getInstance()->haveArg('D')) { // use the command line param if set la = IService::getInstance()->getArg('D'); } else if (IService::getInstance()->ConfigFile.exists ("ListenAddress")) { // use the config file param if set la = IService::getInstance()->ConfigFile.getVar ("ListenAddress").asString(); } else { la = listenAddress; } setListenAddress (la); IService::getInstance()->ConfigFile.setCallback("ListenAddress", cfcbListenAddress); } // listen socket is TCP void CLoginServer::init (CCallbackServer &server, TNewClientCallback ncl) { init (server.listenAddress ().asIPString()); // add callback to the server server.addCallbackArray (ClientCallbackArray, sizeof (ClientCallbackArray) / sizeof (ClientCallbackArray[0])); server.setConnectionCallback (ClientConnection, NULL); NewClientCallback = ncl; Server = &server; ModeTcp = true; } // listen socket is UDP void CLoginServer::init (CUdpSock &server, TDisconnectClientCallback dc) { init (server.localAddr ().asIPString()); DisconnectClientCallback = dc; ModeTcp = false; } void CLoginServer::init (const std::string &listenAddr, TDisconnectClientCallback dc) { init (listenAddr); DisconnectClientCallback = dc; ModeTcp = false; } void CLoginServer::addNewCookieCallback(TNewCookieCallback newCookieCb) { NewCookieCallback = newCookieCb; } string CLoginServer::isValidCookie (const CLoginCookie &lc, string &userName, string &userPriv, string &userExtended, uint32 &instanceId, uint32 &charSlot) { userName = userPriv = ""; if (!AcceptInvalidCookie && !lc.isValid()) return "The cookie is invalid"; // verify that the user was pending list::iterator it; for (it = PendingUsers.begin(); it != PendingUsers.end (); it++) { CPendingUser &pu = *it; if (pu.Cookie == lc) { nlinfo ("LS: Cookie '%s' is valid and pending (user %s), send the client connection to the WS", lc.toString ().c_str (), pu.UserName.c_str()); // warn the WS that the client effectively connected uint8 con = 1; CMessage msgout ("CC"); uint32 userid = lc.getUserId(); msgout.serial (userid); msgout.serial (con); CUnifiedNetwork::getInstance()->send("WS", msgout); userName = pu.UserName; userPriv = pu.UserPriv; userExtended = pu.UserExtended; instanceId = pu.InstanceId; charSlot = pu.CharSlot; // ok, it was validate, remove it PendingUsers.erase (it); return ""; } } // we accept invalid cookie and it is one, fake if (AcceptInvalidCookie) { userName = "InvalidUserName"; userPriv = DefaultUserPriv; instanceId = 0xffffffff; return ""; } // problem return "I didn't receive the cookie from WS"; } void CLoginServer::connectToWS () { CUnifiedNetwork::getInstance()->addCallbackArray(WSCallbackArray, sizeof(WSCallbackArray)/sizeof(WSCallbackArray[0])); } void CLoginServer::clientDisconnected (uint32 userId) { uint8 con = 0; CMessage msgout ("CC"); msgout.serial (userId); msgout.serial (con); CUnifiedNetwork::getInstance()->send("WS", msgout); // remove the user association if (ModeTcp) UserIdSockAssociations.erase (userId); } /// Call this method to retrieve the listen address const std::string &CLoginServer::getListenAddress() { return ListenAddr; } bool CLoginServer::acceptsInvalidCookie() { return AcceptInvalidCookie; } // // Commands // NLMISC_CATEGORISED_COMMAND(nel, lsUsers, "displays the list of all connected users", "") { nlunreferenced(rawCommandString); nlunreferenced(quiet); nlunreferenced(human); if(args.size() != 0) return false; if (ModeTcp) { log.displayNL ("Display the %d connected users :", UserIdSockAssociations.size()); for (map::iterator it = UserIdSockAssociations.begin(); it != UserIdSockAssociations.end (); it++) { log.displayNL ("> %u %s", (*it).first, (*it).second->asString().c_str()); } log.displayNL ("End of the list"); } else { log.displayNL ("No user list in udp mode"); } return true; } NLMISC_CATEGORISED_COMMAND(nel, lsPending, "displays the list of all pending users", "") { nlunreferenced(rawCommandString); nlunreferenced(quiet); nlunreferenced(human); if(args.size() != 0) return false; log.displayNL ("Display the %d pending users :", PendingUsers.size()); for (list::iterator it = PendingUsers.begin(); it != PendingUsers.end (); it++) { log.displayNL ("> %s %s", (*it).Cookie.toString().c_str(), (*it).UserName.c_str()); } log.displayNL ("End of the list"); return true; } NLMISC_CATEGORISED_DYNVARIABLE(nel, string, LSListenAddress, "the listen address sended to the client to connect on this front_end") { nlunreferenced(human); if (get) { *pointer = ListenAddr; } else { if ((*pointer).find (":") == string::npos) { nlwarning ("LS: You must set the address + port (ie: \"itsalive.nevrax.org:38000\")"); return; } else if ((*pointer).empty()) { ListenAddr = Server->listenAddress ().asIPString(); } else { ListenAddr = *pointer; } nlinfo ("LS: Listen Address that will be send to client is '%s'", ListenAddr.c_str()); } } NLMISC_CATEGORISED_VARIABLE(nel, string, DefaultUserPriv, "Default User priv for people who don't use the login system"); } // NLNET