mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-04-27 07:48:01 -07:00
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
2005 lines
83 KiB
C++
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();
|
|
}
|