This commit is contained in:
2025-09-03 19:18:44 +01:00
parent eb620087c9
commit 695964a636
29 changed files with 631 additions and 571 deletions

View File

@@ -1,5 +1,3 @@
{ {
"recommendations": [ "recommendations": ["orta.vscode-jest"]
"orta.vscode-jest"
]
} }

32
.vscode/launch.json vendored
View File

@@ -1,20 +1,16 @@
{ {
// Use IntelliSense to learn about possible attributes. // Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes. // Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "pwa-node", "type": "pwa-node",
"request": "launch", "request": "launch",
"name": "Launch Program", "name": "Launch Program",
"skipFiles": [ "skipFiles": ["<node_internals>/**"],
"<node_internals>/**" "program": "${workspaceFolder}/dist/index.js",
], "outFiles": ["${workspaceFolder}/**/*.js"]
"program": "${workspaceFolder}/dist/index.js", }
"outFiles": [ ]
"${workspaceFolder}/**/*.js"
]
}
]
} }

View File

@@ -1,3 +1,3 @@
{ {
"editor.tabSize": 2 "editor.tabSize": 2
} }

30
.vscode/tasks.json vendored
View File

@@ -1,18 +1,16 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{ {
"type": "typescript", "type": "typescript",
"tsconfig": "tsconfig.json", "tsconfig": "tsconfig.json",
"option": "watch", "option": "watch",
"problemMatcher": [ "problemMatcher": ["$tsc-watch"],
"$tsc-watch" "group": {
], "kind": "build",
"group": { "isDefault": true
"kind": "build", },
"isDefault": true "label": "tsc: watch - tsconfig.json"
}, }
"label": "tsc: watch - tsconfig.json" ]
}
]
} }

View File

@@ -12,11 +12,11 @@
## Connection Endpoints ## Connection Endpoints
| Protocol | Endpoint | | Protocol | Endpoint |
|----------|----------| | --------------- | --------------------------------- |
| TCP | `chat.3t.network:10009` | | TCP | `chat.3t.network:10009` |
| WebSocket (TLS) | `wss://chat.3t.network:443/BENNC` | | WebSocket (TLS) | `wss://chat.3t.network:443/BENNC` |
| WebSocket | `ws://chat.3t.network:80/BENNC` | | WebSocket | `ws://chat.3t.network:80/BENNC` |
## Core Requirements ## Core Requirements
@@ -30,6 +30,7 @@
## Message Structure ## Message Structure
**Client → Server:** **Client → Server:**
``` ```
┌─────────────┬─────────────┬─────────────────────────┐ ┌─────────────┬─────────────┬─────────────────────────┐
│ Message Type│ Length │ Data │ │ Message Type│ Length │ Data │
@@ -38,6 +39,7 @@
``` ```
**Server → Client:** **Server → Client:**
``` ```
┌─────────────┬─────────────┬─────────────┬─────────────────────┐ ┌─────────────┬─────────────┬─────────────┬─────────────────────┐
│ Message Type│ Sender ID │ Length │ Data │ │ Message Type│ Sender ID │ Length │ Data │
@@ -45,7 +47,7 @@
└─────────────┴─────────────┴─────────────┴─────────────────────┘ └─────────────┴─────────────┴─────────────┴─────────────────────┘
``` ```
*Sender ID is randomly generated per connection to identify message source.* _Sender ID is randomly generated per connection to identify message source._
## Encryption Implementation ## Encryption Implementation
@@ -67,22 +69,23 @@
## Message Types Reference ## Message Types Reference
| ID | Name | Subscribable | Encrypted | Compressed | Purpose | | ID | Name | Subscribable | Encrypted | Compressed | Purpose |
|----|------|--------------|-----------|------------|---------| | ------ | ------------------ | ------------ | --------- | ---------- | ----------------------------- |
| 0x0000 | Subscribe | ❌ | ❌ | ❌ | Subscribe to message type | | 0x0000 | Subscribe | ❌ | ❌ | ❌ | Subscribe to message type |
| 0x0001 | Basic Message | ✅ | ✅ | ❌ | Chat messages | | 0x0001 | Basic Message | ✅ | ✅ | ❌ | Chat messages |
| 0x0002 | Request User Data | ✅ | ✅ | ❌ | Request user info | | 0x0002 | Request User Data | ✅ | ✅ | ❌ | Request user info |
| 0x0003 | User Data Response | ✅ | ✅ | ❌ | Respond with user info | | 0x0003 | User Data Response | ✅ | ✅ | ❌ | Respond with user info |
| 0x0005 | Keepalive | ❌ | ❌ | ❌ | Prevent connection timeout | | 0x0005 | Keepalive | ❌ | ❌ | ❌ | Prevent connection timeout |
| 0x0006 | Advanced Text | ✅ | ✅ | ✅ | Long/rich text messages | | 0x0006 | Advanced Text | ✅ | ✅ | ✅ | Long/rich text messages |
| 0x0007 | Edit Advanced Text | ✅ | ✅ | ✅ | Edit/delete advanced text | | 0x0007 | Edit Advanced Text | ✅ | ✅ | ✅ | Edit/delete advanced text |
| 0xFFFF | Unsubscribe | ❌ | ❌ | ❌ | Unsubscribe from message type | | 0xFFFF | Unsubscribe | ❌ | ❌ | ❌ | Unsubscribe from message type |
--- ---
## Message Type Specifications ## Message Type Specifications
### Subscribe (0x0000) ### Subscribe (0x0000)
Subscribe to receive messages of specified type. Must resubscribe after disconnect. Subscribe to receive messages of specified type. Must resubscribe after disconnect.
**Data:** Message type to subscribe to (2 bytes, big-endian) **Data:** Message type to subscribe to (2 bytes, big-endian)
@@ -90,6 +93,7 @@ Subscribe to receive messages of specified type. Must resubscribe after disconne
--- ---
### Basic Message (0x0001) ### Basic Message (0x0001)
UTF-8 chat messages. 16-byte nonce + encrypted data ≤ 1000 bytes total. UTF-8 chat messages. 16-byte nonce + encrypted data ≤ 1000 bytes total.
**Data:** Encrypted UTF-8 string **Data:** Encrypted UTF-8 string
@@ -97,9 +101,11 @@ UTF-8 chat messages. 16-byte nonce + encrypted data ≤ 1000 bytes total.
--- ---
### Request User Data (0x0002) ### Request User Data (0x0002)
Request user information from all users. Clients should respond with User Data Response (0x0003). Request user information from all users. Clients should respond with User Data Response (0x0003).
**Data Structure:** **Data Structure:**
- Username length (2 bytes, big-endian) - Username length (2 bytes, big-endian)
- Username (up to 32 bytes, UTF-8) - Username (up to 32 bytes, UTF-8)
- Color RGB (3 bytes: R, G, B values) - Color RGB (3 bytes: R, G, B values)
@@ -109,9 +115,11 @@ Request user information from all users. Clients should respond with User Data R
--- ---
### User Data Response (0x0003) ### User Data Response (0x0003)
Send user information in response to Request User Data (0x0002). Send user information in response to Request User Data (0x0002).
**Data Structure:** Same as Request User Data **Data Structure:** Same as Request User Data
- Username length (2 bytes, big-endian) - Username length (2 bytes, big-endian)
- Username (up to 32 bytes, UTF-8) - Username (up to 32 bytes, UTF-8)
- Color RGB (3 bytes: R, G, B values) - Color RGB (3 bytes: R, G, B values)
@@ -121,6 +129,7 @@ Send user information in response to Request User Data (0x0002).
--- ---
### Keepalive (0x0005) ### Keepalive (0x0005)
Prevent connection timeout when idle. Send every 30 seconds when no other traffic. Not forwarded to other clients and cannot be subscribed to. Prevent connection timeout when idle. Send every 30 seconds when no other traffic. Not forwarded to other clients and cannot be subscribed to.
**Data:** None - empty message **Data:** None - empty message
@@ -128,6 +137,7 @@ Prevent connection timeout when idle. Send every 30 seconds when no other traffi
--- ---
### Unsubscribe (0xFFFF) ### Unsubscribe (0xFFFF)
Stop receiving messages of specified type. Stop receiving messages of specified type.
**Data:** Message type to unsubscribe from (2 bytes, big-endian) **Data:** Message type to unsubscribe from (2 bytes, big-endian)
@@ -135,9 +145,11 @@ Stop receiving messages of specified type.
--- ---
### Advanced Text (0x0006) ### Advanced Text (0x0006)
Multi-packet messages for long text with markdown formatting and ZSTD compression. Multi-packet messages for long text with markdown formatting and ZSTD compression.
**Implementation Notes:** **Implementation Notes:**
- Text is compressed **before** splitting into packets - Text is compressed **before** splitting into packets
- Header is not compressed - Header is not compressed
- Packets may arrive out of order - use packet numbers to reconstruct - Packets may arrive out of order - use packet numbers to reconstruct
@@ -145,12 +157,14 @@ Multi-packet messages for long text with markdown formatting and ZSTD compressio
- Warn users before displaying very large messages - Warn users before displaying very large messages
**Data Structure:** **Data Structure:**
- Message ID (4 bytes, big-endian) - Message ID (4 bytes, big-endian)
- Packet number (2 bytes, big-endian, 0-indexed) - Packet number (2 bytes, big-endian, 0-indexed)
- Final packet number (2 bytes, big-endian, 0-indexed) - Final packet number (2 bytes, big-endian, 0-indexed)
- Compressed markdown text (remaining bytes, ZSTD) - Compressed markdown text (remaining bytes, ZSTD)
### Edit Advanced Text (0x0007) ### Edit Advanced Text (0x0007)
Edit or delete existing Advanced Text messages. Use same Message ID to replace existing message. Empty compressed text deletes the message. Client replaces original when final packet received. Edit or delete existing Advanced Text messages. Use same Message ID to replace existing message. Empty compressed text deletes the message. Client replaces original when final packet received.
**Data Structure:** Same as Advanced Text (0x0006) **Data Structure:** Same as Advanced Text (0x0006)
@@ -160,12 +174,14 @@ Edit or delete existing Advanced Text messages. Use same Message ID to replace e
## Implementation Guidelines ## Implementation Guidelines
### Validation Requirements ### Validation Requirements
- All length fields must not exceed their specified maximums - All length fields must not exceed their specified maximums
- Username/Client ID lengths must match actual string lengths - Username/Client ID lengths must match actual string lengths
- Message data must not exceed 1000 bytes (including nonce) - Message data must not exceed 1000 bytes (including nonce)
- Packet numbers must be sequential and within final packet range - Packet numbers must be sequential and within final packet range
### Connection Lifecycle ### Connection Lifecycle
1. **Connect** to server (TCP/WebSocket) 1. **Connect** to server (TCP/WebSocket)
2. **Subscribe** to required message types 2. **Subscribe** to required message types
3. **Send keepalive** every 30 seconds when idle 3. **Send keepalive** every 30 seconds when idle
@@ -173,6 +189,7 @@ Edit or delete existing Advanced Text messages. Use same Message ID to replace e
5. **Handle** out-of-order packets for Advanced Text 5. **Handle** out-of-order packets for Advanced Text
### Common Issues ### Common Issues
- **Encryption fails**: Ensure message type in additional data matches packet header - **Encryption fails**: Ensure message type in additional data matches packet header
- **Messages dropped**: Check total size ≤ 1000 bytes including nonce - **Messages dropped**: Check total size ≤ 1000 bytes including nonce
- **Connection timeout**: Verify keepalive frequency - **Connection timeout**: Verify keepalive frequency

