diff --git a/packages/main/src/NPSMessage.js b/packages/main/src/NPSMessage.js new file mode 100644 index 0000000..cf3f776 --- /dev/null +++ b/packages/main/src/NPSMessage.js @@ -0,0 +1,45 @@ +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..11dad46 --- /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..3b99c89 --- /dev/null +++ b/packages/main/src/NPSUserLoginPayload.js @@ -0,0 +1,73 @@ +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..e413578 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); }); } @@ -133,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 0e4dd49..302ffdf 100644 --- a/packages/main/src/nps.js +++ b/packages/main/src/nps.js @@ -1,8 +1,36 @@ +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()}`); }