From 4abae2ceb22fa5a6a96cf35e606e1a1e64afb3ba Mon Sep 17 00:00:00 2001 From: Nicholas Wehr Date: Thu, 6 Jun 2024 13:41:52 -0600 Subject: [PATCH] added google recaptcha v2 validator to faucet service --- packages/faucet/README.md | 60 ++++++++++++------- packages/faucet/package.json | 3 +- packages/faucet/src/api/requestparser.spec.ts | 6 +- packages/faucet/src/api/requestparser.ts | 5 +- packages/faucet/src/api/webserver.ts | 23 ++++++- yarn.lock | 8 +++ 6 files changed, 78 insertions(+), 27 deletions(-) diff --git a/packages/faucet/README.md b/packages/faucet/README.md index e983367c06..856c6c0847 100644 --- a/packages/faucet/README.md +++ b/packages/faucet/README.md @@ -44,29 +44,35 @@ start Starts the faucet Environment variables -FAUCET_CONCURRENCY Number of distributor accounts. Defaults to 5. -FAUCET_PORT Port of the webserver. Defaults to 8000. -FAUCET_MEMO Memo for send transactions. Defaults to unset. -FAUCET_GAS_PRICE Gas price for transactions as a comma separated list. - Defaults to "0.025ucosm". -FAUCET_GAS_LIMIT Gas limit for send transactions. Defaults to 100000. -FAUCET_MNEMONIC Secret mnemonic that serves as the base secret for the - faucet HD accounts -FAUCET_PATH_PATTERN The pattern of BIP32 paths for the faucet accounts. - Must contain one "a" placeholder that is replaced with - the account index. - Defaults to the Cosmos Hub path "m/44'/118'/0'/0/a". -FAUCET_ADDRESS_PREFIX The bech32 address prefix. Defaults to "cosmos". -FAUCET_TOKENS A comma separated list of token denoms, e.g. - "uatom" or "ucosm, mstake". -FAUCET_CREDIT_AMOUNT_TKN Send this amount of TKN to a user requesting TKN. TKN is - a placeholder for the token's denom. Defaults to 10000000. -FAUCET_REFILL_FACTOR Send factor times credit amount on refilling. Defauls to 8. -FAUCET_REFILL_THRESHOLD Refill when balance gets below factor times credit amount. - Defaults to 20. -FAUCET_COOLDOWN_TIME Time (in seconds) after which an address can request - more tokens. Can be set to "0". Defaults to 24 hours - if unset or an empty string. +FAUCET_CONCURRENCY Number of distributor accounts. Defaults to 5. +FAUCET_PORT Port of the webserver. Defaults to 8000. +FAUCET_MEMO Memo for send transactions. Defaults to unset. +FAUCET_GAS_PRICE Gas price for transactions as a comma separated list. + Defaults to "0.025ucosm". +FAUCET_GAS_LIMIT Gas limit for send transactions. Defaults to 100000. +FAUCET_MNEMONIC Secret mnemonic that serves as the base secret for the + faucet HD accounts +FAUCET_PATH_PATTERN The pattern of BIP32 paths for the faucet accounts. + Must contain one "a" placeholder that is replaced with + the account index. + Defaults to the Cosmos Hub path "m/44'/118'/0'/0/a". +FAUCET_ADDRESS_PREFIX The bech32 address prefix. Defaults to "cosmos". +FAUCET_TOKENS A comma separated list of token denoms, e.g. + "uatom" or "ucosm, mstake". +FAUCET_CREDIT_AMOUNT_TKN Send this amount of TKN to a user requesting TKN. TKN is + a placeholder for the token's denom. Defaults to 10000000. +FAUCET_REFILL_FACTOR Send factor times credit amount on refilling. Defauls to 8. +FAUCET_REFILL_THRESHOLD Refill when balance gets below factor times credit amount. + Defaults to 20. +FAUCET_COOLDOWN_TIME Time (in seconds) after which an address can request + more tokens. Can be set to "0". Defaults to 24 hours + if unset or an empty string. +GOOGLE_RECAPTCHA_SECRET_KEY The secret key for validating input with the recaptcha v2 + service. If this value is set, then each call to the `/credit` + endpoint will require a valid recaptcha response string in + the JSON POST data named `recaptcha` in addition to the `denom` + and `address`. + Defaults to unset (disabled) ``` ### Faucet HD wallet @@ -134,6 +140,14 @@ curl --header "Content-Type: application/json" \ http://localhost:8000/credit ``` +### Using the faucet with Recaptcha validation enabled +``` +curl --header "Content-Type: application/json" \ + --request POST \ + --data '{"denom":"ucosm","address":"cosmos1yre6ac7qfgyfgvh58ph0rgw627rhw766y430qq", "recaptcha": "03AFcWeA6KFdGLxDQIx_UZ9Y9IMlAJyen-DkT3k..."}' \ + http://localhost:8000/credit +``` + ### Checking the faucets status The faucet provides a simple status check in the form of an http GET request. As diff --git a/packages/faucet/package.json b/packages/faucet/package.json index 20d4acf2c4..cbb5188702 100644 --- a/packages/faucet/package.json +++ b/packages/faucet/package.json @@ -49,7 +49,8 @@ "@cosmjs/utils": "workspace:^", "@koa/cors": "^3.3", "koa": "^2.13", - "koa-bodyparser": "^4.3" + "koa-bodyparser": "^4.3", + "undici": "^6.18.2" }, "devDependencies": { "@istanbuljs/nyc-config-typescript": "^1.0.1", diff --git a/packages/faucet/src/api/requestparser.spec.ts b/packages/faucet/src/api/requestparser.spec.ts index c8ae110c92..324d3d36db 100644 --- a/packages/faucet/src/api/requestparser.spec.ts +++ b/packages/faucet/src/api/requestparser.spec.ts @@ -3,7 +3,11 @@ import { RequestParser } from "./requestparser"; describe("RequestParser", () => { it("can process valid credit request with denom", () => { const body = { address: "abc", denom: "utkn" }; - expect(RequestParser.parseCreditBody(body)).toEqual({ address: "abc", denom: "utkn" }); + expect(RequestParser.parseCreditBody(body)).toEqual({ + address: "abc", + denom: "utkn", + recaptcha: undefined, + }); }); it("throws helpful error message when ticker is found", () => { diff --git a/packages/faucet/src/api/requestparser.ts b/packages/faucet/src/api/requestparser.ts index 2ff99d3c9d..cf29d2bb00 100644 --- a/packages/faucet/src/api/requestparser.ts +++ b/packages/faucet/src/api/requestparser.ts @@ -7,6 +7,8 @@ export interface CreditRequestBodyData { readonly denom: string; /** The recipient address */ readonly address: string; + /** The recaptcha v2 response */ + readonly recaptcha: string | undefined; } export interface CreditRequestBodyDataWithTicker { @@ -22,7 +24,7 @@ export class RequestParser { throw new HttpError(400, "Request body must be a dictionary."); } - const { address, denom, ticker } = body as any; + const { address, denom, ticker, recaptcha } = body as any; if (typeof ticker !== "undefined") { throw new HttpError(400, "The 'ticker' field was removed in CosmJS 0.23. Please use 'denom' instead."); @@ -47,6 +49,7 @@ export class RequestParser { return { address: address, denom: denom, + recaptcha: recaptcha, }; } } diff --git a/packages/faucet/src/api/webserver.ts b/packages/faucet/src/api/webserver.ts index c17b0656eb..dc8ccde23d 100644 --- a/packages/faucet/src/api/webserver.ts +++ b/packages/faucet/src/api/webserver.ts @@ -1,6 +1,8 @@ import Koa from "koa"; import cors = require("@koa/cors"); import bodyParser from "koa-bodyparser"; +import { request } from "undici"; +import qs from "node:querystring"; import { isValidAddress } from "../addresses"; import * as constants from "../constants"; @@ -59,7 +61,7 @@ export class Webserver { // context.request.body is set by the bodyParser() plugin const requestBody = (context.request as any).body; const creditBody = RequestParser.parseCreditBody(requestBody); - const { address, denom } = creditBody; + const { address, denom, recaptcha } = creditBody; if (!isValidAddress(address, constants.addressPrefix)) { throw new HttpError(400, "Address is not in the expected format for this chain."); @@ -82,6 +84,25 @@ export class Webserver { throw new HttpError(422, `Token is not available. Available tokens are: ${availableTokens}`); } + // if enabled, require recaptcha validation + if (process.env.GOOGLE_RECAPTCHA_SECRET_KEY !== undefined) { + const { body } = await request("https://www.google.com/recaptcha/api/siteverify", { + method: "POST", + headers: { + "content-type": "application/x-www-form-urlencoded", + }, + body: qs.stringify({ + secret: process.env.GOOGLE_RECAPTCHA_SECRET_KEY, + response: recaptcha, + }), + }); + const verify_data = (await body.json()) as { success: boolean }; + if (!verify_data.success) { + console.error(`recaptcha validation FAILED ${JSON.stringify(verify_data, null, 4)}`); + throw new HttpError(423, `Recaptcha failed to verify`); + } + } + try { // Count addresses to prevent draining this.addressCounter.set(address, new Date()); diff --git a/yarn.lock b/yarn.lock index 867ad54d77..55028d5a8c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -606,6 +606,7 @@ __metadata: source-map-support: ^0.5.19 ts-node: ^8 typescript: ~4.9 + undici: ^6.18.2 webpack: ^5.76.0 webpack-cli: ^4.6.0 bin: @@ -7697,6 +7698,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^6.18.2": + version: 6.18.2 + resolution: "undici@npm:6.18.2" + checksum: c20e47bd4f959c00d24516756b178190f0a9ae000007e875f1f68c8e7f3f9a68b0a7faa03f3d030ddd71a9e3feb558fbce661b5229a0aa8380cfbe1cea4281e4 + languageName: node + linkType: hard + "unique-filename@npm:^3.0.0": version: 3.0.0 resolution: "unique-filename@npm:3.0.0"