Skip to content

Instantly share code, notes, and snippets.

@guest271314
Forked from robertrypula/web-socket-server.js
Last active March 4, 2025 08:42
Show Gist options
  • Save guest271314/735377527389f1de6145f0ac71ca1e86 to your computer and use it in GitHub Desktop.
Save guest271314/735377527389f1de6145f0ac71ca1e86 to your computer and use it in GitHub Desktop.
WebSocket - binary broadcast example (JavaScript runtime agnostic implementation without any dependency)
// 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};
// Copyright (c) 2019-2021 Robert Rypuła - https://github.com/robertrypula
// Fork of https://gist.github.com/robertrypula/b813ffe23a9489bae1b677f1608676c8
// guest271314 2024
// Do What the Fuck You Want to Public License WTFPLv2 http://www.wtfpl.net/about/
// https://raw.githubusercontent.com/kawanet/sha1-uint8array/main/lib/sha1-uint8array.ts
import { createHash } from "./sha1-uint8array.min.js";
const debugBuffer = (bufferName, buffer) => {
const length = buffer ? buffer.length : "---";
console.log(`:: DEBUG - ${bufferName} | ${length} | `, buffer, "\n");
};
/*
https://tools.ietf.org/html/rfc6455#section-5.2
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-------------------------------+
| |Masking-key, if MASK set to 1 |
+-------------------------------+-------------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+---------------------------------------------------------------+
OpCode
%x0 denotes a continuation frame
%x1 denotes a text frame
%x2 denotes a binary frame
%x3–7 are reserved for further non-control frames
%x8 denotes a connection close
%x9 denotes a ping
%xA denotes a pong
%xB-F are reserved for further control frames
*/
// ---------------------------------------------------------
const createWebSocketFrame = (payload) => {
const payloadLengthByteCount = payload.length < 126 ? 0 : 2;
const buffer = new ArrayBuffer(2 + payloadLengthByteCount + payload.length);
const view = new DataView(buffer);
let payloadOffset = 2;
if (payload.length >= Math.pow(2, 16)) {
throw new Error("Payload equal or bigger than 64 KiB is not supported");
}
view.setUint8(0, 0b10000010);
view.setUint8(1, payload.length < 126 ? payload.length : 126);
if (payloadLengthByteCount > 0) {
view.setUint16(2, payload.length);
payloadOffset += payloadLengthByteCount;
}
for (let i = 0, j = payloadOffset; i < payload.length; i++, j++) {
view.setUint8(j, payload[i]);
}
return buffer;
};
// ---------------------------------------------------------
const getParsedBuffer = (buffer) => {
// console.log({ buffer });
const view = new DataView(buffer.buffer);
let bufferRemainingBytes;
let currentOffset = 0;
let maskingKey;
let payload;
if (currentOffset + 2 > buffer.length) {
return { payload: null, bufferRemainingBytes: buffer };
}
const firstByte = view.getUint8(currentOffset++);
const secondByte = view.getUint8(currentOffset++);
const isFinalFrame = !!((firstByte >>> 7) & 0x1);
const opCode = firstByte & 0xf;
const isMasked = !!((secondByte >>> 7) & 0x1); // https://security.stackexchange.com/questions/113297
let payloadLength = secondByte & 0x7f;
if (!isFinalFrame) {
console.log("[not final frame detected]\n");
}
if (opCode === 0x8) {
console.log("[connection close frame]\n");
// TODO read payload, for example payload equal to <0x03 0xe9> means 1001:
// 1001 indicates that an endpoint is "going away", such as a server
// going down or a browser having navigated away from a page.
// More info here: https://tools.ietf.org/html/rfc6455#section-7.4
// Normal close frame
// https://stackoverflow.com/a/17177146
// new Uint8Array([0x88, 0x00]); // 136, 0
return { payload: null, bufferRemainingBytes: null };
}
if (opCode !== 0x2 && opCode !== 0x0) {
throw new Error("Only binary and continuation frames are supported");
}
if (payloadLength > 125) {
if (payloadLength === 126) {
if (currentOffset + 2 > buffer.length) {
return { payload: null, bufferRemainingBytes: buffer };
}
payloadLength = view.getUint16(currentOffset);
currentOffset += 2;
} else {
throw new Error("Payload equal or bigger than 64 KiB is not supported");
}
}
if (isMasked) {
if (currentOffset + 4 > buffer.length) {
return { payload: null, bufferRemainingBytes: buffer };
}
maskingKey = view.getUint32(currentOffset);
currentOffset += 4;
}
if (currentOffset + payloadLength > buffer.length) {
console.log("[misalignment between WebSocket frame and NodeJs Buffer]\n");
return { payload: null, bufferRemainingBytes: buffer };
}
payload = new Uint8Array(payloadLength);
if (isMasked) {
for (let i = 0, j = 0; i < payloadLength; ++i, j = i % 4) {
const shift = j === 3 ? 0 : (3 - j) << 3;
const mask = (shift === 0 ? maskingKey : maskingKey >>> shift) & 0xff;
payload[i] = mask ^ view.getUint8(currentOffset++);
}
} else {
for (let i = 0; i < payloadLength; i++) {
payload[i] = view.getUint8(currentOffset++);
}
}
bufferRemainingBytes = new Uint8Array(buffer.length - currentOffset);
for (let i = 0; i < bufferRemainingBytes.length; i++) {
bufferRemainingBytes[i] = view.getUint8(currentOffset++);
}
return { payload, bufferRemainingBytes };
};
function parseWebSocketFrame(buffer) {
let bufferToParse = buffer;
let parsedBuffer;
do {
parsedBuffer = getParsedBuffer(bufferToParse);
/*
debugBuffer("buffer", buffer);
debugBuffer("bufferToParse", bufferToParse);
debugBuffer("parsedBuffer.payload", parsedBuffer.payload);
debugBuffer(
"parsedBuffer.bufferRemainingBytes",
parsedBuffer.bufferRemainingBytes,
);
*/
if (parsedBuffer.payload === null) {
return parsedBuffer.payload;
}
bufferToParse = parsedBuffer.bufferRemainingBytes;
if (parsedBuffer.payload) {
// console.log(parsedBuffer);
break;
}
} while (parsedBuffer.payload && parsedBuffer.bufferRemainingBytes.length);
return createWebSocketFrame(parsedBuffer.payload);
}
// https://stackoverflow.com/a/77398427
async function digest(message, algo = "SHA-1") {
const key = `${message}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`;
if ((globalThis?.webcrypto || globalThis.crypto)?.subtle?.digest) {
const bytes = new Uint8Array(
await crypto.subtle.digest(
algo,
new TextEncoder().encode(
`${message}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`,
),
),
);
return btoa(String.fromCodePoint(...bytes));
} else {
// txiki.js doesn't support crypto.subtle
return btoa(
String.fromCodePoint.apply(
null,
createHash().update(new TextEncoder().encode(key)).digest(),
),
);
}
}
// Get Request-Line and Headers
// TODO: Needs work and testing
function getHeaders(r) {
console.log(r);
const header = r.match(/.+/g).map((line) => line.split(/:\s|\s\/\s/));
const [requestLine] = header.shift();
const [method, uri, protocol] = requestLine.split(/\s/);
const headers = new Headers(header);
console.log({ method, uri, protocol, headers });
const url = new URL(uri, `http://${headers.get("host")}`);
const { servertype } = Object.fromEntries(url.searchParams);
return { headers, uri, protocol, servertype };
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment