Files
DarkflameServer/dDatabase/GameDatabase/SQLite/Tables/MessageCaptures.cpp
Aaron Kimbrell 332bc04ce8 feat(capture): record whole packets of an account, a character or everything on every server
Staff arm a packet capture on the dashboard; master passes MESSAGE_CAPTURE_CONTROL ARM to
every world, auth and chat and arms its own. Each server's PacketCapture tap (dServer receive,
and a send hook in RakPeer::Send so replica constructions are seen too) records into one
preallocated chunk per server and ships sealed chunks through master on the main loop when
capture_flush_bytes or capture_flush_interval_ms is reached; past capture_buffer_max_mb the
oldest chunks are dropped and the dashboard records a gap. Nothing is armed: one flag check.

- targets: an account (from its login; packets before the login are kept per connection
  and added once auth or the world knows whose they are), a character (from when it is
  picked), or everything; up to 8 at once (a bit each in the record mask)
- worlds and auth record their clients' packets and the master link messages of a captured
  player (session keys by name, zone transfers by request, player added/removed, migration);
  chat finds the player in each packet; master records server traffic for everything
- secrets are never recorded: structs that carry them (login request, login response user
  key, world validation session key, session key messages between servers) are read,
  blanked and written again before recording; auth keeps only the handshake and login
- PacketDecoder: a registry by service and message id names every packet and decodes the
  registered structs; CaptureBundle is the file format (DLUBNDL1, metadata, records);
  CaptureTools orders records on one timeline, pulls movement out, makes bundles portable
  or anonymous and diffs replays
- the dashboard keeps packet captures in message_capture_sessions (capture_kind 1) and
  their packets in a file under capture_dir, one write per batch; arming is audited
- MESSAGE_CAPTURE_CONTROL/DATA only gain appended enum values and trailing fields

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 22:34:34 -05:00

143 lines
8.0 KiB
C++

