From 60c41014054cb78390e115bc59b5962aa64fc6da Mon Sep 17 00:00:00 2001 From: guest271314 Date: Fri, 9 May 2025 04:30:38 +0000 Subject: [PATCH 1/6] Create ws-server.js Interoperability is all over the place. For WebSocket clients - Chromium 138 Developer Build (Linux) { conn: { [Symbol(kHandle)]: {} } } { opcode: 8 } Close opcode. TypeError: Parameter 1 is required in 'respond'. at $ (polyfills.js:28:14849) at respond (polyfills.js:28:21853) at pull (core.js{ conn: { [Symbol(kHandle)]: {} } } { opcode: 8 } Close opcode. WebSocket client connection closed Error: EPIPE: broken pipe :1:11961) - Firefox Nightly 140.0a1 (2025-05-07) (64-bit) Works with 127.0.0.1:8080 Does not work with 0.0.0.0:8080 - Bun 1.2.13 Works with builtin or Undici. Always causes broken pipe in tjs. { conn: { [Symbol(kHandle)]: {} } } { opcode: 8 } Close opcode. WebSocket client connection closed Error: EPIPE: broken pipe - Deno 2.3.1+d372c0d (canary, release, x86_64-unknown-linux-gnu) Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost. - Node.js v24.0.0-nightly202505066102159fa1 Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost. For WebSocketStream clients - Chromium 138 Developer Build (Linux) Works. writer.close() does not cause broken pipe. A different error. Server does not exit. TypeError: Parameter 1 is required in 'respond'. at $ (polyfills.js:28:14849) at respond (polyfills.js:28:21853) at pull (core.js:1:11961) - Deno Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost. - Node.js Does nothing. Using builtin or Undici. 0.0.0.0, 127.0.0.1, or localhost. --- src/js/polyfills/ws-server.js | 229 ++++++++++++++++++++++++++++++++++ 1 file changed, 229 insertions(+) create mode 100644 src/js/polyfills/ws-server.js diff --git a/src/js/polyfills/ws-server.js b/src/js/polyfills/ws-server.js new file mode 100644 index 00000000..bdabb058 --- /dev/null +++ b/src/js/polyfills/ws-server.js @@ -0,0 +1,229 @@ +class WebSocketConnection { + readable; + writable; + writer; + buffer = new Uint8Array(0); + closed = !1; + opcodes = { TEXT: 1, BINARY: 2, PING: 9, PONG: 10, CLOSE: 8 }; + constructor(readable, writable) { + this.readable = readable; + if (writable instanceof WritableStreamDefaultWriter) { + this.writer = writable; + } else if (writable instanceof WritableStream) { + this.writable = writable; + this.writer = this.writable.getWriter(); + } else { + this.writer = writable; + } + } + async processWebSocketStream() { + try { + for await (const frame of this.readable) { + const uint8 = new Uint8Array(this.buffer.length + frame.length); + uint8.set(this.buffer, 0); + uint8.set(frame, this.buffer.length); + this.buffer = uint8; + this.processFrame(); + } + console.log("WebSocket connection closed."); + } catch (e) { + console.log(e); + this.writer.close().catch(console.log); + } + } + writeFrame(opcode, payload) { + this.writer.write(this.encodeMessage(opcode, payload)); + } + send(obj) { + let opcode, payload; + if (obj instanceof Uint8Array) { + opcode = this.opcodes.BINARY; + payload = obj; + } else if (typeof obj == "string") { + opcode = this.opcodes.TEXT; + payload = obj; + } else { + throw new Error("Cannot send object. Must be string or Uint8Array"); + } + this.writeFrame(opcode, payload); + } + close(code, reason) { + const opcode = this.opcodes.CLOSE; + let buffer; + if (code) { + buffer = new Uint8Array(reason.length + 2); + const view = new DataView(buffer.buffer); + view.setUint16(0, code, !1); + buffer.set(reason, 2); + } else { + buffer = new Uint8Array(0); + } + console.log({ opcode }); + this.writeFrame(opcode, buffer); + this.writer.close(); + this.closed = !0; + } + processFrame() { + let length, maskBytes; + const buf = this.buffer, view = new DataView(buf.buffer); + if (buf.length < 2) { + return !1; + } + let idx = 2, + b1 = view.getUint8(0), + fin = b1 & 128, + opcode = b1 & 15, + b2 = view.getUint8(1), + mask = b2 & 128; + length = b2 & 127; + if (length > 125) { + if (buf.length < 8) { + return !1; + } + if (length == 126) { + length = view.getUint16(2, !1); + idx += 2; + } else if (length == 127) { + if (view.getUint32(2, !1) != 0) { + this.close(1009, ""); + } + length = view.getUint32(6, !1); + idx += 8; + } + } + if (buf.length < idx + 4 + length) { + return !1; + } + maskBytes = buf.subarray(idx, idx + 4); + idx += 4; + let payload = buf.subarray(idx, idx + length); + payload = this.unmask(maskBytes, payload); + // Do stuff with payload from client/peer + // console.log(`Got payload from client/peer`); + // Here input is echoed to output + this.handleFrame(opcode, payload); + this.buffer = buf.subarray(idx + length); + if (this.buffer.length === 0) { + // console.log(`this.buffer.length: ${this.buffer.length}.`); + return !1; + } + return !0; + } + handleFrame(opcode, buffer) { + // console.log({ opcode, length: buffer.length }); + const view = new DataView(buffer.buffer); + let payload; + switch (opcode) { + case this.opcodes.TEXT: + payload = buffer; + this.writeFrame(opcode, payload); + break; + case this.opcodes.BINARY: + payload = buffer; + this.writeFrame(opcode, payload); + break; + case this.opcodes.PING: + this.writeFrame(this.opcodes.PONG, buffer); + break; + case this.opcodes.PONG: + break; + case this.opcodes.CLOSE: + let code, reason; + if (buffer.length >= 2) { + code = view.getUint16(0, !1); + reason = (new TextDecoder()).decode(buffer); + } + this.close(code, reason); + console.log("Close opcode."); + break; + default: + this.close(1002, "unknown opcode"); + } + } + unmask(maskBytes2, data) { + let payload = new Uint8Array(data.length); + for (var i = 0; i < data.length; i++) { + payload[i] = maskBytes2[i % 4] ^ data[i]; + } + return payload; + } + encodeMessage(opcode, payload) { + let buf, b1 = 128 | opcode, b2 = 0, length = payload.length; + if (length < 126) { + buf = new Uint8Array(payload.length + 2 + 0); + const view = new DataView(buf.buffer); + b2 |= length; + view.setUint8(0, b1); + view.setUint8(1, b2); + buf.set(payload, 2); + } else if (length < 65536) { + buf = new Uint8Array(payload.length + 2 + 2); + const view = new DataView(buf.buffer); + b2 |= 126; + view.setUint8(0, b1); + view.setUint8(1, b2); + view.setUint16(2, length); + buf.set(payload, 4); + } else { + buf = new Uint8Array(payload.length + 2 + 8); + const view = new DataView(buf.buffer); + b2 |= 127; + view.setUint8(0, b1); + view.setUint8(1, b2); + view.setUint32(2, 0, !1); + view.setUint32(6, length, !1); + buf.set(payload, 10); + } + return buf; + } + static KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + static async hashWebSocketKey(secKeyWebSocket, writable) { + // Use Web Cryptography API crypto.subtle where defined + if (globalThis.crypto.subtle) { + const encoder = new TextEncoder(), + key = btoa( + [ + ...new Uint8Array( + await crypto.subtle.digest( + "SHA-1", + encoder.encode( + `${secKeyWebSocket}${WebSocketConnection.KEY_SUFFIX}`, + ), + ), + ), + ].map((s) => String.fromCodePoint(s)).join(""), + ); + const header = `HTTP/1.1 101 Web Socket Protocol Handshake\r +Upgrade: WebSocket\r +Connection: Upgrade\r +sec-websocket-accept: ` + key + `\r +\r +`; + return writable instanceof WritableStream + ? (new Response(header)).body.pipeTo(writable, { preventClose: !0 }) + : writable.write(encoder.encode(header)); + } else { + // txiki.js does not support Web Cryptography API crypto.subtle + // Use txiki.js specific tjs:hashing or + // https://raw.githubusercontent.com/kawanet/sha1-uint8array/main/lib/sha1-uint8array.ts + const { createHash } = await import("tjs:hashing"); + const encoder = new TextEncoder(); + const hash = createHash("sha1").update( + `${secKeyWebSocket}${WebSocketConnection.KEY_SUFFIX}`, + ).bytes(); + const key = btoa( + String.fromCodePoint(...hash), + ); + const header = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "sec-websocket-accept: " + key + "\r\n\r\n"; + const encoded = encoder.encode(header); + return writable instanceof WritableStream + ? new Response(encode).body.pipeTo(writable, { preventClose: !0 }) + : writable.write(encoded); + } + } +} + +export { WebSocketConnection }; From 9d98272983774b6d98fd0ef7bdb9fd0015f5eeb5 Mon Sep 17 00:00:00 2001 From: guest271314 Date: Sun, 11 May 2025 21:47:01 +0000 Subject: [PATCH 2/6] Write to resizable ArrayBuffer, resize to 0 when connection closed It should be possible to use subarray() instead of slice() https://github.com/quickjs-ng/quickjs/issues/1052 --- src/js/polyfills/ws-server.js | 77 ++++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 32 deletions(-) diff --git a/src/js/polyfills/ws-server.js b/src/js/polyfills/ws-server.js index bdabb058..4443de7b 100644 --- a/src/js/polyfills/ws-server.js +++ b/src/js/polyfills/ws-server.js @@ -2,7 +2,7 @@ class WebSocketConnection { readable; writable; writer; - buffer = new Uint8Array(0); + buffer = new ArrayBuffer(0, { maxByteLength: 1024**2 }); closed = !1; opcodes = { TEXT: 1, BINARY: 2, PING: 9, PONG: 10, CLOSE: 8 }; constructor(readable, writable) { @@ -12,29 +12,33 @@ class WebSocketConnection { } else if (writable instanceof WritableStream) { this.writable = writable; this.writer = this.writable.getWriter(); - } else { - this.writer = writable; } } async processWebSocketStream() { try { for await (const frame of this.readable) { - const uint8 = new Uint8Array(this.buffer.length + frame.length); - uint8.set(this.buffer, 0); - uint8.set(frame, this.buffer.length); - this.buffer = uint8; - this.processFrame(); + const { byteLength } = this.buffer; + console.log(byteLength + frame.length); + this.buffer.resize(byteLength + frame.length); + const view = new DataView(this.buffer); + for (let i = 0, j = byteLength; i < frame.length; i++, j++) { + view.setUint8(j, frame.at(i)); + } + await this.processFrame(); } console.log("WebSocket connection closed."); } catch (e) { console.log(e); - this.writer.close().catch(console.log); + // this.writer.close().catch(console.log); } } - writeFrame(opcode, payload) { - this.writer.write(this.encodeMessage(opcode, payload)); + async writeFrame(opcode, payload) { + await this.writer.ready; + return this.writer.write(this.encodeMessage(opcode, payload)) + .catch(console.log); } - send(obj) { + async send(obj) { + console.log({ obj }); let opcode, payload; if (obj instanceof Uint8Array) { opcode = this.opcodes.BINARY; @@ -45,9 +49,9 @@ class WebSocketConnection { } else { throw new Error("Cannot send object. Must be string or Uint8Array"); } - this.writeFrame(opcode, payload); + await this.writeFrame(opcode, payload); } - close(code, reason) { + async close(code, reason) { const opcode = this.opcodes.CLOSE; let buffer; if (code) { @@ -58,14 +62,19 @@ class WebSocketConnection { } else { buffer = new Uint8Array(0); } - console.log({ opcode }); - this.writeFrame(opcode, buffer); - this.writer.close(); + console.log({ opcode, reason, buffer }); + await this.writeFrame(opcode, buffer); + await this.writer.close().catch((e) => { + console.log(e); + this.buffer.resize(0); + }); + await this.writer.closed; + this.buffer.resize(0); this.closed = !0; } - processFrame() { + async processFrame() { let length, maskBytes; - const buf = this.buffer, view = new DataView(buf.buffer); + const buf = new Uint8Array(this.buffer), view = new DataView(buf.buffer); if (buf.length < 2) { return !1; } @@ -98,32 +107,36 @@ class WebSocketConnection { idx += 4; let payload = buf.subarray(idx, idx + length); payload = this.unmask(maskBytes, payload); - // Do stuff with payload from client/peer - // console.log(`Got payload from client/peer`); - // Here input is echoed to output - this.handleFrame(opcode, payload); - this.buffer = buf.subarray(idx + length); - if (this.buffer.length === 0) { - // console.log(`this.buffer.length: ${this.buffer.length}.`); + await this.handleFrame(opcode, payload); + + if (idx + length === 0) { + console.log(`this.buffer.length: ${this.buffer.byteLength}.`); return !1; } + // It should be possible to use subarray() here + // https://github.com/quickjs-ng/quickjs/issues/1052 + const data = buf.slice(idx + length); + this.buffer.resize(data.length); + for (let i = 0; i < this.buffer.byteLength; i++) { + view.setUint8(i, data.at(i)); + } return !0; } - handleFrame(opcode, buffer) { - // console.log({ opcode, length: buffer.length }); + async handleFrame(opcode, buffer) { + console.log({ opcode, length: buffer.length }); const view = new DataView(buffer.buffer); let payload; switch (opcode) { case this.opcodes.TEXT: payload = buffer; - this.writeFrame(opcode, payload); + await this.writeFrame(opcode, payload); break; case this.opcodes.BINARY: payload = buffer; - this.writeFrame(opcode, payload); + await this.writeFrame(opcode, payload); break; case this.opcodes.PING: - this.writeFrame(this.opcodes.PONG, buffer); + await this.writeFrame(this.opcodes.PONG, buffer); break; case this.opcodes.PONG: break; @@ -142,7 +155,7 @@ class WebSocketConnection { } unmask(maskBytes2, data) { let payload = new Uint8Array(data.length); - for (var i = 0; i < data.length; i++) { + for (let i = 0; i < data.length; i++) { payload[i] = maskBytes2[i % 4] ^ data[i]; } return payload; From 9a81c402d65ceab2c7b71b6c1599afd2dc882fa2 Mon Sep 17 00:00:00 2001 From: guest271314 Date: Tue, 13 May 2025 13:40:47 +0000 Subject: [PATCH 3/6] Read, write to same ArrayBuffer, avoid TypeError: ArrayBuffer is detached or resized https://github.com/quickjs-ng/quickjs/issues/1052 --- src/js/polyfills/ws-server.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/js/polyfills/ws-server.js b/src/js/polyfills/ws-server.js index 4443de7b..96294731 100644 --- a/src/js/polyfills/ws-server.js +++ b/src/js/polyfills/ws-server.js @@ -2,7 +2,7 @@ class WebSocketConnection { readable; writable; writer; - buffer = new ArrayBuffer(0, { maxByteLength: 1024**2 }); + buffer = new ArrayBuffer(0, { maxByteLength: 1024 ** 2 }); closed = !1; opcodes = { TEXT: 1, BINARY: 2, PING: 9, PONG: 10, CLOSE: 8 }; constructor(readable, writable) { @@ -29,6 +29,7 @@ class WebSocketConnection { console.log("WebSocket connection closed."); } catch (e) { console.log(e); + console.trace(); // this.writer.close().catch(console.log); } } @@ -113,13 +114,10 @@ class WebSocketConnection { console.log(`this.buffer.length: ${this.buffer.byteLength}.`); return !1; } - // It should be possible to use subarray() here - // https://github.com/quickjs-ng/quickjs/issues/1052 - const data = buf.slice(idx + length); - this.buffer.resize(data.length); - for (let i = 0; i < this.buffer.byteLength; i++) { - view.setUint8(i, data.at(i)); + for (let i = 0, j = idx + length; j < this.buffer.byteLength; i++, j++) { + view.setUint8(i, view.getUint8(j)); } + this.buffer.resize(this.buffer.byteLength - (idx + length)); return !0; } async handleFrame(opcode, buffer) { From 2a3a65e3e9bd6a073406b31a9060dbaa864a0d70 Mon Sep 17 00:00:00 2001 From: guest271314 Date: Sat, 2 Aug 2025 20:25:39 +0000 Subject: [PATCH 4/6] Update ws-server.js --- src/js/polyfills/ws-server.js | 303 ++++++++++++++++++---------------- 1 file changed, 162 insertions(+), 141 deletions(-) diff --git a/src/js/polyfills/ws-server.js b/src/js/polyfills/ws-server.js index 96294731..edf3f2f3 100644 --- a/src/js/polyfills/ws-server.js +++ b/src/js/polyfills/ws-server.js @@ -1,7 +1,22 @@ +// JavaScript runtime agnostic WebSocket server +// +// Fork of https://gist.github.com/d0ruk/3921918937e234988dfaccfdee781bd3 +// +// The Definitive Guide to HTML5 WebSocket by Vanessa Wang, Frank Salim, and Peter Moskovits +// p. 51, Building a Simple WebSocket Server +// +// guest271314 2025 +// Do What the Fuck You Want to Public License WTFPLv2 http://www.wtfpl.net/about/ + class WebSocketConnection { readable; writable; writer; + incomingStream = new ReadableStream({ + start: (_) => { + return this.incomingStreamController = _; + }, + }); buffer = new ArrayBuffer(0, { maxByteLength: 1024 ** 2 }); closed = !1; opcodes = { TEXT: 1, BINARY: 2, PING: 9, PONG: 10, CLOSE: 8 }; @@ -17,66 +32,33 @@ class WebSocketConnection { async processWebSocketStream() { try { for await (const frame of this.readable) { - const { byteLength } = this.buffer; - console.log(byteLength + frame.length); - this.buffer.resize(byteLength + frame.length); - const view = new DataView(this.buffer); - for (let i = 0, j = byteLength; i < frame.length; i++, j++) { - view.setUint8(j, frame.at(i)); + if (!this.closed) { + const { byteLength } = this.buffer; + this.buffer.resize(byteLength + frame.length); + const view = new DataView(this.buffer); + for (let i = 0, j = byteLength; i < frame.length; i++, j++) { + view.setUint8(j, frame.at(i)); + } + const processedFrame = await this.processFrame(); + if (processedFrame === this.opcodes.CLOSE) { + console.log(processedFrame); + break; + } + } else { + break; } - await this.processFrame(); } console.log("WebSocket connection closed."); } catch (e) { - console.log(e); + console.log(navigator.userAgent, e); console.trace(); - // this.writer.close().catch(console.log); - } - } - async writeFrame(opcode, payload) { - await this.writer.ready; - return this.writer.write(this.encodeMessage(opcode, payload)) - .catch(console.log); - } - async send(obj) { - console.log({ obj }); - let opcode, payload; - if (obj instanceof Uint8Array) { - opcode = this.opcodes.BINARY; - payload = obj; - } else if (typeof obj == "string") { - opcode = this.opcodes.TEXT; - payload = obj; - } else { - throw new Error("Cannot send object. Must be string or Uint8Array"); - } - await this.writeFrame(opcode, payload); - } - async close(code, reason) { - const opcode = this.opcodes.CLOSE; - let buffer; - if (code) { - buffer = new Uint8Array(reason.length + 2); - const view = new DataView(buffer.buffer); - view.setUint16(0, code, !1); - buffer.set(reason, 2); - } else { - buffer = new Uint8Array(0); } - console.log({ opcode, reason, buffer }); - await this.writeFrame(opcode, buffer); - await this.writer.close().catch((e) => { - console.log(e); - this.buffer.resize(0); - }); - await this.writer.closed; - this.buffer.resize(0); - this.closed = !0; } async processFrame() { let length, maskBytes; - const buf = new Uint8Array(this.buffer), view = new DataView(buf.buffer); - if (buf.length < 2) { + const buffer = new Uint8Array(this.buffer), + view = new DataView(buffer.buffer); + if (buffer.length < 2) { return !1; } let idx = 2, @@ -87,7 +69,7 @@ class WebSocketConnection { mask = b2 & 128; length = b2 & 127; if (length > 125) { - if (buf.length < 8) { + if (buffer.length < 8) { return !1; } if (length == 126) { @@ -95,62 +77,111 @@ class WebSocketConnection { idx += 2; } else if (length == 127) { if (view.getUint32(2, !1) != 0) { - this.close(1009, ""); + await this.close(1009, ""); + return this.opcodes.CLOSE; } length = view.getUint32(6, !1); idx += 8; } } - if (buf.length < idx + 4 + length) { + if (buffer.length < idx + 4 + length) { return !1; } - maskBytes = buf.subarray(idx, idx + 4); + maskBytes = buffer.subarray(idx, idx + 4); idx += 4; - let payload = buf.subarray(idx, idx + length); + let payload = buffer.subarray(idx, idx + length); payload = this.unmask(maskBytes, payload); - await this.handleFrame(opcode, payload); - + this.incomingStreamController.enqueue({ opcode, payload }); + if (this.buffer.byteLength === 0 && this.closed) { + return !0; + } if (idx + length === 0) { - console.log(`this.buffer.length: ${this.buffer.byteLength}.`); return !1; } + for (let i = 0, j = idx + length; j < this.buffer.byteLength; i++, j++) { view.setUint8(i, view.getUint8(j)); } this.buffer.resize(this.buffer.byteLength - (idx + length)); - return !0; + return opcode === this.opcodes.CLOSE ? opcode : !0; } - async handleFrame(opcode, buffer) { - console.log({ opcode, length: buffer.length }); - const view = new DataView(buffer.buffer); - let payload; - switch (opcode) { - case this.opcodes.TEXT: - payload = buffer; - await this.writeFrame(opcode, payload); - break; - case this.opcodes.BINARY: - payload = buffer; - await this.writeFrame(opcode, payload); - break; - case this.opcodes.PING: - await this.writeFrame(this.opcodes.PONG, buffer); - break; + async send(obj) { + let opcode, payload; + if (obj instanceof Uint8Array) { + opcode = this.opcodes.BINARY; + payload = obj; + } else if (typeof obj == "string") { + opcode = this.opcodes.TEXT; + payload = new TextEncoder().encode(obj); + } else { + throw new Error("Cannot send object. Must be string or Uint8Array"); + } + await this.writeFrame(opcode, payload); + } + async writeFrame(opcode, buffer) { + await this.writer.ready; + if (opcode === this.opcodes.TEXT) { + return await this.writer.write(this.encodeMessage(opcode, buffer)) + .catch(console.lconsole.logog); + } + if (opcode === this.opcodes.BINARY) { + return await this.writer.write(this.encodeMessage(opcode, buffer)) + .catch(console.log); + } + if (opcode === this.opcodes.PING) { + return await this.writer.write( + this.encodeMessage(this.opcodes.PONG, buffer), + ) + .catch(console.log); + } + /* case this.opcodes.PONG: break; - case this.opcodes.CLOSE: - let code, reason; - if (buffer.length >= 2) { - code = view.getUint16(0, !1); - reason = (new TextDecoder()).decode(buffer); - } - this.close(code, reason); - console.log("Close opcode."); - break; - default: - this.close(1002, "unknown opcode"); + */ + if (opcode === this.opcodes.CLOSE) { + const view = new DataView(buffer.buffer); + let code, reason; + if (buffer.length >= 2) { + code = view.getUint16(0, !1); + reason = buffer.subarray(2); + } + return await this.close(code, reason) + .then(({ closeCode, reason }) => console.log({ closeCode, reason })); + } else { + return await this.close(1002, "unknown opcode"); } } + async close(code, reason) { + const opcode = this.opcodes.CLOSE; + let buffer, view; + if (code) { + buffer = new Uint8Array(reason.length + 2); + view = new DataView(buffer.buffer); + view.setUint16(0, code, !1); + buffer.set(reason, 2); + } else { + buffer = new Uint8Array(0); + } + // console.log({ opcode, reason, buffer }, new TextDecoder().decode(reason)); + this.incomingStreamController.close(); + await this.writer.write(this.encodeMessage(opcode, buffer)) + .catch(console.log); + await this.writer.close(); + await this.writer.closed; + await Promise.allSettled([ + this.readable.cancel(), + ]).catch(console.log); + this.buffer.resize(0); + this.closed = !0; + const closeCodes = { + closeCode: view.getUint16(0, !1), + reason: new TextDecoder().decode(reason), + }; + if (closeCodes.closeCode === 1000) { + console.log(closeCodes); + } + return closeCodes; + } unmask(maskBytes2, data) { let payload = new Uint8Array(data.length); for (let i = 0; i < data.length; i++) { @@ -159,81 +190,71 @@ class WebSocketConnection { return payload; } encodeMessage(opcode, payload) { - let buf, b1 = 128 | opcode, b2 = 0, length = payload.length; + // https://codereview.stackexchange.com/a/297758/47730 + let buffer, b1 = 128 | opcode, b2 = 0, length = payload.length, index; + const extra = [2, 4, 10]; if (length < 126) { - buf = new Uint8Array(payload.length + 2 + 0); - const view = new DataView(buf.buffer); + index = 0; b2 |= length; - view.setUint8(0, b1); - view.setUint8(1, b2); - buf.set(payload, 2); } else if (length < 65536) { - buf = new Uint8Array(payload.length + 2 + 2); - const view = new DataView(buf.buffer); + index = 1; b2 |= 126; - view.setUint8(0, b1); - view.setUint8(1, b2); - view.setUint16(2, length); - buf.set(payload, 4); } else { - buf = new Uint8Array(payload.length + 2 + 8); - const view = new DataView(buf.buffer); + index = 2; b2 |= 127; - view.setUint8(0, b1); - view.setUint8(1, b2); - view.setUint32(2, 0, !1); - view.setUint32(6, length, !1); - buf.set(payload, 10); } - return buf; + buffer = new Uint8Array(payload.length + extra[index]); + const view = new DataView(buffer.buffer); + view.setUint8(0, b1); + view.setUint8(1, b2); + if (length >= 126 && length < 65536) { + view.setUint16(2, length); + } else if (length >= 65536) { + view.setUint32(2, 0, false); + view.setUint32(6, length, false); + } + buffer.set(payload, extra[index]); + return buffer; } static KEY_SUFFIX = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + // https://codereview.stackexchange.com/a/297758/47730 static async hashWebSocketKey(secKeyWebSocket, writable) { // Use Web Cryptography API crypto.subtle where defined - if (globalThis.crypto.subtle) { - const encoder = new TextEncoder(), - key = btoa( - [ - ...new Uint8Array( - await crypto.subtle.digest( - "SHA-1", - encoder.encode( - `${secKeyWebSocket}${WebSocketConnection.KEY_SUFFIX}`, - ), + console.log(secKeyWebSocket, globalThis?.crypto?.subtle); + const encoder = new TextEncoder(); + let key; + if (globalThis?.crypto?.subtle) { + key = btoa( + [ + ...new Uint8Array( + await crypto.subtle.digest( + "SHA-1", + encoder.encode( + `${secKeyWebSocket}${WebSocketConnection.KEY_SUFFIX}`, ), ), - ].map((s) => String.fromCodePoint(s)).join(""), - ); - const header = `HTTP/1.1 101 Web Socket Protocol Handshake\r -Upgrade: WebSocket\r -Connection: Upgrade\r -sec-websocket-accept: ` + key + `\r -\r -`; - return writable instanceof WritableStream - ? (new Response(header)).body.pipeTo(writable, { preventClose: !0 }) - : writable.write(encoder.encode(header)); + ), + ].map((s) => String.fromCodePoint(s)).join(""), + ); } else { // txiki.js does not support Web Cryptography API crypto.subtle - // Use txiki.js specific tjs:hashing or + // Use txiki.js specific tjs:hashing or // https://raw.githubusercontent.com/kawanet/sha1-uint8array/main/lib/sha1-uint8array.ts - const { createHash } = await import("tjs:hashing"); - const encoder = new TextEncoder(); + const { createHash } = await import("./sha1-uint8array.min.js"); const hash = createHash("sha1").update( `${secKeyWebSocket}${WebSocketConnection.KEY_SUFFIX}`, - ).bytes(); - const key = btoa( + ).digest(); + key = btoa( String.fromCodePoint(...hash), ); - const header = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + - "Upgrade: WebSocket\r\n" + - "Connection: Upgrade\r\n" + - "sec-websocket-accept: " + key + "\r\n\r\n"; - const encoded = encoder.encode(header); - return writable instanceof WritableStream - ? new Response(encode).body.pipeTo(writable, { preventClose: !0 }) - : writable.write(encoded); } + const header = "HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + + "Upgrade: WebSocket\r\n" + + "Connection: Upgrade\r\n" + + "Sec-Websocket-Accept: " + key + "\r\n\r\n"; + return writable instanceof WritableStream + ? new Response(header).body.pipeTo(writable, { preventClose: true }) + : writable.write(encoder.encode(header)); } } From 3bc1d5a736bb37399f17640aa2d39eae88cb9ab9 Mon Sep 17 00:00:00 2001 From: guest271314 Date: Sat, 2 Aug 2025 20:36:40 +0000 Subject: [PATCH 5/6] Use tjs:hashing --- src/js/polyfills/ws-server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/polyfills/ws-server.js b/src/js/polyfills/ws-server.js index edf3f2f3..01dd2a6a 100644 --- a/src/js/polyfills/ws-server.js +++ b/src/js/polyfills/ws-server.js @@ -240,10 +240,10 @@ class WebSocketConnection { // txiki.js does not support Web Cryptography API crypto.subtle // Use txiki.js specific tjs:hashing or // https://raw.githubusercontent.com/kawanet/sha1-uint8array/main/lib/sha1-uint8array.ts - const { createHash } = await import("./sha1-uint8array.min.js"); + const { createHash } = await import("tjs:hashing"); const hash = createHash("sha1").update( `${secKeyWebSocket}${WebSocketConnection.KEY_SUFFIX}`, - ).digest(); + ).bytes(); key = btoa( String.fromCodePoint(...hash), ); From c3ec4009208bd595b2553270bf799553904faf07 Mon Sep 17 00:00:00 2001 From: guest271314 Date: Sat, 2 Aug 2025 21:08:51 +0000 Subject: [PATCH 6/6] Update ws-server.js --- src/js/polyfills/ws-server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/polyfills/ws-server.js b/src/js/polyfills/ws-server.js index 01dd2a6a..efaa0da3 100644 --- a/src/js/polyfills/ws-server.js +++ b/src/js/polyfills/ws-server.js @@ -122,7 +122,7 @@ class WebSocketConnection { await this.writer.ready; if (opcode === this.opcodes.TEXT) { return await this.writer.write(this.encodeMessage(opcode, buffer)) - .catch(console.lconsole.logog); + .catch(console.log); } if (opcode === this.opcodes.BINARY) { return await this.writer.write(this.encodeMessage(opcode, buffer))