Cockatrice/servatrice/src/serversocketinterface.cpp
ZeldaZach b02adccf87 Support Qt6, Min Qt5.8, Fix Win32, Fix Servatrice
Add lock around deleting arrows for commanding cards

Add support for Qt6 w/ Backwards Qt5

Handle Qt5/6 cross compilation better

Last cleanups

caps matter

Fix serv

Prevent crash on 6.3.0 Linux & bump to 5.8 min

Prevent out of bounds indexing

Delete shutdown timer if it exists

Fixup ticket comments, remove unneeded guards

Try to add support for missing OSes

Update .ci/release_template.md

Update PR based on comments

Update XML name after done and remove Hirsute

Address local game crash

Address comments from PR (again)
Tests don't work on mac, will see if a problem on other OSes

make soundengine more consistent across qt versions

disable tests on distros that are covered by others

Fix Oracle Crash due to bad memory access

Update Oracle to use new Qt6 way of adding translations

Add support for Qt5/Qt6 compiling of Cockatrice

Remove unneeded calls to QtMath/cmath/math.h

Update how we handle bitwise comparisons for enums with Tray Icon

Change header guards to not duplicate function

Leave comment & Fix Path for GHA Qt

Update common/server.h

Update cockatrice/src/window_main.cpp

Rollback change on cmake module path for NSIS

check docker image requirements

add size limit to ccache

put variables in quotes

properly set build type on mac

avoid names used in cmake

fix up cmake module path

cmake 3.10 does not recognize prepend

Support Tests in FindQtRuntime

set ccache size on non debug builds as well

immediately return when removing non existing client

handle incTxBytes with a signal instead

don't set common link libraries in cockatrice/CMakeLists.txt

add comments

set macos qt version to 6

Try upgrading XCode versions to latest they can be supported on

Ensure Qt gets linked

add tmate so i can see what's going on

Qt6 points two directories further down than Qt5 with regard to the top lib path, so we need to account for this

Establish Plugins directory for Qt6

Establish TLS plugins for Qt6 services

Minor change for release channel network manager

Let windows build in parallel cores

Wrong symbols

Qt6 patch up for signal

add missing qt6 package on deb builds

boolean expressions are hard

negative indexes should go to the end

Intentionally fail cache

move size checks to individual zone types

Hardcode libs needed for building on Windows, as the regex was annoying

Update wording

use the --parallel option in all builds

clean up the .ci scripts some more

tweak fedora build

add os parameter to compile.sh

I don't really like this but it seems the easiest way
I'd prefer if these types of quirks would live in the main configuration
file, the yml

fixup yml

readd appended cache key to vcpkg step

fix windows 32 quirk

the json hash is already added to the key as well

remove os parameter and clean up ci files

set name_build.sh to output relative paths

set backwards compatible version of xcode and qt on mac

set QTDIR for mac builds on qt5

has no effect for qt6

export BUILD_DIR to name_build.sh

merge mac build steps

merge homebrew steps, set package suffix

link qt5

remove brew link

set qtdir to qt5 only

compile.sh vars need to be empty not 0

fix sets manager search bar on qt 5.12/15

fix oracle subprocess errors being ignored on qt 5

clean up translation loading

move en@source translation file so it will not get included in packages
NOTE: this needs to be done at transifex as well!

Use generator platform over osname

Short circuit if not Win defined
2022-05-06 17:31:08 -04:00

