Skip to content

Commit

Permalink
update: version 0.1.2
Browse files Browse the repository at this point in the history
  • Loading branch information
babiabeo committed Mar 6, 2024
1 parent 050af55 commit b65d7fb
Show file tree
Hide file tree
Showing 8 changed files with 80 additions and 72 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ import { Deko } from "@babia/deko";
Or use `jsr:` specifier if you want to use an install step.

```ts
import { Deko } from "jsr:@babia/deko@^0.1.0";
import { Deko } from "jsr:@babia/deko@^0.1.2";
```

## Examples
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@babia/deko",
"version": "0.1.1",
"version": "0.1.2",
"imports": {
"@std/bytes": "jsr:@std/bytes@^0.218.2",
"@std/encoding": "jsr:@std/encoding@^0.218.2",
Expand Down
2 changes: 0 additions & 2 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
* If you want to connect WebSocket on browsers, consider to use {@linkcode WebSocket} instead.
*
* Reference: {@link https://datatracker.ietf.org/doc/html/rfc6455 | RFC 6455}.
*
* @module
*/

export * from "./src/client.ts";
Expand Down
25 changes: 23 additions & 2 deletions src/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,39 @@ export function hton16(n: number) {
/** 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 & 0x0000_0000_0000_FF00n) >> 8n),
Number((bn & 0x0000_0000_0000_00FFn) >> 0n),
];
}

/** Get big-endian 64-bit long from buffer. */
export function getUint64(buffer: Uint8Array) {
// deno-fmt-ignore
return (
(BigInt(buffer[0]) << 56n) |
(BigInt(buffer[1]) << 48n) |
(BigInt(buffer[2]) << 40n) |
(BigInt(buffer[3]) << 32n) |
(BigInt(buffer[4]) << 24n) |
(BigInt(buffer[5]) << 16n) |
(BigInt(buffer[6]) << 8n) |
(BigInt(buffer[7]) << 0n)
);
}

/** Get big-endian 16-bit short from buffer. */
export function getUint16(buffer: Uint8Array) {
return (buffer[0] << 8) | (buffer[1] << 0);
}

/** Check if payload is a valid UTF-8. */
export function isUTF8(payload: Uint8Array) {
try {
Expand Down
57 changes: 29 additions & 28 deletions src/client.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { writeAll } from "@std/io";
import { concat } from "@std/bytes";

import { decode, encode, hton16, hton64, isUTF8 } from "./_utils.ts";
import { decode, encode, getUint16, hton16, hton64, isUTF8 } from "./_utils.ts";
import { createSecKey, readHandshake, verifyHandshake } from "./handshake.ts";
import { Message, readMessage } from "./message.ts";
import {
Expand Down Expand Up @@ -103,7 +103,11 @@ export class Deko {
return this.#state;
}

/** Headers used when connecting. */
/**
* Headers used when connecting.
*
* @deprecated Will be removed in `v0.1.3`
*/
get headers(): Headers {
return this.#headers;
}
Expand Down Expand Up @@ -202,14 +206,14 @@ export class Deko {
const maskey = mask ?? createMaskingKey();

if (len < 126) {
header[1] = len;
header[1] = len | 0x80;
} else if (len <= 0xFFFF) {
header[1] = 126;
header[1] = 126 | 0x80;
header.push(
...hton16(len),
);
} else if (len <= 0x7FFFFFFF) {
header[1] = 127;
header[1] = 127 | 0x80;
header.push(
...hton64(len),
);
Expand All @@ -220,13 +224,12 @@ export class Deko {
});
}

let head = new Uint8Array(header);
head[1] |= 0x80;
head = concat([head, maskey]);
unmask(payload, maskey);
head = concat([head, payload]);

await writeAll(this.#conn, head);
const head = new Uint8Array(header);
const frame = concat([head, maskey, payload]);

await writeAll(this.#conn, frame);
}

/** Closes the WebSocket connection. */
Expand All @@ -236,30 +239,27 @@ export class Deko {
}

const loose = !!(options.loose);
const closeCode = options.code !== undefined
? loose ? options.code : handleCloseCode(options.code)
const code = options.code !== undefined
? (loose ? options.code : handleCloseCode(options.code))
: CloseCode.NormalClosure;
const closeReason = options.reason ?? "";
const reason = encode(options.reason ?? "");

this.#state = DekoState.CLOSING;
try {
let closeFrame: Uint8Array;
if (closeCode > 0) {
const reason = encode(closeReason);
closeFrame = new Uint8Array(reason.byteLength + 2);
closeFrame.set([closeCode >> 8, closeCode & 0xFF]);
closeFrame.set(reason, 2);
} else {
closeFrame = new Uint8Array();
let data = new Uint8Array(0);
if (code > 0) {
data = new Uint8Array(2 + reason.byteLength);
data.set(hton16(code));
data.set(reason, 2);
}

await this.send({ opcode: OpCode.Close, payload: closeFrame, fin: true });
await this.send({ opcode: OpCode.Close, payload: data, fin: true });
} catch (e) {
this.onError(e);
} finally {
this.fragments = [];
this.conn.close();
this.onClose(closeCode, closeReason);
this.onClose(code, decode(reason));
this.#state = DekoState.CLOSED;
}
}
Expand All @@ -278,14 +278,14 @@ export class Deko {
this.#headers.append("Sec-WebSocket-Version", "13");

if (this.#protocols.length) {
this.headers.append(
this.#headers.append(
"Sec-WebSocket-Protocol",
this.#protocols.join(", "),
);
}

let request = `GET ${pathname}${search} HTTP/1.1\r\n`;
for (const [key, value] of this.headers) {
for (const [key, value] of this.#headers) {
request += `${key}: ${value}\r\n`;
}
request += "\r\n";
Expand All @@ -310,9 +310,10 @@ export class Deko {
if (msg.fin && !isUTF8(msg.payload)) {
this.onError(new InvalidUTF8Error());
await this.close({ code: 0, loose: true });
} else {
this.onMessage(msg);
break;
}

this.onMessage(msg);
break;
}
case OpCode.BinaryFrame:
Expand All @@ -339,7 +340,7 @@ export class Deko {
break;
}

const code = (msg.payload[0] << 8) | msg.payload[1];
const code = getUint16(msg.payload);
const mes = msg.payload.subarray(2);
if (!isUTF8(mes)) {
this.onError(new InvalidUTF8Error());
Expand Down
25 changes: 6 additions & 19 deletions src/frame.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Reader } from "@std/io";

import { getUint16, getUint64 } from "./_utils.ts";
import { unmask } from "./mask.ts";
import { Deko } from "./client.ts";
import { CloseCode } from "./close.ts";
Expand Down Expand Up @@ -65,7 +66,7 @@ export class FrameClass {
}

/** Returns `true` if frame data is valid. */
async validate() {
validate() {
const { fin, rsv, opcode, len } = this.#data;

if (rsv) {
Expand All @@ -74,7 +75,6 @@ export class FrameClass {
"Reserved fields must be 0",
),
);
await this.#client.close({ code: 0, loose: true });
return false;
}

Expand All @@ -87,7 +87,6 @@ export class FrameClass {
"Found reserved opcode",
),
);
await this.#client.close({ code: 0, loose: true });
return false;
}

Expand All @@ -98,7 +97,6 @@ export class FrameClass {
"Control frame must not be fragmented",
),
);
await this.#client.close({ code: 0, loose: true });
return false;
}

Expand All @@ -108,7 +106,6 @@ export class FrameClass {
"Control frame payloads must not exceed 125 bytes",
),
);
await this.#client.close({ code: 0, loose: true });
return false;
}
}
Expand All @@ -120,7 +117,6 @@ export class FrameClass {
this.#client.onError(
new InvalidFrameError("There is no message to continue"),
);
await this.#client.close({ code: 0, loose: true });
return false;
}

Expand All @@ -140,20 +136,10 @@ export class FrameClass {

if (len === 126) {
const sub = await readExact(client.conn, 2);
const view = new DataView(
sub.buffer,
sub.byteOffset,
sub.byteLength,
);
len = view.getUint16(0);
len = getUint16(sub);
} else if (len === 127) {
const sub = await readExact(client.conn, 8);
const view = new DataView(
sub.buffer,
sub.byteOffset,
sub.byteLength,
);
const u64 = view.getBigUint64(0);
const u64 = getUint64(sub);
if (u64 > BigInt(Number.MAX_SAFE_INTEGER)) {
await client.close({
code: CloseCode.MessageTooBig,
Expand All @@ -179,7 +165,8 @@ export class FrameClass {
fin, rsv, opcode, len, payload, mask: key
});

if (!(await frame.validate())) {
if (!frame.validate()) {
await client.close({ code: 0, loose: true });
return;
}

Expand Down
5 changes: 3 additions & 2 deletions src/handshake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,9 @@ export async function readHandshake(reader: Reader) {

export async function verifyHandshake(response: string, key: string) {
const lines = response.split(/\r\n|\r|\n/);
if (!lines.shift()?.includes("101 Switching Protocols")) {
throw new BadHandshakeError("Server does not accept handshake");
const status = lines.shift();
if (!status?.includes("101 Switching Protocols")) {
throw new BadHandshakeError(`Server does not accept handshake: ${status}`);
}

let protocol = "";
Expand Down
34 changes: 17 additions & 17 deletions src/message.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { concat } from "@std/bytes";

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

Expand All @@ -24,36 +25,24 @@ export async function readMessage(client: Deko) {
const { fin, opcode, payload, mask } = frame.data;

if (isCtrl(opcode)) {
const msg: Message = { fin, opcode, payload, mask };
return msg;
return { fin, opcode, payload, mask };
}

if (!frame.data.fin) {
client.fragments.push({ fin, opcode, payload, mask });
if (!fin) {
client.fragments.push({ fin: false, opcode, payload, mask });
continue;
}

if (client.fragments.length === 0) {
const msg: Message = { fin, opcode, payload, mask };
return msg;
return { fin: true, opcode, payload, mask };
}

if (isNonCtrl(opcode)) {
await client.close({ code: 0, loose: true });
return;
}

const data = concat([
...client.fragments.map((mes) => mes.payload),
payload,
]);

const msg: Message = {
fin,
mask,
opcode: client.fragments[0].opcode,
payload: data,
};
const msg = finalMessage(client.fragments, payload, mask);

client.fragments = [];
return msg;
Expand All @@ -63,3 +52,14 @@ export async function readMessage(client: Deko) {
return;
}
}

/** Concatenate all fragments to a single message. */
function finalMessage(
fragments: Message[],
fin: Uint8Array,
mask?: Uint8Array,
): Message {
const opcode = fragments[0].opcode;
const payload = concat([...fragments.map((m) => m.payload), fin]);
return { fin: true, opcode, payload, mask };
}

0 comments on commit b65d7fb

Please sign in to comment.