UNPKG

quico

Version:

A pure JavaScript implementation of QUIC, HTTP/3, QPACK, and WebTransport for Node.js

1,462 lines (1,239 loc) 42.6 kB
/* * quico: HTTP/3 and QUIC implementation for Node.js * Copyright 2025 colocohen * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at: * * https://www.apache.org/licenses/LICENSE-2.0 * * This file is part of the open-source project hosted at: * https://github.com/colocohen/quico * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. */ import { concatUint8Arrays, writeVarInt, readVarInt } from './utils.js'; var huffman_codes = new Uint32Array([ 0x1ff8,//(0) 0x7fffd8,//(1) 0xfffffe2,//(2) 0xfffffe3,//(3) 0xfffffe4,//(4) 0xfffffe5,//(5) 0xfffffe6,//(6) 0xfffffe7,//(7) 0xfffffe8,//(8) 0xffffea,//(9) 0x3ffffffc,//(10) 0xfffffe9,//(11) 0xfffffea,//(12) 0x3ffffffd,//(13) 0xfffffeb,//(14) 0xfffffec,//(15) 0xfffffed,//(16) 0xfffffee,//(17) 0xfffffef,//(18) 0xffffff0,//(19) 0xffffff1,//(20) 0xffffff2,//(21) 0x3ffffffe,//(22) 0xffffff3,//(23) 0xffffff4,//(24) 0xffffff5,//(25) 0xffffff6,//(26) 0xffffff7,//(27) 0xffffff8,//(28) 0xffffff9,//(29) 0xffffffa,//(30) 0xffffffb,//(31) 0x14,//' ' (32) 0x3f8,//'!' (33) 0x3f9,//'"' (34) 0xffa,//'#' (35) 0x1ff9,//'$' (36) 0x15,//'%' (37) 0xf8,//'&' (38) 0x7fa,//''' (39) 0x3fa,//'(' (40) 0x3fb,//')' (41) 0xf9,//'*' (42) 0x7fb,//'+' (43) 0xfa,//',' (44) 0x16,//'-' (45) 0x17,//'.' (46) 0x18,//'/' (47) 0x0,//'0' (48) 0x1,//'1' (49) 0x2,//'2' (50) 0x19,//'3' (51) 0x1a,//'4' (52) 0x1b,//'5' (53) 0x1c,//'6' (54) 0x1d,//'7' (55) 0x1e,//'8' (56) 0x1f,//'9' (57) 0x5c,//':' (58) 0xfb,//';' (59) 0x7ffc,//'<' (60) 0x20,//'=' (61) 0xffb,//'>' (62) 0x3fc,//'?' (63) 0x1ffa,//'@' (64) 0x21,//'A' (65) 0x5d,//'B' (66) 0x5e,//'C' (67) 0x5f,//'D' (68) 0x60,//'E' (69) 0x61,//'F' (70) 0x62,//'G' (71) 0x63,//'H' (72) 0x64,//'I' (73) 0x65,//'J' (74) 0x66,//'K' (75) 0x67,//'L' (76) 0x68,//'M' (77) 0x69,//'N' (78) 0x6a,//'O' (79) 0x6b,//'P' (80) 0x6c,//'Q' (81) 0x6d,//'R' (82) 0x6e,//'S' (83) 0x6f,//'T' (84) 0x70,//'U' (85) 0x71,//'V' (86) 0x72,//'W' (87) 0xfc,//'X' (88) 0x73,//'Y' (89) 0xfd,//'Z' (90) 0x1ffb,//'[' (91) 0x7fff0,//'\' (92) 0x1ffc,//']' (93) 0x3ffc,//'^' (94) 0x22,//'_' (95) 0x7ffd,//'`' (96) 0x3,//'a' (97) 0x23,//'b' (98) 0x4,//'c' (99) 0x24,//'d' (100) 0x5,//'e' (101) 0x25,//'f' (102) 0x26,//'g' (103) 0x27,//'h' (104) 0x6,//'i' (105) 0x74,//'j' (106) 0x75,//'k' (107) 0x28,//'l' (108) 0x29,//'m' (109) 0x2a,//'n' (110) 0x7,//'o' (111) 0x2b,//'p' (112) 0x76,//'q' (113) 0x2c,//'r' (114) 0x8,//'s' (115) 0x9,//'t' (116) 0x2d,//'u' (117) 0x77,//'v' (118) 0x78,//'w' (119) 0x79,//'x' (120) 0x7a,//'y' (121) 0x7b,//'z' (122) 0x7ffe,//'{' (123) 0x7fc,//'|' (124) 0x3ffd,//'}' (125) 0x1ffd,//'~' (126) 0xffffffc,//(127) 0xfffe6,//(128) 0x3fffd2,//(129) 0xfffe7,//(130) 0xfffe8,//(131) 0x3fffd3,//(132) 0x3fffd4,//(133) 0x3fffd5,//(134) 0x7fffd9,//(135) 0x3fffd6,//(136) 0x7fffda,//(137) 0x7fffdb,//(138) 0x7fffdc,//(139) 0x7fffdd,//(140) 0x7fffde,//(141) 0xffffeb,//(142) 0x7fffdf,//(143) 0xffffec,//(144) 0xffffed,//(145) 0x3fffd7,//(146) 0x7fffe0,//(147) 0xffffee,//(148) 0x7fffe1,//(149) 0x7fffe2,//(150) 0x7fffe3,//(151) 0x7fffe4,//(152) 0x1fffdc,//(153) 0x3fffd8,//(154) 0x7fffe5,//(155) 0x3fffd9,//(156) 0x7fffe6,//(157) 0x7fffe7,//(158) 0xffffef,//(159) 0x3fffda,//(160) 0x1fffdd,//(161) 0xfffe9,//(162) 0x3fffdb,//(163) 0x3fffdc,//(164) 0x7fffe8,//(165) 0x7fffe9,//(166) 0x1fffde,//(167) 0x7fffea,//(168) 0x3fffdd,//(169) 0x3fffde,//(170) 0xfffff0,//(171) 0x1fffdf,//(172) 0x3fffdf,//(173) 0x7fffeb,//(174) 0x7fffec,//(175) 0x1fffe0,//(176) 0x1fffe1,//(177) 0x3fffe0,//(178) 0x1fffe2,//(179) 0x7fffed,//(180) 0x3fffe1,//(181) 0x7fffee,//(182) 0x7fffef,//(183) 0xfffea,//(184) 0x3fffe2,//(185) 0x3fffe3,//(186) 0x3fffe4,//(187) 0x7ffff0,//(188) 0x3fffe5,//(189) 0x3fffe6,//(190) 0x7ffff1,//(191) 0x3ffffe0,//(192) 0x3ffffe1,//(193) 0xfffeb,//(194) 0x7fff1,//(195) 0x3fffe7,//(196) 0x7ffff2,//(197) 0x3fffe8,//(198) 0x1ffffec,//(199) 0x3ffffe2,//(200) 0x3ffffe3,//(201) 0x3ffffe4,//(202) 0x7ffffde,//(203) 0x7ffffdf,//(204) 0x3ffffe5,//(205) 0xfffff1,//(206) 0x1ffffed,//(207) 0x7fff2,//(208) 0x1fffe3,//(209) 0x3ffffe6,//(210) 0x7ffffe0,//(211) 0x7ffffe1,//(212) 0x3ffffe7,//(213) 0x7ffffe2,//(214) 0xfffff2,//(215) 0x1fffe4,//(216) 0x1fffe5,//(217) 0x3ffffe8,//(218) 0x3ffffe9,//(219) 0xffffffd,//(220) 0x7ffffe3,//(221) 0x7ffffe4,//(222) 0x7ffffe5,//(223) 0xfffec,//(224) 0xfffff3,//(225) 0xfffed,//(226) 0x1fffe6,//(227) 0x3fffe9,//(228) 0x1fffe7,//(229) 0x1fffe8,//(230) 0x7ffff3,//(231) 0x3fffea,//(232) 0x3fffeb,//(233) 0x1ffffee,//(234) 0x1ffffef,//(235) 0xfffff4,//(236) 0xfffff5,//(237) 0x3ffffea,//(238) 0x7ffff4,//(239) 0x3ffffeb,//(240) 0x7ffffe6,//(241) 0x3ffffec,//(242) 0x3ffffed,//(243) 0x7ffffe7,//(244) 0x7ffffe8,//(245) 0x7ffffe9,//(246) 0x7ffffea,//(247) 0x7ffffeb,//(248) 0xffffffe,//(249) 0x7ffffec,//(250) 0x7ffffed,//(251) 0x7ffffee,//(252) 0x7ffffef,//(253) 0x7fffff0,//(254) 0x3ffffee,//(255) 0x3fffffff,//EOS (256) ]); var huffman_bits = new Uint8Array([13,23,28,28,28,28,28,28,28,24,30,28,28,30,28,28,28,28,28,28,28,28,30,28,28,28,28,28,28,28,28,28,6,10,10,12,13,6,8,11,10,10,8,11,8,6,6,6,5,5,5,6,6,6,6,6,6,6,7,8,15,6,12,10,13,6,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,8,7,8,13,19,13,14,6,15,5,6,5,6,5,6,6,6,5,7,7,6,6,6,5,6,7,6,5,5,6,7,7,7,7,7,15,11,14,13,28,20,22,20,20,22,22,22,23,22,23,23,23,23,23,24,23,24,24,22,23,24,23,23,23,23,21,22,23,22,23,23,24,22,21,20,22,22,23,23,21,23,22,22,24,21,22,23,23,21,21,22,21,23,22,23,23,20,22,22,22,23,22,22,23,26,26,20,19,22,23,22,25,26,26,26,27,27,26,24,25,19,21,26,27,27,26,27,24,21,21,26,26,28,27,27,27,20,24,20,21,22,21,21,23,22,22,25,25,24,24,26,23,26,27,26,26,27,27,27,27,27,28,27,27,27,27,27,26,30]); function buildHuffmanDecodeTrie() { var root = {}; for (var i = 0; i < huffman_codes.length; i++) { var code = huffman_codes[i]; var length = huffman_bits[i]; var node = root; for (var j = length - 1; j >= 0; j--) { var bit = (code >> j) & 1; if (!node[bit]) node[bit] = {}; node = node[bit]; } node.symbol = i; } return root; } var huffman_flat_decode_tables = buildHuffmanDecodeTrie(); var qpack_static_table_entries = [ [":authority", ""], [":path", "/"], ["age", "0"], ["content-disposition", ""], ["content-length", "0"], ["cookie", ""], ["date", ""], ["etag", ""], ["if-modified-since", ""], ["if-none-match", ""], ["last-modified", ""], ["link", ""], ["location", ""], ["referer", ""], ["set-cookie", ""], [":method", "CONNECT"], [":method", "DELETE"], [":method", "GET"], [":method", "HEAD"], [":method", "OPTIONS"], [":method", "POST"], [":method", "PUT"], [":scheme", "http"], [":scheme", "https"], [":status", "103"], [":status", "200"], [":status", "304"], [":status", "404"], [":status", "503"], ["accept", "*/*"], ["accept", "application/dns-message"], ["accept-encoding", "gzip, deflate, br"], ["accept-ranges", "bytes"], ["access-control-allow-headers", "cache-control"], ["access-control-allow-headers", "content-type"], ["access-control-allow-origin", "*"], ["cache-control", "max-age=0"], ["cache-control", "max-age=2592000"], ["cache-control", "max-age=604800"], ["cache-control", "no-cache"], ["cache-control", "no-store"], ["cache-control", "public, max-age=31536000"], ["content-encoding", "br"], ["content-encoding", "gzip"], ["content-type", "application/dns-message"], ["content-type", "application/javascript"], ["content-type", "application/json"], ["content-type", "application/x-www-form-urlencoded"], ["content-type", "image/gif"], ["content-type", "image/jpeg"], ["content-type", "image/png"], ["content-type", "text/css"], ["content-type", "text/html; charset=utf-8"], ["content-type", "text/plain"], ["content-type", "text/plain;charset=utf-8"], ["range", "bytes=0-"], ["strict-transport-security", "max-age=31536000"], ["strict-transport-security", "max-age=31536000; includesubdomains"], ["strict-transport-security", "max-age=31536000; includesubdomains; preload"], ["vary", "accept-encoding"], ["vary", "origin"], ["x-content-type-options", "nosniff"], ["x-xss-protection", "1; mode=block"], [":status", "100"], [":status", "204"], [":status", "206"], [":status", "302"], [":status", "400"], [":status", "403"], [":status", "421"], [":status", "425"], [":status", "500"], ["accept-language", ""], ["access-control-allow-credentials", "FALSE"], ["access-control-allow-credentials", "TRUE"], ["access-control-allow-headers", "*"], ["access-control-allow-methods", "get"], ["access-control-allow-methods", "get, post, options"], ["access-control-allow-methods", "options"], ["access-control-expose-headers", "content-length"], ["access-control-request-headers", "content-type"], ["access-control-request-method", "get"], ["access-control-request-method", "post"], ["alt-svc", "clear"], ["authorization", ""], ["content-security-policy", "script-src 'none'; object-src 'none'; base-uri 'none'"], ["early-data", "1"], ["expect-ct", ""], ["forwarded", ""], ["if-range", ""], ["origin", ""], ["purpose", "prefetch"], ["server", ""], ["timing-allow-origin", "*"], ["upgrade-insecure-requests", "1"], ["user-agent", ""], ["x-forwarded-for", ""], ["x-frame-options", "deny"], ["x-frame-options", "sameorigin"] ]; function decodeVarInt(buf, prefixBits, pos) { var maxPrefix = (1 << prefixBits) - 1; var byte = buf[pos]; var value = byte & maxPrefix; pos++; if (value < maxPrefix) // נגמר במרווח-הבייט הראשון return { value, next: pos }; var m = 0; while (true) { byte = buf[pos++]; value += (byte & 0x7f) << m; if ((byte & 0x80) === 0) break; m += 7; } return { value, next: pos }; } function huffmanEncode(text) { var input = new TextEncoder().encode(text); // UTF-8 -> bytes var bitBuffer = 0; var bitLen = 0; var output = []; for (var i = 0; i < input.length; i++) { var sym = input[i]; var code = huffman_codes[sym]; var nbits = huffman_bits[sym]; bitBuffer = (bitBuffer << nbits) | code; bitLen += nbits; while (bitLen >= 8) { bitLen -= 8; output.push((bitBuffer >> bitLen) & 0xff); } } // Padding: לפי התקן, ממלאים 1-ים עד סוף בייט if (bitLen > 0) { bitBuffer = (bitBuffer << (8 - bitLen)) | ((1 << (8 - bitLen)) - 1); output.push(bitBuffer & 0xff); } return new Uint8Array(output); } // פונקציית פיענוח לפי עץ function decodeHuffman(buf) { var output = []; var node = huffman_flat_decode_tables; var current = 0; var nbits = 0; for (var i = 0; i < buf.length; i++) { current = (current << 8) | buf[i]; nbits += 8; while (nbits > 0) { var bit = (current >> (nbits - 1)) & 1; node = node[bit]; if (!node) throw new Error("Invalid Huffman encoding"); nbits--; if (node.symbol !== undefined) { output.push(node.symbol); node = huffman_flat_decode_tables; } } } // בדיקה לסיומת padding חוקית (לפי התקן — רק 1s מותר בסוף) var padding = (1 << nbits) - 1; if ((current & padding) !== padding) { throw new Error("Invalid Huffman padding"); } return new TextDecoder().decode(Uint8Array.from(output)); } function parse_qpack_header_block(buf) { var pos = 0; var headers = []; // Required Insert Count (prefix-8) var ric = decodeVarInt(buf, 8, pos); pos = ric.next; // Delta Base (prefix-7 + S-bit) var firstDbByte = buf[pos]; var postBase = (firstDbByte & 0x80) !== 0; // S-bit var db = decodeVarInt(buf, 7, pos); pos = db.next; // Base Index = RIC ± DB לפי S-bit var baseIndex = postBase ? ric.value + db.value : ric.value - db.value; // Header Field Lines while (pos < buf.length) { var byte = buf[pos]; // A. Indexed Field Line – 1xxxxxxx if ((byte & 0x80) === 0x80) { var fromStatic = (byte & 0x40) !== 0; // T-bit var idx = decodeVarInt(buf, 6, pos); // prefix-6 pos = idx.next; headers.push({ type: "indexed", from_static_table: fromStatic, index: idx.value }); continue; } // B. Literal With Name Reference – 01xxxxxx if ((byte & 0xC0) === 0x40) { var neverIndexed = (byte & 0x20) !== 0; // N-bit var fromStatic = (byte & 0x10) !== 0; // T-bit var nameIdx = decodeVarInt(buf, 4, pos); // prefix-4 pos = nameIdx.next; var valH = (buf[pos] & 0x80) !== 0; var valLen = decodeVarInt(buf, 7, pos); // prefix-7 pos = valLen.next; var valBytes = buf.slice(pos, pos + valLen.value); pos += valLen.value; var value = valH ? decodeHuffman(valBytes) : new TextDecoder().decode(valBytes); headers.push({ type: "literal_with_name_ref", never_indexed: neverIndexed, from_static_table: fromStatic, name_index: nameIdx.value, value }); continue; } // C. Literal With Literal Name – 001xxxxx if ((byte & 0xE0) === 0x20) { var neverIndexed = (byte & 0x10) !== 0; // N-bit var nameH = (byte & 0x08) !== 0; // H-bit var nameLen = decodeVarInt(buf, 3, pos); // prefix-3 pos = nameLen.next; var nameBytes = buf.slice(pos, pos + nameLen.value); pos += nameLen.value; var name = nameH ? decodeHuffman(nameBytes) : new TextDecoder().decode(nameBytes); var valH = (buf[pos] & 0x80) !== 0; // H-bit var valLen = decodeVarInt(buf, 7, pos); // prefix-7 pos = valLen.next; var valBytes = buf.slice(pos, pos + valLen.value); pos += valLen.value; var value = valH ? decodeHuffman(valBytes) : new TextDecoder().decode(valBytes); headers.push({ type: "literal_with_literal_name", never_indexed: neverIndexed, name, value }); continue; } // לא אמור להגיע לכאן – תקלה לפי התקן throw new Error( `Unknown header-block instruction at byte ${pos} (0x${byte.toString(16)})` ); } return { insert_count: ric.value, delta_base: db.value, post_base: postBase, base_index: baseIndex, headers }; } function parse_qpack_header_block_old(buf) { var pos = 0; var headers = []; /* 1) Field-section prefix */ var ric = decodeVarInt(buf, 8, pos); // Required Insert Count (prefix-8) pos = ric.next; var db = decodeVarInt(buf, 7, pos); // Delta Base (prefix-7) pos = db.next; /* 2) Field-line representations */ while (pos < buf.length) { var byte = buf[pos]; /* A. Indexed Field Line – 1xxxxxxx */ if ((byte & 0x80) === 0x80) { var fromStatic = (byte & 0x40) !== 0; // T-bit var idx = decodeVarInt(buf, 6, pos); // prefix-6 pos = idx.next; headers.push({ type: "indexed", from_static_table: fromStatic, index: idx.value }); continue; } /* B. Literal Field Line + Name Reference – 01xxxxxx */ if ((byte & 0xC0) === 0x40) { var neverIndexed = (byte & 0x20) !== 0; // N-bit var fromStatic = (byte & 0x10) !== 0; // T-bit var nameIdx = decodeVarInt(buf, 4, pos); // prefix-4 pos = nameIdx.next; var valH = (buf[pos] & 0x80) !== 0; var valLen = decodeVarInt(buf, 7, pos); // prefix-7 pos = valLen.next; var valBytes = buf.slice(pos, pos + valLen.value); pos += valLen.value; var value = valH ? decodeHuffman(valBytes) : new TextDecoder().decode(valBytes); headers.push({ type: "literal_with_name_ref", never_indexed: neverIndexed, from_static_table: fromStatic, name_index: nameIdx.value, value }); continue; } /* C. Literal Field Line + Literal Name – 001xxxxx */ if ((byte & 0xE0) === 0x20) { var neverIndexed = (byte & 0x10) !== 0; // N-bit var nameH = (byte & 0x08) !== 0; // H-bit (שם) :contentReference[oaicite:0]{index=0} var nameLen = decodeVarInt(buf, 3, pos); // prefix-3 pos = nameLen.next; var nameBytes = buf.slice(pos, pos + nameLen.value); pos += nameLen.value; var name = nameH ? decodeHuffman(nameBytes) : new TextDecoder().decode(nameBytes); var valH = (buf[pos] & 0x80) !== 0; // H-bit (ערך) var valLen = decodeVarInt(buf, 7, pos); // prefix-7 pos = valLen.next; var valBytes = buf.slice(pos, pos + valLen.value); pos += valLen.value; var value = valH ? decodeHuffman(valBytes) : new TextDecoder().decode(valBytes); headers.push({ type: "literal_with_literal_name", never_indexed: neverIndexed, name, value }); continue; } /* לא אמור להגיע לכאן – פסילה לפי התקן */ throw new Error( `Unknown header-block instruction at byte ${pos} (0x${byte.toString(16)})` ); } return { insert_count: ric.value, delta_base: db.value, headers: headers }; } // הנחות: // - chunks: Array<Uint8Array>, לפי הסדר (head → tail) // - from_offset: offset לוגי (למי שצריך; נעדכן אותו בסוף) // - readVarInt(u8, off) => { value, byteLength } // - concatUint8Arrays(arr) קיים אצלך function extract_h3_frames_from_chunks(chunks, from_offset) { // אם אין צ'אנקים – אין מה לפרסר if (!chunks || chunks.length === 0) { return { frames: [], new_from_offset: from_offset }; } // בונים buffers החל מהמיקום הלוגי from_offset בלי לשנות את chunks var buffers = []; var acc = 0; for (var i = 0; i < chunks.length; i++) { var c = chunks[i]; var nextAcc = acc + c.length; if (from_offset < nextAcc) { // from_offset נופל לתוך הצ'אנק הזה var start = from_offset - acc; buffers.push(c.slice(start)); // כל שאר הצ'אנקים כמות שהם for (var j = i + 1; j < chunks.length; j++) { buffers.push(chunks[j]); } break; } acc = nextAcc; } if (buffers.length === 0) { // from_offset מעבר לסוף הנתונים שיש כרגע return { frames: [], new_from_offset: from_offset }; } var combined = concatUint8Arrays(buffers); var offset = 0; var frames = []; // קריאת VarInt בטוחה מתוך 'combined' עם קידום offset מקומי function safeReadVarInt() { if (offset >= combined.length) return null; var firstByte = combined[offset]; var lengthBits = firstByte >> 6; // 0..3 → 1,2,4,8 bytes var neededLength = 1 << lengthBits; if (offset + neededLength > combined.length) return null; var res = readVarInt(combined, offset); if (!res || typeof res.byteLength !== 'number') return null; offset += res.byteLength; return res; } while (offset < combined.length) { var startOffset = offset; var frameType = safeReadVarInt(); if (!frameType) break; var lengthInfo = safeReadVarInt(); if (!lengthInfo) { // לא הצלחנו אפילו לקרוא את האורך – נחזור לנק' ההתחלה ונחכה לנתונים נוספים offset = startOffset; break; } var payloadLength = lengthInfo.value >>> 0; // מניחים אורך עד 2^32-1 if (offset + payloadLength > combined.length) { // פריים חלקי – rollback מלא offset = startOffset; break; } var payload = combined.slice(offset, offset + payloadLength); frames.push({ frame_type: frameType.value, payload: payload }); offset += payloadLength; } // לא משנים את chunks; רק מקדמים את from_offset לפי כמה באמת נקרא return { frames: frames, new_from_offset: from_offset + offset }; } function extract_h3_frames_from_chunks2(chunks, from_offset) { var offsets = Object.keys(chunks).map(Number).sort((a, b) => a - b); var buffers = []; var totalLength = 0; // מחברים את כל הצ’אנקים החל מ־from_offset for (var i = 0; i < offsets.length; i++) { var base = offsets[i]; var chunk = chunks[base]; if (from_offset >= base && from_offset < base + chunk.length) { var start = from_offset - base; var sliced = chunk.slice(start); buffers.push(sliced); totalLength += sliced.length; for (var j = i + 1; j < offsets.length; j++) { buffers.push(chunks[offsets[j]]); totalLength += chunks[offsets[j]].length; } break; } } if (buffers.length === 0) return { frames: [], new_from_offset: from_offset }; var combined = concatUint8Arrays(buffers); var offset = 0; var frames = []; // פונקציית עזר בטוחה לקריאת VarInt function safeReadVarInt() { if (offset >= combined.length) return null; var firstByte = combined[offset]; var lengthBits = firstByte >> 6; var neededLength = 1 << lengthBits; if (offset + neededLength > combined.length) return null; var res = readVarInt(combined, offset); if (!res || typeof res.byteLength !== 'number') return null; offset += res.byteLength; return res; } while (offset < combined.length) { var startOffset = offset; var frameType = safeReadVarInt(); if (!frameType) break; var lengthInfo = safeReadVarInt(); if (!lengthInfo) { offset = startOffset; // rollback – אי אפשר אפילו לקרוא אורך break; } var payloadLength = lengthInfo.value; if (offset + payloadLength > combined.length) { offset = startOffset; // rollback break; } var payload = combined.slice(offset, offset + payloadLength); frames.push({ frame_type: frameType.value, payload }); offset += payloadLength; } // עדכון chunks כדי להסיר את מה שקראנו if (offset > 0) { var bytesToRemove = offset; var newChunks = {}; var processed = 0; var currentOffset = from_offset; for (var k = 0; k < offsets.length; k++) { var base = offsets[k]; var chunk = chunks[base]; if (currentOffset >= base + chunk.length) continue; var relStart = Math.max(currentOffset - base, 0); var relEnd = Math.min(chunk.length, currentOffset + bytesToRemove - base); if (relEnd < chunk.length) { var leftover = chunk.slice(relEnd); var newBase = base + relEnd; newChunks[newBase] = leftover; } bytesToRemove -= (relEnd - relStart); if (bytesToRemove <= 0) break; } for (var key in chunks) delete chunks[key]; for (var key in newChunks) chunks[key] = newChunks[key]; from_offset += offset; } return { frames, new_from_offset: from_offset }; } function build_h3_frames(frames) { var parts = []; for (var i = 0; i < frames.length; i++) { var frame = frames[i]; // כל חלק מהפריים כ־Uint8Array var typeBytes = writeVarInt(frame.frame_type); var lenBytes = writeVarInt(frame.payload.length); var payload = frame.payload; parts.push(typeBytes, lenBytes, payload); } return concatUint8Arrays(parts); } /* חישוב אורך varint לפי HPACK/QPACK (prefix-N) */ function computeVarIntLen(buf, pos, prefixBits) { if (pos >= buf.length) return null; var first = buf[pos]; var prefixMask = (1 << prefixBits) - 1; var prefixVal = first & prefixMask; // אם הערך קטן מהמקסימום – varint של בייט אחד if (prefixVal < prefixMask) return 1; // אחרת ממשיכים ב-Base128 עד שבייט בלי MSB=1 var len = 1; var idx = pos + 1; while (idx < buf.length) { len++; if ((buf[idx] & 0x80) === 0) return len; // הסתיים idx++; } return null; // חסר נתונים } /* קריאה בטוחה של varint */ function safeDecodeVarInt(buf, posRef, prefixBits) { var len = computeVarIntLen(buf, posRef.pos, prefixBits); if (len === null) return null; // לא שלם var res = decodeVarInt(buf, prefixBits, posRef.pos); // הפונקציה שלך posRef.pos = res.next; return res.value; } /* ---------- פונקציית החילוץ העיקרית ---------- */ // הנחות: // - chunks: Array<Uint8Array>, לפי הסדר (head → tail) // - from_offset: מספר מצטבר (רק להחזרה/מתן עקיבה) // - concatUint8Arrays(arr) קיים אצלך // - safeDecodeVarInt(buf, posRef, prefixBits) -> number|null (posRef.pos מתקדם בפנים) // - decodeHuffman(u8) קיים אם אתה תומך בהאףמן; אחרת אפשר להחזיר שגיאה/לקרוא כ-raw function extract_qpack_encoder_instructions_from_chunks(chunks, from_offset) { // אם אין צ'אנקים – אין מה לפרסר if (!chunks || chunks.length === 0) { return { instructions: [], new_from_offset: from_offset }; } // מחברים ל-buffer יחיד החל מ-from_offset, בלי לשנות את chunks var buffers = []; var acc = 0; for (var i = 0; i < chunks.length; i++) { var c = chunks[i]; var nextAcc = acc + c.length; if (from_offset < nextAcc) { // from_offset בתוך הצ'אנק הזה var start = from_offset - acc; buffers.push(c.slice(start)); // כל שאר הצ'אנקים כפי שהם for (var j = i + 1; j < chunks.length; j++) { buffers.push(chunks[j]); } break; } acc = nextAcc; } if (buffers.length === 0) { // from_offset מעבר לנתונים שיש כרגע return { instructions: [], new_from_offset: from_offset }; } var combined = concatUint8Arrays(buffers); var posRef = { pos: 0 }; var instructions = []; // 2) לולאת פירוש הוראות QPACK-Encoder while (posRef.pos < combined.length) { var startPos = posRef.pos; if (posRef.pos >= combined.length) break; var byte = combined[posRef.pos]; // --- A. Insert With Name Reference (1xxxxxxx) --- if ((byte & 0x80) === 0x80) { var fromStatic = (byte & 0x40) !== 0; var nameIdx = safeDecodeVarInt(combined, posRef, 6); if (nameIdx === null) break; // חסר נתונים // value length (Huffman flag בביט העליון של בייט האורך) if (posRef.pos >= combined.length) { posRef.pos = startPos; break; } var valHuffman = (combined[posRef.pos] & 0x80) !== 0; var valLen = safeDecodeVarInt(combined, posRef, 7); if (valLen === null || posRef.pos + valLen > combined.length) { posRef.pos = startPos; break; } var valBytes = combined.slice(posRef.pos, posRef.pos + valLen); posRef.pos += valLen; var value = valHuffman ? decodeHuffman(valBytes) : new TextDecoder().decode(valBytes); instructions.push({ type: 'insert_with_name_ref', from_static_table: fromStatic, name_index: nameIdx, value: value }); continue; } // --- B. Insert Without Name Reference (01xxxxxx) --- if ((byte & 0xC0) === 0x40) { // שים לב: בדפוס הזה ה־N-flag (שם בהאףמן) נמצא בביט 0x20 של הבייט הראשון var nameH = (byte & 0x20) !== 0; var nameLen = safeDecodeVarInt(combined, posRef, 5); if (nameLen === null || posRef.pos + nameLen > combined.length) { posRef.pos = startPos; break; } var nameBytes = combined.slice(posRef.pos, posRef.pos + nameLen); posRef.pos += nameLen; // value length (Huffman flag בביט העליון של בייט האורך) if (posRef.pos >= combined.length) { posRef.pos = startPos; break; } var valH = (combined[posRef.pos] & 0x80) !== 0; var valLen2 = safeDecodeVarInt(combined, posRef, 7); if (valLen2 === null || posRef.pos + valLen2 > combined.length) { posRef.pos = startPos; break; } var valBytes2 = combined.slice(posRef.pos, posRef.pos + valLen2); posRef.pos += valLen2; var nameStr = nameH ? decodeHuffman(nameBytes) : new TextDecoder().decode(nameBytes); var valueStr = valH ? decodeHuffman(valBytes2) : new TextDecoder().decode(valBytes2); instructions.push({ type: 'insert_without_name_ref', name: nameStr, value: valueStr }); continue; } // --- C. Set Dynamic Table Capacity (001xxxxx) --- if ((byte & 0xE0) === 0x20) { var capacity = safeDecodeVarInt(combined, posRef, 5); if (capacity === null) { posRef.pos = startPos; break; } instructions.push({ type: 'set_dynamic_table_capacity', capacity: capacity }); continue; } // --- D. Duplicate (0000xxxx) --- if ((byte & 0xF0) === 0x00) { var dupIndex = safeDecodeVarInt(combined, posRef, 4); if (dupIndex === null) { posRef.pos = startPos; break; } instructions.push({ type: 'duplicate', index: dupIndex }); continue; } // אופקוד לא מוכר או לא נתמך – נעצור עד שיגיע עוד מידע / נטפל חיצונית break; } // כמה בייטים פרשנו מתוך ה־view המאוחד var consumed = posRef.pos | 0; // לא משנים את chunks; רק מקדמים offset לוגי return { instructions: instructions, new_from_offset: from_offset + consumed }; } function extract_qpack_encoder_instructions_from_chunks2(chunks, from_offset) { /* 1) חיבור הצ’אנקים החל מ-from_offset */ var offsets = Object.keys(chunks).map(Number).sort(function (a, b) { return a - b; }); var buffers = []; var totalLen = 0; for (var i = 0; i < offsets.length; i++) { var base = offsets[i]; var chunk = chunks[base]; if (from_offset >= base && from_offset < base + chunk.length) { var start = from_offset - base; var sliced = chunk.slice(start); buffers.push(sliced); totalLen += sliced.length; for (var j = i + 1; j < offsets.length; j++) { buffers.push(chunks[offsets[j]]); totalLen += chunks[offsets[j]].length; } break; } } if (buffers.length === 0) { return { instructions: [], new_from_offset: from_offset }; } var combined = concatUint8Arrays(buffers); var posRef = { pos: 0 }; var instructions = []; /* 2) לולאת פיענוח ההוראות */ while (posRef.pos < combined.length) { var startPos = posRef.pos; var byte = combined[posRef.pos]; /* --- A. Insert With Name Reference (1xxxxxxx) --- */ if ((byte & 0x80) === 0x80) { var fromStatic = (byte & 0x40) !== 0; var nameIdx = safeDecodeVarInt(combined, posRef, 6); if (nameIdx === null) break; // לא שלם // value length var valHuffman = (combined[posRef.pos] & 0x80) !== 0; var valLen = safeDecodeVarInt(combined, posRef, 7); if (valLen === null || posRef.pos + valLen > combined.length) { posRef.pos = startPos; break; } var valBytes = combined.slice(posRef.pos, posRef.pos + valLen); posRef.pos += valLen; var value = valHuffman ? decodeHuffman(valBytes) : new TextDecoder().decode(valBytes); instructions.push({ type: 'insert_with_name_ref', from_static_table: fromStatic, name_index: nameIdx, value: value }); continue; } /* --- B. Insert Without Name Reference (01xxxxxx) --- */ if ((byte & 0xC0) === 0x40) { var nameH = (byte & 0x20) !== 0; var nameLen = safeDecodeVarInt(combined, posRef, 5); if (nameLen === null || posRef.pos + nameLen > combined.length) { posRef.pos = startPos; break; } var nameBytes = combined.slice(posRef.pos, posRef.pos + nameLen); posRef.pos += nameLen; var valH = (combined[posRef.pos] & 0x80) !== 0; var valLen2 = safeDecodeVarInt(combined, posRef, 7); if (valLen2 === null || posRef.pos + valLen2 > combined.length) { posRef.pos = startPos; break; } var valBytes2 = combined.slice(posRef.pos, posRef.pos + valLen2); posRef.pos += valLen2; var nameStr = nameH ? decodeHuffman(nameBytes) : new TextDecoder().decode(nameBytes); var valueStr = valH ? decodeHuffman(valBytes2) : new TextDecoder().decode(valBytes2); instructions.push({ type: 'insert_without_name_ref', name: nameStr, value: valueStr }); continue; } /* --- C. Set Dynamic Table Capacity (001xxxxx) --- */ if ((byte & 0xE0) === 0x20) { var capacity = safeDecodeVarInt(combined, posRef, 5); if (capacity === null) { posRef.pos = startPos; break; } instructions.push({ type: 'set_dynamic_table_capacity', capacity: capacity }); continue; } /* --- D. Duplicate (0000xxxx) --- */ if ((byte & 0xF0) === 0x00) { var dupIndex = safeDecodeVarInt(combined, posRef, 4); if (dupIndex === null) { posRef.pos = startPos; break; } instructions.push({ type: 'duplicate', index: dupIndex }); continue; } /* לא מוכר - נעצור */ break; } var consumed = posRef.pos; // כמה בייטים הצלחנו לפרש /* 3) ניקוי הצ’אנקים והתקדמות from_offset */ if (consumed > 0) { var bytesLeft = consumed; var newChunks = {}; var currOff = from_offset; for (var k = 0; k < offsets.length; k++) { var base = offsets[k]; var chunk = chunks[base]; if (currOff >= base + chunk.length) continue; var relStart = Math.max(currOff - base, 0); var relEnd = Math.min(chunk.length, currOff + bytesLeft - base); if (relEnd < chunk.length) { var leftover = chunk.slice(relEnd); newChunks[base + relEnd] = leftover; } bytesLeft -= (relEnd - relStart); if (bytesLeft <= 0) break; } for (var key in chunks) delete chunks[key]; for (var nk in newChunks) chunks[nk] = newChunks[nk]; from_offset += consumed; } return { instructions: instructions, new_from_offset: from_offset }; } var h3_settings_frame_params = [ [0x01, "SETTINGS_QPACK_MAX_TABLE_CAPACITY"], [0x06, "SETTINGS_MAX_FIELD_SECTION_SIZE"], [0x07, "SETTINGS_QPACK_BLOCKED_STREAMS"], [0x08, "SETTINGS_ENABLE_CONNECT_PROTOCOL"], [0x33, "SETTINGS_H3_DATAGRAM"], [0x2b603742, "SETTINGS_ENABLE_WEBTRANSPORT"], // תקני לפי draft [0x0d, "SETTINGS_NO_RFC9114_LEGACY_CODEPOINT"], [0x14E9CD29, "SETTINGS_WT_MAX_SESSIONS"], [0x4d44, "SETTINGS_ENABLE_METADATA"] // provisional ]; var h3_name_to_id = {}; var h3_id_to_name = {}; for (var i = 0; i < h3_settings_frame_params.length; i++) { var [id, name] = h3_settings_frame_params[i]; h3_name_to_id[name] = id; h3_id_to_name[id] = name; } function parse_h3_settings_frame(buf) { var settings = {}; var offset = 0; while (offset < buf.length) { var idRes = readVarInt(buf, offset); if (!idRes) break; offset += idRes.byteLength; var valRes = readVarInt(buf, offset); if (!valRes) break; offset += valRes.byteLength; var id = idRes.value; var value = valRes.value; var name = h3_id_to_name[id] || `UNKNOWN_0x${id.toString(16)}`; settings[name] = value; } return settings; } function build_settings_frame(settings_named) { var frame_payload = []; for (var name in settings_named) { var id = h3_name_to_id[name]; if (id === undefined) { throw new Error("Unknown setting name: " + name); } var value = settings_named[name]; frame_payload.push(...writeVarInt(id)); frame_payload.push(...writeVarInt(value)); } return new Uint8Array(frame_payload); } function build_control_stream_old(settings_named) { var setting_ids = { SETTINGS_QPACK_MAX_TABLE_CAPACITY: 0x01, SETTINGS_MAX_FIELD_SECTION_SIZE: 0x06, SETTINGS_ENABLE_WEBTRANSPORT: 0x2b603742, // תקני לפי draft SETTINGS_H3_DATAGRAM: 0x33, // תקני לפי RFC 9297 SETTINGS_NO_RFC9114_LEGACY_CODEPOINT: 0x0d, SETTINGS_ENABLE_CONNECT_PROTOCOL: 0x08, // תקני לפי RFC 9220 SETTINGS_WT_MAX_SESSIONS: 0x14E9CD29 }; var frame_payload = []; for (var name in settings_named) { var id = setting_ids[name]; if (id === undefined) { throw new Error("Unknown setting name: " + name); } var value = settings_named[name]; frame_payload.push(...writeVarInt(id)); frame_payload.push(...writeVarInt(value)); } var frame_header = [ ...writeVarInt(0x04), // SETTINGS frame type ...writeVarInt(frame_payload.length) ]; return new Uint8Array([ 0x00, // Stream Type: Control Stream ...frame_header, ...frame_payload ]); } function encodeInt(value, prefixBits) { var max = (1 << prefixBits) - 1; if (value < max) return [value]; // נכנס כולו בפריפיקס var bytes = [max]; value -= max; while (value >= 128) { // המשך varint (7-bit groups) bytes.push((value & 0x7F) | 0x80); value >>= 7; } bytes.push(value); return bytes; } function encodeStringLiteral(bytes, hFlag /* 0/1 */) { var lenBytes = encodeInt(bytes.length, 7); // prefix-7 lenBytes[0] |= (hFlag << 7); // מוסיפים H return lenBytes.concat(Array.from(bytes)); } /* ---------- בניית HEADERS (Literal) ---------- */ function build_http3_literal_headers_frame(headers) { var out = []; out.push(0x00, 0x00); // QPACK prefix for (var header_name in headers) { var nameBytes = new TextEncoder().encode(header_name.toLowerCase()); var valueBytes = new TextEncoder().encode(String(headers[header_name])); /* בייט ראשון: 001 | N=0 | H=0 | NameLen(3+) */ var nameLenEnc = encodeInt(nameBytes.length, 3); // prefix-3 var firstByte = 0x20 | nameLenEnc[0]; // 0b0010_0000 out.push(firstByte, ...nameLenEnc.slice(1), ...nameBytes); /* value: H=0 + prefix-7 */ out.push(...encodeStringLiteral(valueBytes, 0)); } return new Uint8Array(out); } function build_qpack_block_header_ack(stream_id) { return concatUint8Arrays([ Uint8Array.from([0x81]), // instruction type writeVarInt(stream_id) // full VarInt ]); } function build_qpack_known_received_count(count) { if (count <= 0) return null; // אין מה לשלוח var buf = writeVarInt(count); // VarInt עם prefix-6 buf[0] &= 0x3F; // מוודא ששני הביטים העליונים 00 return buf; } function parse_webtransport_datagram(payload) { var result = readVarInt(payload, 0); if (!result) { throw new Error("Invalid VarInt at beginning of payload"); } var stream_id = result.value; var data = payload.slice(result.byteLength); // שאר ה־payload זה הנתונים return { stream_id: stream_id, data: data }; } function build_close_webtransport(errorCode, reason) { var reasonBytes = reason ? new TextEncoder().encode(reason) : new Uint8Array(); var payload = [ ...writeVarInt(errorCode), ...writeVarInt(reasonBytes.length), ...reasonBytes ]; var frame = [ ...writeVarInt(0x2843), // type ...writeVarInt(payload.length), // length ...payload ]; return new Uint8Array(frame); } export { build_h3_frames, build_settings_frame, parse_h3_settings_frame, extract_qpack_encoder_instructions_from_chunks, extract_h3_frames_from_chunks, parse_qpack_header_block, build_http3_literal_headers_frame, parse_webtransport_datagram, build_close_webtransport, build_qpack_block_header_ack, build_qpack_known_received_count, qpack_static_table_entries };