import * as net from "node:net"; const buildHuffmanTree = (values) => { const root = []; for (const [value, path] of values) { let cur = root; for (let i = 0; i < path.length; i++) { const decision = Number(path[i]); if (i === path.length - 1) { cur[decision] = value; } else { if (!cur[decision]) { cur[decision] = []; } } cur = cur[decision]; } } return root; }; const HUFFMAN_EOS = Symbol("EOS"); const HUFFMAN_TREE = buildHuffmanTree([ [String.fromCharCode(0), "1111111111000"], [String.fromCharCode(1), "11111111111111111011000"], [String.fromCharCode(2), "1111111111111111111111100010"], [String.fromCharCode(3), "1111111111111111111111100011"], [String.fromCharCode(4), "1111111111111111111111100100"], [String.fromCharCode(5), "1111111111111111111111100101"], [String.fromCharCode(6), "1111111111111111111111100110"], [String.fromCharCode(7), "1111111111111111111111100111"], [String.fromCharCode(8), "1111111111111111111111101000"], [String.fromCharCode(9), "111111111111111111101010"], [String.fromCharCode(10), "111111111111111111111111111100"], [String.fromCharCode(11), "1111111111111111111111101001"], [String.fromCharCode(12), "1111111111111111111111101010"], [String.fromCharCode(13), "111111111111111111111111111101"], [String.fromCharCode(14), "1111111111111111111111101011"], [String.fromCharCode(15), "1111111111111111111111101100"], [String.fromCharCode(16), "1111111111111111111111101101"], [String.fromCharCode(17), "1111111111111111111111101110"], [String.fromCharCode(18), "1111111111111111111111101111"], [String.fromCharCode(19), "1111111111111111111111110000"], [String.fromCharCode(20), "1111111111111111111111110001"], [String.fromCharCode(21), "1111111111111111111111110010"], [String.fromCharCode(22), "111111111111111111111111111110"], [String.fromCharCode(23), "1111111111111111111111110011"], [String.fromCharCode(24), "1111111111111111111111110100"], [String.fromCharCode(25), "1111111111111111111111110101"], [String.fromCharCode(26), "1111111111111111111111110110"], [String.fromCharCode(27), "1111111111111111111111110111"], [String.fromCharCode(28), "1111111111111111111111111000"], [String.fromCharCode(29), "1111111111111111111111111001"], [String.fromCharCode(30), "1111111111111111111111111010"], [String.fromCharCode(31), "1111111111111111111111111011"], [String.fromCharCode(32), "010100"], [String.fromCharCode(33), "1111111000"], [String.fromCharCode(34), "1111111001"], [String.fromCharCode(35), "111111111010"], [String.fromCharCode(36), "1111111111001"], [String.fromCharCode(37), "010101"], [String.fromCharCode(38), "11111000"], [String.fromCharCode(39), "11111111010"], [String.fromCharCode(40), "1111111010"], [String.fromCharCode(41), "1111111011"], [String.fromCharCode(42), "11111001"], [String.fromCharCode(43), "11111111011"], [String.fromCharCode(44), "11111010"], [String.fromCharCode(45), "010110"], [String.fromCharCode(46), "010111"], [String.fromCharCode(47), "011000"], [String.fromCharCode(48), "00000"], [String.fromCharCode(49), "00001"], [String.fromCharCode(50), "00010"], [String.fromCharCode(51), "011001"], [String.fromCharCode(52), "011010"], [String.fromCharCode(53), "011011"], [String.fromCharCode(54), "011100"], [String.fromCharCode(55), "011101"], [String.fromCharCode(56), "011110"], [String.fromCharCode(57), "011111"], [String.fromCharCode(58), "1011100"], [String.fromCharCode(59), "11111011"], [String.fromCharCode(60), "111111111111100"], [String.fromCharCode(61), "100000"], [String.fromCharCode(62), "111111111011"], [String.fromCharCode(63), "1111111100"], [String.fromCharCode(64), "1111111111010"], [String.fromCharCode(65), "100001"], [String.fromCharCode(66), "1011101"], [String.fromCharCode(67), "1011110"], [String.fromCharCode(68), "1011111"], [String.fromCharCode(69), "1100000"], [String.fromCharCode(70), "1100001"], [String.fromCharCode(71), "1100010"], [String.fromCharCode(72), "1100011"], [String.fromCharCode(73), "1100100"], [String.fromCharCode(74), "1100101"], [String.fromCharCode(75), "1100110"], [String.fromCharCode(76), "1100111"], [String.fromCharCode(77), "1101000"], [String.fromCharCode(78), "1101001"], [String.fromCharCode(79), "1101010"], [String.fromCharCode(80), "1101011"], [String.fromCharCode(81), "1101100"], [String.fromCharCode(82), "1101101"], [String.fromCharCode(83), "1101110"], [String.fromCharCode(84), "1101111"], [String.fromCharCode(85), "1110000"], [String.fromCharCode(86), "1110001"], [String.fromCharCode(87), "1110010"], [String.fromCharCode(88), "11111100"], [String.fromCharCode(89), "1110011"], [String.fromCharCode(90), "11111101"], [String.fromCharCode(91), "1111111111011"], [String.fromCharCode(92), "1111111111111110000"], [String.fromCharCode(93), "1111111111100"], [String.fromCharCode(94), "11111111111100"], [String.fromCharCode(95), "100010"], [String.fromCharCode(96), "111111111111101"], [String.fromCharCode(97), "00011"], [String.fromCharCode(98), "100011"], [String.fromCharCode(99), "00100"], [String.fromCharCode(100), "100100"], [String.fromCharCode(101), "00101"], [String.fromCharCode(102), "100101"], [String.fromCharCode(103), "100110"], [String.fromCharCode(104), "100111"], [String.fromCharCode(105), "00110"], [String.fromCharCode(106), "1110100"], [String.fromCharCode(107), "1110101"], [String.fromCharCode(108), "101000"], [String.fromCharCode(109), "101001"], [String.fromCharCode(110), "101010"], [String.fromCharCode(111), "00111"], [String.fromCharCode(112), "101011"], [String.fromCharCode(113), "1110110"], [String.fromCharCode(114), "101100"], [String.fromCharCode(115), "01000"], [String.fromCharCode(116), "01001"], [String.fromCharCode(117), "101101"], [String.fromCharCode(118), "1110111"], [String.fromCharCode(119), "1111000"], [String.fromCharCode(120), "1111001"], [String.fromCharCode(121), "1111010"], [String.fromCharCode(122), "1111011"], [String.fromCharCode(123), "111111111111110"], [String.fromCharCode(124), "11111111100"], [String.fromCharCode(125), "11111111111101"], [String.fromCharCode(126), "1111111111101"], [String.fromCharCode(127), "1111111111111111111111111100"], [String.fromCharCode(128), "11111111111111100110"], [String.fromCharCode(129), "1111111111111111010010"], [String.fromCharCode(130), "11111111111111100111"], [String.fromCharCode(131), "11111111111111101000"], [String.fromCharCode(132), "1111111111111111010011"], [String.fromCharCode(133), "1111111111111111010100"], [String.fromCharCode(134), "1111111111111111010101"], [String.fromCharCode(135), "11111111111111111011001"], [String.fromCharCode(136), "1111111111111111010110"], [String.fromCharCode(137), "11111111111111111011010"], [String.fromCharCode(138), "11111111111111111011011"], [String.fromCharCode(139), "11111111111111111011100"], [String.fromCharCode(140), "11111111111111111011101"], [String.fromCharCode(141), "11111111111111111011110"], [String.fromCharCode(142), "111111111111111111101011"], [String.fromCharCode(143), "11111111111111111011111"], [String.fromCharCode(144), "111111111111111111101100"], [String.fromCharCode(145), "111111111111111111101101"], [String.fromCharCode(146), "1111111111111111010111"], [String.fromCharCode(147), "11111111111111111100000"], [String.fromCharCode(148), "111111111111111111101110"], [String.fromCharCode(149), "11111111111111111100001"], [String.fromCharCode(150), "11111111111111111100010"], [String.fromCharCode(151), "11111111111111111100011"], [String.fromCharCode(152), "11111111111111111100100"], [String.fromCharCode(153), "111111111111111011100"], [String.fromCharCode(154), "1111111111111111011000"], [String.fromCharCode(155), "11111111111111111100101"], [String.fromCharCode(156), "1111111111111111011001"], [String.fromCharCode(157), "11111111111111111100110"], [String.fromCharCode(158), "11111111111111111100111"], [String.fromCharCode(159), "111111111111111111101111"], [String.fromCharCode(160), "1111111111111111011010"], [String.fromCharCode(161), "111111111111111011101"], [String.fromCharCode(162), "11111111111111101001"], [String.fromCharCode(163), "1111111111111111011011"], [String.fromCharCode(164), "1111111111111111011100"], [String.fromCharCode(165), "11111111111111111101000"], [String.fromCharCode(166), "11111111111111111101001"], [String.fromCharCode(167), "111111111111111011110"], [String.fromCharCode(168), "11111111111111111101010"], [String.fromCharCode(169), "1111111111111111011101"], [String.fromCharCode(170), "1111111111111111011110"], [String.fromCharCode(171), "111111111111111111110000"], [String.fromCharCode(172), "111111111111111011111"], [String.fromCharCode(173), "1111111111111111011111"], [String.fromCharCode(174), "11111111111111111101011"], [String.fromCharCode(175), "11111111111111111101100"], [String.fromCharCode(176), "111111111111111100000"], [String.fromCharCode(177), "111111111111111100001"], [String.fromCharCode(178), "1111111111111111100000"], [String.fromCharCode(179), "111111111111111100010"], [String.fromCharCode(180), "11111111111111111101101"], [String.fromCharCode(181), "1111111111111111100001"], [String.fromCharCode(182), "11111111111111111101110"], [String.fromCharCode(183), "11111111111111111101111"], [String.fromCharCode(184), "11111111111111101010"], [String.fromCharCode(185), "1111111111111111100010"], [String.fromCharCode(186), "1111111111111111100011"], [String.fromCharCode(187), "1111111111111111100100"], [String.fromCharCode(188), "11111111111111111110000"], [String.fromCharCode(189), "1111111111111111100101"], [String.fromCharCode(190), "1111111111111111100110"], [String.fromCharCode(191), "11111111111111111110001"], [String.fromCharCode(192), "11111111111111111111100000"], [String.fromCharCode(193), "11111111111111111111100001"], [String.fromCharCode(194), "11111111111111101011"], [String.fromCharCode(195), "1111111111111110001"], [String.fromCharCode(196), "1111111111111111100111"], [String.fromCharCode(197), "11111111111111111110010"], [String.fromCharCode(198), "1111111111111111101000"], [String.fromCharCode(199), "1111111111111111111101100"], [String.fromCharCode(200), "11111111111111111111100010"], [String.fromCharCode(201), "11111111111111111111100011"], [String.fromCharCode(202), "11111111111111111111100100"], [String.fromCharCode(203), "111111111111111111111011110"], [String.fromCharCode(204), "111111111111111111111011111"], [String.fromCharCode(205), "11111111111111111111100101"], [String.fromCharCode(206), "111111111111111111110001"], [String.fromCharCode(207), "1111111111111111111101101"], [String.fromCharCode(208), "1111111111111110010"], [String.fromCharCode(209), "111111111111111100011"], [String.fromCharCode(210), "11111111111111111111100110"], [String.fromCharCode(211), "111111111111111111111100000"], [String.fromCharCode(212), "111111111111111111111100001"], [String.fromCharCode(213), "11111111111111111111100111"], [String.fromCharCode(214), "111111111111111111111100010"], [String.fromCharCode(215), "111111111111111111110010"], [String.fromCharCode(216), "111111111111111100100"], [String.fromCharCode(217), "111111111111111100101"], [String.fromCharCode(218), "11111111111111111111101000"], [String.fromCharCode(219), "11111111111111111111101001"], [String.fromCharCode(220), "1111111111111111111111111101"], [String.fromCharCode(221), "111111111111111111111100011"], [String.fromCharCode(222), "111111111111111111111100100"], [String.fromCharCode(223), "111111111111111111111100101"], [String.fromCharCode(224), "11111111111111101100"], [String.fromCharCode(225), "111111111111111111110011"], [String.fromCharCode(226), "11111111111111101101"], [String.fromCharCode(227), "111111111111111100110"], [String.fromCharCode(228), "1111111111111111101001"], [String.fromCharCode(229), "111111111111111100111"], [String.fromCharCode(230), "111111111111111101000"], [String.fromCharCode(231), "11111111111111111110011"], [String.fromCharCode(232), "1111111111111111101010"], [String.fromCharCode(233), "1111111111111111101011"], [String.fromCharCode(234), "1111111111111111111101110"], [String.fromCharCode(235), "1111111111111111111101111"], [String.fromCharCode(236), "111111111111111111110100"], [String.fromCharCode(237), "111111111111111111110101"], [String.fromCharCode(238), "11111111111111111111101010"], [String.fromCharCode(239), "11111111111111111110100"], [String.fromCharCode(240), "11111111111111111111101011"], [String.fromCharCode(241), "111111111111111111111100110"], [String.fromCharCode(242), "11111111111111111111101100"], [String.fromCharCode(243), "11111111111111111111101101"], [String.fromCharCode(244), "111111111111111111111100111"], [String.fromCharCode(245), "111111111111111111111101000"], [String.fromCharCode(246), "111111111111111111111101001"], [String.fromCharCode(247), "111111111111111111111101010"], [String.fromCharCode(248), "111111111111111111111101011"], [String.fromCharCode(249), "1111111111111111111111111110"], [String.fromCharCode(250), "111111111111111111111101100"], [String.fromCharCode(251), "111111111111111111111101101"], [String.fromCharCode(252), "111111111111111111111101110"], [String.fromCharCode(253), "111111111111111111111101111"], [String.fromCharCode(254), "111111111111111111111110000"], [String.fromCharCode(255), "11111111111111111111101110"], [HUFFMAN_EOS, "111111111111111111111111111111"], ]); class HPackCtx { #dynamicTable; static #STATIC_TABLE = { 1: [":authority", ""], 2: [":method", "GET"], 3: [":method", "POST"], 4: [":path", "/"], 5: [":path", "/index.html"], 6: [":scheme", "http"], 7: [":scheme", "https"], 8: [":status", "200"], 9: [":status", "204"], 10: [":status", "206"], 11: [":status", "304"], 12: [":status", "400"], 13: [":status", "404"], 14: [":status", "500"], 15: ["accept-charset", ""], 16: ["accept-encoding", "gzip, deflate"], 17: ["accept-language", ""], 18: ["accept-ranges", ""], 19: ["accept", ""], 20: ["access-control-allow-origin", ""], 21: ["age", ""], 22: ["allow", ""], 23: ["authorization", ""], 24: ["cache-control", ""], 25: ["content-disposition", ""], 26: ["content-encoding", ""], 27: ["content-language", ""], 28: ["content-length", ""], 29: ["content-location", ""], 30: ["content-range", ""], 31: ["content-type", ""], 32: ["cookie", ""], 33: ["date", ""], 34: ["etag", ""], 35: ["expect", ""], 36: ["expires", ""], 37: ["from", ""], 38: ["host", ""], 39: ["if-match", ""], 40: ["if-modified-since", ""], 41: ["if-none-match", ""], 42: ["if-range", ""], 43: ["if-unmodified-since", ""], 44: ["last-modified", ""], 45: ["link", ""], 46: ["location", ""], 47: ["max-forwards", ""], 48: ["proxy-authenticate", ""], 49: ["proxy-authorization", ""], 50: ["range", ""], 51: ["referer", ""], 52: ["refresh", ""], 53: ["retry-after", ""], 54: ["server", ""], 55: ["set-cookie", ""], 56: ["strict-transport-security", ""], 57: ["transfer-encoding", ""], 58: ["user-agent", ""], 59: ["vary", ""], 60: ["via", ""], 61: ["www-authenticate", ""], }; static #STATIC_TABLE_MAX = 61; constructor() { this.#dynamicTable = {}; } #indexTable = (index) => { return index < HPackCtx.#STATIC_TABLE_MAX ? HPackCtx.#STATIC_TABLE[index] : this.#dynamicTable[index - HPackCtx.#STATIC_TABLE_MAX]; }; decode = (block) => { const fields = []; while (block.length > 0) { let size = 0; const firstBit = block[0] & 128; const secondBit = block[0] & 64; const thirdBit = block[0] & 32; const fourthBit = block[0] & 16; let field; const decodeInteger = (mask) => { const int = block[size] & (0xff >> mask); if (int === 0xff >> mask) { throw new Error("long integer, todo"); } size += 1; return int; }; const decodeHuffman = (length) => { let string = ""; let remaining = length; let cur = HUFFMAN_TREE; while (remaining > 0) { const nextOctet = block[size]; for (let i = 7; i >= 0; i--) { const nextBit = (nextOctet >> i) & 0x01; cur = cur[nextBit ? 1 : 0]; if (typeof cur === "string") { string += cur; cur = HUFFMAN_TREE; } else if (typeof cur === "symbol" && cur === HUFFMAN_EOS) { throw new Error("what"); } } size++; remaining--; } return string; }; const decodeString = () => { const huffman = block[size] & 128; const length = decodeInteger(1); if (huffman) { return decodeHuffman(length); } else { size += length; return new TextDecoder().decode(block.subarray(size, length)); } }; // Indexed Header Field Representation if (firstBit) { const index = decodeInteger(1); const tabled = this.#indexTable(index); field = tabled; } else { // Literal Header Field with Incremental Indexing if (secondBit) { const index = decodeInteger(2); let headerName; if (index === 0) { headerName = decodeString(); } else { headerName = this.#indexTable(index)[0]; } const headerValue = decodeString(); field = [headerName, headerValue]; } else { throw new Error("some other encoding"); } } if (typeof field === "undefined") { throw new Error("field was not set"); } fields.push(field); if (size === 0) { throw new Error("size was not set"); } block = block.subarray(size); } return fields; }; encode = () => {}; } const FRAME_TYPE = { DATA: 0x0, HEADERS: 0x1, PRIORITY: 0x2, RST_STREAM: 0x3, SETTINGS: 0x04, PUSH_PROMISE: 0x5, PING: 0x6, GOAWAY: 0x7, WINDOW_UPDATE: 0x08, CONTINUATION: 0x09, }; const FRAME_TYPE_NAME = Object.fromEntries( Object.entries(FRAME_TYPE).map(([k, v]) => [v, k]) ); const SETTING = { SETTINGS_HEADER_TABLE_SIZE: 0x01, SETTINGS_ENABLE_PUSH: 0x02, SETTINGS_MAX_CONCURRENT_STREAMS: 0x03, SETTINGS_INITIAL_WINDOW_SIZE: 0x04, SETTINGS_MAX_FRAME_SIZE: 0x05, SETTINGS_MAX_HEADER_LIST_SIZE: 0x06, }; const SETTING_NAME = Object.fromEntries( Object.entries(SETTING).map(([k, v]) => [v, k]) ); const FRAME_HEADER_SIZE = 3 + 1 + 1 + 4; const frameReader = (frameCb) => { const STATE = { PREFACE: 0, FRAME_HEAD: 1, FRAME_PAYLOAD: 2, }; const CONNECTION_PREFACE = new TextEncoder().encode( "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" ); let state = STATE.PREFACE; let frameHead; let buf = Buffer.from([]); return (data) => { buf = Buffer.concat([buf, data]); while (true) { switch (state) { case STATE.PREFACE: { if (buf.length < 24) { return; } const preface = buf.subarray(0, 24); buf = buf.subarray(24); if (Buffer.compare(preface, CONNECTION_PREFACE) !== 0) { frameCb(new Error(`invalid preface from ${peer}`)); return; } state = STATE.FRAME_HEAD; break; } case STATE.FRAME_HEAD: { if (buf.length < FRAME_HEADER_SIZE) { return; } const frameHeader = buf.subarray(0, FRAME_HEADER_SIZE); buf = buf.subarray(FRAME_HEADER_SIZE); const length = (frameHeader[0] << 16) | (frameHeader[1] << 8) | frameHeader[2]; const type = frameHeader[3]; const flags = frameHeader[4]; const streamIdentifier = frameHeader.readUint32BE(5) & (0xff_ff_ff_ff >> 1); state = STATE.FRAME_PAYLOAD; frameHead = { length, type, flags, streamIdentifier, }; break; } case STATE.FRAME_PAYLOAD: { if (buf.length < frameHead.length) { return; } const payload = buf.subarray(0, frameHead.length); buf = buf.subarray(frameHead.length); frameCb(null, { ...frameHead, payload, }); state = STATE.FRAME_HEAD; break; } default: throw new Error("unknown state", state); } } }; }; const encodeFrame = (frame) => { if (typeof frame.flags !== "number") { throw new Error(`Flags of frame are not number: ${frame.flags}`); } if (typeof frame.type !== "number") { throw new Error(`Type of frame is not a number: ${frame.type}`); } const length = frame.payload.length; const buffer = Buffer.alloc(FRAME_HEADER_SIZE + length); if (length > 2 ** 24) { throw new Error(`Frame is too long: ${length}`); } buffer[0] = length >> 16; buffer[1] = (length >> 8) & 0xff; buffer[2] = length & 0xff; if (!(frame.type in FRAME_TYPE_NAME)) { throw new Error(`Trying to write unknown frame type: ${frame.type}`); } buffer[3] = frame.type; if (frame.flags > 0xff) { throw new Error(`Frame flags do not fit in a byte: ${frame.flags}`); } buffer[4] = frame.flags; buffer.writeUint32BE(length, 5); frame.payload.copy(buffer, FRAME_HEADER_SIZE); return buffer; }; /** * @param {net.Socket} socket */ const handleConnection = (socket) => { const peer = `${socket.remoteAddress}:${socket.remotePort}`; console.log(`received connection from ${peer}`); const hpackDecode = new HPackCtx(); const hpackEncode = new HPackCtx(); const peerSettings = new Map(); const streams = new Map(); socket.write( encodeFrame({ type: FRAME_TYPE.SETTINGS, flags: 0, payload: Buffer.from([]), }) ); const onData = frameReader((err, frame) => { if (err) { console.warn("error from frame layer", err); socket.destroy(); return; } console.log("received frame", FRAME_TYPE_NAME[frame.type], frame); switch (frame.type) { case FRAME_TYPE.HEADERS: { if (!streams.has(frame.streamIdentifier)) { streams.set(frame.streamIdentifier, { headerBuffer: Buffer.from([]), endHeaders: false, }); } // END_HEADERS if ((frame.flags & 0x04) !== 0) { streams.get(frame.streamIdentifier).endHeaders = true; } // PRIORITY const priorityFlag = (frame.flags & 0x20) !== 0; // PADDED const paddedFlag = (frame.flags & 0x08) !== 0; let payload = frame.payload; let paddingLength = 0; if (paddedFlag) { paddingLength = payload[0]; payload = payload.subarray(1); } if (priorityFlag) { // skip over Exclusive/Stream Dependency, Weight payload = payload.subarray(5); } if (paddedFlag) { if (paddingLength > payload.length) { console.warn("too much padding"); socket.destroy(); return; } payload = payload.subarray(0, payload.length - paddingLength); } if (streams.get(frame.streamIdentifier).endHeaders) { const fieldBlockFragement = payload; const fields = hpackDecode.decode(fieldBlockFragement); console.log("headers", fields); // we got a request!!! } else { throw new Error("expecting CONTINUATION is not yet supported"); } break; } case FRAME_TYPE.SETTINGS: { // ACK if ((frame.flags & 0x1) !== 0) { if (frame.length !== 0) { console.warn("received non-empty SETTINGS ack frame"); socket.destroy(); return; } break; } if (frame.streamIdentifier !== 0) { console.warn("stream identifier for a SETTINGS"); socket.destroy(); return; } if (frame.length % 6 !== 0) { console.warn("invalid length for SETTINGS frame"); socket.destroy(); return; } for (let i = 0; i < frame.length; i += 6) { const identifier = frame.payload.readUint16BE(i); const value = frame.payload.readUint32BE(i + 2); console.log("SETTINGS setting", SETTING_NAME[identifier], "=", value); peerSettings[SETTING_NAME[identifier]] = value; } break; } case FRAME_TYPE.WINDOW_UPDATE: { // whatever const increment = frame.payload.readUint32BE(); console.log("incrementing transfer window by", increment); break; } default: { console.warn( `unsupported frame type ${FRAME_TYPE_NAME[frame.type] ?? frame.type}` ); } } }); socket.on("data", onData); socket.on("error", (err) => { console.warn(`error from ${peer}:`, err); }); socket.on("close", () => { console.log(`connection closed for ${peer}`); }); }; const server = net.createServer(handleConnection).on("error", (err) => { console.error(`error: ${err}`); }); server.listen(8080, () => { console.log("Listening on", server.address()); });