Compare commits

...

4 Commits

Author SHA1 Message Date
8d54b2c043 ignore drop after logout
All checks were successful
Build / build (pull_request) Successful in 1m1s
2025-09-01 05:20:14 +02:00
b23979855e logging 2025-09-01 05:20:14 +02:00
c835471f0f forcibly close game ws connections when nonce is invalidated 2025-09-01 05:20:14 +02:00
f82bd09ee4 feat: support websocket connections from game client 2025-09-01 03:52:36 +02:00
6 changed files with 76 additions and 8 deletions

View File

@ -8,7 +8,7 @@ import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } f
import type { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "../../types/loginTypes.ts"; import type { IDatabaseAccountJson, ILoginRequest, ILoginResponse } from "../../types/loginTypes.ts";
import { logger } from "../../utils/logger.ts"; import { logger } from "../../utils/logger.ts";
import { version_compare } from "../../helpers/inventoryHelpers.ts"; import { version_compare } from "../../helpers/inventoryHelpers.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts"; import { handleNonceInvalidation } from "../../services/wsService.ts";
export const loginController: RequestHandler = async (request, response) => { 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 const loginRequest = JSON.parse(String(request.body)) as ILoginRequest; // parse octet stream of json data to json object
@ -88,7 +88,7 @@ export const loginController: RequestHandler = async (request, response) => {
account.LastLogin = new Date(); account.LastLogin = new Date();
await account.save(); await account.save();
sendWsBroadcastTo(account._id.toString(), { nonce_updated: true }); handleNonceInvalidation(account._id.toString());
response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel)); response.json(createLoginResponse(myAddress, myUrlBase, account.toJSON(), buildLabel));
}; };

View File

@ -1,6 +1,6 @@
import type { RequestHandler } from "express"; import type { RequestHandler } from "express";
import { Account } from "../../models/loginModel.ts"; import { Account } from "../../models/loginModel.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts"; import { handleNonceInvalidation } from "../../services/wsService.ts";
export const logoutController: RequestHandler = async (req, res) => { export const logoutController: RequestHandler = async (req, res) => {
if (!req.query.accountId) { if (!req.query.accountId) {
@ -21,7 +21,7 @@ export const logoutController: RequestHandler = async (req, res) => {
} }
); );
if (stat.modifiedCount) { if (stat.modifiedCount) {
sendWsBroadcastTo(req.query.accountId as string, { nonce_updated: true }); handleNonceInvalidation(req.query.accountId as string);
} }
res.writeHead(200, { res.writeHead(200, {

View File

@ -17,7 +17,7 @@ import { InventorySlot } from "../../types/inventoryTypes/inventoryTypes.ts";
import { ExportDojoRecipes } from "warframe-public-export-plus"; import { ExportDojoRecipes } from "warframe-public-export-plus";
import type { IInventoryChanges } from "../../types/purchaseTypes.ts"; import type { IInventoryChanges } from "../../types/purchaseTypes.ts";
import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts"; import type { TInventoryDatabaseDocument } from "../../models/inventoryModels/inventoryModel.ts";
import { sendWsBroadcastEx } from "../../services/wsService.ts"; import { sendWsBroadcastEx, sendWsBroadcastTo } from "../../services/wsService.ts";
import { parseFusionTreasure } from "../../helpers/inventoryHelpers.ts"; import { parseFusionTreasure } from "../../helpers/inventoryHelpers.ts";
export const sellController: RequestHandler = async (req, res) => { export const sellController: RequestHandler = async (req, res) => {
@ -308,6 +308,9 @@ export const sellController: RequestHandler = async (req, res) => {
inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges" inventoryChanges: inventoryChanges // "inventoryChanges" for this response instead of the usual "InventoryChanges"
}); });
sendWsBroadcastEx({ update_inventory: true }, accountId, parseInt(String(req.query.wsid))); sendWsBroadcastEx({ update_inventory: true }, accountId, parseInt(String(req.query.wsid)));
if (req.query.wsid) {
sendWsBroadcastTo(accountId, { sync_inventory: true });
}
}; };
interface ISellRequest { interface ISellRequest {

View File

@ -1,6 +1,7 @@
import { getAccountIdForRequest } from "../../services/loginService.ts"; import { getAccountIdForRequest } from "../../services/loginService.ts";
import { getInventory, addItem } from "../../services/inventoryService.ts"; import { getInventory, addItem } from "../../services/inventoryService.ts";
import type { RequestHandler } from "express"; import type { RequestHandler } from "express";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
export const addItemsController: RequestHandler = async (req, res) => { export const addItemsController: RequestHandler = async (req, res) => {
const accountId = await getAccountIdForRequest(req); const accountId = await getAccountIdForRequest(req);
@ -9,6 +10,7 @@ export const addItemsController: RequestHandler = async (req, res) => {
for (const request of requests) { for (const request of requests) {
await addItem(inventory, request.ItemType, request.ItemCount, true, undefined, request.Fingerprint, true); await addItem(inventory, request.ItemType, request.ItemCount, true, undefined, request.Fingerprint, true);
} }
sendWsBroadcastTo(accountId, { sync_inventory: true });
await inventory.save(); await inventory.save();
res.end(); res.end();
}; };

View File

@ -1,5 +1,6 @@
import { applyClientEquipmentUpdates, getInventory } from "../../services/inventoryService.ts"; import { applyClientEquipmentUpdates, getInventory } from "../../services/inventoryService.ts";
import { getAccountIdForRequest } from "../../services/loginService.ts"; import { getAccountIdForRequest } from "../../services/loginService.ts";
import { sendWsBroadcastTo } from "../../services/wsService.ts";
import type { IOid } from "../../types/commonTypes.ts"; import type { IOid } from "../../types/commonTypes.ts";
import type { IEquipmentClient } from "../../types/equipmentTypes.ts"; import type { IEquipmentClient } from "../../types/equipmentTypes.ts";
import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts"; import type { TEquipmentKey } from "../../types/inventoryTypes/inventoryTypes.ts";
@ -23,6 +24,7 @@ export const addXpController: RequestHandler = async (req, res) => {
} }
applyClientEquipmentUpdates(inventory, gear, category as TEquipmentKey); applyClientEquipmentUpdates(inventory, gear, category as TEquipmentKey);
} }
sendWsBroadcastTo(accountId, { sync_inventory: true });
await inventory.save(); await inventory.save();
res.end(); res.end();
}; };

