Cockatrice/servatrice/src/serversocketinterface.cpp
ctrlaltca d30691559a
Some improvements to Servatice network code (#3969)
* Some improvements to Servatice network code

1. fix crash on fuzzy connection (tcp server only)
2. ensure websockets are parent()ed to avoid leaking them
3. quick catch disconnect()ed sockets instead of waiting for a socket error to happen
4. supporto mulltiple connection pools on the websocket server; they are still bound to the same thread due to a qt5 limitation.
2020-04-24 16:26:59 -04:00

1815 lines
77 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 "decklist.h"
#include "main.h"
#include "pb/command_deck_del.pb.h"
#include "pb/command_deck_del_dir.pb.h"
#include "pb/command_deck_download.pb.h"
#include "pb/command_deck_list.pb.h"
#include "pb/command_deck_new_dir.pb.h"
#include "pb/command_deck_upload.pb.h"
#include "pb/command_replay_delete_match.pb.h"
#include "pb/command_replay_download.pb.h"
#include "pb/command_replay_list.pb.h"
#include "pb/command_replay_modify_match.pb.h"
#include "pb/commands.pb.h"
#include "pb/event_add_to_list.pb.h"
#include "pb/event_connection_closed.pb.h"
#include "pb/event_notify_user.pb.h"
#include "pb/event_remove_from_list.pb.h"
#include "pb/event_server_identification.pb.h"
#include "pb/event_server_message.pb.h"
#include "pb/event_user_message.pb.h"
#include "pb/response_ban_history.pb.h"
#include "pb/response_deck_download.pb.h"
#include "pb/response_deck_list.pb.h"
#include "pb/response_deck_upload.pb.h"
#include "pb/response_forgotpasswordrequest.pb.h"
#include "pb/response_register.pb.h"
#include "pb/response_replay_download.pb.h"
#include "pb/response_replay_list.pb.h"
#include "pb/response_viewlog_history.pb.h"
#include "pb/response_warn_history.pb.h"
#include "pb/response_warn_list.pb.h"
#include "pb/serverinfo_ban.pb.h"
#include "pb/serverinfo_chat_message.pb.h"
#include "pb/serverinfo_deckstorage.pb.h"
#include "pb/serverinfo_replay.pb.h"
#include "pb/serverinfo_user.pb.h"
#include "servatrice.h"
#include "servatrice_database_interface.h"
#include "server_logger.h"
#include "server_player.h"
#include "server_response_containers.h"
#include "settingscache.h"
#include "version_string.h"
#include <QDateTime>
#include <QDebug>
#include <QHostAddress>
#include <QSqlError>
#include <QSqlQuery>
#include <QString>
#include <iostream>
#include <string>
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);
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)
{
qDebug() << "Socket error:" << socketError;
prepareDestroy();
}
void AbstractServerSocketInterface::catchSocketDisconnected()
{
prepareDestroy();
}
void AbstractServerSocketInterface::transmitProtocolItem(const ServerMessage &item)
{
outputQueueMutex.lock();
outputQueue.append(item);
outputQueueMutex.unlock();
emit outputQueueChanged();
}
void AbstractServerSocketInterface::logDebugMessage(const QString &message)
{
logger->logMessage(message, this);
}
Response::ResponseCode AbstractServerSocketInterface::processExtendedSessionCommand(int cmdType,
const SessionCommand &cmd,
ResponseContainer &rc)
{
switch ((SessionCommand::SessionCommandType)cmdType) {
case SessionCommand::ADD_TO_LIST:
return cmdAddToList(cmd.GetExtension(Command_AddToList::ext), rc);
case SessionCommand::REMOVE_FROM_LIST:
return cmdRemoveFromList(cmd.GetExtension(Command_RemoveFromList::ext), rc);
case SessionCommand::DECK_LIST:
return cmdDeckList(cmd.GetExtension(Command_DeckList::ext), rc);
case SessionCommand::DECK_NEW_DIR:
return cmdDeckNewDir(cmd.GetExtension(Command_DeckNewDir::ext), rc);
case SessionCommand::DECK_DEL_DIR:
return cmdDeckDelDir(cmd.GetExtension(Command_DeckDelDir::ext), rc);
case SessionCommand::DECK_DEL:
return cmdDeckDel(cmd.GetExtension(Command_DeckDel::ext), rc);
case SessionCommand::DECK_UPLOAD:
return cmdDeckUpload(cmd.GetExtension(Command_DeckUpload::ext), rc);
case SessionCommand::DECK_DOWNLOAD:
return cmdDeckDownload(cmd.GetExtension(Command_DeckDownload::ext), rc);
case SessionCommand::REPLAY_LIST:
return cmdReplayList(cmd.GetExtension(Command_ReplayList::ext), rc);
case SessionCommand::REPLAY_DOWNLOAD:
return cmdReplayDownload(cmd.GetExtension(Command_ReplayDownload::ext), rc);
case SessionCommand::REPLAY_MODIFY_MATCH:
return cmdReplayModifyMatch(cmd.GetExtension(Command_ReplayModifyMatch::ext), rc);
case SessionCommand::REPLAY_DELETE_MATCH:
return cmdReplayDeleteMatch(cmd.GetExtension(Command_ReplayDeleteMatch::ext), rc);
case SessionCommand::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::ACCOUNT_PASSWORD:
return cmdAccountPassword(cmd.GetExtension(Command_AccountPassword::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::VIEWLOG_HISTORY:
return cmdGetLogHistory(cmd.GetExtension(Command_ViewLogHistory::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);
default:
return Response::RespFunctionNotAllowed;
}
}
Response::ResponseCode AbstractServerSocketInterface::cmdAddToList(const Command_AddToList &cmd, ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
QString list = QString::fromStdString(cmd.list());
QString user = QString::fromStdString(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));
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 = QString::fromStdString(cmd.list());
QString user = QString::fromStdString(cmd.user_name());
if ((list != "buddy") && (list != "ignore"))
return Response::RespContextError;
if (list == "buddy")
if (!databaseInterface->isInBuddyList(QString::fromStdString(userInfo->name()), user))
return Response::RespContextError;
if (list == "ignore")
if (!databaseInterface->isInIgnoreList(QString::fromStdString(userInfo->name()), user))
return Response::RespContextError;
int id1 = userInfo->id();
int id2 = sqlInterface->getUserIdInDB(user);
if (id2 < 0)
return Response::RespNameNotFound;
QSqlQuery *query =
sqlInterface->prepareQuery("delete from {prefix}_" + list + "list where id_user1 = :id1 and id_user2 = :id2");
query->bindValue(":id1", id1);
query->bindValue(":id2", id2);
if (!sqlInterface->execSqlQuery(query))
return Response::RespInternalError;
Event_RemoveFromList event;
event.set_list_name(cmd.list());
event.set_user_name(cmd.user_name());
rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event));
return Response::RespOk;
}
int AbstractServerSocketInterface::getDeckPathId(int basePathId, QStringList path)
{
if (path.isEmpty())
return 0;
if (path[0].isEmpty())
return 0;
QSqlQuery *query = sqlInterface->prepareQuery("select id from {prefix}_decklist_folders where id_parent = "
":id_parent and name = :name and id_user = :id_user");
query->bindValue(":id_parent", basePathId);
query->bindValue(":name", path.takeFirst());
query->bindValue(":id_user", userInfo->id());
if (!sqlInterface->execSqlQuery(query))
return -1;
if (!query->next())
return -1;
int id = query->value(0).toInt();
if (path.isEmpty())
return id;
else
return getDeckPathId(id, path);
}
int AbstractServerSocketInterface::getDeckPathId(const QString &path)
{
return getDeckPathId(0, path.split("/"));
}
bool AbstractServerSocketInterface::deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder)
{
QSqlQuery *query = sqlInterface->prepareQuery(
"select id, name from {prefix}_decklist_folders where id_parent = :id_parent and id_user = :id_user");
query->bindValue(":id_parent", folderId);
query->bindValue(":id_user", userInfo->id());
if (!sqlInterface->execSqlQuery(query))
return false;
QMap<int, QString> results;
while (query->next())
results[query->value(0).toInt()] = query->value(1).toString();
foreach (int key, results.keys()) {
ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items();
newItem->set_id(key);
newItem->set_name(results.value(key).toStdString());
if (!deckListHelper(newItem->id(), newItem->mutable_folder()))
return false;
}
query = sqlInterface->prepareQuery("select id, name, upload_time from {prefix}_decklist_files where id_folder = "
":id_folder and id_user = :id_user");
query->bindValue(":id_folder", folderId);
query->bindValue(":id_user", userInfo->id());
if (!sqlInterface->execSqlQuery(query))
return false;
while (query->next()) {
ServerInfo_DeckStorage_TreeItem *newItem = folder->add_items();
newItem->set_id(query->value(0).toInt());
newItem->set_name(query->value(1).toString().toStdString());
ServerInfo_DeckStorage_File *newFile = newItem->mutable_file();
newFile->set_creation_time(query->value(2).toDateTime().toTime_t());
}
return true;
}
// CHECK AUTHENTICATION!
// Also check for every function that data belonging to other users cannot be accessed.
Response::ResponseCode AbstractServerSocketInterface::cmdDeckList(const Command_DeckList & /*cmd*/,
ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
sqlInterface->checkSql();
Response_DeckList *re = new Response_DeckList;
ServerInfo_DeckStorage_Folder *root = re->mutable_root();
if (!deckListHelper(0, root))
return Response::RespContextError;
rc.setResponseExtension(re);
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdDeckNewDir(const Command_DeckNewDir &cmd,
ResponseContainer & /*rc*/)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
sqlInterface->checkSql();
int folderId = getDeckPathId(QString::fromStdString(cmd.path()));
if (folderId == -1)
return Response::RespNameNotFound;
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", QString::fromStdString(cmd.dir_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(QString::fromStdString(cmd.path()));
if ((basePathId == -1) || (basePathId == 0))
return Response::RespNameNotFound;
deckDelDirHelper(basePathId);
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdDeckDel(const Command_DeckDel &cmd, ResponseContainer & /*rc*/)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
sqlInterface->checkSql();
QSqlQuery *query =
sqlInterface->prepareQuery("select id from {prefix}_decklist_files where id = :id and id_user = :id_user");
query->bindValue(":id", cmd.deck_id());
query->bindValue(":id_user", userInfo->id());
sqlInterface->execSqlQuery(query);
if (!query->next())
return Response::RespNameNotFound;
query = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id = :id");
query->bindValue(":id", cmd.deck_id());
sqlInterface->execSqlQuery(query);
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Command_DeckUpload &cmd,
ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
if (!cmd.has_deck_list())
return Response::RespInvalidData;
sqlInterface->checkSql();
QString deckStr = QString::fromStdString(cmd.deck_list());
DeckList deck(deckStr);
QString deckName = deck.getName();
if (deckName.isEmpty())
deckName = "Unnamed deck";
if (cmd.has_path()) {
int folderId = getDeckPathId(QString::fromStdString(cmd.path()));
if (folderId == -1)
return Response::RespNameNotFound;
QSqlQuery *query =
sqlInterface->prepareQuery("insert into {prefix}_decklist_files (id_folder, id_user, name, upload_time, "
"content) values(:id_folder, :id_user, :name, NOW(), :content)");
query->bindValue(":id_folder", folderId);
query->bindValue(":id_user", userInfo->id());
query->bindValue(":name", deckName);
query->bindValue(":content", deckStr);
sqlInterface->execSqlQuery(query);
Response_DeckUpload *re = new Response_DeckUpload;
ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file();
fileInfo->set_id(query->lastInsertId().toInt());
fileInfo->set_name(deckName.toStdString());
fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toTime_t());
rc.setResponseExtension(re);
} else if (cmd.has_deck_id()) {
QSqlQuery *query =
sqlInterface->prepareQuery("update {prefix}_decklist_files set name=:name, upload_time=NOW(), "
"content=:content where id = :id_deck and id_user = :id_user");
query->bindValue(":id_deck", cmd.deck_id());
query->bindValue(":id_user", userInfo->id());
query->bindValue(":name", deckName);
query->bindValue(":content", deckStr);
sqlInterface->execSqlQuery(query);
if (query->numRowsAffected() == 0)
return Response::RespNameNotFound;
Response_DeckUpload *re = new Response_DeckUpload;
ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file();
fileInfo->set_id(cmd.deck_id());
fileInfo->set_name(deckName.toStdString());
fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toTime_t());
rc.setResponseExtension(re);
} else
return Response::RespInvalidData;
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownload(const Command_DeckDownload &cmd,
ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
DeckList *deck;
try {
deck = sqlInterface->getDeckFromDatabase(cmd.deck_id(), userInfo->id());
} catch (Response::ResponseCode &r) {
return r;
}
Response_DeckDownload *re = new Response_DeckDownload;
re->set_deck(deck->writeToString_Native().toStdString());
rc.setResponseExtension(re);
delete deck;
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdReplayList(const Command_ReplayList & /*cmd*/,
ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
Response_ReplayList *re = new Response_ReplayList;
QSqlQuery *query1 = sqlInterface->prepareQuery(
"select a.id_game, a.replay_name, b.room_name, b.time_started, b.time_finished, b.descr, a.do_not_hide from "
"{prefix}_replays_access a left join {prefix}_games b on b.id = a.id_game where a.id_player = :id_player and "
"(a.do_not_hide = 1 or date_add(b.time_started, interval 7 day) > now())");
query1->bindValue(":id_player", userInfo->id());
sqlInterface->execSqlQuery(query1);
while (query1->next()) {
ServerInfo_ReplayMatch *matchInfo = re->add_match_list();
const int gameId = query1->value(0).toInt();
matchInfo->set_game_id(gameId);
matchInfo->set_room_name(query1->value(2).toString().toStdString());
const int timeStarted = query1->value(3).toDateTime().toTime_t();
const int timeFinished = query1->value(4).toDateTime().toTime_t();
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;
}
// MODERATOR FUNCTIONS.
// May be called by admins and moderators. Permission is checked by the calling function.
Response::ResponseCode AbstractServerSocketInterface::cmdGetLogHistory(const Command_ViewLogHistory &cmd,
ResponseContainer &rc)
{
QList<ServerInfo_ChatMessage> messageList;
QString userName = QString::fromStdString(cmd.user_name());
QString ipAddress = QString::fromStdString(cmd.ip_address());
QString gameName = QString::fromStdString(cmd.game_name());
QString gameID = QString::fromStdString(cmd.game_id());
QString message = QString::fromStdString(cmd.message());
bool chatType = false;
bool gameType = false;
bool roomType = false;
for (int i = 0; i != cmd.log_location_size(); ++i) {
if (QString::fromStdString(cmd.log_location(i)).simplified() == "room")
roomType = true;
if (QString::fromStdString(cmd.log_location(i)).simplified() == "game")
gameType = true;
if (QString::fromStdString(cmd.log_location(i)).simplified() == "chat")
chatType = true;
}
int dateRange = cmd.date_range();
int maximumResults = cmd.maximum_results();
Response_ViewLogHistory *re = new Response_ViewLogHistory;
if (servatrice->getEnableLogQuery()) {
QListIterator<ServerInfo_ChatMessage> messageIterator(sqlInterface->getMessageLogHistory(
userName, ipAddress, gameName, gameID, message, chatType, gameType, roomType, dateRange, maximumResults));
while (messageIterator.hasNext())
re->add_log_message()->CopyFrom(messageIterator.next());
} else {
ServerInfo_ChatMessage chatMessage;
// create dummy chat message for room tab in the event the query is for room messages (and possibly not others)
chatMessage.set_time(QString(tr("Log query disabled, please contact server owner for details.")).toStdString());
chatMessage.set_sender_id(QString("").toStdString());
chatMessage.set_sender_name(QString("").toStdString());
chatMessage.set_sender_ip(QString("").toStdString());
chatMessage.set_message(QString("").toStdString());
chatMessage.set_target_type(QString("room").toStdString());
chatMessage.set_target_id(QString("").toStdString());
chatMessage.set_target_name(QString("").toStdString());
messageList << chatMessage;
// create dummy chat message for room tab in the event the query is for game messages (and possibly not others)
chatMessage.set_time(QString(tr("Log query disabled, please contact server owner for details.")).toStdString());
chatMessage.set_sender_id(QString("").toStdString());
chatMessage.set_sender_name(QString("").toStdString());
chatMessage.set_sender_ip(QString("").toStdString());
chatMessage.set_message(QString("").toStdString());
chatMessage.set_target_type(QString("game").toStdString());
chatMessage.set_target_id(QString("").toStdString());
chatMessage.set_target_name(QString("").toStdString());
messageList << chatMessage;
// create dummy chat message for room tab in the event the query is for chat messages (and possibly not others)
chatMessage.set_time(QString(tr("Log query disabled, please contact server owner for details.")).toStdString());
chatMessage.set_sender_id(QString("").toStdString());
chatMessage.set_sender_name(QString("").toStdString());
chatMessage.set_sender_ip(QString("").toStdString());
chatMessage.set_message(QString("").toStdString());
chatMessage.set_target_type(QString("chat").toStdString());
chatMessage.set_target_id(QString("").toStdString());
chatMessage.set_target_name(QString("").toStdString());
messageList << chatMessage;
QListIterator<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 = QString::fromStdString(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();
QStringList warningsList = officialWarnings.split(",", QString::SkipEmptyParts);
foreach (QString warning, warningsList) {
re->add_warning(warning.toStdString());
}
re->set_user_name(cmd.user_name());
re->set_user_clientid(cmd.user_clientid());
rc.setResponseExtension(re);
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdGetWarnHistory(const Command_GetWarnHistory &cmd,
ResponseContainer &rc)
{
QList<ServerInfo_Warning> warnList;
QString userName = QString::fromStdString(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;
}
Response::ResponseCode AbstractServerSocketInterface::cmdWarnUser(const Command_WarnUser &cmd,
ResponseContainer & /*rc*/)
{
if (!sqlInterface->checkSql())
return Response::RespInternalError;
QString userName = QString::fromStdString(cmd.user_name()).simplified();
QString warningReason = QString::fromStdString(cmd.reason()).simplified();
QString clientID = QString::fromStdString(cmd.clientid()).simplified();
QString sendingModerator = QString::fromStdString(userInfo->name()).simplified();
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) {
Event_NotifyUser event;
event.set_type(Event_NotifyUser::WARNING);
event.set_warning_reason(cmd.reason());
SessionEvent *se = user->prepareSessionEvent(event);
user->sendProtocolItem(*se);
delete se;
}
QListIterator<QString> modIterator(moderatorList);
foreach (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 = QString::fromStdString(cmd.user_name()).simplified();
QString address = QString::fromStdString(cmd.address()).simplified();
QString clientID = QString::fromStdString(cmd.clientid()).simplified();
if (userName.isEmpty() && address.isEmpty() && clientID.isEmpty())
return Response::RespOk;
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", QString::fromStdString(cmd.reason()));
query->bindValue(":visible_reason", QString::fromStdString(cmd.visible_reason()));
query->bindValue(":client_id", QString::fromStdString(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 *query = sqlInterface->prepareQuery("select name from {prefix}_users where clientid = :client_id");
query->bindValue(":client_id", QString::fromStdString(cmd.clientid()));
sqlInterface->execSqlQuery(query);
if (!sqlInterface->execSqlQuery(query)) {
qDebug("ClientID username ban lookup failed: SQL Error");
} else {
while (query->next()) {
userName = query->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(cmd.visible_reason());
if (minutes)
event.set_end_time(QDateTime::currentDateTime().addSecs(60 * minutes).toTime_t());
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);
}
}
QListIterator<QString> modIterator(moderatorList);
foreach (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: " + QString::fromStdString(cmd.reason()));
notificationMessage.append("\n Visible Reason: " + QString::fromStdString(cmd.visible_reason()));
sendServerMessage(moderator.simplified(), notificationMessage);
}
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const Command_Register &cmd,
ResponseContainer &rc)
{
QString userName = QString::fromStdString(cmd.user_name());
QString clientId = QString::fromStdString(cmd.clientid());
qDebug() << "Got register command: " << userName;
bool registrationEnabled = settingsCache->value("registration/enabled", false).toBool();
if (!registrationEnabled) {
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT",
"Server functionality disabled", false);
return Response::RespRegistrationDisabled;
}
QString emailBlackList = servatrice->getEmailBlackList();
QString emailAddress = QString::fromStdString(cmd.email());
QStringList emailBlackListFilters = emailBlackList.split(",", QString::SkipEmptyParts);
// verify that users email/provider is not blacklisted
if (!emailBlackList.trimmed().isEmpty()) {
foreach (QString blackListEmailAddress, emailBlackListFilters) {
if (emailAddress.contains(blackListEmailAddress, Qt::CaseInsensitive)) {
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(),
this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(),
"REGISTER_ACCOUNT", "Email used is blacklisted", false);
return Response::RespEmailBlackListed;
}
}
}
bool requireEmailForRegistration = settingsCache->value("registration/requireemail", true).toBool();
if (requireEmailForRegistration) {
QRegExp rx("\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,4}\\b");
if (emailAddress.isEmpty() || !rx.exactMatch(emailAddress))
return Response::RespEmailRequiredToRegister;
}
// TODO: Move this method outside of the db interface
QString errorString;
if (!sqlInterface->usernameIsValid(userName, errorString)) {
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.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(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT",
"Username is invalid", false);
return Response::RespUsernameInvalid;
}
if (sqlInterface->userExists(userName)) {
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT",
"Username already exists", false);
return Response::RespUserAlreadyExists;
}
if (servatrice->getMaxAccountsPerEmail() > 0 &&
sqlInterface->checkNumberOfUserAccounts(emailAddress) >= servatrice->getMaxAccountsPerEmail()) {
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.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(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.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).toTime_t());
rc.setResponseExtension(re);
return Response::RespUserIsBanned;
}
if (tooManyRegistrationAttempts(this->getAddress())) {
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT",
"Too many registration attempts from this ip address", false);
return Response::RespTooManyRequests;
}
QString realName = QString::fromStdString(cmd.real_name());
ServerInfo_User_Gender gender = cmd.gender();
QString country = QString::fromStdString(cmd.country());
QString password = QString::fromStdString(cmd.password());
if (!isPasswordLongEnough(password.length())) {
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT",
"Password is too short", false);
return Response::RespPasswordTooShort;
}
QString token;
bool requireEmailActivation = settingsCache->value("registration/requireemailactivation", true).toBool();
bool regSucceeded = sqlInterface->registerUser(userName, realName, gender, password, emailAddress, country, token,
!requireEmailActivation);
if (regSucceeded) {
qDebug() << "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(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT",
"", true);
return Response::RespRegistrationAcceptedNeedsActivation;
} else {
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT",
"", true);
return Response::RespRegistrationAccepted;
}
} else {
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "REGISTER_ACCOUNT",
"Unknown reason for failure", false);
return Response::RespRegistrationFailed;
}
}
bool AbstractServerSocketInterface::tooManyRegistrationAttempts(const QString &ipAddress)
{
// TODO: implement
Q_UNUSED(ipAddress);
return false;
}
Response::ResponseCode AbstractServerSocketInterface::cmdActivateAccount(const Command_Activate &cmd,
ResponseContainer & /*rc*/)
{
QString userName = QString::fromStdString(cmd.user_name());
QString token = QString::fromStdString(cmd.token());
QString clientID = QString::fromStdString(cmd.clientid());
if (clientID.isEmpty())
clientID = "UNKNOWN";
if (sqlInterface->activateUser(userName, token)) {
qDebug() << "Accepted activation for user" << QString::fromStdString(cmd.user_name());
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
clientID, "ACTIVATE_ACCOUNT", "", true);
return Response::RespActivationAccepted;
} else {
qDebug() << "Failed activation for user" << QString::fromStdString(cmd.user_name());
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
clientID, "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 = QString::fromStdString(cmd.real_name());
QString emailAddress = QString::fromStdString(cmd.email());
ServerInfo_User_Gender gender = cmd.gender();
QString country = QString::fromStdString(cmd.country());
QString userName = QString::fromStdString(userInfo->name());
QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_users set realname=:realName, email=:email, "
"gender=:gender, country=:country where name=:userName");
query->bindValue(":realName", realName);
query->bindValue(":email", emailAddress);
query->bindValue(":gender", sqlInterface->getGenderChar(gender));
query->bindValue(":country", country);
query->bindValue(":userName", userName);
if (!sqlInterface->execSqlQuery(query))
return Response::RespInternalError;
userInfo->set_real_name(cmd.real_name());
userInfo->set_email(cmd.email());
userInfo->set_gender(cmd.gender());
userInfo->set_country(cmd.country());
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdAccountImage(const Command_AccountImage &cmd,
ResponseContainer & /* rc */)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
QByteArray image(cmd.image().c_str(), cmd.image().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(), cmd.image().length());
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdAccountPassword(const Command_AccountPassword &cmd,
ResponseContainer & /* rc */)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
QString oldPassword = QString::fromStdString(cmd.old_password());
QString newPassword = QString::fromStdString(cmd.new_password());
if (!isPasswordLongEnough(newPassword.length()))
return Response::RespPasswordTooShort;
QString userName = QString::fromStdString(userInfo->name());
if (!databaseInterface->changeUserPassword(userName, oldPassword, newPassword, false))
return Response::RespWrongPassword;
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordRequest(const Command_ForgotPasswordRequest &cmd,
ResponseContainer &rc)
{
qDebug() << "Received forgot password request from user: " << QString::fromStdString(cmd.user_name());
if (!servatrice->getEnableForgotPassword()) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "PASSWORD_RESET_REQUEST",
"Server functionality disabled", false);
return Response::RespFunctionNotAllowed;
}
if (!sqlInterface->userExists(QString::fromStdString(cmd.user_name()))) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "PASSWORD_RESET_REQUEST",
"User does not exist", false);
return Response::RespFunctionNotAllowed;
}
if (sqlInterface->doesForgotPasswordExist(QString::fromStdString(cmd.user_name()))) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.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(), QString::fromStdString(cmd.user_name()),
QString::fromStdString(cmd.clientid()), banReason, banTimeRemaining)) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "PASSWORD_RESET_REQUEST",
"User is banned", false);
return Response::RespFunctionNotAllowed;
}
if (servatrice->getEnableForgotPasswordChallenge()) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "PASSWORD_RESET_REQUEST",
"Request does not exist, challenge requested", true);
Response_ForgotPasswordRequest *re = new Response_ForgotPasswordRequest;
re->set_challenge_email(true);
rc.setResponseExtension(re);
return Response::RespOk;
} else {
if (sqlInterface->addForgotPassword(QString::fromStdString(cmd.user_name()))) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(),
"PASSWORD_RESET_REQUEST",
"Request does not exist, challenge not requested", true);
Response_ForgotPasswordRequest *re = new Response_ForgotPasswordRequest;
re->set_challenge_email(false);
rc.setResponseExtension(re);
return Response::RespOk;
}
}
return Response::RespFunctionNotAllowed;
}
Response::ResponseCode AbstractServerSocketInterface::cmdForgotPasswordReset(const Command_ForgotPasswordReset &cmd,
ResponseContainer &rc)
{
Q_UNUSED(rc);
qDebug() << "Received forgot password reset from user: " << QString::fromStdString(cmd.user_name());
if (!sqlInterface->doesForgotPasswordExist(QString::fromStdString(cmd.user_name()))) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "PASSWORD_RESET",
"Request does not exist for user", false);
return Response::RespFunctionNotAllowed;
}
if (!sqlInterface->validateTableColumnStringData("{prefix}_users", "token", QString::fromStdString(cmd.user_name()),
QString::fromStdString(cmd.token()))) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "PASSWORD_RESET",
"Failed token validation", false);
return Response::RespFunctionNotAllowed;
}
if (sqlInterface->changeUserPassword(QString::fromStdString(cmd.user_name()), "",
QString::fromStdString(cmd.new_password()), true)) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(), "PASSWORD_RESET", "",
true);
sqlInterface->removeForgotPassword(QString::fromStdString(cmd.user_name()));
return Response::RespOk;
}
return Response::RespFunctionNotAllowed;
}
Response::ResponseCode
AbstractServerSocketInterface::cmdForgotPasswordChallenge(const Command_ForgotPasswordChallenge &cmd,
ResponseContainer &rc)
{
Q_UNUSED(rc);
qDebug() << "Received forgot password challenge from user: " << QString::fromStdString(cmd.user_name());
if (sqlInterface->doesForgotPasswordExist(QString::fromStdString(cmd.user_name()))) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(),
"PASSWORD_RESET_CHALLANGE", "Request does not exist for user", false);
return Response::RespOk;
}
if (sqlInterface->validateTableColumnStringData("{prefix}_users", "email", QString::fromStdString(cmd.user_name()),
QString::fromStdString(cmd.email()))) {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(),
"PASSWORD_RESET_CHALLANGE", "", true);
if (sqlInterface->addForgotPassword(QString::fromStdString(cmd.user_name())))
return Response::RespOk;
} else {
if (servatrice->getEnableForgotPasswordAudit())
sqlInterface->addAuditRecord(QString::fromStdString(cmd.user_name()).simplified(), this->getAddress(),
QString::fromStdString(cmd.clientid()).simplified(),
"PASSWORD_RESET_CHALLANGE", "Failed to answer email challenge question",
false);
}
return Response::RespFunctionNotAllowed;
}
// 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, QString::fromStdString(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::fromStdString("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::fromStdString("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 and/or judge status has been revoked.");
event.set_end_time(QDateTime::currentDateTime().toTime_t());
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 = QString::fromStdString(cmd.user_name());
if (cmd.has_should_be_mod()) {
if (cmd.should_be_mod()) {
if (!addAdminFlagToUser(userName, 2)) {
return Response::RespInternalError;
}
} else {
if (!removeAdminFlagFromUser(userName, 2)) {
return Response::RespInternalError;
}
}
}
if (cmd.has_should_be_judge()) {
if (cmd.should_be_judge()) {
if (!addAdminFlagToUser(userName, 4)) {
return Response::RespInternalError;
}
} else {
if (!removeAdminFlagFromUser(userName, 4)) {
return Response::RespInternalError;
}
}
}
return Response::RespOk;
}
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(error(QAbstractSocket::SocketError)), this,
SLOT(catchSocketError(QAbstractSocket::SocketError)));
connect(socket, SIGNAL(disconnected()), this, SLOT(catchSocketDisconnected()));
}
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 = item.ByteSizeLong();
#else
unsigned int size = item.ByteSize();
#endif
buf.resize(size + 4);
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);
totalBytes += size + 4;
locker.relock();
}
locker.unlock();
servatrice->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;
try {
newCommandContainer.ParseFromArray(inputBuffer.data(), messageLength);
} catch (std::exception &e) {
qDebug() << "Caught std::exception in" << __FILE__ << __LINE__ <<
#ifdef _MSC_VER // Visual Studio
__FUNCTION__;
#else
__PRETTY_FUNCTION__;
#endif
qDebug() << "Exception:" << e.what();
qDebug() << "Message coming from:" << getAddress();
qDebug() << "Message length:" << messageLength;
qDebug() << "Message content:" << inputBuffer.toHex();
} catch (...) {
qDebug() << "Unhandled exception in" << __FILE__ << __LINE__ <<
#ifdef _MSC_VER // Visual Studio
__FUNCTION__;
#else
__PRETTY_FUNCTION__;
#endif
qDebug() << "Message coming from:" << getAddress();
}
inputBuffer.remove(0, messageLength);
messageInProgress = false;
// 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
} 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)
{
}
WebsocketServerSocketInterface::~WebsocketServerSocketInterface()
{
logger->logMessage("WebsocketServerSocketInterface destructor", this);
flushOutputQueue();
}
void WebsocketServerSocketInterface::initConnection(void *_socket)
{
socket = (QWebSocket *)_socket;
socket->setParent(this);
address = socket->peerAddress();
QByteArray websocketIPHeader = settingsCache->value("server/web_socket_ip_header", "").toByteArray();
if (websocketIPHeader.length() > 0) {
#if QT_VERSION >= 0x050600
if (socket->request().hasRawHeader(websocketIPHeader)) {
QString header(socket->request().rawHeader(websocketIPHeader));
QHostAddress parsed(header);
if (!parsed.isNull())
address = parsed;
}
#else
logger->logMessage(QString("Reading the websocket IP header is unsupported on this version of QT."));
#endif
}
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 a 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;
int totalBytes = 0;
while (!outputQueue.isEmpty()) {
ServerMessage item = outputQueue.takeFirst();
locker.unlock();
QByteArray buf;
#if GOOGLE_PROTOBUF_VERSION > 3001000
unsigned int size = item.ByteSizeLong();
#else
unsigned int size = item.ByteSize();
#endif
buf.resize(size);
item.SerializeToArray(buf.data(), size);
// In case socket->write() calls catchSocketError(), the mutex must not be locked during this call.
writeToSocket(buf);
totalBytes += size;
locker.relock();
}
locker.unlock();
servatrice->incTxBytes(totalBytes);
// see above wrt mutex
flushSocket();
}
void WebsocketServerSocketInterface::binaryMessageReceived(const QByteArray &message)
{
servatrice->incRxBytes(message.size());
CommandContainer newCommandContainer;
try {
newCommandContainer.ParseFromArray(message.data(), message.size());
} catch (std::exception &e) {
qDebug() << "Caught std::exception in" << __FILE__ << __LINE__ <<
#ifdef _MSC_VER // Visual Studio
__FUNCTION__;
#else
__PRETTY_FUNCTION__;
#endif
qDebug() << "Exception:" << e.what();
qDebug() << "Message coming from:" << getAddress();
qDebug() << "Message length:" << message.size();
qDebug() << "Message content:" << message.toHex();
} catch (...) {
qDebug() << "Unhandled exception in" << __FILE__ << __LINE__ <<
#ifdef _MSC_VER // Visual Studio
__FUNCTION__;
#else
__PRETTY_FUNCTION__;
#endif
qDebug() << "Message coming from:" << getAddress();
}
processCommandContainer(newCommandContainer);
}
bool AbstractServerSocketInterface::isPasswordLongEnough(const int passwordLength)
{
return passwordLength >= servatrice->getMinPasswordLength();
}