From 0e45f10d99589348cc95bd044e9d06bc8dc1a10a Mon Sep 17 00:00:00 2001
From: MeiMei <30769358+mei23@users.noreply.github.com>
Date: Sat, 6 Feb 2021 11:46:47 +0900
Subject: [PATCH] Improve captcha (#7138)

---
 package.json                      |  2 --
 src/@types/recaptcha-promise.d.ts | 16 ---------
 src/misc/captcha.ts               | 56 +++++++++++++++++++++++++++++++
 src/server/api/private/signup.ts  | 24 ++++---------
 yarn.lock                         | 24 -------------
 5 files changed, 62 insertions(+), 60 deletions(-)
 delete mode 100644 src/@types/recaptcha-promise.d.ts
 create mode 100644 src/misc/captcha.ts

diff --git a/package.json b/package.json
index efc20ae894..35ccc0be58 100644
--- a/package.json
+++ b/package.json
@@ -152,7 +152,6 @@
 		"gulp-tslint": "8.1.4",
 		"gulp-typescript": "6.0.0-alpha.1",
 		"hard-source-webpack-plugin": "0.13.1",
-		"hcaptcha": "0.0.2",
 		"html-minifier": "4.0.0",
 		"http-proxy-agent": "4.0.1",
 		"http-signature": "1.3.5",
@@ -208,7 +207,6 @@
 		"random-seed": "0.3.0",
 		"ratelimiter": "3.4.1",
 		"re2": "1.15.9",
-		"recaptcha-promise": "1.0.0",
 		"reconnecting-websocket": "4.4.0",
 		"redis": "3.0.2",
 		"redis-lock": "0.1.4",
diff --git a/src/@types/recaptcha-promise.d.ts b/src/@types/recaptcha-promise.d.ts
deleted file mode 100644
index cfbd5eebf2..0000000000
--- a/src/@types/recaptcha-promise.d.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-declare module 'recaptcha-promise' {
-	interface IVerifyOptions {
-		secret_key?: string;
-	}
-
-	interface IVerify {
-		(response: string, remoteAddress?: string): Promise<boolean>;
-		init(options: IVerifyOptions): IVerify;
-	}
-
-	namespace recaptchaPromise {} // Hack
-
-	const verify: IVerify;
-
-	export = verify;
-}
diff --git a/src/misc/captcha.ts b/src/misc/captcha.ts
new file mode 100644
index 0000000000..87ec143ca8
--- /dev/null
+++ b/src/misc/captcha.ts
@@ -0,0 +1,56 @@
+import fetch from 'node-fetch';
+import { URLSearchParams } from 'url';
+import { getAgentByUrl } from './fetch';
+import config from '../config';
+
+export async function verifyRecaptcha(secret: string, response: string) {
+	const result = await getCaptchaResponse('https://www.recaptcha.net/recaptcha/api/siteverify', secret, response).catch(e => {
+		throw `recaptcha-request-failed: ${e}`;
+	});
+
+	if (result.success !== true) {
+		const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
+		throw `recaptcha-failed: ${errorCodes}`;
+	}
+}
+
+export async function verifyHcaptcha(secret: string, response: string) {
+	const result = await getCaptchaResponse('https://hcaptcha.com/siteverify', secret, response).catch(e => {
+		throw `hcaptcha-request-failed: ${e}`;
+	});
+
+	if (result.success !== true) {
+		const errorCodes = result['error-codes'] ? result['error-codes']?.join(', ') : '';
+		throw `hcaptcha-failed: ${errorCodes}`;
+	}
+}
+
+type CaptchaResponse = {
+	success: boolean;
+	'error-codes'?: string[];
+};
+
+async function getCaptchaResponse(url: string, secret: string, response: string): Promise<CaptchaResponse> {
+	const params = new URLSearchParams({
+		secret,
+		response
+	});
+
+	const res = await fetch(url, {
+		method: 'POST',
+		body: params,
+		headers: {
+			'User-Agent': config.userAgent
+		},
+		timeout: 10 * 1000,
+		agent: getAgentByUrl
+	}).catch(e => {
+		throw `${e.message || e}`;
+	});
+
+	if (!res.ok) {
+		throw `${res.status}`;
+	}
+
+	return await res.json() as CaptchaResponse;
+}
diff --git a/src/server/api/private/signup.ts b/src/server/api/private/signup.ts
index 6dc252ac45..3d467a0e68 100644
--- a/src/server/api/private/signup.ts
+++ b/src/server/api/private/signup.ts
@@ -1,7 +1,6 @@
 import * as Koa from 'koa';
 import { fetchMeta } from '../../../misc/fetch-meta';
-import { verify } from 'hcaptcha';
-import * as recaptcha from 'recaptcha-promise';
+import { verifyHcaptcha, verifyRecaptcha } from '../../../misc/captcha';
 import { Users, RegistrationTickets } from '../../../models';
 import { signup } from '../common/signup';
 
@@ -14,26 +13,15 @@ export default async (ctx: Koa.Context) => {
 	// ただしテスト時はこの機構は障害となるため無効にする
 	if (process.env.NODE_ENV !== 'test') {
 		if (instance.enableHcaptcha && instance.hcaptchaSecretKey) {
-			const success = await verify(instance.hcaptchaSecretKey, body['hcaptcha-response']).then(
-				({ success }) => success,
-				() => false,
-			);
-
-			if (!success) {
-				ctx.throw(400, 'hcaptcha-failed');
-			}
+			await verifyHcaptcha(instance.hcaptchaSecretKey, body['hcaptcha-response']).catch(e => {
+				ctx.throw(400, e);
+			});
 		}
 
 		if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
-			recaptcha.init({
-				secret_key: instance.recaptchaSecretKey
+			await verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(e => {
+				ctx.throw(400, e);
 			});
-
-			const success = await recaptcha(body['g-recaptcha-response']);
-
-			if (!success) {
-				ctx.throw(400, 'recaptcha-failed');
-			}
 		}
 	}
 
diff --git a/yarn.lock b/yarn.lock
index c34ee573a6..8c12fcc034 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1773,13 +1773,6 @@ aws4@^1.8.0:
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
   integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
 
-axios@^0.20.0:
-  version "0.20.0"
-  resolved "https://registry.yarnpkg.com/axios/-/axios-0.20.0.tgz#057ba30f04884694993a8cd07fa394cff11c50bd"
-  integrity sha512-ANA4rr2BDcmmAQLOKft2fufrtuvlqR+cXNNinUmvfeSNCOF98PZL+7M/v1zIdGo7OLjEA9J2gXJL+j4zGsl0bA==
-  dependencies:
-    follow-redirects "^1.10.0"
-
 bach@^1.0.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/bach/-/bach-1.2.0.tgz#4b3ce96bf27134f79a1b414a51c14e34c3bd9880"
@@ -4275,11 +4268,6 @@ flush-write-stream@^1.0.2:
     inherits "^2.0.3"
     readable-stream "^2.3.6"
 
-follow-redirects@^1.10.0:
-  version "1.13.0"
-  resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db"
-  integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA==
-
 for-in@^1.0.1, for-in@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80"
@@ -4825,11 +4813,6 @@ hash-sum@^2.0.0:
   resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a"
   integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==
 
-hcaptcha@0.0.2:
-  version "0.0.2"
-  resolved "https://registry.yarnpkg.com/hcaptcha/-/hcaptcha-0.0.2.tgz#18f4c055a2315db9f732ac77f9d0e30026bb2eb7"
-  integrity sha512-wWOncj/sY+q8s7tV12tjn3cFNoQhSu3l/7nTJi4QkFKALQi9XnduoXrV/KFzLg5lnB+5560zSAoi9YdYPDw6Eg==
-
 he@1.2.0, he@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
@@ -8701,13 +8684,6 @@ readdirp@~3.3.0:
   dependencies:
     picomatch "^2.0.7"
 
-recaptcha-promise@1.0.0:
-  version "1.0.0"
-  resolved "https://registry.yarnpkg.com/recaptcha-promise/-/recaptcha-promise-1.0.0.tgz#df16f208197fbfd571950cfb32ec3160e3909e0f"
-  integrity sha512-aiJNjKa13YqjF0QmiBUSFpUHjgjJAkRGBndbhHUrwyaxpGdzTxnsLlVEKZvh0gj75AJ/H8H6Bn9qCs8fVc3X1g==
-  dependencies:
-    axios "^0.20.0"
-
 rechoir@^0.6.2:
   version "0.6.2"
   resolved "https://registry.yarnpkg.com/rechoir/-/rechoir-0.6.2.tgz#85204b54dba82d5742e28c96756ef43af50e3384"