/*************************************************************************** * Copyright (C) 2008 by Max-Wilhelm Bruker * * brukie@laptop * * * * This program is free software; you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation; either version 2 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program; if not, write to the * * Free Software Foundation, Inc., * * 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. * ***************************************************************************/ #include "serversocketinterface.h" #include "email_parser.h" #include "main.h" #include "servatrice.h" #include "servatrice_database_interface.h" #include "server_logger.h" #include "settingscache.h" #include "version_string.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include inline Q_LOGGING_CATEGORY(AbstractServerSocketInterfaceLog, "abstract_server_socket_interface"); inline Q_LOGGING_CATEGORY(TcpServerSocketInterfaceLog, "tcp_server_socket_interface"); inline Q_LOGGING_CATEGORY(WebsocketServerSocketInterfaceLog, "websocket_server_socket_interface"); static const int protocolVersion = 14; AbstractServerSocketInterface::AbstractServerSocketInterface(Servatrice *_server, Servatrice_DatabaseInterface *_databaseInterface, QObject *parent) : Server_ProtocolHandler(_server, _databaseInterface, parent), servatrice(_server), sqlInterface(reinterpret_cast(databaseInterface)) { // Never call flushOutputQueue directly from outputQueueChanged. In case of a socket error, // it could lead to this object being destroyed while another function is still on the call stack. -> mutex // deadlocks etc. connect(this, SIGNAL(outputQueueChanged()), this, SLOT(flushOutputQueue()), Qt::QueuedConnection); } bool AbstractServerSocketInterface::initSession() { Event_ServerIdentification identEvent; identEvent.set_server_name(servatrice->getServerName().toStdString()); identEvent.set_server_version(VERSION_STRING); identEvent.set_protocol_version(protocolVersion); if (servatrice->getAuthenticationMethod() == Servatrice::AuthenticationSql) { identEvent.set_server_options(Event_ServerIdentification::SupportsPasswordHash); } SessionEvent *identSe = prepareSessionEvent(identEvent); sendProtocolItem(*identSe); delete identSe; // allow unlimited number of connections from the trusted sources QString trustedSources = settingsCache->value("security/trusted_sources", "127.0.0.1,::1").toString(); if (trustedSources.contains(getAddress(), Qt::CaseInsensitive)) { return true; } int maxUsers = servatrice->getMaxUsersPerAddress(); if ((maxUsers > 0) && (servatrice->getUsersWithAddress(getPeerAddress()) > maxUsers)) { Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::TOO_MANY_CONNECTIONS); SessionEvent *se = prepareSessionEvent(event); sendProtocolItem(*se); delete se; return false; } return true; } void AbstractServerSocketInterface::catchSocketError(QAbstractSocket::SocketError socketError) { qCWarning(AbstractServerSocketInterfaceLog) << "Socket error:" << socketError; prepareDestroy(); } void AbstractServerSocketInterface::catchSocketDisconnected() { prepareDestroy(); } void AbstractServerSocketInterface::transmitProtocolItem(const ServerMessage &item) { outputQueueMutex.lock(); outputQueue.append(item); outputQueueMutex.unlock(); emit outputQueueChanged(); } void AbstractServerSocketInterface::logDebugMessage(const QString &message) { logger->logMessage(message, this); } Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionCommand(int cmdType, const SessionCommand &cmd, ResponseContainer &rc) { switch ((SessionCommand::SessionCommandType)cmdType) { case SessionCommand::ADD_TO_LIST: return cmdAddToList(cmd.GetExtension(Command_AddToList::ext), rc); case SessionCommand::REMOVE_FROM_LIST: return cmdRemoveFromList(cmd.GetExtension(Command_RemoveFromList::ext), rc); case SessionCommand::DECK_LIST: return cmdDeckList(cmd.GetExtension(Command_DeckList::ext), rc); case SessionCommand::DECK_NEW_DIR: return cmdDeckNewDir(cmd.GetExtension(Command_DeckNewDir::ext), rc); case SessionCommand::DECK_DEL_DIR: return cmdDeckDelDir(cmd.GetExtension(Command_DeckDelDir::ext), rc); case SessionCommand::DECK_DEL: return cmdDeckDel(cmd.GetExtension(Command_DeckDel::ext), rc); case SessionCommand::DECK_UPLOAD: return cmdDeckUpload(cmd.GetExtension(Command_DeckUpload::ext), rc); case SessionCommand::DECK_DOWNLOAD: return cmdDeckDownload(cmd.GetExtension(Command_DeckDownload::ext), rc); case SessionCommand::REPLAY_LIST: return cmdReplayList(cmd.GetExtension(Command_ReplayList::ext), rc); case SessionCommand::REPLAY_DOWNLOAD: return cmdReplayDownload(cmd.GetExtension(Command_ReplayDownload::ext), rc); case SessionCommand::REPLAY_MODIFY_MATCH: return cmdReplayModifyMatch(cmd.GetExtension(Command_ReplayModifyMatch::ext), rc); case SessionCommand::REPLAY_DELETE_MATCH: return cmdReplayDeleteMatch(cmd.GetExtension(Command_ReplayDeleteMatch::ext), rc); case SessionCommand::REPLAY_GET_CODE: return cmdReplayGetCode(cmd.GetExtension(Command_ReplayGetCode::ext), rc); case SessionCommand::REPLAY_SUBMIT_CODE: return cmdReplaySubmitCode(cmd.GetExtension(Command_ReplaySubmitCode::ext), rc); case SessionCommand::REGISTER: return cmdRegisterAccount(cmd.GetExtension(Command_Register::ext), rc); break; case SessionCommand::ACTIVATE: return cmdActivateAccount(cmd.GetExtension(Command_Activate::ext), rc); break; case SessionCommand::FORGOT_PASSWORD_REQUEST: return cmdForgotPasswordRequest(cmd.GetExtension(Command_ForgotPasswordRequest::ext), rc); break; case SessionCommand::FORGOT_PASSWORD_RESET: return cmdForgotPasswordReset(cmd.GetExtension(Command_ForgotPasswordReset::ext), rc); break; case SessionCommand::FORGOT_PASSWORD_CHALLENGE: return cmdForgotPasswordChallenge(cmd.GetExtension(Command_ForgotPasswordChallenge::ext), rc); break; case SessionCommand::ACCOUNT_EDIT: return cmdAccountEdit(cmd.GetExtension(Command_AccountEdit::ext), rc); case SessionCommand::ACCOUNT_IMAGE: return cmdAccountImage(cmd.GetExtension(Command_AccountImage::ext), rc); case SessionCommand::SET_CARD_ART_PARAMS: return cmdSetCardArtParams(cmd.GetExtension(Command_SetCardArtParams::ext), rc); case SessionCommand::ACCOUNT_PASSWORD: return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::ext), rc); case SessionCommand::REQUEST_PASSWORD_SALT: return cmdRequestPasswordSalt(cmd.GetExtension(Command_RequestPasswordSalt::ext), rc); break; case SessionCommand::REPORT: return cmdReport(cmd.GetExtension(Command_Report::ext), rc); case SessionCommand::REPORT_MY_LIST: return cmdReportMyList(cmd.GetExtension(Command_ReportMyList::ext), rc); case SessionCommand::REPORT_ADD_COMMENT: return cmdReportAddComment(cmd.GetExtension(Command_ReportAddComment::ext), rc); case SessionCommand::REPORT_DETAILS: return cmdReportDetails(cmd.GetExtension(Command_ReportDetails::ext), rc); default: return Response::RespFunctionNotAllowed; } } Response::ResponseCode AbstractServerSocketInterface::processExtendedModeratorCommand(int cmdType, const ModeratorCommand &cmd, ResponseContainer &rc) { switch ((ModeratorCommand::ModeratorCommandType)cmdType) { case ModeratorCommand::BAN_FROM_SERVER: return cmdBanFromServer(cmd.GetExtension(Command_BanFromServer::ext), rc); case ModeratorCommand::BAN_HISTORY: return cmdGetBanHistory(cmd.GetExtension(Command_GetBanHistory::ext), rc); case ModeratorCommand::WARN_USER: return cmdWarnUser(cmd.GetExtension(Command_WarnUser::ext), rc); case ModeratorCommand::WARN_HISTORY: return cmdGetWarnHistory(cmd.GetExtension(Command_GetWarnHistory::ext), rc); case ModeratorCommand::WARN_LIST: return cmdGetWarnList(cmd.GetExtension(Command_GetWarnList::ext), rc); case ModeratorCommand::REPORT_LIST: return cmdReportList(cmd.GetExtension(Command_ReportList::ext), rc); case ModeratorCommand::REPORT_ASSIGN: return cmdReportAssign(cmd.GetExtension(Command_ReportAssign::ext), rc); case ModeratorCommand::REPORT_RESOLVE: return cmdReportResolve(cmd.GetExtension(Command_ReportResolve::ext), rc); case ModeratorCommand::VIEWLOG_HISTORY: return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::ext), rc); case ModeratorCommand::GRANT_REPLAY_ACCESS: return cmdGrantReplayAccess(cmd.GetExtension(Command_GrantReplayAccess::ext), rc); case ModeratorCommand::REPLAY_DOWNLOAD_BY_GAME_ID: return cmdReplayDownloadByGameId(cmd.GetExtension(Command_ReplayDownloadByGameId::ext), rc); case ModeratorCommand::FORCE_ACTIVATE_USER: return cmdForceActivateUser(cmd.GetExtension(Command_ForceActivateUser::ext), rc); case ModeratorCommand::GET_ADMIN_NOTES: return cmdGetAdminNotes(cmd.GetExtension(Command_GetAdminNotes::ext), rc); case ModeratorCommand::UPDATE_ADMIN_NOTES: return cmdUpdateAdminNotes(cmd.GetExtension(Command_UpdateAdminNotes::ext), rc); case ModeratorCommand::ADD_CARD_ART_RULE: return cmdAddCardArtRule(cmd.GetExtension((Command_AddCardArtRule::ext)), rc); case ModeratorCommand::REMOVE_CARD_ART_RULE: return cmdRemoveCardArtRule(cmd.GetExtension((Command_RemoveCardArtRule::ext)), rc); case ModeratorCommand::LIST_CARD_ART_RULES: return cmdListCardArtRules(cmd.GetExtension((Command_ListCardArtRules::ext)), rc); case ModeratorCommand::REPORT_USER_INFO: return cmdReportUserInfo(cmd.GetExtension(Command_ReportUserInfo::ext), rc); case ModeratorCommand::REPORT_STATS: return cmdReportStats(cmd.GetExtension(Command_ReportStats::ext), rc); case ModeratorCommand::GET_USER_SESSIONS: return cmdGetUserSessions(cmd.GetExtension(Command_GetUserSessions::ext), rc); case ModeratorCommand::GET_USER_ALTS: return cmdGetUserAlts(cmd.GetExtension(Command_GetUserAlts::ext), rc); case ModeratorCommand::GET_MODERATOR_LAST_LOGINS: return cmdGetModeratorLastLogins(cmd.GetExtension(Command_GetModeratorLastLogins::ext), rc); case ModeratorCommand::REMOVE_USER_AVATAR: return cmdRemoveUserAvatar(cmd.GetExtension(Command_RemoveUserAvatar::ext), rc); default: return Response::RespFunctionNotAllowed; } } Response::ResponseCode AbstractServerSocketInterface::processExtendedAdminCommand(int cmdType, const AdminCommand &cmd, ResponseContainer &rc) { switch ((AdminCommand::AdminCommandType)cmdType) { case AdminCommand::SHUTDOWN_SERVER: return cmdShutdownServer(cmd.GetExtension(Command_ShutdownServer::ext), rc); case AdminCommand::UPDATE_SERVER_MESSAGE: return cmdUpdateServerMessage(cmd.GetExtension(Command_UpdateServerMessage::ext), rc); case AdminCommand::RELOAD_CONFIG: return cmdReloadConfig(cmd.GetExtension(Command_ReloadConfig::ext), rc); case AdminCommand::ADJUST_MOD: return cmdAdjustMod(cmd.GetExtension(Command_AdjustMod::ext), rc); case AdminCommand::RESET_USER_PASSWORD: return cmdResetUserPassword(cmd.GetExtension(Command_ResetUserPassword::ext), rc); default: return Response::RespFunctionNotAllowed; } } Response::ResponseCode AbstractServerSocketInterface::cmdAddToList(const Command_AddToList &cmd, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } QString list = nameFromStdString(cmd.list()); QString user = nameFromStdString(cmd.user_name()); if ((list != "buddy") && (list != "ignore")) { return Response::RespContextError; } if (list == "buddy") { if (databaseInterface->isInBuddyList(QString::fromStdString(userInfo->name()), user)) { return Response::RespContextError; } } if (list == "ignore") { if (databaseInterface->isInIgnoreList(QString::fromStdString(userInfo->name()), user)) { return Response::RespContextError; } } int id1 = userInfo->id(); int id2 = sqlInterface->getUserIdInDB(user); if (id2 < 0) { return Response::RespNameNotFound; } if (id1 == id2) { return Response::RespContextError; } QSqlQuery *query = sqlInterface->prepareQuery("insert into {prefix}_" + list + "list (id_user1, id_user2) values(:id1, :id2)"); query->bindValue(":id1", id1); query->bindValue(":id2", id2); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } Event_AddToList event; event.set_list_name(cmd.list()); event.mutable_user_info()->CopyFrom(databaseInterface->getUserData(user)); // The buddy/ignore list entry is only used to display the user's basic // profile: never leak the target's email address or client id. event.mutable_user_info()->clear_email(); event.mutable_user_info()->clear_clientid(); rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdRemoveFromList(const Command_RemoveFromList &cmd, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } QString list = nameFromStdString(cmd.list()); QString user = nameFromStdString(cmd.user_name()); if ((list != "buddy") && (list != "ignore")) { return Response::RespContextError; } if (list == "buddy") { if (!databaseInterface->isInBuddyList(QString::fromStdString(userInfo->name()), user)) { return Response::RespContextError; } } if (list == "ignore") { if (!databaseInterface->isInIgnoreList(QString::fromStdString(userInfo->name()), user)) { return Response::RespContextError; } } int id1 = userInfo->id(); int id2 = sqlInterface->getUserIdInDB(user); if (id2 < 0) { return Response::RespNameNotFound; } QSqlQuery *query = sqlInterface->prepareQuery("delete from {prefix}_" + list + "list where id_user1 = :id1 and id_user2 = :id2"); query->bindValue(":id1", id1); query->bindValue(":id2", id2); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } Event_RemoveFromList event; event.set_list_name(cmd.list()); event.set_user_name(cmd.user_name()); rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); return Response::RespOk; } int AbstractServerSocketInterface::getDeckPathId(int basePathId, QStringList path) { if (path.isEmpty()) { return 0; } if (path[0].isEmpty()) { return 0; } QSqlQuery *query = sqlInterface->prepareQuery("select id from {prefix}_decklist_folders where id_parent = " ":id_parent and name = :name and id_user = :id_user"); query->bindValue(":id_parent", basePathId); query->bindValue(":name", path.takeFirst()); query->bindValue(":id_user", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return -1; } if (!query->next()) { return -1; } int id = query->value(0).toInt(); if (path.isEmpty()) { return id; } else { return getDeckPathId(id, path); } } int AbstractServerSocketInterface::getDeckPathId(const QString &path) { return getDeckPathId(0, path.split("/")); } bool AbstractServerSocketInterface::deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder) { QSqlQuery *query = sqlInterface->prepareQuery( "select id, name from {prefix}_decklist_folders where id_parent = :id_parent and id_user = :id_user"); query->bindValue(":id_parent", folderId); query->bindValue(":id_user", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return false; } QMap results; while (query->next()) { results[query->value(0).toInt()] = query->value(1).toString(); } for (int key : results.keys()) { ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items(); newItem->set_id(key); newItem->set_name(results.value(key).toStdString()); if (!deckListHelper(newItem->id(), newItem->mutable_folder())) { return false; } } query = sqlInterface->prepareQuery("select id, name, upload_time from {prefix}_decklist_files where id_folder = " ":id_folder and id_user = :id_user"); query->bindValue(":id_folder", folderId); query->bindValue(":id_user", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return false; } while (query->next()) { ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items(); newItem->set_id(query->value(0).toInt()); newItem->set_name(query->value(1).toString().toStdString()); ServerInfo_DeckStorage_File *newFile = newItem->mutable_file(); newFile->set_creation_time(query->value(2).toDateTime().toSecsSinceEpoch()); } return true; } // CHECK AUTHENTICATION! // Also check for every function that data belonging to other users cannot be accessed. Response::ResponseCode AbstractServerSocketInterface::cmdDeckList(const Command_DeckList & /*cmd*/, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } sqlInterface->checkSql(); Response_DeckList *re = new Response_DeckList; ServerInfo_DeckStorage_Folder *root = re->mutable_root(); if (!deckListHelper(0, root)) { return Response::RespContextError; } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdDeckNewDir(const Command_DeckNewDir &cmd, ResponseContainer & /*rc*/) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } sqlInterface->checkSql(); QString path = nameFromStdString(cmd.path()); int folderId = getDeckPathId(path); if (folderId == -1) { return Response::RespNameNotFound; } QString name = nameFromStdString(cmd.dir_name()); if (path.length() + name.length() + 1 > MAX_NAME_LENGTH) { return Response::RespContextError; // do not allow creation of paths that would be too long to delete } QSqlQuery *query = sqlInterface->prepareQuery( "insert into {prefix}_decklist_folders (id_parent, id_user, name) values(:id_parent, :id_user, :name)"); query->bindValue(":id_parent", folderId); query->bindValue(":id_user", userInfo->id()); query->bindValue(":name", name); if (!sqlInterface->execSqlQuery(query)) { return Response::RespContextError; } return Response::RespOk; } void AbstractServerSocketInterface::deckDelDirHelper(int basePathId) { sqlInterface->checkSql(); QSqlQuery *query = sqlInterface->prepareQuery("select id from {prefix}_decklist_folders where id_parent = :id_parent"); query->bindValue(":id_parent", basePathId); sqlInterface->execSqlQuery(query); while (query->next()) { deckDelDirHelper(query->value(0).toInt()); } query = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id_folder = :id_folder"); query->bindValue(":id_folder", basePathId); sqlInterface->execSqlQuery(query); query = sqlInterface->prepareQuery("delete from {prefix}_decklist_folders where id = :id"); query->bindValue(":id", basePathId); sqlInterface->execSqlQuery(query); } void AbstractServerSocketInterface::sendServerMessage(const QString userName, const QString message) { AbstractServerSocketInterface *user = static_cast(server->getUsers().value(userName)); if (!user) { return; } Event_UserMessage event; event.set_sender_name("Servatrice"); event.set_receiver_name(userName.toStdString()); event.set_message(message.toStdString()); SessionEvent *se = user->prepareSessionEvent(event); user->sendProtocolItem(*se); delete se; } Response::ResponseCode AbstractServerSocketInterface::cmdDeckDelDir(const Command_DeckDelDir &cmd, ResponseContainer & /*rc*/) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } sqlInterface->checkSql(); int basePathId = getDeckPathId(nameFromStdString(cmd.path())); if ((basePathId == -1) || (basePathId == 0)) { return Response::RespNameNotFound; } deckDelDirHelper(basePathId); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdDeckDel(const Command_DeckDel &cmd, ResponseContainer & /*rc*/) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } sqlInterface->checkSql(); QSqlQuery *query = sqlInterface->prepareQuery("select id from {prefix}_decklist_files where id = :id and id_user = :id_user"); query->bindValue(":id", cmd.deck_id()); query->bindValue(":id_user", userInfo->id()); sqlInterface->execSqlQuery(query); if (!query->next()) { return Response::RespNameNotFound; } query = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id"); query->bindValue(":id", cmd.deck_id()); sqlInterface->execSqlQuery(query); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Command_DeckUpload &cmd, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } if (!cmd.has_deck_list()) { return Response::RespInvalidData; } sqlInterface->checkSql(); QString deckStr = fileFromStdString(cmd.deck_list()); DeckList deck; if (!deck.loadFromString_Native(deckStr)) { return Response::RespContextError; } QString deckName = deck.getName(); if (deckName.isEmpty()) { deckName = "Unnamed deck"; } if (cmd.has_path()) { int folderId = getDeckPathId(nameFromStdString(cmd.path())); if (folderId == -1) { return Response::RespNameNotFound; } QSqlQuery *query = sqlInterface->prepareQuery("insert into {prefix}_decklist_files (id_folder, id_user, name, upload_time, " "content) values(:id_folder, :id_user, :name, NOW(), :content)"); query->bindValue(":id_folder", folderId); query->bindValue(":id_user", userInfo->id()); query->bindValue(":name", deckName); query->bindValue(":content", deckStr); sqlInterface->execSqlQuery(query); Response_DeckUpload *re = new Response_DeckUpload; ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file(); fileInfo->set_id(query->lastInsertId().toInt()); fileInfo->set_name(deckName.toStdString()); fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch()); rc.setResponseExtension(re); } else if (cmd.has_deck_id()) { QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_decklist_files set name=:name, upload_time=NOW(), " "content=:content where id = :id_deck and id_user = :id_user"); query->bindValue(":id_deck", cmd.deck_id()); query->bindValue(":id_user", userInfo->id()); query->bindValue(":name", deckName); query->bindValue(":content", deckStr); sqlInterface->execSqlQuery(query); if (query->numRowsAffected() == 0) { return Response::RespNameNotFound; } Response_DeckUpload *re = new Response_DeckUpload; ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file(); fileInfo->set_id(cmd.deck_id()); fileInfo->set_name(deckName.toStdString()); fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch()); rc.setResponseExtension(re); } else { return Response::RespInvalidData; } return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownload(const Command_DeckDownload &cmd, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } DeckList *deck; try { deck = sqlInterface->getDeckFromDatabase(cmd.deck_id(), userInfo->id()); } catch (Response::ResponseCode &r) { return r; } Response_DeckDownload *re = new Response_DeckDownload; re->set_deck(deck->writeToString_Native().toStdString()); rc.setResponseExtension(re); delete deck; return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReplayList(const Command_ReplayList & /*cmd*/, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } Response_ReplayList *re = new Response_ReplayList; QSqlQuery *query1 = sqlInterface->prepareQuery( "select a.id_game, a.replay_name, b.room_name, b.time_started, b.time_finished, b.descr, a.do_not_hide from " "{prefix}_replays_access a left join {prefix}_games b on b.id = a.id_game where a.id_player = :id_player and " "(a.do_not_hide = 1 or date_add(b.time_started, interval 7 day) > now())"); query1->bindValue(":id_player", userInfo->id()); sqlInterface->execSqlQuery(query1); while (query1->next()) { ServerInfo_ReplayMatch *matchInfo = re->add_match_list(); const int gameId = query1->value(0).toInt(); matchInfo->set_game_id(gameId); matchInfo->set_room_name(query1->value(2).toString().toStdString()); const int timeStarted = query1->value(3).toDateTime().toSecsSinceEpoch(); const int timeFinished = query1->value(4).toDateTime().toSecsSinceEpoch(); matchInfo->set_time_started(timeStarted); matchInfo->set_length(timeFinished - timeStarted); matchInfo->set_game_name(query1->value(5).toString().toStdString()); const QString replayName = query1->value(1).toString(); matchInfo->set_do_not_hide(query1->value(6).toBool()); { QSqlQuery *query2 = sqlInterface->prepareQuery("select player_name from {prefix}_games_players where id_game = :id_game"); query2->bindValue(":id_game", gameId); sqlInterface->execSqlQuery(query2); while (query2->next()) { matchInfo->add_player_names(query2->value(0).toString().toStdString()); } } { QSqlQuery *query3 = sqlInterface->prepareQuery("select id, duration from {prefix}_replays where id_game = :id_game"); query3->bindValue(":id_game", gameId); sqlInterface->execSqlQuery(query3); while (query3->next()) { ServerInfo_Replay *replayInfo = matchInfo->add_replay_list(); replayInfo->set_replay_id(query3->value(0).toInt()); replayInfo->set_replay_name(replayName.toStdString()); replayInfo->set_duration(query3->value(1).toInt()); } } } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReplayDownload(const Command_ReplayDownload &cmd, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } { QSqlQuery *query = sqlInterface->prepareQuery("select 1 from {prefix}_replays_access a left join {prefix}_replays b on " "a.id_game = b.id_game where b.id = :id_replay and a.id_player = :id_player"); query->bindValue(":id_replay", cmd.replay_id()); query->bindValue(":id_player", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } if (!query->next()) { return Response::RespAccessDenied; } } QSqlQuery *query = sqlInterface->prepareQuery("select replay from {prefix}_replays where id = :id_replay"); query->bindValue(":id_replay", cmd.replay_id()); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } if (!query->next()) { return Response::RespNameNotFound; } QByteArray data = query->value(0).toByteArray(); Response_ReplayDownload *re = new Response_ReplayDownload; re->set_replay_data(data.data(), data.size()); rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd, ResponseContainer & /*rc*/) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } if (!sqlInterface->checkSql()) { return Response::RespInternalError; } QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_replays_access set do_not_hide=:do_not_hide where " "id_player = :id_player and id_game = :id_game"); query->bindValue(":id_player", userInfo->id()); query->bindValue(":id_game", cmd.game_id()); query->bindValue(":do_not_hide", cmd.do_not_hide()); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; } Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd, ResponseContainer & /*rc*/) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } if (!sqlInterface->checkSql()) { return Response::RespInternalError; } QSqlQuery *query = sqlInterface->prepareQuery( "delete from {prefix}_replays_access where id_player = :id_player and id_game = :id_game"); query->bindValue(":id_player", userInfo->id()); query->bindValue(":id_game", cmd.game_id()); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound; } /** * Generates a hash for the given replay folder, used for auth when replay sharing. * This is a separate function in case we change the hash implementation in the future. * * Currently, we append together the first 128 bytes of the first 3 replays in the game. * Then we md5 hash it, base64 encode it, and truncate the result to 10 characters. * * @param gameId The replay match to hash * @return The hash as a QString. Returns an empty string if failed */ QString AbstractServerSocketInterface::createHashForReplay(int gameId) { QSqlQuery *query = sqlInterface->prepareQuery("select replay from {prefix}_replays where id_game = :id_game limit 3"); query->bindValue(":id_game", gameId); if (!sqlInterface->execSqlQuery(query)) { return ""; } QByteArray replaysBytes; while (query->next()) { QByteArray replay = query->value(0).toByteArray(); replay.truncate(128); replaysBytes.append(replay); } auto hash = QCryptographicHash::hash(replaysBytes, QCryptographicHash::Md5).toBase64(QByteArray::OmitTrailingEquals); hash.truncate(10); return hash; } Response::ResponseCode AbstractServerSocketInterface::cmdReplayGetCode(const Command_ReplayGetCode &cmd, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } // Check that user has access to replay match { QSqlQuery *query = sqlInterface->prepareQuery( "select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player"); query->bindValue(":id_game", cmd.game_id()); query->bindValue(":id_player", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } if (!query->next()) { return Response::RespAccessDenied; } } QString hash = createHashForReplay(cmd.game_id()); if (hash.isEmpty()) { return Response::RespInternalError; } // code is of the form - QString code = QString(QString::number(cmd.game_id()) + "-" + hash); Response_ReplayGetCode *re = new Response_ReplayGetCode; re->set_replay_code(code.toStdString()); rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReplaySubmitCode(const Command_ReplaySubmitCode &cmd, ResponseContainer & /*rc*/) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } // code is of the form - QString code = QString::fromStdString(cmd.replay_code()); QStringList split = code.split("-"); if (split.size() != 2) { // always return the same error response if code is incorrect, to not leak info to user return Response::RespNameNotFound; } QString gameId = split[0]; QString hash = split[1]; // Determine if the replay actually exists (and grab the replay name while at it) auto *replayExistsQuery = sqlInterface->prepareQuery("select replay_name from {prefix}_replays_access where id_game = :id_game limit 1"); replayExistsQuery->bindValue(":id_game", gameId); if (!sqlInterface->execSqlQuery(replayExistsQuery)) { return Response::RespInternalError; } if (!replayExistsQuery->next()) { return Response::RespNameNotFound; } const auto &replayName = replayExistsQuery->value(0).toString(); // Check if hash is correct if (hash != createHashForReplay(gameId.toInt())) { return Response::RespNameNotFound; } // Determine if user already has access to replay auto *alreadyAccessQuery = sqlInterface->prepareQuery( "select 1 from {prefix}_replays_access where id_game = :id_game and id_player = :id_player"); alreadyAccessQuery->bindValue(":id_game", gameId); alreadyAccessQuery->bindValue(":id_player", userInfo->id()); if (!sqlInterface->execSqlQuery(alreadyAccessQuery)) { return Response::RespInternalError; } if (alreadyAccessQuery->next()) { return Response::RespOk; } // Grant the User access to the replay auto *grantReplayAccessQuery = sqlInterface->prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name, do_not_hide) " "values(:idgame, :idplayer, :replayname, 0)"); grantReplayAccessQuery->bindValue(":idgame", gameId); grantReplayAccessQuery->bindValue(":idplayer", userInfo->id()); grantReplayAccessQuery->bindValue(":replayname", replayName); if (!sqlInterface->execSqlQuery(grantReplayAccessQuery)) { return Response::RespInternalError; } // update user's view Event_ReplayAdded event; SessionEvent *se = prepareSessionEvent(event); sendProtocolItem(*se); delete se; return Response::RespOk; } // MODERATOR FUNCTIONS. // May be called by admins and moderators. Permission is checked by the calling function. Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Command_ViewLogHistory &cmd, ResponseContainer &rc) { QList messageList; QString userName = nameFromStdString(cmd.user_name()); QString ipAddress = nameFromStdString(cmd.ip_address()); QString gameName = nameFromStdString(cmd.game_name()); QString gameID = nameFromStdString(cmd.game_id()); QString message = textFromStdString(cmd.message()); bool chatType = false; bool gameType = false; bool roomType = false; for (int i = 0; i != cmd.log_location_size(); ++i) { if (nameFromStdString(cmd.log_location(i)).simplified() == "room") { roomType = true; } if (nameFromStdString(cmd.log_location(i)).simplified() == "game") { gameType = true; } if (nameFromStdString(cmd.log_location(i)).simplified() == "chat") { chatType = true; } } int dateRange = cmd.date_range(); int maximumResults = cmd.maximum_results(); Response_ViewLogHistory *re = new Response_ViewLogHistory; if (servatrice->getEnableLogQuery()) { QListIterator messageIterator(sqlInterface->getMessageLogHistory( userName, ipAddress, gameName, gameID, message, chatType, gameType, roomType, dateRange, maximumResults)); while (messageIterator.hasNext()) { re->add_log_message()->CopyFrom(messageIterator.next()); } } else { ServerInfo_ChatMessage chatMessage; // create dummy chat message for room tab in the event the query is for room messages (and possibly not others) chatMessage.set_time(QString(tr("Log query disabled, please contact server owner for details.")).toStdString()); chatMessage.set_sender_id(QString("").toStdString()); chatMessage.set_sender_name(QString("").toStdString()); chatMessage.set_sender_ip(QString("").toStdString()); chatMessage.set_message(QString("").toStdString()); chatMessage.set_target_type(QString("room").toStdString()); chatMessage.set_target_id(QString("").toStdString()); chatMessage.set_target_name(QString("").toStdString()); messageList << chatMessage; // create dummy chat message for room tab in the event the query is for game messages (and possibly not others) chatMessage.set_time(QString(tr("Log query disabled, please contact server owner for details.")).toStdString()); chatMessage.set_sender_id(QString("").toStdString()); chatMessage.set_sender_name(QString("").toStdString()); chatMessage.set_sender_ip(QString("").toStdString()); chatMessage.set_message(QString("").toStdString()); chatMessage.set_target_type(QString("game").toStdString()); chatMessage.set_target_id(QString("").toStdString()); chatMessage.set_target_name(QString("").toStdString()); messageList << chatMessage; // create dummy chat message for room tab in the event the query is for chat messages (and possibly not others) chatMessage.set_time(QString(tr("Log query disabled, please contact server owner for details.")).toStdString()); chatMessage.set_sender_id(QString("").toStdString()); chatMessage.set_sender_name(QString("").toStdString()); chatMessage.set_sender_ip(QString("").toStdString()); chatMessage.set_message(QString("").toStdString()); chatMessage.set_target_type(QString("chat").toStdString()); chatMessage.set_target_id(QString("").toStdString()); chatMessage.set_target_name(QString("").toStdString()); messageList << chatMessage; QListIterator messageIterator(messageList); while (messageIterator.hasNext()) { re->add_log_message()->CopyFrom(messageIterator.next()); } } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdGetBanHistory(const Command_GetBanHistory &cmd, ResponseContainer &rc) { QList banList; QString userName = nameFromStdString(cmd.user_name()); Response_BanHistory *re = new Response_BanHistory; QListIterator banIterator(sqlInterface->getUserBanHistory(userName)); while (banIterator.hasNext()) { re->add_ban_list()->CopyFrom(banIterator.next()); } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdGetWarnList(const Command_GetWarnList &cmd, ResponseContainer &rc) { Response_WarnList *re = new Response_WarnList; QString officialWarnings = settingsCache->value("server/officialwarnings").toString(); const QList categories = parseWarningCategories(officialWarnings); for (const WarningCategory &category : categories) { re->add_warning(category.name.toStdString()); re->add_warning_il(category.startingIl); } re->set_user_name(nameFromStdString(cmd.user_name()).toStdString()); re->set_user_clientid(nameFromStdString(cmd.user_clientid()).toStdString()); rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdGetWarnHistory(const Command_GetWarnHistory &cmd, ResponseContainer &rc) { QList warnList; QString userName = nameFromStdString(cmd.user_name()); Response_WarnHistory *re = new Response_WarnHistory; QListIterator warnIterator(sqlInterface->getUserWarnHistory(userName)); while (warnIterator.hasNext()) { re->add_warn_list()->CopyFrom(warnIterator.next()); } rc.setResponseExtension(re); return Response::RespOk; } void AbstractServerSocketInterface::removeSaidMessages(const QString &userName, int amount) { for (auto *room : rooms.values()) { room->removeSaidMessages(userName, amount); } } Response::ResponseCode AbstractServerSocketInterface::cmdWarnUser(const Command_WarnUser &cmd, ResponseContainer & /*rc*/) { if (!sqlInterface->checkSql()) { // sql database is required, without database there are no moderators anyway return Response::RespInternalError; } QString userName = nameFromStdString(cmd.user_name()).simplified(); QString warningReason = textFromStdString(cmd.reason()).simplified(); QString clientId = nameFromStdString(cmd.clientid()).simplified(); QString sendingModerator = QString::fromStdString(userInfo->name()).simplified(); int amountRemove = cmd.remove_messages(); if (amountRemove != 0) { removeSaidMessages(userName, amountRemove); } if (sqlInterface->addWarning(userName, sendingModerator, warningReason, clientId)) { servatrice->clientsLock.lockForRead(); AbstractServerSocketInterface *user = static_cast(server->getUsers().value(userName)); QList moderatorList = server->getOnlineModeratorList(); servatrice->clientsLock.unlock(); if (user != nullptr) { Event_NotifyUser event; event.set_type(Event_NotifyUser::WARNING); event.set_warning_reason(warningReason.toStdString()); SessionEvent *se = user->prepareSessionEvent(event); user->sendProtocolItem(*se); delete se; } for (QString &moderator : moderatorList) { QString notificationMessage = sendingModerator + " has sent a warning with the following information"; notificationMessage.append("\n Username: " + userName); notificationMessage.append("\n Reason: " + warningReason); sendServerMessage(moderator.simplified(), notificationMessage); } return Response::RespOk; } else { return Response::RespInternalError; } } Response::ResponseCode AbstractServerSocketInterface::cmdBanFromServer(const Command_BanFromServer &cmd, ResponseContainer & /*rc*/) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } QString userName = nameFromStdString(cmd.user_name()).simplified(); QString address = nameFromStdString(cmd.address()).simplified(); QString clientId = nameFromStdString(cmd.clientid()).simplified(); QString visibleReason = textFromStdString(cmd.visible_reason()); if (userName.isEmpty() && address.isEmpty() && clientId.isEmpty()) { return Response::RespOk; } int amountRemove = cmd.remove_messages(); if (amountRemove != 0) { removeSaidMessages(userName, amountRemove); } QString trustedSources = settingsCache->value("server/trusted_sources", "127.0.0.1,::1").toString(); int minutes = cmd.minutes(); if (trustedSources.contains(address, Qt::CaseInsensitive)) { address = ""; } QSqlQuery *query = sqlInterface->prepareQuery( "insert into {prefix}_bans (user_name, ip_address, id_admin, time_from, minutes, reason, visible_reason, " "clientid) values(:user_name, :ip_address, :id_admin, NOW(), :minutes, :reason, :visible_reason, :client_id)"); query->bindValue(":user_name", userName); query->bindValue(":ip_address", address); query->bindValue(":id_admin", userInfo->id()); query->bindValue(":minutes", minutes); query->bindValue(":reason", textFromStdString(cmd.reason())); query->bindValue(":visible_reason", visibleReason); query->bindValue(":client_id", nameFromStdString(cmd.clientid())); sqlInterface->execSqlQuery(query); servatrice->clientsLock.lockForRead(); QList moderatorList = server->getOnlineModeratorList(); QList userList = servatrice->getUsersWithAddressAsList(QHostAddress(address)); if (!userName.isEmpty()) { AbstractServerSocketInterface *user = static_cast(server->getUsers().value(userName)); if (user && !userList.contains(user)) { userList.append(user); } } if (userName.isEmpty() && address.isEmpty() && (!clientId.isEmpty())) { QSqlQuery *clientIdQuery = sqlInterface->prepareQuery("select name from {prefix}_users where clientid = :client_id"); clientIdQuery->bindValue(":client_id", nameFromStdString(cmd.clientid())); sqlInterface->execSqlQuery(clientIdQuery); if (!sqlInterface->execSqlQuery(clientIdQuery)) { qCWarning(AbstractServerSocketInterfaceLog) << "ClientID username ban lookup failed: SQL Error"; } else { while (clientIdQuery->next()) { userName = clientIdQuery->value(0).toString(); AbstractServerSocketInterface *user = static_cast(server->getUsers().value(userName)); if (user && !userList.contains(user)) { userList.append(user); } } } } servatrice->clientsLock.unlock(); if (!userList.isEmpty()) { Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::BANNED); if (cmd.has_visible_reason()) { event.set_reason_str(visibleReason.toStdString()); } if (minutes) { event.set_end_time(QDateTime::currentDateTime().addSecs(60 * minutes).toSecsSinceEpoch()); } for (int i = 0; i < userList.size(); ++i) { SessionEvent *se = userList[i]->prepareSessionEvent(event); userList[i]->sendProtocolItem(*se); delete se; QMetaObject::invokeMethod(userList[i], "prepareDestroy", Qt::QueuedConnection); } } for (QString &moderator : moderatorList) { QString notificationMessage = QString::fromStdString(userInfo->name()).simplified() + " has placed a ban with the following information"; if (!userName.isEmpty()) { notificationMessage.append("\n Username: " + userName); } if (!address.isEmpty()) { notificationMessage.append("\n IP Address: " + address); } if (!clientId.isEmpty()) { notificationMessage.append("\n Client ID: " + clientId); } notificationMessage.append("\n Length: " + QString::number(minutes) + " minute(s)"); notificationMessage.append("\n Internal Reason: " + textFromStdString(cmd.reason())); notificationMessage.append("\n Visible Reason: " + textFromStdString(cmd.visible_reason())); sendServerMessage(moderator.simplified(), notificationMessage); } return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReportList(const Command_ReportList &cmd, ResponseContainer &rc) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } const bool unresolvedOnly = cmd.unresolved_only(); const int offset = static_cast(cmd.offset()); const int limit = qMin(static_cast(cmd.limit()), 1000); // Columns: 0=id, 1=reporter_name, 2=reported_user_name, 3=game_id, // 4=category, 5=description, 6=created_at, 7=status, // 8=resolution_note, 9=assigned_mod_name, // 10=room_id, 11=replay_id QString whereClause; if (unresolvedOnly) { whereClause = "WHERE r.status = 'open' OR r.status = 'assigned' "; } // Total count query QSqlQuery *countQuery = sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports r " + whereClause); int totalCount = 0; if (sqlInterface->execSqlQuery(countQuery) && countQuery->next()) { totalCount = countQuery->value(0).toInt(); } QString queryStr = "SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, " "r.category, r.description, r.created_at, r.status, " "r.resolution_note, u.name AS assigned_mod_name, r.room_id, " "(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) " "AS replay_id " "FROM {prefix}_reports r " "LEFT JOIN {prefix}_users u ON r.assigned_to = u.id "; if (unresolvedOnly) { queryStr += "WHERE r.status = 'open' OR r.status = 'assigned' "; } queryStr += "ORDER BY r.created_at DESC LIMIT :limit OFFSET :offset"; QSqlQuery *query = sqlInterface->prepareQuery(queryStr); query->bindValue(":limit", limit); query->bindValue(":offset", offset); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } Response_ReportList *re = new Response_ReportList; re->set_total_count(totalCount); while (query->next()) { ServerInfo_Report *info = re->add_reports(); info->set_report_id(query->value(0).toInt()); info->set_reporter_name(query->value(1).toString().toStdString()); info->set_reported_user_name(query->value(2).toString().toStdString()); if (!query->value(3).isNull()) { info->set_game_id(query->value(3).toInt()); } info->set_category(query->value(4).toString().toStdString()); info->set_description(query->value(5).toString().toStdString()); info->set_report_time(query->value(6).toDateTime().toSecsSinceEpoch()); info->set_status(query->value(7).toString().toStdString()); if (!query->value(8).isNull()) { info->set_resolution_note(query->value(8).toString().toStdString()); } if (!query->value(9).isNull()) { info->set_assigned_mod_name(query->value(9).toString().toStdString()); } if (!query->value(10).isNull()) { info->set_room_id(query->value(10).toInt()); } if (!query->value(11).isNull()) { info->set_replay_id(query->value(11).toInt()); } } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReportAssign(const Command_ReportAssign &cmd, ResponseContainer & /*rc*/) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } int reportId = cmd.report_id(); int modId = userInfo->id(); QSqlQuery *lookupQuery = sqlInterface->prepareQuery("SELECT status FROM {prefix}_reports WHERE id = :id"); lookupQuery->bindValue(":id", reportId); if (!sqlInterface->execSqlQuery(lookupQuery)) { return Response::RespInternalError; } if (!lookupQuery->next()) { return Response::RespNameNotFound; } if (lookupQuery->value(0).toString() != "open") { return Response::RespInvalidData; } QSqlQuery *query = sqlInterface->prepareQuery("UPDATE {prefix}_reports " "SET status = 'assigned', assigned_to = :mod_id " "WHERE id = :id AND status = 'open'"); query->bindValue(":mod_id", modId); query->bindValue(":id", reportId); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } if (query->numRowsAffected() == 0) { // The report was taken by another moderator between the lookup and this update. return Response::RespInvalidData; } return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReportResolve(const Command_ReportResolve &cmd, ResponseContainer & /*rc*/) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } int reportId = cmd.report_id(); QString note = textFromStdString(cmd.resolution_note()); bool dismissed = cmd.dismissed(); QString newStatus = dismissed ? "dismissed" : "resolved"; QSqlQuery *query = sqlInterface->prepareQuery("UPDATE {prefix}_reports " "SET status = :status, resolution_note = :note, " "resolution_time = NOW(), resolved_by = :mod_id " "WHERE id = :id AND status IN ('open', 'assigned')"); query->bindValue(":status", newStatus); query->bindValue(":note", note); query->bindValue(":mod_id", userInfo->id()); query->bindValue(":id", reportId); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } if (query->numRowsAffected() == 0) { return Response::RespInvalidData; } sqlInterface->addAuditRecord(QString::number(reportId), this->getAddress(), QString::fromStdString(userInfo->clientid()), dismissed ? "REPORT_DISMISSED" : "REPORT_RESOLVED", QString("Report #%1 %2").arg(reportId).arg(newStatus), true); // Notifying the reporter is best-effort: the resolve already succeeded. If any of the lookup // queries below fail, the report is simply left with notified = 0 and the notification is // delivered later via sendPendingReportNotifications. QSqlQuery *lookupQuery = sqlInterface->prepareQuery("SELECT reporter_id, reported_user_name FROM {prefix}_reports WHERE id = :id"); lookupQuery->bindValue(":id", reportId); QString reporterName; QString reportedUser; if (sqlInterface->execSqlQuery(lookupQuery) && lookupQuery->next()) { int reporterId = lookupQuery->value(0).toInt(); reportedUser = lookupQuery->value(1).toString(); QSqlQuery *nameQuery = sqlInterface->prepareQuery("SELECT name FROM {prefix}_users WHERE id = :id"); nameQuery->bindValue(":id", reporterId); if (sqlInterface->execSqlQuery(nameQuery) && nameQuery->next()) { reporterName = nameQuery->value(0).toString(); } } const QString ownName = QString::fromStdString(userInfo->name()); if (!reporterName.isEmpty()) { if (reporterName == ownName) { // Resolving your own report: no self-notification needed, but mark it as notified // so it is not delivered on the next login via sendPendingReportNotifications. QSqlQuery *notifiedQuery = sqlInterface->prepareQuery("UPDATE {prefix}_reports SET notified = 1 WHERE id = :id"); notifiedQuery->bindValue(":id", reportId); sqlInterface->execSqlQuery(notifiedQuery); } else { QReadLocker clientsLocker(&servatrice->clientsLock); AbstractServerSocketInterface *reporter = static_cast(server->getUsers().value(reporterName)); if (reporter) { Event_NotifyUser event; event.set_type(Event_NotifyUser::REPORT_RESOLVED); event.set_custom_title(dismissed ? tr("Report Dismissed").toStdString() : tr("Report Resolved").toStdString()); QString content = dismissed ? tr("Your report about %1 has been dismissed.").arg(reportedUser) : tr("Your report about %1 has been resolved.").arg(reportedUser); if (!note.isEmpty()) { content += "\n" + tr("Note: %1").arg(note); } event.set_custom_content(content.toStdString()); SessionEvent *se = reporter->prepareSessionEvent(event); reporter->sendProtocolItem(*se); delete se; // Only mark the report as notified if the reporter is still connected; otherwise the // notification is delivered on their next login via sendPendingReportNotifications. QSqlQuery *notifiedQuery = sqlInterface->prepareQuery("UPDATE {prefix}_reports SET notified = 1 WHERE id = :id"); notifiedQuery->bindValue(":id", reportId); sqlInterface->execSqlQuery(notifiedQuery); } } } return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReportUserInfo(const Command_ReportUserInfo &cmd, ResponseContainer &rc) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } QString userName = nameFromStdString(cmd.user_name()); QSqlQuery *userQuery = sqlInterface->prepareQuery("SELECT u.admin, u.active, u.registrationDate, u.adminnotes " "FROM {prefix}_users u " "WHERE u.name = :name"); userQuery->bindValue(":name", userName); if (!sqlInterface->execSqlQuery(userQuery)) { return Response::RespInternalError; } if (!userQuery->next()) { return Response::RespNameNotFound; } Response_ReportUserInfo *re = new Response_ReportUserInfo; re->set_user_name(cmd.user_name()); re->set_is_admin(userQuery->value(0).toBool()); re->set_is_active(userQuery->value(1).toBool()); re->set_registration_time(userQuery->value(2).toDateTime().toSecsSinceEpoch()); if (!userQuery->value(3).isNull()) { re->set_admin_notes(userQuery->value(3).toString().toStdString()); } QSqlQuery *reportCountQuery = sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE reported_user_name = :name"); reportCountQuery->bindValue(":name", userName); if (sqlInterface->execSqlQuery(reportCountQuery) && reportCountQuery->next()) { re->set_total_reports(reportCountQuery->value(0).toInt()); } QSqlQuery *banCountQuery = sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_bans WHERE user_name = :name"); banCountQuery->bindValue(":name", userName); if (sqlInterface->execSqlQuery(banCountQuery) && banCountQuery->next()) { re->set_total_bans(banCountQuery->value(0).toInt()); } QSqlQuery *warnCountQuery = sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_warnings WHERE user_name = :name"); warnCountQuery->bindValue(":name", userName); if (sqlInterface->execSqlQuery(warnCountQuery) && warnCountQuery->next()) { re->set_total_warns(warnCountQuery->value(0).toInt()); } QSqlQuery *lastLoginQuery = sqlInterface->prepareQuery("SELECT UNIX_TIMESTAMP(a.last_login) " "FROM {prefix}_user_analytics a " "JOIN {prefix}_users u ON u.id = a.id " "WHERE u.name = :name"); lastLoginQuery->bindValue(":name", userName); if (sqlInterface->execSqlQuery(lastLoginQuery) && lastLoginQuery->next() && !lastLoginQuery->value(0).isNull()) { re->set_last_login(lastLoginQuery->value(0).toLongLong()); } QSqlQuery *recentQuery = sqlInterface->prepareQuery("SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, " "r.category, r.description, r.created_at, r.status, " "r.resolution_note, ru.name AS assigned_mod_name " "FROM {prefix}_reports r " "LEFT JOIN {prefix}_users ru ON r.assigned_to = ru.id " "WHERE r.reported_user_name = :name " "ORDER BY r.created_at DESC LIMIT 10"); recentQuery->bindValue(":name", userName); if (sqlInterface->execSqlQuery(recentQuery)) { while (recentQuery->next()) { ServerInfo_Report *info = re->add_recent_reports(); info->set_report_id(recentQuery->value(0).toInt()); info->set_reporter_name(recentQuery->value(1).toString().toStdString()); info->set_reported_user_name(recentQuery->value(2).toString().toStdString()); if (!recentQuery->value(3).isNull()) { info->set_game_id(recentQuery->value(3).toInt()); } info->set_category(recentQuery->value(4).toString().toStdString()); info->set_description(recentQuery->value(5).toString().toStdString()); info->set_report_time(recentQuery->value(6).toDateTime().toSecsSinceEpoch()); info->set_status(recentQuery->value(7).toString().toStdString()); if (!recentQuery->value(8).isNull()) { info->set_resolution_note(recentQuery->value(8).toString().toStdString()); } if (!recentQuery->value(9).isNull()) { info->set_assigned_mod_name(recentQuery->value(9).toString().toStdString()); } } } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReportStats(const Command_ReportStats & /*cmd */, ResponseContainer &rc) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } if (const auto cached = servatrice->getCachedReportStats()) { rc.setResponseExtension(new Response_ReportStats(*cached)); return Response::RespOk; } Response_ReportStats *re = new Response_ReportStats; QSqlQuery *overviewQuery = sqlInterface->prepareQuery( "SELECT COUNT(*) AS total, " "SUM(status IN ('open', 'assigned')) AS pending, " "SUM(status = 'assigned') AS assigned, " "SUM(status IN ('resolved', 'dismissed')) AS resolved, " "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)) AS last24h, " "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)) AS last7d, " "SUM(created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)) AS last30d, " "SUM(created_at >= DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)) AS this_week " "FROM {prefix}_reports"); if (!sqlInterface->execSqlQuery(overviewQuery) || !overviewQuery->next()) { delete re; return Response::RespInternalError; } re->set_total_reports(overviewQuery->value(0).toInt()); re->set_total_pending(overviewQuery->value(1).toInt()); re->set_total_assigned(overviewQuery->value(2).toInt()); re->set_total_resolved(overviewQuery->value(3).toInt()); re->set_reports_last_24h(overviewQuery->value(4).toInt()); re->set_reports_last_7d(overviewQuery->value(5).toInt()); re->set_reports_last_30d(overviewQuery->value(6).toInt()); re->set_reports_this_week(overviewQuery->value(7).toInt()); bool allQueriesOk = true; QSqlQuery *lastWeekQuery = sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE created_at >= DATE_SUB(CURDATE(), " "INTERVAL WEEKDAY(CURDATE()) + 7 DAY) " "AND created_at < DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY)"); if (sqlInterface->execSqlQuery(lastWeekQuery) && lastWeekQuery->next()) { re->set_reports_last_week(lastWeekQuery->value(0).toInt()); } else { allQueriesOk = false; } QSqlQuery *avgResQuery = sqlInterface->prepareQuery( "SELECT AVG(TIMESTAMPDIFF(HOUR, created_at, resolution_time)) " "FROM {prefix}_reports WHERE status IN ('resolved','dismissed') AND resolution_time IS NOT NULL"); if (sqlInterface->execSqlQuery(avgResQuery) && avgResQuery->next()) { if (!avgResQuery->value(0).isNull()) { re->set_avg_resolution_hours(avgResQuery->value(0).toDouble()); } } else { allQueriesOk = false; } QSqlQuery *catQuery = sqlInterface->prepareQuery( "SELECT category, COUNT(*) AS cnt FROM {prefix}_reports GROUP BY category ORDER BY cnt DESC LIMIT 10"); if (sqlInterface->execSqlQuery(catQuery)) { while (catQuery->next()) { ReportCategoryCount *cc = re->add_category_counts(); cc->set_category(catQuery->value(0).toString().toStdString()); cc->set_count(catQuery->value(1).toInt()); } } else { allQueriesOk = false; } QSqlQuery *topReportedQuery = sqlInterface->prepareQuery("SELECT reported_user_name, COUNT(*) AS cnt FROM {prefix}_reports " "GROUP BY reported_user_name ORDER BY cnt DESC LIMIT 10"); if (sqlInterface->execSqlQuery(topReportedQuery)) { while (topReportedQuery->next()) { ReportTopUser *tu = re->add_top_reported_users(); tu->set_user_name(topReportedQuery->value(0).toString().toStdString()); tu->set_count(topReportedQuery->value(1).toInt()); } } else { allQueriesOk = false; } QSqlQuery *topReporterQuery = sqlInterface->prepareQuery("SELECT reporter_name, COUNT(*) AS cnt FROM {prefix}_reports " "GROUP BY reporter_name ORDER BY cnt DESC LIMIT 10"); if (sqlInterface->execSqlQuery(topReporterQuery)) { while (topReporterQuery->next()) { ReportTopUser *tu = re->add_top_reporters(); tu->set_user_name(topReporterQuery->value(0).toString().toStdString()); tu->set_count(topReporterQuery->value(1).toInt()); } } else { allQueriesOk = false; } if (allQueriesOk) { servatrice->cacheReportStats(*re); } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdGetUserSessions(const Command_GetUserSessions &cmd, ResponseContainer &rc) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } const QString userName = nameFromStdString(cmd.user_name()); if (userName.isEmpty()) { return Response::RespContextError; } Response_UserSessions *re = new Response_UserSessions; const int limit = qMin(static_cast(cmd.limit()), 500); const QList sessions = sqlInterface->getUserSessions(userName, limit); for (const ServerInfo_UserSession &session : sessions) { re->add_sessions()->CopyFrom(session); } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdGetUserAlts(const Command_GetUserAlts &cmd, ResponseContainer &rc) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } const QString userName = nameFromStdString(cmd.user_name()); if (userName.isEmpty()) { return Response::RespContextError; } Response_UserAlts *re = new Response_UserAlts; const QList alts = sqlInterface->getUserAlts(userName); for (const ServerInfo_UserAlt &alt : alts) { re->add_alts()->CopyFrom(alt); } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdGetModeratorLastLogins(const Command_GetModeratorLastLogins &, ResponseContainer &rc) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } Response_ModeratorLastLogins *re = new Response_ModeratorLastLogins; const QList logins = sqlInterface->getModeratorLastLogins(); for (const ServerInfo_ModeratorLogin &login : logins) { re->add_logins()->CopyFrom(login); } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdResetUserPassword(const Command_ResetUserPassword &cmd, ResponseContainer &rc) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } const QString userName = nameFromStdString(cmd.user_name()).simplified(); if (userName.isEmpty()) { return Response::RespContextError; } // Look up the target user's privilege level to prevent escalation. QSqlQuery *privQuery = sqlInterface->prepareQuery("SELECT admin FROM {prefix}_users WHERE name = :name"); privQuery->bindValue(":name", userName); if (!sqlInterface->execSqlQuery(privQuery) || !privQuery->next()) { return Response::RespNameNotFound; } const int targetAdmin = privQuery->value(0).toInt(); const bool targetIsAdmin = targetAdmin & 1; const bool targetIsMod = targetAdmin & 2; const bool callerIsAdmin = userInfo->user_level() & ServerInfo_User::IsAdmin; // A moderator must not reset the password of an admin or another moderator. if (!callerIsAdmin && (targetIsAdmin || targetIsMod)) { return Response::RespAccessDenied; } const QString tempPassword = PasswordHasher::generateRandomSalt(); if (!sqlInterface->changeUserPassword(userName, tempPassword, true)) { return Response::RespInternalError; } sqlInterface->setForcePasswordChange(userName, true); sqlInterface->addAuditRecord(userName, this->getAddress(), QString::fromStdString(userInfo->clientid()), "PASSWORD_RESET", "Admin password reset", true); // Notify the affected user if they are currently online. QReadLocker clientsLocker(&servatrice->clientsLock); AbstractServerSocketInterface *targetSession = static_cast(server->getUsers().value(userName)); if (targetSession) { Event_NotifyUser event; event.set_type(Event_NotifyUser::CUSTOM); event.set_custom_title(tr("Password Reset").toStdString()); event.set_custom_content(tr("An administrator has reset your password. Please log in with the new " "password provided to you and change it immediately.") .toStdString()); SessionEvent *se = targetSession->prepareSessionEvent(event); targetSession->sendProtocolItem(*se); delete se; } Response_ResetUserPassword *re = new Response_ResetUserPassword; re->set_user_name(userName.toStdString()); re->set_temporary_password(tempPassword.toStdString()); rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdRemoveUserAvatar(const Command_RemoveUserAvatar &cmd, ResponseContainer &rc) { if (!sqlInterface->checkSql()) { return Response::RespInternalError; } const QString userName = nameFromStdString(cmd.user_name()).simplified(); if (userName.isEmpty()) { return Response::RespContextError; } if (!sqlInterface->removeUserAvatar(userName)) { return Response::RespInternalError; } sqlInterface->addAuditRecord(userName, this->getAddress(), QString::fromStdString(userInfo->clientid()), "REMOVE_USER_AVATAR", "Moderator removed user avatar", true); Response_RemoveUserAvatar *re = new Response_RemoveUserAvatar; re->set_user_name(userName.toStdString()); rc.setResponseExtension(re); return Response::RespOk; } void AbstractServerSocketInterface::sendPendingReportNotifications(ResponseContainer &rc) { if (!sqlInterface->checkSql()) { return; } QSqlQuery *query = sqlInterface->prepareQuery("SELECT id, reported_user_name, status, resolution_note " "FROM {prefix}_reports " "WHERE reporter_id = :reporter_id AND notified = 0 " "AND status IN ('resolved', 'dismissed')"); query->bindValue(":reporter_id", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return; } while (query->next()) { const int reportId = query->value(0).toInt(); const QString reportedUser = query->value(1).toString(); const bool dismissed = query->value(2).toString() == "dismissed"; const QString note = query->value(3).toString(); Event_NotifyUser event; event.set_type(Event_NotifyUser::REPORT_RESOLVED); event.set_custom_title(dismissed ? tr("Report Dismissed").toStdString() : tr("Report Resolved").toStdString()); QString content = dismissed ? tr("Your report about %1 has been dismissed.").arg(reportedUser) : tr("Your report about %1 has been resolved.").arg(reportedUser); if (!note.isEmpty()) { content += "\n" + tr("Note: %1").arg(note); } event.set_custom_content(content.toStdString()); rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); QSqlQuery *notifiedQuery = sqlInterface->prepareQuery("UPDATE {prefix}_reports SET notified = 1 WHERE id = :id"); notifiedQuery->bindValue(":id", reportId); sqlInterface->execSqlQuery(notifiedQuery); } QSqlQuery *commentQuery = sqlInterface->prepareQuery("SELECT c.id, c.report_id, c.author_name, c.comment_text " "FROM {prefix}_report_comments c " "JOIN {prefix}_reports r ON r.id = c.report_id " "WHERE c.notified = 0 " "AND c.author_id != :user_id " "AND ((c.is_moderator = 1 AND r.reporter_id = :user_id) " "OR (c.is_moderator = 0 AND r.assigned_to = :user_id) " "OR (c.is_moderator = 1 AND r.assigned_to = :user_id)) " "ORDER BY c.created_at ASC"); commentQuery->bindValue(":user_id", userInfo->id()); if (!sqlInterface->execSqlQuery(commentQuery)) { return; } while (commentQuery->next()) { const qlonglong commentId = commentQuery->value(0).toLongLong(); const int reportId = commentQuery->value(1).toInt(); const QString authorName = commentQuery->value(2).toString(); const QString commentText = commentQuery->value(3).toString(); Event_NotifyUser event; event.set_type(Event_NotifyUser::REPORT_COMMENT); event.set_custom_title(tr("New Comment on Report #%1").arg(reportId).toStdString()); event.set_custom_content(tr("%1 commented:\n%2").arg(authorName, commentText).toStdString()); rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event)); QSqlQuery *notifiedQuery = sqlInterface->prepareQuery("UPDATE {prefix}_report_comments SET notified = 1 WHERE id = :id"); notifiedQuery->bindValue(":id", commentId); sqlInterface->execSqlQuery(notifiedQuery); } } void AbstractServerSocketInterface::onLogin(ResponseContainer &rc) { sendPendingReportNotifications(rc); } Response::ResponseCode AbstractServerSocketInterface::cmdReportMyList(const Command_ReportMyList & /*cmd */, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } if (!sqlInterface->checkSql()) { return Response::RespInternalError; } sendPendingReportNotifications(rc); QString queryStr = "SELECT r.id, r.reporter_name, r.reported_user_name, r.game_id, " "r.category, r.description, r.created_at, r.status, " "r.resolution_note, u.name AS assigned_mod_name, r.room_id, " "(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) " "AS replay_id " "FROM {prefix}_reports r " "LEFT JOIN {prefix}_users u ON r.assigned_to = u.id " "WHERE r.reporter_id = :reporter_id " "ORDER BY r.created_at DESC LIMIT 200"; QSqlQuery *query = sqlInterface->prepareQuery(queryStr); query->bindValue(":reporter_id", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } Response_ReportMyList *re = new Response_ReportMyList; while (query->next()) { ServerInfo_Report *info = re->add_reports(); info->set_report_id(query->value(0).toInt()); info->set_reporter_name(query->value(1).toString().toStdString()); info->set_reported_user_name(query->value(2).toString().toStdString()); if (!query->value(3).isNull()) { info->set_game_id(query->value(3).toInt()); } info->set_category(query->value(4).toString().toStdString()); info->set_description(query->value(5).toString().toStdString()); info->set_report_time(query->value(6).toDateTime().toSecsSinceEpoch()); info->set_status(query->value(7).toString().toStdString()); if (!query->value(8).isNull()) { info->set_resolution_note(query->value(8).toString().toStdString()); } if (!query->value(9).isNull()) { info->set_assigned_mod_name(query->value(9).toString().toStdString()); } if (!query->value(10).isNull()) { info->set_room_id(query->value(10).toInt()); } if (!query->value(11).isNull()) { info->set_replay_id(query->value(11).toInt()); } } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReportDetails(const Command_ReportDetails &cmd, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } if (!sqlInterface->checkSql()) { return Response::RespInternalError; } const int reportId = cmd.report_id(); // Columns: 0=id, 1=reporter_id, 2=reporter_name, 3=reported_user_name, 4=game_id, // 5=category, 6=description, 7=created_at, 8=status, // 9=resolution_note, 10=assigned_mod_name, 11=chat_log, // 12=room_id, 13=resolution_time, 14=replay_id QString queryStr = "SELECT r.id, r.reporter_id, r.reporter_name, r.reported_user_name, r.game_id, " "r.category, r.description, r.created_at, r.status, " "r.resolution_note, u.name AS assigned_mod_name, r.chat_log, r.room_id, " "r.resolution_time, " "(SELECT id FROM {prefix}_replays WHERE id_game = r.game_id ORDER BY id DESC LIMIT 1) " "AS replay_id " "FROM {prefix}_reports r " "LEFT JOIN {prefix}_users u ON r.assigned_to = u.id " "WHERE r.id = :id"; QSqlQuery *query = sqlInterface->prepareQuery(queryStr); query->bindValue(":id", reportId); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } if (!query->next()) { return Response::RespNameNotFound; } const bool isMod = userInfo->user_level() & ServerInfo_User::IsModerator; const int reporterId = query->value(1).toInt(); if (reporterId != userInfo->id() && !isMod) { return Response::RespAccessDenied; } ServerInfo_Report *info = new ServerInfo_Report; info->set_report_id(query->value(0).toInt()); info->set_reporter_name(query->value(2).toString().toStdString()); info->set_reported_user_name(query->value(3).toString().toStdString()); if (!query->value(4).isNull()) { info->set_game_id(query->value(4).toInt()); } info->set_category(query->value(5).toString().toStdString()); info->set_description(query->value(6).toString().toStdString()); info->set_report_time(query->value(7).toDateTime().toSecsSinceEpoch()); info->set_status(query->value(8).toString().toStdString()); if (!query->value(9).isNull()) { info->set_resolution_note(query->value(9).toString().toStdString()); } if (!query->value(10).isNull()) { info->set_assigned_mod_name(query->value(10).toString().toStdString()); } if (!query->value(11).isNull()) { info->set_chat_log(query->value(11).toString().toStdString()); } if (!query->value(12).isNull()) { info->set_room_id(query->value(12).toInt()); } if (!query->value(13).isNull()) { info->set_resolution_time(query->value(13).toDateTime().toSecsSinceEpoch()); } if (!query->value(14).isNull()) { info->set_replay_id(query->value(14).toInt()); } QSqlQuery *commentQuery = sqlInterface->prepareQuery("SELECT c.author_name, c.comment_text, c.created_at, c.is_moderator " "FROM {prefix}_report_comments c " "WHERE c.report_id = :report_id ORDER BY c.created_at ASC"); commentQuery->bindValue(":report_id", reportId); if (sqlInterface->execSqlQuery(commentQuery)) { while (commentQuery->next()) { ServerInfo_ReportComment *comment = info->add_comments(); comment->set_author_name(commentQuery->value(0).toString().toStdString()); comment->set_comment_text(commentQuery->value(1).toString().toStdString()); comment->set_comment_time(commentQuery->value(2).toDateTime().toSecsSinceEpoch()); comment->set_is_moderator(commentQuery->value(3).toBool()); } } Response_ReportDetails *re = new Response_ReportDetails; re->set_allocated_report(info); rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReportAddComment(const Command_ReportAddComment &cmd, ResponseContainer & /*rc*/) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } if (!sqlInterface->checkSql()) { return Response::RespInternalError; } int reportId = cmd.report_id(); QString commentText = textFromStdString(cmd.comment()).trimmed(); if (commentText.isEmpty()) { return Response::RespInvalidData; } const int maxCommentsPerHour = settingsCache->value("reporting/max_comments_per_hour", 30).toInt(); if (maxCommentsPerHour > 0) { QSqlQuery *countQuery = sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_report_comments WHERE author_id = :id " "AND created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)"); countQuery->bindValue(":id", userInfo->id()); if (sqlInterface->execSqlQuery(countQuery) && countQuery->next() && countQuery->value(0).toInt() >= maxCommentsPerHour) { return Response::RespTooManyRequests; } } QSqlQuery *checkQuery = sqlInterface->prepareQuery( "SELECT reporter_id, reporter_name, assigned_to, status FROM {prefix}_reports WHERE id = :id"); checkQuery->bindValue(":id", reportId); if (!sqlInterface->execSqlQuery(checkQuery)) { return Response::RespInternalError; } if (!checkQuery->next()) { return Response::RespNameNotFound; } int reporterId = checkQuery->value(0).toInt(); QString reporterName = checkQuery->value(1).toString(); int assignedToId = checkQuery->value(2).toInt(); bool isMod = userInfo->user_level() & ServerInfo_User::IsModerator; // Only the reporter (by id) or a moderator may comment on a report. if (reporterId != userInfo->id() && !isMod) { return Response::RespAccessDenied; } QString reportStatus = checkQuery->value(3).toString(); if (reportStatus == "resolved" || reportStatus == "dismissed") { return Response::RespInvalidData; } QSqlQuery *insertQuery = sqlInterface->prepareQuery( "INSERT INTO {prefix}_report_comments (report_id, author_name, author_id, comment_text, created_at, " "is_moderator) " "VALUES (:report_id, :author_name, :author_id, :comment_text, NOW(), :is_moderator)"); insertQuery->bindValue(":report_id", reportId); insertQuery->bindValue(":author_name", QString::fromStdString(userInfo->name())); insertQuery->bindValue(":author_id", userInfo->id()); insertQuery->bindValue(":comment_text", commentText); insertQuery->bindValue(":is_moderator", isMod); if (!sqlInterface->execSqlQuery(insertQuery)) { return Response::RespInternalError; } const qlonglong commentId = insertQuery->lastInsertId().toLongLong(); QStringList recipients; if (isMod) { // A moderator comment reaches both the reporter and (if assigned) the assigned moderator. recipients.append(reporterName); if (assignedToId > 0) { QSqlQuery *modNameQuery = sqlInterface->prepareQuery("SELECT name FROM {prefix}_users WHERE id = :id"); modNameQuery->bindValue(":id", assignedToId); if (sqlInterface->execSqlQuery(modNameQuery) && modNameQuery->next()) { recipients.append(modNameQuery->value(0).toString()); } } } else if (assignedToId > 0) { QSqlQuery *modNameQuery = sqlInterface->prepareQuery("SELECT name FROM {prefix}_users WHERE id = :id"); modNameQuery->bindValue(":id", assignedToId); if (sqlInterface->execSqlQuery(modNameQuery) && modNameQuery->next()) { recipients.append(modNameQuery->value(0).toString()); } } const QString ownName = QString::fromStdString(userInfo->name()); bool allNotified = !recipients.isEmpty(); QReadLocker clientsLocker(&servatrice->clientsLock); for (const QString ¬ifyName : recipients) { if (notifyName == ownName) { continue; } AbstractServerSocketInterface *notify = static_cast(server->getUsers().value(notifyName)); if (!notify) { // The recipient is offline; leave `notified` = 0 so the notification is // delivered on their next login via sendPendingReportNotifications. allNotified = false; continue; } Event_NotifyUser event; event.set_type(Event_NotifyUser::REPORT_COMMENT); event.set_custom_title(tr("New Comment on Report #%1").arg(reportId).toStdString()); event.set_custom_content( tr("%1 commented:\n%2").arg(QString::fromStdString(userInfo->name()), commentText).toStdString()); SessionEvent *se = notify->prepareSessionEvent(event); notify->sendProtocolItem(*se); delete se; } if (allNotified) { QSqlQuery *notifiedQuery = sqlInterface->prepareQuery("UPDATE {prefix}_report_comments SET notified = 1 WHERE id = :id"); notifiedQuery->bindValue(":id", commentId); sqlInterface->execSqlQuery(notifiedQuery); } return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReplayDownloadByGameId(const Command_ReplayDownloadByGameId &cmd, ResponseContainer &rc) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } if (!sqlInterface->checkSql()) { return Response::RespInternalError; } QSqlQuery *query = sqlInterface->prepareQuery("SELECT r.id, r.replay FROM {prefix}_replays r " "WHERE r.id_game = :game_id ORDER BY r.id DESC LIMIT 1"); query->bindValue(":game_id", cmd.game_id()); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } if (!query->next()) { return Response::RespNameNotFound; } int replayId = query->value(0).toInt(); QByteArray data = query->value(1).toByteArray(); Response_ReplayDownloadByGameId *re = new Response_ReplayDownloadByGameId; re->set_replay_data(data.data(), data.size()); re->set_replay_id(replayId); rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const Command_Register &cmd, ResponseContainer &rc) { QString userName = nameFromStdString(cmd.user_name()); QString clientId = nameFromStdString(cmd.clientid()); qCDebug(AbstractServerSocketInterfaceLog) << "Got register command for user:" << userName; bool registrationEnabled = settingsCache->value("registration/enabled", false).toBool(); if (!registrationEnabled) { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "Server functionality disabled", false); } return Response::RespRegistrationDisabled; } const QString emailBlackList = servatrice->getEmailBlackList(); const QString emailWhiteList = servatrice->getEmailWhiteList(); const auto parsedEmailParts = EmailParser::parseEmailAddress(nameFromStdString(cmd.email())); const auto emailUser = parsedEmailParts.first; const auto emailDomain = parsedEmailParts.second; const QStringList emailBlackListFilters = emailBlackList.split(",", Qt::SkipEmptyParts); const QStringList emailWhiteListFilters = emailWhiteList.split(",", Qt::SkipEmptyParts); bool requireEmailForRegistration = settingsCache->value("registration/requireemail", true).toBool(); if (requireEmailForRegistration && emailUser.isEmpty()) { return Response::RespEmailRequiredToRegister; } // If a whitelist exists, ensure the email address domain IS in the whitelist if (!emailWhiteListFilters.isEmpty() && !emailWhiteListFilters.contains(emailDomain, Qt::CaseInsensitive)) { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "Email used is not whitelisted", false); } auto *re = new Response_Register; re->set_denied_reason_str( "The email address provider used during registration has not been approved for use on this server."); rc.setResponseExtension(re); return Response::RespEmailBlackListed; } // If a blacklist exists, ensure the email address domain is NOT in the blacklist if (!emailBlackListFilters.isEmpty() && emailBlackListFilters.contains(emailDomain, Qt::CaseInsensitive)) { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "Email used is blacklisted", false); } return Response::RespEmailBlackListed; } //! \todo Move this method outside of the db interface. QString errorString; if (!sqlInterface->usernameIsValid(userName, errorString)) { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "Username is invalid", false); } Response_Register *re = new Response_Register; re->set_denied_reason_str(errorString.toStdString()); rc.setResponseExtension(re); return Response::RespUsernameInvalid; } if (userName.toLower().simplified() == "servatrice") { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "Username is invalid", false); } return Response::RespUsernameInvalid; } if (sqlInterface->userExists(userName)) { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "Username already exists", false); } return Response::RespUserAlreadyExists; } const auto parsedEmailAddress = EmailParser::getParsedEmailAddress(parsedEmailParts); if (servatrice->getMaxAccountsPerEmail() > 0 && sqlInterface->checkNumberOfUserAccounts(parsedEmailAddress) >= servatrice->getMaxAccountsPerEmail()) { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "Too many usernames registered with this email address", false); } return Response::RespTooManyRequests; } QString banReason; int banSecondsRemaining; if (sqlInterface->checkUserIsBanned(this->getAddress(), userName, clientId, banReason, banSecondsRemaining)) { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "User is banned", false); } Response_Register *re = new Response_Register; re->set_denied_reason_str(banReason.toStdString()); if (banSecondsRemaining != 0) { re->set_denied_end_time(QDateTime::currentDateTime().addSecs(banSecondsRemaining).toSecsSinceEpoch()); } rc.setResponseExtension(re); return Response::RespUserIsBanned; } if (tooManyRegistrationAttempts(this->getAddress())) { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "Too many registration attempts from this ip address", false); } return Response::RespTooManyRequests; } QString realName = nameFromStdString(cmd.real_name()); QString country = nameFromStdString(cmd.country()); QString password; bool passwordNeedsHash = false; if (cmd.has_password()) { if (cmd.password().length() > MAX_NAME_LENGTH) { return Response::RespRegistrationFailed; } password = QString::fromStdString(cmd.password()); passwordNeedsHash = true; if (!isPasswordLongEnough(password.length())) { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "Password is too short", false); } return Response::RespPasswordTooShort; } } else if (cmd.hashed_password().length() > MAX_NAME_LENGTH) { return Response::RespRegistrationFailed; } else { password = QString::fromStdString(cmd.hashed_password()); } bool requireEmailActivation = settingsCache->value("registration/requireemailactivation", true).toBool(); bool regSucceeded = sqlInterface->registerUser(userName, realName, password, passwordNeedsHash, parsedEmailAddress, country, !requireEmailActivation); if (regSucceeded) { qCDebug(AbstractServerSocketInterfaceLog) << "Accepted register command for user:" << userName; if (requireEmailActivation) { QSqlQuery *query = sqlInterface->prepareQuery("insert into {prefix}_activation_emails (name) values(:name)"); query->bindValue(":name", userName); if (!sqlInterface->execSqlQuery(query)) { return Response::RespRegistrationFailed; } if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "", true); } return Response::RespRegistrationAcceptedNeedsActivation; } else { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "", true); } return Response::RespRegistrationAccepted; } } else { if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "REGISTER_ACCOUNT", "Unknown reason for failure", false); } return Response::RespRegistrationFailed; } } bool AbstractServerSocketInterface::tooManyRegistrationAttempts(const QString &ipAddress) { //! \todo Implement registration attempt limiting. Q_UNUSED(ipAddress); return false; } Response::ResponseCode AbstractServerSocketInterface::cmdActivateAccount(const Command_Activate &cmd, ResponseContainer & /*rc*/) { QString userName = nameFromStdString(cmd.user_name()); QString token = nameFromStdString(cmd.token()); QString clientId = nameFromStdString(cmd.clientid()); if (clientId.isEmpty()) { clientId = "UNKNOWN"; } if (sqlInterface->activateUser(userName, token)) { qCDebug(AbstractServerSocketInterfaceLog) << "Accepted activation for user" << userName; if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "ACTIVATE_ACCOUNT", "", true); } return Response::RespActivationAccepted; } else { qCDebug(AbstractServerSocketInterfaceLog) << "Failed activation for user" << userName; if (servatrice->getEnableRegistrationAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "ACTIVATE_ACCOUNT", "Failed to activate account, incorrect activation token", false); } return Response::RespActivationFailed; } } Response::ResponseCode AbstractServerSocketInterface::cmdAccountEdit(const Command_AccountEdit &cmd, ResponseContainer & /* rc */) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } QString realName = nameFromStdString(cmd.real_name()); const auto parsedEmailAddress = EmailParser::getParsedEmailAddress(nameFromStdString(cmd.email())); QString country = nameFromStdString(cmd.country()); bool checkedPassword = false; QString userName = QString::fromStdString(userInfo->name()); if (cmd.has_password_check()) { if (cmd.password_check().length() > MAX_NAME_LENGTH) { return Response::RespWrongPassword; } QString password = QString::fromStdString(cmd.password_check()); QString clientId = QString::fromStdString(userInfo->clientid()); QString reasonStr{}; int secondsLeft{}; AuthenticationResult checkStatus = databaseInterface->checkUserPassword(this, userName, password, clientId, reasonStr, secondsLeft, true); if (checkStatus == PasswordRight) { checkedPassword = true; } else { // the user already logged in with this info, the only change is their password return Response::RespWrongPassword; } } QStringList queryList({}); if (cmd.has_real_name()) { queryList << "realname=:realName"; } if (cmd.has_email()) { // a real password is required in order to change the email address if (usingRealPassword || checkedPassword) { queryList << "email=:email"; } else { return Response::RespFunctionNotAllowed; } } if (cmd.has_country()) { queryList << "country=:country"; } if (queryList.isEmpty()) { return Response::RespOk; } QString queryText = QString("update {prefix}_users set %1 where name=:userName").arg(queryList.join(", ")); QSqlQuery *query = sqlInterface->prepareQuery(queryText); if (cmd.has_real_name()) { auto _realName = nameFromStdString(cmd.real_name()); query->bindValue(":realName", _realName); } if (cmd.has_email()) { const auto _parsedEmailAddress = EmailParser::getParsedEmailAddress(nameFromStdString(cmd.email())); query->bindValue(":email", _parsedEmailAddress); } if (cmd.has_country()) { auto _country = nameFromStdString(cmd.country()); query->bindValue(":country", _country); } query->bindValue(":userName", userName); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } if (cmd.has_real_name()) { userInfo->set_real_name(realName.toStdString()); } if (cmd.has_email()) { userInfo->set_email(parsedEmailAddress.toStdString()); } if (cmd.has_country()) { userInfo->set_country(country.toStdString()); } return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdAccountImage(const Command_AccountImage &cmd, ResponseContainer & /* rc */) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } size_t length = qMin(cmd.image().length(), (size_t)MAX_FILE_LENGTH); QByteArray image(cmd.image().c_str(), length); int id = userInfo->id(); QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_users set avatar_bmp=:image where id=:id"); query->bindValue(":image", image); query->bindValue(":id", id); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } userInfo->set_avatar_bmp(cmd.image().c_str(), length); return Response::RespOk; } bool AbstractServerSocketInterface::isCardNameAllowed(const QString &cardName, const QString &cardProviderId) { QSqlQuery *q = sqlInterface->prepareQuery( "SELECT mode FROM {prefix}_card_art_name_rules WHERE card_name = :name AND card_provider_id = :provider"); q->bindValue(":name", cardName); q->bindValue(":provider", cardProviderId); if (!sqlInterface->execSqlQuery(q)) { qWarning() << "Card art rule lookup failed; failing open for" << cardName; return true; } if (!q->next()) { return true; // default allow } return q->value(0).toString() != "DENY"; } Response::ResponseCode AbstractServerSocketInterface::cmdSetCardArtParams(const Command_SetCardArtParams &cmd, ResponseContainer & /* rc */) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } const QString cardName = QString::fromStdString(cmd.card_name()); const QString cardProviderId = QString::fromStdString(cmd.card_provider_id()); if (cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { return Response::RespInvalidData; } if (cardName.isEmpty()) { // Removal path QSqlQuery *q = sqlInterface->prepareQuery("UPDATE {prefix}_users SET card_art_params = NULL WHERE id = :id"); q->bindValue(":id", userInfo->id()); if (!sqlInterface->execSqlQuery(q)) { return Response::RespInternalError; } userInfo->clear_card_art_params(); server->broadcastUserInfoUpdate(this); return Response::RespOk; } if (!isCardNameAllowed(cardName, cardProviderId)) { return Response::RespFunctionNotAllowed; } // Clamp everything to sane ranges server-side so a malicious client // can't store garbage that breaks other clients' rendering. const double marginPctL = qBound(0.0, cmd.margin_pct_l(), 0.95); const double marginPctR = qBound(0.0, cmd.margin_pct_r(), 0.95); const double verticalOffset = qBound(0.0, cmd.vertical_offset(), 1.0); const double zoom = qBound(0.1, cmd.zoom(), 4.0); QJsonObject obj; obj["card_name"] = cardName; obj["card_provider_id"] = cardProviderId; obj["marginPctL"] = marginPctL; obj["marginPctR"] = marginPctR; obj["verticalOffset"] = verticalOffset; obj["zoom"] = zoom; const QString json = QString::fromUtf8(QJsonDocument(obj).toJson(QJsonDocument::Compact)); QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_users set card_art_params=:params where id=:id"); query->bindValue(":params", json); query->bindValue(":id", userInfo->id()); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } // Keep the in-memory userInfo in sync auto *cap = userInfo->mutable_card_art_params(); cap->set_card_name(cmd.card_name()); cap->set_card_provider_id(cmd.card_provider_id()); cap->set_margin_pct_l(marginPctL); cap->set_margin_pct_r(marginPctR); cap->set_vertical_offset(verticalOffset); cap->set_zoom(zoom); const QString name = QString::fromStdString(userInfo->name()); server->broadcastUserInfoUpdate(this); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdAddCardArtRule(const Command_AddCardArtRule &cmd, ResponseContainer &) { const QString cardName = QString::fromStdString(cmd.card_name()); const QString cardProviderId = QString::fromStdString(cmd.card_provider_id()); const QString mode = QString::fromStdString(cmd.mode()); if (mode != "ALLOW" && mode != "DENY") { return Response::RespInvalidData; } if (cardName.isEmpty() || cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { return Response::RespInvalidData; } QSqlQuery *q = sqlInterface->prepareQuery("INSERT INTO {prefix}_card_art_name_rules " "(card_name, card_provider_id, mode, reason, created_by) " "VALUES (:name, :provider, :mode, :reason, :uid) " "ON DUPLICATE KEY UPDATE mode=:mode2, reason=:reason2"); q->bindValue(":name", cardName); q->bindValue(":provider", cardProviderId); q->bindValue(":mode", mode); q->bindValue(":mode2", mode); q->bindValue(":reason", QString::fromStdString(cmd.reason())); q->bindValue(":reason2", QString::fromStdString(cmd.reason())); q->bindValue(":uid", userInfo->id()); if (!sqlInterface->execSqlQuery(q)) { return Response::RespInternalError; } return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdRemoveCardArtRule(const Command_RemoveCardArtRule &cmd, ResponseContainer &) { auto cardName = QString::fromStdString(cmd.card_name()); auto cardProviderId = QString::fromStdString(cmd.card_provider_id()); if (cardName.length() > MAX_NAME_LENGTH || cardProviderId.length() > MAX_NAME_LENGTH) { return Response::RespInvalidData; } QSqlQuery *q = sqlInterface->prepareQuery( "DELETE FROM {prefix}_card_art_name_rules WHERE card_name=:name AND card_provider_id=:provider"); q->bindValue(":name", cardName); q->bindValue(":provider", cardProviderId); if (!sqlInterface->execSqlQuery(q)) { return Response::RespInternalError; } return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdListCardArtRules(const Command_ListCardArtRules &, ResponseContainer &rc) { QSqlQuery *q = sqlInterface->prepareQuery( "SELECT card_name, card_provider_id, mode, reason FROM {prefix}_card_art_name_rules"); if (!sqlInterface->execSqlQuery(q)) { return Response::RespInternalError; } auto *re = new Response_ListCardArtRules; while (q->next()) { auto *entry = re->add_entries(); entry->set_card_name(q->value(0).toString().toStdString()); entry->set_card_provider_id(q->value(1).toString().toStdString()); entry->set_mode(q->value(2).toString().toStdString()); entry->set_reason(q->value(3).toString().toStdString()); } rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdAccountPassword(const Command_AccountPassword &cmd, ResponseContainer & /* rc */) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } if (cmd.old_password().length() > MAX_NAME_LENGTH) { return Response::RespWrongPassword; } QString oldPassword = QString::fromStdString(cmd.old_password()); QString newPassword; bool newPasswordNeedsHash = false; if (cmd.has_new_password()) { if (cmd.new_password().length() > MAX_NAME_LENGTH) { return Response::RespContextError; } newPassword = QString::fromStdString(cmd.new_password()); newPasswordNeedsHash = true; if (!isPasswordLongEnough(newPassword.length())) { return Response::RespPasswordTooShort; } } else if (cmd.hashed_new_password().length() > MAX_NAME_LENGTH) { return Response::RespContextError; } else { newPassword = QString::fromStdString(cmd.hashed_new_password()); } QString userName = QString::fromStdString(userInfo->name()); if (!databaseInterface->changeUserPassword(userName, oldPassword, true, newPassword, newPasswordNeedsHash)) { return Response::RespWrongPassword; } databaseInterface->setForcePasswordChange(userName, false); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordRequest(const Command_ForgotPasswordRequest &cmd, ResponseContainer &rc) { const QString userName = nameFromStdString(cmd.user_name()); const QString clientId = nameFromStdString(cmd.clientid()); qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password request from user:" << userName; if (!servatrice->getEnableForgotPassword()) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET_REQUEST", "Server functionality disabled", false); } return Response::RespFunctionNotAllowed; } if (servatrice->getEnableForgotPasswordChallenge()) { Response_ForgotPasswordRequest *re = new Response_ForgotPasswordRequest; re->set_challenge_email(true); rc.setResponseExtension(re); return Response::RespOk; } if (!sqlInterface->userExists(userName)) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET_REQUEST", "User does not exist", false); } return Response::RespFunctionNotAllowed; } return continuePasswordRequest(userName, clientId, rc); } Response::ResponseCode AbstractServerSocketInterface::continuePasswordRequest(const QString &userName, const QString &clientId, ResponseContainer &rc, bool challenged) { if (sqlInterface->doesForgotPasswordExist(userName)) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET_REQUEST", "Request already exists", true); } Response_ForgotPasswordRequest *re = new Response_ForgotPasswordRequest; re->set_challenge_email(false); rc.setResponseExtension(re); return Response::RespOk; } QString banReason; int banTimeRemaining; if (sqlInterface->checkUserIsBanned(this->getAddress(), userName, clientId, banReason, banTimeRemaining)) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET_REQUEST", "User is banned", false); } return Response::RespFunctionNotAllowed; } if (!sqlInterface->addForgotPassword(userName)) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET_REQUEST", "Failed to create password reset", false); } return Response::RespFunctionNotAllowed; } if (servatrice->getEnableForgotPasswordAudit()) { QString details = challenged ? "Request does not exist, challenge passed" : "Request does not exist, challenge not requested"; sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET_REQUEST", details, true); } Response_ForgotPasswordRequest *re = new Response_ForgotPasswordRequest; re->set_challenge_email(false); rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordReset(const Command_ForgotPasswordReset &cmd, ResponseContainer &rc) { Q_UNUSED(rc); QString userName = nameFromStdString(cmd.user_name()); QString clientId = nameFromStdString(cmd.clientid()); qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password reset from user:" << userName; if (!sqlInterface->doesForgotPasswordExist(userName)) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET", "Request does not exist for user", false); } return Response::RespFunctionNotAllowed; } if (!sqlInterface->validateTableColumnStringData("{prefix}_users", "token", userName, nameFromStdString(cmd.token()))) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), userName.simplified(), "PASSWORD_RESET", "Failed token validation", false); } return Response::RespFunctionNotAllowed; } QString password; bool passwordNeedsHash = false; if (cmd.has_new_password()) { if (cmd.new_password().length() > MAX_NAME_LENGTH) { return Response::RespContextError; } password = QString::fromStdString(cmd.new_password()); passwordNeedsHash = true; } else if (cmd.hashed_new_password().length() > MAX_NAME_LENGTH) { return Response::RespContextError; } else { password = QString::fromStdString(cmd.hashed_new_password()); } if (sqlInterface->changeUserPassword(nameFromStdString(cmd.user_name()), password, passwordNeedsHash)) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET", "", true); } sqlInterface->setForcePasswordChange(nameFromStdString(cmd.user_name()), false); sqlInterface->removeForgotPassword(nameFromStdString(cmd.user_name())); return Response::RespOk; } return Response::RespFunctionNotAllowed; } Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordChallenge(const Command_ForgotPasswordChallenge &cmd, ResponseContainer &rc) { const QString userName = nameFromStdString(cmd.user_name()); const QString clientId = nameFromStdString(cmd.clientid()); qCDebug(AbstractServerSocketInterfaceLog) << "Received reset password challenge from user:" << userName; if (!servatrice->getEnableForgotPasswordChallenge()) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET_CHALLENGE", "Feature not enabled", false); } return Response::RespFunctionNotAllowed; } if (!sqlInterface->userExists(userName)) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET_CHALLENGE", "User does not exist", false); } return Response::RespFunctionNotAllowed; } const auto parsedEmailAddress = EmailParser::getParsedEmailAddress(nameFromStdString(cmd.email())); if (!sqlInterface->validateTableColumnStringData("{prefix}_users", "email", userName, parsedEmailAddress)) { if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET_CHALLENGE", "Failed to answer email challenge question", false); } return Response::RespFunctionNotAllowed; } if (servatrice->getEnableForgotPasswordAudit()) { sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(), "PASSWORD_RESET_CHALLENGE", "", true); } return continuePasswordRequest(userName, clientId, rc, true); } Response::ResponseCode AbstractServerSocketInterface::cmdRequestPasswordSalt(const Command_RequestPasswordSalt &cmd, ResponseContainer &rc) { const QString userName = nameFromStdString(cmd.user_name()); QString passwordSalt = sqlInterface->getUserSalt(userName); if (passwordSalt.isEmpty()) { if (server->getRegOnlyServerEnabled()) { return Response::RespRegistrationRequired; } else { // user does not exist but is allowed to log in unregistered without password return Response::RespOk; } } auto *re = new Response_PasswordSalt; re->set_password_salt(passwordSalt.toStdString()); rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReport(const Command_Report &cmd, ResponseContainer & /*rc*/) { if (authState != PasswordRight) { return Response::RespFunctionNotAllowed; } if (!sqlInterface->checkSql()) { return Response::RespInternalError; } if (!cmd.has_reported_user() || !cmd.has_category() || !cmd.has_description()) { return Response::RespInvalidData; } QString reporterName = QString::fromStdString(userInfo->name()); QString reportedUser = nameFromStdString(cmd.reported_user()); QString category = nameFromStdString(cmd.category()); QString description = textFromStdString(cmd.description()); QString chatLog = textTailFromStdString(cmd.chat_log()); if (reportedUser.isEmpty() || category.isEmpty() || description.isEmpty()) { return Response::RespInvalidData; } static const QStringList validCategories = {"cheating", "bug_abuse", "verbal_abuse", "other"}; if (!validCategories.contains(category.toLower())) { return Response::RespInvalidData; } const int maxReportsPerDay = settingsCache->value("reporting/max_reports_per_day", 10).toInt(); if (maxReportsPerDay > 0) { QSqlQuery *countQuery = sqlInterface->prepareQuery("SELECT COUNT(*) FROM {prefix}_reports WHERE reporter_id = :id " "AND created_at >= DATE_SUB(NOW(), INTERVAL 1 DAY)"); countQuery->bindValue(":id", userInfo->id()); if (sqlInterface->execSqlQuery(countQuery) && countQuery->next() && countQuery->value(0).toInt() >= maxReportsPerDay) { return Response::RespTooManyRequests; } } int reportedUserId = -1; QSqlQuery *lookupQuery = sqlInterface->prepareQuery("SELECT id FROM {prefix}_users WHERE name = :name"); lookupQuery->bindValue(":name", reportedUser); if (sqlInterface->execSqlQuery(lookupQuery) && lookupQuery->next()) { reportedUserId = lookupQuery->value(0).toInt(); } if (reportedUserId == -1) { return Response::RespNameNotFound; } int roomId = 0; const int gameId = cmd.game_id(); if (gameId > 0) { QReadLocker roomsLocker(&servatrice->roomsLock); const QMap &rooms = servatrice->getRooms(); for (auto it = rooms.constBegin(); it != rooms.constEnd(); ++it) { QReadLocker gamesLocker(&it.value()->gamesLock); if (it.value()->getGames().contains(gameId)) { roomId = it.key(); break; } } } QSqlQuery *query = sqlInterface->prepareQuery("insert into {prefix}_reports " "(reporter_id, reporter_name, reported_user_id, reported_user_name, " "game_id, room_id, category, description, chat_log, created_at, status) " "values " "(:reporter_id, :reporter_name, :reported_user_id, :reported_user_name, " ":game_id, :room_id, :category, :description, :chat_log, NOW(), 'open')"); query->bindValue(":reporter_id", userInfo->id()); query->bindValue(":reporter_name", reporterName); query->bindValue(":reported_user_id", reportedUserId); query->bindValue(":reported_user_name", reportedUser); query->bindValue(":game_id", gameId > 0 ? gameId : QVariant()); query->bindValue(":room_id", roomId > 0 ? roomId : QVariant()); query->bindValue(":category", category); query->bindValue(":description", description); query->bindValue(":chat_log", chatLog.isEmpty() ? QVariant() : chatLog); if (!sqlInterface->execSqlQuery(query)) { return Response::RespInternalError; } return Response::RespOk; } // ADMIN FUNCTIONS. // Permission is checked by the calling function. Response::ResponseCode AbstractServerSocketInterface::cmdUpdateServerMessage(const Command_UpdateServerMessage & /*cmd*/, ResponseContainer & /*rc*/) { QMetaObject::invokeMethod(server, "updateLoginMessage"); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdShutdownServer(const Command_ShutdownServer &cmd, ResponseContainer & /*rc*/) { QMetaObject::invokeMethod(server, "scheduleShutdown", Q_ARG(QString, textFromStdString(cmd.reason())), Q_ARG(int, cmd.minutes())); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdReloadConfig(const Command_ReloadConfig & /* cmd */, ResponseContainer & /*rc*/) { logDebugMessage("Received admin command: reloading configuration"); settingsCache->sync(); QMetaObject::invokeMethod(server, "setRequiredFeatures", Q_ARG(QString, server->getRequiredFeatures())); return Response::RespOk; } bool AbstractServerSocketInterface::addAdminFlagToUser(const QString &userName, int flag) { QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_users set admin = (admin | :adminlevel) where name = :username"); query->bindValue(":adminlevel", flag); query->bindValue(":username", userName); if (!sqlInterface->execSqlQuery(query)) { logger->logMessage(QString("Failed to promote user %1: %2").arg(userName).arg(query->lastError().text())); return false; } AbstractServerSocketInterface *user = static_cast(server->getUsers().value(userName)); if (user) { Event_NotifyUser event; event.set_type(Event_NotifyUser::PROMOTED); SessionEvent *se = user->prepareSessionEvent(event); user->sendProtocolItem(*se); delete se; } return true; } bool AbstractServerSocketInterface::removeAdminFlagFromUser(const QString &userName, int flag) { QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_users set admin = (admin & ~ :adminlevel) where name = :username"); query->bindValue(":adminlevel", flag); query->bindValue(":username", userName); if (!sqlInterface->execSqlQuery(query)) { logger->logMessage(QString("Failed to demote user %1: %2").arg(userName).arg(query->lastError().text())); return false; } AbstractServerSocketInterface *user = static_cast(server->getUsers().value(userName)); if (user) { Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::DEMOTED); event.set_reason_str("Your moderator and/or judge status has been revoked."); event.set_end_time(QDateTime::currentDateTime().toSecsSinceEpoch()); SessionEvent *se = user->prepareSessionEvent(event); user->sendProtocolItem(*se); delete se; } QMetaObject::invokeMethod(user, "prepareDestroy", Qt::QueuedConnection); return true; } Response::ResponseCode AbstractServerSocketInterface::cmdAdjustMod(const Command_AdjustMod &cmd, ResponseContainer & /*rc*/) { QString userName = nameFromStdString(cmd.user_name()); if (cmd.has_should_be_mod()) { if (cmd.should_be_mod()) { if (!addAdminFlagToUser(userName, 2)) { return Response::RespInternalError; } } else { if (!removeAdminFlagFromUser(userName, 2)) { return Response::RespInternalError; } } } if (cmd.has_should_be_judge()) { if (cmd.should_be_judge()) { if (!addAdminFlagToUser(userName, 4)) { return Response::RespInternalError; } } else { if (!removeAdminFlagFromUser(userName, 4)) { return Response::RespInternalError; } } } return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdGrantReplayAccess(const Command_GrantReplayAccess &cmd, ResponseContainer & /*rc*/) { // Determine if the replay actually exists already auto *replayExistsQuery = sqlInterface->prepareQuery("select count(*) from {prefix}_replays_access where id_game = :idgame"); replayExistsQuery->bindValue(":idgame", cmd.replay_id()); if (!sqlInterface->execSqlQuery(replayExistsQuery)) { return Response::RespInternalError; } if (!replayExistsQuery->next()) { return Response::RespInternalError; } const auto &replayExists = replayExistsQuery->value(0).toInt() > 0; if (!replayExists) { return Response::RespContextError; } // Determine the Moderator's User ID (As it's not apart of client, only username is) auto *getModeratorUserIdQuery = sqlInterface->prepareQuery("select id from {prefix}_users WHERE name = :name"); getModeratorUserIdQuery->bindValue(":name", QString::fromStdString(cmd.moderator_name())); if (!sqlInterface->execSqlQuery(getModeratorUserIdQuery)) { return Response::RespInternalError; } if (!getModeratorUserIdQuery->next()) { return Response::RespInternalError; } const auto &moderator_id = getModeratorUserIdQuery->value(0).toString(); // Grant the Moderator access to the replay auto *grantReplayAccessQuery = sqlInterface->prepareQuery("insert into {prefix}_replays_access (id_game, id_player, replay_name, do_not_hide) " "values(:idgame, :idplayer, :replayname, 0)"); grantReplayAccessQuery->bindValue(":idgame", cmd.replay_id()); grantReplayAccessQuery->bindValue(":idplayer", moderator_id); grantReplayAccessQuery->bindValue(":replayname", "Moderator Access Replay Grant"); if (!sqlInterface->execSqlQuery(grantReplayAccessQuery)) { return Response::RespInternalError; } return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdForceActivateUser(const Command_ForceActivateUser &cmd, ResponseContainer &rc) { // Determine if user exists auto *getUserTokenQuery = sqlInterface->prepareQuery("select token from {prefix}_users WHERE name = :name"); getUserTokenQuery->bindValue(":name", QString::fromStdString(cmd.username_to_activate())); if (!sqlInterface->execSqlQuery(getUserTokenQuery)) { // Internal server error return Response::RespInternalError; } if (!getUserTokenQuery->next()) { // User doesn't exist return Response::RespNameNotFound; } const auto &token = getUserTokenQuery->value(0).toString(); // Add audit log that Moderator activated account on behalf of user const auto &msg = QString("Attempt Force Activation by %1").arg(QString::fromStdString(cmd.moderator_name())); sqlInterface->addAuditRecord(QString::fromStdString(cmd.username_to_activate()), this->getAddress(), "UNKNOWN", "ACTIVATE_ACCOUNT", msg, true); // Build up activation request Command_Activate cmdActivate; cmdActivate.set_user_name(cmd.username_to_activate()); cmdActivate.set_token(token.toStdString()); // Send activation request -- Either User exists or User activated return cmdActivateAccount(cmdActivate, rc); } Response::ResponseCode AbstractServerSocketInterface::cmdGetAdminNotes(const Command_GetAdminNotes &cmd, ResponseContainer &rc) { auto *getAdminNotesQuery = sqlInterface->prepareQuery("select adminnotes from {prefix}_users WHERE name = :name"); getAdminNotesQuery->bindValue(":name", QString::fromStdString(cmd.user_name())); if (!sqlInterface->execSqlQuery(getAdminNotesQuery)) { // Internal server error return Response::RespInternalError; } if (!getAdminNotesQuery->next()) { // User doesn't exist return Response::RespNameNotFound; } const auto &adminNotes = getAdminNotesQuery->value(0).toString(); Response_GetAdminNotes *re = new Response_GetAdminNotes; re->set_user_name(cmd.user_name()); re->set_notes(adminNotes.toStdString()); rc.setResponseExtension(re); return Response::RespOk; } Response::ResponseCode AbstractServerSocketInterface::cmdUpdateAdminNotes(const Command_UpdateAdminNotes &cmd, ResponseContainer & /*rc*/) { auto *updateAdminNotesQuery = sqlInterface->prepareQuery("update {prefix}_users set adminnotes = :adminnotes where name = :name"); updateAdminNotesQuery->bindValue(":adminnotes", QString::fromStdString(cmd.notes())); updateAdminNotesQuery->bindValue(":name", QString::fromStdString(cmd.user_name())); if (!sqlInterface->execSqlQuery(updateAdminNotesQuery)) { return Response::RespInternalError; } if (updateAdminNotesQuery->numRowsAffected() == 0) { return Response::RespNameNotFound; } return Response::RespOk; } TcpServerSocketInterface::TcpServerSocketInterface(Servatrice *_server, Servatrice_DatabaseInterface *_databaseInterface, QObject *parent) : AbstractServerSocketInterface(_server, _databaseInterface, parent), messageInProgress(false), handshakeStarted(false) { socket = new QTcpSocket(this); socket->setSocketOption(QAbstractSocket::LowDelayOption, 1); connect(socket, SIGNAL(readyRead()), this, SLOT(readClient())); connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); connect(socket, SIGNAL(errorOccurred(QAbstractSocket::SocketError)), this, SLOT(catchSocketError(QAbstractSocket::SocketError))); } TcpServerSocketInterface::~TcpServerSocketInterface() { logger->logMessage("TcpServerSocketInterface destructor", this); flushOutputQueue(); } void TcpServerSocketInterface::initConnection(int socketDescriptor) { // Add this object to the server's list of connections before it can receive socket events. // Otherwise, in case a of a socket error, it could be removed from the list before it is added. server->addClient(this); socket->setSocketDescriptor(socketDescriptor); logger->logMessage(QString("Incoming connection: %1").arg(socket->peerAddress().toString()), this); initSessionDeprecated(); } void TcpServerSocketInterface::initSessionDeprecated() { // dirty hack to make v13 client display the correct error message QByteArray buf; buf.append(""); writeToSocket(buf); flushSocket(); } void TcpServerSocketInterface::flushOutputQueue() { QMutexLocker locker(&outputQueueMutex); if (outputQueue.isEmpty()) { return; } int totalBytes = 0; while (!outputQueue.isEmpty()) { ServerMessage item = outputQueue.takeFirst(); locker.unlock(); QByteArray buf; #if GOOGLE_PROTOBUF_VERSION > 3001000 unsigned int size = static_cast(item.ByteSizeLong()); #else unsigned int size = static_cast(item.ByteSize()); #endif buf.resize(size + 4); if (item.SerializeToArray(buf.data() + 4, size)) { buf.data()[3] = (unsigned char)size; buf.data()[2] = (unsigned char)(size >> 8); buf.data()[1] = (unsigned char)(size >> 16); buf.data()[0] = (unsigned char)(size >> 24); // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. writeToSocket(buf); } else { qCWarning(TcpServerSocketInterfaceLog) << "serialisation error!"; } totalBytes += size + 4; locker.relock(); } locker.unlock(); emit incTxBytes(totalBytes); // see above wrt mutex flushSocket(); } void TcpServerSocketInterface::readClient() { QByteArray data = socket->readAll(); servatrice->incRxBytes(data.size()); inputBuffer.append(data); do { if (!messageInProgress) { if (inputBuffer.size() >= 4) { messageLength = (((quint32)(unsigned char)inputBuffer[0]) << 24) + (((quint32)(unsigned char)inputBuffer[1]) << 16) + (((quint32)(unsigned char)inputBuffer[2]) << 8) + ((quint32)(unsigned char)inputBuffer[3]); inputBuffer.remove(0, 4); messageInProgress = true; } else { return; } } if (inputBuffer.size() < messageLength || messageLength < 0) { return; } CommandContainer newCommandContainer; bool ok; try { ok = newCommandContainer.ParseFromArray(inputBuffer.data(), messageLength); } catch (std::exception &e) { qCWarning(TcpServerSocketInterfaceLog) << "Caught std::exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio __FUNCTION__ #else __PRETTY_FUNCTION__ #endif << Qt::endl << "Exception:" << e.what() << Qt::endl << "Message coming from:" << getAddress() << Qt::endl << "Message length:" << messageLength << Qt::endl << "Message content:" << inputBuffer.toHex(); } catch (...) { qCWarning(TcpServerSocketInterfaceLog) << "Unhandled exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio __FUNCTION__ #else __PRETTY_FUNCTION__ #endif << Qt::endl << "Message coming from:" << getAddress(); } inputBuffer.remove(0, messageLength); messageInProgress = false; if (ok) { // dirty hack to make v13 client display the correct error message if (handshakeStarted) { processCommandContainer(newCommandContainer); } else if (!newCommandContainer.has_cmd_id()) { handshakeStarted = true; if (!initTcpSession()) { prepareDestroy(); } } // end of hack } else { qCWarning(TcpServerSocketInterfaceLog) << "parsing error!"; } } while (!inputBuffer.isEmpty()); } bool TcpServerSocketInterface::initTcpSession() { if (!initSession()) { return false; } // limit the number of websocket users based on configuration settings bool enforceUserLimit = settingsCache->value("security/enable_max_user_limit", false).toBool(); if (enforceUserLimit) { int userLimit = settingsCache->value("security/max_users_tcp", 500).toInt(); int playerCount = (server->getTCPUserCount() + 1); if (playerCount > userLimit) { std::cerr << "Max Tcp Users Limit Reached, please increase the max_users_tcp setting." << std::endl; logger->logMessage(QString("Max Tcp Users Limit Reached, please increase the max_users_tcp setting."), this); Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::USER_LIMIT_REACHED); SessionEvent *se = prepareSessionEvent(event); sendProtocolItem(*se); delete se; return false; } } return true; } WebsocketServerSocketInterface::WebsocketServerSocketInterface(Servatrice *_server, Servatrice_DatabaseInterface *_databaseInterface, QObject *parent) : AbstractServerSocketInterface(_server, _databaseInterface, parent), socket(nullptr) { } WebsocketServerSocketInterface::~WebsocketServerSocketInterface() { logger->logMessage("WebsocketServerSocketInterface destructor", this); flushOutputQueue(); } void WebsocketServerSocketInterface::initConnection(void *_socket) { if (_socket == nullptr) { return; } socket = (QWebSocket *)_socket; socket->setParent(this); // https://bugreports.qt.io/browse/QTBUG-70693 socket->setMaxAllowedIncomingMessageSize(1500000); // 1.5MB address = socket->peerAddress(); QByteArray websocketIPHeader = settingsCache->value("server/web_socket_ip_header", "").toByteArray(); if (websocketIPHeader.length() > 0 && socket->request().hasRawHeader(websocketIPHeader)) { QString header(socket->request().rawHeader(websocketIPHeader)); QHostAddress parsed(header); if (!parsed.isNull()) { address = parsed; } } connect(socket, SIGNAL(binaryMessageReceived(const QByteArray &)), this, SLOT(binaryMessageReceived(const QByteArray &))); connect(socket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(catchSocketError(QAbstractSocket::SocketError))); connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected())); // Add this object to the server's list of connections before it can receive socket events. // Otherwise, in case of a socket error, it could be removed from the list before it is added. server->addClient(this); logger->logMessage( QString("Incoming websocket connection: %1 (%2)").arg(address.toString()).arg(socket->peerAddress().toString()), this); if (!initWebsocketSession()) { prepareDestroy(); } } bool WebsocketServerSocketInterface::initWebsocketSession() { if (!initSession()) { return false; } // limit the number of websocket users based on configuration settings bool enforceUserLimit = settingsCache->value("security/enable_max_user_limit", false).toBool(); if (enforceUserLimit) { int userLimit = settingsCache->value("security/max_users_websocket", 500).toInt(); int playerCount = (server->getWebSocketUserCount() + 1); if (playerCount > userLimit) { std::cerr << "Max Websocket Users Limit Reached, please increase the max_users_websocket setting." << std::endl; logger->logMessage( QString("Max Websocket Users Limit Reached, please increase the max_users_websocket setting."), this); Event_ConnectionClosed event; event.set_reason(Event_ConnectionClosed::USER_LIMIT_REACHED); SessionEvent *se = prepareSessionEvent(event); sendProtocolItem(*se); delete se; return false; } } return true; } void WebsocketServerSocketInterface::flushOutputQueue() { QMutexLocker locker(&outputQueueMutex); if (outputQueue.isEmpty()) { return; } qint64 totalBytes = 0; while (!outputQueue.isEmpty()) { ServerMessage item = outputQueue.takeFirst(); locker.unlock(); QByteArray buf; #if GOOGLE_PROTOBUF_VERSION > 3001000 unsigned int size = static_cast(item.ByteSizeLong()); #else unsigned int size = static_cast(item.ByteSize()); #endif buf.resize(size); if (item.SerializeToArray(buf.data(), size)) { // In case socket->write() calls catchSocketError(), the mutex must not be locked during this call. writeToSocket(buf); } else { qCWarning(TcpServerSocketInterfaceLog) << "serialisation error!"; } totalBytes += size; locker.relock(); } locker.unlock(); emit incTxBytes(totalBytes); // see above wrt mutex flushSocket(); } void WebsocketServerSocketInterface::binaryMessageReceived(const QByteArray &message) { servatrice->incRxBytes(message.size()); CommandContainer newCommandContainer; bool ok; try { ok = newCommandContainer.ParseFromArray(message.data(), message.size()); } catch (std::exception &e) { qCWarning(WebsocketServerSocketInterfaceLog) << "Caught std::exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio __FUNCTION__ #else __PRETTY_FUNCTION__ #endif << Qt::endl << "Exception:" << e.what() << Qt::endl << "Message coming from:" << getAddress() << Qt::endl << "Message length:" << message.size() << Qt::endl << "Message content:" << message.toHex(); } catch (...) { qCWarning(WebsocketServerSocketInterfaceLog) << "Unhandled exception in" << __FILE__ << __LINE__ << #ifdef _MSC_VER // Visual Studio __FUNCTION__ #else __PRETTY_FUNCTION__ #endif << Qt::endl << "Message coming from:" << getAddress(); } if (ok) { processCommandContainer(newCommandContainer); } else { qCWarning(WebsocketServerSocketInterfaceLog) << "parsing error!"; } } bool AbstractServerSocketInterface::isPasswordLongEnough(const int passwordLength) { return passwordLength >= servatrice->getMinPasswordLength(); }