#include #include #include "../shared/MiscSupport.h" #include "../shared/ReplyToClient.h" #include "../shared/XmlSupport.h" #include "DatabaseThreadShared.h" #include "../shared/LogFile.h" #include "../shared/SelectableRequestQueue.h" #include "../shared/CommandDispatcher.h" #include "FormatTime.h" #include "../shared/MultiCast.h" #include "UserInfo.h" /* Known issue: We send the current time to help with the age column. If we * didn't do that, and the client's computer was even a few seconds off, that * column would not look right. * * The problem is that we report the time as part of the user info message. * so we would not report the time for a DEMO user. So things would probably * look a little off for him. * * Now that we are adding the ability to see everything in Eastern time, even * if the rest of the computer is in a different time zone, the problem gets * worse. The age column will pretty much be way off if you log in as demo and * force everything to Eastern time. */ ///////////////////////////////////////////////////////////////////// // Standard Messages ///////////////////////////////////////////////////////////////////// // Something is wrong and retries won't fix it. Don't talk to me again // until you've changed something. For example, a bad password. static void disconnectForGood(XmlNode &message) { message["STATUS"].properties["DISCONNECT_FOR_GOOD"] = "1"; } // There are a limited number of good responses to this. The client // converts the string back to an enum. static void anotherUserAbort(XmlNode &message) { message["ACCOUNT_STATUS"].properties["STATE"] = "another user"; disconnectForGood(message); } static void badUsernameAbort(XmlNode &message) { message["ACCOUNT_STATUS"].properties["STATE"] = "bad username"; disconnectForGood(message); } static void badPasswordAbort(XmlNode &message) { message["ACCOUNT_STATUS"].properties["STATE"] = "bad password"; disconnectForGood(message); } static void requirePaymentAbort(XmlNode &message) { message["ACCOUNT_STATUS"].properties["STATE"] = "require payment"; disconnectForGood(message); } static void statusGood(XmlNode &message, std::string nextPayment = "", long oddsMaker = 0) { message["ACCOUNT_STATUS"].properties["STATE"] = "good"; if (!nextPayment.empty()) { message["ACCOUNT_STATUS"].properties["NEXT_PAYMENT"] = nextPayment; } if (oddsMaker > 0) { message["ACCOUNT_STATUS"].properties["ODDSMAKER"] = ltoa(oddsMaker); } } ///////////////////////////////////////////////////////////////////// // This is not thread safe so we each have our own copy. ///////////////////////////////////////////////////////////////////// static LastIdCacheInfo lastIdCache; ///////////////////////////////////////////////////////////////////// // Random numbers // // There are lots of options. We have to initialize the state or it // is very obvious and things repeat a lot each time we start. We // define our own state buffer because otherwise this is not thread // safe. ///////////////////////////////////////////////////////////////////// static struct drand48_data randomState; static int myRandom() { long int result; lrand48_r(&randomState, &result); return result; } static void initMyRandom() { srand48_r(TimeVal(true).asMicroseconds(), &randomState); } ///////////////////////////////////////////////////////////////////// // Classes // // These reference each other, so we declare them both here. ///////////////////////////////////////////////////////////////////// class UserInfo; class UserInfoManager : private ThreadClass, NoAssign, NoCopy, ThreadMonitor::Extra { private: enum { mtLogin, /* Client is sending his credentials (i.e. password, previous * server tokens, etc.), and a request to restart fresh or to * restart from a specific point */ mtConfirmLogin, /* On a normal login the server will include a session id, * etc, which the user can use to reconnect. Until the * user confirms this message, all data is suspened. This * is the client's confirmation to the server. */ mtSpecialLogin, /* This is a different type of login. For one thing, the * password is verified by a challenge response system. * For anther thing, the client sends us a different * username which we have to translate into a Trade-Ideas * username. */ mtSpecialVerify, /* This is where the client is answering the login * challenge. There is a lot of overlap between this * and mtLogin. The final step is to call mtConfirmLogin. */ mtProxyLogin, /* The bulk of the work is being done by the proxy. The * proxy gives us a small amount of information that we store * in case anyone asks. The proxy will cut us off if the * the user is no longer allowed to log in. */ mtExecutionMessage, /* We record these in the database more or less as is. */ mtSetTimeFormat, /* How do we export the time? time_t or Eastern? */ mtQuit }; // Some information is available on a read-only basis to the world, but may // also be modified by this thread. Grab the mutex to read from another // thread. Grab the mutex and be in this thread to modify the data. There // is no need or desire to hold the mutex if you are reading from this // thread. It is possible that some of the operations in this thread will // take longer than we'd like, so we try to lock things as little as possible // in this thread. pthread_mutex_t _mutex; // The functions in this class which modify the list of users automatically // call the lock and unlock fuctions. An individual user object doesn't have // to worry about these. If there was some complicated state, it would, // but most of these are set in the initial login process. confirmLogin() // makes very simple changes which should be thread safe without a lock. void lock(); void unlock(); // These are protected by the mutex std::map< SocketInfo *, UserInfo * > _userInfoBySocket; std::map< SocketInfo *, UserInfoExport > _simpleUsers; std::map< UserId, SocketInfo * > _byUserId; // Only proxy users, not demo. std::set< SocketInfo * > _useEasternTime; // This is not protected by the mutex. It's not used by the other thread. // And the cleanup can be confusing, so we can't easily lock this. We might // be in the process of deleting from the other set, so we might already be // locked. std::map< std::string, UserInfo * > _userInfoByName; void storeUserInfoByName(UserInfo *userInfo); UserInfo *attemptToCreate(std::string username, std::string password, std::string vendorId, std::string uniqueId, AlertId restartAfter, AlertId restartKey, int sequenceNumber, SocketInfo *socket, ExternalRequest::MessageId messageId); void confirmLogin(ExternalRequest *request); void login(ExternalRequest *request); void recordDemoUser(std::string const &password, std::string const &vendorId); void specialLogin(ExternalRequest *request); void specialLoginVerify(ExternalRequest *request); void recordExecutionMessage(ExternalRequest *request); // From ThreadMonitor::Extra virtual std::string getInfoForThreadMonitor(); SelectableRequestQueue _incomingRequests; DatabaseWithRetry _database; // This lists out items that need to be rechecked soon. A new request // is always checked out immediately, so that other threads can immediately // access the data. After that, the item is put into this list. // We store a pointer to the socketInfo for simplicity. That way if an // item is deleted, we don't have to know about it. At the time that we // plan to use an object, we look up the socketInfo to get a userInfo // object, or nothing if that object has been deleted. struct RefreshKey : public std::pair< TimeVal, SocketInfo * > { TimeVal const &getTimeVal() const { return first; } SocketInfo *getSocketInfo() const { return second; } RefreshKey() {} RefreshKey(TimeVal const &timeVal, SocketInfo *socketInfo) : std::pair< TimeVal, SocketInfo * >(timeVal, socketInfo) { } }; std::set< RefreshKey > _refreshSchedule; // The first one looks at a single socket. It checks the credentials, if // they are out of date. On success, the next credential check is // schedules. On failure the UserInfo object is deleted. The second one // checks the credentials of any and all UserInfo objects which are overdue // for a check. void verifyStatus(SocketInfo *socket); void verifyStatusAll(); // This returns the time required until the next item in the _refreshSchedule // is ready to go. This is in the format required by a select statement. // It returns null if there is nothing in the queue, so the select will not // have a timeout value. Otherwise it returns a pointer to a static variable // of the right type and value. timeval *untilNextCheck(); static UserInfoManager *instance; UserInfoManager(); ~UserInfoManager(); protected: void threadFunction(); public: static void initInstance(); static UserInfoManager &getInstance(); // Remove deletes a user associated with a socket. This includes all // necessary cleanup, assuming that the user has been thrown into // _userInfoBySocket. This includes deleting the UserInfo object, if it // exists. RemoveReference removes the association between a name and a // UserInfo object. This is called by ~UserInfo, and is indirectly called // by remove(). void remove(SocketInfo *socket); void removeReference(UserInfo *userInfo); void scheduleCredentialCheck(UserInfo *userInfo); void deleteCredentialCheck(UserInfo *userInfo); DatabaseWithRetry &getDatabase() { return _database; } // These are thread safe. UserInfoExport getInfo(SocketInfo *socket); AlertId getLastId(SocketInfo *socket); bool useEasternTime(SocketInfo *socket); // Do not lock. bool useEasternTimeInternal(SocketInfo *socket); }; class UserInfo : public FixedMalloc, NoAssign, NoCopy { private: const std::string _username; const std::string _password; const std::string _vendorId; const bool _allowMultipleLogins; SocketInfo * const _socket; TimeVal _dataValidUntil; TimeVal _cookieValidUntil; TimeVal _nextCredentialCheck; std::string _lastCookie; UserId _userId; int _sequenceNumber; AlertId _lastAlertId; AlertId _restartKey; const ExternalRequest::MessageId _messageId; bool _idConfirmed; const std::string _uniqueId; bool verifyCredentialsNow(XmlNode &message); //void dump(XmlNode &message); bool updateCookie(std::string desiredCookie); public: std::string getUsername(); bool allowMultipleLogins() const { return _allowMultipleLogins; } SocketInfo *getSocket(); ExternalRequest::MessageId getMessageId() const { return _messageId; } std::string getVendorId() const { return _vendorId; } UserId getUserId() const { return _userId; } UserInfoExport getInfo(); AlertId getLastId(); bool verifyCredentials(XmlNode &message); void confirmLogin(ExternalRequest *request); bool setLastId(AlertId restartAfter, AlertId restartKey, int clientSequenceId, XmlNode &message); TimeVal getNextCredentialCheck(); static void dump(SocketInfo *socket, XmlNode &message); UserInfo(std::string username, std::string password, std::string vendorId, std::string uniqueId, SocketInfo *socket, ExternalRequest::MessageId messageId); ~UserInfo(); }; // Some users will log in through a different process. This involves more // steps. Instead of sending a password, there is a challenge / response // procedure. This is where we store information between the initial request // to log in and the response to the server's challenge. After that we // remove this and switch to the normal data structures. class ChallengeResponseInfo { private: const std::string _specialUsername; const std::string _specialLoginType; std::string _expectedResponse; bool valid() { return !_expectedResponse.empty(); } std::string generateNewPassword(); std::string generateNewChallenge(); static int _counter; public: ChallengeResponseInfo(std::string specialUsername, std::string specialLoginType, std::string clientChallenge, DatabaseWithRetry &database, SocketInfo *socket, ExternalRequest::MessageId messageId); bool verifyResponse(std::string response) { return (valid() && (response == _expectedResponse)); } void getTIInfo(std::string &username, std::string &password, std::string remoteAddress, DatabaseWithRetry &database); static void remove(SocketInfo *socket); }; static std::map< SocketInfo *, ChallengeResponseInfo * > challengesInProgress; ///////////////////////////////////////////////////////////////////// // UserInfoManager ///////////////////////////////////////////////////////////////////// bool UserInfoManager::useEasternTime(SocketInfo *socket) { lock(); const bool result = useEasternTimeInternal(socket); unlock(); return result; } bool UserInfoManager::useEasternTimeInternal(SocketInfo *socket) { return _useEasternTime.count(socket); } void UserInfoManager::scheduleCredentialCheck(UserInfo *userInfo) { TimeVal nextTime = userInfo->getNextCredentialCheck(); if (nextTime) { _refreshSchedule.insert(RefreshKey(nextTime, userInfo->getSocket())); } } void UserInfoManager::deleteCredentialCheck(UserInfo *userInfo) { _refreshSchedule.erase(RefreshKey(userInfo->getNextCredentialCheck(), userInfo->getSocket())); } void UserInfoManager::threadFunction() { ThreadMonitor::find().add(this); // An administrator can send a request to all servers at once. If the // request names a specific user and that user is logged into this server, // we respond. // // This only applies to people who connect via the micro_proxy. Logging // in directly is still supported, but it's clearly on its way out. No need // or intent to add this or any other new features to the old style of login. // // TODO What if someone logs in multiple times? Maybe an eSignal user who // is allowed to do that. // Maybe a race condition: What if I try to log in twice. I'm connected // to two separate micro_proxy servers, so the new client might be connected // for several seconds before the old client is disconnected. Both clients // could be connected to the same ax_alert_server (or other server) instance // at the same time with the same user id. MultiCast::getInstance().addCallback([&](PropertyList const &message) { const std::string originalUserId = MultiCast::getUserId(message); UserId userId = strtollDefault(originalUserId, 0); const bool loggedIn = userId && _byUserId.count(userId); if (loggedIn) MultiCast::respond(message); }, MultiCast::PING_USER); while(true) { _incomingRequests.resetWaitHandle(); verifyStatusAll(); while (Request *current = _incomingRequests.getRequest()) { switch (current->callbackId) { case mtLogin: { ExternalRequest *request = dynamic_cast(current); SocketInfo *socket = request->getSocketInfo(); // This is not required. The client should only try to log in // once. If it tries to log in and it's aready logged in, // the server will ignore the second request. //QueryInfo::releaseQueryInfo(socket); login(request); if (_userInfoBySocket.count(socket) == 0) { CommandDispatcher::getInstance()->unlock(socket); } // else wait for the confirm login command before we unlock the // connection. break; } case mtConfirmLogin: { ExternalRequest *externalRequest = dynamic_cast(current); SocketInfo *socket = externalRequest->getSocketInfo(); //LogFile::primary().quoteAndSend("mtConfirmLogin 1", socket); confirmLogin(externalRequest); //LogFile::primary().quoteAndSend("mtConfirmLogin 2", socket); ExternalRequest::MessageId messageId = externalRequest->getResponseMessageId(); if (messageId.present()) { addToOutputQueue(socket, XmlNode().asString("API"), messageId); } //LogFile::primary().quoteAndSend("mtConfirmLogin 3", socket); CommandDispatcher::getInstance()->unlock(socket); //LogFile::primary().quoteAndSend("mtConfirmLogin 4", socket); break; } case mtSpecialLogin: { ExternalRequest *request = dynamic_cast(current); SocketInfo *socket = request->getSocketInfo(); specialLogin(request); if (challengesInProgress.count(socket) == 0) { CommandDispatcher::getInstance()->unlock(socket); } break; } case mtSpecialVerify: { ExternalRequest *request = dynamic_cast(current); SocketInfo *socket = request->getSocketInfo(); specialLoginVerify(request); if (_userInfoBySocket.count(socket) == 0) { CommandDispatcher::getInstance()->unlock(socket); } break; } case mtProxyLogin: { ExternalRequest *request = dynamic_cast(current); SocketInfo *socket = request->getSocketInfo(); // Should we check for duplicates? Like we do in other login types. UserInfoExport info; info.userId = strtolDefault(request->getProperty("user_id"), 0); std::string status = request->getProperty("status"); if (status == "limited") info.status = sLimited; else if (status == "full") info.status = sFull; else info.status = sNone; TclList msg; msg<unlock(socket); break; } case mtExecutionMessage: { ExternalRequest *externalRequest = dynamic_cast(current); recordExecutionMessage(externalRequest); break; } case mtSetTimeFormat: { ExternalRequest *externalRequest = dynamic_cast(current); SocketInfo *const socket = externalRequest->getSocketInfo(); const bool newValue = externalRequest->getProperty("format") == "1"; lock(); if (newValue) _useEasternTime.insert(socket); else _useEasternTime.erase(socket); unlock(); const ExternalRequest::MessageId messageId = externalRequest->getResponseMessageId(); if (messageId.present()) { XmlNode message; message.properties["format"] = externalRequest->getProperty("format"); message.properties["new_value"] = newValue?"eastern":"unix"; addToOutputQueue(socket, message.asString("API"), messageId); } break; } case mtQuit: { delete current; return; } case DeleteSocketThread::callbackId: { SocketInfo *socket = current->getSocketInfo(); remove(socket); ChallengeResponseInfo::remove(socket); break; } } delete current; } _incomingRequests.waitForRequest(untilNextCheck()); // This is odd. We often seem to wait for about 1/8 of a second too // little and have to go right back to sleep. Sometimes we wake up // early a second time, too. } } std::string UserInfoManager::getInfoForThreadMonitor() { TclList result; // Direct login: Someone gave us a password or used the challenge response // system. Probably people using TI Pro 3.x or 2.x or Scottrade or E*TRADE. result<<"direct login"<<_userInfoBySocket.size(); // Simple login: Someone went directly to us as a DEMO user. Or someone // came to us as via the micro services proxy. Each user should be a // direct login or a simple login, but not both. result<<"simple login"<<_simpleUsers.size(); // People who have requested that all times be sent to them in eastern // time, rather than time_t. This is probably the same as people who are // using Scottrade Elite. result<<"_useEasternTime"<<_useEasternTime.size(); return result; } UserInfoManager::UserInfoManager() : ThreadClass("UserInfoManager"), _incomingRequests("UserInfoManager"), _database(false, "UserInfoManager") { initMyRandom(); pthread_mutex_init(&_mutex, NULL); CommandDispatcher *c = CommandDispatcher::getInstance(); c->listenForCommand("login", &_incomingRequests, mtLogin, true); c->listenForCommand("confirm_login", &_incomingRequests, mtConfirmLogin, false, true); c->listenForCommand("special_login", &_incomingRequests, mtSpecialLogin, true); c->listenForCommand("special_login_verify", &_incomingRequests, mtSpecialVerify, false, true); c->listenForCommand("proxy_login", &_incomingRequests, mtProxyLogin, /*lock*/ true); c->listenForCommand("execution_message", &_incomingRequests, mtExecutionMessage); c->listenForCommand("set_time_format", &_incomingRequests, mtSetTimeFormat); startThread(); } UserInfo *UserInfoManager::attemptToCreate(std::string username, std::string password, std::string vendorId, std::string uniqueId, AlertId restartAfter, AlertId restartKey, int sequenceNumber, SocketInfo *socket, ExternalRequest::MessageId messageId) { // This fuction creates an object, then does some tests on the object, // then returns a pointer to the object if it is good, or deletes it if // it is in an invalid state. This way the client doesn't ever see an // object in an invalid state. The act of constructing and validating // is more complicated than anyone outside of this class should be // exposed to. The construction and testing happens in stanges, and // for simplicity we create the object immediately to try to manage // the shared state between the steps. In pariticular, the getStatusImpl() // function can make some of the same calls as this construction step, // but that function is allowed to delete the object. A constructor // cannot delete itself. TclList logMsg; logMsg<<"login" <<"username" <verifyCredentials(message) && newUser->setLastId(restartAfter, restartKey, sequenceNumber, message); if (messageId.present()) { addToOutputQueue(socket, message.asString("API"), messageId); } logMsg.clear(); logMsg<<"login" <<(success?"SUCCESS":"FAILURE"); LogFile::primary().sendString(logMsg, socket); if (success) { if (!newUser->allowMultipleLogins()) // This is the common case. If someone is already on this server using // the same username, keep this new one, and throw out the old one. // Either way, store this new user in case we have to evict him for the // same reason. storeUserInfoByName(newUser); return newUser; } else { delete newUser; return NULL; } } void UserInfoManager::verifyStatusAll() { // Compare everything to a fixed start time. This guarantees that we will // terminate. If we are really slow, we will only get the items that were // ready before we started. In particular, we won't every cycle all the way // through the list and keep going. TimeVal startTime(true); while (true) { if (_refreshSchedule.empty()) { break; } RefreshKey first = *_refreshSchedule.begin(); if (first.getTimeVal() > startTime) { break; } // This is sometimes redundant. But it's easier and safer always to // do the erase here. If the object is not found, then verifyStatus() // might not do the erase. _refreshSchedule.erase(_refreshSchedule.begin()); verifyStatus(first.getSocketInfo()); } } timeval *UserInfoManager::untilNextCheck() { std::set< RefreshKey >::iterator it = _refreshSchedule.begin(); if (it == _refreshSchedule.end()) { return NULL; } else { static timeval _untilNextCheck; _untilNextCheck = it->getTimeVal().waitTime(); return &_untilNextCheck; } } // Checks the status. Sends status messages to // the client, if necessary. Automatically deletes this object if it is no // longer valid. void UserInfoManager::verifyStatus(SocketInfo *socket) { UserInfo *userInfo = getPropertyDefault(_userInfoBySocket, socket); if (!userInfo) { // This item could have been deleted after it was scheduled. That // is explicitly legal. return; } XmlNode message; bool result = userInfo->verifyCredentials(message); if (!message.empty()) { ExternalRequest::MessageId messageId = userInfo->getMessageId(); if (messageId.present()) { addToOutputQueue(socket, message.asString("API"), messageId); } } if (!result) { remove(socket); } } void UserInfoManager::remove(SocketInfo *socket) { std::map< SocketInfo *, UserInfo * >::iterator it = _userInfoBySocket.find(socket); lock(); if (it == _userInfoBySocket.end()) { const auto simpleUsersIt = _simpleUsers.find(socket); if (simpleUsersIt != _simpleUsers.end()) { _byUserId.erase(simpleUsersIt->second.userId); _simpleUsers.erase(simpleUsersIt); } } else { delete it->second; _userInfoBySocket.erase(it); } _useEasternTime.erase(socket); unlock(); } void UserInfoManager::removeReference(UserInfo *userInfo) { std::map< std::string, UserInfo * >::iterator it = _userInfoByName.find(userInfo->getUsername()); if ((it != _userInfoByName.end()) && (it->second == userInfo)) { _userInfoByName.erase(it); } } UserInfoExport UserInfoManager::getInfo(SocketInfo *socket) { lock(); UserInfoExport result; if (UserInfo *userInfo = getPropertyDefault(_userInfoBySocket, socket)) { result = userInfo->getInfo(); } else if (UserInfoExport const *info = getProperty(_simpleUsers, socket)) { result = *info; } else { result.userId = 0; result.status = sNone; } unlock(); return result; } AlertId UserInfoManager::getLastId(SocketInfo *socket) { lock(); AlertId lastId; if (UserInfo *userInfo = getPropertyDefault(_userInfoBySocket, socket)) { lastId = userInfo->getLastId(); } else { lastId = displayNoAlertData; } unlock(); // If the value is displayNoAlertData, then the caller must check his own // database to get the most recent value. Note, this can also happen for // someone who is not logged in. The caller should be smart enough to know // that that user does not get any data. return lastId; } void UserInfoManager::recordExecutionMessage(ExternalRequest *request) { UserId userId = 0; std::string vendorId; SocketInfo *socket = request->getSocketInfo(); if (UserInfo *userInfo = getPropertyDefault(_userInfoBySocket, socket)) { userId = userInfo->getUserId(); vendorId = userInfo->getVendorId(); } else if (UserInfoExport const *info = getProperty(_simpleUsers, socket)) { userId = info->userId; vendorId = "4.x series"; } _database.tryQueryUntilSuccess ("INSERT INTO execution_messages(timestamp,ti_user_id,remote_user_name," "action,shares,direction,source,vendor_id) VALUES (NOW()," + ntoa(userId) + ", '" + mysqlEscapeString(request->getProperty("remote_user_name")) + "', '" + mysqlEscapeString(request->getProperty("action")) + "', " + ntoa(strtoulDefault(request->getProperty("shares"), 0)) + ", '" + mysqlEscapeString(request->getProperty("direction")) + "', '" + mysqlEscapeString(request->getProperty("source")) + "', '" + mysqlEscapeString(vendorId) + "')"); } void UserInfoManager::confirmLogin(ExternalRequest *request) { if (UserInfo *userInfo = getPropertyDefault(_userInfoBySocket, request->getSocketInfo())) { userInfo->confirmLogin(request); } else { LogFile::primary().quoteAndSend("Unexpected confirmLogin(), ignored.", request->getSocketInfo()); } } void UserInfoManager::storeUserInfoByName(UserInfo *userInfo) { UserInfo *prevUser = _userInfoByName[userInfo->getUsername()]; if (prevUser) { SocketInfo *prevSocket = prevUser->getSocket(); if (userInfo->getSocket() != prevSocket) { ExternalRequest::MessageId messageId = prevUser->getMessageId(); if (messageId.present()) { XmlNode message; anotherUserAbort(message); addToOutputQueue(prevSocket, message.asString("API"), messageId); } remove(prevSocket); } } // Can't save the previous reference. It might have been deleted when // we bumped the previous user, when it deleted itself. _userInfoByName[userInfo->getUsername()] = userInfo; } void UserInfoManager::recordDemoUser(std::string const &password, std::string const &vendorId) { // track:email=philip@trade-ideas.com&first_name=Philip&last_name=Smolen&source=FB std::vector< std::string > pieces = explode("track:", password); if (pieces.size() != 2) return; if (pieces[0] != "") return; PropertyList fields; parseUrlRequest(fields, pieces[1]); std::string sql = "INSERT INTO demo_user SET "; sql += "first=NOW(), last=NOW(), count=1, "; sql += "email='"; sql += mysqlEscapeString(fields["email"]); sql += "', "; sql += "first_name='"; sql += mysqlEscapeString(fields["first_name"]); sql += "', "; sql += "last_name='"; sql += mysqlEscapeString(fields["last_name"]); sql += "', "; sql += "source='"; sql += mysqlEscapeString(fields["source"]); sql += "', "; sql += "vendor_id='"; sql += mysqlEscapeString(vendorId); sql += "' ON DUPLICATE KEY UPDATE last=NOW(), count=count+1"; _database.tryQueryUntilSuccess(sql, "DEMO"); } void UserInfoManager::login(ExternalRequest *request) { SocketInfo *socket = request->getSocketInfo(); if (_userInfoBySocket.count(socket) || _simpleUsers.count(socket) || challengesInProgress.count(socket)) { LogFile::primary().quoteAndSend("Duplicate login request. " "Ignoring second request.", socket); return; } ExternalRequest::MessageId messageId = request->getResponseMessageId(); std::string username = request->getProperty("username"); std::string password = request->getProperty("password"); std::string vendorId = request->getProperty("vendor_id"); std::string uniqueId = request->getProperty("unique_id"); if (username == "DEMO") { ThreadMonitor::find().increment("DEMO login"); TclList logMsg; logMsg<<"login" <<"username" <getProperty("restart_after"), displayNoAlertData); AlertId restartKey = strtolDefault(request->getProperty("restart_key"), -1); int sequenceNumber = strtolDefault(request->getProperty("sequence"), -1); UserInfo *newUser = attemptToCreate(username, password, vendorId, uniqueId, restartAfter, restartKey, sequenceNumber, socket, messageId); if (newUser) { ThreadMonitor::find().increment("successful login"); TclList msg; msg<<"Logged_in" <getSocketInfo(); ChallengeResponseInfo *challenge = getPropertyDefault(challengesInProgress, socket); if (!challenge) { LogFile::primary().quoteAndSend("UserInfo.C" "unexpected special login verify", socket); return; } if (!challenge->verifyResponse(request->getProperty("response"))) { LogFile::primary().quoteAndSend("UserInfo.C", "special login verify failed", socket); return; } std::string username; std::string password; challenge->getTIInfo(username, password, socket->remoteAddr(), _database); LogFile::primary().sendString((TclList()<<"UserInfo.C"<getProperty("vendor_id"); std::string uniqueId = request->getProperty("unique_id"); AlertId restartAfter = strtolDefault(request->getProperty("restart_after"), displayNoAlertData); AlertId restartKey = strtolDefault(request->getProperty("restart_key"), -1); int sequenceNumber = strtolDefault(request->getProperty("sequence"), -1); UserInfo *newUser = attemptToCreate(username, password, vendorId, uniqueId, restartAfter, restartKey, sequenceNumber, socket, request->getResponseMessageId()); if (newUser) { ThreadMonitor::find().increment("successful special login"); TclList msg; msg<<"Logged_in_special" <getSocketInfo(); if (_userInfoBySocket.count(socket) || _simpleUsers.count(socket) || challengesInProgress.count(socket)) { LogFile::primary().quoteAndSend("Duplicate login request. " "Ignoring second request.", socket); return; } challengesInProgress[socket] = new ChallengeResponseInfo(request->getProperty("special_username"), request->getProperty("special_login_type"), request->getProperty("client_challenge"), _database, socket, request->getResponseMessageId()); } void UserInfoManager::lock() { pthread_mutex_lock(&_mutex); } void UserInfoManager::unlock() { pthread_mutex_unlock(&_mutex); } UserInfoManager &UserInfoManager::getInstance() { return *instance; } void UserInfoManager::initInstance() { assert(!instance); instance = new UserInfoManager; } UserInfoManager::~UserInfoManager() { assert(instance = this); instance = NULL; Request *r = new Request(NULL); r->callbackId = mtQuit; _incomingRequests.newRequest(r); waitForThread(); } ///////////////////////////////////////////////////////////////////// // UserInfo ///////////////////////////////////////////////////////////////////// TimeVal UserInfo::getNextCredentialCheck() { return _nextCredentialCheck; } std::string UserInfo::getUsername() { return _username; } SocketInfo *UserInfo::getSocket() { return _socket; } // Returns true if smaller is a prefix of bigger. inline bool prefixMatch(std::string const &bigger, char const *smaller) { // http://stackoverflow.com/questions/5770709/can-i-count-on-my-compiler-to-optimize-strlen-on-const-char return !bigger.compare(0, strlen(smaller), smaller); } // These come from an email from From: Lauri Lane // Date: Thu, Aug 13, 2015 at 3:25 PM via David Aferiat. These are // Scottrade employees who often share an account. We don't get paid for // any of these accounts, so having more people use them doesn't directly // hurt us. static const std::string HELPER[] = { "ST:13768085", "ST:13768084", "ST:72488735", "ST:72488736", "ST:72488738", "ST:11111395", "ST:57716654", "ST:57716653", "ST:57716652" }; static const std::set< std::string> ALLOW_MULTIPLE_LOGINS(HELPER, HELPER + sizeof(HELPER) / sizeof(HELPER[0])); // Should we allow multiple logins? We store this in _allowMultipleLogins so // most people don't have to think about how we got to this answer. static bool allowMultipleLoginsByUsername(std::string const &username) { return prefixMatch(username, "ES:") || ALLOW_MULTIPLE_LOGINS.count(username); } UserInfo::UserInfo(std::string username, std::string password, std::string vendorId, std::string uniqueId, SocketInfo *socket, ExternalRequest::MessageId messageId) : _username(username), _password(password), _vendorId(vendorId), _allowMultipleLogins(allowMultipleLoginsByUsername(username)), _socket(socket), _userId(0), _sequenceNumber(-1), _lastAlertId(displayNoAlertData), _messageId(messageId), _idConfirmed(false), _uniqueId(uniqueId) { } bool UserInfo::setLastId(AlertId restartAfter, /* This is the last id that the * client saw, or -1 to say that * the cleint wants to start a * new connection. */ AlertId restartKey, /* This is something hard for the * client to guess, to keep people * from cheating. This is the * last id at the last time that * we called this function. This * is only used if this is not a * new connection. */ int clientSequenceId, /* This number is bumped each * time we have a new * connection. This is used * internally to make sure we * don't stomp on another user * as we do an update. This is * similar to restartKey, but * this works even in the * middle of the night when * restartKey doesn't change. */ XmlNode &message) { // Call this only after a successful login. Call it only once. // This call implicitly checks if we have been bumped by another user. // This could have happend between the initial login step and this call. // This is an unfortunate side effect of splitting the two calls up. // If this call fails, then we have duplicate user situation. DatabaseWithRetry &database = UserInfoManager::getInstance().getDatabase(); AlertId lastId = ::getLastAlertId(database, lastIdCache, true); //_restartKey = lastId; _restartKey = 42; /* I've been having some strange problems with the restart key. People are * getting kicked off when they shouldn't be. This used to only happen when * we were having other problems, like the database was acting up. But now * it happens a lot. * * One possible problem is a flaw in the logic. Imagine the following * sequence of events: * last_id = 10, previous_last_id=10 * log in with 10 * last_id = 20, previous_last_id = 10 * receive 20 * send confirm 20 * last_id = 20, previous_last_id = 20 * log in with 20 * last_id = 30, previous_last_id = 20 * disconnect * log in with 20 * last_id = 40, previous_last_id = 20 * receive 40 * disconnect * log in with 40 * last_id = 50, previous_last_id=20 * disconnect * log in with 40 * server reports a problem when there is none! * * That's a real potential problem, but it's pretty obscure, so I don't think * it happens very often. I think we must have other problems, too. * * Here's something I see surprisingly often. * {Wed Jun 15 15:48:11 2011} 295 Another_User_Abort Case_3 Expected_sequence 1012 Found_sequence 1012 Expected_cookie {API3: DLL} Found_cookie {API3: DLL} _restartKey 7742196785 AX_last_id 7742198118 AX_previous_id 7742198118 * Note that both database versions are the same, so it looks like something * was confirmed by the client. But the number from the client is lower than * what's in the database, suggesting that it did not receive the update from * the server! Looks like a bug somewhere. Definately not the case * described above. * * For now I'm just disabling the test by making the key never change. We * still check the session id. The key was only for people who were trying * to cheat, and were presumably writing their own library to talk over * the network, not our API or client. */ int oldSequenceNumber = _sequenceNumber; // Start a new sequence number only if the user is starting a new // connection. Otherwise we'd have to confirm these like we do for // the restartKey. int newSequenceNumber = _sequenceNumber; bool startingFresh = false; if (restartAfter < 0) { startingFresh = true; newSequenceNumber++; restartAfter = lastId; } else if ((!allowMultipleLogins()) && (clientSequenceId != oldSequenceNumber)) { LogFile::primary().sendString(TclList()<<"Another_User_Abort" <<"Expected_sequence_number" < sql; sql.push_back("BEGIN"); sql.push_back("SELECT AX_sequence_number, AX_previous_id, AX_last_id " "FROM users WHERE id=" + ntoa(_userId) + " FOR UPDATE"); // This update statement contains all of the functionality that we should // require. But it mysteriously fails some times. The table is updated // correctly, but the status we get is wrong. It's like sometimes it reports // the number of rows changed, not the number of rows matched. // // AX_previous_id is the last id that was confirmed. AX_last_id is the id // that we want to use from here, forward. If something fails right after // this update, the client will still have the value of AX_previous_id. If // something fails after the client gets the upcoming message, the client // will have the value of AX_last_id. So we have to accept either of these // values from the client on a login attempt. sql.push_back("UPDATE users SET AX_last_id=" + ntoa(_restartKey) + ", AX_sequence_number=" + ntoa(newSequenceNumber) + " WHERE id=" + ntoa(_userId) + " AND AX_sequence_number=" + ntoa(oldSequenceNumber) + ((!startingFresh) ?(" AND " + ntoa(restartKey) + " IN (AX_last_id,AX_previous_id)") :"")); sql.push_back("COMMIT"); DatabaseWithRetry::ResultList dbResult = database.tryAllUntilSuccess(sql.begin(), sql.end()); if ((!allowMultipleLogins()) && (dbResult[1]->numRows() != 1)) { TclList logMsg; logMsg<<"UserInfo.C" <<"Another_User_Abort" <<"Case_2" <<"Unexpected row count" <numRows() <getIntegerField("AX_sequence_number", -1)) && (startingFresh || (restartKey == dbResult[1]->getIntegerField("AX_last_id", -1)) || (restartKey == dbResult[1]->getIntegerField("AX_previous_id", -1))); bool updateReportsMatch = dbResult[2]->getAffectedRows(); if (matched != updateReportsMatch) { TclList logMsg; logMsg<<"UserInfo.C" <<"Unexpected mismatch" <<"Select reports" <<(matched?"success":"failure") <getIntegerField("AX_sequence_number", -1) <getIntegerField("AX_last_id", -1) <getIntegerField("AX_previous_id", -1) <getIntegerField("AX_sequence_number", -1) <getIntegerField("AX_last_id", -1) <getIntegerField("AX_previous_id", -1) <rowIsValid()) { // Invalid user name badUsernameAbort(message); _userId = 0; TclList logMsg; logMsg<<"UserInfo.C" <<"verifyCredentialsNow()" <<"bad username"; LogFile::primary().sendString(logMsg, _socket); return false; } if (!loginResult->getIntegerField("password_valid", 0)) { // Invalid password badPasswordAbort(message); _userId = 0; TclList logMsg; logMsg<<"UserInfo.C" <<"verifyCredentialsNow()" <<"bad password"; LogFile::primary().sendString(logMsg, _socket); return false; } if (_sequenceNumber == -1) { // This is our first credential check. // Automatically update this the first time, but never again. _sequenceNumber = loginResult->getIntegerField("AX_sequence_number", -1); _lastCookie = loginResult->getStringField("cookie"); } else if ((!allowMultipleLogins()) && ((_sequenceNumber != loginResult->getIntegerField("AX_sequence_number", -2)) || (_lastCookie != loginResult->getStringField("cookie")) || ((_restartKey != loginResult->getIntegerField("AX_last_id", -2)) && (_restartKey != loginResult->getIntegerField("AX_previous_id", -2))))) { // We've been bumped. LogFile::primary(). sendString(TclList()<<"Another_User_Abort" <<"Case_3" <<"Expected_sequence" <<_sequenceNumber <<"Found_sequence" <getIntegerField("AX_sequence_number", -2) <<"Expected_cookie" <<_lastCookie <<"Found_cookie" <getStringField("cookie") <<"_restartKey" <<_restartKey <<"AX_last_id" <getStringField("AX_last_id") <<"AX_previous_id" <getStringField("AX_previous_id"), _socket); anotherUserAbort(message); _userId = 0; return false; } _userId = loginResult->getIntegerField("id", 0); _dataValidUntil = loginResult->getIntegerField("data_valid_until", 0); _cookieValidUntil = loginResult->getIntegerField("cookie_valid_until", 0); _nextCredentialCheck = currentTime; _nextCredentialCheck.addSeconds(45); if (_idConfirmed) { // Don't try to touch the cookies before we verify that the user is // allowed to connect. When a user first connects, we come here // before we've had a chance to check out the sequence number and // restart key. We might be looking at a user who got bumped and // doesn't know it yet. Without this check, this user would bump // whoever is currently on, but will fail later tests, and will // not get back on, himself. if (_cookieValidUntil < currentTime) { // Always call updateCookie() for the side effects. Ignore the result // if allowMultipleLogins() is true. if ((!updateCookie(desiredCookie)) && (!allowMultipleLogins())) { anotherUserAbort(message); _userId = 0; return false; } } if (_cookieValidUntil < _nextCredentialCheck) // Check a little but sooner if the cookie will expire soon. Try to be // precise with the cookies. We use these to say how long someone was // logged in. if (!allowMultipleLogins()) // The condition above doesn't seem to be necessary. We take some // short-cuts if allowMultipleLogins() is true. We don't want to get // into a tight loop, constantly trying to do the credential check. _nextCredentialCheck = _cookieValidUntil; } if (_dataValidUntil < currentTime) { requirePaymentAbort(message); _userId = 0; TclList logMsg; logMsg<<"UserInfo.C" <<"verifyCredentialsNow()" <<"require payment"; LogFile::primary().sendString(logMsg, _socket); return false; } const long maxOddsMaker = 0x7fffffff; long oddsMaker = 0; if (loginResult->fieldIsEmpty("oddsmaker_free")) { oddsMaker = maxOddsMaker; } else { oddsMaker = loginResult->getIntegerField("oddsmaker_free", 0) - loginResult->getIntegerField("oddsmaker_total", 0); if (oddsMaker < 0) { oddsMaker = 0; } else if (oddsMaker > maxOddsMaker) { oddsMaker = maxOddsMaker; } } statusGood(message, loginResult->getStringField("next_payment"), oddsMaker); return true; } bool UserInfo::updateCookie(std::string desiredCookie) { std::vector< std::string > sql; sql.push_back("BEGIN"); // The following line is just a semaphore. The next UPDATE statement might // not lock the database if this is a new user and there are no records to // update. sql.push_back("SELECT MIN(cookie_id) FROM user_cookie FOR UPDATE"); sql.push_back("UPDATE user_cookie, users SET invalidated_from='" + mysqlEscapeString(_socket->remoteAddr()) + "' WHERE user_id=" + ntoa(_userId) + " AND invalidated_from IS NULL AND AX_sequence_number=" + ntoa(_sequenceNumber) + " AND id=user_id"); sql.push_back("INSERT INTO user_cookie (cookie, user_id, requested_from, cookie_creation_time, cookie_valid_start, cookie_valid_end, confirmed_from, unique_id) SELECT '" + mysqlEscapeString(desiredCookie) + "', " + ntoa(_userId) + ", '" + mysqlEscapeString(_socket->remoteAddr()) //+ "', NOW(), NOW(), NOW() + INTERVAL 5 MINUTE, '" + "', NOW(), NOW(), NOW() + INTERVAL 1 HOUR, '" + mysqlEscapeString(_socket->remoteAddr()) + "', " + (_uniqueId.empty()?"NULL": ("'" + mysqlEscapeString(_uniqueId) + "'")) + " FROM users where id=" + ntoa(_userId) + " AND AX_sequence_number=" + ntoa(_sequenceNumber)); sql.push_back("SELECT COUNT(*) FROM users WHERE id=" + ntoa(_userId) + " AND AX_sequence_number=" + ntoa(_sequenceNumber)); sql.push_back("COMMIT"); DatabaseWithRetry::ResultList results = UserInfoManager::getInstance().getDatabase().tryAllUntilSuccess(sql.begin(), sql.end()); if (results[4]->getStringField(0) == "1") { // Success _cookieValidUntil.currentTime(); _cookieValidUntil.addHours(1); _lastCookie = desiredCookie; return true; } else { // Someone got in ahead of us. After we last read or set the // sequence number, someone bumped the sequence number. TclList error; error<<"Failed in updateCookies" <<"userId" <<_userId <<"sequenceNumber" <<_sequenceNumber <<"desiredCookie" <getAffectedRows() <getAffectedRows() <getAffectedRows() <getAffectedRows() <getAffectedRows(); error<<"affected_rows" <getProperty("key"), -2); if (key == _restartKey) { // Make both of these point to the same value, to minimize the // chance of overlap between two sessions. AX_last_id is the last // id that we sent to the client. AX_previous_id is the last id that // was confirmed. Once we get a valid confirmation, the two should // be the same. std::string sql = "UPDATE users SET AX_previous_id=AX_last_id WHERE id=" + ntoa(_userId) + " AND AX_last_id=" + ntoa(key) + " AND AX_sequence_number=" + ntoa(_sequenceNumber); UserInfoManager::getInstance().getDatabase().tryQueryUntilSuccess(sql); // Assume success for simplicity. The only way we could fail is // if someone logs in right before this step. In that case we'll // catch the problem next time. _idConfirmed = true; // We could not create the first cookie until right now. // Call verifyCredentials, rather than calling updateCookie() // directly, so we don't create a new cookie unless we have to. _cookieValidUntil.clear(); _nextCredentialCheck.clear(); XmlNode message; verifyCredentials(message); } } } UserInfo::~UserInfo() { UserInfoManager::getInstance().removeReference(this); } /* *void UserInfo::dump(XmlNode &message) *{ * message.properties["STATUS"] = _idConfirmed?"sFull":"sSuspended"; * message.properties["USERNAME"] = _username; * message.properties["PASSWORD"] = * _password.empty()?"not specified":"specified"; *if (_dataValidUntil) * { * message.properties["DATA_VALID_UNTIL"] = _dataValidUntil.ctimeString(); * } *if (_cookieValidUntil) * { * message.properties["COOKIE_VALID_UNTIL"] = *_cookieValidUntil.ctimeString(); * } *if (_nextCredentialCheck) * { * message.properties["NEXT_CREDENTIAL_CHECK"] = *_nextCredentialCheck.ctimeString(); * } *message.properties["VENDOR_ID"] = _vendorId; *message.properties["COOKIE"] = _lastCookie; *message.properties["USER_ID"] = itoa(_userId); *message.properties["SEQUENCE_NUMBER"] = itoa(_sequenceNumber); *message.properties["LAST_ALERT_ID"] = itoa(_lastAlertId); *message.properties["RESTART_KEY"] = itoa(_restartKey); *message.properties["ID_CONFIRMED"] = _idConfirmed?"yes":"no"; * */ UserInfoExport UserInfo::getInfo() { UserInfoExport result; result.userId = _userId; result.status = _idConfirmed?sFull:sSuspended; return result; } AlertId UserInfo::getLastId() { return _lastAlertId; } ///////////////////////////////////////////////////////////////////// // ChallengeResponseInfo ///////////////////////////////////////////////////////////////////// std::string ChallengeResponseInfo::generateNewPassword() { const int length = 10; const int first = ' ' + 1; const int last = 126; char result[length]; for (int i = 0; i < length; i++) { result[i] = (myRandom() % (last - first + 1)) + first; } return std::string(result, length); } void ChallengeResponseInfo::getTIInfo(std::string &username, std::string &password, std::string remoteAddress, DatabaseWithRetry &database) { assert(valid()); // We need to find the account, if it exists, and return the username and // password. If it does not exist, we need to create it. // // Originally we'd always call INSERT IGNORE. Either the there was no // account, so INSERT IGNORE would create it. Or the account already // existed and INSERT IGNORE would do nothing. Either way we found a valid // account. // // But something changed around MySQL 5.1.22. Now when an insert is ignored, // the id field is still auto-incremented. That means every time an E*TRADE // or Scottrade user tried to log in, the id field would increment. // Everything worked as expected, except that the id numbers were growing // much more quickly than they should. As of 12/27/2015 we have 619,516 // accounts but the highest id number is 14,490,791. // // https://www.percona.com/blog/2011/11/29/avoiding-auto-increment-holes-on-innodb-with-insert-ignore/ // // This isn't terrible, but I'm worried what would happen if we ran out of id // numbers. I could always change the database to use 64 bit integers. But // a lot of C++ code uses a 32 bit integer to store the user id. I wouldn't // want to track down all of those occurrences. // // The new algorithm first looks for the account. If it's not found, then we // create the account. Then we look for the account again. We use INSERT // IGNORE in case some other process created the account right after we // checked. So this will never fail to find a valid account. In some rare // cases it might still skip a number. const std::string findAccountSql = "SELECT username, password FROM users, special_login " "WHERE username = CONCAT(prefix, ':" + mysqlEscapeString(_specialUsername) + "') AND " "login_type = '" + mysqlEscapeString(_specialLoginType) + "'"; MysqlResultRef result = database.tryQueryUntilSuccess(findAccountSql); if (result->rowIsValid()) ThreadMonitor::find().increment("special_login_found_account"); else { ThreadMonitor::find().increment("special_login_create_account"); const std::string createAccountSql = "INSERT IGNORE INTO users(username, password, id, creation, " "initial_ip, authorization_type, authorization_code, status, " "authorization_expires, class, valid_exchanges, " "paid_exchanges, oddsmaker_free) " "SELECT " "CONCAT(prefix, ':" + mysqlEscapeString(_specialUsername) + "'), " "'" + mysqlEscapeString(generateNewPassword()) + "', " "0, NOW(), '" + mysqlEscapeString(remoteAddress) + "', " "'other_payments', authorization_code, 'immune', " "authorization_expires, class, exchanges, exchanges, " "oddsmaker_free " "FROM special_login " "WHERE login_type = '" + mysqlEscapeString(_specialLoginType) + "'"; database.tryQueryUntilSuccess(createAccountSql); result = database.tryQueryUntilSuccess(findAccountSql); } username = result->getStringField("username"); password = result->getStringField("password"); } int ChallengeResponseInfo::_counter; std::string ChallengeResponseInfo::generateNewChallenge() { return ntoa(TimeVal(true).asMicroseconds()) + ntoa(_counter++) + ntoa(myRandom() / 3); } ChallengeResponseInfo::ChallengeResponseInfo(std::string specialUsername, std::string specialLoginType, std::string clientChallenge, DatabaseWithRetry &database, SocketInfo *socket, ExternalRequest::MessageId messageId) : _specialUsername(specialUsername), _specialLoginType(specialLoginType) { const std::string serverChallenge = generateNewChallenge(); XmlNode message; XmlNode &body = message["VERIFY"]; body.properties["CHALLENGE"] = serverChallenge; if (!(specialUsername.empty() || specialLoginType.empty())) { std::string sql = "SELECT MD5(CONCAT('fromserver:" + mysqlEscapeString(serverChallenge) + ":', from_server_password)) " "AS expected_response "; if (!clientChallenge.empty()) { sql += ", MD5(CONCAT('toserver:" + mysqlEscapeString(clientChallenge) + ":', to_server_password)) " "AS response "; } sql += "FROM special_login " "WHERE login_type = '" + mysqlEscapeString(_specialLoginType) + "'"; MysqlResultRef result = database.tryQueryUntilSuccess(sql); _expectedResponse = result->getStringField("expected_response"); const std::string responseToClient = result->getStringField("response"); if (!responseToClient.empty()) { // This could be blank because the client didn't request it, or // because of some error, like an invalid special login type. body.properties["RESPONSE"] = responseToClient; } //message["DEBUG"].properties["SQL"] = sql; //message["DEBUG"].properties["EXPECTED_RESPONSE"] = _expectedResponse; } addToOutputQueue(socket, message.asString("API"), messageId); // We don't go out of our way to hide errors or to report them. It is // temping to add a field to say when we succeed or fail. If a cleint does // not send it's own challenge, it has no way to know if the request // succeeded or not. Is that good or bad? It is also tempting to return // the MD5 of a random string for the response field, in case of an error. // That might confound a hacker. In the abscense of any compelling reason to // go one way or the other, I took the simplest path. } void ChallengeResponseInfo::remove(SocketInfo *socket) { challengesInProgress.erase(socket); std::map< SocketInfo *, ChallengeResponseInfo * >::iterator it = challengesInProgress.find(socket); if (it != challengesInProgress.end()) { delete it->second; challengesInProgress.erase(it); } } ///////////////////////////////////////////////////////////////////// // Global ///////////////////////////////////////////////////////////////////// AlertId userInfoGetLastId(SocketInfo *socket) { return UserInfoManager::getInstance().getLastId(socket); } UserInfoExport userInfoGetInfo(SocketInfo *socket) { return UserInfoManager::getInstance().getInfo(socket); } bool userInfoUseEasternTime(SocketInfo *socket) { return UserInfoManager::getInstance().useEasternTime(socket); } void initUserInfoManager() { UserInfoManager::initInstance(); } UserInfoManager *UserInfoManager::instance;