From 04b504fa18a96c59f20995d166855af4fd938687 Mon Sep 17 00:00:00 2001 From: Sainan <63328889+Sainan@users.noreply.github.com> Date: Sun, 9 Nov 2025 08:39:41 -0800 Subject: [PATCH] feat: add config options for bootstrapper tunables (#3010) Reviewed-on: https://onlyg.it/OpenWF/SpaceNinjaServer/pulls/3010 Co-authored-by: Sainan <63328889+Sainan@users.noreply.github.com> Co-committed-by: Sainan <63328889+Sainan@users.noreply.github.com> --- src/controllers/api/loginController.ts | 9 +++++ src/controllers/custom/tunablesController.ts | 11 ++---- src/services/configService.ts | 8 ++++ src/services/configWatcherService.ts | 16 +++++++- src/services/tunablesService.ts | 41 ++++++++++++++++++++ src/services/wsService.ts | 21 +++++----- 6 files changed, 89 insertions(+), 17 deletions(-) create mode 100644 src/services/tunablesService.ts diff --git a/src/controllers/api/loginController.ts b/src/controllers/api/loginController.ts index 8efcadaa..f5332fff 100644 --- a/src/controllers/api/loginController.ts +++ b/src/controllers/api/loginController.ts @@ -12,10 +12,19 @@ import { handleNonceInvalidation } from "../../services/wsService.ts"; import { getInventory } from "../../services/inventoryService.ts"; import { createMessage } from "../../services/inboxService.ts"; import { fromStoreItem } from "../../services/itemDataService.ts"; +import { getTokenForClient } from "../../services/tunablesService.ts"; +import type { AddressInfo } from "node:net"; export const loginController: RequestHandler = async (request, response) => { const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object + if (config.tunables?.useLoginToken) { + if (request.query.token !== getTokenForClient((request.socket.address() as AddressInfo).address)) { + response.status(400).json({ error: "missing or incorrect token" }); + return; + } + } + const account = await Account.findOne({ email: loginRequest.email }); const buildLabel: string = diff --git a/src/controllers/custom/tunablesController.ts b/src/controllers/custom/tunablesController.ts index 7405075a..0bcc0450 100644 --- a/src/controllers/custom/tunablesController.ts +++ b/src/controllers/custom/tunablesController.ts @@ -1,14 +1,11 @@ import type { RequestHandler } from "express"; import type { ITunables } from "../../types/bootstrapperTypes.ts"; +import { getTunablesForClient } from "../../services/tunablesService.ts"; +import type { AddressInfo } from "node:net"; // This endpoint is specific to the OpenWF Bootstrapper: https://openwf.io/bootstrapper-manual -export const tunablesController: RequestHandler = (_req, res) => { - const tunables: ITunables = {}; - //tunables.prohibit_skip_mission_start_timer = true; - //tunables.prohibit_fov_override = true; - //tunables.prohibit_freecam = true; - //tunables.prohibit_teleport = true; - //tunables.prohibit_scripts = true; +export const tunablesController: RequestHandler = (req, res) => { + const tunables: ITunables = getTunablesForClient((req.socket.address() as AddressInfo).address); res.json(tunables); }; diff --git a/src/services/configService.ts b/src/services/configService.ts index 112f0b01..81e238b9 100644 --- a/src/services/configService.ts +++ b/src/services/configService.ts @@ -75,6 +75,14 @@ export interface IConfig { circuitGameModes?: string[]; darvoStockMultiplier?: number; }; + tunables?: { + useLoginToken?: boolean; + prohibitSkipMissionStartTimer?: boolean; + prohibitFovOverride?: boolean; + prohibitFreecam?: boolean; + prohibitTeleport?: boolean; + prohibitScripts?: boolean; + }; dev?: { keepVendorsExpired?: boolean; }; diff --git a/src/services/configWatcherService.ts b/src/services/configWatcherService.ts index b33efd0a..f410c123 100644 --- a/src/services/configWatcherService.ts +++ b/src/services/configWatcherService.ts @@ -11,11 +11,14 @@ import { } from "./configService.ts"; import { saveConfig, shouldReloadConfig } from "./configWriterService.ts"; import { getWebBindings, startWebServer, stopWebServer } from "./webService.ts"; -import { sendWsBroadcast } from "./wsService.ts"; +import { forEachWsClient, sendWsBroadcast, type IWsMsgToClient } from "./wsService.ts"; import varzia from "../../static/fixed_responses/worldState/varzia.json" with { type: "json" }; +import { getTunablesForClient } from "./tunablesService.ts"; chokidar.watch(configPath).on("change", () => { if (shouldReloadConfig()) { + const prevTunables = JSON.stringify(config.tunables); + logger.info("Detected a change to config file, reloading its contents."); try { loadConfig(); @@ -26,6 +29,17 @@ chokidar.watch(configPath).on("change", () => { validateConfig(); syncConfigWithDatabase(); + if (JSON.stringify(config.tunables) != prevTunables) { + logger.debug(`tunables changed, informing clients`); + forEachWsClient(client => { + if (client.isGame) { + client.send( + JSON.stringify({ tunables: getTunablesForClient(client.address) } satisfies IWsMsgToClient) + ); + } + }); + } + const configBindings = configGetWebBindings(); const bindings = getWebBindings(); if ( diff --git a/src/services/tunablesService.ts b/src/services/tunablesService.ts new file mode 100644 index 00000000..05ffdc99 --- /dev/null +++ b/src/services/tunablesService.ts @@ -0,0 +1,41 @@ +import crypto from "node:crypto"; +import { args } from "../helpers/commandLineArguments.ts"; +import type { ITunables } from "../types/bootstrapperTypes.ts"; +import { config } from "./configService.ts"; + +let secret; +if (args.secret) { + secret = args.secret; // Maintain same secret across hot reloads in dev mode +} else { + secret = ""; + for (let i = 0; i != 10; ++i) { + secret += String.fromCharCode(Math.floor(Math.random() * 26) + 0x41); + } +} + +export const getTokenForClient = (clientAddress: string): string => { + return crypto.createHmac("sha256", secret).update(clientAddress).digest("hex"); +}; + +export const getTunablesForClient = (clientAddress: string): ITunables => { + const tunables: ITunables = {}; + if (config.tunables?.useLoginToken) { + tunables.token = getTokenForClient(clientAddress); + } + if (config.tunables?.prohibitSkipMissionStartTimer) { + tunables.prohibit_skip_mission_start_timer = true; + } + if (config.tunables?.prohibitFovOverride) { + tunables.prohibit_fov_override = true; + } + if (config.tunables?.prohibitFreecam) { + tunables.prohibit_freecam = true; + } + if (config.tunables?.prohibitTeleport) { + tunables.prohibit_teleport = true; + } + if (config.tunables?.prohibitScripts) { + tunables.prohibit_scripts = true; + } + return tunables; +}; diff --git a/src/services/wsService.ts b/src/services/wsService.ts index 9538deb9..1be8d550 100644 --- a/src/services/wsService.ts +++ b/src/services/wsService.ts @@ -9,6 +9,7 @@ import type { HydratedDocument } from "mongoose"; import { logError, logger } from "../utils/logger.ts"; import type { Request } from "express"; import type { ITunables } from "../types/bootstrapperTypes.ts"; +import type { AddressInfo } from "node:net"; let wsServer: WebSocketServer | undefined; let wssServer: WebSocketServer | undefined; @@ -48,6 +49,7 @@ let lastWsid: number = 0; interface IWsCustomData extends WebSocket { id: number; + address: string; accountId?: string; isGame?: boolean; } @@ -66,7 +68,7 @@ interface IWsMsgFromClient { sync_inventory?: boolean; } -interface IWsMsgToClient { +export interface IWsMsgToClient { // common wsid?: number; @@ -104,6 +106,7 @@ const wsOnConnect = (ws: WebSocket, req: http.IncomingMessage): void => { } (ws as IWsCustomData).id = ++lastWsid; + (ws as IWsCustomData).address = (req.socket.address() as AddressInfo).address; ws.send(JSON.stringify({ wsid: lastWsid } satisfies IWsMsgToClient)); // eslint-disable-next-line @typescript-eslint/no-misused-promises @@ -212,7 +215,7 @@ const wsOnConnect = (ws: WebSocket, req: http.IncomingMessage): void => { }); }; -const forEachClient = (cb: (client: IWsCustomData) => void): void => { +export const forEachWsClient = (cb: (client: IWsCustomData) => void): void => { if (wsServer) { for (const client of wsServer.clients) { cb(client as IWsCustomData); @@ -227,7 +230,7 @@ const forEachClient = (cb: (client: IWsCustomData) => void): void => { export const haveGameWs = (accountId: string): boolean => { let ret = false; - forEachClient(client => { + forEachWsClient(client => { if (client.isGame && client.accountId == accountId) { ret = true; } @@ -237,14 +240,14 @@ export const haveGameWs = (accountId: string): boolean => { export const sendWsBroadcast = (data: IWsMsgToClient): void => { const msg = JSON.stringify(data); - forEachClient(client => { + forEachWsClient(client => { client.send(msg); }); }; export const sendWsBroadcastTo = (accountId: string, data: IWsMsgToClient): void => { const msg = JSON.stringify(data); - forEachClient(client => { + forEachWsClient(client => { if (client.accountId == accountId) { client.send(msg); } @@ -253,7 +256,7 @@ export const sendWsBroadcastTo = (accountId: string, data: IWsMsgToClient): void export const sendWsBroadcastToGame = (accountId: string, data: IWsMsgToClient): void => { const msg = JSON.stringify(data); - forEachClient(client => { + forEachWsClient(client => { if (client.isGame && client.accountId == accountId) { client.send(msg); } @@ -262,7 +265,7 @@ export const sendWsBroadcastToGame = (accountId: string, data: IWsMsgToClient): export const sendWsBroadcastEx = (data: IWsMsgToClient, accountId?: string, excludeWsid?: number): void => { const msg = JSON.stringify(data); - forEachClient(client => { + forEachWsClient(client => { if ((!accountId || client.accountId == accountId) && client.id != excludeWsid) { client.send(msg); } @@ -271,7 +274,7 @@ export const sendWsBroadcastEx = (data: IWsMsgToClient, accountId?: string, excl export const sendWsBroadcastToWebui = (data: IWsMsgToClient, accountId?: string, excludeWsid?: number): void => { const msg = JSON.stringify(data); - forEachClient(client => { + forEachWsClient(client => { if (!client.isGame && (!accountId || client.accountId == accountId) && client.id != excludeWsid) { client.send(msg); } @@ -294,7 +297,7 @@ export const broadcastInventoryUpdate = (req: Request): void => { }; export const handleNonceInvalidation = (accountId: string): void => { - forEachClient(client => { + forEachWsClient(client => { if (client.accountId == accountId) { if (client.isGame) { client.accountId = undefined; // prevent processing of the close event