summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYusuke Sakurai <kerokerokerop@gmail.com>2019-01-07 04:26:18 +0900
committerRyan Dahl <ry@tinyclouds.org>2019-01-06 14:26:18 -0500
commit7907bfc4c91f5287237d87571d1933db4ae7a4fa (patch)
tree6c14062ed9e08bb7543053b760dacd043acd874d
parentc164e696d7f924fe785421058d834934b7014429 (diff)
Add web socket module (denoland/deno_std#84)
Original: https://github.com/denoland/deno_std/commit/2606e295c77fb9d5796d527ed15f2dab3de1a696
-rw-r--r--examples/ws.ts39
-rw-r--r--net/ioutil.ts36
-rw-r--r--net/ioutil_test.ts62
-rw-r--r--net/sha1.ts382
-rw-r--r--net/sha1_test.ts8
-rw-r--r--net/ws.ts350
-rw-r--r--net/ws_test.ts138
7 files changed, 1015 insertions, 0 deletions
diff --git a/examples/ws.ts b/examples/ws.ts
new file mode 100644
index 000000000..f8e711c49
--- /dev/null
+++ b/examples/ws.ts
@@ -0,0 +1,39 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { serve } from "https://deno.land/x/net/http.ts";
+import {
+ acceptWebSocket,
+ isWebSocketCloseEvent,
+ isWebSocketPingEvent
+} from "https://deno.land/x/net/ws.ts";
+
+async function main() {
+ console.log("websocket server is running on 0.0.0.0:8080");
+ for await (const req of serve("0.0.0.0:8080")) {
+ if (req.url === "/ws") {
+ (async () => {
+ const sock = await acceptWebSocket(req);
+ console.log("socket connected!");
+ for await (const ev of sock.receive()) {
+ 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);
+ }
+ }
+ })();
+ }
+ }
+}
+
+main();
diff --git a/net/ioutil.ts b/net/ioutil.ts
new file mode 100644
index 000000000..68d6e5190
--- /dev/null
+++ b/net/ioutil.ts
@@ -0,0 +1,36 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { BufReader } from "./bufio.ts";
+
+/* Read big endian 16bit short from BufReader */
+export async function readShort(buf: BufReader): Promise<number> {
+ const [high, low] = [await buf.readByte(), await buf.readByte()];
+ return (high << 8) | low;
+}
+
+/* Read big endian 32bit integer from BufReader */
+export async function readInt(buf: BufReader): Promise<number> {
+ const [high, low] = [await readShort(buf), await readShort(buf)];
+ return (high << 16) | low;
+}
+
+const BIT32 = 0xffffffff;
+/* Read big endian 64bit long from BufReader */
+export async function readLong(buf: BufReader): Promise<number> {
+ const [high, low] = [await readInt(buf), await readInt(buf)];
+ // ECMAScript doesn't support 64bit bit ops.
+ return high ? high * (BIT32 + 1) + low : low;
+}
+
+/* Slice number into 64bit big endian byte array */
+export function sliceLongToBytes(d: number, dest = new Array(8)): number[] {
+ let mask = 0xff;
+ let low = (d << 32) >>> 32;
+ let high = (d - low) / (BIT32 + 1);
+ let shift = 24;
+ for (let i = 0; i < 4; i++) {
+ dest[i] = (high >>> shift) & mask;
+ dest[i + 4] = (low >>> shift) & mask;
+ shift -= 8;
+ }
+ return dest;
+}
diff --git a/net/ioutil_test.ts b/net/ioutil_test.ts
new file mode 100644
index 000000000..422901e4a
--- /dev/null
+++ b/net/ioutil_test.ts
@@ -0,0 +1,62 @@
+import { Reader, ReadResult } from "deno";
+import { assertEqual, test } from "../testing/mod.ts";
+import { readInt, readLong, readShort, sliceLongToBytes } from "./ioutil.ts";
+import { BufReader } from "./bufio.ts";
+
+class BinaryReader implements Reader {
+ index = 0;
+
+ constructor(private bytes: Uint8Array = new Uint8Array(0)) {}
+
+ async read(p: Uint8Array): Promise<ReadResult> {
+ p.set(this.bytes.subarray(this.index, p.byteLength));
+ this.index += p.byteLength;
+ return { nread: p.byteLength, eof: false };
+ }
+}
+
+test(async function testReadShort() {
+ const r = new BinaryReader(new Uint8Array([0x12, 0x34]));
+ const short = await readShort(new BufReader(r));
+ assertEqual(short, 0x1234);
+});
+
+test(async function testReadInt() {
+ const r = new BinaryReader(new Uint8Array([0x12, 0x34, 0x56, 0x78]));
+ const int = await readInt(new BufReader(r));
+ assertEqual(int, 0x12345678);
+});
+
+test(async function testReadLong() {
+ const r = new BinaryReader(
+ new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x12, 0x34, 0x56, 0x78])
+ );
+ const long = await readLong(new BufReader(r));
+ assertEqual(long, 0x1234567812345678);
+});
+
+test(async function testReadLong2() {
+ const r = new BinaryReader(
+ new Uint8Array([0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78])
+ );
+ const long = await readLong(new BufReader(r));
+ assertEqual(long, 0x12345678);
+});
+
+test(async function testSliceLongToBytes() {
+ const arr = sliceLongToBytes(0x1234567890abcdef);
+ const actual = readLong(new BufReader(new BinaryReader(new Uint8Array(arr))));
+ const expected = readLong(
+ new BufReader(
+ new BinaryReader(
+ new Uint8Array([0x12, 0x34, 0x56, 0x78, 0x90, 0xab, 0xcd, 0xef])
+ )
+ )
+ );
+ assertEqual(actual, expected);
+});
+
+test(async function testSliceLongToBytes2() {
+ const arr = sliceLongToBytes(0x12345678);
+ assertEqual(arr, [0, 0, 0, 0, 0x12, 0x34, 0x56, 0x78]);
+});
diff --git a/net/sha1.ts b/net/sha1.ts
new file mode 100644
index 000000000..036c3c552
--- /dev/null
+++ b/net/sha1.ts
@@ -0,0 +1,382 @@
+/*
+ * [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 = [-2147483648, 8388608, 32768, 128];
+const SHIFT = [24, 16, 8, 0];
+
+const blocks = [];
+
+export class Sha1 {
+ blocks;
+ block;
+ start;
+ bytes;
+ hBytes;
+ finalized;
+ hashed;
+ first;
+
+ h0 = 0x67452301;
+ h1 = 0xefcdab89;
+ h2 = 0x98badcfe;
+ h3 = 0x10325476;
+ h4 = 0xc3d2e1f0;
+ lastByteIndex = 0;
+
+ constructor(sharedMemory: boolean = false) {
+ if (sharedMemory) {
+ blocks[0] = blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ this.blocks = blocks;
+ } else {
+ this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
+ }
+
+ 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;
+ this.first = true;
+ }
+
+ update(data: string | ArrayBuffer) {
+ if (this.finalized) {
+ return;
+ }
+ let message;
+ let notString = typeof data !== "string";
+ if (notString && data instanceof ArrayBuffer) {
+ message = new Uint8Array(data);
+ } else {
+ message = data;
+ }
+ let code,
+ index = 0,
+ i,
+ length = message.length || 0,
+ blocks = this.blocks;
+
+ while (index < length) {
+ if (this.hashed) {
+ this.hashed = false;
+ blocks[0] = this.block;
+ blocks[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ }
+
+ if (notString) {
+ for (i = this.start; index < length && i < 64; ++index) {
+ blocks[i >> 2] |= message[index] << SHIFT[i++ & 3];
+ }
+ } else {
+ for (i = this.start; index < length && i < 64; ++index) {
+ code = message.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.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 - this.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 % 4294967296;
+ }
+ return this;
+ }
+
+ finalize() {
+ if (this.finalized) {
+ return;
+ }
+ this.finalized = true;
+ let blocks = this.blocks,
+ 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[16] = blocks[1] = blocks[2] = blocks[3] = blocks[4] = blocks[5] = blocks[6] = blocks[7] = blocks[8] = blocks[9] = blocks[10] = blocks[11] = blocks[12] = blocks[13] = blocks[14] = blocks[15] = 0;
+ }
+ blocks[14] = (this.hBytes << 3) | (this.bytes >>> 29);
+ blocks[15] = this.bytes << 3;
+ this.hash();
+ }
+
+ hash() {
+ let a = this.h0,
+ b = this.h1,
+ c = this.h2,
+ d = this.h3,
+ e = this.h4;
+ let f,
+ j,
+ t,
+ 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() {
+ this.finalize();
+
+ let h0 = this.h0,
+ h1 = this.h1,
+ h2 = this.h2,
+ h3 = this.h3,
+ 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() {
+ return this.hex();
+ }
+
+ digest() {
+ this.finalize();
+
+ let h0 = this.h0,
+ h1 = this.h1,
+ h2 = this.h2,
+ h3 = this.h3,
+ 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() {
+ return this.digest();
+ }
+
+ arrayBuffer() {
+ this.finalize();
+
+ let buffer = new ArrayBuffer(20);
+ let dataView = new DataView(buffer);
+ dataView.setUint32(0, this.h0);
+ dataView.setUint32(4, this.h1);
+ dataView.setUint32(8, this.h2);
+ dataView.setUint32(12, this.h3);
+ dataView.setUint32(16, this.h4);
+ return buffer;
+ }
+}
diff --git a/net/sha1_test.ts b/net/sha1_test.ts
new file mode 100644
index 000000000..1d3673c43
--- /dev/null
+++ b/net/sha1_test.ts
@@ -0,0 +1,8 @@
+import {assertEqual, test} from "../testing/mod.ts";
+import {Sha1} from "./sha1.ts";
+
+test(function testSha1() {
+ const sha1 = new Sha1();
+ sha1.update("abcde");
+ assertEqual(sha1.toString(), "03de6c570bfe24bfc328ccd7ca46b76eadaf4334")
+});
diff --git a/net/ws.ts b/net/ws.ts
new file mode 100644
index 000000000..5ce96b3ca
--- /dev/null
+++ b/net/ws.ts
@@ -0,0 +1,350 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { Buffer, Writer, Conn } from "deno";
+import { ServerRequest } from "./http.ts";
+import { BufReader, BufWriter } from "./bufio.ts";
+import { readLong, readShort, sliceLongToBytes } from "./ioutil.ts";
+import { Sha1 } from "./sha1.ts";
+
+export const OpCodeContinue = 0x0;
+export const OpCodeTextFrame = 0x1;
+export const OpCodeBinaryFrame = 0x2;
+export const OpCodeClose = 0x8;
+export const OpcodePing = 0x9;
+export const OpcodePong = 0xa;
+
+export type WebSocketEvent =
+ | string
+ | Uint8Array
+ | WebSocketCloseEvent
+ | WebSocketPingEvent
+ | WebSocketPongEvent;
+
+export type WebSocketCloseEvent = {
+ code: number;
+ reason?: string;
+};
+
+export function isWebSocketCloseEvent(a): a is WebSocketCloseEvent {
+ return a && typeof a["code"] === "number";
+}
+
+export type WebSocketPingEvent = ["ping", Uint8Array];
+
+export function isWebSocketPingEvent(a): a is WebSocketPingEvent {
+ return Array.isArray(a) && a[0] === "ping" && a[1] instanceof Uint8Array;
+}
+
+export type WebSocketPongEvent = ["pong", Uint8Array];
+
+export function isWebSocketPongEvent(a): a is WebSocketPongEvent {
+ return Array.isArray(a) && a[0] === "pong" && a[1] instanceof Uint8Array;
+}
+
+export class SocketClosedError extends Error {}
+
+export type WebSocketFrame = {
+ isLastFrame: boolean;
+ opcode: number;
+ mask?: Uint8Array;
+ payload: Uint8Array;
+};
+
+export type WebSocket = {
+ readonly isClosed: boolean;
+ receive(): AsyncIterableIterator<WebSocketEvent>;
+ send(data: string | Uint8Array): Promise<void>;
+ ping(data?: string | Uint8Array): Promise<void>;
+ close(code: number, reason?: string): Promise<void>;
+};
+
+class WebSocketImpl implements WebSocket {
+ encoder = new TextEncoder();
+ constructor(private conn: Conn, private mask?: Uint8Array) {}
+
+ async *receive(): AsyncIterableIterator<WebSocketEvent> {
+ let frames: WebSocketFrame[] = [];
+ let payloadsLength = 0;
+ for await (const frame of receiveFrame(this.conn)) {
+ unmask(frame.payload, frame.mask);
+ switch (frame.opcode) {
+ case OpCodeTextFrame:
+ case OpCodeBinaryFrame:
+ case OpCodeContinue:
+ 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 === OpCodeTextFrame) {
+ // text
+ yield new Buffer(concat).toString();
+ } else {
+ // binary
+ yield concat;
+ }
+ frames = [];
+ payloadsLength = 0;
+ }
+ break;
+ case OpCodeClose:
+ const code = (frame.payload[0] << 16) | frame.payload[1];
+ const reason = new Buffer(
+ frame.payload.subarray(2, frame.payload.length)
+ ).toString();
+ this._isClosed = true;
+ yield { code, reason };
+ return;
+ case OpcodePing:
+ yield ["ping", frame.payload] as WebSocketPingEvent;
+ break;
+ case OpcodePong:
+ yield ["pong", frame.payload] as WebSocketPongEvent;
+ break;
+ }
+ }
+ }
+
+ async send(data: string | Uint8Array): Promise<void> {
+ if (this.isClosed) {
+ throw new SocketClosedError("socket has been closed");
+ }
+ const opcode =
+ typeof data === "string" ? OpCodeTextFrame : OpCodeBinaryFrame;
+ const payload = typeof data === "string" ? this.encoder.encode(data) : data;
+ const isLastFrame = true;
+ await writeFrame(
+ {
+ isLastFrame,
+ opcode,
+ payload,
+ mask: this.mask
+ },
+ this.conn
+ );
+ }
+
+ async ping(data: string | Uint8Array): Promise<void> {
+ const payload = typeof data === "string" ? this.encoder.encode(data) : data;
+ await writeFrame(
+ {
+ isLastFrame: true,
+ opcode: OpCodeClose,
+ mask: this.mask,
+ payload
+ },
+ this.conn
+ );
+ }
+
+ private _isClosed = false;
+ get isClosed() {
+ 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 = this.encoder.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: OpCodeClose,
+ mask: this.mask,
+ payload
+ },
+ this.conn
+ );
+ } catch (e) {
+ throw e;
+ } finally {
+ this.ensureSocketClosed();
+ }
+ }
+
+ private ensureSocketClosed(): Error {
+ if (this.isClosed) return;
+ try {
+ this.conn.close();
+ } catch (e) {
+ console.error(e);
+ } finally {
+ this._isClosed = true;
+ }
+ }
+}
+
+export async function* receiveFrame(
+ conn: Conn
+): AsyncIterableIterator<WebSocketFrame> {
+ let receiving = true;
+ const reader = new BufReader(conn);
+ while (receiving) {
+ const frame = await readFrame(reader);
+ switch (frame.opcode) {
+ case OpCodeTextFrame:
+ case OpCodeBinaryFrame:
+ case OpCodeContinue:
+ yield frame;
+ break;
+ case OpCodeClose:
+ await writeFrame(
+ {
+ isLastFrame: true,
+ opcode: OpCodeClose,
+ payload: frame.payload
+ },
+ conn
+ );
+ conn.close();
+ yield frame;
+ receiving = false;
+ break;
+ case OpcodePing:
+ await writeFrame(
+ {
+ isLastFrame: true,
+ opcode: OpcodePong,
+ payload: frame.payload
+ },
+ conn
+ );
+ yield frame;
+ break;
+ case OpcodePong:
+ yield frame;
+ break;
+ }
+ }
+}
+
+export async function writeFrame(frame: WebSocketFrame, writer: Writer) {
+ let payloadLength = frame.payload.byteLength;
+ let header: Uint8Array;
+ const hasMask = (frame.mask ? 1 : 0) << 7;
+ if (payloadLength < 126) {
+ header = new Uint8Array([
+ (0b1000 << 4) | frame.opcode,
+ hasMask | payloadLength
+ ]);
+ } else if (payloadLength < 0xffff) {
+ header = new Uint8Array([
+ (0b1000 << 4) | frame.opcode,
+ hasMask | 0b01111110,
+ payloadLength >>> 8,
+ payloadLength & 0x00ff
+ ]);
+ } else {
+ header = new Uint8Array([
+ (0b1000 << 4) | frame.opcode,
+ hasMask | 0b01111111,
+ ...sliceLongToBytes(payloadLength)
+ ]);
+ }
+ if (frame.mask) {
+ unmask(frame.payload, frame.mask);
+ }
+ const bytes = new Uint8Array(header.length + payloadLength);
+ bytes.set(header, 0);
+ bytes.set(frame.payload, header.length);
+ const w = new BufWriter(writer);
+ await w.write(bytes);
+ await w.flush();
+}
+
+export function unmask(payload: Uint8Array, mask: Uint8Array) {
+ if (mask) {
+ for (let i = 0; i < payload.length; i++) {
+ payload[i] ^= mask[i % 4];
+ }
+ }
+}
+
+export function acceptable(req: ServerRequest): boolean {
+ return (
+ req.headers.get("upgrade") === "websocket" &&
+ req.headers.has("sec-websocket-key")
+ );
+}
+
+export async function acceptWebSocket(req: ServerRequest): Promise<WebSocket> {
+ if (acceptable(req)) {
+ const sock = new WebSocketImpl(req.conn);
+ const secKey = req.headers.get("sec-websocket-key");
+ const secAccept = createSecAccept(secKey);
+ await req.respond({
+ status: 101,
+ headers: new Headers({
+ Upgrade: "websocket",
+ Connection: "Upgrade",
+ "Sec-WebSocket-Accept": secAccept
+ })
+ });
+ return sock;
+ }
+ throw new Error("request is not acceptable");
+}
+
+const kGUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
+
+export function createSecAccept(nonce: string) {
+ const sha1 = new Sha1();
+ sha1.update(nonce + kGUID);
+ const bytes = sha1.digest();
+ const hash = bytes.reduce(
+ (data, byte) => data + String.fromCharCode(byte),
+ ""
+ );
+ return btoa(hash);
+}
+
+export async function readFrame(buf: BufReader): Promise<WebSocketFrame> {
+ let b = await buf.readByte();
+ 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();
+ const hasMask = b >>> 7;
+ let payloadLength = b & 0b01111111;
+ if (payloadLength === 126) {
+ payloadLength = await readShort(buf);
+ } else if (payloadLength === 127) {
+ payloadLength = await readLong(buf);
+ }
+ // 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
+ };
+}
diff --git a/net/ws_test.ts b/net/ws_test.ts
new file mode 100644
index 000000000..62e5a6089
--- /dev/null
+++ b/net/ws_test.ts
@@ -0,0 +1,138 @@
+// Copyright 2018-2019 the Deno authors. All rights reserved. MIT license.
+import { Buffer } from "deno";
+import { BufReader } from "./bufio.ts";
+import { test, assert, assertEqual } from "../testing/mod.ts";
+import {
+ createSecAccept,
+ OpCodeBinaryFrame,
+ OpCodeContinue,
+ OpcodePing,
+ OpcodePong,
+ OpCodeTextFrame,
+ readFrame,
+ unmask
+} from "./ws.ts";
+import { serve } from "./http.ts";
+
+test(async function testReadUnmaskedTextFrame() {
+ // 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);
+ assertEqual(frame.opcode, OpCodeTextFrame);
+ assertEqual(frame.mask, undefined);
+ assertEqual(new Buffer(frame.payload).toString(), "Hello");
+ assertEqual(frame.isLastFrame, true);
+});
+
+test(async function testReadMakedTextFrame() {
+ //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);
+ console.dir(frame);
+ assertEqual(frame.opcode, OpCodeTextFrame);
+ unmask(frame.payload, frame.mask);
+ assertEqual(new Buffer(frame.payload).toString(), "Hello");
+ assertEqual(frame.isLastFrame, true);
+});
+
+test(async function testReadUnmaskedSplittedTextFrames() {
+ 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)]);
+ assertEqual(f1.isLastFrame, false);
+ assertEqual(f1.mask, undefined);
+ assertEqual(f1.opcode, OpCodeTextFrame);
+ assertEqual(new Buffer(f1.payload).toString(), "Hel");
+
+ assertEqual(f2.isLastFrame, true);
+ assertEqual(f2.mask, undefined);
+ assertEqual(f2.opcode, OpCodeContinue);
+ assertEqual(new Buffer(f2.payload).toString(), "lo");
+});
+
+test(async function testReadUnmaksedPingPongFrame() {
+ // 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);
+ assertEqual(ping.opcode, OpcodePing);
+ assertEqual(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);
+ assertEqual(pong.opcode, OpcodePong);
+ assert(pong.mask !== undefined);
+ unmask(pong.payload, pong.mask);
+ assertEqual(new Buffer(pong.payload).toString(), "Hello");
+});
+
+test(async function testReadUnmaksedBigBinaryFrame() {
+ let a = [0x82, 0x7e, 0x01, 0x00];
+ for (let i = 0; i < 256; i++) {
+ a.push(i);
+ }
+ const buf = new BufReader(new Buffer(new Uint8Array(a)));
+ const bin = await readFrame(buf);
+ assertEqual(bin.opcode, OpCodeBinaryFrame);
+ assertEqual(bin.isLastFrame, true);
+ assertEqual(bin.mask, undefined);
+ assertEqual(bin.payload.length, 256);
+});
+
+test(async function testReadUnmaskedBigBigBinaryFrame() {
+ let a = [0x82, 0x7f, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00];
+ for (let i = 0; i < 0xffff; i++) {
+ a.push(i);
+ }
+ const buf = new BufReader(new Buffer(new Uint8Array(a)));
+ const bin = await readFrame(buf);
+ assertEqual(bin.opcode, OpCodeBinaryFrame);
+ assertEqual(bin.isLastFrame, true);
+ assertEqual(bin.mask, undefined);
+ assertEqual(bin.payload.length, 0xffff + 1);
+});
+
+test(async function testCreateSecAccept() {
+ const nonce = "dGhlIHNhbXBsZSBub25jZQ==";
+ const d = createSecAccept(nonce);
+ assertEqual(d, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo=");
+});