From a2d9deb08ad4e20cb9e42082bab4ead3eda766ca Mon Sep 17 00:00:00 2001 From: Molly Draven Date: Sat, 1 Jun 2024 17:24:30 -0400 Subject: [PATCH 1/2] refactor: Add NPS data handling and web request handling functions --- packages/main/src/NPSMessage.js | 42 +++++++++++ packages/main/src/NPSMessageHeader.js | 69 ++++++++++++++++++ packages/main/src/NPSMessagePayload.js | 91 ++++++++++++++++++++++++ packages/main/src/NPSUserLoginPayload.js | 71 ++++++++++++++++++ packages/main/src/index.js | 15 +++- packages/main/src/nps.js | 33 ++++++++- 6 files changed, 316 insertions(+), 5 deletions(-) create mode 100644 packages/main/src/NPSMessage.js create mode 100644 packages/main/src/NPSMessageHeader.js create mode 100644 packages/main/src/NPSMessagePayload.js create mode 100644 packages/main/src/NPSUserLoginPayload.js diff --git a/packages/main/src/NPSMessage.js b/packages/main/src/NPSMessage.js new file mode 100644 index 0000000..92944f4 --- /dev/null +++ b/packages/main/src/NPSMessage.js @@ -0,0 +1,42 @@ +import { NPSMessageHeader } from "./NPSMessageHeader.js"; +import { NPSMessagePayload } from "./NPSMessagePayload.js"; + +export class NPSMessage { + constructor() { + this._header = new NPSMessageHeader(); + this.data = new NPSMessagePayload(); + } + /** + * + * @param {Buffer} data + * @returns {NPSMessage} + */ + static parse(data) { + const self = new NPSMessage(); + if (data.length < 8) { + throw new Error(`Invalid message length: ${data.length}`); + } + + self._header = NPSMessageHeader.parse(data); + + const expectedLength = self._header.messageLength - self._header.dataOffset; + + self.data = NPSMessagePayload.parse(data.subarray(self._header.dataOffset), expectedLength); + + return self; + } + + /** + * @returns Buffer + */ + toBuffer() { + return Buffer.concat([this._header.toBuffer(), this.data.toBuffer()]); + } + + /** + * @returns string + */ + toString() { + return `${this._header.toString()}, Data: ${this.data.toString()}`; + } +} diff --git a/packages/main/src/NPSMessageHeader.js b/packages/main/src/NPSMessageHeader.js new file mode 100644 index 0000000..eb37b24 --- /dev/null +++ b/packages/main/src/NPSMessageHeader.js @@ -0,0 +1,69 @@ +export class NPSMessageHeader { + constructor() { + this._dataStart = -1; + this.messageId = -1; + this.messageLength = -1; + this.version = -1; + } + + /** + * + * @param {Buffer} data + * @returns NPSMessageHeader + */ + static parse(data) { + const self = new NPSMessageHeader(); + if (data.length < 6) { + throw new Error("Invalid header length"); + } + self.messageId = data.readUInt16BE(0); + self.messageLength = data.readUInt16BE(2); + + self.version = data.readUInt16BE(4); + + if (self.version === 257) { + self._dataStart = 12; + } else { + self._dataStart = 6; + } + + return self; + } + + get dataOffset() { + return this._dataStart; + } + + /** + * @private + * @returns Buffer + */ + _writeExtraData() { + const buffer = Buffer.alloc(6); + buffer.writeUInt16BE(0, 0); + buffer.writeUInt32BE(this.messageLength, 2); + return buffer; + } + + /** + * @returns Buffer + */ + toBuffer() { + let buffer = Buffer.alloc(6); + buffer.writeUInt16BE(this.messageId, 0); + buffer.writeUInt16BE(this.messageLength, 2); + buffer.writeUInt16BE(this.version, 4); + + if (this.version === 257) { + return Buffer.concat([buffer, this._writeExtraData()]); + } + return buffer; + } + + /** + * @returns string + */ + toString() { + return `ID: ${this.messageId}, Length: ${this.messageLength}, Version: ${this.version}`; + } +} diff --git a/packages/main/src/NPSMessagePayload.js b/packages/main/src/NPSMessagePayload.js new file mode 100644 index 0000000..0b3acc4 --- /dev/null +++ b/packages/main/src/NPSMessagePayload.js @@ -0,0 +1,91 @@ +/** + * @interface INPSPayload + */ +/** + * @interface INPSPayload + * @static parse + * @property {Buffer} data + */ +export class INPSPayload { + constructor() { + this.data = Buffer.alloc(0); + this.toBuffer = function () { }; + this.toString = function () { }; + } + + /** + * @param {Buffer} data + * @returns INPSPayload + */ + static parse(data) { + const self = new NPSMessagePayload(); + self.data = data; + return self; + } +} + +/** + * To be used as a base class for NPS message payloads. + * + * @implements {INPSPayload} + * @class + * @property {Buffer} data + * + * @example + * class MyPayload extends NPSMessagePayload { + * constructor() { + * super(); + * this.myProperty = 0; + * } + * + * static parse(data) { + * this.myProperty = data.readUInt32LE(0); + * } + * + * toBuffer() { + * const buffer = Buffer.alloc(4); + * buffer.writeUInt32LE(this.myProperty, 0); + * return buffer; + * } + * + * toString() { + * return `MyPayload: ${this.myProperty}`; + * } + * } + */ + +export class NPSMessagePayload { + constructor() { + this.data = Buffer.alloc(0); + } + + /** + * + * @param {Buffer} data + * @returns NPSMessagePayload + */ + static parse(data, len = data.length) { + if (data.length !== len) { + throw new Error( + `Invalid payload length: ${data.length}, expected: ${len}` + ); + } + const self = new NPSMessagePayload(); + self.data = data; + return self; + } + + /** + * @returns Buffer + */ + toBuffer() { + return this.data; + } + + /** + * @returns string + */ + toString() { + return this.data.toString("hex"); + } +} diff --git a/packages/main/src/NPSUserLoginPayload.js b/packages/main/src/NPSUserLoginPayload.js new file mode 100644 index 0000000..6440d47 --- /dev/null +++ b/packages/main/src/NPSUserLoginPayload.js @@ -0,0 +1,71 @@ +import { NPSMessagePayload } from "./NPSMessagePayload.js"; + +/** + * @typedef INPSPayload + * @type {import("./NPSMessagePayload.js").INPSPayload} + */ + +/** + * @implements {INPSPayload} + * @extends {NPSMessagePayload} + * Payload for the NPSUserLogin message. + */ +export class NPSUserLoginPayload extends NPSMessagePayload { + constructor() { + super(); + this.data = Buffer.alloc(0); + this.ticket = ""; + this.sessionKey = ""; + this.gameId = ""; + } + + /** + * + * @param {number} len + * @param {Buffer} data + * @returns {NPSUserLoginPayload} + */ + static parse(data, len = data.length) { + if (data.length !== len) { + throw new Error( + `Invalid payload length: ${data.length}, expected: ${len}` + ); + } + + const self = new NPSUserLoginPayload(); + try { + let offset = 0; + let nextLen = data.readUInt16BE(0); + self.ticket = data.toString("utf8", 2, nextLen + 2); + offset = nextLen + 2; + offset += 2; // Skip one empty word + nextLen = data.readUInt16BE(offset); + self.sessionKey = data.toString("hex", offset + 2, offset + 2 + nextLen); + offset += nextLen + 2; + nextLen = data.readUInt16BE(offset); + self.gameId = data.subarray(offset + 2, offset + 2 + nextLen).toString("utf8"); + } catch (error) { + if (!(error instanceof Error)) { + throw new Error(`Error parsing payload: ${error}`); + } + console.error(`Error parsing payload: ${error.message}`); + throw new Error(`Error parsing payload: ${error.message}`); + } + + return self; + } + + /** + * @returns {Buffer} + */ + toBuffer() { + throw new Error("Method not implemented."); + } + + /** + * @returns {string} + */ + toString() { + return `Ticket: ${this.ticket}, SessionKey: ${this.sessionKey}, GameId: ${this.gameId}`; + } +} diff --git a/packages/main/src/index.js b/packages/main/src/index.js index 454a323..81727da 100644 --- a/packages/main/src/index.js +++ b/packages/main/src/index.js @@ -21,6 +21,8 @@ import { UserLoginService } from "./UserLoginService.js"; import { WebServer } from "./WebServer.js"; import { onNPSData } from "./nps.js"; import { onWebRequest } from "./web.js"; +import crypto from "node:crypto"; +import * as Sentry from "@sentry/node"; /** @type {WebServer} */ let authServer; @@ -33,12 +35,21 @@ let personaServer; /** * @param {import("node:net").Socket} socket - * @param {(port:number, data: Buffer) => void} onData + * @param {(port:number, data: Buffer, sendToClient: (data: Buffer) => void) => void} onData */ function onSocketConnection(socket, onData) { console.log("Connection established"); + + const connectionId = crypto.randomUUID(); + + Sentry.setTag("connection_id", connectionId); + + const sendToClient = (/** @type {Buffer} */ data) => { + socket.write(data); + }; + socket.on("data", (data) => { - onData(socket.localPort ?? -1, data); + onData(socket.localPort ?? -1, data, sendToClient); }); } diff --git a/packages/main/src/nps.js b/packages/main/src/nps.js index 0e4dd49..5cbfd7c 100644 --- a/packages/main/src/nps.js +++ b/packages/main/src/nps.js @@ -1,8 +1,35 @@ +import { NPSMessage } from "./NPSMessage.js"; +import { NPSUserLoginPayload } from "./NPSUserLoginPayload.js"; + +/** + * @typedef INPSPayload + * @type {import("./NPSMessagePayload.js").INPSPayload} + */ + +/** @type {Map INPSPayload>} */ +const payloadMap = new Map(); + + + +payloadMap.set(1281, NPSUserLoginPayload.parse); + /** * @param {number} port * @param {Buffer} data + * @param {(data: Buffer) => void} sendToClient */ -export function onNPSData(port, data) { - const hex = data.toString("hex"); - console.log(`Data received: ${hex}`); +export function onNPSData(port, data, sendToClient) { + const message = NPSMessage.parse(data); + console.log(`Received message on port ${port}: ${message.toString()}`); + + const messageType = payloadMap.get(message._header.messageId); + + if (!messageType) { + console.error(`Unknown message type: ${message._header.messageId}`); + return; + } + + const payload = messageType(message.data.data, message._header.messageLength - message._header.dataOffset); + + console.log(`Parsed payload: ${payload.toString()}`); } From 410512fa03995e0a17f7deeb743e857f0820b79a Mon Sep 17 00:00:00 2001 From: "deepsource-autofix[bot]" <62050782+deepsource-autofix[bot]@users.noreply.github.com> Date: Sat, 1 Jun 2024 21:24:51 +0000 Subject: [PATCH 2/2] style: format code with Prettier This commit fixes the style issues introduced in a2d9deb according to the output from Prettier. Details: https://github.com/rustymotors/obsidian/pull/19 --- packages/main/src/NPSMessage.js | 5 +++- packages/main/src/NPSMessagePayload.js | 30 ++++++++++++------------ packages/main/src/NPSUserLoginPayload.js | 6 +++-- packages/main/src/index.js | 2 +- packages/main/src/nps.js | 13 +++++----- 5 files changed, 31 insertions(+), 25 deletions(-) diff --git a/packages/main/src/NPSMessage.js b/packages/main/src/NPSMessage.js index 92944f4..cf3f776 100644 --- a/packages/main/src/NPSMessage.js +++ b/packages/main/src/NPSMessage.js @@ -21,7 +21,10 @@ export class NPSMessage { const expectedLength = self._header.messageLength - self._header.dataOffset; - self.data = NPSMessagePayload.parse(data.subarray(self._header.dataOffset), expectedLength); + self.data = NPSMessagePayload.parse( + data.subarray(self._header.dataOffset), + expectedLength, + ); return self; } diff --git a/packages/main/src/NPSMessagePayload.js b/packages/main/src/NPSMessagePayload.js index 0b3acc4..11dad46 100644 --- a/packages/main/src/NPSMessagePayload.js +++ b/packages/main/src/NPSMessagePayload.js @@ -7,21 +7,21 @@ * @property {Buffer} data */ export class INPSPayload { - constructor() { - this.data = Buffer.alloc(0); - this.toBuffer = function () { }; - this.toString = function () { }; - } + constructor() { + this.data = Buffer.alloc(0); + this.toBuffer = function () {}; + this.toString = function () {}; + } - /** - * @param {Buffer} data - * @returns INPSPayload - */ - static parse(data) { - const self = new NPSMessagePayload(); - self.data = data; - return self; - } + /** + * @param {Buffer} data + * @returns INPSPayload + */ + static parse(data) { + const self = new NPSMessagePayload(); + self.data = data; + return self; + } } /** @@ -67,7 +67,7 @@ export class NPSMessagePayload { static parse(data, len = data.length) { if (data.length !== len) { throw new Error( - `Invalid payload length: ${data.length}, expected: ${len}` + `Invalid payload length: ${data.length}, expected: ${len}`, ); } const self = new NPSMessagePayload(); diff --git a/packages/main/src/NPSUserLoginPayload.js b/packages/main/src/NPSUserLoginPayload.js index 6440d47..3b99c89 100644 --- a/packages/main/src/NPSUserLoginPayload.js +++ b/packages/main/src/NPSUserLoginPayload.js @@ -28,7 +28,7 @@ export class NPSUserLoginPayload extends NPSMessagePayload { static parse(data, len = data.length) { if (data.length !== len) { throw new Error( - `Invalid payload length: ${data.length}, expected: ${len}` + `Invalid payload length: ${data.length}, expected: ${len}`, ); } @@ -43,7 +43,9 @@ export class NPSUserLoginPayload extends NPSMessagePayload { self.sessionKey = data.toString("hex", offset + 2, offset + 2 + nextLen); offset += nextLen + 2; nextLen = data.readUInt16BE(offset); - self.gameId = data.subarray(offset + 2, offset + 2 + nextLen).toString("utf8"); + self.gameId = data + .subarray(offset + 2, offset + 2 + nextLen) + .toString("utf8"); } catch (error) { if (!(error instanceof Error)) { throw new Error(`Error parsing payload: ${error}`); diff --git a/packages/main/src/index.js b/packages/main/src/index.js index 81727da..e413578 100644 --- a/packages/main/src/index.js +++ b/packages/main/src/index.js @@ -144,7 +144,7 @@ export default function main() { "Rusty Motors", "A test shard", "10.10.5.20", - "Group - 1" + "Group - 1", ); const userLoginService = new UserLoginService(); diff --git a/packages/main/src/nps.js b/packages/main/src/nps.js index 5cbfd7c..302ffdf 100644 --- a/packages/main/src/nps.js +++ b/packages/main/src/nps.js @@ -1,16 +1,14 @@ import { NPSMessage } from "./NPSMessage.js"; import { NPSUserLoginPayload } from "./NPSUserLoginPayload.js"; -/** - * @typedef INPSPayload +/** + * @typedef INPSPayload * @type {import("./NPSMessagePayload.js").INPSPayload} - */ + */ /** @type {Map INPSPayload>} */ const payloadMap = new Map(); - - payloadMap.set(1281, NPSUserLoginPayload.parse); /** @@ -29,7 +27,10 @@ export function onNPSData(port, data, sendToClient) { return; } - const payload = messageType(message.data.data, message._header.messageLength - message._header.dataOffset); + const payload = messageType( + message.data.data, + message._header.messageLength - message._header.dataOffset, + ); console.log(`Parsed payload: ${payload.toString()}`); }