diff --git a/autobahn/.gitignore b/autobahn/.gitignore index 87087d6..041c157 100644 --- a/autobahn/.gitignore +++ b/autobahn/.gitignore @@ -1 +1,2 @@ -reports/ \ No newline at end of file +reports/ +*.txt \ No newline at end of file diff --git a/deno.json b/deno.json index 416ffb9..6e0fd44 100644 --- a/deno.json +++ b/deno.json @@ -1,12 +1,14 @@ { "name": "@babia/deko", - "version": "0.1.2", + "version": "0.1.3", "imports": { - "@std/bytes": "jsr:@std/bytes@^0.218.2", "@std/encoding": "jsr:@std/encoding@^0.218.2", "@std/io": "jsr:@std/io@^0.218.2" }, "exports": "./mod.ts", + "fmt": { + "exclude": ["README.md"] + }, "exclude": [".vscode", "autobahn", "docs"], "tasks": { "autobahn": "deno run --allow-net test/autobahn.ts", diff --git a/src/_utils.ts b/src/_utils.ts index 61cfd75..1817c0a 100644 --- a/src/_utils.ts +++ b/src/_utils.ts @@ -10,25 +10,21 @@ export function decode(input: BufferSource) { /** host-to-network short (htons). */ export function hton16(n: number) { - return [ - (n & 0xFF00) >> 8, - (n & 0x00FF) >> 0, - ]; + return [(n >> 8) & 0xFF, n & 0xFF]; } /** host-to-network long long (htonll). */ export function hton64(n: number): number[] { const bn = BigInt(n); - // deno-fmt-ignore return [ - Number((bn & 0xFF00_0000_0000_0000n) >> 56n), - Number((bn & 0x00FF_0000_0000_0000n) >> 48n), - Number((bn & 0x0000_FF00_0000_0000n) >> 40n), - Number((bn & 0x0000_00FF_0000_0000n) >> 32n), - Number((bn & 0x0000_0000_FF00_0000n) >> 24n), - Number((bn & 0x0000_0000_00FF_0000n) >> 16n), - Number((bn & 0x0000_0000_0000_FF00n) >> 8n), - Number((bn & 0x0000_0000_0000_00FFn) >> 0n), + Number((bn >> 56n) & 0xFFn), + Number((bn >> 48n) & 0xFFn), + Number((bn >> 40n) & 0xFFn), + Number((bn >> 32n) & 0xFFn), + Number((bn >> 24n) & 0xFFn), + Number((bn >> 16n) & 0xFFn), + Number((bn >> 8n) & 0xFFn), + Number(bn & 0xFFn), ]; } @@ -43,13 +39,13 @@ export function getUint64(buffer: Uint8Array) { (BigInt(buffer[4]) << 24n) | (BigInt(buffer[5]) << 16n) | (BigInt(buffer[6]) << 8n) | - (BigInt(buffer[7]) << 0n) + (BigInt(buffer[7])) ); } /** Get big-endian 16-bit short from buffer. */ export function getUint16(buffer: Uint8Array) { - return (buffer[0] << 8) | (buffer[1] << 0); + return (buffer[0] << 8) | buffer[1]; } /** Check if payload is a valid UTF-8. */ diff --git a/src/client.ts b/src/client.ts index 5f02ed9..8b5db3a 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,5 +1,4 @@ import { writeAll } from "@std/io"; -import { concat } from "@std/bytes"; import { decode, encode, getUint16, hton16, hton64, isUTF8 } from "./_utils.ts"; import { createSecKey, readHandshake, verifyHandshake } from "./handshake.ts"; @@ -51,8 +50,6 @@ export class Deko { #lastPong: number; #state: DekoState; - /** The current fragments. */ - fragments: Message[]; /** Called when the WebSocket connection is opened. */ onOpen: DekoOpenEvent; /** Called when receiving a message. */ @@ -68,7 +65,6 @@ export class Deko { this.#uri = new URL(config.uri); this.#headers = new Headers(config.headers); - this.fragments = []; this.onClose = () => {}; this.onError = () => {}; this.onMessage = () => {}; @@ -103,15 +99,6 @@ export class Deko { return this.#state; } - /** - * Headers used when connecting. - * - * @deprecated Will be removed in `v0.1.3` - */ - get headers(): Headers { - return this.#headers; - } - /** The sub-protocol selected by the server. */ get protocol(): string { return this.#protocol; @@ -199,37 +186,38 @@ export class Deko { throw new Deno.errors.NotConnected("Client is not connected"); } + let pos = 0; const { fin, opcode, payload, mask } = message; - const len = payload.byteLength; - const header = [Number(fin) << 7 | opcode]; - const maskey = mask ?? createMaskingKey(); + + // max_len = payload_len + max_header_len + const frame = new Uint8Array(len + 16); + const key = mask ?? createMaskingKey(); + + frame[0] = (+fin) << 7 | opcode; if (len < 126) { - header[1] = len | 0x80; - } else if (len <= 0xFFFF) { - header[1] = 126 | 0x80; - header.push( - ...hton16(len), - ); - } else if (len <= 0x7FFFFFFF) { - header[1] = 127 | 0x80; - header.push( - ...hton64(len), - ); + frame[1] = len | 0x80; + pos += 2; + } else if (len < 65536) { + frame[1] = 254; // 126 | 0x80 + frame.set(hton16(len), 2); + pos += 4; } else { - return this.close({ - code: CloseCode.MessageTooBig, - reason: "Frame too large", - }); + frame[1] = 255; // 127 | 0x80 + frame.set(hton64(len), 2); + pos += 10; } - unmask(payload, maskey); + if (len > 0) unmask(payload, key); + + frame.set(key, pos); + pos += 4; - const head = new Uint8Array(header); - const frame = concat([head, maskey, payload]); + frame.set(payload, pos); + pos += len; - await writeAll(this.#conn, frame); + await writeAll(this.#conn, frame.subarray(0, pos)); } /** Closes the WebSocket connection. */ @@ -242,24 +230,32 @@ export class Deko { const code = options.code !== undefined ? (loose ? options.code : handleCloseCode(options.code)) : CloseCode.NormalClosure; - const reason = encode(options.reason ?? ""); + const reason = options.reason ?? ""; this.#state = DekoState.CLOSING; + try { - let data = new Uint8Array(0); - if (code > 0) { - data = new Uint8Array(2 + reason.byteLength); + let pos = 0; + const encoded = encode(reason); + const maxLen = 2 + encoded.byteLength; + const data = new Uint8Array(maxLen); + + if (code !== 0) { data.set(hton16(code)); - data.set(reason, 2); + data.set(encoded, 2); + pos += maxLen; } - await this.send({ opcode: OpCode.Close, payload: data, fin: true }); + await this.send({ + opcode: OpCode.Close, + payload: data.subarray(0, pos), + fin: true, + }); } catch (e) { this.onError(e); } finally { - this.fragments = []; this.conn.close(); - this.onClose(code, decode(reason)); + this.onClose(code, reason); this.#state = DekoState.CLOSED; } } @@ -298,8 +294,9 @@ export class Deko { /** Listens and reads incoming messages. */ async #listen() { + const fragments: Message[] = []; while (this.state === DekoState.OPEN) { - const msg = await readMessage(this); + const msg = await readMessage(this, fragments); if (!msg) { this.onError(new ReadFailedError()); break; diff --git a/src/frame.ts b/src/frame.ts index 8101d75..769a593 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -5,6 +5,7 @@ import { unmask } from "./mask.ts"; import { Deko } from "./client.ts"; import { CloseCode } from "./close.ts"; import { InvalidFrameError, ReadFrameError } from "./errors.ts"; +import { Message } from "./message.ts"; /** WebSocket Opcodes define the interpretation of the payload data. */ export enum OpCode { @@ -66,7 +67,7 @@ export class FrameClass { } /** Returns `true` if frame data is valid. */ - validate() { + valid(fragments: Message[]) { const { fin, rsv, opcode, len } = this.#data; if (rsv) { @@ -110,10 +111,7 @@ export class FrameClass { } } - if ( - fin && opcode === OpCode.Continuation && - !this.#client.fragments.length - ) { + if (fin && opcode === OpCode.Continuation && !fragments.length) { this.#client.onError( new InvalidFrameError("There is no message to continue"), ); @@ -165,11 +163,6 @@ export class FrameClass { fin, rsv, opcode, len, payload, mask: key }); - if (!frame.validate()) { - await client.close({ code: 0, loose: true }); - return; - } - return frame; } } diff --git a/src/handshake.ts b/src/handshake.ts index 475611e..1be5092 100644 --- a/src/handshake.ts +++ b/src/handshake.ts @@ -6,12 +6,13 @@ import { BadHandshakeError } from "./errors.ts"; export async function readHandshake(reader: Reader) { let total = 0; + const msg = new Uint8Array(1024); const buffer = new Uint8Array(1); - for (; total < 1024; total++) { + for (; total < 1024; ++total) { if (total > 5) { - const line = decode(msg.slice(total - 4, total)); + const line = decode(msg.subarray(total - 4, total)); if (line === "\r\n\r\n") { break; } @@ -25,7 +26,7 @@ export async function readHandshake(reader: Reader) { msg[total] = buffer[0]; } - return decode(msg.slice(0, total)); + return decode(msg.subarray(0, total)); } export async function verifyHandshake(response: string, key: string) { diff --git a/src/message.ts b/src/message.ts index 3786c1b..6bd54eb 100644 --- a/src/message.ts +++ b/src/message.ts @@ -1,5 +1,3 @@ -import { concat } from "@std/bytes"; - import { Deko } from "./client.ts"; import { FrameClass, isCtrl, isNonCtrl, OpCode } from "./frame.ts"; @@ -16,11 +14,16 @@ export interface Message { } /** Reads incoming message from WebSocket. */ -export async function readMessage(client: Deko) { +export async function readMessage(client: Deko, fragments: Message[]) { try { while (true) { const frame = await FrameClass.from(client); - if (!frame) break; + + if (!frame) return; + if (!frame.valid(fragments)) { + await client.close({ code: 0, loose: true }); + return; + } const { fin, opcode, payload, mask } = frame.data; @@ -29,11 +32,11 @@ export async function readMessage(client: Deko) { } if (!fin) { - client.fragments.push({ fin: false, opcode, payload, mask }); + fragments.push({ fin: false, opcode, payload, mask }); continue; } - if (client.fragments.length === 0) { + if (fragments.length === 0) { return { fin: true, opcode, payload, mask }; } @@ -42,9 +45,8 @@ export async function readMessage(client: Deko) { return; } - const msg = finalMessage(client.fragments, payload, mask); - - client.fragments = []; + const msg = finalMessage(fragments, payload, mask); + fragments.length = 0; return msg; } } catch (e) { @@ -56,10 +58,21 @@ export async function readMessage(client: Deko) { /** Concatenate all fragments to a single message. */ function finalMessage( fragments: Message[], - fin: Uint8Array, + data: Uint8Array, mask?: Uint8Array, ): Message { + let pos = 0; + const len = fragments.reduce( + (size, fragment) => size + fragment.payload.byteLength, + data.byteLength, + ); const opcode = fragments[0].opcode; - const payload = concat([...fragments.map((m) => m.payload), fin]); + const payload = new Uint8Array(len); + for (const fragment of fragments) { + payload.set(fragment.payload, pos); + pos += fragment.payload.byteLength; + } + payload.set(data, pos); + return { fin: true, opcode, payload, mask }; } diff --git a/test/autobahn.ts b/test/autobahn.ts index 20e66a8..aca9c6a 100644 --- a/test/autobahn.ts +++ b/test/autobahn.ts @@ -12,7 +12,9 @@ async function nextTest() { let ws: Deko; if (currentTest > testCount) { - ws = new Deko({ uri: "ws://localhost:9001/updateReports?agent=deko" }); + ws = new Deko({ + uri: "ws://localhost:9001/updateReports?agent=deko@0.1.3", + }); await ws.connect(); return; } @@ -20,7 +22,7 @@ async function nextTest() { console.log(`Running test case ${currentTest}/${testCount}`); ws = new Deko( - { uri: `ws://localhost:9001/runCase?case=${currentTest}&agent=deko` }, + { uri: `ws://localhost:9001/runCase?case=${currentTest}&agent=deko@0.1.3` }, ); ws.onMessage = async (mes) => { await ws.send(mes);