Skip to content

Commit

Permalink
update: version 0.1.3
Browse files Browse the repository at this point in the history
  • Loading branch information
babiabeo committed Mar 16, 2024
1 parent b65d7fb commit 030022f
Show file tree
Hide file tree
Showing 8 changed files with 93 additions and 88 deletions.
3 changes: 2 additions & 1 deletion autobahn/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
reports/
reports/
*.txt
6 changes: 4 additions & 2 deletions deno.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
26 changes: 11 additions & 15 deletions src/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
];
}

Expand All @@ -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. */
Expand Down
85 changes: 41 additions & 44 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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. */
Expand All @@ -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 = () => {};
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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. */
Expand All @@ -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;
}
}
Expand Down Expand Up @@ -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;
Expand Down
13 changes: 3 additions & 10 deletions src/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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"),
);
Expand Down Expand Up @@ -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;
}
}
Expand Down
7 changes: 4 additions & 3 deletions src/handshake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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) {
Expand Down
35 changes: 24 additions & 11 deletions src/message.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { concat } from "@std/bytes";

import { Deko } from "./client.ts";
import { FrameClass, isCtrl, isNonCtrl, OpCode } from "./frame.ts";

Expand All @@ -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;

Expand All @@ -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 };
}

Expand All @@ -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) {
Expand All @@ -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 };
}
6 changes: 4 additions & 2 deletions test/autobahn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@ 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;
}

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);
Expand Down

0 comments on commit 030022f

Please sign in to comment.