From 6abf126c2a7a451cded8c6b5e6ddf1b69c84055d Mon Sep 17 00:00:00 2001 From: Casper Beyer Date: Tue, 2 Feb 2021 19:05:46 +0800 Subject: chore: remove std directory (#9361) This removes the std folder from the tree. Various parts of the tests are pretty tightly dependent on std (47 direct imports and 75 indirect imports, not counting the cli tests that use them as fixtures) so I've added std as a submodule for now. --- std/ws/README.md | 114 ---------- std/ws/example_server.ts | 58 ------ std/ws/example_test.ts | 2 - std/ws/mod.ts | 531 ----------------------------------------------- std/ws/test.ts | 495 ------------------------------------------- 5 files changed, 1200 deletions(-) delete mode 100644 std/ws/README.md delete mode 100644 std/ws/example_server.ts delete mode 100644 std/ws/example_test.ts delete mode 100644 std/ws/mod.ts delete mode 100644 std/ws/test.ts (limited to 'std/ws') diff --git a/std/ws/README.md b/std/ws/README.md deleted file mode 100644 index 658f2e038..000000000 --- a/std/ws/README.md +++ /dev/null @@ -1,114 +0,0 @@ -# ws - -ws module is made to provide helpers to create WebSocket server. For client -WebSockets, use the -[WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API). - -## Usage - -```ts -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { serve } from "https://deno.land/std@$STD_VERSION/http/server.ts"; -import { - acceptWebSocket, - isWebSocketCloseEvent, - isWebSocketPingEvent, - WebSocket, -} from "https://deno.land/std@$STD_VERSION/ws/mod.ts"; - -async function handleWs(sock: WebSocket) { - console.log("socket connected!"); - try { - for await (const ev of sock) { - 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 (err) { - console.error(`failed to receive frame: ${err}`); - - if (!sock.isClosed) { - await sock.close(1000).catch(console.error); - } - } -} - -if (import.meta.main) { - /** websocket echo server */ - const port = Deno.args[0] || "8080"; - console.log(`websocket server is running on :${port}`); - for await (const req of serve(`:${port}`)) { - const { conn, r: bufReader, w: bufWriter, headers } = req; - acceptWebSocket({ - conn, - bufReader, - bufWriter, - headers, - }) - .then(handleWs) - .catch(async (err) => { - console.error(`failed to accept websocket: ${err}`); - await req.respond({ status: 400 }); - }); - } -} -``` - -## 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. - -### 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. diff --git a/std/ws/example_server.ts b/std/ws/example_server.ts deleted file mode 100644 index 0e99bd03a..000000000 --- a/std/ws/example_server.ts +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { serve } from "../http/server.ts"; -import { - acceptWebSocket, - isWebSocketCloseEvent, - isWebSocketPingEvent, - WebSocket, -} from "./mod.ts"; - -async function handleWs(sock: WebSocket): Promise { - console.log("socket connected!"); - try { - for await (const ev of sock) { - 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 (err) { - console.error(`failed to receive frame: ${err}`); - - if (!sock.isClosed) { - await sock.close(1000).catch(console.error); - } - } -} - -if (import.meta.main) { - /** websocket echo server */ - const port = Deno.args[0] || "8080"; - console.log(`websocket server is running on :${port}`); - for await (const req of serve(`:${port}`)) { - const { conn, r: bufReader, w: bufWriter, headers } = req; - acceptWebSocket({ - conn, - bufReader, - bufWriter, - headers, - }) - .then(handleWs) - .catch(async (e) => { - console.error(`failed to accept websocket: ${e}`); - await req.respond({ status: 400 }); - }); - } -} diff --git a/std/ws/example_test.ts b/std/ws/example_test.ts deleted file mode 100644 index b94121607..000000000 --- a/std/ws/example_test.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import "./example_server.ts"; diff --git a/std/ws/mod.ts b/std/ws/mod.ts deleted file mode 100644 index 0d2141a75..000000000 --- a/std/ws/mod.ts +++ /dev/null @@ -1,531 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { decode, encode } from "../encoding/utf8.ts"; -import { hasOwnProperty } from "../_util/has_own_property.ts"; -import { BufReader, BufWriter } from "../io/bufio.ts"; -import { readLong, readShort, sliceLongToBytes } from "../io/ioutil.ts"; -import { Sha1 } from "../hash/sha1.ts"; -import { writeResponse } from "../http/_io.ts"; -import { TextProtoReader } from "../textproto/mod.ts"; -import { Deferred, deferred } from "../async/deferred.ts"; -import { assert } from "../_util/assert.ts"; -import { concat } from "../bytes/mod.ts"; - -export enum OpCode { - Continue = 0x0, - TextFrame = 0x1, - BinaryFrame = 0x2, - Close = 0x8, - Ping = 0x9, - Pong = 0xa, -} - -export type WebSocketEvent = - | string - | Uint8Array - | WebSocketCloseEvent // Received after closing connection finished. - | WebSocketPingEvent // Received after pong frame responded. - | WebSocketPongEvent; - -export interface WebSocketCloseEvent { - code: number; - reason?: string; -} - -/** Returns true if input value is a WebSocketCloseEvent, false otherwise. */ -export function isWebSocketCloseEvent( - a: WebSocketEvent, -): a is WebSocketCloseEvent { - return hasOwnProperty(a, "code"); -} - -export type WebSocketPingEvent = ["ping", Uint8Array]; - -/** Returns true if input value is a WebSocketPingEvent, false otherwise. */ -export function isWebSocketPingEvent( - a: WebSocketEvent, -): a is WebSocketPingEvent { - return Array.isArray(a) && a[0] === "ping" && a[1] instanceof Uint8Array; -} - -export type WebSocketPongEvent = ["pong", Uint8Array]; - -/** Returns true if input value is a WebSocketPongEvent, false otherwise. */ -export function isWebSocketPongEvent( - a: WebSocketEvent, -): a is WebSocketPongEvent { - return Array.isArray(a) && a[0] === "pong" && a[1] instanceof Uint8Array; -} - -export type WebSocketMessage = string | Uint8Array; - -export interface WebSocketFrame { - isLastFrame: boolean; - opcode: OpCode; - mask?: Uint8Array; - payload: Uint8Array; -} - -export interface WebSocket extends AsyncIterable { - readonly conn: Deno.Conn; - readonly isClosed: boolean; - - [Symbol.asyncIterator](): AsyncIterableIterator; - - /** - * @throws `Deno.errors.ConnectionReset` - */ - send(data: WebSocketMessage): Promise; - - /** - * @param data - * @throws `Deno.errors.ConnectionReset` - */ - ping(data?: WebSocketMessage): Promise; - - /** Close connection after sending close frame to peer. - * This is canonical way of disconnection but it may hang because of peer's response delay. - * Default close code is 1000 (Normal Closure) - * @throws `Deno.errors.ConnectionReset` - */ - close(): Promise; - close(code: number): Promise; - close(code: number, reason: string): Promise; - - /** Close connection forcely without sending close frame to peer. - * This is basically undesirable way of disconnection. Use carefully. */ - closeForce(): 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 inputted writer. */ -export async function writeFrame( - frame: WebSocketFrame, - writer: Deno.Writer, -): Promise { - 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 = concat(header, frame.mask); - } - unmask(frame.payload, frame.mask); - header = concat(header, frame.payload); - const w = BufWriter.create(writer); - await w.write(header); - await w.flush(); -} - -/** Read websocket frame from given BufReader - * @throws `Deno.errors.UnexpectedEof` When peer closed connection without close frame - * @throws `Error` Frame is invalid - */ -export async function readFrame(buf: BufReader): Promise { - let b = await buf.readByte(); - assert(b !== null); - 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(); - assert(b !== null); - const hasMask = b >>> 7; - let payloadLength = b & 0b01111111; - if (payloadLength === 126) { - const l = await readShort(buf); - assert(l !== null); - payloadLength = l; - } else if (payloadLength === 127) { - const l = await readLong(buf); - assert(l !== null); - payloadLength = Number(l); - } - // mask - let mask: Uint8Array | undefined; - if (hasMask) { - mask = new Uint8Array(4); - assert((await buf.readFull(mask)) !== null); - } - // payload - const payload = new Uint8Array(payloadLength); - assert((await buf.readFull(payload)) !== null); - return { - isLastFrame, - opcode, - mask, - payload, - }; -} - -class WebSocketImpl implements WebSocket { - readonly conn: Deno.Conn; - private readonly mask?: Uint8Array; - private readonly bufReader: BufReader; - private readonly bufWriter: BufWriter; - private sendQueue: Array<{ - frame: WebSocketFrame; - d: Deferred; - }> = []; - - constructor({ - conn, - bufReader, - bufWriter, - mask, - }: { - conn: Deno.Conn; - bufReader?: BufReader; - bufWriter?: BufWriter; - mask?: Uint8Array; - }) { - this.conn = conn; - this.mask = mask; - this.bufReader = bufReader || new BufReader(conn); - this.bufWriter = bufWriter || new BufWriter(conn); - } - - async *[Symbol.asyncIterator](): AsyncIterableIterator { - let frames: WebSocketFrame[] = []; - let payloadsLength = 0; - while (!this._isClosed) { - let frame: WebSocketFrame; - try { - frame = await readFrame(this.bufReader); - } catch (e) { - this.ensureSocketClosed(); - break; - } - 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 this.enqueue({ - opcode: OpCode.Pong, - payload: frame.payload, - isLastFrame: true, - }); - yield ["ping", frame.payload] as WebSocketPingEvent; - break; - case OpCode.Pong: - yield ["pong", frame.payload] as WebSocketPongEvent; - break; - default: - } - } - } - - private dequeue(): void { - const [entry] = this.sendQueue; - if (!entry) return; - if (this._isClosed) return; - const { d, frame } = entry; - writeFrame(frame, this.bufWriter) - .then(() => d.resolve()) - .catch((e) => d.reject(e)) - .finally(() => { - this.sendQueue.shift(); - this.dequeue(); - }); - } - - private enqueue(frame: WebSocketFrame): Promise { - if (this._isClosed) { - throw new Deno.errors.ConnectionReset("Socket has already been closed"); - } - const d = deferred(); - this.sendQueue.push({ d, frame }); - if (this.sendQueue.length === 1) { - this.dequeue(); - } - return d; - } - - send(data: WebSocketMessage): Promise { - const opcode = typeof data === "string" - ? OpCode.TextFrame - : OpCode.BinaryFrame; - const payload = typeof data === "string" ? encode(data) : data; - const isLastFrame = true; - const frame = { - isLastFrame, - opcode, - payload, - mask: this.mask, - }; - return this.enqueue(frame); - } - - ping(data: WebSocketMessage = ""): Promise { - const payload = typeof data === "string" ? encode(data) : data; - const frame = { - isLastFrame: true, - opcode: OpCode.Ping, - mask: this.mask, - payload, - }; - return this.enqueue(frame); - } - - private _isClosed = false; - get isClosed(): boolean { - return this._isClosed; - } - - async close(code = 1000, reason?: string): Promise { - 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 this.enqueue({ - isLastFrame: true, - opcode: OpCode.Close, - mask: this.mask, - payload, - }); - } catch (e) { - throw e; - } finally { - this.ensureSocketClosed(); - } - } - - closeForce(): void { - this.ensureSocketClosed(); - } - - private ensureSocketClosed(): void { - if (this.isClosed) return; - try { - this.conn.close(); - } catch (e) { - console.error(e); - } finally { - this._isClosed = true; - const rest = this.sendQueue; - this.sendQueue = []; - rest.forEach((e) => - e.d.reject( - new Deno.errors.ConnectionReset("Socket has already been closed"), - ) - ); - } - } -} - -/** Returns true if input headers are usable for WebSocket, otherwise false. */ -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 value of Sec-WebSocket-Accept header from inputted 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 inputted TCP connection into WebSocket connection. */ -export async function acceptWebSocket(req: { - conn: Deno.Conn; - bufWriter: BufWriter; - bufReader: BufReader; - headers: Headers; -}): Promise { - 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); - const newHeaders = new Headers({ - Upgrade: "websocket", - Connection: "Upgrade", - "Sec-WebSocket-Accept": secAccept, - }); - const secProtocol = headers.get("sec-websocket-protocol"); - if (typeof secProtocol === "string") { - newHeaders.set("Sec-WebSocket-Protocol", secProtocol); - } - const secVersion = headers.get("sec-websocket-version"); - if (typeof secVersion === "string") { - newHeaders.set("Sec-WebSocket-Version", secVersion); - } - await writeResponse(bufWriter, { - status: 101, - headers: newHeaders, - }); - return sock; - } - throw new Error("request is not acceptable"); -} - -const kSecChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-.~_"; - -/** Returns base64 encoded 16 bytes string for Sec-WebSocket-Key header. */ -export function createSecKey(): string { - let key = ""; - for (let i = 0; i < 16; i++) { - const j = Math.floor(Math.random() * kSecChars.length); - key += kSecChars[j]; - } - return btoa(key); -} - -export async function handshake( - url: URL, - headers: Headers, - bufReader: BufReader, - bufWriter: BufWriter, -): Promise { - const { hostname, pathname, search } = 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}${search} 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 === null) { - throw new Deno.errors.UnexpectedEof(); - } - const m = statusLine.match(/^(?\S+) (?\S+) /); - if (!m) { - throw new Error("ws: invalid status line: " + statusLine); - } - - assert(m.groups); - 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 === null) { - throw new Deno.errors.UnexpectedEof(); - } - - 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}`, - ); - } -} - -export function createWebSocket(params: { - conn: Deno.Conn; - bufWriter?: BufWriter; - bufReader?: BufReader; - mask?: Uint8Array; -}): WebSocket { - return new WebSocketImpl(params); -} diff --git a/std/ws/test.ts b/std/ws/test.ts deleted file mode 100644 index c437d8c28..000000000 --- a/std/ws/test.ts +++ /dev/null @@ -1,495 +0,0 @@ -// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. -import { BufReader, BufWriter } from "../io/bufio.ts"; -import { - assert, - assertEquals, - assertThrowsAsync, - fail, -} from "../testing/asserts.ts"; -import { TextProtoReader } from "../textproto/mod.ts"; -import * as bytes from "../bytes/mod.ts"; -import { - acceptable, - acceptWebSocket, - createSecAccept, - createSecKey, - createWebSocket, - handshake, - OpCode, - readFrame, - unmask, - writeFrame, -} from "./mod.ts"; -import { decode, encode } from "../encoding/utf8.ts"; -import { delay } from "../async/delay.ts"; -import { serve } from "../http/server.ts"; -import { deferred } from "../async/deferred.ts"; - -Deno.test("[ws] read unmasked text frame", async () => { - // unmasked single text frame with payload "Hello" - const buf = new BufReader( - new Deno.Buffer(new Uint8Array([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])), - ); - const frame = await readFrame(buf); - assertEquals(frame.opcode, OpCode.TextFrame); - assertEquals(frame.mask, undefined); - const actual = new TextDecoder().decode( - new Deno.Buffer(frame.payload).bytes(), - ); - assertEquals(actual, "Hello"); - assertEquals(frame.isLastFrame, true); -}); - -Deno.test("[ws] read masked text frame", async () => { - // a masked single text frame with payload "Hello" - const buf = new BufReader( - new Deno.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); - const actual = new TextDecoder().decode( - new Deno.Buffer(frame.payload).bytes(), - ); - assertEquals(actual, "Hello"); - assertEquals(frame.isLastFrame, true); -}); - -Deno.test("[ws] read unmasked split text frames", async () => { - const buf1 = new BufReader( - new Deno.Buffer(new Uint8Array([0x01, 0x03, 0x48, 0x65, 0x6c])), - ); - const buf2 = new BufReader( - new Deno.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); - const actual1 = new TextDecoder().decode(new Deno.Buffer(f1.payload).bytes()); - assertEquals(actual1, "Hel"); - - assertEquals(f2.isLastFrame, true); - assertEquals(f2.mask, undefined); - assertEquals(f2.opcode, OpCode.Continue); - const actual2 = new TextDecoder().decode(new Deno.Buffer(f2.payload).bytes()); - assertEquals(actual2, "lo"); -}); - -Deno.test("[ws] read unmasked ping / pong frame", async () => { - // unmasked ping with payload "Hello" - const buf = new BufReader( - new Deno.Buffer(new Uint8Array([0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f])), - ); - const ping = await readFrame(buf); - assertEquals(ping.opcode, OpCode.Ping); - const actual1 = new TextDecoder().decode( - new Deno.Buffer(ping.payload).bytes(), - ); - assertEquals(actual1, "Hello"); - // deno-fmt-ignore - const pongFrame = [0x8a, 0x85, 0x37, 0xfa, 0x21, 0x3d, 0x7f, 0x9f, 0x4d, 0x51, 0x58] - const buf2 = new BufReader(new Deno.Buffer(new Uint8Array(pongFrame))); - const pong = await readFrame(buf2); - assertEquals(pong.opcode, OpCode.Pong); - assert(pong.mask !== undefined); - unmask(pong.payload, pong.mask); - const actual2 = new TextDecoder().decode( - new Deno.Buffer(pong.payload).bytes(), - ); - assertEquals(actual2, "Hello"); -}); - -Deno.test("[ws] read unmasked big binary frame", async () => { - const payloadLength = 0x100; - const a = [0x82, 0x7e, 0x01, 0x00]; - for (let i = 0; i < payloadLength; i++) { - a.push(i); - } - const buf = new BufReader(new Deno.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); -}); - -Deno.test("[ws] read unmasked bigger binary frame", async () => { - 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 Deno.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); -}); - -Deno.test("[ws] createSecAccept", () => { - const nonce = "dGhlIHNhbXBsZSBub25jZQ=="; - const d = createSecAccept(nonce); - assertEquals(d, "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="); -}); - -Deno.test("[ws] acceptable", () => { - 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"], - ]), - }), - ); -}); - -Deno.test("[ws] acceptable should return false when headers invalid", () => { - 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, - ); -}); - -Deno.test("[ws] write and read masked frame", async () => { - const mask = new Uint8Array([0, 1, 2, 3]); - const msg = "hello"; - const buf = new Deno.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)); -}); - -Deno.test("[ws] handshake should not send search when it's empty", async () => { - const writer = new Deno.Buffer(); - const reader = new Deno.Buffer(encode("HTTP/1.1 400\r\n")); - - await assertThrowsAsync( - async (): Promise => { - await handshake( - new URL("ws://example.com"), - new Headers(), - new BufReader(reader), - new BufWriter(writer), - ); - }, - ); - - const tpReader = new TextProtoReader(new BufReader(writer)); - const statusLine = await tpReader.readLine(); - - assertEquals(statusLine, "GET / HTTP/1.1"); -}); - -Deno.test( - "[ws] handshake should send search correctly", - async function wsHandshakeWithSearch(): Promise { - const writer = new Deno.Buffer(); - const reader = new Deno.Buffer(encode("HTTP/1.1 400\r\n")); - - await assertThrowsAsync( - async (): Promise => { - await handshake( - new URL("ws://example.com?a=1"), - new Headers(), - new BufReader(reader), - new BufWriter(writer), - ); - }, - ); - - const tpReader = new TextProtoReader(new BufReader(writer)); - const statusLine = await tpReader.readLine(); - - assertEquals(statusLine, "GET /?a=1 HTTP/1.1"); - }, -); - -Deno.test("[ws] ws.close() should use 1000 as close code", async () => { - const buf = new Deno.Buffer(); - const bufr = new BufReader(buf); - const conn = dummyConn(buf, buf); - const ws = createWebSocket({ conn }); - await ws.close(); - const frame = await readFrame(bufr); - assertEquals(frame.opcode, OpCode.Close); - const code = (frame.payload[0] << 8) | frame.payload[1]; - assertEquals(code, 1000); -}); - -function dummyConn(r: Deno.Reader, w: Deno.Writer): Deno.Conn { - return { - rid: -1, - closeWrite: (): Promise => Promise.resolve(), - read: (x: Uint8Array): Promise => r.read(x), - write: (x: Uint8Array): Promise => w.write(x), - close: (): void => {}, - localAddr: { transport: "tcp", hostname: "0.0.0.0", port: 0 }, - remoteAddr: { transport: "tcp", hostname: "0.0.0.0", port: 0 }, - }; -} - -function delayedWriter(ms: number, dest: Deno.Writer): Deno.Writer { - return { - write(p: Uint8Array): Promise { - return new Promise((resolve) => { - setTimeout(async (): Promise => { - resolve(await dest.write(p)); - }, ms); - }); - }, - }; -} -Deno.test({ - name: "[ws] WebSocket.send(), WebSocket.ping() should be exclusive", - fn: async (): Promise => { - const buf = new Deno.Buffer(); - const conn = dummyConn(new Deno.Buffer(), delayedWriter(1, buf)); - const sock = createWebSocket({ conn }); - // Ensure send call - await Promise.all([ - sock.send("first"), - sock.send("second"), - sock.ping(), - sock.send(new Uint8Array([3])), - ]); - const bufr = new BufReader(buf); - const first = await readFrame(bufr); - const second = await readFrame(bufr); - const ping = await readFrame(bufr); - const third = await readFrame(bufr); - assertEquals(first.opcode, OpCode.TextFrame); - assertEquals(decode(first.payload), "first"); - assertEquals(first.opcode, OpCode.TextFrame); - assertEquals(decode(second.payload), "second"); - assertEquals(ping.opcode, OpCode.Ping); - assertEquals(third.opcode, OpCode.BinaryFrame); - assertEquals(bytes.equals(third.payload, new Uint8Array([3])), true); - }, -}); - -Deno.test("[ws] createSecKeyHasCorrectLength", () => { - // Note: relies on --seed=86 being passed to deno to reproduce failure in - // #4063. - const secKey = createSecKey(); - assertEquals(atob(secKey).length, 16); -}); - -Deno.test( - "[ws] WebSocket should throw `Deno.errors.ConnectionReset` when peer closed connection without close frame", - async () => { - const buf = new Deno.Buffer(); - const eofReader: Deno.Reader = { - read(_: Uint8Array): Promise { - return Promise.resolve(null); - }, - }; - const conn = dummyConn(eofReader, buf); - const sock = createWebSocket({ conn }); - sock.closeForce(); - await assertThrowsAsync( - () => sock.send("hello"), - Deno.errors.ConnectionReset, - ); - await assertThrowsAsync(() => sock.ping(), Deno.errors.ConnectionReset); - await assertThrowsAsync(() => sock.close(0), Deno.errors.ConnectionReset); - }, -); - -Deno.test( - "[ws] WebSocket shouldn't throw `Deno.errors.UnexpectedEof`", - async () => { - const buf = new Deno.Buffer(); - const eofReader: Deno.Reader = { - read(_: Uint8Array): Promise { - return Promise.resolve(null); - }, - }; - const conn = dummyConn(eofReader, buf); - const sock = createWebSocket({ conn }); - const it = sock[Symbol.asyncIterator](); - const { value, done } = await it.next(); - assertEquals(value, undefined); - assertEquals(done, true); - }, -); - -Deno.test({ - name: - "[ws] WebSocket should reject sending promise when connection reset forcely", - fn: async () => { - const buf = new Deno.Buffer(); - let timer: number | undefined; - const lazyWriter: Deno.Writer = { - write(_: Uint8Array): Promise { - return new Promise((resolve) => { - timer = setTimeout(() => resolve(0), 1000); - }); - }, - }; - const conn = dummyConn(buf, lazyWriter); - const sock = createWebSocket({ conn }); - const onError = (e: unknown): unknown => e; - const p = Promise.all([ - sock.send("hello").catch(onError), - sock.send(new Uint8Array([1, 2])).catch(onError), - sock.ping().catch(onError), - ]); - sock.closeForce(); - assertEquals(sock.isClosed, true); - const [a, b, c] = await p; - assert(a instanceof Deno.errors.ConnectionReset); - assert(b instanceof Deno.errors.ConnectionReset); - assert(c instanceof Deno.errors.ConnectionReset); - clearTimeout(timer); - // Wait for another event loop turn for `timeout` op promise - // to resolve, otherwise we'll get "op leak". - await delay(10); - }, -}); - -Deno.test("[ws] WebSocket should act as asyncIterator", async () => { - const pingHello = new Uint8Array([0x89, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]); - const hello = new Uint8Array([0x81, 0x05, 0x48, 0x65, 0x6c, 0x6c, 0x6f]); - const close = new Uint8Array([0x88, 0x04, 0x03, 0xf3, 0x34, 0x32]); - - enum Frames { - ping, - hello, - close, - end, - } - - let frame = Frames.ping; - - const reader: Deno.Reader = { - read(p: Uint8Array): Promise { - if (frame === Frames.ping) { - frame = Frames.hello; - p.set(pingHello); - return Promise.resolve(pingHello.byteLength); - } - - if (frame === Frames.hello) { - frame = Frames.close; - p.set(hello); - return Promise.resolve(hello.byteLength); - } - - if (frame === Frames.close) { - frame = Frames.end; - p.set(close); - return Promise.resolve(close.byteLength); - } - - return Promise.resolve(null); - }, - }; - - const conn = dummyConn(reader, new Deno.Buffer()); - const sock = createWebSocket({ conn }); - - const events = []; - for await (const wsEvent of sock) { - events.push(wsEvent); - } - - assertEquals(events.length, 3); - assertEquals(events[0], ["ping", encode("Hello")]); - assertEquals(events[1], "Hello"); - assertEquals(events[2], { code: 1011, reason: "42" }); -}); - -Deno.test("[ws] WebSocket protocol", async () => { - const promise = deferred(); - const server = serve({ port: 5839 }); - - const ws = new WebSocket("ws://localhost:5839", ["foo", "bar"]); - ws.onopen = () => { - assertEquals(ws.protocol, "foo, bar"); - ws.close(); - }; - ws.onerror = () => fail(); - ws.onclose = () => { - server.close(); - promise.resolve(); - }; - - const x = await server[Symbol.asyncIterator]().next(); - if (!x.done) { - const { conn, r: bufReader, w: bufWriter, headers } = x.value; - await acceptWebSocket({ - conn, - bufReader, - bufWriter, - headers, - }); - - await promise; - } else { - fail(); - } -}); -- cgit v1.2.3