View File

@ -6,7 +6,7 @@ import { Account } from "../models/loginModel.ts";
import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "./loginService.ts"; import { createAccount, createNonce, getUsernameFromEmail, isCorrectPassword } from "./loginService.ts";
import type { IDatabaseAccountJson } from "../types/loginTypes.ts"; import type { IDatabaseAccountJson } from "../types/loginTypes.ts";
import type { HydratedDocument } from "mongoose"; import type { HydratedDocument } from "mongoose";
import { logError } from "../utils/logger.ts"; import { logError, logger } from "../utils/logger.ts";
let wsServer: WebSocketServer | undefined; let wsServer: WebSocketServer | undefined;
let wssServer: WebSocketServer | undefined; let wssServer: WebSocketServer | undefined;
@ -47,6 +47,7 @@ let lastWsid: number = 0;
interface IWsCustomData extends ws { interface IWsCustomData extends ws {
id: number; id: number;
accountId?: string; accountId?: string;
isGame?: boolean;
} }
interface IWsMsgFromClient { interface IWsMsgFromClient {
@ -55,11 +56,18 @@ interface IWsMsgFromClient {
password: string; password: string;
isRegister: boolean; isRegister: boolean;
}; };
auth_game?: {
accountId: string;
nonce: number;
};
logout?: boolean; logout?: boolean;
} }
interface IWsMsgToClient { interface IWsMsgToClient {
//wsid?: number; // common
wsid?: number;
// to webui
reload?: boolean; reload?: boolean;
ports?: { ports?: {
http: number | undefined; http: number | undefined;
@ -77,6 +85,9 @@ interface IWsMsgToClient {
nonce_updated?: boolean; nonce_updated?: boolean;
update_inventory?: boolean; update_inventory?: boolean;
logged_out?: boolean; logged_out?: boolean;
// to game
sync_inventory?: boolean;
} }
const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => { const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
@ -87,11 +98,12 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
} }
(ws as IWsCustomData).id = ++lastWsid; (ws as IWsCustomData).id = ++lastWsid;
ws.send(JSON.stringify({ wsid: lastWsid })); ws.send(JSON.stringify({ wsid: lastWsid } satisfies IWsMsgToClient));
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on("message", async msg => { ws.on("message", async msg => {
try { try {
//console.log(String(msg));
const data = JSON.parse(String(msg)) as IWsMsgFromClient; const data = JSON.parse(String(msg)) as IWsMsgFromClient;
if (data.auth) { if (data.auth) {
let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email }); let account: IDatabaseAccountJson | null = await Account.findOne({ email: data.auth.email });
@ -137,6 +149,19 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
); );
} }
} }
if (data.auth_game) {
(ws as IWsCustomData).isGame = true;
if (data.auth_game.nonce) {
const account: IDatabaseAccountJson | null = await Account.findOne({
_id: data.auth_game.accountId,
Nonce: data.auth_game.nonce
});
if (account) {
(ws as IWsCustomData).accountId = account.id;
logger.debug(`got bootstrapper connection for ${account.id}`);
}
}
}
if (data.logout) { if (data.logout) {
const accountId = (ws as IWsCustomData).accountId; const accountId = (ws as IWsCustomData).accountId;
(ws as IWsCustomData).accountId = undefined; (ws as IWsCustomData).accountId = undefined;
@ -154,6 +179,17 @@ const wsOnConnect = (ws: ws, req: http.IncomingMessage): void => {
logError(e as Error, `processing websocket message`); logError(e as Error, `processing websocket message`);
} }
}); });
// eslint-disable-next-line @typescript-eslint/no-misused-promises
ws.on("close", async () => {
if ((ws as IWsCustomData).isGame && (ws as IWsCustomData).accountId) {
logger.debug(`lost bootstrapper connection for ${(ws as IWsCustomData).accountId}`);
const account = await Account.findOne({ _id: (ws as IWsCustomData).accountId });
if (account?.Nonce) {
account.Dropped = true;
await account.save();
}
}
});
}; };
export const sendWsBroadcast = (data: IWsMsgToClient): void => { export const sendWsBroadcast = (data: IWsMsgToClient): void => {
@ -211,3 +247,28 @@ export const sendWsBroadcastEx = (data: IWsMsgToClient, accountId?: string, excl
} }
} }
}; };
export const handleNonceInvalidation = (accountId: string): void => {
if (wsServer) {
for (const client of wsServer.clients) {
if ((client as IWsCustomData).accountId == accountId) {
if ((client as IWsCustomData).isGame) {
client.close();
} else {
client.send(JSON.stringify({ nonce_updated: true } satisfies IWsMsgToClient));
}
}
}
}
if (wssServer) {
for (const client of wssServer.clients) {
if ((client as IWsCustomData).accountId == accountId) {
if ((client as IWsCustomData).isGame) {
client.close();
} else {
client.send(JSON.stringify({ nonce_updated: true } satisfies IWsMsgToClient));
}
}
}
}
};