mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-09-21 09:05:10 -07:00
* [Server] Add deck share links and public deck visibility * Address server review comments for deck share links * Document transaction teardown in deck share rollback paths * Address second round of deck share review comments --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
4427 lines
183 KiB
C++
4427 lines
183 KiB
C++
/***************************************************************************
|
|
* 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 "deck_tag_serialization.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 <QDateTime>
|
|
#include <QDebug>
|
|
#include <QElapsedTimer>
|
|
#include <QHostAddress>
|
|
#include <QJsonDocument>
|
|
#include <QJsonObject>
|
|
#include <QLoggingCategory>
|
|
#include <QRandomGenerator>
|
|
#include <QRegularExpression>
|
|
#include <QSqlError>
|
|
#include <QSqlQuery>
|
|
#include <QString>
|
|
#include <algorithm>
|
|
#include <game/server_player.h>
|
|
#include <google/protobuf/descriptor.h>
|
|
#include <iostream>
|
|
#include <libcockatrice/deck_list/deck_list.h>
|
|
#include <libcockatrice/protocol/get_pb_extension.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_del.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_del_dir.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_download.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_download_public.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_list_other_user.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_new_dir.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_set_visibility.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_share_create.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_share_download.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_share_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_share_list_mine.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_share_remove.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_deck_upload.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_get_server_stats.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_replay_delete_match.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_replay_download.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_replay_download_by_game_id.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_replay_get_code.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_replay_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_replay_modify_match.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_replay_submit_code.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_report.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_report_add_comment.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_report_assign.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_report_details.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_report_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_report_my_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_report_resolve.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_report_stats.pb.h>
|
|
#include <libcockatrice/protocol/pb/command_report_user_info.pb.h>
|
|
#include <libcockatrice/protocol/pb/commands.pb.h>
|
|
#include <libcockatrice/protocol/pb/event_add_to_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/event_connection_closed.pb.h>
|
|
#include <libcockatrice/protocol/pb/event_notify_user.pb.h>
|
|
#include <libcockatrice/protocol/pb/event_remove_from_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/event_replay_added.pb.h>
|
|
#include <libcockatrice/protocol/pb/event_server_identification.pb.h>
|
|
#include <libcockatrice/protocol/pb/event_server_message.pb.h>
|
|
#include <libcockatrice/protocol/pb/event_user_joined.pb.h>
|
|
#include <libcockatrice/protocol/pb/event_user_message.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_ban_history.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_card_art_rule_entry.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_deck_download.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_deck_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_deck_share_create.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_deck_share_download.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_deck_share_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_deck_share_list_mine.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_deck_upload.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_forgotpasswordrequest.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_get_admin_notes.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_get_server_stats.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_moderator_last_logins.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_password_salt.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_register.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_remove_user_avatar.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_replay_download.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_replay_download_by_game_id.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_replay_get_code.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_replay_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_report_details.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_report_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_report_my_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_report_stats.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_report_user_info.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_reset_user_password.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_user_alts.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_user_sessions.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_viewlog_history.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_warn_history.pb.h>
|
|
#include <libcockatrice/protocol/pb/response_warn_list.pb.h>
|
|
#include <libcockatrice/protocol/pb/serverinfo_ban.pb.h>
|
|
#include <libcockatrice/protocol/pb/serverinfo_chat_message.pb.h>
|
|
#include <libcockatrice/protocol/pb/serverinfo_deckstorage.pb.h>
|
|
#include <libcockatrice/protocol/pb/serverinfo_moderator_login.pb.h>
|
|
#include <libcockatrice/protocol/pb/serverinfo_replay.pb.h>
|
|
#include <libcockatrice/protocol/pb/serverinfo_user.pb.h>
|
|
#include <libcockatrice/protocol/pb/serverinfo_user_alt.pb.h>
|
|
#include <libcockatrice/protocol/pb/serverinfo_user_session.pb.h>
|
|
#include <libcockatrice/utility/passwordhasher.h>
|
|
#include <libcockatrice/utility/string_limits.h>
|
|
#include <libcockatrice/utility/warning_categories.h>
|
|
#include <server_response_containers.h>
|
|
#include <server_room.h>
|
|
#include <string>
|
|
|
|
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<Servatrice_DatabaseInterface *>(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);
|
|
}
|
|
|
|
void AbstractServerSocketInterface::processCommandContainer(const CommandContainer &cont)
|
|
{
|
|
QElapsedTimer timer;
|
|
timer.start();
|
|
Server_ProtocolHandler::processCommandContainer(cont);
|
|
const qint64 elapsedMs = timer.nsecsElapsed() / 1000000;
|
|
|
|
// The base dispatch is an if/else-if chain — at most one family is
|
|
// actually processed. Recording every family in the container would
|
|
// let an unauthenticated client stampforge developer/moderator/admin
|
|
// samples by batching them alongside a session command the server
|
|
// actually runs. Mirror the base's selection and skip entirely when
|
|
// deleted or when no family matched.
|
|
if (deleted) {
|
|
return;
|
|
}
|
|
|
|
// When getPbExtension returns -1 (no extension set) and the kind is
|
|
// non-zero, typeIdFor wraps into the previous kind's range instead of
|
|
// hitting the typeId < 0 guard in observeCommand. Skip such entries.
|
|
int kind = -1;
|
|
if (cont.game_command_size()) {
|
|
kind = 2;
|
|
} else if (cont.room_command_size()) {
|
|
kind = 1;
|
|
} else if (cont.session_command_size()) {
|
|
kind = 0;
|
|
} else if (cont.moderator_command_size()) {
|
|
kind = 3;
|
|
} else if (cont.admin_command_size()) {
|
|
kind = 4;
|
|
} else if (cont.developer_command_size()) {
|
|
kind = 5;
|
|
}
|
|
|
|
if (kind >= 0) {
|
|
auto recordDispatched = [&](int familyKind, const auto &cmds) {
|
|
for (const auto &cmd : cmds) {
|
|
const int ext = getPbExtension(cmd);
|
|
if (ext >= 0) {
|
|
servatrice->getMetricsRegistry().observeCommand(MetricsRegistry::typeIdFor(familyKind, ext),
|
|
elapsedMs);
|
|
}
|
|
}
|
|
};
|
|
|
|
if (kind == 0) {
|
|
recordDispatched(kind, cont.session_command());
|
|
} else if (kind == 1) {
|
|
recordDispatched(kind, cont.room_command());
|
|
} else if (kind == 2) {
|
|
recordDispatched(kind, cont.game_command());
|
|
} else if (kind == 3) {
|
|
recordDispatched(kind, cont.moderator_command());
|
|
} else if (kind == 4) {
|
|
recordDispatched(kind, cont.admin_command());
|
|
} else {
|
|
recordDispatched(kind, cont.developer_command());
|
|
}
|
|
}
|
|
|
|
const int slowCommandMs = servatrice->getMetricsSlowCommandMs();
|
|
if (slowCommandMs > 0 && elapsedMs >= slowCommandMs) {
|
|
const ServerInfo_User *info = getUserInfo();
|
|
const QString user = authState == PasswordRight && info ? QString::fromStdString(info->name())
|
|
: QStringLiteral("unauthenticated");
|
|
qCWarning(AbstractServerSocketInterfaceLog) << "slow command container from" << user << "processed in"
|
|
<< elapsedMs << "ms (" << cont.ByteSizeLong() << "bytes)";
|
|
}
|
|
}
|
|
|
|
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_LIST_OTHER_USER:
|
|
return cmdDeckListOtherUser(cmd.GetExtension(Command_DeckListOtherUser::ext), rc);
|
|
case SessionCommand::DECK_SET_VISIBILITY:
|
|
return cmdDeckSetVisibility(cmd.GetExtension(Command_DeckSetVisibility::ext), rc);
|
|
case SessionCommand::DECK_DOWNLOAD_PUBLIC:
|
|
return cmdDeckDownloadPublic(cmd.GetExtension(Command_DeckDownloadPublic::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::DECK_SHARE_CREATE:
|
|
return cmdDeckShareCreate(cmd.GetExtension(Command_DeckShareCreate::ext), rc);
|
|
case SessionCommand::DECK_SHARE_LIST:
|
|
return cmdDeckShareList(cmd.GetExtension(Command_DeckShareList::ext), rc);
|
|
case SessionCommand::DECK_SHARE_LIST_MINE:
|
|
return cmdDeckShareListMine(cmd.GetExtension(Command_DeckShareListMine::ext), rc);
|
|
case SessionCommand::DECK_SHARE_REMOVE:
|
|
return cmdDeckShareRemove(cmd.GetExtension(Command_DeckShareRemove::ext), rc);
|
|
case SessionCommand::DECK_SHARE_DOWNLOAD:
|
|
return cmdDeckShareDownload(cmd.GetExtension(Command_DeckShareDownload::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, true);
|
|
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;
|
|
}
|
|
}
|
|
|
|
// DEVELOPER FUNCTIONS.
|
|
// Permission is checked by processDeveloperCommandContainer. Only stats-style
|
|
// queries live here, never community moderation or server administration.
|
|
Response::ResponseCode AbstractServerSocketInterface::processExtendedDeveloperCommand(int cmdType,
|
|
const DeveloperCommand &cmd,
|
|
ResponseContainer &rc)
|
|
{
|
|
switch ((DeveloperCommand::DeveloperCommandType)cmdType) {
|
|
case DeveloperCommand::GET_SERVER_STATS:
|
|
return cmdGetServerStats(cmd.GetExtension(Command_GetServerStats::ext), rc);
|
|
case DeveloperCommand::VIEWLOG_HISTORY: {
|
|
// Same query as the moderator log view, carried by the developer
|
|
// command family, but narrows out private chats and sender IPs.
|
|
return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::dev_ext), rc, false);
|
|
}
|
|
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,
|
|
int userId,
|
|
bool inheritedPublic,
|
|
bool publicOnly)
|
|
{
|
|
QSqlQuery *query = sqlInterface->prepareQuery("select id, name, is_public from {prefix}_decklist_folders where "
|
|
"id_parent = :id_parent and id_user = :id_user");
|
|
query->bindValue(":id_parent", folderId);
|
|
query->bindValue(":id_user", userId);
|
|
if (!sqlInterface->execSqlQuery(query)) {
|
|
return false;
|
|
}
|
|
|
|
QList<std::pair<int, std::pair<QString, bool>>> folderRows;
|
|
while (query->next()) {
|
|
folderRows.append({query->value(0).toInt(), {query->value(1).toString(), query->value(2).toBool()}});
|
|
}
|
|
std::sort(folderRows.begin(), folderRows.end(), [](const auto &a, const auto &b) { return a.first < b.first; });
|
|
|
|
for (const auto &[folderIdValue, folderInfo] : folderRows) {
|
|
const QString name = folderInfo.first;
|
|
const bool ownPublic = folderInfo.second;
|
|
const bool effectivePublic = inheritedPublic || ownPublic;
|
|
|
|
ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items();
|
|
newItem->set_id(folderIdValue);
|
|
newItem->set_name(name.toStdString());
|
|
newItem->mutable_folder()->set_is_public(ownPublic);
|
|
|
|
if (!deckListHelper(newItem->id(), newItem->mutable_folder(), userId, effectivePublic, publicOnly)) {
|
|
return false;
|
|
}
|
|
|
|
if (publicOnly && !effectivePublic && newItem->mutable_folder()->items_size() == 0) {
|
|
folder->mutable_items()->RemoveLast();
|
|
}
|
|
}
|
|
|
|
query = sqlInterface->prepareQuery("select id, name, upload_time, is_public, banner_card_name, "
|
|
"banner_card_provider, color_identity, tags from {prefix}_decklist_files where "
|
|
"id_folder = :id_folder and id_user = :id_user");
|
|
query->bindValue(":id_folder", folderId);
|
|
query->bindValue(":id_user", userId);
|
|
if (!sqlInterface->execSqlQuery(query)) {
|
|
return false;
|
|
}
|
|
|
|
while (query->next()) {
|
|
const bool ownPublic = query->value(3).toBool();
|
|
if (publicOnly && !(inheritedPublic || ownPublic)) {
|
|
continue;
|
|
}
|
|
|
|
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());
|
|
newFile->set_is_public(ownPublic);
|
|
newFile->set_banner_card_name(query->value(4).toString().toStdString());
|
|
newFile->set_banner_card_provider(query->value(5).toString().toStdString());
|
|
newFile->set_color_identity(query->value(6).toString().toStdString());
|
|
for (const QString &tag : deserializeDeckTags(query->value(7).toString())) {
|
|
newFile->add_tags(tag.toStdString());
|
|
}
|
|
}
|
|
|
|
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, userInfo->id(), false, false)) {
|
|
return Response::RespContextError;
|
|
}
|
|
|
|
rc.setResponseExtension(re);
|
|
return Response::RespOk;
|
|
}
|
|
|
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckListOtherUser(const Command_DeckListOtherUser &cmd,
|
|
ResponseContainer &rc)
|
|
{
|
|
if (authState != PasswordRight) {
|
|
return Response::RespFunctionNotAllowed;
|
|
}
|
|
|
|
sqlInterface->checkSql();
|
|
|
|
const QString userName = nameFromStdString(cmd.user_name());
|
|
const int userId = sqlInterface->getUserIdInDB(userName);
|
|
if (userId == -1) {
|
|
return Response::RespNameNotFound;
|
|
}
|
|
|
|
Response_DeckList *re = new Response_DeckList;
|
|
ServerInfo_DeckStorage_Folder *root = re->mutable_root();
|
|
|
|
if (!deckListHelper(0, root, userId, false, true)) {
|
|
return Response::RespContextError;
|
|
}
|
|
|
|
rc.setResponseExtension(re);
|
|
return Response::RespOk;
|
|
}
|
|
|
|
int AbstractServerSocketInterface::getDeckOwnerId(int deckId)
|
|
{
|
|
QSqlQuery *query = sqlInterface->prepareQuery("select id_user from {prefix}_decklist_files where id = :id");
|
|
query->bindValue(":id", deckId);
|
|
if (!sqlInterface->execSqlQuery(query)) {
|
|
return -1;
|
|
}
|
|
if (!query->next()) {
|
|
return -1;
|
|
}
|
|
return query->value(0).toInt();
|
|
}
|
|
|
|
bool AbstractServerSocketInterface::isDeckEffectivelyPublic(int deckId)
|
|
{
|
|
QSqlQuery *query =
|
|
sqlInterface->prepareQuery("select is_public, id_folder from {prefix}_decklist_files where id = :id");
|
|
query->bindValue(":id", deckId);
|
|
if (!sqlInterface->execSqlQuery(query)) {
|
|
return false;
|
|
}
|
|
if (!query->next()) {
|
|
return false;
|
|
}
|
|
if (query->value(0).toBool()) {
|
|
return true;
|
|
}
|
|
|
|
int folderId = query->value(1).toInt();
|
|
int guard = 0;
|
|
while (folderId != 0 && guard < 100) {
|
|
QSqlQuery *folderQuery =
|
|
sqlInterface->prepareQuery("select is_public, id_parent from {prefix}_decklist_folders where id = :id");
|
|
folderQuery->bindValue(":id", folderId);
|
|
if (!sqlInterface->execSqlQuery(folderQuery)) {
|
|
return false;
|
|
}
|
|
if (!folderQuery->next()) {
|
|
return false;
|
|
}
|
|
if (folderQuery->value(0).toBool()) {
|
|
return true;
|
|
}
|
|
folderId = folderQuery->value(1).toInt();
|
|
++guard;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckSetVisibility(const Command_DeckSetVisibility &cmd,
|
|
ResponseContainer & /*rc*/)
|
|
{
|
|
if (authState != PasswordRight) {
|
|
return Response::RespFunctionNotAllowed;
|
|
}
|
|
|
|
sqlInterface->checkSql();
|
|
|
|
if (cmd.has_deck_id()) {
|
|
QSqlQuery *query =
|
|
sqlInterface->prepareQuery("select 1 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("update {prefix}_decklist_files set is_public = :is_public where id = :id "
|
|
"and id_user = :id_user");
|
|
query->bindValue(":is_public", cmd.is_public() ? 1 : 0);
|
|
query->bindValue(":id", cmd.deck_id());
|
|
query->bindValue(":id_user", userInfo->id());
|
|
if (!sqlInterface->execSqlQuery(query)) {
|
|
return Response::RespContextError;
|
|
}
|
|
} else if (cmd.has_folder_path()) {
|
|
const int folderId = getDeckPathId(nameFromStdString(cmd.folder_path()));
|
|
if (folderId == -1 || folderId == 0) {
|
|
return Response::RespNameNotFound;
|
|
}
|
|
|
|
QSqlQuery *query =
|
|
sqlInterface->prepareQuery("update {prefix}_decklist_folders set is_public = :is_public where id = :id "
|
|
"and id_user = :id_user");
|
|
query->bindValue(":is_public", cmd.is_public() ? 1 : 0);
|
|
query->bindValue(":id", folderId);
|
|
query->bindValue(":id_user", userInfo->id());
|
|
if (!sqlInterface->execSqlQuery(query)) {
|
|
return Response::RespContextError;
|
|
}
|
|
} else {
|
|
return Response::RespInvalidData;
|
|
}
|
|
|
|
return Response::RespOk;
|
|
}
|
|
|
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownloadPublic(const Command_DeckDownloadPublic &cmd,
|
|
ResponseContainer &rc)
|
|
{
|
|
if (authState != PasswordRight) {
|
|
return Response::RespFunctionNotAllowed;
|
|
}
|
|
|
|
sqlInterface->checkSql();
|
|
|
|
const int deckId = cmd.deck_id();
|
|
const int ownerId = getDeckOwnerId(deckId);
|
|
if (ownerId == -1 || !isDeckEffectivelyPublic(deckId)) {
|
|
return Response::RespNameNotFound;
|
|
}
|
|
|
|
DeckList *deck;
|
|
try {
|
|
deck = sqlInterface->getDeckFromDatabase(deckId, ownerId);
|
|
} 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::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<AbstractServerSocketInterface *>(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;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
/** @brief Keeps only the WUBRG colors from a color identity string, deduplicated. */
|
|
QString sanitizeColorIdentity(const QString &colorIdentity)
|
|
{
|
|
QString sanitized;
|
|
for (const QChar &color : colorIdentity) {
|
|
const QChar upper = color.toUpper();
|
|
if (QStringLiteral("WUBRG").contains(upper) && !sanitized.contains(upper)) {
|
|
sanitized.append(upper);
|
|
}
|
|
}
|
|
return sanitized;
|
|
}
|
|
} // namespace
|
|
|
|
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";
|
|
}
|
|
|
|
// The server derives the banner card and tags from the deck itself. Only the
|
|
// color identity must come from the client, since the server has no card
|
|
// database to compute it. All values are bounded to the column sizes.
|
|
const QString bannerCardName = deck.getBannerCard().name.left(255);
|
|
const QString bannerCardProvider = deck.getBannerCard().providerId.left(32);
|
|
const QString tagsJson = serializeDeckTags(deck.getTags());
|
|
const QString colorIdentity = sanitizeColorIdentity(nameFromStdString(cmd.color_identity()));
|
|
|
|
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, is_public, banner_card_name, banner_card_provider, color_identity, "
|
|
"tags) values(:id_folder, :id_user, :name, NOW(), :content, :is_public, "
|
|
":banner_card_name, :banner_card_provider, :color_identity, :tags)");
|
|
query->bindValue(":id_folder", folderId);
|
|
query->bindValue(":id_user", userInfo->id());
|
|
query->bindValue(":name", deckName);
|
|
query->bindValue(":content", deckStr);
|
|
query->bindValue(":is_public", cmd.has_is_public() && cmd.is_public() ? 1 : 0);
|
|
query->bindValue(":banner_card_name", bannerCardName);
|
|
query->bindValue(":banner_card_provider", bannerCardProvider);
|
|
query->bindValue(":color_identity", colorIdentity);
|
|
query->bindValue(":tags", tagsJson);
|
|
if (!sqlInterface->execSqlQuery(query)) {
|
|
return Response::RespContextError;
|
|
}
|
|
|
|
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());
|
|
fileInfo->mutable_file()->set_is_public(cmd.has_is_public() && cmd.is_public());
|
|
rc.setResponseExtension(re);
|
|
} else if (cmd.has_deck_id()) {
|
|
QString updateQuery = "update {prefix}_decklist_files set name=:name, upload_time=NOW(), content=:content, "
|
|
"banner_card_name=:banner_card_name, banner_card_provider=:banner_card_provider, "
|
|
"color_identity=:color_identity, tags=:tags";
|
|
if (cmd.has_is_public()) {
|
|
updateQuery += ", is_public=:is_public";
|
|
}
|
|
updateQuery += " where id = :id_deck and id_user = :id_user";
|
|
|
|
QSqlQuery *query = sqlInterface->prepareQuery(updateQuery);
|
|
query->bindValue(":id_deck", cmd.deck_id());
|
|
query->bindValue(":id_user", userInfo->id());
|
|
query->bindValue(":name", deckName);
|
|
query->bindValue(":content", deckStr);
|
|
query->bindValue(":banner_card_name", bannerCardName);
|
|
query->bindValue(":banner_card_provider", bannerCardProvider);
|
|
query->bindValue(":color_identity", colorIdentity);
|
|
query->bindValue(":tags", tagsJson);
|
|
if (cmd.has_is_public()) {
|
|
query->bindValue(":is_public", cmd.is_public() ? 1 : 0);
|
|
}
|
|
if (!sqlInterface->execSqlQuery(query)) {
|
|
return Response::RespContextError;
|
|
}
|
|
|
|
if (query->numRowsAffected() == 0) {
|
|
return Response::RespNameNotFound;
|
|
}
|
|
|
|
QSqlQuery *visibilityQuery =
|
|
sqlInterface->prepareQuery("select is_public from {prefix}_decklist_files where id = :id and "
|
|
"id_user = :id_user");
|
|
visibilityQuery->bindValue(":id", cmd.deck_id());
|
|
visibilityQuery->bindValue(":id_user", userInfo->id());
|
|
if (!sqlInterface->execSqlQuery(visibilityQuery)) {
|
|
return Response::RespContextError;
|
|
}
|
|
const bool isPublic = visibilityQuery->next() && visibilityQuery->value(0).toBool();
|
|
|
|
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());
|
|
fileInfo->mutable_file()->set_is_public(isPublic);
|
|
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;
|
|
}
|
|
|
|
namespace
|
|
{
|
|
/** @brief Builds a cryptographically random, URL-safe share token. */
|
|
QString generateShareToken()
|
|
{
|
|
QByteArray bytes(32, Qt::Uninitialized);
|
|
QRandomGenerator::system()->fillRange(reinterpret_cast<quint32 *>(bytes.data()), bytes.size() / sizeof(quint32));
|
|
return QString::fromLatin1(bytes.toBase64(QByteArray::Base64UrlEncoding | QByteArray::OmitTrailingEquals));
|
|
}
|
|
|
|
/** @brief Extracts the share metadata for a deck, materializing its content. */
|
|
DeckShareItemRecord makeShareItemFromDeck(const DeckList &deck, const QString &colorIdentity)
|
|
{
|
|
DeckShareItemRecord item;
|
|
item.name = deck.getName();
|
|
if (item.name.isEmpty()) {
|
|
item.name = "Unnamed deck";
|
|
}
|
|
item.tags = deck.getTags();
|
|
item.bannerCard = deck.getBannerCard().name;
|
|
item.gameFormat = deck.getGameFormat();
|
|
item.colorIdentity = sanitizeColorIdentity(colorIdentity);
|
|
item.content = deck.writeToString_Native();
|
|
return item;
|
|
}
|
|
} // namespace
|
|
|
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareCreate(const Command_DeckShareCreate &cmd,
|
|
ResponseContainer &rc)
|
|
{
|
|
if (authState != PasswordRight) {
|
|
return Response::RespFunctionNotAllowed;
|
|
}
|
|
|
|
sqlInterface->checkSql();
|
|
|
|
const int maxItems = servatrice->getDeckShareMaxDecksPerShare();
|
|
if (maxItems > 0 && cmd.items_size() > maxItems) {
|
|
return Response::RespInvalidData;
|
|
}
|
|
|
|
// Per-user rate limit so share links cannot be used to build an unbounded
|
|
// word-of-mouth leak of public decks.
|
|
const int maxSharesPerDay = servatrice->getDeckShareMaxSharesPerDay();
|
|
if (maxSharesPerDay > 0) {
|
|
QSqlQuery *countQuery = sqlInterface->prepareQuery("select count(*) from {prefix}_deck_share where "
|
|
"created_by = :created_by and created_at >= "
|
|
"DATE_SUB(NOW(), INTERVAL 1 DAY)");
|
|
countQuery->bindValue(":created_by", userInfo->id());
|
|
if (!sqlInterface->execSqlQuery(countQuery) || !countQuery->next()) {
|
|
return Response::RespContextError;
|
|
}
|
|
if (countQuery->value(0).toInt() >= maxSharesPerDay) {
|
|
return Response::RespTooManyRequests;
|
|
}
|
|
}
|
|
|
|
QList<DeckShareItemRecord> items;
|
|
if (cmd.items_size() > 0) {
|
|
for (const DeckShareItem &shareItem : cmd.items()) {
|
|
if (shareItem.has_deck_list()) {
|
|
DeckList deck;
|
|
if (!deck.loadFromString_Native(fileFromStdString(shareItem.deck_list()))) {
|
|
return Response::RespContextError;
|
|
}
|
|
items.append(makeShareItemFromDeck(deck, nameFromStdString(shareItem.color_identity())));
|
|
} else if (shareItem.has_deck_id()) {
|
|
DeckList *deck;
|
|
try {
|
|
deck = sqlInterface->getDeckFromDatabase(shareItem.deck_id(), userInfo->id());
|
|
} catch (Response::ResponseCode &r) {
|
|
return r;
|
|
}
|
|
items.append(makeShareItemFromDeck(*deck, nameFromStdString(shareItem.color_identity())));
|
|
delete deck;
|
|
} else {
|
|
return Response::RespInvalidData;
|
|
}
|
|
}
|
|
} else if (cmd.has_folder_path()) {
|
|
const int folderId = getDeckPathId(nameFromStdString(cmd.folder_path()));
|
|
if (folderId == -1) {
|
|
return Response::RespNameNotFound;
|
|
}
|
|
|
|
// Drain the deck list before resolving each deck: getDeckFromDatabase
|
|
// issues its own query on the same cached statement set.
|
|
QSqlQuery *query =
|
|
sqlInterface->prepareQuery("select id, color_identity 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 Response::RespContextError;
|
|
}
|
|
QList<std::pair<int, QString>> deckRows;
|
|
while (query->next()) {
|
|
deckRows.append({query->value(0).toInt(), query->value(1).toString()});
|
|
}
|
|
for (const auto &[deckId, colorIdentity] : deckRows) {
|
|
DeckList *deck;
|
|
try {
|
|
deck = sqlInterface->getDeckFromDatabase(deckId, userInfo->id());
|
|
} catch (Response::ResponseCode &r) {
|
|
return r;
|
|
}
|
|
items.append(makeShareItemFromDeck(*deck, colorIdentity));
|
|
delete deck;
|
|
}
|
|
} else {
|
|
return Response::RespInvalidData;
|
|
}
|
|
|
|
if (items.isEmpty() || (maxItems > 0 && items.size() > maxItems)) {
|
|
return Response::RespInvalidData;
|
|
}
|
|
|
|
QString shareName = nameFromStdString(cmd.name());
|
|
if (shareName.isEmpty()) {
|
|
shareName = "Shared decks";
|
|
}
|
|
|
|
const QString token = generateShareToken();
|
|
qint64 expiresAt = 0;
|
|
if (!sqlInterface->createDeckShare(token, shareName, userInfo->id(), items, servatrice->getDeckShareExpiryDays(),
|
|
expiresAt)) {
|
|
return Response::RespInvalidData;
|
|
}
|
|
|
|
Response_DeckShareCreate *re = new Response_DeckShareCreate;
|
|
re->set_token(token.toStdString());
|
|
re->set_expires_at(expiresAt);
|
|
re->set_item_count(items.size());
|
|
rc.setResponseExtension(re);
|
|
|
|
return Response::RespOk;
|
|
}
|
|
|
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareListMine(const Command_DeckShareListMine & /*cmd*/,
|
|
ResponseContainer &rc)
|
|
{
|
|
if (authState != PasswordRight) {
|
|
return Response::RespFunctionNotAllowed;
|
|
}
|
|
|
|
sqlInterface->checkSql();
|
|
|
|
QList<DeckShareSummaryRecord> shares;
|
|
if (!sqlInterface->getDeckSharesForUser(userInfo->id(), shares)) {
|
|
return Response::RespContextError;
|
|
}
|
|
|
|
Response_DeckShareListMine *re = new Response_DeckShareListMine;
|
|
for (const DeckShareSummaryRecord &share : shares) {
|
|
ServerInfo_DeckShareSummary *summary = re->add_shares();
|
|
summary->set_id(share.id);
|
|
summary->set_name(share.name.toStdString());
|
|
summary->set_creation_time(share.creationTime);
|
|
summary->set_expires_at(share.expiresAt);
|
|
summary->set_item_count(share.itemCount);
|
|
}
|
|
rc.setResponseExtension(re);
|
|
|
|
return Response::RespOk;
|
|
}
|
|
|
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareRemove(const Command_DeckShareRemove &cmd,
|
|
ResponseContainer & /*rc*/)
|
|
{
|
|
if (authState != PasswordRight) {
|
|
return Response::RespFunctionNotAllowed;
|
|
}
|
|
|
|
if (!cmd.has_share_id()) {
|
|
return Response::RespInvalidData;
|
|
}
|
|
|
|
sqlInterface->checkSql();
|
|
|
|
if (!sqlInterface->deleteDeckShare(cmd.share_id(), userInfo->id())) {
|
|
return Response::RespNameNotFound;
|
|
}
|
|
|
|
return Response::RespOk;
|
|
}
|
|
|
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareList(const Command_DeckShareList &cmd,
|
|
ResponseContainer &rc)
|
|
{
|
|
if (authState != PasswordRight) {
|
|
return Response::RespFunctionNotAllowed;
|
|
}
|
|
|
|
sqlInterface->checkSql();
|
|
|
|
QString name;
|
|
qint64 expiresAt = 0;
|
|
QList<DeckShareItemRecord> items;
|
|
if (!sqlInterface->getDeckShareList(nameFromStdString(cmd.token()), name, expiresAt, items)) {
|
|
return Response::RespNameNotFound;
|
|
}
|
|
|
|
Response_DeckShareList *re = new Response_DeckShareList;
|
|
re->set_name(name.toStdString());
|
|
re->set_expires_at(expiresAt);
|
|
for (const DeckShareItemRecord &item : items) {
|
|
ServerInfo_DeckShareItem *itemInfo = re->add_items();
|
|
itemInfo->set_id(item.id);
|
|
itemInfo->set_name(item.name.toStdString());
|
|
for (const QString &tag : item.tags) {
|
|
itemInfo->add_tags(tag.toStdString());
|
|
}
|
|
itemInfo->set_banner_card(item.bannerCard.toStdString());
|
|
itemInfo->set_game_format(item.gameFormat.toStdString());
|
|
itemInfo->set_color_identity(item.colorIdentity.toStdString());
|
|
}
|
|
rc.setResponseExtension(re);
|
|
|
|
return Response::RespOk;
|
|
}
|
|
|
|
Response::ResponseCode AbstractServerSocketInterface::cmdDeckShareDownload(const Command_DeckShareDownload &cmd,
|
|
ResponseContainer &rc)
|
|
{
|
|
if (authState != PasswordRight) {
|
|
return Response::RespFunctionNotAllowed;
|
|
}
|
|
|
|
sqlInterface->checkSql();
|
|
|
|
QString content;
|
|
if (!sqlInterface->getDeckShareItem(nameFromStdString(cmd.token()), cmd.item_id(), content)) {
|
|
return Response::RespNameNotFound;
|
|
}
|
|
|
|
Response_DeckShareDownload *re = new Response_DeckShareDownload;
|
|
re->set_deck(content.toStdString());
|
|
rc.setResponseExtension(re);
|
|
|
|
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 <game-id>-<hash>
|
|
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 <game-id>-<hash>
|
|
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,
|
|
bool allowPrivateChat)
|
|
{
|
|
|
|
QList<ServerInfo_ChatMessage> messageList;
|
|
QString userName = nameFromStdString(cmd.user_name());
|
|
QString ipAddress = allowPrivateChat ? nameFromStdString(cmd.ip_address()) : QString();
|
|
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" && allowPrivateChat) {
|
|
chatType = true;
|
|
}
|
|
}
|
|
|
|
// For callers that must not see private conversations, never leave the
|
|
// target-type filter empty: if the request only asked for "chat" (or for
|
|
// nothing at all) the query below would carry no target_type restriction
|
|
// and would return every row, private messages included. Fall back to the
|
|
// game/room diagnostics the caller is allowed to see.
|
|
if (!allowPrivateChat && !gameType && !roomType) {
|
|
gameType = true;
|
|
roomType = true;
|
|
}
|
|
|
|
int dateRange = cmd.date_range();
|
|
int maximumResults = cmd.maximum_results();
|
|
|
|
Response_ViewLogHistory *re = new Response_ViewLogHistory;
|
|
|
|
if (servatrice->getEnableLogQuery()) {
|
|
QListIterator<ServerInfo_ChatMessage> messageIterator(sqlInterface->getMessageLogHistory(
|
|
userName, ipAddress, gameName, gameID, message, chatType, gameType, roomType, dateRange, maximumResults));
|
|
while (messageIterator.hasNext()) {
|
|
ServerInfo_ChatMessage chatMessage = messageIterator.next();
|
|
if (!allowPrivateChat) {
|
|
chatMessage.clear_sender_ip();
|
|
}
|
|
re->add_log_message()->CopyFrom(chatMessage);
|
|
}
|
|
} 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<ServerInfo_ChatMessage> 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<ServerInfo_Ban> banList;
|
|
QString userName = nameFromStdString(cmd.user_name());
|
|
|
|
Response_BanHistory *re = new Response_BanHistory;
|
|
QListIterator<ServerInfo_Ban> 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<WarningCategory> 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<ServerInfo_Warning> warnList;
|
|
QString userName = nameFromStdString(cmd.user_name());
|
|
|
|
Response_WarnHistory *re = new Response_WarnHistory;
|
|
QListIterator<ServerInfo_Warning> 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<AbstractServerSocketInterface *>(server->getUsers().value(userName));
|
|
QList<QString> 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<QString> moderatorList = server->getOnlineModeratorList();
|
|
QList<AbstractServerSocketInterface *> userList = servatrice->getUsersWithAddressAsList(QHostAddress(address));
|
|
|
|
if (!userName.isEmpty()) {
|
|
AbstractServerSocketInterface *user =
|
|
static_cast<AbstractServerSocketInterface *>(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<AbstractServerSocketInterface *>(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<int>(cmd.offset());
|
|
const int limit = qMin(static_cast<int>(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<AbstractServerSocketInterface *>(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::cmdGetServerStats(const Command_GetServerStats & /*cmd */,
|
|
ResponseContainer &rc)
|
|
{
|
|
if (!sqlInterface->checkSql()) {
|
|
return Response::RespInternalError;
|
|
}
|
|
|
|
// Servatrice::statusUpdate() periodically snapshots server health into the
|
|
// uptime table. Serve the freshest snapshot for this server.
|
|
const auto snapshot = sqlInterface->getLatestUptimeSnapshot(servatrice->getServerID());
|
|
if (!snapshot.valid) {
|
|
// No snapshot yet (fresh server, or statusUpdate() has not ticked).
|
|
return Response::RespInternalError;
|
|
}
|
|
|
|
auto *re = new Response_GetServerStats;
|
|
re->set_users_count(snapshot.usersCount);
|
|
re->set_mods_count(snapshot.modsCount);
|
|
re->set_games_count(snapshot.gamesCount);
|
|
re->set_tx_bytes(snapshot.txBytes);
|
|
re->set_rx_bytes(snapshot.rxBytes);
|
|
re->set_uptime_secs(snapshot.uptimeSecs);
|
|
re->set_timest(snapshot.timest);
|
|
|
|
// Live metrics from the in-process MetricsRegistry (resets on server restart)
|
|
re->set_cards_in_games(static_cast<google::protobuf::uint64>(servatrice->getCardsInGamesTotal()));
|
|
re->set_eventloop_stalls_total(static_cast<google::protobuf::uint64>(servatrice->getEventLoopStallsTotal()));
|
|
re->set_eventloop_last_stall_ms(static_cast<google::protobuf::uint64>(servatrice->getEventLoopLastStallMs()));
|
|
re->set_eventloop_max_stall_ms(static_cast<google::protobuf::uint64>(servatrice->getEventLoopMaxStallMs()));
|
|
re->set_total_commands(static_cast<google::protobuf::uint64>(servatrice->getMetricsRegistry().totalCommands()));
|
|
re->set_total_command_time_ms(
|
|
static_cast<google::protobuf::uint64>(servatrice->getMetricsRegistry().totalTimeMs()));
|
|
re->set_active_command_types(servatrice->getMetricsRegistry().activeTypeCount());
|
|
|
|
const auto gameStart = servatrice->getMetricsRegistry().getGameStartSnapshot();
|
|
re->set_game_start_count(static_cast<google::protobuf::uint64>(gameStart.count));
|
|
re->set_game_start_total_ms(static_cast<google::protobuf::uint64>(gameStart.totalMs));
|
|
|
|
// Per-command breakdown: resolve protobuf extension names via the descriptor pool
|
|
static const char *messageNames[] = {"SessionCommand", "RoomCommand", "GameCommand",
|
|
"ModeratorCommand", "AdminCommand", "DeveloperCommand"};
|
|
const auto activeStats = servatrice->getMetricsRegistry().collectActiveStats();
|
|
for (const auto &stat : activeStats) {
|
|
const int kind = stat.typeId / MetricsRegistry::KindStride;
|
|
const int number = stat.typeId % MetricsRegistry::KindStride;
|
|
|
|
QString label;
|
|
if (kind >= 0 && kind < MetricsRegistry::NumKinds) {
|
|
const google::protobuf::DescriptorPool *pool = google::protobuf::DescriptorPool::generated_pool();
|
|
const google::protobuf::Descriptor *message = pool->FindMessageTypeByName(messageNames[kind]);
|
|
const google::protobuf::FieldDescriptor *extension =
|
|
message ? pool->FindExtensionByNumber(message, number) : nullptr;
|
|
if (extension) {
|
|
label = QString::fromLatin1(MetricsRegistry::KindNames[kind]) + QStringLiteral("/") +
|
|
QString::fromStdString(std::string(extension->message_type()->name()));
|
|
}
|
|
}
|
|
if (label.isEmpty()) {
|
|
label = QString::number(stat.typeId);
|
|
}
|
|
|
|
CommandStats *cs = re->add_command_stats();
|
|
cs->set_kind_index(static_cast<google::protobuf::uint32>(kind));
|
|
cs->set_extension_number(static_cast<google::protobuf::uint32>(number));
|
|
cs->set_command_name(label.toStdString());
|
|
cs->set_count(static_cast<google::protobuf::uint64>(stat.count));
|
|
cs->set_total_ms(static_cast<google::protobuf::uint64>(stat.totalMs));
|
|
}
|
|
|
|
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<int>(cmd.limit()), 500);
|
|
const QList<ServerInfo_UserSession> 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<ServerInfo_UserAlt> 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<ServerInfo_ModeratorLogin> 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<AbstractServerSocketInterface *>(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<AbstractServerSocketInterface *>(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<int, Server_Room *> &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<AbstractServerSocketInterface *>(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<AbstractServerSocketInterface *>(server->getUsers().value(userName));
|
|
if (user) {
|
|
Event_ConnectionClosed event;
|
|
event.set_reason(Event_ConnectionClosed::DEMOTED);
|
|
event.set_reason_str("Your moderator, judge, and/or developer 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;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cmd.has_should_be_developer()) {
|
|
if (cmd.should_be_developer()) {
|
|
if (!addAdminFlagToUser(userName, 8)) {
|
|
return Response::RespInternalError;
|
|
}
|
|
} else {
|
|
if (!removeAdminFlagFromUser(userName, 8)) {
|
|
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("<?xml version=\"1.0\"?><cockatrice_server_stream version=\"14\">");
|
|
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<unsigned int>(item.ByteSizeLong());
|
|
#else
|
|
unsigned int size = static_cast<unsigned int>(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<unsigned int>(item.ByteSizeLong());
|
|
#else
|
|
unsigned int size = static_cast<unsigned int>(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();
|
|
}
|