mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-05 01:23:24 -08:00
Some checks are pending
CodeQL / Analyze (cpp) (push) Waiting to run
CodeQL / Analyze (actions) (push) Waiting to run
Build Desktop / Configure (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Fedora 43 (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 (push) Blocked by required conditions
Build Desktop / Arch (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (push) Blocked by required conditions
Build Desktop / macOS 14 (push) Blocked by required conditions
Build Desktop / macOS 15 (push) Blocked by required conditions
Build Desktop / macOS 26 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / Prune caches (push) Waiting to run
Build Docker / Servatrice (arm) (push) Waiting to run
Build Docker / Servatrice (x86) (push) Waiting to run
Build Docker / Publish multi-platform Servatrice image (push) Blocked by required conditions
* [Security] Add per-address rate limiting for auth endpoints Introduce a thread-safe RateLimiter that tracks attempts per key (IP address) within a sliding time window, and wire it into the authentication endpoints: - Login: failed login attempts from an address are counted; once the configured maximum is exceeded within the window, further logins from that address are rejected with RespTooManyRequests. A successful login clears the failed attempts for that address. - Registration: implement the previously stubbed tooManyRegistrationAttempts, limiting how many accounts can be created per address per window. - Forgot-password: throttle both the email-request and the email-challenge paths per address. New [security] settings with defaults: max_login_attempts_per_ip=5 / login_attempt_window_seconds=900 max_registrations_per_ip=2 / registration_window_seconds=3600 max_forgot_password_requests_per_ip=3 / forgot_password_window_seconds=3600 Adds unit tests for the RateLimiter (window limit, over-limit blocking, clearing, per-key independence). Took 3 minutes * [Server] Fix rate limiter: disable semantics, draining window, bounded keys, pre-work login gate - recordAttempt()/isBlocked() return false when maxAttempts <= 0 or windowSeconds <= 0, so the documented 'set to 0 to disable' actually disables instead of blocking every address permanently - Keys at their limit are rejected without recording, so repeated attempts at the attacker's pace cannot keep sliding the window and holding the lockout open; a block now clears once the recorded attempts age out of windowSeconds - Attempts map is bounded: an opportunistic prune (every 60s) erases keys whose attempts have fully aged out - Login is throttled before the work: isLoginRateLimited() (a new virtual on Server, backed by isBlocked()) gates loginUser(), so a locked-out address no longer burns a database round trip and password verification per attempt - recordAttemptAt()/isBlockedAt() time seams make the sliding-window behaviour deterministically testable; tests rewritten for the real semantics (no reliance on the maxAttempts=0 bug, no sleeps) * [Server] Inform client of login/registration rate limit timeout - Add retryAfterSeconds/retryAfterSecondsAt to RateLimiter to compute seconds until oldest attempt exits the sliding window - Expose loginRateLimitRemainingSeconds via Server/Servatrice - Include denied_end_time (absolute epoch) in RespTooManyRequests for login pre-auth and when transitioning to lockout after failed attempt - Include denied_end_time in registration rate limit response - Client: show retry time for login RespTooManyRequests - Client: simplify registration RespTooManyRequests message to show retry time when provided - Add test for retryAfterSecondsAt * [Client] Restore registration guidance and drop duplicated rate limit reason The registration RespTooManyRequests case now only shows the retry-after copy when the server actually sent a denied_end_time; the per-email account cap path (no end time) falls back to the previous guidance about already having an account and contacting the server operator. Drop the canned denied_reason_str from the new login rate limit branches so the client no longer prints the same sentence twice. * [Server] Exempt trusted_sources from auth rate limits Addresses in security/trusted_sources bypass per-address limits, matching the existing max_users_per_address bypass in initSession. Without this, Webatrice behind local nginx (all clients as 127.0.0.1) would lock out every web user after 5 mistyped passwords, and cap the whole server at 2 registrations / 3 forgot-password requests per window. Adds Servatrice::isRateLimitExempt helper; recordFailedLogin, isLoginRateLimited, recordRegistrationAttempt and recordForgotPasswordAttempt all check it. # Conflicts: # servatrice/src/servatrice.cpp * [Server] Key login rate limit by (address, username) A successful login cleared the whole per-IP failure history, so 4 bad guesses plus 1 self-login (or guest UnknownUser login) from the same address bypassed the limiter. Failures are now bucketed per (address, lowercase username) and a success clears only its own pair. --------- Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
295 lines
9.1 KiB
C++
295 lines
9.1 KiB
C++
#ifndef SERVER_H
|
|
#define SERVER_H
|
|
|
|
#include "server_player_reference.h"
|
|
|
|
#include <QList>
|
|
#include <QMap>
|
|
#include <QMultiMap>
|
|
#include <QMutex>
|
|
#include <QMutexLocker>
|
|
#include <QObject>
|
|
#include <QReadWriteLock>
|
|
#include <QString>
|
|
#include <QtGlobal>
|
|
#include <qtmetamacros.h>
|
|
|
|
class Server_DatabaseInterface;
|
|
class Server_Room;
|
|
class Server_ProtocolHandler;
|
|
class Server_AbstractUserInterface;
|
|
class IslMessage;
|
|
class SessionEvent;
|
|
class RoomEvent;
|
|
class ServerInfo_Game;
|
|
class ServerInfo_Room;
|
|
class Response;
|
|
class GameEventContainer;
|
|
class CommandContainer;
|
|
class Command_JoinGame;
|
|
class QThread;
|
|
class ServerInfo_User;
|
|
|
|
enum AuthenticationResult
|
|
{
|
|
NotLoggedIn,
|
|
PasswordRight,
|
|
UnknownUser,
|
|
WouldOverwriteOldSession,
|
|
UserIsBanned,
|
|
UsernameInvalid,
|
|
RegistrationRequired,
|
|
UserIsInactive,
|
|
ClientIdRequired,
|
|
PasswordChangeRequired
|
|
};
|
|
|
|
class Server : public QObject
|
|
{
|
|
Q_OBJECT
|
|
signals:
|
|
void pingClockTimeout();
|
|
void sigSendIslMessage(const IslMessage &message, int serverId);
|
|
void endSession(qint64 sessionId);
|
|
private slots:
|
|
void broadcastRoomUpdate(const ServerInfo_Room &roomInfo, bool sendToIsl = false);
|
|
|
|
public:
|
|
mutable QReadWriteLock clientsLock, roomsLock; // locking order: roomsLock before clientsLock
|
|
explicit Server(QObject *parent = nullptr);
|
|
~Server() override = default;
|
|
AuthenticationResult loginUser(Server_ProtocolHandler *session,
|
|
QString &name,
|
|
const QString &password,
|
|
bool passwordNeedsHash,
|
|
QString &reason,
|
|
int &secondsLeft,
|
|
QString &clientid,
|
|
QString &clientVersion,
|
|
QString &connectionType);
|
|
void broadcastUserInfoUpdate(Server_ProtocolHandler *source);
|
|
|
|
const QMap<int, Server_Room *> &getRooms()
|
|
{
|
|
return rooms;
|
|
}
|
|
|
|
Server_AbstractUserInterface *findUser(const QString &userName) const;
|
|
const QMap<QString, Server_ProtocolHandler *> &getUsers() const
|
|
{
|
|
return users;
|
|
}
|
|
const QMap<qint64, Server_ProtocolHandler *> &getUsersBySessionId() const
|
|
{
|
|
return usersBySessionId;
|
|
}
|
|
virtual QMap<QString, bool> getServerRequiredFeatureList() const
|
|
{
|
|
return QMap<QString, bool>();
|
|
}
|
|
void addClient(Server_ProtocolHandler *player);
|
|
void removeClient(Server_ProtocolHandler *player);
|
|
QList<QString> getOnlineModeratorList() const;
|
|
virtual QString getLoginMessage() const
|
|
{
|
|
return QString();
|
|
}
|
|
virtual SessionEvent *getLoginSessionEvent() const
|
|
{
|
|
return nullptr;
|
|
}
|
|
virtual QString getRequiredFeatures() const
|
|
{
|
|
return QString();
|
|
}
|
|
virtual bool permitUnregisteredUsers() const
|
|
{
|
|
return true;
|
|
}
|
|
virtual bool getGameShouldPing() const
|
|
{
|
|
return false;
|
|
}
|
|
virtual bool getClientIDRequiredEnabled() const
|
|
{
|
|
return false;
|
|
}
|
|
virtual bool getRegOnlyServerEnabled() const
|
|
{
|
|
return false;
|
|
}
|
|
virtual bool getMaxUserLimitEnabled() const
|
|
{
|
|
return false;
|
|
}
|
|
virtual bool getEnableLogQuery() const
|
|
{
|
|
return false;
|
|
}
|
|
virtual bool getStoreReplaysEnabled() const
|
|
{
|
|
return true;
|
|
}
|
|
virtual int getIdleClientTimeout() const
|
|
{
|
|
return 0;
|
|
}
|
|
virtual int getClientKeepAlive() const
|
|
{
|
|
return 0;
|
|
}
|
|
virtual int getMaxGameInactivityTime() const
|
|
{
|
|
return 9999999;
|
|
}
|
|
virtual int getMaxPlayerInactivityTime() const
|
|
{
|
|
return 9999999;
|
|
}
|
|
virtual int getMessageCountingInterval() const
|
|
{
|
|
return 0;
|
|
}
|
|
virtual int getMaxMessageCountPerInterval() const
|
|
{
|
|
return 0;
|
|
}
|
|
virtual int getMaxMessageSizePerInterval() const
|
|
{
|
|
return 0;
|
|
}
|
|
virtual int getMaxGamesPerUser() const
|
|
{
|
|
return -1;
|
|
}
|
|
virtual int getCommandCountingInterval() const
|
|
{
|
|
return 0;
|
|
}
|
|
virtual int getMaxCommandCountPerInterval() const
|
|
{
|
|
return 0;
|
|
}
|
|
virtual int getMaxUserTotal() const
|
|
{
|
|
return 9999999;
|
|
}
|
|
virtual int getServerID() const
|
|
{
|
|
return 0;
|
|
}
|
|
virtual bool permitCreateGameAsJudge() const
|
|
{
|
|
return false;
|
|
}
|
|
/// Called once per actual game start with how long bringing every player's
|
|
/// zones online took, so servers can spot deck sizes that wedge threads.
|
|
virtual void observeGameStartDurationMs(qint64 /* elapsedMs */)
|
|
{
|
|
}
|
|
/**
|
|
* @brief Record a failed login attempt for the (address, username) pair; returns true if now locked out.
|
|
*
|
|
* Keyed by pair so a successful login as one user cannot wipe out another
|
|
* user's failure history from the same address (e.g. a guest login or the
|
|
* attacker's own account clearing the way for further guessing).
|
|
*/
|
|
virtual bool recordFailedLogin(const QString & /*ipAddress*/, const QString & /*userName*/)
|
|
{
|
|
return false;
|
|
}
|
|
/**
|
|
* @brief True if the (address, username) pair is already locked out of logging in.
|
|
*
|
|
* Consulted before any authentication work (database round trip, password
|
|
* verification) so a blocked pair cannot burn server CPU per attempt.
|
|
*/
|
|
virtual bool isLoginRateLimited(const QString & /*ipAddress*/, const QString & /*userName*/)
|
|
{
|
|
return false;
|
|
}
|
|
/**
|
|
* @brief Seconds remaining before the login rate limit for the (address, username) pair lifts.
|
|
* Returns 0 if not limited.
|
|
*/
|
|
virtual int loginRateLimitRemainingSeconds(const QString & /*ipAddress*/, const QString & /*userName*/)
|
|
{
|
|
return 0;
|
|
}
|
|
/** @brief Clear any failed-login lockout for the (address, username) pair, e.g. after a successful login. */
|
|
virtual void clearFailedLogins(const QString & /*ipAddress*/, const QString & /*userName*/)
|
|
{
|
|
}
|
|
|
|
Server_DatabaseInterface *getDatabaseInterface() const;
|
|
int getNextLocalGameId()
|
|
{
|
|
QMutexLocker locker(&nextLocalGameIdMutex);
|
|
return ++nextLocalGameId;
|
|
}
|
|
|
|
void sendIsl_Response(const Response &item, int serverId = -1, qint64 sessionId = -1);
|
|
void sendIsl_SessionEvent(const SessionEvent &item, int serverId = -1, qint64 sessionId = -1);
|
|
void sendIsl_GameEventContainer(const GameEventContainer &item, int serverId = -1, qint64 sessionId = -1);
|
|
void sendIsl_RoomEvent(const RoomEvent &item, int serverId = -1, qint64 sessionId = -1);
|
|
void sendIsl_GameCommand(const CommandContainer &item, int serverId, qint64 sessionId, int roomId, int playerId);
|
|
void sendIsl_RoomCommand(const CommandContainer &item, int serverId, qint64 sessionId, int roomId);
|
|
|
|
const QMap<QString, Server_AbstractUserInterface *> &getExternalUsers() const
|
|
{
|
|
return externalUsers;
|
|
}
|
|
|
|
void addPersistentPlayer(const QString &userName, int roomId, int gameId, int playerId);
|
|
void removePersistentPlayer(const QString &userName, int roomId, int gameId, int playerId);
|
|
QList<PlayerReference> getPersistentPlayerReferences(const QString &userName) const;
|
|
int getUsersCount() const;
|
|
int getGamesCount() const;
|
|
int getTCPUserCount() const
|
|
{
|
|
return tcpUserCount;
|
|
}
|
|
int getWebSocketUserCount() const
|
|
{
|
|
return webSocketUserCount;
|
|
}
|
|
|
|
private:
|
|
QMultiMap<QString, PlayerReference> persistentPlayers;
|
|
mutable QReadWriteLock persistentPlayersLock;
|
|
int nextLocalGameId, tcpUserCount, webSocketUserCount;
|
|
QMutex nextLocalGameIdMutex;
|
|
|
|
protected slots:
|
|
void externalUserJoined(const ServerInfo_User &userInfo);
|
|
void externalUserLeft(const QString &userName);
|
|
void externalRoomUserJoined(int roomId, const ServerInfo_User &userInfo);
|
|
void externalRoomUserLeft(int roomId, const QString &userName);
|
|
void externalRoomSay(int roomId, const QString &userName, const QString &message);
|
|
void externalRoomRemoveMessages(int roomId, const QString &userName, int amount);
|
|
void externalRoomGameListChanged(int roomId, const ServerInfo_Game &gameInfo);
|
|
void
|
|
externalJoinGameCommandReceived(const Command_JoinGame &cmd, int cmdId, int roomId, int serverId, qint64 sessionId);
|
|
void
|
|
externalGameCommandContainerReceived(const CommandContainer &cont, int playerId, int serverId, qint64 sessionId);
|
|
void externalGameEventContainerReceived(const GameEventContainer &cont, qint64 sessionId);
|
|
void externalResponseReceived(const Response &resp, qint64 sessionId);
|
|
|
|
virtual void doSendIslMessage(const IslMessage & /* msg */, int /* serverId */)
|
|
{
|
|
}
|
|
|
|
protected:
|
|
void prepareDestroy();
|
|
void setDatabaseInterface(Server_DatabaseInterface *_databaseInterface);
|
|
QList<Server_ProtocolHandler *> clients;
|
|
QMap<qint64, Server_ProtocolHandler *> usersBySessionId;
|
|
QMap<QString, Server_ProtocolHandler *> users;
|
|
QMap<qint64, Server_AbstractUserInterface *> externalUsersBySessionId;
|
|
QMap<QString, Server_AbstractUserInterface *> externalUsers;
|
|
QMap<int, Server_Room *> rooms;
|
|
QMap<QThread *, Server_DatabaseInterface *> databaseInterfaces;
|
|
void addRoom(Server_Room *newRoom);
|
|
};
|
|
|
|
#endif
|