diff --git a/package.json b/package.json index a7708eff..d761ba18 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,8 @@ "tslint": "^6.1.3", "tslint-config-common": "^1.6.2", "typedoc": "^0.25.12", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "websocket": "^1.0.35" }, "jest": { "verbose": true, diff --git a/src/ws/WsFrameDecoder.ts b/src/ws/WsFrameDecoder.ts new file mode 100644 index 00000000..4b7a76ef --- /dev/null +++ b/src/ws/WsFrameDecoder.ts @@ -0,0 +1,119 @@ +import {StreamingOctetReader} from '@jsonjoy.com/util/lib/buffers/StreamingOctetReader'; +import {WsFrameOpcode} from './constants'; +import {WsFrameDecodingError} from './errors'; +import {WsCloseFrame, WsFrameHeader, WsPingFrame, WsPongFrame} from './frames'; + +export class WsFrameDecoder { + public readonly reader = new StreamingOctetReader(); + + public push(uint8: Uint8Array): void { + this.reader.push(uint8); + } + + public readFrameHeader(): WsFrameHeader | undefined { + try { + const reader = this.reader; + if (reader.size() < 2) return undefined; + const b0 = reader.u8(); + const b1 = reader.u8(); + const fin = <0 | 1>(b0 >>> 7); + const opcode = b0 & 0b1111; + const maskBit = b1 >>> 7; + let length = b1 & 0b01111111; + if (length === 126) { + if (reader.size() < 2) return undefined; + length = (reader.u8() << 8) | reader.u8(); + } else if (length === 127) { + if (reader.size() < 8) return undefined; + reader.skip(4); + length = reader.u32(); + } + let mask: undefined | [number, number, number, number]; + if (maskBit) { + if (reader.size() < 4) return undefined; + mask = [reader.u8(), reader.u8(), reader.u8(), reader.u8()]; + } + if (opcode >= WsFrameOpcode.MIN_CONTROL_OPCODE) { + switch (opcode) { + case WsFrameOpcode.CLOSE: { + return new WsCloseFrame(fin, opcode, length, mask, 0, ''); + } + case WsFrameOpcode.PING: { + if (length > 125) throw new WsFrameDecodingError(); + const data = mask ? reader.bufXor(length, mask, 0) : reader.buf(length); + return new WsPingFrame(fin, opcode, length, mask, data); + } + case WsFrameOpcode.PONG: { + if (length > 125) throw new WsFrameDecodingError(); + const data = mask ? reader.bufXor(length, mask, 0) : reader.buf(length); + return new WsPongFrame(fin, opcode, length, mask, data); + } + default: { + throw new WsFrameDecodingError(); + } + } + } + return new WsFrameHeader(fin, opcode, length, mask); + } catch (err) { + if (err instanceof RangeError) return undefined; + throw err; + } + } + + /** + * Read application data of a frame and copy it to the destination buffer. + * Receives the frame header and the number of bytes that still need to be + * copied, returns back the number of bytes that still need to be copied in + * subsequent calls. + * + * @param frame Frame header. + * @param remaining How many bytes are remaining to be copied. + * @param dst The destination buffer to write to. + * @param pos Position in the destination buffer to start writing to. + * @returns The number of bytes that still need to be copied in the next call. + */ + public readFrameData(frame: WsFrameHeader, remaining: number, dst: Uint8Array, pos: number): number { + const reader = this.reader; + const mask = frame.mask; + const readSize = Math.min(reader.size(), remaining); + if (!mask) reader.copy(readSize, dst, pos); + else { + const alreadyRead = frame.length - remaining; + reader.copyXor(readSize, dst, pos, mask, alreadyRead); + } + return remaining - readSize; + } + + public copyFrameData(frame: WsFrameHeader, dst: Uint8Array, pos: number): void { + const reader = this.reader; + const mask = frame.mask; + const readSize = frame.length; + if (!mask) reader.copy(readSize, dst, pos); + else reader.copyXor(readSize, dst, pos, mask, 0); + } + + /** + * Reads application data of the CLOSE frame and sets the code and reason + * properties of the frame. + * + * @param frame Close frame. + */ + public readCloseFrameData(frame: WsCloseFrame): void { + let length = frame.length; + if (length > 125) throw new WsFrameDecodingError(); + let code = 0; + let reason = ''; + if (length > 0) { + if (length < 2) throw new WsFrameDecodingError(); + const reader = this.reader; + const mask = frame.mask; + const octet1 = reader.u8() ^ (mask ? mask[0] : 0); + const octet2 = reader.u8() ^ (mask ? mask[1] : 0); + code = (octet1 << 8) | octet2; + length -= 2; + if (length) reason = reader.utf8(length, mask ?? [0, 0, 0, 0], 2); + } + frame.code = code; + frame.reason = reason; + } +} diff --git a/src/ws/WsFrameEncoder.ts b/src/ws/WsFrameEncoder.ts new file mode 100644 index 00000000..f05792f9 --- /dev/null +++ b/src/ws/WsFrameEncoder.ts @@ -0,0 +1,126 @@ +import {Writer} from '@jsonjoy.com/util/lib/buffers/Writer'; +import {WsFrameOpcode} from './constants'; +import {WsFrameEncodingError} from './errors'; +import type {IWriter, IWriterGrowable} from '@jsonjoy.com/util/lib/buffers'; + +const maskBuf = new Uint8Array(4); +const maskBufView = new DataView(maskBuf.buffer, maskBuf.byteOffset, maskBuf.byteLength); + +export class WsFrameEncoder { + constructor(public readonly writer: W = new Writer() as any) {} + + public encodePing(data: Uint8Array | null): Uint8Array { + this.writePing(data); + return this.writer.flush(); + } + + public encodePong(data: Uint8Array | null): Uint8Array { + this.writePong(data); + return this.writer.flush(); + } + + public encodeClose(reason: string, code = 0): Uint8Array { + this.writeClose(reason, code); + return this.writer.flush(); + } + + public encodeHdr(fin: 0 | 1, opcode: WsFrameOpcode, length: number, mask: number): Uint8Array { + this.writeHdr(fin, opcode, length, mask); + return this.writer.flush(); + } + + public encodeDataMsgHdrFast(length: number): Uint8Array { + this.writeDataMsgHdrFast(length); + return this.writer.flush(); + } + + public writePing(data: Uint8Array | null): void { + let length = 0; + if (data && (length = data.length)) { + this.writeHdr(1, WsFrameOpcode.PING, length, 0); + this.writer.buf(data, length); + } else { + this.writeHdr(1, WsFrameOpcode.PING, 0, 0); + } + } + + public writePong(data: Uint8Array | null): void { + let length = 0; + if (data && (length = data.length)) { + this.writeHdr(1, WsFrameOpcode.PONG, length, 0); + this.writer.buf(data, length); + } else { + this.writeHdr(1, WsFrameOpcode.PONG, 0, 0); + } + } + + public writeClose(reason: string, code = 0): void { + if (reason || code) { + const reasonLength = reason.length; + const length = 2 + reasonLength; + const writer = this.writer; + writer.ensureCapacity( + 2 + // Frame header + 2 + // Close code 2 bytes + reasonLength * 4, // Close reason, max 4 bytes per UTF-8 char + ); + const lengthX = writer.x + 1; + this.writeHdr(1, WsFrameOpcode.CLOSE, length, 0); + writer.u16(code); + if (reasonLength) { + const utf8Length = writer.utf8(reason); + if (utf8Length !== reasonLength) { + if (utf8Length > 126 - 2) throw new WsFrameEncodingError(); + writer.uint8[lengthX] = (writer.uint8[lengthX] & 0b10000000) | (utf8Length + 2); + } + } + } else { + this.writeHdr(1, WsFrameOpcode.CLOSE, 0, 0); + } + } + + public writeHdr(fin: 0 | 1, opcode: WsFrameOpcode, length: number, mask: number): void { + const octet1 = (fin << 7) | opcode; + const maskBit = mask ? 0b10000000 : 0b00000000; + const writer = this.writer; + if (length < 126) { + const octet2 = maskBit | length; + writer.u16((octet1 << 8) | octet2); + } else if (length < 0x10000) { + const octet2 = maskBit | 126; + writer.u32(((octet1 << 8) | octet2) * 0x10000 + length); + } else { + const octet2 = maskBit | 127; + writer.u16((octet1 << 8) | octet2); + writer.u32(0); + writer.u32(length); + } + if (mask) writer.u32(mask); + } + + public writeDataMsgHdrFast(length: number): void { + const writer = this.writer; + if (length < 126) { + writer.u16(0b10000010_00000000 + length); + return; + } + if (length < 0x10000) { + writer.u32(0b10000010_01111110_00000000_00000000 + length); + return; + } + writer.u16(0b10000010_01111111); + writer.u32(0); + writer.u32(length); + } + + public writeBufXor(buf: Uint8Array, mask: number): void { + maskBufView.setUint32(0, mask, false); + const writer = this.writer; + const length = buf.length; + writer.ensureCapacity(length); + let x = writer.x; + const uint8 = writer.uint8; + for (let i = 0; i < length; i++) uint8[x++] = buf[i] ^ maskBuf[i & 3]; + writer.x = x; + } +} diff --git a/src/ws/__tests__/decoder.spec.ts b/src/ws/__tests__/decoder.spec.ts new file mode 100644 index 00000000..d96ab607 --- /dev/null +++ b/src/ws/__tests__/decoder.spec.ts @@ -0,0 +1,362 @@ +import {WsFrameDecoder} from '../WsFrameDecoder'; +import {WsFrameOpcode} from '../constants'; +import {WsCloseFrame, WsFrameHeader, WsPingFrame, WsPongFrame} from '../frames'; + +const {frame: WebSocketFrame} = require('websocket'); + +describe('data frames', () => { + test('can read final text packet with mask', () => { + const buf = Buffer.from( + new Uint8Array([ + 129, + 136, // Header + 136, + 35, + 93, + 205, // Mask + 231, + 85, + 56, + 191, + 177, + 19, + 109, + 253, // Payload + ]), + ); + const decoder = new WsFrameDecoder(); + decoder.push(buf); + const frame = decoder.readFrameHeader()!; + const dst = Buffer.alloc(frame.length); + let remaining = frame.length; + remaining = decoder.readFrameData(frame, remaining, dst, 0); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(1); + expect(frame.length).toBe(8); + expect(frame.mask).toEqual([136, 35, 93, 205]); + expect(dst.toString()).toBe('over9000'); + }); + + test('can decode multiple chunks', () => { + const decoder = new WsFrameDecoder(); + const chunks: string[] = [ + 'gpbMadbAlzLn7P1F9LW4ALruvAC4p+5Frb2RNA==', + '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y1VBGNZFB1WTC1xRkwkVA9YcD3zWPEMNy1VGEdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VGENZFB1WTC1xRkwkVA9YcD3zWPEMNy1VGE9ZFB1WTC1xRkwkVA9YcD3zWPEMNy1VGEtZFB1WTC1xRkwkVA9YcD3zWPEMNy1VGFdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VGFNZFB1WTC1xRkwkVA9YcD3zWPEMNy1VGF9ZFB1WTC1xRkwkVA9YcD3zWPEMNy1VGFtZFB1WTC1xRkwkVA9YcD3zWPEMNy1VGGdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VGGNZFB1WTC1xRkwkVA9YcD3zWPEMNy1VHEdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VHENZFB1WTC1xRkwkVA9YcD3zWPEMNy1VHE9ZFB1WTC1xRkwkVA9YcD3zWPEMNy1VHEtZFB1WTC1xRkwkVA9YcD3zWPEMNy1VHFdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VHFNZFB1WTC1xRkwkVA9YcD3zWPEMNy1VHF9ZFB1WTC1xRkwkVA9YcD3zWPEMNy1VHFtZFB1WTC1xRkwkVA9YcD3zWPEMNy1VHGdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VHGNZFB1WTC1xRkwkVA9YcD3zWPEMNy1VEEdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VEENZFB1WTC1xRkwkVA9YcD3zWPEMNy1VEE9ZFB1WTC1xRkwkVA9YcD3zWPEMNy1VEEtZFB1WTC1xRkwkVA9YcD3zWPEMNy1VEFdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VEFNZFB1WTC1xRkwkVA9YcD3zWPEMNy1VEF9ZFB1WTC1xRkwkVA9YcD3zWPEMNy1VEFtZFB1WTC1xRkwkVA9YcD3zWPEMNy1VEGdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VEGNZFB1WTC1xRkwkVA9YcD3zWPEMNy1VFEdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VFENZFB1WTC1xRkwkVA9YcD3zWPEMNy1VFE9ZFB1WTC1xRkwkVA9YcD3zWPEMNy1VFEtZFB1WTC1xRkwkVA9YcD3zWPEMNy1VFFdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VFFNZFB1WTC1xRkwkVA9YcD3zWPEMNy1VFF9ZFB1WTC1xRkwkVA9YcD3zWPEMNy1VFFtZFB1WTC1xRkwkVA9YcD3zWPEMNy1VFGdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VFGNZFB1WTC1xRkwkVA9YcD3zWPEMNy1VKEdZFB1WTC1xRkwkVA9YcD3zWPEMNy1VKENZFB1WTC1xRkwkVA9YcD3yn', + ]; + for (const chunk of chunks) { + const buf = Buffer.from(chunk, 'base64'); + decoder.push(buf); + } + const frames: WsFrameHeader[] = []; + const payloads: Uint8Array[] = []; + let currentFrame: WsFrameHeader | undefined; + while (true) { + if (currentFrame) { + const length = currentFrame.length; + if (length <= decoder.reader.size()) { + const buf = new Uint8Array(length); + decoder.copyFrameData(currentFrame, buf, 0); + payloads.push(buf); + currentFrame = undefined; + } else break; + } + const frame = decoder.readFrameHeader(); + if (!frame) break; + else if (frame instanceof WsFrameHeader) { + frames.push(frame); + if (frame.length) currentFrame = frame; + } + } + expect(frames.length).toBe(5); + expect(frames[0].fin).toBe(1); + expect(frames[1].fin).toBe(1); + expect(frames[2].fin).toBe(1); + expect(frames[3].fin).toBe(1); + expect(frames[4].fin).toBe(1); + expect(frames[0].opcode).toBe(2); + expect(frames[1].opcode).toBe(2); + expect(frames[2].opcode).toBe(2); + expect(frames[3].opcode).toBe(2); + expect(frames[4].opcode).toBe(2); + expect(frames[0].length).toBe(22); + expect(frames[1].length).toBe(2195); + expect(frames[2].length).toBe(2301); + expect(frames[3].length).toBe(2301); + expect(frames[4].length).toBe(1921); + expect(Buffer.from(payloads[0]).toString()).toBe('[[1,1,"util.ping",{}]]'); + expect(Buffer.from(payloads[1]).toString()).toBe( + '[[1,2,"util.ping",{}],[1,3,"util.ping",{}],[1,4,"util.ping",{}],[1,5,"util.ping",{}],[1,6,"util.ping",{}],[1,7,"util.ping",{}],[1,8,"util.ping",{}],[1,9,"util.ping",{}],[1,10,"util.ping",{}],[1,11,"util.ping",{}],[1,12,"util.ping",{}],[1,13,"util.ping",{}],[1,14,"util.ping",{}],[1,15,"util.ping",{}],[1,16,"util.ping",{}],[1,17,"util.ping",{}],[1,18,"util.ping",{}],[1,19,"util.ping",{}],[1,20,"util.ping",{}],[1,21,"util.ping",{}],[1,22,"util.ping",{}],[1,23,"util.ping",{}],[1,24,"util.ping",{}],[1,25,"util.ping",{}],[1,26,"util.ping",{}],[1,27,"util.ping",{}],[1,28,"util.ping",{}],[1,29,"util.ping",{}],[1,30,"util.ping",{}],[1,31,"util.ping",{}],[1,32,"util.ping",{}],[1,33,"util.ping",{}],[1,34,"util.ping",{}],[1,35,"util.ping",{}],[1,36,"util.ping",{}],[1,37,"util.ping",{}],[1,38,"util.ping",{}],[1,39,"util.ping",{}],[1,40,"util.ping",{}],[1,41,"util.ping",{}],[1,42,"util.ping",{}],[1,43,"util.ping",{}],[1,44,"util.ping",{}],[1,45,"util.ping",{}],[1,46,"util.ping",{}],[1,47,"util.ping",{}],[1,48,"util.ping",{}],[1,49,"util.ping",{}],[1,50,"util.ping",{}],[1,51,"util.ping",{}],[1,52,"util.ping",{}],[1,53,"util.ping",{}],[1,54,"util.ping",{}],[1,55,"util.ping",{}],[1,56,"util.ping",{}],[1,57,"util.ping",{}],[1,58,"util.ping",{}],[1,59,"util.ping",{}],[1,60,"util.ping",{}],[1,61,"util.ping",{}],[1,62,"util.ping",{}],[1,63,"util.ping",{}],[1,64,"util.ping",{}],[1,65,"util.ping",{}],[1,66,"util.ping",{}],[1,67,"util.ping",{}],[1,68,"util.ping",{}],[1,69,"util.ping",{}],[1,70,"util.ping",{}],[1,71,"util.ping",{}],[1,72,"util.ping",{}],[1,73,"util.ping",{}],[1,74,"util.ping",{}],[1,75,"util.ping",{}],[1,76,"util.ping",{}],[1,77,"util.ping",{}],[1,78,"util.ping",{}],[1,79,"util.ping",{}],[1,80,"util.ping",{}],[1,81,"util.ping",{}],[1,82,"util.ping",{}],[1,83,"util.ping",{}],[1,84,"util.ping",{}],[1,85,"util.ping",{}],[1,86,"util.ping",{}],[1,87,"util.ping",{}],[1,88,"util.ping",{}],[1,89,"util.ping",{}],[1,90,"util.ping",{}],[1,91,"util.ping",{}],[1,92,"util.ping",{}],[1,93,"util.ping",{}],[1,94,"util.ping",{}],[1,95,"util.ping",{}],[1,96,"util.ping",{}],[1,97,"util.ping",{}],[1,98,"util.ping",{}],[1,99,"util.ping",{}],[1,100,"util.ping",{}],[1,101,"util.ping",{}]]', + ); + expect(Buffer.from(payloads[2]).toString()).toBe( + '[[1,102,"util.ping",{}],[1,103,"util.ping",{}],[1,104,"util.ping",{}],[1,105,"util.ping",{}],[1,106,"util.ping",{}],[1,107,"util.ping",{}],[1,108,"util.ping",{}],[1,109,"util.ping",{}],[1,110,"util.ping",{}],[1,111,"util.ping",{}],[1,112,"util.ping",{}],[1,113,"util.ping",{}],[1,114,"util.ping",{}],[1,115,"util.ping",{}],[1,116,"util.ping",{}],[1,117,"util.ping",{}],[1,118,"util.ping",{}],[1,119,"util.ping",{}],[1,120,"util.ping",{}],[1,121,"util.ping",{}],[1,122,"util.ping",{}],[1,123,"util.ping",{}],[1,124,"util.ping",{}],[1,125,"util.ping",{}],[1,126,"util.ping",{}],[1,127,"util.ping",{}],[1,128,"util.ping",{}],[1,129,"util.ping",{}],[1,130,"util.ping",{}],[1,131,"util.ping",{}],[1,132,"util.ping",{}],[1,133,"util.ping",{}],[1,134,"util.ping",{}],[1,135,"util.ping",{}],[1,136,"util.ping",{}],[1,137,"util.ping",{}],[1,138,"util.ping",{}],[1,139,"util.ping",{}],[1,140,"util.ping",{}],[1,141,"util.ping",{}],[1,142,"util.ping",{}],[1,143,"util.ping",{}],[1,144,"util.ping",{}],[1,145,"util.ping",{}],[1,146,"util.ping",{}],[1,147,"util.ping",{}],[1,148,"util.ping",{}],[1,149,"util.ping",{}],[1,150,"util.ping",{}],[1,151,"util.ping",{}],[1,152,"util.ping",{}],[1,153,"util.ping",{}],[1,154,"util.ping",{}],[1,155,"util.ping",{}],[1,156,"util.ping",{}],[1,157,"util.ping",{}],[1,158,"util.ping",{}],[1,159,"util.ping",{}],[1,160,"util.ping",{}],[1,161,"util.ping",{}],[1,162,"util.ping",{}],[1,163,"util.ping",{}],[1,164,"util.ping",{}],[1,165,"util.ping",{}],[1,166,"util.ping",{}],[1,167,"util.ping",{}],[1,168,"util.ping",{}],[1,169,"util.ping",{}],[1,170,"util.ping",{}],[1,171,"util.ping",{}],[1,172,"util.ping",{}],[1,173,"util.ping",{}],[1,174,"util.ping",{}],[1,175,"util.ping",{}],[1,176,"util.ping",{}],[1,177,"util.ping",{}],[1,178,"util.ping",{}],[1,179,"util.ping",{}],[1,180,"util.ping",{}],[1,181,"util.ping",{}],[1,182,"util.ping",{}],[1,183,"util.ping",{}],[1,184,"util.ping",{}],[1,185,"util.ping",{}],[1,186,"util.ping",{}],[1,187,"util.ping",{}],[1,188,"util.ping",{}],[1,189,"util.ping",{}],[1,190,"util.ping",{}],[1,191,"util.ping",{}],[1,192,"util.ping",{}],[1,193,"util.ping",{}],[1,194,"util.ping",{}],[1,195,"util.ping",{}],[1,196,"util.ping",{}],[1,197,"util.ping",{}],[1,198,"util.ping",{}],[1,199,"util.ping",{}],[1,200,"util.ping",{}],[1,201,"util.ping",{}]]', + ); + expect(Buffer.from(payloads[3]).toString()).toBe( + '[[1,202,"util.ping",{}],[1,203,"util.ping",{}],[1,204,"util.ping",{}],[1,205,"util.ping",{}],[1,206,"util.ping",{}],[1,207,"util.ping",{}],[1,208,"util.ping",{}],[1,209,"util.ping",{}],[1,210,"util.ping",{}],[1,211,"util.ping",{}],[1,212,"util.ping",{}],[1,213,"util.ping",{}],[1,214,"util.ping",{}],[1,215,"util.ping",{}],[1,216,"util.ping",{}],[1,217,"util.ping",{}],[1,218,"util.ping",{}],[1,219,"util.ping",{}],[1,220,"util.ping",{}],[1,221,"util.ping",{}],[1,222,"util.ping",{}],[1,223,"util.ping",{}],[1,224,"util.ping",{}],[1,225,"util.ping",{}],[1,226,"util.ping",{}],[1,227,"util.ping",{}],[1,228,"util.ping",{}],[1,229,"util.ping",{}],[1,230,"util.ping",{}],[1,231,"util.ping",{}],[1,232,"util.ping",{}],[1,233,"util.ping",{}],[1,234,"util.ping",{}],[1,235,"util.ping",{}],[1,236,"util.ping",{}],[1,237,"util.ping",{}],[1,238,"util.ping",{}],[1,239,"util.ping",{}],[1,240,"util.ping",{}],[1,241,"util.ping",{}],[1,242,"util.ping",{}],[1,243,"util.ping",{}],[1,244,"util.ping",{}],[1,245,"util.ping",{}],[1,246,"util.ping",{}],[1,247,"util.ping",{}],[1,248,"util.ping",{}],[1,249,"util.ping",{}],[1,250,"util.ping",{}],[1,251,"util.ping",{}],[1,252,"util.ping",{}],[1,253,"util.ping",{}],[1,254,"util.ping",{}],[1,255,"util.ping",{}],[1,256,"util.ping",{}],[1,257,"util.ping",{}],[1,258,"util.ping",{}],[1,259,"util.ping",{}],[1,260,"util.ping",{}],[1,261,"util.ping",{}],[1,262,"util.ping",{}],[1,263,"util.ping",{}],[1,264,"util.ping",{}],[1,265,"util.ping",{}],[1,266,"util.ping",{}],[1,267,"util.ping",{}],[1,268,"util.ping",{}],[1,269,"util.ping",{}],[1,270,"util.ping",{}],[1,271,"util.ping",{}],[1,272,"util.ping",{}],[1,273,"util.ping",{}],[1,274,"util.ping",{}],[1,275,"util.ping",{}],[1,276,"util.ping",{}],[1,277,"util.ping",{}],[1,278,"util.ping",{}],[1,279,"util.ping",{}],[1,280,"util.ping",{}],[1,281,"util.ping",{}],[1,282,"util.ping",{}],[1,283,"util.ping",{}],[1,284,"util.ping",{}],[1,285,"util.ping",{}],[1,286,"util.ping",{}],[1,287,"util.ping",{}],[1,288,"util.ping",{}],[1,289,"util.ping",{}],[1,290,"util.ping",{}],[1,291,"util.ping",{}],[1,292,"util.ping",{}],[1,293,"util.ping",{}],[1,294,"util.ping",{}],[1,295,"util.ping",{}],[1,296,"util.ping",{}],[1,297,"util.ping",{}],[1,298,"util.ping",{}],[1,299,"util.ping",{}],[1,300,"util.ping",{}],[1,301,"util.ping",{}]]', + ); + }); + + test('can read final text packet without mask', () => { + const buf = Buffer.from(new Uint8Array([129, 8, 111, 118, 101, 114, 57, 48, 48, 48])); + const decoder = new WsFrameDecoder(); + decoder.push(buf); + const frame = decoder.readFrameHeader()!; + const dst = Buffer.alloc(frame.length); + let remaining = frame.length; + remaining = decoder.readFrameData(frame, remaining, dst, 0); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(1); + expect(frame.length).toBe(8); + expect(frame.mask).toEqual(undefined); + expect(dst.toString()).toBe('over9000'); + }); + + test('can read final masked text frame', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(4), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = true; + frame0.mask = true; + frame0.binaryPayload = Buffer.from('hello world'); + frame0.opcode = 1; + const buf = frame0.toBuffer(); + const decoder = new WsFrameDecoder(); + decoder.push(buf); + const frame = decoder.readFrameHeader()!; + const dst = Buffer.alloc(frame.length); + let remaining = frame.length; + remaining = decoder.readFrameData(frame, remaining, dst, 0); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(1); + expect(frame.length).toBe(11); + expect(frame.mask).toBeInstanceOf(Array); + expect(dst.toString()).toBe('hello world'); + }); + + test('can read non-final masked text frame', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(4), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = false; + frame0.mask = true; + frame0.binaryPayload = Buffer.from('hello world'); + frame0.opcode = 1; + const buf = frame0.toBuffer(); + const decoder = new WsFrameDecoder(); + const slice1 = buf.slice(0, 2); + const slice2 = buf.slice(2, 6); + const slice3 = buf.slice(6, 10); + const slice4 = buf.slice(10); + decoder.push(slice1); + decoder.push(slice2); + decoder.push(slice3); + decoder.push(slice4); + const frame = decoder.readFrameHeader()!; + const dst = Buffer.alloc(frame.length); + let remaining = frame.length; + remaining = decoder.readFrameData(frame, remaining, dst, 0); + expect(frame.fin).toBe(0); + expect(frame.opcode).toBe(1); + expect(frame.length).toBe(11); + expect(frame.mask).toBeInstanceOf(Array); + expect(dst.toString()).toBe('hello world'); + }); + + test('can read non-final masked binary frame', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(4), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = false; + frame0.mask = true; + frame0.binaryPayload = Buffer.from('hello world'); + frame0.opcode = 2; + const buf = frame0.toBuffer(); + const decoder = new WsFrameDecoder(); + decoder.push(buf); + const frame = decoder.readFrameHeader()!; + const dst = Buffer.alloc(frame.length); + let remaining = frame.length; + remaining = decoder.readFrameData(frame, remaining, dst, 0); + expect(frame.fin).toBe(0); + expect(frame.opcode).toBe(2); + expect(frame.length).toBe(11); + expect(frame.mask).toBeInstanceOf(Array); + expect(dst.toString()).toBe('hello world'); + }); + + test('can read non-final non-masked binary frame', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(4), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = false; + frame0.mask = false; + frame0.binaryPayload = Buffer.from('hello world'); + frame0.opcode = 2; + const buf = frame0.toBuffer(); + const decoder = new WsFrameDecoder(); + decoder.push(buf); + const frame = decoder.readFrameHeader()!; + const dst = Buffer.alloc(frame.length); + let remaining = frame.length; + remaining = decoder.readFrameData(frame, remaining, dst, 0); + expect(frame.fin).toBe(0); + expect(frame.opcode).toBe(2); + expect(frame.length).toBe(11); + expect(frame.mask).toBe(undefined); + expect(dst.toString()).toBe('hello world'); + }); + + test('can decode a frame with a continuation frame', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(4), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = false; + frame0.mask = true; + frame0.binaryPayload = Buffer.from('hello '); + frame0.opcode = 2; + const frame1 = new WebSocketFrame(Buffer.alloc(4), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame1.fin = true; + frame1.mask = true; + frame1.binaryPayload = Buffer.from('world'); + frame1.opcode = 0; + const buf0 = frame0.toBuffer(); + const buf1 = frame1.toBuffer(); + const dst = Buffer.alloc(11); + const decoder = new WsFrameDecoder(); + decoder.push(buf0); + const header0 = decoder.readFrameHeader()!; + let remaining0 = header0.length; + remaining0 = decoder.readFrameData(header0, remaining0, dst, 0); + expect(header0.fin).toBe(0); + decoder.push(buf1); + const header1 = decoder.readFrameHeader()!; + let remaining1 = header1.length; + remaining1 = decoder.readFrameData(header1, remaining1, dst, 6); + expect(header1.fin).toBe(1); + expect(dst.toString()).toBe('hello world'); + }); +}); + +describe('control frames', () => { + test('can read CLOSE frame with masked UTF-8 payload', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(256), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = true; + frame0.mask = true; + frame0.binaryPayload = Buffer.from('something 🤷‍♂️ happened'); + frame0.closeStatus = 1000; + frame0.opcode = WsFrameOpcode.CLOSE; + const buf = frame0.toBuffer(); + const decoder = new WsFrameDecoder(); + decoder.push(buf); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsCloseFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.CLOSE); + expect(frame.length).toBe(frame0.binaryPayload.length + 2); + expect(frame.mask).toBeInstanceOf(Array); + expect((frame as WsCloseFrame).code).toBe(0); + expect((frame as WsCloseFrame).reason).toBe(''); + decoder.readCloseFrameData(frame as WsCloseFrame); + expect(frame).toBeInstanceOf(WsCloseFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.CLOSE); + expect(frame.length).toBe(frame0.binaryPayload.length + 2); + expect(frame.mask).toBeInstanceOf(Array); + expect((frame as WsCloseFrame).code).toBe(1000); + expect((frame as WsCloseFrame).reason).toBe('something 🤷‍♂️ happened'); + }); + + test('can read CLOSE frame with un-masked UTF-8 payload', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(256), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = true; + frame0.mask = false; + frame0.binaryPayload = Buffer.from('something 🤷‍♂️ happened'); + frame0.closeStatus = 1000; + frame0.opcode = WsFrameOpcode.CLOSE; + const buf = frame0.toBuffer(); + const decoder = new WsFrameDecoder(); + decoder.push(buf); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsCloseFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.CLOSE); + expect(frame.length).toBe(frame0.binaryPayload.length + 2); + expect(frame.mask).toBe(undefined); + expect((frame as WsCloseFrame).code).toBe(0); + expect((frame as WsCloseFrame).reason).toBe(''); + decoder.readCloseFrameData(frame as WsCloseFrame); + expect(frame).toBeInstanceOf(WsCloseFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.CLOSE); + expect(frame.length).toBe(frame0.binaryPayload.length + 2); + expect(frame.mask).toBe(undefined); + expect((frame as WsCloseFrame).code).toBe(1000); + expect((frame as WsCloseFrame).reason).toBe('something 🤷‍♂️ happened'); + }); + + test('can read PING frame with masked bytes', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(256), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = true; + frame0.mask = true; + frame0.binaryPayload = new Uint8Array([1, 2, 3]); + frame0.opcode = WsFrameOpcode.PING; + const buf0 = frame0.toBuffer(); + const decoder = new WsFrameDecoder(); + decoder.push(buf0); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsPingFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.PING); + expect(frame.length).toBe(3); + expect(frame.mask).toBeInstanceOf(Array); + expect((frame as WsPingFrame).data).toEqual(new Uint8Array([1, 2, 3])); + }); + + test('can read PING frame with un-masked bytes', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(256), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = true; + frame0.mask = false; + frame0.binaryPayload = Buffer.from(new Uint8Array([1, 2, 3])); + frame0.opcode = WsFrameOpcode.PING; + const buf0 = frame0.toBuffer(); + const decoder = new WsFrameDecoder(); + decoder.push(buf0); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsPingFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.PING); + expect(frame.length).toBe(3); + expect(frame.mask).toBe(undefined); + expect((frame as WsPingFrame).data).toEqual(new Uint8Array([1, 2, 3])); + }); + + test('can read PONG frame with masked bytes', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(256), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = true; + frame0.mask = true; + frame0.binaryPayload = new Uint8Array([1, 2, 3]); + frame0.opcode = WsFrameOpcode.PONG; + const buf0 = frame0.toBuffer(); + const decoder = new WsFrameDecoder(); + decoder.push(buf0); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsPongFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.PONG); + expect(frame.length).toBe(3); + expect(frame.mask).toBeInstanceOf(Array); + expect((frame as WsPongFrame).data).toEqual(new Uint8Array([1, 2, 3])); + }); + + test('can read PONG frame with un-masked bytes', () => { + const frame0 = new WebSocketFrame(Buffer.alloc(256), Buffer.alloc(128), {maxReceivedFrameSize: 1000000}); + frame0.fin = true; + frame0.mask = false; + frame0.binaryPayload = Buffer.from(new Uint8Array([1, 2, 3])); + frame0.opcode = WsFrameOpcode.PONG; + const buf0 = frame0.toBuffer(); + const slice0 = buf0.slice(0, 2); + const slice1 = buf0.slice(2); + const decoder = new WsFrameDecoder(); + decoder.push(slice0); + decoder.push(slice1); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsPongFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.PONG); + expect(frame.length).toBe(3); + expect(frame.mask).toBe(undefined); + expect((frame as WsPongFrame).data).toEqual(new Uint8Array([1, 2, 3])); + }); +}); diff --git a/src/ws/__tests__/encoder.spec.ts b/src/ws/__tests__/encoder.spec.ts new file mode 100644 index 00000000..dd24f9bf --- /dev/null +++ b/src/ws/__tests__/encoder.spec.ts @@ -0,0 +1,208 @@ +import {WsFrameDecoder} from '../WsFrameDecoder'; +import {WsFrameEncoder} from '../WsFrameEncoder'; +import {WsFrameOpcode} from '../constants'; +import {WsCloseFrame, WsFrameHeader, WsPingFrame, WsPongFrame} from '../frames'; + +describe('control frames', () => { + test('can encode an empty PING frame', () => { + const encoder = new WsFrameEncoder(); + const encoded = encoder.encodePing(null); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsPingFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.PING); + expect(frame.length).toBe(0); + expect(frame.mask).toBeUndefined(); + expect((frame as WsPingFrame).data).toEqual(new Uint8Array(0)); + }); + + test('can encode a PING frame with data', () => { + const encoder = new WsFrameEncoder(); + const encoded = encoder.encodePing(new Uint8Array([1, 2, 3, 4])); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsPingFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.PING); + expect(frame.length).toBe(4); + expect(frame.mask).toBeUndefined(); + expect((frame as WsPingFrame).data).toEqual(new Uint8Array([1, 2, 3, 4])); + }); + + test('can encode an empty PONG frame', () => { + const encoder = new WsFrameEncoder(); + const encoded = encoder.encodePong(null); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsPongFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.PONG); + expect(frame.length).toBe(0); + expect(frame.mask).toBeUndefined(); + expect((frame as WsPingFrame).data).toEqual(new Uint8Array(0)); + }); + + test('can encode a PONG frame with data', () => { + const encoder = new WsFrameEncoder(); + const encoded = encoder.encodePong(new Uint8Array([1, 2, 3, 4])); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsPongFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.PONG); + expect(frame.length).toBe(4); + expect(frame.mask).toBeUndefined(); + expect((frame as WsPingFrame).data).toEqual(new Uint8Array([1, 2, 3, 4])); + }); + + test('can encode an empty CLOSE frame', () => { + const encoder = new WsFrameEncoder(); + const encoded = encoder.encodeClose(''); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsCloseFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.CLOSE); + expect(frame.length).toBe(0); + expect(frame.mask).toBeUndefined(); + }); + + test('can encode a CLOSE frame with code and reason', () => { + const encoder = new WsFrameEncoder(); + const encoded = encoder.encodeClose('gg wp', 123); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + decoder.readCloseFrameData(frame as WsCloseFrame); + expect(frame).toBeInstanceOf(WsCloseFrame); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.CLOSE); + expect(frame.length).toBe(2 + 5); + expect(frame.mask).toBeUndefined(); + expect((frame as WsCloseFrame).code).toBe(123); + expect((frame as WsCloseFrame).reason).toBe('gg wp'); + }); +}); + +describe('data frames', () => { + test('can encode an empty BINARY data frame', () => { + const encoder = new WsFrameEncoder(); + const encoded = encoder.encodeHdr(1, WsFrameOpcode.BINARY, 0, 0); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsFrameHeader); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.BINARY); + expect(frame.length).toBe(0); + expect(frame.mask).toBeUndefined(); + }); + + test('can encode a BINARY data frame with data', () => { + const encoder = new WsFrameEncoder(); + encoder.writeHdr(1, WsFrameOpcode.BINARY, 5, 0); + encoder.writer.buf(new Uint8Array([1, 2, 3, 4, 5]), 5); + const encoded = encoder.writer.flush(); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsFrameHeader); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.BINARY); + expect(frame.length).toBe(5); + expect(frame.mask).toBeUndefined(); + const data = decoder.reader.buf(5); + expect(data).toEqual(new Uint8Array([1, 2, 3, 4, 5])); + }); + + test('can encode a fast BINARY data frame with data', () => { + const encoder = new WsFrameEncoder(); + const data = new Uint8Array(333); + encoder.writeDataMsgHdrFast(data.length); + encoder.writer.buf(data, data.length); + const encoded = encoder.writer.flush(); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsFrameHeader); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.BINARY); + expect(frame.length).toBe(data.length); + expect(frame.mask).toBeUndefined(); + const data2 = decoder.reader.buf(frame.length); + expect(data2).toEqual(data); + }); + + describe('can encode different message sizes', () => { + const sizes = [0, 1, 2, 125, 126, 127, 128, 129, 255, 1234, 65535, 65536, 65537, 7777777, 2 ** 31 - 1]; + const encoder = new WsFrameEncoder(); + const decoder = new WsFrameDecoder(); + for (const size of sizes) { + test(`size ${size}`, () => { + const encoded = encoder.encodeHdr(1, WsFrameOpcode.BINARY, size, 0); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsFrameHeader); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.BINARY); + expect(frame.length).toBe(size); + }); + } + }); + + test('can encode a masked frame', () => { + const encoder = new WsFrameEncoder(); + const data = new Uint8Array([1, 2, 3, 4, 5]); + const mask = 123456789; + encoder.writeHdr(1, WsFrameOpcode.BINARY, data.length, mask); + encoder.writeBufXor(data, mask); + const encoded = encoder.writer.flush(); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame = decoder.readFrameHeader()!; + expect(frame).toBeInstanceOf(WsFrameHeader); + expect(frame.fin).toBe(1); + expect(frame.opcode).toBe(WsFrameOpcode.BINARY); + expect(frame.length).toBe(data.length); + expect(frame.mask).toEqual([7, 91, 205, 21]); + const data2 = decoder.reader.bufXor(frame.length, frame.mask!, 0); + expect(data2).toEqual(data); + }); + + test('can encode and decode a fragmented message', () => { + const encoder = new WsFrameEncoder(); + const data1 = new Uint8Array([1, 2, 3]); + const data2 = new Uint8Array([4, 5]); + const mask1 = 333444555; + const mask2 = 123123123; + encoder.writeHdr(0, WsFrameOpcode.BINARY, data1.length, mask1); + encoder.writeBufXor(data1, mask1); + encoder.writeHdr(1, WsFrameOpcode.CONTINUE, data2.length, mask2); + encoder.writeBufXor(data2, mask2); + const encoded = encoder.writer.flush(); + const decoder = new WsFrameDecoder(); + decoder.push(encoded); + const frame0 = decoder.readFrameHeader()!; + expect(frame0).toBeInstanceOf(WsFrameHeader); + expect(frame0.fin).toBe(0); + expect(frame0.opcode).toBe(WsFrameOpcode.BINARY); + expect(frame0.length).toBe(data1.length); + expect(frame0.mask).toEqual([19, 223, 245, 203]); + const data3 = decoder.reader.bufXor(frame0.length, frame0.mask!, 0); + expect(data3).toEqual(data1); + const frame1 = decoder.readFrameHeader()!; + expect(frame1).toBeInstanceOf(WsFrameHeader); + expect(frame1.fin).toBe(1); + expect(frame1.opcode).toBe(WsFrameOpcode.CONTINUE); + expect(frame1.length).toBe(data2.length); + expect(frame1.mask).toEqual([7, 86, 181, 179]); + const data4 = decoder.reader.bufXor(frame1.length, frame1.mask!, 0); + expect(data4).toEqual(data2); + }); +}); diff --git a/src/ws/constants.ts b/src/ws/constants.ts new file mode 100644 index 00000000..7e57c2ab --- /dev/null +++ b/src/ws/constants.ts @@ -0,0 +1,16 @@ +export enum WsFrameOpcode { + // Continuation fragment of a data frame + CONTINUE = 0, + + // Data frames + TEXT = 1, + BINARY = 2, + + // Control frames + // eslint-disable-next-line + MIN_CONTROL_OPCODE = 8, + // eslint-disable-next-line + CLOSE = 8, + PING = 9, + PONG = 10, +} diff --git a/src/ws/errors.ts b/src/ws/errors.ts new file mode 100644 index 00000000..dec8534b --- /dev/null +++ b/src/ws/errors.ts @@ -0,0 +1,11 @@ +export class WsFrameDecodingError extends Error { + constructor() { + super('WS_FRAME_DECODING'); + } +} + +export class WsFrameEncodingError extends Error { + constructor() { + super('WS_FRAME_ENCODING'); + } +} diff --git a/src/ws/frames.ts b/src/ws/frames.ts new file mode 100644 index 00000000..e8c8f4e8 --- /dev/null +++ b/src/ws/frames.ts @@ -0,0 +1,45 @@ +export class WsFrameHeader { + constructor( + public readonly fin: 0 | 1, + public readonly opcode: number, + public readonly length: number, + public readonly mask: undefined | [number, number, number, number], + ) {} +} + +export class WsPingFrame extends WsFrameHeader { + constructor( + fin: 0 | 1, + opcode: number, + length: number, + mask: undefined | [number, number, number, number], + public readonly data: Uint8Array, + ) { + super(fin, opcode, length, mask); + } +} + +export class WsPongFrame extends WsFrameHeader { + constructor( + fin: 0 | 1, + opcode: number, + length: number, + mask: undefined | [number, number, number, number], + public readonly data: Uint8Array, + ) { + super(fin, opcode, length, mask); + } +} + +export class WsCloseFrame extends WsFrameHeader { + constructor( + fin: 0 | 1, + opcode: number, + length: number, + mask: undefined | [number, number, number, number], + public code: number, + public reason: string, + ) { + super(fin, opcode, length, mask); + } +} diff --git a/src/ws/index.ts b/src/ws/index.ts new file mode 100644 index 00000000..ddd48ff8 --- /dev/null +++ b/src/ws/index.ts @@ -0,0 +1,4 @@ +export * from './constants'; +export * from './errors'; +export * from './frames'; +export * from './WsFrameDecoder'; diff --git a/yarn.lock b/yarn.lock index ea0a97ea..191dc09a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1091,6 +1091,13 @@ buffer@^6.0.3: base64-js "^1.3.1" ieee754 "^1.2.1" +bufferutil@^4.0.1: + version "4.0.9" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.9.tgz#6e81739ad48a95cad45a279588e13e95e24a800a" + integrity sha512-WDtdLmJvAuNNPzByAYpRo2rF1Mmradw6gvWsQKf63476DDXmomT9zUiGypLcG4ibIM67vhAj8jJRdbmEws2Aqw== + dependencies: + node-gyp-build "^4.3.0" + builtin-modules@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz" @@ -1293,6 +1300,21 @@ cross-spawn@^7.0.0, cross-spawn@^7.0.3: shebang-command "^2.0.0" which "^2.0.1" +d@1, d@^1.0.1, d@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.2.tgz#2aefd554b81981e7dccf72d6842ae725cb17e5de" + integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw== + dependencies: + es5-ext "^0.10.64" + type "^2.7.2" + +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + debug@^4.1.0, debug@^4.1.1, debug@^4.3.1: version "4.3.4" resolved "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz" @@ -1387,6 +1409,33 @@ error-ex@^1.3.1: dependencies: is-arrayish "^0.2.1" +es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.63, es5-ext@^0.10.64, es5-ext@~0.10.14: + version "0.10.64" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.64.tgz#12e4ffb48f1ba2ea777f1fcdd1918ef73ea21714" + integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + esniff "^2.0.1" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.4" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.4.tgz#f4e7d28013770b4208ecbf3e0bf14d3bcb557b8c" + integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg== + dependencies: + d "^1.0.2" + ext "^1.7.0" + escalade@^3.1.1: version "3.1.2" resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz" @@ -1402,11 +1451,29 @@ escape-string-regexp@^2.0.0: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz" integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== +esniff@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/esniff/-/esniff-2.0.1.tgz#a4d4b43a5c71c7ec51c51098c1d8a29081f9b308" + integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg== + dependencies: + d "^1.0.1" + es5-ext "^0.10.62" + event-emitter "^0.3.5" + type "^2.7.2" + esprima@^4.0.0: version "4.0.1" resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== +event-emitter@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/event-emitter/-/event-emitter-0.3.5.tgz#df8c69eef1647923c7157b9ce83840610b02cc39" + integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA== + dependencies: + d "1" + es5-ext "~0.10.14" + event-lite@^0.1.1: version "0.1.3" resolved "https://registry.npmjs.org/event-lite/-/event-lite-0.1.3.tgz" @@ -1458,6 +1525,13 @@ expect@^29.0.0, expect@^29.7.0: jest-message-util "^29.7.0" jest-util "^29.7.0" +ext@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz" @@ -1778,6 +1852,11 @@ is-stream@^2.0.0: resolved "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz" integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + isarray@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz" @@ -2424,6 +2503,11 @@ mri@^1.2.0: resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + ms@2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz" @@ -2475,6 +2559,11 @@ natural-compare@^1.4.0: resolved "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz" integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + node-addon-api@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz" @@ -2492,6 +2581,11 @@ node-gyp-build-optional-packages@5.1.1: dependencies: detect-libc "^2.0.1" +node-gyp-build@^4.3.0: + version "4.8.4" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.8.4.tgz#8a70ee85464ae52327772a90d66c6077a900cfc8" + integrity sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ== + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" @@ -3153,6 +3247,18 @@ type-fest@^0.21.3: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz" integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== +type@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486" + integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ== + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + typedoc@^0.25.12: version "0.25.13" resolved "https://registry.npmjs.org/typedoc/-/typedoc-0.25.13.tgz" @@ -3186,6 +3292,13 @@ update-browserslist-db@^1.0.13: escalade "^3.1.1" picocolors "^1.0.0" +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + util-deprecate@^1.0.1: version "1.0.2" resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" @@ -3222,6 +3335,18 @@ walker@^1.0.8: dependencies: makeerror "1.0.12" +websocket@^1.0.35: + version "1.0.35" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.35.tgz#374197207d7d4cc4c36cbf8a1bb886ee52a07885" + integrity sha512-/REy6amwPZl44DDzvRCkaI1q1bIiQB0mEFQLUrhz3z2EK91cp3n72rAjUlrTP0zV22HJIUOVHQGPxhFRjxjt+Q== + dependencies: + bufferutil "^4.0.1" + debug "^2.2.0" + es5-ext "^0.10.63" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" + which@^2.0.1: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz" @@ -3274,6 +3399,11 @@ y18n@^5.0.5: resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug== + yallist@4.0.0, yallist@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz"