From 5a426348b60a570ad69e515df0346497e7df78b6 Mon Sep 17 00:00:00 2001 From: Chris Date: Sat, 21 Dec 2024 15:54:45 +0000 Subject: [PATCH] Hotfix custom captcha config (#1584) --- packages/database/src/databases/provider.ts | 6 +- packages/provider/src/api/captcha.ts | 5 +- packages/provider/src/api/public.ts | 1 - .../src/tasks/imgCaptcha/imgCaptchaTasks.ts | 22 ---- .../tasks/imgCaptcha/imgCaptchaTasksUtils.ts | 56 +++++++- .../imgCaptchaTasksUtils.unit.test.ts | 122 +++++++++++++++++- packages/types-database/src/types/provider.ts | 13 ++ 7 files changed, 196 insertions(+), 29 deletions(-) diff --git a/packages/database/src/databases/provider.ts b/packages/database/src/databases/provider.ts index 1fc4bb4965..42dc266735 100644 --- a/packages/database/src/databases/provider.ts +++ b/packages/database/src/databases/provider.ts @@ -736,7 +736,7 @@ export class ProviderDatabase const docs = await this.tables?.commitment .find({ $or: [ - { $storedAtTimestamp: { $exists: false } }, + { storedAtTimestamp: { $exists: false } }, { $expr: { $lt: ["$storedAtTimestamp", "$lastUpdatedTimestamp"] }, }, @@ -793,7 +793,7 @@ export class ProviderDatabase const docs = await this.tables?.powcaptcha .find({ $or: [ - { $storedAtTimestamp: { $exists: false } }, + { storedAtTimestamp: { $exists: false } }, { $expr: { $lt: ["$storedAtTimestamp", "$lastUpdatedTimestamp"] }, }, @@ -1285,7 +1285,7 @@ export class ProviderDatabase ): Promise { const cursor: ScheduledTaskRecord | undefined | null = await this.tables?.scheduler - ?.findOne({ taskId: taskId, status: status }) + ?.findOne({ _id: taskId, status: status }) .lean(); return cursor ? cursor : undefined; } diff --git a/packages/provider/src/api/captcha.ts b/packages/provider/src/api/captcha.ts index d1cc58f5e0..c96d8f978d 100644 --- a/packages/provider/src/api/captcha.ts +++ b/packages/provider/src/api/captcha.ts @@ -37,6 +37,7 @@ import type { ProviderEnvironment } from "@prosopo/types-env"; import { flatten } from "@prosopo/util"; import express, { type Router } from "express"; import { getBotScore } from "../tasks/detection/getBotScore.js"; +import { getCaptchaConfig } from "../tasks/imgCaptcha/imgCaptchaTasksUtils.js"; import { Tasks } from "../tasks/tasks.js"; import { getIPAddress } from "../util.js"; import { handleErrors } from "./errorHandler.js"; @@ -108,7 +109,9 @@ export function prosopoRouter(env: ProviderEnvironment): Router { ); } - const captchaConfig = await tasks.imgCaptchaManager.getCaptchaConfig( + const captchaConfig = await getCaptchaConfig( + tasks.db, + env.config, ipAddress, user, dapp, diff --git a/packages/provider/src/api/public.ts b/packages/provider/src/api/public.ts index cd89133a0a..543a602d78 100644 --- a/packages/provider/src/api/public.ts +++ b/packages/provider/src/api/public.ts @@ -19,7 +19,6 @@ import express, { type Router } from "express"; import { Tasks } from "../tasks/tasks.js"; import { handleErrors } from "./errorHandler.js"; -const NO_IP_ADDRESS = "NO_IP_ADDRESS" as const; const DEFAULT_FRICTIONLESS_THRESHOLD = 0.5; /** diff --git a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts index 18c12beaf6..0e055ca51c 100644 --- a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts +++ b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts @@ -465,28 +465,6 @@ export class ImgCaptchaManager { }; } - async getCaptchaConfig( - ipAddress: Address4 | Address6, - user: string, - dapp: string, - ): Promise { - const ipRule = await checkIpRules(this.db, ipAddress, dapp); - if (ipRule) { - return { - solved: { count: ipRule?.captchaConfig?.solved.count || 0 }, - unsolved: { count: ipRule?.captchaConfig?.unsolved.count || 0 }, - }; - } - const userRule = await checkUserRules(this.db, user, dapp); - if (userRule) { - return { - solved: { count: userRule?.captchaConfig?.solved.count || 0 }, - unsolved: { count: userRule?.captchaConfig?.unsolved.count || 0 }, - }; - } - return this.config.captchas; - } - checkLangRules(acceptLanguage: string): number { return checkLangRules(this.config, acceptLanguage); } diff --git a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasksUtils.ts b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasksUtils.ts index d187bed703..555a759b7a 100644 --- a/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasksUtils.ts +++ b/packages/provider/src/tasks/imgCaptcha/imgCaptchaTasksUtils.ts @@ -16,7 +16,15 @@ import { CaptchaMerkleTree, computeCaptchaSolutionHash, } from "@prosopo/datasets"; -import type { CaptchaSolution } from "@prosopo/types"; +import type { + CaptchaConfig, + CaptchaSolution, + ProsopoConfigOutput, +} from "@prosopo/types"; +import type { IProviderDatabase } from "@prosopo/types-database"; +import type { Address4, Address6 } from "ip-address"; +import { checkIpRules } from "../../rules/ip.js"; +import { checkUserRules } from "../../rules/user.js"; /** * Build merkle tree and get commitment from contract, returning the tree, commitment, and commitmentId @@ -47,3 +55,49 @@ export const buildTreeAndGetCommitmentId = ( return { tree, commitmentId }; }; + +/** + * Get the captcha config for the user and ip address or return the default captcha config + * @param db + * @param config + * @param ipAddress + * @param user + * @param dapp + */ +export const getCaptchaConfig = async ( + db: IProviderDatabase, + config: ProsopoConfigOutput, + ipAddress: Address4 | Address6, + user: string, + dapp: string, +): Promise => { + const ipRule = await checkIpRules(db, ipAddress, dapp); + if (ipRule) { + return { + solved: { + count: + ipRule?.captchaConfig?.solved.count || config.captchas.solved.count, + }, + unsolved: { + count: + ipRule?.captchaConfig?.unsolved.count || + config.captchas.unsolved.count, + }, + }; + } + const userRule = await checkUserRules(db, user, dapp); + if (userRule) { + return { + solved: { + count: + userRule?.captchaConfig?.solved.count || config.captchas.solved.count, + }, + unsolved: { + count: + userRule?.captchaConfig?.unsolved.count || + config.captchas.unsolved.count, + }, + }; + } + return config.captchas; +}; diff --git a/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasksUtils.unit.test.ts b/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasksUtils.unit.test.ts index fbbf75aa5d..6a0fb5eae3 100644 --- a/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasksUtils.unit.test.ts +++ b/packages/provider/src/tests/unit/tasks/imgCaptcha/imgCaptchaTasksUtils.unit.test.ts @@ -17,8 +17,21 @@ import { computeCaptchaSolutionHash, } from "@prosopo/datasets"; import type { CaptchaSolution } from "@prosopo/types"; +import { + BlockRuleType, + type IPAddressBlockRule, + type IProviderDatabase, + type UserAccountBlockRule, + type UserAccountBlockRuleRecord, +} from "@prosopo/types-database"; +import { Address4 } from "ip-address"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import { buildTreeAndGetCommitmentId } from "../../../../tasks/imgCaptcha/imgCaptchaTasksUtils.js"; +import { checkIpRules } from "../../../../rules/ip.js"; +import { checkUserRules } from "../../../../rules/user.js"; +import { + buildTreeAndGetCommitmentId, + getCaptchaConfig, +} from "../../../../tasks/imgCaptcha/imgCaptchaTasksUtils.js"; vi.mock("@prosopo/datasets", () => ({ CaptchaMerkleTree: vi.fn().mockImplementation(() => ({ @@ -28,6 +41,18 @@ vi.mock("@prosopo/datasets", () => ({ computeCaptchaSolutionHash: vi.fn(), })); +vi.mock("../../../../rules/ip.js", () => { + return { + checkIpRules: vi.fn(), + }; +}); + +vi.mock("../../../../rules/user.js", () => { + return { + checkUserRules: vi.fn(), + }; +}); + describe("buildTreeAndGetCommitmentId", () => { const mockCaptchaSolutions = [ { challenge: "challenge1", solution: "solution1", salt: "salt1" }, @@ -79,3 +104,98 @@ describe("buildTreeAndGetCommitmentId", () => { ); }); }); + +describe("getCaptchaConfig", () => { + it("should return the default captcha config if no rules are found", async () => { + const db = { + getIPBlockRuleRecord: vi.fn().mockResolvedValue(null), + getUserBlockRuleRecord: vi.fn().mockResolvedValue(null), + } as unknown as IProviderDatabase; + const config = { + captchas: { + solved: { count: 1 }, + unsolved: { count: 2 }, + }, + }; + const ipAddress = new Address4("1.1.1.1"); + const user = "mockedUser"; + const dapp = "mockedDapp"; + + // @ts-ignore + const result = await getCaptchaConfig(db, config, ipAddress, user, dapp); + + expect(result).toEqual({ + solved: { count: 1 }, + unsolved: { count: 2 }, + }); + }); + it("should return the users config if there is one specified against the user's ip address", async () => { + const ipRule: IPAddressBlockRule = { + ip: 16843009, // 1.1.1.1 + global: false, + hardBlock: false, + type: BlockRuleType.ipAddress, + captchaConfig: { + solved: { count: 3 }, + unsolved: { count: 4 }, + }, + }; + // biome-ignore lint/suspicious/noExplicitAny: tests + (checkIpRules as any).mockReturnValue(ipRule); + const db = { + getIPBlockRuleRecord: vi.fn().mockResolvedValue(ipRule), + getUserBlockRuleRecord: vi.fn().mockResolvedValue(null), + } as unknown as IProviderDatabase; + const config = { + captchas: { + solved: { count: 1 }, + unsolved: { count: 2 }, + }, + }; + const ipAddress = new Address4("1.1.1.1"); // 16843009 + const user = "mockedUser"; + const dapp = "mockedDapp"; + // @ts-ignore + const result = await getCaptchaConfig(db, config, ipAddress, user, dapp); + expect(result).toEqual({ + solved: { count: 3 }, + unsolved: { count: 4 }, + }); + }); + it("should return the user's config if there is one specified against the user's account", async () => { + const userRule: UserAccountBlockRule = { + userAccount: "mockedUser", + dappAccount: "mockedDapp", + global: false, + hardBlock: false, + type: BlockRuleType.userAccount, + captchaConfig: { + solved: { count: 5 }, + unsolved: { count: 6 }, + }, + }; + // biome-ignore lint/suspicious/noExplicitAny: tests + (checkIpRules as any).mockReturnValue(null); + const db = { + getIPBlockRuleRecord: vi.fn().mockResolvedValue(null), + getUserBlockRuleRecord: vi.fn().mockResolvedValue(userRule), + } as unknown as IProviderDatabase; + // biome-ignore lint/suspicious/noExplicitAny: tests + (checkUserRules as any).mockReturnValue(userRule); + const config = { + captchas: { + solved: { count: 1 }, + unsolved: { count: 2 }, + }, + }; + const ipAddress = new Address4("1.1.1.1"); + const user = "mockedUser"; + const dapp = "mockedDapp"; + // @ts-ignore + const result = await getCaptchaConfig(db, config, ipAddress, user, dapp); + expect(result).toEqual({ + solved: { count: 5 }, + unsolved: { count: 6 }, + }); + }); +}); diff --git a/packages/types-database/src/types/provider.ts b/packages/types-database/src/types/provider.ts index 196aae013d..5f32e23c4d 100644 --- a/packages/types-database/src/types/provider.ts +++ b/packages/types-database/src/types/provider.ts @@ -196,6 +196,8 @@ export const PoWCaptchaRecordSchema = new Schema({ // Set an index on the captchaId field, ascending PoWCaptchaRecordSchema.index({ challenge: 1 }); +PoWCaptchaRecordSchema.index({ storedAtTimestamp: 1 }); +PoWCaptchaRecordSchema.index({ storedAtTimestamp: 1, lastUpdatedTimestamp: 1 }); export const UserCommitmentRecordSchema = new Schema({ userAccount: { type: String, required: true }, @@ -223,6 +225,12 @@ export const UserCommitmentRecordSchema = new Schema({ }); // Set an index on the commitment id field, descending UserCommitmentRecordSchema.index({ id: -1 }); +UserCommitmentRecordSchema.index({ storedAtTimestamp: 1 }); +UserCommitmentRecordSchema.index({ + storedAtTimestamp: 1, + lastUpdatedTimestamp: 1, +}); +UserCommitmentRecordSchema.index({ userAccount: 1, dappAccount: 1 }); export const DatasetRecordSchema = new Schema({ contentTree: { type: [[String]], required: true }, @@ -332,6 +340,9 @@ export const ScheduledTaskRecordSchema = new Schema({ required: false, }, }); +ScheduledTaskRecordSchema.index({ processName: 1 }); +ScheduledTaskRecordSchema.index({ processName: 1, status: 1 }); +ScheduledTaskRecordSchema.index({ _id: 1, status: 1 }); export type FrictionlessToken = { token: string; @@ -371,6 +382,8 @@ export const SessionRecordSchema = new Schema({ }, }); +SessionRecordSchema.index({ sessionId: 1 }, { unique: true }); + export type BlockRule = { global: boolean; type: BlockRuleType;