diff --git a/.pnp.cjs b/.pnp.cjs index a010c15041..864b4eec9f 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -2732,6 +2732,10 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "unbox-primitive",\ "npm:1.0.1"\ ],\ + [\ + "undici",\ + "npm:6.18.2"\ + ],\ [\ "unique-filename",\ "npm:3.0.0"\ @@ -3524,6 +3528,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["source-map-support", "npm:0.5.21"],\ ["ts-node", "virtual:4f1584ad4aba8733a24be7c8aebbffafef25607f2d00f4b314cf96717145c692763628a31c2b85d4686fbb091ff21ebffa3cc337399c042c19a32b9bdb786464#npm:8.10.2"],\ ["typescript", "patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587"],\ + ["undici", "npm:6.18.2"],\ ["webpack", "virtual:45dc8d177c5463b02ae2b62c45461f6704449bac45b0b4bf10ceca81013a617a6fa5aaf2547e43076d50ac57cad5c9979a6da6e8adf35b42d844e73e8c014613#npm:5.76.1"],\ ["webpack-cli", "virtual:45dc8d177c5463b02ae2b62c45461f6704449bac45b0b4bf10ceca81013a617a6fa5aaf2547e43076d50ac57cad5c9979a6da6e8adf35b42d844e73e8c014613#npm:4.7.0"]\ ],\ @@ -12921,6 +12926,15 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { "linkType": "HARD"\ }]\ ]],\ + ["undici", [\ + ["npm:6.18.2", {\ + "packageLocation": "./.yarn/cache/undici-npm-6.18.2-c1115b72ab-c20e47bd4f.zip/node_modules/undici/",\ + "packageDependencies": [\ + ["undici", "npm:6.18.2"]\ + ],\ + "linkType": "HARD"\ + }]\ + ]],\ ["unique-filename", [\ ["npm:3.0.0", {\ "packageLocation": "./.yarn/cache/unique-filename-npm-3.0.0-77d68e0a45-8e2f59b356.zip/node_modules/unique-filename/",\ diff --git a/.yarn/cache/fsevents-patch-2882183fbf-8.zip b/.yarn/cache/fsevents-patch-2882183fbf-8.zip deleted file mode 100644 index 0462fc6257..0000000000 --- a/.yarn/cache/fsevents-patch-2882183fbf-8.zip +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:1799807fd3c46bf8a480f38cdc9167580e203445956052633cbd7400e558a034 -size 23675 diff --git a/.yarn/cache/undici-npm-6.18.2-c1115b72ab-c20e47bd4f.zip b/.yarn/cache/undici-npm-6.18.2-c1115b72ab-c20e47bd4f.zip new file mode 100644 index 0000000000..565a3c8efa --- /dev/null +++ b/.yarn/cache/undici-npm-6.18.2-c1115b72ab-c20e47bd4f.zip @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ba1e16828c522ec925485239e75e5436b76abb0b39e0e573b6ae1b1becb8842 +size 353805 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"