diff options
Diffstat (limited to 'std/ws')
-rw-r--r-- | std/ws/README.md | 114 | ||||
-rw-r--r-- | std/ws/example_server.ts | 58 | ||||
-rw-r--r-- | std/ws/example_test.ts | 2 | ||||
-rw-r--r-- | std/ws/mod.ts | 531 | ||||
-rw-r--r-- | std/ws/test.ts | 495 |
5 files changed, 0 insertions, 1200 deletions
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<void> { - 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<WebSocketEvent> { - readonly conn: Deno.Conn; - readonly isClosed: boolean; - - [Symbol.asyncIterator](): AsyncIterableIterator<WebSocketEvent>; - - /** - * @throws `Deno.errors.ConnectionReset` - */ - send(data: WebSocketMessage): Promise<void>; - - /** - * @param data - * @throws `Deno.errors.ConnectionReset` - */ - ping(data?: WebSocketMessage): Promise<void>; - - /** 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<void>; - close(code: number): Promise<void>; - close(code: number, reason: string): Promise<void>; - - /** 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<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 = 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<WebSocketFrame> { - 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<void>; - }> = []; - - 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<WebSocketEvent> { - 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<void> { - if (this._isClosed) { - throw new Deno.errors.ConnectionReset("Socket has already been closed"); - } - const d = deferred<void>(); - this.sendQueue.push({ d, frame }); - if (this.sendQueue.length === 1) { - this.dequeue(); - } - return d; - } - - send(data: WebSocketMessage): Promise<void> { - 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<void> { - 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<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 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<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); - 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<void> { - 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(/^(?<version>\S+) (?<statusCode>\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<void> => { - 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<void> { - const writer = new Deno.Buffer(); - const reader = new Deno.Buffer(encode("HTTP/1.1 400\r\n")); - - await assertThrowsAsync( - async (): Promise<void> => { - 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<void> => Promise.resolve(), - read: (x: Uint8Array): Promise<number | null> => r.read(x), - write: (x: Uint8Array): Promise<number> => 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<number> { - return new Promise<number>((resolve) => { - setTimeout(async (): Promise<void> => { - resolve(await dest.write(p)); - }, ms); - }); - }, - }; -} -Deno.test({ - name: "[ws] WebSocket.send(), WebSocket.ping() should be exclusive", - fn: async (): Promise<void> => { - 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<number | null> { - 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<number | null> { - 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<number> { - 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<number | null> { - 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(); - } -}); |