/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
tests/unit/websocket_test.ts
1 295 строк
36 KB
Bartek Iwańczuk
fix(ext/websocket): disable HTTP/2 server push on wss upgrade path (#36327)
27 июл 2026, 15:54
Не верифицирован
27 июл 2026, 15:54
b05ad48
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. // deno-lint-ignore-file no-console import { assert, assertEquals, assertThrows, delay, fail, } from "./test_util.ts"; const servePort = 4248; const serveUrl = `ws://localhost:${servePort}/`; Deno.test({ permissions: "none" }, function websocketPermissionless() { assertThrows( () => new WebSocket("ws://localhost"), Deno.errors.NotCapable, ); }); Deno.test(async function websocketConstructorTakeURLObjectAsParameter() { const { promise, resolve, reject } = Promise.withResolvers<void>(); const ws = new WebSocket(new URL("ws://localhost:4242/")); assertEquals(ws.url, "ws://localhost:4242/"); ws.onerror = (e) => reject(e); ws.onopen = () => ws.close(); ws.onclose = () => { resolve(); }; await promise; }); Deno.test(async function websocketH2SendSmallPacket() { const { promise, resolve, reject } = Promise.withResolvers<void>(); const ws = new WebSocket(new URL("wss://localhost:4249/")); assertEquals(ws.url, "wss://localhost:4249/"); let messageCount = 0; ws.onerror = (e) => reject(e); ws.onopen = () => { ws.send("a".repeat(16)); ws.send("a".repeat(16)); ws.send("a".repeat(16)); }; ws.onmessage = () => { if (++messageCount == 3) { ws.close(); } }; ws.onclose = () => { resolve(); }; await promise; }); // Regression test: the WebSocket-over-HTTP/2 client must disable HTTP/2 server // push (SETTINGS_ENABLE_PUSH = 0). Leaving push enabled exposes an h2 // state-machine assertion that a malicious server can trip via a crafted // PUSH_PROMISE sequence, aborting the whole process. The test server attempts a // server push and reports the outcome as its first message: a safe client makes // h2 reject the push ("push-rejected"). Deno.test(async function websocketH2DisablesServerPush() { const { promise, resolve, reject } = Promise.withResolvers<void>(); const ws = new WebSocket(new URL("wss://localhost:4266/")); ws.onerror = (e) => reject(e); ws.onmessage = (m) => { try { assertEquals(m.data, "push-rejected"); } finally { ws.close(); } }; ws.onclose = () => resolve(); await promise; }); Deno.test(async function websocketH2SendLargePacket() { const { promise, resolve, reject } = Promise.withResolvers<void>(); const ws = new WebSocket(new URL("wss://localhost:4249/")); assertEquals(ws.url, "wss://localhost:4249/"); let messageCount = 0; ws.onerror = (e) => reject(e); ws.onopen = () => { ws.send("a".repeat(65000)); ws.send("a".repeat(65000)); ws.send("a".repeat(65000)); }; ws.onmessage = () => { if (++messageCount == 3) { ws.close(); } }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test(async function websocketSendLargePacket() { const { promise, resolve, reject } = Promise.withResolvers<void>(); const ws = new WebSocket(new URL("wss://localhost:4243/")); assertEquals(ws.url, "wss://localhost:4243/"); ws.onerror = (e) => reject(e); ws.onopen = () => { ws.send("a".repeat(65000)); }; ws.onmessage = () => { ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test(async function websocketSendLargeBinaryPacket() { const { promise, resolve, reject } = Promise.withResolvers<void>(); const ws = new WebSocket(new URL("wss://localhost:4243/")); ws.binaryType = "arraybuffer"; assertEquals(ws.url, "wss://localhost:4243/"); ws.onerror = (e) => reject(e); ws.onopen = () => { ws.send(new Uint8Array(65000)); }; ws.onmessage = (msg: MessageEvent) => { assertEquals(msg.data.byteLength, 65000); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test(async function websocketSendLargeBlobPacket() { const { promise, resolve, reject } = Promise.withResolvers<void>(); const ws = new WebSocket(new URL("wss://localhost:4243/")); ws.binaryType = "arraybuffer"; assertEquals(ws.url, "wss://localhost:4243/"); ws.onerror = (e) => reject(e); ws.onopen = () => { ws.send(new Blob(["a".repeat(65000)])); }; ws.onmessage = (msg: MessageEvent) => { assertEquals(msg.data.byteLength, 65000); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); // https://github.com/denoland/deno/pull/17762 // https://github.com/denoland/deno/issues/17761 Deno.test(async function websocketPingPong() { const { promise, resolve, reject } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4245/"); assertEquals(ws.url, "ws://localhost:4245/"); ws.onerror = (e) => reject(e); ws.onmessage = (e) => { ws.send(e.data); }; ws.onclose = () => { resolve(); }; await promise; ws.close(); }); // TODO(mmastrac): This requires us to ignore bad certs // Deno.test(async function websocketSecureConnect() { // const { promise, resolve } = Promise.withResolvers<void>(); // const ws = new WebSocket("wss://localhost:4243/"); // assertEquals(ws.url, "wss://localhost:4243/"); // ws.onerror = (error) => { // console.log(error); // fail(); // }; // ws.onopen = () => ws.close(); // ws.onclose = () => { // resolve(); // }; // await promise; // }); // https://github.com/denoland/deno/issues/18700 Deno.test( { sanitizeOps: false, sanitizeResources: false }, async function websocketWriteLock() { const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const server = Deno.serve({ handler: (req) => { const { socket, response } = Deno.upgradeWebSocket(req); socket.onopen = function () { setTimeout(() => socket.send("Hello"), 500); }; socket.onmessage = function (e) { assertEquals(e.data, "Hello"); ac.abort(); }; return response; }, signal: ac.signal, onListen: () => listeningDeferred.resolve(), hostname: "localhost", port: servePort, }); await listeningDeferred.promise; const deferred = Promise.withResolvers<void>(); const ws = new WebSocket(serveUrl); assertEquals(ws.url, serveUrl); ws.onerror = () => fail(); ws.onmessage = (e) => { assertEquals(e.data, "Hello"); setTimeout(() => { ws.send(e.data); }, 1000); deferred.resolve(); }; ws.onclose = () => { deferred.resolve(); }; await Promise.all([deferred.promise, server.finished]); ws.close(); }, ); // https://github.com/denoland/deno/issues/18775 Deno.test({ sanitizeOps: false, sanitizeResources: false, }, async function websocketDoubleClose() { const deferred = Promise.withResolvers<void>(); const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const server = Deno.serve({ handler: (req) => { const { response, socket } = Deno.upgradeWebSocket(req); let called = false; socket.onopen = () => socket.send("Hello"); socket.onmessage = () => { assert(!called); called = true; socket.send("bye"); socket.close(); }; socket.onclose = () => ac.abort(); socket.onerror = () => fail(); return response; }, signal: ac.signal, onListen: () => listeningDeferred.resolve(), hostname: "localhost", port: servePort, }); await listeningDeferred.promise; const ws = new WebSocket(serveUrl); assertEquals(ws.url, serveUrl); ws.onerror = () => fail(); ws.onmessage = (m: MessageEvent) => { if (m.data == "Hello") ws.send("bye"); }; ws.onclose = () => { deferred.resolve(); }; await Promise.all([deferred.promise, server.finished]); }); // https://github.com/denoland/deno/issues/19483 Deno.test({ sanitizeOps: false, sanitizeResources: false, }, async function websocketCloseFlushes() { const deferred = Promise.withResolvers<void>(); const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const server = Deno.serve({ handler: (req) => { const { response, socket } = Deno.upgradeWebSocket(req); socket.onopen = () => socket.send("Hello"); socket.onmessage = () => { socket.send("Bye"); socket.close(1000); }; socket.onclose = () => ac.abort(); socket.onerror = () => fail(); return response; }, signal: ac.signal, onListen: () => listeningDeferred.resolve(), hostname: "localhost", port: servePort, }); await listeningDeferred.promise; const ws = new WebSocket(serveUrl); assertEquals(ws.url, serveUrl); let seenBye = false; ws.onerror = () => fail(); ws.onmessage = ({ data }) => { if (data == "Hello") { ws.send("Hello!"); } else { assertEquals(data, "Bye"); seenBye = true; } }; ws.onclose = (e) => { assertEquals(e.code, 1000); deferred.resolve(); }; await Promise.all([deferred.promise, server.finished]); assert(seenBye); }); // https://github.com/denoland/deno/issues/19283 Deno.test({ sanitizeOps: false, sanitizeResources: false, }, async function websocketUrlCredentialsAsBasicAuth() { const deferred = Promise.withResolvers<void>(); const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); let seenAuth: string | null = null; const server = Deno.serve({ handler: (req) => { seenAuth = req.headers.get("authorization"); if (seenAuth !== "Basic " + btoa("user:p@ss")) { return new Response("unauthorized", { status: 401 }); } const { response, socket } = Deno.upgradeWebSocket(req); socket.onopen = () => socket.close(); socket.onclose = () => ac.abort(); return response; }, signal: ac.signal, onListen: () => listeningDeferred.resolve(), hostname: "localhost", port: servePort, }); await listeningDeferred.promise; // Percent-encoded userinfo (p%40ss decodes to p@ss). const ws = new WebSocket(`ws://user:p%40ss@localhost:${servePort}/`); // Credentials are stripped from the URL exposed via `url`. assertEquals(ws.url, serveUrl); ws.onerror = () => fail(); ws.onclose = () => deferred.resolve(); await Promise.all([deferred.promise, server.finished]); assertEquals(seenAuth, "Basic " + btoa("user:p@ss")); }); Deno.test( { sanitizeOps: false }, function websocketConstructorWithPrototypePollution() { const originalSymbolIterator = Array.prototype[Symbol.iterator]; try { Array.prototype[Symbol.iterator] = () => { throw Error("unreachable"); }; assertThrows(() => { new WebSocket( new URL("ws://localhost:4242/"), // Allow `Symbol.iterator` to be called in WebIDL conversion to `sequence<DOMString>` // deno-lint-ignore no-explicit-any ["soap", "soap"].values() as any, ); }, DOMException); } finally { Array.prototype[Symbol.iterator] = originalSymbolIterator; } }, ); Deno.test({ sanitizeOps: false, sanitizeResources: false, }, async function websocketTlsSocketWorks() { const cert = await Deno.readTextFile("tests/testdata/tls/localhost.crt"); const key = await Deno.readTextFile("tests/testdata/tls/localhost.key"); const messages: string[] = [], errors: { server?: Event; client?: Event }[] = []; const promise = new Promise((okay, nope) => { const ac = new AbortController(); const server = Deno.serve({ handler: (req) => { const { response, socket } = Deno.upgradeWebSocket(req); socket.onopen = () => socket.send("ping"); socket.onmessage = (e) => { messages.push(e.data); socket.close(); }; socket.onerror = (e) => errors.push({ server: e }); socket.onclose = () => ac.abort(); return response; }, signal: ac.signal, hostname: "localhost", port: servePort, cert, key, }); setTimeout(() => { const ws = new WebSocket(`wss://localhost:${servePort}`); ws.onmessage = (e) => { messages.push(e.data); ws.send("pong"); }; ws.onerror = (e) => { errors.push({ client: e }); nope(); }; ws.onclose = () => okay(server.finished); }, 1000); }); const finished = await promise; assertEquals(errors, []); assertEquals(messages, ["ping", "pong"]); await finished; }); // https://github.com/denoland/deno/issues/15340 Deno.test( async function websocketServerFieldInit() { const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const server = Deno.serve({ handler: (req) => { const { socket, response } = Deno.upgradeWebSocket(req, { idleTimeout: 0, }); socket.onopen = function () { assert(typeof socket.url == "string"); assert(socket.readyState == WebSocket.OPEN); assert(socket.protocol == ""); assert(socket.binaryType == "arraybuffer"); socket.close(); }; socket.onclose = () => ac.abort(); return response; }, signal: ac.signal, onListen: () => listeningDeferred.resolve(), hostname: "localhost", port: servePort, }); await listeningDeferred.promise; const deferred = Promise.withResolvers<void>(); const ws = new WebSocket(serveUrl); assertEquals(ws.url, serveUrl); ws.onerror = () => fail(); ws.onclose = () => { deferred.resolve(); }; await Promise.all([deferred.promise, server.finished]); }, ); Deno.test( { sanitizeOps: false }, async function websocketServerGetsGhosted() { const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const server = Deno.serve({ handler: (req) => { const { socket, response } = Deno.upgradeWebSocket(req, { idleTimeout: 2, }); socket.onerror = () => socket.close(); socket.onclose = () => ac.abort(); return response; }, signal: ac.signal, onListen: () => listeningDeferred.resolve(), hostname: "localhost", port: servePort, }); await listeningDeferred.promise; const r = await fetch("http://localhost:4545/ghost_ws_client"); assertEquals(r.status, 200); await r.body?.cancel(); await server.finished; }, ); Deno.test("invalid scheme", () => { assertThrows(() => new WebSocket("foo://localhost:4242")); }); Deno.test("fragment", () => { assertThrows(() => new WebSocket("ws://localhost:4242/#")); assertThrows(() => new WebSocket("ws://localhost:4242/#foo")); }); Deno.test("duplicate protocols", () => { assertThrows(() => new WebSocket("ws://localhost:4242", ["foo", "foo"])); }); Deno.test("invalid server", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:2121"); let err = false; ws.onerror = (e) => { assert("error" in e); err = true; }; ws.onclose = () => { if (err) { resolve(); } else { fail(); } }; ws.onopen = () => fail(); await promise; }); Deno.test("connect & close", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.onerror = () => fail(); ws.onopen = () => { ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("connect & abort", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.close(); let err = false; ws.onerror = () => { err = true; }; ws.onclose = () => { if (err) { resolve(); } else { fail(); } }; ws.onopen = () => fail(); await promise; }); Deno.test("connect & close custom valid code", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.onerror = () => fail(); ws.onopen = () => ws.close(1000); ws.onclose = () => { resolve(); }; await promise; }); Deno.test("connect & close custom invalid code", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.onerror = () => fail(); ws.onopen = () => { assertThrows(() => ws.close(1001)); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("connect & close custom valid reason", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.onerror = () => fail(); ws.onopen = () => ws.close(1000, "foo"); ws.onclose = () => { resolve(); }; await promise; }); Deno.test("connect & close custom invalid reason", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.onerror = () => fail(); ws.onopen = () => { assertThrows(() => ws.close(1000, "".padEnd(124, "o"))); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("echo string", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.onerror = () => fail(); ws.onopen = () => ws.send("foo"); ws.onmessage = (e) => { assertEquals(e.data, "foo"); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("echo string tls", async () => { const deferred1 = Promise.withResolvers<void>(); const deferred2 = Promise.withResolvers<void>(); const ws = new WebSocket("wss://localhost:4243"); ws.onerror = () => fail(); ws.onopen = () => ws.send("foo"); ws.onmessage = (e) => { assertEquals(e.data, "foo"); ws.close(); deferred1.resolve(); }; ws.onclose = () => { deferred2.resolve(); }; await deferred1.promise; await deferred2.promise; }); Deno.test("websocket error", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("wss://localhost:4242"); ws.onopen = () => fail(); ws.onerror = (err) => { assert(err instanceof ErrorEvent); assertEquals( err.message, "NetworkError: failed to connect to WebSocket: received corrupt message of type InvalidContentType", ); resolve(); }; await promise; }); Deno.test("echo blob with binaryType blob", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); const blob = new Blob(["foo"]); ws.onerror = () => fail(); ws.onopen = () => ws.send(blob); ws.onmessage = (e) => { e.data.text().then((actual: string) => { blob.text().then((expected) => { assertEquals(actual, expected); }); }); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("echo blob with binaryType arraybuffer", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.binaryType = "arraybuffer"; const blob = new Blob(["foo"]); ws.onerror = () => fail(); ws.onopen = () => ws.send(blob); ws.onmessage = (e) => { blob.arrayBuffer().then((expected) => { assertEquals(e.data, expected); }); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("echo uint8array with binaryType blob", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); const uint = new Uint8Array([102, 111, 111]); ws.onerror = () => fail(); ws.onopen = () => ws.send(uint); ws.onmessage = (e) => { e.data.arrayBuffer().then((actual: ArrayBuffer) => { assertEquals(actual, uint.buffer); }); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("echo uint8array with binaryType arraybuffer", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.binaryType = "arraybuffer"; const uint = new Uint8Array([102, 111, 111]); ws.onerror = () => fail(); ws.onopen = () => ws.send(uint); ws.onmessage = (e) => { assertEquals(e.data, uint.buffer); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("echo arraybuffer with binaryType blob", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); const buffer = new ArrayBuffer(3); ws.onerror = () => fail(); ws.onopen = () => ws.send(buffer); ws.onmessage = (e) => { e.data.arrayBuffer().then((actual: ArrayBuffer) => { assertEquals(actual, buffer); }); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("echo arraybuffer with binaryType arraybuffer", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.binaryType = "arraybuffer"; const buffer = new ArrayBuffer(3); ws.onerror = () => fail(); ws.onopen = () => ws.send(buffer); ws.onmessage = (e) => { assertEquals(e.data, buffer); ws.close(); }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("echo blob mixed with string", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); ws.binaryType = "arraybuffer"; const blob = new Blob(["foo"]); ws.onerror = () => fail(); ws.onopen = () => { ws.send(blob); ws.send("bar"); }; const messages: (ArrayBuffer | string)[] = []; ws.onmessage = (e) => { messages.push(e.data); if (messages.length === 2) { assertEquals(messages[0], new Uint8Array([102, 111, 111]).buffer); assertEquals(messages[1], "bar"); ws.close(); } }; ws.onclose = () => { resolve(); }; await promise; }); Deno.test("Event Handlers order", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); const arr: number[] = []; ws.onerror = () => fail(); ws.addEventListener("message", () => arr.push(1)); ws.onmessage = () => fail(); ws.addEventListener("message", () => { arr.push(3); ws.close(); assertEquals(arr, [1, 2, 3]); }); ws.onmessage = () => arr.push(2); ws.onopen = () => ws.send("Echo"); ws.onclose = () => { resolve(); }; await promise; }); Deno.test("Close without frame", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4244"); ws.onerror = () => fail(); ws.onclose = (e) => { assertEquals(e.code, 1005); resolve(); }; await promise; }); Deno.test("Close connection", async () => { const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const server = Deno.serve({ handler: (req) => { const { socket, response } = Deno.upgradeWebSocket(req); socket.onmessage = function (e) { socket.close(1008); assertEquals(e.data, "Hello"); }; socket.onclose = () => { ac.abort(); }; socket.onerror = () => fail(); return response; }, signal: ac.signal, onListen: () => listeningDeferred.resolve(), hostname: "localhost", port: servePort, }); await listeningDeferred.promise; const conn = await Deno.connect({ port: servePort, hostname: "localhost" }); await conn.write( new TextEncoder().encode( "GET / HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n", ), ); // Write a 2 text frame saying "Hello" await conn.write(new Uint8Array([0x81, 0x05])); await conn.write(new TextEncoder().encode("Hello")); // We are a bad client so we won't acknowledge the close frame await conn.write(new Uint8Array([0x81, 0x05])); await conn.write(new TextEncoder().encode("Hello")); await server.finished; conn.close(); }); Deno.test("send to a closed socket", async () => { const { promise, resolve } = Promise.withResolvers<void>(); const ws = new WebSocket("ws://localhost:4242"); const blob = new Blob(["foo"]); ws.onerror = () => fail(); ws.onopen = () => { ws.close(); ws.send(blob); }; ws.onclose = () => { resolve(); }; await promise; }); // https://github.com/denoland/deno/issues/25126 Deno.test("websocket close ongoing handshake", async () => { // First try to close without any delay { const { promise, resolve } = Promise.withResolvers<void>(); let gotError1 = false; const ws = new WebSocket("ws://localhost:4264"); ws.onopen = () => fail(); ws.onerror = (e) => { assertEquals((e as ErrorEvent).error.code, "EINTR"); gotError1 = true; }; ws.onclose = () => resolve(); ws.close(); await promise; assert(gotError1); } await delay(50); // Wait a bit before trying again. { const { promise: promise2, resolve: resolve2 } = Promise.withResolvers< void >(); const ws2 = new WebSocket("ws://localhost:4264"); ws2.onopen = () => fail(); let gotError2 = false; ws2.onerror = (e) => { assertEquals((e as ErrorEvent).error.code, "EINTR"); gotError2 = true; }; ws2.onclose = () => resolve2(); await delay(50); // wait a bit this time before calling close ws2.close(); await promise2; assert(gotError2); } }); // Regression test: non-ASCII bytes in Sec-WebSocket-Protocol header // should not cause a panic. Deno.test( { sanitizeOps: false, sanitizeResources: false }, async function websocketNonAsciiProtocolHeader() { const port = 14248; const listener = Deno.listen({ port, hostname: "127.0.0.1" }); const serverTask = (async () => { const conn = await listener.accept(); const buf = new Uint8Array(4096); let total = 0; while (true) { const n = await conn.read(buf.subarray(total)); if (n === null) break; total += n; const text = new TextDecoder().decode(buf.subarray(0, total)); if (text.includes("\r\n\r\n")) break; } // Parse Sec-WebSocket-Key from the request const request = new TextDecoder().decode(buf.subarray(0, total)); const keyMatch = request.match(/sec-websocket-key:\s*(\S+)/i); assert(keyMatch !== null, "Expected Sec-WebSocket-Key in: " + request); const key = keyMatch[1]; // Compute accept key const hash = await crypto.subtle.digest( "SHA-1", new TextEncoder().encode(key + "258EAFA5-E914-47DA-95CA-5AB5DF85B1E3"), ); const accept = btoa( String.fromCharCode(...new Uint8Array(hash)), ); // Build response with non-ASCII bytes in Sec-WebSocket-Protocol const headerPart = `HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\nSec-WebSocket-Protocol: `; const headerPartBytes = new TextEncoder().encode(headerPart); // 0xFF is non-ASCII const nonAscii = new Uint8Array([0xFF, 0xFE]); const ending = new TextEncoder().encode("\r\n\r\n"); const response = new Uint8Array( headerPartBytes.length + nonAscii.length + ending.length, ); response.set(headerPartBytes, 0); response.set(nonAscii, headerPartBytes.length); response.set(ending, headerPartBytes.length + nonAscii.length); await conn.write(response); // Send a close frame await conn.write(new Uint8Array([0x88, 0x02, 0x03, 0xE8])); // Give it a moment then clean up await delay(100); conn.close(); })(); const { promise, resolve, reject } = Promise.withResolvers<void>(); const ws = new WebSocket(`ws://127.0.0.1:${port}`); ws.onerror = () => {}; ws.onopen = () => { // If we get here without panicking, the fix works. ws.close(); }; ws.onclose = () => resolve(); setTimeout(() => reject(new Error("timeout")), 5000); await promise; await serverTask; listener.close(); }, ); function createOnErrorCb(ac: AbortController): (err: unknown) => Response { return (err) => { console.error(err); ac.abort(); return new Response("Internal server error", { status: 500 }); }; } function onListen( resolve: (value: void | PromiseLike<void>) => void, ): (_addr: Deno.Addr) => void { return () => { resolve(); }; } Deno.test("WebSocket headers", async () => { const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const doneDeferred = Promise.withResolvers<void>(); await using server = Deno.serve({ handler: (request) => { assertEquals(request.headers.get("Authorization"), "Bearer foo"); const { response, socket, } = Deno.upgradeWebSocket(request); socket.onerror = (e) => { console.error(e); fail(); }; socket.onmessage = (m) => { socket.send(m.data); socket.close(1001); }; socket.onclose = () => doneDeferred.resolve(); return response; }, port: servePort, signal: ac.signal, onListen: onListen(listeningDeferred.resolve), onError: createOnErrorCb(ac), }); await listeningDeferred.promise; const def = Promise.withResolvers<void>(); const ws = new WebSocket(`ws://localhost:${servePort}`, { headers: { "Authorization": "Bearer foo", }, }); ws.onmessage = (m) => assertEquals(m.data, "foo"); ws.onerror = (e) => { console.error(e); fail(); }; ws.onclose = () => def.resolve(); ws.onopen = () => ws.send("foo"); await def.promise; await doneDeferred.promise; ac.abort(); await server.finished; }); Deno.test("WebSocket with custom http client with unix proxy", { ignore: Deno.build.os === "windows", }, async () => { const socketPath = Deno.makeTempFileSync(); Deno.removeSync(socketPath); const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const doneDeferred = Promise.withResolvers<void>(); await using server = Deno.serve({ handler: (request) => { const { response, socket } = Deno.upgradeWebSocket(request); socket.onerror = (e) => { console.error(e); fail(); }; socket.onmessage = (m) => { socket.send(m.data); socket.close(1001); }; socket.onclose = () => doneDeferred.resolve(); return response; }, signal: ac.signal, onListen: onListen(listeningDeferred.resolve), onError: createOnErrorCb(ac), transport: "unix", path: socketPath, }); await listeningDeferred.promise; const def = Promise.withResolvers<void>(); using client = Deno.createHttpClient({ proxy: { transport: "unix", path: socketPath, }, }); const ws = new WebSocket(`ws://localhost:8000/`, { client }); ws.onmessage = (m) => assertEquals(m.data, "foo"); ws.onerror = (e) => { console.error(e); fail(); }; ws.onclose = () => def.resolve(); ws.onopen = () => ws.send("foo"); await def.promise; await doneDeferred.promise; ac.abort(); await server.finished; }); Deno.test("WebSocket with custom http client with vsock proxy", { ignore: Deno.build.os !== "linux", }, async () => { const socketPath = Deno.makeTempFileSync(); Deno.removeSync(socketPath); const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const doneDeferred = Promise.withResolvers<void>(); await using server = Deno.serve({ handler: (request) => { const { response, socket } = Deno.upgradeWebSocket(request); socket.onerror = (e) => { console.error(e); fail(); }; socket.onmessage = (m) => { socket.send(m.data); socket.close(1001); }; socket.onclose = () => doneDeferred.resolve(); return response; }, signal: ac.signal, onListen: onListen(listeningDeferred.resolve), onError: createOnErrorCb(ac), transport: "vsock", cid: -1, port: 4242, }); await listeningDeferred.promise; const def = Promise.withResolvers<void>(); using client = Deno.createHttpClient({ proxy: { transport: "vsock", cid: 1, port: 4242, }, }); const ws = new WebSocket(`ws://localhost:8000/`, { client }); ws.onmessage = (m) => assertEquals(m.data, "foo"); ws.onerror = (e) => { console.error(e); fail(); }; ws.onclose = () => def.resolve(); ws.onopen = () => ws.send("foo"); await def.promise; await doneDeferred.promise; ac.abort(); await server.finished; }); Deno.test("WebSocket custom host header", async () => { const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const doneDeferred = Promise.withResolvers<void>(); await using server = Deno.serve({ handler: (request) => { assertEquals(request.headers.get("host"), "example.com"); assertEquals(request.url, "http://example.com/"); const { response, socket, } = Deno.upgradeWebSocket(request); socket.onerror = (e) => { console.error(e); fail(); }; socket.onmessage = (m) => { socket.send(m.data); socket.close(1001); }; socket.onclose = () => doneDeferred.resolve(); return response; }, port: servePort, signal: ac.signal, onListen: onListen(listeningDeferred.resolve), onError: createOnErrorCb(ac), }); await listeningDeferred.promise; const def = Promise.withResolvers<void>(); using client = Deno.createHttpClient({ allowHost: true }); const ws = new WebSocket(`ws://localhost:${servePort}`, { headers: { "Host": "example.com", }, client, }); ws.onmessage = (m) => assertEquals(m.data, "foo"); ws.onerror = (e) => { console.error(e); fail(); }; ws.onclose = () => def.resolve(); ws.onopen = () => ws.send("foo"); await def.promise; await doneDeferred.promise; ac.abort(); await server.finished; }); Deno.test("WebSocket close with reason but no code doesn't send 1005", async () => { const ac = new AbortController(); const listeningDeferred = Promise.withResolvers<void>(); const server = Deno.serve({ handler: (request) => { const { response, socket } = Deno.upgradeWebSocket(request); socket.onerror = () => fail(); socket.onopen = () => { socket.close(undefined, "A"); }; socket.onclose = () => ac.abort(); return response; }, port: servePort, signal: ac.signal, onListen: () => listeningDeferred.resolve(), hostname: "localhost", onError: createOnErrorCb(ac), }); await listeningDeferred.promise; const conn = await Deno.connect({ port: servePort, hostname: "localhost" }); await conn.write( new TextEncoder().encode( "GET / HTTP/1.1\r\nConnection: Upgrade\r\nUpgrade: websocket\r\nSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\nSec-WebSocket-Version: 13\r\n\r\n", ), ); const response = new Uint8Array(2048); let totalRead = 0; let n = await conn.read(response); assert(n !== null && n > 0); totalRead += n; const headerEnd = new TextDecoder().decode(response.subarray(0, totalRead)) .indexOf("\r\n\r\n"); assert(headerEnd > 0); const frameStart = headerEnd + 4; // Keep reading until we have the WebSocket frame header and payload while (totalRead < frameStart + 3) { n = await conn.read(response.subarray(totalRead)); assert(n !== null && n > 0); totalRead += n; } // [FIN+opcode][length][payload] assertEquals(response[frameStart], 0x88); const payloadLength = response[frameStart + 1] & 0x7F; // Payload should just be "A" (1 byte), NOT 1005 code (2 bytes) + "A" assertEquals(payloadLength, 1); assertEquals(response[frameStart + 2], "A".charCodeAt(0)); conn.close(); await server.finished; });