diff --git a/src/routes/postBranding.ts b/src/routes/postBranding.ts index 267f913..5ca8cb8 100644 --- a/src/routes/postBranding.ts +++ b/src/routes/postBranding.ts @@ -63,13 +63,15 @@ export async function postBranding(req: Request, res: Response) { userAgent, userAgentHeader: req.headers["user-agent"], videoDuration, + videoID, userID, service, dearrow: { title, thumbnail, downvote, - } + }, + endpoint: "dearrow-postBranding", })) { Logger.warn(`Rejecting submission based on invalid data: ${hashedUserID} ${videoID} ${videoDuration} ${userAgent} ${req.headers["user-agent"]} ${title.title} ${thumbnail.timestamp}`); res.status(200).send("OK"); diff --git a/src/routes/postCasual.ts b/src/routes/postCasual.ts index fae7471..b8a3cc4 100644 --- a/src/routes/postCasual.ts +++ b/src/routes/postCasual.ts @@ -14,6 +14,8 @@ import { QueryCacher } from "../utils/queryCacher"; import { acquireLock } from "../utils/redisLock"; import { checkBanStatus } from "../utils/checkBan"; import { canSubmitDeArrow } from "../utils/permissions"; +import { isRequestInvalid } from "../utils/requestValidator"; +import { parseUserAgent } from "../utils/userAgent"; interface ExistingVote { UUID: BrandingUUID; @@ -22,6 +24,7 @@ interface ExistingVote { export async function postCasual(req: Request, res: Response) { const { videoID, userID, downvote } = req.body as CasualVoteSubmission; + const userAgent = req.body.userAgent ?? parseUserAgent(req.get("user-agent")) ?? ""; let categories = req.body.categories as CasualCategory[]; const title = (req.body.title as string)?.toLowerCase(); const service = getService(req.body.service); @@ -36,6 +39,19 @@ export async function postCasual(req: Request, res: Response) { return res.status(400).send("Bad Request"); } + if (isRequestInvalid({ + userID, + videoID, + userAgent, + userAgentHeader: req.headers["user-agent"], + casualCategories: categories, + service, + endpoint: "dearrow-postCasual", + })) { + Logger.warn(`Casual vote rejected by request validator: ${userAgent} ${req.headers["user-agent"]} ${categories} ${service} ${videoID}`); + return res.status(200).send("OK"); + } + try { const hashedUserID = await getHashCache(userID); const hashedVideoID = await getHashCache(videoID, 1); @@ -134,4 +150,4 @@ async function handleExistingVotes(videoID: VideoID, service: Service, titleID: [videoID, service, titleID, hashedUserID, hashedIP, category, now]); return false; -} \ No newline at end of file +} diff --git a/src/routes/postSkipSegments.ts b/src/routes/postSkipSegments.ts index 533f043..6047587 100644 --- a/src/routes/postSkipSegments.ts +++ b/src/routes/postSkipSegments.ts @@ -514,9 +514,11 @@ export async function postSkipSegments(req: Request, res: Response): Promise { return privateDB.prepare("run", @@ -15,7 +16,7 @@ function logUserNameChange(userID: string, newUserName: string, oldUserName: str export async function setUsername(req: Request, res: Response): Promise { const userIDInput = req.query.userID as string; - const adminUserIDInput = req.query.adminUserID as string; + const adminUserIDInput = req.query.adminUserID as string | undefined; let userName = req.query.username as string; let hashedUserID: HashedUserID; @@ -29,16 +30,22 @@ 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); } @@ -80,8 +83,6 @@ export async function setUsername(req: Request, res: Response): Promise boolean; type CompiledSegmentCheck = (input: IncomingSegment) => boolean; -type InputExtractor = (input: RequestValidatorInput) => string | number | undefined | null; +type InputExtractor = ( + input: RequestValidatorInput, +) => string | number | undefined | null; type SegmentExtractor = (input: IncomingSegment) => string | undefined | null; type BooleanRules = "titleOriginal" | "thumbnailOriginal" | "dearrowDownvote"; type RuleEntry = @@ -27,14 +37,17 @@ type RuleEntry = let compiledRules: CompiledValidityCheck; +function patternToRegex(pattern: ValidatorPattern): RegExp { + return typeof pattern === "string" + ? new RegExp(pattern, "i") + : new RegExp(...pattern); +} + function compilePattern( pattern: ValidatorPattern, extractor: InputExtractor, ): CompiledValidityCheck { - const regex = - typeof pattern === "string" - ? new RegExp(pattern, "i") - : new RegExp(...pattern); + const regex = patternToRegex(pattern); return (input: RequestValidatorInput) => { const field = extractor(input); @@ -47,10 +60,7 @@ function compileSegmentPattern( pattern: ValidatorPattern, extractor: SegmentExtractor, ): CompiledSegmentCheck { - const regex = - typeof pattern === "string" - ? new RegExp(pattern, "i") - : new RegExp(...pattern); + const regex = patternToRegex(pattern); return (input: IncomingSegment) => { const field = extractor(input); @@ -93,6 +103,11 @@ export function compileRules( ), ); break; + case "videoID": + ruleComponents.push( + compilePattern(rulePattern, (input) => input.videoID), + ); + break; case "userID": ruleComponents.push( compilePattern(rulePattern, (input) => input.userID), @@ -153,7 +168,8 @@ export function compileRules( break; case "titleOriginal": ruleComponents.push( - (input) => input.dearrow?.title?.original === rulePattern, + (input) => + input.dearrow?.title?.original === rulePattern, ); break; case "thumbnailTimestamp": @@ -172,10 +188,39 @@ export function compileRules( break; case "dearrowDownvote": ruleComponents.push( - (input) => - input.dearrow?.downvote === rulePattern, + (input) => input.dearrow?.downvote === rulePattern, ); break; + case "newUsername": + ruleComponents.push( + compilePattern( + rulePattern, + (input) => input.newUsername, + ), + ); + break; + case "endpoint": + ruleComponents.push( + compilePattern(rulePattern, (input) => input.endpoint), + ); + break; + case "casualCategory": { + const regex = patternToRegex(rulePattern); + ruleComponents.push((input) => { + if (input.casualCategories === undefined) { + return false; + } + for (const category of input.casualCategories) { + if (regex.test(category)) return true; + } + return false; + }); + break; + } + default: { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const _exhaustive: never = ruleKey; + } } } if (segmentRuleComponents.length > 0) {