View File

@@ -1,4 +1,5 @@
# BENNC-JS # BENNC-JS
[![Build Status](https://drone.jacknet.io/api/badges/TerribleCodeClub/bennc-js/status.svg)](https://drone.jacknet.io/TerribleCodeClub/bennc-js) [![Build Status](https://drone.jacknet.io/api/badges/TerribleCodeClub/bennc-js/status.svg)](https://drone.jacknet.io/TerribleCodeClub/bennc-js)
An implementation of the [BENNC](https://wiki.jacknet.io/books/simontech/chapter/bennc) client specification. An implementation of the [BENNC](https://wiki.jacknet.io/books/simontech/chapter/bennc) client specification.
@@ -8,15 +9,18 @@ An implementation of the [BENNC](https://wiki.jacknet.io/books/simontech/chapter
To build the BENNC-JS library, first clone this repository. To build the BENNC-JS library, first clone this repository.
Run the following commands from the root of the repository: Run the following commands from the root of the repository:
```bash ```bash
$ npm install $ npm install
$ npm run build $ npm run build
``` ```
The build output will be saved to the `dist` directory. The build output will be saved to the `dist` directory.
## Development instructions ## Development instructions
Requirements: Requirements:
- The latest LTS builds of Node and npm. - The latest LTS builds of Node and npm.
Follow the instructions below to lint, test and build BENNC-JS. Follow the instructions below to lint, test and build BENNC-JS.
@@ -34,6 +38,7 @@ $ npm run lint
$ npm install $ npm install
$ npm run test $ npm run test
``` ```
#### Build #### Build
```bash ```bash

View File

@@ -1,9 +1,9 @@
export { numberToUint16BE, numberToUint32BE } from './utilities/number' export { numberToUint16BE, numberToUint32BE } from "./utilities/number";
export { unpackIncomingPacket } from './messages/packet' export { unpackIncomingPacket } from "./messages/packet";
export { packers, unpackers } from './mapping' export { packers, unpackers } from "./mapping";
export { MessageTypes } from './common' export { MessageTypes } from "./common";
export { IncomingPacket, OutgoingPacket } from './messages/packet' export { IncomingPacket, OutgoingPacket } from "./messages/packet";
export { SubscribeMessage } from './messages/subscribe' export { SubscribeMessage } from "./messages/subscribe";
export { BasicMessage } from './messages/basic' export { BasicMessage } from "./messages/basic";
export { UserDataRequestMessage } from './messages/userDataRequest' export { UserDataRequestMessage } from "./messages/userDataRequest";
export { UserDataResponseMessage } from './messages/userDataResponse' export { UserDataResponseMessage } from "./messages/userDataResponse";

View File

@@ -1,12 +1,12 @@
import { encrypt } from 'romulus-js' import { encrypt } from "romulus-js";
import { DEFAULT_KEY, MessageTypes } from '../common' import { DEFAULT_KEY, MessageTypes } from "../common";
import { numberToUint16BE } from '../utilities/number' import { numberToUint16BE } from "../utilities/number";
import { packOutgoingPacket } from './packet' import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Basic) const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Basic);
export interface BasicMessage { export interface BasicMessage {
data: Uint8Array data: Uint8Array;
} }
/** /**
@@ -15,12 +15,15 @@ export interface BasicMessage {
* @param key The key to encrypt the data with. * @param key The key to encrypt the data with.
* @returns An encrypted outgoing basic message (0x0001) packet. * @returns An encrypted outgoing basic message (0x0001) packet.
*/ */
export function packBasicMessage (message: Uint8Array, key: Uint8Array = DEFAULT_KEY): Uint8Array { export function packBasicMessage(
const data = encrypt(message, MESSAGE_TYPE, key) message: Uint8Array,
key: Uint8Array = DEFAULT_KEY,
): Uint8Array {
const data = encrypt(message, MESSAGE_TYPE, key);
return packOutgoingPacket({ return packOutgoingPacket({
messageType: MESSAGE_TYPE, messageType: MESSAGE_TYPE,
data: data data: data,
}) });
} }
/** /**
@@ -28,6 +31,6 @@ export function packBasicMessage (message: Uint8Array, key: Uint8Array = DEFAULT
* @param data The data section of an incoming basic message (0x0001) message. * @param data The data section of an incoming basic message (0x0001) message.
* @returns An encrypted unpacked basic message (0x0001) message. * @returns An encrypted unpacked basic message (0x0001) message.
*/ */
export function unpackBasicMessage (data: Uint8Array): Uint8Array { export function unpackBasicMessage(data: Uint8Array): Uint8Array {
return data return data;
} }

View File

@@ -1,16 +1,16 @@
import { MessageTypes } from '../common' import { MessageTypes } from "../common";
import { numberToUint16BE } from '../utilities/number' import { numberToUint16BE } from "../utilities/number";
import { packOutgoingPacket } from './packet' import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Keepalive) const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Keepalive);
/** /**
* Create an outgoing keepalive (0x0005) packet. * Create an outgoing keepalive (0x0005) packet.
* @returns An outgoing keepalive (0x0005) packet. * @returns An outgoing keepalive (0x0005) packet.
*/ */
export function packKeepaliveMessage (): Uint8Array { export function packKeepaliveMessage(): Uint8Array {
return packOutgoingPacket({ return packOutgoingPacket({
messageType: MESSAGE_TYPE, messageType: MESSAGE_TYPE,
data: new Uint8Array(0) data: new Uint8Array(0),
}) });
} }

View File

@@ -1,16 +1,16 @@
import { MAX_DATA_LENGTH } from '../common' import { MAX_DATA_LENGTH } from "../common";
import { numberToUint16BE } from '../utilities/number' import { numberToUint16BE } from "../utilities/number";
import { SmartBuffer } from '../utilities/smart-buffer' import { SmartBuffer } from "../utilities/smart-buffer";
export interface IncomingPacket { export interface IncomingPacket {
messageType: number messageType: number;
senderId: number senderId: number;
data: Uint8Array data: Uint8Array;
} }
export interface OutgoingPacket { export interface OutgoingPacket {
messageType: Uint8Array messageType: Uint8Array;
data: Uint8Array data: Uint8Array;
} }
/** /**
@@ -18,19 +18,21 @@ export interface OutgoingPacket {
* @param outgoingPacket The message type and data to send. * @param outgoingPacket The message type and data to send.
* @returns A buffer containing the ready-to-send packet. * @returns A buffer containing the ready-to-send packet.
*/ */
export function packOutgoingPacket (outgoingPacket: OutgoingPacket): Uint8Array { export function packOutgoingPacket(outgoingPacket: OutgoingPacket): Uint8Array {
// Verify that the data does not exceed the maximum data length. // Verify that the data does not exceed the maximum data length.
if (outgoingPacket.data.length > MAX_DATA_LENGTH) { if (outgoingPacket.data.length > MAX_DATA_LENGTH) {
throw RangeError(`Specified data of length ${outgoingPacket.data.length} exceeds max data length ${MAX_DATA_LENGTH}.`) throw RangeError(
`Specified data of length ${outgoingPacket.data.length} exceeds max data length ${MAX_DATA_LENGTH}.`,
);
} }
// Prepare the outgoing packet. // Prepare the outgoing packet.
const buffer = new SmartBuffer() const buffer = new SmartBuffer();
buffer.writeBytes(outgoingPacket.messageType) buffer.writeBytes(outgoingPacket.messageType);
buffer.writeBytes(numberToUint16BE(outgoingPacket.data.length)) buffer.writeBytes(numberToUint16BE(outgoingPacket.data.length));
buffer.writeBytes(outgoingPacket.data) buffer.writeBytes(outgoingPacket.data);
return buffer.data return buffer.data;
} }
/** /**
@@ -38,17 +40,19 @@ export function packOutgoingPacket (outgoingPacket: OutgoingPacket): Uint8Array
* @param incomingPacket The incoming buffer from a WebSocket to unpack. * @param incomingPacket The incoming buffer from a WebSocket to unpack.
* @returns The unpacked data. * @returns The unpacked data.
*/ */
export function unpackIncomingPacket (incomingPacket: ArrayBuffer): IncomingPacket { export function unpackIncomingPacket(
const buffer = SmartBuffer.from(incomingPacket) incomingPacket: ArrayBuffer,
): IncomingPacket {
const buffer = SmartBuffer.from(incomingPacket);
const messageType = buffer.readUInt16() const messageType = buffer.readUInt16();
const senderId = buffer.readUInt32() const senderId = buffer.readUInt32();
const length = buffer.readUInt16() const length = buffer.readUInt16();
const data = buffer.readBytes(length) const data = buffer.readBytes(length);
return { return {
messageType: messageType, messageType: messageType,
senderId: senderId, senderId: senderId,
data: data data: data,
} };
} }

View File

@@ -1,11 +1,11 @@
import { MessageTypes } from '../common' import { MessageTypes } from "../common";
import { numberToUint16BE } from '../utilities/number' import { numberToUint16BE } from "../utilities/number";
import { packOutgoingPacket } from './packet' import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Subscribe) const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Subscribe);
export interface SubscribeMessage { export interface SubscribeMessage {
messageType: number messageType: number;
} }
/** /**
@@ -13,10 +13,10 @@ export interface SubscribeMessage {
* @param properties The properties for the message. * @param properties The properties for the message.
* @returns An outgoing subscribe (0x0000) packet. * @returns An outgoing subscribe (0x0000) packet.
*/ */
export function packSubscribeMessage (properties: SubscribeMessage): Uint8Array { export function packSubscribeMessage(properties: SubscribeMessage): Uint8Array {
const data = numberToUint16BE(properties.messageType) const data = numberToUint16BE(properties.messageType);
return packOutgoingPacket({ return packOutgoingPacket({
messageType: MESSAGE_TYPE, messageType: MESSAGE_TYPE,
data: data data: data,
}) });
} }

View File

@@ -1,11 +1,11 @@
import { MessageTypes } from '../common' import { MessageTypes } from "../common";
import { numberToUint16BE } from '../utilities/number' import { numberToUint16BE } from "../utilities/number";
import { packOutgoingPacket } from './packet' import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Unsubscribe) const MESSAGE_TYPE = numberToUint16BE(MessageTypes.Unsubscribe);
export interface UnsubscribeMessage { export interface UnsubscribeMessage {
messageType: number messageType: number;
} }
/** /**
@@ -13,10 +13,12 @@ export interface UnsubscribeMessage {
* @param properties The properties for the message. * @param properties The properties for the message.
* @returns An outgoing unsubscribe (0xFFFF) packet. * @returns An outgoing unsubscribe (0xFFFF) packet.
*/ */
export function packUnsubscribeMessage (properties: UnsubscribeMessage): Uint8Array { export function packUnsubscribeMessage(
const data = numberToUint16BE(properties.messageType) properties: UnsubscribeMessage,
): Uint8Array {
const data = numberToUint16BE(properties.messageType);
return packOutgoingPacket({ return packOutgoingPacket({
messageType: MESSAGE_TYPE, messageType: MESSAGE_TYPE,
data: data data: data,
}) });
} }

View File

@@ -1,16 +1,16 @@
import Color from 'color' import Color from "color";
import { encrypt } from 'romulus-js' import { encrypt } from "romulus-js";
import { DEFAULT_KEY, MessageTypes } from '../common' import { DEFAULT_KEY, MessageTypes } from "../common";
import { numberToUint16BE } from '../utilities/number' import { numberToUint16BE } from "../utilities/number";
import { SmartBuffer } from '../utilities/smart-buffer' import { SmartBuffer } from "../utilities/smart-buffer";
import { packOutgoingPacket } from './packet' import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.UserDataRequest) const MESSAGE_TYPE = numberToUint16BE(MessageTypes.UserDataRequest);
export interface UserDataRequestMessage { export interface UserDataRequestMessage {
username: string username: string;
colour: Color colour: Color;
clientId: string clientId: string;
} }
/** /**
@@ -19,33 +19,36 @@ export interface UserDataRequestMessage {
* @param key The key to encrypt the data with. * @param key The key to encrypt the data with.
* @returns An outgoing user data request (0x0002) packet. * @returns An outgoing user data request (0x0002) packet.
*/ */
export function packUserDataRequestMessage (properties: UserDataRequestMessage, key: Uint8Array = DEFAULT_KEY): Uint8Array { export function packUserDataRequestMessage(
const encoder = new TextEncoder() properties: UserDataRequestMessage,
key: Uint8Array = DEFAULT_KEY,
): Uint8Array {
const encoder = new TextEncoder();
// Prepare data in correct format. // Prepare data in correct format.
const username = encoder.encode(properties.username) const username = encoder.encode(properties.username);
const usernameLength = numberToUint16BE(username.length) const usernameLength = numberToUint16BE(username.length);
const colour = new Uint8Array(properties.colour.array()) const colour = new Uint8Array(properties.colour.array());
const clientId = encoder.encode(properties.clientId) const clientId = encoder.encode(properties.clientId);
const clientIdLength = numberToUint16BE(clientId.length) const clientIdLength = numberToUint16BE(clientId.length);
// Pack data. // Pack data.
const packedData = new SmartBuffer() const packedData = new SmartBuffer();
packedData.writeBytes(usernameLength) packedData.writeBytes(usernameLength);
packedData.writeBytes(username) packedData.writeBytes(username);
packedData.pad(32 - username.length) packedData.pad(32 - username.length);
packedData.writeBytes(colour) packedData.writeBytes(colour);
packedData.writeBytes(clientIdLength) packedData.writeBytes(clientIdLength);
packedData.writeBytes(clientId) packedData.writeBytes(clientId);
packedData.pad(32 - clientId.length) packedData.pad(32 - clientId.length);
// Encrypt the data. // Encrypt the data.
const data = encrypt(packedData.data, MESSAGE_TYPE, key) const data = encrypt(packedData.data, MESSAGE_TYPE, key);
return packOutgoingPacket({ return packOutgoingPacket({
messageType: MESSAGE_TYPE, messageType: MESSAGE_TYPE,
data: data data: data,
}) });
} }
/** /**
@@ -53,23 +56,25 @@ export function packUserDataRequestMessage (properties: UserDataRequestMessage,
* @param data The decrypted data section of an incoming user data request (0x0002) message. * @param data The decrypted data section of an incoming user data request (0x0002) message.
* @returns An unpacked user data request (0x0002) message. * @returns An unpacked user data request (0x0002) message.
*/ */
export function unpackUserDataRequestMessage (data: Uint8Array): UserDataRequestMessage { export function unpackUserDataRequestMessage(
data: Uint8Array,
): UserDataRequestMessage {
// Unpack and read data in correct format. // Unpack and read data in correct format.
const packedData = SmartBuffer.from(data) const packedData = SmartBuffer.from(data);
const usernameLength = packedData.readUInt16() const usernameLength = packedData.readUInt16();
const username = packedData.readBytes(usernameLength) const username = packedData.readBytes(usernameLength);
packedData.cursor = 34 packedData.cursor = 34;
const colour = packedData.readBytes(3) const colour = packedData.readBytes(3);
const clientIdLength = packedData.readUInt16() const clientIdLength = packedData.readUInt16();
const clientId = packedData.readBytes(clientIdLength) const clientId = packedData.readBytes(clientIdLength);
const decoder = new TextDecoder() const decoder = new TextDecoder();
// Return data in correct format. // Return data in correct format.
return { return {
username: decoder.decode(username), username: decoder.decode(username),
colour: Color.rgb(colour), colour: Color.rgb(colour),
clientId: decoder.decode(clientId) clientId: decoder.decode(clientId),
} };
} }

View File

@@ -1,16 +1,16 @@
import Color from 'color' import Color from "color";
import { encrypt } from 'romulus-js' import { encrypt } from "romulus-js";
import { DEFAULT_KEY, MessageTypes } from '../common' import { DEFAULT_KEY, MessageTypes } from "../common";
import { numberToUint16BE } from '../utilities/number' import { numberToUint16BE } from "../utilities/number";
import { SmartBuffer } from '../utilities/smart-buffer' import { SmartBuffer } from "../utilities/smart-buffer";
import { packOutgoingPacket } from './packet' import { packOutgoingPacket } from "./packet";
const MESSAGE_TYPE = numberToUint16BE(MessageTypes.UserDataResponse) const MESSAGE_TYPE = numberToUint16BE(MessageTypes.UserDataResponse);
export interface UserDataResponseMessage { export interface UserDataResponseMessage {
username: string username: string;
colour: Color colour: Color;
clientId: string clientId: string;
} }
/** /**
@@ -19,32 +19,35 @@ export interface UserDataResponseMessage {
* @param key The key to encrypt the data with. * @param key The key to encrypt the data with.
* @returns The data section of an outgoing user data response (0x0003) message. * @returns The data section of an outgoing user data response (0x0003) message.
*/ */
export function packUserDataResponseMessage (properties: UserDataResponseMessage, key: Uint8Array = DEFAULT_KEY): Uint8Array { export function packUserDataResponseMessage(
const encoder = new TextEncoder() properties: UserDataResponseMessage,
key: Uint8Array = DEFAULT_KEY,
): Uint8Array {
const encoder = new TextEncoder();
// Prepare data in correct format. // Prepare data in correct format.
const username = encoder.encode(properties.username) const username = encoder.encode(properties.username);
const usernameLength = numberToUint16BE(username.length) const usernameLength = numberToUint16BE(username.length);
const colour = new Uint8Array(properties.colour.array()) const colour = new Uint8Array(properties.colour.array());
const clientId = encoder.encode(properties.clientId) const clientId = encoder.encode(properties.clientId);
const clientIdLength = numberToUint16BE(clientId.length) const clientIdLength = numberToUint16BE(clientId.length);
// Pack data. // Pack data.
const packedData = new SmartBuffer() const packedData = new SmartBuffer();
packedData.writeBytes(usernameLength) packedData.writeBytes(usernameLength);
packedData.writeBytes(username) packedData.writeBytes(username);
packedData.pad(32 - username.length) packedData.pad(32 - username.length);
packedData.writeBytes(colour) packedData.writeBytes(colour);
packedData.writeBytes(clientIdLength) packedData.writeBytes(clientIdLength);
packedData.writeBytes(clientId) packedData.writeBytes(clientId);
packedData.pad(32 - clientId.length) packedData.pad(32 - clientId.length);
// Return encrypted data. // Return encrypted data.
const data = encrypt(packedData.data, MESSAGE_TYPE, key) const data = encrypt(packedData.data, MESSAGE_TYPE, key);
return packOutgoingPacket({ return packOutgoingPacket({
messageType: MESSAGE_TYPE, messageType: MESSAGE_TYPE,
data: data data: data,
}) });
} }
/** /**
@@ -52,23 +55,25 @@ export function packUserDataResponseMessage (properties: UserDataResponseMessage
* @param data The decrypted data section of an incoming user data response (0x0003) message. * @param data The decrypted data section of an incoming user data response (0x0003) message.
* @returns A unpacked user data response (0x0003) message. * @returns A unpacked user data response (0x0003) message.
*/ */
export function unpackUserDataResponseMessage (data: Uint8Array): UserDataResponseMessage { export function unpackUserDataResponseMessage(
data: Uint8Array,
): UserDataResponseMessage {
// Unpack and read data in correct format. // Unpack and read data in correct format.
const packedData = SmartBuffer.from(data) const packedData = SmartBuffer.from(data);
const usernameLength = packedData.readUInt16() const usernameLength = packedData.readUInt16();
const username = packedData.readBytes(usernameLength) const username = packedData.readBytes(usernameLength);
packedData.cursor = 34 packedData.cursor = 34;
const colour = packedData.readBytes(3) const colour = packedData.readBytes(3);
const clientIdLength = packedData.readUInt16() const clientIdLength = packedData.readUInt16();
const clientId = packedData.readBytes(clientIdLength) const clientId = packedData.readBytes(clientIdLength);
const decoder = new TextDecoder() const decoder = new TextDecoder();
// Return data in correct format. // Return data in correct format.
return { return {
username: decoder.decode(username), username: decoder.decode(username),
colour: Color.rgb(colour), colour: Color.rgb(colour),
clientId: decoder.decode(clientId) clientId: decoder.decode(clientId),
} };
} }

View File

@@ -3,11 +3,11 @@
* @param number The number to pack. * @param number The number to pack.
* @returns The packed buffer. * @returns The packed buffer.
*/ */
export function numberToUint16BE (number: number): Uint8Array { export function numberToUint16BE(number: number): Uint8Array {
const ret = new Uint8Array(2) const ret = new Uint8Array(2);
ret[0] = (number & 0xFF00) >> 8 ret[0] = (number & 0xff00) >> 8;
ret[1] = (number & 0x00FF) >> 0 ret[1] = (number & 0x00ff) >> 0;
return ret return ret;
} }
/** /**
@@ -15,11 +15,11 @@ export function numberToUint16BE (number: number): Uint8Array {
* @param number The number to pack. * @param number The number to pack.
* @returns The packed buffer. * @returns The packed buffer.
*/ */
export function numberToUint32BE (number: number): Uint8Array { export function numberToUint32BE(number: number): Uint8Array {
const ret = new Uint8Array(4) const ret = new Uint8Array(4);
ret[0] = (number & 0xFF000000) >> 24 ret[0] = (number & 0xff000000) >> 24;
ret[1] = (number & 0x00FF0000) >> 16 ret[1] = (number & 0x00ff0000) >> 16;
ret[2] = (number & 0x0000FF00) >> 8 ret[2] = (number & 0x0000ff00) >> 8;
ret[3] = (number & 0x000000FF) >> 0 ret[3] = (number & 0x000000ff) >> 0;
return ret return ret;
} }

View File

@@ -1,62 +1,64 @@
import { numberToUint16BE, numberToUint32BE } from './number' import { numberToUint16BE, numberToUint32BE } from "./number";
export class SmartBuffer { export class SmartBuffer {
private _data: number[] private _data: number[];
private _cursor: number private _cursor: number;
/** /**
* Wrap a buffer to track position and provide useful read / write functionality. * Wrap a buffer to track position and provide useful read / write functionality.
* @param data Buffer to wrap (optional). * @param data Buffer to wrap (optional).
*/ */
constructor (length: number = 0) { constructor(length: number = 0) {
this._data = new Array<number>(length) this._data = new Array<number>(length);
this._cursor = 0 this._cursor = 0;
} }
/** /**
* Return a regular buffer. * Return a regular buffer.
*/ */
get data (): Uint8Array { get data(): Uint8Array {
return new Uint8Array(this._data) return new Uint8Array(this._data);
} }
/** /**
* Update the smart buffer to wrap new data. * Update the smart buffer to wrap new data.
*/ */
set data (data: Uint8Array) { set data(data: Uint8Array) {
this._data = Array.from(data) this._data = Array.from(data);
this.cursor = 0 this.cursor = 0;
} }
/** /**
* Get the length of the smart buffer data. * Get the length of the smart buffer data.
*/ */
get length (): number { get length(): number {
return this._data.length return this._data.length;
} }
/** /**
* Set the length of the smart buffer data. * Set the length of the smart buffer data.
*/ */
set length (length: number) { set length(length: number) {
this._data.length = length this._data.length = length;
} }
/** /**
* Get the current cursor position of the smart buffer. * Get the current cursor position of the smart buffer.
*/ */
get cursor (): number { get cursor(): number {
return this._cursor return this._cursor;
} }
/** /**
* Set the cursor position of the smart buffer. * Set the cursor position of the smart buffer.
*/ */
set cursor (position: number) { set cursor(position: number) {
if (position < 0) { if (position < 0) {
throw RangeError(`Cannot seek to ${this.cursor} of ${this.length} bytes.`) throw RangeError(
`Cannot seek to ${this.cursor} of ${this.length} bytes.`,
);
} }
this._cursor = position this._cursor = position;
} }
/** /**
@@ -64,33 +66,33 @@ export class SmartBuffer {
* @param data The object to convert to a new SmartBuffer. * @param data The object to convert to a new SmartBuffer.
* @returns A new SmartBuffer. * @returns A new SmartBuffer.
*/ */
static from (data: number[] | ArrayBuffer): SmartBuffer { static from(data: number[] | ArrayBuffer): SmartBuffer {
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
if (data instanceof ArrayBuffer) { if (data instanceof ArrayBuffer) {
smartBuffer._data = Array.from(new Uint8Array(data)) smartBuffer._data = Array.from(new Uint8Array(data));
} else { } else {
smartBuffer._data = Array.from(data) smartBuffer._data = Array.from(data);
} }
return smartBuffer return smartBuffer;
} }
/** /**
* Pads bytes to the end of the smart buffer. * Pads bytes to the end of the smart buffer.
* @param length The number of bytes to pad. * @param length The number of bytes to pad.
*/ */
pad (length: number): void { pad(length: number): void {
this._data.push(...Array<number>(length).fill(0)) this._data.push(...Array<number>(length).fill(0));
this.cursor += length this.cursor += length;
} }
/** /**
* Return the data from the specified range. * Return the data from the specified range.
* @param start The start position. * @param start The start position.
* @param end The end position. * @param end The end position.
* @returns A new buffer containing data from the specified range. * @returns A new buffer containing data from the specified range.
*/ */
slice (start: number, end: number): Uint8Array { slice(start: number, end: number): Uint8Array {
return new Uint8Array(this._data.slice(start, end)) return new Uint8Array(this._data.slice(start, end));
} }
/** /**
@@ -99,66 +101,66 @@ export class SmartBuffer {
* @param deleteCount The number of items to remove before inserting new data. * @param deleteCount The number of items to remove before inserting new data.
* @param items The items to insert at the specified position. * @param items The items to insert at the specified position.
*/ */
splice (start: number, deleteCount: number, ...items: number[]): void { splice(start: number, deleteCount: number, ...items: number[]): void {
if (this.length < start) { if (this.length < start) {
this._data.push(...Array<number>(start)) this._data.push(...Array<number>(start));
} }
this._data.splice(this.cursor, deleteCount, ...items) this._data.splice(this.cursor, deleteCount, ...items);
} }
/** /**
* Read a UInt16 number from the smart buffer at the current cursor position, and increment the cursor. * Read a UInt16 number from the smart buffer at the current cursor position, and increment the cursor.
* @returns A number represented by the bytes at the current cursor position. * @returns A number represented by the bytes at the current cursor position.
*/ */
readUInt16 (): number { readUInt16(): number {
const num = this.slice(this.cursor, this.cursor + 2) const num = this.slice(this.cursor, this.cursor + 2);
this.cursor += 2 this.cursor += 2;
return (num[0] << 8 | num[1]) >>> 0 return ((num[0] << 8) | num[1]) >>> 0;
} }
/** /**
* Read a UInt32 number from the smart buffer at the current cursor position, and increment the cursor. * Read a UInt32 number from the smart buffer at the current cursor position, and increment the cursor.
* @returns A number represented by the bytes at the current cursor position. * @returns A number represented by the bytes at the current cursor position.
*/ */
readUInt32 (): number { readUInt32(): number {
const num = this.slice(this.cursor, this.cursor + 4) const num = this.slice(this.cursor, this.cursor + 4);
this.cursor += 4 this.cursor += 4;
return (num[0] << 24 | num[1] << 16 | num[2] << 8 | num[3]) >>> 0 return ((num[0] << 24) | (num[1] << 16) | (num[2] << 8) | num[3]) >>> 0;
} }
/** /**
* Read the specified number of bytes from the smart buffer at the current cursor position, and increment the cursor. * Read the specified number of bytes from the smart buffer at the current cursor position, and increment the cursor.
* @returns A buffer containing the bytes read from the current cursor position. * @returns A buffer containing the bytes read from the current cursor position.
*/ */
readBytes (length: number): Uint8Array { readBytes(length: number): Uint8Array {
this.cursor += length this.cursor += length;
return this.slice(this.cursor - length, this.cursor) return this.slice(this.cursor - length, this.cursor);
} }
/** /**
* Write a UInt16 number to the smart buffer at the current cursor position, and increment the cursor. * Write a UInt16 number to the smart buffer at the current cursor position, and increment the cursor.
* @param number The number to write in UInt16 format at the current cursor position. * @param number The number to write in UInt16 format at the current cursor position.
*/ */
writeUInt16 (number: number): void { writeUInt16(number: number): void {
this.splice(this.cursor, 2, ...numberToUint16BE(number)) this.splice(this.cursor, 2, ...numberToUint16BE(number));
this.cursor += 2 this.cursor += 2;
} }
/** /**
* Write a UInt32 number to the smart buffer at the current cursor position, and increment the cursor. * Write a UInt32 number to the smart buffer at the current cursor position, and increment the cursor.
* @param number The number to write in UInt32 format at the current cursor position. * @param number The number to write in UInt32 format at the current cursor position.
*/ */
writeUInt32 (number: number): void { writeUInt32(number: number): void {
this.splice(this.cursor, 4, ...numberToUint32BE(number)) this.splice(this.cursor, 4, ...numberToUint32BE(number));
this.cursor += 4 this.cursor += 4;
} }
/** /**
* Write the specified bytes to the smart buffer at the current cursor position, and increment the cursor. * Write the specified bytes to the smart buffer at the current cursor position, and increment the cursor.
* @param buffer The bytes to write at the current cursor position. * @param buffer The bytes to write at the current cursor position.
*/ */
writeBytes (buffer: number[] | Uint8Array): void { writeBytes(buffer: number[] | Uint8Array): void {
this.splice(this.cursor, buffer.length, ...buffer) this.splice(this.cursor, buffer.length, ...buffer);
this.cursor += buffer.length this.cursor += buffer.length;
} }
} }

