From 021f72162dfb7ff83dd59d7107f0eb08eaf3bbbb Mon Sep 17 00:00:00 2001 From: Grant Date: Thu, 11 Jul 2024 21:14:12 -0600 Subject: [PATCH] duct-tape google recaptcha --- package-lock.json | 7 ++ packages/client/package.json | 1 + .../src/components/Info/InfoSidebar.tsx | 1 + packages/client/src/lib/network.ts | 9 ++ packages/client/src/lib/recaptcha.ts | 32 ++++++ packages/lib/src/net.ts | 3 + packages/server/src/lib/Logger.ts | 1 + packages/server/src/lib/Recaptcha.ts | 104 ++++++++++++++++++ packages/server/src/lib/SocketServer.ts | 6 + packages/server/src/types.ts | 6 + 10 files changed, 170 insertions(+) create mode 100644 packages/client/src/lib/recaptcha.ts create mode 100644 packages/server/src/lib/Recaptcha.ts diff --git a/package-lock.json b/package-lock.json index 7e86061..b848f75 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6332,6 +6332,12 @@ "@types/express": "*" } }, + "node_modules/@types/grecaptcha": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/grecaptcha/-/grecaptcha-3.0.9.tgz", + "integrity": "sha512-fFxMtjAvXXMYTzDFK5NpcVB7WHnrHVLl00QzEGpuFxSAC789io6M+vjcn+g5FTEamIJtJr/IHkCDsqvJxeWDyw==", + "dev": true + }, "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", @@ -16104,6 +16110,7 @@ }, "devDependencies": { "@tsconfig/vite-react": "^3.0.0", + "@types/grecaptcha": "^3.0.9", "@types/lodash.throttle": "^4.1.9", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", diff --git a/packages/client/package.json b/packages/client/package.json index 415e1ca..dba4e41 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -36,6 +36,7 @@ }, "devDependencies": { "@tsconfig/vite-react": "^3.0.0", + "@types/grecaptcha": "^3.0.9", "@types/lodash.throttle": "^4.1.9", "@types/react": "^18.2.48", "@types/react-dom": "^18.2.18", diff --git a/packages/client/src/components/Info/InfoSidebar.tsx b/packages/client/src/components/Info/InfoSidebar.tsx index 078ef62..b03bea3 100644 --- a/packages/client/src/components/Info/InfoSidebar.tsx +++ b/packages/client/src/components/Info/InfoSidebar.tsx @@ -62,6 +62,7 @@ export const InfoSidebar = () => { Build {__COMMIT_HASH__} +
); diff --git a/packages/client/src/lib/network.ts b/packages/client/src/lib/network.ts index a136f8b..2c0180f 100644 --- a/packages/client/src/lib/network.ts +++ b/packages/client/src/lib/network.ts @@ -11,6 +11,7 @@ import { } from "@sc07-canvas/lib/src/net"; import { toast } from "react-toastify"; import { handleAlert, handleDismiss } from "./alerts"; +import { Recaptcha } from "./recaptcha"; export interface INetworkEvents { connected: () => void; @@ -91,6 +92,14 @@ class Network extends EventEmitter { console.log("Reconnect failed"); }); + this.socket.on("recaptcha", (site_key) => { + Recaptcha.load(site_key); + }); + + this.socket.on("recaptcha_challenge", (ack) => { + Recaptcha.executeChallenge(ack); + }); + this.socket.on("user", (user) => { this.emit("user", user); }); diff --git a/packages/client/src/lib/recaptcha.ts b/packages/client/src/lib/recaptcha.ts new file mode 100644 index 0000000..0746f33 --- /dev/null +++ b/packages/client/src/lib/recaptcha.ts @@ -0,0 +1,32 @@ +class Recaptcha_ { + load(site_key: string) { + const script = document.createElement("script"); + script.setAttribute( + "src", + `https://www.google.com/recaptcha/api.js?render=explicit` + ); + document.head.appendChild(script); + + script.onload = () => { + grecaptcha.ready(() => { + grecaptcha.render("grecaptcha-badge", { + sitekey: site_key, + badge: "inline", + size: "invisible", + }); + + console.log("Google Recaptcha Loaded!"); + }); + }; + } + + executeChallenge(ack: (token: string) => void) { + console.log("[Recaptcha] Received challenge request..."); + grecaptcha.execute().then((token) => { + console.log("[Recaptcha] Sending challenge token back"); + ack(token as any); + }); + } +} + +export const Recaptcha = new Recaptcha_(); diff --git a/packages/lib/src/net.ts b/packages/lib/src/net.ts index 41e9bdc..6605058 100644 --- a/packages/lib/src/net.ts +++ b/packages/lib/src/net.ts @@ -23,6 +23,9 @@ export interface ServerToClientEvents { alert: (alert: IAlert) => void; alert_dismiss: (id: string) => void; + recaptcha: (site_key: string) => void; + recaptcha_challenge: (ack: (token: string) => void) => void; + /* --- subscribe events --- */ /** diff --git a/packages/server/src/lib/Logger.ts b/packages/server/src/lib/Logger.ts index f7e5651..f2ee3d2 100644 --- a/packages/server/src/lib/Logger.ts +++ b/packages/server/src/lib/Logger.ts @@ -38,6 +38,7 @@ export const LoggerType = createEnum([ "JOB_WORKER", "CANVAS_WORK", "WORKER_ROOT", + "RECAPTCHA", ]); export const getLogger = (module?: keyof typeof LoggerType) => diff --git a/packages/server/src/lib/Recaptcha.ts b/packages/server/src/lib/Recaptcha.ts new file mode 100644 index 0000000..4d2b29c --- /dev/null +++ b/packages/server/src/lib/Recaptcha.ts @@ -0,0 +1,104 @@ +import { Socket } from "socket.io"; +import { User } from "../models/User"; +import { getLogger } from "./Logger"; +import { + ClientToServerEvents, + ServerToClientEvents, +} from "@sc07-canvas/lib/src/net"; + +const Logger = getLogger("RECAPTCHA"); + +class Recaptcha_ { + disabled = false; + chance: number | null = null; + + constructor() { + this.disabled = + !process.env.RECAPTCHA_SITE_KEY || + !process.env.RECAPTCHA_SECRET_KEY || + !process.env.RECAPTCHA_PIXEL_CHANCE; + + if (!process.env.RECAPTCHA_PIXEL_CHANCE) { + Logger.warn("No RECAPTCHA_PIXEL_CHANCE set, captchas will not be sent!"); + } else { + this.chance = parseFloat(process.env.RECAPTCHA_PIXEL_CHANCE); + + if (this.chance > 1 || this.chance < 0) { + this.chance = null; + this.disabled = true; + Logger.warn("RECAPTCHA_PIXEL_CHANCE is not within (0 + ): boolean { + if (this.disabled || !this.chance) return false; + + if (Math.random() > this.chance) { + socket.emitWithAck("recaptcha_challenge").then((token) => { + this.verifyToken(token).then(async (data) => { + if (!data.success) { + this.notifyStaffOfError(data).then(() => {}); + } else { + if (data.score < 0.5 || true) { + try { + const user = (await User.fromAuthSession( + socket.request.session.user! + ))!; + this.notifyStaff(user, data.score).then(() => {}); + } catch (e) {} + } + } + }); + }); + return true; + } + + return false; + } + + async verifyToken( + token: string + ): Promise< + | { success: true; challenge_ts: string; hostname: string; score: number } + | { success: false; "error-codes": string[] } + > { + return await fetch( + `https://www.google.com/recaptcha/api/siteverify?secret=${process.env.RECAPTCHA_SECRET_KEY!}&response=${token}`, + { + method: "POST", + } + ).then((a) => a.json()); + } + + async notifyStaff(user: User, score: number) { + return await fetch(process.env.DISCORD_WEBHOOK!, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: `User ${user.sub} got a low score ${score}`, + }), + }); + } + + async notifyStaffOfError(obj: any) { + return await fetch(process.env.DISCORD_WEBHOOK!, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + content: + "Error while verifying captcha\n```\n" + + JSON.stringify(obj, null, 2) + + "\n```", + }), + }); + } +} + +export const Recaptcha = new Recaptcha_(); diff --git a/packages/server/src/lib/SocketServer.ts b/packages/server/src/lib/SocketServer.ts index 1099983..b9b4b9b 100644 --- a/packages/server/src/lib/SocketServer.ts +++ b/packages/server/src/lib/SocketServer.ts @@ -15,6 +15,7 @@ import { prisma } from "./prisma"; import { getLogger } from "./Logger"; import { Redis } from "./redis"; import { User } from "../models/User"; +import { Recaptcha } from "./Recaptcha"; const Logger = getLogger("SOCKET"); @@ -192,6 +193,9 @@ export class SocketServer { ); } + if (process.env.RECAPTCHA_SITE_KEY) + socket.emit("recaptcha", process.env.RECAPTCHA_SITE_KEY); + socket.emit("config", getClientConfig()); { let _clientNotifiedAboutCache = false; @@ -296,6 +300,8 @@ export class SocketServer { return; } + Recaptcha.maybeChallenge(socket); + await user.modifyStack(-1); await Canvas.setPixel( user, diff --git a/packages/server/src/types.ts b/packages/server/src/types.ts index 4347b15..727a6e7 100644 --- a/packages/server/src/types.ts +++ b/packages/server/src/types.ts @@ -60,6 +60,12 @@ declare global { MATRIX_HOMESERVER: string; ELEMENT_HOST: string; MATRIX_GENERAL_ALIAS: string; + + RECAPTCHA_SITE_KEY?: string; + RECAPTCHA_SECRET_KEY?: string; + RECAPTCHA_PIXEL_CHANCE?: string; + + DISCORD_WEBHOOK?: string; } } } -- GitLab