Skip to content

Commit

Permalink
Frictionless with account, ip blocking, acc blocking (#1483)
Browse files Browse the repository at this point in the history
  • Loading branch information
HughParry authored Oct 29, 2024
1 parent 8f9094a commit cb064a7
Show file tree
Hide file tree
Showing 15 changed files with 194 additions and 46 deletions.
2 changes: 2 additions & 0 deletions packages/api/src/api/ProviderApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,12 @@ export default class ProviderApi
public getFrictionlessCaptcha(
token: string,
dapp: string,
user: string,
): Promise<GetFrictionlessCaptchaResponse> {
const body = {
[ApiParams.token]: token,
[ApiParams.dapp]: dapp,
[ApiParams.user]: user,
};
return this.post(ApiPaths.GetFrictionlessCaptchaChallenge, body);
}
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/src/commands/addBlockRules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ export default (
demandOption: true,
default: true,
desc: "Whether the ip is to be blocked globally or not",
} as const)
.option("hardBlock", {
type: "string" as const,
demandOption: true,
default: false,
desc: "Hardblock stops requests, softblock informs frictionless",
} as const),
handler: async (argv: ArgumentsCamelCase) => {
try {
Expand All @@ -65,12 +71,14 @@ export default (
await tasks.clientTaskManager.addIPBlockRules(
argv.ips as unknown as string[],
argv.global as boolean,
argv.hardBlock as boolean,
argv.dapp as unknown as string,
);
}
if (argv.users) {
await tasks.clientTaskManager.addUserBlockRules(
argv.users as unknown as string[],
argv.hardBlock as boolean,
argv.dapp as unknown as string,
);
}
Expand Down
21 changes: 16 additions & 5 deletions packages/procaptcha-frictionless/src/ProcaptchaFrictionless.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import { ExtensionWeb2 } from "@prosopo/account";
import { ProviderApi } from "@prosopo/api";
import { ProsopoEnvError } from "@prosopo/common";
import detect from "@prosopo/detector";
Expand All @@ -33,6 +34,8 @@ const customDetectBot: BotDetectionFunction = async (
) => {
const botScore: { token: string } = await detect();

const userAccount = await new ExtensionWeb2().getAccount(config);

if (!config.account.address) {
throw new ProsopoEnvError("GENERAL.SITE_KEY_MISSING");
}
Expand All @@ -47,13 +50,15 @@ const customDetectBot: BotDetectionFunction = async (
const captcha = await providerApi.getFrictionlessCaptcha(
botScore.token,
config.account.address,
userAccount.account.address,
);

return {
captchaType: captcha.captchaType,
sessionId: captcha.sessionId,
provider: provider,
status: captcha.status,
userAccount: userAccount,
};
};

Expand All @@ -73,15 +78,21 @@ export const ProcaptchaFrictionless = ({
try {
const result = await detectBot(configOutput);

const frictionlessState: FrictionlessState = {
provider: result.provider,
sessionId: result.sessionId,
userAccount: result.userAccount,
};

if (result.captchaType === "image") {
setComponentToRender(
<Procaptcha config={config} callbacks={callbacks} />,
<Procaptcha
config={config}
callbacks={callbacks}
frictionlessState={frictionlessState}
/>,
);
} else {
const frictionlessState: FrictionlessState = {
provider: result.provider,
sessionId: result.sessionId,
};
setComponentToRender(
<ProcaptchaPow
config={config}
Expand Down
13 changes: 0 additions & 13 deletions packages/procaptcha-pow/src/components/Captcha.tsx

This file was deleted.

10 changes: 8 additions & 2 deletions packages/procaptcha-pow/src/services/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,10 +151,16 @@ export const Manager = (
const config = getConfig();

// check if account exists in extension
const ext = config.web2 ? new ExtensionWeb2() : new ExtensionWeb3();
const selectAccount = async () => {
if (frictionlessState) {
return frictionlessState.userAccount;
}
const ext = config.web2 ? new ExtensionWeb2() : new ExtensionWeb3();
return await ext.getAccount(config);
};

// use the passed in account (could be web3) or create a new account
const user = await ext.getAccount(config);
const user = await selectAccount();
const userAccount = user.account.address;

// set the account created or injected by the extension
Expand Down
14 changes: 12 additions & 2 deletions packages/procaptcha/src/modules/Manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {
ApiParams,
type CaptchaResponseBody,
type CaptchaSolution,
type FrictionlessState,
type ProcaptchaCallbacks,
type ProcaptchaClientConfigInput,
type ProcaptchaClientConfigOutput,
Expand Down Expand Up @@ -69,6 +70,7 @@ export function Manager(
state: ProcaptchaState,
onStateUpdate: ProcaptchaStateUpdateFn,
callbacks: ProcaptchaCallbacks,
frictionlessState?: FrictionlessState,
) {
const events = getDefaultEvents(onStateUpdate, state, callbacks);

Expand Down Expand Up @@ -423,8 +425,16 @@ export function Manager(
}

// check if account exists in extension
const ext = config.web2 ? new ExtensionWeb2() : new ExtensionWeb3();
const account = await ext.getAccount(config);
const selectAccount = async () => {
if (frictionlessState) {
return frictionlessState.userAccount;
}
const ext = config.web2 ? new ExtensionWeb2() : new ExtensionWeb3();
return await ext.getAccount(config);
};

const account = await selectAccount();

// Store the account in local storage
storage.setAccount(account.account.address);

Expand Down
7 changes: 4 additions & 3 deletions packages/provider/src/api/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const blockMiddleware = (env: ProviderEnvironment) => {
const rule = await env.getDb().getIPBlockRuleRecord(ipAddress.bigInt());
if (rule && BigInt(rule.ip) === ipAddress.bigInt()) {
// block by IP address globally
if (rule.global) {
if (rule.global && rule.hardBlock) {
return res.status(401).json({ error: "Unauthorized" });
}

Expand All @@ -53,7 +53,7 @@ export const blockMiddleware = (env: ProviderEnvironment) => {
.getDb()
.getIPBlockRuleRecord(ipAddress.bigInt(), dappAccount);
if (
dappRule &&
dappRule?.hardBlock &&
dappRule.dappAccount === dappAccount &&
BigInt(dappRule.ip) === ipAddress.bigInt()
) {
Expand All @@ -70,7 +70,8 @@ export const blockMiddleware = (env: ProviderEnvironment) => {
if (
rule &&
rule.userAccount === userAccount &&
rule.dappAccount === dappAccount
rule.dappAccount === dappAccount &&
rule.hardBlock
) {
return res.status(401).json({ error: "Unauthorized" });
}
Expand Down
43 changes: 23 additions & 20 deletions packages/provider/src/api/captcha.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,33 +423,36 @@ export function prosopoRouter(env: ProviderEnvironment): Router {
ApiPaths.GetFrictionlessCaptchaChallenge,
async (req, res, next) => {
try {
const { token, dapp } =
const { token, dapp, user } =
GetFrictionlessCaptchaChallengeRequestBody.parse(req.body);
const botScore = await getBotScore(token);
const clientConfig = await tasks.db.getClientRecord(dapp);
const botThreshold =
clientConfig?.settings?.frictionlessThreshold ||
DEFAULT_FRICTIONLESS_THRESHOLD;

if (Number(botScore) > botThreshold) {
const response: GetFrictionlessCaptchaResponse = {
[ApiParams.captchaType]: "image",
[ApiParams.status]: "ok",
};
return res.json(response);
}
const sessionRecord: Session = {
sessionId: uuidv4(),
createdAt: new Date(),
};

await tasks.db.storeSessionRecord(sessionRecord);

const response: GetFrictionlessCaptchaResponse = {
[ApiParams.captchaType]: "pow",
[ApiParams.sessionId]: sessionRecord.sessionId,
[ApiParams.status]: "ok",
};
// Check if the IP address is blocked
const ipAddress = getIPAddress(req.ip || "");
const isIpBlocked = await tasks.frictionlessManager.checkIpRules(
ipAddress,
dapp,
);
if (isIpBlocked)
return res.json(tasks.frictionlessManager.sendImageCaptcha());

// Check if the user is blocked
const isUserBlocked = await tasks.frictionlessManager.checkUserRules(
user,
dapp,
);
if (isUserBlocked)
return res.json(tasks.frictionlessManager.sendImageCaptcha());

// If the bot score is greater than the threshold, send an image captcha
if (Number(botScore) > botThreshold)
return res.json(tasks.frictionlessManager.sendImageCaptcha());

const response = await tasks.frictionlessManager.sendPowCaptcha();
return res.json(response);
} catch (err) {
console.error("Error in frictionless captcha challenge:", err);
Expand Down
4 changes: 4 additions & 0 deletions packages/provider/src/tasks/client/clientTasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export class ClientTaskManager {
async addIPBlockRules(
ips: string[],
global: boolean,
hardBlock: boolean,
dappAccount?: string,
): Promise<void> {
const rules: IPAddressBlockRule[] = ips.map((ip) => {
Expand All @@ -222,13 +223,15 @@ export class ClientTaskManager {
global,
type: BlockRuleType.ipAddress,
dappAccount,
hardBlock,
};
});
await this.providerDB.storeIPBlockRuleRecords(rules);
}

async addUserBlockRules(
userAccounts: string[],
hardBlock: boolean,
dappAccount: string,
): Promise<void> {
validateAddress(dappAccount, false, 42);
Expand All @@ -240,6 +243,7 @@ export class ClientTaskManager {
type: BlockRuleType.userAccount,
// TODO don't store global on these
global: false,
hardBlock,
};
});
await this.providerDB.storeUserBlockRuleRecords(rules);
Expand Down
105 changes: 105 additions & 0 deletions packages/provider/src/tasks/frictionless/frictionlessTasks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Copyright 2021-2024 Prosopo (UK) Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import type { KeyringPair } from "@polkadot/keyring/types";
import { stringToHex, u8aToHex } from "@polkadot/util";
import { ProsopoEnvError, getLoggerDefault } from "@prosopo/common";
import {
ApiParams,
type CaptchaResult,
CaptchaStatus,
type GetFrictionlessCaptchaResponse,
POW_SEPARATOR,
type PoWCaptcha,
type PoWChallengeId,
type RequestHeaders,
} from "@prosopo/types";
import type { IProviderDatabase, Session } from "@prosopo/types-database";
import { at, verifyRecency } from "@prosopo/util";
import type { Address4, Address6 } from "ip-address";
import { v4 as uuidv4 } from "uuid";

const logger = getLoggerDefault();
const DEFAULT_POW_DIFFICULTY = 4;

export class FrictionlessManager {
pair: KeyringPair;
db: IProviderDatabase;

constructor(pair: KeyringPair, db: IProviderDatabase) {
this.pair = pair;
this.db = db;
}

async checkIpRules(
ipAddress: Address4 | Address6,
dapp: string,
): Promise<boolean> {
const rule = await this.db.getIPBlockRuleRecord(ipAddress.bigInt());

if (rule && BigInt(rule.ip) === ipAddress.bigInt()) {
// block by IP address globally
if (rule.global) {
return true;
}

const dappRule = await this.db.getIPBlockRuleRecord(
ipAddress.bigInt(),
dapp,
);
if (
dappRule &&
dappRule.dappAccount === dapp &&
BigInt(dappRule.ip) === ipAddress.bigInt()
) {
return true;
}
}
return false;
}

async checkUserRules(user: string, dapp: string): Promise<boolean> {
const userRule = await this.db.getUserBlockRuleRecord(user, dapp);

if (
userRule &&
userRule.userAccount === user &&
userRule.dappAccount === dapp
) {
return true;
}
return false;
}

sendImageCaptcha(): GetFrictionlessCaptchaResponse {
return {
[ApiParams.captchaType]: "image",
[ApiParams.status]: "ok",
};
}

async sendPowCaptcha(): Promise<GetFrictionlessCaptchaResponse> {
const sessionRecord: Session = {
sessionId: uuidv4(),
createdAt: new Date(),
};

await this.db.storeSessionRecord(sessionRecord);

return {
[ApiParams.captchaType]: "pow",
[ApiParams.sessionId]: sessionRecord.sessionId,
[ApiParams.status]: "ok",
};
}
}
Loading

0 comments on commit cb064a7

Please sign in to comment.