2005 lines
83 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_password_salt.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 "server_room.h"
#include "settingscache.h"
#include "stringsizes.h"
#include "version_string.h"
#include <QDateTime>
#include <QDebug>
#include <QHostAddress>
#include <QRegularExpression>
#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);
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)
{
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);
case SessionCommand::REQUEST_PASSWORD_SALT:
return cmdRequestPasswordSalt(cmd.GetExtension(Command_RequestPasswordSalt::ext), rc);
break;
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 = 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));
rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event));
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdRemoveFromList(const Command_RemoveFromList &cmd,
ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
QString list = nameFromStdString(cmd.list());
QString user = nameFromStdString(cmd.user_name());
if ((list != "buddy") && (list != "ignore"))
return Response::RespContextError;
if (list == "buddy")
if (!databaseInterface->isInBuddyList(QString::fromStdString(userInfo->name()), user))
return Response::RespContextError;
if (list == "ignore")
if (!databaseInterface->isInIgnoreList(QString::fromStdString(userInfo->name()), user))
return Response::RespContextError;
int id1 = userInfo->id();
int id2 = sqlInterface->getUserIdInDB(user);
if (id2 < 0)
return Response::RespNameNotFound;
QSqlQuery *query =
sqlInterface->prepareQuery("delete from {prefix}_" + list + "list where id_user1 = :id1 and id_user2 = :id2");
query->bindValue(":id1", id1);
query->bindValue(":id2", id2);
if (!sqlInterface->execSqlQuery(query))
return Response::RespInternalError;
Event_RemoveFromList event;
event.set_list_name(cmd.list());
event.set_user_name(cmd.user_name());
rc.enqueuePreResponseItem(ServerMessage::SESSION_EVENT, prepareSessionEvent(event));
return Response::RespOk;
}
int AbstractServerSocketInterface::getDeckPathId(int basePathId, QStringList path)
{
if (path.isEmpty())
return 0;
if (path[0].isEmpty())
return 0;
QSqlQuery *query = sqlInterface->prepareQuery("select id from {prefix}_decklist_folders where id_parent = "
":id_parent and name = :name and id_user = :id_user");
query->bindValue(":id_parent", basePathId);
query->bindValue(":name", path.takeFirst());
query->bindValue(":id_user", userInfo->id());
if (!sqlInterface->execSqlQuery(query))
return -1;
if (!query->next())
return -1;
int id = query->value(0).toInt();
if (path.isEmpty())
return id;
else
return getDeckPathId(id, path);
}
int AbstractServerSocketInterface::getDeckPathId(const QString &path)
{
return getDeckPathId(0, path.split("/"));
}
bool AbstractServerSocketInterface::deckListHelper(int folderId, ServerInfo_DeckStorage_Folder *folder)
{
QSqlQuery *query = sqlInterface->prepareQuery(
"select id, name from {prefix}_decklist_folders where id_parent = :id_parent and id_user = :id_user");
query->bindValue(":id_parent", folderId);
query->bindValue(":id_user", userInfo->id());
if (!sqlInterface->execSqlQuery(query))
return false;
QMap<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().toSecsSinceEpoch());
}
return true;
}
// CHECK AUTHENTICATION!
// Also check for every function that data belonging to other users cannot be accessed.
Response::ResponseCode AbstractServerSocketInterface::cmdDeckList(const Command_DeckList & /*cmd*/,
ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
sqlInterface->checkSql();
Response_DeckList *re = new Response_DeckList;
ServerInfo_DeckStorage_Folder *root = re->mutable_root();
if (!deckListHelper(0, root))
return Response::RespContextError;
rc.setResponseExtension(re);
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdDeckNewDir(const Command_DeckNewDir &cmd,
ResponseContainer & /*rc*/)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
sqlInterface->checkSql();
QString path = nameFromStdString(cmd.path());
int folderId = getDeckPathId(path);
if (folderId == -1)
return Response::RespNameNotFound;
QString name = nameFromStdString(cmd.dir_name());
if (path.length() + name.length() + 1 > MAX_NAME_LENGTH)
return Response::RespContextError; // do not allow creation of paths that would be too long to delete
QSqlQuery *query = sqlInterface->prepareQuery(
"insert into {prefix}_decklist_folders (id_parent, id_user, name) values(:id_parent, :id_user, :name)");
query->bindValue(":id_parent", folderId);
query->bindValue(":id_user", userInfo->id());
query->bindValue(":name", name);
if (!sqlInterface->execSqlQuery(query))
return Response::RespContextError;
return Response::RespOk;
}
void AbstractServerSocketInterface::deckDelDirHelper(int basePathId)
{
sqlInterface->checkSql();
QSqlQuery *query =
sqlInterface->prepareQuery("select id from {prefix}_decklist_folders where id_parent = :id_parent");
query->bindValue(":id_parent", basePathId);
sqlInterface->execSqlQuery(query);
while (query->next())
deckDelDirHelper(query->value(0).toInt());
query = sqlInterface->prepareQuery("delete from {prefix}_decklist_files where id_folder = :id_folder");
query->bindValue(":id_folder", basePathId);
sqlInterface->execSqlQuery(query);
query = sqlInterface->prepareQuery("delete from {prefix}_decklist_folders where id = :id");
query->bindValue(":id", basePathId);
sqlInterface->execSqlQuery(query);
}
void AbstractServerSocketInterface::sendServerMessage(const QString userName, const QString message)
{
AbstractServerSocketInterface *user =
static_cast<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;
}
Response::ResponseCode AbstractServerSocketInterface::cmdDeckUpload(const Command_DeckUpload &cmd,
ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
if (!cmd.has_deck_list())
return Response::RespInvalidData;
sqlInterface->checkSql();
QString deckStr = fileFromStdString(cmd.deck_list());
DeckList deck;
if (!deck.loadFromString_Native(deckStr))
return Response::RespContextError;
QString deckName = deck.getName();
if (deckName.isEmpty())
deckName = "Unnamed deck";
if (cmd.has_path()) {
int folderId = getDeckPathId(nameFromStdString(cmd.path()));
if (folderId == -1)
return Response::RespNameNotFound;
QSqlQuery *query =
sqlInterface->prepareQuery("insert into {prefix}_decklist_files (id_folder, id_user, name, upload_time, "
"content) values(:id_folder, :id_user, :name, NOW(), :content)");
query->bindValue(":id_folder", folderId);
query->bindValue(":id_user", userInfo->id());
query->bindValue(":name", deckName);
query->bindValue(":content", deckStr);
sqlInterface->execSqlQuery(query);
Response_DeckUpload *re = new Response_DeckUpload;
ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file();
fileInfo->set_id(query->lastInsertId().toInt());
fileInfo->set_name(deckName.toStdString());
fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch());
rc.setResponseExtension(re);
} else if (cmd.has_deck_id()) {
QSqlQuery *query =
sqlInterface->prepareQuery("update {prefix}_decklist_files set name=:name, upload_time=NOW(), "
"content=:content where id = :id_deck and id_user = :id_user");
query->bindValue(":id_deck", cmd.deck_id());
query->bindValue(":id_user", userInfo->id());
query->bindValue(":name", deckName);
query->bindValue(":content", deckStr);
sqlInterface->execSqlQuery(query);
if (query->numRowsAffected() == 0)
return Response::RespNameNotFound;
Response_DeckUpload *re = new Response_DeckUpload;
ServerInfo_DeckStorage_TreeItem *fileInfo = re->mutable_new_file();
fileInfo->set_id(cmd.deck_id());
fileInfo->set_name(deckName.toStdString());
fileInfo->mutable_file()->set_creation_time(QDateTime::currentDateTime().toSecsSinceEpoch());
rc.setResponseExtension(re);
} else
return Response::RespInvalidData;
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdDeckDownload(const Command_DeckDownload &cmd,
ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
DeckList *deck;
try {
deck = sqlInterface->getDeckFromDatabase(cmd.deck_id(), userInfo->id());
} catch (Response::ResponseCode &r) {
return r;
}
Response_DeckDownload *re = new Response_DeckDownload;
re->set_deck(deck->writeToString_Native().toStdString());
rc.setResponseExtension(re);
delete deck;
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdReplayList(const Command_ReplayList & /*cmd*/,
ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
Response_ReplayList *re = new Response_ReplayList;
QSqlQuery *query1 = sqlInterface->prepareQuery(
"select a.id_game, a.replay_name, b.room_name, b.time_started, b.time_finished, b.descr, a.do_not_hide from "
"{prefix}_replays_access a left join {prefix}_games b on b.id = a.id_game where a.id_player = :id_player and "
"(a.do_not_hide = 1 or date_add(b.time_started, interval 7 day) > now())");
query1->bindValue(":id_player", userInfo->id());
sqlInterface->execSqlQuery(query1);
while (query1->next()) {
ServerInfo_ReplayMatch *matchInfo = re->add_match_list();
const int gameId = query1->value(0).toInt();
matchInfo->set_game_id(gameId);
matchInfo->set_room_name(query1->value(2).toString().toStdString());
const int timeStarted = query1->value(3).toDateTime().toSecsSinceEpoch();
const int timeFinished = query1->value(4).toDateTime().toSecsSinceEpoch();
matchInfo->set_time_started(timeStarted);
matchInfo->set_length(timeFinished - timeStarted);
matchInfo->set_game_name(query1->value(5).toString().toStdString());
const QString replayName = query1->value(1).toString();
matchInfo->set_do_not_hide(query1->value(6).toBool());
{
QSqlQuery *query2 =
sqlInterface->prepareQuery("select player_name from {prefix}_games_players where id_game = :id_game");
query2->bindValue(":id_game", gameId);
sqlInterface->execSqlQuery(query2);
while (query2->next())
matchInfo->add_player_names(query2->value(0).toString().toStdString());
}
{
QSqlQuery *query3 =
sqlInterface->prepareQuery("select id, duration from {prefix}_replays where id_game = :id_game");
query3->bindValue(":id_game", gameId);
sqlInterface->execSqlQuery(query3);
while (query3->next()) {
ServerInfo_Replay *replayInfo = matchInfo->add_replay_list();
replayInfo->set_replay_id(query3->value(0).toInt());
replayInfo->set_replay_name(replayName.toStdString());
replayInfo->set_duration(query3->value(1).toInt());
}
}
}
rc.setResponseExtension(re);
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdReplayDownload(const Command_ReplayDownload &cmd,
ResponseContainer &rc)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
{
QSqlQuery *query =
sqlInterface->prepareQuery("select 1 from {prefix}_replays_access a left join {prefix}_replays b on "
"a.id_game = b.id_game where b.id = :id_replay and a.id_player = :id_player");
query->bindValue(":id_replay", cmd.replay_id());
query->bindValue(":id_player", userInfo->id());
if (!sqlInterface->execSqlQuery(query))
return Response::RespInternalError;
if (!query->next())
return Response::RespAccessDenied;
}
QSqlQuery *query = sqlInterface->prepareQuery("select replay from {prefix}_replays where id = :id_replay");
query->bindValue(":id_replay", cmd.replay_id());
if (!sqlInterface->execSqlQuery(query))
return Response::RespInternalError;
if (!query->next())
return Response::RespNameNotFound;
QByteArray data = query->value(0).toByteArray();
Response_ReplayDownload *re = new Response_ReplayDownload;
re->set_replay_data(data.data(), data.size());
rc.setResponseExtension(re);
return Response::RespOk;
}
Response::ResponseCode AbstractServerSocketInterface::cmdReplayModifyMatch(const Command_ReplayModifyMatch &cmd,
ResponseContainer & /*rc*/)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
if (!sqlInterface->checkSql())
return Response::RespInternalError;
QSqlQuery *query = sqlInterface->prepareQuery("update {prefix}_replays_access set do_not_hide=:do_not_hide where "
"id_player = :id_player and id_game = :id_game");
query->bindValue(":id_player", userInfo->id());
query->bindValue(":id_game", cmd.game_id());
query->bindValue(":do_not_hide", cmd.do_not_hide());
if (!sqlInterface->execSqlQuery(query))
return Response::RespInternalError;
return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound;
}
Response::ResponseCode AbstractServerSocketInterface::cmdReplayDeleteMatch(const Command_ReplayDeleteMatch &cmd,
ResponseContainer & /*rc*/)
{
if (authState != PasswordRight)
return Response::RespFunctionNotAllowed;
if (!sqlInterface->checkSql())
return Response::RespInternalError;
QSqlQuery *query = sqlInterface->prepareQuery(
"delete from {prefix}_replays_access where id_player = :id_player and id_game = :id_game");
query->bindValue(":id_player", userInfo->id());
query->bindValue(":id_game", cmd.game_id());
if (!sqlInterface->execSqlQuery(query))
return Response::RespInternalError;
return query->numRowsAffected() > 0 ? Response::RespOk : Response::RespNameNotFound;
}
// 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 = nameFromStdString(cmd.user_name());
QString ipAddress = nameFromStdString(cmd.ip_address());
QString gameName = nameFromStdString(cmd.game_name());
QString gameID = nameFromStdString(cmd.game_id());
QString message = textFromStdString(cmd.message());
bool chatType = false;
bool gameType = false;
bool roomType = false;
for (int i = 0; i != cmd.log_location_size(); ++i) {
if (nameFromStdString(cmd.log_location(i)).simplified() == "room")
roomType = true;
if (nameFromStdString(cmd.log_location(i)).simplified() == "game")
gameType = true;
if (nameFromStdString(cmd.log_location(i)).simplified() == "chat")
chatType = true;
}
int dateRange = cmd.date_range();
int maximumResults = cmd.maximum_results();
Response_ViewLogHistory *re = new Response_ViewLogHistory;
if (servatrice->getEnableLogQuery()) {
QListIterator<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 = 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();
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
QStringList warningsList = officialWarnings.split(",", Qt::SkipEmptyParts);
#else
QStringList warningsList = officialWarnings.split(",", QString::SkipEmptyParts);
#endif
foreach (QString warning, warningsList) {
re->add_warning(warning.toStdString());
}
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)) {
qDebug("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;
}
QPair<QString, QString> AbstractServerSocketInterface::parseEmailAddress(const QString &emailAddress)
{
// https://www.regular-expressions.info/email.html
static const QRegularExpression emailRegex(R"(^([A-Z0-9._%+-]+)@([A-Z0-9.-]+\.[A-Z]{2,})$)",
QRegularExpression::CaseInsensitiveOption);
const auto match = emailRegex.match(emailAddress);
if (emailAddress.isEmpty() || !match.hasMatch()) {
return {};
}
QString capturedEmailUser = match.captured(1);
QString capturedEmailAddressDomain = match.captured(2);
// Replace googlemail.com with gmail.com, as is standard nowadays
// https://www.gmass.co/blog/domains-gmail-com-googlemail-com-and-google-com/
if (capturedEmailAddressDomain.toLower() == "googlemail.com") {
capturedEmailAddressDomain = "gmail.com";
}
// Trim out dots and pluses from Google/Gmail domains
if (capturedEmailAddressDomain.toLower() == "gmail.com") {
// Remove all content after first plus sign (as unnecessary with gmail)
// https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html
const int firstPlusSign = capturedEmailUser.indexOf("+");
if (firstPlusSign != -1) {
capturedEmailUser = capturedEmailUser.left(firstPlusSign);
}
// Remove all periods (as unnecessary with gmail)
// https://gmail.googleblog.com/2008/03/2-hidden-ways-to-get-more-from-your.html
capturedEmailUser.replace(".", "");
}
return {capturedEmailUser, capturedEmailAddressDomain};
}
Response::ResponseCode AbstractServerSocketInterface::cmdRegisterAccount(const Command_Register &cmd,
ResponseContainer &rc)
{
QString userName = nameFromStdString(cmd.user_name());
QString clientId = nameFromStdString(cmd.clientid());
qDebug() << "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();
auto parsedEmailAddress = parseEmailAddress(nameFromStdString(cmd.email()));
const QString emailUser = parsedEmailAddress.first;
const QString emailDomain = parsedEmailAddress.second;
#if (QT_VERSION >= QT_VERSION_CHECK(5, 14, 0))
const QStringList emailBlackListFilters = emailBlackList.split(",", Qt::SkipEmptyParts);
const QStringList emailWhiteListFilters = emailWhiteList.split(",", Qt::SkipEmptyParts);
#else
const QStringList emailBlackListFilters = emailBlackList.split(",", QString::SkipEmptyParts);
const QStringList emailWhiteListFilters = emailWhiteList.split(",", QString::SkipEmptyParts);
#endif
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;
}
QString emailAddress = emailUser + "@" + emailDomain;
if (servatrice->getMaxAccountsPerEmail() > 0 &&
sqlInterface->checkNumberOfUserAccounts(emailAddress) >= 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, emailAddress,
country, !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(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
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)) {
qDebug() << "Accepted activation for user" << userName;
if (servatrice->getEnableRegistrationAudit())
sqlInterface->addAuditRecord(userName.simplified(), this->getAddress(), clientId.simplified(),
"ACTIVATE_ACCOUNT", "", true);
return Response::RespActivationAccepted;
} else {
qDebug() << "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());
QString emailAddress = 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()) {
QString realName = nameFromStdString(cmd.real_name());
query->bindValue(":realName", realName);
}
if (cmd.has_email()) {
QString emailAddress = nameFromStdString(cmd.email());
query->bindValue(":email", emailAddress);
}
if (cmd.has_country()) {
QString 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(emailAddress.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;
}
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;
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());
qDebug() << "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());
qDebug() << "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->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());
qDebug() << "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;
}
if (!sqlInterface->validateTableColumnStringData("{prefix}_users", "email", userName,
nameFromStdString(cmd.email()))) {
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;
}
// 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 and/or judge status has been revoked.");
event.set_end_time(QDateTime::currentDateTime().toSecsSinceEpoch());
SessionEvent *se = user->prepareSessionEvent(event);
user->sendProtocolItem(*se);
delete se;
}
QMetaObject::invokeMethod(user, "prepareDestroy", Qt::QueuedConnection);
return true;
}
Response::ResponseCode AbstractServerSocketInterface::cmdAdjustMod(const Command_AdjustMod &cmd,
ResponseContainer & /*rc*/)
{
QString userName = nameFromStdString(cmd.user_name());
if (cmd.has_should_be_mod()) {
if (cmd.should_be_mod()) {
if (!addAdminFlagToUser(userName, 2)) {
return Response::RespInternalError;
}
} else {
if (!removeAdminFlagFromUser(userName, 2)) {
return Response::RespInternalError;
}
}
}
if (cmd.has_should_be_judge()) {
if (cmd.should_be_judge()) {
if (!addAdminFlagToUser(userName, 4)) {
return Response::RespInternalError;
}
} else {
if (!removeAdminFlagFromUser(userName, 4)) {
return Response::RespInternalError;
}
}
}
return Response::RespOk;
}
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();
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;
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), 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);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
// https://bugreports.qt.io/browse/QTBUG-70693
socket->setMaxAllowedIncomingMessageSize(1500000); // 1.5MB
#endif
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 = 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();
emit 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();
}