From 3febb86b93ca3107aa8580b8b573670a98e5dd04 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 09:16:09 -0400 Subject: [PATCH 01/23] refactor: :art: cleaner packet routing in gateway --- docker-compose.yml | 4 +- packages/database/index.ts | 1 - .../database/src/functions/createNewCar.ts | 293 -------------- .../src/functions/getAbstractPartTypeId.ts | 53 --- .../src/functions/getWarehouseInventory.ts | 70 ---- .../src/functions/transferPartAssembly.ts | 153 -------- packages/database/src/models/Lobby.ts | 98 ----- packages/database/src/models/Player.ts | 46 --- packages/database/src/models/Session.ts | 10 - packages/database/src/seeders/index.ts | 206 ---------- packages/database/test/Player.test.ts | 8 - packages/gateway/src/GatewayServer.ts | 149 +++---- packages/gateway/src/index.ts | 364 +++++++++--------- packages/gateway/src/mcotsPortRouter.ts | 104 +++++ packages/gateway/src/npsPortRouter.ts | 126 ++++++ packages/gateway/src/portRouters.ts | 69 ++++ packages/gateway/src/socketUtility.ts | 44 +++ packages/gateway/src/types.ts | 19 + packages/gateway/test/portRouters.test.ts | 146 +++++++ packages/gateway/test/socketUtility.test.ts | 103 +++++ packages/gateway/tsconfig.json | 2 +- packages/nps/index.ts | 4 - packages/nps/services/account.ts | 58 --- packages/nps/test/account.test.ts | 59 --- packages/shared-packets/index.ts | 3 + .../shared-packets/src/GameMessageHeader.ts | 128 ++++++ .../shared-packets/src/GameMessagePayload.ts | 57 +++ packages/shared-packets/src/GamePacket.ts | 117 ++++++ .../shared-packets/src/ServerMessageHeader.ts | 13 + .../src/ServerMessagePayload.ts | 28 ++ .../shared-packets/src/ServerPacket.test.ts | 15 +- packages/shared-packets/src/ServerPacket.ts | 44 ++- packages/shared-packets/src/types.ts | 3 + .../{src => test}/BasePacket.test.ts | 4 +- .../{src => test}/BufferSerializer.test.ts | 2 +- .../{src => test}/GenericReplyPayload.test.ts | 2 +- .../GenericRequestPayload.test.ts | 2 +- .../{src => test}/ServerMessageHeader.test.ts | 2 +- packages/shared/index.ts | 4 +- packages/shared/src/BaseSerialized.ts | 4 +- packages/shared/src/NetworkMessage.ts | 5 +- packages/shared/src/OldServerMessage.ts | 12 +- packages/shared/src/RawMessage.ts | 5 +- packages/shared/src/SerializedBuffer.ts | 6 +- packages/shared/src/SerializedBufferOld.ts | 13 +- packages/shared/src/ServerMessage.ts | 5 +- .../transactions/src/_getPlayerPhysical.ts | 17 +- .../transactions/src/_getPlayerRaceHistory.ts | 8 +- packages/transactions/src/internal.ts | 270 ++++++++----- packages/transactions/src/login.ts | 5 +- server.ts | 89 +++-- src/chat/index.ts | 18 +- tsconfig.base.json | 14 +- 53 files changed, 1565 insertions(+), 1519 deletions(-) delete mode 100644 packages/database/src/functions/createNewCar.ts delete mode 100644 packages/database/src/functions/getAbstractPartTypeId.ts delete mode 100644 packages/database/src/functions/getWarehouseInventory.ts delete mode 100644 packages/database/src/functions/transferPartAssembly.ts delete mode 100644 packages/database/src/models/Lobby.ts delete mode 100644 packages/database/src/models/Player.ts delete mode 100644 packages/database/src/models/Session.ts delete mode 100644 packages/database/src/seeders/index.ts delete mode 100644 packages/database/test/Player.test.ts create mode 100644 packages/gateway/src/mcotsPortRouter.ts create mode 100644 packages/gateway/src/npsPortRouter.ts create mode 100644 packages/gateway/src/portRouters.ts create mode 100644 packages/gateway/src/socketUtility.ts create mode 100644 packages/gateway/src/types.ts create mode 100644 packages/gateway/test/portRouters.test.ts create mode 100644 packages/gateway/test/socketUtility.test.ts delete mode 100644 packages/nps/services/account.ts delete mode 100644 packages/nps/test/account.test.ts create mode 100644 packages/shared-packets/src/GameMessageHeader.ts create mode 100644 packages/shared-packets/src/GameMessagePayload.ts create mode 100644 packages/shared-packets/src/GamePacket.ts rename packages/shared-packets/{src => test}/BasePacket.test.ts (94%) rename packages/shared-packets/{src => test}/BufferSerializer.test.ts (96%) rename packages/shared-packets/{src => test}/GenericReplyPayload.test.ts (95%) rename packages/shared-packets/{src => test}/GenericRequestPayload.test.ts (95%) rename packages/shared-packets/{src => test}/ServerMessageHeader.test.ts (97%) diff --git a/docker-compose.yml b/docker-compose.yml index 6715d6366..f454b8cd7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,5 @@ +name: rustyserver + services: nginx: build: @@ -63,4 +65,4 @@ services: # - db volumes: - pgadmin: + pgadmin: diff --git a/packages/database/index.ts b/packages/database/index.ts index c6862e9c3..0b6b47d05 100644 --- a/packages/database/index.ts +++ b/packages/database/index.ts @@ -4,7 +4,6 @@ export { updateSessionKey, updateUser, } from "./src/DatabaseManager.js"; -export { getWarehouseInventory } from "./src/functions/getWarehouseInventory.js"; export type { WarehouseInventory } from "./src/functions/getWarehouseInventory.js"; export * from "./src/services/database.js"; import * as DatabaseSchema from "./src/__generated__/schema.json"; diff --git a/packages/database/src/functions/createNewCar.ts b/packages/database/src/functions/createNewCar.ts deleted file mode 100644 index 9903baa37..000000000 --- a/packages/database/src/functions/createNewCar.ts +++ /dev/null @@ -1,293 +0,0 @@ -import { eq } from "drizzle-orm"; -import { getDatabase } from "rusty-motors-database"; -import { - brandedPart as brandedPartSchema, - part as partSchema, - player as playerSchema, - stockAssembly as stockAssemblySchema, - stockVehicleAttributes as stockVehicleAttributesSchema, - tunables as tunablesSchema, - warehouse as warehouseSchema, -} from "rusty-motors-schema"; -import { getServerLogger } from "rusty-motors-shared"; -import { transferPartAssembly } from "./transferPartAssembly"; -/** - * Create a new car - * - * This function creates a new car for a player. - * - * The car is created using a branded part, a skin, and a trade-in part. - * The car is added to the player's lot and the trade-in part is removed from the player's lot. - * The player's account is debited for the cost of the car and the trade-in value is added to the player's account. - * The car is removed from the wholesaler's lot. - * The transaction is committed if all operations are successful. - * If any operation fails, the transaction is rolled back and an error is thrown. - * - * @param {number} lotOwnerId - * @param {number} brandedPartId - * @param {number} _skinId - * @param {number} playerId - * @param {number} tradeInId - * @returns {Promise} The new car's ID - * @throws {Error} If the lot owner ID is not found - * @throws {Error} If the branded part ID is not found - * @throws {Error} If the skin ID is not found - * @throws {Error} If the player ID is not found - * @throws {Error} If the trade-in ID is not found - * @throws {Error} If the car is out of stock - * @throws {Error} If the trade-in is not owned by the player - * @throws {Error} If the trade-in value cannot be determined - * @throws {Error} If the trade-in lot is not found - * @throws {Error} If the trade-in can not be scrapped - * @throws {Error} If the trade-in value can not be added to the player's account - * @throws {Error} If the trade-in cannot be removed from the player's lot - * @throws {Error} If the player does not have enough money to buy the car - * @throws {Error} If the car cannot be removed from the wholesaler's lot - * @throws {Error} If the car cannot be added to the player's lot - * @throws {Error} If the part cannot be created - * @throws {Error} If the vehicle cannot be created - * @throws {Error} If the player does not have enough room in their lot - * @throws {Error} If the sale cannot be recorded - */ -export async function createNewCar( - lotOwnerId: number, - brandedPartId: number, - _skinId: number, - playerId: number, - tradeInId: number, -): Promise { - const log = getServerLogger({ name: "createNewCar" }); - - log.debug(`Creating new car for player ${playerId}`); - let currentAbstractCarId = 0; - let currentPartId = 0; - let currentParentPartId = 0; - let currentBrandedPartId = 0; - let currentAttachmentPointId = 0; - let currentMaxItemWear = 0; - let currentRetailPrice = 0; - let scrapyardLotId = 0; - let tradeInValue = 0; - let cost = 0; - let rc = 0; - let dealOfTheDayBrandedPartId = 0; - let dealOfTheDayDiscount = 0; - let ownerID = 0; - let retailPrice = 0; - let maxItemWear = 0; - let tradeInPartCount = 0; - let carPartCount = 0; - let currectPartCounter = 0; - let carClass = 0; - - type record = { - partId: number; - parentPartId: number; - brandedPartId: number; - attachmentPointId: number; - abstractPartTypeId: number; - parentAbstractPartTypeId: number; - maxItemWear: number; - retailPrice: number; - }; - - const partRecords: record[] = []; - - const db = getDatabase(); - - const parts = await db - .select() - .from(stockAssemblySchema) - .where(eq(brandedPartSchema.brandedPartId, brandedPartId)); - - if (parts.length === 0) { - throw Error(`Branded part ${brandedPartId} not found`); - } - - await db.transaction(async (tx) => { - log.debug("Transaction started"); - - let tradeInValue = 0; - let scrapyardLotId = 0; - let dealOfTheDayBrandedPartId: number | null = null; - let dealOfTheDayDiscount = 0; - let ownerID = 0; - let retailPrice = 0; - let maxItemWear = 0; - let tradeInPartCount = 0; - let carPartCount = 0; - - dealOfTheDayBrandedPartId = await tx - .select({ - brandedPartId: warehouseSchema.brandedPartId, - }) - .from(warehouseSchema) - .where(eq(warehouseSchema.playerId, lotOwnerId)) - .limit(1) - .then((result) => { - return result[0]?.brandedPartId ?? null; - }); - - if (!dealOfTheDayBrandedPartId) { - log.debug("Deal of the day not found"); - } - - const lotExists: boolean = await tx - .select({ - playerID: warehouseSchema.playerId, - }) - .from(warehouseSchema) - .where(eq(warehouseSchema.playerId, lotOwnerId)) - .limit(1) - .then((result) => { - return !!result[0]; - }); - - if (!lotExists) { - tx.rollback(); - throw new Error(`Lot owner ${lotOwnerId} not found`); - } - - if (tradeInId) { - const validTradeIn = await tx - .select({ - ownerID: partSchema.ownerId, - }) - .from(partSchema) - .where(eq(partSchema.partId, tradeInId)) - .limit(1) - .then((result) => { - return result[0]?.ownerID === playerId; - }); - - if (!validTradeIn) { - tx.rollback(); - throw new Error( - `Trade-in ${tradeInId} not owned by player ${playerId}`, - ); - } - - tradeInValue = await tx - .select({ - scrapValue: partSchema.scrapValue, - }) - .from(partSchema) - .where(eq(partSchema.partId, tradeInId)) - .limit(1) - .then((result) => { - return result[0]?.scrapValue ?? 0; - }); - - if (!tradeInValue) { - tx.rollback(); - throw new Error(`Trade-in value not found for part ${tradeInId}`); - } - - const scrapyardLotFound = await tx - .select({ - playerID: warehouseSchema.playerId, - }) - .from(warehouseSchema) - .where(eq(warehouseSchema.playerId, scrapyardLotId)) - .then((result) => { - return !!result[0]; - }); - - if (!scrapyardLotFound) { - tx.rollback(); - throw new Error(`Scrapyard lot ${scrapyardLotId} not found`); - } - - try { - const resultOfScrap = await transferPartAssembly( - tradeInId, - scrapyardLotId, - ); - } catch (error) { - log.error(`Error scrapping trade-in ${tradeInId}: ${error}`); - tx.rollback(); - throw error; - } - - // Get the owner - - const newOwner = await tx - .select() - .from(playerSchema) - .where(eq(playerSchema.playerId, playerId)) - .limit(1) - .then((result) => { - return result[0]; - }); - - if (!newOwner) { - log.error(`Player ${playerId} not found`); - tx.rollback(); - throw new Error(`Player ${playerId} not found`); - } - - const oldBankBalance = newOwner.bankBalance; - - if (oldBankBalance === null) { - log.error(`Error getting bank balance for player ${playerId}`); - tx.rollback(); - throw new Error(`Error getting bank balance for player ${playerId}`); - } - - if (tradeInValue > 0) { - const newbankBalance = oldBankBalance + tradeInValue; - try { - await tx - .update(playerSchema) - .set({ - bankBalance: newbankBalance, - }) - .where(eq(playerSchema.playerId, playerId)); - } catch (error) { - log.error( - `Error adding trade-in value to player ${playerId}: ${error}`, - ); - tx.rollback(); - throw new Error( - `Error adding trade-in value to player ${playerId}: ${error}`, - ); - } - } - - // Old car trade-in complete - } - - const result = await tx - .select({ - carClass: stockVehicleAttributesSchema.carClass, - retailPrice: stockVehicleAttributesSchema.retailPrice, - }) - .from(stockVehicleAttributesSchema) - .where(eq(stockVehicleAttributesSchema.brandedPartId, brandedPartId)) - .limit(1); - - if (typeof result[0] === "undefined") { - tx.rollback(); - throw new Error(`Car ${brandedPartId} out of stock`); - } - - carClass = result[0].carClass; - retailPrice = result[0].retailPrice; - - if (dealOfTheDayBrandedPartId === brandedPartId) { - dealOfTheDayDiscount = await tx - .select({ - discount: tunablesSchema.dealOfTheDayDiscount, - }) - .from(tunablesSchema) - .limit(1) - .then((result) => { - return result[0]?.discount ?? 0; - }); - } - - log.debug("Transaction committed"); - }); - log.resetName(); - return Promise.resolve(0); -} diff --git a/packages/database/src/functions/getAbstractPartTypeId.ts b/packages/database/src/functions/getAbstractPartTypeId.ts deleted file mode 100644 index d6ca9567b..000000000 --- a/packages/database/src/functions/getAbstractPartTypeId.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { eq } from "drizzle-orm"; -import { getDatabase } from "rusty-motors-database"; -import { - brandedPart as brandedPartSchema, - part as partSchema, - partType as partTypeSchema, -} from "rusty-motors-schema"; -import { getServerLogger } from "rusty-motors-shared"; - -/** - * Get the abstract part type id from the partId - * - * @param partId The part id - * @returns The abstract part type id - * @throws {Error} If the part ID is not found - * @throws {Error} If the abstract part type ID is not found - */ -export async function getAbstractPartTypeId(partId: number): Promise { - const log = getServerLogger(); - log.setName("getAbstractPartTypeId"); - - log.debug(`Getting abstract part type ID for part ${partId}`); - - const db = getDatabase(); - - const abstractPartTypeId = await db - .select() - .from(partSchema) - .leftJoin( - brandedPartSchema, - eq(partSchema.brandedPartId, brandedPartSchema.brandedPartId), - ) - .leftJoin( - partTypeSchema, - eq(brandedPartSchema.partTypeId, partTypeSchema.partTypeId), - ) - .where(eq(partSchema.partId, partId)) - .limit(1) - .then((rows) => { - if (rows.length === 0) { - throw new Error(`Part ${partId} not found`); - } - - return rows[0]?.part_type?.abstractPartTypeId; - }); - - if (typeof abstractPartTypeId === "undefined") { - log.error(`Abstract part type ID not found for part ${partId}`); - throw new Error(`Abstract part type ID not found for part ${partId}`); - } - - return abstractPartTypeId; -} diff --git a/packages/database/src/functions/getWarehouseInventory.ts b/packages/database/src/functions/getWarehouseInventory.ts deleted file mode 100644 index e104e6f89..000000000 --- a/packages/database/src/functions/getWarehouseInventory.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { db, getTuneables, sql } from "../../index.js"; -import { getServerLogger } from "rusty-motors-shared"; - -export type WarehouseInventory = { - inventory: { - brandedPartId: number; - retailPrice: number | null; - isDealOfTheDay: number; - }[]; - dealOfTheDayDiscount: number; -}; - -export async function getWarehouseInventory( - warehouseId: number, - brandId: number, -): Promise { - const log = getServerLogger({ name: "getWarehouseInventory" }); - - log.debug( - `Getting warehouse inventory for part ${brandId} in warehouse ${warehouseId}`, - ); - - let inventoryCars: { - brandedPartId: number; - retailPrice: number | null; - isDealOfTheDay: number; - }[] = []; - - const tunables = getTuneables(); - - const dealOfTheDayDiscount = tunables.getDealOfTheDayDiscount(); - const dealOfTheDayBrandedPartId = tunables.getDealOfTheDayBrandedPartId(); - - if (dealOfTheDayDiscount < 1) { - log.warn("Deal of the day not found"); - } - - if (brandId > 0) { - inventoryCars = await db.query(sql` - SELECT - brandedPartId: warehouseSchema.brandedPartId, - retailPrice: stockVehicleAttributesSchema.retailPrice, - isDealOfTheDay: warehouseSchema.isDealOfTheDay, - FROM warehouse w - LEFT JOIN branded_part bp ON w.brandedPartId = bp.brandedPartId - LEFT JOIN model m ON bp.modelId = m.modelId - LEFT JOIN stock_vehicle_attributes sva ON w.brandedPartId = sva.brandedPartId - WHERE w.playerId = ${warehouseId} AND m.brandId = ${brandId} - `); - } else { - inventoryCars = await db.query(sql` - SELECT - brandedPartId: warehouseSchema.brandedPartId, - retailPrice: stockVehicleAttributesSchema.retailPrice, - isDealOfTheDay: warehouseSchema.isDealOfTheDay, - FROM warehouse w - LEFT JOIN branded_part bp ON w.brandedPartId = bp.brandedPartId - LEFT JOIN model m ON bp.modelId = m.modelId - LEFT JOIN stock_vehicle_attributes sva ON w.brandedPartId = sva.brandedPartId - WHERE w.playerId = ${warehouseId} - `); - } - - const inventory = { - inventory: inventoryCars, - dealOfTheDayDiscount: dealOfTheDayDiscount ?? 0, - }; - - return inventory; -} diff --git a/packages/database/src/functions/transferPartAssembly.ts b/packages/database/src/functions/transferPartAssembly.ts deleted file mode 100644 index 1c6252a95..000000000 --- a/packages/database/src/functions/transferPartAssembly.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { eq } from "drizzle-orm"; -import { getDatabase } from "rusty-motors-database"; -import { - part as partSchema, - player as playerSchema, - vehicle as vehicleSchema, -} from "rusty-motors-schema"; -import { getServerLogger } from "rusty-motors-shared"; -import { getAbstractPartTypeId } from "./getAbstractPartTypeId"; - -const ABSTRACT_PART_TYPE_ID_CAR = 101; - -/** - * Transfer a part assembly - * - * This function transfers a part assembly from one owner to another, including all child parts. - * - * This function does NOT use a transaction. - * - * @param {number} partId - * @param {number} newOwnerId - * - * @returns {Promise} - * @throws {Error} If the part ID is not found - * @throws {Error} If the new owner ID is not found - * @throws {Error} If the part cannot be transferred - */ -export async function transferPartAssembly( - partId: number, - newOwnerId: number, -): Promise { - const log = getServerLogger(); - const db = getDatabase(); - log.setName("transferPartAssembly"); - - log.debug(`Transferring part assembly ${partId} to new owner ${newOwnerId}`); - - const topPart = await db - .select() - .from(partSchema) - .where(eq(partSchema.partId, partId)) - .limit(1) - .then((rows) => rows[0]); - - if (typeof topPart === "undefined") { - log.error(`Part ${partId} not found`); - throw new Error(`Part ${partId} not found`); - } - - if (topPart.ownerId === null) { - log.error(`Part ${partId} has no owner`); - throw new Error(`Part ${partId} has no owner`); - } - - if (topPart.ownerId === newOwnerId) { - log.error(`Part ${partId} is already owned by ${newOwnerId}`); - throw new Error(`Part ${partId} is already owned by ${newOwnerId}`); - } - - const newOwnerExists = await db - .select() - .from(playerSchema) - .where(eq(playerSchema.playerId, newOwnerId)) - .limit(1) - .then((rows) => rows[0] !== undefined); - - if (!newOwnerExists) { - log.error(`Owner ${newOwnerId} not found`); - throw new Error(`Owner ${newOwnerId} not found`); - } - - const children = await db - .select() - .from(partSchema) - .where(eq(partSchema.parentPartId, partId)) - .then((rows) => rows); - - if (children.length === 0) { - log.error(`Part ${partId} has no children`); - throw new Error(`Part ${partId} has no children`); - } - - try { - // If the part is a car, update the owner ID in the vehicle table - const isPartACar = await getAbstractPartTypeId(topPart.brandedPartId).then( - (abstractPartTypeId) => abstractPartTypeId === ABSTRACT_PART_TYPE_ID_CAR, - ); - - if (isPartACar) { - const car = await db - .select() - .from(vehicleSchema) - .where(eq(vehicleSchema.vehicleId, partId)) - .limit(1) - .then((rows) => rows[0]); - - if (typeof car === "undefined") { - log.error(`Vehicle ${partId} not found`); - throw Error(`Vehicle ${partId} not found`); - } - - // Remove the vehicle from the old owner's lot - const oldOwner = await db - .select() - .from(playerSchema) - .where(eq(playerSchema.playerId, topPart.ownerId)) - .limit(1) - .then((rows) => rows[0]); - - if (typeof oldOwner === "undefined") { - log.error(`Owner ${topPart.ownerId} not found`); - throw Error(`Owner ${topPart.ownerId} not found`); - } - - if (oldOwner.numCarsOwned > 0) { - oldOwner.numCarsOwned--; - try { - await db - .update(playerSchema) - .set(oldOwner) - .where(eq(playerSchema.playerId, topPart.ownerId)); - } catch (error) { - log.error( - `Error updating old owner ${topPart.ownerId}: ${String(error)}`, - ); - throw new Error( - `Error updating old owner ${topPart.ownerId}: ${String(error)}`, - ); - } - } else { - log.error(`Owner ${topPart.ownerId} has no cars`); - throw Error(`Owner ${topPart.ownerId} has no cars`); - } - } - - // Transfer the children - - for (const child of children) { - await transferPartAssembly(child.partId, newOwnerId); - } - - // Update the parent part's owner ID - await db - .update(partSchema) - .set({ ownerId: newOwnerId }) - .where(eq(partSchema.partId, partId)); - } catch (error) { - log.error(`Error transferring part ${partId}: ${String(error)}`); - throw new Error(`Error transferring part ${partId}: ${String(error)}`); - } - - log.debug(`Part assembly ${partId} transferred to new owner ${newOwnerId}`); -} diff --git a/packages/database/src/models/Lobby.ts b/packages/database/src/models/Lobby.ts deleted file mode 100644 index 5a54095dd..000000000 --- a/packages/database/src/models/Lobby.ts +++ /dev/null @@ -1,98 +0,0 @@ -import { ServerError } from "rusty-motors-shared"; -import { SerializedBuffer } from "rusty-motors-shared"; - -export class LobbyModel extends SerializedBuffer { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - override deserialize(_inputBuffer: Buffer): LobbyModel { - throw new ServerError("Method not implemented."); - } - override serialize(): Buffer { - throw new ServerError("Method not implemented."); - } - serializeSize(): number { - throw new ServerError("Method not implemented."); - } - static schema = `CREATE TABLE IF NOT EXISTS "lobbies" - ( - "lobyID" integer NOT NULL, - "raceTypeID" integer NOT NULL, - "turfID" integer NOT NULL, - "riffName" character(32) NOT NULL, - "eTerfName" character(265) NOT NULL, - "clientArt" character(11) NOT NULL, - "elementID" integer NOT NULL, - "terfLength" integer NOT NULL, - "startSlice" integer NOT NULL, - "endSlice" integer NOT NULL, - "dragStageLeft" integer NOT NULL, - "dragStageRight" integer NOT NULL, - "dragStagingSlice" integer NOT NULL, - "gridSpreadFactor" real NOT NULL, - "linear" smallint NOT NULL, - "numPlayersMin" smallint NOT NULL, - "numPlayersMax" smallint NOT NULL, - "numPlayersDefault" smallint NOT NULL, - "bnumPlayersEnable" smallint NOT NULL, - "numLapsMin" smallint NOT NULL, - "numLapsMax" smallint NOT NULL, - "numLapsDefault" smallint NOT NULL, - "bnumLapsEnabled" smallint NOT NULL, - "numRoundsMin" smallint NOT NULL, - "numRoundsMax" smallint NOT NULL, - "numRoundsDefault" smallint NOT NULL, - "bnumRoundsEnabled" smallint NOT NULL, - "bWeatherDefault" smallint NOT NULL, - "bWeatherEnabled" smallint NOT NULL, - "bNightDefault" smallint NOT NULL, - "bNightEnabled" smallint NOT NULL, - "bBackwardDefault" smallint NOT NULL, - "bBackwardEnabled" smallint NOT NULL, - "bTrafficDefault" smallint NOT NULL, - "bTrafficEnabled" smallint NOT NULL, - "bDamageDefault" smallint NOT NULL, - "bDamageEnabled" smallint NOT NULL, - "bAIDefault" smallint NOT NULL, - "bAIEnabled" smallint NOT NULL, - "topDog" character(13) NOT NULL, - "terfOwner" character(33) NOT NULL, - "qualifingTime" integer NOT NULL, - "clubNumPlayers" integer NOT NULL, - "clubNumLaps" integer NOT NULL, - "clubNumRounds" integer NOT NULL, - "bClubNight" smallint NOT NULL, - "bClubWeather" smallint NOT NULL, - "bClubBackwards" smallint NOT NULL, - "topSeedsMP" integer NOT NULL, - "lobbyDifficulty" integer NOT NULL, - "ttPointForQualify" integer NOT NULL, - "ttCashForQualify" integer NOT NULL, - "ttPointBonusFasterIncs" integer NOT NULL, - "ttCashBonusFasterIncs" integer NOT NULL, - "ttTimeIncrements" integer NOT NULL, - "victoryPoints1" integer NOT NULL, - "victoryCash1" integer NOT NULL, - "victoryPoints2" integer NOT NULL, - "victoryCash2" integer NOT NULL, - "victoryPoints3" integer NOT NULL, - "victoryCash3" integer NOT NULL, - "minLevel" smallint NOT NULL, - "minResetSlice" integer NOT NULL, - "maxResetSlice" integer NOT NULL, - "bnewbieFlag" smallint NOT NULL, - "bdriverHelmetFlag" smallint NOT NULL, - "clubNumPlayersMax" smallint NOT NULL, - "clubNumPlayersMin" smallint NOT NULL, - "clubNumPlayersDefault" smallint NOT NULL, - "numClubsMax" smallint NOT NULL, - "numClubsMin" smallint NOT NULL, - "racePointsFactor" real NOT NULL, - "bodyClassMax" smallint NOT NULL, - "powerClassMax" smallint NOT NULL, - "clubLogoID" integer NOT NULL, - "teamtWeather" smallint NOT NULL, - "teamtNight" smallint NOT NULL, - "teamtBackwards" smallint NOT NULL, - "teamtNumLaps" smallint NOT NULL, - "raceCashFactor" real NOT NULL - );`; -} diff --git a/packages/database/src/models/Player.ts b/packages/database/src/models/Player.ts deleted file mode 100644 index d2b2cc455..000000000 --- a/packages/database/src/models/Player.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { ServerError } from "rusty-motors-shared"; -import { SerializedBuffer } from "rusty-motors-shared"; - -export class PlayerModel extends SerializedBuffer { - override serialize(): Buffer { - throw new ServerError("Method not implemented."); - } - serializeSize(): number { - throw new ServerError("Method not implemented."); - } - - static schema = `CREATE TABLE Player ( - PlayerID int NOT NULL, - CustomerID int NOT NULL, - PlayerTypeID int NOT NULL, - StockClassicClass char NOT NULL, - StockMuscleClass char NOT NULL, - ModifiedClassicClass char NOT NULL, - ModifiedMuscleClass char NOT NULL, - OutlawClass char NOT NULL, - DragClass char NOT NULL, - ChallengeScore int NOT NULL, - ChallengeRung int NOT NULL, - LastLoggedIn datetime NOT NULL, - TotalTimePlayed datetime NOT NULL, - TimesLoggedIn smallint NOT NULL, - NumUnreadMail smallint NOT NULL, - BankBalance int NOT NULL, - NumCarsOwned smallint NOT NULL, - IsLoggedIn tinyint NOT NULL, - DriverStyle tinyint NOT NULL, - LPCode smallint NOT NULL, - CarInfoSetting int NOT NULL, - CarNum1 varchar(2) NOT NULL, - CarNum2 varchar(2) NOT NULL, - CarNum3 varchar(2) NOT NULL, - CarNum4 varchar(2) NOT NULL, - CarNum5 varchar(2) NOT NULL, - CarNum6 varchar(2) NOT NULL, - LPText varchar(8) NOT NULL, - DLNumber varchar(20) NOT NULL, - Persona varchar(30) NOT NULL, - Address varchar(128) NOT NULL, - Residence varchar(20) NOT NULL -);`; -} diff --git a/packages/database/src/models/Session.ts b/packages/database/src/models/Session.ts deleted file mode 100644 index 617f43bcb..000000000 --- a/packages/database/src/models/Session.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @global - * @typedef {object} Session - * @property {number} customer_id - * @property {string} sessionkey - * @property {string} skey - * @property {string} context_id - * @property {string} connection_id - */ -export default {}; diff --git a/packages/database/src/seeders/index.ts b/packages/database/src/seeders/index.ts deleted file mode 100644 index c37a7309a..000000000 --- a/packages/database/src/seeders/index.ts +++ /dev/null @@ -1,206 +0,0 @@ -const playerCsvHeader = [ - "player_id", - "customer_id", - "player_type_id", - "sanctioned_score", - "challenge_score", - "last_logged_in", - "times_logged_in", - "bank_balance", - "num_cars_owned", - "is_logged_in", - "driver_style", - "lp_code", - "lp_text", - "car_num_1", - "car_num_2", - "car_num_3", - "car_num_4", - "car_num_5", - "car_num_6", - "dl_number", - "persona", - "address", - "residence", - "vehicle_id", - "current_race_id", - "offline_driver_skill", - "offline_grudge", - "offline_reputation", - "total_time_played", - "car_info_setting", - "stock_classic_class", - "stock_muscle_class", - "modified_classic_class", - "modified_muscle_class", - "outlaw_class", - "drag_class", - "challenge_rung", - "offline_ai_car_class", - "offline_ai_skin_id", - "offline_ai_car_bpt_id", - "offline_ai_state", - "bodytype", - "skin_color", - "hair_color", - "shirt_color", - "pants_color", - "offline_driver_style", - "offline_driver_attitude", - "evaded_fuzz", - "pinks_won", - "num_unread_mail", - "total_races_run", - "total_races_won", - "total_races_completed", - "total_winnings", - "insurance_risk_points", - "insurance_rating", - "challenge_races_run", - "challenge_races_won", - "challenge_races_completed", - "cars_lost", - "cars_won", -]; - -const playerCsvRow = [ - [ - 6, - 0, - 1, - 0, - 0, - new Date(0), - 0, - 999999, - 0, - 0, - 0, - 0, - null, - 0, - 0, - 0, - 0, - 0, - 0, - "xxx", - "All Factory Ford", - "1240 A Street", - "ABCDEFGHIJKLMNOPQRST", - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - null, - ], -]; - -export function csvRowToPlayer( - row: (string | number | Date | null)[], -): typeof playerSchema.$inferSelect { - return { - playerId: row[0] as number, - customerId: row[1] as number, - playerTypeId: row[2] as number, - sanctionedScore: row[3] as number, - challengeScore: row[4] as number, - lastLoggedIn: row[5] as Date, - timesLoggedIn: row[6] as number, - bankBalance: row[7] as number, - numCarsOwned: row[8] as number, - isLoggedIn: row[9] as number, - driverStyle: row[10] as number, - lpCode: row[11] as number, - lpText: row[12] as string | null, - carNum1: row[13] as string, - carNum2: row[14] as string, - carNum3: row[15] as string, - carNum4: row[16] as string, - carNum5: row[17] as string, - carNum6: row[18] as string, - dlNumber: row[19] as string, - persona: row[20] as string, - address: row[21] as string, - residence: row[22] as string, - vehicleId: row[23] as number | null, - currentRaceId: row[24] as number | null, - offlineDriverSkill: row[25] as number | null, - offlineGrudge: row[26] as number | null, - offlineReputation: row[27] as number | null, - totalTimePlayed: row[28] as number | null, - carInfoSetting: row[29] as number | null, - stockClassicClass: row[30] as number | null, - stockMuscleClass: row[31] as number | null, - modifiedClassicClass: row[32] as number | null, - modifiedMuscleClass: row[33] as number | null, - outlawClass: row[34] as number | null, - dragClass: row[35] as number | null, - challengeRung: row[36] as number | null, - offlineAiCarClass: row[37] as number | null, - offlineAiSkinId: row[38] as number | null, - offlineAiCarBptId: row[39] as number | null, - offlineAiState: row[40] as number | null, - bodytype: row[41] as number | null, - skinColor: row[42] as number | null, - hairColor: row[43] as number | null, - shirtColor: row[44] as number | null, - pantsColor: row[45] as number | null, - offlineDriverStyle: row[46] as number | null, - offlineDriverAttitude: row[47] as number | null, - evadedFuzz: row[48] as number | null, - pinksWon: row[49] as number | null, - numUnreadMail: row[50] as number | null, - totalRacesRun: row[51] as number | null, - totalRacesWon: row[52] as number | null, - totalRacesCompleted: row[53] as number | null, - totalWinnings: row[54] as number | null, - insuranceRiskPoints: row[55] as number | null, - insuranceRating: row[56] as number | null, - challengeRacesRun: row[57] as number | null, - challengeRacesWon: row[58] as number | null, - challengeRacesCompleted: row[59] as number | null, - carsLost: row[60] as number | null, - carsWon: row[61] as number | null, - }; -} - -for (const row of playerCsvRow) { - if (row.length !== playerCsvHeader.length) { - throw new Error( - `Row length does not match header length: got ${row.length}, expected ${playerCsvHeader.length}`, - ); - } - - const player = [ - csvRowToPlayer( - row.map((value, index) => { - if (typeof value === "undefined") { - throw new Error(`Undefined value at index ${playerCsvHeader[index]}`); - } - - return value; - }), - ), - ]; -} diff --git a/packages/database/test/Player.test.ts b/packages/database/test/Player.test.ts deleted file mode 100644 index 45c333938..000000000 --- a/packages/database/test/Player.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { PlayerModel } from "../src/models/Player.js"; - -describe("Player model", function () { - it("should have a schema property", function () { - expect(PlayerModel.schema).not.equal(""); - }); -}); diff --git a/packages/gateway/src/GatewayServer.ts b/packages/gateway/src/GatewayServer.ts index ae856d22c..1a518ce75 100644 --- a/packages/gateway/src/GatewayServer.ts +++ b/packages/gateway/src/GatewayServer.ts @@ -1,6 +1,6 @@ import { Socket, createServer as createSocketServer } from "node:net"; import FastifySensible from "@fastify/sensible"; -import fastify, { type FastifyInstance } from "fastify"; +import fastify from "fastify"; import { ConsoleThread } from "rusty-motors-cli"; import { receiveLobbyData } from "rusty-motors-lobby"; import { receiveLoginData } from "rusty-motors-login"; @@ -9,6 +9,7 @@ import { Configuration, getServerConfiguration, type ServerLogger, + type State, } from "rusty-motors-shared"; import { addOnDataHandler, @@ -26,23 +27,10 @@ import { gameMessageProcessors, } from "rusty-motors-nps"; import { receiveChatData } from "rusty-motors-chat"; - -/** - * Options for the GatewayServer. - */ -type GatewayOptions = { - config?: Configuration; - log?: ServerLogger; - backlogAllowedCount?: number; - listeningPortList?: number[]; - socketConnectionHandler?: ({ - incomingSocket, - log, - }: { - incomingSocket: Socket; - log?: ServerLogger; - }) => void; -}; +import type { GatewayOptions } from "./types.js"; +import { addPortRouter } from "./portRouters.js"; +import { npsPortRouter } from "./npsPortRouter.js"; +import { mcotsPortRouter } from "./mcotsPortRouter.js"; /** * Gateway server @@ -100,24 +88,12 @@ export class Gateway { Gateway._instance = this; } - /** - * @return {FastifyInstance} - */ - getWebServer(): FastifyInstance { - if (this.webServer === undefined) { - throw Error("webServer is undefined"); - } - return this.webServer; - } - start() { this.log.debug("Starting GatewayServer in start()"); this.log.info("Server starting"); // Check if there are any listening ports specified - if (this.listeningPortList.length === 0) { - throw Error("No listening ports specified"); - } + this.ensureListeningPortsSpecified(); // Mark the GatewayServer as running this.log.debug("Marking GatewayServer as running"); @@ -127,23 +103,13 @@ export class Gateway { this.init(); this.listeningPortList.forEach(async (port) => { - const server = createSocketServer((s) => { - this.socketconnection({ - incomingSocket: s, - log: this.log, - }); - }); - - // Listen on the specified port - - server.listen(port, "0.0.0.0", this.backlogAllowedCount, () => { - this.log.debug(`Listening on port ${port}`); - }); - - // Add the server to the list of servers - this.activeServers.push(server); + this.startNewServer(port); }); + this.startWebServer(); + } + + private startWebServer() { if (this.webServer === undefined) { throw Error("webServer is undefined"); } @@ -166,6 +132,29 @@ export class Gateway { ); } + private ensureListeningPortsSpecified() { + if (this.listeningPortList.length === 0) { + throw Error("No listening ports specified"); + } + } + + private startNewServer(port: number) { + const server = createSocketServer((s) => { + this.socketconnection({ + incomingSocket: s, + log: this.log, + }); + }); + + // Listen on the specified port + server.listen(port, "0.0.0.0", this.backlogAllowedCount, () => { + this.log.debug(`Listening on port ${port}`); + }); + + // Add the server to the list of servers + this.activeServers.push(server); + } + async restart() { // Stop the GatewayServer await this.stop(); @@ -190,19 +179,7 @@ export class Gateway { this.status = "stopping"; // Stop the servers - this.activeServers.forEach((server) => { - server.close(); - }); - - // Stop the read thread - if (this.readThread !== undefined) { - this.readThread.stop(); - } - - if (this.webServer === undefined) { - throw Error("webServer is undefined"); - } - await this.webServer.close(); + await this.shutdownServers(); // Stop the timer if (this.timer !== null) { @@ -218,6 +195,22 @@ export class Gateway { createInitialState({}).save(); } + private async shutdownServers() { + this.activeServers.forEach((server) => { + server.close(); + }); + + // Stop the read thread + if (this.readThread !== undefined) { + this.readThread.stop(); + } + + if (this.webServer === undefined) { + throw Error("webServer is undefined"); + } + await this.webServer.close(); + } + /** * @param {string} event */ @@ -253,19 +246,11 @@ export class Gateway { this.webServer = fastify({}); this.webServer.register(FastifySensible); - let state = fetchStateFromDatabase(); - - state = addOnDataHandler(state, 8226, receiveLoginData); - state = addOnDataHandler(state, 8227, receiveChatData); - state = addOnDataHandler(state, 8228, receivePersonaData); - state = addOnDataHandler(state, 7003, receiveLobbyData); - state = addOnDataHandler(state, 9000, receiveChatData); - state = addOnDataHandler(state, 43300, receiveTransactionsData); - - state.save(); - - populatePortToMessageTypes(portToMessageTypes); - populateGameMessageProcessors(gameMessageProcessors); + addPortRouter(8226, npsPortRouter); + addPortRouter(8227, npsPortRouter); + addPortRouter(8228, npsPortRouter); + addPortRouter(7003, npsPortRouter); + addPortRouter(43300, mcotsPortRouter); this.log.debug("GatewayServer initialized"); } @@ -320,6 +305,26 @@ export class Gateway { /** @type {Gateway | undefined} */ Gateway._instance = undefined; +/** + * Registers various data handlers to the provided state. + * + * This function adds handlers for different types of data, such as login data, + * chat data, persona data, lobby data, and transaction data. Each handler is + * associated with a specific code. + * + * @param state - The initial state to which the data handlers will be added. + * @returns The updated state with all the data handlers registered. + */ +function registerDataHandlers(state: State) { + state = addOnDataHandler(state, 8226, receiveLoginData); + state = addOnDataHandler(state, 8227, receiveChatData); + state = addOnDataHandler(state, 8228, receivePersonaData); + state = addOnDataHandler(state, 7003, receiveLobbyData); + state = addOnDataHandler(state, 9000, receiveChatData); + state = addOnDataHandler(state, 43300, receiveTransactionsData); + return state; +} + /** * Get a singleton instance of GatewayServer * diff --git a/packages/gateway/src/index.ts b/packages/gateway/src/index.ts index 0935c0ca9..1e625445e 100644 --- a/packages/gateway/src/index.ts +++ b/packages/gateway/src/index.ts @@ -14,44 +14,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { - type OnDataHandler, - type ServerLogger, - type ServiceResponse, - fetchStateFromDatabase, - getOnDataHandler, -} from "rusty-motors-shared"; +import { type ServerLogger } from "rusty-motors-shared"; import { getServerLogger } from "rusty-motors-shared"; -import { newSocket, type ConnectedSocket } from "rusty-motors-socket"; import { Socket } from "node:net"; -import { getGatewayServer } from "./GatewayServer.js"; -import { getPortMessageType, UserStatusManager } from "rusty-motors-nps"; -import { BasePacket } from "rusty-motors-shared-packets"; -import * as Sentry from "@sentry/node"; -import { socketErrorHandler } from "./socketErrorHandler.js"; - -/** - * Handle the end of a socket connection - * - * @param {object} options - * @param {string} options.connectionId The connection ID - * @param {import("pino").Logger} [options.log=getServerLogger({ name: "socketEndHandler" })] The logger to use - */ -// export function socketEndHandler({ -// connectionId, -// log = getServerLogger({ -// name: "socketEndHandler", -// }), -// }: { -// connectionId: string; -// log?: ServerLogger; -// }) { -// log.debug(`Connection ${connectionId} ended`); - -// // Remove the socket from the global state -// removeSocket(fetchStateFromDatabase(), connectionId).save(); -// } +import { randomUUID } from "node:crypto"; +import { tagSocketWithId } from "./socketUtility.js"; +import { getPortRouter } from "./portRouters.js"; /** * Handle incoming TCP connections @@ -64,7 +33,7 @@ import { socketErrorHandler } from "./socketErrorHandler.js"; export function onSocketConnection({ incomingSocket, log = getServerLogger({ - name: "onDataHandler", + name: "gatewayServer.onSocketConnection", }), }: { incomingSocket: Socket; @@ -73,160 +42,185 @@ export function onSocketConnection({ // Get the local port and remote address const { localPort, remoteAddress } = incomingSocket; + // If the local port or remote address is undefined, throw an error if (localPort === undefined || remoteAddress === undefined) { throw Error("localPort or remoteAddress is undefined"); } - const socket = newSocket(incomingSocket); - - // ======================= - // Handle incoming socket in shadow mode - // ======================= - - try { - // Get expected message type - const messageType = getPortMessageType(localPort); - log.debug(`[${socket.id}] Expected message type: ${messageType}`); - - switch (messageType) { - case "Game": { - // Handle game messages - // Create a new user status - const userStatus = UserStatusManager.newUserStatus(); - log.debug(`[${socket.id}] Created new user status`); - - UserStatusManager.addUserStatus(userStatus); - log.debug(`[${socket.id}] Added user status to manager`); - - break; - } - - case "Server": { - // Handle server messages - break; - } - - default: { - log.warn(`[${socket.id}] No message type found`); - break; - } - } - } catch (error) { - log.error(`[${socket.id}] Error handling socket: ${error}`); - } - - - - socket.on("error", (error) => - socketErrorHandler({ connectionId: socket.id, error }), + const socketWithId = tagSocketWithId( + incomingSocket, + Date.now(), + randomUUID(), ); - // Add the data handler to the socket - socket.on("inData", (data) => { - socketDataHandler(socket, data, log); - }); - - log.debug( - `[${socket.id}] Socket connection established on port ${localPort} from ${remoteAddress}`, - ); - - if (localPort === 7003) { - // Sent ok to login packet - socket.write(Buffer.from([0x02, 0x30, 0x00, 0x00])); - } + /* + * At this point, we have a tagged socket with an ID. + */ + + const portRouter = getPortRouter(localPort); + + // Hand the socket to the port router + portRouter({ taggedSocket: socketWithId }) + .then(() => { + log.debug(`[${socketWithId.id}] Port router finished`); + }) + .catch((error) => { + log.error(`[${socketWithId.id}] Error in port router: ${error}`); + }); + + // // Should end here + + // try { + // // Get expected message type + // const messageType = getPortMessageType(localPort); + // log.debug(`[${socketWithId.id}] Expected message type: ${messageType}`); + + // switch (messageType) { + // case "Game": { + // // Handle game messages + // // Create a new user status + // const userStatus = UserStatusManager.newUserStatus(); + // log.debug(`[${socketWithId.id}] Created new user status`); + + // UserStatusManager.addUserStatus(userStatus); + // log.debug(`[${socketWithId.id}] Added user status to manager`); + + // break; + // } + + // case "Server": { + // // Handle server messages + // break; + // } + + // default: { + // log.warn(`[${socketWithId.id}] No message type found`); + // break; + // } + // } + // } catch (error) { + // log.error(`[${socketWithId.id}] Error handling socket: ${error}`); + // } + + // socketWithId.socket.on("error", (error) => + // socketErrorHandler({ connectionId: socketWithId.id, error }), + // ); + + // // Add the data handler to the socket + // socketWithId.socket.on("data", (data) => { + // socketDataHandler(socketWithId, data, log); + // }); + + // log.debug( + // `[${socketWithId.id}] Socket connection established on port ${localPort} from ${remoteAddress}`, + // ); + + // if (localPort === 7003) { + // // Sent ok to login packet + // log.debug(`[${socketWithId.id}] Sending ok to login packet`); + // socketWithId.socket.write(Buffer.from([0x02, 0x30, 0x00, 0x00])); + // } } -function socketDataHandler( - socket: ConnectedSocket, - incomingDataAsBuffer: Buffer, - log: ServerLogger, -) { - log.trace( - `[${socket.id}] Received data: ${incomingDataAsBuffer.toString("hex")}`, - ); - - // This is a new TCP socket, so it's probably not using HTTP - // Let's look for a port onData handler - /** @type {OnDataHandler | undefined} */ - const portOnDataHandler: OnDataHandler | undefined = getOnDataHandler( - fetchStateFromDatabase(), - socket.port, - ); - - // If there is no onData handler, log a warning and return - if (!portOnDataHandler) { - log.warn(`[${socket.id}] No onData handler found for port ${socket.port}`); - log.warn(`[${socket.id}] Received data: ${socket.peek().toString("hex")}`); - return; - } - - // Deserialize the raw message - const rawMessage = new BasePacket({ - connectionId: socket.id, - messageId: 0, - messageSequence: 0, - messageSource: "", - }); - rawMessage.deserialize(incomingDataAsBuffer); - - // Log the raw message - log.trace(`[${socket.id}] Raw message: ${rawMessage.toHexString()}`); - - log.debug(`[${socket.id}] Handling data with ${portOnDataHandler.name}`); - - Sentry.startSpan( - { - name: "onDataHandler", - op: "onDataHandler", - }, - async () => { - portOnDataHandler({ - connectionId: socket.id, - message: rawMessage, - }) - .then((response: ServiceResponse) => { - log.debug( - `[${socket.id}] Data handler returned with ${response.messages.length} messages`, - ); - const { messages } = response; - - // Log the messages - log.trace( - `[${socket.id}] Messages: ${messages.map((m) => m.toString()).join(", ")}`, - ); - - // Serialize the messages - const serializedMessages = messages.map((m) => m.serialize()); - - try { - // Send the messages - serializedMessages.forEach((m) => { - socket.write(m); - log.trace(`[${socket.id}] Sent message: ${m.toString("hex")}`); - }); - } catch (error) { - const err = new Error( - `[${socket.id}] Error sending messages: ${(error as Error).message}`, - { cause: error }, - ); - throw err; - } - }) - .catch((error: Error) => { - const err = new Error(`[${socket.id}] Error in onData handler: ${error.message}`, { - cause: error, - }); - log.fatal(`${err.message}`); - const id = Sentry.captureException(err); - console.trace(error); - log.fatal(`Sentry event ID: ${id}`); - void getGatewayServer({}).stop(); - Sentry.flush(200).then(() => { - log.debug("Sentry flushed"); - // Call server shutdown - void getGatewayServer({}).shutdown(); - }); - }); - }, - ); -} +// function socketDataHandler( +// taggedSocket: TaggedSocket, +// incomingDataAsBuffer: Buffer, +// log: ServerLogger, +// ) { +// log.trace( +// `[${taggedSocket.id}] Received data: ${incomingDataAsBuffer.toString("hex")}`, +// ); + +// // This is a new TCP socket, so it's probably not using HTTP +// // Let's look for a port onData handler +// /** @type {OnDataHandler | undefined} */ +// const portOnDataHandler: OnDataHandler | undefined = getOnDataHandler( +// fetchStateFromDatabase(), +// taggedSocket.socket.localPort || 0, +// ); + +// // If there is no onData handler, log a warning and return +// if (!portOnDataHandler) { +// log.warn( +// `[${taggedSocket.id}] No onData handler found for port ${taggedSocket.socket.localPort}`, +// ); +// return; +// } + +// // Deserialize the raw message +// const rawMessage = new BasePacket({ +// connectionId: taggedSocket.id, +// messageId: 0, +// sequence: 0, +// messageSource: "", +// }); +// rawMessage.deserialize(incomingDataAsBuffer); + +// // Log the raw message +// log.trace(`[${taggedSocket.id}] Raw message: ${rawMessage.toHexString()}`); + +// log.debug( +// `[${taggedSocket.id}] Handling data with ${portOnDataHandler.name}`, +// ); + +// Sentry.startSpan( +// { +// name: "onDataHandler", +// op: "onDataHandler", +// }, +// async () => { +// portOnDataHandler({ +// connectionId: taggedSocket.id, +// message: rawMessage, +// }) +// .then((response: ServiceResponse) => { +// log.debug( +// `[${taggedSocket.id}] Data handler returned with ${response.messages.length} messages`, +// ); +// const { messages } = response; + +// // Log the messages +// log.trace( +// `[${taggedSocket.id}] Messages: ${messages.map((m) => m.toString()).join(", ")}`, +// ); + +// // Serialize the messages +// const serializedMessages = messages.map((m) => m.serialize()); + +// try { +// // Send the messages +// serializedMessages.forEach((m) => { +// taggedSocket.socket.write(m); +// log.trace( +// `[${taggedSocket.id}] Sent message: ${m.toString("hex")}`, +// ); +// }); +// } catch (error) { +// const err = new Error( +// `[${taggedSocket.id}] Error sending messages: ${(error as Error).message}`, +// { cause: error }, +// ); +// throw err; +// } +// }) +// .catch((error: Error) => { +// const err = new Error( +// `[${taggedSocket.id}] Error in onData handler: ${error.message}`, +// { +// cause: error, +// }, +// ); +// log.fatal(`${err.message}`); +// const id = Sentry.captureException(err); +// console.trace(error); +// log.fatal(`Sentry event ID: ${id}`); +// void getGatewayServer({}).stop(); +// Sentry.flush(200).then(() => { +// log.debug("Sentry flushed"); +// // Call server shutdown +// void getGatewayServer({}).shutdown(); +// }); +// }); +// }, +// ); +// } diff --git a/packages/gateway/src/mcotsPortRouter.ts b/packages/gateway/src/mcotsPortRouter.ts new file mode 100644 index 000000000..70cf93521 --- /dev/null +++ b/packages/gateway/src/mcotsPortRouter.ts @@ -0,0 +1,104 @@ +import { getServerLogger, type ServerLogger } from "rusty-motors-shared"; +import type { TaggedSocket } from "./socketUtility.js"; +import { + ServerPacket, + type SerializableInterface, +} from "rusty-motors-shared-packets"; +import { receiveTransactionsData } from "rusty-motors-transactions"; + +/** + * Handles the routing of messages for the MCOTS (Motor City Online Transaction Server) ports. + * + * @param taggedSocket - The socket object that contains the tagged information for routing. + */ + +export async function mcotsPortRouter({ + taggedSocket, + log = getServerLogger({ + name: "gatewayServer.mcotsPortRouter", + }), +}: { + taggedSocket: TaggedSocket; + log?: ServerLogger; +}): Promise { + const { socket, id } = taggedSocket; + + const port = socket.localPort || 0; + + if (port === 0) { + log.error(`[${id}] Local port is undefined`); + socket.end(); + return; + } + + log.debug(`[${taggedSocket.id}] MCOTS port router started for port ${port}`); + + // Handle the socket connection here + socket.on("data", (data) => { + log.debug(`[${id}] Received data: ${data.toString("hex")}`); + const initialPacket = parseInitialMessage(data); + log.debug(`[${id}] Initial packet(str): ${initialPacket}`); + log.debug(`[${id}] initial Packet(hex): ${initialPacket.toHexString()}`); + routeInitialMessage(id, port, initialPacket) + .then((response) => { + // Send the response back to the client + log.debug(`[${id}] Sending response: ${response.toString("hex")}`); + socket.write(response); + }) + .catch((error) => { + log.error(`[${id}] Error routing initial message: ${error}`); + }); + }); + + socket.on("end", () => { + log.debug(`[${id}] Socket closed`); + }); + + socket.on("error", (error) => { + log.error(`[${id}] Socket error: ${error}`); + }); +} + +function parseInitialMessage(data: Buffer): ServerPacket { + const initialPacket = new ServerPacket(); + initialPacket.deserialize(data); + return initialPacket; +} + +async function routeInitialMessage( + id: string, + port: number, + initialPacket: ServerPacket, + log = getServerLogger({ name: "gatewayServer.routeInitialMessage" }), +): Promise { + // Route the initial message to the appropriate handler + // Messages may be encrypted, this will be handled by the handler + + console.log( + `Routing message for port ${port}: ${initialPacket.toHexString()}`, + ); + let responses: SerializableInterface[] = []; + + switch (port) { + case 43300: + // Handle transactions packet + responses = ( + await receiveTransactionsData({ + connectionId: id, + message: initialPacket, + }) + ).messages; + break; + default: + console.log(`No handler found for port ${port}`); + break; + } + + // Send responses back to the client + log.debug(`[${id}] Sending ${responses.length} responses`); + + // Serialize the responses + const serializedResponses = responses.map((response) => response.serialize()); + + return Buffer.concat(serializedResponses); +} diff --git a/packages/gateway/src/npsPortRouter.ts b/packages/gateway/src/npsPortRouter.ts new file mode 100644 index 000000000..854bde973 --- /dev/null +++ b/packages/gateway/src/npsPortRouter.ts @@ -0,0 +1,126 @@ +import { getServerLogger, type ServerLogger } from "rusty-motors-shared"; +import type { TaggedSocket } from "./socketUtility.js"; +import { + GamePacket, + type SerializableInterface, +} from "rusty-motors-shared-packets"; +import { receiveLobbyData } from "rusty-motors-lobby"; +import { receiveChatData } from "rusty-motors-chat"; +import { receivePersonaData } from "rusty-motors-personas"; +import { receiveLoginData } from "rusty-motors-login"; + +/** + * Handles routing for the NPS (Network Play System) ports. + * + * @param taggedSocket - The socket that has been tagged with additional metadata. + */ + +export async function npsPortRouter({ + taggedSocket, + log = getServerLogger({ + name: "gatewayServer.npsPortRouter", + }), +}: { + taggedSocket: TaggedSocket; + log?: ServerLogger; +}): Promise { + const { socket, id } = taggedSocket; + + const port = socket.localPort || 0; + + if (port === 0) { + log.error(`[${id}] Local port is undefined`); + socket.end(); + return; + } + log.debug(`[${taggedSocket.id}] NPS port router started for port ${port}`); + + if (port === 7003) { + // Sent ok to login packet + log.debug(`[${id}] Sending ok to login packet`); + taggedSocket.socket.write(Buffer.from([0x02, 0x30, 0x00, 0x00])); + } + + // Handle the socket connection here + socket.on("data", (data) => { + log.debug(`[${id}] Received data: ${data.toString("hex")}`); + const initialPacket = parseInitialMessage(data); + log.debug(`[${id}] Initial packet(str): ${initialPacket}`); + log.debug(`[${id}] initial Packet(hex): ${initialPacket.toHexString()}`); + routeInitialMessage(id, port, initialPacket) + .then((response) => { + // Send the response back to the client + log.debug(`[${id}] Sending response: ${response.toString("hex")}`); + socket.write(response); + }) + .catch((error) => { + log.error(`[${id}] Error routing initial message: ${error}`); + }); + }); + + socket.on("end", () => { + log.debug(`[${id}] Socket closed`); + }); + + socket.on("error", (error) => { + log.error(`[${id}] Socket error: ${error}`); + }); +} + +function parseInitialMessage(data: Buffer): GamePacket { + const initialPacket = new GamePacket(); + initialPacket.deserialize(data); + return initialPacket; +} + +async function routeInitialMessage( + id: string, + port: number, + initialPacket: GamePacket, + log = getServerLogger({ name: "gatewayServer.routeInitialMessage" }), +): Promise { + // Route the initial message to the appropriate handler + // Messages may be encrypted, this will be handled by the handler + + console.log( + `Routing message for port ${port}: ${initialPacket.toHexString()}`, + ); + let responses: SerializableInterface[] = []; + + switch (port) { + case 7003: + responses = ( + await receiveLobbyData({ connectionId: id, message: initialPacket }) + ).messages; + break; + case 8226: + // Handle login packet + responses = ( + await receiveLoginData({ connectionId: id, message: initialPacket }) + ).messages; + break; + case 8227: + // Handle chat packet + responses = ( + await receiveChatData({ connectionId: id, message: initialPacket }) + ).messages; + break; + case 8228: + // responses =Handle persona packet + responses = ( + await receivePersonaData({ connectionId: id, message: initialPacket }) + ).messages; + break; + default: + console.log(`No handler found for port ${port}`); + break; + } + + // Send responses back to the client + log.debug(`[${id}] Sending ${responses.length} responses`); + + // Serialize the responses + const serializedResponses = responses.map((response) => response.serialize()); + + return Buffer.concat(serializedResponses); +} diff --git a/packages/gateway/src/portRouters.ts b/packages/gateway/src/portRouters.ts new file mode 100644 index 000000000..78dd23b2f --- /dev/null +++ b/packages/gateway/src/portRouters.ts @@ -0,0 +1,69 @@ +import { type ServerLogger } from "rusty-motors-shared"; +import type { TaggedSocket } from "./socketUtility.js"; +type PortRouter = (portRouterArgs: { + taggedSocket: TaggedSocket; + log?: ServerLogger; +}) => Promise; + +/** + * A map that associates port numbers with their corresponding router functions. + * Each router function takes a `Socket` object as an argument and returns a `Promise`. + */ +const portRouters = new Map(); + +/** + * Registers a router function for a specific port. + * + * @param port - The port number to associate with the router. + * @param router - A function that handles the socket connection for the specified port. + */ + +export function addPortRouter(port: number, router: PortRouter) { + portRouters.set(port, router); +} +/** + * Handles the case where no router is found for the given socket. + * + * This function will terminate the socket connection and throw an error + * indicating that no router was found for the port. + * + * @param taggedSocket - The socket connection that could not be routed. + * @throws {Error} Throws an error indicating no router was found for the port. + */ + +async function notFoundRouter({ + taggedSocket, +}: { + taggedSocket: TaggedSocket; +}) { + taggedSocket.socket.on("error", (error) => { + console.error(`[${taggedSocket.id}] Socket error: ${error}`); + }); + taggedSocket.socket.end(); + throw new Error(`No router found for port ${taggedSocket.socket.localPort}`); +} +/** + * Retrieves the router function associated with a given port. + * + * @param port - The port number for which to retrieve the router. + * @returns A function that takes a socket and returns a promise resolving to void. + * If no router is found for the given port, returns the `notFoundRouter` function. + */ + +export function getPortRouter(port: number): PortRouter { + const router = portRouters.get(port); + if (typeof router === "undefined") { + return notFoundRouter; + } + return router; +} + +/** + * Clears all entries from the portRouters map. + * + * This function removes all key-value pairs from the portRouters map, + * effectively resetting it to an empty state. + */ +export function clearPortRouters() { + portRouters.clear(); +} diff --git a/packages/gateway/src/socketUtility.ts b/packages/gateway/src/socketUtility.ts new file mode 100644 index 000000000..af669b10e --- /dev/null +++ b/packages/gateway/src/socketUtility.ts @@ -0,0 +1,44 @@ +import type { Socket } from "net"; + +export type TaggedSocket = { + id: string; + socket: Socket; + connectionStamp: number; +}; + +/** + * Tags a socket with an ID and a connection timestamp. + * + * @param socket - The socket to be tagged. + * @param connectionStamp - The timestamp of the connection. + * @param id - The unique identifier to tag the socket with. + * @returns An object containing the id, socket, and connectionStamp. + */ + +export function tagSocketWithId( + socket: Socket, + connectionStamp: number, + id: string, +): TaggedSocket { + return { id, socket, connectionStamp }; +} + +/** + * Attempts to write data to a socket and returns a promise that resolves when the write is successful, + * or rejects if an error occurs during the write operation. + * + * @param socket - The tagged socket to which the data will be written. + * @param data - The string data to be written to the socket. + * @returns A promise that resolves when the data is successfully written, or rejects with an error if the write fails. + */ +export async function trySocketWrite(socket: TaggedSocket, data: string): Promise { + return new Promise((resolve, reject) => { + socket.socket.write(data, (error) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); +} diff --git a/packages/gateway/src/types.ts b/packages/gateway/src/types.ts new file mode 100644 index 000000000..95e2e9c36 --- /dev/null +++ b/packages/gateway/src/types.ts @@ -0,0 +1,19 @@ +import type { Socket } from "net"; +import type { Configuration, ServerLogger } from "rusty-motors-shared"; + +/** + * Options for the GatewayServer. + */ +export type GatewayOptions = { + config?: Configuration; + log?: ServerLogger; + backlogAllowedCount?: number; + listeningPortList?: number[]; + socketConnectionHandler?: ({ + incomingSocket, + log, + }: { + incomingSocket: Socket; + log?: ServerLogger; + }) => void; +}; diff --git a/packages/gateway/test/portRouters.test.ts b/packages/gateway/test/portRouters.test.ts new file mode 100644 index 000000000..d5f4316a9 --- /dev/null +++ b/packages/gateway/test/portRouters.test.ts @@ -0,0 +1,146 @@ +import { describe, expect, it, vi } from "vitest"; +import { + addPortRouter, + clearPortRouters, + getPortRouter, +} from "../src/portRouters.js"; +import { beforeEach } from "vitest"; + +describe("addPortRouter", () => { + it("should add a router for a specific port", () => { + // arrange + const port = 8080; + const mockRouter = vi.fn().mockResolvedValue(undefined); + + // act + addPortRouter(port, mockRouter); + const retrievedRouter = getPortRouter(port); + + // assert + expect(retrievedRouter).toBe(mockRouter); + }); + + it("should overwrite an existing router for the same port", () => { + // arrange + const port = 8080; + const mockRouter1 = vi.fn().mockResolvedValue(undefined); + const mockRouter2 = vi.fn().mockResolvedValue(undefined); + + // act + addPortRouter(port, mockRouter1); + addPortRouter(port, mockRouter2); + const retrievedRouter = getPortRouter(port); + + // assert + expect(retrievedRouter).toBe(mockRouter2); + }); + + it("should handle multiple ports correctly", () => { + // arrange + const port1 = 8080; + const port2 = 9090; + const mockRouter1 = vi.fn().mockResolvedValue(undefined); + const mockRouter2 = vi.fn().mockResolvedValue(undefined); + + // act + addPortRouter(port1, mockRouter1); + addPortRouter(port2, mockRouter2); + const retrievedRouter1 = getPortRouter(port1); + const retrievedRouter2 = getPortRouter(port2); + + // assert + expect(retrievedRouter1).toBe(mockRouter1); + expect(retrievedRouter2).toBe(mockRouter2); + }); + + describe("getPortRouter", () => { + beforeEach(() => { + clearPortRouters(); + vi.resetAllMocks(); + }); + + it("should return the correct router for a specific port", () => { + // arrange + const port = 8080; + const mockRouter = vi.fn().mockResolvedValue(undefined); + addPortRouter(port, mockRouter); + + // act + const retrievedRouter = getPortRouter(port); + + // assert + expect(retrievedRouter).toBe(mockRouter); + }); + + it("should return notFoundRouter if no router is found for the port", () => { + // arrange + const port = 8080; + // act + const retrievedRouter = getPortRouter(port); + + // assert + expect(retrievedRouter).toBeInstanceOf(Function); + expect(retrievedRouter.name).toBe("notFoundRouter"); + }); + + it("should return the correct router after overwriting an existing router for the same port", () => { + // arrange + const port = 8080; + const mockRouter1 = vi.fn().mockResolvedValue(undefined); + const mockRouter2 = vi.fn().mockResolvedValue(undefined); + addPortRouter(port, mockRouter1); + addPortRouter(port, mockRouter2); + + // act + const retrievedRouter = getPortRouter(port); + + // assert + expect(retrievedRouter).toBe(mockRouter2); + }); + + it("should handle multiple ports correctly", () => { + // arrange + const port1 = 8080; + const port2 = 9090; + const mockRouter1 = vi.fn().mockResolvedValue(undefined); + const mockRouter2 = vi.fn().mockResolvedValue(undefined); + addPortRouter(port1, mockRouter1); + addPortRouter(port2, mockRouter2); + + // act + const retrievedRouter1 = getPortRouter(port1); + const retrievedRouter2 = getPortRouter(port2); + + // assert + expect(retrievedRouter1).toBe(mockRouter1); + expect(retrievedRouter2).toBe(mockRouter2); + }); + }); +describe("clearPortRouters", () => { + beforeEach(() => { + clearPortRouters(); + vi.resetAllMocks(); + }); + + it("should clear all routers", () => { + // arrange + const port1 = 8080; + const port2 = 9090; + const mockRouter1 = vi.fn().mockResolvedValue(undefined); + const mockRouter2 = vi.fn().mockResolvedValue(undefined); + addPortRouter(port1, mockRouter1); + addPortRouter(port2, mockRouter2); + + // act + clearPortRouters(); + const retrievedRouter1 = getPortRouter(port1); + const retrievedRouter2 = getPortRouter(port2); + + // assert + expect(retrievedRouter1).toBeInstanceOf(Function); + expect(retrievedRouter1.name).toBe("notFoundRouter"); + expect(retrievedRouter2).toBeInstanceOf(Function); + expect(retrievedRouter2.name).toBe("notFoundRouter"); + }); +}); +}); diff --git a/packages/gateway/test/socketUtility.test.ts b/packages/gateway/test/socketUtility.test.ts new file mode 100644 index 000000000..df3eaff6b --- /dev/null +++ b/packages/gateway/test/socketUtility.test.ts @@ -0,0 +1,103 @@ +import type { Socket } from "net"; +import { describe, expect, it, vi } from "vitest"; +import { tagSocketWithId, trySocketWrite } from "../src/socketUtility.js"; + +describe("tagSocketWithId", () => { + it("returns an object with the correct properties", () => { + // arrange + const mockSocket = {} as Socket; + const connectionStamp = Date.now(); + const id = "12345"; + + // act + const result = tagSocketWithId(mockSocket, connectionStamp, id); + + // assert + expect(result).toHaveProperty("id"); + expect(result).toHaveProperty("socket"); + expect(result).toHaveProperty("connectionStamp"); + }); + + it("returns an object with the correct values", () => { + // arrange + const mockSocket = {} as Socket; + const connectionStamp = Date.now(); + const id = "12345"; + + // act + const result = tagSocketWithId(mockSocket, connectionStamp, id); + + // assert + expect(result.id).toBe(id); + expect(result.socket).toBe(mockSocket); + expect(result.connectionStamp).toBe(connectionStamp); + }); + + describe("tagSocketWithId", () => { + it("returns an object with the correct properties", () => { + // arrange + const mockSocket = {} as Socket; + const connectionStamp = Date.now(); + const id = "12345"; + + // act + const result = tagSocketWithId(mockSocket, connectionStamp, id); + + // assert + expect(result).toHaveProperty("id"); + expect(result).toHaveProperty("socket"); + expect(result).toHaveProperty("connectionStamp"); + }); + + it("returns an object with the correct values", () => { + // arrange + const mockSocket = {} as Socket; + const connectionStamp = Date.now(); + const id = "12345"; + + // act + const result = tagSocketWithId(mockSocket, connectionStamp, id); + + // assert + expect(result.id).toBe(id); + expect(result.socket).toBe(mockSocket); + expect(result.connectionStamp).toBe(connectionStamp); + }); + }); + + describe("trySocketWrite", () => { + it("resolves when data is successfully written", async () => { + // arrange + const mockTaggedSocket = { + id: "12345", + connectionStamp: Date.now(), + socket: { + write: vi.fn((_data, callback) => callback()), + }, + }; + const data = "test data"; + + // act & assert + await expect(trySocketWrite(mockTaggedSocket, data)).resolves.toBeUndefined(); + expect(mockTaggedSocket.socket.write).toHaveBeenCalledWith(data, expect.any(Function)); + }); + + it("rejects when an error occurs during write", async () => { + // arrange + const mockTaggedSocket = { + id: "12345", + connectionStamp: Date.now(), + socket: { + write: vi.fn((_data, callback) => callback(new Error("Write error"))), + }, + }; + const data = "test data"; + + // act & assert + await expect(trySocketWrite(mockTaggedSocket, data)).rejects.toThrow( + "Write error", + ); + expect(mockTaggedSocket.socket.write).toHaveBeenCalledWith(data, expect.any(Function)); + }); + }); +}); diff --git a/packages/gateway/tsconfig.json b/packages/gateway/tsconfig.json index a23e9e608..a141ce182 100644 --- a/packages/gateway/tsconfig.json +++ b/packages/gateway/tsconfig.json @@ -4,5 +4,5 @@ "incremental": true, "composite": true }, - "include": ["index.ts", "src"] + "include": ["index.ts", "src", "test/socketUtility.test.ts"] } diff --git a/packages/nps/index.ts b/packages/nps/index.ts index d20b9a179..ae866827b 100644 --- a/packages/nps/index.ts +++ b/packages/nps/index.ts @@ -16,10 +16,6 @@ export { MiniUserInfo, MiniUserList } from "./messageStructs/MiniUserList.js"; export { ProfileList } from "./messageStructs/ProfileList.js"; export { UserInfo } from "./messageStructs/UserInfo.js"; export { UserStatus } from "./messageStructs/UserStatus.js"; -export { - getUser, - populateGameUsers, -} from "./services/account.js"; export { gameProfiles, getCustomerId, diff --git a/packages/nps/services/account.ts b/packages/nps/services/account.ts deleted file mode 100644 index 76b315705..000000000 --- a/packages/nps/services/account.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { LoginSchema, db } from "rusty-motors-database"; -import { getServerLogger } from "rusty-motors-shared"; -import type { DatabaseSchema } from "rusty-motors-database"; - -const log = getServerLogger({}); - -export async function populateGameUsers(): Promise { - await LoginSchema(db).insertOrIgnore({ - customer_id: 1, - login_name: "admin", - password: "admin", - login_level: 1, - }); -} - -/** - * Retrieves a user from the database based on the provided username and password. - * - * @param username - The username of the user to retrieve. - * @param password - The password of the user to retrieve. - * @returns A Promise that resolves to the user record from the database, or null if the user is not found. - */ -export async function getUser( - username: string, - password: string, -): Promise { - log.debug( - `Getting user: ${username}, password: ${"*".repeat(password.length)}`, - ); - - const userAccount = await LoginSchema(db).findOne({ - login_name: username, - password, - }); - - if (!userAccount) { - log.warn(`User ${username} not found`); - } - - return userAccount; -} - -/** - * Checks if the user is a super user. - * - * @param username - The username of the user. - * @param password - The password of the user. - * @returns A promise that resolves to a boolean indicating if the user is a super user. - */ -export async function isSuperUser( - username: string, - password: string, -): Promise { - const user = await getUser(username, password); - return user ? user.login_level === 1 : false; -} - -// Path: packages/nps/services/account.ts diff --git a/packages/nps/test/account.test.ts b/packages/nps/test/account.test.ts deleted file mode 100644 index 7dd0a79e6..000000000 --- a/packages/nps/test/account.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - getUser, - isSuperUser, - populateGameUsers, -} from "../services/account.js"; - -describe("getUser", () => { - it("returns the user record if found", async () => { - // arrange - const username = "admin"; - const password = "admin"; - populateGameUsers(); - - // act - const result = await getUser(username, password); - - // assert - expect(result).not.toBeNull(); - expect(result?.login_name).toBe(username); - expect(result?.password).toBe(password); - }); - - it("returns null if the user is not found", async () => { - // arrange - const username = "nonexistent"; - const password = "password"; - - // act - const result = await getUser(username, password); - - // assert - expect(result).toBeNull(); - }); - - it("returns true if the user is a super user", async () => { - // arrange - const username = "admin"; - const password = "admin"; - - // act - const result = await isSuperUser(username, password); - - // assert - expect(result).toBe(true); - }); - - it("returns false if the user is not a super user", async () => { - // arrange - const username = "regularuser"; - const password = "password"; - - // act - const result = await isSuperUser(username, password); - - // assert - expect(result).toBe(false); - }); -}); diff --git a/packages/shared-packets/index.ts b/packages/shared-packets/index.ts index 8ff1717f0..dc898b27a 100644 --- a/packages/shared-packets/index.ts +++ b/packages/shared-packets/index.ts @@ -1,4 +1,7 @@ export * from "./src/types.js"; +export { GameMessageHeader } from "./src/GameMessageHeader.js"; +export { GameMessagePayload } from "./src/GameMessagePayload.js"; +export { GamePacket } from "./src/GamePacket.js"; export { ServerMessageHeader } from "./src/ServerMessageHeader.js"; export { ServerMessagePayload } from "./src/ServerMessagePayload.js"; export { ServerPacket } from "./src/ServerPacket.js"; diff --git a/packages/shared-packets/src/GameMessageHeader.ts b/packages/shared-packets/src/GameMessageHeader.ts new file mode 100644 index 000000000..002fdca71 --- /dev/null +++ b/packages/shared-packets/src/GameMessageHeader.ts @@ -0,0 +1,128 @@ +import { BufferSerializer } from "./BufferSerializer.js"; +import type { SerializableInterface } from "./types.js"; + +/** + * + */ + +export class GameMessageHeader + extends BufferSerializer + implements SerializableInterface +{ + private id: number = 0; // 2 bytes + private length: number = 0; // 2 bytes + private version: 0 | 257 = 257; // 2 bytes + + private shouldEncryptPayload: boolean = false; + + constructor() { + super(); + } + + static copy(header: GameMessageHeader): GameMessageHeader { + const newHeader = new GameMessageHeader(); + newHeader.id = header.id; + newHeader.length = header.length; + newHeader.version = header.version; + return newHeader; + } + + override getByteSize(): number { + return this.getVersion() === 257 ? 12 : 4; + } + + getVersion(): number { + return this.version; + } + getId(): number { + return this.id; + } + getLength(): number { + return this.length; + } + setVersion(version: 0 | 257): void { + if (version !== 0 && version !== 257) { + throw new Error(`Invalid version ${parseInt(version)}`); + } + this.version = version; + } + setId(id: number): void { + this.id = id; + } + setLength(length: number): void { + this.length = length; + } + + private serializeV0(): Buffer { + const buffer = Buffer.alloc(this.getByteSize()); + buffer.writeUInt16BE(this.id, 0); + buffer.writeUInt16BE(this.length, 2); + + return buffer; + } + + private serializeV1(): Buffer { + const buffer = Buffer.alloc(this.getByteSize()); + buffer.writeUInt16BE(this.id, 0); + buffer.writeUInt16BE(this.length, 2); + buffer.writeUInt16BE(this.version, 4); + buffer.writeUInt16BE(0, 6); + buffer.writeUInt32BE(this.length, 8); + + return buffer; + } + + override serialize(): Buffer { + return this.version === 0 ? this.serializeV0() : this.serializeV1(); + } + + private deserializeV0(data: Buffer): void { + this.id = data.readUInt16BE(0); + this.length = data.readUInt16BE(2); + } + + private deserializeV1(data: Buffer): void { + this.id = data.readUInt16BE(0); + this.length = data.readUInt16BE(2); + // Skip version + // Skip padding + this.length = data.readUInt32BE(8); + } + + override deserialize(data: Buffer): void { + if (data.length < 4) { + throw new Error( + `Data is too short. Expected at least 4 bytes, got ${data.length} bytes`, + ); + } + + if (this.version === 0) { + this.deserializeV0(data); + } else { + this.deserializeV1(data); + } + } + + /** + * Sets the encryption status for the payload. + * + * @param encrypted - A boolean indicating whether the payload should be encrypted (true) or not (false). + */ + setPayloadEncryption(encrypted: boolean): void { + this.shouldEncryptPayload = encrypted; + } + + /** + * Determines if the payload should be encrypted. + * + * @returns {boolean} True if the payload should be encrypted, otherwise false. + */ + isPayloadEncrypted(): boolean { + return this.shouldEncryptPayload; + } + + override toString(): string { + return `GameMessageHeader {id: ${this.id}, length: ${this.length}, version: ${this.version}}`; + } + +} diff --git a/packages/shared-packets/src/GameMessagePayload.ts b/packages/shared-packets/src/GameMessagePayload.ts new file mode 100644 index 000000000..2e5654f9b --- /dev/null +++ b/packages/shared-packets/src/GameMessagePayload.ts @@ -0,0 +1,57 @@ +import { BufferSerializer } from "./BufferSerializer.js"; +import type { SerializableInterface } from "./types.js"; + +export class GameMessagePayload + extends BufferSerializer + implements SerializableInterface +{ + public messageId: number = 0; // 2 bytes + + private isEncrypted: boolean = false; // Not serialized + + static copy(payload: GameMessagePayload): GameMessagePayload { + const newPayload = new GameMessagePayload(); + newPayload.messageId = payload.messageId; + newPayload._data = Buffer.from(payload._data); + return newPayload; + } + + override getByteSize(): number { + return 2 + this._data.length; + } + + override serialize(): Buffer { + const buffer = Buffer.alloc(this.getByteSize()); + buffer.writeUInt16LE(this.messageId, 0); + this._data.copy(buffer, 2); + + return buffer; + } + + override deserialize(data: Buffer): GameMessagePayload { + this._assertEnoughData(data, 2); + + this.messageId = data.readUInt16LE(0); + this._data = data.subarray(2); + + return this; + } + + getMessageId(): number { + return this.messageId; + } + + setMessageId(messageId: number): GameMessagePayload { + this.messageId = messageId; + return this; + } + + isPayloadEncrypted(): boolean { + return this.isEncrypted; + } + + setPayloadEncryption(encrypted: boolean): GameMessagePayload { + this.isEncrypted = encrypted; + return this; + } +} diff --git a/packages/shared-packets/src/GamePacket.ts b/packages/shared-packets/src/GamePacket.ts new file mode 100644 index 000000000..1b2e9b1d8 --- /dev/null +++ b/packages/shared-packets/src/GamePacket.ts @@ -0,0 +1,117 @@ +import { BasePacket } from "./BasePacket.js"; +import { GameMessageHeader } from "./GameMessageHeader.js"; +import { GameMessagePayload } from "./GameMessagePayload.js"; +import type { SerializableMessage } from "./types.js"; + +export class GamePacket extends BasePacket implements SerializableMessage { + protected override header: GameMessageHeader = new GameMessageHeader(); + data: GameMessagePayload = new GameMessagePayload(); + + constructor() { + super({}); + } + + /** + * Creates a copy of the given `GamePacket` with the option to replace its data. + * + * @param originalPacket - The original `GamePacket` to be copied. + * @param newData - An optional `Buffer` containing new data to be deserialized into the new packet. + * If not provided, the data from the original packet will be copied. + * @returns A new `ServerPacket` instance with the same message ID and header as the original, + * and either the deserialized new data or a copy of the original data. + */ + static copy(originalPacket: GamePacket, newData: Buffer): GamePacket { + const newPacket = new GamePacket(); + newPacket.deserialize(originalPacket.serialize()); + + if (newData) { + newPacket.data.deserialize(newData); + } else { + newPacket.data = GameMessagePayload.copy(originalPacket.data); + } + + return newPacket; + } + + override getDataBuffer(): Buffer { + return this.data.serialize(); + } + override setDataBuffer(data: Buffer): GamePacket { + if (this.data.getByteSize() > 2) { + throw new Error( + `ServerPacket data buffer is already set, use copy() to create a new ServerPacket`, + ); + } + + this.data.deserialize(data); + return this; + } + + /** The message length is the length of the message data, not including the id */ + override getByteSize(): number { + return this.header.getByteSize() + this.data.getByteSize(); + } + + setLength(length: number): GamePacket { + this.header.setLength(length); + return this; + } + + setPayloadEncryption(encrypted: boolean): GamePacket { + this.header.setPayloadEncryption(encrypted); + return this; + } + + getMessageId(): number { + return this.data.getMessageId(); + } + + getLength(): number { + return this.header.getLength(); + } + + isPayloadEncrypted(): boolean { + return this.header.isPayloadEncrypted(); + } + + override serialize(): Buffer { + try { + const buffer = Buffer.alloc(this.getByteSize()); + this.header.serialize().copy(buffer); + this.data.serialize().copy(buffer, this.header.getByteSize()); + + return buffer; + } catch (error) { + const err = new Error("Error serializing ServerMessage"); + err.cause = error; + throw error; + } + } + + override deserialize(data: Buffer): GamePacket { + this._assertEnoughData(data, 6); + + const version = this.identifyVersion(data); + console.log("version", version); + this.header.setVersion(version); + + this._assertEnoughData(data, this.header.getByteSize()); + + this.header.deserialize(data); + this.data.deserialize(data.subarray(this.header.getByteSize())); + + return this; + } + + override toString(): string { + return `GamePacket {length: ${this.getLength()}, messageId: ${this.getMessageId()}}`; + } + + private identifyVersion(data: Buffer): 0 | 257 { + if (data.length < 6) { + return 0; + } + + return data.readUInt16BE(4) === 0x101 ? 257 : 0; + } +} diff --git a/packages/shared-packets/src/ServerMessageHeader.ts b/packages/shared-packets/src/ServerMessageHeader.ts index d50c1cdb4..3854baf0b 100644 --- a/packages/shared-packets/src/ServerMessageHeader.ts +++ b/packages/shared-packets/src/ServerMessageHeader.ts @@ -15,6 +15,19 @@ export class ServerMessageHeader private sequence: number = 0; // 4 bytes private flags: number = 0; // 1 + constructor() { + super(); + } + + static copy(header: ServerMessageHeader): ServerMessageHeader { + const newHeader = new ServerMessageHeader(); + newHeader.length = header.length; + newHeader.signature = header.signature; + newHeader.sequence = header.sequence; + newHeader.flags = header.flags; + return newHeader; + } + getDataOffset(): number { return 11; } diff --git a/packages/shared-packets/src/ServerMessagePayload.ts b/packages/shared-packets/src/ServerMessagePayload.ts index 1d0637a1a..e5c721592 100644 --- a/packages/shared-packets/src/ServerMessagePayload.ts +++ b/packages/shared-packets/src/ServerMessagePayload.ts @@ -6,6 +6,16 @@ export class ServerMessagePayload implements SerializableInterface { public messageId: number = 0; // 2 bytes + + private previousMessageId: number = 0; // Not serialized + private isEncrypted: boolean = false; // Not serialized + + static copy(payload: ServerMessagePayload): ServerMessagePayload { + const newPayload = new ServerMessagePayload(); + newPayload.messageId = payload.messageId; + newPayload._data = Buffer.from(payload._data); + return newPayload; + } override getByteSize(): number { return 2 + this._data.length; @@ -36,4 +46,22 @@ export class ServerMessagePayload this.messageId = messageId; return this; } + + getPreviousMessageId(): number { + return this.previousMessageId; + } + + setPreviousMessageId(previousMessageId: number): ServerMessagePayload { + this.previousMessageId = previousMessageId; + return this; + } + + isPayloadEncrypted(): boolean { + return this.isEncrypted; + } + + setEncrypted(encrypted: boolean): ServerMessagePayload { + this.isEncrypted = encrypted; + return this; + } } diff --git a/packages/shared-packets/src/ServerPacket.test.ts b/packages/shared-packets/src/ServerPacket.test.ts index 87737c458..349db6c0f 100644 --- a/packages/shared-packets/src/ServerPacket.test.ts +++ b/packages/shared-packets/src/ServerPacket.test.ts @@ -38,7 +38,8 @@ describe("ServerMessagePayload", () => { describe("ServerPacket", () => { it("should serialize correctly", () => { - const packet = new ServerPacket(1234); + const packet = new ServerPacket(); + packet.setMessageId(1234); packet.setLength(11); packet.setSignature("TOMC"); packet.setSequence(5678); @@ -61,7 +62,8 @@ describe("ServerMessagePayload", () => { buffer.writeUInt8(0x08, 10); buffer.writeUInt16LE(1234, 11); - const packet = new ServerPacket(0); + const packet = new ServerPacket(); + packet.setMessageId(1234); packet.deserialize(buffer); expect(packet.getLength()).toBe(11); @@ -72,7 +74,8 @@ describe("ServerMessagePayload", () => { }); it("should throw error if signature is invalid during serialization", () => { - const packet = new ServerPacket(1234); + const packet = new ServerPacket(); + packet.setMessageId(1234); packet.setLength(11); packet.setSignature("INVALID"); packet.setSequence(5678); @@ -84,7 +87,8 @@ describe("ServerMessagePayload", () => { }); it("should throw error if sequence is zero during serialization", () => { - const packet = new ServerPacket(1234); + const packet = new ServerPacket(); + packet.setMessageId(1234); packet.setLength(11); packet.setSignature("TOMC"); packet.setSequence(0); @@ -96,7 +100,8 @@ describe("ServerMessagePayload", () => { }); it("should convert to string correctly", () => { - const packet = new ServerPacket(1234); + const packet = new ServerPacket(); + packet.setMessageId(1234); packet.setLength(11); packet.setSignature("TOMC"); packet.setSequence(5678); diff --git a/packages/shared-packets/src/ServerPacket.ts b/packages/shared-packets/src/ServerPacket.ts index 496a9282d..5b58700cb 100644 --- a/packages/shared-packets/src/ServerPacket.ts +++ b/packages/shared-packets/src/ServerPacket.ts @@ -4,18 +4,45 @@ import { ServerMessagePayload } from "./ServerMessagePayload.js"; import type { SerializableMessage } from "./types.js"; export class ServerPacket extends BasePacket implements SerializableMessage { - protected override header: ServerMessageHeader; - data: ServerMessagePayload; + protected override header: ServerMessageHeader = new ServerMessageHeader(); + data: ServerMessagePayload = new ServerMessagePayload(); - constructor(messageId: number) { + constructor() { super({}); - this.header = new ServerMessageHeader(); - this.data = new ServerMessagePayload().setMessageId(messageId); } + + /** + * Creates a copy of the given `ServerPacket` with the option to replace its data. + * + * @param originalPacket - The original `ServerPacket` to be copied. + * @param newData - An optional `Buffer` containing new data to be deserialized into the new packet. + * If not provided, the data from the original packet will be copied. + * @returns A new `ServerPacket` instance with the same message ID and header as the original, + * and either the deserialized new data or a copy of the original data. + */ + static copy(originalPacket: ServerPacket, newData: Buffer): ServerPacket { + const newPacket = new ServerPacket(); + newPacket.header = ServerMessageHeader.copy(originalPacket.header); + + if (newData) { + newPacket.data.deserialize(newData); + } else { + newPacket.data = ServerMessagePayload.copy(originalPacket.data); + } + + return newPacket; + } + override getDataBuffer(): Buffer { return this.data.serialize(); } override setDataBuffer(data: Buffer): ServerPacket { + if (this.data.getByteSize() > 2) { + throw new Error( + `ServerPacket data buffer is already set, use copy() to create a new ServerPacket`, + ); + } + this.data.deserialize(data); return this; } @@ -49,6 +76,11 @@ export class ServerPacket extends BasePacket implements SerializableMessage { return this.data.getMessageId(); } + setMessageId(messageId: number): ServerPacket { + this.data.setMessageId(messageId); + return this; + } + getLength(): number { return this.header.getLength(); } @@ -103,7 +135,7 @@ export class ServerPacket extends BasePacket implements SerializableMessage { this._assertEnoughData(data, this.header.getByteSize()); this.header.deserialize(data); - this.setDataBuffer(data.subarray(this.header.getDataOffset())); + this.data.deserialize(data.subarray(this.header.getDataOffset())); return this; } diff --git a/packages/shared-packets/src/types.ts b/packages/shared-packets/src/types.ts index eeea1b6ab..2167d56d2 100644 --- a/packages/shared-packets/src/types.ts +++ b/packages/shared-packets/src/types.ts @@ -18,6 +18,9 @@ export interface SerializableMessage extends SerializableInterface { getDataBuffer(): Buffer; setDataBuffer(data: Buffer): void; + + toString(): string; + toHexString(): string; } export enum MessageSources { diff --git a/packages/shared-packets/src/BasePacket.test.ts b/packages/shared-packets/test/BasePacket.test.ts similarity index 94% rename from packages/shared-packets/src/BasePacket.test.ts rename to packages/shared-packets/test/BasePacket.test.ts index 69e1ea519..449b8f0e4 100644 --- a/packages/shared-packets/src/BasePacket.test.ts +++ b/packages/shared-packets/test/BasePacket.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "vitest"; -import { BasePacket } from "./BasePacket.js"; -import { BufferSerializer } from "./BufferSerializer.js"; +import { BasePacket } from "../src/BasePacket.js"; +import { BufferSerializer } from "../src/BufferSerializer.js"; describe("BasePacket", () => { it("should initialize with default values", () => { diff --git a/packages/shared-packets/src/BufferSerializer.test.ts b/packages/shared-packets/test/BufferSerializer.test.ts similarity index 96% rename from packages/shared-packets/src/BufferSerializer.test.ts rename to packages/shared-packets/test/BufferSerializer.test.ts index 490e1ca95..9d65b7365 100644 --- a/packages/shared-packets/src/BufferSerializer.test.ts +++ b/packages/shared-packets/test/BufferSerializer.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect, beforeEach } from "vitest"; -import { BufferSerializer } from "./BufferSerializer.js"; +import { BufferSerializer } from "../src/BufferSerializer.js"; describe("BufferSerializer", () => { let bufferSerializer: BufferSerializer; diff --git a/packages/shared-packets/src/GenericReplyPayload.test.ts b/packages/shared-packets/test/GenericReplyPayload.test.ts similarity index 95% rename from packages/shared-packets/src/GenericReplyPayload.test.ts rename to packages/shared-packets/test/GenericReplyPayload.test.ts index 6ce36f69c..a576efffe 100644 --- a/packages/shared-packets/src/GenericReplyPayload.test.ts +++ b/packages/shared-packets/test/GenericReplyPayload.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { GenericReplyPayload } from "./GenericReplyPayload.js"; +import { GenericReplyPayload } from "../src/GenericReplyPayload.js"; import { Buffer } from "buffer"; describe("GenericReplyPayload", () => { diff --git a/packages/shared-packets/src/GenericRequestPayload.test.ts b/packages/shared-packets/test/GenericRequestPayload.test.ts similarity index 95% rename from packages/shared-packets/src/GenericRequestPayload.test.ts rename to packages/shared-packets/test/GenericRequestPayload.test.ts index a9e81aff1..6c0573ea0 100644 --- a/packages/shared-packets/src/GenericRequestPayload.test.ts +++ b/packages/shared-packets/test/GenericRequestPayload.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { GenericRequestPayload } from "./GenericRequestPayload.js"; +import { GenericRequestPayload } from "../src/GenericRequestPayload.js"; import { Buffer } from "buffer"; describe("GenericRequestPayload", () => { diff --git a/packages/shared-packets/src/ServerMessageHeader.test.ts b/packages/shared-packets/test/ServerMessageHeader.test.ts similarity index 97% rename from packages/shared-packets/src/ServerMessageHeader.test.ts rename to packages/shared-packets/test/ServerMessageHeader.test.ts index b032cb788..87f5bdec4 100644 --- a/packages/shared-packets/src/ServerMessageHeader.test.ts +++ b/packages/shared-packets/test/ServerMessageHeader.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { ServerMessageHeader } from "./ServerMessageHeader.js"; +import { ServerMessageHeader } from "../src/ServerMessageHeader.js"; describe("ServerMessageHeader", () => { it("should serialize correctly", () => { diff --git a/packages/shared/index.ts b/packages/shared/index.ts index 9b660b584..516bfdca5 100644 --- a/packages/shared/index.ts +++ b/packages/shared/index.ts @@ -57,7 +57,7 @@ export interface ConnectionRecord { } // Function to convert ARGB to 32-bit integer -function argbToInt(alpha: number, red: number, green: number, blue: number) { +export function argbToInt(alpha: number, red: number, green: number, blue: number) { return ( ((alpha & 0xff) << 24) | ((red & 0xff) << 16) | @@ -67,7 +67,7 @@ function argbToInt(alpha: number, red: number, green: number, blue: number) { } // Function to convert 32-bit integer to ARGB -function intToArgb(int: number) { +export function intToArgb(int: number) { return { alpha: (int >> 24) & 0xff, red: (int >> 16) & 0xff, diff --git a/packages/shared/src/BaseSerialized.ts b/packages/shared/src/BaseSerialized.ts index 99c1a5a40..59d8c3244 100644 --- a/packages/shared/src/BaseSerialized.ts +++ b/packages/shared/src/BaseSerialized.ts @@ -1,7 +1,7 @@ export interface Serializable { data: Buffer; serialize(): Buffer; - deserialize(buffer: Buffer): Serializable; + deserialize(buffer: Buffer): T; length: number; toString(): string; asHex(): string; @@ -30,7 +30,7 @@ export class BaseSerialized implements Serializable { throw Error("Not implemented"); } - deserialize(_buffer: Buffer): Serializable { + deserialize(_buffer: Buffer): T { throw Error("Not implemented"); } diff --git a/packages/shared/src/NetworkMessage.ts b/packages/shared/src/NetworkMessage.ts index bf84671ef..4c65bc088 100644 --- a/packages/shared/src/NetworkMessage.ts +++ b/packages/shared/src/NetworkMessage.ts @@ -1,3 +1,4 @@ +import type { Serializable } from "./BaseSerialized.js"; import { SerializedBuffer } from "./SerializedBuffer.js"; /** @@ -27,7 +28,7 @@ export class NetworkMessage extends SerializedBuffer { this._data.copy(buffer, 12); return buffer; } - override deserialize(buffer: Buffer) { + override deserialize(buffer: Buffer): T { if (buffer.length < 12) { throw Error(`Unable to get header from buffer, got ${buffer.length}`); } @@ -46,7 +47,7 @@ export class NetworkMessage extends SerializedBuffer { throw Error(`Checksum ${checksum} does not match length ${length}`); } - return this; + return this as unknown as T; } override set data(data: Buffer) { diff --git a/packages/shared/src/OldServerMessage.ts b/packages/shared/src/OldServerMessage.ts index 1efefd11b..0ae4c43a3 100644 --- a/packages/shared/src/OldServerMessage.ts +++ b/packages/shared/src/OldServerMessage.ts @@ -5,8 +5,8 @@ import { serverHeader } from "./serverHeader.js"; * A server message is a message that is passed between the server and the client. It has an 11 byte header. @see {@link serverHeader} * * @mixin {SerializableMixin} + * @deprecated */ - export class OldServerMessage extends SerializedBufferOld { _header: serverHeader; _msgNo: number; @@ -21,6 +21,7 @@ export class OldServerMessage extends SerializedBufferOld { } /** + * @deprecated * @param {Buffer} buffer * @returns {OldServerMessage} */ @@ -33,6 +34,9 @@ export class OldServerMessage extends SerializedBufferOld { return this; } + /** + * @deprecated + */ override serialize() { const buffer = Buffer.alloc(this._header.length + 2); this._header._doSerialize().copy(buffer); @@ -41,6 +45,7 @@ export class OldServerMessage extends SerializedBufferOld { } /** + * @deprecated * @param {Buffer} buffer */ override setBuffer(buffer: Buffer) { @@ -48,6 +53,9 @@ export class OldServerMessage extends SerializedBufferOld { this._header.length = buffer.length + this._header._size - 2; } + /** + * @deprecated + */ updateMsgNo() { this._msgNo = this.data.readInt16LE(0); } @@ -59,7 +67,7 @@ export class OldServerMessage extends SerializedBufferOld { })}`; } - toHexString() { + override toHexString() { return this.serialize().toString("hex"); } } diff --git a/packages/shared/src/RawMessage.ts b/packages/shared/src/RawMessage.ts index 14334e02d..508e989ff 100644 --- a/packages/shared/src/RawMessage.ts +++ b/packages/shared/src/RawMessage.ts @@ -1,3 +1,4 @@ +import type { Serializable } from "./BaseSerialized.js"; import { SerializedBuffer } from "./SerializedBuffer.js"; /** @@ -16,7 +17,7 @@ export class RawMessage extends SerializedBuffer { this._data.copy(buffer, 4); return buffer; } - override deserialize(buffer: Buffer) { + override deserialize(buffer: Buffer): T { if (buffer.length < 4) { throw Error(`Unable to get header from buffer, got ${buffer.length}`); } @@ -26,7 +27,7 @@ export class RawMessage extends SerializedBuffer { } this._messageId = buffer.readUInt16BE(0); this._data = buffer.subarray(4, 4 + length); - return this; + return this as unknown as T; } get messageId(): number { diff --git a/packages/shared/src/SerializedBuffer.ts b/packages/shared/src/SerializedBuffer.ts index 2a4c6c388..2e38a8326 100644 --- a/packages/shared/src/SerializedBuffer.ts +++ b/packages/shared/src/SerializedBuffer.ts @@ -1,4 +1,4 @@ -import { BaseSerialized } from "./BaseSerialized.js"; +import { BaseSerialized, type Serializable } from "./BaseSerialized.js"; /** * A serialized buffer, prefixed with its 2-byte length. @@ -16,7 +16,7 @@ export class SerializedBuffer extends BaseSerialized { throw err; } } - override deserialize(buffer: Buffer): SerializedBuffer { + override deserialize(buffer: Buffer): T { try { const length = buffer.readUInt16BE(0); if (buffer.length < 2 + length) { @@ -25,7 +25,7 @@ export class SerializedBuffer extends BaseSerialized { ); } this._data = buffer.subarray(2, 2 + length); - return this; + return this as unknown as T; } catch (error) { const err = Error(`Error deserializing buffer: ${String(error)}`); err.cause = error; diff --git a/packages/shared/src/SerializedBufferOld.ts b/packages/shared/src/SerializedBufferOld.ts index 12da805c9..f2d0322c3 100644 --- a/packages/shared/src/SerializedBufferOld.ts +++ b/packages/shared/src/SerializedBufferOld.ts @@ -1,3 +1,4 @@ +import type { SerializableInterface } from "rusty-motors-shared-packets"; import { SerializableMixin, AbstractSerializable } from "./messageFactory.js"; /** @@ -8,8 +9,8 @@ import { SerializableMixin, AbstractSerializable } from "./messageFactory.js"; */ export class SerializedBufferOld extends SerializableMixin( - AbstractSerializable, -) { + AbstractSerializable, +) implements SerializableInterface { constructor() { super(); } @@ -23,6 +24,10 @@ export class SerializedBufferOld extends SerializableMixin( return this; } + deserialize(data: Buffer): void { + this.setBuffer(data); + } + serialize() { return this.data; } @@ -35,6 +40,10 @@ export class SerializedBufferOld extends SerializableMixin( return this.data.length; } + getByteSize() { + return this.size(); + } + toHexString() { return this.data.toString("hex"); } diff --git a/packages/shared/src/ServerMessage.ts b/packages/shared/src/ServerMessage.ts index bddb401dd..e73d98ab0 100644 --- a/packages/shared/src/ServerMessage.ts +++ b/packages/shared/src/ServerMessage.ts @@ -1,3 +1,4 @@ +import type { Serializable } from "./BaseSerialized.js"; import { SerializedBuffer } from "./SerializedBuffer.js"; class HeaderShim { @@ -46,7 +47,7 @@ export class ServerMessage extends SerializedBuffer { this._data.copy(buffer, 11); return buffer; } - override deserialize(buffer: Buffer) { + override deserialize(buffer: Buffer): T { if (buffer.length < 11) { throw Error(`Unable to get header from buffer, got ${buffer.length}`); } @@ -58,7 +59,7 @@ export class ServerMessage extends SerializedBuffer { this._sequence = buffer.readInt32LE(6); this._flags = buffer.readInt8(10); this._data = buffer.subarray(11, 11 + length); - return this; + return this as unknown as T; } override get data(): Buffer { diff --git a/packages/transactions/src/_getPlayerPhysical.ts b/packages/transactions/src/_getPlayerPhysical.ts index 546dd57d8..227f8e690 100644 --- a/packages/transactions/src/_getPlayerPhysical.ts +++ b/packages/transactions/src/_getPlayerPhysical.ts @@ -1,4 +1,11 @@ -import { cloth_white, cloth_yellow, getServerLogger, hair_red, OldServerMessage, skin_pale } from "rusty-motors-shared"; +import { + cloth_white, + cloth_yellow, + getServerLogger, + hair_red, + OldServerMessage, + skin_pale, +} from "rusty-motors-shared"; import { GenericRequestMessage } from "./GenericRequestMessage.js"; import { PlayerPhysicalMessage } from "./PlayerPhysicalMessage.js"; import type { MessageHandlerArgs, MessageHandlerResult } from "./handlers.js"; @@ -13,7 +20,9 @@ export async function _getPlayerPhysical({ const getPlayerPhysicalMessage = new GenericRequestMessage(); getPlayerPhysicalMessage.deserialize(packet.data); - log.debug(`[${connectionId}] Received GenericRequestMessage: ${getPlayerPhysicalMessage.toString()}`); + log.debug( + `[${connectionId}] Received GenericRequestMessage: ${getPlayerPhysicalMessage.toString()}`, + ); const playerId = getPlayerPhysicalMessage.data.readUInt32LE(0); @@ -26,7 +35,9 @@ export async function _getPlayerPhysical({ playerPhysicalMessage._shirtColor = cloth_white; playerPhysicalMessage._pantsColor = cloth_yellow; - log.debug(`[${connectionId}] Sending PlayerPhysicalMessage: ${playerPhysicalMessage.toString()}`); + log.debug( + `[${connectionId}] Sending PlayerPhysicalMessage: ${playerPhysicalMessage.toString()}`, + ); const responsePacket = new OldServerMessage(); responsePacket._header.sequence = packet._header.sequence; diff --git a/packages/transactions/src/_getPlayerRaceHistory.ts b/packages/transactions/src/_getPlayerRaceHistory.ts index 5a8e4a4e9..e0f7424c7 100644 --- a/packages/transactions/src/_getPlayerRaceHistory.ts +++ b/packages/transactions/src/_getPlayerRaceHistory.ts @@ -8,14 +8,12 @@ import type { MessageHandlerArgs, MessageHandlerResult } from "./handlers.js"; import { getRacingHistoryRecords } from "./database/racingHistoryRecords.js"; import { GenericReplyPayload } from "rusty-motors-shared-packets"; -const log = getServerLogger({ - name: "transactions/_getPlayerRaceHistory", -}); - export async function _getPlayerRaceHistory({ connectionId, packet, - log, + log = getServerLogger({ + name: "transactions._getPlayerRaceHistory", + }), }: MessageHandlerArgs): Promise { log.debug(`[${connectionId}] Handling _getPlayerRaceHistory...`); diff --git a/packages/transactions/src/internal.ts b/packages/transactions/src/internal.ts index b78e1d929..b8b4f44ef 100644 --- a/packages/transactions/src/internal.ts +++ b/packages/transactions/src/internal.ts @@ -14,7 +14,13 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { getServerConfiguration, type ServerLogger } from "rusty-motors-shared"; +import { + getServerConfiguration, + McosEncryption, + SerializedBufferOld, + type ServerLogger, + type State, +} from "rusty-motors-shared"; import { fetchStateFromDatabase, getEncryption, @@ -22,43 +28,34 @@ import { } from "rusty-motors-shared"; import { getServerLogger } from "rusty-motors-shared"; import { OldServerMessage } from "rusty-motors-shared"; -import { SerializedBufferOld } from "rusty-motors-shared"; -import { ServerMessage } from "rusty-motors-shared"; -import { messageHandlers } from "./handlers.js"; -import type { BufferSerializer } from "rusty-motors-shared-packets"; +import { messageHandlers, type MessageHandlerResult } from "./handlers.js"; +import { + ServerPacket, + type BufferSerializer, +} from "rusty-motors-shared-packets"; import { _MSG_STRING } from "./_MSG_STRING.js"; -/** - * - * - * @param {ServerMessage} message - * @return {boolean} - */ -function isMessageEncrypted( - message: OldServerMessage | ServerMessage, -): boolean { - return message._header.flags - 8 >= 0; -} - /** * Route or process MCOTS commands - * @param {import("./handlers.js").MessageHandlerArgs} args - * @returns {Promise} + * @param {MessageHandlerArgs} args + * @returns {Promise} */ async function processInput({ connectionId, - packet, + inboundMessage, log = getServerLogger({ name: "transactionServer", }), -}: import("./handlers.js").MessageHandlerArgs): Promise< - import("./handlers.js").MessageHandlerResult -> { - const currentMessageNo = packet._msgNo; +}: { + connectionId: string; + inboundMessage: ServerPacket; + log?: ServerLogger; +}): Promise { + const currentMessageNo = inboundMessage.getMessageId(); const currentMessageString = _MSG_STRING(currentMessageNo); log.debug( - `[${connectionId}] Processing message: ${currentMessageNo} (${currentMessageString}), sequence: ${packet._header.sequence}`, + `[${connectionId}] Processing message: ${currentMessageNo} (${currentMessageString}), sequence: ${inboundMessage.getSequence()}`, ); const result = messageHandlers.find( @@ -66,6 +63,10 @@ async function processInput({ ); if (typeof result !== "undefined") { + // Turn this into an OldServerMessage for compatibility + const packet = new OldServerMessage(); + packet._doDeserialize(inboundMessage.serialize()); + try { const responsePackets = await result.handler({ connectionId, @@ -74,9 +75,9 @@ async function processInput({ }); return responsePackets; } catch (error) { - const err = Error(`[${connectionId}] Error processing message`, - { cause: error } - ); + const err = Error(`[${connectionId}] Error processing message`, { + cause: error, + }); throw err; } } @@ -100,7 +101,7 @@ export async function receiveTransactionsData({ connectionId, message, log = getServerLogger({ - name: "transactionServer", + name: "transactionServer.receiveTransactionsData", level: getServerConfiguration({}).logLevel ?? "info", }), }: { @@ -113,16 +114,19 @@ export async function receiveTransactionsData({ }> { log.debug(`[${connectionId}] Entering transaction module`); - // Going to use ServerMessage in this module + // Normalize the message + + const inboundMessage = new ServerPacket(0); + inboundMessage.deserialize(message.serialize()); - const inboundMessage = new OldServerMessage(); - inboundMessage._doDeserialize(message.serialize()); log.debug( `[${connectionId}] Received message: ${inboundMessage.toHexString()}`, ); + let decryptedMessage: ServerPacket; + // Is the message encrypted? - if (isMessageEncrypted(inboundMessage)) { + if (inboundMessage.isPayloadEncrypted()) { log.debug(`[${connectionId}] Message is encrypted`); // Get the encryyption settings for this connection const state = fetchStateFromDatabase(); @@ -135,51 +139,39 @@ export async function receiveTransactionsData({ // log the old buffer log.debug( - `[${connectionId}] Inbound buffer: ${inboundMessage.data.toString("hex")}`, + `[${connectionId}] Inbound buffer: ${inboundMessage.data.toHexString()}`, ); - try { - const decryptedMessage = encryptionSettings.dataEncryption.decrypt( - inboundMessage.data, - ); - updateEncryption(state, encryptionSettings).save(); - - // Verify the length of the message - verifyLength(inboundMessage.data, decryptedMessage); - - // Assuming the message was decrypted successfully, update the buffer - log.debug( - `[${connectionId}] Decrypted buffer: ${decryptedMessage.toString("hex")}`, - ); - - inboundMessage.setBuffer(decryptedMessage); - inboundMessage._header.flags -= 8; - inboundMessage.updateMsgNo(); - - log.debug( - `[${connectionId}] Decrypted message: ${inboundMessage.toHexString()}`, - ); - } catch (error) { - const err = Error(`[${connectionId}] Unable to decrypt message`, { - cause: error, - }); - throw err; - } + decryptedMessage = decryptMessage( + encryptionSettings, + inboundMessage, + state, + log, + connectionId, + ); + } else { + log.debug(`[${connectionId}] Message is not encrypted`); + decryptedMessage = inboundMessage; } + // Process the message + const response = await processInput({ connectionId, - packet: inboundMessage, + inboundMessage: decryptedMessage, log, }); // Loop through the outbound messages and encrypt them - const outboundMessages: SerializedBufferOld[] = []; + const outboundMessages: ServerPacket[] = []; - response.messages.forEach((outboundMessage) => { + response.messages.forEach((message) => { log.debug(`[${connectionId}] Processing outbound message`); - if (isMessageEncrypted(outboundMessage)) { + const outboundMessage = new ServerPacket(0); + outboundMessage.deserialize(message.serialize()); + + if (outboundMessage.isPayloadEncrypted()) { const state = fetchStateFromDatabase(); const encryptionSettings = getEncryption(state, connectionId); @@ -190,49 +182,22 @@ export async function receiveTransactionsData({ // log the old buffer log.debug( - `[${connectionId}] Outbound buffer: ${outboundMessage.data.toString("hex")}`, + `[${connectionId}] Outbound buffer: ${outboundMessage.data.toHexString()}`, ); - try { - const encryptedMessage = encryptionSettings.dataEncryption.encrypt( - outboundMessage.data, - ); - updateEncryption(state, encryptionSettings).save(); - - // Verify the length of the message - verifyLength(outboundMessage.data, encryptedMessage); - - // Assuming the message was decrypted successfully, update the buffer - - log.debug( - `[${connectionId}] Encrypted buffer: ${encryptedMessage.toString("hex")}`, - ); - - outboundMessage.setBuffer(encryptedMessage); - - log.debug( - `[${connectionId}] Encrypted message: ${outboundMessage.toHexString()}`, - ); - - const outboundRawMessage = new SerializedBufferOld(); - outboundRawMessage.setBuffer(outboundMessage.serialize()); - log.debug( - `[${connectionId}] Outbound message: ${outboundRawMessage.toHexString()}`, - ); - outboundMessages.push(outboundRawMessage); - } catch (error) { - const err = Error(`[${connectionId}] Unable to encrypt message`, { - cause: error, - }); - throw err; - } + const encryptedMessage = encryptOutboundMessage( + encryptionSettings, + outboundMessage, + state, + log, + connectionId, + ); + outboundMessages.push(encryptedMessage); } else { - const outboundRawMessage = new SerializedBufferOld(); - outboundRawMessage.setBuffer(outboundMessage.serialize()); log.debug( - `[${connectionId}] Outbound message: ${outboundRawMessage.toHexString()}`, + `[${connectionId}] Outbound message: ${outboundMessage.toHexString()}`, ); - outboundMessages.push(outboundRawMessage); + outboundMessages.push(outboundMessage); } }); @@ -240,12 +205,109 @@ export async function receiveTransactionsData({ `[${connectionId}] Exiting transaction module with ${outboundMessages.length} messages`, ); + // Convert the outbound messages to SerializedBufferOld + const outboundMessagesSerialized = outboundMessages.map((message) => { + const serialized = new SerializedBufferOld(); + serialized._doDeserialize(message.serialize()); + return serialized; + }); + return { connectionId, - messages: outboundMessages, + messages: outboundMessagesSerialized, }; } +/** + * Decrypts an inbound message using the provided encryption settings and updates the state. + * + * @param encryptionSettings - The encryption settings to use for decryption. + * @param inboundMessage - The message to be decrypted. + * @param state - The current state of the server. + * @param log - The logger instance to use for logging. Defaults to a server logger with the name "transactionServer.decryptMessage". + * @param connectionId - The ID of the connection associated with the message. + * @returns The decrypted message as a `ServerPacket`. + * @throws Will throw an error if the message cannot be decrypted. + */ +function decryptMessage( + encryptionSettings: McosEncryption, + inboundMessage: ServerPacket, + state: State, + log: ServerLogger = getServerLogger({ + name: "transactionServer.decryptMessage", + }), + connectionId: string, +): ServerPacket { + try { + const decryptedMessage = encryptionSettings.dataEncryption.decrypt( + inboundMessage.data.serialize(), + ); + updateEncryption(state, encryptionSettings).save(); + + // Verify the length of the message + verifyLength(inboundMessage.data.serialize(), decryptedMessage); + + // Assuming the message was decrypted successfully, update the buffer + log.debug( + `[${connectionId}] Decrypted buffer: ${decryptedMessage.toString("hex")}`, + ); + + const outboundMessage = ServerPacket.copy(inboundMessage, decryptedMessage); + outboundMessage.setPayloadEncryption(false); + + log.debug( + `[${connectionId}] Decrypted message: ${inboundMessage.toHexString()}`, + ); + + return outboundMessage; + } catch (error) { + const err = Error(`[${connectionId}] Unable to decrypt message`, { + cause: error, + }); + throw err; + } +} + +function encryptOutboundMessage( + encryptionSettings: McosEncryption, + unencryptedMessage: ServerPacket, + state: State, + log: ServerLogger, + connectionId: string, +): ServerPacket { + try { + const encryptedMessage = encryptionSettings.dataEncryption.encrypt( + unencryptedMessage.data.serialize(), + ); + updateEncryption(state, encryptionSettings).save(); + + // Verify the length of the message + verifyLength(unencryptedMessage.data.serialize(), encryptedMessage); + + // Assuming the message was decrypted successfully, update the buffer + log.debug( + `[${connectionId}] Encrypted buffer: ${encryptedMessage.toString("hex")}`, + ); + + const outboundMessage = ServerPacket.copy( + unencryptedMessage, + encryptedMessage, + ); + outboundMessage.setPayloadEncryption(true); + + log.debug( + `[${connectionId}] Encrypted message: ${outboundMessage.toHexString()}`, + ); + + return outboundMessage; + } catch (error) { + const err = Error(`[${connectionId}] Unable to encrypt message`, { + cause: error, + }); + throw err; + } +} + /** * @param {Buffer} buffer * @param {Buffer} buffer2 diff --git a/packages/transactions/src/login.ts b/packages/transactions/src/login.ts index fd756fe34..f3b091858 100644 --- a/packages/transactions/src/login.ts +++ b/packages/transactions/src/login.ts @@ -50,8 +50,7 @@ export async function login({ // Normalize the packet - const outgoingPacket = new ServerPacket(response.messageId); - outgoingPacket.deserialize(response.serialize()); + const outgoingPacket = ServerPacket.copy(incomingPacket, response.serialize()); outgoingPacket.setSequence(incomingPacket.getSequence()); outgoingPacket.setPayloadEncryption(true); outgoingPacket.setSignature("TOMC"); @@ -59,7 +58,7 @@ export async function login({ log.debug(`[${connectionId}] Sending response: ${outgoingPacket.toString()}`); log.debug( - `[${connectionId}] Sending response: ${outgoingPacket.serialize().toString("hex")}`, + `[${connectionId}] Sending response(hex): ${outgoingPacket.serialize().toString("hex")}`, ); const responsePacket = new OldServerMessage(); diff --git a/server.ts b/server.ts index 21f5642e6..dfd405097 100755 --- a/server.ts +++ b/server.ts @@ -22,61 +22,60 @@ import { getServerConfiguration } from "rusty-motors-shared"; import { getServerLogger } from "rusty-motors-shared"; const coreLogger = getServerLogger({ - name: "core", + name: "core", }); try { - verifyLegacyCipherSupport(); + verifyLegacyCipherSupport(); } catch (err) { - coreLogger.fatal(`Error in core server: ${String(err)}`); - exit(1); + coreLogger.fatal(`Error in core server: ${String(err)}`); + exit(1); } try { - if (typeof process.env["EXTERNAL_HOST"] === "undefined") { - console.error("Please set EXTERNAL_HOST"); - process.exit(1); - } - if (typeof process.env["CERTIFICATE_FILE"] === "undefined") { - console.error("Please set CERTIFICATE_FILE"); - process.exit(1); - } - if (typeof process.env["PRIVATE_KEY_FILE"] === "undefined") { - console.error("Please set PRIVATE_KEY_FILE"); - process.exit(1); - } - if (typeof process.env["PUBLIC_KEY_FILE"] === "undefined") { - console.error("Please set PUBLIC_KEY_FILE"); - process.exit(1); - } - const config = getServerConfiguration({ - host: process.env["EXTERNAL_HOST"], - certificateFile: process.env["CERTIFICATE_FILE"], - privateKeyFile: process.env["PRIVATE_KEY_FILE"], - publicKeyFile: process.env["PUBLIC_KEY_FILE"], - logLevel: process.env["MCO_LOG_LEVEL"] || "info", - }); + if (typeof process.env["EXTERNAL_HOST"] === "undefined") { + console.error("Please set EXTERNAL_HOST"); + process.exit(1); + } + if (typeof process.env["CERTIFICATE_FILE"] === "undefined") { + console.error("Please set CERTIFICATE_FILE"); + process.exit(1); + } + if (typeof process.env["PRIVATE_KEY_FILE"] === "undefined") { + console.error("Please set PRIVATE_KEY_FILE"); + process.exit(1); + } + if (typeof process.env["PUBLIC_KEY_FILE"] === "undefined") { + console.error("Please set PUBLIC_KEY_FILE"); + process.exit(1); + } + const config = getServerConfiguration({ + host: process.env["EXTERNAL_HOST"], + certificateFile: process.env["CERTIFICATE_FILE"], + privateKeyFile: process.env["PRIVATE_KEY_FILE"], + publicKeyFile: process.env["PUBLIC_KEY_FILE"], + logLevel: process.env["MCO_LOG_LEVEL"] || "info", + }); - const appLog = getServerLogger({ - level: config.logLevel, - name: "app", - }); + const appLog = getServerLogger({ + level: config.logLevel, + name: "app", + }); - const listeningPortList = [ - 6660, 7003, 8228, 8226, 8227, 9000, 9001, 9002, 9003, 9004, 9005, 9006, - 9007, 9008, 9009, 9010, 9011, 9012, 9013, 9014, 43200, 43300, 43400, - 53303, - ]; + const listeningPortList = [ + 6660, 7003, 8228, 8226, 8227, 9000, 9001, 9002, 9003, 9004, 9005, 9006, + 9007, 9008, 9009, 9010, 9011, 9012, 9013, 9014, 43200, 43300, 43400, 53303, + ]; - const gatewayServer = getGatewayServer({ - config, - log: appLog, - listeningPortList, - }); + const gatewayServer = getGatewayServer({ + config, + log: appLog, + listeningPortList, + }); - gatewayServer.start(); + gatewayServer.start(); } catch (err) { - Sentry.captureException(err); - coreLogger.fatal(`Error in core server: ${String(err)}`); - process.exit(1); + Sentry.captureException(err); + coreLogger.fatal(`Error in core server: ${String(err)}`); + process.exit(1); } diff --git a/src/chat/index.ts b/src/chat/index.ts index fa3f1ae6c..f2e317ef1 100644 --- a/src/chat/index.ts +++ b/src/chat/index.ts @@ -10,6 +10,7 @@ import { handleReceiveEmailMessage, } from "./inGameEmails.js"; import { bufferToHexString } from "./toHexString.js"; +import * as Sentry from "@sentry/node"; const handlers = new Map Buffer[]>(); handlers.set(0x0524, handleReceiveEmailMessage); @@ -34,8 +35,21 @@ async function receiveChatData({ log.info(`Received chat data from connection ${connectionId}`); log.debug(`Message: ${message.toHexString()}`); - const inboundMessage = ChatMessage.fromBuffer(message.serialize()); - + let inboundMessage: ChatMessage; + + try { + inboundMessage = ChatMessage.fromBuffer(message.serialize()); + } catch (error) { + const err = Error(`[${connectionId}] Error deserializing message`, { + cause: error, + }); + log.error(err.message); + Sentry.captureException(err); + return { + connectionId, + messages: [], + }; + } log.debug(`Deserialized message: ${inboundMessage.toString()}`); const id = inboundMessage.messageId; diff --git a/tsconfig.base.json b/tsconfig.base.json index 3159f30da..4d6a20b0c 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -1,12 +1,16 @@ { "compilerOptions": { - "incremental": true, - "target": "ES2022", - "module": "NodeNext", "lib": [ - "ES2022" + "es2023" ], - "moduleResolution": "NodeNext", + "module": "node16", + "target": "es2022", + "moduleResolution": "node16", + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": true, + "checkJs": true, + "incremental": true, "noImplicitOverride": true, "noImplicitAny": true, "strictNullChecks": true, From 8978456f956597d9b19c4b79c61b3c133460f814 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 09:59:10 -0400 Subject: [PATCH 02/23] fix(chat): fix error deserializing message in receiveChatData function --- src/chat/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/chat/index.ts b/src/chat/index.ts index f2e317ef1..327eecaf2 100644 --- a/src/chat/index.ts +++ b/src/chat/index.ts @@ -40,7 +40,7 @@ async function receiveChatData({ try { inboundMessage = ChatMessage.fromBuffer(message.serialize()); } catch (error) { - const err = Error(`[${connectionId}] Error deserializing message`, { + const err = new Error(`[${connectionId}] Error deserializing message`, { cause: error, }); log.error(err.message); From ff70b7d25a3b4f64ab870ba3bc4d0b7509a6e161 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 09:59:42 -0400 Subject: [PATCH 03/23] feat(packets): add tests for GameMessageHeader serialization and deserialization --- .../test/GameMessageHeader.test.ts | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 packages/shared-packets/test/GameMessageHeader.test.ts diff --git a/packages/shared-packets/test/GameMessageHeader.test.ts b/packages/shared-packets/test/GameMessageHeader.test.ts new file mode 100644 index 000000000..9501f1267 --- /dev/null +++ b/packages/shared-packets/test/GameMessageHeader.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from "vitest"; +import { GameMessageHeader } from "../src/GameMessageHeader.js"; + +describe("GameMessageHeader", () => { + it("should serialize and deserialize correctly for version 0", () => { + const header = new GameMessageHeader(); + header.setId(1234); + header.setLength(5678); + header.setVersion(0); + + const buffer = header.serialize(); + expect(buffer.length).toBe(4); + expect(buffer.readUInt16BE(0)).toBe(1234); + expect(buffer.readUInt16BE(2)).toBe(5678); + + const newHeader = new GameMessageHeader(); + newHeader.setVersion(0); + newHeader.deserialize(buffer); + + expect(newHeader.getId()).toBe(1234); + expect(newHeader.getLength()).toBe(5678); + }); + + it("should serialize and deserialize correctly for version 257", () => { + const header = new GameMessageHeader(); + header.setId(1234); + header.setLength(5678); + header.setVersion(257); + + const buffer = header.serialize(); + expect(buffer.length).toBe(12); + expect(buffer.readUInt16BE(0)).toBe(1234); + expect(buffer.readUInt16BE(2)).toBe(5678); + expect(buffer.readUInt16BE(4)).toBe(257); + expect(buffer.readUInt32BE(8)).toBe(5678); + + const newHeader = new GameMessageHeader(); + newHeader.setVersion(257); + newHeader.deserialize(buffer); + + expect(newHeader.getId()).toBe(1234); + expect(newHeader.getLength()).toBe(5678); + expect(newHeader.getVersion()).toBe(257); + }); + + it("should throw error if data is too short during deserialization", () => { + const header = new GameMessageHeader(); + const buffer = Buffer.alloc(2); + + expect(() => header.deserialize(buffer)).toThrow( + "Data is too short. Expected at least 4 bytes, got 2 bytes", + ); + }); + + it("should set and get payload encryption status correctly", () => { + const header = new GameMessageHeader(); + header.setPayloadEncryption(true); + expect(header.isPayloadEncrypted()).toBe(true); + + header.setPayloadEncryption(false); + expect(header.isPayloadEncrypted()).toBe(false); + }); + + it("should convert to string correctly", () => { + const header = new GameMessageHeader(); + header.setId(1234); + header.setLength(5678); + header.setVersion(257); + + const str = header.toString(); + expect(str).toBe( + "GameMessageHeader {id: 1234, length: 5678, version: 257}", + ); + }); + + it("should copy correctly", () => { + const header = new GameMessageHeader(); + header.setId(1234); + header.setLength(5678); + header.setVersion(257); + + const copiedHeader = GameMessageHeader.copy(header); + expect(copiedHeader.getId()).toBe(1234); + expect(copiedHeader.getLength()).toBe(5678); + expect(copiedHeader.getVersion()).toBe(257); + }); +}); From 53bf529ef4851079ceb0cdd0e5786dc9ec3938b3 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 10:00:57 -0400 Subject: [PATCH 04/23] refactor(tests): remove redundant test case in socketUtility.test.ts --- packages/gateway/test/socketUtility.test.ts | 29 ++++++++------------- 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/packages/gateway/test/socketUtility.test.ts b/packages/gateway/test/socketUtility.test.ts index df3eaff6b..252f1ff0d 100644 --- a/packages/gateway/test/socketUtility.test.ts +++ b/packages/gateway/test/socketUtility.test.ts @@ -48,21 +48,6 @@ describe("tagSocketWithId", () => { expect(result).toHaveProperty("socket"); expect(result).toHaveProperty("connectionStamp"); }); - - it("returns an object with the correct values", () => { - // arrange - const mockSocket = {} as Socket; - const connectionStamp = Date.now(); - const id = "12345"; - - // act - const result = tagSocketWithId(mockSocket, connectionStamp, id); - - // assert - expect(result.id).toBe(id); - expect(result.socket).toBe(mockSocket); - expect(result.connectionStamp).toBe(connectionStamp); - }); }); describe("trySocketWrite", () => { @@ -78,8 +63,13 @@ describe("tagSocketWithId", () => { const data = "test data"; // act & assert - await expect(trySocketWrite(mockTaggedSocket, data)).resolves.toBeUndefined(); - expect(mockTaggedSocket.socket.write).toHaveBeenCalledWith(data, expect.any(Function)); + await expect( + trySocketWrite(mockTaggedSocket, data), + ).resolves.toBeUndefined(); + expect(mockTaggedSocket.socket.write).toHaveBeenCalledWith( + data, + expect.any(Function), + ); }); it("rejects when an error occurs during write", async () => { @@ -97,7 +87,10 @@ describe("tagSocketWithId", () => { await expect(trySocketWrite(mockTaggedSocket, data)).rejects.toThrow( "Write error", ); - expect(mockTaggedSocket.socket.write).toHaveBeenCalledWith(data, expect.any(Function)); + expect(mockTaggedSocket.socket.write).toHaveBeenCalledWith( + data, + expect.any(Function), + ); }); }); }); From 77d691a4841c5b9321017b7557e6622692537d3a Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 10:04:04 -0400 Subject: [PATCH 05/23] refactor(types): remove duplicated toString and toHexString methods from SerializableMessage --- packages/shared-packets/src/types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/shared-packets/src/types.ts b/packages/shared-packets/src/types.ts index 2167d56d2..eeea1b6ab 100644 --- a/packages/shared-packets/src/types.ts +++ b/packages/shared-packets/src/types.ts @@ -18,9 +18,6 @@ export interface SerializableMessage extends SerializableInterface { getDataBuffer(): Buffer; setDataBuffer(data: Buffer): void; - - toString(): string; - toHexString(): string; } export enum MessageSources { From e5f913b588f4e32c91f18627feec2f7950ec6152 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 10:06:51 -0400 Subject: [PATCH 06/23] refactor(packets): update ServerPacket constructor usage --- packages/mcots/src/messageProcessors/processServerLogin.ts | 3 ++- packages/mcots/src/messageProcessors/processStockCarInfo.ts | 3 ++- packages/mcots/src/messageProcessors/sendSuccess.ts | 3 ++- packages/transactions/src/internal.ts | 4 ++-- packages/transactions/src/login.ts | 2 +- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/mcots/src/messageProcessors/processServerLogin.ts b/packages/mcots/src/messageProcessors/processServerLogin.ts index 2b5df0633..b2c6ca7e7 100644 --- a/packages/mcots/src/messageProcessors/processServerLogin.ts +++ b/packages/mcots/src/messageProcessors/processServerLogin.ts @@ -27,7 +27,8 @@ export async function processServerLogin( log.debug(`Sending LoginCompleteMessage: ${response.toString()}`); // Send response packet - const responsePacket = new ServerPacket(response.getMessageId()); + const responsePacket = new ServerPacket(); + responsePacket.setMessageId(response.getMessageId()); responsePacket.setDataBuffer(response.serialize()); responsePacket.setSequence(message.sequence); diff --git a/packages/mcots/src/messageProcessors/processStockCarInfo.ts b/packages/mcots/src/messageProcessors/processStockCarInfo.ts index 114b377fa..454ae7be7 100644 --- a/packages/mcots/src/messageProcessors/processStockCarInfo.ts +++ b/packages/mcots/src/messageProcessors/processStockCarInfo.ts @@ -51,7 +51,8 @@ export async function processStockCarInfo( responsePacket.setDealerId(lotOwnerId); responsePacket.setBrandId(brandId); - const response = new ServerPacket(141); + const response = new ServerPacket(); + response.setMessageId(141); if (inventoryCars.inventory.length > StockCarInfo.MAX_CARS_PER_MESSAGE) { log.error( diff --git a/packages/mcots/src/messageProcessors/sendSuccess.ts b/packages/mcots/src/messageProcessors/sendSuccess.ts index 16df99596..ae27e7acc 100644 --- a/packages/mcots/src/messageProcessors/sendSuccess.ts +++ b/packages/mcots/src/messageProcessors/sendSuccess.ts @@ -9,7 +9,8 @@ export function sendSuccess( pReply.setMessageId(101); pReply.msgReply = 438; - const response = new ServerPacket(101); + const response = new ServerPacket(); + response.setMessageId(101); response.setDataBuffer(pReply.serialize()); response.setSequence(message.sequence); diff --git a/packages/transactions/src/internal.ts b/packages/transactions/src/internal.ts index b8b4f44ef..29d0b8406 100644 --- a/packages/transactions/src/internal.ts +++ b/packages/transactions/src/internal.ts @@ -116,7 +116,7 @@ export async function receiveTransactionsData({ // Normalize the message - const inboundMessage = new ServerPacket(0); + const inboundMessage = new ServerPacket(); inboundMessage.deserialize(message.serialize()); log.debug( @@ -168,7 +168,7 @@ export async function receiveTransactionsData({ response.messages.forEach((message) => { log.debug(`[${connectionId}] Processing outbound message`); - const outboundMessage = new ServerPacket(0); + const outboundMessage = new ServerPacket(); outboundMessage.deserialize(message.serialize()); if (outboundMessage.isPayloadEncrypted()) { diff --git a/packages/transactions/src/login.ts b/packages/transactions/src/login.ts index f3b091858..30e3927c6 100644 --- a/packages/transactions/src/login.ts +++ b/packages/transactions/src/login.ts @@ -17,7 +17,7 @@ export async function login({ log, }: MessageHandlerArgs): Promise { // Normalize the packet - const incomingPacket = new ServerPacket(0); + const incomingPacket = new ServerPacket(); incomingPacket.deserialize(packet.serialize()); log.debug( From 5f610422b56cd2e3275bd64aa8552da0544784ac Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 10:08:44 -0400 Subject: [PATCH 07/23] refactor(types): remove duplicated import in SerializedBuffer.ts --- packages/shared/src/SerializedBuffer.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/shared/src/SerializedBuffer.ts b/packages/shared/src/SerializedBuffer.ts index 2e38a8326..e5efffd5c 100644 --- a/packages/shared/src/SerializedBuffer.ts +++ b/packages/shared/src/SerializedBuffer.ts @@ -1,4 +1,5 @@ -import { BaseSerialized, type Serializable } from "./BaseSerialized.js"; +import { BaseSerialized, } from "./BaseSerialized.js"; +import type { Serializable } from "./BaseSerialized.js"; /** * A serialized buffer, prefixed with its 2-byte length. From e36215ce78f1a0d7b5daf19cac71daa8a4bdb195 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 10:10:57 -0400 Subject: [PATCH 08/23] refactor(portRouters): add logging to notFoundRouter --- packages/gateway/src/portRouters.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/gateway/src/portRouters.ts b/packages/gateway/src/portRouters.ts index 78dd23b2f..b23bdeb18 100644 --- a/packages/gateway/src/portRouters.ts +++ b/packages/gateway/src/portRouters.ts @@ -1,4 +1,4 @@ -import { type ServerLogger } from "rusty-motors-shared"; +import { getServerLogger, type ServerLogger } from "rusty-motors-shared"; import type { TaggedSocket } from "./socketUtility.js"; type PortRouter = (portRouterArgs: { taggedSocket: TaggedSocket; @@ -33,14 +33,18 @@ export function addPortRouter(port: number, router: PortRouter) { async function notFoundRouter({ taggedSocket, + log = getServerLogger({ + name: "gatewayServer.notFoundRouter", + }), }: { taggedSocket: TaggedSocket; + log?: ServerLogger; }) { taggedSocket.socket.on("error", (error) => { console.error(`[${taggedSocket.id}] Socket error: ${error}`); }); taggedSocket.socket.end(); - throw new Error(`No router found for port ${taggedSocket.socket.localPort}`); + log.error(`[${taggedSocket.id}] No router found for port ${taggedSocket.socket.localPort}`); } /** * Retrieves the router function associated with a given port. From deea9b4fda732fface9810343647a7019b7ab2d7 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 10:12:27 -0400 Subject: [PATCH 09/23] refactor(portRouters): add error logging for notFoundRouter --- packages/gateway/src/portRouters.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/gateway/src/portRouters.ts b/packages/gateway/src/portRouters.ts index b23bdeb18..d42dc6735 100644 --- a/packages/gateway/src/portRouters.ts +++ b/packages/gateway/src/portRouters.ts @@ -44,7 +44,9 @@ async function notFoundRouter({ console.error(`[${taggedSocket.id}] Socket error: ${error}`); }); taggedSocket.socket.end(); - log.error(`[${taggedSocket.id}] No router found for port ${taggedSocket.socket.localPort}`); + log.error( + `[${taggedSocket.id}] No router found for port ${taggedSocket.socket.localPort}`, + ); } /** * Retrieves the router function associated with a given port. @@ -55,6 +57,9 @@ async function notFoundRouter({ */ export function getPortRouter(port: number): PortRouter { + if (!Number.isInteger(port) || port < 0 || port > 65535) { + throw new Error(`Invalid port number: ${port}`); + } const router = portRouters.get(port); if (typeof router === "undefined") { return notFoundRouter; From c38cc6fb46297ec6526d0f967d9b38da5c5582f0 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 10:13:50 -0400 Subject: [PATCH 10/23] refactor(mcotsPortRouter): replace console.log with log.debug for routing message --- packages/gateway/src/mcotsPortRouter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/gateway/src/mcotsPortRouter.ts b/packages/gateway/src/mcotsPortRouter.ts index 70cf93521..48fe0ebbb 100644 --- a/packages/gateway/src/mcotsPortRouter.ts +++ b/packages/gateway/src/mcotsPortRouter.ts @@ -74,7 +74,7 @@ async function routeInitialMessage( // Route the initial message to the appropriate handler // Messages may be encrypted, this will be handled by the handler - console.log( + log.debug( `Routing message for port ${port}: ${initialPacket.toHexString()}`, ); let responses: SerializableInterface[] = []; From 7792fb6aecd6affe398a29e70ad5f0a35f012dab Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 19:56:40 -0400 Subject: [PATCH 11/23] test: :bug: correct payload and header serialization and update tests --- .../shared-packets/src/GameMessageHeader.ts | 9 +- .../shared-packets/src/GameMessagePayload.ts | 25 +----- packages/shared-packets/src/GamePacket.ts | 7 +- .../shared-packets/test/GamePacket.test.ts | 83 +++++++++++++++++++ 4 files changed, 96 insertions(+), 28 deletions(-) create mode 100644 packages/shared-packets/test/GamePacket.test.ts diff --git a/packages/shared-packets/src/GameMessageHeader.ts b/packages/shared-packets/src/GameMessageHeader.ts index 002fdca71..09095023e 100644 --- a/packages/shared-packets/src/GameMessageHeader.ts +++ b/packages/shared-packets/src/GameMessageHeader.ts @@ -73,7 +73,7 @@ export class GameMessageHeader } override serialize(): Buffer { - return this.version === 0 ? this.serializeV0() : this.serializeV1(); + return this.version === 257 ? this.serializeV1() : this.serializeV0(); } private deserializeV0(data: Buffer): void { @@ -96,10 +96,10 @@ export class GameMessageHeader ); } - if (this.version === 0) { - this.deserializeV0(data); - } else { + if (this.version === 257) { this.deserializeV1(data); + } else { + this.deserializeV0(data); } } @@ -124,5 +124,4 @@ export class GameMessageHeader override toString(): string { return `GameMessageHeader {id: ${this.id}, length: ${this.length}, version: ${this.version}}`; } - } diff --git a/packages/shared-packets/src/GameMessagePayload.ts b/packages/shared-packets/src/GameMessagePayload.ts index 2e5654f9b..baffa4301 100644 --- a/packages/shared-packets/src/GameMessagePayload.ts +++ b/packages/shared-packets/src/GameMessagePayload.ts @@ -5,44 +5,25 @@ export class GameMessagePayload extends BufferSerializer implements SerializableInterface { - public messageId: number = 0; // 2 bytes - private isEncrypted: boolean = false; // Not serialized static copy(payload: GameMessagePayload): GameMessagePayload { const newPayload = new GameMessagePayload(); - newPayload.messageId = payload.messageId; newPayload._data = Buffer.from(payload._data); return newPayload; } override getByteSize(): number { - return 2 + this._data.length; + return this._data.length; } override serialize(): Buffer { - const buffer = Buffer.alloc(this.getByteSize()); - buffer.writeUInt16LE(this.messageId, 0); - this._data.copy(buffer, 2); - - return buffer; + return this._data; } override deserialize(data: Buffer): GameMessagePayload { - this._assertEnoughData(data, 2); - - this.messageId = data.readUInt16LE(0); - this._data = data.subarray(2); - - return this; - } - - getMessageId(): number { - return this.messageId; - } + this._data = data; - setMessageId(messageId: number): GameMessagePayload { - this.messageId = messageId; return this; } diff --git a/packages/shared-packets/src/GamePacket.ts b/packages/shared-packets/src/GamePacket.ts index 1b2e9b1d8..62c99fda1 100644 --- a/packages/shared-packets/src/GamePacket.ts +++ b/packages/shared-packets/src/GamePacket.ts @@ -36,6 +36,11 @@ export class GamePacket extends BasePacket implements SerializableMessage { override getDataBuffer(): Buffer { return this.data.serialize(); } + + getVersion(): number { + return this.header.getVersion(); + } + override setDataBuffer(data: Buffer): GamePacket { if (this.data.getByteSize() > 2) { throw new Error( @@ -63,7 +68,7 @@ export class GamePacket extends BasePacket implements SerializableMessage { } getMessageId(): number { - return this.data.getMessageId(); + return this.header.getId(); } getLength(): number { diff --git a/packages/shared-packets/test/GamePacket.test.ts b/packages/shared-packets/test/GamePacket.test.ts new file mode 100644 index 000000000..44f40cad6 --- /dev/null +++ b/packages/shared-packets/test/GamePacket.test.ts @@ -0,0 +1,83 @@ +import { describe, it, expect } from "vitest"; +import { Buffer } from "buffer"; +import { GamePacket } from "../src/GamePacket.js"; + +describe("GamePacket", () => { + it("should deserialize correctly v0 correctly", () => { + const buffer = Buffer.alloc(11); + buffer.writeUInt16BE(1234, 0); // Message ID + buffer.writeUInt16BE(11, 2); // Length + + buffer.write("test da", 4); // Data + + const packet = new GamePacket(); + packet.deserialize(buffer); + + expect(packet.getMessageId()).toBe(1234); + expect(packet.getDataBuffer().toString("hex")).equals( + Buffer.from("test da").toString("hex"), + ); + }); + + it("should deserialize correctly v1 correctly", () => { + const buffer = Buffer.alloc(26); + buffer.writeUInt16BE(1234, 0); // Message ID + buffer.writeUInt16BE(11, 2); // Length + buffer.writeUInt16BE(0x101, 4); // Version + buffer.writeUInt32BE(11, 8); // Checksum + buffer.write("test data", 12); // Data + + const packet = new GamePacket(); + packet.deserialize(buffer); + + expect(packet.getMessageId()).toBe(1234); + expect(packet.getDataBuffer().toString("hex")).equals( + Buffer.from("test data\u0000\u0000\u0000\u0000\u0000").toString("hex"), + ); + }); + + it("should throw error if data is insufficient for header", () => { + const buffer = Buffer.alloc(5); // Less than required for header + + const packet = new GamePacket(); + expect(() => packet.deserialize(buffer)).toThrow( + "Data is too short. Expected at least 6 bytes, got 5 bytes", + ); + }); + + it("should throw error if data is insufficient for full packet", () => { + const buffer = Buffer.alloc(10); // Less than required for full packet + buffer.writeUInt16BE(0x101, 4); // Version + + const packet = new GamePacket(); + expect(() => packet.deserialize(buffer)).toThrow( + "Data is too short. Expected at least 12 bytes, got 10 bytes", + ); + }); + + it("should identify version correctly", () => { + const buffer = Buffer.alloc(15); + buffer.writeUInt16BE(11, 0); // Length + buffer.writeUInt16BE(0x101, 4); // Version + buffer.writeUInt16BE(1234, 6); // Message ID + buffer.write("test data", 8, "utf8"); // Data + + const packet = new GamePacket(); + packet.deserialize(buffer); + + expect(packet.getVersion()).toBe(257); + }); + + it("should handle version 0 correctly", () => { + const buffer = Buffer.alloc(15); + buffer.writeUInt16BE(1234, 0); // Message ID + buffer.writeUInt16BE(11, 4); // Length + buffer.writeUInt16BE(0x100, 4); // Version + buffer.write("test data", 8, "utf8"); // Data + + const packet = new GamePacket(); + packet.deserialize(buffer); + + expect(packet.getVersion()).toBe(0); + }); +}); From 775370a15e24f84140d1249fef798cf315778afe Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 20:01:15 -0400 Subject: [PATCH 12/23] refactor(gateway): move tests to a seperate tsconfig file --- packages/gateway/tsconfig.json | 2 +- packages/gateway/tsconfig.test.json | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 packages/gateway/tsconfig.test.json diff --git a/packages/gateway/tsconfig.json b/packages/gateway/tsconfig.json index a141ce182..a23e9e608 100644 --- a/packages/gateway/tsconfig.json +++ b/packages/gateway/tsconfig.json @@ -4,5 +4,5 @@ "incremental": true, "composite": true }, - "include": ["index.ts", "src", "test/socketUtility.test.ts"] + "include": ["index.ts", "src"] } diff --git a/packages/gateway/tsconfig.test.json b/packages/gateway/tsconfig.test.json new file mode 100644 index 000000000..ec7d9532e --- /dev/null +++ b/packages/gateway/tsconfig.test.json @@ -0,0 +1,6 @@ +{ + "extends": "./tsconfig.json", + "include": [ + "test/**/*.test.ts" + ], +} \ No newline at end of file From 2b04e31ea9b2c69501aa02c092ce031265c7a20e Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 20:07:11 -0400 Subject: [PATCH 13/23] refactor(tests): add ServerMessagePayload copy test --- .../test/ServerMessagePayload.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 packages/shared-packets/test/ServerMessagePayload.test.ts diff --git a/packages/shared-packets/test/ServerMessagePayload.test.ts b/packages/shared-packets/test/ServerMessagePayload.test.ts new file mode 100644 index 000000000..d1c01b381 --- /dev/null +++ b/packages/shared-packets/test/ServerMessagePayload.test.ts @@ -0,0 +1,18 @@ +import { describe, it, expect } from "vitest"; +import { Buffer } from "buffer"; +import { ServerMessagePayload } from "../src/ServerMessagePayload.js"; + +describe("ServerMessagePayload", () => { + it("should copy correctly", () => { + const originalPayload = new ServerMessagePayload(); + originalPayload.setMessageId(1234); + originalPayload["_data"] = Buffer.from("test data"); + + const copiedPayload = ServerMessagePayload.copy(originalPayload); + + expect(copiedPayload.getMessageId()).toBe(1234); + expect(copiedPayload["_data"].toString("utf8")).toBe("test data"); + expect(copiedPayload).not.toBe(originalPayload); + expect(copiedPayload["_data"]).not.toBe(originalPayload["_data"]); + }); +}); From 12e9715c80d224872523c0d84469763f99b052c2 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Thu, 10 Oct 2024 20:09:18 -0400 Subject: [PATCH 14/23] refactor(tests): add GameMessagePayload copy test --- .../test/GameMessagePayload.test.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 packages/shared-packets/test/GameMessagePayload.test.ts diff --git a/packages/shared-packets/test/GameMessagePayload.test.ts b/packages/shared-packets/test/GameMessagePayload.test.ts new file mode 100644 index 000000000..39ccb7246 --- /dev/null +++ b/packages/shared-packets/test/GameMessagePayload.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from "vitest"; +import { GameMessagePayload } from "../src/GameMessagePayload.js"; + +describe("GameMessagePayload", () => { + it("should create a copy of the payload", () => { + const originalPayload = new GameMessagePayload(); + originalPayload.deserialize(Buffer.from("test data")); + + const copiedPayload = GameMessagePayload.copy(originalPayload); + + expect(copiedPayload).not.toBe(originalPayload); + expect(copiedPayload.serialize()).toEqual(originalPayload.serialize()); + }); + + it("should return the correct byte size", () => { + const payload = new GameMessagePayload(); + payload.deserialize(Buffer.from("test data")); + + expect(payload.getByteSize()).toBe(9); + }); + + it("should serialize the payload correctly", () => { + const payload = new GameMessagePayload(); + const buffer = Buffer.from("test data"); + payload.deserialize(buffer); + + expect(payload.serialize()).toEqual(buffer); + }); + + it("should deserialize the payload correctly", () => { + const payload = new GameMessagePayload(); + const buffer = Buffer.from("test data"); + payload.deserialize(buffer); + + expect(payload.serialize()).toEqual(buffer); + }); + + it("should correctly indicate if the payload is encrypted", () => { + const payload = new GameMessagePayload(); + + expect(payload.isPayloadEncrypted()).toBe(false); + + payload.setPayloadEncryption(true); + expect(payload.isPayloadEncrypted()).toBe(true); + }); + + it("should set the payload encryption correctly", () => { + const payload = new GameMessagePayload(); + + payload.setPayloadEncryption(true); + expect(payload.isPayloadEncrypted()).toBe(true); + + payload.setPayloadEncryption(false); + expect(payload.isPayloadEncrypted()).toBe(false); + }); +}); From dd26e11b69262c48a0a52770de693831854bceb7 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Fri, 11 Oct 2024 07:49:46 -0400 Subject: [PATCH 15/23] refactor(gateway): update tsconfig include patterns --- .github/workflows/node.yml | 6 - .../core/src/serializationHelpers.test.ts | 335 ------------------ packages/core/src/serializationHelpers.ts | 187 ---------- packages/gateway/index.ts | 1 - packages/gateway/src/encryption.ts | 61 ++-- packages/gateway/tsconfig.json | 2 +- packages/sessions/index.ts | 8 - packages/sessions/package.json | 29 -- packages/sessions/src/index.ts | 233 ------------ packages/sessions/test/index.test.ts | 128 ------- packages/sessions/tsconfig.json | 8 - .../shared-packets/src/GameMessageHeader.ts | 7 +- .../shared-packets/src/GameMessagePayload.ts | 2 +- packages/shared-packets/src/GamePacket.ts | 4 +- .../shared-packets/src/ServerMessageHeader.ts | 6 +- .../src/ServerMessagePayload.ts | 6 +- packages/shared-packets/src/ServerPacket.ts | 2 +- .../shared-packets/test/GamePacket.test.ts | 34 +- .../{src => test}/ServerPacket.test.ts | 8 +- packages/shared-packets/tsconfig.json | 2 +- packages/shared/index.ts | 8 +- .../shared/src/verifyLegacyCipherSupport.ts | 15 + server.ts | 2 +- test/factoryMocks.ts | 5 +- 24 files changed, 109 insertions(+), 990 deletions(-) delete mode 100644 packages/core/src/serializationHelpers.test.ts delete mode 100644 packages/core/src/serializationHelpers.ts delete mode 100644 packages/sessions/index.ts delete mode 100644 packages/sessions/package.json delete mode 100644 packages/sessions/src/index.ts delete mode 100644 packages/sessions/test/index.test.ts delete mode 100644 packages/sessions/tsconfig.json rename packages/shared-packets/{src => test}/ServerPacket.test.ts (93%) create mode 100644 packages/shared/src/verifyLegacyCipherSupport.ts diff --git a/.github/workflows/node.yml b/.github/workflows/node.yml index 4a194fd59..cafde7f64 100644 --- a/.github/workflows/node.yml +++ b/.github/workflows/node.yml @@ -108,12 +108,6 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} run: | codecovcli --verbose do-upload --fail-on-error --flag persona --name persona --dir packages/persona - - name: Codecov upload sessions coverage - if: ${{ always() }} # using always() to always run this step because i am uploading test results and coverage in one step - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - run: | - codecovcli --verbose do-upload --fail-on-error --flag sessions --name sessions --dir packages/sessions - name: Codecov upload shard coverage if: ${{ always() }} # using always() to always run this step because i am uploading test results and coverage in one step env: diff --git a/packages/core/src/serializationHelpers.test.ts b/packages/core/src/serializationHelpers.test.ts deleted file mode 100644 index 2fe247f86..000000000 --- a/packages/core/src/serializationHelpers.test.ts +++ /dev/null @@ -1,335 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - clamp16, - clamp32, - deserializeBool, - deserializeByte, - deserializeDWord, - deserializeFloat, - deserializeString, - deserializeWord, - serializeBool, - serializeByte, - serializeDWord, - serializeFloat, - serializeString, - serializeWord, - sizeOfBool, - sizeOfByte, - sizeOfDWord, - sizeOfFloat, - sizeOfString, - sizeOfWord, -} from "./serializationHelpers.js"; - -describe("serializationHelpers", () => { - describe("serializeBool()", () => { - it("should serialize a boolean value", () => { - // Arrange - const input = true; - const expected = Buffer.from([1]); - - // Act - const actual = serializeBool(input); - - // Assert - expect(actual).toEqual(expected); - }); - - it("should serialize a boolean value", () => { - // Arrange - const input = false; - const expected = Buffer.from([0]); - - // Act - const actual = serializeBool(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("deserializeBool()", () => { - it("should deserialize a boolean value", () => { - // Arrange - const input = Buffer.from([1]); - const expected = true; - - // Act - const actual = deserializeBool(input); - - // Assert - expect(actual).toEqual(expected); - }); - - it("should deserialize a boolean value", () => { - // Arrange - const input = Buffer.from([0]); - const expected = false; - - // Act - const actual = deserializeBool(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("sizeOfBool()", () => { - it("should return the size of a boolean value", () => { - // Arrange - const expected = 1; - - // Act - const actual = sizeOfBool(); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("serializeByte()", () => { - it("should serialize a byte value", () => { - // Arrange - const input = 1; - const expected = Buffer.from([1]); - - // Act - const actual = serializeByte(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("deserializeByte()", () => { - it("should deserialize a byte value", () => { - // Arrange - const input = Buffer.from([1]); - const expected = 1; - - // Act - const actual = deserializeByte(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("sizeOfByte()", () => { - it("should return the size of a byte value", () => { - // Arrange - const expected = 1; - - // Act - const actual = sizeOfByte(); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("serializeWord()", () => { - it("should serialize a word value", () => { - // Arrange - const input = 1; - const expected = Buffer.from([0, 1]); - - // Act - const actual = serializeWord(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("deserializeWord()", () => { - it("should deserialize a word value", () => { - // Arrange - const input = Buffer.from([0, 1]); - const expected = 1; - - // Act - const actual = deserializeWord(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("sizeOfWord()", () => { - it("should return the size of a word value", () => { - // Arrange - const expected = 2; - - // Act - const actual = sizeOfWord(); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("serializeDWord()", () => { - it("should serialize a dword value", () => { - // Arrange - const input = 1; - const expected = Buffer.from([0, 0, 0, 1]); - - // Act - const actual = serializeDWord(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("deserializeDWord()", () => { - it("should deserialize a dword value", () => { - // Arrange - const input = Buffer.from([0, 0, 0, 1]); - const expected = 1; - - // Act - const actual = deserializeDWord(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("sizeOfDWord()", () => { - it("should return the size of a dword value", () => { - // Arrange - const expected = 4; - - // Act - const actual = sizeOfDWord(); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("serializeFloat()", () => { - it("should serialize a float value", () => { - // Arrange - const input = 1; - const expected = Buffer.from([63, 128, 0, 0]); - - // Act - const actual = serializeFloat(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("deserializeFloat()", () => { - it("should deserialize a float value", () => { - // Arrange - const input = Buffer.from([63, 128, 0, 0]); - const expected = 1; - - // Act - const actual = deserializeFloat(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("sizeOfFloat()", () => { - it("should return the size of a float value", () => { - // Arrange - const expected = 4; - - // Act - const actual = sizeOfFloat(); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("serializeString()", () => { - it("should serialize a string value", () => { - // Arrange - const input = "test"; - const expected = Buffer.from([0, 4, 116, 101, 115, 116]); - - // Act - const actual = serializeString(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); - - describe("deserializeString()", () => { - it("should deserialize a string value", () => { - // Arrange - const input = Buffer.from([0, 4, 116, 101, 115, 116]); - const expected = "test"; - - // Act - const actual = deserializeString(input); - - // Assert - expect(actual).toEqual(expected); - }); - - it("should throw an error if the size is bigger than the buffer length - 2", () => { - // Arrange - const input = Buffer.from([0, 5, 116, 101, 115, 116]); - - // Act - const actual = () => deserializeString(input); - - // Assert - expect(actual).toThrowError("Size is bigger than the buffer length - 2"); - }); - }); - - describe("sizeOfString()", () => { - it("should return the size of a string value", () => { - // Arrange - const input = "test"; - const expected = 6; - - // Act - const actual = sizeOfString(input); - - // Assert - expect(actual).toEqual(expected); - }); - }); -}); - -describe("clamp16()", () => { - it("should clamp a value between 0 and 65535", () => { - // Arrange - const input = 65536; - const expected = 65535; - - // Act - const actual = clamp16(input); - - // Assert - expect(actual).toEqual(expected); - }); -}); - -describe("clamp32()", () => { - it("should clamp a value between 0 and 4294967295", () => { - // Arrange - const input = 4294967296; - const expected = 4294967295; - - // Act - const actual = clamp32(input); - - // Assert - expect(actual).toEqual(expected); - }); -}); diff --git a/packages/core/src/serializationHelpers.ts b/packages/core/src/serializationHelpers.ts deleted file mode 100644 index 9e4185cf7..000000000 --- a/packages/core/src/serializationHelpers.ts +++ /dev/null @@ -1,187 +0,0 @@ -import { ServerError } from "rusty-motors-shared"; - -/** - * Clamp a value between 0 and 255 - * @param {number} value - * @returns {number} - */ -export function clamp16(value: number): number { - return Math.max(0, Math.min(65535, value)); -} - -/** - * Clamp a value between 0 and 65535 - * @param {number} value - * @returns {number} - */ -export function clamp32(value: number): number { - return Math.max(0, Math.min(4294967295, value)); -} - -/** - * Serializes a boolean to a buffer. - * @param {boolean} bool - * @returns {Buffer} - */ -export function serializeBool(bool: boolean): Buffer { - const buf = Buffer.alloc(1); - - buf.writeUInt8(bool ? 1 : 0); - - return buf; -} - -/** - * Serializes a byte to a buffer. - * @param {number} byte - * @returns {Buffer} - */ -export function serializeByte(byte: number): Buffer { - const buf = Buffer.alloc(1); - - buf.writeUInt8(byte); - - return buf; -} - -/** - * Serializes a word to a buffer. - * @param {number} word - * @returns {Buffer} - */ -export function serializeWord(word: number): Buffer { - const buf = Buffer.alloc(2); - - buf.writeUInt16BE(word); - - return buf; -} - -/** - * Serializes a dword to a buffer. - * @param {number} dword - * @returns {Buffer} - */ -export function serializeDWord(dword: number): Buffer { - const buf = Buffer.alloc(4); - - buf.writeUInt32BE(dword); - - return buf; -} - -/** - * Serializes a float to a buffer. - * @param {number} f - * @returns {Buffer} - */ -export function serializeFloat(f: number): Buffer { - const buf = Buffer.alloc(4); - - buf.writeFloatBE(f); - - return buf; -} - -/** - * Serializes a string to a buffer. The buffer will be prefixed with the length of the string. - * @param {string} str - * @returns {Buffer} - */ -export function serializeString(str: string): Buffer { - const buf = Buffer.alloc(str.length + 2); - - buf.writeUInt16BE(str.length); - buf.write(str, 2); - - return buf; -} - -/** - * Deserializes a boolean from a buffer. - * @param {Buffer} buff - * @returns {boolean} - */ -export function deserializeBool(buff: Buffer): boolean { - return buff.readUInt8() === 1; -} - -/** - * Deserializes a byte from a buffer. - * @param {Buffer} buff - * @returns {number} - */ -export function deserializeByte(buff: Buffer): number { - return buff.readUInt8(); -} - -/** - * Deserializes a word from a buffer. - * @param {Buffer} buff - * @returns {number} - */ -export function deserializeWord(buff: Buffer): number { - return buff.readUInt16BE(); -} - -/** - * Deserializes a dword from a buffer. - * @param {Buffer} buff - * @returns {number} - */ -export function deserializeDWord(buff: Buffer): number { - return buff.readUInt32BE(); -} - -/** - * Deserializes a float from a buffer. - * @param {Buffer} buff - * @returns {number} - */ -export function deserializeFloat(buff: Buffer): number { - return buff.readFloatBE(); -} - -/** - * Deserializes a string from a buffer. The buffer is expected to be prefixed with the length of the string. - * @param {Buffer} buf - * @returns {string} - */ -export function deserializeString(buf: Buffer): string { - const size = buf.readUInt16BE(); - if (size > buf.length - 2) { - throw new ServerError("Size is bigger than the buffer length - 2"); - } - const str = buf.subarray(2, size + 2).toString("utf8"); - - return str; -} - -export function sizeOfBool() { - return 1; -} - -export function sizeOfByte() { - return 1; -} - -export function sizeOfWord() { - return 2; -} - -export function sizeOfDWord() { - return 4; -} - -export function sizeOfFloat() { - return 4; -} - -/** - * Returns the size of a string, including the length prefix. - * @param {string} string - * @returns {number} - */ -export function sizeOfString(string: string): number { - return string.length + 2; -} diff --git a/packages/gateway/index.ts b/packages/gateway/index.ts index 60b379531..34622ec62 100644 --- a/packages/gateway/index.ts +++ b/packages/gateway/index.ts @@ -2,5 +2,4 @@ export { getGatewayServer, Gateway } from "./src/GatewayServer.js"; export { createCommandEncryptionPair, createDataEncryptionPair, - verifyLegacyCipherSupport, } from "./src/encryption.js"; diff --git a/packages/gateway/src/encryption.ts b/packages/gateway/src/encryption.ts index f68a5fc8c..6ff9af740 100644 --- a/packages/gateway/src/encryption.ts +++ b/packages/gateway/src/encryption.ts @@ -14,36 +14,50 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -import { createCipheriv, createDecipheriv, getCiphers } from "node:crypto"; -import { McosEncryptionPair } from "rusty-motors-shared"; +import { createCipheriv, createDecipheriv } from "node:crypto"; +import { + McosEncryptionPair, + verifyLegacyCipherSupport, +} from "rusty-motors-shared"; /** * This function creates a new encryption pair for use with the game server * * @param {string} key The key to use for encryption * @returns {McosEncryptionPair} The encryption pair + * @throws Error if the key is too short + * @throws Error if the server does not support the legacy ciphers */ export function createCommandEncryptionPair(key: string): McosEncryptionPair { - if (key.length < 16) { - throw Error("Key too short"); - } + try { + verifyLegacyCipherSupport(); + + if (key.length < 16) { + throw Error("Key too short"); + } - const sKey = key.slice(0, 16); + const sKey = key.slice(0, 16); - // Deepcode ignore HardcodedSecret: This uses an empty IV - const desIV = Buffer.alloc(8); + // Deepcode ignore HardcodedSecret: This uses an empty IV + const desIV = Buffer.alloc(8); - const gsCipher = createCipheriv("des-cbc", Buffer.from(sKey, "hex"), desIV); - gsCipher.setAutoPadding(false); + const gsCipher = createCipheriv("des-cbc", Buffer.from(sKey, "hex"), desIV); + gsCipher.setAutoPadding(false); - const gsDecipher = createDecipheriv( - "des-cbc", - Buffer.from(sKey, "hex"), - desIV, - ); - gsDecipher.setAutoPadding(false); + const gsDecipher = createDecipheriv( + "des-cbc", + Buffer.from(sKey, "hex"), + desIV, + ); + gsDecipher.setAutoPadding(false); - return new McosEncryptionPair(gsCipher, gsDecipher); + return new McosEncryptionPair(gsCipher, gsDecipher); + } catch (error) { + const err = new Error(`Error creating command encryption pair: ${error}`, { + cause: error, + }); + throw err; + } } /** @@ -66,16 +80,3 @@ export function createDataEncryptionPair(key: string): McosEncryptionPair { return new McosEncryptionPair(tsCipher, tsDecipher); } - -/** - * This function checks if the server supports the legacy ciphers - * - * @returns void - * @throws Error if the server does not support the legacy ciphers - */ -export function verifyLegacyCipherSupport() { - const cipherList = getCiphers(); - if (!cipherList.includes("des-cbc") || !cipherList.includes("rc4")) { - throw Error("Legacy ciphers not available"); - } -} diff --git a/packages/gateway/tsconfig.json b/packages/gateway/tsconfig.json index a23e9e608..789270755 100644 --- a/packages/gateway/tsconfig.json +++ b/packages/gateway/tsconfig.json @@ -4,5 +4,5 @@ "incremental": true, "composite": true }, - "include": ["index.ts", "src"] + "include": ["index.ts", "src/**/*.ts"], } diff --git a/packages/sessions/index.ts b/packages/sessions/index.ts deleted file mode 100644 index 6cd038bec..000000000 --- a/packages/sessions/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { - saveClientConnection, - findClientByCustomerId, - hasClientEncryptionPair, - newClientConnection, - setClientEncryption, - clearConnectedClients, -} from "./src/index.js"; diff --git a/packages/sessions/package.json b/packages/sessions/package.json deleted file mode 100644 index f3a83908f..000000000 --- a/packages/sessions/package.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "name": "rusty-motors-sessions", - "version": "1.0.0", - "exports": { - ".": { - "import": "./index.js", - "require": "./index.js" - } - }, - "type": "module", - "scripts": { - "check": "tsc", - "lint": "npx @biomejs/biome lint --write .", - "format": "npx @biomejs/biome format --write .", - "test": "vitest run --coverage" - }, - "keywords": [], - "author": "", - "license": "AGPL-3.0", - "dependencies": { - "@sentry/profiling-node": "8.33.1", - "short-unique-id": "^5.2.0" - }, - "description": "", - "devDependencies": { - "@vitest/coverage-v8": "2.1.2", - "vitest": "^2.1.2" - } -} diff --git a/packages/sessions/src/index.ts b/packages/sessions/src/index.ts deleted file mode 100644 index e36c3b33f..000000000 --- a/packages/sessions/src/index.ts +++ /dev/null @@ -1,233 +0,0 @@ -import { createCipheriv, createDecipheriv } from "node:crypto"; - -/** - * Represents a pair of encryption and decryption functions. - */ -type CipherPair = { - /** The encryption function */ - encrypt: (data: Buffer) => Buffer; - /** The decryption function */ - decrypt: (data: Buffer) => Buffer; -}; - -/** - * Generates a pair of cipher and decipher functions for game encryption. - * @returns The cipher and decipher functions. - */ -function createGameEncryptionPair(key: string): CipherPair { - try { - assertStringIsHex(key); - if (key.length !== 16) { - throw Error( - `Invalid game key length: ${key.length}. The key must be 16 bytes long.`, - ); - } - - // The key used by the game 8 bytes long. - // Since the key is in hex format, we need to slice it to 16 characters. - key = key.slice(0, 16); - - // The IV is intentionally required to be all zeros. - const iv = Buffer.alloc(8); - const keyBuffer = Buffer.from(key, "hex"); - - // The algorithm is intentionally set to "des-cbc". - // This is because the game uses this insecure algorithm. - // We are intentionally using an insecure algorithm here to match the game. - const cipher = createCipheriv("des-cbc", keyBuffer, iv); - const decipher = createDecipheriv("des-cbc", keyBuffer, iv); - - return { - encrypt: cipher.update.bind(cipher), - decrypt: decipher.update.bind(decipher), - }; - } catch (error: unknown) { - const err = new Error(`Failed to create game encryption pair`); - err.cause = error; - throw err; - } -} - -/** - * Generates a pair of encryption and decryption functions for the server. - * - * @param key - The key to use for encryption and decryption. Must be 16 hex characters. - * @returns {CipherPair} The encryption and decryption functions. - */ -function createServerEncryptionPair(key: string): CipherPair { - try { - assertStringExists(key); - assertStringIsHex(key); - if (key.length !== 16) { - throw Error( - `Invalid server key length: ${key.length}. The key must be 16 bytes long.`, - ); - } - - // The IV is intentionally required to be empty. - const iv = Buffer.alloc(0); - const keyBuffer = Buffer.from(key, "hex"); - - // The algorithm is intentionally set to "rc4". - // This is because the game uses this insecure algorithm. - // We are intentionally using an insecure algorithm here to match the game. - const cipher = createCipheriv("rc4", keyBuffer, iv); - const decipher = createDecipheriv("rc4", keyBuffer, iv); - - return { - encrypt: cipher.update.bind(cipher), - decrypt: decipher.update.bind(decipher), - }; - } catch (error: unknown) { - const err = new Error(`Failed to create server encryption pair`); - err.cause = error; - throw err; - } -} - -type ConnectedClient = { - /** The connection ID for the client */ - connectionId: string; - /** The customer ID for the client */ - customerId: number; - /** The session key for the client */ - sessionKey?: string; - /** The game encryption pair for the client, if known */ - gameEncryptionPair?: ReturnType; - /** The server encryption pair for the client, if known */ - serverEncryptionPair?: ReturnType; - /** Whether the game encryption handshake is complete */ - gameEncryptionHandshakeComplete: boolean; - /** Whether the server encryption handshake is complete */ - serverEncryptionHandshakeComplete: boolean; -}; - -/** - * Sets the client encryption for a connected client. - * - * @param client - The connected client to set the encryption for. - * @param sessionKey - The session key to associate with the client. - * @returns The updated connected client with the encryption set. - */ -export function setClientEncryption( - client: ConnectedClient, - sessionKey: string, -): ConnectedClient { - try { - const gameEncryptionPair = createGameEncryptionPair(sessionKey); - const serverEncryptionPair = createServerEncryptionPair(sessionKey); - client.sessionKey = sessionKey; - client.gameEncryptionPair = gameEncryptionPair; - client.serverEncryptionPair = serverEncryptionPair; - } catch (error: unknown) { - const err = new Error(`Failed to set client encryption`); - err.cause = error; - throw err; - } - return client; -} - -/** - * Represents a record of connected clients. - * The key is the connection ID. - * The value is the connected client. - */ -const connectedClients: Record = {}; - -/** - * Finds a connected client by their customer ID. - * - * @param customerId - The customer ID to search for. - * @returns The connected client with the specified customer ID. - * @throws Error if no client is found with the given customer ID. - */ -export function findClientByCustomerId(customerId: number): ConnectedClient { - const client = Object.values(connectedClients).find( - (client) => client.customerId === customerId, - ); - if (typeof client === "undefined") { - throw new Error(`Client with customer ID ${customerId} not found`); - } - return client; -} - -type connectionType = "game" | "server"; - -/** - * Checks if a client has an encryption pair based on the connection type. - * @param client - The connected client. - * @param connectionType - The type of connection ("game" or "server"). - * @returns A boolean indicating whether the client has an encryption pair. - */ -export function hasClientEncryptionPair( - client: ConnectedClient, - connectionType: connectionType, -): boolean { - if (connectionType === "game") { - return !!client.gameEncryptionPair; - } else { - return !!client.serverEncryptionPair; - } -} - -/** - * Creates a new client connection. - * - * @param connectionId - The ID of the connection. - * @param customerId - The ID of the customer. - * @param sessionKey - The session key (optional). - * @returns A ConnectedClient object representing the new client connection. - */ -export function newClientConnection( - connectionId: string, - customerId: number, - sessionKey?: string, -): ConnectedClient { - return { - connectionId, - customerId, - sessionKey, - gameEncryptionHandshakeComplete: false, - serverEncryptionHandshakeComplete: false, - }; -} - -/** - * Saves the client connection with the specified connection ID. - * - * @param connectionId - The ID of the connection. - * @param client - The connected client to be saved. - */ -export function saveClientConnection( - connectionId: string, - client: ConnectedClient, -): void { - connectedClients[connectionId] = client; -} - -/** - * Clears all connected clients. - */ -export function clearConnectedClients(): void { - for (const connectionId in connectedClients) { - delete connectedClients[connectionId]; - } -} - -function assertStringExists(str: string): void { - if (str === "" || typeof str === "undefined") { - throw new Error("String not provided"); - } -} - -/** - * Asserts that a given string is a valid hexadecimal string. - * - * @param str - The string to be validated. - * @throws {Error} If the string is not a valid hexadecimal string. - */ -function assertStringIsHex(str: string): void { - if (!/^[0-9a-fA-F]+$/.test(str)) { - throw new Error(`Invalid hex string: ${str}`); - } -} diff --git a/packages/sessions/test/index.test.ts b/packages/sessions/test/index.test.ts deleted file mode 100644 index 57070ffa3..000000000 --- a/packages/sessions/test/index.test.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { beforeEach, describe, expect, it } from "vitest"; -import { - saveClientConnection, - clearConnectedClients, - findClientByCustomerId, - hasClientEncryptionPair, - newClientConnection, - setClientEncryption, -} from "../index.js"; - -describe("Client connections", () => { - beforeEach(() => { - clearConnectedClients(); - }); - - describe("newClientConnection", () => { - it("should create a new client connection", () => { - const connectionId = "123"; - const customerId = 456; - - const client = newClientConnection(connectionId, customerId); - - expect(client.connectionId).toBe(connectionId); - expect(client.customerId).toBe(customerId); - expect(client.gameEncryptionHandshakeComplete).toBe(false); - expect(client.serverEncryptionHandshakeComplete).toBe(false); - }); - }); - - describe("saveClientConnection", () => { - it("should save a client connection", () => { - const connectionId = "123"; - const customerId = 456; - const client = newClientConnection(connectionId, customerId); - - saveClientConnection(connectionId, client); - - expect(findClientByCustomerId(customerId)).toBe(client); - }); - }); - - describe("findClientByCustomerId", () => { - it("should find a client by customer ID", () => { - const connectionId = "123"; - const customerId = 456; - const client = newClientConnection(connectionId, customerId); - saveClientConnection(connectionId, client); - - expect(findClientByCustomerId(customerId)).toBe(client); - }); - - it("should throw an error if the client is not found", () => { - const customerId = 456; - - expect(() => findClientByCustomerId(customerId)).toThrow( - `Client with customer ID ${customerId} not found`, - ); - }); - }); - - describe("setClientEncryption", () => { - it("should set the client encryption pair", () => { - const connectionId = "123"; - const customerId = 456; - const sessionKey = "ea25e21a2a022d71"; - - const client = newClientConnection(connectionId, customerId); - saveClientConnection(connectionId, client); - - setClientEncryption(client, sessionKey); - - expect(client.sessionKey).toBe(sessionKey); - }); - - it("should throw an error if the session key is not provided", () => { - const connectionId = "123"; - const customerId = 456; - - const client = newClientConnection(connectionId, customerId); - saveClientConnection(connectionId, client); - - expect(() => setClientEncryption(client, "")).toThrow(); - }); - - it("should throw an error if the session key is invalid", () => { - const connectionId = "123"; - const customerId = 456; - - const client = newClientConnection(connectionId, customerId); - saveClientConnection(connectionId, client); - - expect(() => setClientEncryption(client, "invalid")).toThrow(); - }); - }); - - describe("hasClientEncryptionPair", () => { - it("should return true if the client has an encryption pair", () => { - const connectionId = "123"; - const customerId = 456; - const sessionKey = "ea25e21a2a022d71"; - const client = newClientConnection(connectionId, customerId); - saveClientConnection(connectionId, client); - - expect(hasClientEncryptionPair(client, "game")).toBe(false); - expect(hasClientEncryptionPair(client, "server")).toBe(false); - - setClientEncryption(client, sessionKey); - - expect(hasClientEncryptionPair(client, "game")).toBe(true); - expect(hasClientEncryptionPair(client, "server")).toBe(true); - }); - }); - - describe("clearConnectedClients", () => { - it("should clear all connected clients", () => { - const connectionId = "123"; - const customerId = 456; - const client = newClientConnection(connectionId, customerId); - saveClientConnection(connectionId, client); - - clearConnectedClients(); - - expect(() => findClientByCustomerId(customerId)).toThrow( - `Client with customer ID ${customerId} not found`, - ); - }); - }); -}); diff --git a/packages/sessions/tsconfig.json b/packages/sessions/tsconfig.json deleted file mode 100644 index 6a3ee5ec8..000000000 --- a/packages/sessions/tsconfig.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "extends": "./../../tsconfig.base.json", - "compilerOptions": { - "incremental": true, - "composite": true - }, - "include": ["index.ts", "src", "test"] -} diff --git a/packages/shared-packets/src/GameMessageHeader.ts b/packages/shared-packets/src/GameMessageHeader.ts index 09095023e..be5dad1b5 100644 --- a/packages/shared-packets/src/GameMessageHeader.ts +++ b/packages/shared-packets/src/GameMessageHeader.ts @@ -2,9 +2,12 @@ import { BufferSerializer } from "./BufferSerializer.js"; import type { SerializableInterface } from "./types.js"; /** - * + * Represents the header of a game message. + * The header contains the message ID, the length of the message data, + * and the version of the message. + * + * This is a big-endian structure. */ - export class GameMessageHeader extends BufferSerializer implements SerializableInterface diff --git a/packages/shared-packets/src/GameMessagePayload.ts b/packages/shared-packets/src/GameMessagePayload.ts index baffa4301..35b343949 100644 --- a/packages/shared-packets/src/GameMessagePayload.ts +++ b/packages/shared-packets/src/GameMessagePayload.ts @@ -9,7 +9,7 @@ export class GameMessagePayload static copy(payload: GameMessagePayload): GameMessagePayload { const newPayload = new GameMessagePayload(); - newPayload._data = Buffer.from(payload._data); + newPayload.deserialize(payload.serialize()); return newPayload; } diff --git a/packages/shared-packets/src/GamePacket.ts b/packages/shared-packets/src/GamePacket.ts index 62c99fda1..c77c5b0e4 100644 --- a/packages/shared-packets/src/GamePacket.ts +++ b/packages/shared-packets/src/GamePacket.ts @@ -20,14 +20,14 @@ export class GamePacket extends BasePacket implements SerializableMessage { * @returns A new `ServerPacket` instance with the same message ID and header as the original, * and either the deserialized new data or a copy of the original data. */ - static copy(originalPacket: GamePacket, newData: Buffer): GamePacket { + static copy(originalPacket: GamePacket, newData?: Buffer): GamePacket { const newPacket = new GamePacket(); newPacket.deserialize(originalPacket.serialize()); if (newData) { newPacket.data.deserialize(newData); } else { - newPacket.data = GameMessagePayload.copy(originalPacket.data); + newPacket.data.deserialize(originalPacket.data.serialize()); } return newPacket; diff --git a/packages/shared-packets/src/ServerMessageHeader.ts b/packages/shared-packets/src/ServerMessageHeader.ts index 3854baf0b..7693192e1 100644 --- a/packages/shared-packets/src/ServerMessageHeader.ts +++ b/packages/shared-packets/src/ServerMessageHeader.ts @@ -2,7 +2,11 @@ import { BufferSerializer } from "./BufferSerializer.js"; import type { SerializableInterface } from "./types.js"; /** - * + * Represents the header of a server message. + * The header contains the length of the message data, + * the signature of the message, + * + * This is a little-endian structure. */ export class ServerMessageHeader diff --git a/packages/shared-packets/src/ServerMessagePayload.ts b/packages/shared-packets/src/ServerMessagePayload.ts index e5c721592..c44d1cd33 100644 --- a/packages/shared-packets/src/ServerMessagePayload.ts +++ b/packages/shared-packets/src/ServerMessagePayload.ts @@ -5,15 +5,13 @@ export class ServerMessagePayload extends BufferSerializer implements SerializableInterface { - public messageId: number = 0; // 2 bytes - + private messageId: number = 0; // 2 bytes private previousMessageId: number = 0; // Not serialized private isEncrypted: boolean = false; // Not serialized static copy(payload: ServerMessagePayload): ServerMessagePayload { const newPayload = new ServerMessagePayload(); - newPayload.messageId = payload.messageId; - newPayload._data = Buffer.from(payload._data); + newPayload.deserialize(payload.serialize()); return newPayload; } diff --git a/packages/shared-packets/src/ServerPacket.ts b/packages/shared-packets/src/ServerPacket.ts index 5b58700cb..f49051528 100644 --- a/packages/shared-packets/src/ServerPacket.ts +++ b/packages/shared-packets/src/ServerPacket.ts @@ -20,7 +20,7 @@ export class ServerPacket extends BasePacket implements SerializableMessage { * @returns A new `ServerPacket` instance with the same message ID and header as the original, * and either the deserialized new data or a copy of the original data. */ - static copy(originalPacket: ServerPacket, newData: Buffer): ServerPacket { + static copy(originalPacket: ServerPacket, newData?: Buffer): ServerPacket { const newPacket = new ServerPacket(); newPacket.header = ServerMessageHeader.copy(originalPacket.header); diff --git a/packages/shared-packets/test/GamePacket.test.ts b/packages/shared-packets/test/GamePacket.test.ts index 44f40cad6..d49f2f9a2 100644 --- a/packages/shared-packets/test/GamePacket.test.ts +++ b/packages/shared-packets/test/GamePacket.test.ts @@ -3,7 +3,7 @@ import { Buffer } from "buffer"; import { GamePacket } from "../src/GamePacket.js"; describe("GamePacket", () => { - it("should deserialize correctly v0 correctly", () => { + it("should deserialize v0 correctly", () => { const buffer = Buffer.alloc(11); buffer.writeUInt16BE(1234, 0); // Message ID buffer.writeUInt16BE(11, 2); // Length @@ -19,7 +19,7 @@ describe("GamePacket", () => { ); }); - it("should deserialize correctly v1 correctly", () => { + it("should deserialize v1 correctly", () => { const buffer = Buffer.alloc(26); buffer.writeUInt16BE(1234, 0); // Message ID buffer.writeUInt16BE(11, 2); // Length @@ -36,6 +36,32 @@ describe("GamePacket", () => { ); }); + it("should be able to make a copy of the packet", () => { + const buffer = Buffer.alloc(11); + buffer.writeUInt16BE(1234, 0); // Message ID + buffer.writeUInt16BE(11, 2); // Length + buffer.write("test da", 4); // Data + + const packet = new GamePacket(); + packet.deserialize(buffer); + + const copy = GamePacket.copy(packet); + expect(copy.serialize().toString("hex")).equals(packet.serialize().toString("hex")); + }); + + it("should be able to make a copy of the packet with new data", () => { + const buffer = Buffer.alloc(11); + buffer.writeUInt16BE(1234, 0); // Message ID + buffer.writeUInt16BE(11, 2); // Length + buffer.write("test da", 4); // Data + + const packet = new GamePacket(); + packet.deserialize(buffer); + + const copy = GamePacket.copy(packet, Buffer.from("new data")); + expect(copy.serialize().toString("hex")).not.equals(packet.serialize().toString("hex")); + }); + it("should throw error if data is insufficient for header", () => { const buffer = Buffer.alloc(5); // Less than required for header @@ -55,7 +81,7 @@ describe("GamePacket", () => { ); }); - it("should identify version correctly", () => { + it("should identify version v1 correctly", () => { const buffer = Buffer.alloc(15); buffer.writeUInt16BE(11, 0); // Length buffer.writeUInt16BE(0x101, 4); // Version @@ -68,7 +94,7 @@ describe("GamePacket", () => { expect(packet.getVersion()).toBe(257); }); - it("should handle version 0 correctly", () => { + it("should handle version v0 correctly", () => { const buffer = Buffer.alloc(15); buffer.writeUInt16BE(1234, 0); // Message ID buffer.writeUInt16BE(11, 4); // Length diff --git a/packages/shared-packets/src/ServerPacket.test.ts b/packages/shared-packets/test/ServerPacket.test.ts similarity index 93% rename from packages/shared-packets/src/ServerPacket.test.ts rename to packages/shared-packets/test/ServerPacket.test.ts index 349db6c0f..e2ec5f8c4 100644 --- a/packages/shared-packets/src/ServerPacket.test.ts +++ b/packages/shared-packets/test/ServerPacket.test.ts @@ -1,7 +1,7 @@ import { describe, it, expect } from "vitest"; import { Buffer } from "buffer"; -import { ServerMessagePayload } from "./ServerMessagePayload.js"; -import { ServerPacket } from "./ServerPacket.js"; +import { ServerMessagePayload } from "../src/ServerMessagePayload.js"; +import { ServerPacket } from "../src/ServerPacket.js"; describe("ServerMessagePayload", () => { it("should serialize correctly", () => { @@ -108,7 +108,9 @@ describe("ServerMessagePayload", () => { packet.setPayloadEncryption(true); const str = packet.toString(); - expect(str).toBe("ServerPacket {length: 11, sequence: 5678, messageId: 1234}"); + expect(str).toBe( + "ServerPacket {length: 11, sequence: 5678, messageId: 1234}", + ); }); }); }); diff --git a/packages/shared-packets/tsconfig.json b/packages/shared-packets/tsconfig.json index ba43189cf..39f6a71bb 100644 --- a/packages/shared-packets/tsconfig.json +++ b/packages/shared-packets/tsconfig.json @@ -4,5 +4,5 @@ "incremental": true, "composite": true }, - "include": ["index.ts", "src"] + "include": ["index.ts", "src/**/*.ts"], } diff --git a/packages/shared/index.ts b/packages/shared/index.ts index 516bfdca5..071b27dce 100644 --- a/packages/shared/index.ts +++ b/packages/shared/index.ts @@ -34,6 +34,7 @@ export { findSessionByConnectionId, updateEncryption, } from "./src/State.js"; +export { ensureLegacyCipherCompatibility as verifyLegacyCipherSupport } from "./src/verifyLegacyCipherSupport.js"; export type { State } from "./src/State.js"; export type { OnDataHandler, ServiceResponse } from "./src/State.js"; export { LegacyMessage } from "./src/LegacyMessage.js"; @@ -57,7 +58,12 @@ export interface ConnectionRecord { } // Function to convert ARGB to 32-bit integer -export function argbToInt(alpha: number, red: number, green: number, blue: number) { +export function argbToInt( + alpha: number, + red: number, + green: number, + blue: number, +) { return ( ((alpha & 0xff) << 24) | ((red & 0xff) << 16) | diff --git a/packages/shared/src/verifyLegacyCipherSupport.ts b/packages/shared/src/verifyLegacyCipherSupport.ts new file mode 100644 index 000000000..93f5ef4d7 --- /dev/null +++ b/packages/shared/src/verifyLegacyCipherSupport.ts @@ -0,0 +1,15 @@ +import { getCiphers } from "node:crypto"; + +/** + * This function checks if the server supports the legacy ciphers + * + * @returns void + * @throws Error if the server does not support the legacy ciphers + */ + +export function ensureLegacyCipherCompatibility() { + const cipherList = getCiphers(); + if (!cipherList.includes("des-cbc") || !cipherList.includes("rc4")) { + throw new Error("Legacy ciphers not available"); + } +} diff --git a/server.ts b/server.ts index dfd405097..af36a6825 100755 --- a/server.ts +++ b/server.ts @@ -17,7 +17,7 @@ import { exit } from "node:process"; import * as Sentry from "@sentry/node"; import { getGatewayServer } from "rusty-motors-gateway"; -import { verifyLegacyCipherSupport } from "rusty-motors-gateway"; +import { verifyLegacyCipherSupport } from "rusty-motors-shared"; import { getServerConfiguration } from "rusty-motors-shared"; import { getServerLogger } from "rusty-motors-shared"; diff --git a/test/factoryMocks.ts b/test/factoryMocks.ts index e5c7c7b58..b8bb1eafb 100644 --- a/test/factoryMocks.ts +++ b/test/factoryMocks.ts @@ -1,6 +1,5 @@ import { expect, it, vi } from "vitest"; -import { verifyLegacyCipherSupport } from "../packages/gateway/src/encryption.js"; - +import { ensureLegacyCipherCompatibility } from "../packages/shared/src/verifyLegacyCipherSupport.js"; export function mockPino() { vi.mock("pino", () => { @@ -30,5 +29,5 @@ export function unmockPino() { } it("should have crypto", () => { - expect(() => verifyLegacyCipherSupport()).not.toThrow(); + expect(() => ensureLegacyCipherCompatibility()).not.toThrow(); }); From 49b75145fc6f532dd8804a0924f80836f75c6ff6 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Fri, 11 Oct 2024 18:37:31 -0400 Subject: [PATCH 16/23] refactor(gateway): add validation for port number in addPortRouter function --- packages/gateway/src/portRouters.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/gateway/src/portRouters.ts b/packages/gateway/src/portRouters.ts index d42dc6735..9d5960ec1 100644 --- a/packages/gateway/src/portRouters.ts +++ b/packages/gateway/src/portRouters.ts @@ -19,6 +19,9 @@ const portRouters = new Map(); */ export function addPortRouter(port: number, router: PortRouter) { + if (!Number.isInteger(port) || port < 0 || port > 65535) { + throw new Error(`Invalid port number: ${port}`); + } portRouters.set(port, router); } /** From 4def44fd91a1012943340c2331df0468f299f709 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Fri, 11 Oct 2024 18:39:04 -0400 Subject: [PATCH 17/23] refactor(tests): move socketErrorHandler.test.ts to the test directory --- .../{src => test}/socketErrorHandler.test.ts | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) rename packages/gateway/{src => test}/socketErrorHandler.test.ts (76%) diff --git a/packages/gateway/src/socketErrorHandler.test.ts b/packages/gateway/test/socketErrorHandler.test.ts similarity index 76% rename from packages/gateway/src/socketErrorHandler.test.ts rename to packages/gateway/test/socketErrorHandler.test.ts index 98c905e50..533f53ca8 100644 --- a/packages/gateway/src/socketErrorHandler.test.ts +++ b/packages/gateway/test/socketErrorHandler.test.ts @@ -1,16 +1,14 @@ import { describe, it, expect, vi } from "vitest"; -import { socketErrorHandler } from "./socketErrorHandler.js"; +import { socketErrorHandler } from "../src/socketErrorHandler.js"; import { type ServerLogger } from "rusty-motors-shared"; describe("socketErrorHandler", () => { - it("should log a debug message when error code is ECONNRESET", () => { const connectionId = "12345"; const error = { code: "ECONNRESET" } as NodeJS.ErrnoException; - const mockLogger = { - debug: vi.fn(), - } as unknown as ServerLogger; - + const mockLogger = { + debug: vi.fn(), + } as unknown as ServerLogger; socketErrorHandler({ connectionId, error, log: mockLogger }); @@ -25,14 +23,12 @@ describe("socketErrorHandler", () => { code: "EUNKNOWN", message: "Unknown error", } as NodeJS.ErrnoException; - const mockLogger = { - debug: vi.fn(), - } as unknown as ServerLogger; - + const mockLogger = { + debug: vi.fn(), + } as unknown as ServerLogger; expect(() => socketErrorHandler({ connectionId, error, log: mockLogger }), ).toThrow(`Socket error: ${error.message} on connection ${connectionId}`); }); - }); From 0ba96ae4a161804cac27b3bfd486a44e8a4f1768 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Fri, 11 Oct 2024 18:49:02 -0400 Subject: [PATCH 18/23] fix(GamePacket): ensure sufficient data before deserialization - Added checks to ensure there is enough data before deserializing the header and data. - Improved data handling by verifying data length at multiple stages: - Before identifying version. - Before deserializing the header. - Before deserializing the data. --- package.json | 4 +- packages/gateway/test/npsPortRouter.test.ts | 136 ++++++++++++++++++ packages/gateway/test/portRouters.test.ts | 35 +++++ .../shared-packets/src/GameMessageHeader.ts | 11 ++ packages/shared-packets/src/GamePacket.ts | 4 +- .../shared-packets/test/GamePacket.test.ts | 32 ++++- pnpm-lock.yaml | 130 ++++++++--------- 7 files changed, 278 insertions(+), 74 deletions(-) create mode 100644 packages/gateway/test/npsPortRouter.test.ts diff --git a/package.json b/package.json index 0f219b960..4938d4a79 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "@tsconfig/node-lts": "^20.1.3", "@tsconfig/node20": "^20.1.4", "@types/chai": "5.0.0", - "@types/node": "^22.7.5", + "@types/node": "^20.16.11", "@types/sinon": "17.0.3", "@types/sinon-chai": "4.0.0", "@typescript-eslint/eslint-plugin": "^8.8.1", @@ -114,4 +114,4 @@ } }, "packageManager": "pnpm@9.12.1+sha512.e5a7e52a4183a02d5931057f7a0dbff9d5e9ce3161e33fa68ae392125b79282a8a8a470a51dfc8a0ed86221442eb2fb57019b0990ed24fab519bf0e1bc5ccfc4" -} +} \ No newline at end of file diff --git a/packages/gateway/test/npsPortRouter.test.ts b/packages/gateway/test/npsPortRouter.test.ts new file mode 100644 index 000000000..1f528af13 --- /dev/null +++ b/packages/gateway/test/npsPortRouter.test.ts @@ -0,0 +1,136 @@ +import { describe, it, expect, vi } from "vitest"; +import { npsPortRouter } from "../src/npsPortRouter.js"; +import type { TaggedSocket } from "../src/socketUtility.js"; +import { GamePacket } from "rusty-motors-shared-packets"; +import { write } from "fs"; + +describe("npsPortRouter", () => { + it("should log an error and close the socket if local port is undefined", async () => { + const mockSocket = { + localPort: undefined, + end: vi.fn(), + on: vi.fn(), + }; + const mockLogger = { + error: vi.fn(), + debug: vi.fn(), + }; + const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id" }; + + await npsPortRouter({ taggedSocket, log: mockLogger }); + + expect(mockLogger.error).toHaveBeenCalledWith( + "[test-id] Local port is undefined", + ); + expect(mockSocket.end).toHaveBeenCalled(); + }); + + it("should log the start of the router and send ok to login packet for port 7003", async () => { + const mockSocket = { + localPort: 7003, + write: vi.fn(), + on: vi.fn(), + }; + const mockLogger = { + error: vi.fn(), + debug: vi.fn(), + }; + const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id" }; + + await npsPortRouter({ taggedSocket, log: mockLogger }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "[test-id] NPS port router started for port 7003", + ); + expect(mockLogger.debug).toHaveBeenCalledWith( + "[test-id] Sending ok to login packet", + ); + expect(mockSocket.write).toHaveBeenCalledWith( + Buffer.from([0x02, 0x30, 0x00, 0x00]), + ); + }); + + it("should handle data event and route initial message", async () => { + const mockSocket = { + localPort: 7003, + write: vi.fn(), + on: vi.fn((event, callback) => { + if (event === "data") { + callback(Buffer.from([0x01, 0x02, 0x03])); + } + }), + }; + const mockLogger = { + error: vi.fn(), + debug: vi.fn(), + }; + const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id" }; + + const mockGamePacket = { + deserialize: vi.fn(), + toHexString: vi.fn().mockReturnValue("010203"), + }; + vi.spyOn(GamePacket.prototype, "deserialize").mockImplementation( + mockGamePacket.deserialize, + ); + vi.spyOn(GamePacket.prototype, "toHexString").mockImplementation( + mockGamePacket.toHexString, + ); + + await npsPortRouter({ taggedSocket, log: mockLogger }); + + expect(mockLogger.debug).toHaveBeenCalledWith( + "[test-id] Received data: 010203", + ); + expect(mockLogger.debug).toHaveBeenCalledWith( + "[test-id] Initial packet(str): GamePacket {length: 0, messageId: 0}", + ); + expect(mockLogger.debug).toHaveBeenCalledWith( + "[test-id] initial Packet(hex): 010203", + ); + }); + + it("should log socket end event", async () => { + const mockSocket = { + localPort: 7003, + on: vi.fn((event, callback) => { + if (event === "end") { + callback(); + } + }), + write: vi.fn(), + }; + const mockLogger = { + error: vi.fn(), + debug: vi.fn(), + }; + const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id" }; + + await npsPortRouter({ taggedSocket, log: mockLogger }); + + expect(mockLogger.debug).toHaveBeenCalledWith("[test-id] Socket closed"); + }); + + it("should log socket error event", async () => { + const mockSocket = { + localPort: 7003, + on: vi.fn((event, callback) => { + if (event === "error") { + callback(new Error("Test error")); + } + }), + write: vi.fn(), + }; + const mockLogger = { + error: vi.fn(), + debug: vi.fn(), + }; + const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id" }; + + await npsPortRouter({ taggedSocket, log: mockLogger }); + + expect(mockLogger.error).toHaveBeenCalledWith( + "[test-id] Socket error: Error: Test error", + ); + }); +}); diff --git a/packages/gateway/test/portRouters.test.ts b/packages/gateway/test/portRouters.test.ts index d5f4316a9..311999da6 100644 --- a/packages/gateway/test/portRouters.test.ts +++ b/packages/gateway/test/portRouters.test.ts @@ -142,5 +142,40 @@ describe("clearPortRouters", () => { expect(retrievedRouter2).toBeInstanceOf(Function); expect(retrievedRouter2.name).toBe("notFoundRouter"); }); + + describe("addPortRouter", () => { + it("should throw an error if the port number is not an integer", () => { + // arrange + const port = 8080.5; + const mockRouter = vi.fn().mockResolvedValue(undefined); + + // act & assert + expect(() => addPortRouter(port, mockRouter)).toThrow( + `Invalid port number: ${port}` + ); + }); + + it("should throw an error if the port number is negative", () => { + // arrange + const port = -1; + const mockRouter = vi.fn().mockResolvedValue(undefined); + + // act & assert + expect(() => addPortRouter(port, mockRouter)).toThrow( + `Invalid port number: ${port}` + ); + }); + + it("should throw an error if the port number is greater than 65535", () => { + // arrange + const port = 65536; + const mockRouter = vi.fn().mockResolvedValue(undefined); + + // act & assert + expect(() => addPortRouter(port, mockRouter)).toThrow( + `Invalid port number: ${port}` + ); + }); + }); }); }); diff --git a/packages/shared-packets/src/GameMessageHeader.ts b/packages/shared-packets/src/GameMessageHeader.ts index be5dad1b5..1945186a1 100644 --- a/packages/shared-packets/src/GameMessageHeader.ts +++ b/packages/shared-packets/src/GameMessageHeader.ts @@ -84,7 +84,18 @@ export class GameMessageHeader this.length = data.readUInt16BE(2); } + private assertV1Checksum(data: Buffer): void { + const length = data.readUInt16BE(2); + const checksum = data.readUInt32BE(8); + if (checksum !== length) { + throw new Error( + `Checksum mismatch. Expected ${length}, got ${checksum}`, + ); + } + } + private deserializeV1(data: Buffer): void { + this.assertV1Checksum(data); this.id = data.readUInt16BE(0); this.length = data.readUInt16BE(2); // Skip version diff --git a/packages/shared-packets/src/GamePacket.ts b/packages/shared-packets/src/GamePacket.ts index c77c5b0e4..31c27ed0a 100644 --- a/packages/shared-packets/src/GamePacket.ts +++ b/packages/shared-packets/src/GamePacket.ts @@ -97,12 +97,14 @@ export class GamePacket extends BasePacket implements SerializableMessage { this._assertEnoughData(data, 6); const version = this.identifyVersion(data); - console.log("version", version); this.header.setVersion(version); this._assertEnoughData(data, this.header.getByteSize()); this.header.deserialize(data); + + this._assertEnoughData(data, this.header.getLength()); + this.data.deserialize(data.subarray(this.header.getByteSize())); return this; diff --git a/packages/shared-packets/test/GamePacket.test.ts b/packages/shared-packets/test/GamePacket.test.ts index d49f2f9a2..65a3d776a 100644 --- a/packages/shared-packets/test/GamePacket.test.ts +++ b/packages/shared-packets/test/GamePacket.test.ts @@ -71,22 +71,42 @@ describe("GamePacket", () => { ); }); - it("should throw error if data is insufficient for full packet", () => { - const buffer = Buffer.alloc(10); // Less than required for full packet + it("should throw error if checksum is incorrect for v1 packet", () => { + const buffer = Buffer.alloc(26); + buffer.writeUInt16BE(1234, 0); // Message ID + buffer.writeUInt16BE(11, 2); // Length buffer.writeUInt16BE(0x101, 4); // Version + buffer.writeUInt32BE(26, 8); // Checksum + buffer.write("test data", 12); // Data + + buffer.writeUInt32BE(0, 8); // Incorrect checksum + + const packet = new GamePacket(); + expect(() => packet.deserialize(buffer)).toThrow( + "Checksum mismatch. Expected 11, got 0", + ); + }); + it("should throw error if data is insufficient for full v1 packet", () => { + const buffer = Buffer.alloc(25); // 1 byte less than required for v1 packet + buffer.writeUInt16BE(1234, 0); // Message ID + buffer.writeUInt16BE(26, 2); // Length + buffer.writeUInt16BE(0x101, 4); // Version + buffer.writeUInt32BE(26, 8); // Checksum + const packet = new GamePacket(); expect(() => packet.deserialize(buffer)).toThrow( - "Data is too short. Expected at least 12 bytes, got 10 bytes", + "Data is too short. Expected at least 26 bytes, got 25 bytes" ); }); it("should identify version v1 correctly", () => { const buffer = Buffer.alloc(15); - buffer.writeUInt16BE(11, 0); // Length + buffer.writeUInt16BE(1234, 0); // Message ID + buffer.writeUInt16BE(11, 2); // Length buffer.writeUInt16BE(0x101, 4); // Version - buffer.writeUInt16BE(1234, 6); // Message ID - buffer.write("test data", 8, "utf8"); // Data + buffer.writeUInt32BE(11, 8); // Checksum + buffer.write("test data", 12, "utf8"); // Data const packet = new GamePacket(); packet.deserialize(buffer); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3a46998df..3209672fe 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -94,14 +94,14 @@ importers: version: 5.1.1 ts-node: specifier: 10.9.2 - version: 10.9.2(@types/node@22.7.5)(typescript@5.6.3) + version: 10.9.2(@types/node@20.16.11)(typescript@5.6.3) devDependencies: '@biomejs/biome': specifier: 1.9.3 version: 1.9.3 '@commitlint/cli': specifier: ^19.5.0 - version: 19.5.0(@types/node@22.7.5)(typescript@5.6.3) + version: 19.5.0(@types/node@20.16.11)(typescript@5.6.3) '@commitlint/config-conventional': specifier: ^19.5.0 version: 19.5.0 @@ -124,8 +124,8 @@ importers: specifier: 5.0.0 version: 5.0.0 '@types/node': - specifier: ^22.7.5 - version: 22.7.5 + specifier: ^20.16.11 + version: 20.16.11 '@types/sinon': specifier: 17.0.3 version: 17.0.3 @@ -140,7 +140,7 @@ importers: version: 8.8.1(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3) '@vitest/coverage-v8': specifier: ^2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) eslint: specifier: ^9.12.0 version: 9.12.0(jiti@1.21.6) @@ -182,7 +182,7 @@ importers: version: 5.0.5(@typescript-eslint/parser@8.8.1(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3))(eslint@9.12.0(jiti@1.21.6))(typescript@5.6.3) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/cli: dependencies: @@ -192,10 +192,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/database: dependencies: @@ -220,10 +220,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/gateway: dependencies: @@ -236,10 +236,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/lobby: dependencies: @@ -252,10 +252,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/login: dependencies: @@ -268,10 +268,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/mcots: dependencies: @@ -284,10 +284,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/nps: dependencies: @@ -300,10 +300,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/patch: dependencies: @@ -313,10 +313,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/persona: dependencies: @@ -329,10 +329,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/shard: dependencies: @@ -342,10 +342,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/shared: dependencies: @@ -358,10 +358,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/shared-packets: dependencies: @@ -371,10 +371,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages/transactions: dependencies: @@ -384,10 +384,10 @@ importers: devDependencies: '@vitest/coverage-v8': specifier: 2.1.2 - version: 2.1.2(vitest@2.1.2(@types/node@22.7.5)) + version: 2.1.2(vitest@2.1.2(@types/node@20.16.11)) vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) src/chat: dependencies: @@ -400,7 +400,7 @@ importers: version: 2.0.6 vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) src/socket: dependencies: @@ -413,7 +413,7 @@ importers: version: 2.0.6 vitest: specifier: ^2.1.2 - version: 2.1.2(@types/node@22.7.5) + version: 2.1.2(@types/node@20.16.11) packages: @@ -2455,8 +2455,8 @@ packages: '@types/mysql@2.15.26': resolution: {integrity: sha512-DSLCOXhkvfS5WNNPbfn2KdICAmk8lLc+/PNvnPnF7gOdMZCxopXduqv0OQ13y/yA/zXTSikZZqVgybUxOEg6YQ==} - '@types/node@22.7.5': - resolution: {integrity: sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==} + '@types/node@20.16.11': + resolution: {integrity: sha512-y+cTCACu92FyA5fgQSAI8A1H429g7aSK2HsO7K4XYUWc4dY5IUz55JSDIYT6/VsOLfGy8vmvQYC2hfb0iF16Uw==} '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -6900,11 +6900,11 @@ snapshots: '@biomejs/cli-win32-x64@1.9.3': optional: true - '@commitlint/cli@19.5.0(@types/node@22.7.5)(typescript@5.6.3)': + '@commitlint/cli@19.5.0(@types/node@20.16.11)(typescript@5.6.3)': dependencies: '@commitlint/format': 19.5.0 '@commitlint/lint': 19.5.0 - '@commitlint/load': 19.5.0(@types/node@22.7.5)(typescript@5.6.3) + '@commitlint/load': 19.5.0(@types/node@20.16.11)(typescript@5.6.3) '@commitlint/read': 19.5.0 '@commitlint/types': 19.5.0 tinyexec: 0.3.0 @@ -6951,7 +6951,7 @@ snapshots: '@commitlint/rules': 19.5.0 '@commitlint/types': 19.5.0 - '@commitlint/load@19.5.0(@types/node@22.7.5)(typescript@5.6.3)': + '@commitlint/load@19.5.0(@types/node@20.16.11)(typescript@5.6.3)': dependencies: '@commitlint/config-validator': 19.5.0 '@commitlint/execute-rule': 19.5.0 @@ -6959,7 +6959,7 @@ snapshots: '@commitlint/types': 19.5.0 chalk: 5.3.0 cosmiconfig: 9.0.0(typescript@5.6.3) - cosmiconfig-typescript-loader: 5.0.0(@types/node@22.7.5)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3) + cosmiconfig-typescript-loader: 5.0.0(@types/node@20.16.11)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3) lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 lodash.uniq: 4.5.0 @@ -8388,15 +8388,15 @@ snapshots: '@types/connect@3.4.36': dependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 '@types/conventional-commits-parser@5.0.0': dependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 '@types/cross-spawn@6.0.6': dependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 '@types/debug@4.1.12': dependencies: @@ -8426,9 +8426,9 @@ snapshots: '@types/mysql@2.15.26': dependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 - '@types/node@22.7.5': + '@types/node@20.16.11': dependencies: undici-types: 6.19.8 @@ -8442,13 +8442,13 @@ snapshots: '@types/pg@8.11.10': dependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 pg-protocol: 1.7.0 pg-types: 4.0.2 '@types/pg@8.6.1': dependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 pg-protocol: 1.7.0 pg-types: 2.2.0 @@ -8605,7 +8605,7 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitest/coverage-v8@2.1.2(vitest@2.1.2(@types/node@22.7.5))': + '@vitest/coverage-v8@2.1.2(vitest@2.1.2(@types/node@20.16.11))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 0.2.3 @@ -8619,7 +8619,7 @@ snapshots: std-env: 3.7.0 test-exclude: 7.0.1 tinyrainbow: 1.2.0 - vitest: 2.1.2(@types/node@22.7.5) + vitest: 2.1.2(@types/node@20.16.11) transitivePeerDependencies: - supports-color @@ -8630,13 +8630,13 @@ snapshots: chai: 5.1.1 tinyrainbow: 1.2.0 - '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.5))': + '@vitest/mocker@2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@20.16.11))': dependencies: '@vitest/spy': 2.1.2 estree-walker: 3.0.3 magic-string: 0.30.12 optionalDependencies: - vite: 5.4.8(@types/node@22.7.5) + vite: 5.4.8(@types/node@20.16.11) '@vitest/pretty-format@2.1.2': dependencies: @@ -9230,9 +9230,9 @@ snapshots: core-util-is@1.0.3: {} - cosmiconfig-typescript-loader@5.0.0(@types/node@22.7.5)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3): + cosmiconfig-typescript-loader@5.0.0(@types/node@20.16.11)(cosmiconfig@9.0.0(typescript@5.6.3))(typescript@5.6.3): dependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 cosmiconfig: 9.0.0(typescript@5.6.3) jiti: 1.21.6 typescript: 5.6.3 @@ -11935,14 +11935,14 @@ snapshots: ts-interface-checker@0.1.13: {} - ts-node@10.9.2(@types/node@22.7.5)(typescript@5.6.3): + ts-node@10.9.2(@types/node@20.16.11)(typescript@5.6.3): dependencies: '@cspotcode/source-map-support': 0.8.1 '@tsconfig/node10': 1.0.11 '@tsconfig/node12': 1.0.11 '@tsconfig/node14': 1.0.3 '@tsconfig/node16': 1.0.4 - '@types/node': 22.7.5 + '@types/node': 20.16.11 acorn: 8.12.1 acorn-walk: 8.3.4 arg: 4.1.3 @@ -12060,12 +12060,12 @@ snapshots: vary@1.1.2: {} - vite-node@2.1.2(@types/node@22.7.5): + vite-node@2.1.2(@types/node@20.16.11): dependencies: cac: 6.7.14 debug: 4.3.7(supports-color@5.5.0) pathe: 1.1.2 - vite: 5.4.8(@types/node@22.7.5) + vite: 5.4.8(@types/node@20.16.11) transitivePeerDependencies: - '@types/node' - less @@ -12077,19 +12077,19 @@ snapshots: - supports-color - terser - vite@5.4.8(@types/node@22.7.5): + vite@5.4.8(@types/node@20.16.11): dependencies: esbuild: 0.21.5 postcss: 8.4.47 rollup: 4.24.0 optionalDependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 fsevents: 2.3.3 - vitest@2.1.2(@types/node@22.7.5): + vitest@2.1.2(@types/node@20.16.11): dependencies: '@vitest/expect': 2.1.2 - '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@22.7.5)) + '@vitest/mocker': 2.1.2(@vitest/spy@2.1.2)(vite@5.4.8(@types/node@20.16.11)) '@vitest/pretty-format': 2.1.2 '@vitest/runner': 2.1.2 '@vitest/snapshot': 2.1.2 @@ -12104,11 +12104,11 @@ snapshots: tinyexec: 0.3.0 tinypool: 1.0.1 tinyrainbow: 1.2.0 - vite: 5.4.8(@types/node@22.7.5) - vite-node: 2.1.2(@types/node@22.7.5) + vite: 5.4.8(@types/node@20.16.11) + vite-node: 2.1.2(@types/node@20.16.11) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 transitivePeerDependencies: - less - lightningcss @@ -12194,7 +12194,7 @@ snapshots: wkx@0.5.0: dependencies: - '@types/node': 22.7.5 + '@types/node': 20.16.11 word-wrap@1.2.5: {} From 79c0ce255c303e38d84e1f71379af1a9eeced56b Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Sat, 12 Oct 2024 09:42:26 -0400 Subject: [PATCH 19/23] test(mcotsPortRouter): add unit tests for error logging and data handling - Verify error logging and socket closure if local port is undefined. - Handle data event and route initial message. - Mock socket and log objects for testing. - Ensure proper deserialization and hex string conversion of server packets. --- packages/gateway/test/mcotsPortRouter.test.ts | 112 ++++++++++++++++++ packages/gateway/test/npsPortRouter.test.ts | 17 +-- packages/gateway/tsconfig.json | 2 +- 3 files changed, 123 insertions(+), 8 deletions(-) create mode 100644 packages/gateway/test/mcotsPortRouter.test.ts diff --git a/packages/gateway/test/mcotsPortRouter.test.ts b/packages/gateway/test/mcotsPortRouter.test.ts new file mode 100644 index 000000000..c744f5f18 --- /dev/null +++ b/packages/gateway/test/mcotsPortRouter.test.ts @@ -0,0 +1,112 @@ +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { mcotsPortRouter } from "../src/mcotsPortRouter.js"; +import type { TaggedSocket } from "../src/socketUtility.js"; +import { ServerPacket } from "rusty-motors-shared-packets"; + +describe("mcotsPortRouter", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + + it("should log an error and close the socket if local port is undefined", async () => { + const mockSocket = { + localPort: undefined, + end: vi.fn(), + on: vi.fn(), + }; + const mockLogger = { + error: vi.fn(), + debug: vi.fn(), + }; + const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id" }; + + await mcotsPortRouter({ taggedSocket, log: mockLogger }); + + expect(mockLogger.error).toHaveBeenCalledWith( + "[test-id] Local port is undefined", + ); + expect(mockSocket.end).toHaveBeenCalled(); + }); + + it("should handle data event and route initial message", async () => { + const mockSocket = { + localPort: 43300, + write: vi.fn(), + on: vi.fn((event, callback) => { + if (event === "data") { + callback(Buffer.from([0x74, 0x65, 0x73, 0x74, 0x2d, 0x64, 0x61, 0x74, 0x61])); + } + }), + }; + const mockLog = { + debug: vi.fn(), + error: vi.fn(), + }; + const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id-mcots" }; + + const mockServerPacket = { + deserialize: vi.fn(), + toHexString: vi.fn().mockReturnValue("746573742d64617461"), + }; + vi.spyOn(ServerPacket.prototype, "deserialize").mockImplementation( + mockServerPacket.deserialize, + ); + vi.spyOn(ServerPacket.prototype, "toHexString").mockImplementation( + mockServerPacket.toHexString, + ); + + await mcotsPortRouter({ taggedSocket, log: mockLog }); + + expect(mockLog.debug).toHaveBeenCalledWith( + "[test-id-mcots] Received data: 746573742d64617461", + ); + expect(mockLog.debug).toHaveBeenCalledWith( + "[test-id-mcots] Initial packet(str): ServerPacket {length: 0, sequence: 0, messageId: 0}", + ); + expect(mockLog.debug).toHaveBeenCalledWith( + "[test-id-mcots] initial Packet(hex): 746573742d64617461", + ); + }); + + it("should log socket end event", async () => { + const mockSocket = { + localPort: 43300, + on: vi.fn((event, callback) => { + if (event === "end") { + callback(); + } + }), + }; + const mockLogger = { + error: vi.fn(), + debug: vi.fn(), + }; + const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id" }; + + await mcotsPortRouter({ taggedSocket, log: mockLogger }); + + expect(mockLogger.debug).toHaveBeenCalledWith("[test-id] Socket closed"); + }); + + it("should log socket error event", async () => { + const mockSocket = { + localPort: 43300, + on: vi.fn((event, callback) => { + if (event === "error") { + callback(new Error("test-error")); + } + }), + }; + const mockLogger = { + error: vi.fn(), + debug: vi.fn(), + }; + const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id" }; + + await mcotsPortRouter({ taggedSocket, log: mockLogger }); + + expect(mockLogger.error).toHaveBeenCalledWith( + "[test-id] Socket error: Error: test-error", + ); + }); +}); diff --git a/packages/gateway/test/npsPortRouter.test.ts b/packages/gateway/test/npsPortRouter.test.ts index 1f528af13..02b01e72f 100644 --- a/packages/gateway/test/npsPortRouter.test.ts +++ b/packages/gateway/test/npsPortRouter.test.ts @@ -1,10 +1,13 @@ -import { describe, it, expect, vi } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; import { npsPortRouter } from "../src/npsPortRouter.js"; import type { TaggedSocket } from "../src/socketUtility.js"; import { GamePacket } from "rusty-motors-shared-packets"; -import { write } from "fs"; describe("npsPortRouter", () => { + beforeEach(() => { + vi.resetAllMocks(); + }); + it("should log an error and close the socket if local port is undefined", async () => { const mockSocket = { localPort: undefined, @@ -52,7 +55,7 @@ describe("npsPortRouter", () => { it("should handle data event and route initial message", async () => { const mockSocket = { - localPort: 7003, + localPort: 8228, write: vi.fn(), on: vi.fn((event, callback) => { if (event === "data") { @@ -64,7 +67,7 @@ describe("npsPortRouter", () => { error: vi.fn(), debug: vi.fn(), }; - const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id" }; + const taggedSocket: TaggedSocket = { socket: mockSocket, id: "test-id-nps" }; const mockGamePacket = { deserialize: vi.fn(), @@ -80,13 +83,13 @@ describe("npsPortRouter", () => { await npsPortRouter({ taggedSocket, log: mockLogger }); expect(mockLogger.debug).toHaveBeenCalledWith( - "[test-id] Received data: 010203", + "[test-id-nps] Received data: 010203", ); expect(mockLogger.debug).toHaveBeenCalledWith( - "[test-id] Initial packet(str): GamePacket {length: 0, messageId: 0}", + "[test-id-nps] Initial packet(str): GamePacket {length: 0, messageId: 0}", ); expect(mockLogger.debug).toHaveBeenCalledWith( - "[test-id] initial Packet(hex): 010203", + "[test-id-nps] initial Packet(hex): 010203", ); }); diff --git a/packages/gateway/tsconfig.json b/packages/gateway/tsconfig.json index 789270755..9d9e04652 100644 --- a/packages/gateway/tsconfig.json +++ b/packages/gateway/tsconfig.json @@ -4,5 +4,5 @@ "incremental": true, "composite": true }, - "include": ["index.ts", "src/**/*.ts"], + "include": ["index.ts", "src/**/*.ts", "test/mcotsPortRouter.test.ts"], } From 3faae732d0bd37b503e760ea2942cb094b89ca2b Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Sat, 12 Oct 2024 10:01:39 -0400 Subject: [PATCH 20/23] refactor(GamePacket): update return type in copy method --- packages/shared-packets/src/GamePacket.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared-packets/src/GamePacket.ts b/packages/shared-packets/src/GamePacket.ts index 31c27ed0a..20457dd7e 100644 --- a/packages/shared-packets/src/GamePacket.ts +++ b/packages/shared-packets/src/GamePacket.ts @@ -17,7 +17,7 @@ export class GamePacket extends BasePacket implements SerializableMessage { * @param originalPacket - The original `GamePacket` to be copied. * @param newData - An optional `Buffer` containing new data to be deserialized into the new packet. * If not provided, the data from the original packet will be copied. - * @returns A new `ServerPacket` instance with the same message ID and header as the original, + * @returns A new `GamePacket` instance with the same message ID and header as the original, * and either the deserialized new data or a copy of the original data. */ static copy(originalPacket: GamePacket, newData?: Buffer): GamePacket { From e7623e2bc58cc43f9893b4ab0011e157d7fc4e9f Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Sat, 12 Oct 2024 10:03:06 -0400 Subject: [PATCH 21/23] refactor(GamePacket): update error message in setDataBuffer method --- packages/shared-packets/src/GamePacket.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shared-packets/src/GamePacket.ts b/packages/shared-packets/src/GamePacket.ts index 20457dd7e..0cc5dad95 100644 --- a/packages/shared-packets/src/GamePacket.ts +++ b/packages/shared-packets/src/GamePacket.ts @@ -44,7 +44,7 @@ export class GamePacket extends BasePacket implements SerializableMessage { override setDataBuffer(data: Buffer): GamePacket { if (this.data.getByteSize() > 2) { throw new Error( - `ServerPacket data buffer is already set, use copy() to create a new ServerPacket`, + `GamePacket data buffer is already set, use copy() to create a new ServerPacket`, ); } From 4cc5a3616042cada910d01db995ba3bd78e8a4b6 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Sat, 12 Oct 2024 10:23:13 -0400 Subject: [PATCH 22/23] refactor(mcotsPortRouter): handle error when parsing initial message --- packages/gateway/src/mcotsPortRouter.ts | 34 +++++++++++++------------ 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/packages/gateway/src/mcotsPortRouter.ts b/packages/gateway/src/mcotsPortRouter.ts index 48fe0ebbb..4663123ad 100644 --- a/packages/gateway/src/mcotsPortRouter.ts +++ b/packages/gateway/src/mcotsPortRouter.ts @@ -35,19 +35,23 @@ export async function mcotsPortRouter({ // Handle the socket connection here socket.on("data", (data) => { - log.debug(`[${id}] Received data: ${data.toString("hex")}`); - const initialPacket = parseInitialMessage(data); - log.debug(`[${id}] Initial packet(str): ${initialPacket}`); - log.debug(`[${id}] initial Packet(hex): ${initialPacket.toHexString()}`); - routeInitialMessage(id, port, initialPacket) - .then((response) => { - // Send the response back to the client - log.debug(`[${id}] Sending response: ${response.toString("hex")}`); - socket.write(response); - }) - .catch((error) => { - log.error(`[${id}] Error routing initial message: ${error}`); - }); + try { + log.debug(`[${id}] Received data: ${data.toString("hex")}`); + const initialPacket = parseInitialMessage(data); + log.debug(`[${id}] Initial packet(str): ${initialPacket}`); + log.debug(`[${id}] initial Packet(hex): ${initialPacket.toHexString()}`); + routeInitialMessage(id, port, initialPacket) + .then((response) => { + // Send the response back to the client + log.debug(`[${id}] Sending response: ${response.toString("hex")}`); + socket.write(response); + }) + .catch((error) => { + log.error(`[${id}] Error routing initial message: ${error}`); + }); + } catch (error) { + log.error(`[${id}] Error parsing initial message: ${error}`); + } }); socket.on("end", () => { @@ -74,9 +78,7 @@ async function routeInitialMessage( // Route the initial message to the appropriate handler // Messages may be encrypted, this will be handled by the handler - log.debug( - `Routing message for port ${port}: ${initialPacket.toHexString()}`, - ); + log.debug(`Routing message for port ${port}: ${initialPacket.toHexString()}`); let responses: SerializableInterface[] = []; switch (port) { From 49defc2ee706000cf7572f6a3793bf741ddbba3e Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Sat, 12 Oct 2024 10:23:23 -0400 Subject: [PATCH 23/23] refactor(State): remove unused onDataHandlers property --- packages/gateway/src/GatewayServer.ts | 20 ---------- packages/shared/index.ts | 2 - packages/shared/src/State.ts | 54 --------------------------- 3 files changed, 76 deletions(-) diff --git a/packages/gateway/src/GatewayServer.ts b/packages/gateway/src/GatewayServer.ts index 1a518ce75..e4de3b2cf 100644 --- a/packages/gateway/src/GatewayServer.ts +++ b/packages/gateway/src/GatewayServer.ts @@ -305,26 +305,6 @@ export class Gateway { /** @type {Gateway | undefined} */ Gateway._instance = undefined; -/** - * Registers various data handlers to the provided state. - * - * This function adds handlers for different types of data, such as login data, - * chat data, persona data, lobby data, and transaction data. Each handler is - * associated with a specific code. - * - * @param state - The initial state to which the data handlers will be added. - * @returns The updated state with all the data handlers registered. - */ -function registerDataHandlers(state: State) { - state = addOnDataHandler(state, 8226, receiveLoginData); - state = addOnDataHandler(state, 8227, receiveChatData); - state = addOnDataHandler(state, 8228, receivePersonaData); - state = addOnDataHandler(state, 7003, receiveLobbyData); - state = addOnDataHandler(state, 9000, receiveChatData); - state = addOnDataHandler(state, 43300, receiveTransactionsData); - return state; -} - /** * Get a singleton instance of GatewayServer * diff --git a/packages/shared/index.ts b/packages/shared/index.ts index 071b27dce..5750f7d46 100644 --- a/packages/shared/index.ts +++ b/packages/shared/index.ts @@ -26,8 +26,6 @@ export { addSession, createInitialState, fetchStateFromDatabase, - addOnDataHandler, - getOnDataHandler, addEncryption, getEncryption, McosSession, diff --git a/packages/shared/src/State.ts b/packages/shared/src/State.ts index 8def5ad46..221d2ed16 100644 --- a/packages/shared/src/State.ts +++ b/packages/shared/src/State.ts @@ -181,7 +181,6 @@ export interface State { encryptions: Record; sessions: Record; // queuedConnections: Record; - onDataHandlers: Record; save: (state?: State) => void; } @@ -212,7 +211,6 @@ export function createInitialState({ encryptions: {}, sessions: {}, // queuedConnections: {}, - onDataHandlers: {}, save: function (state?: State) { if (typeof state === "undefined") { state = this as State; @@ -226,58 +224,6 @@ export function createInitialState({ }; } -/** - * Add a data handler to the state. - * - * This function adds a data handler to the state. - * The returned state is a new state object, and the original state is not - * modified. You should then call the save function on the new state to update - * the database. - * - * @param {State} state The state to add the data handler to. - * @param {number} port The port to add the data handler for. - * @param {OnDataHandler} handler The data - * handler to - * add. - * @returns {State} The state with the data handler added. - */ -export function addOnDataHandler( - state: State, - port: number, - handler: OnDataHandler, -): State { - const onDataHandlers = state.onDataHandlers; - onDataHandlers[port.toString()] = handler; - const newState = { - ...state, - onDataHandlers, - }; - return newState; -} - -/** - * Get a data handler for a port from the state. - * - * This function gets a data handler for a port from the state. - * - * @param {State} state The state to get the data handler from. - * @param {number} port The port to get the data handler for. - * @returns {OnDataHandler | undefined} The - * data - * handler - * for the - * given port, - * or undefined - * if no data - * handler exists - */ -export function getOnDataHandler( - state: State, - port: number, -): OnDataHandler | undefined { - return state.onDataHandlers[port.toString()]; -} - /** * Add an encryption to the state. *