这是indexloc提供的服务,不要输入任何密码
Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
261 changes: 261 additions & 0 deletions src/js/polyfills/ws-server.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,261 @@
// 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;

Check failure on line 12 in src/js/polyfills/ws-server.js

View workflow job for this annotation

GitHub Actions / Lint

Expected indentation of 4 spaces but found 2
writable;

Check failure on line 13 in src/js/polyfills/ws-server.js

View workflow job for this annotation

GitHub Actions / Lint

Expected indentation of 4 spaces but found 2
writer;

Check failure on line 14 in src/js/polyfills/ws-server.js

View workflow job for this annotation

GitHub Actions / Lint

Expected indentation of 4 spaces but found 2
incomingStream = new ReadableStream({

Check failure on line 15 in src/js/polyfills/ws-server.js

View workflow job for this annotation

GitHub Actions / Lint

Expected indentation of 4 spaces but found 2
start: (_) => {

Check failure on line 16 in src/js/polyfills/ws-server.js

View workflow job for this annotation

GitHub Actions / Lint

Unexpected block statement surrounding arrow body; move the returned value immediately after the `=>`

Check failure on line 16 in src/js/polyfills/ws-server.js

View workflow job for this annotation

GitHub Actions / Lint

Unexpected parentheses around single function argument

Check failure on line 16 in src/js/polyfills/ws-server.js

View workflow job for this annotation

GitHub Actions / Lint

Expected indentation of 8 spaces but found 4
return this.incomingStreamController = _;

Check failure on line 17 in src/js/polyfills/ws-server.js

View workflow job for this annotation

GitHub Actions / Lint

Expected indentation of 12 spaces but found 6
},

Check failure on line 18 in src/js/polyfills/ws-server.js

View workflow job for this annotation

GitHub Actions / Lint

Expected indentation of 8 spaces but found 4
});

Check failure on line 19 in src/js/polyfills/ws-server.js

View workflow job for this annotation

GitHub Actions / Lint

Expected indentation of 4 spaces but found 2
buffer = new ArrayBuffer(0, { maxByteLength: 1024 ** 2 });
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();
}
}
async processWebSocketStream() {
try {
for await (const frame of this.readable) {
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;
}
}
console.log("WebSocket connection closed.");
} catch (e) {
console.log(navigator.userAgent, e);
console.trace();
}
}
async processFrame() {
let length, maskBytes;
const buffer = new Uint8Array(this.buffer),
view = new DataView(buffer.buffer);
if (buffer.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 (buffer.length < 8) {
return !1;
}
if (length == 126) {
length = view.getUint16(2, !1);
idx += 2;
} else if (length == 127) {
if (view.getUint32(2, !1) != 0) {
await this.close(1009, "");
return this.opcodes.CLOSE;
}
length = view.getUint32(6, !1);
idx += 8;
}
}
if (buffer.length < idx + 4 + length) {
return !1;
}
maskBytes = buffer.subarray(idx, idx + 4);
idx += 4;
let payload = buffer.subarray(idx, idx + length);
payload = this.unmask(maskBytes, payload);
this.incomingStreamController.enqueue({ opcode, payload });
if (this.buffer.byteLength === 0 && this.closed) {
return !0;
}
if (idx + length === 0) {
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 opcode === this.opcodes.CLOSE ? opcode : !0;
}
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.log);
}
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;
*/
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++) {
payload[i] = maskBytes2[i % 4] ^ data[i];
}
return payload;
}
encodeMessage(opcode, payload) {
// 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) {
index = 0;
b2 |= length;
} else if (length < 65536) {
index = 1;
b2 |= 126;
} else {
index = 2;
b2 |= 127;
}
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
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(""),
);
} 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 hash = createHash("sha1").update(
`${secKeyWebSocket}${WebSocketConnection.KEY_SUFFIX}`,
).bytes();
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";
return writable instanceof WritableStream
? new Response(header).body.pipeTo(writable, { preventClose: true })
: writable.write(encoder.encode(header));
}
}

export { WebSocketConnection };
Loading