-
-
Save guest271314/735377527389f1de6145f0ac71ca1e86 to your computer and use it in GitHub Desktop.
WebSocket - binary broadcast example (JavaScript runtime agnostic implementation without any dependency)
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
// 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