summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--package-lock.json17
-rw-r--r--package.json2
-rw-r--r--src/gateway/events/Connection.ts132
-rw-r--r--src/gateway/util/Send.ts3
-rw-r--r--src/gateway/util/WebSocket.ts5
5 files changed, 119 insertions, 40 deletions
diff --git a/package-lock.json b/package-lock.json

index ce23ed7b..e1bcc6db 100644 --- a/package-lock.json +++ b/package-lock.json
@@ -30,7 +30,6 @@ "fido2-lib": "^3.5.9", "file-type": "^22.0.1", "form-data": "^4.0.5", - "harmony-erlpack": "^0.0.1", "i18next": "^26.0.6", "i18next-fs-backend": "^2.6.4", "i18next-http-middleware": "^3.9.3", @@ -50,6 +49,7 @@ "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "typeorm": "^0.3.28", + "wetf": "^0.9.14", "wretch": "^3.0.7", "ws": "^8.20.0" }, @@ -3993,12 +3993,6 @@ "dev": true, "license": "ISC" }, - "node_modules/harmony-erlpack": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/harmony-erlpack/-/harmony-erlpack-0.0.1.tgz", - "integrity": "sha512-T3sahrFXojE5nAW9v+uz/DkA4TD6IQFSvOuCp05VFQi9fZC76bUzT1sXQtzyDMXmRuU0jJeZs8LQ4tityaUZiA==", - "license": "AGPL-3.0" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -7038,6 +7032,15 @@ "tslib": "^2.7.0" } }, + "node_modules/wetf": { + "version": "0.9.14", + "resolved": "https://registry.npmjs.org/wetf/-/wetf-0.9.14.tgz", + "integrity": "sha512-pL/SPqwqVRIfRIvYi57ZBKCN9u9eSGRpE2ngpEw0MqyVpT0YhLByCesaHI7uIJ7hiLOUH824sdMwURLh4xzfjA==", + "license": "MIT", + "funding": { + "url": "https://github.com/timotejroiko/wetf?sponsor=1" + } + }, "node_modules/whatwg-encoding": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", diff --git a/package.json b/package.json
index 3206e54d..2fea5390 100644 --- a/package.json +++ b/package.json
@@ -98,7 +98,6 @@ "fido2-lib": "^3.5.9", "file-type": "^22.0.1", "form-data": "^4.0.5", - "harmony-erlpack": "^0.0.1", "i18next": "^26.0.6", "i18next-fs-backend": "^2.6.4", "i18next-http-middleware": "^3.9.3", @@ -118,6 +117,7 @@ "reflect-metadata": "^0.2.2", "tslib": "^2.8.1", "typeorm": "^0.3.28", + "wetf": "^0.9.14", "wretch": "^3.0.7", "ws": "^8.20.0" }, diff --git a/src/gateway/events/Connection.ts b/src/gateway/events/Connection.ts
index e3e6d2f0..0d35bd9e 100644 --- a/src/gateway/events/Connection.ts +++ b/src/gateway/events/Connection.ts
@@ -27,15 +27,9 @@ import { Close } from "./Close"; import { Message } from "./Message"; import { Deflate, Inflate } from "fast-zlib"; import { URL } from "node:url"; -import { Config, ErlpackType } from "@spacebar/util"; +import { Config } from "@spacebar/util"; import { Decoder, Encoder } from "@toondepauw/node-zstd"; - -let erlpack: ErlpackType | null = null; -try { - erlpack = require("@yukikaze-bot/erlpack") as ErlpackType; -} catch (e) { - console.log("Failed to import @yukikaze-bot/erlpack: ", e); -} +import { deflate } from "node:zlib"; // TODO: check rate limit // TODO: specify rate limit in config @@ -103,33 +97,16 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In const { searchParams } = new URL(`http://localhost${request.url}`); // @ts-ignore socket.encoding = searchParams.get("encoding") || "json"; - if (!["json", "etf"].includes(socket.encoding)) { - console.error(`[Gateway/${socket.ipAddress}] Unknown encoding: ${socket.encoding}`); - return socket.close(CLOSECODES.Decode_error); - } - - if (socket.encoding === "etf" && !erlpack) throw new Error("Erlpack is not installed: 'npm i @yukikaze-bot/erlpack'"); - + // @ts-ignore + socket.compress = searchParams.get("compress") || ""; socket.version = Number(searchParams.get("version")) || 8; + if (socket.version != 8) { console.error(`[Gateway/${socket.ipAddress}] Invalid API version: ${socket.version}`); return socket.close(CLOSECODES.Invalid_API_version); } - // @ts-ignore - socket.compress = searchParams.get("compress") || ""; - if (socket.compress) { - if (socket.compress === "zlib-stream") { - socket.deflate = new Deflate(); - socket.inflate = new Inflate(); - } else if (socket.compress === "zstd-stream") { - socket.zstdEncoder = new Encoder(6); - socket.zstdDecoder = new Decoder(); - } else { - console.error(`[Gateway/${socket.user_id}] Unknown compression: ${socket.compress}`); - return socket.close(CLOSECODES.Decode_error); - } - } + await setupMessageEncoding(socket); socket.recentTransactions = []; socket.events = {}; @@ -152,3 +129,100 @@ export async function Connection(this: WS.Server, socket: WebSocket, request: In return socket.close(CLOSECODES.Unknown_error); } } + +export async function setupMessageEncoding(socket: WebSocket) { + if (!["json", "etf"].includes(socket.encoding)) { + console.error(`[Gateway/${socket.ipAddress}] Unknown encoding: ${socket.encoding}`); + return socket.close(CLOSECODES.Decode_error); + } + + // if (socket.encoding === "etf" && !erlpack) throw new Error("Erlpack is not installed: 'npm i @yukikaze-bot/erlpack'"); + + if (socket.compress) { + if (socket.compress === "zlib-stream") { + socket.encodeProcessor = new ZlibStreamDeflateOperator(); + socket.decodeProcessor = new ZlibStreamInflateOperator(); + // socket.deflate = new Deflate(); + // socket.inflate = new Inflate(); + } else if (socket.compress === "zstd-stream") { + // socket.zstdEncoder = new Encoder(6); + // socket.zstdDecoder = new Decoder(); + socket.encodeProcessor = new ZstdStreamDeflateOperator(); + socket.decodeProcessor = new ZstdStreamInflateOperator(); + } else { + console.error(`[Gateway/${socket.user_id}] Unknown compression: ${socket.compress}`); + return socket.close(CLOSECODES.Decode_error); + } + } +} + +// hopefully this makes stuff more extensible in the future +export abstract class DataPipelineOperator { + preProcessor?: DataPipelineOperator; + abstract process(data: ArrayBufferLike): Promise<ArrayBufferLike>; + // default implementation: + async dispose(): Promise<void> { + await this.preProcessor?.dispose(); + } + + constructor(preProcessor?: DataPipelineOperator) { + this.preProcessor = preProcessor; + } +} + +class ZlibStreamDeflateOperator extends DataPipelineOperator { + #deflater = new Deflate(); + async process(data: ArrayBufferLike): Promise<ArrayBufferLike> { + if (this.preProcessor) data = await this.preProcessor.process(data); + return Promise.try(() => { + const deflatedBuffer = this.#deflater.process(data); + return bufferToArrayBuffer(deflatedBuffer); + }); + } + async dispose() { + this.#deflater.close(); + await super.dispose?.(); + } +} +class ZlibStreamInflateOperator extends DataPipelineOperator { + #inflater = new Inflate(); + async process(data: ArrayBufferLike): Promise<ArrayBufferLike> { + if (this.preProcessor) data = await this.preProcessor.process(data); + return Promise.try(() => { + const deflatedBuffer = this.#inflater.process(data); + return bufferToArrayBuffer(deflatedBuffer); + }); + } + async dispose() { + this.#inflater.close(); + await super.dispose?.(); + } +} + +class ZstdStreamDeflateOperator extends DataPipelineOperator { + #deflater = new Encoder(6); + async process(data: ArrayBufferLike): Promise<ArrayBufferLike> { + if (this.preProcessor) data = await this.preProcessor.process(data); + const deflatedBuffer = await this.#deflater.encode(arrayBufferToBuffer(data)); + return bufferToArrayBuffer(deflatedBuffer); + } + // Dispose: the ZSTD Encoder has no dispose signature +} +class ZstdStreamInflateOperator extends DataPipelineOperator { + #inflater = new Decoder(); + async process(data: ArrayBufferLike): Promise<ArrayBufferLike> { + if (this.preProcessor) data = await this.preProcessor.process(data); + const deflatedBuffer = await this.#inflater.decode(arrayBufferToBuffer(data)); + return bufferToArrayBuffer(deflatedBuffer); + } + // Dispose: the ZSTD Decoder has no dispose signature +} + +// Yes, this is slightly inefficient, but allows us to have a consistent API for the data pipeline operators... +function bufferToArrayBuffer(data: Buffer): ArrayBufferLike { + return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); +} + +function arrayBufferToBuffer(data: ArrayBufferLike): Buffer { + return Buffer.from(data); +} diff --git a/src/gateway/util/Send.ts b/src/gateway/util/Send.ts
index 6a9c639d..a42c9f3c 100644 --- a/src/gateway/util/Send.ts +++ b/src/gateway/util/Send.ts
@@ -20,8 +20,7 @@ import { Payload, WebSocket } from "@spacebar/gateway"; import fs from "node:fs/promises"; import path from "node:path"; -import { ErlpackType, JSONReplacer } from "@spacebar/util"; -import * as erlpack from "harmony-erlpack"; +import { JSONReplacer } from "@spacebar/util"; // let erlpack: ErlpackType | null = null; // try { diff --git a/src/gateway/util/WebSocket.ts b/src/gateway/util/WebSocket.ts
index 41dc93c1..7efc8938 100644 --- a/src/gateway/util/WebSocket.ts +++ b/src/gateway/util/WebSocket.ts
@@ -19,9 +19,10 @@ import { Intents, ListenEventOpts, Permissions, Session } from "@spacebar/util"; import WS from "ws"; import { Deflate, Inflate } from "fast-zlib"; -import { Capabilities } from "./Capabilities"; import { Decoder, Encoder } from "@toondepauw/node-zstd"; +import { Capabilities } from "./Capabilities"; import { QoSPayload } from "../opcodes/Heartbeat"; +import { DataPipelineOperator } from "../events/Connection"; export interface WebSocket extends WS { recentTransactions: string[]; @@ -31,6 +32,8 @@ export interface WebSocket extends WS { accessToken: string; encoding: "etf" | "json"; compress?: "zlib-stream" | "zstd-stream"; + encodeProcessor: DataPipelineOperator; + decodeProcessor: DataPipelineOperator; ipAddress?: string; userAgent?: string; // for cdn request signing fingerprint?: string;