From dd26e11b69262c48a0a52770de693831854bceb7 Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Fri, 11 Oct 2024 07:49:46 -0400 Subject: [PATCH] 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(); });