Skip to content

Commit 11a0970

Browse files
committed
Allow captcha config to be set on an IP and user basis
1 parent 4d972d7 commit 11a0970

File tree

12 files changed

+300
-95
lines changed

12 files changed

+300
-95
lines changed

packages/provider/src/api/block.ts

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14-
import type { KeyringPair } from "@polkadot/keyring/types";
15-
import { hexToU8a, isHex } from "@polkadot/util";
16-
import { validateAddress } from "@polkadot/util-crypto/address";
17-
import { ProsopoApiError, ProsopoEnvError } from "@prosopo/common";
14+
15+
import { getLoggerDefault } from "@prosopo/common";
1816
import { ApiPrefix } from "@prosopo/types";
1917
import type { ProviderEnvironment } from "@prosopo/types-env";
2018
import type { NextFunction, Request, Response } from "express";
21-
import { Address6 } from "ip-address";
19+
import { checkIpRules } from "../rules/ip.js";
20+
import { checkUserRules } from "../rules/user.js";
2221
import { getIPAddress } from "../util.js";
2322

23+
const logger = getLoggerDefault();
24+
2425
export const blockMiddleware = (env: ProviderEnvironment) => {
2526
return async (req: Request, res: Response, next: NextFunction) => {
2627
try {
@@ -32,7 +33,7 @@ export const blockMiddleware = (env: ProviderEnvironment) => {
3233

3334
// if no IP block
3435
if (!req.ip) {
35-
console.log("No IP", req.ip);
36+
logger.info("No IP", req.ip);
3637
return res.status(401).json({ error: "Unauthorized" });
3738
}
3839

@@ -41,47 +42,32 @@ export const blockMiddleware = (env: ProviderEnvironment) => {
4142
const ipAddress = getIPAddress(req.ip || "");
4243
const userAccount = req.headers["Prosopo-User"] || req.body.user;
4344
const dappAccount = req.headers["Prosopo-Site-Key"] || req.body.dapp;
44-
const rule = await env.getDb().getIPBlockRuleRecord(ipAddress.bigInt());
45-
if (rule && BigInt(rule.ip) === ipAddress.bigInt()) {
46-
// block by IP address globally
47-
if (rule.global && rule.hardBlock) {
48-
return res.status(401).json({ error: "Unauthorized" });
49-
}
5045

51-
if (dappAccount) {
52-
const dappRule = await env
53-
.getDb()
54-
.getIPBlockRuleRecord(ipAddress.bigInt(), dappAccount);
55-
if (
56-
dappRule?.hardBlock &&
57-
dappRule.dappAccount === dappAccount &&
58-
BigInt(dappRule.ip) === ipAddress.bigInt()
59-
) {
60-
return res.status(401).json({ error: "Unauthorized" });
61-
}
62-
}
46+
// get matching IP rules
47+
const rule = await checkIpRules(env.getDb(), ipAddress, dappAccount);
48+
49+
// block if hard block
50+
if (rule?.hardBlock) {
51+
return res.status(401).json({ error: "Unauthorized" });
6352
}
6453

65-
if (userAccount && dappAccount) {
66-
const rule = await env
67-
.getDb()
68-
.getUserBlockRuleRecord(userAccount, dappAccount);
54+
// get matching user rules
55+
const userRule = await checkUserRules(
56+
env.getDb(),
57+
userAccount,
58+
dappAccount,
59+
);
6960

70-
if (
71-
rule &&
72-
rule.userAccount === userAccount &&
73-
rule.dappAccount === dappAccount &&
74-
rule.hardBlock
75-
) {
76-
return res.status(401).json({ error: "Unauthorized" });
77-
}
61+
// block if hard block
62+
if (userRule?.hardBlock) {
63+
return res.status(401).json({ error: "Unauthorized" });
7864
}
7965

8066
next();
8167
return;
8268
} catch (err) {
83-
console.error("Block Middleware Error:", err);
84-
res.status(401).json({ error: "Unauthorized", message: err });
69+
logger.error("Block Middleware Error:", err);
70+
res.status(401).json({ error: "Unauthorized" });
8571
return;
8672
}
8773
};

packages/provider/src/api/captcha.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
6262
*/
6363
router.post(ApiPaths.GetImageCaptchaChallenge, async (req, res, next) => {
6464
let parsed: CaptchaRequestBodyTypeOutput;
65+
66+
if (!req.ip) {
67+
return next(
68+
new ProsopoApiError("API.BAD_REQUEST", {
69+
context: { code: 400, error: "IP address not found" },
70+
}),
71+
);
72+
}
73+
74+
const ipAddress = getIPAddress(req.ip || "");
75+
6576
try {
6677
parsed = CaptchaRequestBody.parse(req.body);
6778
} catch (err) {
@@ -97,12 +108,19 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
97108
);
98109
}
99110

111+
const captchaConfig = await tasks.imgCaptchaManager.getCaptchaConfig(
112+
ipAddress,
113+
user,
114+
dapp,
115+
);
116+
100117
const taskData =
101118
await tasks.imgCaptchaManager.getRandomCaptchasAndRequestHash(
102119
datasetId,
103120
user,
104-
getIPAddress(req.ip || ""),
121+
ipAddress,
105122
flatten(req.headers),
123+
captchaConfig,
106124
);
107125
const captchaResponse: CaptchaResponseBody = {
108126
[ApiParams.status]: "ok",

packages/provider/src/rules/ip.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import type { BlockRule, IProviderDatabase } from "@prosopo/types-database";
2+
import type { Address4, Address6 } from "ip-address";
3+
4+
export const checkIpRules = async (
5+
db: IProviderDatabase,
6+
ipAddress: Address4 | Address6,
7+
dapp: string,
8+
): Promise<BlockRule | undefined> => {
9+
const rule = await db.getIPBlockRuleRecord(ipAddress.bigInt());
10+
11+
if (rule && BigInt(rule.ip) === ipAddress.bigInt()) {
12+
// block by IP address globally
13+
if (rule.global) {
14+
return rule;
15+
}
16+
17+
if (rule.dappAccount === dapp) {
18+
return rule;
19+
}
20+
}
21+
22+
const dappRule = await db.getIPBlockRuleRecord(ipAddress.bigInt(), dapp);
23+
if (
24+
dappRule &&
25+
dappRule.dappAccount === dapp &&
26+
BigInt(dappRule.ip) === ipAddress.bigInt()
27+
) {
28+
return rule;
29+
}
30+
31+
return undefined;
32+
};

packages/provider/src/rules/lang.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import type { ProsopoConfigOutput } from "@prosopo/types";
2+
3+
export const checkLangRules = (
4+
config: ProsopoConfigOutput,
5+
acceptLanguage: string,
6+
): number => {
7+
const lConfig = config.lRules;
8+
let lScore = 0;
9+
if (lConfig) {
10+
const languages = acceptLanguage
11+
.split(",")
12+
.map((lang) => lang.trim().split(";")[0]);
13+
14+
for (const lang of languages) {
15+
if (lang && lConfig[lang]) {
16+
lScore += lConfig[lang];
17+
}
18+
}
19+
}
20+
return lScore;
21+
};

packages/provider/src/rules/user.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { BlockRule, IProviderDatabase } from "@prosopo/types-database";
2+
3+
export const checkUserRules = async (
4+
db: IProviderDatabase,
5+
user: string,
6+
dapp: string,
7+
): Promise<BlockRule | undefined> => {
8+
const userRule = await db.getUserBlockRuleRecord(user, dapp);
9+
10+
if (
11+
userRule &&
12+
userRule.userAccount === user &&
13+
userRule.dappAccount === dapp
14+
) {
15+
return userRule;
16+
}
17+
return undefined;
18+
};

packages/provider/src/tasks/frictionless/frictionlessTasks.ts

Lines changed: 6 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ import { at, verifyRecency } from "@prosopo/util";
3030
import type { Address4, Address6 } from "ip-address";
3131
import type { ObjectId } from "mongoose";
3232
import { v4 as uuidv4 } from "uuid";
33-
34-
const logger = getLoggerDefault();
35-
const DEFAULT_POW_DIFFICULTY = 4;
33+
import { checkIpRules } from "../../rules/ip.js";
34+
import { checkLangRules } from "../../rules/lang.js";
35+
import { checkUserRules } from "../../rules/user.js";
3636

3737
export class FrictionlessManager {
3838
config: ProsopoConfigOutput;
@@ -53,57 +53,15 @@ export class FrictionlessManager {
5353
ipAddress: Address4 | Address6,
5454
dapp: string,
5555
): Promise<boolean> {
56-
const rule = await this.db.getIPBlockRuleRecord(ipAddress.bigInt());
57-
58-
if (rule && BigInt(rule.ip) === ipAddress.bigInt()) {
59-
// block by IP address globally
60-
if (rule.global) {
61-
return true;
62-
}
63-
64-
const dappRule = await this.db.getIPBlockRuleRecord(
65-
ipAddress.bigInt(),
66-
dapp,
67-
);
68-
if (
69-
dappRule &&
70-
dappRule.dappAccount === dapp &&
71-
BigInt(dappRule.ip) === ipAddress.bigInt()
72-
) {
73-
return true;
74-
}
75-
}
76-
return false;
56+
return !!(await checkIpRules(this.db, ipAddress, dapp));
7757
}
7858

7959
async checkUserRules(user: string, dapp: string): Promise<boolean> {
80-
const userRule = await this.db.getUserBlockRuleRecord(user, dapp);
81-
82-
if (
83-
userRule &&
84-
userRule.userAccount === user &&
85-
userRule.dappAccount === dapp
86-
) {
87-
return true;
88-
}
89-
return false;
60+
return !!(await checkUserRules(this.db, user, dapp));
9061
}
9162

9263
checkLangRules(acceptLanguage: string): number {
93-
const lConfig = this.config.lRules;
94-
let lScore = 0;
95-
if (lConfig) {
96-
const languages = acceptLanguage
97-
.split(",")
98-
.map((lang) => lang.trim().split(";")[0]);
99-
100-
for (const lang of languages) {
101-
if (lang && lConfig[lang]) {
102-
lScore += lConfig[lang];
103-
}
104-
}
105-
}
106-
return lScore;
64+
return checkLangRules(this.config, acceptLanguage);
10765
}
10866

10967
sendImageCaptcha(): GetFrictionlessCaptchaResponse {

packages/provider/src/tasks/imgCaptcha/imgCaptchaTasks.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import {
3131
type Hash,
3232
type ImageVerificationResponse,
3333
type PendingCaptchaRequest,
34+
type ProsopoCaptchaCountConfigSchemaOutput,
35+
type ProsopoConfigOutput,
3436
type RequestHeaders,
3537
} from "@prosopo/types";
3638
import type {
@@ -39,25 +41,28 @@ import type {
3941
} from "@prosopo/types-database";
4042
import { at } from "@prosopo/util";
4143
import type { Address4, Address6 } from "ip-address";
44+
import { checkIpRules } from "../../rules/ip.js";
45+
import { checkLangRules } from "../../rules/lang.js";
46+
import { checkUserRules } from "../../rules/user.js";
4247
import { shuffleArray } from "../../util.js";
4348
import { buildTreeAndGetCommitmentId } from "./imgCaptchaTasksUtils.js";
4449

4550
export class ImgCaptchaManager {
4651
db: IProviderDatabase;
4752
pair: KeyringPair;
4853
logger: Logger;
49-
captchaConfig: CaptchaConfig;
54+
config: ProsopoConfigOutput;
5055

5156
constructor(
5257
db: IProviderDatabase,
5358
pair: KeyringPair,
5459
logger: Logger,
55-
captchaConfig: CaptchaConfig,
60+
config: ProsopoConfigOutput,
5661
) {
5762
this.db = db;
5863
this.pair = pair;
5964
this.logger = logger;
60-
this.captchaConfig = captchaConfig;
65+
this.config = config;
6166
}
6267

6368
async getCaptchaWithProof(
@@ -85,6 +90,7 @@ export class ImgCaptchaManager {
8590
userAccount: string,
8691
ipAddress: Address4 | Address6,
8792
headers: RequestHeaders,
93+
captchaConfig: CaptchaConfig,
8894
): Promise<{
8995
captchas: Captcha[];
9096
requestHash: string;
@@ -103,10 +109,10 @@ export class ImgCaptchaManager {
103109
}
104110

105111
const unsolvedCount: number = Math.abs(
106-
Math.trunc(this.captchaConfig.unsolved.count),
112+
Math.trunc(captchaConfig.unsolved.count),
107113
);
108114
const solvedCount: number = Math.abs(
109-
Math.trunc(this.captchaConfig.solved.count),
115+
Math.trunc(captchaConfig.solved.count),
110116
);
111117

112118
if (!solvedCount) {
@@ -141,7 +147,7 @@ export class ImgCaptchaManager {
141147
.map((captcha) => captcha.timeLimitMs || DEFAULT_IMAGE_CAPTCHA_TIMEOUT)
142148
.reduce((a, b) => a + b, 0);
143149
const deadlineTs = timeLimit + currentTime;
144-
const currentBlockNumber = 0; //TEMP
150+
145151
await this.db.storeDappUserPending(
146152
userAccount,
147153
requestHash,
@@ -457,4 +463,30 @@ export class ImgCaptchaManager {
457463
commitmentId: solution.id.toString(),
458464
};
459465
}
466+
467+
async getCaptchaConfig(
468+
ipAddress: Address4 | Address6,
469+
user: string,
470+
dapp: string,
471+
): Promise<CaptchaConfig> {
472+
const ipRule = await checkIpRules(this.db, ipAddress, dapp);
473+
if (ipRule) {
474+
return {
475+
solved: { count: ipRule?.imageRounds?.solved || 0 },
476+
unsolved: { count: ipRule?.imageRounds?.unsolved || 0 },
477+
};
478+
}
479+
const userRule = await checkUserRules(this.db, user, dapp);
480+
if (userRule) {
481+
return {
482+
solved: { count: userRule?.imageRounds?.solved || 0 },
483+
unsolved: { count: userRule?.imageRounds?.unsolved || 0 },
484+
};
485+
}
486+
return this.config.captchas;
487+
}
488+
489+
checkLangRules(acceptLanguage: string): number {
490+
return checkLangRules(this.config, acceptLanguage);
491+
}
460492
}

packages/provider/src/tasks/tasks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export class Tasks {
6060
this.db,
6161
this.pair,
6262
this.logger,
63-
this.captchaConfig,
63+
this.config,
6464
);
6565
this.clientTaskManager = new ClientTaskManager(
6666
this.config,

0 commit comments

Comments
 (0)