#include "SQLiteDatabase.h"
#include <sstream>
// The session filters bind the same parameters whether they are on or off (as in ChatLog.cpp), so the list and the
// count take one argument list
namespace {
std::string Where(const IMessageCaptures::SessionQuery& q) {
std::string where = " WHERE 1 = 1";
where += q.characterId == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND character_id = ?)";
where += q.accountId == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND account_id = ?)";
where += q.startedBy.empty() ? " AND (? = '' AND ? = '')" : " AND (? <> '' AND LOWER(started_by) = LOWER(?))";
where += q.since == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND started_at >= ?)";
where += q.until == 0 ? " AND (? = 0 AND ? = 0)" : " AND (? <> 0 AND started_at < ?)";
where += q.unfinishedOnly ? " AND (? <> 0 AND ended_at = 0)" : " AND (? = 0)";
return where;
}
std::string OrderBy(const IMessageCaptures::SessionQuery& q) {
using eOrder = IMessageCaptures::eSessionOrder;
const std::string direction = q.ascending ? " ASC" : " DESC";
std::string column = "started_at";
switch (q.order) {
// MySQL compares names without case; so does this
case eOrder::CHARACTER: column = "character_name COLLATE NOCASE"; break;
case eOrder::STARTED_BY: column = "started_by COLLATE NOCASE"; break;
case eOrder::MESSAGES: column = "message_count"; break;
case eOrder::BYTES: column = "byte_count"; break;
default: break;
}
// Ties in id order, the same on every database
return " ORDER BY " + column + direction + ", id" + direction;
}
IMessageCaptures::MessageCaptureSession Session(CppSQLite3Query& r) {
IMessageCaptures::MessageCaptureSession s;
s.id = static_cast<uint64_t>(r.getInt64Field("id"));
s.characterId = r.getInt64Field("character_id");
s.characterName = r.getStringField("character_name");
s.accountId = static_cast<uint32_t>(r.getInt64Field("account_id"));
s.accountName = r.getStringField("account_name");
s.startedById = static_cast<uint32_t>(r.getInt64Field("started_by_id"));
s.startedBy = r.getStringField("started_by");
s.startedAt = r.getInt64Field("started_at");
s.endsAt = r.getInt64Field("ends_at");
s.endedAt = r.getInt64Field("ended_at");
s.endReason = r.getStringField("end_reason");
s.toServer = r.getIntField("to_server") != 0;
s.toClient = r.getIntField("to_client") != 0;
s.onlyMessages = r.getStringField("only_messages");
s.skipMessages = r.getStringField("skip_messages");
s.zoneId = static_cast<uint32_t>(r.getInt64Field("zone_id"));
s.instanceId = static_cast<uint32_t>(r.getInt64Field("instance_id"));
s.cloneId = static_cast<uint32_t>(r.getInt64Field("clone_id"));
s.zones = r.getStringField("zones");
s.messageCount = static_cast<uint64_t>(r.getInt64Field("message_count"));
s.byteCount = static_cast<uint64_t>(r.getInt64Field("byte_count"));
s.dropped = static_cast<uint64_t>(r.getInt64Field("dropped"));
s.kind = static_cast<uint8_t>(r.getIntField("capture_kind"));
s.target = r.getStringField("capture_target");
return s;
}
}
uint64_t SQLiteDatabase::InsertMessageCaptureSession(const MessageCaptureSession& s) {
ExecuteInsert("INSERT INTO message_capture_sessions (character_id, character_name, account_id, account_name, started_by_id, started_by, started_at, ends_at, "
"ended_at, end_reason, to_server, to_client, only_messages, skip_messages, zone_id, instance_id, clone_id, zones, message_count, byte_count, dropped, capture_kind, capture_target) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
s.characterId, s.characterName, s.accountId, s.accountName, s.startedById, s.startedBy, s.startedAt, s.endsAt, s.endedAt, s.endReason,
s.toServer, s.toClient, s.onlyMessages, s.skipMessages, s.zoneId, s.instanceId, s.cloneId, s.zones, s.messageCount, s.byteCount, s.dropped, static_cast<uint32_t>(s.kind), s.target);
auto [_, last] = ExecuteSelect("SELECT last_insert_rowid() AS id;"); // this connection's insert
return last.eof() ? 0 : static_cast<uint64_t>(last.getInt64Field("id"));
}
void SQLiteDatabase::UpdateMessageCaptureSession(const MessageCaptureSession& s) {
ExecuteUpdate("UPDATE message_capture_sessions SET ends_at = ?, ended_at = ?, end_reason = ?, zone_id = ?, instance_id = ?, clone_id = ?, zones = ?, "
"message_count = ?, byte_count = ?, dropped = ? WHERE id = ?;",
s.endsAt, s.endedAt, s.endReason, s.zoneId, s.instanceId, s.cloneId, s.zones, s.messageCount, s.byteCount, s.dropped, s.id);
}
void SQLiteDatabase::InsertMessageCaptureEntries(const std::vector<MessageCaptureRecord>& entries) {
if (entries.empty()) return;
DatabaseTransaction transaction(*this);
for (const auto& e : entries) {
std::istringstream payload(e.payload);
ExecuteInsert("INSERT INTO message_capture_entries (session_id, seq, time_ms, direction, message_id, object_id, bits, dropped_before, zone_id, instance_id, "
"clone_id, payload, decoded) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);",
e.sessionId, e.seq, e.timeMs, static_cast<uint32_t>(e.direction), static_cast<uint32_t>(e.messageId), e.objectId, e.bits, e.droppedBefore, e.zoneId, e.instanceId, e.cloneId,
static_cast<const std::istream*>(&payload), e.decoded);
}
transaction.Commit();
}
std::optional<IMessageCaptures::MessageCaptureSession> SQLiteDatabase::GetMessageCaptureSession(uint64_t id) {
auto [_, result] = ExecuteSelect("SELECT * FROM message_capture_sessions WHERE id = ?;", id);
if (result.eof()) return std::nullopt;
return Session(result);
}
std::vector<IMessageCaptures::MessageCaptureSession> SQLiteDatabase::GetMessageCaptureSessions(const SessionQuery& q) {
std::vector<MessageCaptureSession> sessions;
auto [_, result] = ExecuteSelect("SELECT * FROM message_capture_sessions" + Where(q) + OrderBy(q) + " LIMIT ? OFFSET ?;",
q.characterId, q.characterId, q.accountId, q.accountId, q.startedBy, q.startedBy, q.since, q.since, q.until, q.until, q.unfinishedOnly, q.limit, q.offset);
for (; !result.eof(); result.nextRow()) sessions.push_back(Session(result));
return sessions;
}
uint64_t SQLiteDatabase::CountMessageCaptureSessions(const SessionQuery& q) {
auto [_, result] = ExecuteSelect("SELECT COUNT(*) AS count FROM message_capture_sessions" + Where(q) + ";",
q.characterId, q.characterId, q.accountId, q.accountId, q.startedBy, q.startedBy, q.since, q.since, q.until, q.until, q.unfinishedOnly);
return result.eof() ? 0 : static_cast<uint64_t>(result.getInt64Field("count"));
}
std::vector<IMessageCaptures::MessageCaptureRecord> SQLiteDatabase::GetMessageCaptureEntries(uint64_t sessionId, uint32_t afterSeq, uint32_t limit) {
std::vector<MessageCaptureRecord> entries;
auto [_, result] = ExecuteSelect("SELECT * FROM message_capture_entries WHERE session_id = ? AND seq > ? ORDER BY seq LIMIT ?;", sessionId, afterSeq, limit);
for (; !result.eof(); result.nextRow()) {
MessageCaptureRecord e;
e.sessionId = static_cast<uint64_t>(result.getInt64Field("session_id"));
e.seq = static_cast<uint32_t>(result.getInt64Field("seq"));
e.timeMs = result.getInt64Field("time_ms");
e.direction = static_cast<uint8_t>(result.getIntField("direction"));
e.messageId = static_cast<uint16_t>(result.getIntField("message_id"));
e.objectId = result.getInt64Field("object_id");
e.bits = static_cast<uint32_t>(result.getInt64Field("bits"));
e.droppedBefore = static_cast<uint32_t>(result.getInt64Field("dropped_before"));
e.zoneId = static_cast<uint32_t>(result.getInt64Field("zone_id"));
e.instanceId = static_cast<uint32_t>(result.getInt64Field("instance_id"));
e.cloneId = static_cast<uint32_t>(result.getInt64Field("clone_id"));
int length = 0;
const auto* blob = result.getBlobField("payload", length);
if (blob && length > 0) e.payload.assign(reinterpret_cast<const char*>(blob), length);
e.decoded = result.getStringField("decoded");
entries.push_back(std::move(e));
}
return entries;
}
void SQLiteDatabase::DeleteMessageCaptureSession(uint64_t id) {
ExecuteDelete("DELETE FROM message_capture_entries WHERE session_id = ?;", id);
ExecuteDelete("DELETE FROM message_capture_sessions WHERE id = ?;", id);
}