Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | // The implementation of the [HTTP/2 Header Compression][http2-compression] spec is separated from |
michael@0 | 2 | // the 'integration' part which handles HEADERS and PUSH_PROMISE frames. The compression itself is |
michael@0 | 3 | // implemented in the first part of the file, and consists of three classes: `HeaderTable`, |
michael@0 | 4 | // `HeaderSetDecompressor` and `HeaderSetCompressor`. The two latter classes are |
michael@0 | 5 | // [Transform Stream][node-transform] subclasses that operate in [object mode][node-objectmode]. |
michael@0 | 6 | // These transform chunks of binary data into `[name, value]` pairs and vice versa, and store their |
michael@0 | 7 | // state in `HeaderTable` instances. |
michael@0 | 8 | // |
michael@0 | 9 | // The 'integration' part is also implemented by two [Transform Stream][node-transform] subclasses |
michael@0 | 10 | // that operate in [object mode][node-objectmode]: the `Compressor` and the `Decompressor`. These |
michael@0 | 11 | // provide a layer between the [framer](framer.html) and the |
michael@0 | 12 | // [connection handling component](connection.html). |
michael@0 | 13 | // |
michael@0 | 14 | // [node-transform]: http://nodejs.org/api/stream.html#stream_class_stream_transform |
michael@0 | 15 | // [node-objectmode]: http://nodejs.org/api/stream.html#stream_new_stream_readable_options |
michael@0 | 16 | // [http2-compression]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05 |
michael@0 | 17 | |
michael@0 | 18 | exports.HeaderTable = HeaderTable; |
michael@0 | 19 | exports.HuffmanTable = HuffmanTable; |
michael@0 | 20 | exports.HeaderSetCompressor = HeaderSetCompressor; |
michael@0 | 21 | exports.HeaderSetDecompressor = HeaderSetDecompressor; |
michael@0 | 22 | exports.Compressor = Compressor; |
michael@0 | 23 | exports.Decompressor = Decompressor; |
michael@0 | 24 | |
michael@0 | 25 | var TransformStream = require('stream').Transform; |
michael@0 | 26 | var assert = require('assert'); |
michael@0 | 27 | var util = require('util'); |
michael@0 | 28 | |
michael@0 | 29 | // Header compression |
michael@0 | 30 | // ================== |
michael@0 | 31 | |
michael@0 | 32 | // The HeaderTable class |
michael@0 | 33 | // --------------------- |
michael@0 | 34 | |
michael@0 | 35 | // The [Header Table] is a component used to associate headers to index values. It is basically an |
michael@0 | 36 | // ordered list of `[name, value]` pairs, so it's implemented as a subclass of `Array`. |
michael@0 | 37 | // In this implementation, the Header Table and the [Static Table] are handled as a single table. |
michael@0 | 38 | // [Header Table]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-3.1.2 |
michael@0 | 39 | // [Static Table]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-B |
michael@0 | 40 | function HeaderTable(log, limit) { |
michael@0 | 41 | var self = HeaderTable.staticTable.map(entryFromPair); |
michael@0 | 42 | self._log = log; |
michael@0 | 43 | self._limit = limit || DEFAULT_HEADER_TABLE_LIMIT; |
michael@0 | 44 | self._staticLength = self.length; |
michael@0 | 45 | self._size = 0; |
michael@0 | 46 | self._enforceLimit = HeaderTable.prototype._enforceLimit; |
michael@0 | 47 | self.add = HeaderTable.prototype.add; |
michael@0 | 48 | self.setSizeLimit = HeaderTable.prototype.setSizeLimit; |
michael@0 | 49 | return self; |
michael@0 | 50 | } |
michael@0 | 51 | |
michael@0 | 52 | // There are few more sets that are needed for the compression/decompression process that are all |
michael@0 | 53 | // subsets of the Header Table, and are implemented as flags on header table entries: |
michael@0 | 54 | // |
michael@0 | 55 | // * [Reference Set][referenceset]: contains a group of headers used as a reference for the |
michael@0 | 56 | // differential encoding of a new set of headers. (`reference` flag) |
michael@0 | 57 | // * Emitted headers: the headers that are already emitted as part of the current decompression |
michael@0 | 58 | // process (not part of the spec, `emitted` flag) |
michael@0 | 59 | // * Headers to be kept: headers that should not be removed as the last step of the encoding process |
michael@0 | 60 | // (not part of the spec, `keep` flag) |
michael@0 | 61 | // |
michael@0 | 62 | // [referenceset]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-3.1.3 |
michael@0 | 63 | // |
michael@0 | 64 | // Relations of the sets: |
michael@0 | 65 | // |
michael@0 | 66 | // ,----------------------------------. |
michael@0 | 67 | // | Header Table | |
michael@0 | 68 | // | | |
michael@0 | 69 | // | ,----------------------------. | |
michael@0 | 70 | // | | Reference Set | | |
michael@0 | 71 | // | | | | |
michael@0 | 72 | // | | ,---------. ,---------. | | |
michael@0 | 73 | // | | | Keep | | Emitted | | | |
michael@0 | 74 | // | | | | | | | | |
michael@0 | 75 | // | | `---------' `---------' | | |
michael@0 | 76 | // | `----------------------------' | |
michael@0 | 77 | // `----------------------------------' |
michael@0 | 78 | function entryFromPair(pair) { |
michael@0 | 79 | var entry = pair.slice(); |
michael@0 | 80 | entry.reference = false; |
michael@0 | 81 | entry.emitted = false; |
michael@0 | 82 | entry.keep = false; |
michael@0 | 83 | entry._size = size(entry); |
michael@0 | 84 | return entry; |
michael@0 | 85 | } |
michael@0 | 86 | |
michael@0 | 87 | // The encoder decides how to update the header table and as such can control how much memory is |
michael@0 | 88 | // used by the header table. To limit the memory requirements on the decoder side, the header table |
michael@0 | 89 | // size is bounded. |
michael@0 | 90 | // |
michael@0 | 91 | // * The default header table size limit is 4096 bytes. |
michael@0 | 92 | // * The size of an entry is defined as follows: the size of an entry is the sum of its name's |
michael@0 | 93 | // length in bytes, of its value's length in bytes and of 32 bytes. |
michael@0 | 94 | // * The size of a header table is the sum of the size of its entries. |
michael@0 | 95 | var DEFAULT_HEADER_TABLE_LIMIT = 4096; |
michael@0 | 96 | |
michael@0 | 97 | function size(entry) { |
michael@0 | 98 | return (new Buffer(entry[0] + entry[1], 'utf8')).length + 32; |
michael@0 | 99 | } |
michael@0 | 100 | |
michael@0 | 101 | // The `add(index, entry)` can be used to [manage the header table][tablemgmt]: |
michael@0 | 102 | // [tablemgmt]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-3.3 |
michael@0 | 103 | // |
michael@0 | 104 | // * it pushes the new `entry` at the beggining of the table |
michael@0 | 105 | // * before doing such a modification, it has to be ensured that the header table size will stay |
michael@0 | 106 | // lower than or equal to the header table size limit. To achieve this, entries are evicted from |
michael@0 | 107 | // the end of the header table until the size of the header table is less than or equal to |
michael@0 | 108 | // `(this._limit - entry.size)`, or until the table is empty. |
michael@0 | 109 | // |
michael@0 | 110 | // <---------- Index Address Space ----------> |
michael@0 | 111 | // <-- Header Table --> <-- Static Table --> |
michael@0 | 112 | // +---+-----------+---+ +---+-----------+---+ |
michael@0 | 113 | // | 0 | ... | k | |k+1| ... | n | |
michael@0 | 114 | // +---+-----------+---+ +---+-----------+---+ |
michael@0 | 115 | // ^ | |
michael@0 | 116 | // | V |
michael@0 | 117 | // Insertion Point Drop Point |
michael@0 | 118 | |
michael@0 | 119 | HeaderTable.prototype._enforceLimit = function _enforceLimit(limit) { |
michael@0 | 120 | var droppedEntries = []; |
michael@0 | 121 | var dropPoint = this.length - this._staticLength; |
michael@0 | 122 | while ((this._size > limit) && (dropPoint > 0)) { |
michael@0 | 123 | dropPoint -= 1; |
michael@0 | 124 | var dropped = this.splice(dropPoint, 1)[0]; |
michael@0 | 125 | this._size -= dropped._size; |
michael@0 | 126 | droppedEntries[dropPoint] = dropped; |
michael@0 | 127 | } |
michael@0 | 128 | return droppedEntries; |
michael@0 | 129 | }; |
michael@0 | 130 | |
michael@0 | 131 | HeaderTable.prototype.add = function(entry) { |
michael@0 | 132 | var limit = this._limit - entry._size; |
michael@0 | 133 | var droppedEntries = this._enforceLimit(limit); |
michael@0 | 134 | |
michael@0 | 135 | if (this._size <= limit) { |
michael@0 | 136 | this.unshift(entry); |
michael@0 | 137 | this._size += entry._size; |
michael@0 | 138 | } |
michael@0 | 139 | |
michael@0 | 140 | return droppedEntries; |
michael@0 | 141 | }; |
michael@0 | 142 | |
michael@0 | 143 | // The table size limit can be changed externally. In this case, the same eviction algorithm is used |
michael@0 | 144 | HeaderTable.prototype.setSizeLimit = function setSizeLimit(limit) { |
michael@0 | 145 | this._limit = limit; |
michael@0 | 146 | this._enforceLimit(this._limit); |
michael@0 | 147 | }; |
michael@0 | 148 | |
michael@0 | 149 | // [The Static Table](http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-B) |
michael@0 | 150 | // ------------------ |
michael@0 | 151 | // [statictable]:http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#appendix-B |
michael@0 | 152 | |
michael@0 | 153 | // The table is generated with feeding the table from the spec to the following sed command: |
michael@0 | 154 | // |
michael@0 | 155 | // sed -re "s/\s*\| [0-9]+\s*\| ([^ ]*)/ [ '\1'/g" -e "s/\|\s([^ ]*)/, '\1'/g" -e 's/ \|/],/g' |
michael@0 | 156 | |
michael@0 | 157 | HeaderTable.staticTable = [ |
michael@0 | 158 | [ ':authority' , '' ], |
michael@0 | 159 | [ ':method' , 'GET' ], |
michael@0 | 160 | [ ':method' , 'POST' ], |
michael@0 | 161 | [ ':path' , '/' ], |
michael@0 | 162 | [ ':path' , '/index.html' ], |
michael@0 | 163 | [ ':scheme' , 'http' ], |
michael@0 | 164 | [ ':scheme' , 'https' ], |
michael@0 | 165 | [ ':status' , '200' ], |
michael@0 | 166 | [ ':status' , '500' ], |
michael@0 | 167 | [ ':status' , '404' ], |
michael@0 | 168 | [ ':status' , '403' ], |
michael@0 | 169 | [ ':status' , '400' ], |
michael@0 | 170 | [ ':status' , '401' ], |
michael@0 | 171 | [ 'accept-charset' , '' ], |
michael@0 | 172 | [ 'accept-encoding' , '' ], |
michael@0 | 173 | [ 'accept-language' , '' ], |
michael@0 | 174 | [ 'accept-ranges' , '' ], |
michael@0 | 175 | [ 'accept' , '' ], |
michael@0 | 176 | [ 'access-control-allow-origin' , '' ], |
michael@0 | 177 | [ 'age' , '' ], |
michael@0 | 178 | [ 'allow' , '' ], |
michael@0 | 179 | [ 'authorization' , '' ], |
michael@0 | 180 | [ 'cache-control' , '' ], |
michael@0 | 181 | [ 'content-disposition' , '' ], |
michael@0 | 182 | [ 'content-encoding' , '' ], |
michael@0 | 183 | [ 'content-language' , '' ], |
michael@0 | 184 | [ 'content-length' , '' ], |
michael@0 | 185 | [ 'content-location' , '' ], |
michael@0 | 186 | [ 'content-range' , '' ], |
michael@0 | 187 | [ 'content-type' , '' ], |
michael@0 | 188 | [ 'cookie' , '' ], |
michael@0 | 189 | [ 'date' , '' ], |
michael@0 | 190 | [ 'etag' , '' ], |
michael@0 | 191 | [ 'expect' , '' ], |
michael@0 | 192 | [ 'expires' , '' ], |
michael@0 | 193 | [ 'from' , '' ], |
michael@0 | 194 | [ 'host' , '' ], |
michael@0 | 195 | [ 'if-match' , '' ], |
michael@0 | 196 | [ 'if-modified-since' , '' ], |
michael@0 | 197 | [ 'if-none-match' , '' ], |
michael@0 | 198 | [ 'if-range' , '' ], |
michael@0 | 199 | [ 'if-unmodified-since' , '' ], |
michael@0 | 200 | [ 'last-modified' , '' ], |
michael@0 | 201 | [ 'link' , '' ], |
michael@0 | 202 | [ 'location' , '' ], |
michael@0 | 203 | [ 'max-forwards' , '' ], |
michael@0 | 204 | [ 'proxy-authenticate' , '' ], |
michael@0 | 205 | [ 'proxy-authorization' , '' ], |
michael@0 | 206 | [ 'range' , '' ], |
michael@0 | 207 | [ 'referer' , '' ], |
michael@0 | 208 | [ 'refresh' , '' ], |
michael@0 | 209 | [ 'retry-after' , '' ], |
michael@0 | 210 | [ 'server' , '' ], |
michael@0 | 211 | [ 'set-cookie' , '' ], |
michael@0 | 212 | [ 'strict-transport-security' , '' ], |
michael@0 | 213 | [ 'transfer-encoding' , '' ], |
michael@0 | 214 | [ 'user-agent' , '' ], |
michael@0 | 215 | [ 'vary' , '' ], |
michael@0 | 216 | [ 'via' , '' ], |
michael@0 | 217 | [ 'www-authenticate' , '' ] |
michael@0 | 218 | ]; |
michael@0 | 219 | |
michael@0 | 220 | // The HeaderSetDecompressor class |
michael@0 | 221 | // ------------------------------- |
michael@0 | 222 | |
michael@0 | 223 | // A `HeaderSetDecompressor` instance is a transform stream that can be used to *decompress a |
michael@0 | 224 | // single header set*. Its input is a stream of binary data chunks and its output is a stream of |
michael@0 | 225 | // `[name, value]` pairs. |
michael@0 | 226 | // |
michael@0 | 227 | // Currently, it is not a proper streaming decompressor implementation, since it buffer its input |
michael@0 | 228 | // until the end os the stream, and then processes the whole header block at once. |
michael@0 | 229 | |
michael@0 | 230 | util.inherits(HeaderSetDecompressor, TransformStream); |
michael@0 | 231 | function HeaderSetDecompressor(log, table) { |
michael@0 | 232 | TransformStream.call(this, { objectMode: true }); |
michael@0 | 233 | |
michael@0 | 234 | this._log = log.child({ component: 'compressor' }); |
michael@0 | 235 | this._table = table; |
michael@0 | 236 | this._chunks = []; |
michael@0 | 237 | } |
michael@0 | 238 | |
michael@0 | 239 | // `_transform` is the implementation of the [corresponding virtual function][_transform] of the |
michael@0 | 240 | // TransformStream class. It collects the data chunks for later processing. |
michael@0 | 241 | // [_transform]: http://nodejs.org/api/stream.html#stream_transform_transform_chunk_encoding_callback |
michael@0 | 242 | HeaderSetDecompressor.prototype._transform = function _transform(chunk, encoding, callback) { |
michael@0 | 243 | this._chunks.push(chunk); |
michael@0 | 244 | callback(); |
michael@0 | 245 | }; |
michael@0 | 246 | |
michael@0 | 247 | // `execute(rep)` executes the given [header representation][representation]. |
michael@0 | 248 | // [representation]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-3.1.4 |
michael@0 | 249 | |
michael@0 | 250 | // The *JavaScript object representation* of a header representation: |
michael@0 | 251 | // |
michael@0 | 252 | // { |
michael@0 | 253 | // name: String || Integer, // string literal or index |
michael@0 | 254 | // value: String || Integer, // string literal or index |
michael@0 | 255 | // index: Boolean // with or without indexing |
michael@0 | 256 | // } |
michael@0 | 257 | // |
michael@0 | 258 | // *Important:* to ease the indexing of the header table, indexes start at 0 instead of 1. |
michael@0 | 259 | // |
michael@0 | 260 | // Examples: |
michael@0 | 261 | // |
michael@0 | 262 | // Indexed: |
michael@0 | 263 | // { name: 2 , value: 2 , index: false } |
michael@0 | 264 | // { name: -1 , value: -1 , index: false } // reference set emptying |
michael@0 | 265 | // Literal: |
michael@0 | 266 | // { name: 2 , value: 'X', index: false } // without indexing |
michael@0 | 267 | // { name: 2 , value: 'Y', index: true } // with indexing |
michael@0 | 268 | // { name: 'A', value: 'Z', index: true } // with indexing, literal name |
michael@0 | 269 | HeaderSetDecompressor.prototype._execute = function _execute(rep) { |
michael@0 | 270 | this._log.trace({ key: rep.name, value: rep.value, index: rep.index }, |
michael@0 | 271 | 'Executing header representation'); |
michael@0 | 272 | |
michael@0 | 273 | var entry, pair; |
michael@0 | 274 | |
michael@0 | 275 | // * An _indexed representation_ with an index value of 0 (in our representation, it means -1) |
michael@0 | 276 | // entails the following actions: |
michael@0 | 277 | // * If the following byte starts with a set bit, the reference set is emptied. |
michael@0 | 278 | // * Else, reduce the size of the header table to the value encoded with a 7-bit prefix |
michael@0 | 279 | // * An _indexed representation_ corresponding to an entry _present_ in the reference set |
michael@0 | 280 | // entails the following actions: |
michael@0 | 281 | // * The entry is removed from the reference set. |
michael@0 | 282 | // * An _indexed representation_ corresponding to an entry _not present_ in the reference set |
michael@0 | 283 | // entails the following actions: |
michael@0 | 284 | // * If referencing an element of the static table: |
michael@0 | 285 | // * The header field corresponding to the referenced entry is emitted |
michael@0 | 286 | // * The referenced static entry is added to the header table |
michael@0 | 287 | // * A reference to this new header table entry is added to the reference set (except if |
michael@0 | 288 | // this new entry didn't fit in the header table) |
michael@0 | 289 | // * If referencing an element of the header table: |
michael@0 | 290 | // * The header field corresponding to the referenced entry is emitted |
michael@0 | 291 | // * The referenced header table entry is added to the reference set |
michael@0 | 292 | if (typeof rep.value === 'number') { |
michael@0 | 293 | var index = rep.value; |
michael@0 | 294 | entry = this._table[index]; |
michael@0 | 295 | |
michael@0 | 296 | if (index == -1) { |
michael@0 | 297 | if (rep.index) { |
michael@0 | 298 | for (var i = 0; i < this._table.length; i++) { |
michael@0 | 299 | this._table[i].reference = false; |
michael@0 | 300 | } |
michael@0 | 301 | } else { |
michael@0 | 302 | // Set a new maximum size |
michael@0 | 303 | this.setTableSizeLimit(rep.name); |
michael@0 | 304 | } |
michael@0 | 305 | } |
michael@0 | 306 | |
michael@0 | 307 | else if (entry.reference) { |
michael@0 | 308 | entry.reference = false; |
michael@0 | 309 | } |
michael@0 | 310 | |
michael@0 | 311 | else { |
michael@0 | 312 | pair = entry.slice(); |
michael@0 | 313 | this.push(pair); |
michael@0 | 314 | |
michael@0 | 315 | if (index >= this._table.length - this._table._staticLength) { |
michael@0 | 316 | entry = entryFromPair(pair); |
michael@0 | 317 | this._table.add(entry); |
michael@0 | 318 | } |
michael@0 | 319 | |
michael@0 | 320 | entry.reference = true; |
michael@0 | 321 | entry.emitted = true; |
michael@0 | 322 | } |
michael@0 | 323 | } |
michael@0 | 324 | |
michael@0 | 325 | // * A _literal representation_ that is _not added_ to the header table entails the following |
michael@0 | 326 | // action: |
michael@0 | 327 | // * The header is emitted. |
michael@0 | 328 | // * A _literal representation_ that is _added_ to the header table entails the following further |
michael@0 | 329 | // actions: |
michael@0 | 330 | // * The header is added to the header table. |
michael@0 | 331 | // * The new entry is added to the reference set. |
michael@0 | 332 | else { |
michael@0 | 333 | if (typeof rep.name === 'number') { |
michael@0 | 334 | pair = [this._table[rep.name][0], rep.value]; |
michael@0 | 335 | } else { |
michael@0 | 336 | pair = [rep.name, rep.value]; |
michael@0 | 337 | } |
michael@0 | 338 | |
michael@0 | 339 | if (rep.index) { |
michael@0 | 340 | entry = entryFromPair(pair); |
michael@0 | 341 | entry.reference = true; |
michael@0 | 342 | entry.emitted = true; |
michael@0 | 343 | this._table.add(entry); |
michael@0 | 344 | } |
michael@0 | 345 | |
michael@0 | 346 | this.push(pair); |
michael@0 | 347 | } |
michael@0 | 348 | }; |
michael@0 | 349 | |
michael@0 | 350 | // `_flush` is the implementation of the [corresponding virtual function][_flush] of the |
michael@0 | 351 | // TransformStream class. The whole decompressing process is done in `_flush`. It gets called when |
michael@0 | 352 | // the input stream is over. |
michael@0 | 353 | // [_flush]: http://nodejs.org/api/stream.html#stream_transform_flush_callback |
michael@0 | 354 | HeaderSetDecompressor.prototype._flush = function _flush(callback) { |
michael@0 | 355 | var buffer = concat(this._chunks); |
michael@0 | 356 | |
michael@0 | 357 | // * processes the header representations |
michael@0 | 358 | buffer.cursor = 0; |
michael@0 | 359 | while (buffer.cursor < buffer.length) { |
michael@0 | 360 | this._execute(HeaderSetDecompressor.header(buffer)); |
michael@0 | 361 | } |
michael@0 | 362 | |
michael@0 | 363 | // * [emits the reference set](http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-3.2.2) |
michael@0 | 364 | for (var index = 0; index < this._table.length; index++) { |
michael@0 | 365 | var entry = this._table[index]; |
michael@0 | 366 | if (entry.reference && !entry.emitted) { |
michael@0 | 367 | this.push(entry.slice()); |
michael@0 | 368 | } |
michael@0 | 369 | entry.emitted = false; |
michael@0 | 370 | } |
michael@0 | 371 | |
michael@0 | 372 | callback(); |
michael@0 | 373 | }; |
michael@0 | 374 | |
michael@0 | 375 | // The HeaderSetCompressor class |
michael@0 | 376 | // ----------------------------- |
michael@0 | 377 | |
michael@0 | 378 | // A `HeaderSetCompressor` instance is a transform stream that can be used to *compress a single |
michael@0 | 379 | // header set*. Its input is a stream of `[name, value]` pairs and its output is a stream of |
michael@0 | 380 | // binary data chunks. |
michael@0 | 381 | // |
michael@0 | 382 | // It is a real streaming compressor, since it does not wait until the header set is complete. |
michael@0 | 383 | // |
michael@0 | 384 | // The compression algorithm is (intentionally) not specified by the spec. Therefore, the current |
michael@0 | 385 | // compression algorithm can probably be improved in the future. |
michael@0 | 386 | |
michael@0 | 387 | util.inherits(HeaderSetCompressor, TransformStream); |
michael@0 | 388 | function HeaderSetCompressor(log, table) { |
michael@0 | 389 | TransformStream.call(this, { objectMode: true }); |
michael@0 | 390 | |
michael@0 | 391 | this._log = log.child({ component: 'compressor' }); |
michael@0 | 392 | this._table = table; |
michael@0 | 393 | this.push = TransformStream.prototype.push.bind(this); |
michael@0 | 394 | } |
michael@0 | 395 | |
michael@0 | 396 | HeaderSetCompressor.prototype.send = function send(rep) { |
michael@0 | 397 | this._log.trace({ key: rep.name, value: rep.value, index: rep.index }, |
michael@0 | 398 | 'Emitting header representation'); |
michael@0 | 399 | |
michael@0 | 400 | if (!rep.chunks) { |
michael@0 | 401 | rep.chunks = HeaderSetCompressor.header(rep); |
michael@0 | 402 | } |
michael@0 | 403 | rep.chunks.forEach(this.push); |
michael@0 | 404 | }; |
michael@0 | 405 | |
michael@0 | 406 | // `_transform` is the implementation of the [corresponding virtual function][_transform] of the |
michael@0 | 407 | // TransformStream class. It processes the input headers one by one: |
michael@0 | 408 | // [_transform]: http://nodejs.org/api/stream.html#stream_transform_transform_chunk_encoding_callback |
michael@0 | 409 | HeaderSetCompressor.prototype._transform = function _transform(pair, encoding, callback) { |
michael@0 | 410 | var name = pair[0].toLowerCase(); |
michael@0 | 411 | var value = pair[1]; |
michael@0 | 412 | var entry, rep; |
michael@0 | 413 | |
michael@0 | 414 | // * tries to find full (name, value) or name match in the header table |
michael@0 | 415 | var nameMatch = -1, fullMatch = -1; |
michael@0 | 416 | for (var droppedIndex = 0; droppedIndex < this._table.length; droppedIndex++) { |
michael@0 | 417 | entry = this._table[droppedIndex]; |
michael@0 | 418 | if (entry[0] === name) { |
michael@0 | 419 | if (entry[1] === value) { |
michael@0 | 420 | fullMatch = droppedIndex; |
michael@0 | 421 | break; |
michael@0 | 422 | } else if (nameMatch === -1) { |
michael@0 | 423 | nameMatch = droppedIndex; |
michael@0 | 424 | } |
michael@0 | 425 | } |
michael@0 | 426 | } |
michael@0 | 427 | |
michael@0 | 428 | // * if there's full match, it will be an indexed representation (or more than one) depending |
michael@0 | 429 | // on its presence in the reference, the emitted and the keep set: |
michael@0 | 430 | // |
michael@0 | 431 | // * If the entry is outside the reference set, then a single indexed representation puts the |
michael@0 | 432 | // entry into it and emits the header. Note that if the matched entry is in the static table, |
michael@0 | 433 | // then it has to be added to the header table. |
michael@0 | 434 | // |
michael@0 | 435 | // * If it's already in the keep set, then 4 indexed representations are needed: |
michael@0 | 436 | // |
michael@0 | 437 | // 1. removes it from the reference set |
michael@0 | 438 | // 2. puts it back in the reference set and emits the header once |
michael@0 | 439 | // 3. removes it again |
michael@0 | 440 | // 4. puts it back and emits it again for the second time |
michael@0 | 441 | // |
michael@0 | 442 | // It won't be emitted at the end of the decoding process since it's now in the emitted set. |
michael@0 | 443 | // |
michael@0 | 444 | // * If it's in the emitted set, then 2 indexed representations are needed: |
michael@0 | 445 | // |
michael@0 | 446 | // 1. removes it from the reference set |
michael@0 | 447 | // 2. puts it back in the reference set and emits the header once |
michael@0 | 448 | // |
michael@0 | 449 | // * If it's in the reference set, but outside the keep set and the emitted set, then this |
michael@0 | 450 | // header is common with the previous header set, and is still untouched. We mark it to keep |
michael@0 | 451 | // in the reference set (that means don't remove at the end of the encoding process). |
michael@0 | 452 | if (fullMatch !== -1) { |
michael@0 | 453 | rep = { name: fullMatch, value: fullMatch, index: false }; |
michael@0 | 454 | |
michael@0 | 455 | if (!entry.reference) { |
michael@0 | 456 | if (fullMatch >= this._table.length - this._table._staticLength) { |
michael@0 | 457 | entry = entryFromPair(pair); |
michael@0 | 458 | this._table.add(entry); |
michael@0 | 459 | } |
michael@0 | 460 | this.send(rep); |
michael@0 | 461 | entry.reference = true; |
michael@0 | 462 | entry.emitted = true; |
michael@0 | 463 | } |
michael@0 | 464 | |
michael@0 | 465 | else if (entry.keep) { |
michael@0 | 466 | this.send(rep); |
michael@0 | 467 | this.send(rep); |
michael@0 | 468 | this.send(rep); |
michael@0 | 469 | this.send(rep); |
michael@0 | 470 | entry.keep = false; |
michael@0 | 471 | entry.emitted = true; |
michael@0 | 472 | } |
michael@0 | 473 | |
michael@0 | 474 | else if (entry.emitted) { |
michael@0 | 475 | this.send(rep); |
michael@0 | 476 | this.send(rep); |
michael@0 | 477 | } |
michael@0 | 478 | |
michael@0 | 479 | else { |
michael@0 | 480 | entry.keep = true; |
michael@0 | 481 | } |
michael@0 | 482 | } |
michael@0 | 483 | |
michael@0 | 484 | // * otherwise, it will be a literal representation (with a name index if there's a name match) |
michael@0 | 485 | else { |
michael@0 | 486 | entry = entryFromPair(pair); |
michael@0 | 487 | entry.emitted = true; |
michael@0 | 488 | |
michael@0 | 489 | var indexing = (entry._size < this._table._limit / 2); |
michael@0 | 490 | |
michael@0 | 491 | if (indexing) { |
michael@0 | 492 | entry.reference = true; |
michael@0 | 493 | var droppedEntries = this._table.add(entry); |
michael@0 | 494 | for (droppedIndex in droppedEntries) { |
michael@0 | 495 | droppedIndex = Number(droppedIndex) |
michael@0 | 496 | var dropped = droppedEntries[droppedIndex]; |
michael@0 | 497 | if (dropped.keep) { |
michael@0 | 498 | rep = { name: droppedIndex, value: droppedIndex, index: false }; |
michael@0 | 499 | this.send(rep); |
michael@0 | 500 | this.send(rep); |
michael@0 | 501 | } |
michael@0 | 502 | } |
michael@0 | 503 | } |
michael@0 | 504 | |
michael@0 | 505 | this.send({ name: (nameMatch !== -1) ? nameMatch : name, value: value, index: indexing }); |
michael@0 | 506 | } |
michael@0 | 507 | |
michael@0 | 508 | callback(); |
michael@0 | 509 | }; |
michael@0 | 510 | |
michael@0 | 511 | // `_flush` is the implementation of the [corresponding virtual function][_flush] of the |
michael@0 | 512 | // TransformStream class. It gets called when there's no more header to compress. The final step: |
michael@0 | 513 | // [_flush]: http://nodejs.org/api/stream.html#stream_transform_flush_callback |
michael@0 | 514 | HeaderSetCompressor.prototype._flush = function _flush(callback) { |
michael@0 | 515 | // * removing entries from the header set that are not marked to be kept or emitted |
michael@0 | 516 | for (var index = 0; index < this._table.length; index++) { |
michael@0 | 517 | var entry = this._table[index]; |
michael@0 | 518 | if (entry.reference && !entry.keep && !entry.emitted) { |
michael@0 | 519 | this.send({ name: index, value: index, index: false }); |
michael@0 | 520 | entry.reference = false; |
michael@0 | 521 | } |
michael@0 | 522 | entry.keep = false; |
michael@0 | 523 | entry.emitted = false; |
michael@0 | 524 | } |
michael@0 | 525 | |
michael@0 | 526 | callback(); |
michael@0 | 527 | }; |
michael@0 | 528 | |
michael@0 | 529 | // [Detailed Format](http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-05#section-4) |
michael@0 | 530 | // ----------------- |
michael@0 | 531 | |
michael@0 | 532 | // ### Integer representation ### |
michael@0 | 533 | // |
michael@0 | 534 | // The algorithm to represent an integer I is as follows: |
michael@0 | 535 | // |
michael@0 | 536 | // 1. If I < 2^N - 1, encode I on N bits |
michael@0 | 537 | // 2. Else, encode 2^N - 1 on N bits and do the following steps: |
michael@0 | 538 | // 1. Set I to (I - (2^N - 1)) and Q to 1 |
michael@0 | 539 | // 2. While Q > 0 |
michael@0 | 540 | // 1. Compute Q and R, quotient and remainder of I divided by 2^7 |
michael@0 | 541 | // 2. If Q is strictly greater than 0, write one 1 bit; otherwise, write one 0 bit |
michael@0 | 542 | // 3. Encode R on the next 7 bits |
michael@0 | 543 | // 4. I = Q |
michael@0 | 544 | |
michael@0 | 545 | HeaderSetCompressor.integer = function writeInteger(I, N) { |
michael@0 | 546 | var limit = Math.pow(2,N) - 1; |
michael@0 | 547 | if (I < limit) { |
michael@0 | 548 | return [new Buffer([I])]; |
michael@0 | 549 | } |
michael@0 | 550 | |
michael@0 | 551 | var bytes = []; |
michael@0 | 552 | if (N !== 0) { |
michael@0 | 553 | bytes.push(limit); |
michael@0 | 554 | } |
michael@0 | 555 | I -= limit; |
michael@0 | 556 | |
michael@0 | 557 | var Q = 1, R; |
michael@0 | 558 | while (Q > 0) { |
michael@0 | 559 | Q = Math.floor(I / 128); |
michael@0 | 560 | R = I % 128; |
michael@0 | 561 | |
michael@0 | 562 | if (Q > 0) { |
michael@0 | 563 | R += 128; |
michael@0 | 564 | } |
michael@0 | 565 | bytes.push(R); |
michael@0 | 566 | |
michael@0 | 567 | I = Q; |
michael@0 | 568 | } |
michael@0 | 569 | |
michael@0 | 570 | return [new Buffer(bytes)]; |
michael@0 | 571 | }; |
michael@0 | 572 | |
michael@0 | 573 | // The inverse algorithm: |
michael@0 | 574 | // |
michael@0 | 575 | // 1. Set I to the number coded on the lower N bits of the first byte |
michael@0 | 576 | // 2. If I is smaller than 2^N - 1 then return I |
michael@0 | 577 | // 2. Else the number is encoded on more than one byte, so do the following steps: |
michael@0 | 578 | // 1. Set M to 0 |
michael@0 | 579 | // 2. While returning with I |
michael@0 | 580 | // 1. Let B be the next byte (the first byte if N is 0) |
michael@0 | 581 | // 2. Read out the lower 7 bits of B and multiply it with 2^M |
michael@0 | 582 | // 3. Increase I with this number |
michael@0 | 583 | // 4. Increase M by 7 |
michael@0 | 584 | // 5. Return I if the most significant bit of B is 0 |
michael@0 | 585 | |
michael@0 | 586 | HeaderSetDecompressor.integer = function readInteger(buffer, N) { |
michael@0 | 587 | var limit = Math.pow(2,N) - 1; |
michael@0 | 588 | |
michael@0 | 589 | var I = buffer[buffer.cursor] & limit; |
michael@0 | 590 | if (N !== 0) { |
michael@0 | 591 | buffer.cursor += 1; |
michael@0 | 592 | } |
michael@0 | 593 | |
michael@0 | 594 | if (I === limit) { |
michael@0 | 595 | var M = 0; |
michael@0 | 596 | do { |
michael@0 | 597 | I += (buffer[buffer.cursor] & 127) << M; |
michael@0 | 598 | M += 7; |
michael@0 | 599 | buffer.cursor += 1; |
michael@0 | 600 | } while (buffer[buffer.cursor - 1] & 128); |
michael@0 | 601 | } |
michael@0 | 602 | |
michael@0 | 603 | return I; |
michael@0 | 604 | }; |
michael@0 | 605 | |
michael@0 | 606 | // ### Huffman Encoding ### |
michael@0 | 607 | |
michael@0 | 608 | function HuffmanTable(table) { |
michael@0 | 609 | function createTree(codes, position) { |
michael@0 | 610 | if (codes.length === 1) { |
michael@0 | 611 | return [table.indexOf(codes[0])]; |
michael@0 | 612 | } |
michael@0 | 613 | |
michael@0 | 614 | else { |
michael@0 | 615 | position = position || 0; |
michael@0 | 616 | var zero = []; |
michael@0 | 617 | var one = []; |
michael@0 | 618 | for (var i = 0; i < codes.length; i++) { |
michael@0 | 619 | var string = codes[i]; |
michael@0 | 620 | if (string[position] === '0') { |
michael@0 | 621 | zero.push(string); |
michael@0 | 622 | } else { |
michael@0 | 623 | one.push(string); |
michael@0 | 624 | } |
michael@0 | 625 | } |
michael@0 | 626 | return [createTree(zero, position + 1), createTree(one, position + 1)]; |
michael@0 | 627 | } |
michael@0 | 628 | } |
michael@0 | 629 | |
michael@0 | 630 | this.tree = createTree(table); |
michael@0 | 631 | |
michael@0 | 632 | this.codes = table.map(function(bits) { |
michael@0 | 633 | return parseInt(bits, 2); |
michael@0 | 634 | }); |
michael@0 | 635 | this.lengths = table.map(function(bits) { |
michael@0 | 636 | return bits.length; |
michael@0 | 637 | }); |
michael@0 | 638 | } |
michael@0 | 639 | |
michael@0 | 640 | HuffmanTable.prototype.encode = function encode(buffer) { |
michael@0 | 641 | var result = []; |
michael@0 | 642 | var space = 8; |
michael@0 | 643 | |
michael@0 | 644 | function add(data) { |
michael@0 | 645 | if (space === 8) { |
michael@0 | 646 | result.push(data); |
michael@0 | 647 | } else { |
michael@0 | 648 | result[result.length - 1] |= data; |
michael@0 | 649 | } |
michael@0 | 650 | } |
michael@0 | 651 | |
michael@0 | 652 | for (var i = 0; i < buffer.length; i++) { |
michael@0 | 653 | var byte = buffer[i]; |
michael@0 | 654 | var code = this.codes[byte]; |
michael@0 | 655 | var length = this.lengths[byte]; |
michael@0 | 656 | |
michael@0 | 657 | while (length !== 0) { |
michael@0 | 658 | if (space >= length) { |
michael@0 | 659 | add(code << (space - length)); |
michael@0 | 660 | code = 0; |
michael@0 | 661 | space -= length; |
michael@0 | 662 | length = 0; |
michael@0 | 663 | } else { |
michael@0 | 664 | var shift = length - space; |
michael@0 | 665 | var msb = code >> shift; |
michael@0 | 666 | add(msb); |
michael@0 | 667 | code -= msb << shift; |
michael@0 | 668 | length -= space; |
michael@0 | 669 | space = 0; |
michael@0 | 670 | } |
michael@0 | 671 | |
michael@0 | 672 | if (space === 0) { |
michael@0 | 673 | space = 8; |
michael@0 | 674 | } |
michael@0 | 675 | } |
michael@0 | 676 | } |
michael@0 | 677 | |
michael@0 | 678 | if (space !== 8) { |
michael@0 | 679 | add(this.codes[256] >> (this.lengths[256] - space)); |
michael@0 | 680 | } |
michael@0 | 681 | |
michael@0 | 682 | return new Buffer(result); |
michael@0 | 683 | } |
michael@0 | 684 | |
michael@0 | 685 | HuffmanTable.prototype.decode = function decode(buffer) { |
michael@0 | 686 | var result = []; |
michael@0 | 687 | var subtree = this.tree; |
michael@0 | 688 | |
michael@0 | 689 | for (var i = 0; i < buffer.length; i++) { |
michael@0 | 690 | var byte = buffer[i]; |
michael@0 | 691 | |
michael@0 | 692 | for (var j = 0; j < 8; j++) { |
michael@0 | 693 | var bit = (byte & 128) ? 1 : 0; |
michael@0 | 694 | byte = byte << 1; |
michael@0 | 695 | |
michael@0 | 696 | subtree = subtree[bit]; |
michael@0 | 697 | if (subtree.length === 1) { |
michael@0 | 698 | result.push(subtree[0]); |
michael@0 | 699 | subtree = this.tree; |
michael@0 | 700 | } |
michael@0 | 701 | } |
michael@0 | 702 | } |
michael@0 | 703 | |
michael@0 | 704 | return new Buffer(result); |
michael@0 | 705 | } |
michael@0 | 706 | |
michael@0 | 707 | // The initializer arrays for the Huffman tables are generated with feeding the tables from the |
michael@0 | 708 | // spec to this sed command: |
michael@0 | 709 | // |
michael@0 | 710 | // sed -e "s/^.* [|]//g" -e "s/|//g" -e "s/ .*//g" -e "s/^/ '/g" -e "s/$/',/g" |
michael@0 | 711 | |
michael@0 | 712 | HuffmanTable.huffmanTable = new HuffmanTable([ |
michael@0 | 713 | '111111111111111111110111010', |
michael@0 | 714 | '111111111111111111110111011', |
michael@0 | 715 | '111111111111111111110111100', |
michael@0 | 716 | '111111111111111111110111101', |
michael@0 | 717 | '111111111111111111110111110', |
michael@0 | 718 | '111111111111111111110111111', |
michael@0 | 719 | '111111111111111111111000000', |
michael@0 | 720 | '111111111111111111111000001', |
michael@0 | 721 | '111111111111111111111000010', |
michael@0 | 722 | '111111111111111111111000011', |
michael@0 | 723 | '111111111111111111111000100', |
michael@0 | 724 | '111111111111111111111000101', |
michael@0 | 725 | '111111111111111111111000110', |
michael@0 | 726 | '111111111111111111111000111', |
michael@0 | 727 | '111111111111111111111001000', |
michael@0 | 728 | '111111111111111111111001001', |
michael@0 | 729 | '111111111111111111111001010', |
michael@0 | 730 | '111111111111111111111001011', |
michael@0 | 731 | '111111111111111111111001100', |
michael@0 | 732 | '111111111111111111111001101', |
michael@0 | 733 | '111111111111111111111001110', |
michael@0 | 734 | '111111111111111111111001111', |
michael@0 | 735 | '111111111111111111111010000', |
michael@0 | 736 | '111111111111111111111010001', |
michael@0 | 737 | '111111111111111111111010010', |
michael@0 | 738 | '111111111111111111111010011', |
michael@0 | 739 | '111111111111111111111010100', |
michael@0 | 740 | '111111111111111111111010101', |
michael@0 | 741 | '111111111111111111111010110', |
michael@0 | 742 | '111111111111111111111010111', |
michael@0 | 743 | '111111111111111111111011000', |
michael@0 | 744 | '111111111111111111111011001', |
michael@0 | 745 | '11101000', |
michael@0 | 746 | '111111111100', |
michael@0 | 747 | '11111111111010', |
michael@0 | 748 | '111111111111100', |
michael@0 | 749 | '111111111111101', |
michael@0 | 750 | '100100', |
michael@0 | 751 | '1101110', |
michael@0 | 752 | '111111111111110', |
michael@0 | 753 | '11111111010', |
michael@0 | 754 | '11111111011', |
michael@0 | 755 | '1111111010', |
michael@0 | 756 | '11111111100', |
michael@0 | 757 | '11101001', |
michael@0 | 758 | '100101', |
michael@0 | 759 | '00100', |
michael@0 | 760 | '0000', |
michael@0 | 761 | '00101', |
michael@0 | 762 | '00110', |
michael@0 | 763 | '00111', |
michael@0 | 764 | '100110', |
michael@0 | 765 | '100111', |
michael@0 | 766 | '101000', |
michael@0 | 767 | '101001', |
michael@0 | 768 | '101010', |
michael@0 | 769 | '101011', |
michael@0 | 770 | '101100', |
michael@0 | 771 | '111101100', |
michael@0 | 772 | '11101010', |
michael@0 | 773 | '111111111111111110', |
michael@0 | 774 | '101101', |
michael@0 | 775 | '11111111111111100', |
michael@0 | 776 | '111101101', |
michael@0 | 777 | '11111111111011', |
michael@0 | 778 | '1101111', |
michael@0 | 779 | '11101011', |
michael@0 | 780 | '11101100', |
michael@0 | 781 | '11101101', |
michael@0 | 782 | '11101110', |
michael@0 | 783 | '1110000', |
michael@0 | 784 | '111101110', |
michael@0 | 785 | '111101111', |
michael@0 | 786 | '111110000', |
michael@0 | 787 | '111110001', |
michael@0 | 788 | '1111111011', |
michael@0 | 789 | '111110010', |
michael@0 | 790 | '11101111', |
michael@0 | 791 | '111110011', |
michael@0 | 792 | '111110100', |
michael@0 | 793 | '111110101', |
michael@0 | 794 | '111110110', |
michael@0 | 795 | '111110111', |
michael@0 | 796 | '11110000', |
michael@0 | 797 | '11110001', |
michael@0 | 798 | '111111000', |
michael@0 | 799 | '111111001', |
michael@0 | 800 | '111111010', |
michael@0 | 801 | '111111011', |
michael@0 | 802 | '111111100', |
michael@0 | 803 | '1111111100', |
michael@0 | 804 | '11111111111100', |
michael@0 | 805 | '111111111111111111111011010', |
michael@0 | 806 | '1111111111100', |
michael@0 | 807 | '11111111111101', |
michael@0 | 808 | '101110', |
michael@0 | 809 | '1111111111111111110', |
michael@0 | 810 | '01000', |
michael@0 | 811 | '101111', |
michael@0 | 812 | '01001', |
michael@0 | 813 | '110000', |
michael@0 | 814 | '0001', |
michael@0 | 815 | '110001', |
michael@0 | 816 | '110010', |
michael@0 | 817 | '110011', |
michael@0 | 818 | '01010', |
michael@0 | 819 | '1110001', |
michael@0 | 820 | '1110010', |
michael@0 | 821 | '01011', |
michael@0 | 822 | '110100', |
michael@0 | 823 | '01100', |
michael@0 | 824 | '01101', |
michael@0 | 825 | '01110', |
michael@0 | 826 | '11110010', |
michael@0 | 827 | '01111', |
michael@0 | 828 | '10000', |
michael@0 | 829 | '10001', |
michael@0 | 830 | '110101', |
michael@0 | 831 | '1110011', |
michael@0 | 832 | '110110', |
michael@0 | 833 | '11110011', |
michael@0 | 834 | '11110100', |
michael@0 | 835 | '11110101', |
michael@0 | 836 | '11111111111111101', |
michael@0 | 837 | '11111111101', |
michael@0 | 838 | '11111111111111110', |
michael@0 | 839 | '111111111101', |
michael@0 | 840 | '111111111111111111111011011', |
michael@0 | 841 | '111111111111111111111011100', |
michael@0 | 842 | '111111111111111111111011101', |
michael@0 | 843 | '111111111111111111111011110', |
michael@0 | 844 | '111111111111111111111011111', |
michael@0 | 845 | '111111111111111111111100000', |
michael@0 | 846 | '111111111111111111111100001', |
michael@0 | 847 | '111111111111111111111100010', |
michael@0 | 848 | '111111111111111111111100011', |
michael@0 | 849 | '111111111111111111111100100', |
michael@0 | 850 | '111111111111111111111100101', |
michael@0 | 851 | '111111111111111111111100110', |
michael@0 | 852 | '111111111111111111111100111', |
michael@0 | 853 | '111111111111111111111101000', |
michael@0 | 854 | '111111111111111111111101001', |
michael@0 | 855 | '111111111111111111111101010', |
michael@0 | 856 | '111111111111111111111101011', |
michael@0 | 857 | '111111111111111111111101100', |
michael@0 | 858 | '111111111111111111111101101', |
michael@0 | 859 | '111111111111111111111101110', |
michael@0 | 860 | '111111111111111111111101111', |
michael@0 | 861 | '111111111111111111111110000', |
michael@0 | 862 | '111111111111111111111110001', |
michael@0 | 863 | '111111111111111111111110010', |
michael@0 | 864 | '111111111111111111111110011', |
michael@0 | 865 | '111111111111111111111110100', |
michael@0 | 866 | '111111111111111111111110101', |
michael@0 | 867 | '111111111111111111111110110', |
michael@0 | 868 | '111111111111111111111110111', |
michael@0 | 869 | '111111111111111111111111000', |
michael@0 | 870 | '111111111111111111111111001', |
michael@0 | 871 | '111111111111111111111111010', |
michael@0 | 872 | '111111111111111111111111011', |
michael@0 | 873 | '111111111111111111111111100', |
michael@0 | 874 | '111111111111111111111111101', |
michael@0 | 875 | '111111111111111111111111110', |
michael@0 | 876 | '111111111111111111111111111', |
michael@0 | 877 | '11111111111111111110000000', |
michael@0 | 878 | '11111111111111111110000001', |
michael@0 | 879 | '11111111111111111110000010', |
michael@0 | 880 | '11111111111111111110000011', |
michael@0 | 881 | '11111111111111111110000100', |
michael@0 | 882 | '11111111111111111110000101', |
michael@0 | 883 | '11111111111111111110000110', |
michael@0 | 884 | '11111111111111111110000111', |
michael@0 | 885 | '11111111111111111110001000', |
michael@0 | 886 | '11111111111111111110001001', |
michael@0 | 887 | '11111111111111111110001010', |
michael@0 | 888 | '11111111111111111110001011', |
michael@0 | 889 | '11111111111111111110001100', |
michael@0 | 890 | '11111111111111111110001101', |
michael@0 | 891 | '11111111111111111110001110', |
michael@0 | 892 | '11111111111111111110001111', |
michael@0 | 893 | '11111111111111111110010000', |
michael@0 | 894 | '11111111111111111110010001', |
michael@0 | 895 | '11111111111111111110010010', |
michael@0 | 896 | '11111111111111111110010011', |
michael@0 | 897 | '11111111111111111110010100', |
michael@0 | 898 | '11111111111111111110010101', |
michael@0 | 899 | '11111111111111111110010110', |
michael@0 | 900 | '11111111111111111110010111', |
michael@0 | 901 | '11111111111111111110011000', |
michael@0 | 902 | '11111111111111111110011001', |
michael@0 | 903 | '11111111111111111110011010', |
michael@0 | 904 | '11111111111111111110011011', |
michael@0 | 905 | '11111111111111111110011100', |
michael@0 | 906 | '11111111111111111110011101', |
michael@0 | 907 | '11111111111111111110011110', |
michael@0 | 908 | '11111111111111111110011111', |
michael@0 | 909 | '11111111111111111110100000', |
michael@0 | 910 | '11111111111111111110100001', |
michael@0 | 911 | '11111111111111111110100010', |
michael@0 | 912 | '11111111111111111110100011', |
michael@0 | 913 | '11111111111111111110100100', |
michael@0 | 914 | '11111111111111111110100101', |
michael@0 | 915 | '11111111111111111110100110', |
michael@0 | 916 | '11111111111111111110100111', |
michael@0 | 917 | '11111111111111111110101000', |
michael@0 | 918 | '11111111111111111110101001', |
michael@0 | 919 | '11111111111111111110101010', |
michael@0 | 920 | '11111111111111111110101011', |
michael@0 | 921 | '11111111111111111110101100', |
michael@0 | 922 | '11111111111111111110101101', |
michael@0 | 923 | '11111111111111111110101110', |
michael@0 | 924 | '11111111111111111110101111', |
michael@0 | 925 | '11111111111111111110110000', |
michael@0 | 926 | '11111111111111111110110001', |
michael@0 | 927 | '11111111111111111110110010', |
michael@0 | 928 | '11111111111111111110110011', |
michael@0 | 929 | '11111111111111111110110100', |
michael@0 | 930 | '11111111111111111110110101', |
michael@0 | 931 | '11111111111111111110110110', |
michael@0 | 932 | '11111111111111111110110111', |
michael@0 | 933 | '11111111111111111110111000', |
michael@0 | 934 | '11111111111111111110111001', |
michael@0 | 935 | '11111111111111111110111010', |
michael@0 | 936 | '11111111111111111110111011', |
michael@0 | 937 | '11111111111111111110111100', |
michael@0 | 938 | '11111111111111111110111101', |
michael@0 | 939 | '11111111111111111110111110', |
michael@0 | 940 | '11111111111111111110111111', |
michael@0 | 941 | '11111111111111111111000000', |
michael@0 | 942 | '11111111111111111111000001', |
michael@0 | 943 | '11111111111111111111000010', |
michael@0 | 944 | '11111111111111111111000011', |
michael@0 | 945 | '11111111111111111111000100', |
michael@0 | 946 | '11111111111111111111000101', |
michael@0 | 947 | '11111111111111111111000110', |
michael@0 | 948 | '11111111111111111111000111', |
michael@0 | 949 | '11111111111111111111001000', |
michael@0 | 950 | '11111111111111111111001001', |
michael@0 | 951 | '11111111111111111111001010', |
michael@0 | 952 | '11111111111111111111001011', |
michael@0 | 953 | '11111111111111111111001100', |
michael@0 | 954 | '11111111111111111111001101', |
michael@0 | 955 | '11111111111111111111001110', |
michael@0 | 956 | '11111111111111111111001111', |
michael@0 | 957 | '11111111111111111111010000', |
michael@0 | 958 | '11111111111111111111010001', |
michael@0 | 959 | '11111111111111111111010010', |
michael@0 | 960 | '11111111111111111111010011', |
michael@0 | 961 | '11111111111111111111010100', |
michael@0 | 962 | '11111111111111111111010101', |
michael@0 | 963 | '11111111111111111111010110', |
michael@0 | 964 | '11111111111111111111010111', |
michael@0 | 965 | '11111111111111111111011000', |
michael@0 | 966 | '11111111111111111111011001', |
michael@0 | 967 | '11111111111111111111011010', |
michael@0 | 968 | '11111111111111111111011011', |
michael@0 | 969 | '11111111111111111111011100' |
michael@0 | 970 | ]); |
michael@0 | 971 | |
michael@0 | 972 | // ### String literal representation ### |
michael@0 | 973 | // |
michael@0 | 974 | // Literal **strings** can represent header names or header values. There's two variant of the |
michael@0 | 975 | // string encoding: |
michael@0 | 976 | // |
michael@0 | 977 | // String literal with Huffman encoding: |
michael@0 | 978 | // |
michael@0 | 979 | // 0 1 2 3 4 5 6 7 |
michael@0 | 980 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 981 | // | 1 | Value Length Prefix (7) | |
michael@0 | 982 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 983 | // | Value Length (0-N bytes) | |
michael@0 | 984 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 985 | // ... |
michael@0 | 986 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 987 | // | Huffman Encoded Data |Padding| |
michael@0 | 988 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 989 | // |
michael@0 | 990 | // String literal without Huffman encoding: |
michael@0 | 991 | // |
michael@0 | 992 | // 0 1 2 3 4 5 6 7 |
michael@0 | 993 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 994 | // | 0 | Value Length Prefix (7) | |
michael@0 | 995 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 996 | // | Value Length (0-N bytes) | |
michael@0 | 997 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 998 | // ... |
michael@0 | 999 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 1000 | // | Field Bytes Without Encoding | |
michael@0 | 1001 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 1002 | |
michael@0 | 1003 | HeaderSetCompressor.string = function writeString(str) { |
michael@0 | 1004 | str = new Buffer(str, 'utf8'); |
michael@0 | 1005 | |
michael@0 | 1006 | var huffman = HuffmanTable.huffmanTable.encode(str); |
michael@0 | 1007 | if (huffman.length < str.length) { |
michael@0 | 1008 | var length = HeaderSetCompressor.integer(huffman.length, 7) |
michael@0 | 1009 | length[0][0] |= 128; |
michael@0 | 1010 | return length.concat(huffman); |
michael@0 | 1011 | } |
michael@0 | 1012 | |
michael@0 | 1013 | else { |
michael@0 | 1014 | length = HeaderSetCompressor.integer(str.length, 7) |
michael@0 | 1015 | return length.concat(str); |
michael@0 | 1016 | } |
michael@0 | 1017 | }; |
michael@0 | 1018 | |
michael@0 | 1019 | HeaderSetDecompressor.string = function readString(buffer) { |
michael@0 | 1020 | var huffman = buffer[buffer.cursor] & 128; |
michael@0 | 1021 | var length = HeaderSetDecompressor.integer(buffer, 7); |
michael@0 | 1022 | var encoded = buffer.slice(buffer.cursor, buffer.cursor + length); |
michael@0 | 1023 | buffer.cursor += length; |
michael@0 | 1024 | return (huffman ? HuffmanTable.huffmanTable.decode(encoded) : encoded).toString('utf8'); |
michael@0 | 1025 | }; |
michael@0 | 1026 | |
michael@0 | 1027 | // ### Header represenations ### |
michael@0 | 1028 | |
michael@0 | 1029 | // The JavaScript object representation is described near the |
michael@0 | 1030 | // `HeaderSetDecompressor.prototype._execute()` method definition. |
michael@0 | 1031 | // |
michael@0 | 1032 | // **All binary header representations** start with a prefix signaling the representation type and |
michael@0 | 1033 | // an index represented using prefix coded integers: |
michael@0 | 1034 | // |
michael@0 | 1035 | // 0 1 2 3 4 5 6 7 |
michael@0 | 1036 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 1037 | // | 1 | Index (7+) | Indexed Representation |
michael@0 | 1038 | // +---+---------------------------+ |
michael@0 | 1039 | // |
michael@0 | 1040 | // 0 1 2 3 4 5 6 7 |
michael@0 | 1041 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 1042 | // | 0 | 1 | Index (6+) | |
michael@0 | 1043 | // +---+---+---+-------------------+ Literal w/o Indexing |
michael@0 | 1044 | // | Value Length (8+) | |
michael@0 | 1045 | // +-------------------------------+ w/ Indexed Name |
michael@0 | 1046 | // | Value String (Length octets) | |
michael@0 | 1047 | // +-------------------------------+ |
michael@0 | 1048 | // |
michael@0 | 1049 | // 0 1 2 3 4 5 6 7 |
michael@0 | 1050 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 1051 | // | 0 | 1 | 0 | |
michael@0 | 1052 | // +---+---+---+-------------------+ |
michael@0 | 1053 | // | Name Length (8+) | |
michael@0 | 1054 | // +-------------------------------+ Literal w/o Indexing |
michael@0 | 1055 | // | Name String (Length octets) | |
michael@0 | 1056 | // +-------------------------------+ w/ New Name |
michael@0 | 1057 | // | Value Length (8+) | |
michael@0 | 1058 | // +-------------------------------+ |
michael@0 | 1059 | // | Value String (Length octets) | |
michael@0 | 1060 | // +-------------------------------+ |
michael@0 | 1061 | // |
michael@0 | 1062 | // 0 1 2 3 4 5 6 7 |
michael@0 | 1063 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 1064 | // | 0 | 0 | Index (6+) | |
michael@0 | 1065 | // +---+---+---+-------------------+ Literal w/ Incremental Indexing |
michael@0 | 1066 | // | Value Length (8+) | |
michael@0 | 1067 | // +-------------------------------+ w/ Indexed Name |
michael@0 | 1068 | // | Value String (Length octets) | |
michael@0 | 1069 | // +-------------------------------+ |
michael@0 | 1070 | // |
michael@0 | 1071 | // 0 1 2 3 4 5 6 7 |
michael@0 | 1072 | // +---+---+---+---+---+---+---+---+ |
michael@0 | 1073 | // | 0 | 0 | 0 | |
michael@0 | 1074 | // +---+---+---+-------------------+ |
michael@0 | 1075 | // | Name Length (8+) | |
michael@0 | 1076 | // +-------------------------------+ Literal w/ Incremental Indexing |
michael@0 | 1077 | // | Name String (Length octets) | |
michael@0 | 1078 | // +-------------------------------+ w/ New Name |
michael@0 | 1079 | // | Value Length (8+) | |
michael@0 | 1080 | // +-------------------------------+ |
michael@0 | 1081 | // | Value String (Length octets) | |
michael@0 | 1082 | // +-------------------------------+ |
michael@0 | 1083 | // |
michael@0 | 1084 | // The **Indexed Representation** consists of the 1-bit prefix and the Index that is represented as |
michael@0 | 1085 | // a 7-bit prefix coded integer and nothing else. |
michael@0 | 1086 | // |
michael@0 | 1087 | // After the first bits, **all literal representations** specify the header name, either as a |
michael@0 | 1088 | // pointer to the Header Table (Index) or a string literal. When the string literal representation |
michael@0 | 1089 | // is used, the Index is set to 0 and the string literal starts at the second byte. |
michael@0 | 1090 | // |
michael@0 | 1091 | // For **all literal representations**, the specification of the header value comes next. It is |
michael@0 | 1092 | // always represented as a string. |
michael@0 | 1093 | |
michael@0 | 1094 | var representations = { |
michael@0 | 1095 | indexed : { prefix: 7, pattern: 0x80 }, |
michael@0 | 1096 | literal : { prefix: 6, pattern: 0x40 }, |
michael@0 | 1097 | literalIncremental : { prefix: 6, pattern: 0x00 } |
michael@0 | 1098 | }; |
michael@0 | 1099 | |
michael@0 | 1100 | HeaderSetCompressor.header = function writeHeader(header) { |
michael@0 | 1101 | var representation, buffers = []; |
michael@0 | 1102 | |
michael@0 | 1103 | if (typeof header.value === 'number') { |
michael@0 | 1104 | representation = representations.indexed; |
michael@0 | 1105 | } else if (header.index) { |
michael@0 | 1106 | representation = representations.literalIncremental; |
michael@0 | 1107 | } else { |
michael@0 | 1108 | representation = representations.literal; |
michael@0 | 1109 | } |
michael@0 | 1110 | |
michael@0 | 1111 | if (representation === representations.indexed) { |
michael@0 | 1112 | buffers.push(HeaderSetCompressor.integer(header.value + 1, representation.prefix)); |
michael@0 | 1113 | if (header.value == -1) { |
michael@0 | 1114 | if (header.index) { |
michael@0 | 1115 | buffers.push(HeaderSetCompressor.integer(0x80, 8)); |
michael@0 | 1116 | } else { |
michael@0 | 1117 | buffers.push(HeaderSetCompressor.integer(header.name, 7)); |
michael@0 | 1118 | } |
michael@0 | 1119 | } |
michael@0 | 1120 | } |
michael@0 | 1121 | |
michael@0 | 1122 | else { |
michael@0 | 1123 | if (typeof header.name === 'number') { |
michael@0 | 1124 | buffers.push(HeaderSetCompressor.integer(header.name + 1, representation.prefix)); |
michael@0 | 1125 | } else { |
michael@0 | 1126 | buffers.push(HeaderSetCompressor.integer(0, representation.prefix)); |
michael@0 | 1127 | buffers.push(HeaderSetCompressor.string(header.name)); |
michael@0 | 1128 | } |
michael@0 | 1129 | buffers.push(HeaderSetCompressor.string(header.value)); |
michael@0 | 1130 | } |
michael@0 | 1131 | |
michael@0 | 1132 | buffers[0][0][0] |= representation.pattern; |
michael@0 | 1133 | |
michael@0 | 1134 | return Array.prototype.concat.apply([], buffers); // array of arrays of buffers -> array of buffers |
michael@0 | 1135 | }; |
michael@0 | 1136 | |
michael@0 | 1137 | HeaderSetDecompressor.header = function readHeader(buffer) { |
michael@0 | 1138 | var representation, header = {}; |
michael@0 | 1139 | |
michael@0 | 1140 | var firstByte = buffer[buffer.cursor]; |
michael@0 | 1141 | if (firstByte & 0x80) { |
michael@0 | 1142 | representation = representations.indexed; |
michael@0 | 1143 | } else if (firstByte & 0x40) { |
michael@0 | 1144 | representation = representations.literal; |
michael@0 | 1145 | } else { |
michael@0 | 1146 | representation = representations.literalIncremental; |
michael@0 | 1147 | } |
michael@0 | 1148 | |
michael@0 | 1149 | if (representation === representations.indexed) { |
michael@0 | 1150 | header.value = header.name = HeaderSetDecompressor.integer(buffer, representation.prefix) - 1; |
michael@0 | 1151 | header.index = false; |
michael@0 | 1152 | if (header.value === -1) { |
michael@0 | 1153 | if (buffer[buffer.cursor] & 0x80) { |
michael@0 | 1154 | header.index = true; |
michael@0 | 1155 | buffer.cursor += 1; |
michael@0 | 1156 | } else { |
michael@0 | 1157 | header.name = HeaderSetDecompressor.integer(buffer, 7); |
michael@0 | 1158 | } |
michael@0 | 1159 | } |
michael@0 | 1160 | } |
michael@0 | 1161 | |
michael@0 | 1162 | else { |
michael@0 | 1163 | header.name = HeaderSetDecompressor.integer(buffer, representation.prefix) - 1; |
michael@0 | 1164 | if (header.name === -1) { |
michael@0 | 1165 | header.name = HeaderSetDecompressor.string(buffer); |
michael@0 | 1166 | } |
michael@0 | 1167 | header.value = HeaderSetDecompressor.string(buffer); |
michael@0 | 1168 | header.index = (representation === representations.literalIncremental); |
michael@0 | 1169 | } |
michael@0 | 1170 | |
michael@0 | 1171 | return header; |
michael@0 | 1172 | }; |
michael@0 | 1173 | |
michael@0 | 1174 | // Integration with HTTP/2 |
michael@0 | 1175 | // ======================= |
michael@0 | 1176 | |
michael@0 | 1177 | // This section describes the interaction between the compressor/decompressor and the rest of the |
michael@0 | 1178 | // HTTP/2 implementation. The `Compressor` and the `Decompressor` makes up a layer between the |
michael@0 | 1179 | // [framer](framer.html) and the [connection handling component](connection.html). They let most |
michael@0 | 1180 | // frames pass through, except HEADERS and PUSH_PROMISE frames. They convert the frames between |
michael@0 | 1181 | // these two representations: |
michael@0 | 1182 | // |
michael@0 | 1183 | // { { |
michael@0 | 1184 | // type: 'HEADERS', type: 'HEADERS', |
michael@0 | 1185 | // flags: {}, flags: {}, |
michael@0 | 1186 | // stream: 1, <===> stream: 1, |
michael@0 | 1187 | // headers: { data: Buffer |
michael@0 | 1188 | // N1: 'V1', } |
michael@0 | 1189 | // N2: ['V1', 'V2', ...], |
michael@0 | 1190 | // // ... |
michael@0 | 1191 | // } |
michael@0 | 1192 | // } |
michael@0 | 1193 | // |
michael@0 | 1194 | // There are possibly several binary frame that belong to a single non-binary frame. |
michael@0 | 1195 | |
michael@0 | 1196 | var MAX_HTTP_PAYLOAD_SIZE = 16383; |
michael@0 | 1197 | |
michael@0 | 1198 | // The Compressor class |
michael@0 | 1199 | // -------------------- |
michael@0 | 1200 | |
michael@0 | 1201 | // The Compressor transform stream is basically stateless. |
michael@0 | 1202 | util.inherits(Compressor, TransformStream); |
michael@0 | 1203 | function Compressor(log, type) { |
michael@0 | 1204 | TransformStream.call(this, { objectMode: true }); |
michael@0 | 1205 | |
michael@0 | 1206 | this._log = log.child({ component: 'compressor' }); |
michael@0 | 1207 | |
michael@0 | 1208 | assert((type === 'REQUEST') || (type === 'RESPONSE')); |
michael@0 | 1209 | this._table = new HeaderTable(this._log); |
michael@0 | 1210 | } |
michael@0 | 1211 | |
michael@0 | 1212 | // Changing the header table size |
michael@0 | 1213 | Compressor.prototype.setTableSizeLimit = function setTableSizeLimit(size) { |
michael@0 | 1214 | this._table.setSizeLimit(size); |
michael@0 | 1215 | }; |
michael@0 | 1216 | |
michael@0 | 1217 | // `compress` takes a header set, and compresses it using a new `HeaderSetCompressor` stream |
michael@0 | 1218 | // instance. This means that from now on, the advantages of streaming header encoding are lost, |
michael@0 | 1219 | // but the API becomes simpler. |
michael@0 | 1220 | Compressor.prototype.compress = function compress(headers) { |
michael@0 | 1221 | var compressor = new HeaderSetCompressor(this._log, this._table); |
michael@0 | 1222 | for (var name in headers) { |
michael@0 | 1223 | var value = headers[name]; |
michael@0 | 1224 | name = String(name).toLowerCase(); |
michael@0 | 1225 | |
michael@0 | 1226 | // * To allow for better compression efficiency, the Cookie header field MAY be split into |
michael@0 | 1227 | // separate header fields, each with one or more cookie-pairs. |
michael@0 | 1228 | if (name == 'cookie') { |
michael@0 | 1229 | if (!(value instanceof Array)) { |
michael@0 | 1230 | value = [value] |
michael@0 | 1231 | } |
michael@0 | 1232 | value = Array.prototype.concat.apply([], value.map(function(cookie) { |
michael@0 | 1233 | return String(cookie).split(';').map(trim) |
michael@0 | 1234 | })); |
michael@0 | 1235 | } |
michael@0 | 1236 | |
michael@0 | 1237 | // * To preserve the order of a comma-separated list, the ordered values for a single header |
michael@0 | 1238 | // field name appearing in different header fields are concatenated into a single value. |
michael@0 | 1239 | // A zero-valued octet (0x0) is used to delimit multiple values. |
michael@0 | 1240 | // * Header fields containing multiple values MUST be concatenated into a single value unless |
michael@0 | 1241 | // the ordering of that header field is known to be not significant. |
michael@0 | 1242 | // * Currently, only the Cookie header is considered to be order-insensitive. |
michael@0 | 1243 | if ((value instanceof Array) && (name !== 'cookie')) { |
michael@0 | 1244 | value = value.join('\0'); |
michael@0 | 1245 | } |
michael@0 | 1246 | |
michael@0 | 1247 | if (value instanceof Array) { |
michael@0 | 1248 | for (var i = 0; i < value.length; i++) { |
michael@0 | 1249 | compressor.write([name, String(value[i])]); |
michael@0 | 1250 | } |
michael@0 | 1251 | } else { |
michael@0 | 1252 | compressor.write([name, String(value)]); |
michael@0 | 1253 | } |
michael@0 | 1254 | } |
michael@0 | 1255 | compressor.end(); |
michael@0 | 1256 | |
michael@0 | 1257 | var chunk, chunks = []; |
michael@0 | 1258 | while (chunk = compressor.read()) { |
michael@0 | 1259 | chunks.push(chunk); |
michael@0 | 1260 | } |
michael@0 | 1261 | return concat(chunks); |
michael@0 | 1262 | }; |
michael@0 | 1263 | |
michael@0 | 1264 | // When a `frame` arrives |
michael@0 | 1265 | Compressor.prototype._transform = function _transform(frame, encoding, done) { |
michael@0 | 1266 | // * and it is a HEADERS or PUSH_PROMISE frame |
michael@0 | 1267 | // * it generates a header block using the compress method |
michael@0 | 1268 | // * cuts the header block into `chunks` that are not larger than `MAX_HTTP_PAYLOAD_SIZE` |
michael@0 | 1269 | // * for each chunk, it pushes out a chunk frame that is identical to the original, except |
michael@0 | 1270 | // the `data` property which holds the given chunk, the type of the frame which is always |
michael@0 | 1271 | // CONTINUATION except for the first frame, and the END_HEADERS/END_PUSH_STREAM flag that |
michael@0 | 1272 | // marks the last frame and the END_STREAM flag which is always false before the end |
michael@0 | 1273 | if (frame.type === 'HEADERS' || frame.type === 'PUSH_PROMISE') { |
michael@0 | 1274 | var buffer = this.compress(frame.headers); |
michael@0 | 1275 | |
michael@0 | 1276 | var chunks = cut(buffer, MAX_HTTP_PAYLOAD_SIZE); |
michael@0 | 1277 | |
michael@0 | 1278 | for (var i = 0; i < chunks.length; i++) { |
michael@0 | 1279 | var chunkFrame; |
michael@0 | 1280 | var first = (i === 0); |
michael@0 | 1281 | var last = (i === chunks.length - 1); |
michael@0 | 1282 | |
michael@0 | 1283 | if (first) { |
michael@0 | 1284 | chunkFrame = util._extend({}, frame); |
michael@0 | 1285 | chunkFrame.flags = util._extend({}, frame.flags); |
michael@0 | 1286 | chunkFrame.flags['END_' + frame.type] = last; |
michael@0 | 1287 | } else { |
michael@0 | 1288 | chunkFrame = { |
michael@0 | 1289 | type: 'CONTINUATION', |
michael@0 | 1290 | flags: { END_HEADERS: last }, |
michael@0 | 1291 | stream: frame.stream |
michael@0 | 1292 | }; |
michael@0 | 1293 | } |
michael@0 | 1294 | chunkFrame.data = chunks[i]; |
michael@0 | 1295 | |
michael@0 | 1296 | this.push(chunkFrame); |
michael@0 | 1297 | } |
michael@0 | 1298 | } |
michael@0 | 1299 | |
michael@0 | 1300 | // * otherwise, the frame is forwarded without taking any action |
michael@0 | 1301 | else { |
michael@0 | 1302 | this.push(frame); |
michael@0 | 1303 | } |
michael@0 | 1304 | |
michael@0 | 1305 | done(); |
michael@0 | 1306 | }; |
michael@0 | 1307 | |
michael@0 | 1308 | // The Decompressor class |
michael@0 | 1309 | // ---------------------- |
michael@0 | 1310 | |
michael@0 | 1311 | // The Decompressor is a stateful transform stream, since it has to collect multiple frames first, |
michael@0 | 1312 | // and the decoding comes after unifying the payload of those frames. |
michael@0 | 1313 | // |
michael@0 | 1314 | // If there's a frame in progress, `this._inProgress` is `true`. The frames are collected in |
michael@0 | 1315 | // `this._frames`, and the type of the frame and the stream identifier is stored in `this._type` |
michael@0 | 1316 | // and `this._stream` respectively. |
michael@0 | 1317 | util.inherits(Decompressor, TransformStream); |
michael@0 | 1318 | function Decompressor(log, type) { |
michael@0 | 1319 | TransformStream.call(this, { objectMode: true }); |
michael@0 | 1320 | |
michael@0 | 1321 | this._log = log.child({ component: 'compressor' }); |
michael@0 | 1322 | |
michael@0 | 1323 | assert((type === 'REQUEST') || (type === 'RESPONSE')); |
michael@0 | 1324 | this._table = new HeaderTable(this._log); |
michael@0 | 1325 | |
michael@0 | 1326 | this._inProgress = false; |
michael@0 | 1327 | this._base = undefined; |
michael@0 | 1328 | } |
michael@0 | 1329 | |
michael@0 | 1330 | // Changing the header table size |
michael@0 | 1331 | Decompressor.prototype.setTableSizeLimit = function setTableSizeLimit(size) { |
michael@0 | 1332 | this._table.setSizeLimit(size); |
michael@0 | 1333 | }; |
michael@0 | 1334 | |
michael@0 | 1335 | // `decompress` takes a full header block, and decompresses it using a new `HeaderSetDecompressor` |
michael@0 | 1336 | // stream instance. This means that from now on, the advantages of streaming header decoding are |
michael@0 | 1337 | // lost, but the API becomes simpler. |
michael@0 | 1338 | Decompressor.prototype.decompress = function decompress(block) { |
michael@0 | 1339 | var decompressor = new HeaderSetDecompressor(this._log, this._table); |
michael@0 | 1340 | decompressor.end(block); |
michael@0 | 1341 | |
michael@0 | 1342 | var headers = {}; |
michael@0 | 1343 | var pair; |
michael@0 | 1344 | while (pair = decompressor.read()) { |
michael@0 | 1345 | var name = pair[0]; |
michael@0 | 1346 | // * After decompression, header fields that have values containing zero octets (0x0) MUST be |
michael@0 | 1347 | // split into multiple header fields before being processed. |
michael@0 | 1348 | var values = pair[1].split('\0'); |
michael@0 | 1349 | for (var i = 0; i < values.length; i++) { |
michael@0 | 1350 | var value = values[i]; |
michael@0 | 1351 | if (name in headers) { |
michael@0 | 1352 | if (headers[name] instanceof Array) { |
michael@0 | 1353 | headers[name].push(value); |
michael@0 | 1354 | } else { |
michael@0 | 1355 | headers[name] = [headers[name], value]; |
michael@0 | 1356 | } |
michael@0 | 1357 | } else { |
michael@0 | 1358 | headers[name] = value; |
michael@0 | 1359 | } |
michael@0 | 1360 | } |
michael@0 | 1361 | } |
michael@0 | 1362 | |
michael@0 | 1363 | // * If there are multiple Cookie header fields after decompression, these MUST be concatenated |
michael@0 | 1364 | // into a single octet string using the two octet delimiter of 0x3B, 0x20 (the ASCII |
michael@0 | 1365 | // string "; "). |
michael@0 | 1366 | if (('cookie' in headers) && (headers['cookie'] instanceof Array)) { |
michael@0 | 1367 | headers['cookie'] = headers['cookie'].join('; ') |
michael@0 | 1368 | } |
michael@0 | 1369 | |
michael@0 | 1370 | return headers; |
michael@0 | 1371 | }; |
michael@0 | 1372 | |
michael@0 | 1373 | // When a `frame` arrives |
michael@0 | 1374 | Decompressor.prototype._transform = function _transform(frame, encoding, done) { |
michael@0 | 1375 | // * and the collection process is already `_inProgress`, the frame is simply stored, except if |
michael@0 | 1376 | // it's an illegal frame |
michael@0 | 1377 | if (this._inProgress) { |
michael@0 | 1378 | if ((frame.type !== 'CONTINUATION') || (frame.stream !== this._base.stream)) { |
michael@0 | 1379 | this._log.error('A series of HEADER frames were not continuous'); |
michael@0 | 1380 | this.emit('error', 'PROTOCOL_ERROR'); |
michael@0 | 1381 | return; |
michael@0 | 1382 | } |
michael@0 | 1383 | this._frames.push(frame); |
michael@0 | 1384 | } |
michael@0 | 1385 | |
michael@0 | 1386 | // * and the collection process is not `_inProgress`, but the new frame's type is HEADERS or |
michael@0 | 1387 | // PUSH_PROMISE, a new collection process begins |
michael@0 | 1388 | else if ((frame.type === 'HEADERS') || (frame.type === 'PUSH_PROMISE')) { |
michael@0 | 1389 | this._inProgress = true; |
michael@0 | 1390 | this._base = util._extend({}, frame); |
michael@0 | 1391 | this._frames = [frame]; |
michael@0 | 1392 | } |
michael@0 | 1393 | |
michael@0 | 1394 | // * otherwise, the frame is forwarded without taking any action |
michael@0 | 1395 | else { |
michael@0 | 1396 | this.push(frame); |
michael@0 | 1397 | } |
michael@0 | 1398 | |
michael@0 | 1399 | // * When the frame signals that it's the last in the series, the header block chunks are |
michael@0 | 1400 | // concatenated, the headers are decompressed, and a new frame gets pushed out with the |
michael@0 | 1401 | // decompressed headers. |
michael@0 | 1402 | if (this._inProgress && (frame.flags.END_HEADERS || frame.flags.END_PUSH_PROMISE)) { |
michael@0 | 1403 | var buffer = concat(this._frames.map(function(frame) { |
michael@0 | 1404 | return frame.data; |
michael@0 | 1405 | })); |
michael@0 | 1406 | try { |
michael@0 | 1407 | var headers = this.decompress(buffer); |
michael@0 | 1408 | } catch(error) { |
michael@0 | 1409 | this._log.error({ err: error }, 'Header decompression error'); |
michael@0 | 1410 | this.emit('error', 'COMPRESSION_ERROR'); |
michael@0 | 1411 | return; |
michael@0 | 1412 | } |
michael@0 | 1413 | this.push(util._extend(this._base, { headers: headers })); |
michael@0 | 1414 | this._inProgress = false; |
michael@0 | 1415 | } |
michael@0 | 1416 | |
michael@0 | 1417 | done(); |
michael@0 | 1418 | }; |
michael@0 | 1419 | |
michael@0 | 1420 | // Helper functions |
michael@0 | 1421 | // ================ |
michael@0 | 1422 | |
michael@0 | 1423 | // Concatenate an array of buffers into a new buffer |
michael@0 | 1424 | function concat(buffers) { |
michael@0 | 1425 | var size = 0; |
michael@0 | 1426 | for (var i = 0; i < buffers.length; i++) { |
michael@0 | 1427 | size += buffers[i].length; |
michael@0 | 1428 | } |
michael@0 | 1429 | |
michael@0 | 1430 | var concatenated = new Buffer(size); |
michael@0 | 1431 | for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) { |
michael@0 | 1432 | buffers[j].copy(concatenated, cursor); |
michael@0 | 1433 | } |
michael@0 | 1434 | |
michael@0 | 1435 | return concatenated; |
michael@0 | 1436 | } |
michael@0 | 1437 | |
michael@0 | 1438 | // Cut `buffer` into chunks not larger than `size` |
michael@0 | 1439 | function cut(buffer, size) { |
michael@0 | 1440 | var chunks = []; |
michael@0 | 1441 | var cursor = 0; |
michael@0 | 1442 | do { |
michael@0 | 1443 | var chunkSize = Math.min(size, buffer.length - cursor); |
michael@0 | 1444 | chunks.push(buffer.slice(cursor, cursor + chunkSize)); |
michael@0 | 1445 | cursor += chunkSize; |
michael@0 | 1446 | } while(cursor < buffer.length); |
michael@0 | 1447 | return chunks; |
michael@0 | 1448 | } |
michael@0 | 1449 | |
michael@0 | 1450 | function trim(string) { |
michael@0 | 1451 | return string.trim() |
michael@0 | 1452 | } |