diff --git a/backend/utils/antibot.js b/backend/utils/antibot.js new file mode 100644 index 00000000..a31fec7c --- /dev/null +++ b/backend/utils/antibot.js @@ -0,0 +1,123 @@ +var { obfuscate } = require("javascript-obfuscator"); +var fs = require("fs"); +var path = require("path"); + +var settingsPath = path.resolve(process.cwd(), "../nwotdata/settings.json"); +if (!fs.existsSync(settingsPath)) { // Do I even need this? + settingsPath = path.resolve(process.cwd(), "settings_example.json"); +} +var settings = JSON.parse(fs.readFileSync(settingsPath, "utf8")); + +class Antibot { + checkSolves = []; + solves = []; + + hash = Math.floor(Math.random() * 999); + + constructor() { + this.clientChecks = (settings.antibot && settings.antibot.client_checks) || []; + this.botGlobals = (settings.antibot && settings.antibot.bot_globals) || []; + + if (!this.clientChecks.length && !this.botGlobals.length) { + this.enabled = false; + return; + } + this.enabled = true; + + for (let i = 0; i < 3; i++) { + this.solves.push(this.clientChecks[Math.floor(Math.random() * this.clientChecks.length)]); + } + + for (let i = 0; i < this.solves.length; i++) { + this.checkSolves.push(Math.floor(Math.random() * 999) + ""); + } + } + + verifyMessage(ws, data) { + if (!this.enabled) return false; + if (!ws.sdata) return false; + + if (ws.sdata.antibot_verified) return false; + + if (ws.sdata.user && (ws.sdata.user.operator || ws.sdata.user.superuser || ws.sdata.user.staff)) { + ws.sdata.antibot_verified = true; + return false; + } + + if (data.kind == "antibot_response") { + if (this.verifyCode(data.code)) { + ws.sdata.antibot_verified = true; + return false; + } + return true; + } + + return true; + } + + sendChallenge(ws) { + if (!this.enabled) return; + if (!ws.sdata) return; + var code = this.generateCode(); + try { + ws.send(JSON.stringify({ + kind: "antibot_challenge", + code: code + })); + } catch(e) {} + } + + generateCode() { + if (!this.enabled) return null; + let clientChecksCode = this.solves.map((z, i) => { + return `if(${z[0]} == ${JSON.stringify(z[1])}) parts.push("${this.checkSolves[i]}")`; + }); + + let botGlobalsCode = this.botGlobals.map((g) => { + return `if(typeof ${g} !== "undefined") return 0`; + }); + + return obfuscate( + ` + let parts = []; + ${botGlobalsCode.join(";\n")} + ${clientChecksCode.join(";\n")} + return ${this.hash}*parts.map(z => z.split("").map(z => z.charCodeAt(0)).reduce((a, b)=>a+b)).reduce((a, b) => a + b); + `, + { + compact: true, + controlFlowFlattening: true, + controlFlowFlatteningThreshold: 1, + numbersToExpressions: true, + simplify: true, + stringArrayShuffle: true, + splitStrings: true, + stringArrayThreshold: 0.52, + }, + ).getObfuscatedCode(); + } + + verifyCode(code) { + if (!this.enabled) return true; + let solve = this.hash * + this.checkSolves.map((z) => + z.split("").map((z) => z.charCodeAt(0)).reduce((a, b) => a + b) + ).reduce((a, b) => a + b); + + if (solve !== code) { + console.log( + "Antibot failed! Code:", + code, + "Needed:", + solve, + "Hash:", + this.hash, + "Solves:", + this.solves, + ); + } + return solve == code; + } +} + +module.exports = Antibot; diff --git a/package.json b/package.json index f85f0fbc..d95f3e24 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "OurWorldOfText server", "dependencies": { "adm-zip": "^0.5.16", + "javascript-obfuscator": "^5.5.0", "nodemailer": "^7.0.10", "pg": "^8.13.1", "sqlite3": "^5.1.7", diff --git a/runserver.js b/runserver.js index 929c83d4..ccb5c18c 100644 --- a/runserver.js +++ b/runserver.js @@ -27,6 +27,7 @@ const utils = require("./backend/utils/utils.js"); const rate_limiter = require("./backend/utils/rate_limiter.js"); const ipaddress = require("./backend/framework/ipaddress.js"); const prompt = require("./backend/utils/prompt.js"); +const Antibot = require("./backend/utils/antibot.js"); const restrictions = require("./backend/utils/restrictions.js"); const frameUtils = require("./backend/framework/utils.js"); const serverUtil = require("./backend/framework/server.js"); @@ -900,6 +901,14 @@ async function initializeServer() { global_data.checkCSRF = httpServer.checkCSRF; global_data.createCSRF = httpServer.createCSRF; + var antibot = new Antibot(); + if(antibot.enabled) { + console.log("Antibot enabled (" + antibot.clientChecks.length + " checks, " + antibot.botGlobals.length + " bot globals)"); + } else { + console.log("Antibot disabled"); + } + global_data.antibot = antibot; + if(accountSystem == "local") { await loadEmail(); } @@ -2239,6 +2248,8 @@ async function manageWebsocketConnection(ws, req) { initial_user_count })); + global_data.antibot.sendChallenge(ws); + if(client_cursor_pos[world.id]) { var world_cursors = client_cursor_pos[world.id]; for(var csr_channel in world_cursors) { @@ -2304,6 +2315,7 @@ async function manageWebsocketConnection(ws, req) { } return send_ws(JSON.stringify(res)); } + if(global_data.antibot.verifyMessage(ws, msg)) return; // Begin calling a websocket function for the necessary request if(!websockets.hasOwnProperty(kind)) { return;