View File

@@ -1,36 +1,37 @@
import { MessageTypes } from '../../src/common' import { MessageTypes } from "../../src/common";
import { packers, unpackers } from '../../src/mapping' import { packers, unpackers } from "../../src/mapping";
const KEY = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]) const KEY = new Uint8Array([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
]);
test('Create a basic message (0x0001) packet.', () => { test("Create a basic message (0x0001) packet.", () => {
// Given // Given
const encoder = new TextEncoder() const encoder = new TextEncoder();
const message = encoder.encode('Hello, World!') const message = encoder.encode("Hello, World!");
// When // When
const packedPacket = packers[MessageTypes.Basic]( const packedPacket = packers[MessageTypes.Basic](message, KEY);
message,
KEY
)
// Then // Then
// We can't check the contents of the data as it's encrypted with a random nonce. // We can't check the contents of the data as it's encrypted with a random nonce.
// Check the message type and length. // Check the message type and length.
expect(packedPacket.slice(0, 4)).toMatchObject(new Uint8Array([0x00, 0x01, 0x00, 0x2D])) expect(packedPacket.slice(0, 4)).toMatchObject(
new Uint8Array([0x00, 0x01, 0x00, 0x2d]),
);
// Check the total length is as expected. // Check the total length is as expected.
expect(packedPacket.length).toBe(49) expect(packedPacket.length).toBe(49);
}) });
test('Parse a basic message (0x0001).', () => { test("Parse a basic message (0x0001).", () => {
// Given // Given
const data = new Uint8Array([1, 2, 3, 4]) const data = new Uint8Array([1, 2, 3, 4]);
// When // When
const unpackedPacket = unpackers[MessageTypes.Basic](data) const unpackedPacket = unpackers[MessageTypes.Basic](data);
// Then // Then
expect(unpackedPacket) expect(unpackedPacket);
expect(unpackedPacket).toMatchObject(data) expect(unpackedPacket).toMatchObject(data);
}) });

View File

@@ -1,11 +1,11 @@
import { MessageTypes } from '../../src/common' import { MessageTypes } from "../../src/common";
import { packers } from '../../src/mapping' import { packers } from "../../src/mapping";
test('Create a keepalive (0x0005) packet.', () => { test("Create a keepalive (0x0005) packet.", () => {
// When // When
const packedPacket = packers[MessageTypes.Keepalive]() const packedPacket = packers[MessageTypes.Keepalive]();
// Then // Then
const expectedResult = new Uint8Array([0x00, 0x05, 0x00, 0x00]) const expectedResult = new Uint8Array([0x00, 0x05, 0x00, 0x00]);
expect(packedPacket).toMatchObject(expectedResult) expect(packedPacket).toMatchObject(expectedResult);
}) });

View File

