summaryrefslogtreecommitdiff
path: root/std/ws
diff options
context:
space:
mode:
Diffstat (limited to 'std/ws')
-rw-r--r--std/ws/README.md200
-rw-r--r--std/ws/example_client.ts58
-rw-r--r--std/ws/example_server.ts64
-rw-r--r--std/ws/mod.ts507
-rw-r--r--std/ws/sha1.ts374
-rw-r--r--std/ws/sha1_test.ts24
-rw-r--r--std/ws/test.ts227
7 files changed, 1454 insertions, 0 deletions
diff --git a/std/ws/README.md b/std/ws/README.md
new file mode 100644
index 000000000..e00c469ff
--- /dev/null
+++ b/std/ws/README.md
@@ -0,0 +1,200 @@
+# ws
+
+ws module is made to provide helpers to create WebSocket client/server.
+
+## Usage
+
+### Server
+
+```ts
+import { serve } from "https://deno.land/std/http/server.ts";
+import {
+ acceptWebSocket,
+ isWebSocketCloseEvent,
+ isWebSocketPingEvent,
+ WebSocket
+} from "https://deno.land/std/ws/mod.ts";
+
+const port = Deno.args[1] || "8080";
+async function main(): Promise<void> {
+ console.log(`websocket server is running on :${port}`);
+ for await (const req of serve(`:${port}`)) {
+ const { headers, conn } = req;
+ acceptWebSocket({
+ conn,
+ headers,
+ bufReader: req.r,
+ bufWriter: req.w
+ })
+ .then(
+ async (sock: WebSocket): Promise<void> => {
+ console.log("socket connected!");
+ const it = sock.receive();
+ while (true) {
+ try {
+ const { done, value } = await it.next();
+ if (done) {
+ break;
+ }
+ const ev = value;
+ if (typeof ev === "string") {
+ // text message
+ console.log("ws:Text", ev);
+ await sock.send(ev);
+ } else if (ev instanceof Uint8Array) {
+ // binary message
+ console.log("ws:Binary", ev);
+ } else if (isWebSocketPingEvent(ev)) {
+ const [, body] = ev;
+ // ping
+ console.log("ws:Ping", body);
+ } else if (isWebSocketCloseEvent(ev)) {
+ // close
+ const { code, reason } = ev;
+ console.log("ws:Close", code, reason);
+ }
+ } catch (e) {
+ console.error(`failed to receive frame: ${e}`);
+ await sock.close(1000).catch(console.error);
+ }
+ }
+ }
+ )
+ .catch((err: Error): void => {
+ console.error(`failed to accept websocket: ${err}`);
+ });
+ }
+}
+
+if (import.meta.main) {
+ main();
+}
+```
+
+### Client
+
+```ts
+import {
+ connectWebSocket,
+ isWebSocketCloseEvent,
+ isWebSocketPingEvent,
+ isWebSocketPongEvent
+} from "https://deno.land/std/ws/mod.ts";
+import { encode } from "https://deno.land/std/strings/mod.ts";
+import { BufReader } from "https://deno.land/std/io/bufio.ts";
+import { TextProtoReader } from "https://deno.land/std/textproto/mod.ts";
+import { blue, green, red, yellow } from "https://deno.land/std/fmt/colors.ts";
+
+const endpoint = Deno.args[1] || "ws://127.0.0.1:8080";
+async function main(): Promise<void> {
+ const sock = await connectWebSocket(endpoint);
+ console.log(green("ws connected! (type 'close' to quit)"));
+ (async function(): Promise<void> {
+ for await (const msg of sock.receive()) {
+ if (typeof msg === "string") {
+ console.log(yellow("< " + msg));
+ } else if (isWebSocketPingEvent(msg)) {
+ console.log(blue("< ping"));
+ } else if (isWebSocketPongEvent(msg)) {
+ console.log(blue("< pong"));
+ } else if (isWebSocketCloseEvent(msg)) {
+ console.log(red(`closed: code=${msg.code}, reason=${msg.reason}`));
+ }
+ }
+ })();
+ const tpr = new TextProtoReader(new BufReader(Deno.stdin));
+ while (true) {
+ await Deno.stdout.write(encode("> "));
+ const [line, err] = await tpr.readLine();
+ if (err) {
+ console.error(red(`failed to read line from stdin: ${err}`));
+ break;
+ }
+ if (line === "close") {
+ break;
+ } else if (line === "ping") {
+ await sock.ping();
+ } else {
+ await sock.send(line);
+ }
+ await new Promise((resolve): number => setTimeout(resolve, 0));
+ }
+ await sock.close(1000);
+ Deno.exit(0);
+}
+
+if (import.meta.main) {
+ main();
+}
+```
+
+## API
+
+### isWebSocketCloseEvent
+
+Returns true if input value is a WebSocketCloseEvent, false otherwise.
+
+### isWebSocketPingEvent
+
+Returns true if input value is a WebSocketPingEvent, false otherwise.
+
+### isWebSocketPongEvent
+
+Returns true if input value is a WebSocketPongEvent, false otherwise.
+
+### append
+
+This module is used to merge two Uint8Arrays.
+
+- note: This module might move to common/util.
+
+```ts
+import { append } from "https://deno.land/std/ws/mod.ts";
+
+// a = [1], b = [2]
+append(a, b); // output: [1, 2]
+
+// a = [1], b = null
+append(a, b); // output: [1]
+
+// a = [], b = [2]
+append(a, b); // output: [2]
+```
+
+### unmask
+
+Unmask masked WebSocket payload.
+
+### writeFrame
+
+Write WebSocket frame to inputted writer.
+
+### readFrame
+
+Read WebSocket frame from inputted BufReader.
+
+### createMask
+
+Create mask from the client to the server with random 32bit number.
+
+### acceptable
+
+Returns true if input headers are usable for WebSocket, otherwise false
+
+### createSecAccept
+
+Create value of Sec-WebSocket-Accept header from inputted nonce.
+
+### acceptWebSocket
+
+Upgrade inputted TCP connection into WebSocket connection.
+
+### createSecKey
+
+Returns base64 encoded 16 bytes string for Sec-WebSocket-Key header.
+
+### connectWebSocket
+
+Connect to WebSocket endpoint url with inputted endpoint string and headers.
+
+- note: Endpoint must be acceptable for URL.
diff --git a/std/ws/example_client.ts b/std/ws/example_client.ts
new file mode 100644
index 000000000..e3f2f355e
--- /dev/null
+++ b/std/ws/example_client.ts
@@ -0,0 +1,58 @@
+import {
+ connectWebSocket,
+ isWebSocketCloseEvent,
+ isWebSocketPingEvent,
+ isWebSocketPongEvent
+} from "../ws/mod.ts";
+import { encode } from "../strings/mod.ts";
+import { BufReader } from "../io/bufio.ts";
+import { TextProtoReader } from "../textproto/mod.ts";
+import { blue, green, red, yellow } from "../fmt/colors.ts";
+
+const endpoint = Deno.args[1] || "ws://127.0.0.1:8080";
+/** simple websocket cli */
+async function main(): Promise<void> {
+ const sock = await connectWebSocket(endpoint);
+ console.log(green("ws connected! (type 'close' to quit)"));
+ (async function(): Promise<void> {
+ for await (const msg of sock.receive()) {
+ if (typeof msg === "string") {
+ console.log(yellow("< " + msg));
+ } else if (isWebSocketPingEvent(msg)) {
+ console.log(blue("< ping"));
+ } else if (isWebSocketPongEvent(msg)) {
+ console.log(blue("< pong"));
+ } else if (isWebSocketCloseEvent(msg)) {
+ console.log(red(`closed: code=${msg.code}, reason=${msg.reason}`));
+ }
+ }
+ })();
+ const tpr = new TextProtoReader(new BufReader(Deno.stdin));
+ while (true) {
+ await Deno.stdout.write(encode("> "));
+ const [line, err] = await tpr.readLine();
+ if (err) {
+ console.error(red(`failed to read line from stdin: ${err}`));
+ break;
+ }
+ if (line === "close") {
+ break;
+ } else if (line === "ping") {
+ await sock.ping();
+ } else {
+ await sock.send(line);
+ }
+ // FIXME: Without this,
+ // sock.receive() won't resolved though it is readable...
+ await new Promise((resolve): void => {
+ setTimeout(resolve, 0);
+ });
+ }
+ await sock.close(1000);
+ // FIXME: conn.close() won't shutdown process...
+ Deno.exit(0);
+}
+
+if (import.meta.main) {
+ main();
+}
diff --git a/std/ws/example_server.ts b/std/ws/example_server.ts
new file mode 100644
index 000000000..0564439c9
--- /dev/null
+++ b/std/ws/example_server.ts
@@ -0,0 +1,64 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { serve } from "../http/server.ts";
+import {
+ acceptWebSocket,
+ isWebSocketCloseEvent,
+ isWebSocketPingEvent,
+ WebSocket
+} from "./mod.ts";
+
+/** websocket echo server */
+const port = Deno.args[1] || "8080";
+async function main(): Promise<void> {
+ console.log(`websocket server is running on :${port}`);
+ for await (const req of serve(`:${port}`)) {
+ const { headers, conn } = req;
+ acceptWebSocket({
+ conn,
+ headers,
+ bufReader: req.r,
+ bufWriter: req.w
+ })
+ .then(
+ async (sock: WebSocket): Promise<void> => {
+ console.log("socket connected!");
+ const it = sock.receive();
+ while (true) {
+ try {
+ const { done, value } = await it.next();
+ if (done) {
+ break;
+ }
+ const ev = value;
+ if (typeof ev === "string") {
+ // text message
+ console.log("ws:Text", ev);
+ await sock.send(ev);
+ } else if (ev instanceof Uint8Array) {
+ // binary message
+ console.log("ws:Binary", ev);
+ } else if (isWebSocketPingEvent(ev)) {
+ const [, body] = ev;
+ // ping
+ console.log("ws:Ping", body);
+ } else if (isWebSocketCloseEvent(ev)) {
+ // close
+ const { code, reason } = ev;
+ console.log("ws:Close", code, reason);
+ }
+ } catch (e) {
+ console.error(`failed to receive frame: ${e}`);
+ await sock.close(1000).catch(console.error);
+ }
+ }
+ }
+ )
+ .catch((err: Error): void => {
+ console.error(`failed to accept websocket: ${err}`);
+ });
+ }
+}
+
+if (import.meta.main) {
+ main();
+}
diff --git a/std/ws/mod.ts b/std/ws/mod.ts
new file mode 100644
index 000000000..11e3ee23e
--- /dev/null
+++ b/std/ws/mod.ts
@@ -0,0 +1,507 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+
+import { decode, encode } from "../strings/mod.ts";
+import { hasOwnProperty } from "../util/has_own_property.ts";
+
+type Conn = Deno.Conn;
+type Writer = Deno.Writer;
+import { BufReader, BufWriter, UnexpectedEOFError } from "../io/bufio.ts";
+import { readLong, readShort, sliceLongToBytes } from "../io/ioutil.ts";
+import { Sha1 } from "./sha1.ts";
+import { writeResponse } from "../http/server.ts";
+import { TextProtoReader } from "../textproto/mod.ts";
+
+export enum OpCode {
+ Continue = 0x0,
+ TextFrame = 0x1,
+ BinaryFrame = 0x2,
+ Close = 0x8,
+ Ping = 0x9,
+ Pong = 0xa
+}
+
+export type WebSocketEvent =
+ | string
+ | Uint8Array
+ | WebSocketCloseEvent
+ | WebSocketPingEvent
+ | WebSocketPongEvent;
+
+export interface WebSocketCloseEvent {
+ code: number;
+ reason?: string;
+}
+
+export function isWebSocketCloseEvent(
+ a: WebSocketEvent
+): a is WebSocketCloseEvent {
+ return hasOwnProperty(a, "code");
+}
+
+export type WebSocketPingEvent = ["ping", Uint8Array];
+
+export function isWebSocketPingEvent(
+ a: WebSocketEvent
+): a is WebSocketPingEvent {
+ return Array.isArray(a) && a[0] === "ping" && a[1] instanceof Uint8Array;
+}
+
+export type WebSocketPongEvent = ["pong", Uint8Array];
+
+export function isWebSocketPongEvent(
+ a: WebSocketEvent
+): a is WebSocketPongEvent {
+ return Array.isArray(a) && a[0] === "pong" && a[1] instanceof Uint8Array;
+}
+
+export type WebSocketMessage = string | Uint8Array;
+
+// TODO move this to common/util module
+export function append(a: Uint8Array, b: Uint8Array): Uint8Array {
+ if (a == null || !a.length) {
+ return b;
+ }
+ if (b == null || !b.length) {
+ return a;
+ }
+ const output = new Uint8Array(a.length + b.length);
+ output.set(a, 0);
+ output.set(b, a.length);
+ return output;
+}
+
+export class SocketClosedError extends Error {}
+
+export interface WebSocketFrame {
+ isLastFrame: boolean;
+ opcode: OpCode;
+ mask?: Uint8Array;
+ payload: Uint8Array;
+}
+
+export interface WebSocket {
+ readonly conn: Conn;
+ readonly isClosed: boolean;
+
+ receive(): AsyncIterableIterator<WebSocketEvent>;
+
+ send(data: WebSocketMessage): Promise<void>;
+
+ ping(data?: WebSocketMessage): Promise<void>;
+
+ close(code: number, reason?: string): Promise<void>;
+}
+
+/** Unmask masked websocket payload */
+export function unmask(payload: Uint8Array, mask?: Uint8Array): void {
+ if (mask) {
+ for (let i = 0, len = payload.length; i < len; i++) {
+ payload[i] ^= mask![i & 3];
+ }
+ }
+}
+
+/** Write websocket frame to given writer */
+export async function writeFrame(
+ frame: WebSocketFrame,
+ writer: Writer
+): Promise<void> {
+ const payloadLength = frame.payload.byteLength;
+ let header: Uint8Array;
+ const hasMask = frame.mask ? 0x80 : 0;
+ if (frame.mask && frame.mask.byteLength !== 4) {
+ throw new Error(
+ "invalid mask. mask must be 4 bytes: length=" + frame.mask.byteLength
+ );
+ }
+ if (payloadLength < 126) {
+ header = new Uint8Array([0x80 | frame.opcode, hasMask | payloadLength]);
+ } else if (payloadLength < 0xffff) {
+ header = new Uint8Array([
+ 0x80 | frame.opcode,
+ hasMask | 0b01111110,
+ payloadLength >>> 8,
+ payloadLength & 0x00ff
+ ]);
+ } else {
+ header = new Uint8Array([
+ 0x80 | frame.opcode,
+ hasMask | 0b01111111,
+ ...sliceLongToBytes(payloadLength)
+ ]);
+ }
+ if (frame.mask) {
+ header = append(header, frame.mask);
+ }
+ unmask(frame.payload, frame.mask);
+ header = append(header, frame.payload);
+ const w = BufWriter.create(writer);
+ await w.write(header);
+ await w.flush();
+}
+
+/** Read websocket frame from given BufReader */
+export async function readFrame(buf: BufReader): Promise<WebSocketFrame> {
+ let b = await buf.readByte();
+ if (b === Deno.EOF) throw new UnexpectedEOFError();
+ let isLastFrame = false;
+ switch (b >>> 4) {
+ case 0b1000:
+ isLastFrame = true;
+ break;
+ case 0b0000:
+ isLastFrame = false;
+ break;
+ default:
+ throw new Error("invalid signature");
+ }
+ const opcode = b & 0x0f;
+ // has_mask & payload
+ b = await buf.readByte();
+ if (b === Deno.EOF) throw new UnexpectedEOFError();
+ const hasMask = b >>> 7;
+ let payloadLength = b & 0b01111111;
+ if (payloadLength === 126) {
+ const l = await readShort(buf);
+ if (l === Deno.EOF) throw new UnexpectedEOFError();
+ payloadLength = l;
+ } else if (payloadLength === 127) {
+ const l = await readLong(buf);
+ if (l === Deno.EOF) throw new UnexpectedEOFError();
+ payloadLength = Number(l);
+ }
+ // mask
+ let mask;
+ if (hasMask) {
+ mask = new Uint8Array(4);
+ await buf.readFull(mask);
+ }
+ // payload
+ const payload = new Uint8Array(payloadLength);
+ await buf.readFull(payload);
+ return {
+ isLastFrame,
+ opcode,
+ mask,
+ payload
+ };
+}
+
+// Create client-to-server mask, random 32bit number
+function createMask(): Uint8Array {
+ return crypto.getRandomValues(new Uint8Array(4));
+}
+
+class WebSocketImpl implements WebSocket {
+ private readonly mask?: Uint8Array;
+ private readonly bufReader: BufReader;
+ private readonly bufWriter: BufWriter;
+
+ constructor(
+ readonly conn: Conn,
+ opts: {
+ bufReader?: BufReader;
+ bufWriter?: BufWriter;
+ mask?: Uint8Array;
+ }
+ ) {
+ this.mask = opts.mask;
+ this.bufReader = opts.bufReader || new BufReader(conn);
+ this.bufWriter = opts.bufWriter || new BufWriter(conn);
+ }
+
+ async *receive(): AsyncIterableIterator<WebSocketEvent> {
+ let frames: WebSocketFrame[] = [];
+ let payloadsLength = 0;
+ while (true) {
+ const frame = await readFrame(this.bufReader);
+ unmask(frame.payload, frame.mask);
+ switch (frame.opcode) {
+ case OpCode.TextFrame:
+ case OpCode.BinaryFrame:
+ case OpCode.Continue:
+ frames.push(frame);
+ payloadsLength += frame.payload.length;
+ if (frame.isLastFrame) {
+ const concat = new Uint8Array(payloadsLength);
+ let offs = 0;
+ for (const frame of frames) {
+ concat.set(frame.payload, offs);
+ offs += frame.payload.length;
+ }
+ if (frames[0].opcode === OpCode.TextFrame) {
+ // text
+ yield decode(concat);
+ } else {
+ // binary
+ yield concat;
+ }
+ frames = [];
+ payloadsLength = 0;
+ }
+ break;
+ case OpCode.Close:
+ // [0x12, 0x34] -> 0x1234
+ const code = (frame.payload[0] << 8) | frame.payload[1];
+ const reason = decode(
+ frame.payload.subarray(2, frame.payload.length)
+ );
+ await this.close(code, reason);
+ yield { code, reason };
+ return;
+ case OpCode.Ping:
+ await writeFrame(
+ {
+ opcode: OpCode.Pong,
+ payload: frame.payload,
+ isLastFrame: true
+ },
+ this.bufWriter
+ );
+ yield ["ping", frame.payload] as WebSocketPingEvent;
+ break;
+ case OpCode.Pong:
+ yield ["pong", frame.payload] as WebSocketPongEvent;
+ break;
+ default:
+ }
+ }
+ }
+
+ async send(data: WebSocketMessage): Promise<void> {
+ if (this.isClosed) {
+ throw new SocketClosedError("socket has been closed");
+ }
+ const opcode =
+ typeof data === "string" ? OpCode.TextFrame : OpCode.BinaryFrame;
+ const payload = typeof data === "string" ? encode(data) : data;
+ const isLastFrame = true;
+ await writeFrame(
+ {
+ isLastFrame,
+ opcode,
+ payload,
+ mask: this.mask
+ },
+ this.bufWriter
+ );
+ }
+
+ async ping(data: WebSocketMessage = ""): Promise<void> {
+ const payload = typeof data === "string" ? encode(data) : data;
+ await writeFrame(
+ {
+ isLastFrame: true,
+ opcode: OpCode.Ping,
+ mask: this.mask,
+ payload
+ },
+ this.bufWriter
+ );
+ }
+
+ private _isClosed = false;
+ get isClosed(): boolean {
+ return this._isClosed;
+ }
+
+ async close(code: number, reason?: string): Promise<void> {
+ try {
+ const header = [code >>> 8, code & 0x00ff];
+ let payload: Uint8Array;
+ if (reason) {
+ const reasonBytes = encode(reason);
+ payload = new Uint8Array(2 + reasonBytes.byteLength);
+ payload.set(header);
+ payload.set(reasonBytes, 2);
+ } else {
+ payload = new Uint8Array(header);
+ }
+ await writeFrame(
+ {
+ isLastFrame: true,
+ opcode: OpCode.Close,
+ mask: this.mask,
+ payload
+ },
+ this.bufWriter
+ );
+ } catch (e) {
+ throw e;
+ } finally {
+ this.ensureSocketClosed();
+ }
+ }
+
+ private ensureSocketClosed(): void {
+ if (this.isClosed) {
+ return;
+ }
+ try {
+ this.conn.close();
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this._isClosed = true;
+ }
+ }
+}
+
+/** Return whether given headers is acceptable for websocket */
+export function acceptable(req: { headers: Headers }): boolean {
+ const upgrade = req.headers.get("upgrade");
+ if (!upgrade || upgrade.toLowerCase() !== "websocket") {
+ return false;
+ }
+ const secKey = req.headers.get("sec-websocket-key");
+ return (
+ req.headers.has("sec-websocket-key") &&
+ typeof secKey === "string" &&
+ secKey.length > 0
+ );
+}
+
+const kGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+/** Create sec-websocket-accept header value with given nonce */
+export function createSecAccept(nonce: string): string {
+ const sha1 = new Sha1();
+ sha1.update(nonce + kGUID);
+ const bytes = sha1.digest();
+ return btoa(String.fromCharCode(...bytes));
+}
+
+/** Upgrade given TCP connection into websocket connection */
+export async function acceptWebSocket(req: {
+ conn: Conn;
+ bufWriter: BufWriter;
+ bufReader: BufReader;
+ headers: Headers;
+}): Promise<WebSocket> {
+ const { conn, headers, bufReader, bufWriter } = req;
+ if (acceptable(req)) {
+ const sock = new WebSocketImpl(conn, { bufReader, bufWriter });
+ const secKey = headers.get("sec-websocket-key");
+ if (typeof secKey !== "string") {
+ throw new Error("sec-websocket-key is not provided");
+ }
+ const secAccept = createSecAccept(secKey);
+ await writeResponse(bufWriter, {
+ status: 101,
+ headers: new Headers({
+ Upgrade: "websocket",
+ Connection: "Upgrade",
+ "Sec-WebSocket-Accept": secAccept
+ })
+ });
+ return sock;
+ }
+ throw new Error("request is not acceptable");
+}
+
+const kSecChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-.~_";
+
+/** Create WebSocket-Sec-Key. Base64 encoded 16 bytes string */
+export function createSecKey(): string {
+ let key = "";
+ for (let i = 0; i < 16; i++) {
+ const j = Math.round(Math.random() * kSecChars.length);
+ key += kSecChars[j];
+ }
+ return btoa(key);
+}
+
+async function handshake(
+ url: URL,
+ headers: Headers,
+ bufReader: BufReader,
+ bufWriter: BufWriter
+): Promise<void> {
+ const { hostname, pathname, searchParams } = url;
+ const key = createSecKey();
+
+ if (!headers.has("host")) {
+ headers.set("host", hostname);
+ }
+ headers.set("upgrade", "websocket");
+ headers.set("connection", "upgrade");
+ headers.set("sec-websocket-key", key);
+ headers.set("sec-websocket-version", "13");
+
+ let headerStr = `GET ${pathname}?${searchParams || ""} HTTP/1.1\r\n`;
+ for (const [key, value] of headers) {
+ headerStr += `${key}: ${value}\r\n`;
+ }
+ headerStr += "\r\n";
+
+ await bufWriter.write(encode(headerStr));
+ await bufWriter.flush();
+
+ const tpReader = new TextProtoReader(bufReader);
+ const statusLine = await tpReader.readLine();
+ if (statusLine === Deno.EOF) {
+ throw new UnexpectedEOFError();
+ }
+ const m = statusLine.match(/^(?<version>\S+) (?<statusCode>\S+) /);
+ if (!m) {
+ throw new Error("ws: invalid status line: " + statusLine);
+ }
+
+ // @ts-ignore
+ const { version, statusCode } = m.groups;
+ if (version !== "HTTP/1.1" || statusCode !== "101") {
+ throw new Error(
+ `ws: server didn't accept handshake: ` +
+ `version=${version}, statusCode=${statusCode}`
+ );
+ }
+
+ const responseHeaders = await tpReader.readMIMEHeader();
+ if (responseHeaders === Deno.EOF) {
+ throw new UnexpectedEOFError();
+ }
+
+ const expectedSecAccept = createSecAccept(key);
+ const secAccept = responseHeaders.get("sec-websocket-accept");
+ if (secAccept !== expectedSecAccept) {
+ throw new Error(
+ `ws: unexpected sec-websocket-accept header: ` +
+ `expected=${expectedSecAccept}, actual=${secAccept}`
+ );
+ }
+}
+
+/**
+ * Connect to given websocket endpoint url.
+ * Endpoint must be acceptable for URL.
+ */
+export async function connectWebSocket(
+ endpoint: string,
+ headers: Headers = new Headers()
+): Promise<WebSocket> {
+ const url = new URL(endpoint);
+ const { hostname } = url;
+ let conn: Conn;
+ if (url.protocol === "http:" || url.protocol === "ws:") {
+ const port = parseInt(url.port || "80");
+ conn = await Deno.dial({ hostname, port });
+ } else if (url.protocol === "https:" || url.protocol === "wss:") {
+ const port = parseInt(url.port || "443");
+ conn = await Deno.dialTLS({ hostname, port });
+ } else {
+ throw new Error("ws: unsupported protocol: " + url.protocol);
+ }
+ const bufWriter = new BufWriter(conn);
+ const bufReader = new BufReader(conn);
+ try {
+ await handshake(url, headers, bufReader, bufWriter);
+ } catch (err) {
+ conn.close();
+ throw err;
+ }
+ return new WebSocketImpl(conn, {
+ bufWriter,
+ bufReader,
+ mask: createMask()
+ });
+}
diff --git a/std/ws/sha1.ts b/std/ws/sha1.ts
new file mode 100644
index 000000000..dc8ba680c
--- /dev/null
+++ b/std/ws/sha1.ts
@@ -0,0 +1,374 @@
+/*
+ * [js-sha1]{@link https://github.com/emn178/js-sha1}
+ *
+ * @version 0.6.0
+ * @author Chen, Yi-Cyuan [emn178@gmail.com]
+ * @copyright Chen, Yi-Cyuan 2014-2017
+ * @license MIT
+ */
+/*jslint bitwise: true */
+
+const HEX_CHARS = "0123456789abcdef".split("");
+const EXTRA = Uint32Array.of(-2147483648, 8388608, 32768, 128);
+const SHIFT = Uint32Array.of(24, 16, 8, 0);
+
+const blocks = new Uint32Array(80);
+
+export class Sha1 {
+ private _blocks: Uint32Array;
+ private _block: number;
+ private _start: number;
+ private _bytes: number;
+ private _hBytes: number;
+ private _finalized: boolean;
+ private _hashed: boolean;
+
+ private _h0 = 0x67452301;
+ private _h1 = 0xefcdab89;
+ private _h2 = 0x98badcfe;
+ private _h3 = 0x10325476;
+ private _h4 = 0xc3d2e1f0;
+ private _lastByteIndex = 0;
+
+ constructor(sharedMemory = false) {
+ if (sharedMemory) {
+ this._blocks = blocks.fill(0, 0, 17);
+ } else {
+ this._blocks = new Uint32Array(80);
+ }
+
+ this._h0 = 0x67452301;
+ this._h1 = 0xefcdab89;
+ this._h2 = 0x98badcfe;
+ this._h3 = 0x10325476;
+ this._h4 = 0xc3d2e1f0;
+
+ this._block = this._start = this._bytes = this._hBytes = 0;
+ this._finalized = this._hashed = false;
+ }
+
+ update(data: string | ArrayBuffer | ArrayBufferView): void {
+ if (this._finalized) {
+ return;
+ }
+ let notString = true;
+ let message;
+ if (data instanceof ArrayBuffer) {
+ message = new Uint8Array(data);
+ } else if (ArrayBuffer.isView(data)) {
+ message = new Uint8Array(data.buffer);
+ } else {
+ notString = false;
+ message = String(data);
+ }
+ let code;
+ let index = 0;
+ let i;
+ const start = this._start;
+ const length = message.length || 0;
+ const blocks = this._blocks;
+
+ while (index < length) {
+ if (this._hashed) {
+ this._hashed = false;
+ blocks[0] = this._block;
+ blocks.fill(0, 1, 17);
+ }
+
+ if (notString) {
+ for (i = start; index < length && i < 64; ++index) {
+ blocks[i >> 2] |= (message[index] as number) << SHIFT[i++ & 3];
+ }
+ } else {
+ for (i = start; index < length && i < 64; ++index) {
+ code = (message as string).charCodeAt(index);
+ if (code < 0x80) {
+ blocks[i >> 2] |= code << SHIFT[i++ & 3];
+ } else if (code < 0x800) {
+ blocks[i >> 2] |= (0xc0 | (code >> 6)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ } else if (code < 0xd800 || code >= 0xe000) {
+ blocks[i >> 2] |= (0xe0 | (code >> 12)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ } else {
+ code =
+ 0x10000 +
+ (((code & 0x3ff) << 10) |
+ ((message as string).charCodeAt(++index) & 0x3ff));
+ blocks[i >> 2] |= (0xf0 | (code >> 18)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 12) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | ((code >> 6) & 0x3f)) << SHIFT[i++ & 3];
+ blocks[i >> 2] |= (0x80 | (code & 0x3f)) << SHIFT[i++ & 3];
+ }
+ }
+ }
+
+ this._lastByteIndex = i;
+ this._bytes += i - start;
+ if (i >= 64) {
+ this._block = blocks[16];
+ this._start = i - 64;
+ this.hash();
+ this._hashed = true;
+ } else {
+ this._start = i;
+ }
+ }
+ if (this._bytes > 4294967295) {
+ this._hBytes += (this._bytes / 4294967296) >>> 0;
+ this._bytes = this._bytes >>> 0;
+ }
+ }
+
+ finalize(): void {
+ if (this._finalized) {
+ return;
+ }
+ this._finalized = true;
+ const blocks = this._blocks;
+ const i = this._lastByteIndex;
+ blocks[16] = this._block;
+ blocks[i >> 2] |= EXTRA[i & 3];
+ this._block = blocks[16];
+ if (i >= 56) {
+ if (!this._hashed) {
+ this.hash();
+ }
+ blocks[0] = this._block;
+ blocks.fill(0, 1, 17);
+ }
+ blocks[14] = (this._hBytes << 3) | (this._bytes >>> 29);
+ blocks[15] = this._bytes << 3;
+ this.hash();
+ }
+
+ hash(): void {
+ let a = this._h0;
+ let b = this._h1;
+ let c = this._h2;
+ let d = this._h3;
+ let e = this._h4;
+ let f, j, t;
+ const blocks = this._blocks;
+
+ for (j = 16; j < 80; ++j) {
+ t = blocks[j - 3] ^ blocks[j - 8] ^ blocks[j - 14] ^ blocks[j - 16];
+ blocks[j] = (t << 1) | (t >>> 31);
+ }
+
+ for (j = 0; j < 20; j += 5) {
+ f = (b & c) | (~b & d);
+ t = (a << 5) | (a >>> 27);
+ e = (t + f + e + 1518500249 + blocks[j]) >>> 0;
+ b = (b << 30) | (b >>> 2);
+
+ f = (a & b) | (~a & c);
+ t = (e << 5) | (e >>> 27);
+ d = (t + f + d + 1518500249 + blocks[j + 1]) >>> 0;
+ a = (a << 30) | (a >>> 2);
+
+ f = (e & a) | (~e & b);
+ t = (d << 5) | (d >>> 27);
+ c = (t + f + c + 1518500249 + blocks[j + 2]) >>> 0;
+ e = (e << 30) | (e >>> 2);
+
+ f = (d & e) | (~d & a);
+ t = (c << 5) | (c >>> 27);
+ b = (t + f + b + 1518500249 + blocks[j + 3]) >>> 0;
+ d = (d << 30) | (d >>> 2);
+
+ f = (c & d) | (~c & e);
+ t = (b << 5) | (b >>> 27);
+ a = (t + f + a + 1518500249 + blocks[j + 4]) >>> 0;
+ c = (c << 30) | (c >>> 2);
+ }
+
+ for (; j < 40; j += 5) {
+ f = b ^ c ^ d;
+ t = (a << 5) | (a >>> 27);
+ e = (t + f + e + 1859775393 + blocks[j]) >>> 0;
+ b = (b << 30) | (b >>> 2);
+
+ f = a ^ b ^ c;
+ t = (e << 5) | (e >>> 27);
+ d = (t + f + d + 1859775393 + blocks[j + 1]) >>> 0;
+ a = (a << 30) | (a >>> 2);
+
+ f = e ^ a ^ b;
+ t = (d << 5) | (d >>> 27);
+ c = (t + f + c + 1859775393 + blocks[j + 2]) >>> 0;
+ e = (e << 30) | (e >>> 2);
+
+ f = d ^ e ^ a;
+ t = (c << 5) | (c >>> 27);
+ b = (t + f + b + 1859775393 + blocks[j + 3]) >>> 0;
+ d = (d << 30) | (d >>> 2);
+
+ f = c ^ d ^ e;
+ t = (b << 5) | (b >>> 27);
+ a = (t + f + a + 1859775393 + blocks[j + 4]) >>> 0;
+ c = (c << 30) | (c >>> 2);
+ }
+
+ for (; j < 60; j += 5) {
+ f = (b & c) | (b & d) | (c & d);
+ t = (a << 5) | (a >>> 27);
+ e = (t + f + e - 1894007588 + blocks[j]) >>> 0;
+ b = (b << 30) | (b >>> 2);
+
+ f = (a & b) | (a & c) | (b & c);
+ t = (e << 5) | (e >>> 27);
+ d = (t + f + d - 1894007588 + blocks[j + 1]) >>> 0;
+ a = (a << 30) | (a >>> 2);
+
+ f = (e & a) | (e & b) | (a & b);
+ t = (d << 5) | (d >>> 27);
+ c = (t + f + c - 1894007588 + blocks[j + 2]) >>> 0;
+ e = (e << 30) | (e >>> 2);
+
+ f = (d & e) | (d & a) | (e & a);
+ t = (c << 5) | (c >>> 27);
+ b = (t + f + b - 1894007588 + blocks[j + 3]) >>> 0;
+ d = (d << 30) | (d >>> 2);
+
+ f = (c & d) | (c & e) | (d & e);
+ t = (b << 5) | (b >>> 27);
+ a = (t + f + a - 1894007588 + blocks[j + 4]) >>> 0;
+ c = (c << 30) | (c >>> 2);
+ }
+
+ for (; j < 80; j += 5) {
+ f = b ^ c ^ d;
+ t = (a << 5) | (a >>> 27);
+ e = (t + f + e - 899497514 + blocks[j]) >>> 0;
+ b = (b << 30) | (b >>> 2);
+
+ f = a ^ b ^ c;
+ t = (e << 5) | (e >>> 27);
+ d = (t + f + d - 899497514 + blocks[j + 1]) >>> 0;
+ a = (a << 30) | (a >>> 2);
+
+ f = e ^ a ^ b;
+ t = (d << 5) | (d >>> 27);
+ c = (t + f + c - 899497514 + blocks[j + 2]) >>> 0;
+ e = (e << 30) | (e >>> 2);
+
+ f = d ^ e ^ a;
+ t = (c << 5) | (c >>> 27);
+ b = (t + f + b - 899497514 + blocks[j + 3]) >>> 0;
+ d = (d << 30) | (d >>> 2);
+
+ f = c ^ d ^ e;
+ t = (b << 5) | (b >>> 27);
+ a = (t + f + a - 899497514 + blocks[j + 4]) >>> 0;
+ c = (c << 30) | (c >>> 2);
+ }
+
+ this._h0 = (this._h0 + a) >>> 0;
+ this._h1 = (this._h1 + b) >>> 0;
+ this._h2 = (this._h2 + c) >>> 0;
+ this._h3 = (this._h3 + d) >>> 0;
+ this._h4 = (this._h4 + e) >>> 0;
+ }
+
+ hex(): string {
+ this.finalize();
+
+ const h0 = this._h0;
+ const h1 = this._h1;
+ const h2 = this._h2;
+ const h3 = this._h3;
+ const h4 = this._h4;
+
+ return (
+ HEX_CHARS[(h0 >> 28) & 0x0f] +
+ HEX_CHARS[(h0 >> 24) & 0x0f] +
+ HEX_CHARS[(h0 >> 20) & 0x0f] +
+ HEX_CHARS[(h0 >> 16) & 0x0f] +
+ HEX_CHARS[(h0 >> 12) & 0x0f] +
+ HEX_CHARS[(h0 >> 8) & 0x0f] +
+ HEX_CHARS[(h0 >> 4) & 0x0f] +
+ HEX_CHARS[h0 & 0x0f] +
+ HEX_CHARS[(h1 >> 28) & 0x0f] +
+ HEX_CHARS[(h1 >> 24) & 0x0f] +
+ HEX_CHARS[(h1 >> 20) & 0x0f] +
+ HEX_CHARS[(h1 >> 16) & 0x0f] +
+ HEX_CHARS[(h1 >> 12) & 0x0f] +
+ HEX_CHARS[(h1 >> 8) & 0x0f] +
+ HEX_CHARS[(h1 >> 4) & 0x0f] +
+ HEX_CHARS[h1 & 0x0f] +
+ HEX_CHARS[(h2 >> 28) & 0x0f] +
+ HEX_CHARS[(h2 >> 24) & 0x0f] +
+ HEX_CHARS[(h2 >> 20) & 0x0f] +
+ HEX_CHARS[(h2 >> 16) & 0x0f] +
+ HEX_CHARS[(h2 >> 12) & 0x0f] +
+ HEX_CHARS[(h2 >> 8) & 0x0f] +
+ HEX_CHARS[(h2 >> 4) & 0x0f] +
+ HEX_CHARS[h2 & 0x0f] +
+ HEX_CHARS[(h3 >> 28) & 0x0f] +
+ HEX_CHARS[(h3 >> 24) & 0x0f] +
+ HEX_CHARS[(h3 >> 20) & 0x0f] +
+ HEX_CHARS[(h3 >> 16) & 0x0f] +
+ HEX_CHARS[(h3 >> 12) & 0x0f] +
+ HEX_CHARS[(h3 >> 8) & 0x0f] +
+ HEX_CHARS[(h3 >> 4) & 0x0f] +
+ HEX_CHARS[h3 & 0x0f] +
+ HEX_CHARS[(h4 >> 28) & 0x0f] +
+ HEX_CHARS[(h4 >> 24) & 0x0f] +
+ HEX_CHARS[(h4 >> 20) & 0x0f] +
+ HEX_CHARS[(h4 >> 16) & 0x0f] +
+ HEX_CHARS[(h4 >> 12) & 0x0f] +
+ HEX_CHARS[(h4 >> 8) & 0x0f] +
+ HEX_CHARS[(h4 >> 4) & 0x0f] +
+ HEX_CHARS[h4 & 0x0f]
+ );
+ }
+
+ toString(): string {
+ return this.hex();
+ }
+
+ digest(): number[] {
+ this.finalize();
+
+ const h0 = this._h0;
+ const h1 = this._h1;
+ const h2 = this._h2;
+ const h3 = this._h3;
+ const h4 = this._h4;
+
+ return [
+ (h0 >> 24) & 0xff,
+ (h0 >> 16) & 0xff,
+ (h0 >> 8) & 0xff,
+ h0 & 0xff,
+ (h1 >> 24) & 0xff,
+ (h1 >> 16) & 0xff,
+ (h1 >> 8) & 0xff,
+ h1 & 0xff,
+ (h2 >> 24) & 0xff,
+ (h2 >> 16) & 0xff,
+ (h2 >> 8) & 0xff,
+ h2 & 0xff,
+ (h3 >> 24) & 0xff,
+ (h3 >> 16) & 0xff,
+ (h3 >> 8) & 0xff,
+ h3 & 0xff,
+ (h4 >> 24) & 0xff,
+ (h4 >> 16) & 0xff,
+ (h4 >> 8) & 0xff,
+ h4 & 0xff
+ ];
+ }
+
+ array(): number[] {
+ return this.digest();
+ }
+
+ arrayBuffer(): ArrayBuffer {
+ this.finalize();
+ return Uint32Array.of(this._h0, this._h1, this._h2, this._h3, this._h4)
+ .buffer;
+ }
+}
diff --git a/std/ws/sha1_test.ts b/std/ws/sha1_test.ts
new file mode 100644
index 000000000..8ece1a7e8
--- /dev/null
+++ b/std/ws/sha1_test.ts
@@ -0,0 +1,24 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { test } from "../testing/mod.ts";
+import { assertEquals } from "../testing/asserts.ts";
+import { Sha1 } from "./sha1.ts";
+
+test(function testSha1(): void {
+ const sha1 = new Sha1();
+ sha1.update("abcde");
+ assertEquals(sha1.toString(), "03de6c570bfe24bfc328ccd7ca46b76eadaf4334");
+});
+
+test(function testSha1WithArray(): void {
+ const data = Uint8Array.of(0x61, 0x62, 0x63, 0x64, 0x65);
+ const sha1 = new Sha1();
+ sha1.update(data);
+ assertEquals(sha1.toString(), "03de6c570bfe24bfc328ccd7ca46b76eadaf4334");
+});
+
+test(function testSha1WithBuffer(): void {
+ const data = Uint8Array.of(0x61, 0x62, 0x63, 0x64, 0x65);
+ const sha1 = new Sha1();
+ sha1.update(data.buffer);
+ assertEquals(sha1.toString(), "03de6c570bfe24bfc328ccd7ca46b76eadaf4334");
+});
diff --git a/std/ws/test.ts b/std/ws/test.ts
new file mode 100644
index 000000000..4351a391b
--- /dev/null
+++ b/std/ws/test.ts
@@ -0,0 +1,227 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { BufReader } from "../io/bufio.ts";
+import { assert, assertEquals, assertThrowsAsync } from "../testing/asserts.ts";
+import { runIfMain, test } from "../testing/mod.ts";
+import {
+ acceptable,
+ connectWebSocket,
+ createSecAccept,
+ OpCode,
+ readFrame,
+ unmask,
+ writeFrame
+} from "./mod.ts";
+import { encode } from "../strings/mod.ts";
+
+const { Buffer } = Deno;
+
+test(async function wsReadUnmaskedTextFrame(): Promise<void> {
+ // unmasked single text frame with payload "Hello"
+ const buf = new BufReader(
+ new Buffer(new Uint8Array([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]))
+ );
+ const frame = await readFrame(buf);
+ assertEquals(frame.opcode, OpCode.TextFrame);
+ assertEquals(frame.mask, undefined);
+ assertEquals(new Buffer(frame.payload).toString(), "Hello");
+ assertEquals(frame.isLastFrame, true);
+});
+
+test(async function wsReadMaskedTextFrame(): Promise<void> {
+ //a masked single text frame with payload "Hello"
+ const buf = new BufReader(
+ new Buffer(
+ new Uint8Array([
+ 0x81,
+ 0x85,
+ 0x37,
+ 0xfa,
+ 0x21,
+ 0x3d,
+ 0x7f,
+ 0x9f,
+ 0x4d,
+ 0x51,
+ 0x58
+ ])
+ )
+ );
+ const frame = await readFrame(buf);
+ assertEquals(frame.opcode, OpCode.TextFrame);
+ unmask(frame.payload, frame.mask);
+ assertEquals(new Buffer(frame.payload).toString(), "Hello");
+ assertEquals(frame.isLastFrame, true);
+});
+
+test(async function wsReadUnmaskedSplitTextFrames(): Promise<void> {
+ const buf1 = new BufReader(
+ new Buffer(new Uint8Array([0x01, 0x03, 0x48, 0x65, 0x6c]))
+ );
+ const buf2 = new BufReader(
+ new Buffer(new Uint8Array([0x80, 0x02, 0x6c, 0x6f]))
+ );
+ const [f1, f2] = await Promise.all([readFrame(buf1), readFrame(buf2)]);
+ assertEquals(f1.isLastFrame, false);
+ assertEquals(f1.mask, undefined);
+ assertEquals(f1.opcode, OpCode.TextFrame);
+ assertEquals(new Buffer(f1.payload).toString(), "Hel");
+
+ assertEquals(f2.isLastFrame, true);
+ assertEquals(f2.mask, undefined);
+ assertEquals(f2.opcode, OpCode.Continue);
+ assertEquals(new Buffer(f2.payload).toString(), "lo");
+});
+
+test(async function wsReadUnmaskedPingPongFrame(): Promise<void> {
+ // unmasked ping with payload "Hello"
+ const buf = new BufReader(
+ new Buffer(new Uint8Array([0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]))
+ );
+ const ping = await readFrame(buf);
+ assertEquals(ping.opcode, OpCode.Ping);
+ assertEquals(new Buffer(ping.payload).toString(), "Hello");
+
+ const buf2 = new BufReader(
+ new Buffer(
+ new Uint8Array([
+ 0x8a,
+ 0x85,
+ 0x37,
+ 0xfa,
+ 0x21,
+ 0x3d,
+ 0x7f,
+ 0x9f,
+ 0x4d,
+ 0x51,
+ 0x58
+ ])
+ )
+ );
+ const pong = await readFrame(buf2);
+ assertEquals(pong.opcode, OpCode.Pong);
+ assert(pong.mask !== undefined);
+ unmask(pong.payload, pong.mask);
+ assertEquals(new Buffer(pong.payload).toString(), "Hello");
+});
+
+test(async function wsReadUnmaskedBigBinaryFrame(): Promise<void> {
+ const payloadLength = 0x100;
+ const a = [0x82, 0x7e, 0x01, 0x00];
+ for (let i = 0; i < payloadLength; i++) {
+ a.push(i);
+ }
+ const buf = new BufReader(new Buffer(new Uint8Array(a)));
+ const bin = await readFrame(buf);
+ assertEquals(bin.opcode, OpCode.BinaryFrame);
+ assertEquals(bin.isLastFrame, true);
+ assertEquals(bin.mask, undefined);
+ assertEquals(bin.payload.length, payloadLength);
+});
+
+test(async function wsReadUnmaskedBigBigBinaryFrame(): Promise<void> {
+ const payloadLength = 0x10000;
+ const a = [0x82, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00];
+ for (let i = 0; i < payloadLength; i++) {
+ a.push(i);
+ }
+ const buf = new BufReader(new Buffer(new Uint8Array(a)));
+ const bin = await readFrame(buf);
+ assertEquals(bin.opcode, OpCode.BinaryFrame);
+ assertEquals(bin.isLastFrame, true);
+ assertEquals(bin.mask, undefined);
+ assertEquals(bin.payload.length, payloadLength);
+});
+
+test(async function wsCreateSecAccept(): Promise<void> {
+ const nonce = "dGhlIHNhbXBsZSBub25jZQ==";
+ const d = createSecAccept(nonce);
+ assertEquals(d, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
+});
+
+test(function wsAcceptable(): void {
+ const ret = acceptable({
+ headers: new Headers({
+ upgrade: "websocket",
+ "sec-websocket-key": "aaa"
+ })
+ });
+ assertEquals(ret, true);
+
+ assert(
+ acceptable({
+ headers: new Headers([
+ ["connection", "Upgrade"],
+ ["host", "127.0.0.1:9229"],
+ [
+ "sec-websocket-extensions",
+ "permessage-deflate; client_max_window_bits"
+ ],
+ ["sec-websocket-key", "dGhlIHNhbXBsZSBub25jZQ=="],
+ ["sec-websocket-version", "13"],
+ ["upgrade", "WebSocket"]
+ ])
+ })
+ );
+});
+
+test(function wsAcceptableInvalid(): void {
+ assertEquals(
+ acceptable({
+ headers: new Headers({ "sec-websocket-key": "aaa" })
+ }),
+ false
+ );
+ assertEquals(
+ acceptable({
+ headers: new Headers({ upgrade: "websocket" })
+ }),
+ false
+ );
+ assertEquals(
+ acceptable({
+ headers: new Headers({ upgrade: "invalid", "sec-websocket-key": "aaa" })
+ }),
+ false
+ );
+ assertEquals(
+ acceptable({
+ headers: new Headers({ upgrade: "websocket", "sec-websocket-ky": "" })
+ }),
+ false
+ );
+});
+
+test("connectWebSocket should throw invalid scheme of url", async (): Promise<
+ void
+> => {
+ await assertThrowsAsync(
+ async (): Promise<void> => {
+ await connectWebSocket("file://hoge/hoge");
+ }
+ );
+});
+
+test(async function wsWriteReadMaskedFrame(): Promise<void> {
+ const mask = new Uint8Array([0, 1, 2, 3]);
+ const msg = "hello";
+ const buf = new Buffer();
+ const r = new BufReader(buf);
+ await writeFrame(
+ {
+ isLastFrame: true,
+ mask,
+ opcode: OpCode.TextFrame,
+ payload: encode(msg)
+ },
+ buf
+ );
+ const frame = await readFrame(r);
+ assertEquals(frame.opcode, OpCode.TextFrame);
+ assertEquals(frame.isLastFrame, true);
+ assertEquals(frame.mask, mask);
+ unmask(frame.payload, frame.mask);
+ assertEquals(frame.payload, encode(msg));
+});
+
+runIfMain(import.meta);