testing/xpcshell/node-http2/node_modules/http2-protocol/lib/compressor.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

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 }

mercurial