From c2a3630d49420c5539d27d553870e918694dfde9 Mon Sep 17 00:00:00 2001 From: mini-bomba <55105495+mini-bomba@users.noreply.github.com> Date: Tue, 29 Aug 2023 13:28:08 +0200 Subject: [PATCH] create an isUserBanned utility function --- src/routes/getUserInfo.ts | 7 ++-- src/routes/getUserStats.ts | 4 +- src/routes/postSkipSegments.ts | 20 +++------- src/routes/setUsername.ts | 68 ++++++++++++++++----------------- src/routes/voteOnSponsorTime.ts | 25 ++++++++---- src/utils/checkBan.ts | 27 +++++++++++++ 6 files changed, 88 insertions(+), 63 deletions(-) create mode 100644 src/utils/checkBan.ts diff --git a/src/routes/getUserInfo.ts b/src/routes/getUserInfo.ts index 97acd96..a167c7c 100644 --- a/src/routes/getUserInfo.ts +++ b/src/routes/getUserInfo.ts @@ -8,12 +8,12 @@ import { getReputation } from "../utils/reputation"; import { Category, SegmentUUID } from "../types/segments.model"; import { config } from "../config"; import { canSubmit } from "../utils/permissions"; +import { isUserBanned } from "../utils/checkBan"; const maxRewardTime = config.maxRewardTimePerSegmentInSeconds; async function dbGetSubmittedSegmentSummary(userID: HashedUserID): Promise<{ minutesSaved: number, segmentCount: number }> { try { - const userBanCount = (await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID]))?.userCount; - const countShadowHidden = userBanCount > 0 ? 2 : 1; // if shadowbanned, count shadowhidden as well + const countShadowHidden = await isUserBanned(userID) ? 2 : 1; // if shadowbanned, count shadowhidden as well const row = await db.prepare("get", `SELECT SUM(CASE WHEN "actionType" = 'chapter' THEN 0 ELSE ((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views" END) as "minutesSaved", count(*) as "segmentCount" FROM "sponsorTimes" @@ -111,8 +111,7 @@ async function dbGetActiveWarningReasonForUser(userID: HashedUserID): Promise { try { - const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID], { useReplica: true }); - return row?.userCount > 0 ?? false; + return await isUserBanned(userID); } catch (err) /* istanbul ignore next */ { return false; } diff --git a/src/routes/getUserStats.ts b/src/routes/getUserStats.ts index ca6f279..9511f84 100644 --- a/src/routes/getUserStats.ts +++ b/src/routes/getUserStats.ts @@ -4,6 +4,7 @@ import { Request, Response } from "express"; import { HashedUserID, UserID } from "../types/user.model"; import { config } from "../config"; import { Logger } from "../utils/logger"; +import { isUserBanned } from "../utils/checkBan"; type nestedObj = Record>; const maxRewardTimePerSegmentInSeconds = config.maxRewardTimePerSegmentInSeconds ?? 86400; @@ -34,8 +35,7 @@ async function dbGetUserSummary(userID: HashedUserID, fetchCategoryStats: boolea `; } try { - const userBanCount = (await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID]))?.userCount; - const countShadowHidden = userBanCount > 0 ? 2 : 1; // if shadowbanned, count shadowhidden as well + const countShadowHidden = await isUserBanned(userID) ? 2 : 1; // if shadowbanned, count shadowhidden as well const row = await db.prepare("get", ` SELECT SUM(CASE WHEN "actionType" = 'chapter' THEN 0 ELSE ((CASE WHEN "endTime" - "startTime" > ? THEN ? ELSE "endTime" - "startTime" END) / 60) * "views" END) as "minutesSaved", ${additionalQuery} diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts index d1e2e9b..ee4a459 100644 --- a/src/routes/postSkipSegments.ts +++ b/src/routes/postSkipSegments.ts @@ -9,7 +9,7 @@ import { getIP } from "../utils/getIP"; import { getFormattedTime } from "../utils/getFormattedTime"; import { dispatchEvent } from "../utils/webhookUtils"; import { Request, Response } from "express"; -import { ActionType, Category, IncomingSegment, IPAddress, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model"; +import { ActionType, Category, HashedIP, IncomingSegment, IPAddress, SegmentUUID, Service, VideoDuration, VideoID } from "../types/segments.model"; import { deleteLockCategories } from "./deleteLockCategories"; import { QueryCacher } from "../utils/queryCacher"; import { getReputation } from "../utils/reputation"; @@ -23,8 +23,8 @@ import { vote } from "./voteOnSponsorTime"; import { canSubmit } from "../utils/permissions"; import { getVideoDetails, videoDetails } from "../utils/getVideoDetails"; import * as youtubeID from "../utils/youtubeID"; -import { banUser } from "./shadowBanUser"; import { acquireLock } from "../utils/redisLock"; +import { checkBanStatus } from "../utils/checkBan"; type CheckResult = { pass: boolean, @@ -544,7 +544,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise Logger.error(`Error banning user after submitting from a banned IP: ${e}`)); - } - for (const segmentInfo of segments) { // Full segments are always rejected since there can only be one, so shadow hide wouldn't work if (segmentInfo.ignoreSegment - || (shadowBanCount && segmentInfo.actionType === ActionType.Full)) { + || (isBanned && segmentInfo.actionType === ActionType.Full)) { continue; } @@ -586,7 +578,7 @@ export async function postSkipSegments(req: Request, res: Response): Promise { return privateDB.prepare("run", @@ -12,12 +14,12 @@ function logUserNameChange(userID: string, newUserName: string, oldUserName: str } export async function setUsername(req: Request, res: Response): Promise { - let userID = req.query.userID as string; + const userIDInput = req.query.userID as string; + const adminUserIDInput = req.query.adminUserID as string; let userName = req.query.username as string; + let hashedUserID: HashedUserID; - let adminUserIDInput = req.query.adminUserID as string; - - if (userID == undefined || userName == undefined || userID === "undefined" || userName.length > 64) { + if (userIDInput == undefined || userName == undefined || userIDInput === "undefined" || userName.length > 64) { //invalid request return res.sendStatus(400); } @@ -35,41 +37,37 @@ export async function setUsername(req: Request, res: Response): Promise 0) { + return res.sendStatus(200); + } + + timings.push(Date.now()); + + if (await isUserBanned(hashedUserID)) { + return res.sendStatus(200); + } } - - timings.push(Date.now()); - - const row = await db.prepare("get", `SELECT count(*) as "userCount" FROM "userNames" WHERE "userID" = ? AND "locked" = 1`, [userID]); - if (adminUserIDInput === undefined && row.userCount > 0) { - return res.sendStatus(200); - } - - timings.push(Date.now()); - - const shadowBanRow = await db.prepare("get", `SELECT count(*) as "userCount" FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID]); - if (adminUserIDInput === undefined && shadowBanRow.userCount > 0) { - return res.sendStatus(200); - } - - timings.push(Date.now()); } catch (error) /* istanbul ignore next */ { Logger.error(error as string); @@ -78,7 +76,7 @@ export async function setUsername(req: Request, res: Response): Promise { // Check if they've already made a vote const usersLastVoteInfo = await privateDB.prepare("get", `select count(*) as votes, category from "categoryVotes" where "UUID" = ? and "userID" = ? group by category`, [UUID, userID], { useReplica: true }); @@ -244,8 +245,7 @@ async function categoryVote(UUID: SegmentUUID, userID: UserID, isVIP: boolean, i const timeSubmitted = Date.now(); const voteAmount = (isVIP || isTempVIP) ? 500 : 1; - const ableToVote = finalResponse.finalStatus === 200 - && (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [userID], { useReplica: true })) === undefined; + const ableToVote = finalResponse.finalStatus === 200; // ban status checks handled by vote() (caller function) if (ableToVote) { // Add the vote @@ -336,6 +336,9 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID const nonAnonUserID = await getHashCache(paramUserID); const userID = await getHashCache(paramUserID + UUID); + //hash the ip 5000 times so no one can get it from the database + const hashedIP: HashedIP = await getHashCache((ip + config.globalSalt) as IPAddress); + const lock = await acquireLock(`voteOnSponsorTime:${UUID}.${paramUserID}`); if (!lock.status) { return { status: 429, message: "Vote already in progress" }; @@ -350,9 +353,6 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID webhookMessage: null }; - //hash the ip 5000 times so no one can get it from the database - const hashedIP: HashedIP = await getHashCache((ip + config.globalSalt) as IPAddress); - const segmentInfo: DBSegment = await db.prepare("get", `SELECT * from "sponsorTimes" WHERE "UUID" = ?`, [UUID]); // segment doesnt exist if (!segmentInfo) { @@ -362,6 +362,7 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID const isTempVIP = await isUserTempVIP(nonAnonUserID, segmentInfo.videoID); const isVIP = await isUserVIP(nonAnonUserID); + const isBanned = await checkBanStatus(nonAnonUserID, hashedIP); // propagates IP bans //check if user voting on own submission const isOwnSubmission = nonAnonUserID === segmentInfo.userID; @@ -380,11 +381,19 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID if (warnings.length >= config.maxNumberOfActiveWarnings) { const warningReason = warnings[0]?.reason; + lock.unlock(); return { status: 403, message: "Vote rejected due to a tip from a moderator. This means that we noticed you were making some common mistakes that are not malicious, and we just want to clarify the rules. " + "Could you please send a message in Discord or Matrix so we can further help you?" + `${(warningReason.length > 0 ? ` Warning reason: '${warningReason}'` : "")}` }; } + // we can return out of the function early if the user is banned after warning checks + // returning before warning checks would make them not appear on vote if the user is also banned + if (isBanned) { + lock.unlock(); + return { status: 200 }; + } + // no type but has category, categoryVote if (!type && category) { const result = categoryVote(UUID, nonAnonUserID, isVIP, isTempVIP, isOwnSubmission, category, hashedIP, finalResponse); @@ -486,13 +495,13 @@ export async function vote(ip: IPAddress, UUID: SegmentUUID, paramUserID: UserID } // Only change the database if they have made a submission before and haven't voted recently + // ban status check was handled earlier (w/ early return) const ableToVote = isVIP || isTempVIP || ( (!(isOwnSubmission && incrementAmount > 0 && oldIncrementAmount >= 0) && !(originalType === VoteType.Malicious && segmentInfo.actionType !== ActionType.Chapter) && !finalResponse.blockVote && finalResponse.finalStatus === 200 && (await db.prepare("get", `SELECT "userID" FROM "sponsorTimes" WHERE "userID" = ? AND "category" = ? AND "votes" > -2 AND "hidden" = 0 AND "shadowHidden" = 0 LIMIT 1`, [nonAnonUserID, segmentInfo.category], { useReplica: true }) !== undefined) - && (await db.prepare("get", `SELECT "userID" FROM "shadowBannedUsers" WHERE "userID" = ?`, [nonAnonUserID], { useReplica: true })) === undefined && (await privateDB.prepare("get", `SELECT "UUID" FROM "votes" WHERE "UUID" = ? AND "hashedIP" = ? AND "userID" != ?`, [UUID, hashedIP, userID], { useReplica: true })) === undefined) ); diff --git a/src/utils/checkBan.ts b/src/utils/checkBan.ts new file mode 100644 index 0000000..36eb41a --- /dev/null +++ b/src/utils/checkBan.ts @@ -0,0 +1,27 @@ +import { HashedUserID } from "../types/user.model"; +import { db } from "../databases/databases"; +import { Category, HashedIP } from "../types/segments.model"; +import { banUser } from "../routes/shadowBanUser"; +import { config } from "../config"; +import { Logger } from "./logger"; + +export async function isUserBanned(userID: HashedUserID): Promise { + return (await db.prepare("get", `SELECT 1 FROM "shadowBannedUsers" WHERE "userID" = ? LIMIT 1`, [userID], { useReplica: true })) !== undefined; +} + +export async function isIPBanned(ip: HashedIP): Promise { + return (await db.prepare("get", `SELECT 1 FROM "shadowBannedIPs" WHERE "hashedIP" = ? LIMIT 1`, [ip], { useReplica: true })) !== undefined; +} + +// NOTE: this function will propagate IP bans +export async function checkBanStatus(userID: HashedUserID, ip: HashedIP): Promise { + const userBanStatus = await isUserBanned(userID); + const ipBanStatus = await isIPBanned(ip); + + if (!userBanStatus && ipBanStatus) { + // Make sure the whole user is banned + banUser(userID, true, true, 1, config.categoryList as Category[], config.deArrowTypes) + .catch((e) => Logger.error(`Error banning user after submitting from a banned IP: ${e}`)); + } + return userBanStatus || ipBanStatus; +}