@@ -1,15 +1,18 @@
import { packOutgoingPacket, unpackIncomingPacket } from '../../src/messages/packet' import {
packOutgoingPacket,
unpackIncomingPacket,
} from "../../src/messages/packet";
test('Pack an outgoing packet.', () => { test("Pack an outgoing packet.", () => {
// Given // Given
const messageType = new Uint8Array([0x12, 0x34]) const messageType = new Uint8Array([0x12, 0x34]);
const data = new Uint8Array([0x12, 0x34, 0x56, 0x78]) const data = new Uint8Array([0x12, 0x34, 0x56, 0x78]);
// When // When
const packedPacket = packOutgoingPacket({ const packedPacket = packOutgoingPacket({
messageType: messageType, messageType: messageType,
data: data data: data,
}) });
// Then // Then
const expectedResult = new Uint8Array([ const expectedResult = new Uint8Array([
@@ -18,12 +21,12 @@ test('Pack an outgoing packet.', () => {
// Data length // Data length
0x00, 0x04, 0x00, 0x04,
// Data // Data
0x12, 0x34, 0x56, 0x78 0x12, 0x34, 0x56, 0x78,
]) ]);
expect(packedPacket).toMatchObject(expectedResult) expect(packedPacket).toMatchObject(expectedResult);
}) });
test('Unpack an incoming packet.', () => { test("Unpack an incoming packet.", () => {
// Given // Given
const incomingPacket = new Uint8Array([ const incomingPacket = new Uint8Array([
// Message type // Message type
@@ -33,14 +36,16 @@ test('Unpack an incoming packet.', () => {
// Data length // Data length
0x00, 0x04, 0x00, 0x04,
// Data // Data
0x12, 0x34, 0x56, 0x78 0x12, 0x34, 0x56, 0x78,
]) ]);
// When // When
const unpackedResult = unpackIncomingPacket(incomingPacket) const unpackedResult = unpackIncomingPacket(incomingPacket);
// Then // Then
expect(unpackedResult.messageType).toBe(0x1234) expect(unpackedResult.messageType).toBe(0x1234);
expect(unpackedResult.senderId).toBe(0xaabbccdd) expect(unpackedResult.senderId).toBe(0xaabbccdd);
expect(unpackedResult.data).toMatchObject(new Uint8Array([0x12, 0x34, 0x56, 0x78])) expect(unpackedResult.data).toMatchObject(
}) new Uint8Array([0x12, 0x34, 0x56, 0x78]),
);
});

View File

@@ -1,14 +1,16 @@
import { MessageTypes } from '../../src/common' import { MessageTypes } from "../../src/common";
import { packers } from '../../src/mapping' import { packers } from "../../src/mapping";
test('Create a subscribe (0x0000) packet.', () => { test("Create a subscribe (0x0000) packet.", () => {
// Given // Given
const messageType = 0xabcd const messageType = 0xabcd;
// When // When
const packedPacket = packers[MessageTypes.Subscribe]({ messageType: messageType }) const packedPacket = packers[MessageTypes.Subscribe]({
messageType: messageType,
});
// Then // Then
const expectedResult = new Uint8Array([0x00, 0x00, 0x00, 0x02, 0xab, 0xcd]) const expectedResult = new Uint8Array([0x00, 0x00, 0x00, 0x02, 0xab, 0xcd]);
expect(packedPacket).toMatchObject(expectedResult) expect(packedPacket).toMatchObject(expectedResult);
}) });

View File

@@ -1,14 +1,16 @@
import { MessageTypes } from '../../src/common' import { MessageTypes } from "../../src/common";
import { packers } from '../../src/mapping' import { packers } from "../../src/mapping";
test('Create an unsubscribe (0xffff) packet.', () => { test("Create an unsubscribe (0xffff) packet.", () => {
// Given // Given
const messageType = 0xabcd const messageType = 0xabcd;
// When // When
const packedPacket = packers[MessageTypes.Unsubscribe]({ messageType: messageType }) const packedPacket = packers[MessageTypes.Unsubscribe]({
messageType: messageType,
});
// Then // Then
const expectedResult = new Uint8Array([0xff, 0xff, 0x00, 0x02, 0xab, 0xcd]) const expectedResult = new Uint8Array([0xff, 0xff, 0x00, 0x02, 0xab, 0xcd]);
expect(packedPacket).toMatchObject(expectedResult) expect(packedPacket).toMatchObject(expectedResult);
}) });

View File

@@ -1,52 +1,56 @@
import Color from 'color' import Color from "color";
import { MessageTypes } from '../../src/common' import { MessageTypes } from "../../src/common";
import { packers, unpackers } from '../../src/mapping' import { packers, unpackers } from "../../src/mapping";
const KEY = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F]) const KEY = new Uint8Array([
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0e, 0x0f,
]);
test('Create a user data request (0x0002) packet.', () => { test("Create a user data request (0x0002) packet.", () => {
// Given // Given
const username = 'Butlersaurus' const username = "Butlersaurus";
const colour = Color('#FF4000') const colour = Color("#FF4000");
const clientId = 'Mercury' const clientId = "Mercury";
// When // When
const packedPacket = packers[MessageTypes.UserDataRequest]( const packedPacket = packers[MessageTypes.UserDataRequest](
{ {
username: username, username: username,
colour: colour, colour: colour,
clientId: clientId clientId: clientId,
}, },
KEY KEY,
) );
// Then // Then
// We can't check the contents of the data as it's encrypted with a random nonce. // We can't check the contents of the data as it's encrypted with a random nonce.
// Check the message type and length. // Check the message type and length.
expect(packedPacket.slice(0, 4)).toMatchObject(new Uint8Array([0x00, 0x02, 0x00, 0x67])) expect(packedPacket.slice(0, 4)).toMatchObject(
new Uint8Array([0x00, 0x02, 0x00, 0x67]),
);
// Check the total length is as expected. // Check the total length is as expected.
expect(packedPacket.length).toBe(107) expect(packedPacket.length).toBe(107);
}) });
test('Parse a user data request (0x0002).', () => { test("Parse a user data request (0x0002).", () => {
// Given // Given
const data = new Uint8Array([ const data = new Uint8Array([
0, 12, 0, 12, 66, 117, 116, 108, 101, 114, 115, 97, 117, 114, 117, 115, 0, 0, 0, 0,
66, 117, 116, 108, 101, 114, 115, 97, 117, 114, 117, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 64, 0, 0, 7, 77, 101,
255, 64, 0, 114, 99, 117, 114, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 7, 0, 0, 0, 0, 0, 0, 0, 0,
77, 101, 114, 99, 117, 114, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]);
]) const username = "Butlersaurus";
const username = 'Butlersaurus' const colour = Color("#FF4000");
const colour = Color('#FF4000') const clientId = "Mercury";
const clientId = 'Mercury'
// When // When
const unpackedPacket = unpackers[MessageTypes.UserDataRequest](data) const unpackedPacket = unpackers[MessageTypes.UserDataRequest](data);
// Then // Then
expect(unpackedPacket.username).toBe(username) expect(unpackedPacket.username).toBe(username);
expect(unpackedPacket.colour).toMatchObject(colour) expect(unpackedPacket.colour).toMatchObject(colour);
expect(unpackedPacket.clientId).toBe(clientId) expect(unpackedPacket.clientId).toBe(clientId);
}) });

View File

@@ -1,52 +1,56 @@
import Color from 'color' import Color from "color";
import { MessageTypes } from '../../src/common' import { MessageTypes } from "../../src/common";
import { packers, unpackers } from '../../src/mapping' import { packers, unpackers } from "../../src/mapping";
const KEY = new Uint8Array([0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F]) const KEY = new Uint8Array([
0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c,
0x0d, 0x0e, 0x0f,
]);
test('Create a user data response (0x0003) packet.', () => { test("Create a user data response (0x0003) packet.", () => {
// Given // Given
const username = 'Butlersaurus' const username = "Butlersaurus";
const colour = Color('#FF4000') const colour = Color("#FF4000");
const clientId = 'Mercury' const clientId = "Mercury";
// When // When
const packedPacket = packers[MessageTypes.UserDataResponse]( const packedPacket = packers[MessageTypes.UserDataResponse](
{ {
username: username, username: username,
colour: colour, colour: colour,
clientId: clientId clientId: clientId,
}, },
KEY KEY,
) );
// Then // Then
// We can't check the contents of the data as it's encrypted with a random nonce. // We can't check the contents of the data as it's encrypted with a random nonce.
// Check the message type and length. // Check the message type and length.
expect(packedPacket.slice(0, 4)).toMatchObject(new Uint8Array([0x00, 0x03, 0x00, 0x67])) expect(packedPacket.slice(0, 4)).toMatchObject(
new Uint8Array([0x00, 0x03, 0x00, 0x67]),
);
// Check the total length is as expected. // Check the total length is as expected.
expect(packedPacket.length).toBe(107) expect(packedPacket.length).toBe(107);
}) });
test('Parse a user data response (0x0003).', () => { test("Parse a user data response (0x0003).", () => {
// Given // Given
const data = new Uint8Array([ const data = new Uint8Array([
0, 12, 0, 12, 66, 117, 116, 108, 101, 114, 115, 97, 117, 114, 117, 115, 0, 0, 0, 0,
66, 117, 116, 108, 101, 114, 115, 97, 117, 114, 117, 115, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 64, 0, 0, 7, 77, 101,
255, 64, 0, 114, 99, 117, 114, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 7, 0, 0, 0, 0, 0, 0, 0, 0,
77, 101, 114, 99, 117, 114, 121, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]);
]) const username = "Butlersaurus";
const username = 'Butlersaurus' const colour = Color("#FF4000");
const colour = Color('#FF4000') const clientId = "Mercury";
const clientId = 'Mercury'
// When // When
const unpackedPacket = unpackers[MessageTypes.UserDataResponse](data) const unpackedPacket = unpackers[MessageTypes.UserDataResponse](data);
// Then // Then
expect(unpackedPacket.username).toBe(username) expect(unpackedPacket.username).toBe(username);
expect(unpackedPacket.colour).toMatchObject(colour) expect(unpackedPacket.colour).toMatchObject(colour);
expect(unpackedPacket.clientId).toBe(clientId) expect(unpackedPacket.clientId).toBe(clientId);
}) });

View File

@@ -1,19 +1,19 @@
import { numberToUint16BE, numberToUint32BE } from '../../src/utilities/number' import { numberToUint16BE, numberToUint32BE } from "../../src/utilities/number";
test('Test number conversion to Uint16 big endian buffer.', () => { test("Test number conversion to Uint16 big endian buffer.", () => {
// When // When
const result = numberToUint16BE(1234) const result = numberToUint16BE(1234);
// Then // Then
const expectedResult = new Uint8Array([0x04, 0xd2]) const expectedResult = new Uint8Array([0x04, 0xd2]);
expect(result).toMatchObject(expectedResult) expect(result).toMatchObject(expectedResult);
}) });
test('Test number conversion to Uint32 big endian buffer.', () => { test("Test number conversion to Uint32 big endian buffer.", () => {
// When // When
const result = numberToUint32BE(123456) const result = numberToUint32BE(123456);
// Then // Then
const expectedResult = new Uint8Array([0x00, 0x01, 0xE2, 0x40]) const expectedResult = new Uint8Array([0x00, 0x01, 0xe2, 0x40]);
expect(result).toMatchObject(expectedResult) expect(result).toMatchObject(expectedResult);
}) });

View File

@@ -1,230 +1,240 @@
import { SmartBuffer } from '../../src/utilities/smart-buffer' import { SmartBuffer } from "../../src/utilities/smart-buffer";
test('Read a UInt16.', () => { test("Read a UInt16.", () => {
// Given // Given
const buffer = [0x30, 0x39] const buffer = [0x30, 0x39];
// When // When
const smartBuffer = SmartBuffer.from(buffer) const smartBuffer = SmartBuffer.from(buffer);
// Then // Then
expect(smartBuffer.readUInt16()).toBe(12345) expect(smartBuffer.readUInt16()).toBe(12345);
}) });
test('Read a UInt32.', () => { test("Read a UInt32.", () => {
// Given // Given
const buffer = [0x49, 0x96, 0x02, 0xD2] const buffer = [0x49, 0x96, 0x02, 0xd2];
// When // When
const smartBuffer = SmartBuffer.from(buffer) const smartBuffer = SmartBuffer.from(buffer);
// Then // Then
expect(smartBuffer.readUInt32()).toBe(1234567890) expect(smartBuffer.readUInt32()).toBe(1234567890);
}) });
test('Read a buffer.', () => { test("Read a buffer.", () => {
// Given // Given
const buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] const buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// When // When
const smartBuffer = SmartBuffer.from(buffer) const smartBuffer = SmartBuffer.from(buffer);
// Then // Then
const result = smartBuffer.readBytes(4) const result = smartBuffer.readBytes(4);
expect(result).toMatchObject(new Uint8Array([0, 1, 2, 3])) expect(result).toMatchObject(new Uint8Array([0, 1, 2, 3]));
}) });
test('Read a UInt16 from an offset.', () => { test("Read a UInt16 from an offset.", () => {
// Given // Given
const buffer = [0x00, 0x00, 0x30, 0x39] const buffer = [0x00, 0x00, 0x30, 0x39];
// When // When
const smartBuffer = SmartBuffer.from(buffer) const smartBuffer = SmartBuffer.from(buffer);
smartBuffer.cursor = 2 smartBuffer.cursor = 2;
// Then // Then
expect(smartBuffer.readUInt16()).toBe(12345) expect(smartBuffer.readUInt16()).toBe(12345);
}) });
test('Read a UInt32 from an offset.', () => { test("Read a UInt32 from an offset.", () => {
// Given // Given
const buffer = [0x00, 0x00, 0x49, 0x96, 0x02, 0xD2] const buffer = [0x00, 0x00, 0x49, 0x96, 0x02, 0xd2];
// When // When
const smartBuffer = SmartBuffer.from(buffer) const smartBuffer = SmartBuffer.from(buffer);
smartBuffer.cursor = 2 smartBuffer.cursor = 2;
// Then // Then
expect(smartBuffer.readUInt32()).toBe(1234567890) expect(smartBuffer.readUInt32()).toBe(1234567890);
}) });
test('Read a buffer from an offset.', () => { test("Read a buffer from an offset.", () => {
// Given // Given
const buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] const buffer = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
// When // When
const smartBuffer = SmartBuffer.from(buffer) const smartBuffer = SmartBuffer.from(buffer);
smartBuffer.cursor = 2 smartBuffer.cursor = 2;
// Then // Then
expect(smartBuffer.readBytes(4)).toMatchObject(new Uint8Array([2, 3, 4, 5])) expect(smartBuffer.readBytes(4)).toMatchObject(new Uint8Array([2, 3, 4, 5]));
}) });
test('Write a UInt16.', () => { test("Write a UInt16.", () => {
// Given // Given
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// When // When
smartBuffer.writeUInt16(12345) smartBuffer.writeUInt16(12345);
// Then // Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0x30, 0x39])) expect(smartBuffer.data).toMatchObject(new Uint8Array([0x30, 0x39]));
}) });
test('Write a UInt32.', () => { test("Write a UInt32.", () => {
// Given // Given
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// When // When
smartBuffer.writeUInt32(1234567890) smartBuffer.writeUInt32(1234567890);
// Then // Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0x49, 0x96, 0x02, 0xD2])) expect(smartBuffer.data).toMatchObject(
}) new Uint8Array([0x49, 0x96, 0x02, 0xd2]),
);
});
test('Write a buffer.', () => { test("Write a buffer.", () => {
// Given // Given
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// When // When
smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
// Then // Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) expect(smartBuffer.data).toMatchObject(
}) new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
);
});
test('Write a UInt16 at an offset.', () => { test("Write a UInt16 at an offset.", () => {
// Given // Given
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// When // When
smartBuffer.cursor = 2 smartBuffer.cursor = 2;
smartBuffer.writeUInt16(12345) smartBuffer.writeUInt16(12345);
// Then // Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0x00, 0x00, 0x30, 0x39])) expect(smartBuffer.data).toMatchObject(
}) new Uint8Array([0x00, 0x00, 0x30, 0x39]),
);
});
test('Write a UInt32 at an offset.', () => { test("Write a UInt32 at an offset.", () => {
// Given // Given
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// When // When
smartBuffer.cursor = 2 smartBuffer.cursor = 2;
smartBuffer.writeUInt32(1234567890) smartBuffer.writeUInt32(1234567890);
// Then // Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0x00, 0x00, 0x49, 0x96, 0x02, 0xD2])) expect(smartBuffer.data).toMatchObject(
}) new Uint8Array([0x00, 0x00, 0x49, 0x96, 0x02, 0xd2]),
);
});
test('Write a buffer at an offset.', () => { test("Write a buffer at an offset.", () => {
// Given // Given
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// When // When
smartBuffer.cursor = 2 smartBuffer.cursor = 2;
smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
// Then // Then
expect(smartBuffer.data).toMatchObject(new Uint8Array([0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9])) expect(smartBuffer.data).toMatchObject(
}) new Uint8Array([0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9]),
);
});
test('Cursor is correctly incremented after reading a UInt16.', () => { test("Cursor is correctly incremented after reading a UInt16.", () => {
// Given // Given
const buffer = new Uint8Array(4) const buffer = new Uint8Array(4);
// When // When
const smartBuffer = SmartBuffer.from(buffer) const smartBuffer = SmartBuffer.from(buffer);
// Then // Then
smartBuffer.readUInt16() smartBuffer.readUInt16();
expect(smartBuffer.cursor).toBe(2) expect(smartBuffer.cursor).toBe(2);
}) });
test('Cursor is correctly incremented after reading a UInt32.', () => { test("Cursor is correctly incremented after reading a UInt32.", () => {
// Given // Given
const buffer = new Uint8Array(4) const buffer = new Uint8Array(4);
// When // When
const smartBuffer = SmartBuffer.from(buffer) const smartBuffer = SmartBuffer.from(buffer);
// Then // Then
smartBuffer.readUInt32() smartBuffer.readUInt32();
expect(smartBuffer.cursor).toBe(4) expect(smartBuffer.cursor).toBe(4);
}) });
test('Cursor is correctly incremented after reading a buffer.', () => { test("Cursor is correctly incremented after reading a buffer.", () => {
// Given // Given
const buffer = new Uint8Array(8) const buffer = new Uint8Array(8);
// When // When
const smartBuffer = SmartBuffer.from(buffer) const smartBuffer = SmartBuffer.from(buffer);
// Then // Then
smartBuffer.readBytes(4) smartBuffer.readBytes(4);
expect(smartBuffer.cursor).toBe(4) expect(smartBuffer.cursor).toBe(4);
}) });
test('Cursor is correctly incremented after writing a UInt16.', () => { test("Cursor is correctly incremented after writing a UInt16.", () => {
// Given // Given
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// When // When
smartBuffer.writeUInt16(12345) smartBuffer.writeUInt16(12345);
// Then // Then
expect(smartBuffer.cursor).toBe(2) expect(smartBuffer.cursor).toBe(2);
}) });
test('Cursor is correctly incremented after writing a UInt32.', () => { test("Cursor is correctly incremented after writing a UInt32.", () => {
// Given // Given
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// When // When
smartBuffer.writeUInt32(1234567890) smartBuffer.writeUInt32(1234567890);
// Then // Then
expect(smartBuffer.cursor).toBe(4) expect(smartBuffer.cursor).toBe(4);
}) });
test('Cursor is correctly incremented after writing a buffer.', () => { test("Cursor is correctly incremented after writing a buffer.", () => {
// Given // Given
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// When // When
smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]) smartBuffer.writeBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
// Then // Then
expect(smartBuffer.cursor).toBe(10) expect(smartBuffer.cursor).toBe(10);
}) });
test('Seek to position below 0 throws range error.', () => { test("Seek to position below 0 throws range error.", () => {
// When // When
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// Then // Then
expect(() => { expect(() => {
smartBuffer.cursor = -1 smartBuffer.cursor = -1;
}).toThrow(RangeError) }).toThrow(RangeError);
}) });
test('Pad some data.', () => { test("Pad some data.", () => {
// Given // Given
const smartBuffer = new SmartBuffer() const smartBuffer = new SmartBuffer();
// When // When
smartBuffer.pad(10) smartBuffer.pad(10);
// Then // Then
expect(smartBuffer.length).toBe(10) expect(smartBuffer.length).toBe(10);
}) });

View File

@@ -1,19 +1,16 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */ /* Visit https://aka.ms/tsconfig.json to read more about this file */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"module": "commonjs", /* Specify what module code is generated. */ "module": "commonjs" /* Specify what module code is generated. */,
"rootDir": "src", /* Specify the root folder within your source files. */ "rootDir": "src" /* Specify the root folder within your source files. */,
"sourceMap": true, /* Create source map files for emitted JavaScript files. */ "sourceMap": true /* Create source map files for emitted JavaScript files. */,
"outDir": "dist", /* Specify an output folder for all emitted files. */ "outDir": "dist" /* Specify an output folder for all emitted files. */,
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
"strict": true, /* Enable all strict type-checking options. */ "strict": true /* Enable all strict type-checking options. */,
"skipLibCheck": true, /* Skip type checking all .d.ts files. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */,
"declaration": true "declaration": true
}, },
"exclude": [ "exclude": ["tests", "dist"]
"tests",
"dist"
]
} }