diff options
Diffstat (limited to 'testing/xpcshell/node-http2/lib/protocol/compressor.js')
-rw-r--r-- | testing/xpcshell/node-http2/lib/protocol/compressor.js | 1428 |
1 files changed, 1428 insertions, 0 deletions
diff --git a/testing/xpcshell/node-http2/lib/protocol/compressor.js b/testing/xpcshell/node-http2/lib/protocol/compressor.js new file mode 100644 index 0000000000..6f91f86ec9 --- /dev/null +++ b/testing/xpcshell/node-http2/lib/protocol/compressor.js @@ -0,0 +1,1428 @@ +// The implementation of the [HTTP/2 Header Compression][http2-compression] spec is separated from +// the 'integration' part which handles HEADERS and PUSH_PROMISE frames. The compression itself is +// implemented in the first part of the file, and consists of three classes: `HeaderTable`, +// `HeaderSetDecompressor` and `HeaderSetCompressor`. The two latter classes are +// [Transform Stream][node-transform] subclasses that operate in [object mode][node-objectmode]. +// These transform chunks of binary data into `[name, value]` pairs and vice versa, and store their +// state in `HeaderTable` instances. +// +// The 'integration' part is also implemented by two [Transform Stream][node-transform] subclasses +// that operate in [object mode][node-objectmode]: the `Compressor` and the `Decompressor`. These +// provide a layer between the [framer](framer.html) and the +// [connection handling component](connection.html). +// +// [node-transform]: https://nodejs.org/api/stream.html#stream_class_stream_transform +// [node-objectmode]: https://nodejs.org/api/stream.html#stream_new_stream_readable_options +// [http2-compression]: https://tools.ietf.org/html/rfc7541 + +exports.HeaderTable = HeaderTable; +exports.HuffmanTable = HuffmanTable; +exports.HeaderSetCompressor = HeaderSetCompressor; +exports.HeaderSetDecompressor = HeaderSetDecompressor; +exports.Compressor = Compressor; +exports.Decompressor = Decompressor; + +var TransformStream = require('stream').Transform; +var assert = require('assert'); +var util = require('util'); + +// Header compression +// ================== + +// The HeaderTable class +// --------------------- + +// The [Header Table] is a component used to associate headers to index values. It is basically an +// ordered list of `[name, value]` pairs, so it's implemented as a subclass of `Array`. +// In this implementation, the Header Table and the [Static Table] are handled as a single table. +// [Header Table]: https://tools.ietf.org/html/rfc7541#section-2.3.2 +// [Static Table]: https://tools.ietf.org/html/rfc7541#section-2.3.1 +function HeaderTable(log, limit) { + var self = HeaderTable.staticTable.map(entryFromPair); + self._log = log; + self._limit = limit || DEFAULT_HEADER_TABLE_LIMIT; + self._staticLength = self.length; + self._size = 0; + self._enforceLimit = HeaderTable.prototype._enforceLimit; + self.add = HeaderTable.prototype.add; + self.setSizeLimit = HeaderTable.prototype.setSizeLimit; + return self; +} + +function entryFromPair(pair) { + var entry = pair.slice(); + entry._size = size(entry); + return entry; +} + +// The encoder decides how to update the header table and as such can control how much memory is +// used by the header table. To limit the memory requirements on the decoder side, the header table +// size is bounded. +// +// * The default header table size limit is 4096 bytes. +// * The size of an entry is defined as follows: the size of an entry is the sum of its name's +// length in bytes, of its value's length in bytes and of 32 bytes. +// * The size of a header table is the sum of the size of its entries. +var DEFAULT_HEADER_TABLE_LIMIT = 4096; + +function size(entry) { + return (Buffer.from(entry[0] + entry[1], 'utf8')).length + 32; +} + +// The `add(index, entry)` can be used to [manage the header table][tablemgmt]: +// [tablemgmt]: https://tools.ietf.org/html/rfc7541#section-4 +// +// * it pushes the new `entry` at the beggining of the table +// * before doing such a modification, it has to be ensured that the header table size will stay +// lower than or equal to the header table size limit. To achieve this, entries are evicted from +// the end of the header table until the size of the header table is less than or equal to +// `(this._limit - entry.size)`, or until the table is empty. +// +// <---------- Index Address Space ----------> +// <-- Static Table --> <-- Header Table --> +// +---+-----------+---+ +---+-----------+---+ +// | 0 | ... | k | |k+1| ... | n | +// +---+-----------+---+ +---+-----------+---+ +// ^ | +// | V +// Insertion Point Drop Point + +HeaderTable.prototype._enforceLimit = function _enforceLimit(limit) { + var droppedEntries = []; + while ((this._size > 0) && (this._size > limit)) { + var dropped = this.pop(); + this._size -= dropped._size; + droppedEntries.unshift(dropped); + } + return droppedEntries; +}; + +HeaderTable.prototype.add = function(entry) { + var limit = this._limit - entry._size; + var droppedEntries = this._enforceLimit(limit); + + if (this._size <= limit) { + this.splice(this._staticLength, 0, entry); + this._size += entry._size; + } + + return droppedEntries; +}; + +// The table size limit can be changed externally. In this case, the same eviction algorithm is used +HeaderTable.prototype.setSizeLimit = function setSizeLimit(limit) { + this._limit = limit; + this._enforceLimit(this._limit); +}; + +// [The Static Table](https://tools.ietf.org/html/rfc7541#section-2.3.1) +// ------------------ + +// The table is generated with feeding the table from the spec to the following sed command: +// +// sed -re "s/\s*\| [0-9]+\s*\| ([^ ]*)/ [ '\1'/g" -e "s/\|\s([^ ]*)/, '\1'/g" -e 's/ \|/],/g' + +HeaderTable.staticTable = [ + [ ':authority' , '' ], + [ ':method' , 'GET' ], + [ ':method' , 'POST' ], + [ ':path' , '/' ], + [ ':path' , '/index.html' ], + [ ':scheme' , 'http' ], + [ ':scheme' , 'https' ], + [ ':status' , '200' ], + [ ':status' , '204' ], + [ ':status' , '206' ], + [ ':status' , '304' ], + [ ':status' , '400' ], + [ ':status' , '404' ], + [ ':status' , '500' ], + [ 'accept-charset' , '' ], + [ 'accept-encoding' , 'gzip, deflate'], + [ 'accept-language' , '' ], + [ 'accept-ranges' , '' ], + [ 'accept' , '' ], + [ 'access-control-allow-origin' , '' ], + [ 'age' , '' ], + [ 'allow' , '' ], + [ 'authorization' , '' ], + [ 'cache-control' , '' ], + [ 'content-disposition' , '' ], + [ 'content-encoding' , '' ], + [ 'content-language' , '' ], + [ 'content-length' , '' ], + [ 'content-location' , '' ], + [ 'content-range' , '' ], + [ 'content-type' , '' ], + [ 'cookie' , '' ], + [ 'date' , '' ], + [ 'etag' , '' ], + [ 'expect' , '' ], + [ 'expires' , '' ], + [ 'from' , '' ], + [ 'host' , '' ], + [ 'if-match' , '' ], + [ 'if-modified-since' , '' ], + [ 'if-none-match' , '' ], + [ 'if-range' , '' ], + [ 'if-unmodified-since' , '' ], + [ 'last-modified' , '' ], + [ 'link' , '' ], + [ 'location' , '' ], + [ 'max-forwards' , '' ], + [ 'proxy-authenticate' , '' ], + [ 'proxy-authorization' , '' ], + [ 'range' , '' ], + [ 'referer' , '' ], + [ 'refresh' , '' ], + [ 'retry-after' , '' ], + [ 'server' , '' ], + [ 'set-cookie' , '' ], + [ 'strict-transport-security' , '' ], + [ 'transfer-encoding' , '' ], + [ 'user-agent' , '' ], + [ 'vary' , '' ], + [ 'via' , '' ], + [ 'www-authenticate' , '' ] +]; + +// The HeaderSetDecompressor class +// ------------------------------- + +// A `HeaderSetDecompressor` instance is a transform stream that can be used to *decompress a +// single header set*. Its input is a stream of binary data chunks and its output is a stream of +// `[name, value]` pairs. +// +// Currently, it is not a proper streaming decompressor implementation, since it buffer its input +// until the end os the stream, and then processes the whole header block at once. + +util.inherits(HeaderSetDecompressor, TransformStream); +function HeaderSetDecompressor(log, table) { + TransformStream.call(this, { objectMode: true }); + + this._log = log.child({ component: 'compressor' }); + this._table = table; + this._chunks = []; +} + +// `_transform` is the implementation of the [corresponding virtual function][_transform] of the +// TransformStream class. It collects the data chunks for later processing. +// [_transform]: https://nodejs.org/api/stream.html#stream_transform_transform_chunk_encoding_callback +HeaderSetDecompressor.prototype._transform = function _transform(chunk, encoding, callback) { + this._chunks.push(chunk); + callback(); +}; + +// `execute(rep)` executes the given [header representation][representation]. +// [representation]: https://tools.ietf.org/html/rfc7541#section-6 + +// The *JavaScript object representation* of a header representation: +// +// { +// name: String || Integer, // string literal or index +// value: String || Integer, // string literal or index +// index: Boolean // with or without indexing +// } +// +// *Important:* to ease the indexing of the header table, indexes start at 0 instead of 1. +// +// Examples: +// +// Indexed: +// { name: 2 , value: 2 , index: false } +// Literal: +// { name: 2 , value: 'X', index: false } // without indexing +// { name: 2 , value: 'Y', index: true } // with indexing +// { name: 'A', value: 'Z', index: true } // with indexing, literal name +HeaderSetDecompressor.prototype._execute = function _execute(rep) { + this._log.trace({ key: rep.name, value: rep.value, index: rep.index }, + 'Executing header representation'); + + var entry, pair; + + if (rep.contextUpdate) { + this._table.setSizeLimit(rep.newMaxSize); + } + + // * An _indexed representation_ entails the following actions: + // * The header field corresponding to the referenced entry is emitted + else if (typeof rep.value === 'number') { + var index = rep.value; + entry = this._table[index]; + + pair = entry.slice(); + this.push(pair); + } + + // * A _literal representation_ that is _not added_ to the header table entails the following + // action: + // * The header is emitted. + // * A _literal representation_ that is _added_ to the header table entails the following further + // actions: + // * The header is added to the header table. + // * The header is emitted. + else { + if (typeof rep.name === 'number') { + pair = [this._table[rep.name][0], rep.value]; + } else { + pair = [rep.name, rep.value]; + } + + if (rep.index) { + entry = entryFromPair(pair); + this._table.add(entry); + } + + this.push(pair); + } +}; + +// `_flush` is the implementation of the [corresponding virtual function][_flush] of the +// TransformStream class. The whole decompressing process is done in `_flush`. It gets called when +// the input stream is over. +// [_flush]: https://nodejs.org/api/stream.html#stream_transform_flush_callback +HeaderSetDecompressor.prototype._flush = function _flush(callback) { + var buffer = concat(this._chunks); + + // * processes the header representations + buffer.cursor = 0; + while (buffer.cursor < buffer.length) { + this._execute(HeaderSetDecompressor.header(buffer)); + } + + callback(); +}; + +// The HeaderSetCompressor class +// ----------------------------- + +// A `HeaderSetCompressor` instance is a transform stream that can be used to *compress a single +// header set*. Its input is a stream of `[name, value]` pairs and its output is a stream of +// binary data chunks. +// +// It is a real streaming compressor, since it does not wait until the header set is complete. +// +// The compression algorithm is (intentionally) not specified by the spec. Therefore, the current +// compression algorithm can probably be improved in the future. + +util.inherits(HeaderSetCompressor, TransformStream); +function HeaderSetCompressor(log, table) { + TransformStream.call(this, { objectMode: true }); + + this._log = log.child({ component: 'compressor' }); + this._table = table; + this.push = TransformStream.prototype.push.bind(this); +} + +HeaderSetCompressor.prototype.send = function send(rep) { + this._log.trace({ key: rep.name, value: rep.value, index: rep.index }, + 'Emitting header representation'); + + if (!rep.chunks) { + rep.chunks = HeaderSetCompressor.header(rep); + } + rep.chunks.forEach(this.push); +}; + +// `_transform` is the implementation of the [corresponding virtual function][_transform] of the +// TransformStream class. It processes the input headers one by one: +// [_transform]: https://nodejs.org/api/stream.html#stream_transform_transform_chunk_encoding_callback +HeaderSetCompressor.prototype._transform = function _transform(pair, encoding, callback) { + var name = pair[0].toLowerCase(); + var value = pair[1]; + var entry, rep; + + // * tries to find full (name, value) or name match in the header table + var nameMatch = -1, fullMatch = -1; + for (var droppedIndex = 0; droppedIndex < this._table.length; droppedIndex++) { + entry = this._table[droppedIndex]; + if (entry[0] === name) { + if (entry[1] === value) { + fullMatch = droppedIndex; + break; + } else if (nameMatch === -1) { + nameMatch = droppedIndex; + } + } + } + + var mustNeverIndex = ((name === 'cookie' && value.length < 20) || + (name === 'set-cookie' && value.length < 20) || + name === 'authorization'); + + if (fullMatch !== -1 && !mustNeverIndex) { + this.send({ name: fullMatch, value: fullMatch, index: false }); + } + + // * otherwise, it will be a literal representation (with a name index if there's a name match) + else { + entry = entryFromPair(pair); + + var indexing = (entry._size < this._table._limit / 2) && !mustNeverIndex; + + if (indexing) { + this._table.add(entry); + } + + this.send({ name: (nameMatch !== -1) ? nameMatch : name, value: value, index: indexing, mustNeverIndex: mustNeverIndex, contextUpdate: false }); + } + + callback(); +}; + +// `_flush` is the implementation of the [corresponding virtual function][_flush] of the +// TransformStream class. It gets called when there's no more header to compress. The final step: +// [_flush]: https://nodejs.org/api/stream.html#stream_transform_flush_callback +HeaderSetCompressor.prototype._flush = function _flush(callback) { + callback(); +}; + +// [Detailed Format](https://tools.ietf.org/html/rfc7541#section-5) +// ----------------- + +// ### Integer representation ### +// +// The algorithm to represent an integer I is as follows: +// +// 1. If I < 2^N - 1, encode I on N bits +// 2. Else, encode 2^N - 1 on N bits and do the following steps: +// 1. Set I to (I - (2^N - 1)) and Q to 1 +// 2. While Q > 0 +// 1. Compute Q and R, quotient and remainder of I divided by 2^7 +// 2. If Q is strictly greater than 0, write one 1 bit; otherwise, write one 0 bit +// 3. Encode R on the next 7 bits +// 4. I = Q + +HeaderSetCompressor.integer = function writeInteger(I, N) { + var limit = Math.pow(2,N) - 1; + if (I < limit) { + return [Buffer.from([I])]; + } + + var bytes = []; + if (N !== 0) { + bytes.push(limit); + } + I -= limit; + + var Q = 1, R; + while (Q > 0) { + Q = Math.floor(I / 128); + R = I % 128; + + if (Q > 0) { + R += 128; + } + bytes.push(R); + + I = Q; + } + + return [Buffer.from(bytes)]; +}; + +// The inverse algorithm: +// +// 1. Set I to the number coded on the lower N bits of the first byte +// 2. If I is smaller than 2^N - 1 then return I +// 2. Else the number is encoded on more than one byte, so do the following steps: +// 1. Set M to 0 +// 2. While returning with I +// 1. Let B be the next byte (the first byte if N is 0) +// 2. Read out the lower 7 bits of B and multiply it with 2^M +// 3. Increase I with this number +// 4. Increase M by 7 +// 5. Return I if the most significant bit of B is 0 + +HeaderSetDecompressor.integer = function readInteger(buffer, N) { + var limit = Math.pow(2,N) - 1; + + var I = buffer[buffer.cursor] & limit; + if (N !== 0) { + buffer.cursor += 1; + } + + if (I === limit) { + var M = 0; + do { + I += (buffer[buffer.cursor] & 127) << M; + M += 7; + buffer.cursor += 1; + } while (buffer[buffer.cursor - 1] & 128); + } + + return I; +}; + +// ### Huffman Encoding ### + +function HuffmanTable(table) { + function createTree(codes, position) { + if (codes.length === 1) { + return [table.indexOf(codes[0])]; + } + + else { + position = position || 0; + var zero = []; + var one = []; + for (var i = 0; i < codes.length; i++) { + var string = codes[i]; + if (string[position] === '0') { + zero.push(string); + } else { + one.push(string); + } + } + return [createTree(zero, position + 1), createTree(one, position + 1)]; + } + } + + this.tree = createTree(table); + + this.codes = table.map(function(bits) { + return parseInt(bits, 2); + }); + this.lengths = table.map(function(bits) { + return bits.length; + }); +} + +HuffmanTable.prototype.encode = function encode(buffer) { + var result = []; + var space = 8; + + function add(data) { + if (space === 8) { + result.push(data); + } else { + result[result.length - 1] |= data; + } + } + + for (var i = 0; i < buffer.length; i++) { + var byte = buffer[i]; + var code = this.codes[byte]; + var length = this.lengths[byte]; + + while (length !== 0) { + if (space >= length) { + add(code << (space - length)); + code = 0; + space -= length; + length = 0; + } else { + var shift = length - space; + var msb = code >> shift; + add(msb); + code -= msb << shift; + length -= space; + space = 0; + } + + if (space === 0) { + space = 8; + } + } + } + + if (space !== 8) { + add(this.codes[256] >> (this.lengths[256] - space)); + } + + return Buffer.from(result); +}; + +HuffmanTable.prototype.decode = function decode(buffer) { + var result = []; + var subtree = this.tree; + + for (var i = 0; i < buffer.length; i++) { + var byte = buffer[i]; + + for (var j = 0; j < 8; j++) { + var bit = (byte & 128) ? 1 : 0; + byte = byte << 1; + + subtree = subtree[bit]; + if (subtree.length === 1) { + result.push(subtree[0]); + subtree = this.tree; + } + } + } + + return Buffer.from(result); +}; + +// The initializer arrays for the Huffman tables are generated with feeding the tables from the +// spec to this sed command: +// +// sed -e "s/^.* [|]//g" -e "s/|//g" -e "s/ .*//g" -e "s/^/ '/g" -e "s/$/',/g" + +HuffmanTable.huffmanTable = new HuffmanTable([ + '1111111111000', + '11111111111111111011000', + '1111111111111111111111100010', + '1111111111111111111111100011', + '1111111111111111111111100100', + '1111111111111111111111100101', + '1111111111111111111111100110', + '1111111111111111111111100111', + '1111111111111111111111101000', + '111111111111111111101010', + '111111111111111111111111111100', + '1111111111111111111111101001', + '1111111111111111111111101010', + '111111111111111111111111111101', + '1111111111111111111111101011', + '1111111111111111111111101100', + '1111111111111111111111101101', + '1111111111111111111111101110', + '1111111111111111111111101111', + '1111111111111111111111110000', + '1111111111111111111111110001', + '1111111111111111111111110010', + '111111111111111111111111111110', + '1111111111111111111111110011', + '1111111111111111111111110100', + '1111111111111111111111110101', + '1111111111111111111111110110', + '1111111111111111111111110111', + '1111111111111111111111111000', + '1111111111111111111111111001', + '1111111111111111111111111010', + '1111111111111111111111111011', + '010100', + '1111111000', + '1111111001', + '111111111010', + '1111111111001', + '010101', + '11111000', + '11111111010', + '1111111010', + '1111111011', + '11111001', + '11111111011', + '11111010', + '010110', + '010111', + '011000', + '00000', + '00001', + '00010', + '011001', + '011010', + '011011', + '011100', + '011101', + '011110', + '011111', + '1011100', + '11111011', + '111111111111100', + '100000', + '111111111011', + '1111111100', + '1111111111010', + '100001', + '1011101', + '1011110', + '1011111', + '1100000', + '1100001', + '1100010', + '1100011', + '1100100', + '1100101', + '1100110', + '1100111', + '1101000', + '1101001', + '1101010', + '1101011', + '1101100', + '1101101', + '1101110', + '1101111', + '1110000', + '1110001', + '1110010', + '11111100', + '1110011', + '11111101', + '1111111111011', + '1111111111111110000', + '1111111111100', + '11111111111100', + '100010', + '111111111111101', + '00011', + '100011', + '00100', + '100100', + '00101', + '100101', + '100110', + '100111', + '00110', + '1110100', + '1110101', + '101000', + '101001', + '101010', + '00111', + '101011', + '1110110', + '101100', + '01000', + '01001', + '101101', + '1110111', + '1111000', + '1111001', + '1111010', + '1111011', + '111111111111110', + '11111111100', + '11111111111101', + '1111111111101', + '1111111111111111111111111100', + '11111111111111100110', + '1111111111111111010010', + '11111111111111100111', + '11111111111111101000', + '1111111111111111010011', + '1111111111111111010100', + '1111111111111111010101', + '11111111111111111011001', + '1111111111111111010110', + '11111111111111111011010', + '11111111111111111011011', + '11111111111111111011100', + '11111111111111111011101', + '11111111111111111011110', + '111111111111111111101011', + '11111111111111111011111', + '111111111111111111101100', + '111111111111111111101101', + '1111111111111111010111', + '11111111111111111100000', + '111111111111111111101110', + '11111111111111111100001', + '11111111111111111100010', + '11111111111111111100011', + '11111111111111111100100', + '111111111111111011100', + '1111111111111111011000', + '11111111111111111100101', + '1111111111111111011001', + '11111111111111111100110', + '11111111111111111100111', + '111111111111111111101111', + '1111111111111111011010', + '111111111111111011101', + '11111111111111101001', + '1111111111111111011011', + '1111111111111111011100', + '11111111111111111101000', + '11111111111111111101001', + '111111111111111011110', + '11111111111111111101010', + '1111111111111111011101', + '1111111111111111011110', + '111111111111111111110000', + '111111111111111011111', + '1111111111111111011111', + '11111111111111111101011', + '11111111111111111101100', + '111111111111111100000', + '111111111111111100001', + '1111111111111111100000', + '111111111111111100010', + '11111111111111111101101', + '1111111111111111100001', + '11111111111111111101110', + '11111111111111111101111', + '11111111111111101010', + '1111111111111111100010', + '1111111111111111100011', + '1111111111111111100100', + '11111111111111111110000', + '1111111111111111100101', + '1111111111111111100110', + '11111111111111111110001', + '11111111111111111111100000', + '11111111111111111111100001', + '11111111111111101011', + '1111111111111110001', + '1111111111111111100111', + '11111111111111111110010', + '1111111111111111101000', + '1111111111111111111101100', + '11111111111111111111100010', + '11111111111111111111100011', + '11111111111111111111100100', + '111111111111111111111011110', + '111111111111111111111011111', + '11111111111111111111100101', + '111111111111111111110001', + '1111111111111111111101101', + '1111111111111110010', + '111111111111111100011', + '11111111111111111111100110', + '111111111111111111111100000', + '111111111111111111111100001', + '11111111111111111111100111', + '111111111111111111111100010', + '111111111111111111110010', + '111111111111111100100', + '111111111111111100101', + '11111111111111111111101000', + '11111111111111111111101001', + '1111111111111111111111111101', + '111111111111111111111100011', + '111111111111111111111100100', + '111111111111111111111100101', + '11111111111111101100', + '111111111111111111110011', + '11111111111111101101', + '111111111111111100110', + '1111111111111111101001', + '111111111111111100111', + '111111111111111101000', + '11111111111111111110011', + '1111111111111111101010', + '1111111111111111101011', + '1111111111111111111101110', + '1111111111111111111101111', + '111111111111111111110100', + '111111111111111111110101', + '11111111111111111111101010', + '11111111111111111110100', + '11111111111111111111101011', + '111111111111111111111100110', + '11111111111111111111101100', + '11111111111111111111101101', + '111111111111111111111100111', + '111111111111111111111101000', + '111111111111111111111101001', + '111111111111111111111101010', + '111111111111111111111101011', + '1111111111111111111111111110', + '111111111111111111111101100', + '111111111111111111111101101', + '111111111111111111111101110', + '111111111111111111111101111', + '111111111111111111111110000', + '11111111111111111111101110', + '111111111111111111111111111111' +]); + +// ### String literal representation ### +// +// Literal **strings** can represent header names or header values. There's two variant of the +// string encoding: +// +// String literal with Huffman encoding: +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 1 | Value Length Prefix (7) | +// +---+---+---+---+---+---+---+---+ +// | Value Length (0-N bytes) | +// +---+---+---+---+---+---+---+---+ +// ... +// +---+---+---+---+---+---+---+---+ +// | Huffman Encoded Data |Padding| +// +---+---+---+---+---+---+---+---+ +// +// String literal without Huffman encoding: +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 0 | Value Length Prefix (7) | +// +---+---+---+---+---+---+---+---+ +// | Value Length (0-N bytes) | +// +---+---+---+---+---+---+---+---+ +// ... +// +---+---+---+---+---+---+---+---+ +// | Field Bytes Without Encoding | +// +---+---+---+---+---+---+---+---+ + +HeaderSetCompressor.string = function writeString(str) { + str = Buffer.from(str, 'utf8'); + + var huffman = HuffmanTable.huffmanTable.encode(str); + if (huffman.length < str.length) { + var length = HeaderSetCompressor.integer(huffman.length, 7); + length[0][0] |= 128; + return length.concat(huffman); + } + + else { + length = HeaderSetCompressor.integer(str.length, 7); + return length.concat(str); + } +}; + +HeaderSetDecompressor.string = function readString(buffer) { + var huffman = buffer[buffer.cursor] & 128; + var length = HeaderSetDecompressor.integer(buffer, 7); + var encoded = buffer.slice(buffer.cursor, buffer.cursor + length); + buffer.cursor += length; + return (huffman ? HuffmanTable.huffmanTable.decode(encoded) : encoded).toString('utf8'); +}; + +// ### Header represenations ### + +// The JavaScript object representation is described near the +// `HeaderSetDecompressor.prototype._execute()` method definition. +// +// **All binary header representations** start with a prefix signaling the representation type and +// an index represented using prefix coded integers: +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 1 | Index (7+) | Indexed Representation +// +---+---------------------------+ +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 0 | 1 | Index (6+) | +// +---+---+---+-------------------+ Literal w/ Indexing +// | Value Length (8+) | +// +-------------------------------+ w/ Indexed Name +// | Value String (Length octets) | +// +-------------------------------+ +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 0 | 1 | 0 | +// +---+---+---+-------------------+ +// | Name Length (8+) | +// +-------------------------------+ Literal w/ Indexing +// | Name String (Length octets) | +// +-------------------------------+ w/ New Name +// | Value Length (8+) | +// +-------------------------------+ +// | Value String (Length octets) | +// +-------------------------------+ +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 0 | 0 | 0 | 0 | Index (4+) | +// +---+---+---+-------------------+ Literal w/o Incremental Indexing +// | Value Length (8+) | +// +-------------------------------+ w/ Indexed Name +// | Value String (Length octets) | +// +-------------------------------+ +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 0 | 0 | 0 | 0 | 0 | +// +---+---+---+-------------------+ +// | Name Length (8+) | +// +-------------------------------+ Literal w/o Incremental Indexing +// | Name String (Length octets) | +// +-------------------------------+ w/ New Name +// | Value Length (8+) | +// +-------------------------------+ +// | Value String (Length octets) | +// +-------------------------------+ +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 0 | 0 | 0 | 1 | Index (4+) | +// +---+---+---+-------------------+ Literal never indexed +// | Value Length (8+) | +// +-------------------------------+ w/ Indexed Name +// | Value String (Length octets) | +// +-------------------------------+ +// +// 0 1 2 3 4 5 6 7 +// +---+---+---+---+---+---+---+---+ +// | 0 | 0 | 0 | 1 | 0 | +// +---+---+---+-------------------+ +// | Name Length (8+) | +// +-------------------------------+ Literal never indexed +// | Name String (Length octets) | +// +-------------------------------+ w/ New Name +// | Value Length (8+) | +// +-------------------------------+ +// | Value String (Length octets) | +// +-------------------------------+ +// +// The **Indexed Representation** consists of the 1-bit prefix and the Index that is represented as +// a 7-bit prefix coded integer and nothing else. +// +// After the first bits, **all literal representations** specify the header name, either as a +// pointer to the Header Table (Index) or a string literal. When the string literal representation +// is used, the Index is set to 0 and the string literal starts at the second byte. +// +// For **all literal representations**, the specification of the header value comes next. It is +// always represented as a string. + +var representations = { + indexed : { prefix: 7, pattern: 0x80 }, + literalIncremental : { prefix: 6, pattern: 0x40 }, + contextUpdate : { prefix: 0, pattern: 0x20 }, + literalNeverIndexed : { prefix: 4, pattern: 0x10 }, + literal : { prefix: 4, pattern: 0x00 } +}; + +HeaderSetCompressor.header = function writeHeader(header) { + var representation, buffers = []; + + if (header.contextUpdate) { + representation = representations.contextUpdate; + } else if (typeof header.value === 'number') { + representation = representations.indexed; + } else if (header.index) { + representation = representations.literalIncremental; + } else if (header.mustNeverIndex) { + representation = representations.literalNeverIndexed; + } else { + representation = representations.literal; + } + + if (representation === representations.contextUpdate) { + buffers.push(HeaderSetCompressor.integer(header.newMaxSize, 5)); + } + + else if (representation === representations.indexed) { + buffers.push(HeaderSetCompressor.integer(header.value + 1, representation.prefix)); + } + + else { + if (typeof header.name === 'number') { + buffers.push(HeaderSetCompressor.integer(header.name + 1, representation.prefix)); + } else { + buffers.push(HeaderSetCompressor.integer(0, representation.prefix)); + buffers.push(HeaderSetCompressor.string(header.name)); + } + buffers.push(HeaderSetCompressor.string(header.value)); + } + + buffers[0][0][0] |= representation.pattern; + + return Array.prototype.concat.apply([], buffers); // array of arrays of buffers -> array of buffers +}; + +HeaderSetDecompressor.header = function readHeader(buffer) { + var representation, header = {}; + + var firstByte = buffer[buffer.cursor]; + if (firstByte & 0x80) { + representation = representations.indexed; + } else if (firstByte & 0x40) { + representation = representations.literalIncremental; + } else if (firstByte & 0x20) { + representation = representations.contextUpdate; + } else if (firstByte & 0x10) { + representation = representations.literalNeverIndexed; + } else { + representation = representations.literal; + } + + header.value = header.name = -1; + header.index = false; + header.contextUpdate = false; + header.newMaxSize = 0; + header.mustNeverIndex = false; + + if (representation === representations.contextUpdate) { + header.contextUpdate = true; + header.newMaxSize = HeaderSetDecompressor.integer(buffer, 5); + } + + else if (representation === representations.indexed) { + header.value = header.name = HeaderSetDecompressor.integer(buffer, representation.prefix) - 1; + } + + else { + header.name = HeaderSetDecompressor.integer(buffer, representation.prefix) - 1; + if (header.name === -1) { + header.name = HeaderSetDecompressor.string(buffer); + } + header.value = HeaderSetDecompressor.string(buffer); + header.index = (representation === representations.literalIncremental); + header.mustNeverIndex = (representation === representations.literalNeverIndexed); + } + + return header; +}; + +// Integration with HTTP/2 +// ======================= + +// This section describes the interaction between the compressor/decompressor and the rest of the +// HTTP/2 implementation. The `Compressor` and the `Decompressor` makes up a layer between the +// [framer](framer.html) and the [connection handling component](connection.html). They let most +// frames pass through, except HEADERS and PUSH_PROMISE frames. They convert the frames between +// these two representations: +// +// { { +// type: 'HEADERS', type: 'HEADERS', +// flags: {}, flags: {}, +// stream: 1, <===> stream: 1, +// headers: { data: Buffer +// N1: 'V1', } +// N2: ['V1', 'V2', ...], +// // ... +// } +// } +// +// There are possibly several binary frame that belong to a single non-binary frame. + +var MAX_HTTP_PAYLOAD_SIZE = 16384; + +// The Compressor class +// -------------------- + +// The Compressor transform stream is basically stateless. +util.inherits(Compressor, TransformStream); +function Compressor(log, type) { + TransformStream.call(this, { objectMode: true }); + + this._log = log.child({ component: 'compressor' }); + + assert((type === 'REQUEST') || (type === 'RESPONSE')); + this._table = new HeaderTable(this._log); + + this.tableSizeChangePending = false; + this.lowestTableSizePending = 0; + this.tableSizeSetting = DEFAULT_HEADER_TABLE_LIMIT; +} + +// Changing the header table size +Compressor.prototype.setTableSizeLimit = function setTableSizeLimit(size) { + this._table.setSizeLimit(size); + if (!this.tableSizeChangePending || size < this.lowestTableSizePending) { + this.lowestTableSizePending = size; + } + this.tableSizeSetting = size; + this.tableSizeChangePending = true; +}; + +// `compress` takes a header set, and compresses it using a new `HeaderSetCompressor` stream +// instance. This means that from now on, the advantages of streaming header encoding are lost, +// but the API becomes simpler. +Compressor.prototype.compress = function compress(headers) { + var compressor = new HeaderSetCompressor(this._log, this._table); + + if (this.tableSizeChangePending) { + if (this.lowestTableSizePending < this.tableSizeSetting) { + compressor.send({contextUpdate: true, newMaxSize: this.lowestTableSizePending, + name: "", value: "", index: 0}); + } + compressor.send({contextUpdate: true, newMaxSize: this.tableSizeSetting, + name: "", value: "", index: 0}); + this.tableSizeChangePending = false; + } + var colonHeaders = []; + var nonColonHeaders = []; + + // To ensure we send colon headers first + for (var name in headers) { + if (name.trim()[0] === ':') { + colonHeaders.push(name); + } else { + nonColonHeaders.push(name); + } + } + + function compressHeader(name) { + var value = headers[name]; + name = String(name).toLowerCase(); + + // * To allow for better compression efficiency, the Cookie header field MAY be split into + // separate header fields, each with one or more cookie-pairs. + if (name == 'cookie') { + if (!(value instanceof Array)) { + value = [value]; + } + value = Array.prototype.concat.apply([], value.map(function(cookie) { + return String(cookie).split(';').map(trim); + })); + } + + if (value instanceof Array) { + for (var i = 0; i < value.length; i++) { + compressor.write([name, String(value[i])]); + } + } else { + compressor.write([name, String(value)]); + } + } + + colonHeaders.forEach(compressHeader); + nonColonHeaders.forEach(compressHeader); + + compressor.end(); + + var chunk, chunks = []; + while (chunk = compressor.read()) { + chunks.push(chunk); + } + + function insertSoftIllegalHpack(originalCompressed) { + var illegalLiteral = Buffer.from([ + 0x00, // Literal, no index + 0x08, // Name: not huffman encoded, 8 bytes long + 0x3a, + 0x69, + 0x6c, + 0x6c, + 0x65, + 0x67, + 0x61, + 0x6c, // :illegal + 0x10, // Value: not huffman encoded, 16 bytes long + // REALLY NOT LEGAL + 0x52, + 0x45, + 0x41, + 0x4c, + 0x4c, + 0x59, + 0x20, + 0x4e, + 0x4f, + 0x54, + 0x20, + 0x4c, + 0x45, + 0x47, + 0x41, + 0x4c, + ]); + var newBufferLength = originalCompressed.length + illegalLiteral.length; + var concatenated = Buffer.alloc(newBufferLength); + originalCompressed.copy(concatenated, 0); + illegalLiteral.copy(concatenated, originalCompressed.length); + return concatenated; + } + + function insertHardIllegalHpack(originalCompressed) { + // Now we have to add an invalid header + var illegalIndexed = HeaderSetCompressor.integer(5000, 7); + // The above returns an array of buffers, but there's only one buffer, so + // get rid of the array. + illegalIndexed = illegalIndexed[0]; + // Set the first bit to 1 to signal this is an indexed representation + illegalIndexed[0] |= 0x80; + var newBufferLength = originalCompressed.length + illegalIndexed.length; + var concatenated = Buffer.alloc(newBufferLength); + originalCompressed.copy(concatenated, 0); + illegalIndexed.copy(concatenated, originalCompressed.length); + return concatenated; + } + + if ("x-softillegalhpack" in headers) { + return insertSoftIllegalHpack(concat(chunks)); + } + + if ("x-hardillegalhpack" in headers) { + return insertHardIllegalHpack(concat(chunks)); + } + + return concat(chunks); +}; + +// When a `frame` arrives +Compressor.prototype._transform = function _transform(frame, encoding, done) { + // * and it is a HEADERS or PUSH_PROMISE frame + // * it generates a header block using the compress method + // * cuts the header block into `chunks` that are not larger than `MAX_HTTP_PAYLOAD_SIZE` + // * for each chunk, it pushes out a chunk frame that is identical to the original, except + // the `data` property which holds the given chunk, the type of the frame which is always + // CONTINUATION except for the first frame, and the END_HEADERS/END_PUSH_STREAM flag that + // marks the last frame and the END_STREAM flag which is always false before the end + if (frame.type === 'HEADERS' || frame.type === 'PUSH_PROMISE') { + var buffer = this.compress(frame.headers); + + // This will result in CONTINUATIONs from a PUSH_PROMISE being 4 bytes shorter than they could + // be, but that's not the end of the world, and it prevents us from going over MAX_HTTP_PAYLOAD_SIZE + // on the initial PUSH_PROMISE frame. + var adjustment = frame.type === 'PUSH_PROMISE' ? 4 : 0; + var chunks = cut(buffer, MAX_HTTP_PAYLOAD_SIZE - adjustment); + + for (var i = 0; i < chunks.length; i++) { + var chunkFrame; + var first = (i === 0); + var last = (i === chunks.length - 1); + + if (first) { + chunkFrame = util._extend({}, frame); + chunkFrame.flags = util._extend({}, frame.flags); + chunkFrame.flags['END_' + frame.type] = last; + } else { + chunkFrame = { + type: 'CONTINUATION', + flags: { END_HEADERS: last }, + stream: frame.stream + }; + } + chunkFrame.data = chunks[i]; + + this.push(chunkFrame); + } + } + + // * otherwise, the frame is forwarded without taking any action + else { + this.push(frame); + } + + done(); +}; + +// The Decompressor class +// ---------------------- + +// The Decompressor is a stateful transform stream, since it has to collect multiple frames first, +// and the decoding comes after unifying the payload of those frames. +// +// If there's a frame in progress, `this._inProgress` is `true`. The frames are collected in +// `this._frames`, and the type of the frame and the stream identifier is stored in `this._type` +// and `this._stream` respectively. +util.inherits(Decompressor, TransformStream); +function Decompressor(log, type) { + TransformStream.call(this, { objectMode: true }); + + this._log = log.child({ component: 'compressor' }); + + assert((type === 'REQUEST') || (type === 'RESPONSE')); + this._table = new HeaderTable(this._log); + + this._inProgress = false; + this._base = undefined; +} + +// Changing the header table size +Decompressor.prototype.setTableSizeLimit = function setTableSizeLimit(size) { + this._table.setSizeLimit(size); +}; + +// `decompress` takes a full header block, and decompresses it using a new `HeaderSetDecompressor` +// stream instance. This means that from now on, the advantages of streaming header decoding are +// lost, but the API becomes simpler. +Decompressor.prototype.decompress = function decompress(block) { + var decompressor = new HeaderSetDecompressor(this._log, this._table); + decompressor.end(block); + + var seenNonColonHeader = false; + var headers = {}; + var pair; + while (pair = decompressor.read()) { + var name = pair[0]; + var value = pair[1]; + var isColonHeader = (name.trim()[0] === ':'); + if (seenNonColonHeader && isColonHeader) { + this.emit('error', 'PROTOCOL_ERROR'); + return headers; + } + seenNonColonHeader = !isColonHeader; + if (name in headers) { + if (headers[name] instanceof Array) { + headers[name].push(value); + } else { + headers[name] = [headers[name], value]; + } + } else { + headers[name] = value; + } + } + + // * If there are multiple Cookie header fields after decompression, these MUST be concatenated + // into a single octet string using the two octet delimiter of 0x3B, 0x20 (the ASCII + // string "; "). + if (('cookie' in headers) && (headers['cookie'] instanceof Array)) { + headers['cookie'] = headers['cookie'].join('; '); + } + + return headers; +}; + +// When a `frame` arrives +Decompressor.prototype._transform = function _transform(frame, encoding, done) { + // * and the collection process is already `_inProgress`, the frame is simply stored, except if + // it's an illegal frame + if (this._inProgress) { + if ((frame.type !== 'CONTINUATION') || (frame.stream !== this._base.stream)) { + this._log.error('A series of HEADER frames were not continuous'); + this.emit('error', 'PROTOCOL_ERROR'); + return; + } + this._frames.push(frame); + } + + // * and the collection process is not `_inProgress`, but the new frame's type is HEADERS or + // PUSH_PROMISE, a new collection process begins + else if ((frame.type === 'HEADERS') || (frame.type === 'PUSH_PROMISE')) { + this._inProgress = true; + this._base = util._extend({}, frame); + this._frames = [frame]; + } + + // * otherwise, the frame is forwarded without taking any action + else { + this.push(frame); + } + + // * When the frame signals that it's the last in the series, the header block chunks are + // concatenated, the headers are decompressed, and a new frame gets pushed out with the + // decompressed headers. + if (this._inProgress && (frame.flags.END_HEADERS || frame.flags.END_PUSH_PROMISE)) { + var buffer = concat(this._frames.map(function(frame) { + return frame.data; + })); + try { + var headers = this.decompress(buffer); + } catch(error) { + this._log.error({ err: error }, 'Header decompression error'); + this.emit('error', 'COMPRESSION_ERROR'); + return; + } + this.push(util._extend(this._base, { headers: headers })); + this._inProgress = false; + } + + done(); +}; + +// Helper functions +// ================ + +// Concatenate an array of buffers into a new buffer +function concat(buffers) { + var size = 0; + for (var i = 0; i < buffers.length; i++) { + size += buffers[i].length; + } + + var concatenated = Buffer.alloc(size); + for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) { + buffers[j].copy(concatenated, cursor); + } + + return concatenated; +} + +// Cut `buffer` into chunks not larger than `size` +function cut(buffer, size) { + var chunks = []; + var cursor = 0; + do { + var chunkSize = Math.min(size, buffer.length - cursor); + chunks.push(buffer.slice(cursor, cursor + chunkSize)); + cursor += chunkSize; + } while(cursor < buffer.length); + return chunks; +} + +function trim(string) { + return string.trim(); +} |