Commit graph

102 commits

Author SHA1 Message Date
AI-scor
eb5672014d
Apply nullptr across the tree (#7890)
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 / vcpkg baseline (push) Waiting to run
Build Desktop / Debian 13 (push) Blocked by required conditions
Build Desktop / Fedora 45 release (push) Blocked by required conditions
Build Desktop / Fedora 45 debug (push) Blocked by required conditions
Build Desktop / Servatrice_Debian 12 (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 / Debian 12 (push) Blocked by required conditions
Build Desktop / Fedora 44 (push) Blocked by required conditions
Build Desktop / Ubuntu 24.04 (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 sanitize (push) Blocked by required conditions
Build Desktop / Ubuntu 26.04 cover (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 27 Debug (push) Blocked by required conditions
Build Desktop / Windows 10 (push) Blocked by required conditions
Build Desktop / macOS 13 Intel (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
2026-11-10 20:48:18 +01:00
AI-scor
e97d3e5519
[Server] Clarify the guards in Server::loginUser (#7886) 2026-11-10 20:40:00 +01:00
AI-scor
223ab844a0
[Server] Clarify pingClockTimeout rate limits and idle timeout (#7881) 2026-11-10 18:51:54 +01:00
AI-scor
a0113d0e8a
[Refactor] Name the gates in Server_AbstractPlayer::moveCard (#7879) 2026-11-10 18:25:24 +01:00
AI-scor
9dc6483f1c
[Refactor] Clarify the restriction checks in Server_Game::checkJoin (#7876) 2026-11-10 16:21:50 +01:00
BruebachL
c4ac80e282
[Server] Create the worker database interfaces through the factory (#7860) 2026-11-10 12:16:34 +01:00
BruebachL
0caacb27c1
[Server] Move the database lifecycle calls onto the interface (#7859) 2026-11-10 12:16:34 +01:00
BruebachL
526d528e96
[Server] Serve the e-mail queue through the database interface (#7858) 2026-11-10 12:16:33 +01:00
BruebachL
9087d0d3fe
[Server] Serve the bootstrap rooms through the database interface (#7857) 2026-11-10 12:16:33 +01:00
BruebachL
09d0bed997
[Server] Record uptime through the database interface (#7856) 2026-11-10 12:16:33 +01:00
BruebachL
f1ee6bdc2d
[Server] Serve the server list through the database interface (#7855) 2026-11-10 12:16:32 +01:00
BruebachL
f8f8fffa6b
[Server] Serve the login message through the database interface (#7854) 2026-11-10 12:16:32 +01:00
BruebachL
99bdcebf2e
[Server] Serve deck storage listings through the database interface (#7851)
deckListHelper still prepared the folder and deck-file statements inline through
Servatrice_DatabaseInterface, and the four deck commands reached checkSql,
getUserIdInDB and getDeckFromDatabase through a concrete pointer. Add
DeckListFolderRecord, DeckListFileRecord, getDeckListFolders and
getDeckListFiles to Server_DatabaseInterface with fail-closed defaults, serve
all four commands from the injected interface, and drop the sqlInterface member
so the handler no longer holds a database implementation at all. Folder
ordering, recursion order, public-only pruning and every response code are
unchanged.
2026-11-10 12:16:30 +01:00
BruebachL
1822d16a72
[Server] Serve card-art rules and params through the database interface (#7850)
The four card-art commands still prepared their statements inline through
Servatrice_DatabaseInterface: the two card_art_params updates in
cmdSetCardArtParams, the rule upsert and delete, and the rule listing that
mapped rows to protocol entries. Add CardArtRuleRecord, upsertCardArtRule,
removeCardArtRule, getCardArtRules, setCardArtParams and clearCardArtParams to
Server_DatabaseInterface with fail-closed defaults, implement them with the
previous statements, and keep the validation, clamping, in-memory userInfo sync
and broadcast in the handler.
2026-11-10 12:16:30 +01:00
BruebachL
2608228bdd
[Server] Serve account moderation through the database interface (#7849)
The moderator account commands still reached Servatrice_DatabaseInterface through
its concrete class: cmdResetUserPassword ran the privilege lookup inline and
called the concrete-only changeUserPasswordWithLegacyBackup, while the avatar,
banner and blacklisted-banner commands called concrete-only helpers. Promote
those four methods to Server_DatabaseInterface, move RemoveUserBannerResult into
the interface, and add getUserAdminFlags for the reset privilege lookup. The
escalation guard, audit records, had_banner response field and every response
code stay in the handler.
2026-11-10 12:16:29 +01:00
BruebachL
f7c6e81347
[Server] Serve account self-service updates through the database interface (#7848)
cmdAccountEdit assembled a dynamic UPDATE for the real name, email and country
columns in the socket handler, and cmdAccountImage wrote the avatar blob there
too. Add AccountUpdateRecord, updateUserAccount and setUserAvatar to
Server_DatabaseInterface with fail-closed defaults, implement them with the
previous query text and bind values, and keep the password gate, the email
permission check, the empty-edit early return and the in-memory userInfo updates
in the handler.
2026-11-10 12:16:29 +01:00
BruebachL
8c8dd0c072
[Server] Serve admin actions through the database interface (#7847)
cmdBanFromServer, the admin-flag helpers, cmdForceActivateUser and the
admin-notes commands still prepared their statements inline through
Servatrice_DatabaseInterface, and addWarning was only reachable through the
concrete Servatrice class. Add setAdminFlag, getUserToken, getAdminNotes,
setAdminNotes, addBan and getUserNamesByClientId to Server_DatabaseInterface
with fail-closed defaults, promote addWarning, and model the outcomes as
UserFieldResult and AdminFlagUpdateResult. Notifications, the audit record,
the ban disconnect fan-out and every response code stay in the handler.
2026-11-10 12:16:28 +01:00
BruebachL
948035af06
[Server] Serve report mutations through the database interface (#7846)
cmdReportAssign, cmdReportResolve, cmdReportReopen and cmdReportAddComment still
ran their lookups and updates inline through Servatrice_DatabaseInterface. Add
assignReport, resolveReport, reopenReport, getUserNameById,
getReportNotificationTarget, countReportCommentsInLastHour,
getReportCommentTarget and addReportComment to Server_DatabaseInterface with
fail-closed defaults, and model the outcomes as ReportAssignResult and
ReportUpdateResult so "no such report", "wrong state" and "query failed" stay
distinguishable. Authorization checks, audit records, notification recipients
and every response code stay in the handler.
2026-11-10 12:16:28 +01:00
BruebachL
a54e13756c
[Server] Serve report aggregates through the database interface (#7845)
cmdReportStats and cmdReportUserInfo still ran their queries inline through
Servatrice_DatabaseInterface and built the response in the socket handler. Add
ReportStatsRecord, ReportUserInfoRecord and their count records to
Server_DatabaseInterface with fail-closed defaults, implement getReportStats and
getReportUserInfo with the previous statements and query order, and keep
presence flags for the fields the old code set only on a successful query so
the proto2 wire output is unchanged. The stats cache decision and every response
code stay in the handler.
2026-11-10 12:16:28 +01:00
BruebachL
2ba80cb61d
[Server] Serve report list views through the database interface (#7844)
cmdReportList, cmdReportMyList and cmdReportDetails still prepared their SELECTs
inline through Servatrice_DatabaseInterface and mapped the result columns to
protocol records in the socket handler. Add getReportList,
getReportsSubmittedBy, getReportForDetails and getReportComments to
Server_DatabaseInterface with fail-closed defaults and a ReportLookupResult
outcome, implement them with the previous statements, joins and null handling,
and keep the detail authorization check and the tolerated comment-query failure
in the handler in the same order. Response codes are unchanged.
2026-11-10 12:16:27 +01:00
BruebachL
fb836c6e0c
[Server] Serve report submission through the database interface (#7843)
cmdReport and sendPendingReportNotifications still issued their SQL inline
through Servatrice_DatabaseInterface: the daily report cap, the reported-user
lookup, the report insert, both pending-notification selects and the notified
flips. Add the two notification records and seven report methods to
Server_DatabaseInterface with fail-closed defaults, implement them with the
previous statements and binds, and let both functions ask the injected
interface. Check order, response codes and notification payloads are unchanged.
2026-11-10 12:16:27 +01:00
BruebachL
9b00972b4d
[Server] Serve password reset from the database interface (#7842)
cmdForgotPasswordRequest, continuePasswordRequest, cmdForgotPasswordReset,
cmdForgotPasswordChallenge, cmdRequestPasswordSalt and cmdSubmitPasswordVerifier
still called Servatrice_DatabaseInterface, and two of them validated the account
token and email through the generic validateTableColumnStringData helper. Add
doesForgotPasswordExist, addForgotPassword and getUsableLegacyBackup to
Server_DatabaseInterface with fail-closed defaults, replace the table/column
probes with userTokenMatches and userEmailMatches using the same statement and
case-insensitive comparison, and let the commands ask the injected interface.
Audit records, check order and every response code are unchanged.
2026-11-10 12:16:26 +01:00
BruebachL
d05198101d
[Server] Serve account registration from the database interface (#7841)
cmdRegisterAccount and cmdActivateAccount still called
Servatrice_DatabaseInterface for username validation, user existence, the
per-email account limit, ban checks, registration and activation, and
cmdRegisterAccount issued the activation-email insert inline. Add
addActivationEmail to Server_DatabaseInterface with a false default, implement
it with the same statement and bind value, and let both commands ask the
injected interface. Validation order and every response code are unchanged.
2026-11-10 12:16:26 +01:00
BruebachL
7719ff021c
[Server] Expose the latest uptime snapshot on the database interface (#7840)
cmdGetServerStats reached through Servatrice_DatabaseInterface for checkSql and
getLatestUptimeSnapshot, a concrete-only method. Move the UptimeSnapshot record
to Server_DatabaseInterface with an invalid default for non-SQL stores, mark the
Servatrice implementation as an override, and let the command ask the injected
interface. The statement, its bind value and the invalid-snapshot response are
unchanged.
2026-11-10 12:16:25 +01:00
BruebachL
03adbd9787
[Server] Move replay code exchange into the database interface (#7839)
createHashForReplay, cmdReplayGetCode, cmdReplaySubmitCode,
cmdReplayDownloadByGameId and cmdGrantReplayAccess still issued their own SQL
through Servatrice_DatabaseInterface. Add checkReplayAccess, getReplayName,
getReplayHashSource, addReplayAccess, getLatestReplayForGame and
grantReplayAccessByName to Server_DatabaseInterface, implement them with the
same statements, bind order and response mapping, and let the commands ask the
injected interface. Hashing and code parsing stay in the handler; the shared
insert is reused by both grant paths.
2026-11-10 12:16:25 +01:00
BruebachL
b2e69d3443
[Server] Move replay access reads and writes into the database interface (#7838)
cmdReplayList, cmdReplayDownload, cmdReplayModifyMatch and cmdReplayDeleteMatch issued their own SQL through Servatrice_DatabaseInterface. Add ReplayMatchRecord/ReplayEntry and the ReplayAccessResult/ReplayWriteResult enums to Server_DatabaseInterface, implement the list, data, do-not-hide and delete operations with the same statements and response mapping, and let the commands ask the injected interface. checkSql stays in the two write commands where it already was.
2026-11-10 12:16:24 +01:00
BruebachL
ba037cbf04
[Server] Move deck share storage into the database interface (#7837)
The five deck-share commands reached through Servatrice_DatabaseInterface for share lookups, and cmdDeckShareCreate still issued the per-user rate-limit count and the folder deck listing inline. Move DeckShareItemRecord and DeckShareSummaryRecord to Server_DatabaseInterface, declare the share operations plus getDeckSharesCreatedToday and getDeckFilesInFolder there with failing defaults, and switch the commands to the injected interface. The MySQL statements and every response code stay as they were, including the -1-on-failure sentinel for the count.
2026-11-10 12:16:24 +01:00
BruebachL
417e7f7db4
[Server] Expose the audit log on the database interface (#7835)
addAuditRecord existed only on Servatrice_DatabaseInterface, so 37 moderator and admin commands reached through the concrete class just to append a log row. Declare it on Server_DatabaseInterface with an empty default, mark the Servatrice implementation as an override, and switch the handler's call sites to the injected interface. No SQL moves; the argument hashes are unchanged.
2026-11-10 12:16:24 +01:00
BruebachL
7734a94f83
[Server] Move deck file writes into the database interface (#7834)
Deleting a deck, inserting an uploaded deck, and updating one in place were SQL statements in the socket handler, including the dynamic is_public clause and the follow-up visibility read. Add DeckFileData and DeckFileWriteResult to Server_DatabaseInterface with create/update/delete operations, implement them in Servatrice_DatabaseInterface with the same statements and bind order, and let cmdDeckUpload and cmdDeckDel ask the injected interface.
2026-11-10 12:16:23 +01:00
BruebachL
23781af1f0
[Server] Move deck folder writes into the database interface (#7833)
Setting a deck or folder public, creating a folder, and deleting a folder tree were SQL statements in the socket handler. Declare them on Server_DatabaseInterface with a DeckStorageResult so the caller can still tell a missing row from a failed write, implement them in Servatrice_DatabaseInterface with the same statements, and let the three folder commands ask the injected interface. Readiness probing keeps its previous ignore-the-result shape.
2026-11-10 12:16:23 +01:00
BruebachL
8c0e796b0d
[Server] Move deck-path and visibility lookups into the database interface (#7832)
Resolving a deck folder path, reading a deck's owner, and walking its folder chain for public visibility were SQL queries living in the socket handler, keyed to the logged-in user through userInfo. Declare the three lookups on Server_DatabaseInterface, implement them in Servatrice_DatabaseInterface with the user id as an explicit parameter, and let the deck commands ask the injected interface.
2026-11-10 12:16:22 +01:00
BruebachL
28f92ac5f5
[Server] Expose chat-log reads on the database interface (#7830)
getMessageLogHistory, getPrivateChatHistory, and getUserIdInDB were reachable only through Servatrice_DatabaseInterface, so the two log-history commands in the socket handler needed the concrete class. Declare the two readers on Server_DatabaseInterface with empty/false defaults, mark the Servatrice implementations as overrides, and resolve the private-chat peer through the already-promoted getUserId.
2026-11-10 12:16:21 +01:00
BruebachL
2ca3117765
[Server] Expose moderation history reads on the database interface (#7829)
The ban/warn history, session, alt-account, and moderator-login readers existed only on Servatrice_DatabaseInterface, so the socket handler had to hold the concrete class to call them and to ask checkSql() whether the store was reachable. Declare them on Server_DatabaseInterface with empty-list defaults and a checkSql() that reports ready for stores without a connection, mark the Servatrice implementations as overrides, and let those five commands ask the injected interface.
2026-11-10 12:16:21 +01:00
BruebachL
52d8f84ec8
[Server] Move buddy and ignore list writes into the database interface (#7827)
cmdAddToList/cmdRemoveFromList issued raw SQL against the buddy and ignore tables and looked accounts up through getUserIdInDB, a method that only exists on the MySQL implementation. Add getUserId plus add/remove operations to Server_DatabaseInterface, implement them in Servatrice_DatabaseInterface over the shared single-row helpers, and let the handler ask the injected interface instead of the concrete class.
2026-11-10 12:16:20 +01:00
SwitchOCE
7709184ecc
[Server] Let users fetch their own private chat history (#7506)
* [Server] Let users fetch their own private chat history

Players lose their private conversations on reconnect or on another
device. Servatrice can already log private messages, but only
moderators can read the logs.

Add Command_GetPrivateChatHistory / Response_PrivateChatHistory so a
registered user can page through their own logged conversation with one
other user. Operators opt in with logging/allow_user_chat_history
(default off, and log_user_msg_chat must be on). The caller is matched
by account id so reused guest or deleted-account names can't read older
messages; only time, sender name and text are returned. Pages hold
1..200 messages and never split a second, since log_time has 1-second
resolution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* [Server] Harden private chat history identity matching

Match both participants by account id and name so a federated user's remote id cannot collide with a local account, and stop storing foreign ids in the local chat log. Extract the paging assembly into a pure helper and cover the whole-second boundary rules with gtests.

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-09 13:21:43 +01:00
Bryce!
078d0858a3
[Game] Reveal multiple distinct random hand cards to a recipient (#7753)
* Add distinct random hand reveals with private recipient delivery

* Route random hand reveals through PlayerActions

Keep command creation, dispatch and response handling in the game action
layer. HandMenu requests the typed operation and presents the count and
failure dialogs. Show the chosen recipient, distinguish the count action
from the immediate single-card reveal, and use the game window as parent.

* Apply project formatting to random-reveal warning construction

* Simplify random reveal to follow existing command patterns

Forward server dispatch through cmdRevealRandomCards and use ordinary PlayerActions command sending. Remove the bespoke server-version error UI and long-lived-object guards.

Reduce the test from 246 to 150 lines with two focused cases using the current RoleInfo player API, preserving distinct-card, private-event and rejected-count coverage.

* Extract the shared reveal helper and name the test's CardRef fields

RickyRister asked for cmdRevealCards and cmdRevealRandomCards to share a
helper instead of threading a randomCount parameter through the reveal
virtual. Restore the three-argument cmdRevealCards signature, resolve the
reveal preconditions and zone in resolveRevealZone, and let both commands
build their card list and hand it to revealCards, which owns the private and
public Event_RevealCards construction and the enqueue. The random command no
longer fabricates a Command_RevealCards with the -2 sentinel to re-enter the
legacy path; its behaviour is unchanged.

Name the CardRef fields in random_reveal_test instead of relying on
declaration order, and state in the user documentation that a rejected
reveal is not reported in the client, matching what the client does.

---------

Co-authored-by: Lukas Brübach <44814898+BruebachL@users.noreply.github.com>
2026-11-08 16:41:14 +01:00
BruebachL
5ea16c805f
[Server/Client/Protocol] Shuffle or reorder player seats before a game starts (#7398)
* [Protocol] Add shuffle players option and player order command/event

Adds a 'Shuffle players at start' option to game creation plus a local
player id on the game state so clients can reconcile seat changes, and a
SET_PLAYER_ORDER command (1206) with a PLAYER_ORDER_CHANGED event (2023)
so the game host can set or randomize the seating order.

Took 6 minutes

* [Server] Shuffle and reorder player seats before a game starts

Auto-shuffles the seats at game start when enabled (now for games with
two or more players instead of three or more) and handles the new
SET_PLAYER_ORDER command so the game host or a moderator can reorder or
randomize the seats before the game starts. Re-seating re-keys the
player ids while keeping the per-user game/seat routing and persistent
player records in sync, then broadcasts the new order to everyone.

* [Client] Add shuffle players option and turn order dialog

Adds the 'Shuffle players at start' checkbox to game creation and a
host-only 'Set turn order...' action that opens a dialog to reorder or
randomize the seats before the game starts. Seating changes are shown in
the game's message log.

* [Client] Move the turn order dialog strings into retranslateUi

Follows the DlgInviteToGame convention so the dialog can be retranslated
instead of freezing its strings at construction time.

* [Server] Keep player ids stable across seat shuffles

* [Client] Lay out table and turn order dialog in seat order

* [Server] Send game-joined player list in seat order

* [Client] Reword the shuffle option to match its behaviour

* [Client] Address the seat-order review comments

- Give the game setup checkboxes a row of their own so they no longer
  collide in the create-game dialog
- Rename DlgTurnOrder::randomize() to isRandomizeRequested()
- Document what the ids in GameState::seatOrder mean
- Keep "Set turn order..." in sync with the game: hidden for non-hosts
  and once the game has started, re-shown when the game ends or hosting
  moves to the local player (new GameState::hostChanged signal)

* [Client] Do not preview a randomized turn order

Randomizing is a request to the server, not a local shuffle. Shuffling the
list locally meant the dialog showed an order the server then discarded, and
let the host re-open it until a favourable order appeared. The dialog now
switches to a mode that says the server will randomize and toggles back to the
manual list on request.

* [Client] Let moderators set the turn order

The server accepts SET_PLAYER_ORDER from the host or a moderator, but the
client hid the action from everyone except the host, making the moderator
branch unreachable. Visibility and the handler now share canSetTurnOrder().

* [Server] Rely on the player order event instead of a full state resend

cmdSetPlayerOrder re-sent the whole game state after reseating, so every
client reloaded its deck view mid-lobby for no reason. The new seating reaches
clients through the Event_PlayerOrderChanged that reseatPlayers broadcasts.

* [Client] Show random turn order in the game selector

The restrictions column listed "open decklists" but not the new seating
option, so a joining player had no way to see that seats will be randomized
each game. Game info already shows the disabled checkbox.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-04 15:44:50 +01:00
RickyRister
62e590d300
[ServerGame] Refactor out spectator/judge bools into RoleInfo (#7761) 2026-11-02 00:37:05 -07:00
BruebachL
a74dfeef6e
[Build] Configure AUTOMOC, AUTOUIC and AUTORCC once (#7637)
The root already set CMAKE_AUTOMOC; add CMAKE_AUTOUIC and CMAKE_AUTORCC
next to it and drop the per-directory copies from 18 CMakeLists files.
These variables are inherited by the subdirectories, so the duplicates
never changed anything.

qt_standard_project_setup is not used: besides the AUTO* variables it
sets CMAKE_RUNTIME_OUTPUT_DIRECTORY on Windows, which breaks the
install(DIRECTORY "$<TARGET_FILE_DIR:...>") copies and the per-app
symbol paths, and appends to CMAKE_INSTALL_RPATH on Linux.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-02 07:06:47 +01:00
BruebachL
50a2ae9cc8
[Server/Client] Enforce and complete the forced password change at login (#7680)
checkUserPassword ignored force_password_change on the challenge-response
path, so a reset password stayed valid. Returning PasswordChangeRequired
was not enough on its own: the session was dropped and the client told the
user to ask the administrator for the temporary password they had just used.

The pending session now keeps its identity without registering a user
session or announcing a join, may only ping and change its password, and
the client completes the change in-session before reconnecting.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-11-02 07:01:29 +01:00
BruebachL
52d3978f84
[Build] Let AUTOMOC process library headers (#7638)
The 13 libraries using qt6_wrap_cpp(MOC_SOURCES ${HEADERS}) listed their\nheaders twice and depended on a generated source list. Add the headers to the\ntarget sources directly and delete the qt6_wrap_cpp call; AUTOMOC scans them\nand generates the same moc output.\n\nThe change exposed that server_card_counter_test never defined the rng global\nthat the server remote library references; every sibling server test does.\nGive the test the same definition so the target links.\n\nVerification: full client, Oracle, server and test build passed.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-31 18:49:05 +01:00
BruebachL
b4e24f958a
[Network] Do not format discarded debug messages, parse without memmove (#7606)
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
* [Network] Do not format discarded debug messages, parse without memmove

Protocol debug logging gets the full protobuf message rendered through
TextFormat and reflection. On the server this happened before every
session/room/game/moderator/admin/developer command, even though the
default Server_ProtocolHandler::logDebugMessage() is a no-op and even
when servatrice has no log file configured. On the clients the same
string was built as an argument to qCDebug(), which evaluates its
arguments regardless of whether the category is enabled.

Server_ProtocolHandler now exposes a virtual logsDebugMessages() hook.
The base implementation returns false, so handlers without a log
destination spend nothing. AbstractServerSocketInterface overrides it
with ServerLogger::isLoggingEnabled(), which checks the log file and the
server/writelog setting that logMessage() itself consults. All debug
message call sites check the hook before formatting. The client
send/receive paths check QLoggingCategory::isDebugEnabled() first, the
same pattern AbstractClient already uses for its slow-command warning.
ISL message decoding does the same.

Byte stream decoding called inputBuffer.remove(0, n) for the length
header and then again for every message. Each call shifts all remaining
bytes, so a read carrying k messages cost O(k * bytes remaining). The
three receive loops (remote client, TCP server interface, ISL interface)
now parse at a read offset and compact the consumed prefix once, when
they stop because a full message is not buffered yet. No buffering or
size-limit behavior changes.

Also in this batch:
- Server_ProtocolHandler::processGameCommandContainer() copied the whole
  games map under gameListMutex just to look up one game id. A new
  getGameInfo() performs the single lookup and returns the room/player
  ids.
- The TCP receive loop's `ok` flag is initialized, so a ParseFromArray
  exception can no longer leave it indeterminate before `if (ok)`.
- The exception log prints only the message being parsed instead of
  hex-dumping the entire pending buffer.

Full build and ctest (628 tests) pass, including the server tests.

* [Network] Address review comments

- Move getGameInfo() out of the header into server_abstractuserinterface.cpp.
- Drop the consumed prefix once after the parse loop instead of on every
  early return: the two "not enough data" exits now break out of the loop.
  Applied to the remote client, TCP server and ISL receive loops.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-25 12:49:24 +01:00
BruebachL
a139084ad6
[Security] Add challenge-response auth with scrypt verifiers and stop storing plaintext passwords (#7193)
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 challenge-response auth with scrypt verifiers and stop storing plaintext passwords

Challenge-response authentication: the client derives a scrypt verifier
(RFC 7914, EVP_PBE_scrypt, N=32768, r=8, p=1) and authenticates with
HMAC-SHA256(key, nonce), so neither the password nor its hash is
transmitted. The stored format becomes
"$scrypt$<n>$<r>$<p>$<salt>$<verifier>" and Response_PasswordSalt
now carries the cost parameters. Strict servers only accept scrypt
verifiers; legacy accounts are migrated after a successful login.

Fix #344 for challenge-response servers: a saved profile stores the
derived verifier under the password key instead of the plaintext password.
The connect dialog loads it without revealing it, autoconnect passes it
through, the change-password dialog no longer prefills the old password
field with it, and the client only persists the verifier when
"Save password" is checked.

Took 3 minutes

Took 1 minute

Took 10 seconds

Took 7 minutes

* [Security] Harden credential format acceptance and challenge nonce validation

* [Security] Refuse KDF downgrade; trim profile name and warn on silent credential drop

* [Security] Add SupportsAll ServerOptions enumerator to fix enum validation assert

The server advertised both SupportsPasswordHash (1) and
SupportsChallengeResponseAuth (2) via a bitwise-OR that produced the
value 3, which no enumerator declared. Protobuf's proto2 enum
validation rejects the value in _internal_set_server_options, crashing
servatrice on the debug assertion. Add a SupportsAll = 3 enumerator with
an identical wire value so the bitmask reads unchanged while the value
is now a documented, valid member of the enum.

* [Security] Normalize username when issuing challenge nonce

cmdRequestPasswordSalt bound the challenge nonce to the raw, un-simplified
username while cmdLogin verifies it against the simplified name. A user
who typed leading or trailing whitespace in the username box would be
silently rejected during challenge-response login even with the correct
password. Simplify at the nonce-issuing site so both commands agree on
the normalized name.

Also drive-by formatting: collapse the now single-expression
serverOptions initializer onto one line.

* [Security] Keep an encrypted legacy password backup for old-client logins

Migrating an account to scrypt stranded pre-challenge clients, which can
only present the legacy salt+hash: after migration the server served them
a scrypt salt they cannot use, so the only recovery was a password reset.

When security/legacy_backup_key is configured (64 hex chars,
openssl rand -hex 32), submitPasswordVerifier now seals the legacy
credential with AES-256-GCM under that key before overwriting the row,
storing it in password_legacy_backup with the time it was last used. Old
clients keep working: cmdRequestPasswordSalt serves the backup's 16-char
salt in password_salt so they can derive the legacy hash they were
always able to compute, while new clients derive the verifier from the
new scrypt_salt field, which always carries the verifier salt. The
non-challenge login path accepts the plaintext or legacy hash against
the decrypted backup and refreshes its window; a migrated row with no
usable backup fails closed.

Backups age out so a database dump taken after the window closes never
carries a crackable legacy hash: getUsableLegacyBackup purges on first
use past security/legacy_backup_ttl_days (default 90, clamped 1..3650),
and a six-hourly sweep drops backups idle accounts never touch.
changeUserPassword clears the backup since a new credential invalidates
it. Without a configured key nothing is sealed or purged, so a
temporarily missing key cannot destroy existing backups.

The AAD is the simplified user name on both sides, sealing the blob to
its row so it cannot be moved between accounts. Sealing failures
degrade to the previous lockout behaviour rather than blocking
migration. The columns extend the unmerged 0036->0037 migration, so the
schema version stays 37.

* [Security] Address review: proto compat, plaintext verify, AAD case, strict scope

Fix the two blockers and follow-ups from the 2026-10-16 review:

- Advertise challenge-response in a new supports_challenge_response
  field instead of the ServerOptions enum and drop SupportsAll:
  proto2 enums are closed, so already-released clients parsed
  server_options = 3 as NoOptions and fell back to plaintext
  passwords. server_options stays exactly SupportsPasswordHash
  on the wire; old clients ignore the new field.
- Verify a plaintext password directly against a migrated scrypt
  row with verifyPassword() so email changes (cmdAccountEdit)
  and old clients sending password keep working without a
  legacy backup key. Pre-hashed values are still checked
  against the backup, and its TTL window is refreshed only
  when the backup is what actually matched.
- Use the stored row name as the AES-GCM AAD on both seal
  and open so a login whose case differs from the stored
  name (utf8mb4_unicode_ci) still opens its backup.
- Require challenge-response in strict mode only for
  registered SQL accounts (userExists gate), leaving
  guest/unregistered logins and none/password auth
  methods unaffected.
- Document in servatrice.ini.example that a captured
  legacy hash can take over a not-yet-migrated account
  and that strict only constrains registered SQL
  accounts. Schema version is 38.

* [Security] Raise legacy backup TTL default to 180 days

Gives operators and slow-to-upgrade users a longer downgrade
window for pre-challenge clients before unused encrypted
legacy password backups age out.

* [Security] Fix servatrice.cpp formatting per clang-format-18

* [Security] Add missing forward declaration for Command_SubmitPasswordVerifier

The cmdSubmitPasswordVerifier declaration references the proto-generated
type, which otherwise does not name a type in this header and breaks
every C++ build.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: BruebachL <bruebachl@cockatrice.example>
2026-10-16 22:52:26 +02:00
BruebachL
155f7b0143
[Servatrice] Relay room channel messages over ISL (#7480)
* [Servatrice] Relay room channel messages over ISL

* [Servatrice] Skip audit log for unknown ISL channel per review

- externalRoomSay drops messages for channels not in the local
  catalog, matching Server_Room::say, instead of logging what was
  never delivered

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-16 22:46:40 +02:00
BruebachL
ce51e7147d
[Servatrice] Log room channel messages in audit log (#7479)
* [Servatrice] Log room channel messages and bump schema to 38

* Fixup: show room_channel audit rows and keep migration nullable

The log history query returns room_channel rows, but the log tab
dropped them. Render them in the room table with an extra Channel
column. Also drop the NOT NULL on target_type from the 37->38
migration so migrated schemas match the nullable servatrice.sql
definition (and strict-mode ALTERs cannot fail on legacy NULL rows).
Update the channel tests to the flag-based privilege model: the admin
fixture now carries IsModerator alongside IsAdmin, as the database
layer assigns.

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-16 22:46:39 +02:00
BruebachL
05dcde69db
[Server] Gate room channels on privilege levels (#7477)
* [Server] Gate room channels on privilege levels

* [Server] Filter channel catalog per user, split read/write gating, add banners

- Server_Room::getInfo() now takes the requesting user and only lists
  channels that user may read. Previously every room member received the
  full catalog (e.g. the moderators channel) while message delivery was
  already gated, so restricted channels were visible but silent.
- Split channel access into mayReadChannel/mayWriteChannel: history
  replay, catalog listing, and event fan-out use the read level
  (access_level); Command_RoomChannelSay uses the write level
  (write_access_level, falling back to the read level when unset).
- Plumb write_access_level and banner_message through both room sources
  (SQL rooms_channels table and config-file channels via writeaccess
  and banner keys).
- Schema 38 -> 39: add write_access_level (NULL means inherit the read
  level) and banner_message columns; also widen access_level to the
  full six-value privilege enum to match the 37 -> 38 migration.

* Fixup: evaluate channel access per user flag instead of ladder

User flags are independent bits: IsDeveloper does not imply
IsModerator/IsAdmin, so developer-only users must not reach
moderator/admin channels. Replace the userPrivilegeLevel ladder with
hasChannelPrivilege: moderator channels need IsModerator (admins hold
it alongside IsAdmin), admin channels need IsAdmin, developer channels
need IsDeveloper or IsAdmin, judge channels admit IsJudge/IsModerator.
Covers catalog filtering, history replay, write gating and fan-out,
and corrects the ini example and proto docs.

* [Server] Make channel privilege helpers static per review

* [Server] Fail closed on unknown channel access levels per review

- roomChannelAccessFromKey returns nullopt for unknown keywords
  instead of silently falling back to Public
- skip channels with unknown read/write levels with qWarning, empty
  read defaults to Public, empty write means no override
- mayWriteChannel now also requires read access so write-below-read
  configs cannot post into invisible channels

* [Server] Remove stray conflict marker in channel replay comment

* [Server] Fix servatrice.cpp formatting per clang-format-18

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: BruebachL <bruebachl@cockatrice.example>
2026-10-16 22:46:38 +02:00
BruebachL
17d2e8d86e
[Server] Add room chat channels with per-channel fan-out and history (#7476)
* [Server] Add room chat channels with per-channel fan-out and history

* Fixup: redact all channel histories on message removal

removeSaidMessages only redacted the main history, so banned/warned
content stayed visible to late joiners on every other channel.
Drop the unused channelId parameter and redact up to amount messages
per channel across the whole channelChatHistory map.

* [Server] Validate room channel ids and fix catalog schema per review

- servatrice.sql: expand rooms_channels access_level enum to full set
  to match 0037->0038 migration
- servatrice.cpp: skip empty, reserved (__main__) and duplicate channel
  ids when loading from ini and DB with qWarning
- server_protocolhandler.cpp: clarify catalog replay comment (filtering
  happens in moderator stack)
- server_room: drop duplicate channelIndex, linear search preserves
  catalog order

* [Server] Fix server_room.cpp formatting per clang-format-18

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
Co-authored-by: BruebachL <bruebachl@cockatrice.example>
2026-10-16 22:46:38 +02:00
BruebachL
b11b096828
[Server] Apply card art rules to playmats (#7532)
* [Server] Apply card art rules to playmats

Card art DENY rules were only enforced for user card art
(SetCardArtParams) while playmats using the same art bypassed them.

Move the rule lookup to Servatrice_DatabaseInterface::isCardArtAllowed
with a default-allow base on Server_DatabaseInterface, delegate the
session check to it, reject DENY-listed Command_SetPlaymat with
RespFunctionNotAllowed, and strip DENY-listed deck playmats on
DeckSelect so banned art is never broadcast.

* [Server] Address review nits on playmat card art rules

---------

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-16 19:50:43 +02:00
BruebachL
456d9d9e70
[Server] Inform client of login/registration rate limit timeout (#7468)
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>
2026-10-16 05:47:43 +02:00
BruebachL
cce606764c
[Server] Resolve a zone dump request to an in-range index span (#7401)
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 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
Command_DumpZone.number_cards is an unbounded sint32 supplied by the client, and
cmdDumpZone used it as an index without ever putting it in range.

    for (int i = 0; (i < cards.size()) && (i < numberCards || numberCards == -1); ++i) {
        const auto &findId = cmd.is_reversed() ? cards.size() - numberCards + i : i;
        Server_Card *card = cards[findId];

A forward request happened to be safe, because the loop stopped at the zone's own size.
A reversed one was not. number_cards is documented by the client as -1 for "the whole
zone", and cards.size() - numberCards is therefore cardCount + 1, one past the last card.
A request larger than the zone puts the offset at a negative index instead. Both read
out of bounds, then dereference the result as a Server_Card, so either adjacent heap
memory or a crash.

Any player can reach it: the zone only has to be a public one, or the requester's own,
and both number_cards and is_reversed come straight off the wire. A reversed dump of -1
is enough on any public zone with cards in it.

Resolve the request to a half-open span up front. -1 and any positive count are clamped
to what the zone actually holds, any other negative count resolves to nothing, and a
reversed request takes the span from the end rather than computing an offset from a value
that may not be in range. cards.at() then carries the bound as a second line of defence,
and qsizetype keeps the loop and the list's own indexing type aligned.

Also report card_count as the zone's real size. It was
numberCards < cards.size() ? cards.size() : numberCards, which agrees with the zone size
for every request that was in range and reported the client's own oversized number
otherwise. The client uses card_count to draw placeholders when a zone lists no cards, so
overstating it invents cards that were never there.

Put the resolution in its own translation unit. server_abstract_player.cpp refers to the
RNG global that only servatrice's main.cpp defines, so anything linking that object needs
the whole server binary behind it, which is why no test could reach this code before.

server_dump_zone_test covers each request shape and then asserts the span is contained in
[0, cardCount) across the edges of the request domain, INT_MIN and INT_MAX included.
Reverting the arithmetic makes three of them fail, the two out-of-bounds shapes and the
containment sweep.

The client's own copy of this convention at view_zone.cpp:93 already clamps the way this
now does. Leaving that duplication for a follow-up, since the client reaching the same
answer by a different route is not a defect.

Co-authored-by: Lukas Brübach <Bruebach.Lukas@bdosecurity.de>
2026-10-13 11:23:13 +02:00