diff --git a/package-lock.json b/package-lock.json index 7e860619a79fa74e13327fa60fdf6133c5280d17..b848f759cf65e146ce03a132176d06a08893d2e6 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 415e1ca7184e0224ca8ba2dd76f8143c9851c793..dba4e41a9ef01fedf6c8cac0d7ad19d1e88aaffc 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 078ef6224d4427713819817edf4bf9a09a331b37..b03bea3587ef90d0eeca886417457d8d29ac49e0 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 a136f8b04a7802ef51d4ac71ef8a8e89d3d80017..2c0180f40ec2f030cbf70e07c6805bd4d6aa766e 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 0000000000000000000000000000000000000000..0746f3357d7ee6f91f9be9ee65686c99d858cfd4 --- /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 41e9bdccd8fc6700132681c329af590e40ac1f77..660505888402fe703b86df1adf6e6b8c677343e9 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 f7e5651a5ba53ea1c37e10adb3cf5c1229425a87..f2ee3d2572b5d5810c39ceb1b9b78bb486380007 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 0000000000000000000000000000000000000000..4d2b29c8452b0ac288f2b6102699df6b6033c8aa --- /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 1099983c9015caece31a99ab81f29284f01a60c3..b9b4b9ba7045b67aace7bdd368b1e4f491f7c89f 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 4347b1535aa334579f70ca6244205bc35fdb35d5..727a6e7ed97523087ab2cf8251b51a3875751341 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; } } }