mirror of
https://github.com/Cockatrice/Cockatrice.git
synced 2026-11-06 18:13:29 -08:00
[Alts] Server-side search controls, IP counts, adjustable window
Plumbs the match-type chips through to the server: include_email/include_clientid/include_ip skip disabled identifier types in both matching and frontier expansion, so excluded types (e.g. throwaway domains) never enter the graph at any depth. Session lookback becomes adjustable (session_months, clamped 1-24, default 6). Each alt row carries ip_count, the distinct recent session IP count shown in a new IPs column. All proto additions are optional with old defaults, so old clients/servers interoperate.
This commit is contained in:
parent
2b7ddfd968
commit
b7f04bbb6e
7 changed files with 142 additions and 45 deletions
|
|
@ -1987,6 +1987,8 @@ constexpr int MAX_USER_ALTS_PER_LEVEL = 200;
|
|||
// Identifier-heavy frontiers, e.g. VPN users with hundreds of recent IPs, are
|
||||
// truncated: this tool is deliberately a first-pass sweep, not an audit.
|
||||
constexpr int MAX_USER_ALT_IDENTIFIERS = 200;
|
||||
constexpr int MIN_USER_ALT_MONTHS = 1;
|
||||
constexpr int MAX_USER_ALT_MONTHS = 24;
|
||||
|
||||
QString altInClause(const QString &prefix, const QList<QString> &values)
|
||||
{
|
||||
|
|
@ -2037,7 +2039,12 @@ QList<QString> cappedIdentifiers(const QList<QString> &values)
|
|||
}
|
||||
} // namespace
|
||||
|
||||
QList<ServerInfo_UserAlt> Servatrice_DatabaseInterface::getUserAlts(const QString &userName, int depth)
|
||||
QList<ServerInfo_UserAlt> Servatrice_DatabaseInterface::getUserAlts(const QString &userName,
|
||||
int depth,
|
||||
bool useEmail,
|
||||
bool useClientId,
|
||||
bool useIp,
|
||||
int sessionMonths)
|
||||
{
|
||||
QList<ServerInfo_UserAlt> results;
|
||||
|
||||
|
|
@ -2050,6 +2057,16 @@ QList<ServerInfo_UserAlt> Servatrice_DatabaseInterface::getUserAlts(const QStrin
|
|||
} else if (depth > MAX_USER_ALT_DEPTH) {
|
||||
depth = MAX_USER_ALT_DEPTH;
|
||||
}
|
||||
if (sessionMonths < MIN_USER_ALT_MONTHS) {
|
||||
sessionMonths = MIN_USER_ALT_MONTHS;
|
||||
} else if (sessionMonths > MAX_USER_ALT_MONTHS) {
|
||||
sessionMonths = MAX_USER_ALT_MONTHS;
|
||||
}
|
||||
|
||||
// Interpolated, not bound: placeholders are not allowed in INTERVAL
|
||||
// expressions, and the clamped integer cannot inject SQL.
|
||||
const QString sessionWindow = QStringLiteral(" AND start_time >= DATE_SUB(NOW(), INTERVAL ") +
|
||||
QString::number(sessionMonths) + QStringLiteral(" MONTH)");
|
||||
|
||||
// Seed must exist; its row anchors the result at depth 0.
|
||||
QSqlQuery *seedQuery = prepareQuery("SELECT u.name, u.email, u.clientid, UNIX_TIMESTAMP(u.registrationDate), "
|
||||
|
|
@ -2122,32 +2139,46 @@ QList<ServerInfo_UserAlt> Servatrice_DatabaseInterface::getUserAlts(const QStrin
|
|||
QSet<QString> clientIdKeys;
|
||||
QSet<QString> ipKeys;
|
||||
|
||||
QSqlQuery frontierQuery(sqlDatabase);
|
||||
prepareDynamicQuery(frontierQuery, QStringLiteral("SELECT email, clientid FROM {prefix}_users WHERE name IN ") +
|
||||
frontierClause);
|
||||
altBindValues(&frontierQuery, QStringLiteral(":alt_name"), frontier);
|
||||
if (execSqlQuery(&frontierQuery)) {
|
||||
while (frontierQuery.next()) {
|
||||
// Email comparison is case-insensitive in SQL, so normalise the
|
||||
// keys too or the membership tests below miss real matches.
|
||||
appendIdentifier(emails, emailKeys, frontierQuery.value(0).toString().toLower());
|
||||
appendIdentifier(clientIds, clientIdKeys, frontierQuery.value(1).toString());
|
||||
if (useEmail || useClientId) {
|
||||
QSqlQuery frontierQuery(sqlDatabase);
|
||||
prepareDynamicQuery(frontierQuery,
|
||||
QStringLiteral("SELECT email, clientid FROM {prefix}_users WHERE name IN ") +
|
||||
frontierClause);
|
||||
altBindValues(&frontierQuery, QStringLiteral(":alt_name"), frontier);
|
||||
if (execSqlQuery(&frontierQuery)) {
|
||||
while (frontierQuery.next()) {
|
||||
// Email comparison is case-insensitive in SQL, so normalise the
|
||||
// keys too or the membership tests below miss real matches.
|
||||
if (useEmail) {
|
||||
appendIdentifier(emails, emailKeys, frontierQuery.value(0).toString().toLower());
|
||||
}
|
||||
if (useClientId) {
|
||||
appendIdentifier(clientIds, clientIdKeys, frontierQuery.value(1).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recent session identifiers: IPs plus historic client ids, most recent
|
||||
// first so a capped list keeps what the account actually used lately.
|
||||
QSqlQuery sessionQuery(sqlDatabase);
|
||||
prepareDynamicQuery(sessionQuery, QStringLiteral("SELECT ip_address, clientid, MAX(start_time) AS last_seen "
|
||||
"FROM {prefix}_sessions WHERE user_name IN ") +
|
||||
frontierClause +
|
||||
QStringLiteral(" AND start_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH) "
|
||||
"GROUP BY ip_address, clientid ORDER BY last_seen DESC"));
|
||||
altBindValues(&sessionQuery, QStringLiteral(":alt_name"), frontier);
|
||||
if (execSqlQuery(&sessionQuery)) {
|
||||
while (sessionQuery.next()) {
|
||||
appendIdentifier(ips, ipKeys, sessionQuery.value(0).toString());
|
||||
appendIdentifier(clientIds, clientIdKeys, sessionQuery.value(1).toString());
|
||||
if (useIp || useClientId) {
|
||||
QSqlQuery sessionQuery(sqlDatabase);
|
||||
prepareDynamicQuery(sessionQuery,
|
||||
QStringLiteral("SELECT ip_address, clientid, MAX(start_time) AS last_seen "
|
||||
"FROM {prefix}_sessions WHERE user_name IN ") +
|
||||
frontierClause + sessionWindow +
|
||||
QStringLiteral(" GROUP BY ip_address, clientid "
|
||||
"ORDER BY last_seen DESC"));
|
||||
altBindValues(&sessionQuery, QStringLiteral(":alt_name"), frontier);
|
||||
if (execSqlQuery(&sessionQuery)) {
|
||||
while (sessionQuery.next()) {
|
||||
if (useIp) {
|
||||
appendIdentifier(ips, ipKeys, sessionQuery.value(0).toString());
|
||||
}
|
||||
if (useClientId) {
|
||||
appendIdentifier(clientIds, clientIdKeys, sessionQuery.value(1).toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2179,14 +2210,13 @@ QList<ServerInfo_UserAlt> Servatrice_DatabaseInterface::getUserAlts(const QStrin
|
|||
if (!ipList.isEmpty()) {
|
||||
conditions << QStringLiteral("u.name IN (SELECT DISTINCT s.user_name FROM {prefix}_sessions s WHERE "
|
||||
"s.ip_address IN ") +
|
||||
altInClause(QStringLiteral(":alt_ip"), ipList) +
|
||||
QStringLiteral(" AND s.start_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH))");
|
||||
altInClause(QStringLiteral(":alt_ip"), ipList) + sessionWindow + QStringLiteral(")");
|
||||
}
|
||||
if (!clientIdList.isEmpty()) {
|
||||
conditions << QStringLiteral("u.name IN (SELECT DISTINCT s.user_name FROM {prefix}_sessions s WHERE "
|
||||
"s.clientid IN ") +
|
||||
altInClause(QStringLiteral(":alt_hcid"), clientIdList) +
|
||||
QStringLiteral(" AND s.start_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH))");
|
||||
altInClause(QStringLiteral(":alt_hcid"), clientIdList) + sessionWindow +
|
||||
QStringLiteral(")");
|
||||
}
|
||||
candidateString += conditions.join(QStringLiteral(" OR "));
|
||||
// Already-visited accounts match their own identifiers at every level and
|
||||
|
|
@ -2251,7 +2281,7 @@ QList<ServerInfo_UserAlt> Servatrice_DatabaseInterface::getUserAlts(const QStrin
|
|||
altInClause(QStringLiteral(":alt_ecid"), clientIdList);
|
||||
}
|
||||
evidenceString += evidenceConditions.join(QStringLiteral(" OR "));
|
||||
evidenceString += QStringLiteral(") AND start_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH)");
|
||||
evidenceString += QStringLiteral(")") + sessionWindow;
|
||||
|
||||
QSqlQuery evidenceQuery(sqlDatabase);
|
||||
prepareDynamicQuery(evidenceQuery, evidenceString);
|
||||
|
|
@ -2346,9 +2376,8 @@ QList<ServerInfo_UserAlt> Servatrice_DatabaseInterface::getUserAlts(const QStrin
|
|||
prepareDynamicQuery(countQuery,
|
||||
QStringLiteral("SELECT ip_address, COUNT(DISTINCT user_name) FROM {prefix}_sessions "
|
||||
"WHERE ip_address IN ") +
|
||||
altInClause(QStringLiteral(":alt_ci"), neededIpList) +
|
||||
QStringLiteral(" AND start_time >= DATE_SUB(NOW(), INTERVAL 6 MONTH) "
|
||||
"GROUP BY ip_address"));
|
||||
altInClause(QStringLiteral(":alt_ci"), neededIpList) + sessionWindow +
|
||||
QStringLiteral(" GROUP BY ip_address"));
|
||||
altBindValues(&countQuery, QStringLiteral(":alt_ci"), neededIpList);
|
||||
if (execSqlQuery(&countQuery)) {
|
||||
while (countQuery.next()) {
|
||||
|
|
@ -2385,6 +2414,31 @@ QList<ServerInfo_UserAlt> Servatrice_DatabaseInterface::getUserAlts(const QStrin
|
|||
frontier = nextFrontier;
|
||||
}
|
||||
|
||||
// Distinct recent IP counts per account: cheap group signal without
|
||||
// shipping full address histories to the client.
|
||||
if (!results.isEmpty()) {
|
||||
QList<QString> resultNames;
|
||||
resultNames.reserve(results.size());
|
||||
for (const ServerInfo_UserAlt &alt : results) {
|
||||
resultNames << QString::fromStdString(alt.user_name());
|
||||
}
|
||||
QSqlQuery *ipCountQuery = prepareQuery(
|
||||
QStringLiteral("SELECT user_name, COUNT(DISTINCT ip_address) FROM {prefix}_sessions WHERE user_name IN ") +
|
||||
altInClause(QStringLiteral(":alt_ic"), resultNames) + sessionWindow +
|
||||
QStringLiteral(" GROUP BY user_name"));
|
||||
altBindValues(ipCountQuery, QStringLiteral(":alt_ic"), resultNames);
|
||||
QMap<QString, int> ipCountsByUser;
|
||||
if (execSqlQuery(ipCountQuery)) {
|
||||
while (ipCountQuery->next()) {
|
||||
ipCountsByUser.insert(ipCountQuery->value(0).toString(), ipCountQuery->value(1).toInt());
|
||||
}
|
||||
}
|
||||
for (ServerInfo_UserAlt &alt : results) {
|
||||
alt.set_ip_count(static_cast<google::protobuf::uint32>(
|
||||
ipCountsByUser.value(QString::fromStdString(alt.user_name()), 0)));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue