From 3aa87a40a704df21cadb306055bb55a1588efa7d Mon Sep 17 00:00:00 2001 From: Error430 Date: Sat, 26 Oct 2024 20:30:58 -0400 Subject: [PATCH 1/3] Proxycheck hotfix --- pnpm-lock.yaml | 21 ----- server/package.json | 1 - server/src/config.ts | 5 +- server/src/server.ts | 32 +++---- server/src/utils/apiHelper.ts | 164 +++++++++++++++++++--------------- 5 files changed, 106 insertions(+), 117 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 512d75c88..6768298fb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -136,9 +136,6 @@ importers: dotenv: specifier: ^16.4.5 version: 16.4.5 - proxycheck-ts: - specifier: ^0.0.11 - version: 0.0.11 ts-node: specifier: ^10.9.2 version: 10.9.2(@types/node@22.7.3)(typescript@5.6.2) @@ -1500,9 +1497,6 @@ packages: resolution: {integrity: sha512-1VdUuRnQP4drdFkS8NKvDR1NBgevm8TOuflcaZEKsxw42CxonjW/2vkj1AKlinJb4ZLwBcuWF9GiPr7FQc6AQA==} engines: {node: '>=18.0'} - cross-fetch@4.0.0: - resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==} - cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -2653,9 +2647,6 @@ packages: resolution: {integrity: sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==} engines: {node: '>= 6'} - proxycheck-ts@0.0.11: - resolution: {integrity: sha512-RxkN5FuklqDjiTNdRqair96AgkP/tc6boVZgNcLil17qNNmlZhXHPKhIlw51EdC+vgrsDCk5MA1fUzojEXo4Bg==} - punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -4530,12 +4521,6 @@ snapshots: croner@8.1.1: {} - cross-fetch@4.0.0: - dependencies: - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - cross-spawn@7.0.3: dependencies: path-key: 3.1.1 @@ -5887,12 +5872,6 @@ snapshots: kleur: 3.0.3 sisteransi: 1.0.5 - proxycheck-ts@0.0.11: - dependencies: - cross-fetch: 4.0.0 - transitivePeerDependencies: - - encoding - punycode@2.3.1: {} pure-rand@6.1.0: {} diff --git a/server/package.json b/server/package.json index b05ee6a4e..a4971d200 100644 --- a/server/package.json +++ b/server/package.json @@ -25,7 +25,6 @@ "@damienvesper/bit-buffer": "^1.0.1", "croner": "^8.1.1", "dotenv": "^16.4.5", - "proxycheck-ts": "^0.0.11", "ts-node": "^10.9.2", "uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.48.0", "ws": "^8.18.0" diff --git a/server/src/config.ts b/server/src/config.ts index cd6a0964b..6375ff5e9 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -229,7 +229,10 @@ export interface ConfigType { /** * If specified, the proxycheck.io API will be used to detect and block VPNs and proxies. */ - readonly proxyCheckAPIKey?: string + readonly ipChecker?: { + readonly key: string + readonly baseUrl: string + } } /** diff --git a/server/src/server.ts b/server/src/server.ts index ad1a690f5..5c82cc2d4 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -17,40 +17,31 @@ import { CustomTeam, CustomTeamPlayer, type CustomTeamPlayerContainer } from "./ import { Logger } from "./utils/misc"; import { cors, createServer, forbidden, getIP, textDecoder } from "./utils/serverHelpers"; import { cleanUsername } from "./utils/misc"; -import ProxyCheck from "proxycheck-ts"; - -export interface Punishment { - readonly id: string - readonly ip: string - readonly reportId: string - readonly reason: string - readonly reporter: string - readonly expires?: number - readonly punishmentType: "warn" | "temp" | "perma" -} +import IpChecker from "./utils/apiHelper"; +import { Punishment } from "./utils/apiHelper"; let punishments: Punishment[] = []; -const proxyCheck = Config.protection?.proxyCheckAPIKey - ? new ProxyCheck({ api_key: Config.protection.proxyCheckAPIKey }) +const ipCheck = Config.protection?.ipChecker + ? new IpChecker(Config.protection.ipChecker.baseUrl, Config.protection.ipChecker.key) : undefined; const isVPN = new Map( existsSync("isVPN.json") - ? Object.entries(JSON.parse(readFileSync("isVPN.json", "utf8"))) + ? Object.entries(JSON.parse(readFileSync("isVPN.json", "utf8")) as Record) : undefined ); async function isVPNCheck(ip: string): Promise { - if (!proxyCheck) return false; + if (!ipCheck) return false; let ipIsVPN = isVPN.get(ip); if (ipIsVPN !== undefined) return ipIsVPN; - const result = await proxyCheck.checkIP(ip, { vpn: 3 }, 5000); - if (!result || result.status === "denied" || result.status === "error") return false; + const result = await ipCheck.check(ip); + if (!result?.flagged) return false; - ipIsVPN = result[ip].proxy === "yes" || result[ip].vpn === "yes"; + ipIsVPN = result.flagged; isVPN.set(ip, ipIsVPN); return ipIsVPN; } @@ -119,12 +110,10 @@ if (isMainThread) { removePunishment(ip); } response = { success: false, message: punishment.punishmentType, reason: punishment.reason, reportID: punishment.reportId }; - } else { const teamID = maxTeamSize !== TeamSize.Solo && new URLSearchParams(req.getQuery()).get("teamID"); // must be here or it causes uWS errors if (await isVPNCheck(ip)) { response = { success: false, message: "perma", reason: "VPN/proxy detected. To play the game, please disable it." }; - } else if (teamID) { const team = customTeams.get(teamID); if (team?.gameID !== undefined) { @@ -134,7 +123,6 @@ if (isMainThread) { } else { response = { success: false }; } - } else { response = findGame(); } @@ -394,7 +382,7 @@ if (isMainThread) { teamsCreated = {}; - if (protection.proxyCheckAPIKey) { + if (protection.ipChecker) { writeFileSync("isVPN.json", JSON.stringify(Object.fromEntries(isVPN))); } diff --git a/server/src/utils/apiHelper.ts b/server/src/utils/apiHelper.ts index 52fb94513..1bca76d43 100644 --- a/server/src/utils/apiHelper.ts +++ b/server/src/utils/apiHelper.ts @@ -1,85 +1,105 @@ -import * as https from "https"; -// FIXME this should be in a .env! -export const mod_api_data = { - API_SERVER_URL: "api server url", - API_SERVER_KEY: "Server Key", - API_WEBHOOK_URL: "Logging webhook" -}; +interface IpCheckResponse { + flagged: boolean + message: string +} -/** - * Sends a POST request to the specified URL with the given data. - * @param url URL to contact - * @param data Data to send - * @returns Promise resolving to the response data - */ -export const sendPostRequest = (url: string, data: unknown): Promise => { - return new Promise((resolve, reject) => { - const payload = JSON.stringify(data); +export interface Punishment { + readonly id: string + readonly ip: string + readonly reportId: string + readonly reason: string + readonly reporter: string + readonly expires?: number + readonly punishmentType: "warn" | "temp" | "perma" +} - const options: https.RequestOptions = { - method: "POST", - headers: { - "Content-Type": "application/json", - "api-key": mod_api_data.API_SERVER_KEY - } - }; - - const req = https.request(url, options, res => { - let responseData = ""; +class IpChecker { + private readonly baseUrl: string; + private readonly apiKey: string; - res.on("data", chunk => { - responseData += String(chunk); - }); - - res.on("end", () => { - resolve(responseData); - }); - }); + /** + * Constructs an instance of IpChecker. + * @param baseUrl - The base URL of the API (e.g., 'https://api.example.com') + * @param apiKey - The API key to be sent in the 'api-key' header + */ + constructor(baseUrl: string, apiKey: string) { + this.baseUrl = baseUrl.replace(/\/+$/, ""); // Remove trailing slashes + this.apiKey = apiKey; + } - req.on("error", (error: Error) => { - reject(error); - }); + /** + * Checks if the given IP is flagged by the API. + * If any error occurs, it returns { flagged: false, message: 'IP is not a proxy/VPN' } + * @param ip - The IP address to check + * @returns A promise that resolves to an IpCheckResponse + */ + async check(ip: string): Promise { + try { + this.validateIp(ip); + const url = `${this.baseUrl}/${ip}`; - req.write(payload); - req.end(); - }); -}; + const headers = { + "api-key": this.apiKey + }; -/** - * Sends a GET request to the specified URL with the given data as query parameters. - * @param url URL to contact - * @param data Data to send as query parameters - * @returns Promise resolving to the response data - */ -export const sendGetRequest = (url: string, data: unknown): Promise => { - return new Promise((resolve, reject) => { - const queryString = new URLSearchParams(data as Record).toString(); - const fullUrl = `${url}?${queryString}`; + const response = await fetch(url, { + method: "GET", + headers: headers + }); - const options: https.RequestOptions = { - method: "GET", - headers: { - "Content-Type": "application/json", - "api-key": mod_api_data.API_SERVER_KEY + return await this.handleResponse(response); + } catch (error: unknown) { + // If any error occurs, return that the IP is not a proxy/VPN + if (error instanceof Error) { + console.log(error.message); + } else { + console.log(String(error)); } - }; + return { + flagged: false, + message: "IP is not a proxy/VPN" + }; + } + } - const req = https.request(fullUrl, options, res => { - let responseData = ""; + /** + * Validates the IP address format. + * @param ip - The IP address to validate + */ + private validateIp(ip: string): void { + const ipRegex + = /^(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d{2}|[1-9]?\d)){3}$/; + if (!ipRegex.test(ip)) { + throw new Error(`Invalid IP address: ${ip}`); + } + } - res.on("data", chunk => { - responseData += String(chunk); - }); + /** + * Handles the API response, checking for errors and parsing JSON. + * If the response is not OK, it returns an IpCheckResponse indicating IP is not a proxy/VPN. + * @param response - The fetch Response object + * @returns A promise that resolves to the parsed response data + */ + private async handleResponse(response: Response): Promise { + const contentType = response.headers.get("Content-Type"); + let data: T | string; - res.on("end", () => { - resolve(responseData); - }); - }); + if (contentType?.includes("application/json")) { + data = (await response.json()) as T; + } else { + data = await response.text(); + } + + if (!response.ok) { + // Instead of throwing an error, return an IpCheckResponse indicating IP is not a proxy/VPN + return { + flagged: false, + message: "IP is not a proxy/VPN" + } as unknown as T; + } - req.on("error", (error: Error) => { - reject(error); - }); + return data as T; + } +} - req.end(); - }); -}; +export default IpChecker; From 93f4d8574a8633505b8aead990a72092e9239c1c Mon Sep 17 00:00:00 2001 From: Error430 Date: Sat, 26 Oct 2024 21:09:27 -0400 Subject: [PATCH 2/3] fix to the hotfix --- server/src/config.ts | 1 + server/src/objects/player.ts | 80 ++++++++++++++++++------------------ 2 files changed, 42 insertions(+), 39 deletions(-) diff --git a/server/src/config.ts b/server/src/config.ts index 6375ff5e9..8f6c02015 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -232,6 +232,7 @@ export interface ConfigType { readonly ipChecker?: { readonly key: string readonly baseUrl: string + readonly logURL: string } } diff --git a/server/src/objects/player.ts b/server/src/objects/player.ts index a36927997..de04d4d17 100644 --- a/server/src/objects/player.ts +++ b/server/src/objects/player.ts @@ -24,7 +24,6 @@ import { CountableInventoryItem, InventoryItem } from "../inventory/inventoryIte import { MeleeItem } from "../inventory/meleeItem"; import { ThrowableItem } from "../inventory/throwableItem"; import { type Team } from "../team"; -import { mod_api_data, sendPostRequest } from "../utils/apiHelper"; import { removeFrom } from "../utils/misc"; export interface PlayerContainer { @@ -1199,48 +1198,51 @@ export class Player extends BaseGameObject.derive(ObjectCategory.Player) { playerName: this.spectating?.name ?? "", reportID: reportID })); + if (Config.protection) { + const reportURL = String(Config.protection?.ipChecker?.logURL); + const reportData = { + embeds: [ + { + title: "Report Received", + description: `Report ID: \`${reportID}\``, + color: 16711680, + fields: [ + { + name: "Username", + value: `\`${this.spectating?.name}\`` + }, + { + name: "Time reported", + value: this.game.now + }, + { + name: "Reporter", + value: this.name + } + + ] + } + ] + }; - const reportURL = String(mod_api_data.API_WEBHOOK_URL); - const reportData = { - embeds: [ - { - title: "Report Received", - description: `Report ID: \`${reportID}\``, - color: 16711680, - fields: [ - { - name: "Username", - value: `\`${this.spectating?.name}\`` - }, - { - name: "Time reported", - value: this.game.now - }, - { - name: "Reporter", - value: this.name - } - - ] - } - ] - }; - - /* - Promise result ignored because we don't really care - what happens when we send a post request to Discord - for logging - */ - sendPostRequest(reportURL, reportData) - .catch(error => { + // Send report to Discord + fetch(reportURL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(reportData) + }).catch(error => { console.error("Error: ", error); }); - // Post the report to the server with the json - sendPostRequest(`${mod_api_data.API_SERVER_URL}/reports`, reportJson) - .then(console.log) - .catch((e: unknown) => console.error(e)); - // i love eslint + // Post the report to the server + fetch(`${Config.protection?.punishments?.url}/reports`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(reportJson) + }).then(response => response.json()) + .then(console.log) + .catch((e: unknown) => console.error(e)); + } } } From 29e685520a76c45f62f43ec9619cd3b80e1d95bc Mon Sep 17 00:00:00 2001 From: Error430 Date: Sun, 27 Oct 2024 14:50:41 -0400 Subject: [PATCH 3/3] Fixed whitelisting --- server/src/server.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/server/src/server.ts b/server/src/server.ts index 5c82cc2d4..5fcd58923 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -26,11 +26,13 @@ const ipCheck = Config.protection?.ipChecker ? new IpChecker(Config.protection.ipChecker.baseUrl, Config.protection.ipChecker.key) : undefined; -const isVPN = new Map( - existsSync("isVPN.json") - ? Object.entries(JSON.parse(readFileSync("isVPN.json", "utf8")) as Record) - : undefined -); +const isVPN = Config.protection?.ipChecker + ? new Map() + : new Map( + existsSync("isVPN.json") + ? Object.entries(JSON.parse(readFileSync("isVPN.json", "utf8")) as Record) + : undefined + ); async function isVPNCheck(ip: string): Promise { if (!ipCheck) return false; @@ -382,7 +384,7 @@ if (isMainThread) { teamsCreated = {}; - if (protection.ipChecker) { + if (!Config.protection?.ipChecker) { writeFileSync("isVPN.json", JSON.stringify(Object.fromEntries(isVPN))); }