Last active
May 21, 2025 18:28
-
-
Save guest271314/d330c7cea513f12ef7bf523c56431453 to your computer and use it in GitHub Desktop.
JavaScript runtime agnostic WebSocket server
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// deno bundle https://raw.githubusercontent.com/kawanet/sha1-uint8array/main/lib/sha1-uint8array.ts sha1-uint8array-bundle.js | |
// bun build --minify sha1-uint8array-bundle.js --outfile=sha1-uint8array.min.js | |
var z=function(t){if(t&&!w[t]&&!w[t.toLowerCase()])throw new Error("Digest method not supported");return new E},p=function(t,e,i,s){if(t===0)return e&i|~e&s;if(t===2)return e&i|e&s|i&s;return e^i^s},B=function(){return new Uint8Array(new Uint16Array([65279]).buffer)[0]===254},y=[1518500249|0,1859775393|0,2400959708|0,3395469782|0],w={sha1:1};class E{A=1732584193|0;B=4023233417|0;C=2562383102|0;D=271733878|0;E=3285377520|0;_byte;_word;_size=0;_sp=0;constructor(){if(!u||_>=8000)u=new ArrayBuffer(8000),_=0;this._byte=new Uint8Array(u,_,80),this._word=new Int32Array(u,_,20),_+=80}update(t){if(typeof t==="string")return this._utf8(t);if(t==null)throw new TypeError("Invalid type: "+typeof t);const{byteOffset:e,byteLength:i}=t;let s=i/64|0,r=0;if(s&&!(e&3)&&!(this._size%64)){const h=new Int32Array(t.buffer,e,s*16);while(s--)this._int32(h,r>>2),r+=64;this._size+=r}if(t.BYTES_PER_ELEMENT!==1&&t.buffer){const h=new Uint8Array(t.buffer,e+r,i-r);return this._uint8(h)}if(r===i)return this;return this._uint8(t,r)}_uint8(t,e){const{_byte:i,_word:s}=this,r=t.length;e=e|0;while(e<r){const f=this._size%64;let h=f;while(e<r&&h<64)i[h++]=t[e++];if(h>=64)this._int32(s);this._size+=h-f}return this}_utf8(t){const{_byte:e,_word:i}=this,s=t.length;let r=this._sp;for(let f=0;f<s;){const h=this._size%64;let n=h;while(f<s&&n<64){let o=t.charCodeAt(f++)|0;if(o<128)e[n++]=o;else if(o<2048)e[n++]=192|o>>>6,e[n++]=128|o&63;else if(o<55296||o>57343)e[n++]=224|o>>>12,e[n++]=128|o>>>6&63,e[n++]=128|o&63;else if(r)o=((r&1023)<<10)+(o&1023)+65536,e[n++]=240|o>>>18,e[n++]=128|o>>>12&63,e[n++]=128|o>>>6&63,e[n++]=128|o&63,r=0;else r=o}if(n>=64)this._int32(i),i[0]=i[16];this._size+=n-h}return this._sp=r,this}_int32(t,e){let{A:i,B:s,C:r,D:f,E:h}=this,n=0;e=e|0;while(n<16)c[n++]=x(t[e++]);for(n=16;n<80;n++)c[n]=a(c[n-3]^c[n-8]^c[n-14]^c[n-16]);for(n=0;n<80;n++){const o=n/20|0,b=A(i)+p(o,s,r,f)+h+c[n]+y[o]|0;h=f,f=r,r=g(s),s=i,i=b}this.A=i+this.A|0,this.B=s+this.B|0,this.C=r+this.C|0,this.D=f+this.D|0,this.E=h+this.E|0}digest(t){const{_byte:e,_word:i}=this;let s=this._size%64|0;e[s++]=128;while(s&3)e[s++]=0;if(s>>=2,s>14){while(s<16)i[s++]=0;s=0,this._int32(i)}while(s<16)i[s++]=0;const r=this._size*8,f=(r&4294967295)>>>0,h=(r-f)/4294967296;if(h)i[14]=x(h);if(f)i[15]=x(f);return this._int32(i),t==="hex"?this._hex():this._bin()}_hex(){const{A:t,B:e,C:i,D:s,E:r}=this;return l(t)+l(e)+l(i)+l(s)+l(r)}_bin(){const{A:t,B:e,C:i,D:s,E:r,_byte:f,_word:h}=this;return h[0]=x(t),h[1]=x(e),h[2]=x(i),h[3]=x(s),h[4]=x(r),f.slice(0,20)}}var c=new Int32Array(80),u,_=0,l=(t)=>(t+4294967296).toString(16).substr(-8),d=(t)=>t<<24&4278190080|t<<8&16711680|t>>8&65280|t>>24&255,F=(t)=>t,x=B()?F:d,a=(t)=>t<<1|t>>>31,A=(t)=>t<<5|t>>>27,g=(t)=>t<<30|t>>>2;export{z as createHash}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
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) { | |
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); | |
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) { | |
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); | |
await this.handleFrame(opcode, payload); | |
if (idx + length === 0) { | |
console.log(`this.buffer.length: ${this.buffer.byteLength}.`); | |
return !1; | |
} | |
const data = buf.subarray(idx + length); | |
for (let i = 0; i < this.buffer.byteLength; i++) { | |
view.setUint8(i, data.at(i)); | |
} | |
this.buffer.resize(data.length); | |
return !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; | |
case this.opcodes.PONG: | |
break; | |
case this.opcodes.CLOSE: | |
let code, reason; | |
if (buffer.length >= 2) { | |
code = view.getUint16(0, !1); | |
reason = buffer.subarray(2); | |
} | |
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 (let 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 | |
console.log(secKeyWebSocket, globalThis?.crypto?.subtle); | |
const encoder = new TextEncoder(); | |
if (globalThis?.crypto?.subtle) { | |
const 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("./sha1-uint8array.min.js"); | |
const encoder = new TextEncoder(); | |
const hash = createHash("sha1").update( | |
`${secKeyWebSocket}${WebSocketConnection.KEY_SUFFIX}`, | |
).digest(); | |
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 }; |
txiki.js
import { WebSocketConnection } from "./websocket-server.js";
const decoder = new TextDecoder();
async function handleConnection(conn) {
const writer = conn.writable.getWriter();
const { readable: wsReadable, writable: wsWritable } = new TransformStream({}, {}, {
highWaterMark: 1
}),
wsWriter = wsWritable.getWriter();
let ws;
for await (const value of conn.readable) {
const request = decoder.decode(value);
if (/upgrade: websocket/i.test(request)) {
const [key] = request.match(/(?<=Sec-WebSocket-Key: ).+/i);
const handshake = await WebSocketConnection.hashWebSocketKey(
key,
writer,
);
ws = new WebSocketConnection(wsReadable, writer)
.processWebSocketStream().catch((e) => {
throw e;
});
} else {
await wsWriter.ready;
await wsWriter.write(new Uint8Array(value));
}
}
console.log("WebSocket client connection closed");
await wsWriter.close();
}
const listener = await tjs.listen("tcp", "0.0.0.0", "44818");
const { family, ip, port } = listener.localAddress;
console.log(
`${navigator.userAgent} WebSocket server listening on family: ${family}, ip: ${ip}, port: ${port}`,
);
for await (const conn of listener) {
try {
console.log({ conn });
handleConnection(conn).catch((e) => {apps
console.log({ e });
});
} catch (e) {
listener.close();
console.log(e);
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Node.js