#include #include #include "../shared/MiscSupport.h" #include "../shared/ReplyToClient.h" #include "../shared/XmlSupport.h" #include "../shared/LogFile.h" #include "../shared/SelectableRequestQueue.h" #include "../shared/CommandDispatcher.h" #include "../shared/DatabaseWithRetry.h" #include "../shared/TwoDLookup.h" #include "../shared/GlobalConfigFile.h" #include "UserInfo.h" ///////////////////////////////////////////////////////////////////// // 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 futureTestDriveAbort(XmlNode &message, long startTimeT, std::string startDate) { message["ACCOUNT_STATUS"].properties["STATE"] = "future test drive"; message["ACCOUNT_STATUS"].properties["TEST_DRIVE_START_DATE"] = ntoa(startTimeT); std::string clientMessage = "Your test drive starts " + startDate; message["STATUS"].properties["TI-ErrorMsg"] = clientMessage; 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); } } ///////////////////////////////////////////////////////////////////// // 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. // // If you need random numbers, please consider ../shared/Random.h // rather than copying this. ///////////////////////////////////////////////////////////////////// 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 */ 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. */ mtProxyDebug, /* Information about this proxy. Debug info. Likely to * change. Aimed at a human, not a software client. * Explicitly not allowed before the login. */ 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. void lock(); void unlock(); // These two are protected by the mutex std::map< SocketInfo *, UserInfo * > _userInfoBySocket; std::set< SocketInfo * > _demoUsers; // From ThreadMonitor::Extra virtual std::string getInfoForThreadMonitor(); // 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, int sequenceNumber, SocketInfo *socket, ExternalRequest::MessageId messageId); // True on success. DEMO or real user. bool login(ExternalRequest *request); void recordDemoUser(std::string const &password, std::string const &vendorId, SocketInfo *socket); void specialLogin(ExternalRequest *request); void specialLoginVerify(ExternalRequest *request); SelectableRequestQueue _incomingRequests; DatabaseWithRetry _database; const bool _fakeDatabase; TwoDArray _fakeUserInfo; // 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(); bool fakeDatabase() const { return _fakeDatabase; } TwoDArray const &fakeUserInfo() const { return _fakeUserInfo; } // 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); }; 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; const ExternalRequest::MessageId _messageId; 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() { return _messageId; } std::string getVendorId() const { return _vendorId; } UserId getUserId() const { return _userId; } UserInfoExport getInfo(); bool verifyCredentials(XmlNode &message); TimeVal getNextCredentialCheck(); void checkForMessages(XmlNode &message) const; static void dump(SocketInfo *socket, XmlNode &message); UserInfo(std::string username, std::string password, std::string vendorId, std::string uniqueId, int sequenceNumber, 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 ///////////////////////////////////////////////////////////////////// std::string UserInfoManager::getInfoForThreadMonitor() { TclList msg; msg<<"currently logged in"<<_userInfoBySocket.size() <<"current DEMO users"<<_demoUsers.size(); return msg; } 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); 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); if (login(request)) CommandDispatcher::getInstance()->unlock(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 mtProxyDebug: { ExternalRequest *request = dynamic_cast(current); SocketInfo *socket = request->getSocketInfo(); XmlNode message; message.properties["SOCKET"] = ntoa(SocketInfo::getSerialNumber(socket)); message.properties["HOST"] = getShortHostName(); addToOutputQueue(socket, message.asString("API"), request->getResponseMessageId()); 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. } } UserInfoManager::UserInfoManager() : ThreadClass("UserInfoManager"), _incomingRequests(getName()), _database(DatabaseWithRetry::MASTER, getName()), _fakeDatabase(!_database.probablyWorks()) { if (_fakeDatabase) { // Load some fake user info from a csv file because we have no database. _fakeUserInfo.loadFromCSV("user_info.csv"); TclList msg; msg<requireLogin(); c->listenForCommand("login", &_incomingRequests, mtLogin, true); c->listenForCommand("special_login", &_incomingRequests, mtSpecialLogin, true); c->listenForCommand("special_login_verify", &_incomingRequests, mtSpecialVerify, false, true); c->listenForCommand("proxy_debug", &_incomingRequests, mtProxyDebug); startThread(); } UserInfo *UserInfoManager::attemptToCreate(std::string username, std::string password, std::string vendorId, std::string uniqueId, 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); 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 { closeWhenOutputQueueIsEmpty(socket); 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); closeWhenOutputQueueIsEmpty(socket); } } void UserInfoManager::remove(SocketInfo *socket) { std::map< SocketInfo *, UserInfo * >::iterator it = _userInfoBySocket.find(socket); lock(); if (it == _userInfoBySocket.end()) { _demoUsers.erase(socket); } else { delete it->second; _userInfoBySocket.erase(it); } 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 (_demoUsers.count(socket)) { result.userId = 0; result.status = sLimited; result.username = "DEMO"; // We don't save the password for demo users. Maybe we should. This // breaks the logic used to collect emails from FB and Google logins. // So we can't use this set up to test that feature. result.password = "double proxy"; } else { result.userId = 0; result.status = sNone; } unlock(); return result; } 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); // I explicitly decided not to check for messages on some types // of error. In particular, the "another user" error can come up // multiple ways. In some cases we don't send any info about // messages to the user. //prevUser->checkForMessages(message); addToOutputQueue(prevSocket, message.asString("API"), messageId); } closeWhenOutputQueueIsEmpty(prevSocket); 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, SocketInfo *socket) { // 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"; if (_fakeDatabase) { TclList msg; msg<getSocketInfo(); if (_userInfoBySocket.count(socket) || _demoUsers.count(socket) || challengesInProgress.count(socket)) { LogFile::primary().quoteAndSend("Duplicate login request. " "Ignoring second request.", socket); return false; } 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") { static bool demoModeDisabled = getConfigItem("disable_demo", "0") == "1"; if (demoModeDisabled) { // If demo logins are disabled, then just blindly respond with a // bad password message - the same one that occurs if they actually // try a bad password. See SERV-601. ThreadMonitor::find().increment("DEMO login disabled"); XmlNode message; badPasswordAbort(message); addToOutputQueue(socket, message.asString("API"), messageId); closeWhenOutputQueueIsEmpty(socket); return false; } ThreadMonitor::find().increment("DEMO login"); TclList logMsg; logMsg<<"login" <<"username" <getProperty("sequence"), -1); UserInfo *newUser = attemptToCreate(username, password, vendorId, uniqueId, sequenceNumber, socket, messageId); if (newUser) { ThreadMonitor::find().increment("successful login"); TclList msg; msg<<"Logged_in" <getSocketInfo(); if (_fakeDatabase) { // For simplicity just ignore these. We could make this work if we had to. // Currently no one uses this. Special login is only used by the TI Pro // 3.x series, which doesn't use the micro_proxy. I'm sure that will // change eventually. But I'm not sure we'll ever need to test the special // login feature without the database. TclList msg; msg<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"); int sequenceNumber = strtolDefault(request->getProperty("sequence"), -1); UserInfo *newUser = attemptToCreate(username, password, vendorId, uniqueId, sequenceNumber, socket, request->getResponseMessageId()); if (newUser) { ThreadMonitor::find().increment("successful special login"); TclList msg; msg<<"Logged_in_special" <getSocketInfo(); if (_fakeDatabase) { // For simplicity just ignore these. We could make this work if we had to. // Currently no one uses this. Special login is only used by the TI Pro // 3.x series, which doesn't use the micro_proxy. I'm sure that will // change eventually. But I'm not sure we'll ever need to test the special // login feature without the database. TclList msg; msg<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 ///////////////////////////////////////////////////////////////////// void UserInfo::checkForMessages(XmlNode &message) const { if (_userId == 0) // Is this even possible? return; // getConfigItem: disable_user_info_check_for_messages // getConfigItem: Set this to 1 to disable this feature, or 0 to enable it. // getConfigItem: This feature is new and the details are still changing, // getConfigItem: so this is disabled by default. static const bool disabled = getConfigItem("disable_user_info_check_for_messages", "1") == "1"; if (disabled) return; const std::string sql = "SELECT SUM(IF(requires_acknowledgement = 1 AND acknowledged_date IS NULL, 1, 0)) AS requires_acknowledgement_count, SUM(IF(status = 'read', 1, 0)) AS read_count, SUM(IF(status = 'unread', 1, 0)) AS unread_count FROM notifications WHERE user_id = " + ntoa(_userId) + " AND (start_date IS NULL OR start_date < NOW()) AND (end_date is NULL OR end_date > NOW())"; UserInfoManager &userInfoManager = UserInfoManager::getInstance(); DatabaseWithRetry &database = userInfoManager.getDatabase(); auto result = database.tryQueryUntilSuccess(sql, "checkForMessages()"); auto &properties = message["MESSAGES_TO_USER"].properties; properties["requires_acknowledgement_count"] = result->getStringField(0); properties["read_count"] = result->getStringField(1); properties["unread_count"] = result->getStringField(2); } 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); } // 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:"); } UserInfo::UserInfo(std::string username, std::string password, std::string vendorId, std::string uniqueId, int sequenceNumber, SocketInfo *socket, ExternalRequest::MessageId messageId) : _username(username), _password(password), _vendorId(vendorId), _allowMultipleLogins(allowMultipleLoginsByUsername(username)), _socket(socket), _userId(0), _sequenceNumber(sequenceNumber), _messageId(messageId), _uniqueId(uniqueId) { } // True if everything is okay. bool UserInfo::verifyCredentials(XmlNode &message) { UserInfoManager::getInstance().deleteCredentialCheck(this); bool result = verifyCredentialsNow(message); UserInfoManager::getInstance().scheduleCredentialCheck(this); return result; } bool UserInfo::verifyCredentialsNow(XmlNode &message) { TimeVal currentTime(true); if (currentTime < _nextCredentialCheck) { // We checked recently and we were okay then. return true; } UserInfoManager &userInfoManager = UserInfoManager::getInstance(); DatabaseWithRetry &database = userInfoManager.getDatabase(); const bool fakeDatabase = userInfoManager.fakeDatabase(); TwoDArray const &fakeUserInfo = userInfoManager.fakeUserInfo(); const std::string desiredCookie = "API3" + (_vendorId.empty()?"":(": " + _vendorId)); std::string sql = "SELECT users.*, cookie, password = '" + mysqlEscapeString(_password) + "' AS password_valid" + ", if(authorization_type IN ('creditcard', 'awesomecalls_monthly', 'awesomecalls_annual', 'paypal', 'paypal_us', 'esp_incentive', 'esp_mastertrader', 'esp_standard'), NULL, unix_timestamp(authorization_expires)) AS next_payment" + ", UNIX_TIMESTAMP(authorization_expires) AS data_valid_until, IF(cookie='" + mysqlEscapeString(desiredCookie) + "', UNIX_TIMESTAMP(cookie_valid_end), 0) AS cookie_valid_until" + ", (SELECT DATE(scheduled_start) FROM free_preview WHERE user_id = users.id AND status = 'queued' AND scheduled_start > NOW() ORDER BY creation DESC LIMIT 1) AS upcoming_test_drive_ymd" + ", (SELECT UNIX_TIMESTAMP(scheduled_start) FROM free_preview WHERE user_id = users.id AND status = 'queued' AND scheduled_start > NOW() ORDER BY creation DESC LIMIT 1) AS upcoming_test_drive_timet" + ", (SELECT IF(id IS NOT NULL, 1, 0) FROM free_preview WHERE user_id = users.id AND status = 'in_preview' ORDER BY creation DESC LIMIT 1) AS in_test_drive" + " FROM users " + "LEFT JOIN user_cookie USE INDEX (current) " + "ON (id=user_id) AND (invalidated_from is NULL) " + "WHERE username='" + mysqlEscapeString(_username) + '\''; MysqlResultRef loginResult; UserId userIdFromQuery; if (fakeDatabase) { if (!fakeUserInfo.containsRow(_username)) { // Invalid user name badUsernameAbort(message); _userId = 0; TclList msg; msg<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; } userIdFromQuery = loginResult->getIntegerField("id", 0); } if (!loginResult) { // Fake database. Don't do anything with cookies. } else if (_lastCookie.empty()) { // This is our first credential check. if (_sequenceNumber <= 0) { // The client did not send a sequence number. Assign a new sequence // number now, record it in the database, and report it to the user. std::vector< std::string > sql; sql.push_back("BEGIN"); sql.push_back("UPDATE users " "SET AX_sequence_number = AX_sequence_number + 1 " "WHERE id=" + ntoa(userIdFromQuery)); sql.push_back("SELECT AX_sequence_number FROM users " "WHERE id=" + ntoa(userIdFromQuery)); sql.push_back("COMMIT"); _sequenceNumber = database.tryAllUntilSuccess(sql.begin(), sql.end())[2] ->getIntegerField(0, -1); if (_sequenceNumber <= 0) // This shouldn't happen. This is almost an assertion. But I don't // like to assert anything that I'm reading from an external source. LogFile::primary().sendString(TclList()<getIntegerField("AX_sequence_number", -2))) { anotherUserAbort(message); _userId = 0; return false; } } _lastCookie = loginResult->getStringField("cookie"); } else if ((!allowMultipleLogins()) && ((_sequenceNumber != loginResult->getIntegerField("AX_sequence_number", -2)) || (_lastCookie != loginResult->getStringField("cookie")))) { // 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"), _socket); anotherUserAbort(message); _userId = 0; return false; } _userId = userIdFromQuery; long testDriveStart = loginResult->getIntegerField("upcoming_test_drive_timet", 0); if (!loginResult) { // Fake database. _dataValidUntil.currentTime(); // Now _cookieValidUntil = _dataValidUntil; // Now _dataValidUntil.addHours(24); // One day from now. _cookieValidUntil.addHours(1); // One hour from now. } else { _dataValidUntil = loginResult->getIntegerField("data_valid_until", 0); _cookieValidUntil = loginResult->getIntegerField("cookie_valid_until", 0); } // check for "expired" but upcoming test drive if (_dataValidUntil < currentTime && testDriveStart > currentTime) { std::string testDriveStartYmd = loginResult->getStringField("upcoming_test_drive_ymd"); futureTestDriveAbort(message, testDriveStart, testDriveStartYmd); TclList logMsg; logMsg<<"UserInfo.C" <<"verifyCredentialsNow()" <<"testDriveStart" << testDriveStart <<"future test drive"; LogFile::primary().sendString(logMsg, _socket); return false; } if (_dataValidUntil < currentTime) { requirePaymentAbort(message); // Make sure we don't miss this case. If someone gets cut off, maybe // his payment didn't work or maybe we cut him off for some reason, // he still gets to see his messages. checkForMessages(message); _userId = 0; TclList logMsg; logMsg<<"UserInfo.C" <<"verifyCredentialsNow()" <<"testDriveStart" << testDriveStart <<"require payment"; LogFile::primary().sendString(logMsg, _socket); return false; } _nextCredentialCheck = currentTime; _nextCredentialCheck.addSeconds(45); 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; const long maxOddsMaker = 0x7fffffff; long oddsMaker = 0; if (!loginResult) { // Fake database. oddsMaker = strtolDefault(fakeUserInfo.get("oddsmaker", _username), 0); } else 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; } } const std::string nextPayment = loginResult?loginResult->getStringField("next_payment"):""; statusGood(message, nextPayment, oddsMaker); message["ACCOUNT_STATUS"].properties["USER_ID"] = itoa(_userId); checkForMessages(message); return true; } bool UserInfo::updateCookie(std::string desiredCookie) { if (UserInfoManager::getInstance().fakeDatabase()) { // Do what we normally do on success. _cookieValidUntil.currentTime(); _cookieValidUntil.addHours(1); _lastCookie = desiredCookie; return true; } std::vector< std::string > sql; sql.push_back("BEGIN"); // The following line serves two purposes. // 1) This is a semaphore. This ensures that no one else is calling // updateCookie() for this user, or modifying AX_sequence_number for this // user. // 2) This says whether the update succeeded or not. You should be able to // get this by calling getAffectedRows() on the INSERT statement, but for // some reason I could never make that work. sql.push_back("SELECT COUNT(*) FROM users WHERE id=" + ntoa(_userId) + " AND AX_sequence_number=" + ntoa(_sequenceNumber) + " FOR UPDATE"); // 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. The line above only blocks other C++ users, not web users. 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("COMMIT"); DatabaseWithRetry::ResultList results = UserInfoManager::getInstance().getDatabase().tryAllUntilSuccess(sql.begin(), sql.end()); if (results[1]->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" <dump(message); * } * else if (_demoUsers.count(socket)) * { * message.properties["STATUS"] = "sLimited"; * } * else * { * message.properties["STATUS"] = "sNone"; * } *} */ UserInfoExport UserInfo::getInfo() { UserInfoExport result; result.userId = _userId; result.status = sFull; result.username = _username; result.password = _password; return result; } ///////////////////////////////////////////////////////////////////// // 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(!UserInfoManager::getInstance().fakeDatabase()); // TODO See the change in revision 1.36 of ../ax_alert_server/UserInfo.C. // We should make the same change here. 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. // // To avoid any race conditions, locks, SQL errors, etc., we use a very // simple strategy. We always attempt to create the new entry in the users // table. We don't care if that attempt succeeds or fails. Then we read // the current values from the users table. std::vector< std::string > sql; sql.push_back("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) + "'"); sql.push_back("SELECT username, password FROM users, special_login " "WHERE username = CONCAT(prefix, ':" + mysqlEscapeString(_specialUsername) + "') AND " "login_type = '" + mysqlEscapeString(_specialLoginType) + "'"); DatabaseWithRetry::ResultList results = database.tryAllUntilSuccess(sql.begin(), sql.end()); username = results[1]->getStringField("username"); password = results[1]->getStringField("password"); //TclList logMsg; //logMsg<<"UserInfo.C" // <<"ChallengeResponseInfo::getTIInfo()" // <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 ///////////////////////////////////////////////////////////////////// UserInfoExport userInfoGetInfo(SocketInfo *socket) { return UserInfoManager::getInstance().getInfo(socket); } void initUserInfoManager() { UserInfoManager::initInstance(); } UserInfoManager *UserInfoManager::instance;