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;
}
}
}