diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..64326427 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +.idea + diff --git a/HISTORY.md b/HISTORY.md deleted file mode 100644 index 829fdf65..00000000 --- a/HISTORY.md +++ /dev/null @@ -1,120 +0,0 @@ -Version history -=============== - -### 1.0.1 (2013-10-14) ### - -* Support for ALPN if node supports it (currently needs a custom build) -* Fix for a few small issues -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-1.0.1.tar.gz) - -### 1.0.0 (2013-09-23) ### - -* Exporting Endpoint class -* Support for 'filters' in Endpoint -* The last time-based release -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-1.0.0.tar.gz) - -### 0.4.1 (2013-09-15) ### - -* Major performance improvements -* Minor improvements to error handling -* [Blog post](http://gabor.molnar.es/blog/2013/09/15/gsoc-week-number-13/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.4.1.tar.gz) - -### 0.4.0 (2013-09-09) ### - -* Upgrade to the latest draft: [draft-ietf-httpbis-http2-06][draft-06] -* Support for HTTP trailers -* Support for TLS SNI (Server Name Indication) -* Improved stream scheduling algorithm -* [Blog post](http://gabor.molnar.es/blog/2013/09/09/gsoc-week-number-12/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.4.0.tar.gz) - -[draft-06]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-06 - -### 0.3.1 (2013-09-03) ### - -* Lot of testing, bugfixes -* [Blog post](http://gabor.molnar.es/blog/2013/09/03/gsoc-week-number-11/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.3.1.tar.gz) - -### 0.3.0 (2013-08-27) ### - -* Support for prioritization -* Small API compatibility improvements (compatibility with the standard node.js HTTP API) -* Minor push API change -* Ability to pass an external bunyan logger when creating a Server or Agent -* [Blog post](http://gabor.molnar.es/blog/2013/08/27/gsoc-week-number-10/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.3.0.tar.gz) - -### 0.2.1 (2013-08-20) ### - -* Fixing a flow control bug -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.2.1.tar.gz) - -### 0.2.0 (2013-08-19) ### - -* Exposing server push in the public API -* Connection pooling when operating as client -* Much better API compatibility with the standard node.js HTTPS module -* Logging improvements -* [Blog post](http://gabor.molnar.es/blog/2013/08/19/gsoc-week-number-9/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.2.0.tar.gz) - -### 0.1.1 (2013-08-12) ### - -* Lots of bugfixes -* Proper flow control for outgoing frames -* Basic flow control for incoming frames -* [Blog post](http://gabor.molnar.es/blog/2013/08/12/gsoc-week-number-8/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.1.1.tar.gz) - -### 0.1.0 (2013-08-06) ### - -* First release with public API (similar to the standard node HTTPS module) -* Support for NPN negotiation (no ALPN or Upgrade yet) -* Stream number limitation is in place -* Push streams works but not exposed yet in the public API -* [Blog post](http://gabor.molnar.es/blog/2013/08/05/gsoc-week-number-6-and-number-7/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.1.0.tar.gz) - -### 0.0.6 (2013-07-19) ### - -* `Connection` and `Endpoint` classes are usable, but not yet ready -* Addition of an exmaple server and client -* Using [istanbul](https://github.com/gotwarlost/istanbul) for measuring code coverage -* [Blog post](http://gabor.molnar.es/blog/2013/07/19/gsoc-week-number-5/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.6.tar.gz) - -### 0.0.5 (2013-07-14) ### - -* `Stream` class is done -* Public API stubs are in place -* [Blog post](http://gabor.molnar.es/blog/2013/07/14/gsoc-week-number-4/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.5.tar.gz) - -### 0.0.4 (2013-07-08) ### - -* Added logging -* Started `Stream` class implementation -* [Blog post](http://gabor.molnar.es/blog/2013/07/08/gsoc-week-number-3/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.4.tar.gz) - -### 0.0.3 (2013-07-03) ### - -* Header compression is ready -* [Blog post](http://gabor.molnar.es/blog/2013/07/03/the-http-slash-2-header-compression-implementation-of-node-http2/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.3.tar.gz) - -### 0.0.2 (2013-07-01) ### - -* Frame serialization and deserialization ready and updated to match the newest spec -* Header compression implementation started -* [Blog post](http://gabor.molnar.es/blog/2013/07/01/gsoc-week-number-2/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.2.tar.gz) - -### 0.0.1 (2013-06-23) ### - -* Frame serialization and deserialization largely done -* [Blog post](http://gabor.molnar.es/blog/2013/06/23/gsoc-week-number-1/) -* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.1.tar.gz) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 55c4878c..00000000 --- a/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -The MIT License - -Copyright (C) 2013 Gábor Molnár - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -'Software'), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/README.md b/README.md deleted file mode 100644 index fda7efed..00000000 --- a/README.md +++ /dev/null @@ -1,167 +0,0 @@ -node-http2 -========== - -An HTTP/2 ([draft-ietf-httpbis-http2-06](http://tools.ietf.org/html/draft-ietf-httpbis-http2-06)) -client and server implementation for node.js. - -Installation ------------- - -``` -npm install http2 -``` - -API ---- - -The API is very similar to the [standard node.js HTTPS API](http://nodejs.org/api/https.html). The -goal is the perfect API compatibility, with additional HTTP2 related extensions (like server push). - -Detailed API documentation is primarily maintained in the `lib/http.js` file and is [available in -the wiki](https://github.com/molnarg/node-http2/wiki/Public-API) as well. - -Examples --------- - -### Using as a server ### - -```javascript -var options = { - key: fs.readFileSync('./example/localhost.key'), - cert: fs.readFileSync('./example/localhost.crt') -}; - -require('http2').createServer(options, function(request, response) { - response.end('Hello world!'); -}).listen(8080); -``` - -### Using as a client ### - -```javascript -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - -require('http2').get('https://localhost:8080/', function(response) { - response.pipe(process.stdout); -}); -``` - -### Simple static file server ### - -An simple static file server serving up content from its own directory is available in the `example` -directory. Running the server: - -```bash -$ node ./example/server.js -``` - -### Simple command line client ### - -An example client is also available. Downloading the server's own source code from the server: - -```bash -$ node ./example/client.js 'https://localhost:8080/server.js' >/tmp/server.js -``` - -### Server push ### - -For a server push example, see the source code of the example -[server](https://github.com/molnarg/node-http2/blob/master/example/server.js) and -[client](https://github.com/molnarg/node-http2/blob/master/example/client.js). - -Status ------- - -* ALPN is not yet supported in node.js (see - [this issue](https://github.com/joyent/node/issues/5945)). For ALPN support, you will have to use - [Shigeki Ohtsu's node.js fork](https://github.com/shigeki/node/tree/alpn_support) until this code - gets merged upstream. -* Upgrade mechanism to start HTTP/2 over unencrypted channel is not implemented yet - (issue [#4](https://github.com/molnarg/node-http2/issues/4)) -* Other minor features found in - [this list](https://github.com/molnarg/node-http2/issues?labels=feature) are not implemented yet - -Development ------------ - -### Development dependencies ### - -There's a few library you will need to have installed to do anything described in the following -sections. After installing/cloning node-http2, run `npm install` in its directory to install -development dependencies. - -Used libraries: - -* [mocha](http://visionmedia.github.io/mocha/) for tests -* [chai](http://chaijs.com/) for assertions -* [istanbul](https://github.com/gotwarlost/istanbul) for code coverage analysis -* [docco](http://jashkenas.github.io/docco/) for developer documentation -* [bunyan](https://github.com/trentm/node-bunyan) for logging - -For pretty printing logs, you will also need a global install of bunyan (`npm install -g bunyan`). - -### Developer documentation ### - -The developer documentation is located in the `doc` directory. The docs are usually updated only -before releasing a new version. To regenerate them manually, run `npm run-script prepublish`. -There's a hosted version which is located [here](http://molnarg.github.io/node-http2/doc/). - -### Running the tests ### - -It's easy, just run `npm test`. The tests are written in BDD style, so they are a good starting -point to understand the code. - -### Test coverage ### - -To generate a code coverage report, run `npm test --coverage` (which runs very slowly, be patient). -Code coverage summary as of version 1.0.1: -``` -Statements : 93.26% ( 1563/1676 ) -Branches : 84.85% ( 605/713 ) -Functions : 94.81% ( 201/212 ) -Lines : 93.23% ( 1557/1670 ) -``` - -There's a hosted version of the detailed (line-by-line) coverage report -[here](http://molnarg.github.io/node-http2/coverage/lcov-report/lib/). - -### Logging ### - -Logging is turned off by default. You can turn it on by passing a bunyan logger as `log` option when -creating a server or agent. - -When using the example server or client, it's very easy to turn logging on: set the `HTTP2_LOG` -environment variable to `fatal`, `error`, `warn`, `info`, `debug` or `trace` (the logging level). -To log every single incoming and outgoing data chunk, use `HTTP2_LOG_DATA=1` besides -`HTTP2_LOG=trace`. Log output goes to the standard error output. If the standard error is redirected -into a file, then the log output is in bunyan's JSON format for easier post-mortem analysis. - -Running the example server and client with `info` level logging output: - -```bash -$ HTTP2_LOG=info node ./example/server.js -``` - -```bash -$ HTTP2_LOG=info node ./example/client.js 'http://localhost:8080/server.js' >/dev/null -``` - -Contributors ------------- - -Code contributions are always welcome! People who contributed to node-http2 so far: - -* Nick Hurley -* Mike Belshe - -Special thanks to Google for financing the development of this module as part of their [Summer of -Code program](https://developers.google.com/open-source/soc/) (project: [HTTP/2 prototype server -implementation](https://google-melange.appspot.com/gsoc/project/google/gsoc2013/molnarg/5001)), and -Nick Hurley of Mozilla, my GSoC mentor, who helped with regular code review and technical advices. - -License -------- - -The MIT License - -Copyright (C) 2013 Gábor Molnár diff --git a/coverage/coverage.json b/coverage/coverage.json new file mode 100644 index 00000000..c4de88cb --- /dev/null +++ b/coverage/coverage.json @@ -0,0 +1 @@ +{"/data/upstream/node-http2/lib/http.js":{"path":"/data/upstream/node-http2/lib/http.js","s":{"1":1,"2":1,"3":1,"4":1,"5":1,"6":1,"7":1,"8":1,"9":1,"10":1,"11":1,"12":1,"13":1,"14":1,"15":1,"16":1,"17":1,"18":1,"19":3,"20":1,"21":1,"22":24,"23":24,"24":24,"25":24,"26":24,"27":24,"28":24,"29":24,"30":24,"31":24,"32":24,"33":24,"34":1,"35":1,"36":24,"37":24,"38":73,"39":13,"40":24,"41":24,"42":2,"43":1,"44":24,"45":1,"46":1,"47":60,"48":0,"49":0,"50":60,"51":1,"52":24,"53":168,"54":168,"55":0,"56":0,"57":0,"58":24,"59":73,"60":0,"61":0,"62":73,"63":0,"64":0,"65":1,"66":27,"67":27,"68":27,"69":27,"70":27,"71":1,"72":1,"73":13,"74":12,"75":1,"76":1,"77":31,"78":23,"79":2,"80":0,"81":2,"82":23,"83":8,"84":1,"85":4,"86":0,"87":4,"88":4,"89":0,"90":4,"91":1,"92":1,"93":0,"94":1,"95":1,"96":3,"97":1,"98":2,"99":1,"100":1,"101":1,"102":1,"103":1,"104":1,"105":1,"106":1,"107":13,"108":13,"109":13,"110":13,"111":13,"112":13,"113":10,"114":10,"115":10,"116":10,"117":10,"118":10,"119":10,"120":10,"121":9,"122":9,"123":8,"124":1,"125":10,"126":3,"127":1,"128":1,"129":1,"130":2,"131":2,"132":11,"133":1,"134":1,"135":9,"136":9,"137":9,"138":9,"139":9,"140":10,"141":10,"142":10,"143":9,"144":9,"145":9,"146":1,"147":1,"148":1,"149":1,"150":1,"151":1,"152":1,"153":9,"154":9,"155":1,"156":4,"157":4,"158":1,"159":1,"160":1,"161":1,"162":1,"163":1,"164":0,"165":1,"166":1,"167":1,"168":9,"169":0,"170":9,"171":1,"172":0,"173":0,"174":1,"175":10,"176":1,"177":1,"178":10,"179":9,"180":9,"181":9,"182":1,"183":12,"184":1,"185":1,"186":12,"187":12,"188":12,"189":12,"190":12,"191":12,"192":12,"193":12,"194":1,"195":12,"196":12,"197":12,"198":12,"199":12,"200":12,"201":1,"202":1,"203":12,"204":1,"205":11,"206":12,"207":1,"208":12,"209":12,"210":11,"211":12,"212":12,"213":12,"214":12,"215":1,"216":23,"217":11,"218":1,"219":11,"220":11,"221":1,"222":12,"223":12,"224":1,"225":10,"226":1,"227":2,"228":1,"229":2,"230":0,"231":2,"232":2,"233":2,"234":2,"235":1,"236":12,"237":0,"238":12,"239":1,"240":1,"241":1,"242":1,"243":1,"244":1,"245":4,"246":1,"247":7,"248":1,"249":4,"250":4,"251":4,"252":4,"253":4,"254":4,"255":4,"256":4,"257":4,"258":4,"259":4,"260":1,"261":1,"262":12,"263":8,"264":4,"265":12,"266":12,"267":12,"268":12,"269":12,"270":12,"271":1,"272":1,"273":11,"274":11,"275":7,"276":11,"277":11,"278":1,"279":1,"280":10,"281":1,"282":1,"283":1,"284":1,"285":9,"286":9,"287":9,"288":9,"289":9,"290":9,"291":9,"292":9,"293":9,"294":0,"295":9,"296":9,"297":1,"298":9,"299":9,"300":9,"301":8,"302":8,"303":8,"304":8,"305":8,"306":9,"307":1,"308":1,"309":0,"310":8,"311":7,"312":7,"313":7,"314":1,"315":9,"316":9,"317":9,"318":8,"319":1,"320":11,"321":1,"322":7,"323":7,"324":7,"325":1,"326":8,"327":8,"328":8,"329":8,"330":8,"331":8,"332":8,"333":8,"334":1,"335":1,"336":1,"337":1,"338":1,"339":15,"340":15,"341":15,"342":1,"343":1,"344":10,"345":10,"346":10,"347":1,"348":10,"349":10,"350":10,"351":0,"352":10,"353":10,"354":10,"355":10,"356":10,"357":10,"358":10,"359":10,"360":10,"361":10,"362":10,"363":1,"364":5,"365":5,"366":5,"367":1,"368":0,"369":0,"370":0,"371":1,"372":40,"373":0,"374":40,"375":1,"376":3,"377":1,"378":2,"379":1,"380":1,"381":3,"382":1,"383":2,"384":1,"385":1,"386":3,"387":1,"388":2,"389":1,"390":1,"391":3,"392":1,"393":2,"394":1,"395":1,"396":1,"397":2,"398":2,"399":2,"400":2,"401":0,"402":1,"403":12,"404":1,"405":1,"406":12,"407":12,"408":12,"409":12,"410":1,"411":2,"412":2,"413":2,"414":2,"415":2,"416":2,"417":2,"418":2,"419":2,"420":2,"421":1,"422":1,"423":0,"424":1,"425":0,"426":1},"b":{"1":[13,60],"2":[0,60],"3":[60,60],"4":[0,168],"5":[0,73],"6":[0,73],"7":[12,1],"8":[23,8],"9":[2,21],"10":[0,2],"11":[0,4],"12":[0,4],"13":[0,1],"14":[13,2],"15":[10,3],"16":[13,10,3],"17":[9,9],"18":[8,1],"19":[9,8],"20":[1,2],"21":[1,1],"22":[0,9],"23":[1,0],"24":[1,0],"25":[1,0],"26":[0,9],"27":[9,9],"28":[0,0],"29":[0,0],"30":[1,9],"31":[9,0],"32":[1,11],"33":[11,1],"34":[12,11],"35":[11,12],"36":[1,1],"37":[0,2],"38":[2,2],"39":[2,1,1],"40":[2,2,2],"41":[0,12],"42":[12,0],"43":[0,0],"44":[4,4],"45":[7,7],"46":[4,1],"47":[8,4],"48":[12,12],"49":[12,3],"50":[12,4,1],"51":[12,1],"52":[12,1],"53":[1,11],"54":[12,11],"55":[7,4],"56":[1,10],"57":[1,9],"58":[9,9],"59":[0,9],"60":[9,9],"61":[8,1],"62":[1,8],"63":[1,0],"64":[7,1],"65":[8,1],"66":[0,10],"67":[10,9],"68":[0,0],"69":[0,40],"70":[40,0],"71":[0,0],"72":[1,2],"73":[1,1],"74":[1,2],"75":[1,1],"76":[1,2],"77":[1,1],"78":[1,2],"79":[1,1],"80":[2,0]},"f":{"1":2,"2":3,"3":24,"4":24,"5":2,"6":24,"7":60,"8":24,"9":27,"10":13,"11":31,"12":4,"13":1,"14":3,"15":2,"16":13,"17":9,"18":9,"19":10,"20":1,"21":9,"22":4,"23":1,"24":1,"25":1,"26":9,"27":0,"28":10,"29":12,"30":12,"31":12,"32":12,"33":23,"34":11,"35":12,"36":10,"37":2,"38":12,"39":4,"40":7,"41":4,"42":12,"43":9,"44":9,"45":9,"46":7,"47":8,"48":1,"49":1,"50":15,"51":10,"52":5,"53":0,"54":40,"55":3,"56":3,"57":3,"58":3,"59":2,"60":12,"61":12,"62":2,"63":0,"64":0},"fnMap":{"1":{"name":"noop","line":164,"loc":{"start":{"line":164,"column":0},"end":{"line":164,"column":16}}},"2":{"name":"(anonymous_2)","line":173,"loc":{"start":{"line":173,"column":9},"end":{"line":173,"column":20}}},"3":{"name":"IncomingMessage","line":182,"loc":{"start":{"line":182,"column":0},"end":{"line":182,"column":33}}},"4":{"name":"_onHeaders","line":211,"loc":{"start":{"line":211,"column":39},"end":{"line":211,"column":68}}},"5":{"name":"(anonymous_5)","line":224,"loc":{"start":{"line":224,"column":28},"end":{"line":224,"column":46}}},"6":{"name":"_onEnd","line":229,"loc":{"start":{"line":229,"column":35},"end":{"line":229,"column":53}}},"7":{"name":"_checkSpecialHeader","line":235,"loc":{"start":{"line":235,"column":48},"end":{"line":235,"column":89}}},"8":{"name":"_validateHeaders","line":244,"loc":{"start":{"line":244,"column":45},"end":{"line":244,"column":80}}},"9":{"name":"OutgoingMessage","line":277,"loc":{"start":{"line":277,"column":0},"end":{"line":277,"column":27}}},"10":{"name":"_write","line":289,"loc":{"start":{"line":289,"column":35},"end":{"line":289,"column":78}}},"11":{"name":"_finish","line":297,"loc":{"start":{"line":297,"column":36},"end":{"line":297,"column":55}}},"12":{"name":"setHeader","line":312,"loc":{"start":{"line":312,"column":38},"end":{"line":312,"column":70}}},"13":{"name":"removeHeader","line":324,"loc":{"start":{"line":324,"column":41},"end":{"line":324,"column":69}}},"14":{"name":"getHeader","line":332,"loc":{"start":{"line":332,"column":38},"end":{"line":332,"column":63}}},"15":{"name":"addTrailers","line":336,"loc":{"start":{"line":336,"column":40},"end":{"line":336,"column":71}}},"16":{"name":"Server","line":356,"loc":{"start":{"line":356,"column":0},"end":{"line":356,"column":25}}},"17":{"name":"(anonymous_17)","line":374,"loc":{"start":{"line":374,"column":40},"end":{"line":374,"column":57}}},"18":{"name":"_start","line":403,"loc":{"start":{"line":403,"column":26},"end":{"line":403,"column":50}}},"19":{"name":"_onStream","line":414,"loc":{"start":{"line":414,"column":24},"end":{"line":414,"column":51}}},"20":{"name":"_fallback","line":427,"loc":{"start":{"line":427,"column":29},"end":{"line":427,"column":56}}},"21":{"name":"listen","line":445,"loc":{"start":{"line":445,"column":26},"end":{"line":445,"column":58}}},"22":{"name":"close","line":451,"loc":{"start":{"line":451,"column":25},"end":{"line":451,"column":50}}},"23":{"name":"setTimeout","line":456,"loc":{"start":{"line":456,"column":30},"end":{"line":456,"column":69}}},"24":{"name":"getTimeout","line":463,"loc":{"start":{"line":463,"column":7},"end":{"line":463,"column":29}}},"25":{"name":"setTimeout","line":470,"loc":{"start":{"line":470,"column":7},"end":{"line":470,"column":36}}},"26":{"name":"on","line":481,"loc":{"start":{"line":481,"column":22},"end":{"line":481,"column":51}}},"27":{"name":"addContext","line":490,"loc":{"start":{"line":490,"column":30},"end":{"line":490,"column":73}}},"28":{"name":"createServer","line":496,"loc":{"start":{"line":496,"column":0},"end":{"line":496,"column":48}}},"29":{"name":"IncomingRequest","line":514,"loc":{"start":{"line":514,"column":0},"end":{"line":514,"column":33}}},"30":{"name":"_onHeaders","line":523,"loc":{"start":{"line":523,"column":39},"end":{"line":523,"column":68}}},"31":{"name":"OutgoingResponse","line":554,"loc":{"start":{"line":554,"column":0},"end":{"line":554,"column":34}}},"32":{"name":"writeHead","line":567,"loc":{"start":{"line":567,"column":39},"end":{"line":567,"column":93}}},"33":{"name":"_implicitHeaders","line":591,"loc":{"start":{"line":591,"column":46},"end":{"line":591,"column":74}}},"34":{"name":"write","line":597,"loc":{"start":{"line":597,"column":35},"end":{"line":597,"column":52}}},"35":{"name":"end","line":602,"loc":{"start":{"line":602,"column":33},"end":{"line":602,"column":48}}},"36":{"name":"_onRequestHeaders","line":607,"loc":{"start":{"line":607,"column":47},"end":{"line":607,"column":83}}},"37":{"name":"push","line":611,"loc":{"start":{"line":611,"column":34},"end":{"line":611,"column":57}}},"38":{"name":"on","line":638,"loc":{"start":{"line":638,"column":32},"end":{"line":638,"column":61}}},"39":{"name":"request","line":654,"loc":{"start":{"line":654,"column":18},"end":{"line":654,"column":54}}},"40":{"name":"get","line":657,"loc":{"start":{"line":657,"column":14},"end":{"line":657,"column":46}}},"41":{"name":"Agent","line":664,"loc":{"start":{"line":664,"column":0},"end":{"line":664,"column":24}}},"42":{"name":"request","line":687,"loc":{"start":{"line":687,"column":26},"end":{"line":687,"column":62}}},"43":{"name":"(anonymous_43)","line":744,"loc":{"start":{"line":744,"column":30},"end":{"line":744,"column":47}}},"44":{"name":"negotiated","line":754,"loc":{"start":{"line":754,"column":4},"end":{"line":754,"column":26}}},"45":{"name":"(anonymous_45)","line":782,"loc":{"start":{"line":782,"column":19},"end":{"line":782,"column":38}}},"46":{"name":"get","line":795,"loc":{"start":{"line":795,"column":22},"end":{"line":795,"column":54}}},"47":{"name":"unbundleSocket","line":801,"loc":{"start":{"line":801,"column":0},"end":{"line":801,"column":32}}},"48":{"name":"getMaxSockets","line":813,"loc":{"start":{"line":813,"column":7},"end":{"line":813,"column":32}}},"49":{"name":"setMaxSockets","line":816,"loc":{"start":{"line":816,"column":7},"end":{"line":816,"column":37}}},"50":{"name":"OutgoingRequest","line":826,"loc":{"start":{"line":826,"column":0},"end":{"line":826,"column":27}}},"51":{"name":"_start","line":835,"loc":{"start":{"line":835,"column":35},"end":{"line":835,"column":68}}},"52":{"name":"_fallback","line":869,"loc":{"start":{"line":869,"column":38},"end":{"line":869,"column":66}}},"53":{"name":"setPriority","line":875,"loc":{"start":{"line":875,"column":40},"end":{"line":875,"column":71}}},"54":{"name":"on","line":885,"loc":{"start":{"line":885,"column":31},"end":{"line":885,"column":60}}},"55":{"name":"setNoDelay","line":894,"loc":{"start":{"line":894,"column":39},"end":{"line":894,"column":68}}},"56":{"name":"setSocketKeepAlive","line":902,"loc":{"start":{"line":902,"column":47},"end":{"line":902,"column":97}}},"57":{"name":"setTimeout","line":910,"loc":{"start":{"line":910,"column":39},"end":{"line":910,"column":78}}},"58":{"name":"abort","line":919,"loc":{"start":{"line":919,"column":34},"end":{"line":919,"column":51}}},"59":{"name":"_onPromise","line":930,"loc":{"start":{"line":930,"column":39},"end":{"line":930,"column":76}}},"60":{"name":"IncomingResponse","line":945,"loc":{"start":{"line":945,"column":0},"end":{"line":945,"column":34}}},"61":{"name":"_onHeaders","line":954,"loc":{"start":{"line":954,"column":40},"end":{"line":954,"column":69}}},"62":{"name":"IncomingPromise","line":975,"loc":{"start":{"line":975,"column":0},"end":{"line":975,"column":57}}},"63":{"name":"cancel","line":994,"loc":{"start":{"line":994,"column":35},"end":{"line":994,"column":53}}},"64":{"name":"setPriority","line":998,"loc":{"start":{"line":998,"column":40},"end":{"line":998,"column":71}}}},"statementMap":{"1":{"start":{"line":131,"column":0},"end":{"line":131,"column":25}},"2":{"start":{"line":132,"column":0},"end":{"line":132,"column":25}},"3":{"start":{"line":133,"column":0},"end":{"line":133,"column":27}},"4":{"start":{"line":134,"column":0},"end":{"line":134,"column":50}},"5":{"start":{"line":135,"column":0},"end":{"line":135,"column":48}},"6":{"start":{"line":136,"column":0},"end":{"line":136,"column":42}},"7":{"start":{"line":137,"column":0},"end":{"line":137,"column":42}},"8":{"start":{"line":138,"column":0},"end":{"line":138,"column":50}},"9":{"start":{"line":139,"column":0},"end":{"line":139,"column":70}},"10":{"start":{"line":140,"column":0},"end":{"line":140,"column":27}},"11":{"start":{"line":141,"column":0},"end":{"line":141,"column":29}},"12":{"start":{"line":143,"column":0},"end":{"line":143,"column":41}},"13":{"start":{"line":144,"column":0},"end":{"line":144,"column":42}},"14":{"start":{"line":145,"column":0},"end":{"line":145,"column":42}},"15":{"start":{"line":147,"column":0},"end":{"line":155,"column":2}},"16":{"start":{"line":158,"column":0},"end":{"line":158,"column":70}},"17":{"start":{"line":164,"column":0},"end":{"line":164,"column":18}},"18":{"start":{"line":165,"column":0},"end":{"line":174,"column":2}},"19":{"start":{"line":173,"column":22},"end":{"line":173,"column":34}},"20":{"start":{"line":177,"column":0},"end":{"line":177,"column":60}},"21":{"start":{"line":182,"column":0},"end":{"line":204,"column":1}},"22":{"start":{"line":184,"column":2},"end":{"line":184,"column":25}},"23":{"start":{"line":185,"column":2},"end":{"line":185,"column":20}},"24":{"start":{"line":186,"column":2},"end":{"line":186,"column":37}},"25":{"start":{"line":188,"column":2},"end":{"line":188,"column":55}},"26":{"start":{"line":192,"column":2},"end":{"line":192,"column":27}},"27":{"start":{"line":193,"column":2},"end":{"line":193,"column":28}},"28":{"start":{"line":194,"column":2},"end":{"line":194,"column":28}},"29":{"start":{"line":197,"column":2},"end":{"line":197,"column":20}},"30":{"start":{"line":198,"column":2},"end":{"line":198,"column":28}},"31":{"start":{"line":199,"column":2},"end":{"line":199,"column":36}},"32":{"start":{"line":202,"column":2},"end":{"line":202,"column":53}},"33":{"start":{"line":203,"column":2},"end":{"line":203,"column":45}},"34":{"start":{"line":205,"column":0},"end":{"line":205,"column":110}},"35":{"start":{"line":211,"column":0},"end":{"line":227,"column":2}},"36":{"start":{"line":213,"column":2},"end":{"line":213,"column":33}},"37":{"start":{"line":216,"column":2},"end":{"line":220,"column":3}},"38":{"start":{"line":217,"column":4},"end":{"line":219,"column":5}},"39":{"start":{"line":218,"column":6},"end":{"line":218,"column":41}},"40":{"start":{"line":223,"column":2},"end":{"line":223,"column":18}},"41":{"start":{"line":224,"column":2},"end":{"line":226,"column":5}},"42":{"start":{"line":225,"column":4},"end":{"line":225,"column":36}},"43":{"start":{"line":229,"column":0},"end":{"line":231,"column":2}},"44":{"start":{"line":230,"column":2},"end":{"line":230,"column":40}},"45":{"start":{"line":233,"column":0},"end":{"line":233,"column":44}},"46":{"start":{"line":235,"column":0},"end":{"line":242,"column":2}},"47":{"start":{"line":236,"column":2},"end":{"line":239,"column":3}},"48":{"start":{"line":237,"column":4},"end":{"line":237,"column":91}},"49":{"start":{"line":238,"column":4},"end":{"line":238,"column":40}},"50":{"start":{"line":241,"column":2},"end":{"line":241,"column":15}},"51":{"start":{"line":244,"column":0},"end":{"line":272,"column":2}},"52":{"start":{"line":249,"column":2},"end":{"line":256,"column":3}},"53":{"start":{"line":250,"column":4},"end":{"line":250,"column":35}},"54":{"start":{"line":251,"column":4},"end":{"line":255,"column":5}},"55":{"start":{"line":252,"column":6},"end":{"line":252,"column":84}},"56":{"start":{"line":253,"column":6},"end":{"line":253,"column":42}},"57":{"start":{"line":254,"column":6},"end":{"line":254,"column":13}},"58":{"start":{"line":258,"column":2},"end":{"line":271,"column":3}},"59":{"start":{"line":260,"column":4},"end":{"line":263,"column":5}},"60":{"start":{"line":261,"column":6},"end":{"line":261,"column":42}},"61":{"start":{"line":262,"column":6},"end":{"line":262,"column":13}},"62":{"start":{"line":267,"column":4},"end":{"line":270,"column":5}},"63":{"start":{"line":268,"column":6},"end":{"line":268,"column":42}},"64":{"start":{"line":269,"column":6},"end":{"line":269,"column":13}},"65":{"start":{"line":277,"column":0},"end":{"line":286,"column":1}},"66":{"start":{"line":279,"column":2},"end":{"line":279,"column":22}},"67":{"start":{"line":281,"column":2},"end":{"line":281,"column":21}},"68":{"start":{"line":282,"column":2},"end":{"line":282,"column":29}},"69":{"start":{"line":283,"column":2},"end":{"line":283,"column":27}},"70":{"start":{"line":285,"column":2},"end":{"line":285,"column":34}},"71":{"start":{"line":287,"column":0},"end":{"line":287,"column":107}},"72":{"start":{"line":289,"column":0},"end":{"line":295,"column":2}},"73":{"start":{"line":290,"column":2},"end":{"line":294,"column":3}},"74":{"start":{"line":291,"column":4},"end":{"line":291,"column":49}},"75":{"start":{"line":293,"column":4},"end":{"line":293,"column":75}},"76":{"start":{"line":297,"column":0},"end":{"line":310,"column":2}},"77":{"start":{"line":298,"column":2},"end":{"line":309,"column":3}},"78":{"start":{"line":299,"column":4},"end":{"line":305,"column":5}},"79":{"start":{"line":300,"column":6},"end":{"line":304,"column":7}},"80":{"start":{"line":301,"column":8},"end":{"line":301,"column":49}},"81":{"start":{"line":303,"column":8},"end":{"line":303,"column":44}},"82":{"start":{"line":306,"column":4},"end":{"line":306,"column":22}},"83":{"start":{"line":308,"column":4},"end":{"line":308,"column":49}},"84":{"start":{"line":312,"column":0},"end":{"line":322,"column":2}},"85":{"start":{"line":313,"column":2},"end":{"line":321,"column":3}},"86":{"start":{"line":314,"column":4},"end":{"line":314,"column":63}},"87":{"start":{"line":316,"column":4},"end":{"line":316,"column":30}},"88":{"start":{"line":317,"column":4},"end":{"line":319,"column":5}},"89":{"start":{"line":318,"column":6},"end":{"line":318,"column":63}},"90":{"start":{"line":320,"column":4},"end":{"line":320,"column":32}},"91":{"start":{"line":324,"column":0},"end":{"line":330,"column":2}},"92":{"start":{"line":325,"column":2},"end":{"line":329,"column":3}},"93":{"start":{"line":326,"column":4},"end":{"line":326,"column":66}},"94":{"start":{"line":328,"column":4},"end":{"line":328,"column":45}},"95":{"start":{"line":332,"column":0},"end":{"line":334,"column":2}},"96":{"start":{"line":333,"column":2},"end":{"line":333,"column":43}},"97":{"start":{"line":336,"column":0},"end":{"line":338,"column":2}},"98":{"start":{"line":337,"column":2},"end":{"line":337,"column":28}},"99":{"start":{"line":340,"column":0},"end":{"line":340,"column":44}},"100":{"start":{"line":342,"column":0},"end":{"line":342,"column":94}},"101":{"start":{"line":347,"column":0},"end":{"line":347,"column":36}},"102":{"start":{"line":348,"column":0},"end":{"line":348,"column":24}},"103":{"start":{"line":349,"column":0},"end":{"line":349,"column":42}},"104":{"start":{"line":350,"column":0},"end":{"line":350,"column":44}},"105":{"start":{"line":351,"column":0},"end":{"line":351,"column":42}},"106":{"start":{"line":356,"column":0},"end":{"line":399,"column":1}},"107":{"start":{"line":357,"column":2},"end":{"line":357,"column":38}},"108":{"start":{"line":359,"column":2},"end":{"line":359,"column":74}},"109":{"start":{"line":360,"column":2},"end":{"line":360,"column":36}},"110":{"start":{"line":362,"column":2},"end":{"line":362,"column":37}},"111":{"start":{"line":363,"column":2},"end":{"line":363,"column":43}},"112":{"start":{"line":366,"column":2},"end":{"line":396,"column":3}},"113":{"start":{"line":367,"column":4},"end":{"line":367,"column":54}},"114":{"start":{"line":368,"column":4},"end":{"line":368,"column":23}},"115":{"start":{"line":369,"column":4},"end":{"line":369,"column":47}},"116":{"start":{"line":370,"column":4},"end":{"line":370,"column":46}},"117":{"start":{"line":371,"column":4},"end":{"line":371,"column":47}},"118":{"start":{"line":372,"column":4},"end":{"line":372,"column":79}},"119":{"start":{"line":373,"column":4},"end":{"line":373,"column":56}},"120":{"start":{"line":374,"column":4},"end":{"line":381,"column":7}},"121":{"start":{"line":375,"column":6},"end":{"line":375,"column":73}},"122":{"start":{"line":376,"column":6},"end":{"line":380,"column":7}},"123":{"start":{"line":377,"column":8},"end":{"line":377,"column":22}},"124":{"start":{"line":379,"column":8},"end":{"line":379,"column":25}},"125":{"start":{"line":382,"column":4},"end":{"line":382,"column":64}},"126":{"start":{"line":386,"column":7},"end":{"line":396,"column":3}},"127":{"start":{"line":387,"column":4},"end":{"line":387,"column":60}},"128":{"start":{"line":388,"column":4},"end":{"line":388,"column":25}},"129":{"start":{"line":389,"column":4},"end":{"line":389,"column":43}},"130":{"start":{"line":394,"column":4},"end":{"line":394,"column":81}},"131":{"start":{"line":395,"column":4},"end":{"line":395,"column":95}},"132":{"start":{"line":398,"column":2},"end":{"line":398,"column":58}},"133":{"start":{"line":400,"column":0},"end":{"line":400,"column":93}},"134":{"start":{"line":403,"column":0},"end":{"line":425,"column":2}},"135":{"start":{"line":404,"column":2},"end":{"line":404,"column":67}},"136":{"start":{"line":406,"column":2},"end":{"line":409,"column":54}},"137":{"start":{"line":411,"column":2},"end":{"line":411,"column":39}},"138":{"start":{"line":413,"column":2},"end":{"line":413,"column":18}},"139":{"start":{"line":414,"column":2},"end":{"line":419,"column":5}},"140":{"start":{"line":415,"column":4},"end":{"line":415,"column":48}},"141":{"start":{"line":416,"column":4},"end":{"line":416,"column":46}},"142":{"start":{"line":418,"column":4},"end":{"line":418,"column":78}},"143":{"start":{"line":421,"column":2},"end":{"line":421,"column":60}},"144":{"start":{"line":422,"column":2},"end":{"line":422,"column":58}},"145":{"start":{"line":424,"column":2},"end":{"line":424,"column":44}},"146":{"start":{"line":427,"column":0},"end":{"line":440,"column":2}},"147":{"start":{"line":428,"column":2},"end":{"line":428,"column":69}},"148":{"start":{"line":430,"column":2},"end":{"line":433,"column":52}},"149":{"start":{"line":435,"column":2},"end":{"line":437,"column":3}},"150":{"start":{"line":436,"column":4},"end":{"line":436,"column":64}},"151":{"start":{"line":439,"column":2},"end":{"line":439,"column":34}},"152":{"start":{"line":445,"column":0},"end":{"line":449,"column":2}},"153":{"start":{"line":446,"column":2},"end":{"line":447,"column":55}},"154":{"start":{"line":448,"column":2},"end":{"line":448,"column":53}},"155":{"start":{"line":451,"column":0},"end":{"line":454,"column":2}},"156":{"start":{"line":452,"column":2},"end":{"line":452,"column":35}},"157":{"start":{"line":453,"column":2},"end":{"line":453,"column":31}},"158":{"start":{"line":456,"column":0},"end":{"line":460,"column":2}},"159":{"start":{"line":457,"column":2},"end":{"line":459,"column":3}},"160":{"start":{"line":458,"column":4},"end":{"line":458,"column":47}},"161":{"start":{"line":462,"column":0},"end":{"line":475,"column":3}},"162":{"start":{"line":464,"column":4},"end":{"line":468,"column":5}},"163":{"start":{"line":465,"column":6},"end":{"line":465,"column":34}},"164":{"start":{"line":467,"column":6},"end":{"line":467,"column":23}},"165":{"start":{"line":471,"column":4},"end":{"line":473,"column":5}},"166":{"start":{"line":472,"column":6},"end":{"line":472,"column":37}},"167":{"start":{"line":481,"column":0},"end":{"line":487,"column":2}},"168":{"start":{"line":482,"column":2},"end":{"line":486,"column":3}},"169":{"start":{"line":483,"column":4},"end":{"line":483,"column":60}},"170":{"start":{"line":485,"column":4},"end":{"line":485,"column":58}},"171":{"start":{"line":490,"column":0},"end":{"line":494,"column":2}},"172":{"start":{"line":491,"column":2},"end":{"line":493,"column":3}},"173":{"start":{"line":492,"column":4},"end":{"line":492,"column":51}},"174":{"start":{"line":496,"column":0},"end":{"line":509,"column":1}},"175":{"start":{"line":497,"column":2},"end":{"line":500,"column":3}},"176":{"start":{"line":498,"column":4},"end":{"line":498,"column":30}},"177":{"start":{"line":499,"column":4},"end":{"line":499,"column":24}},"178":{"start":{"line":502,"column":2},"end":{"line":502,"column":35}},"179":{"start":{"line":504,"column":2},"end":{"line":506,"column":3}},"180":{"start":{"line":505,"column":4},"end":{"line":505,"column":42}},"181":{"start":{"line":508,"column":2},"end":{"line":508,"column":16}},"182":{"start":{"line":514,"column":0},"end":{"line":516,"column":1}},"183":{"start":{"line":515,"column":2},"end":{"line":515,"column":37}},"184":{"start":{"line":517,"column":0},"end":{"line":517,"column":114}},"185":{"start":{"line":523,"column":0},"end":{"line":549,"column":2}},"186":{"start":{"line":534,"column":2},"end":{"line":534,"column":75}},"187":{"start":{"line":535,"column":2},"end":{"line":535,"column":75}},"188":{"start":{"line":536,"column":2},"end":{"line":536,"column":80}},"189":{"start":{"line":537,"column":2},"end":{"line":537,"column":75}},"190":{"start":{"line":540,"column":2},"end":{"line":540,"column":32}},"191":{"start":{"line":543,"column":2},"end":{"line":543,"column":59}},"192":{"start":{"line":546,"column":2},"end":{"line":547,"column":80}},"193":{"start":{"line":548,"column":2},"end":{"line":548,"column":21}},"194":{"start":{"line":554,"column":0},"end":{"line":564,"column":1}},"195":{"start":{"line":555,"column":2},"end":{"line":555,"column":29}},"196":{"start":{"line":557,"column":2},"end":{"line":557,"column":55}},"197":{"start":{"line":559,"column":2},"end":{"line":559,"column":23}},"198":{"start":{"line":560,"column":2},"end":{"line":560,"column":24}},"199":{"start":{"line":561,"column":2},"end":{"line":561,"column":23}},"200":{"start":{"line":563,"column":2},"end":{"line":563,"column":65}},"201":{"start":{"line":565,"column":0},"end":{"line":565,"column":116}},"202":{"start":{"line":567,"column":0},"end":{"line":589,"column":2}},"203":{"start":{"line":568,"column":2},"end":{"line":572,"column":3}},"204":{"start":{"line":569,"column":4},"end":{"line":569,"column":93}},"205":{"start":{"line":571,"column":4},"end":{"line":571,"column":27}},"206":{"start":{"line":574,"column":2},"end":{"line":576,"column":3}},"207":{"start":{"line":575,"column":4},"end":{"line":575,"column":40}},"208":{"start":{"line":577,"column":2},"end":{"line":577,"column":26}},"209":{"start":{"line":579,"column":2},"end":{"line":581,"column":3}},"210":{"start":{"line":580,"column":4},"end":{"line":580,"column":46}},"211":{"start":{"line":583,"column":2},"end":{"line":583,"column":92}},"212":{"start":{"line":585,"column":2},"end":{"line":585,"column":52}},"213":{"start":{"line":587,"column":2},"end":{"line":587,"column":31}},"214":{"start":{"line":588,"column":2},"end":{"line":588,"column":26}},"215":{"start":{"line":591,"column":0},"end":{"line":595,"column":2}},"216":{"start":{"line":592,"column":2},"end":{"line":594,"column":3}},"217":{"start":{"line":593,"column":4},"end":{"line":593,"column":36}},"218":{"start":{"line":597,"column":0},"end":{"line":600,"column":2}},"219":{"start":{"line":598,"column":2},"end":{"line":598,"column":26}},"220":{"start":{"line":599,"column":2},"end":{"line":599,"column":64}},"221":{"start":{"line":602,"column":0},"end":{"line":605,"column":2}},"222":{"start":{"line":603,"column":2},"end":{"line":603,"column":26}},"223":{"start":{"line":604,"column":2},"end":{"line":604,"column":62}},"224":{"start":{"line":607,"column":0},"end":{"line":609,"column":2}},"225":{"start":{"line":608,"column":2},"end":{"line":608,"column":33}},"226":{"start":{"line":611,"column":0},"end":{"line":634,"column":2}},"227":{"start":{"line":612,"column":2},"end":{"line":614,"column":3}},"228":{"start":{"line":613,"column":4},"end":{"line":613,"column":33}},"229":{"start":{"line":616,"column":2},"end":{"line":618,"column":3}},"230":{"start":{"line":617,"column":4},"end":{"line":617,"column":51}},"231":{"start":{"line":620,"column":2},"end":{"line":625,"column":22}},"232":{"start":{"line":627,"column":2},"end":{"line":629,"column":72}},"233":{"start":{"line":631,"column":2},"end":{"line":631,"column":48}},"234":{"start":{"line":633,"column":2},"end":{"line":633,"column":42}},"235":{"start":{"line":638,"column":0},"end":{"line":644,"column":2}},"236":{"start":{"line":639,"column":2},"end":{"line":643,"column":3}},"237":{"start":{"line":640,"column":4},"end":{"line":640,"column":60}},"238":{"start":{"line":642,"column":4},"end":{"line":642,"column":61}},"239":{"start":{"line":649,"column":0},"end":{"line":649,"column":40}},"240":{"start":{"line":650,"column":0},"end":{"line":650,"column":42}},"241":{"start":{"line":651,"column":0},"end":{"line":651,"column":44}},"242":{"start":{"line":652,"column":0},"end":{"line":652,"column":22}},"243":{"start":{"line":653,"column":0},"end":{"line":653,"column":32}},"244":{"start":{"line":654,"column":0},"end":{"line":656,"column":2}},"245":{"start":{"line":655,"column":2},"end":{"line":655,"column":75}},"246":{"start":{"line":657,"column":0},"end":{"line":659,"column":2}},"247":{"start":{"line":658,"column":2},"end":{"line":658,"column":71}},"248":{"start":{"line":664,"column":0},"end":{"line":684,"column":1}},"249":{"start":{"line":665,"column":2},"end":{"line":665,"column":26}},"250":{"start":{"line":667,"column":2},"end":{"line":667,"column":38}},"251":{"start":{"line":669,"column":2},"end":{"line":669,"column":36}},"252":{"start":{"line":670,"column":2},"end":{"line":670,"column":74}},"253":{"start":{"line":671,"column":2},"end":{"line":671,"column":22}},"254":{"start":{"line":677,"column":2},"end":{"line":677,"column":24}},"255":{"start":{"line":678,"column":2},"end":{"line":678,"column":50}},"256":{"start":{"line":679,"column":2},"end":{"line":679,"column":49}},"257":{"start":{"line":680,"column":2},"end":{"line":680,"column":51}},"258":{"start":{"line":682,"column":2},"end":{"line":682,"column":42}},"259":{"start":{"line":683,"column":2},"end":{"line":683,"column":44}},"260":{"start":{"line":685,"column":0},"end":{"line":685,"column":91}},"261":{"start":{"line":687,"column":0},"end":{"line":793,"column":2}},"262":{"start":{"line":688,"column":2},"end":{"line":692,"column":3}},"263":{"start":{"line":689,"column":4},"end":{"line":689,"column":33}},"264":{"start":{"line":691,"column":4},"end":{"line":691,"column":40}},"265":{"start":{"line":694,"column":2},"end":{"line":694,"column":59}},"266":{"start":{"line":695,"column":2},"end":{"line":695,"column":50}},"267":{"start":{"line":696,"column":2},"end":{"line":696,"column":65}},"268":{"start":{"line":697,"column":2},"end":{"line":697,"column":37}},"269":{"start":{"line":698,"column":2},"end":{"line":698,"column":37}},"270":{"start":{"line":700,"column":2},"end":{"line":703,"column":3}},"271":{"start":{"line":701,"column":4},"end":{"line":701,"column":85}},"272":{"start":{"line":702,"column":4},"end":{"line":702,"column":70}},"273":{"start":{"line":705,"column":2},"end":{"line":705,"column":47}},"274":{"start":{"line":707,"column":2},"end":{"line":709,"column":3}},"275":{"start":{"line":708,"column":4},"end":{"line":708,"column":37}},"276":{"start":{"line":711,"column":2},"end":{"line":715,"column":14}},"277":{"start":{"line":718,"column":2},"end":{"line":790,"column":3}},"278":{"start":{"line":719,"column":4},"end":{"line":719,"column":39}},"279":{"start":{"line":720,"column":4},"end":{"line":720,"column":53}},"280":{"start":{"line":724,"column":7},"end":{"line":790,"column":3}},"281":{"start":{"line":725,"column":4},"end":{"line":725,"column":65}},"282":{"start":{"line":726,"column":4},"end":{"line":730,"column":7}},"283":{"start":{"line":731,"column":4},"end":{"line":731,"column":50}},"284":{"start":{"line":732,"column":4},"end":{"line":732,"column":53}},"285":{"start":{"line":737,"column":4},"end":{"line":737,"column":24}},"286":{"start":{"line":738,"column":4},"end":{"line":738,"column":47}},"287":{"start":{"line":739,"column":4},"end":{"line":739,"column":46}},"288":{"start":{"line":740,"column":4},"end":{"line":740,"column":38}},"289":{"start":{"line":741,"column":4},"end":{"line":741,"column":37}},"290":{"start":{"line":742,"column":4},"end":{"line":742,"column":46}},"291":{"start":{"line":744,"column":4},"end":{"line":751,"column":7}},"292":{"start":{"line":745,"column":6},"end":{"line":745,"column":73}},"293":{"start":{"line":746,"column":6},"end":{"line":750,"column":7}},"294":{"start":{"line":747,"column":8},"end":{"line":747,"column":21}},"295":{"start":{"line":749,"column":8},"end":{"line":749,"column":47}},"296":{"start":{"line":753,"column":4},"end":{"line":753,"column":20}},"297":{"start":{"line":754,"column":4},"end":{"line":780,"column":5}},"298":{"start":{"line":755,"column":6},"end":{"line":755,"column":19}},"299":{"start":{"line":756,"column":6},"end":{"line":756,"column":99}},"300":{"start":{"line":757,"column":6},"end":{"line":763,"column":7}},"301":{"start":{"line":758,"column":8},"end":{"line":758,"column":48}},"302":{"start":{"line":759,"column":8},"end":{"line":759,"column":44}},"303":{"start":{"line":760,"column":8},"end":{"line":760,"column":69}},"304":{"start":{"line":761,"column":8},"end":{"line":761,"column":46}},"305":{"start":{"line":762,"column":8},"end":{"line":762,"column":54}},"306":{"start":{"line":764,"column":6},"end":{"line":779,"column":7}},"307":{"start":{"line":765,"column":8},"end":{"line":769,"column":9}},"308":{"start":{"line":766,"column":10},"end":{"line":766,"column":27}},"309":{"start":{"line":768,"column":10},"end":{"line":768,"column":31}},"310":{"start":{"line":771,"column":8},"end":{"line":778,"column":9}},"311":{"start":{"line":772,"column":10},"end":{"line":773,"column":59}},"312":{"start":{"line":774,"column":10},"end":{"line":774,"column":41}},"313":{"start":{"line":775,"column":10},"end":{"line":775,"column":35}},"314":{"start":{"line":777,"column":10},"end":{"line":777,"column":36}},"315":{"start":{"line":782,"column":4},"end":{"line":789,"column":7}},"316":{"start":{"line":783,"column":6},"end":{"line":783,"column":21}},"317":{"start":{"line":784,"column":6},"end":{"line":788,"column":7}},"318":{"start":{"line":785,"column":8},"end":{"line":785,"column":57}},"319":{"start":{"line":787,"column":8},"end":{"line":787,"column":40}},"320":{"start":{"line":792,"column":2},"end":{"line":792,"column":17}},"321":{"start":{"line":795,"column":0},"end":{"line":799,"column":2}},"322":{"start":{"line":796,"column":2},"end":{"line":796,"column":48}},"323":{"start":{"line":797,"column":2},"end":{"line":797,"column":16}},"324":{"start":{"line":798,"column":2},"end":{"line":798,"column":17}},"325":{"start":{"line":801,"column":0},"end":{"line":810,"column":1}},"326":{"start":{"line":802,"column":2},"end":{"line":802,"column":36}},"327":{"start":{"line":803,"column":2},"end":{"line":803,"column":35}},"328":{"start":{"line":804,"column":2},"end":{"line":804,"column":40}},"329":{"start":{"line":805,"column":2},"end":{"line":805,"column":37}},"330":{"start":{"line":806,"column":2},"end":{"line":806,"column":37}},"331":{"start":{"line":807,"column":2},"end":{"line":807,"column":18}},"332":{"start":{"line":808,"column":2},"end":{"line":808,"column":23}},"333":{"start":{"line":809,"column":2},"end":{"line":809,"column":22}},"334":{"start":{"line":812,"column":0},"end":{"line":819,"column":3}},"335":{"start":{"line":814,"column":4},"end":{"line":814,"column":39}},"336":{"start":{"line":817,"column":4},"end":{"line":817,"column":40}},"337":{"start":{"line":821,"column":0},"end":{"line":821,"column":34}},"338":{"start":{"line":826,"column":0},"end":{"line":832,"column":1}},"339":{"start":{"line":827,"column":2},"end":{"line":827,"column":29}},"340":{"start":{"line":829,"column":2},"end":{"line":829,"column":24}},"341":{"start":{"line":831,"column":2},"end":{"line":831,"column":26}},"342":{"start":{"line":833,"column":0},"end":{"line":833,"column":114}},"343":{"start":{"line":835,"column":0},"end":{"line":867,"column":2}},"344":{"start":{"line":836,"column":2},"end":{"line":836,"column":23}},"345":{"start":{"line":838,"column":2},"end":{"line":838,"column":55}},"346":{"start":{"line":840,"column":2},"end":{"line":842,"column":3}},"347":{"start":{"line":841,"column":4},"end":{"line":841,"column":46}},"348":{"start":{"line":843,"column":2},"end":{"line":843,"column":30}},"349":{"start":{"line":844,"column":2},"end":{"line":844,"column":22}},"350":{"start":{"line":846,"column":2},"end":{"line":848,"column":3}},"351":{"start":{"line":847,"column":4},"end":{"line":847,"column":83}},"352":{"start":{"line":850,"column":2},"end":{"line":850,"column":53}},"353":{"start":{"line":851,"column":2},"end":{"line":851,"column":38}},"354":{"start":{"line":852,"column":2},"end":{"line":852,"column":39}},"355":{"start":{"line":853,"column":2},"end":{"line":853,"column":34}},"356":{"start":{"line":855,"column":2},"end":{"line":857,"column":74}},"357":{"start":{"line":858,"column":2},"end":{"line":858,"column":31}},"358":{"start":{"line":859,"column":2},"end":{"line":859,"column":26}},"359":{"start":{"line":861,"column":2},"end":{"line":861,"column":35}},"360":{"start":{"line":863,"column":2},"end":{"line":863,"column":51}},"361":{"start":{"line":864,"column":2},"end":{"line":864,"column":69}},"362":{"start":{"line":866,"column":2},"end":{"line":866,"column":56}},"363":{"start":{"line":869,"column":0},"end":{"line":873,"column":2}},"364":{"start":{"line":870,"column":2},"end":{"line":870,"column":59}},"365":{"start":{"line":871,"column":2},"end":{"line":871,"column":39}},"366":{"start":{"line":872,"column":2},"end":{"line":872,"column":35}},"367":{"start":{"line":875,"column":0},"end":{"line":881,"column":2}},"368":{"start":{"line":876,"column":2},"end":{"line":880,"column":3}},"369":{"start":{"line":877,"column":4},"end":{"line":877,"column":35}},"370":{"start":{"line":879,"column":4},"end":{"line":879,"column":63}},"371":{"start":{"line":885,"column":0},"end":{"line":891,"column":2}},"372":{"start":{"line":886,"column":2},"end":{"line":890,"column":3}},"373":{"start":{"line":887,"column":4},"end":{"line":887,"column":60}},"374":{"start":{"line":889,"column":4},"end":{"line":889,"column":61}},"375":{"start":{"line":894,"column":0},"end":{"line":900,"column":2}},"376":{"start":{"line":895,"column":2},"end":{"line":899,"column":3}},"377":{"start":{"line":896,"column":4},"end":{"line":896,"column":37}},"378":{"start":{"line":897,"column":9},"end":{"line":899,"column":3}},"379":{"start":{"line":898,"column":4},"end":{"line":898,"column":59}},"380":{"start":{"line":902,"column":0},"end":{"line":908,"column":2}},"381":{"start":{"line":903,"column":2},"end":{"line":907,"column":3}},"382":{"start":{"line":904,"column":4},"end":{"line":904,"column":58}},"383":{"start":{"line":905,"column":9},"end":{"line":907,"column":3}},"384":{"start":{"line":906,"column":4},"end":{"line":906,"column":80}},"385":{"start":{"line":910,"column":0},"end":{"line":916,"column":2}},"386":{"start":{"line":911,"column":2},"end":{"line":915,"column":3}},"387":{"start":{"line":912,"column":4},"end":{"line":912,"column":47}},"388":{"start":{"line":913,"column":9},"end":{"line":915,"column":3}},"389":{"start":{"line":914,"column":4},"end":{"line":914,"column":69}},"390":{"start":{"line":919,"column":0},"end":{"line":927,"column":2}},"391":{"start":{"line":920,"column":2},"end":{"line":926,"column":3}},"392":{"start":{"line":921,"column":4},"end":{"line":921,"column":25}},"393":{"start":{"line":922,"column":9},"end":{"line":926,"column":3}},"394":{"start":{"line":923,"column":4},"end":{"line":923,"column":32}},"395":{"start":{"line":925,"column":4},"end":{"line":925,"column":45}},"396":{"start":{"line":930,"column":0},"end":{"line":940,"column":2}},"397":{"start":{"line":931,"column":2},"end":{"line":931,"column":71}},"398":{"start":{"line":933,"column":2},"end":{"line":933,"column":53}},"399":{"start":{"line":935,"column":2},"end":{"line":939,"column":3}},"400":{"start":{"line":936,"column":4},"end":{"line":936,"column":31}},"401":{"start":{"line":938,"column":4},"end":{"line":938,"column":21}},"402":{"start":{"line":945,"column":0},"end":{"line":947,"column":1}},"403":{"start":{"line":946,"column":2},"end":{"line":946,"column":37}},"404":{"start":{"line":948,"column":0},"end":{"line":948,"column":116}},"405":{"start":{"line":954,"column":0},"end":{"line":970,"column":2}},"406":{"start":{"line":962,"column":2},"end":{"line":962,"column":86}},"407":{"start":{"line":965,"column":2},"end":{"line":965,"column":59}},"408":{"start":{"line":968,"column":2},"end":{"line":968,"column":89}},"409":{"start":{"line":969,"column":2},"end":{"line":969,"column":21}},"410":{"start":{"line":975,"column":0},"end":{"line":991,"column":1}},"411":{"start":{"line":976,"column":2},"end":{"line":976,"column":30}},"412":{"start":{"line":977,"column":2},"end":{"line":977,"column":22}},"413":{"start":{"line":978,"column":2},"end":{"line":978,"column":20}},"414":{"start":{"line":979,"column":2},"end":{"line":979,"column":36}},"415":{"start":{"line":981,"column":2},"end":{"line":981,"column":37}},"416":{"start":{"line":983,"column":2},"end":{"line":983,"column":34}},"417":{"start":{"line":985,"column":2},"end":{"line":985,"column":40}},"418":{"start":{"line":987,"column":2},"end":{"line":987,"column":60}},"419":{"start":{"line":988,"column":2},"end":{"line":988,"column":69}},"420":{"start":{"line":990,"column":2},"end":{"line":990,"column":56}},"421":{"start":{"line":992,"column":0},"end":{"line":992,"column":114}},"422":{"start":{"line":994,"column":0},"end":{"line":996,"column":2}},"423":{"start":{"line":995,"column":2},"end":{"line":995,"column":39}},"424":{"start":{"line":998,"column":0},"end":{"line":1000,"column":2}},"425":{"start":{"line":999,"column":2},"end":{"line":999,"column":42}},"426":{"start":{"line":1002,"column":0},"end":{"line":1002,"column":76}}},"branchMap":{"1":{"line":217,"type":"if","locations":[{"start":{"line":217,"column":4},"end":{"line":217,"column":4}},{"start":{"line":217,"column":4},"end":{"line":217,"column":4}}]},"2":{"line":236,"type":"if","locations":[{"start":{"line":236,"column":2},"end":{"line":236,"column":2}},{"start":{"line":236,"column":2},"end":{"line":236,"column":2}}]},"3":{"line":236,"type":"binary-expr","locations":[{"start":{"line":236,"column":7},"end":{"line":236,"column":32}},{"start":{"line":236,"column":38},"end":{"line":236,"column":56}}]},"4":{"line":251,"type":"if","locations":[{"start":{"line":251,"column":4},"end":{"line":251,"column":4}},{"start":{"line":251,"column":4},"end":{"line":251,"column":4}}]},"5":{"line":260,"type":"if","locations":[{"start":{"line":260,"column":4},"end":{"line":260,"column":4}},{"start":{"line":260,"column":4},"end":{"line":260,"column":4}}]},"6":{"line":267,"type":"if","locations":[{"start":{"line":267,"column":4},"end":{"line":267,"column":4}},{"start":{"line":267,"column":4},"end":{"line":267,"column":4}}]},"7":{"line":290,"type":"if","locations":[{"start":{"line":290,"column":2},"end":{"line":290,"column":2}},{"start":{"line":290,"column":2},"end":{"line":290,"column":2}}]},"8":{"line":298,"type":"if","locations":[{"start":{"line":298,"column":2},"end":{"line":298,"column":2}},{"start":{"line":298,"column":2},"end":{"line":298,"column":2}}]},"9":{"line":299,"type":"if","locations":[{"start":{"line":299,"column":4},"end":{"line":299,"column":4}},{"start":{"line":299,"column":4},"end":{"line":299,"column":4}}]},"10":{"line":300,"type":"if","locations":[{"start":{"line":300,"column":6},"end":{"line":300,"column":6}},{"start":{"line":300,"column":6},"end":{"line":300,"column":6}}]},"11":{"line":313,"type":"if","locations":[{"start":{"line":313,"column":2},"end":{"line":313,"column":2}},{"start":{"line":313,"column":2},"end":{"line":313,"column":2}}]},"12":{"line":317,"type":"if","locations":[{"start":{"line":317,"column":4},"end":{"line":317,"column":4}},{"start":{"line":317,"column":4},"end":{"line":317,"column":4}}]},"13":{"line":325,"type":"if","locations":[{"start":{"line":325,"column":2},"end":{"line":325,"column":2}},{"start":{"line":325,"column":2},"end":{"line":325,"column":2}}]},"14":{"line":359,"type":"binary-expr","locations":[{"start":{"line":359,"column":15},"end":{"line":359,"column":26}},{"start":{"line":359,"column":30},"end":{"line":359,"column":43}}]},"15":{"line":366,"type":"if","locations":[{"start":{"line":366,"column":2},"end":{"line":366,"column":2}},{"start":{"line":366,"column":2},"end":{"line":366,"column":2}}]},"16":{"line":366,"type":"binary-expr","locations":[{"start":{"line":366,"column":7},"end":{"line":366,"column":18}},{"start":{"line":366,"column":22},"end":{"line":366,"column":34}},{"start":{"line":366,"column":39},"end":{"line":366,"column":50}}]},"17":{"line":375,"type":"binary-expr","locations":[{"start":{"line":375,"column":31},"end":{"line":375,"column":50}},{"start":{"line":375,"column":54},"end":{"line":375,"column":72}}]},"18":{"line":376,"type":"if","locations":[{"start":{"line":376,"column":6},"end":{"line":376,"column":6}},{"start":{"line":376,"column":6},"end":{"line":376,"column":6}}]},"19":{"line":376,"type":"binary-expr","locations":[{"start":{"line":376,"column":11},"end":{"line":376,"column":52}},{"start":{"line":376,"column":57},"end":{"line":376,"column":74}}]},"20":{"line":386,"type":"if","locations":[{"start":{"line":386,"column":7},"end":{"line":386,"column":7}},{"start":{"line":386,"column":7},"end":{"line":386,"column":7}}]},"21":{"line":428,"type":"binary-expr","locations":[{"start":{"line":428,"column":27},"end":{"line":428,"column":46}},{"start":{"line":428,"column":50},"end":{"line":428,"column":68}}]},"22":{"line":446,"type":"cond-expr","locations":[{"start":{"line":446,"column":58},"end":{"line":446,"column":79}},{"start":{"line":446,"column":83},"end":{"line":446,"column":87}}]},"23":{"line":457,"type":"if","locations":[{"start":{"line":457,"column":2},"end":{"line":457,"column":2}},{"start":{"line":457,"column":2},"end":{"line":457,"column":2}}]},"24":{"line":464,"type":"if","locations":[{"start":{"line":464,"column":4},"end":{"line":464,"column":4}},{"start":{"line":464,"column":4},"end":{"line":464,"column":4}}]},"25":{"line":471,"type":"if","locations":[{"start":{"line":471,"column":4},"end":{"line":471,"column":4}},{"start":{"line":471,"column":4},"end":{"line":471,"column":4}}]},"26":{"line":482,"type":"if","locations":[{"start":{"line":482,"column":2},"end":{"line":482,"column":2}},{"start":{"line":482,"column":2},"end":{"line":482,"column":2}}]},"27":{"line":482,"type":"binary-expr","locations":[{"start":{"line":482,"column":7},"end":{"line":482,"column":26}},{"start":{"line":482,"column":32},"end":{"line":482,"column":51}}]},"28":{"line":483,"type":"binary-expr","locations":[{"start":{"line":483,"column":27},"end":{"line":483,"column":35}},{"start":{"line":483,"column":39},"end":{"line":483,"column":58}}]},"29":{"line":491,"type":"if","locations":[{"start":{"line":491,"column":2},"end":{"line":491,"column":2}},{"start":{"line":491,"column":2},"end":{"line":491,"column":2}}]},"30":{"line":497,"type":"if","locations":[{"start":{"line":497,"column":2},"end":{"line":497,"column":2}},{"start":{"line":497,"column":2},"end":{"line":497,"column":2}}]},"31":{"line":504,"type":"if","locations":[{"start":{"line":504,"column":2},"end":{"line":504,"column":2}},{"start":{"line":504,"column":2},"end":{"line":504,"column":2}}]},"32":{"line":568,"type":"if","locations":[{"start":{"line":568,"column":2},"end":{"line":568,"column":2}},{"start":{"line":568,"column":2},"end":{"line":568,"column":2}}]},"33":{"line":579,"type":"if","locations":[{"start":{"line":579,"column":2},"end":{"line":579,"column":2}},{"start":{"line":579,"column":2},"end":{"line":579,"column":2}}]},"34":{"line":579,"type":"binary-expr","locations":[{"start":{"line":579,"column":6},"end":{"line":579,"column":19}},{"start":{"line":579,"column":23},"end":{"line":579,"column":49}}]},"35":{"line":592,"type":"if","locations":[{"start":{"line":592,"column":2},"end":{"line":592,"column":2}},{"start":{"line":592,"column":2},"end":{"line":592,"column":2}}]},"36":{"line":612,"type":"if","locations":[{"start":{"line":612,"column":2},"end":{"line":612,"column":2}},{"start":{"line":612,"column":2},"end":{"line":612,"column":2}}]},"37":{"line":616,"type":"if","locations":[{"start":{"line":616,"column":2},"end":{"line":616,"column":2}},{"start":{"line":616,"column":2},"end":{"line":616,"column":2}}]},"38":{"line":621,"type":"binary-expr","locations":[{"start":{"line":621,"column":16},"end":{"line":621,"column":30}},{"start":{"line":621,"column":34},"end":{"line":621,"column":39}}]},"39":{"line":622,"type":"binary-expr","locations":[{"start":{"line":622,"column":16},"end":{"line":622,"column":32}},{"start":{"line":622,"column":36},"end":{"line":622,"column":65}},{"start":{"line":622,"column":70},"end":{"line":622,"column":101}}]},"40":{"line":623,"type":"binary-expr","locations":[{"start":{"line":623,"column":18},"end":{"line":623,"column":34}},{"start":{"line":623,"column":38},"end":{"line":623,"column":50}},{"start":{"line":623,"column":54},"end":{"line":623,"column":88}}]},"41":{"line":639,"type":"if","locations":[{"start":{"line":639,"column":2},"end":{"line":639,"column":2}},{"start":{"line":639,"column":2},"end":{"line":639,"column":2}}]},"42":{"line":639,"type":"binary-expr","locations":[{"start":{"line":639,"column":6},"end":{"line":639,"column":18}},{"start":{"line":639,"column":23},"end":{"line":639,"column":42}}]},"43":{"line":640,"type":"binary-expr","locations":[{"start":{"line":640,"column":27},"end":{"line":640,"column":35}},{"start":{"line":640,"column":39},"end":{"line":640,"column":58}}]},"44":{"line":655,"type":"binary-expr","locations":[{"start":{"line":655,"column":10},"end":{"line":655,"column":23}},{"start":{"line":655,"column":27},"end":{"line":655,"column":46}}]},"45":{"line":658,"type":"binary-expr","locations":[{"start":{"line":658,"column":10},"end":{"line":658,"column":23}},{"start":{"line":658,"column":27},"end":{"line":658,"column":46}}]},"46":{"line":670,"type":"binary-expr","locations":[{"start":{"line":670,"column":15},"end":{"line":670,"column":26}},{"start":{"line":670,"column":30},"end":{"line":670,"column":43}}]},"47":{"line":688,"type":"if","locations":[{"start":{"line":688,"column":2},"end":{"line":688,"column":2}},{"start":{"line":688,"column":2},"end":{"line":688,"column":2}}]},"48":{"line":694,"type":"binary-expr","locations":[{"start":{"line":694,"column":20},"end":{"line":694,"column":34}},{"start":{"line":694,"column":38},"end":{"line":694,"column":43}}]},"49":{"line":695,"type":"binary-expr","locations":[{"start":{"line":695,"column":21},"end":{"line":695,"column":37}},{"start":{"line":695,"column":41},"end":{"line":695,"column":49}}]},"50":{"line":696,"type":"binary-expr","locations":[{"start":{"line":696,"column":17},"end":{"line":696,"column":33}},{"start":{"line":696,"column":37},"end":{"line":696,"column":49}},{"start":{"line":696,"column":53},"end":{"line":696,"column":64}}]},"51":{"line":697,"type":"binary-expr","locations":[{"start":{"line":697,"column":17},"end":{"line":697,"column":29}},{"start":{"line":697,"column":33},"end":{"line":697,"column":36}}]},"52":{"line":698,"type":"binary-expr","locations":[{"start":{"line":698,"column":17},"end":{"line":698,"column":29}},{"start":{"line":698,"column":33},"end":{"line":698,"column":36}}]},"53":{"line":700,"type":"if","locations":[{"start":{"line":700,"column":2},"end":{"line":700,"column":2}},{"start":{"line":700,"column":2},"end":{"line":700,"column":2}}]},"54":{"line":700,"type":"binary-expr","locations":[{"start":{"line":700,"column":6},"end":{"line":700,"column":20}},{"start":{"line":700,"column":24},"end":{"line":700,"column":52}}]},"55":{"line":707,"type":"if","locations":[{"start":{"line":707,"column":2},"end":{"line":707,"column":2}},{"start":{"line":707,"column":2},"end":{"line":707,"column":2}}]},"56":{"line":718,"type":"if","locations":[{"start":{"line":718,"column":2},"end":{"line":718,"column":2}},{"start":{"line":718,"column":2},"end":{"line":718,"column":2}}]},"57":{"line":724,"type":"if","locations":[{"start":{"line":724,"column":7},"end":{"line":724,"column":7}},{"start":{"line":724,"column":7},"end":{"line":724,"column":7}}]},"58":{"line":745,"type":"binary-expr","locations":[{"start":{"line":745,"column":31},"end":{"line":745,"column":50}},{"start":{"line":745,"column":54},"end":{"line":745,"column":72}}]},"59":{"line":746,"type":"if","locations":[{"start":{"line":746,"column":6},"end":{"line":746,"column":6}},{"start":{"line":746,"column":6},"end":{"line":746,"column":6}}]},"60":{"line":756,"type":"binary-expr","locations":[{"start":{"line":756,"column":31},"end":{"line":756,"column":63}},{"start":{"line":756,"column":67},"end":{"line":756,"column":98}}]},"61":{"line":757,"type":"if","locations":[{"start":{"line":757,"column":6},"end":{"line":757,"column":6}},{"start":{"line":757,"column":6},"end":{"line":757,"column":6}}]},"62":{"line":764,"type":"if","locations":[{"start":{"line":764,"column":6},"end":{"line":764,"column":6}},{"start":{"line":764,"column":6},"end":{"line":764,"column":6}}]},"63":{"line":765,"type":"if","locations":[{"start":{"line":765,"column":8},"end":{"line":765,"column":8}},{"start":{"line":765,"column":8},"end":{"line":765,"column":8}}]},"64":{"line":771,"type":"if","locations":[{"start":{"line":771,"column":8},"end":{"line":771,"column":8}},{"start":{"line":771,"column":8},"end":{"line":771,"column":8}}]},"65":{"line":784,"type":"if","locations":[{"start":{"line":784,"column":6},"end":{"line":784,"column":6}},{"start":{"line":784,"column":6},"end":{"line":784,"column":6}}]},"66":{"line":846,"type":"if","locations":[{"start":{"line":846,"column":2},"end":{"line":846,"column":2}},{"start":{"line":846,"column":2},"end":{"line":846,"column":2}}]},"67":{"line":857,"type":"binary-expr","locations":[{"start":{"line":857,"column":29},"end":{"line":857,"column":44}},{"start":{"line":857,"column":48},"end":{"line":857,"column":50}}]},"68":{"line":876,"type":"if","locations":[{"start":{"line":876,"column":2},"end":{"line":876,"column":2}},{"start":{"line":876,"column":2},"end":{"line":876,"column":2}}]},"69":{"line":886,"type":"if","locations":[{"start":{"line":886,"column":2},"end":{"line":886,"column":2}},{"start":{"line":886,"column":2},"end":{"line":886,"column":2}}]},"70":{"line":886,"type":"binary-expr","locations":[{"start":{"line":886,"column":6},"end":{"line":886,"column":18}},{"start":{"line":886,"column":23},"end":{"line":886,"column":42}}]},"71":{"line":887,"type":"binary-expr","locations":[{"start":{"line":887,"column":27},"end":{"line":887,"column":35}},{"start":{"line":887,"column":39},"end":{"line":887,"column":58}}]},"72":{"line":895,"type":"if","locations":[{"start":{"line":895,"column":2},"end":{"line":895,"column":2}},{"start":{"line":895,"column":2},"end":{"line":895,"column":2}}]},"73":{"line":897,"type":"if","locations":[{"start":{"line":897,"column":9},"end":{"line":897,"column":9}},{"start":{"line":897,"column":9},"end":{"line":897,"column":9}}]},"74":{"line":903,"type":"if","locations":[{"start":{"line":903,"column":2},"end":{"line":903,"column":2}},{"start":{"line":903,"column":2},"end":{"line":903,"column":2}}]},"75":{"line":905,"type":"if","locations":[{"start":{"line":905,"column":9},"end":{"line":905,"column":9}},{"start":{"line":905,"column":9},"end":{"line":905,"column":9}}]},"76":{"line":911,"type":"if","locations":[{"start":{"line":911,"column":2},"end":{"line":911,"column":2}},{"start":{"line":911,"column":2},"end":{"line":911,"column":2}}]},"77":{"line":913,"type":"if","locations":[{"start":{"line":913,"column":9},"end":{"line":913,"column":9}},{"start":{"line":913,"column":9},"end":{"line":913,"column":9}}]},"78":{"line":920,"type":"if","locations":[{"start":{"line":920,"column":2},"end":{"line":920,"column":2}},{"start":{"line":920,"column":2},"end":{"line":920,"column":2}}]},"79":{"line":922,"type":"if","locations":[{"start":{"line":922,"column":9},"end":{"line":922,"column":9}},{"start":{"line":922,"column":9},"end":{"line":922,"column":9}}]},"80":{"line":935,"type":"if","locations":[{"start":{"line":935,"column":2},"end":{"line":935,"column":2}},{"start":{"line":935,"column":2},"end":{"line":935,"column":2}}]}}}} \ No newline at end of file diff --git a/coverage/lcov-report/index.html b/coverage/lcov-report/index.html new file mode 100644 index 00000000..666562ca --- /dev/null +++ b/coverage/lcov-report/index.html @@ -0,0 +1,333 @@ + + + + Code coverage report for All files + + + + + + + +
+

Code coverage report for All files

+

+ + Statements: 93.19% (397 / 426)      + + + Branches: 79.88% (131 / 164)      + + + Functions: 93.75% (60 / 64)      + + + Lines: 93.19% (397 / 426)      + +

+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
lib/93.19%(397 / 426)79.88%(131 / 164)93.75%(60 / 64)93.19%(397 / 426)
+
+
+ + + + + + + + diff --git a/coverage/lcov-report/lib/http.js.html b/coverage/lcov-report/lib/http.js.html new file mode 100644 index 00000000..87dcdf04 --- /dev/null +++ b/coverage/lcov-report/lib/http.js.html @@ -0,0 +1,3311 @@ + + + + Code coverage report for lib/http.js + + + + + + + +
+

Code coverage report for lib/http.js

+

+ + Statements: 93.19% (397 / 426)      + + + Branches: 79.88% (131 / 164)      + + + Functions: 93.75% (60 / 64)      + + + Lines: 93.19% (397 / 426)      + +

+
All files » lib/ » http.js
+
+
+

+
+
1 +2 +3 +4 +5 +6 +7 +8 +9 +10 +11 +12 +13 +14 +15 +16 +17 +18 +19 +20 +21 +22 +23 +24 +25 +26 +27 +28 +29 +30 +31 +32 +33 +34 +35 +36 +37 +38 +39 +40 +41 +42 +43 +44 +45 +46 +47 +48 +49 +50 +51 +52 +53 +54 +55 +56 +57 +58 +59 +60 +61 +62 +63 +64 +65 +66 +67 +68 +69 +70 +71 +72 +73 +74 +75 +76 +77 +78 +79 +80 +81 +82 +83 +84 +85 +86 +87 +88 +89 +90 +91 +92 +93 +94 +95 +96 +97 +98 +99 +100 +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 +114 +115 +116 +117 +118 +119 +120 +121 +122 +123 +124 +125 +126 +127 +128 +129 +130 +131 +132 +133 +134 +135 +136 +137 +138 +139 +140 +141 +142 +143 +144 +145 +146 +147 +148 +149 +150 +151 +152 +153 +154 +155 +156 +157 +158 +159 +160 +161 +162 +163 +164 +165 +166 +167 +168 +169 +170 +171 +172 +173 +174 +175 +176 +177 +178 +179 +180 +181 +182 +183 +184 +185 +186 +187 +188 +189 +190 +191 +192 +193 +194 +195 +196 +197 +198 +199 +200 +201 +202 +203 +204 +205 +206 +207 +208 +209 +210 +211 +212 +213 +214 +215 +216 +217 +218 +219 +220 +221 +222 +223 +224 +225 +226 +227 +228 +229 +230 +231 +232 +233 +234 +235 +236 +237 +238 +239 +240 +241 +242 +243 +244 +245 +246 +247 +248 +249 +250 +251 +252 +253 +254 +255 +256 +257 +258 +259 +260 +261 +262 +263 +264 +265 +266 +267 +268 +269 +270 +271 +272 +273 +274 +275 +276 +277 +278 +279 +280 +281 +282 +283 +284 +285 +286 +287 +288 +289 +290 +291 +292 +293 +294 +295 +296 +297 +298 +299 +300 +301 +302 +303 +304 +305 +306 +307 +308 +309 +310 +311 +312 +313 +314 +315 +316 +317 +318 +319 +320 +321 +322 +323 +324 +325 +326 +327 +328 +329 +330 +331 +332 +333 +334 +335 +336 +337 +338 +339 +340 +341 +342 +343 +344 +345 +346 +347 +348 +349 +350 +351 +352 +353 +354 +355 +356 +357 +358 +359 +360 +361 +362 +363 +364 +365 +366 +367 +368 +369 +370 +371 +372 +373 +374 +375 +376 +377 +378 +379 +380 +381 +382 +383 +384 +385 +386 +387 +388 +389 +390 +391 +392 +393 +394 +395 +396 +397 +398 +399 +400 +401 +402 +403 +404 +405 +406 +407 +408 +409 +410 +411 +412 +413 +414 +415 +416 +417 +418 +419 +420 +421 +422 +423 +424 +425 +426 +427 +428 +429 +430 +431 +432 +433 +434 +435 +436 +437 +438 +439 +440 +441 +442 +443 +444 +445 +446 +447 +448 +449 +450 +451 +452 +453 +454 +455 +456 +457 +458 +459 +460 +461 +462 +463 +464 +465 +466 +467 +468 +469 +470 +471 +472 +473 +474 +475 +476 +477 +478 +479 +480 +481 +482 +483 +484 +485 +486 +487 +488 +489 +490 +491 +492 +493 +494 +495 +496 +497 +498 +499 +500 +501 +502 +503 +504 +505 +506 +507 +508 +509 +510 +511 +512 +513 +514 +515 +516 +517 +518 +519 +520 +521 +522 +523 +524 +525 +526 +527 +528 +529 +530 +531 +532 +533 +534 +535 +536 +537 +538 +539 +540 +541 +542 +543 +544 +545 +546 +547 +548 +549 +550 +551 +552 +553 +554 +555 +556 +557 +558 +559 +560 +561 +562 +563 +564 +565 +566 +567 +568 +569 +570 +571 +572 +573 +574 +575 +576 +577 +578 +579 +580 +581 +582 +583 +584 +585 +586 +587 +588 +589 +590 +591 +592 +593 +594 +595 +596 +597 +598 +599 +600 +601 +602 +603 +604 +605 +606 +607 +608 +609 +610 +611 +612 +613 +614 +615 +616 +617 +618 +619 +620 +621 +622 +623 +624 +625 +626 +627 +628 +629 +630 +631 +632 +633 +634 +635 +636 +637 +638 +639 +640 +641 +642 +643 +644 +645 +646 +647 +648 +649 +650 +651 +652 +653 +654 +655 +656 +657 +658 +659 +660 +661 +662 +663 +664 +665 +666 +667 +668 +669 +670 +671 +672 +673 +674 +675 +676 +677 +678 +679 +680 +681 +682 +683 +684 +685 +686 +687 +688 +689 +690 +691 +692 +693 +694 +695 +696 +697 +698 +699 +700 +701 +702 +703 +704 +705 +706 +707 +708 +709 +710 +711 +712 +713 +714 +715 +716 +717 +718 +719 +720 +721 +722 +723 +724 +725 +726 +727 +728 +729 +730 +731 +732 +733 +734 +735 +736 +737 +738 +739 +740 +741 +742 +743 +744 +745 +746 +747 +748 +749 +750 +751 +752 +753 +754 +755 +756 +757 +758 +759 +760 +761 +762 +763 +764 +765 +766 +767 +768 +769 +770 +771 +772 +773 +774 +775 +776 +777 +778 +779 +780 +781 +782 +783 +784 +785 +786 +787 +788 +789 +790 +791 +792 +793 +794 +795 +796 +797 +798 +799 +800 +801 +802 +803 +804 +805 +806 +807 +808 +809 +810 +811 +812 +813 +814 +815 +816 +817 +818 +819 +820 +821 +822 +823 +824 +825 +826 +827 +828 +829 +830 +831 +832 +833 +834 +835 +836 +837 +838 +839 +840 +841 +842 +843 +844 +845 +846 +847 +848 +849 +850 +851 +852 +853 +854 +855 +856 +857 +858 +859 +860 +861 +862 +863 +864 +865 +866 +867 +868 +869 +870 +871 +872 +873 +874 +875 +876 +877 +878 +879 +880 +881 +882 +883 +884 +885 +886 +887 +888 +889 +890 +891 +892 +893 +894 +895 +896 +897 +898 +899 +900 +901 +902 +903 +904 +905 +906 +907 +908 +909 +910 +911 +912 +913 +914 +915 +916 +917 +918 +919 +920 +921 +922 +923 +924 +925 +926 +927 +928 +929 +930 +931 +932 +933 +934 +935 +936 +937 +938 +939 +940 +941 +942 +943 +944 +945 +946 +947 +948 +949 +950 +951 +952 +953 +954 +955 +956 +957 +958 +959 +960 +961 +962 +963 +964 +965 +966 +967 +968 +969 +970 +971 +972 +973 +974 +975 +976 +977 +978 +979 +980 +981 +982 +983 +984 +985 +986 +987 +988 +989 +990 +991 +992 +993 +994 +995 +996 +997 +998 +999 +1000 +1001 +1002 +1003  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +  +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +1 +  +1 +1 +1 +  +1 +  +  +  +  +  +  +  +  +  +  +1 +  +  +  +  +  +1 +1 +  +  +  +  +  +  +  +3 +  +  +  +1 +  +  +  +  +1 +  +24 +24 +24 +  +24 +  +  +  +24 +24 +24 +  +  +24 +24 +24 +  +  +24 +24 +  +1 +  +  +  +  +  +1 +  +24 +  +  +24 +73 +13 +  +  +  +  +24 +24 +2 +  +  +  +1 +24 +  +  +1 +  +1 +60 +  +  +  +  +60 +  +  +1 +  +  +  +  +24 +168 +168 +  +  +  +  +  +  +24 +  +73 +  +  +  +  +  +  +73 +  +  +  +  +  +  +  +  +  +1 +  +27 +  +27 +27 +27 +  +27 +  +1 +  +1 +13 +12 +  +1 +  +  +  +1 +31 +23 +2 +  +  +2 +  +  +23 +  +8 +  +  +  +1 +4 +  +  +4 +4 +  +  +4 +  +  +  +1 +1 +  +  +1 +  +  +  +1 +3 +  +  +1 +2 +  +  +1 +  +1 +  +  +  +  +1 +1 +1 +1 +1 +  +  +  +  +1 +13 +  +13 +13 +  +13 +13 +  +  +13 +10 +10 +10 +10 +10 +10 +10 +10 +9 +9 +8 +  +1 +  +  +10 +  +  +  +3 +1 +1 +1 +  +  +  +  +2 +2 +  +  +11 +  +1 +  +  +1 +9 +  +9 +  +  +  +  +9 +  +9 +9 +10 +10 +  +10 +  +  +9 +9 +  +9 +  +  +1 +1 +  +1 +  +  +  +  +1 +1 +  +  +1 +  +  +  +  +  +1 +9 +  +9 +  +  +1 +4 +4 +  +  +1 +1 +1 +  +  +  +1 +  +1 +1 +  +  +  +  +  +1 +1 +  +  +  +  +  +  +  +  +1 +9 +  +  +9 +  +  +  +  +1 +  +  +  +  +  +1 +10 +1 +1 +  +  +10 +  +9 +9 +  +  +9 +  +  +  +  +  +1 +12 +  +1 +  +  +  +  +  +1 +  +  +  +  +  +  +  +  +  +  +12 +12 +12 +12 +  +  +12 +  +  +12 +  +  +12 +  +12 +  +  +  +  +  +1 +12 +  +12 +  +12 +12 +12 +  +12 +  +1 +  +1 +12 +1 +  +11 +  +  +12 +1 +  +12 +  +12 +11 +  +  +12 +  +12 +  +12 +12 +  +  +1 +23 +11 +  +  +  +1 +11 +11 +  +  +1 +12 +12 +  +  +1 +10 +  +  +1 +2 +1 +  +  +2 +  +  +  +2 +  +  +  +  +  +  +2 +  +  +  +2 +  +2 +  +  +  +  +1 +12 +  +  +12 +  +  +  +  +  +  +1 +1 +1 +1 +1 +1 +4 +  +1 +7 +  +  +  +  +  +1 +4 +  +4 +  +4 +4 +4 +  +  +  +  +  +4 +4 +4 +4 +  +4 +4 +  +1 +  +1 +12 +8 +  +4 +  +  +12 +12 +12 +12 +12 +  +12 +1 +1 +  +  +11 +  +11 +7 +  +  +11 +  +  +  +  +  +  +11 +1 +1 +  +  +  +10 +1 +1 +  +  +  +  +1 +1 +  +  +  +  +9 +9 +9 +9 +9 +9 +  +9 +9 +9 +  +  +9 +  +  +  +9 +1 +9 +9 +9 +8 +8 +8 +8 +8 +  +9 +1 +1 +  +  +  +  +8 +7 +  +7 +7 +  +1 +  +  +  +  +9 +9 +9 +8 +  +1 +  +  +  +  +11 +  +  +1 +7 +7 +7 +  +  +1 +8 +8 +8 +8 +8 +8 +8 +8 +  +  +1 +  +1 +  +  +1 +  +  +  +1 +  +  +  +  +1 +15 +  +15 +  +15 +  +1 +  +1 +10 +  +10 +  +10 +1 +  +10 +10 +  +10 +  +  +  +10 +10 +10 +10 +  +10 +  +  +10 +10 +  +10 +  +10 +10 +  +10 +  +  +1 +5 +5 +5 +  +  +1 +  +  +  +  +  +  +  +  +  +1 +40 +  +  +40 +  +  +  +  +1 +3 +1 +2 +1 +  +  +  +1 +3 +1 +2 +1 +  +  +  +1 +3 +1 +2 +1 +  +  +  +  +1 +3 +1 +2 +1 +  +1 +  +  +  +  +1 +2 +  +2 +  +2 +2 +  +  +  +  +  +  +  +  +1 +12 +  +1 +  +  +  +  +  +1 +  +  +  +  +  +  +  +12 +  +  +12 +  +  +12 +12 +  +  +  +  +  +1 +2 +2 +2 +2 +  +2 +  +2 +  +2 +  +2 +2 +  +2 +  +1 +  +1 +  +  +  +1 +  +  +  +1 + 
// Public API
+// ==========
+ 
+// The main governing power behind the http2 API design is that it should look very similar to the
+// existing node.js [HTTPS API][1] (which is, in turn, almost identical to the [HTTP API][2]). The
+// additional features of HTTP/2 are exposed as extensions to this API. Furthermore, node-http2
+// should fall back to using HTTP/1.1 if needed. Compatibility with undocumented or deprecated
+// elements of the node.js HTTP/HTTPS API is a non-goal.
+//
+// Additional and modified API elements
+// ------------------------------------
+//
+// - **Class: http2.Endpoint**: an API for using the raw HTTP/2 framing layer. For documentation
+//   see the [lib/endpoint.js](endpoint.html) file.
+//
+// - **Class: http2.Server**
+//   - **Event: 'connection' (socket, [endpoint])**: there's a second argument if the negotiation of
+//     HTTP/2 was successful: the reference to the [Endpoint](endpoint.html) object tied to the
+//     socket.
+//
+// - **http2.createServer(options, [requestListener])**: additional option:
+//   - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object
+//   - **plain**: if `true`, the server will accept HTTP/2 connections over plain TCP instead of
+//     TLS
+//
+// - **Class: http2.ServerResponse**
+//   - **response.push(options)**: initiates a server push. `options` describes the 'imaginary'
+//     request to which the push stream is a response; the possible options are identical to the
+//     ones accepted by `http2.request`. Returns a ServerResponse object that can be used to send
+//     the response headers and content.
+//
+// - **Class: http2.Agent**
+//   - **new Agent(options)**: additional option:
+//     - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object
+//   - **agent.sockets**: only contains TCP sockets that corresponds to HTTP/1 requests.
+//   - **agent.endpoints**: contains [Endpoint](endpoint.html) objects for HTTP/2 connections.
+//
+// - **http2.request(options, [callback])**: additional option:
+//   - **plain**: if `true`, the client will not try to build a TLS tunnel, instead it will use
+//     the raw TCP stream for HTTP/2
+//
+// - **Class: http2.ClientRequest**
+//   - **Event: 'socket' (socket)**: in case of an HTTP/2 incoming message, `socket` is a reference
+//     to the associated [HTTP/2 Stream](stream.html) object (and not to the TCP socket).
+//   - **Event: 'push' (promise)**: signals the intention of a server push associated to this
+//     request. `promise` is an IncomingPromise. If there's no listener for this event, the server
+//     push is cancelled.
+//   - **request.setPriority(priority)**: assign a priority to this request. `priority` is a number
+//     between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.
+//
+// - **Class: http2.IncomingMessage**
+//   - has two subclasses for easier interface description: **IncomingRequest** and
+//     **IncomingResponse**
+//   - **message.socket**: in case of an HTTP/2 incoming message, it's a reference to the associated
+//     [HTTP/2 Stream](stream.html) object (and not to the TCP socket).
+//
+// - **Class: http2.IncomingRequest (IncomingMessage)**
+//   - **message.url**: in case of an HTTP/2 incoming request, the `url` field always contains the
+//     path, and never a full url (it contains the path in most cases in the HTTPS api as well).
+//   - **message.scheme**: additional field. Mandatory HTTP/2 request metadata.
+//   - **message.host**: additional field. Mandatory HTTP/2 request metadata. Note that this
+//     replaces the old Host header field, but node-http2 will add Host to the `message.headers` for
+//     backwards compatibility.
+//
+// - **Class: http2.IncomingPromise (IncomingRequest)**
+//   - contains the metadata of the 'imaginary' request to which the server push is an answer.
+//   - **Event: 'response' (response)**: signals the arrival of the actual push stream. `response`
+//     is an IncomingResponse.
+//   - **Event: 'push' (promise)**: signals the intention of a server push associated to this
+//     request. `promise` is an IncomingPromise. If there's no listener for this event, the server
+//     push is cancelled.
+//   - **promise.cancel()**: cancels the promised server push.
+//   - **promise.setPriority(priority)**: assign a priority to this push stream. `priority` is a
+//     number between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.
+//
+// API elements not yet implemented
+// --------------------------------
+//
+// - **Class: http2.Server**
+//   - **server.maxHeadersCount**
+//
+// API elements that are not applicable to HTTP/2
+// ----------------------------------------------
+//
+// The reason may be deprecation of certain HTTP/1.1 features, or that some API elements simply
+// don't make sense when using HTTP/2. These will not be present when a request is done with HTTP/2,
+// but will function normally when falling back to using HTTP/1.1.
+//
+// - **Class: http2.Server**
+//   - **Event: 'checkContinue'**: not in the spec, yet (see [http-spec#18][expect-continue])
+//   - **Event: 'upgrade'**: upgrade is deprecated in HTTP/2
+//   - **Event: 'timeout'**: HTTP/2 sockets won't timeout because of application level keepalive
+//     (PING frames)
+//   - **Event: 'connect'**: not in the spec, yet (see [http-spec#230][connect])
+//   - **server.setTimeout(msecs, [callback])**
+//   - **server.timeout**
+//
+// - **Class: http2.ServerResponse**
+//   - **Event: 'close'**
+//   - **Event: 'timeout'**
+//   - **response.writeContinue()**
+//   - **response.writeHead(statusCode, [reasonPhrase], [headers])**: reasonPhrase will always be
+//     ignored since [it's not supported in HTTP/2][3]
+//   - **response.setTimeout(timeout, [callback])**
+//
+// - **Class: http2.Agent**
+//   - **agent.maxSockets**: only affects HTTP/1 connection pool. When using HTTP/2, there's always
+//     one connection per host.
+//
+// - **Class: http2.ClientRequest**
+//   - **Event: 'upgrade'**
+//   - **Event: 'connect'**
+//   - **Event: 'continue'**
+//   - **request.setTimeout(timeout, [callback])**
+//   - **request.setNoDelay([noDelay])**
+//   - **request.setSocketKeepAlive([enable], [initialDelay])**
+//
+// - **Class: http2.IncomingMessage**
+//   - **Event: 'close'**
+//   - **message.setTimeout(timeout, [callback])**
+//
+// [1]: http://nodejs.org/api/https.html
+// [2]: http://nodejs.org/api/http.html
+// [3]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-11#section-8.1.3.2
+// [expect-continue]: https://github.com/http2/http2-spec/issues/18
+// [connect]: https://github.com/http2/http2-spec/issues/230
+ 
+// Common server and client side code
+// ==================================
+ 
+var net = require('net');
+var url = require('url');
+var util = require('util');
+var EventEmitter = require('events').EventEmitter;
+var PassThrough = require('stream').PassThrough;
+var Readable = require('stream').Readable;
+var Writable = require('stream').Writable;
+var Endpoint = require('http2-protocol').Endpoint;
+var implementedVersion = require('http2-protocol').ImplementedVersion;
+var http = require('http');
+var https = require('https');
+ 
+exports.STATUS_CODES = http.STATUS_CODES;
+exports.IncomingMessage = IncomingMessage;
+exports.OutgoingMessage = OutgoingMessage;
+ 
+var deprecatedHeaders = [
+  'connection',
+  'host',
+  'keep-alive',
+  'proxy-connection',
+  'te',
+  'transfer-encoding',
+  'upgrade'
+];
+ 
+// When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback
+var supportedProtocols = [implementedVersion, 'http/1.1', 'http/1.0'];
+ 
+// Logging
+// -------
+ 
+// Logger shim, used when no logger is provided by the user.
+function noop() {}
+var defaultLogger = {
+  fatal: noop,
+  error: noop,
+  warn : noop,
+  info : noop,
+  debug: noop,
+  trace: noop,
+ 
+  child: function() { return this; }
+};
+ 
+// Bunyan serializers exported by submodules that are worth adding when creating a logger.
+exports.serializers = require('http2-protocol').serializers;
+ 
+// IncomingMessage class
+// ---------------------
+ 
+function IncomingMessage(stream) {
+  // * This is basically a read-only wrapper for the [Stream](stream.html) class.
+  PassThrough.call(this);
+  stream.pipe(this);
+  this.socket = this.stream = stream;
+ 
+  this._log = stream._log.child({ component: 'http' });
+ 
+  // * HTTP/2.0 does not define a way to carry the version identifier that is included in the
+  //   HTTP/1.1 request/status line. Version is always 2.0.
+  this.httpVersion = '2.0';
+  this.httpVersionMajor = 2;
+  this.httpVersionMinor = 0;
+ 
+  // * `this.headers` will store the regular headers (and none of the special colon headers)
+  this.headers = {};
+  this.trailers = undefined;
+  this._lastHeadersSeen = undefined;
+ 
+  // * Other metadata is filled in when the headers arrive.
+  stream.once('headers', this._onHeaders.bind(this));
+  stream.once('end', this._onEnd.bind(this));
+}
+IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } });
+ 
+// [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-11#section-8.1.3.1)
+// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
+//   of key-value pairs. This includes the target URI for the request, the status code for the
+//   response, as well as HTTP header fields.
+IncomingMessage.prototype._onHeaders = function _onHeaders(headers) {
+  // * Detects malformed headers
+  this._validateHeaders(headers);
+ 
+  // * Store the _regular_ headers in `this.headers`
+  for (var name in headers) {
+    if (name[0] !== ':') {
+      this.headers[name] = headers[name];
+    }
+  }
+ 
+  // * The last header block, if it's not the first, will represent the trailers
+  var self = this;
+  this.stream.on('headers', function(headers) {
+    self._lastHeadersSeen = headers;
+  });
+};
+ 
+IncomingMessage.prototype._onEnd = function _onEnd() {
+  this.trailers = this._lastHeadersSeen;
+};
+ 
+IncomingMessage.prototype.setTimeout = noop;
+ 
+IncomingMessage.prototype._checkSpecialHeader = function _checkSpecialHeader(key, value) {
+  Iif ((typeof value !== 'string') || (value.length === 0)) {
+    this._log.error({ key: key, value: value }, 'Invalid or missing special header field');
+    this.stream.reset('PROTOCOL_ERROR');
+  }
+ 
+  return value;
+};
+ 
+IncomingMessage.prototype._validateHeaders = function _validateHeaders(headers) {
+  // * An HTTP/2.0 request or response MUST NOT include any of the following header fields:
+  //   Connection, Host, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, and Upgrade. A server
+  //   MUST treat the presence of any of these header fields as a stream error of type
+  //   PROTOCOL_ERROR.
+  for (var i = 0; i < deprecatedHeaders.length; i++) {
+    var key = deprecatedHeaders[i];
+    Iif (key in headers) {
+      this._log.error({ key: key, value: headers[key] }, 'Deprecated header found');
+      this.stream.reset('PROTOCOL_ERROR');
+      return;
+    }
+  }
+ 
+  for (var headerName in headers) {
+    // * Empty header name field is malformed
+    Iif (headerName.length <= 1) {
+      this.stream.reset('PROTOCOL_ERROR');
+      return;
+    }
+    // * A request or response containing uppercase header name field names MUST be
+    //   treated as malformed (Section 8.1.3.5). Implementations that detect malformed
+    //   requests or responses need to ensure that the stream ends.
+    Iif(/[A-Z]/.test(headerName)) {
+      this.stream.reset('PROTOCOL_ERROR');
+      return;
+    }
+  }
+};
+ 
+// OutgoingMessage class
+// ---------------------
+ 
+function OutgoingMessage() {
+  // * This is basically a read-only wrapper for the [Stream](stream.html) class.
+  Writable.call(this);
+ 
+  this._headers = {};
+  this._trailers = undefined;
+  this.headersSent = false;
+ 
+  this.on('finish', this._finish);
+}
+OutgoingMessage.prototype = Object.create(Writable.prototype, { constructor: { value: OutgoingMessage } });
+ 
+OutgoingMessage.prototype._write = function _write(chunk, encoding, callback) {
+  if (this.stream) {
+    this.stream.write(chunk, encoding, callback);
+  } else {
+    this.once('socket', this._write.bind(this, chunk, encoding, callback));
+  }
+};
+ 
+OutgoingMessage.prototype._finish = function _finish() {
+  if (this.stream) {
+    if (this._trailers) {
+      Iif (this.request) {
+        this.request.addTrailers(this._trailers);
+      } else {
+        this.stream.headers(this._trailers);
+      }
+    }
+    this.stream.end();
+  } else {
+    this.once('socket', this._finish.bind(this));
+  }
+};
+ 
+OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
+  Iif (this.headersSent) {
+    throw new Error('Can\'t set headers after they are sent.');
+  } else {
+    name = name.toLowerCase();
+    Iif (deprecatedHeaders.indexOf(name) !== -1) {
+      throw new Error('Cannot set deprecated header: ' + name);
+    }
+    this._headers[name] = value;
+  }
+};
+ 
+OutgoingMessage.prototype.removeHeader = function removeHeader(name) {
+  Iif (this.headersSent) {
+    throw new Error('Can\'t remove headers after they are sent.');
+  } else {
+    delete this._headers[name.toLowerCase()];
+  }
+};
+ 
+OutgoingMessage.prototype.getHeader = function getHeader(name) {
+  return this._headers[name.toLowerCase()];
+};
+ 
+OutgoingMessage.prototype.addTrailers = function addTrailers(trailers) {
+  this._trailers = trailers;
+};
+ 
+OutgoingMessage.prototype.setTimeout = noop;
+ 
+OutgoingMessage.prototype._checkSpecialHeader = IncomingMessage.prototype._checkSpecialHeader;
+ 
+// Server side
+// ===========
+ 
+exports.createServer = createServer;
+exports.Server = Server;
+exports.IncomingRequest = IncomingRequest;
+exports.OutgoingResponse = OutgoingResponse;
+exports.ServerResponse = OutgoingResponse; // for API compatibility
+ 
+// Server class
+// ------------
+ 
+function Server(options) {
+  options = util._extend({}, options);
+ 
+  this._log = (options.log || defaultLogger).child({ component: 'http' });
+  this._settings = options.settings;
+ 
+  var start = this._start.bind(this);
+  var fallback = this._fallback.bind(this);
+ 
+  // HTTP2 over TLS (using NPN or ALPN)
+  if ((options.key && options.cert) || options.pfx) {
+    this._log.info('Creating HTTP/2 server over TLS');
+    this._mode = 'tls';
+    options.ALPNProtocols = supportedProtocols;
+    options.NPNProtocols = supportedProtocols;
+    this._server = https.createServer(options);
+    this._originalSocketListeners = this._server.listeners('secureConnection');
+    this._server.removeAllListeners('secureConnection');
+    this._server.on('secureConnection', function(socket) {
+      var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
+      if ((negotiatedProtocol === implementedVersion) && socket.servername) {
+        start(socket);
+      } else {
+        fallback(socket);
+      }
+    });
+    this._server.on('request', this.emit.bind(this, 'request'));
+  }
+ 
+  // HTTP2 over plain TCP
+  else if (options.plain) {
+    this._log.info('Creating HTTP/2 server over plain TCP');
+    this._mode = 'plain';
+    this._server = net.createServer(start);
+  }
+ 
+  // HTTP/2 with HTTP/1.1 upgrade
+  else {
+    this._log.error('Trying to create HTTP/2 server with Upgrade from HTTP/1.1');
+    throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported. Please provide TLS keys.');
+  }
+ 
+  this._server.on('close', this.emit.bind(this, 'close'));
+}
+Server.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Server } });
+ 
+// Starting HTTP/2
+Server.prototype._start = function _start(socket) {
+  var endpoint = new Endpoint(this._log, 'SERVER', this._settings);
+ 
+  this._log.info({ e: endpoint,
+                   client: socket.remoteAddress + ':' + socket.remotePort,
+                   SNI: socket.servername
+                 }, 'New incoming HTTP/2 connection');
+ 
+  endpoint.pipe(socket).pipe(endpoint);
+ 
+  var self = this;
+  endpoint.on('stream', function _onStream(stream) {
+    var response = new OutgoingResponse(stream);
+    var request = new IncomingRequest(stream);
+ 
+    request.once('ready', self.emit.bind(self, 'request', request, response));
+  });
+ 
+  endpoint.on('error', this.emit.bind(this, 'clientError'));
+  socket.on('error', this.emit.bind(this, 'clientError'));
+ 
+  this.emit('connection', socket, endpoint);
+};
+ 
+Server.prototype._fallback = function _fallback(socket) {
+  var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
+ 
+  this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort,
+                   protocol: negotiatedProtocol,
+                   SNI: socket.servername
+                 }, 'Falling back to simple HTTPS');
+ 
+  for (var i = 0; i < this._originalSocketListeners.length; i++) {
+    this._originalSocketListeners[i].call(this._server, socket);
+  }
+ 
+  this.emit('connection', socket);
+};
+ 
+// There are [3 possible signatures][1] of the `listen` function. Every arguments is forwarded to
+// the backing TCP or HTTPS server.
+// [1]: http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback
+Server.prototype.listen = function listen(port, hostname) {
+  this._log.info({ on: ((typeof hostname === 'string') ? (hostname + ':' + port) : port) },
+                 'Listening for incoming connections');
+  this._server.listen.apply(this._server, arguments);
+};
+ 
+Server.prototype.close = function close(callback) {
+  this._log.info('Closing server');
+  this._server.close(callback);
+};
+ 
+Server.prototype.setTimeout = function setTimeout(timeout, callback) {
+  Eif (this._mode === 'tls') {
+    this._server.setTimeout(timeout, callback);
+  }
+};
+ 
+Object.defineProperty(Server.prototype, 'timeout', {
+  get: function getTimeout() {
+    Eif (this._mode === 'tls') {
+      return this._server.timeout;
+    } else {
+      return undefined;
+    }
+  },
+  set: function setTimeout(timeout) {
+    Eif (this._mode === 'tls') {
+      this._server.timeout = timeout;
+    }
+  }
+});
+ 
+// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
+// `server`.There are events on the `http.Server` class where it makes difference whether someone is
+// listening on the event or not. In these cases, we can not simply forward the events from the
+// `server` to `this` since that means a listener. Instead, we forward the subscriptions.
+Server.prototype.on = function on(event, listener) {
+  Iif ((event === 'upgrade') || (event === 'timeout')) {
+    this._server.on(event, listener && listener.bind(this));
+  } else {
+    EventEmitter.prototype.on.call(this, event, listener);
+  }
+};
+ 
+// `addContext` is used to add Server Name Indication contexts
+Server.prototype.addContext = function addContext(hostname, credentials) {
+  if (this._mode === 'tls') {
+    this._server.addContext(hostname, credentials);
+  }
+};
+ 
+function createServer(options, requestListener) {
+  if (typeof options === 'function') {
+    requestListener = options;
+    options = undefined;
+  }
+ 
+  var server = new Server(options);
+ 
+  Eif (requestListener) {
+    server.on('request', requestListener);
+  }
+ 
+  return server;
+}
+ 
+// IncomingRequest class
+// ---------------------
+ 
+function IncomingRequest(stream) {
+  IncomingMessage.call(this, stream);
+}
+IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } });
+ 
+// [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-11#section-8.1.3.1)
+// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
+//   of key-value pairs. This includes the target URI for the request, the status code for the
+//   response, as well as HTTP header fields.
+IncomingRequest.prototype._onHeaders = function _onHeaders(headers) {
+  // * The ":method" header field includes the HTTP method
+  // * The ":scheme" header field includes the scheme portion of the target URI
+  // * The ":authority" header field includes the authority portion of the target URI
+  // * The ":path" header field includes the path and query parts of the target URI.
+  //   This field MUST NOT be empty; URIs that do not contain a path component MUST include a value
+  //   of '/', unless the request is an OPTIONS request for '*', in which case the ":path" header
+  //   field MUST include '*'.
+  // * All HTTP/2.0 requests MUST include exactly one valid value for all of these header fields. A
+  //   server MUST treat the absence of any of these header fields, presence of multiple values, or
+  //   an invalid value as a stream error of type PROTOCOL_ERROR.
+  this.method = this._checkSpecialHeader(':method'   , headers[':method']);
+  this.scheme = this._checkSpecialHeader(':scheme'   , headers[':scheme']);
+  this.host   = this._checkSpecialHeader(':authority', headers[':authority']  );
+  this.url    = this._checkSpecialHeader(':path'     , headers[':path']  );
+ 
+  // * Host header is included in the headers object for backwards compatibility.
+  this.headers.host = this.host;
+ 
+  // * Handling regular headers.
+  IncomingMessage.prototype._onHeaders.call(this, headers);
+ 
+  // * Signaling that the headers arrived.
+  this._log.info({ method: this.method, scheme: this.scheme, host: this.host,
+                   path: this.url, headers: this.headers }, 'Incoming request');
+  this.emit('ready');
+};
+ 
+// OutgoingResponse class
+// ----------------------
+ 
+function OutgoingResponse(stream) {
+  OutgoingMessage.call(this);
+ 
+  this._log = stream._log.child({ component: 'http' });
+ 
+  this.stream = stream;
+  this.statusCode = 200;
+  this.sendDate = true;
+ 
+  this.stream.once('headers', this._onRequestHeaders.bind(this));
+}
+OutgoingResponse.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingResponse } });
+ 
+OutgoingResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase, headers) {
+  if (typeof reasonPhrase === 'string') {
+    this._log.warn('Reason phrase argument was present but ignored by the writeHead method');
+  } else {
+    headers = reasonPhrase;
+  }
+ 
+  for (var name in headers) {
+    this.setHeader(name, headers[name]);
+  }
+  headers = this._headers;
+ 
+  if (this.sendDate && !('date' in this._headers)) {
+    headers.date = (new Date()).toUTCString();
+  }
+ 
+  this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response');
+ 
+  headers[':status'] = this.statusCode = statusCode;
+ 
+  this.stream.headers(headers);
+  this.headersSent = true;
+};
+ 
+OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() {
+  if (!this.headersSent) {
+    this.writeHead(this.statusCode);
+  }
+};
+ 
+OutgoingResponse.prototype.write = function write() {
+  this._implicitHeaders();
+  return OutgoingMessage.prototype.write.apply(this, arguments);
+};
+ 
+OutgoingResponse.prototype.end = function end() {
+  this._implicitHeaders();
+  return OutgoingMessage.prototype.end.apply(this, arguments);
+};
+ 
+OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) {
+  this._requestHeaders = headers;
+};
+ 
+OutgoingResponse.prototype.push = function push(options) {
+  if (typeof options === 'string') {
+    options = url.parse(options);
+  }
+ 
+  Iif (!options.path) {
+    throw new Error('`path` option is mandatory.');
+  }
+ 
+  var promise = util._extend({
+    ':method': (options.method || 'GET').toUpperCase(),
+    ':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'],
+    ':authority': options.hostname || options.host || this._requestHeaders[':authority'],
+    ':path': options.path
+  }, options.headers);
+ 
+  this._log.info({ method: promise[':method'], scheme: promise[':scheme'],
+                   authority: promise[':authority'], path: promise[':path'],
+                   headers: options.headers }, 'Promising push stream');
+ 
+  var pushStream = this.stream.promise(promise);
+ 
+  return new OutgoingResponse(pushStream);
+};
+ 
+// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
+// `request`. See `Server.prototype.on` for explanation.
+OutgoingResponse.prototype.on = function on(event, listener) {
+  Iif (this.request && (event === 'timeout')) {
+    this.request.on(event, listener && listener.bind(this));
+  } else {
+    OutgoingMessage.prototype.on.call(this, event, listener);
+  }
+};
+ 
+// Client side
+// ===========
+ 
+exports.ClientRequest = OutgoingRequest; // for API compatibility
+exports.OutgoingRequest = OutgoingRequest;
+exports.IncomingResponse = IncomingResponse;
+exports.Agent = Agent;
+exports.globalAgent = undefined;
+exports.request = function request(options, callback) {
+  return (options.agent || exports.globalAgent).request(options, callback);
+};
+exports.get = function get(options, callback) {
+  return (options.agent || exports.globalAgent).get(options, callback);
+};
+ 
+// Agent class
+// -----------
+ 
+function Agent(options) {
+  EventEmitter.call(this);
+ 
+  options = util._extend({}, options);
+ 
+  this._settings = options.settings;
+  this._log = (options.log || defaultLogger).child({ component: 'http' });
+  this.endpoints = {};
+ 
+  // * Using an own HTTPS agent, because the global agent does not look at `NPN/ALPNProtocols` when
+  //   generating the key identifying the connection, so we may get useless non-negotiated TLS
+  //   channels even if we ask for a negotiated one. This agent will contain only negotiated
+  //   channels.
+  var agentOptions = {};
+  agentOptions.ALPNProtocols = supportedProtocols;
+  agentOptions.NPNProtocols = supportedProtocols;
+  this._httpsAgent = new https.Agent(agentOptions);
+ 
+  this.sockets = this._httpsAgent.sockets;
+  this.requests = this._httpsAgent.requests;
+}
+Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } });
+ 
+Agent.prototype.request = function request(options, callback) {
+  if (typeof options === 'string') {
+    options = url.parse(options);
+  } else {
+    options = util._extend({}, options);
+  }
+ 
+  options.method = (options.method || 'GET').toUpperCase();
+  options.protocol = options.protocol || 'https:';
+  options.host = options.hostname || options.host || 'localhost';
+  options.port = options.port || 443;
+  options.path = options.path || '/';
+ 
+  if (!options.plain && options.protocol === 'http:') {
+    this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1');
+    throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.');
+  }
+ 
+  var request = new OutgoingRequest(this._log);
+ 
+  if (callback) {
+    request.on('response', callback);
+  }
+ 
+  var key = [
+    !!options.plain,
+    options.host,
+    options.port
+  ].join(':');
+ 
+  // * There's an existing HTTP/2 connection to this host
+  if (key in this.endpoints) {
+    var endpoint = this.endpoints[key];
+    request._start(endpoint.createStream(), options);
+  }
+ 
+  // * HTTP/2 over plain TCP
+  else if (options.plain) {
+    endpoint = new Endpoint(this._log, 'CLIENT', this._settings);
+    endpoint.socket = net.connect({
+      host: options.host,
+      port: options.port,
+      localAddress: options.localAddress
+    });
+    endpoint.pipe(endpoint.socket).pipe(endpoint);
+    request._start(endpoint.createStream(), options);
+  }
+ 
+  // * HTTP/2 over TLS negotiated using NPN or ALPN
+  else {
+    var started = false;
+    options.ALPNProtocols = supportedProtocols;
+    options.NPNProtocols = supportedProtocols;
+    options.servername = options.host; // Server Name Indication
+    options.agent = this._httpsAgent;
+    var httpsRequest = https.request(options);
+ 
+    httpsRequest.on('socket', function(socket) {
+      var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
+      Iif (negotiatedProtocol !== undefined) {
+        negotiated();
+      } else {
+        socket.on('secureConnect', negotiated);
+      }
+    });
+ 
+    var self = this;
+    function negotiated() {
+      var endpoint;
+      var negotiatedProtocol = httpsRequest.socket.alpnProtocol || httpsRequest.socket.npnProtocol;
+      if (negotiatedProtocol === implementedVersion) {
+        httpsRequest.socket.emit('agentRemove');
+        unbundleSocket(httpsRequest.socket);
+        endpoint = new Endpoint(self._log, 'CLIENT', self._settings);
+        endpoint.socket = httpsRequest.socket;
+        endpoint.pipe(endpoint.socket).pipe(endpoint);
+      }
+      if (started) {
+        Eif (endpoint) {
+          endpoint.close();
+        } else {
+          httpsRequest.abort();
+        }
+      } else {
+        if (endpoint) {
+          self._log.info({ e: endpoint, server: options.host + ':' + options.port },
+                         'New outgoing HTTP/2 connection');
+          self.endpoints[key] = endpoint;
+          self.emit(key, endpoint);
+        } else {
+          self.emit(key, undefined);
+        }
+      }
+    }
+ 
+    this.once(key, function(endpoint) {
+      started = true;
+      if (endpoint) {
+        request._start(endpoint.createStream(), options);
+      } else {
+        request._fallback(httpsRequest);
+      }
+    });
+  }
+ 
+  return request;
+};
+ 
+Agent.prototype.get = function get(options, callback) {
+  var request = this.request(options, callback);
+  request.end();
+  return request;
+};
+ 
+function unbundleSocket(socket) {
+  socket.removeAllListeners('data');
+  socket.removeAllListeners('end');
+  socket.removeAllListeners('readable');
+  socket.removeAllListeners('close');
+  socket.removeAllListeners('error');
+  socket.unpipe();
+  delete socket.ondata;
+  delete socket.onend;
+}
+ 
+Object.defineProperty(Agent.prototype, 'maxSockets', {
+  get: function getMaxSockets() {
+    return this._httpsAgent.maxSockets;
+  },
+  set: function setMaxSockets(value) {
+    this._httpsAgent.maxSockets = value;
+  }
+});
+ 
+exports.globalAgent = new Agent();
+ 
+// OutgoingRequest class
+// ---------------------
+ 
+function OutgoingRequest() {
+  OutgoingMessage.call(this);
+ 
+  this._log = undefined;
+ 
+  this.stream = undefined;
+}
+OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } });
+ 
+OutgoingRequest.prototype._start = function _start(stream, options) {
+  this.stream = stream;
+ 
+  this._log = stream._log.child({ component: 'http' });
+ 
+  for (var key in options.headers) {
+    this.setHeader(key, options.headers[key]);
+  }
+  var headers = this._headers;
+  delete headers.host;
+ 
+  Iif (options.auth) {
+    headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64');
+  }
+ 
+  headers[':scheme'] = options.protocol.slice(0, -1);
+  headers[':method'] = options.method;
+  headers[':authority'] = options.host;
+  headers[':path'] = options.path;
+ 
+  this._log.info({ scheme: headers[':scheme'], method: headers[':method'],
+                   authority: headers[':authority'], path: headers[':path'],
+                   headers: (options.headers || {}) }, 'Sending request');
+  this.stream.headers(headers);
+  this.headersSent = true;
+ 
+  this.emit('socket', this.stream);
+ 
+  var response = new IncomingResponse(this.stream);
+  response.once('ready', this.emit.bind(this, 'response', response));
+ 
+  this.stream.on('promise', this._onPromise.bind(this));
+};
+ 
+OutgoingRequest.prototype._fallback = function _fallback(request) {
+  request.on('response', this.emit.bind(this, 'response'));
+  this.stream = this.request = request;
+  this.emit('socket', this.socket);
+};
+ 
+OutgoingRequest.prototype.setPriority = function setPriority(priority) {
+  if (this.stream) {
+    this.stream.priority(priority);
+  } else {
+    this.once('socket', this.setPriority.bind(this, priority));
+  }
+};
+ 
+// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
+// `request`. See `Server.prototype.on` for explanation.
+OutgoingRequest.prototype.on = function on(event, listener) {
+  Iif (this.request && (event === 'upgrade')) {
+    this.request.on(event, listener && listener.bind(this));
+  } else {
+    OutgoingMessage.prototype.on.call(this, event, listener);
+  }
+};
+ 
+// Methods only in fallback mode
+OutgoingRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
+  if (this.request) {
+    this.request.setNoDelay(noDelay);
+  } else if (!this.stream) {
+    this.on('socket', this.setNoDelay.bind(this, noDelay));
+  }
+};
+ 
+OutgoingRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive(enable, initialDelay) {
+  if (this.request) {
+    this.request.setSocketKeepAlive(enable, initialDelay);
+  } else if (!this.stream) {
+    this.on('socket', this.setSocketKeepAlive.bind(this, enable, initialDelay));
+  }
+};
+ 
+OutgoingRequest.prototype.setTimeout = function setTimeout(timeout, callback) {
+  if (this.request) {
+    this.request.setTimeout(timeout, callback);
+  } else if (!this.stream) {
+    this.on('socket', this.setTimeout.bind(this, timeout, callback));
+  }
+};
+ 
+// Aborting the request
+OutgoingRequest.prototype.abort = function abort() {
+  if (this.request) {
+    this.request.abort();
+  } else if (this.stream) {
+    this.stream.reset('CANCEL');
+  } else {
+    this.on('socket', this.abort.bind(this));
+  }
+};
+ 
+// Receiving push promises
+OutgoingRequest.prototype._onPromise = function _onPromise(stream, headers) {
+  this._log.info({ push_stream: stream.id }, 'Receiving push promise');
+ 
+  var promise = new IncomingPromise(stream, headers);
+ 
+  Eif (this.listeners('push').length > 0) {
+    this.emit('push', promise);
+  } else {
+    promise.cancel();
+  }
+};
+ 
+// IncomingResponse class
+// ----------------------
+ 
+function IncomingResponse(stream) {
+  IncomingMessage.call(this, stream);
+}
+IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } });
+ 
+// [Response Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-11#section-8.1.3.2)
+// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
+//   of key-value pairs. This includes the target URI for the request, the status code for the
+//   response, as well as HTTP header fields.
+IncomingResponse.prototype._onHeaders = function _onHeaders(headers) {
+  // * A single ":status" header field is defined that carries the HTTP status code field. This
+  //   header field MUST be included in all responses.
+  // * A client MUST treat the absence of the ":status" header field, the presence of multiple
+  //   values, or an invalid value as a stream error of type PROTOCOL_ERROR.
+  //   Note: currently, we do not enforce it strictly: we accept any format, and parse it as int
+  // * HTTP/2.0 does not define a way to carry the reason phrase that is included in an HTTP/1.1
+  //   status line.
+  this.statusCode = parseInt(this._checkSpecialHeader(':status', headers[':status']));
+ 
+  // * Handling regular headers.
+  IncomingMessage.prototype._onHeaders.call(this, headers);
+ 
+  // * Signaling that the headers arrived.
+  this._log.info({ status: this.statusCode, headers: this.headers}, 'Incoming response');
+  this.emit('ready');
+};
+ 
+// IncomingPromise class
+// -------------------------
+ 
+function IncomingPromise(responseStream, promiseHeaders) {
+  var stream = new Readable();
+  stream._read = noop;
+  stream.push(null);
+  stream._log = responseStream._log;
+ 
+  IncomingRequest.call(this, stream);
+ 
+  this._onHeaders(promiseHeaders);
+ 
+  this._responseStream = responseStream;
+ 
+  var response = new IncomingResponse(this._responseStream);
+  response.once('ready', this.emit.bind(this, 'response', response));
+ 
+  this.stream.on('promise', this._onPromise.bind(this));
+}
+IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } });
+ 
+IncomingPromise.prototype.cancel = function cancel() {
+  this._responseStream.reset('CANCEL');
+};
+ 
+IncomingPromise.prototype.setPriority = function setPriority(priority) {
+  this._responseStream.priority(priority);
+};
+ 
+IncomingPromise.prototype._onPromise = OutgoingRequest.prototype._onPromise;
+ 
+ +
+ + + + + + + + diff --git a/coverage/lcov-report/lib/index.html b/coverage/lcov-report/lib/index.html new file mode 100644 index 00000000..cc7660cc --- /dev/null +++ b/coverage/lcov-report/lib/index.html @@ -0,0 +1,333 @@ + + + + Code coverage report for lib/ + + + + + + + +
+

Code coverage report for lib/

+

+ + Statements: 93.19% (397 / 426)      + + + Branches: 79.88% (131 / 164)      + + + Functions: 93.75% (60 / 64)      + + + Lines: 93.19% (397 / 426)      + +

+
All files » lib/
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FileStatementsBranchesFunctionsLines
http.js93.19%(397 / 426)79.88%(131 / 164)93.75%(60 / 64)93.19%(397 / 426)
+
+
+ + + + + + + + diff --git a/coverage/lcov-report/prettify.css b/coverage/lcov-report/prettify.css new file mode 100644 index 00000000..b317a7cd --- /dev/null +++ b/coverage/lcov-report/prettify.css @@ -0,0 +1 @@ +.pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee} diff --git a/coverage/lcov-report/prettify.js b/coverage/lcov-report/prettify.js new file mode 100644 index 00000000..ef51e038 --- /dev/null +++ b/coverage/lcov-report/prettify.js @@ -0,0 +1 @@ +window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;arat[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]); diff --git a/coverage/lcov.info b/coverage/lcov.info new file mode 100644 index 00000000..2026725f --- /dev/null +++ b/coverage/lcov.info @@ -0,0 +1,727 @@ +TN: +SF:/data/upstream/node-http2/lib/http.js +FN:164,noop +FN:173,(anonymous_2) +FN:182,IncomingMessage +FN:211,_onHeaders +FN:224,(anonymous_5) +FN:229,_onEnd +FN:235,_checkSpecialHeader +FN:244,_validateHeaders +FN:277,OutgoingMessage +FN:289,_write +FN:297,_finish +FN:312,setHeader +FN:324,removeHeader +FN:332,getHeader +FN:336,addTrailers +FN:356,Server +FN:374,(anonymous_17) +FN:403,_start +FN:414,_onStream +FN:427,_fallback +FN:445,listen +FN:451,close +FN:456,setTimeout +FN:463,getTimeout +FN:470,setTimeout +FN:481,on +FN:490,addContext +FN:496,createServer +FN:514,IncomingRequest +FN:523,_onHeaders +FN:554,OutgoingResponse +FN:567,writeHead +FN:591,_implicitHeaders +FN:597,write +FN:602,end +FN:607,_onRequestHeaders +FN:611,push +FN:638,on +FN:654,request +FN:657,get +FN:664,Agent +FN:687,request +FN:744,(anonymous_43) +FN:754,negotiated +FN:782,(anonymous_45) +FN:795,get +FN:801,unbundleSocket +FN:813,getMaxSockets +FN:816,setMaxSockets +FN:826,OutgoingRequest +FN:835,_start +FN:869,_fallback +FN:875,setPriority +FN:885,on +FN:894,setNoDelay +FN:902,setSocketKeepAlive +FN:910,setTimeout +FN:919,abort +FN:930,_onPromise +FN:945,IncomingResponse +FN:954,_onHeaders +FN:975,IncomingPromise +FN:994,cancel +FN:998,setPriority +FNF:64 +FNH:60 +FNDA:2,noop +FNDA:3,(anonymous_2) +FNDA:24,IncomingMessage +FNDA:24,_onHeaders +FNDA:2,(anonymous_5) +FNDA:24,_onEnd +FNDA:60,_checkSpecialHeader +FNDA:24,_validateHeaders +FNDA:27,OutgoingMessage +FNDA:13,_write +FNDA:31,_finish +FNDA:4,setHeader +FNDA:1,removeHeader +FNDA:3,getHeader +FNDA:2,addTrailers +FNDA:13,Server +FNDA:9,(anonymous_17) +FNDA:9,_start +FNDA:10,_onStream +FNDA:1,_fallback +FNDA:9,listen +FNDA:4,close +FNDA:1,setTimeout +FNDA:1,getTimeout +FNDA:1,setTimeout +FNDA:9,on +FNDA:0,addContext +FNDA:10,createServer +FNDA:12,IncomingRequest +FNDA:12,_onHeaders +FNDA:12,OutgoingResponse +FNDA:12,writeHead +FNDA:23,_implicitHeaders +FNDA:11,write +FNDA:12,end +FNDA:10,_onRequestHeaders +FNDA:2,push +FNDA:12,on +FNDA:4,request +FNDA:7,get +FNDA:4,Agent +FNDA:12,request +FNDA:9,(anonymous_43) +FNDA:9,negotiated +FNDA:9,(anonymous_45) +FNDA:7,get +FNDA:8,unbundleSocket +FNDA:1,getMaxSockets +FNDA:1,setMaxSockets +FNDA:15,OutgoingRequest +FNDA:10,_start +FNDA:5,_fallback +FNDA:0,setPriority +FNDA:40,on +FNDA:3,setNoDelay +FNDA:3,setSocketKeepAlive +FNDA:3,setTimeout +FNDA:3,abort +FNDA:2,_onPromise +FNDA:12,IncomingResponse +FNDA:12,_onHeaders +FNDA:2,IncomingPromise +FNDA:0,cancel +FNDA:0,setPriority +DA:131,1 +DA:132,1 +DA:133,1 +DA:134,1 +DA:135,1 +DA:136,1 +DA:137,1 +DA:138,1 +DA:139,1 +DA:140,1 +DA:141,1 +DA:143,1 +DA:144,1 +DA:145,1 +DA:147,1 +DA:158,1 +DA:164,1 +DA:165,1 +DA:173,3 +DA:177,1 +DA:182,1 +DA:184,24 +DA:185,24 +DA:186,24 +DA:188,24 +DA:192,24 +DA:193,24 +DA:194,24 +DA:197,24 +DA:198,24 +DA:199,24 +DA:202,24 +DA:203,24 +DA:205,1 +DA:211,1 +DA:213,24 +DA:216,24 +DA:217,73 +DA:218,13 +DA:223,24 +DA:224,24 +DA:225,2 +DA:229,1 +DA:230,24 +DA:233,1 +DA:235,1 +DA:236,60 +DA:237,0 +DA:238,0 +DA:241,60 +DA:244,1 +DA:249,24 +DA:250,168 +DA:251,168 +DA:252,0 +DA:253,0 +DA:254,0 +DA:258,24 +DA:260,73 +DA:261,0 +DA:262,0 +DA:267,73 +DA:268,0 +DA:269,0 +DA:277,1 +DA:279,27 +DA:281,27 +DA:282,27 +DA:283,27 +DA:285,27 +DA:287,1 +DA:289,1 +DA:290,13 +DA:291,12 +DA:293,1 +DA:297,1 +DA:298,31 +DA:299,23 +DA:300,2 +DA:301,0 +DA:303,2 +DA:306,23 +DA:308,8 +DA:312,1 +DA:313,4 +DA:314,0 +DA:316,4 +DA:317,4 +DA:318,0 +DA:320,4 +DA:324,1 +DA:325,1 +DA:326,0 +DA:328,1 +DA:332,1 +DA:333,3 +DA:336,1 +DA:337,2 +DA:340,1 +DA:342,1 +DA:347,1 +DA:348,1 +DA:349,1 +DA:350,1 +DA:351,1 +DA:356,1 +DA:357,13 +DA:359,13 +DA:360,13 +DA:362,13 +DA:363,13 +DA:366,13 +DA:367,10 +DA:368,10 +DA:369,10 +DA:370,10 +DA:371,10 +DA:372,10 +DA:373,10 +DA:374,10 +DA:375,9 +DA:376,9 +DA:377,8 +DA:379,1 +DA:382,10 +DA:386,3 +DA:387,1 +DA:388,1 +DA:389,1 +DA:394,2 +DA:395,2 +DA:398,11 +DA:400,1 +DA:403,1 +DA:404,9 +DA:406,9 +DA:411,9 +DA:413,9 +DA:414,9 +DA:415,10 +DA:416,10 +DA:418,10 +DA:421,9 +DA:422,9 +DA:424,9 +DA:427,1 +DA:428,1 +DA:430,1 +DA:435,1 +DA:436,1 +DA:439,1 +DA:445,1 +DA:446,9 +DA:448,9 +DA:451,1 +DA:452,4 +DA:453,4 +DA:456,1 +DA:457,1 +DA:458,1 +DA:462,1 +DA:464,1 +DA:465,1 +DA:467,0 +DA:471,1 +DA:472,1 +DA:481,1 +DA:482,9 +DA:483,0 +DA:485,9 +DA:490,1 +DA:491,0 +DA:492,0 +DA:496,1 +DA:497,10 +DA:498,1 +DA:499,1 +DA:502,10 +DA:504,9 +DA:505,9 +DA:508,9 +DA:514,1 +DA:515,12 +DA:517,1 +DA:523,1 +DA:534,12 +DA:535,12 +DA:536,12 +DA:537,12 +DA:540,12 +DA:543,12 +DA:546,12 +DA:548,12 +DA:554,1 +DA:555,12 +DA:557,12 +DA:559,12 +DA:560,12 +DA:561,12 +DA:563,12 +DA:565,1 +DA:567,1 +DA:568,12 +DA:569,1 +DA:571,11 +DA:574,12 +DA:575,1 +DA:577,12 +DA:579,12 +DA:580,11 +DA:583,12 +DA:585,12 +DA:587,12 +DA:588,12 +DA:591,1 +DA:592,23 +DA:593,11 +DA:597,1 +DA:598,11 +DA:599,11 +DA:602,1 +DA:603,12 +DA:604,12 +DA:607,1 +DA:608,10 +DA:611,1 +DA:612,2 +DA:613,1 +DA:616,2 +DA:617,0 +DA:620,2 +DA:627,2 +DA:631,2 +DA:633,2 +DA:638,1 +DA:639,12 +DA:640,0 +DA:642,12 +DA:649,1 +DA:650,1 +DA:651,1 +DA:652,1 +DA:653,1 +DA:654,1 +DA:655,4 +DA:657,1 +DA:658,7 +DA:664,1 +DA:665,4 +DA:667,4 +DA:669,4 +DA:670,4 +DA:671,4 +DA:677,4 +DA:678,4 +DA:679,4 +DA:680,4 +DA:682,4 +DA:683,4 +DA:685,1 +DA:687,1 +DA:688,12 +DA:689,8 +DA:691,4 +DA:694,12 +DA:695,12 +DA:696,12 +DA:697,12 +DA:698,12 +DA:700,12 +DA:701,1 +DA:702,1 +DA:705,11 +DA:707,11 +DA:708,7 +DA:711,11 +DA:718,11 +DA:719,1 +DA:720,1 +DA:724,10 +DA:725,1 +DA:726,1 +DA:731,1 +DA:732,1 +DA:737,9 +DA:738,9 +DA:739,9 +DA:740,9 +DA:741,9 +DA:742,9 +DA:744,9 +DA:745,9 +DA:746,9 +DA:747,0 +DA:749,9 +DA:753,9 +DA:754,1 +DA:755,9 +DA:756,9 +DA:757,9 +DA:758,8 +DA:759,8 +DA:760,8 +DA:761,8 +DA:762,8 +DA:764,9 +DA:765,1 +DA:766,1 +DA:768,0 +DA:771,8 +DA:772,7 +DA:774,7 +DA:775,7 +DA:777,1 +DA:782,9 +DA:783,9 +DA:784,9 +DA:785,8 +DA:787,1 +DA:792,11 +DA:795,1 +DA:796,7 +DA:797,7 +DA:798,7 +DA:801,1 +DA:802,8 +DA:803,8 +DA:804,8 +DA:805,8 +DA:806,8 +DA:807,8 +DA:808,8 +DA:809,8 +DA:812,1 +DA:814,1 +DA:817,1 +DA:821,1 +DA:826,1 +DA:827,15 +DA:829,15 +DA:831,15 +DA:833,1 +DA:835,1 +DA:836,10 +DA:838,10 +DA:840,10 +DA:841,1 +DA:843,10 +DA:844,10 +DA:846,10 +DA:847,0 +DA:850,10 +DA:851,10 +DA:852,10 +DA:853,10 +DA:855,10 +DA:858,10 +DA:859,10 +DA:861,10 +DA:863,10 +DA:864,10 +DA:866,10 +DA:869,1 +DA:870,5 +DA:871,5 +DA:872,5 +DA:875,1 +DA:876,0 +DA:877,0 +DA:879,0 +DA:885,1 +DA:886,40 +DA:887,0 +DA:889,40 +DA:894,1 +DA:895,3 +DA:896,1 +DA:897,2 +DA:898,1 +DA:902,1 +DA:903,3 +DA:904,1 +DA:905,2 +DA:906,1 +DA:910,1 +DA:911,3 +DA:912,1 +DA:913,2 +DA:914,1 +DA:919,1 +DA:920,3 +DA:921,1 +DA:922,2 +DA:923,1 +DA:925,1 +DA:930,1 +DA:931,2 +DA:933,2 +DA:935,2 +DA:936,2 +DA:938,0 +DA:945,1 +DA:946,12 +DA:948,1 +DA:954,1 +DA:962,12 +DA:965,12 +DA:968,12 +DA:969,12 +DA:975,1 +DA:976,2 +DA:977,2 +DA:978,2 +DA:979,2 +DA:981,2 +DA:983,2 +DA:985,2 +DA:987,2 +DA:988,2 +DA:990,2 +DA:992,1 +DA:994,1 +DA:995,0 +DA:998,1 +DA:999,0 +DA:1002,1 +LF:426 +LH:397 +BRDA:217,1,0,13 +BRDA:217,1,1,60 +BRDA:236,2,0,0 +BRDA:236,2,1,60 +BRDA:236,3,0,60 +BRDA:236,3,1,60 +BRDA:251,4,0,0 +BRDA:251,4,1,168 +BRDA:260,5,0,0 +BRDA:260,5,1,73 +BRDA:267,6,0,0 +BRDA:267,6,1,73 +BRDA:290,7,0,12 +BRDA:290,7,1,1 +BRDA:298,8,0,23 +BRDA:298,8,1,8 +BRDA:299,9,0,2 +BRDA:299,9,1,21 +BRDA:300,10,0,0 +BRDA:300,10,1,2 +BRDA:313,11,0,0 +BRDA:313,11,1,4 +BRDA:317,12,0,0 +BRDA:317,12,1,4 +BRDA:325,13,0,0 +BRDA:325,13,1,1 +BRDA:359,14,0,13 +BRDA:359,14,1,2 +BRDA:366,15,0,10 +BRDA:366,15,1,3 +BRDA:366,16,0,13 +BRDA:366,16,1,10 +BRDA:366,16,2,3 +BRDA:375,17,0,9 +BRDA:375,17,1,9 +BRDA:376,18,0,8 +BRDA:376,18,1,1 +BRDA:376,19,0,9 +BRDA:376,19,1,8 +BRDA:386,20,0,1 +BRDA:386,20,1,2 +BRDA:428,21,0,1 +BRDA:428,21,1,1 +BRDA:446,22,0,0 +BRDA:446,22,1,9 +BRDA:457,23,0,1 +BRDA:457,23,1,0 +BRDA:464,24,0,1 +BRDA:464,24,1,0 +BRDA:471,25,0,1 +BRDA:471,25,1,0 +BRDA:482,26,0,0 +BRDA:482,26,1,9 +BRDA:482,27,0,9 +BRDA:482,27,1,9 +BRDA:483,28,0,0 +BRDA:483,28,1,0 +BRDA:491,29,0,0 +BRDA:491,29,1,0 +BRDA:497,30,0,1 +BRDA:497,30,1,9 +BRDA:504,31,0,9 +BRDA:504,31,1,0 +BRDA:568,32,0,1 +BRDA:568,32,1,11 +BRDA:579,33,0,11 +BRDA:579,33,1,1 +BRDA:579,34,0,12 +BRDA:579,34,1,11 +BRDA:592,35,0,11 +BRDA:592,35,1,12 +BRDA:612,36,0,1 +BRDA:612,36,1,1 +BRDA:616,37,0,0 +BRDA:616,37,1,2 +BRDA:621,38,0,2 +BRDA:621,38,1,2 +BRDA:622,39,0,2 +BRDA:622,39,1,1 +BRDA:622,39,2,1 +BRDA:623,40,0,2 +BRDA:623,40,1,2 +BRDA:623,40,2,2 +BRDA:639,41,0,0 +BRDA:639,41,1,12 +BRDA:639,42,0,12 +BRDA:639,42,1,0 +BRDA:640,43,0,0 +BRDA:640,43,1,0 +BRDA:655,44,0,4 +BRDA:655,44,1,4 +BRDA:658,45,0,7 +BRDA:658,45,1,7 +BRDA:670,46,0,4 +BRDA:670,46,1,1 +BRDA:688,47,0,8 +BRDA:688,47,1,4 +BRDA:694,48,0,12 +BRDA:694,48,1,12 +BRDA:695,49,0,12 +BRDA:695,49,1,3 +BRDA:696,50,0,12 +BRDA:696,50,1,4 +BRDA:696,50,2,1 +BRDA:697,51,0,12 +BRDA:697,51,1,1 +BRDA:698,52,0,12 +BRDA:698,52,1,1 +BRDA:700,53,0,1 +BRDA:700,53,1,11 +BRDA:700,54,0,12 +BRDA:700,54,1,11 +BRDA:707,55,0,7 +BRDA:707,55,1,4 +BRDA:718,56,0,1 +BRDA:718,56,1,10 +BRDA:724,57,0,1 +BRDA:724,57,1,9 +BRDA:745,58,0,9 +BRDA:745,58,1,9 +BRDA:746,59,0,0 +BRDA:746,59,1,9 +BRDA:756,60,0,9 +BRDA:756,60,1,9 +BRDA:757,61,0,8 +BRDA:757,61,1,1 +BRDA:764,62,0,1 +BRDA:764,62,1,8 +BRDA:765,63,0,1 +BRDA:765,63,1,0 +BRDA:771,64,0,7 +BRDA:771,64,1,1 +BRDA:784,65,0,8 +BRDA:784,65,1,1 +BRDA:846,66,0,0 +BRDA:846,66,1,10 +BRDA:857,67,0,10 +BRDA:857,67,1,9 +BRDA:876,68,0,0 +BRDA:876,68,1,0 +BRDA:886,69,0,0 +BRDA:886,69,1,40 +BRDA:886,70,0,40 +BRDA:886,70,1,0 +BRDA:887,71,0,0 +BRDA:887,71,1,0 +BRDA:895,72,0,1 +BRDA:895,72,1,2 +BRDA:897,73,0,1 +BRDA:897,73,1,1 +BRDA:903,74,0,1 +BRDA:903,74,1,2 +BRDA:905,75,0,1 +BRDA:905,75,1,1 +BRDA:911,76,0,1 +BRDA:911,76,1,2 +BRDA:913,77,0,1 +BRDA:913,77,1,1 +BRDA:920,78,0,1 +BRDA:920,78,1,2 +BRDA:922,79,0,1 +BRDA:922,79,1,1 +BRDA:935,80,0,2 +BRDA:935,80,1,0 +BRF:164 +BRH:131 +end_of_record diff --git a/doc/compressor.html b/doc/compressor.html deleted file mode 100644 index 087e4a0f..00000000 --- a/doc/compressor.html +++ /dev/null @@ -1,1630 +0,0 @@ - - - - - compressor.js - - - - - -
-
- - - -
    - -
  • -
    -

    compressor.js

    -
    -
  • - - - -
  • -
    - -
    - -
    -

    The implementation of the HTTP/2 Header Compression spec is separated from -the 'integration' part which handles HEADERS and PUSH_PROMISE frames. The compression itself is -implemented in the first part of the file, and consists of three classes: HeaderTable, -HeaderSetDecompressor and HeaderSetCompressor. The two latter classes are -Transform Stream subclasses that operate in object mode. -These transform chunks of binary data into [name, value] pairs and vice versa, and store their -state in HeaderTable instances.

    -

    The 'integration' part is also implemented by two Transform Stream subclasses -that operate in object mode: the Compressor and the Decompressor. These -provide a layer between the framer and the -connection handling component.

    - -
    - -
    exports.HeaderTable = HeaderTable;
    -exports.HeaderSetCompressor = HeaderSetCompressor;
    -exports.HeaderSetDecompressor = HeaderSetDecompressor;
    -exports.Compressor = Compressor;
    -exports.Decompressor = Decompressor;
    -
    -var TransformStream = require('stream').Transform;
    -var assert = require('assert');
    -var util = require('util');
    - -
  • - - -
  • -
    - -
    - -
    -

    Header compression

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The HeaderTable class

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The Header Table is a component used to associate headers to index values. It is -basically an ordered list of [name, value] pairs, so it's implemented as a subclass of Array.

    - -
    - -
    function HeaderTable(log, table, limit) {
    -  var self = table.map(entryFromPair);
    -  self._log = log;
    -  self._limit = limit || DEFAULT_HEADER_TABLE_LIMIT;
    -  self._size = tableSize(self);
    -  self.add = HeaderTable.prototype.add;
    -  return self;
    -}
    - -
  • - - -
  • -
    - -
    - -
    -

    There are few more sets that are needed for the compression/decompression process that are all -subsets of the Header Table, and are implemented as flags on header table entries:

    -
      -
    • Reference Set: contains a group of headers used as a reference for the -differential encoding of a new set of headers. (reference flag)
    • -
    • Emitted headers: the headers that are already emitted as part of the current decompression -process (not part of the spec, emitted flag)
    • -
    • Headers to be kept: headers that should not be removed as the last step of the encoding process -(not part of the spec, keep flag)
    • -
    -

    Relations of the sets:

    -
    ,----------------------------------.
    -|           Header Table           |
    -|                                  |
    -|  ,----------------------------.  |
    -|  |        Reference Set       |  |
    -|  |                            |  |
    -|  |  ,---------.  ,---------.  |  |
    -|  |  |  Keep   |  | Emitted |  |  |
    -|  |  |         |  |         |  |  |
    -|  |  `---------'  `---------'  |  |
    -|  `----------------------------'  |
    -`----------------------------------'
    - -
    - -
    function entryFromPair(pair) {
    -  var entry = pair.slice();
    -  entry.reference = false;
    -  entry.emitted = false;
    -  entry.keep = false;
    -  entry._size = size(entry);
    -  return entry;
    -}
    - -
  • - - -
  • -
    - -
    - -
    -

    The encoder decides how to update the header table and as such can control how much memory is -used by the header table. To limit the memory requirements on the decoder side, the header table -size is bounded.

    -
      -
    • The default header table size limit is 4096 bytes.
    • -
    • The size of an entry is defined as follows: the size of an entry is the sum of its name's -length in bytes, of its value's length in bytes and of 32 bytes.
    • -
    • The size of a header table is the sum of the size of its entries.
    • -
    - -
    - -
    var DEFAULT_HEADER_TABLE_LIMIT = 4096;
    -
    -function size(entry) {
    -  return (new Buffer(entry[0] + entry[1], 'utf8')).length + 32;
    -}
    -
    -function tableSize(table) {
    -  var size = 0;
    -  for (var i = 0; i < table.length; i++) {
    -    size += table[i]._size;
    -  }
    -  return size;
    -}
    - -
  • - - -
  • -
    - -
    - -
    -

    The add(index, entry) can be used to manage the header table:

    -
      -
    • if index is Infinite it pushes the new entry at the end of the table
    • -
    • otherwise, it replaces the entry with the given index with the new entry
    • -
    • before doing such a modification, it has to be ensured that the header table size will stay -lower than or equal to the header table size limit. To achieve this, repeatedly, the first -entry of the header table is removed, until enough space is available for the modification.
    • -
    - -
    - -
    HeaderTable.prototype.add = function(index, entry) {
    -  var limit = this._limit - entry._size;
    -  var droppedEntries = [];
    -
    -  while ((this._size > limit) && (this.length > 0)) {
    -    var dropped = this.shift();
    -    this._size -= dropped._size;
    -    droppedEntries.push(dropped);
    -  }
    -
    -  if (this._size <= limit) {
    -    index -= droppedEntries.length;
    -    if (index < 0) {
    -      this.unshift(entry);
    -    } else {
    -      this.splice(index, 1, entry); // this is like push() if index is Infinity
    -    }
    -    this._size += entry._size;
    -  }
    -
    -  return droppedEntries;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Initial header tables

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Initial request table

    - -
    - -
    HeaderTable.initialRequestTable  = [
    -  [ ':scheme'                     , 'http'  ],
    -  [ ':scheme'                     , 'https' ],
    -  [ ':host'                       , ''      ],
    -  [ ':path'                       , '/'     ],
    -  [ ':method'                     , 'get'   ],
    -  [ 'accept'                      , ''      ],
    -  [ 'accept-charset'              , ''      ],
    -  [ 'accept-encoding'             , ''      ],
    -  [ 'accept-language'             , ''      ],
    -  [ 'cookie'                      , ''      ],
    -  [ 'if-modified-since'           , ''      ],
    -  [ 'user-agent'                  , ''      ],
    -  [ 'referer'                     , ''      ],
    -  [ 'authorization'               , ''      ],
    -  [ 'allow'                       , ''      ],
    -  [ 'cache-control'               , ''      ],
    -  [ 'connection'                  , ''      ],
    -  [ 'content-length'              , ''      ],
    -  [ 'content-type'                , ''      ],
    -  [ 'date'                        , ''      ],
    -  [ 'expect'                      , ''      ],
    -  [ 'from'                        , ''      ],
    -  [ 'if-match'                    , ''      ],
    -  [ 'if-none-match'               , ''      ],
    -  [ 'if-range'                    , ''      ],
    -  [ 'if-unmodified-since'         , ''      ],
    -  [ 'max-forwards'                , ''      ],
    -  [ 'proxy-authorization'         , ''      ],
    -  [ 'range'                       , ''      ],
    -  [ 'via'                         , ''      ]
    -];
    - -
  • - - -
  • -
    - -
    - -
    -

    Initial response table

    - -
    - -
    HeaderTable.initialResponseTable = [
    -  [ ':status'                     , '200'   ],
    -  [ 'age'                         , ''      ],
    -  [ 'cache-control'               , ''      ],
    -  [ 'content-length'              , ''      ],
    -  [ 'content-type'                , ''      ],
    -  [ 'date'                        , ''      ],
    -  [ 'etag'                        , ''      ],
    -  [ 'expires'                     , ''      ],
    -  [ 'last-modified'               , ''      ],
    -  [ 'server'                      , ''      ],
    -  [ 'set-cookie'                  , ''      ],
    -  [ 'vary'                        , ''      ],
    -  [ 'via'                         , ''      ],
    -  [ 'access-control-allow-origin' , ''      ],
    -  [ 'accept-ranges'               , ''      ],
    -  [ 'allow'                       , ''      ],
    -  [ 'connection'                  , ''      ],
    -  [ 'content-disposition'         , ''      ],
    -  [ 'content-encoding'            , ''      ],
    -  [ 'content-language'            , ''      ],
    -  [ 'content-location'            , ''      ],
    -  [ 'content-range'               , ''      ],
    -  [ 'link'                        , ''      ],
    -  [ 'location'                    , ''      ],
    -  [ 'proxy-authenticate'          , ''      ],
    -  [ 'refresh'                     , ''      ],
    -  [ 'retry-after'                 , ''      ],
    -  [ 'strict-transport-security'   , ''      ],
    -  [ 'transfer-encoding'           , ''      ],
    -  [ 'www-authenticate'            , ''      ]
    -];
    - -
  • - - -
  • -
    - -
    - -
    -

    The HeaderSetDecompressor class

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    A HeaderSetDecompressor instance is a transform stream that can be used to decompress a -single header set. Its input is a stream of binary data chunks and its output is a stream of -[name, value] pairs.

    -

    Currently, it is not a proper streaming decompressor implementation, since it buffer its input -until the end os the stream, and then processes the whole header block at once.

    - -
    - -
    util.inherits(HeaderSetDecompressor, TransformStream);
    -function HeaderSetDecompressor(log, table) {
    -  TransformStream.call(this, { objectMode: true });
    -
    -  this._log = log.child({ component: 'compressor' });
    -  this._table = table;
    -  this._chunks = [];
    -}
    - -
  • - - -
  • -
    - -
    - -
    -

    _transform is the implementation of the corresponding virtual function of the -TransformStream class. It collects the data chunks for later processing.

    - -
    - -
    HeaderSetDecompressor.prototype._transform = function _transform(chunk, encoding, callback) {
    -  this._chunks.push(chunk);
    -  callback();
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    execute(rep) executes the given header representation.

    -

    The JavaScript object representation of a header representation:

    -
    {
    -  name: String || Integer,  // string literal or index
    -  value: String || Integer, // string literal or index
    -  index: Integer            // -1       : no indexing
    -                            // 0 - ...  : substitution indexing
    -                            // Infinity : incremental indexing
    -}
    -

    Examples:

    -
    Indexed:
    -{ name: 2  , value: 2  , index: -1       }
    -Literal:
    -{ name: 2  , value: 'X', index: -1       } // without indexing
    -{ name: 2  , value: 'Y', index: Infinity } // incremental indexing
    -{ name: 'A', value: 'Z', index: 123      } // substitution indexing
    - -
    - -
    HeaderSetDecompressor.prototype._execute = function _execute(rep) {
    -  this._log.trace({ key: rep.name, value: rep.value, index: rep.index },
    -                  'Executing header representation');
    -
    -  var index, entry, pair;
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • An indexed representation corresponding to an entry present in the reference set -entails the following actions:
        -
      • The entry is removed from the reference set.
      • -
      -
    • -
    • An indexed representation corresponding to an entry not present in the reference set -entails the following actions:
        -
      • The header corresponding to the entry is emitted.
      • -
      • The entry is added to the reference set.
      • -
      -
    • -
    - -
    - -
      if (typeof rep.value === 'number') {
    -    index = rep.value;
    -    entry = this._table[index];
    -
    -    if (entry.reference) {
    -      entry.reference = false;
    -    } else {
    -      entry.reference = true;
    -      entry.emitted = true;
    -      pair = entry.slice();
    -      this.push(pair);
    -    }
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • A literal representation that is not added to the header table entails the following -action:
        -
      • The header is emitted.
      • -
      -
    • -
    • A literal representation that is added to the header table entails the following further -actions:
        -
      • The header is added to the header table, at the location defined by the representation.
      • -
      • The new entry is added to the reference set.
      • -
      -
    • -
    - -
    - -
      else {
    -    if (typeof rep.name === 'number') {
    -      pair = [this._table[rep.name][0], rep.value];
    -    } else {
    -      pair = [rep.name, rep.value];
    -    }
    -
    -    index = rep.index;
    -    if (index !== -1) {
    -      entry = entryFromPair(pair);
    -      entry.reference = true;
    -      entry.emitted = true;
    -      this._table.add(index, entry);
    -    }
    -
    -    this.push(pair);
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    _flush is the implementation of the corresponding virtual function of the -TransformStream class. The whole decompressing process is done in _flush. It gets called when -the input stream is over.

    - -
    - -
    HeaderSetDecompressor.prototype._flush = function _flush(callback) {
    -  var buffer = concat(this._chunks);
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • processes the header representations
    • -
    - -
    - -
      buffer.cursor = 0;
    -  while (buffer.cursor < buffer.length) {
    -    this._execute(HeaderSetDecompressor.header(buffer));
    -  }
    - -
  • - - -
  • -
    - -
    - -
    - - -
    - -
      for (var index = 0; index < this._table.length; index++) {
    -    var entry = this._table[index];
    -    if (entry.reference && !entry.emitted) {
    -      this.push(entry.slice());
    -    }
    -    entry.emitted = false;
    -  }
    -
    -  callback();
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    The HeaderSetCompressor class

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    A HeaderSetCompressor instance is a transform stream that can be used to compress a single -header set. Its input is a stream of [name, value] pairs and its output is a stream of -binary data chunks.

    -

    It is a real streaming compressor, since it does not wait until the header set is complete.

    -

    The compression algorithm is (intentionally) not specified by the spec. Therefore, the current -compression algorithm can probably be improved in the future.

    - -
    - -
    util.inherits(HeaderSetCompressor, TransformStream);
    -function HeaderSetCompressor(log, table) {
    -  TransformStream.call(this, { objectMode: true });
    -
    -  this._log = log.child({ component: 'compressor' });
    -  this._table = table;
    -  this.push = TransformStream.prototype.push.bind(this);
    -}
    -
    -HeaderSetCompressor.prototype.send = function send(rep) {
    -  this._log.trace({ key: rep.name, value: rep.value, index: rep.index },
    -                  'Emitting header representation');
    -
    -  if (!rep.chunks) {
    -    rep.chunks = HeaderSetCompressor.header(rep);
    -  }
    -  rep.chunks.forEach(this.push);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    _transform is the implementation of the corresponding virtual function of the -TransformStream class. It processes the input headers one by one:

    - -
    - -
    HeaderSetCompressor.prototype._transform = function _transform(pair, encoding, callback) {
    -  var name = pair[0].toLowerCase();
    -  var value = pair[1];
    -  var entry, rep;
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • tries to find full (name, value) or name match in the header table
    • -
    - -
    - -
      var nameMatch = -1, fullMatch = -1;
    -  for (var index = 0; index < this._table.length; index++) {
    -    entry = this._table[index];
    -    if (entry[0] === name) {
    -      if (entry[1] === value) {
    -        fullMatch = index;
    -        break;
    -      } else if (nameMatch === -1) {
    -        nameMatch = index;
    -      }
    -    }
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • if there's full match, it will be an indexed representation (or more than one) depending -on its presence in the reference, the emitted and the keep set:

      -
        -
      • If the entry is outside the reference set, then a single indexed representation puts the -entry into it and emits the header.

        -
      • -
      • If it's already in the keep set, then 4 indexed representations are needed:

        -
          -
        1. removes it from the reference set
        2. -
        3. puts it back in the reference set and emits the header once
        4. -
        5. removes it again
        6. -
        7. puts it back and emits it again for the second time
        8. -
        -

        It won't be emitted at the end of the decoding process since it's now in the emitted set.

        -
      • -
      • If it's in the emitted set, then 2 indexed representations are needed:

        -
          -
        1. removes it from the reference set
        2. -
        3. puts it back in the reference set and emits the header once
        4. -
        -
      • -
      • If it's in the reference set, but outside the keep set and the emitted set, then this -header is common with the previous header set, and is still untouched. We mark it to keep -in the reference set (that means don't remove at the end of the encoding process).

        -
      • -
      -
    • -
    - -
    - -
      if (fullMatch !== -1) {
    -    rep = { name: fullMatch, value: fullMatch, index: -1 };
    -
    -    if (!entry.reference) {
    -      this.send(rep);
    -      entry.reference = true;
    -      entry.emitted = true;
    -    }
    -
    -    else if (entry.keep) {
    -      this.send(rep);
    -      this.send(rep);
    -      this.send(rep);
    -      this.send(rep);
    -      entry.keep = false;
    -      entry.emitted = true;
    -    }
    -
    -    else if (entry.emitted) {
    -      this.send(rep);
    -      this.send(rep);
    -    }
    -
    -    else {
    -      entry.keep = true;
    -    }
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • otherwise, it will be a literal representation (with a name index if there's a name match)
    • -
    - -
    - -
      else {
    -    entry = entryFromPair(pair);
    -    entry.emitted = true;
    -
    -    var insertIndex;
    -    if (entry._size > this._table._limit / 2) {
    -      insertIndex = -1;
    -    } else if (nameMatch !== -1) {
    -      insertIndex = nameMatch;
    -    } else {
    -      insertIndex = Infinity;
    -    }
    -
    -    if (insertIndex !== -1) {
    -      entry.reference = true;
    -      var droppedEntries = this._table.add(insertIndex, entry);
    -      for (index = 0; index < droppedEntries.length; index++) {
    -        var dropped = droppedEntries[index];
    -        if (dropped.keep) {
    -          rep = { name: index, value: index, index: -1 };
    -          this.send(rep);
    -          this.send(rep);
    -        }
    -      }
    -    }
    -
    -    this.send({ name: (nameMatch !== -1) ? nameMatch : name, value: value, index: insertIndex });
    -  }
    -
    -  callback();
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    _flush is the implementation of the corresponding virtual function of the -TransformStream class. It gets called when there's no more header to compress. The final step:

    - -
    - -
    HeaderSetCompressor.prototype._flush = function _flush(callback) {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • removing entries from the header set that are not marked to be kept or emitted
    • -
    - -
    - -
      for (var index = 0; index < this._table.length; index++) {
    -    var entry = this._table[index];
    -    if (entry.reference && !entry.keep && !entry.emitted) {
    -      this.send({ name: index, value: index, index: -1 });
    -      entry.reference = false;
    -    }
    -    entry.keep = false;
    -    entry.emitted = false;
    -  }
    -
    -  callback();
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Detailed Format

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Integer representation

    -

    The algorithm to represent an integer I is as follows:

    -
      -
    1. If I < 2^N - 1, encode I on N bits
    2. -
    3. Else, encode 2^N - 1 on N bits and do the following steps:
        -
      1. Set I to (I - (2^N - 1)) and Q to 1
      2. -
      3. While Q > 0
          -
        1. Compute Q and R, quotient and remainder of I divided by 2^7
        2. -
        3. If Q is strictly greater than 0, write one 1 bit; otherwise, write one 0 bit
        4. -
        5. Encode R on the next 7 bits
        6. -
        7. I = Q
        8. -
        -
      4. -
      -
    4. -
    - -
    - -
    HeaderSetCompressor.integer = function writeInteger(I, N) {
    -  var limit = Math.pow(2,N) - 1;
    -  if (I < limit) {
    -    return [new Buffer([I])];
    -  }
    -
    -  var bytes = [];
    -  if (N !== 0) {
    -    bytes.push(limit);
    -  }
    -  I -= limit;
    -
    -  var Q = 1, R;
    -  while (Q > 0) {
    -    Q = Math.floor(I / 128);
    -    R = I % 128;
    -
    -    if (Q > 0) {
    -      R += 128;
    -    }
    -    bytes.push(R);
    -
    -    I = Q;
    -  }
    -
    -  return [new Buffer(bytes)];
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    The inverse algorithm:

    -
      -
    1. Set I to the number coded on the lower N bits of the first byte
    2. -
    3. If I is smaller than 2^N - 1 then return I
    4. -
    5. Else the number is encoded on more than one byte, so do the following steps:
        -
      1. Set M to 0
      2. -
      3. While returning with I
          -
        1. Let B be the next byte (the first byte if N is 0)
        2. -
        3. Read out the lower 7 bits of B and multiply it with 2^M
        4. -
        5. Increase I with this number
        6. -
        7. Increase M by 7
        8. -
        9. Return I if the most significant bit of B is 0
        10. -
        -
      4. -
      -
    6. -
    - -
    - -
    HeaderSetDecompressor.integer = function readInteger(buffer, N) {
    -  var limit = Math.pow(2,N) - 1;
    -
    -  var I = buffer[buffer.cursor] & limit;
    -  if (N !== 0) {
    -    buffer.cursor += 1;
    -  }
    -
    -  if (I === limit) {
    -    var M = 0;
    -    do {
    -      I += (buffer[buffer.cursor] & 127) << M;
    -      M += 7;
    -      buffer.cursor += 1;
    -    } while (buffer[buffer.cursor - 1] & 128);
    -  }
    -
    -  return I;
    -};
    -
    -
    - -
  • - - -
  • -
    - -
    - -
    -

    String literal representation

    -

    Literal strings can represent header names or header values. They are encoded in two parts:

    -
      -
    1. The string length, defined as the number of bytes needed to store its UTF-8 representation, -is represented as an integer with a zero bits prefix. If the string length is strictly less -than 128, it is represented as one byte.
    2. -
    3. The string value represented as a list of UTF-8 characters.
    4. -
    - -
    - -
    HeaderSetCompressor.string = function writeString(str) {
    -  var encodedString = new Buffer(str, 'utf8');
    -  var encodedLength = HeaderSetCompressor.integer(encodedString.length, 0);
    -  return encodedLength.concat(encodedString);
    -};
    -
    -HeaderSetDecompressor.string = function readString(buffer) {
    -  var length = HeaderSetDecompressor.integer(buffer, 0);
    -  var str = buffer.toString('utf8', buffer.cursor, buffer.cursor + length);
    -  buffer.cursor += length;
    -  return str;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Header represenations

    -

    The JavaScript object representation is described near the -HeaderTable.prototype.execute() method definition.

    -

    All binary header representations start with a prefix signaling the representation type and -an index represented using prefix coded integers:

    -
      0   1   2   3   4   5   6   7
    -+---+---+---+---+---+---+---+---+
    -| 1 |        Index (7+)         |  Indexed Representation
    -+---+---------------------------+
    -
    -+---+---+---+---+---+---+---+---+
    -| 0 | 1 | 1 |    Index (5+)     |  Literal w/o Indexing
    -+---+---+---+-------------------+
    -
    -+---+---+---+---+---+---+---+---+
    -| 0 | 1 | 0 |    Index (5+)     |  Literal w/ Incremental Indexing
    -+---+---+---+-------------------+
    -
    -+---+---+---+---+---+---+---+---+
    -| 0 | 0 |      Index (6+)       |  Literal w/ Substitution Indexing
    -+---+---+-----------------------+
    -

    The Indexed Representation consists of the 1-bit prefix and the Index that is represented as -a 7-bit prefix coded integer and nothing else.

    -

    After the first bits, all literal representations specify the header name, either as a -pointer to the Header Table (Index) or a string literal. When the string literal representation -is used, the Index is set to 0 and the string literal starts at the second byte.

    -

    When using Substitution Indexing, a new index comes next represented as a 0-bit prefix -integer, specifying the record in the Header Table that needs to be replaced.

    -

    For all literal representations, the specification of the header value comes next. It is -always represented as a string.

    - -
    - -
    var representations = {
    -  indexed             : { prefix: 7, pattern: 0x80 },
    -  literal             : { prefix: 5, pattern: 0x60 },
    -  literalIncremental  : { prefix: 5, pattern: 0x40 },
    -  literalSubstitution : { prefix: 6, pattern: 0x00 }
    -};
    -
    -HeaderSetCompressor.header = function writeHeader(header) {
    -  var representation, buffers = [];
    -
    -  if (typeof header.value === 'number') {
    -    representation = representations.indexed;
    -  } else if (header.index === -1) {
    -    representation = representations.literal;
    -  } else if (header.index === Infinity) {
    -    representation = representations.literalIncremental;
    -  } else {
    -    representation = representations.literalSubstitution;
    -  }
    -
    -  if (representation === representations.indexed) {
    -    buffers.push(HeaderSetCompressor.integer(header.value, representation.prefix));
    -
    -  } else {
    -    if (typeof header.name === 'number') {
    -      buffers.push(HeaderSetCompressor.integer(header.name + 1, representation.prefix));
    -    } else {
    -      buffers.push(HeaderSetCompressor.integer(0, representation.prefix));
    -      buffers.push(HeaderSetCompressor.string(header.name));
    -    }
    -
    -    if (representation === representations.literalSubstitution) {
    -      buffers.push(HeaderSetCompressor.integer(header.index, 0));
    -    }
    -
    -    buffers.push(HeaderSetCompressor.string(header.value));
    -  }
    -
    -  buffers[0][0][0] |= representation.pattern;
    -
    -  return Array.prototype.concat.apply([], buffers); // array of arrays of buffers -> array of buffers
    -};
    -
    -HeaderSetDecompressor.header = function readHeader(buffer) {
    -  var representation, header = {};
    -
    -  var firstByte = buffer[buffer.cursor];
    -  if (firstByte & 0x80) {
    -    representation = representations.indexed;
    -  } else if (firstByte & 0x40) {
    -    if (firstByte & 0x20) {
    -      representation = representations.literal;
    -    } else {
    -      representation = representations.literalIncremental;
    -    }
    -  } else {
    -    representation = representations.literalSubstitution;
    -  }
    -
    -  if (representation === representations.indexed) {
    -    header.value = header.name = HeaderSetDecompressor.integer(buffer, representation.prefix);
    -    header.index = -1;
    -
    -  } else {
    -    header.name = HeaderSetDecompressor.integer(buffer, representation.prefix) - 1;
    -    if (header.name === -1) {
    -      header.name = HeaderSetDecompressor.string(buffer);
    -    }
    -
    -    if (representation === representations.literalSubstitution) {
    -      header.index = HeaderSetDecompressor.integer(buffer, 0);
    -    } else if (representation === representations.literalIncremental) {
    -      header.index = Infinity;
    -    } else {
    -      header.index = -1;
    -    }
    -
    -    header.value = HeaderSetDecompressor.string(buffer);
    -  }
    -
    -  return header;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Integration with HTTP/2

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    This section describes the interaction between the compressor/decompressor and the rest of the -HTTP/2 implementation. The Compressor and the Decompressor makes up a layer between the -framer and the connection handling component. They let most -frames pass through, except HEADERS and PUSH_PROMISE frames. They convert the frames between -these two representations:

    -
    {                                   {
    - type: 'HEADERS',                    type: 'HEADERS',
    - flags: {},                          flags: {},
    - stream: 1,               <===>      stream: 1,
    - headers: {                          data: Buffer
    -  N1: 'V1',                         }
    -  N2: ['V1', 'V2', ...],
    -  // ...
    - }
    -}
    -

    There are possibly several binary frame that belong to a single non-binary frame.

    - -
    - -
    var MAX_HTTP_PAYLOAD_SIZE = 16383;
    - -
  • - - -
  • -
    - -
    - -
    -

    The Compressor class

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The Compressor transform stream is basically stateless.

    - -
    - -
    util.inherits(Compressor, TransformStream);
    -function Compressor(log, type) {
    -  TransformStream.call(this, { objectMode: true });
    -
    -  this._log = log.child({ component: 'compressor' });
    -
    -  assert((type === 'REQUEST') || (type === 'RESPONSE'));
    -  var initialTable = (type === 'REQUEST') ? HeaderTable.initialRequestTable
    -                                          : HeaderTable.initialResponseTable;
    -  this._table = new HeaderTable(this._log, initialTable);
    -}
    - -
  • - - -
  • -
    - -
    - -
    -

    compress takes a header set, and compresses it using a new HeaderSetCompressor stream -instance. This means that from now on, the advantages of streaming header encoding are lost, -but the API becomes simpler.

    - -
    - -
    Compressor.prototype.compress = function compress(headers) {
    -  var compressor = new HeaderSetCompressor(this._log, this._table);
    -  for (var name in headers) {
    -    var value = headers[name];
    -    if (value instanceof Array) {
    -      for (var i = 0; i< value.length; i++) {
    -        compressor.write([String(name), String(value[i])]);
    -      }
    -    } else {
    -      compressor.write([String(name), String(value)]);
    -    }
    -  }
    -  compressor.end();
    -
    -  var chunk, chunks = [];
    -  while (chunk = compressor.read()) {
    -    chunks.push(chunk);
    -  }
    -  return concat(chunks);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    When a frame arrives

    - -
    - -
    Compressor.prototype._transform = function _transform(frame, encoding, done) {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • and it is a HEADERS or PUSH_PROMISE frame
        -
      • it generates a header block using the compress method
      • -
      • cuts the header block into chunks that are not larger than MAX_HTTP_PAYLOAD_SIZE
      • -
      • for each chunk, it pushes out a chunk frame that is identical to the original, except -the data property which holds the given chunk, the type of the frame which is always -CONTINUATION except for the first frame, and the END_HEADERS/END_PUSH_STREAM flag that -marks the last frame and the END_STREAM flag which is always false before the end
      • -
      -
    • -
    - -
    - -
      if (frame.type === 'HEADERS' || frame.type === 'PUSH_PROMISE') {
    -    var buffer = this.compress(frame.headers);
    -
    -    var chunks = cut(buffer, MAX_HTTP_PAYLOAD_SIZE);
    -
    -    for (var i = 0; i < chunks.length; i++) {
    -      var chunkFrame;
    -      var first = (i === 0);
    -      var last = (i === chunks.length - 1);
    -
    -      if (first) {
    -        chunkFrame = util._extend({}, frame);
    -        chunkFrame.flags = util._extend({}, frame.flags);
    -        chunkFrame.flags['END_' + frame.type] = last;
    -      } else {
    -        chunkFrame = {
    -          type: 'CONTINUATION',
    -          flags: { END_HEADERS: last },
    -          stream: frame.stream
    -        };
    -      }
    -      if (chunkFrame.type !== 'PUSH_PROMISE') {
    -        chunkFrame.flags.END_STREAM = last && frame.flags.END_STREAM;
    -      }
    -      chunkFrame.data = chunks[i];
    -
    -      this.push(chunkFrame);
    -    }
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • otherwise, the frame is forwarded without taking any action
    • -
    - -
    - -
      else {
    -    this.push(frame);
    -  }
    -
    -  done();
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    The Decompressor class

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The Decompressor is a stateful transform stream, since it has to collect multiple frames first, -and the decoding comes after unifying the payload of those frames.

    -

    If there's a frame in progress, this._inProgress is true. The frames are collected in -this._frames, and the type of the frame and the stream identifier is stored in this._type -and this._stream respectively.

    - -
    - -
    util.inherits(Decompressor, TransformStream);
    -function Decompressor(log, type) {
    -  TransformStream.call(this, { objectMode: true });
    -
    -  this._log = log.child({ component: 'compressor' });
    -
    -  assert((type === 'REQUEST') || (type === 'RESPONSE'));
    -  var initialTable = (type === 'REQUEST') ? HeaderTable.initialRequestTable
    -                                          : HeaderTable.initialResponseTable;
    -  this._table = new HeaderTable(this._log, initialTable);
    -
    -  this._inProgress = false;
    -  this._base = undefined;
    -}
    - -
  • - - -
  • -
    - -
    - -
    -

    decompress takes a full header block, and decompresses it using a new HeaderSetDecompressor -stream instance. This means that from now on, the advantages of streaming header decoding are -lost, but the API becomes simpler.

    - -
    - -
    Decompressor.prototype.decompress = function decompress(block) {
    -  var decompressor = new HeaderSetDecompressor(this._log, this._table);
    -  decompressor.end(block);
    -
    -  var headers = {};
    -  var pair;
    -  while (pair = decompressor.read()) {
    -    var name = pair[0];
    -    var value = pair[1];
    -    if (name in headers) {
    -      if (headers[name] instanceof Array) {
    -        headers[name].push(value);
    -      } else {
    -        headers[name] = [headers[name], value];
    -      }
    -    } else {
    -      headers[name] = value;
    -    }
    -  }
    -
    -  return headers;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    When a frame arrives

    - -
    - -
    Decompressor.prototype._transform = function _transform(frame, encoding, done) {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • and the collection process is already _inProgress, the frame is simply stored, except if -it's an illegal frame
    • -
    - -
    - -
      if (this._inProgress) {
    -    if ((frame.type !== 'CONTINUATION') || (frame.stream !== this._base.stream)) {
    -      this._log.error('A series of HEADER frames were not continuous');
    -      this.emit('error', 'PROTOCOL_ERROR');
    -      return;
    -    }
    -    this._frames.push(frame);
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • and the collection process is not _inProgress, but the new frame's type is HEADERS or -PUSH_PROMISE, a new collection process begins
    • -
    - -
    - -
      else if ((frame.type === 'HEADERS') || (frame.type === 'PUSH_PROMISE')) {
    -    this._inProgress = true;
    -    this._base = frame;
    -    this._frames = [frame];
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • otherwise, the frame is forwarded without taking any action
    • -
    - -
    - -
      else {
    -    this.push(frame);
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • When the frame signals that it's the last in the series, the header block chunks are -concatenated, the headers are decompressed, and a new frame gets pushed out with the -decompressed headers.
    • -
    - -
    - -
      if (this._inProgress && (frame.flags.END_HEADERS || frame.flags.END_PUSH_PROMISE)) {
    -    var buffer = concat(this._frames.map(function(frame) {
    -      return frame.data;
    -    }));
    -    try {
    -      var headers = this.decompress(buffer);
    -    } catch(error) {
    -      this._log.error({ err: error }, 'Header decompression error');
    -      this.emit('error', 'COMPRESSION_ERROR');
    -      return;
    -    }
    -    this.push(util._extend({ headers: headers }, this._base));
    -    this._inProgress = false;
    -  }
    -
    -  done();
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Helper functions

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Concatenate an array of buffers into a new buffer

    - -
    - -
    function concat(buffers) {
    -  var size = 0;
    -  for (var i = 0; i < buffers.length; i++) {
    -    size += buffers[i].length;
    -  }
    -
    -  var concatenated = new Buffer(size);
    -  for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) {
    -    buffers[j].copy(concatenated, cursor);
    -  }
    -
    -  return concatenated;
    -}
    - -
  • - - -
  • -
    - -
    - -
    -

    Cut buffer into chunks not larger than size

    - -
    - -
    function cut(buffer, size) {
    -  var chunks = [];
    -  var cursor = 0;
    -  do {
    -    var chunkSize = Math.min(size, buffer.length - cursor);
    -    chunks.push(buffer.slice(cursor, cursor + chunkSize));
    -    cursor += chunkSize;
    -  } while(cursor < buffer.length);
    -  return chunks;
    -}
    - -
  • - -
-
- - diff --git a/doc/connection.html b/doc/connection.html deleted file mode 100644 index 5c59791b..00000000 --- a/doc/connection.html +++ /dev/null @@ -1,1521 +0,0 @@ - - - - - connection.js - - - - - -
-
- - - -
    - -
  • -
    -

    connection.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    var assert = require('assert');
    - -
  • - - -
  • -
    - -
    - -
    -

    The Connection class

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The Connection class manages HTTP/2 connections. Each instance corresponds to one transport -stream (TCP stream). It operates by sending and receiving frames and is implemented as a -Flow subclass.

    - -
    - -
    var Flow = require('./flow').Flow;
    -
    -exports.Connection = Connection;
    - -
  • - - -
  • -
    - -
    - -
    -

    Public API

    - -
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • new Connection(log, firstStreamId, settings): create a new Connection

      -
    • -
    • Event: 'error' (type): signals a connection level error made by the other end

      -
    • -
    • Event: 'peerError' (type): signals the receipt of a GOAWAY frame that contains an error -code other than NO_ERROR

      -
    • -
    • Event: 'stream' (stream): signals that there's an incoming stream

      -
    • -
    • createStream(): stream: initiate a new stream

      -
    • -
    • set(settings): change the value of one or more settings according to the key-value pairs -of settings

      -
    • -
    • ping([callback]): send a ping and call callback when the answer arrives

      -
    • -
    • close([error]): close the stream with an error code

      -
    • -
    -

    Constructor

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The main aspects of managing the connection are:

    - -
    - -
    function Connection(log, firstStreamId, settings) {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • initializing the base class
    • -
    - -
    - -
      Flow.call(this, 0);
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • logging: every method uses the common logger object
    • -
    - -
    - -
      this._log = log.child({ component: 'connection' });
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • stream management
    • -
    - -
    - -
      this._initializeStreamManagement(firstStreamId);
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • lifecycle management
    • -
    - -
    - -
      this._initializeLifecycleManagement();
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • flow control
    • -
    - -
    - -
      this._initializeFlowControl();
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • settings management
    • -
    - -
    - -
      this._initializeSettingsManagement(settings);
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • multiplexing
    • -
    - -
    - -
      this._initializeMultiplexing();
    -}
    -Connection.prototype = Object.create(Flow.prototype, { constructor: { value: Connection } });
    - -
  • - - -
  • -
    - -
    - -
    -

    Overview

    - -
    - -
  • - - -
  • -
    - -
    - -
    -
             |    ^             |    ^
    -         v    |             v    |
    -    +--------------+   +--------------+
    -+---|   stream1    |---|   stream2    |----      ....      ---+
    -|   | +----------+ |   | +----------+ |                       |
    -|   | | stream1. | |   | | stream2. | |                       |
    -|   +-| upstream |-+   +-| upstream |-+                       |
    -|     +----------+       +----------+                         |
    -|       |     ^             |     ^                           |
    -|       v     |             v     |                           |
    -|       +-----+-------------+-----+--------      ....         |
    -|       ^     |             |     |                           |
    -|       |     v             |     |                           |
    -|   +--------------+        |     |                           |
    -|   |   stream0    |        |     |                           |
    -|   |  connection  |        |     |                           |
    -|   |  management  |     multiplexing                         |
    -|   +--------------+     flow control                         |
    -|                           |     ^                           |
    -|                   _read() |     | _write()                  |
    -|                           v     |                           |
    -|                +------------+ +-----------+                 |
    -|                |output queue| |input queue|                 |
    -+----------------+------------+-+-----------+-----------------+
    -                            |     ^
    -                     read() |     | write()
    -                            v     |
    -

    Stream management

    - -
    - -
  • - - -
  • -
    - -
    - -
    - -
    - -
    var Stream  = require('./stream').Stream;
    - -
  • - - -
  • -
    - -
    - -
    -

    Initialization:

    - -
    - -
    Connection.prototype._initializeStreamManagement = function _initializeStreamManagement(firstStreamId) {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • streams are stored in two data structures:
        -
      • _streamIds is an id -> stream map of the streams that are allowed to receive frames.
      • -
      • _streamPriorities is a priority -> [stream] map of stream that allowed to send frames.
      • -
      -
    • -
    - -
    - -
      this._streamIds = [];
    -  this._streamPriorities = [];
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • The next outbound stream ID and the last inbound stream id
    • -
    - -
    - -
      this._nextStreamId = firstStreamId;
    -  this._lastIncomingStream = 0;
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Calling _writeControlFrame when there's an incoming stream with 0 as stream ID
    • -
    - -
    - -
      this._streamIds[0] = { upstream: { write: this._writeControlFrame.bind(this) } };
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • By default, the number of concurrent outbound streams is not limited. The _streamLimit can -be set by the SETTINGS_MAX_CONCURRENT_STREAMS setting.
    • -
    - -
    - -
      this._streamSlotsFree = Infinity;
    -  this._streamLimit = Infinity;
    -  this.on('RECEIVING_SETTINGS_MAX_CONCURRENT_STREAMS', this._updateStreamLimit);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    _writeControlFrame is called when there's an incoming frame in the _control stream. It -broadcasts the message by creating an event on it.

    - -
    - -
    Connection.prototype._writeControlFrame = function _writeControlFrame(frame) {
    -  if ((frame.type === 'SETTINGS') || (frame.type === 'PING') ||
    -      (frame.type === 'GOAWAY') || (frame.type === 'WINDOW_UPDATE')) {
    -    this._log.debug({ frame: frame }, 'Receiving connection level frame');
    -    this.emit(frame.type, frame);
    -  } else {
    -    this._log.error({ frame: frame }, 'Invalid connection level frame');
    -    this.emit('error', 'PROTOCOL_ERROR');
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Methods to manage the stream slot pool:

    - -
    - -
    Connection.prototype._updateStreamLimit = function _updateStreamLimit(newStreamLimit) {
    -  var wakeup = (this._streamSlotsFree === 0) && (newStreamLimit > this._streamLimit);
    -  this._streamSlotsFree += newStreamLimit - this._streamLimit;
    -  this._streamLimit = newStreamLimit;
    -  if (wakeup) {
    -    this.emit('wakeup');
    -  }
    -};
    -
    -Connection.prototype._changeStreamCount = function _changeStreamCount(change) {
    -  if (change) {
    -    this._log.trace({ free: this._streamSlotsFree, change: change },
    -                    'Changing active stream count.');
    -    var wakeup = (this._streamSlotsFree === 0) && (change < 0);
    -    this._streamSlotsFree -= change;
    -    if (wakeup) {
    -      this.emit('wakeup');
    -    }
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Creating a new inbound or outbound stream with the given id (which is undefined in case of -an outbound stream) consists of three steps:

    -
      -
    1. var stream = new Stream(this._log);
    2. -
    3. this._allocateId(stream, id);
    4. -
    5. this._allocatePriority(stream);
    6. -
    -

    Allocating an ID to a stream

    - -
    - -
    Connection.prototype._allocateId = function _allocateId(stream, id) {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • initiated stream without definite ID
    • -
    - -
    - -
      if (id === undefined) {
    -    id = this._nextStreamId;
    -    this._nextStreamId += 2;
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • incoming stream with a legitim ID (larger than any previous and different parity than ours)
    • -
    - -
    - -
      else if ((id > this._lastIncomingStream) && ((id - this._nextStreamId) % 2 !== 0)) {
    -    this._lastIncomingStream = id;
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • incoming stream with invalid ID
    • -
    - -
    - -
      else {
    -    this._log.error({ stream_id: id, lastIncomingStream: this._lastIncomingStream },
    -                    'Invalid incoming stream ID.');
    -    this.emit('error', 'PROTOCOL_ERROR');
    -    return undefined;
    -  }
    -
    -  assert(!(id in this._streamIds));
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • adding to this._streamIds
    • -
    - -
    - -
      this._log.trace({ s: stream, stream_id: id }, 'Allocating ID for stream.');
    -  this._streamIds[id] = stream;
    -  stream.id = id;
    -  this.emit('new_stream', stream, id);
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • handling stream errors as connection errors
    • -
    - -
    - -
      stream.on('error', this.emit.bind(this, 'error'));
    -
    -  return id;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Allocating a priority to a stream, and managing priority changes

    - -
    - -
    Connection.prototype._allocatePriority = function _allocatePriority(stream) {
    -  this._log.trace({ s: stream }, 'Allocating priority for stream.');
    -  this._insert(stream, stream._priority);
    -  stream.on('priority', this._reprioritize.bind(this, stream));
    -  stream.upstream.on('readable', this.emit.bind(this, 'wakeup'));
    -  this.emit('wakeup');
    -};
    -
    -Connection.prototype._insert = function _insert(stream, priority) {
    -  if (priority in this._streamPriorities) {
    -    this._streamPriorities[priority].push(stream);
    -  } else {
    -    this._streamPriorities[priority] = [stream];
    -  }
    -};
    -
    -Connection.prototype._reprioritize = function _reprioritize(stream, priority) {
    -  var bucket = this._streamPriorities[stream._priority];
    -  var index = bucket.indexOf(stream);
    -  assert(index !== -1);
    -  bucket.splice(index, 1);
    -  if (bucket.length === 0) {
    -    delete this._streamPriorities[stream._priority];
    -  }
    -
    -  this._insert(stream, priority);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Creating an inbound stream with the given ID. It is called when there's an incoming frame to -a previously nonexistent stream.

    - -
    - -
    Connection.prototype._createIncomingStream = function _createIncomingStream(id) {
    -  this._log.debug({ stream_id: id }, 'New incoming stream.');
    -
    -  var stream = new Stream(this._log);
    -  this._allocateId(stream, id);
    -  this._allocatePriority(stream);
    -  this.emit('stream', stream, id);
    -
    -  return stream;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Creating an outbound stream

    - -
    - -
    Connection.prototype.createStream = function createStream() {
    -  this._log.trace('Creating new outbound stream.');
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Receiving is enabled immediately, and an ID gets assigned to the stream
    • -
    - -
    - -
      var stream = new Stream(this._log);
    -  this._allocatePriority(stream);
    -
    -  return stream;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Multiplexing

    - -
    - -
  • - - -
  • -
    - -
    - -
    - -
    - -
    Connection.prototype._initializeMultiplexing = function _initializeMultiplexing() {
    -  this.on('window_update', this.emit.bind(this, 'wakeup'));
    -  this._sendScheduled = false;
    -  this._firstFrameReceived = false;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    The _send method is a virtual method of the Flow class that has to be implemented -by child classes. It reads frames from streams and pushes them to the output buffer.

    - -
    - -
    Connection.prototype._send = function _send(immediate) {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Do not do anything if the connectionis already closed
    • -
    - -
    - -
      if (this._closed) {
    -    return;
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Collapsing multiple calls in a turn into a single deferred call
    • -
    - -
    - -
      if (immediate) {
    -    this._sendScheduled = false;
    -  } else {
    -    if (!this._sendScheduled) {
    -      this._sendScheduled = true;
    -      setImmediate(this._send.bind(this, true));
    -    }
    -    return;
    -  }
    -
    -  this._log.trace('Starting forwarding frames from streams.');
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Looping through priority buckets in priority order.
    • -
    - -
    - -
    priority_loop:
    -  for (var priority in this._streamPriorities) {
    -    var bucket = this._streamPriorities[priority];
    -    var nextBucket = [];
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Forwarding frames from buckets with round-robin scheduling.
        -
      1. pulling out frame
      2. -
      3. if there's no frame, skip this stream
      4. -
      5. if forwarding this frame would make streamCount greater than streamLimit, skip -this stream
      6. -
      7. adding stream to the bucket of the next round
      8. -
      9. assigning an ID to the frame (allocating an ID to the stream if there isn't already)
      10. -
      11. if forwarding a PUSH_PROMISE, allocate ID to the promised stream
      12. -
      13. forwarding the frame, changing streamCount as appropriate
      14. -
      15. stepping to the next stream if there's still more frame needed in the output buffer
      16. -
      17. switching to the bucket of the next round
      18. -
      -
    • -
    - -
    - -
        while (bucket.length > 0) {
    -      for (var index = 0; index < bucket.length; index++) {
    -        var stream = bucket[index];
    -        var frame = stream.upstream.read((this._window > 0) ? this._window : -1);
    -
    -        if (!frame) {
    -          continue;
    -        } else if (frame.count_change > this._streamSlotsFree) {
    -          stream.upstream.unshift(frame);
    -          continue;
    -        }
    -
    -        nextBucket.push(stream);
    -
    -        if (frame.stream === undefined) {
    -          frame.stream = stream.id || this._allocateId(stream);
    -        }
    -
    -        if (frame.type === 'PUSH_PROMISE') {
    -          this._allocatePriority(frame.promised_stream);
    -          frame.promised_stream = this._allocateId(frame.promised_stream);
    -        }
    -
    -        this._log.trace({ s: stream, frame: frame }, 'Forwarding outgoing frame');
    -        var moreNeeded = this.push(frame);
    -        this._changeStreamCount(frame.count_change);
    -
    -        assert(moreNeeded !== null); // The frame shouldn't be unforwarded
    -        if (moreNeeded === false) {
    -          break priority_loop;
    -        }
    -      }
    -
    -      bucket = nextBucket;
    -      nextBucket = [];
    -    }
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • if we couldn't forward any frame, then sleep until window update, or some other wakeup event
    • -
    - -
    - -
      if (moreNeeded === undefined) {
    -    this.once('wakeup', this._send.bind(this));
    -  }
    -
    -  this._log.trace({ moreNeeded: moreNeeded }, 'Stopping forwarding frames from streams.');
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    The _receive method is another virtual method of the Flow class that has to be -implemented by child classes. It forwards the given frame to the appropriate stream:

    - -
    - -
    Connection.prototype._receive = function _receive(frame, done) {
    -  this._log.trace({ frame: frame }, 'Forwarding incoming frame');
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • first frame needs to be checked by the _onFirstFrameReceived method
    • -
    - -
    - -
      if (!this._firstFrameReceived) {
    -    this._firstFrameReceived = true;
    -    this._onFirstFrameReceived(frame);
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • gets the appropriate stream from the stream registry
    • -
    - -
    - -
      var stream = this._streamIds[frame.stream];
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • or creates one if it's not in this.streams
    • -
    - -
    - -
      if (!stream) {
    -    stream = this._createIncomingStream(frame.stream);
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • in case of PUSH_PROMISE, replaces the promised stream id with a new incoming stream
    • -
    - -
    - -
      if (frame.type === 'PUSH_PROMISE') {
    -    frame.promised_stream = this._createIncomingStream(frame.promised_stream);
    -  }
    -
    -  frame.count_change = this._changeStreamCount.bind(this);
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • and writes it to the stream's upstream
    • -
    - -
    - -
      stream.upstream.write(frame);
    -
    -  done();
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Settings management

    - -
    - -
  • - - -
  • -
    - -
    - -
    - -
    - -
    var defaultSettings = {
    -  SETTINGS_FLOW_CONTROL_OPTIONS: true
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Settings management initialization:

    - -
    - -
    Connection.prototype._initializeSettingsManagement = function _initializeSettingsManagement(settings) {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Sending the initial settings.
    • -
    - -
    - -
      this._log.debug('Sending the first SETTINGS frame as part of the connection header.');
    -  this.set(settings || defaultSettings);
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Forwarding SETTINGS frames to the _receiveSettings method
    • -
    - -
    - -
      this.on('SETTINGS', this._receiveSettings);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Checking that the first frame the other endpoint sends is SETTINGS
    • -
    - -
    - -
    Connection.prototype._onFirstFrameReceived = function _onFirstFrameReceived(frame) {
    -  if ((frame.stream === 0) && (frame.type === 'SETTINGS')) {
    -    this._log.debug('Receiving the first SETTINGS frame as part of the connection header.');
    -  } else {
    -    this._log.fatal({ frame: frame }, 'Invalid connection header: first frame is not SETTINGS.');
    -    this.emit('error');
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Handling of incoming SETTINGS frames.

    - -
    - -
    Connection.prototype._receiveSettings = function _receiveSettings(frame) {
    -  for (var name in frame.settings) {
    -    this.emit('RECEIVING_' + name, frame.settings[name]);
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Changing one or more settings value and sending out a SETTINGS frame

    - -
    - -
    Connection.prototype.set = function set(settings) {
    -  this.push({
    -    type: 'SETTINGS',
    -    flags: {},
    -    stream: 0,
    -    settings: settings
    -  });
    -  for (var name in settings) {
    -    this.emit('SENDING_' + name, settings[name]);
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Lifecycle management

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The main responsibilities of lifecycle management code:

    -
      -
    • keeping the connection alive by
        -
      • sending PINGs when the connection is idle
      • -
      • answering PINGs
      • -
      -
    • -
    • ending the connection
    • -
    - -
    - -
    Connection.prototype._initializeLifecycleManagement = function _initializeLifecycleManagement() {
    -  this._pings = {};
    -  this.on('PING', this._receivePing);
    -  this.on('GOAWAY', this._receiveGoaway);
    -  this._closed = false;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Generating a string of length 16 with random hexadecimal digits

    - -
    - -
    Connection.prototype._generatePingId = function _generatePingId() {
    -  do {
    -    var id = '';
    -    for (var i = 0; i < 16; i++) {
    -      id += Math.floor(Math.random()*16).toString(16);
    -    }
    -  } while(id in this._pings);
    -  return id;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Sending a ping and calling callback when the answer arrives

    - -
    - -
    Connection.prototype.ping = function ping(callback) {
    -  var id = this._generatePingId();
    -  var data = new Buffer(id, 'hex');
    -  this._pings[id] = callback;
    -
    -  this._log.debug({ data: data }, 'Sending PING.');
    -  this.push({
    -    type: 'PING',
    -    flags: {
    -      PONG: false
    -    },
    -    stream: 0,
    -    data: data
    -  });
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Answering pings

    - -
    - -
    Connection.prototype._receivePing = function _receivePing(frame) {
    -  if (frame.flags.PONG) {
    -    var id = frame.data.toString('hex');
    -    if (id in this._pings) {
    -      this._log.debug({ data: frame.data }, 'Receiving answer for a PING.');
    -      var callback = this._pings[id];
    -      if (callback) {
    -        callback();
    -      }
    -      delete this._pings[id];
    -    } else {
    -      this._log.warn({ data: frame.data }, 'Unsolicited PING answer.');
    -    }
    -
    -  } else {
    -    this._log.debug({ data: frame.data }, 'Answering PING.');
    -    this.push({
    -      type: 'PING',
    -      flags: {
    -        PONG: true
    -      },
    -      stream: 0,
    -      data: frame.data
    -    });
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Terminating the connection

    - -
    - -
    Connection.prototype.close = function close(error) {
    -  if (this._closed) {
    -    this._log.warn('Trying to close an already closed connection');
    -    return;
    -  }
    -
    -  this._log.debug({ error: error }, 'Closing the connection');
    -  this.push({
    -    type: 'GOAWAY',
    -    flags: {},
    -    stream: 0,
    -    last_stream: this._lastIncomingStream,
    -    error: error || 'NO_ERROR'
    -  });
    -  this.push(null);
    -  this._closed = true;
    -};
    -
    -Connection.prototype._receiveGoaway = function _receiveGoaway(frame) {
    -  this._log.debug({ error: frame.error }, 'Other end closed the connection');
    -  this.push(null);
    -  this._closed = true;
    -  if (frame.error !== 'NO_ERROR') {
    -    this.emit('peerError', frame.error);
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Flow control

    - -
    - -
  • - - -
  • -
    - -
    - -
    - -
    - -
    Connection.prototype._initializeFlowControl = function _initializeFlowControl() {
    - -
  • - - -
  • -
    - -
    - -
    -

    Handling of initial window size of individual streams.

    - -
    - -
      this._initialStreamWindowSize = INITIAL_STREAM_WINDOW_SIZE;
    -  this.on('new_stream', function(stream) {
    -    stream.upstream.setInitialWindow(this._initialStreamWindowSize);
    -    if (this._remoteFlowControlDisabled) {
    -      stream.upstream.disableRemoteFlowControl();
    -    }
    -  });
    -  this.on('RECEIVING_SETTINGS_INITIAL_WINDOW_SIZE', this._setInitialStreamWindowSize);
    -  this.on('RECEIVING_SETTINGS_FLOW_CONTROL_OPTIONS', this._setLocalFlowControl);
    -  this.on('SENDING_SETTINGS_FLOW_CONTROL_OPTIONS', this._setRemoteFlowControl);
    -  this._streamIds[0].upstream.setInitialWindow = function noop() {};
    -  this._streamIds[0].upstream.disableRemoteFlowControl = function noop() {};
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    The initial connection flow control window is 65535 bytes.

    - -
    - -
    var INITIAL_STREAM_WINDOW_SIZE = 65535;
    - -
  • - - -
  • -
    - -
    - -
    -

    A SETTINGS frame can alter the initial flow control window size for all current streams. When the -value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the window size of all -stream by calling the setInitialStreamWindowSize method. The window size has to be modified by -the difference between the new value and the old value.

    - -
    - -
    Connection.prototype._setInitialStreamWindowSize = function _setInitialStreamWindowSize(size) {
    -  if ((this._initialStreamWindowSize === Infinity) && (size !== Infinity)) {
    -    this._log.error('Trying to manipulate initial flow control window size after flow control was turned off.');
    -    this.emit('error', 'FLOW_CONTROL_ERROR');
    -  } else {
    -    this._log.debug({ size: size }, 'Changing stream initial window size.');
    -    this._initialStreamWindowSize = size;
    -    this._streamIds.forEach(function(stream) {
    -      stream.upstream.setInitialWindow(size);
    -    });
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    _setStreamFlowControl() may be used to disable/enable flow control. In practice, it is just -for turning off flow control since it can not be turned on.

    - -
    - -
    Connection.prototype._setLocalFlowControl = function _setLocalFlowControl(disable) {
    -  if (disable) {
    -    this._increaseWindow(Infinity);
    -    this._setInitialStreamWindowSize(Infinity);
    -  } else if (this._initialStreamWindowSize === Infinity) {
    -    this._log.error('Trying to re-enable flow control after it was turned off.');
    -    this.emit('error', 'FLOW_CONTROL_ERROR');
    -  }
    -};
    -
    -Connection.prototype._setRemoteFlowControl = function _setRemoteFlowControl(disable) {
    -  if (disable) {
    -    this.disableRemoteFlowControl();
    -    this._streamIds.forEach(function(stream) {
    -      stream.upstream.disableRemoteFlowControl();
    -    });
    -  } else if (this._remoteFlowControlDisabled) {
    -    this._log.error('Trying to re-enable flow control after it was turned off.');
    -    throw new Error('Trying to re-enable flow control after it was turned off.');
    -  }
    -};
    - -
  • - -
-
- - diff --git a/doc/endpoint.html b/doc/endpoint.html deleted file mode 100644 index e3a8f1dc..00000000 --- a/doc/endpoint.html +++ /dev/null @@ -1,658 +0,0 @@ - - - - - endpoint.js - - - - - -
-
- - - -
    - -
  • -
    -

    endpoint.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    var assert = require('assert');
    -
    -var Serializer   = require('./framer').Serializer;
    -var Deserializer = require('./framer').Deserializer;
    -var Compressor   = require('./compressor').Compressor;
    -var Decompressor = require('./compressor').Decompressor;
    -var Connection   = require('./connection').Connection;
    -var Duplex       = require('stream').Duplex;
    -var Transform    = require('stream').Transform;
    -
    -exports.Endpoint = Endpoint;
    - -
  • - - -
  • -
    - -
    - -
    -

    The Endpoint class

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Public API

    - -
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • new Endpoint(log, role, settings, filters): create a new Endpoint.

      -
        -
      • log: bunyan logger of the parent
      • -
      • role: 'CLIENT' or 'SERVER'
      • -
      • settings: initial HTTP/2 settings
      • -
      • filters: a map of functions that filter the traffic between components (for debugging or -intentional failure injection).

        -

        Filter functions get three arguments:

        -
          -
        1. frame: the current frame
        2. -
        3. forward(frame): function that can be used to forward a frame to the next component
        4. -
        5. done(): callback to signal the end of the filter process
        6. -
        -

        Valid filter names and their position in the stack:

        -
          -
        • beforeSerialization: after compression, before serialization
        • -
        • beforeCompression: after multiplexing, before compression
        • -
        • afterDeserialization: after deserialization, before decompression
        • -
        • afterDecompression: after decompression, before multiplexing
        • -
        -
      • -
      -
    • -
    • Event: 'stream' (Stream): 'stream' event forwarded from the underlying Connection

      -
    • -
    • Event: 'error' (type): signals an error

      -
    • -
    • createStream(): Stream: initiate a new stream (forwarded to the underlying Connection)

      -
    • -
    • close([error]): close the connection with an error code

      -
    • -
    -

    Constructor

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The process of initialization:

    - -
    - -
    function Endpoint(log, role, settings, filters) {
    -  Duplex.call(this);
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Initializing logging infrastructure
    • -
    - -
    - -
      this._log = log.child({ component: 'endpoint', e: this });
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • First part of the handshake process: sending and receiving the client connection header -prelude.
    • -
    - -
    - -
      assert((role === 'CLIENT') || role === 'SERVER');
    -  if (role === 'CLIENT') {
    -    this._writePrelude();
    -  } else {
    -    this._readPrelude();
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Initialization of componenet. This includes the second part of the handshake process: -sending the first SETTINGS frame. This is done by the connection class right after -initialization.
    • -
    - -
    - -
      this._initializeDataFlow(role, settings, filters || {});
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Initialization of management code.
    • -
    - -
    - -
      this._initializeManagement();
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Initializing error handling.
    • -
    - -
    - -
      this._initializeErrorHandling();
    -}
    -Endpoint.prototype = Object.create(Duplex.prototype, { constructor: { value: Endpoint } });
    - -
  • - - -
  • -
    - -
    - -
    -

    Handshake

    - -
    - -
  • - - -
  • -
    - -
    - -
    - -
    - -
    var CLIENT_PRELUDE = new Buffer('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');
    - -
  • - - -
  • -
    - -
    - -
    -

    Writing the client header is simple and synchronous.

    - -
    - -
    Endpoint.prototype._writePrelude = function _writePrelude() {
    -  this._log.debug('Sending the client connection header prelude.');
    -  this.push(CLIENT_PRELUDE);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    The asynchronous process of reading the client header:

    - -
    - -
    Endpoint.prototype._readPrelude = function _readPrelude() {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • progress in the header is tracker using a cursor
    • -
    - -
    - -
      var cursor = 0;
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • _write is temporarily replaced by the comparator function
    • -
    - -
    - -
      this._write = function _temporalWrite(chunk, encoding, done) {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • which compares the stored header with the current chunk byte by byte and emits the -'error' event if there's a byte that doesn't match
    • -
    - -
    - -
        var offset = cursor;
    -    while(cursor < CLIENT_PRELUDE.length && (cursor - offset) < chunk.length) {
    -      if (CLIENT_PRELUDE[cursor] !== chunk[cursor - offset]) {
    -        this._log.fatal({ cursor: cursor, offset: offset, chunk: chunk },
    -                        'Client connection header prelude does not match.');
    -        this._error('handshake', 'PROTOCOL_ERROR');
    -        return;
    -      }
    -      cursor += 1;
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • if the whole header is over, and there were no error then restore the original _write -and call it with the remaining part of the current chunk
    • -
    - -
    - -
        if (cursor === CLIENT_PRELUDE.length) {
    -      this._log.debug('Successfully received the client connection header prelude.');
    -      delete this._write;
    -      chunk = chunk.slice(cursor - offset);
    -      this._write(chunk, encoding, done);
    -    }
    -  };
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Data flow

    - -
    - -
  • - - -
  • -
    - -
    - -
    -
    +---------------------------------------------+
    -|                                             |
    -|   +-------------------------------------+   |
    -|   | +---------+ +---------+ +---------+ |   |
    -|   | | stream1 | | stream2 | |   ...   | |   |
    -|   | +---------+ +---------+ +---------+ |   |
    -|   |             connection              |   |
    -|   +-------------------------------------+   |
    -|             |                 ^             |
    -|        pipe |                 | pipe        |
    -|             v                 |             |
    -|   +------------------+------------------+   |
    -|   |    compressor    |   decompressor   |   |
    -|   +------------------+------------------+   |
    -|             |                 ^             |
    -|        pipe |                 | pipe        |
    -|             v                 |             |
    -|   +------------------+------------------+   |
    -|   |    serializer    |   deserializer   |   |
    -|   +------------------+------------------+   |
    -|             |                 ^             |
    -|     _read() |                 | _write()    |
    -|             v                 |             |
    -|      +------------+     +-----------+       |
    -|      |output queue|     |input queue|       |
    -+------+------------+-----+-----------+-------+
    -              |                 ^
    -       read() |                 | write()
    -              v                 |
    - -
    - -
    function createTransformStream(filter) {
    -  var transform = new Transform({ objectMode: true });
    -  var push = transform.push.bind(transform);
    -  transform._transform = function(frame, encoding, done) {
    -    filter(frame, push, done);
    -  };
    -  return transform;
    -}
    -
    -function pipeAndFilter(stream1, stream2, filter) {
    -  if (filter) {
    -    stream1.pipe(createTransformStream(filter)).pipe(stream2);
    -  } else {
    -    stream1.pipe(stream2);
    -  }
    -}
    -
    -var MAX_HTTP_PAYLOAD_SIZE = 16383;
    -
    -Endpoint.prototype._initializeDataFlow = function _initializeDataFlow(role, settings, filters) {
    -  var firstStreamId, compressorRole, decompressorRole;
    -  if (role === 'CLIENT') {
    -    firstStreamId = 1;
    -    compressorRole = 'REQUEST';
    -    decompressorRole = 'RESPONSE';
    -  } else {
    -    firstStreamId = 2;
    -    compressorRole = 'RESPONSE';
    -    decompressorRole = 'REQUEST';
    -  }
    -
    -  this._serializer   = new Serializer(this._log, MAX_HTTP_PAYLOAD_SIZE);
    -  this._deserializer = new Deserializer(this._log, MAX_HTTP_PAYLOAD_SIZE);
    -  this._compressor   = new Compressor(this._log, compressorRole);
    -  this._decompressor = new Decompressor(this._log, decompressorRole);
    -  this._connection   = new Connection(this._log, firstStreamId, settings);
    -
    -  pipeAndFilter(this._connection, this._compressor, filters.beforeCompression);
    -  pipeAndFilter(this._compressor, this._serializer, filters.beforeSerialization);
    -  pipeAndFilter(this._deserializer, this._decompressor, filters.afterDeserialization);
    -  pipeAndFilter(this._decompressor, this._connection, filters.afterDecompression);
    -};
    -
    -var noread = {};
    -Endpoint.prototype._read = function _read() {
    -  this._readableState.sync = true;
    -  var moreNeeded = noread, chunk;
    -  while (moreNeeded && (chunk = this._serializer.read())) {
    -    moreNeeded = this.push(chunk);
    -  }
    -  if (moreNeeded === noread) {
    -    this._serializer.once('readable', this._read.bind(this));
    -  }
    -  this._readableState.sync = false;
    -};
    -
    -Endpoint.prototype._write = function _write(chunk, encoding, done) {
    -  this._deserializer.write(chunk, encoding, done);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Management

    - -
    - -
  • - - -
  • -
    - -
    - -
    - -
    - -
    Endpoint.prototype._initializeManagement = function _initializeManagement() {
    -  this._connection.on('stream', this.emit.bind(this, 'stream'));
    -};
    -
    -Endpoint.prototype.createStream = function createStream() {
    -  return this._connection.createStream();
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Error handling

    - -
    - -
  • - - -
  • -
    - -
    - -
    - -
    - -
    Endpoint.prototype._initializeErrorHandling = function _initializeErrorHandling() {
    -  this._serializer.on('error', this._error.bind(this, 'serializer'));
    -  this._deserializer.on('error', this._error.bind(this, 'deserializer'));
    -  this._compressor.on('error', this._error.bind(this, 'compressor'));
    -  this._decompressor.on('error', this._error.bind(this, 'decompressor'));
    -  this._connection.on('error', this._error.bind(this, 'connection'));
    -
    -  this._connection.on('peerError', this.emit.bind(this, 'peerError'));
    -};
    -
    -Endpoint.prototype._error = function _error(component, error) {
    -  this._log.fatal({ source: component, message: error }, 'Fatal error, closing connection');
    -  this.close(error);
    -  setImmediate(this.emit.bind(this, 'error', error));
    -};
    -
    -Endpoint.prototype.close = function close(error) {
    -  this._connection.close(error);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Bunyan serializers

    - -
    - -
  • - - -
  • -
    - -
    - -
    - -
    - -
    exports.serializers = {};
    -
    -var nextId = 0;
    -exports.serializers.e = function(endpoint) {
    -  if (!('id' in endpoint)) {
    -    endpoint.id = nextId;
    -    nextId += 1;
    -  }
    -  return endpoint.id;
    -};
    - -
  • - -
-
- - diff --git a/doc/flow.html b/doc/flow.html deleted file mode 100644 index db6a38bd..00000000 --- a/doc/flow.html +++ /dev/null @@ -1,792 +0,0 @@ - - - - - flow.js - - - - - -
-
- - - -
    - -
  • -
    -

    flow.js

    -
    -
  • - - - -
  • -
    - -
    - -
    - -
    - -
    var assert = require('assert');
    - -
  • - - -
  • -
    - -
    - -
    -

    The Flow class

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Flow is a Duplex stream subclass which implements HTTP/2 flow control. It is designed to be -subclassed by Connection and the upstream component of Stream.

    - -
    - -
    var Duplex  = require('stream').Duplex;
    -
    -exports.Flow = Flow;
    - -
  • - - -
  • -
    - -
    - -
    -

    Public API

    - -
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Event: 'error' (type): signals an error

      -
    • -
    • setInitialWindow(size): the initial flow control window size can be changed any time -(as described in the standard) using this method

      -
    • -
    • disableRemoteFlowControl(): sends a WINDOW_UPDATE signaling that we don't want flow control

      -
    • -
    • disableLocalFlowControl(): disables flow control for outgoing frames

      -
    • -
    -

    API for child classes

    - -
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • new Flow([flowControlId]): creating a new flow that will listen for WINDOW_UPDATES frames -with the given flowControlId (or every update frame if not given)

      -
    • -
    • _send(): called when more frames should be pushed. The child class is expected to override -this (instead of the _read method of the Duplex class).

      -
    • -
    • _receive(frame, readyCallback): called when there's an incoming frame. The child class is -expected to override this (instead of the _write method of the Duplex class).

      -
    • -
    • push(frame): bool: schedules frame for sending.

      -

      Returns true if it needs more frames in the output queue, false if the output queue is -full, and null if did not push the frame into the output queue (instead, it pushed it into -the flow control queue).

      -
    • -
    • read(limit): frame: like the regular read, but the 'flow control size' (0 for non-DATA -frames, length of the payload for DATA frames) of the returned frame will be under limit. -Small exception: pass -1 as limit if the max. flow control size is 0. read(0) means the -same thing as in the original API.

      -
    • -
    • getLastQueuedFrame(): frame: returns the last frame in output buffers

      -
    • -
    • _log: the Flow class uses the _log object of the parent

      -
    • -
    -

    Constructor

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    When a HTTP/2.0 connection is first established, new streams are created with an initial flow -control window size of 65535 bytes.

    - -
    - -
    var INITIAL_WINDOW_SIZE = 65535;
    - -
  • - - -
  • -
    - -
    - -
    -

    flowControlId is needed if only specific WINDOW_UPDATEs should be watched.

    - -
    - -
    function Flow(flowControlId) {
    -  Duplex.call(this, { objectMode: true });
    -
    -  this._window = this._initialWindow = INITIAL_WINDOW_SIZE;
    -  this._flowControlId = flowControlId;
    -  this._queue = [];
    -  this._ended = false;
    -  this._received = 0;
    -  this._remoteFlowControlDisabled = false;
    -}
    -Flow.prototype = Object.create(Duplex.prototype, { constructor: { value: Flow } });
    - -
  • - - -
  • -
    - -
    - -
    -

    Incoming frames

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    _receive is called when there's an incoming frame.

    - -
    - -
    Flow.prototype._receive = function _receive(frame, callback) {
    -  throw new Error('The _receive(frame, callback) method has to be overridden by the child class!');
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    _receive is called by _write which in turn is called by Duplex when someone write()s -to the flow. It emits the 'receiving' event and notifies the window size tracking code if the -incoming frame is a WINDOW_UPDATE.

    - -
    - -
    Flow.prototype._write = function _write(frame, encoding, callback) {
    -  if (frame.flags.END_STREAM || (frame.type === 'RST_STREAM')) {
    -    this._ended = true;
    -  }
    -
    -  if ((frame.type === 'DATA') && (frame.data.length > 0) && !this._remoteFlowControlDisabled) {
    -    this._receive(frame, function() {
    -      this._received += frame.data.length;
    -      if (!this._restoreWindowTimer) {
    -        this._restoreWindowTimer = setImmediate(this._restoreWindow.bind(this));
    -      }
    -      callback();
    -    }.bind(this));
    -  }
    -
    -  else {
    -    this._receive(frame, callback);
    -  }
    -
    -  if ((frame.type === 'WINDOW_UPDATE') &&
    -      ((this._flowControlId === undefined) || (frame.stream === this._flowControlId))) {
    -    this._updateWindow(frame);
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    _restoreWindow basically acknowledges the DATA frames received since it's last call. It sends -a WINDOW_UPDATE that restores the flow control window of the remote end.

    - -
    - -
    Flow.prototype._restoreWindow = function _restoreWindow() {
    -  delete this._restoreWindowTimer;
    -  if (!this._ended && !this._remoteFlowControlDisabled && (this._received > 0)) {
    -    this.push({
    -      type: 'WINDOW_UPDATE',
    -      flags: {},
    -      stream: this._flowControlId,
    -      window_size: this._received
    -    });
    -    this._received = 0;
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Must be called after sending a SETTINGS frame that turns off flow control on the remote side.

    - -
    - -
    Flow.prototype.disableRemoteFlowControl = function disableRemoteFlowControl() {
    -  this._log.debug('Turning off remote flow control');
    -  this._remoteFlowControlDisabled = true;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Outgoing frames - sending procedure

    - -
    - -
  • - - -
  • -
    - -
    - -
    -
                                        flow
    -           +-------------------------------------------------+
    -           |                                                 |
    -           +--------+           +---------+                  |
    -   read()  | output |  _read()  | flow    |  _send()         |
    -<----------|        |<----------| control |<-------------    |
    -           | buffer |           | buffer  |                  |
    -           +--------+           +---------+                  |
    -           | input  |                                        |
    ----------->|        |----------------------------------->    |
    -  write()  | buffer |  _write()              _receive()      |
    -           +--------+                                        |
    -           |                                                 |
    -           +-------------------------------------------------+
    -

    _send is called when more frames should be pushed to the output buffer.

    - -
    - -
    Flow.prototype._send = function _send() {
    -  throw new Error('The _send() method has to be overridden by the child class!');
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    _send is called by _read which is in turn called by Duplex when it wants to have more -items in the output queue.

    - -
    - -
    Flow.prototype._read = function _read() {
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • if the flow control queue is empty, then let the user push more frames
    • -
    - -
    - -
      if (this._queue.length === 0) {
    -    this._send();
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • if there are items in the flow control queue, then let's put them into the output queue (to -the extent it is possible with respect to the window size and output queue feedback)
    • -
    - -
    - -
      else if (this._window > 0) {
    -    this._readableState.sync = true; // to avoid reentrant calls
    -    do {
    -      var moreNeeded = this._push(this._queue[0]);
    -      if (moreNeeded !== null) {
    -        this._queue.shift();
    -      }
    -    } while (moreNeeded && (this._queue.length > 0));
    -    this._readableState.sync = false;
    -
    -    assert((moreNeeded == false) ||                              // * output queue is full
    -           (this._queue.length === 0) ||                         // * flow control queue is empty
    -           (!this._window && (this._queue[0].type === 'DATA'))); // * waiting for window update
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • otherwise, come back when the flow control window is positive
    • -
    - -
    - -
      else {
    -    this.once('window_update', this._read);
    -  }
    -};
    -
    -var MAX_PAYLOAD_SIZE = 4096; // Must not be greater than MAX_HTTP_PAYLOAD_SIZE which is 16383
    - -
  • - - -
  • -
    - -
    - -
    -

    read(limit) is like the read of the Readable class, but it guarantess that the 'flow control -size' (0 for non-DATA frames, length of the payload for DATA frames) of the returned frame will -be under limit.

    - -
    - -
    Flow.prototype.read = function read(limit) {
    -  if (limit === 0) {
    -    return Duplex.prototype.read.call(this, 0);
    -  } else if (limit === -1) {
    -    limit = 0;
    -  } else if ((limit === undefined) || (limit > MAX_PAYLOAD_SIZE)) {
    -    limit = MAX_PAYLOAD_SIZE;
    -  }
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • Looking at the first frame in the queue without pulling it out if possible. This will save -a costly unshift if the frame proves to be too large to return.
    • -
    - -
    - -
      var firstInQueue = this._readableState.buffer[0];
    -  var frame = firstInQueue || Duplex.prototype.read.call(this);
    -
    -  if ((frame === null) || (frame.type !== 'DATA') || (frame.data.length <= limit)) {
    -    if (firstInQueue) {
    -      Duplex.prototype.read.call(this);
    -    }
    -    return frame;
    -  }
    -
    -  else if (limit <= 0) {
    -    if (!firstInQueue) {
    -      this.unshift(frame);
    -    }
    -    return null;
    -  }
    -
    -  else {
    -    this._log.trace({ frame: frame, size: frame.data.length, forwardable: limit },
    -                    'Splitting out forwardable part of a DATA frame.');
    -    var forwardable = {
    -      type: 'DATA',
    -      flags: {},
    -      stream: frame.stream,
    -      data: frame.data.slice(0, limit)
    -    };
    -    frame.data = frame.data.slice(limit);
    -
    -    if (!firstInQueue) {
    -      this.unshift(frame);
    -    }
    -    return forwardable;
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    _parentPush pushes the given frame into the output queue

    - -
    - -
    Flow.prototype._parentPush = function _parentPush(frame) {
    -  this._log.trace({ frame: frame }, 'Pushing frame into the output queue');
    -
    -  if (frame && (frame.type === 'DATA') && (this._window !== Infinity)) {
    -    this._log.trace({ window: this._window, by: frame.data.length },
    -                    'Decreasing flow control window size.');
    -    this._window -= frame.data.length;
    -    assert(this._window >= 0);
    -  }
    -
    -  return Duplex.prototype.push.call(this, frame);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    _push(frame) pushes frame into the output queue and decreases the flow control window size. -It is capable of splitting DATA frames into smaller parts, if the window size is not enough to -push the whole frame. The return value is similar to push except that it returns null if it -did not push the whole frame to the output queue (but maybe it did push part of the frame).

    - -
    - -
    Flow.prototype._push = function _push(frame) {
    -  var data = frame && (frame.type === 'DATA') && frame.data;
    -
    -  if (!data || (data.length <= this._window)) {
    -    return this._parentPush(frame);
    -  }
    -
    -  else if (this._window <= 0) {
    -    return null;
    -  }
    -
    -  else {
    -    this._log.trace({ frame: frame, size: frame.data.length, forwardable: this._window },
    -                    'Splitting out forwardable part of a DATA frame.');
    -    frame.data = data.slice(this._window);
    -    this._parentPush({
    -      type: 'DATA',
    -      flags: {},
    -      stream: frame.stream,
    -      data: data.slice(0, this._window)
    -    });
    -    return null;
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Push frame into the flow control queue, or if it's empty, then directly into the output queue

    - -
    - -
    Flow.prototype.push = function push(frame) {
    -  if (frame === null) {
    -    this._log.debug('Enqueueing outgoing End Of Stream');
    -  } else {
    -    this._log.debug({ frame: frame }, 'Enqueueing outgoing frame');
    -  }
    -
    -  var moreNeeded = null;
    -  if (this._queue.length === 0) {
    -    moreNeeded = this._push(frame);
    -  }
    -
    -  if (moreNeeded === null) {
    -    this._queue.push(frame);
    -  }
    -
    -  return moreNeeded;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    getLastQueuedFrame returns the last frame in output buffers. This is primarily used by the -Stream class to mark the last frame with END_STREAM flag.

    - -
    - -
    Flow.prototype.getLastQueuedFrame = function getLastQueuedFrame() {
    -  var readableQueue = this._readableState.buffer;
    -  return this._queue[this._queue.length - 1] || readableQueue[readableQueue.length - 1];
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Outgoing frames - managing the window size

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Flow control window size is manipulated using the _increaseWindow method.

    -
      -
    • Invoking it with Infinite means turning off flow control. Flow control cannot be enabled -again once disabled. Any attempt to re-enable flow control MUST be rejected with a -FLOW_CONTROL_ERROR error code.
    • -
    • A sender MUST NOT allow a flow control window to exceed 2^31 - 1 bytes. The action taken -depends on it being a stream or the connection itself.
    • -
    - -
    - -
    var WINDOW_SIZE_LIMIT = Math.pow(2, 31) - 1;
    -
    -Flow.prototype._increaseWindow = function _increaseWindow(size) {
    -  if ((this._window === Infinity) && (size !== Infinity)) {
    -    this._log.error('Trying to increase flow control window after flow control was turned off.');
    -    this.emit('error', 'FLOW_CONTROL_ERROR');
    -  } else {
    -    this._log.trace({ window: this._window, by: size }, 'Increasing flow control window size.');
    -    this._window += size;
    -    if ((this._window !== Infinity) && (this._window > WINDOW_SIZE_LIMIT)) {
    -      this._log.error('Flow control window grew too large.');
    -      this.emit('error', 'FLOW_CONTROL_ERROR');
    -    } else {
    -      this.emit('window_update');
    -    }
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    The _updateWindow method gets called every time there's an incoming WINDOW_UPDATE frame. It -modifies the flow control window:

    -
      -
    • Flow control can be disabled for an individual stream by sending a WINDOW_UPDATE with the -END_FLOW_CONTROL flag set. The payload of a WINDOW_UPDATE frame that has the END_FLOW_CONTROL -flag set is ignored.
    • -
    • A sender that receives a WINDOW_UPDATE frame updates the corresponding window by the amount -specified in the frame.
    • -
    - -
    - -
    Flow.prototype._updateWindow = function _updateWindow(frame) {
    -  this._increaseWindow(frame.flags.END_FLOW_CONTROL ? Infinity : frame.window_size);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    A SETTINGS frame can alter the initial flow control window size for all current streams. When the -value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the size of all stream by -calling the setInitialWindow method. The window size has to be modified by the difference -between the new value and the old value.

    - -
    - -
    Flow.prototype.setInitialWindow = function setInitialWindow(initialWindow) {
    -  this._increaseWindow(initialWindow - this._initialWindow);
    -  this._initialWindow = initialWindow;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Flow control for outgoing frames can be disabled by the peer with various methods.

    - -
    - -
    Flow.prototype.disableLocalFlowControl = function disableLocalFlowControl() {
    -  this._increaseWindow(Infinity);
    -};
    - -
  • - -
-
- - diff --git a/doc/framer.html b/doc/framer.html deleted file mode 100644 index aff6d81e..00000000 --- a/doc/framer.html +++ /dev/null @@ -1,1387 +0,0 @@ - - - - - framer.js - - - - - -
-
- - - -
    - -
  • -
    -

    framer.js

    -
    -
  • - - - -
  • -
    - -
    - -
    -

    The framer consists of two Transform Stream subclasses that operate in object mode: -the Serializer and the Deserializer

    - -
    - -
    var assert = require('assert');
    -
    -var Transform = require('stream').Transform;
    -
    -exports.Serializer = Serializer;
    -exports.Deserializer = Deserializer;
    -
    -var logData = Boolean(process.env.HTTP2_LOG_DATA);
    - -
  • - - -
  • -
    - -
    - -
    -

    Serializer

    - -
    - -
  • - - -
  • -
    - -
    - -
    -
    Frame Objects
    -* * * * * * * --+---------------------------
    -                |                          |
    -                v                          v           Buffers
    - [] -----> Payload Ser. --[buffers]--> Header Ser. --> * * * *
    -empty      adds payload                adds header
    -array        buffers                     buffer
    - -
    - -
    function Serializer(log, sizeLimit) {
    -  this._log = log.child({ component: 'serializer' });
    -  this._sizeLimit = sizeLimit || MAX_PAYLOAD_SIZE;
    -  Transform.call(this, { objectMode: true });
    -}
    -Serializer.prototype = Object.create(Transform.prototype, { constructor: { value: Serializer } });
    - -
  • - - -
  • -
    - -
    - -
    -

    When there's an incoming frame object, it first generates the frame type specific part of the -frame (payload), and then then adds the header part which holds fields that are common to all -frame types (like the length of the payload).

    - -
    - -
    Serializer.prototype._transform = function _transform(frame, encoding, done) {
    -  this._log.trace({ frame: frame }, 'Outgoing frame');
    -
    -  assert(frame.type in Serializer, 'Unknown frame type: ' + frame.type);
    -
    -  var buffers = [];
    -  Serializer[frame.type](frame, buffers);
    -  Serializer.commonHeader(frame, buffers);
    -
    -  assert(buffers[0].readUInt16BE(0) <= this._sizeLimit, 'Frame too large!');
    -
    -  for (var i = 0; i < buffers.length; i++) {
    -    if (logData) {
    -      this._log.trace({ data: buffers[i] }, 'Outgoing data');
    -    }
    -    this.push(buffers[i]);
    -  }
    -
    -  done();
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Deserializer

    - -
    - -
  • - - -
  • -
    - -
    - -
    -
    Buffers
    -* * * * --------+-------------------------
    -                |                        |
    -                v                        v           Frame Objects
    - {} -----> Header Des. --{frame}--> Payload Des. --> * * * * * * *
    -empty      adds parsed              adds parsed
    -object  header properties        payload properties
    - -
    - -
    function Deserializer(log, sizeLimit) {
    -  this._log = log.child({ component: 'deserializer' });
    -  this._sizeLimit = sizeLimit || MAX_PAYLOAD_SIZE;
    -  Transform.call(this, { objectMode: true });
    -  this._next(COMMON_HEADER_SIZE);
    -}
    -Deserializer.prototype = Object.create(Transform.prototype, { constructor: { value: Deserializer } });
    - -
  • - - -
  • -
    - -
    - -
    -

    The Deserializer is stateful, and it's two main alternating states are: waiting for header and -waiting for payload. The state is stored in the boolean property _waitingForHeader.

    -

    When entering a new state, a _buffer is created that will hold the accumulated data (header or -payload). The _cursor is used to track the progress.

    - -
    - -
    Deserializer.prototype._next = function(size) {
    -  this._cursor = 0;
    -  this._buffer = new Buffer(size);
    -  this._waitingForHeader = !this._waitingForHeader;
    -  if (this._waitingForHeader) {
    -    this._frame = {};
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Parsing an incoming buffer is an iterative process because it can hold multiple frames if it's -large enough. A cursor is used to track the progress in parsing the incoming chunk.

    - -
    - -
    Deserializer.prototype._transform = function _transform(chunk, encoding, done) {
    -  var cursor = 0;
    -
    -  if (logData) {
    -    this._log.trace({ data: chunk }, 'Incoming data');
    -  }
    -
    -  while(cursor < chunk.length) {
    - -
  • - - -
  • -
    - -
    - -
    -

    The content of an incoming buffer is first copied to _buffer. If it can't hold the full -chunk, then only a part of it is copied.

    - -
    - -
        var toCopy = Math.min(chunk.length - cursor, this._buffer.length - this._cursor);
    -    chunk.copy(this._buffer, this._cursor, cursor, cursor + toCopy);
    -    this._cursor += toCopy;
    -    cursor += toCopy;
    - -
  • - - -
  • -
    - -
    - -
    -

    When _buffer is full, it's content gets parsed either as header or payload depending on -the actual state.

    -

    If it's header then the parsed data is stored in a temporary variable and then the -deserializer waits for the specified length payload.

    - -
    - -
        if ((this._cursor === this._buffer.length) && this._waitingForHeader) {
    -      var payloadSize = Deserializer.commonHeader(this._buffer, this._frame);
    -      if (payloadSize <= this._sizeLimit) {
    -        this._next(payloadSize);
    -      } else {
    -        this.emit('error', 'FRAME_TOO_LARGE');
    -        return;
    -      }
    -    }
    - -
  • - - -
  • -
    - -
    - -
    -

    If it's payload then the the frame object is finalized and then gets pushed out. -Unknown frame types are ignored.

    -

    Note: If we just finished the parsing of a header and the payload length is 0, this branch -will also run.

    - -
    - -
        if ((this._cursor === this._buffer.length) && !this._waitingForHeader) {
    -      if (this._frame.type) {
    -        var error = Deserializer[this._frame.type](this._buffer, this._frame);
    -        if (error) {
    -          this._log.error('Incoming frame parsing error: ' + error);
    -          this.emit('error', 'PROTOCOL_ERROR');
    -        } else {
    -          this._log.trace({ frame: this._frame }, 'Incoming frame');
    -          this.push(this._frame);
    -        }
    -      } else {
    -        this._log.warn({ frame: this._frame }, 'Unknown type incoming frame');
    -      }
    -      this._next(COMMON_HEADER_SIZE);
    -    }
    -  }
    -
    -  done();
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Frame Header

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    HTTP/2.0 frames share a common base format consisting of an 8-byte header followed by 0 to 65535 -bytes of data.

    -

    Additional size limits can be set by specific application uses. HTTP limits the frame size to -16,383 octets.

    -
     0                   1                   2                   3
    - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    -|         Length (16)           |   Type (8)    |   Flags (8)   |
    -+-+-------------+---------------+-------------------------------+
    -|R|                 Stream Identifier (31)                      |
    -+-+-------------------------------------------------------------+
    -|                     Frame Data (0...)                       ...
    -+---------------------------------------------------------------+
    -

    The fields of the frame header are defined as:

    -
      -
    • Length: -The length of the frame data expressed as an unsigned 16-bit integer. The 8 bytes of the frame -header are not included in this value.

      -
    • -
    • Type: -The 8-bit type of the frame. The frame type determines how the remainder of the frame header -and data are interpreted. Implementations MUST ignore unsupported and unrecognized frame types.

      -
    • -
    • Flags: -An 8-bit field reserved for frame-type specific boolean flags.

      -

      Flags are assigned semantics specific to the indicated frame type. Flags that have no defined -semantics for a particular frame type MUST be ignored, and MUST be left unset (0) when sending.

      -
    • -
    • R: -A reserved 1-bit field. The semantics of this bit are undefined and the bit MUST remain unset -(0) when sending and MUST be ignored when receiving.

      -
    • -
    • Stream Identifier: -A 31-bit stream identifier (see Section 3.4.1). A value 0 is reserved for frames that are -associated with the connection as a whole as opposed to an individual stream.

      -
    • -
    -

    The structure and content of the remaining frame data is dependent entirely on the frame type.

    - -
    - -
    var COMMON_HEADER_SIZE = 8;
    -var MAX_PAYLOAD_SIZE = 65535;
    -
    -var frameTypes = [];
    -
    -var frameFlags = {};
    -
    -var genericAttributes = ['type', 'flags', 'stream'];
    -
    -var typeSpecificAttributes = {};
    -
    -Serializer.commonHeader = function writeCommonHeader(frame, buffers) {
    -  var headerBuffer = new Buffer(COMMON_HEADER_SIZE);
    -
    -  var size = 0;
    -  for (var i = 0; i < buffers.length; i++) {
    -    size += buffers[i].length;
    -  }
    -  assert(size <= MAX_PAYLOAD_SIZE, size);
    -  headerBuffer.writeUInt16BE(size, 0);
    -
    -  var typeId = frameTypes.indexOf(frame.type);  // If we are here then the type is valid for sure
    -  headerBuffer.writeUInt8(typeId, 2);
    -
    -  var flagByte = 0;
    -  for (var flag in frame.flags) {
    -    var position = frameFlags[frame.type].indexOf(flag);
    -    assert(position !== -1, 'Unknown flag for frame type ' + frame.type + ': ' + flag);
    -    if (frame.flags[flag]) {
    -      flagByte |= (1 << position);
    -    }
    -  }
    -  headerBuffer.writeUInt8(flagByte, 3);
    -
    -  assert((0 <= frame.stream) && (frame.stream < 0x7fffffff), frame.stream);
    -  headerBuffer.writeUInt32BE(frame.stream || 0, 4);
    -
    -  buffers.unshift(headerBuffer);
    -};
    -
    -Deserializer.commonHeader = function readCommonHeader(buffer, frame) {
    -  var length = buffer.readUInt16BE(0);
    -
    -  frame.type = frameTypes[buffer.readUInt8(2)];
    -
    -  frame.flags = {};
    -  var flagByte = buffer.readUInt8(3);
    -  var definedFlags = frameFlags[frame.type];
    -  for (var i = 0; i < definedFlags.length; i++) {
    -    frame.flags[definedFlags[i]] = Boolean(flagByte & (1 << i));
    -  }
    -
    -  frame.stream = buffer.readUInt32BE(4) & 0x7fffffff;
    -
    -  return length;
    -};
    -
    -
    - -
  • - - -
  • -
    - -
    - -
    -

    Frame types

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Every frame type is registered in the following places:

    -
      -
    • frameTypes: a register of frame type codes (used by commonHeader())
    • -
    • frameFlags: a register of valid flags for frame types (used by commonHeader())
    • -
    • typeSpecificAttributes: a register of frame specific frame object attributes (used by -logging code and also serves as documentation for frame objects)
    • -
    -

    DATA Frames

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    DATA frames (type=0x0) convey arbitrary, variable-length sequences of octets associated with a -stream.

    -

    The DATA frame defines the following flags:

    -
      -
    • END_STREAM (0x1): -Bit 1 being set indicates that this frame is the last that the endpoint will send for the -identified stream.
    • -
    • RESERVED (0x2): -Bit 2 is reserved for future use.
    • -
    - -
    - -
    frameTypes[0x0] = 'DATA';
    -
    -frameFlags.DATA = ['END_STREAM', 'RESERVED'];
    -
    -typeSpecificAttributes.DATA = ['data'];
    -
    -Serializer.DATA = function writeData(frame, buffers) {
    -  buffers.push(frame.data);
    -};
    -
    -Deserializer.DATA = function readData(buffer, frame) {
    -  frame.data = buffer;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    HEADERS

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The HEADERS frame (type=0x1) allows the sender to create a stream.

    -

    The HEADERS frame defines the following flags:

    -
      -
    • END_STREAM (0x1): -Bit 1 being set indicates that this frame is the last that the endpoint will send for the -identified stream.
    • -
    • RESERVED (0x2): -Bit 2 is reserved for future use.
    • -
    • END_HEADERS (0x4): -The END_HEADERS bit indicates that this frame contains the entire payload necessary to provide -a complete set of headers.
    • -
    • PRIORITY (0x8): -Bit 4 being set indicates that the first four octets of this frame contain a single reserved -bit and a 31-bit priority.
    • -
    - -
    - -
    frameTypes[0x1] = 'HEADERS';
    -
    -frameFlags.HEADERS = ['END_STREAM', 'RESERVED', 'END_HEADERS', 'PRIORITY'];
    -
    -typeSpecificAttributes.HEADERS = ['priority', 'headers', 'data'];
    - -
  • - - -
  • -
    - -
    - -
    -
     0                   1                   2                   3
    - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    -|X|               (Optional) Priority (31)                      |
    -+-+-------------------------------------------------------------+
    -|                    Header Block (*)                         ...
    -+---------------------------------------------------------------+
    -

    The payload of a HEADERS frame contains a Headers Block

    - -
    - -
    Serializer.HEADERS = function writeHeadersPriority(frame, buffers) {
    -  if (frame.flags.PRIORITY) {
    -    var buffer = new Buffer(4);
    -    assert((0 <= frame.priority) && (frame.priority <= 0xffffffff), frame.priority);
    -    buffer.writeUInt32BE(frame.priority, 0);
    -    buffers.push(buffer);
    -  }
    -  buffers.push(frame.data);
    -};
    -
    -Deserializer.HEADERS = function readHeadersPriority(buffer, frame) {
    -  if (frame.flags.PRIORITY) {
    -    frame.priority = buffer.readUInt32BE(0) & 0x7fffffff;
    -    frame.data = buffer.slice(4);
    -  } else {
    -    frame.data = buffer;
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    PRIORITY

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The PRIORITY frame (type=0x2) specifies the sender-advised priority of a stream.

    -

    The PRIORITY frame does not define any flags.

    - -
    - -
    frameTypes[0x2] = 'PRIORITY';
    -
    -frameFlags.PRIORITY = [];
    -
    -typeSpecificAttributes.PRIORITY = ['priority'];
    - -
  • - - -
  • -
    - -
    - -
    -
     0                   1                   2                   3
    - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    -|X|                   Priority (31)                             |
    -+-+-------------------------------------------------------------+
    -

    The payload of a PRIORITY frame contains a single reserved bit and a 31-bit priority.

    - -
    - -
    Serializer.PRIORITY = function writePriority(frame, buffers) {
    -  var buffer = new Buffer(4);
    -  buffer.writeUInt32BE(frame.priority, 0);
    -  buffers.push(buffer);
    -};
    -
    -Deserializer.PRIORITY = function readPriority(buffer, frame) {
    -  frame.priority = buffer.readUInt32BE(0);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    RST_STREAM

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The RST_STREAM frame (type=0x3) allows for abnormal termination of a stream.

    -

    No type-flags are defined.

    - -
    - -
    frameTypes[0x3] = 'RST_STREAM';
    -
    -frameFlags.RST_STREAM = [];
    -
    -typeSpecificAttributes.RST_STREAM = ['error'];
    - -
  • - - -
  • -
    - -
    - -
    -
     0                   1                   2                   3
    - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    -|                         Error Code (32)                       |
    -+---------------------------------------------------------------+
    -

    The RST_STREAM frame contains a single unsigned, 32-bit integer identifying the error -code (see Error Codes). The error code indicates why the stream is being terminated.

    - -
    - -
    Serializer.RST_STREAM = function writeRstStream(frame, buffers) {
    -  var buffer = new Buffer(4);
    -  var code = errorCodes.indexOf(frame.error);
    -  assert((0 <= code) && (code <= 0xffffffff), code);
    -  buffer.writeUInt32BE(code, 0);
    -  buffers.push(buffer);
    -};
    -
    -Deserializer.RST_STREAM = function readRstStream(buffer, frame) {
    -  frame.error = errorCodes[buffer.readUInt32BE(0)];
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    SETTINGS

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The SETTINGS frame (type=0x4) conveys configuration parameters that affect how endpoints -communicate.

    -

    The SETTINGS frame does not define any flags.

    - -
    - -
    frameTypes[0x4] = 'SETTINGS';
    -
    -frameFlags.SETTINGS = [];
    -
    -typeSpecificAttributes.SETTINGS = ['settings'];
    - -
  • - - -
  • -
    - -
    - -
    -

    The payload of a SETTINGS frame consists of zero or more settings. Each setting consists of an -8-bit reserved field, an unsigned 24-bit setting identifier, and an unsigned 32-bit value.

    -
     0                   1                   2                   3
    - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    -|  Reserved(8)  |             Setting Identifier (24)           |
    -+---------------+-----------------------------------------------+
    -|                        Value (32)                             |
    -+---------------------------------------------------------------+
    -

    Each setting in a SETTINGS frame replaces the existing value for that setting. Settings are -processed in the order in which they appear, and a receiver of a SETTINGS frame does not need to -maintain any state other than the current value of settings. Therefore, the value of a setting -is the last value that is seen by a receiver. This permits the inclusion of the same settings -multiple times in the same SETTINGS frame, though doing so does nothing other than waste -connection capacity.

    - -
    - -
    Serializer.SETTINGS = function writeSettings(frame, buffers) {
    -  var settings = [], settingsLeft = Object.keys(frame.settings);
    -  definedSettings.forEach(function(setting, id) {
    -    if (setting.name in frame.settings) {
    -      settingsLeft.splice(settingsLeft.indexOf(setting.name), 1);
    -      var value = frame.settings[setting.name];
    -      settings.push({ id: id, value: setting.flag ? Boolean(value) : value });
    -    }
    -  });
    -  assert(settingsLeft.length === 0, 'Unknown settings: ' + settingsLeft.join(', '));
    -
    -  var buffer = new Buffer(settings.length * 8);
    -  for (var i = 0; i < settings.length; i++) {
    -    buffer.writeUInt32BE(settings[i].id & 0xffffff, i*8);
    -    buffer.writeUInt32BE(settings[i].value, i*8 + 4);
    -  }
    -
    -  buffers.push(buffer);
    -};
    -
    -Deserializer.SETTINGS = function readSettings(buffer, frame) {
    -  frame.settings = {};
    -
    -  if (buffer.length % 8 !== 0) {
    -    return 'Invalid SETTINGS frame';
    -  }
    -  for (var i = 0; i < buffer.length / 8; i++) {
    -    var id = buffer.readUInt32BE(i*8) & 0xffffff;
    -    var setting = definedSettings[id];
    -    if (setting) {
    -      var value = buffer.readUInt32BE(i*8 + 4);
    -      frame.settings[setting.name] = setting.flag ? Boolean(value & 0x1) : value;
    -    } else {
    -      /* Unknown setting, ignoring */
    -    }
    -  }
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    The following settings are defined:

    - -
    - -
    var definedSettings = [];
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • SETTINGS_MAX_CONCURRENT_STREAMS (4): -indicates the maximum number of concurrent streams that the sender will allow.
    • -
    - -
    - -
    definedSettings[4] = { name: 'SETTINGS_MAX_CONCURRENT_STREAMS', flag: false };
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • SETTINGS_INITIAL_WINDOW_SIZE (7): -indicates the sender's initial stream window size (in bytes) for new streams.
    • -
    - -
    - -
    definedSettings[7] = { name: 'SETTINGS_INITIAL_WINDOW_SIZE', flag: false };
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • SETTINGS_FLOW_CONTROL_OPTIONS (10): -indicates that streams directed to the sender will not be subject to flow control. The least -significant bit (0x1) is set to indicate that new streams are not flow controlled. All other -bits are reserved.
    • -
    - -
    - -
    definedSettings[10] = { name: 'SETTINGS_FLOW_CONTROL_OPTIONS', flag: true };
    - -
  • - - -
  • -
    - -
    - -
    -

    PUSH_PROMISE

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The PUSH_PROMISE frame (type=0x5) is used to notify the peer endpoint in advance of streams the -sender intends to initiate.

    -

    The PUSH_PROMISE frame defines the following flags:

    -
      -
    • END_PUSH_PROMISE (0x1): -The END_PUSH_PROMISE bit indicates that this frame contains the entire payload necessary to -provide a complete set of headers.
    • -
    - -
    - -
    frameTypes[0x5] = 'PUSH_PROMISE';
    -
    -frameFlags.PUSH_PROMISE = ['END_PUSH_PROMISE'];
    -
    -typeSpecificAttributes.PUSH_PROMISE = ['promised_stream', 'headers', 'data'];
    - -
  • - - -
  • -
    - -
    - -
    -
     0                   1                   2                   3
    - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    -|X|                Promised-Stream-ID (31)                      |
    -+-+-------------------------------------------------------------+
    -|                    Header Block (*)                         ...
    -+---------------------------------------------------------------+
    -

    The PUSH_PROMISE frame includes the unsigned 31-bit identifier of -the stream the endpoint plans to create along with a minimal set of headers that provide -additional context for the stream.

    - -
    - -
    Serializer.PUSH_PROMISE = function writePushPromise(frame, buffers) {
    -  var buffer = new Buffer(4);
    -
    -  var promised_stream = frame.promised_stream;
    -  assert((0 <= promised_stream) && (promised_stream <= 0x7fffffff), promised_stream);
    -  buffer.writeUInt32BE(promised_stream, 0);
    -
    -  buffers.push(buffer);
    -  buffers.push(frame.data);
    -};
    -
    -Deserializer.PUSH_PROMISE = function readPushPromise(buffer, frame) {
    -  frame.promised_stream = buffer.readUInt32BE(0) & 0x7fffffff;
    -  frame.data = buffer.slice(4);
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    PING

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The PING frame (type=0x6) is a mechanism for measuring a minimal round-trip time from the -sender, as well as determining whether an idle connection is still functional.

    -

    The PING frame defines one type-specific flag:

    -
      -
    • PONG (0x2): -Bit 2 being set indicates that this PING frame is a PING response.
    • -
    - -
    - -
    frameTypes[0x6] = 'PING';
    -
    -frameFlags.PING = ['PONG'];
    -
    -typeSpecificAttributes.PING = ['data'];
    - -
  • - - -
  • -
    - -
    - -
    -

    In addition to the frame header, PING frames MUST contain 8 additional octets of opaque data.

    - -
    - -
    Serializer.PING = function writePing(frame, buffers) {
    -  buffers.push(frame.data);
    -};
    -
    -Deserializer.PING = function readPing(buffer, frame) {
    -  if (buffer.length !== 8) {
    -    return 'Invalid size PING frame';
    -  }
    -  frame.data = buffer;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    GOAWAY

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The GOAWAY frame (type=0x7) informs the remote peer to stop creating streams on this connection.

    -

    The GOAWAY frame does not define any flags.

    - -
    - -
    frameTypes[0x7] = 'GOAWAY';
    -
    -frameFlags.GOAWAY = [];
    -
    -typeSpecificAttributes.GOAWAY = ['last_stream', 'error'];
    - -
  • - - -
  • -
    - -
    - -
    -
     0                   1                   2                   3
    - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
    -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
    -|X|                  Last-Stream-ID (31)                        |
    -+-+-------------------------------------------------------------+
    -|                      Error Code (32)                          |
    -+---------------------------------------------------------------+
    -

    The last stream identifier in the GOAWAY frame contains the highest numbered stream identifier -for which the sender of the GOAWAY frame has received frames on and might have taken some action -on.

    -

    The GOAWAY frame also contains a 32-bit error code (see Error Codes) that contains the reason for -closing the connection.

    - -
    - -
    Serializer.GOAWAY = function writeGoaway(frame, buffers) {
    -  var buffer = new Buffer(8);
    -
    -  var last_stream = frame.last_stream;
    -  assert((0 <= last_stream) && (last_stream <= 0x7fffffff), last_stream);
    -  buffer.writeUInt32BE(last_stream, 0);
    -
    -  var code = errorCodes.indexOf(frame.error);
    -  assert((0 <= code) && (code <= 0xffffffff), code);
    -  buffer.writeUInt32BE(code, 4);
    -
    -  buffers.push(buffer);
    -};
    -
    -Deserializer.GOAWAY = function readGoaway(buffer, frame) {
    -  frame.last_stream = buffer.readUInt32BE(0) & 0x7fffffff;
    -  frame.error = errorCodes[buffer.readUInt32BE(4)];
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    WINDOW_UPDATE

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The WINDOW_UPDATE frame (type=0x9) is used to implement flow control.

    -

    The WINDOW_UPDATE frame does not define any flags.

    - -
    - -
    frameTypes[0x9] = 'WINDOW_UPDATE';
    -
    -frameFlags.WINDOW_UPDATE = [];
    -
    -typeSpecificAttributes.WINDOW_UPDATE = ['window_size'];
    - -
  • - - -
  • -
    - -
    - -
    -

    The payload of a WINDOW_UPDATE frame is a 32-bit value indicating the additional number of bytes -that the sender can transmit in addition to the existing flow control window. The legal range -for this field is 1 to 2^31 - 1 (0x7fffffff) bytes; the most significant bit of this value is -reserved.

    - -
    - -
    Serializer.WINDOW_UPDATE = function writeWindowUpdate(frame, buffers) {
    -  var buffer = new Buffer(4);
    -
    -  var window_size = frame.window_size;
    -  assert((0 <= window_size) && (window_size <= 0x7fffffff), window_size);
    -  buffer.writeUInt32BE(window_size, 0);
    -
    -  buffers.push(buffer);
    -};
    -
    -Deserializer.WINDOW_UPDATE = function readWindowUpdate(buffer, frame) {
    -  frame.window_size = buffer.readUInt32BE(0) & 0x7fffffff;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    CONTINUATION

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    The CONTINUATION frame (type=0xA) is used to continue a sequence of header block fragments.

    -

    The CONTINUATION frame defines the following flags:

    -
      -
    • END_STREAM (0x1): -Bit 1 being set indicates that this frame is the last that the endpoint will send for the -identified stream.
    • -
    • RESERVED (0x2): -Bit 2 is reserved for future use.
    • -
    • END_HEADERS (0x4): -The END_HEADERS bit indicates that this frame ends the sequence of header block fragments -necessary to provide a complete set of headers.
    • -
    - -
    - -
    frameTypes[0xA] = 'CONTINUATION';
    -
    -frameFlags.CONTINUATION = ['END_STREAM', 'RESERVED', 'END_HEADERS'];
    -
    -typeSpecificAttributes.CONTINUATION = ['headers', 'data'];
    -
    -Serializer.CONTINUATION = function writeContinuation(frame, buffers) {
    -  buffers.push(frame.data);
    -};
    -
    -Deserializer.CONTINUATION = function readContinuation(buffer, frame) {
    -  frame.data = buffer;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -

    Error Codes

    - -
    - -
  • - - -
  • -
    - -
    - -
    - -
    - -
    var errorCodes = [
    -  'NO_ERROR',
    -  'PROTOCOL_ERROR',
    -  'INTERNAL_ERROR',
    -  'FLOW_CONTROL_ERROR',
    -  ,
    -  'STREAM_CLOSED',
    -  'FRAME_TOO_LARGE',
    -  'REFUSED_STREAM',
    -  'CANCEL',
    -  'COMPRESSION_ERROR'
    -];
    - -
  • - - -
  • -
    - -
    - -
    -

    Logging

    - -
    - -
  • - - -
  • -
    - -
    - -
    -

    Bunyan serializers to improve logging output -for debug messages emitted in this component.

    - -
    - -
    exports.serializers = {};
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • frame serializer: it transforms data attributes from Buffers to hex strings and filters out -flags that are not present.
    • -
    - -
    - -
    var frameCounter = 0;
    -exports.serializers.frame = function(frame) {
    -  if (!frame) {
    -    return null;
    -  }
    -
    -  if ('id' in frame) {
    -    return frame.id;
    -  }
    -
    -  frame.id = frameCounter;
    -  frameCounter += 1;
    -
    -  var logEntry = { id: frame.id };
    -  genericAttributes.concat(typeSpecificAttributes[frame.type]).forEach(function(name) {
    -    logEntry[name] = frame[name];
    -  });
    -
    -  if (frame.data instanceof Buffer) {
    -    if (logEntry.data.length > 50) {
    -      logEntry.data = frame.data.slice(0, 47).toString('hex') + '...';
    -    } else {
    -      logEntry.data = frame.data.toString('hex');
    -    }
    -
    -    if (!('length' in logEntry)) {
    -      logEntry.length = frame.data.length;
    -    }
    -  }
    -
    -  if (frame.promised_stream instanceof Object) {
    -    logEntry.promised_stream = 'stream-' + frame.promised_stream.id;
    -  }
    -
    -  logEntry.flags = Object.keys(frame.flags || {}).filter(function(name) {
    -    return frame.flags[name] === true;
    -  });
    -
    -  return logEntry;
    -};
    - -
  • - - -
  • -
    - -
    - -
    -
      -
    • data serializer: it simply transforms a buffer to a hex string.
    • -
    - -
    - -
    exports.serializers.data = function(data) {
    -  return data.toString('hex');
    -};
    - -
  • - -
-
- - diff --git a/doc/http.html b/doc/http.html index b592aaee..29456c79 100644 --- a/doc/http.html +++ b/doc/http.html @@ -16,48 +16,21 @@ Jump To … +
-
- - - - compressor.js - - + @@ -69,10 +42,10 @@
  • -
    +
    -

    Public API

    +

    Public API

    @@ -85,45 +58,55 @@

    Public API

    + +
    + +
  • + + +
  • +
    + +
    + +

    The main governing power behind the http2 API design is that it should look very similar to the existing node.js [HTTPS API][1] (which is, in turn, almost identical to the [HTTP API][2]). The additional features of HTTP/2 are exposed as extensions to this API. Furthermore, node-http2 should fall back to using HTTP/1.1 if needed. Compatibility with undocumented or deprecated elements of the node.js HTTP/HTTPS API is a non-goal.

    -

    Additional and modified API elements

    +

    Additional and modified API elements

  • -
  • +
  • - +
    • Class: http2.Endpoint: an API for using the raw HTTP/2 framing layer. For documentation -see the lib/endpoint.js file.

      +see protocol/endpoint.js.

    • Class: http2.Server

        -
      • Event: 'connection' (socket, [endpoint]): there's a second argument if the negotiation of -HTTP/2 was successful: the reference to the Endpoint object tied to the +
      • Event: ‘connection’ (socket, [endpoint]): there’s a second argument if the negotiation of +HTTP/2 was successful: the reference to the Endpoint object tied to the socket.
    • http2.createServer(options, [requestListener]): additional option:

      • log: an optional bunyan logger object
      • -
      • plain: if true, the server will accept HTTP/2 connections over plain TCP instead of -TLS
    • Class: http2.ServerResponse

        -
      • response.push(options): initiates a server push. options describes the 'imaginary' +
      • response.push(options): initiates a server push. options describes the ‘imaginary’ request to which the push stream is a response; the possible options are identical to the ones accepted by http2.request. Returns a ServerResponse object that can be used to send the response headers and content.
      • @@ -136,21 +119,25 @@

        Additional and modified API elements

    • agent.sockets: only contains TCP sockets that corresponds to HTTP/1 requests.
    • -
    • agent.endpoints: contains Endpoint objects for HTTP/2 connections.
    • +
    • agent.endpoints: contains Endpoint objects for HTTP/2 connections.
  • -
  • http2.request(options, [callback]): additional option:

    +
  • http2.request(options, [callback]):

      -
    • plain: if true, the client will not try to build a TLS tunnel, instead it will use -the raw TCP stream for HTTP/2
    • +
    • similar to http.request
    • +
    +
  • +
  • http2.get(options, [callback]):

    +
      +
    • similar to http.get
  • Class: http2.ClientRequest

      -
    • Event: 'socket' (socket): in case of an HTTP/2 incoming message, socket is a reference -to the associated HTTP/2 Stream object (and not to the TCP socket).
    • -
    • Event: 'push' (promise): signals the intention of a server push associated to this -request. promise is an IncomingPromise. If there's no listener for this event, the server +
    • Event: ‘socket’ (socket): in case of an HTTP/2 incoming message, socket is a reference +to the associated HTTP/2 Stream object (and not to the TCP socket).
    • +
    • Event: ‘push’ (promise): signals the intention of a server push associated to this +request. promise is an IncomingPromise. If there’s no listener for this event, the server push is cancelled.
    • request.setPriority(priority): assign a priority to this request. priority is a number between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.
    • @@ -160,8 +147,8 @@

      Additional and modified API elements

      • has two subclasses for easier interface description: IncomingRequest and IncomingResponse
      • -
      • message.socket: in case of an HTTP/2 incoming message, it's a reference to the associated -HTTP/2 Stream object (and not to the TCP socket).
      • +
      • message.socket: in case of an HTTP/2 incoming message, it’s a reference to the associated +HTTP/2 Stream object (and not to the TCP socket).
    • Class: http2.IncomingRequest (IncomingMessage)

      @@ -176,11 +163,11 @@

      Additional and modified API elements

    • Class: http2.IncomingPromise (IncomingRequest)

        -
      • contains the metadata of the 'imaginary' request to which the server push is an answer.
      • -
      • Event: 'response' (response): signals the arrival of the actual push stream. response +
      • contains the metadata of the ‘imaginary’ request to which the server push is an answer.
      • +
      • Event: ‘response’ (response): signals the arrival of the actual push stream. response is an IncomingResponse.
      • -
      • Event: 'push' (promise): signals the intention of a server push associated to this -request. promise is an IncomingPromise. If there's no listener for this event, the server +
      • Event: ‘push’ (promise): signals the intention of a server push associated to this +request. promise is an IncomingPromise. If there’s no listener for this event, the server push is cancelled.
      • promise.cancel(): cancels the promised server push.
      • promise.setPriority(priority): assign a priority to this push stream. priority is a @@ -188,18 +175,18 @@

        Additional and modified API elements

    -

    API elements not yet implemented

    +

    API elements not yet implemented

  • -
  • +
  • - +
    • Class: http2.Server
        @@ -207,55 +194,55 @@

        API elements not yet implemented

    -

    API elements that are not applicable to HTTP/2

    +

    API elements that are not applicable to HTTP/2

  • -
  • +
  • - +

    The reason may be deprecation of certain HTTP/1.1 features, or that some API elements simply -don't make sense when using HTTP/2. These will not be present when a request is done with HTTP/2, +don’t make sense when using HTTP/2. These will not be present when a request is done with HTTP/2, but will function normally when falling back to using HTTP/1.1.

    • Class: http2.Server

        -
      • Event: 'checkContinue': not in the spec, yet (see http-spec#18)
      • -
      • Event: 'upgrade': upgrade is deprecated in HTTP/2
      • -
      • Event: 'timeout': HTTP/2 sockets won't timeout because of application level keepalive +
      • Event: ‘checkContinue’: not in the spec
      • +
      • Event: ‘upgrade’: upgrade is deprecated in HTTP/2
      • +
      • Event: ‘timeout’: HTTP/2 sockets won’t timeout because of application level keepalive (PING frames)
      • -
      • Event: 'connect': not in the spec, yet (see http-spec#230)
      • +
      • Event: ‘connect’: not yet supported
      • server.setTimeout(msecs, [callback])
      • server.timeout
    • Class: http2.ServerResponse

        -
      • Event: 'close'
      • -
      • Event: 'timeout'
      • +
      • Event: ‘close’
      • +
      • Event: ‘timeout’
      • response.writeContinue()
      • response.writeHead(statusCode, [reasonPhrase], [headers]): reasonPhrase will always be -ignored since it's not supported in HTTP/2
      • +ignored since it’s not supported in HTTP/2
      • response.setTimeout(timeout, [callback])
    • Class: http2.Agent

        -
      • agent.maxSockets: only affects HTTP/1 connection pool. When using HTTP/2, there's always +
      • agent.maxSockets: only affects HTTP/1 connection pool. When using HTTP/2, there’s always one connection per host.
    • Class: http2.ClientRequest

        -
      • Event: 'upgrade'
      • -
      • Event: 'connect'
      • -
      • Event: 'continue'
      • +
      • Event: ‘upgrade’
      • +
      • Event: ‘connect’
      • +
      • Event: ‘continue’
      • request.setTimeout(timeout, [callback])
      • request.setNoDelay([noDelay])
      • request.setSocketKeepAlive([enable], [initialDelay])
      • @@ -263,128 +250,174 @@

        API elements that are not applicable to HTTP/2

      • Class: http2.IncomingMessage

          -
        • Event: 'close'
        • +
        • Event: ‘close’
        • message.setTimeout(timeout, [callback])
      -

      Common server and client side code

  • -
  • +
  • - + +
    +

    Common server and client side code

    + +
    + +
  • + + +
  • +
    + +
    +
    -
    var net = require('net');
    -var url = require('url');
    -var util = require('util');
    -var EventEmitter = require('events').EventEmitter;
    -var PassThrough = require('stream').PassThrough;
    -var Readable = require('stream').Readable;
    -var Writable = require('stream').Writable;
    -var Endpoint = require('./endpoint').Endpoint;
    -var http = require('http');
    -var https = require('https');
    +            
    +var net = require('net');
    +var url = require('url');
    +var util = require('util');
    +var EventEmitter = require('events').EventEmitter;
    +var PassThrough = require('stream').PassThrough;
    +var Readable = require('stream').Readable;
    +var Writable = require('stream').Writable;
    +var protocol = require('./protocol');
    +var Endpoint = protocol.Endpoint;
    +var http = require('http');
    +var https = require('https');
     
     exports.STATUS_CODES = http.STATUS_CODES;
     exports.IncomingMessage = IncomingMessage;
     exports.OutgoingMessage = OutgoingMessage;
    -exports.Endpoint = Endpoint;
    -
    -var deprecatedHeaders = [
    -  'connection',
    -  'host',
    -  'keep-alive',
    -  'proxy-connection',
    -  'te',
    -  'transfer-encoding',
    -  'upgrade'
    +exports.protocol = protocol;
    +
    +var deprecatedHeaders = [
    +  'connection',
    +  'host',
    +  'keep-alive',
    +  'proxy-connection',
    +  'transfer-encoding',
    +  'upgrade'
     ];
  • -
  • +
  • - +
    -

    The implemented version of the HTTP/2 specification is draft 04.

    +

    When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback

    -
    var implementedVersion = 'HTTP-draft-06/2.0';
    +
    var supportedProtocols = [protocol.VERSION, 'http/1.1', 'http/1.0'];
  • -
  • +
  • - +
    -

    When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback

    - -
    - -
    var supportedProtocols = [implementedVersion, 'http/1.1', 'http/1.0'];
    +

    Ciphersuite list based on the recommendations of https://wiki.mozilla.org/Security/Server_Side_TLS +The only modification is that kEDH+AESGCM were placed after DHE and ECDHE suites

    + +
  • + +
    var cipherSuites = [
    +  'ECDHE-RSA-AES128-GCM-SHA256',
    +  'ECDHE-ECDSA-AES128-GCM-SHA256',
    +  'ECDHE-RSA-AES256-GCM-SHA384',
    +  'ECDHE-ECDSA-AES256-GCM-SHA384',
    +  'DHE-RSA-AES128-GCM-SHA256',
    +  'DHE-DSS-AES128-GCM-SHA256',
    +  'ECDHE-RSA-AES128-SHA256',
    +  'ECDHE-ECDSA-AES128-SHA256',
    +  'ECDHE-RSA-AES128-SHA',
    +  'ECDHE-ECDSA-AES128-SHA',
    +  'ECDHE-RSA-AES256-SHA384',
    +  'ECDHE-ECDSA-AES256-SHA384',
    +  'ECDHE-RSA-AES256-SHA',
    +  'ECDHE-ECDSA-AES256-SHA',
    +  'DHE-RSA-AES128-SHA256',
    +  'DHE-RSA-AES128-SHA',
    +  'DHE-DSS-AES128-SHA256',
    +  'DHE-RSA-AES256-SHA256',
    +  'DHE-DSS-AES256-SHA',
    +  'DHE-RSA-AES256-SHA',
    +  'kEDH+AESGCM',
    +  'AES128-GCM-SHA256',
    +  'AES256-GCM-SHA384',
    +  'ECDHE-RSA-RC4-SHA',
    +  'ECDHE-ECDSA-RC4-SHA',
    +  'AES128',
    +  'AES256',
    +  'RC4-SHA',
    +  'HIGH',
    +  '!aNULL',
    +  '!eNULL',
    +  '!EXPORT',
    +  '!DES',
    +  '!3DES',
    +  '!MD5',
    +  '!PSK'
    +].join(':');
    -
  • +
  • - +
    -

    Using ALPN or NPN depending on node.js support (preferring ALPN)

    +

    Logging

    -
    var negotiationMethod = process.features.tls_alpn ? 'ALPN' : 'NPN';
    -var protocolList = process.features.tls_alpn ? 'ALPNProtocols' : 'NPNProtocols';
    -var negotiatedProtocol = process.features.tls_alpn ? 'alpnProtocol' : 'npnProtocol';
    -
  • -
  • +
  • -
    - +
    +
    -

    Logging

    - +
  • -
  • +
  • - +

    Logger shim, used when no logger is provided by the user.

    -
    function noop() {}
    -var defaultLogger = {
    +            
    function noop() {}
    +var defaultLogger = {
       fatal: noop,
       error: noop,
       warn : noop,
    @@ -392,84 +425,81 @@ 

    Logging

    debug: noop, trace: noop, - child: function() { return this; } + child: function() { return this; } };
  • -
  • +
  • - +

    Bunyan serializers exported by submodules that are worth adding when creating a logger.

    -
    exports.serializers = {};
    -var modules = ['./framer', './compressor', './flow', './connection', './stream', './endpoint'];
    -modules.forEach(function(module) {
    -  util._extend(exports.serializers, require(module).serializers);
    -});
    +
    exports.serializers = protocol.serializers;
  • -
  • +
  • -
    - +
    +
    -

    IncomingMessage class

    +

    IncomingMessage class

  • -
  • +
  • - +
    -
    function IncomingMessage(stream) {
    +
    +function IncomingMessage(stream) {
  • -
  • +
  • - +
      -
    • This is basically a read-only wrapper for the Stream class.
    • +
    • This is basically a read-only wrapper for the Stream class.
    -
      PassThrough.call(this);
    -  stream.pipe(this);
    -  this.socket = this.stream = stream;
    +            
      PassThrough.call(this);
    +  stream.pipe(this);
    +  this.socket = this.stream = stream;
     
    -  this._log = stream._log.child({ component: 'http' });
    + this._log = stream._log.child({ component: 'http' });
  • -
  • +
  • - +
    • HTTP/2.0 does not define a way to carry the version identifier that is included in the @@ -478,18 +508,18 @@

      IncomingMessage class

    -
      this.httpVersion = '2.0';
    -  this.httpVersionMajor = 2;
    -  this.httpVersionMinor = 0;
    +
      this.httpVersion = '2.0';
    +  this.httpVersionMajor = 2;
    +  this.httpVersionMinor = 0;
  • -
  • +
  • - +
    • this.headers will store the regular headers (and none of the special colon headers)
    • @@ -497,18 +527,18 @@

      IncomingMessage class

    -
      this.headers = {};
    -  this.trailers = undefined;
    -  this._lastHeadersSeen = undefined;
    +
      this.headers = {};
    +  this.trailers = undefined;
    +  this._lastHeadersSeen = undefined;
  • -
  • +
  • - +
    • Other metadata is filled in when the headers arrive.
    • @@ -516,64 +546,56 @@

      IncomingMessage class

    -
      stream.once('headers', this._onHeaders.bind(this));
    -  stream.once('end', this._onEnd.bind(this));
    +            
      stream.once('headers', this._onHeaders.bind(this));
    +  stream.once('end', this._onEnd.bind(this));
     }
    -IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } });
    +IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } });
  • -
  • +
  • - +
    -

    Request Header Fields -* headers argument: HTTP/2.0 request and response header fields carry information as a series - of key-value pairs. This includes the target URI for the request, the status code for the - response, as well as HTTP header fields.

    +

    Request Header Fields

    +
      +
    • headers argument: HTTP/2.0 request and response header fields carry information as a series +of key-value pairs. This includes the target URI for the request, the status code for the +response, as well as HTTP header fields.
    • +
    -
    IncomingMessage.prototype._onHeaders = function _onHeaders(headers) {
    +
    IncomingMessage.prototype._onHeaders = function _onHeaders(headers) {
  • -
  • +
  • - +
      -
    • An HTTP/2.0 request or response MUST NOT include any of the following header fields: -Connection, Host, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, and Upgrade. A server -MUST treat the presence of any of these header fields as a stream error of type -PROTOCOL_ERROR.
    • +
    • Detects malformed headers
    -
      for (var i = 0; i < deprecatedHeaders.length; i++) {
    -    var key = deprecatedHeaders[i];
    -    if (key in headers) {
    -      this._log.error({ key: key, value: headers[key] }, 'Deprecated header found');
    -      this.stream.emit('error', 'PROTOCOL_ERROR');
    -      return;
    -    }
    -  }
    +
      this._validateHeaders(headers);
  • -
  • +
  • - +
    • Store the regular headers in this.headers
    • @@ -581,150 +603,232 @@

      IncomingMessage class

    -
      for (var name in headers) {
    -    if (name[0] !== ':') {
    -      this.headers[name] = headers[name];
    +            
      for (var name in headers) {
    +    if (name[0] !== ':') {
    +      if (name === 'set-cookie' && !Array.isArray(headers[name])) {
    +        this.headers[name] = [headers[name]];
    +      } else {
    +        this.headers[name] = headers[name];
    +      }
         }
       }
  • -
  • +
  • - +
      -
    • The last header block, if it's not the first, will represent the trailers
    • +
    • The last header block, if it’s not the first, will represent the trailers
    -
      var self = this;
    -  this.stream.on('headers', function(headers) {
    +            
      var self = this;
    +  this.stream.on('headers', function(headers) {
         self._lastHeadersSeen = headers;
       });
     };
     
    -IncomingMessage.prototype._onEnd = function _onEnd() {
    -  this.trailers = this._lastHeadersSeen;
    +IncomingMessage.prototype._onEnd = function _onEnd() {
    +  this.trailers = this._lastHeadersSeen;
     };
     
     IncomingMessage.prototype.setTimeout = noop;
     
    -IncomingMessage.prototype._checkSpecialHeader = function _checkSpecialHeader(key, value) {
    -  if ((typeof value !== 'string') || (value.length === 0)) {
    -    this._log.error({ key: key, value: value }, 'Invalid or missing special header field');
    -    this.stream.emit('error', 'PROTOCOL_ERROR');
    +IncomingMessage.prototype._checkSpecialHeader = function _checkSpecialHeader(key, value) {
    +  if ((typeof value !== 'string') || (value.length === 0)) {
    +    this._log.error({ key: key, value: value }, 'Invalid or missing special header field');
    +    this.stream.reset('PROTOCOL_ERROR');
       }
     
    -  return value;
    -}
    -;
    + return value; +}; + +IncomingMessage.prototype._validateHeaders = function _validateHeaders(headers) {
  • -
  • +
  • -
    - +
    +
    -

    OutgoingMessage class

    +
      +
    • An HTTP/2.0 request or response MUST NOT include any of the following header fields: +Connection, Host, Keep-Alive, Proxy-Connection, Transfer-Encoding, and Upgrade. A server +MUST treat the presence of any of these header fields as a stream error of type +PROTOCOL_ERROR. +If the TE header is present, it’s only valid value is ‘trailers’
    • +
    +
      for (var i = 0; i < deprecatedHeaders.length; i++) {
    +    var key = deprecatedHeaders[i];
    +    if (key in headers || (key === 'te' && headers[key] !== 'trailers')) {
    +      this._log.error({ key: key, value: headers[key] }, 'Deprecated header found');
    +      this.stream.reset('PROTOCOL_ERROR');
    +      return;
    +    }
    +  }
    +
    +  for (var headerName in headers) {
    +
  • -
  • +
  • - +
    +
      +
    • Empty header name field is malformed
    • +
    + +
    + +
        if (headerName.length <= 1) {
    +      this.stream.reset('PROTOCOL_ERROR');
    +      return;
    +    }
    + +
  • + + +
  • +
    +
    + +
    +
      +
    • A request or response containing uppercase header name field names MUST be +treated as malformed (Section 8.1.3.5). Implementations that detect malformed +requests or responses need to ensure that the stream ends.
    • +
    +
    -
    function OutgoingMessage() {
    +
        if(/[A-Z]/.test(headerName)) {
    +      this.stream.reset('PROTOCOL_ERROR');
    +      return;
    +    }
    +  }
    +};
  • -
  • +
  • - + +
    +

    OutgoingMessage class

    + +
    + +
  • + + +
  • +
    + +
    + +
    + +
    + +
    +function OutgoingMessage() {
    + +
  • + + +
  • +
    + +
    +
      -
    • This is basically a read-only wrapper for the Stream class.
    • +
    • This is basically a read-only wrapper for the Stream class.
    -
      Writable.call(this);
    +            
      Writable.call(this);
     
    -  this._headers = {};
    -  this._trailers = undefined;
    -  this.headersSent = false;
    +  this._headers = {};
    +  this._trailers = undefined;
    +  this.headersSent = false;
    +  this.finished = false;
     
    -  this.on('finish', this._finish);
    +  this.on('finish', this._finish);
     }
    -OutgoingMessage.prototype = Object.create(Writable.prototype, { constructor: { value: OutgoingMessage } });
    +OutgoingMessage.prototype = Object.create(Writable.prototype, { constructor: { value: OutgoingMessage } });
     
    -OutgoingMessage.prototype._write = function _write(chunk, encoding, callback) {
    -  if (this.stream) {
    -    this.stream.write(chunk, encoding, callback);
    -  } else {
    -    this.once('socket', this._write.bind(this, chunk, encoding, callback));
    +OutgoingMessage.prototype._write = function _write(chunk, encoding, callback) {
    +  if (this.stream) {
    +    this.stream.write(chunk, encoding, callback);
    +  } else {
    +    this.once('socket', this._write.bind(this, chunk, encoding, callback));
       }
     };
     
    -OutgoingMessage.prototype._finish = function _finish() {
    -  if (this.stream) {
    -    if (this._trailers) {
    -      if (this.request) {
    -        this.request.addTrailers(this._trailers);
    -      } else {
    -        this.stream.headers(this._trailers);
    +OutgoingMessage.prototype._finish = function _finish() {
    +  if (this.stream) {
    +    if (this._trailers) {
    +      if (this.request) {
    +        this.request.addTrailers(this._trailers);
    +      } else {
    +        this.stream.headers(this._trailers);
           }
         }
    -    this.stream.end();
    -  } else {
    -    this.once('socket', this._finish.bind(this));
    +    this.finished = true;
    +    this.stream.end();
    +  } else {
    +    this.once('socket', this._finish.bind(this));
       }
     };
     
    -OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
    -  if (this.headersSent) {
    -    throw new Error('Can\'t set headers after they are sent.');
    -  } else {
    +OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
    +  if (this.headersSent) {
    +    return this.emit('error', new Error('Can\'t set headers after they are sent.'));
    +  } else {
         name = name.toLowerCase();
    -    if (deprecatedHeaders.indexOf(name) !== -1) {
    -      throw new Error('Cannot set deprecated header: ' + name);
    +    if (deprecatedHeaders.indexOf(name) !== -1) {
    +      return this.emit('error', new Error('Cannot set deprecated header: ' + name));
         }
    -    this._headers[name] = value;
    +    this._headers[name] = value;
       }
     };
     
    -OutgoingMessage.prototype.removeHeader = function removeHeader(name) {
    -  if (this.headersSent) {
    -    throw new Error('Can\'t remove headers after they are sent.');
    -  } else {
    -    delete this._headers[name.toLowerCase()];
    +OutgoingMessage.prototype.removeHeader = function removeHeader(name) {
    +  if (this.headersSent) {
    +    return this.emit('error', new Error('Can\'t remove headers after they are sent.'));
    +  } else {
    +    delete this._headers[name.toLowerCase()];
       }
     };
     
    -OutgoingMessage.prototype.getHeader = function getHeader(name) {
    -  return this._headers[name.toLowerCase()];
    +OutgoingMessage.prototype.getHeader = function getHeader(name) {
    +  return this._headers[name.toLowerCase()];
     };
     
    -OutgoingMessage.prototype.addTrailers = function addTrailers(trailers) {
    -  this._trailers = trailers;
    +OutgoingMessage.prototype.addTrailers = function addTrailers(trailers) {
    +  this._trailers = trailers;
     };
     
     OutgoingMessage.prototype.setTimeout = noop;
    @@ -734,357 +838,553 @@ 

    OutgoingMessage class

  • -
  • +
  • -
    - +
    +
    -

    Server side

    +

    Server side

  • -
  • +
  • - +
    -
    exports.createServer = createServer;
    +            
     exports.Server = Server;
     exports.IncomingRequest = IncomingRequest;
     exports.OutgoingResponse = OutgoingResponse;
    -exports.ServerResponse = OutgoingResponse; // for API compatibility
    +exports.ServerResponse = OutgoingResponse; // for API compatibility
  • -
  • +
  • -
    - +
    +
    -

    Server class

    +

    Forward events event on source to all listeners on target.

    +

    Note: The calling context is source.

    +
    function forwardEvent(event, source, target) {
    +  function forward() {
    +    var listeners = target.listeners(event);
    +
    +    var n = listeners.length;
    +
  • -
  • +
  • - + +
    +

    Special case for error event with no listeners.

    + +
    + +
        if (n === 0 && event === 'error') {
    +      var args = [event];
    +      args.push.apply(args, arguments);
    +
    +      target.emit.apply(target, args);
    +      return;
    +    }
    +
    +    for (var i = 0; i < n; ++i) {
    +      listeners[i].apply(source, arguments);
    +    }
    +  }
    +
    +  source.on(event, forward);
    + +
  • + + +
  • +
    + +
    + +
    +

    A reference to the function is necessary to be able to stop +forwarding.

    + +
    + +
      return forward;
    +}
    + +
  • + + +
  • +
    + +
    + +
    +

    Server class

    + +
    + +
  • + + +
  • +
    + +
    +
    -
    function Server(options) {
    -  options = options || {};
    +            
    +function Server(options) {
    +  options = util._extend({}, options);
     
    -  this._log = (options.log || defaultLogger).child({ component: 'http' });
    -  this._settings = options.settings;
    +  this._log = (options.log || defaultLogger).child({ component: 'http' });
    +  this._settings = options.settings;
     
    -  var start = this._start.bind(this);
    -  var fallback = this._fallback.bind(this);
    + var start = this._start.bind(this); + var fallback = this._fallback.bind(this);
  • -
  • +
  • - +

    HTTP2 over TLS (using NPN or ALPN)

    -
      if ((options.key && options.cert) || options.pfx) {
    -    this._log.info('Creating HTTP/2 server over TLS/' + negotiationMethod);
    -    this._mode = 'tls';
    -    options[protocolList] = supportedProtocols;
    -    this._server = https.createServer(options);
    -    this._originalSocketListeners = this._server.listeners('secureConnection');
    -    this._server.removeAllListeners('secureConnection');
    -    this._server.on('secureConnection', function(socket) {
    -      if (socket[negotiatedProtocol] === implementedVersion && socket.servername) {
    +            
      if ((options.key && options.cert) || options.pfx) {
    +    this._log.info('Creating HTTP/2 server over TLS');
    +    this._mode = 'tls';
    +    options.ALPNProtocols = supportedProtocols;
    +    options.NPNProtocols = supportedProtocols;
    +    options.ciphers = options.ciphers || cipherSuites;
    +    options.honorCipherOrder = (options.honorCipherOrder != false);
    +    this._server = https.createServer(options);
    +    this._originalSocketListeners = this._server.listeners('secureConnection');
    +    this._server.removeAllListeners('secureConnection');
    +    this._server.on('secureConnection', function(socket) {
    +      var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
    + +
  • + + +
  • +
    + +
    + +
    +

    It’s true that the client MUST use SNI, but if it doesn’t, we don’t care, don’t fall back to HTTP/1, +since if the ALPN negotiation is otherwise successful, the client thinks we speak HTTP/2 but we don’t.

    + +
    + +
          if (negotiatedProtocol === protocol.VERSION) {
             start(socket);
    -      } else {
    +      } else {
             fallback(socket);
           }
         });
    -    this._server.on('request', this.emit.bind(this, 'request'));
    +    this._server.on('request', this.emit.bind(this, 'request'));
    +
    +    forwardEvent('error', this._server, this);
    +    forwardEvent('listening', this._server, this);
       }
  • -
  • +
  • - +

    HTTP2 over plain TCP

    -
      else if (options.plain) {
    -    this._log.info('Creating HTTP/2 server over plain TCP');
    -    this._mode = 'plain';
    -    this._server = net.createServer(start);
    +            
      else if (options.plain) {
    +    this._log.info('Creating HTTP/2 server over plain TCP');
    +    this._mode = 'plain';
    +    this._server = net.createServer(start);
       }
  • -
  • +
  • - +

    HTTP/2 with HTTP/1.1 upgrade

    -
      else {
    -    this._log.error('Trying to create HTTP/2 server with Upgrade from HTTP/1.1');
    -    throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported. Please provide TLS keys.');
    +            
      else {
    +    this._log.error('Trying to create HTTP/2 server with Upgrade from HTTP/1.1');
    +    throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported. Please provide TLS keys.');
       }
     
    -  this._server.on('close', this.emit.bind(this, 'close'));
    +  this._server.on('close', this.emit.bind(this, 'close'));
     }
    -Server.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Server } });
    +Server.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Server } });
  • -
  • +
  • - +

    Starting HTTP/2

    -
    Server.prototype._start = function _start(socket) {
    -  var endpoint = new Endpoint(this._log, 'SERVER', this._settings);
    +            
    Server.prototype._start = function _start(socket) {
    +  var endpoint = new Endpoint(this._log, 'SERVER', this._settings);
     
    -  this._log.info({ e: endpoint,
    -                   client: socket.remoteAddress + ':' + socket.remotePort,
    +  this._log.info({ e: endpoint,
    +                   client: socket.remoteAddress + ':' + socket.remotePort,
                        SNI: socket.servername
    -                 }, 'New incoming HTTP/2 connection');
    +                 }, 'New incoming HTTP/2 connection');
     
       endpoint.pipe(socket).pipe(endpoint);
     
    -  var self = this;
    -  endpoint.on('stream', function _onStream(stream) {
    -    var response = new OutgoingResponse(stream);
    -    var request = new IncomingRequest(stream);
    +  var self = this;
    +  endpoint.on('stream', function _onStream(stream) {
    +    var response = new OutgoingResponse(stream);
    +    var request = new IncomingRequest(stream);
    + +
  • + + +
  • +
    + +
    + +
    +

    Some conformance to Node.js Https specs allows to distinguish clients:

    + +
    + +
        request.remoteAddress = socket.remoteAddress;
    +    request.remotePort = socket.remotePort;
    +    request.connection = request.socket = response.socket = socket;
     
    -    request.once('ready', self.emit.bind(self, 'request', request, response));
    +    request.once('ready', self.emit.bind(self, 'request', request, response));
       });
     
    -  endpoint.on('error', this.emit.bind(this, 'clientError'));
    +  endpoint.on('error', this.emit.bind(this, 'clientError'));
    +  socket.on('error', this.emit.bind(this, 'clientError'));
    +
    +  this.emit('connection', socket, endpoint);
    +};
    +
    +Server.prototype._fallback = function _fallback(socket) {
    +  var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
    +
    +  this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort,
    +                   protocol: negotiatedProtocol,
    +                   SNI: socket.servername
    +                 }, 'Falling back to simple HTTPS');
    +
    +  for (var i = 0; i < this._originalSocketListeners.length; i++) {
    +    this._originalSocketListeners[i].call(this._server, socket);
    +  }
    +
    +  this.emit('connection', socket);
    +};
    + +
  • + + +
  • +
    + +
    + +
    +

    There are 3 possible signatures of the listen function. Every arguments is forwarded to +the backing TCP or HTTPS server.

    + +
    + +
    Server.prototype.listen = function listen(port, hostname) {
    +  this._log.info({ on: ((typeof hostname === 'string') ? (hostname + ':' + port) : port) },
    +                 'Listening for incoming connections');
    +  this._server.listen.apply(this._server, arguments);
    +
    +  return this._server;
    +};
    +
    +Server.prototype.close = function close(callback) {
    +  this._log.info('Closing server');
    +  this._server.close(callback);
    +};
    +
    +Server.prototype.setTimeout = function setTimeout(timeout, callback) {
    +  if (this._mode === 'tls') {
    +    this._server.setTimeout(timeout, callback);
    +  }
    +};
    +
    +Object.defineProperty(Server.prototype, 'timeout', {
    +  get: function getTimeout() {
    +    if (this._mode === 'tls') {
    +      return this._server.timeout;
    +    } else {
    +      return undefined;
    +    }
    +  },
    +  set: function setTimeout(timeout) {
    +    if (this._mode === 'tls') {
    +      this._server.timeout = timeout;
    +    }
    +  }
    +});
    + +
  • + + +
  • +
    + +
    + +
    +

    Overriding EventEmitter‘s on(event, listener) method to forward certain subscriptions to +server.There are events on the http.Server class where it makes difference whether someone is +listening on the event or not. In these cases, we can not simply forward the events from the +server to this since that means a listener. Instead, we forward the subscriptions.

    + +
    + +
    Server.prototype.on = function on(event, listener) {
    +  if ((event === 'upgrade') || (event === 'timeout')) {
    +    return this._server.on(event, listener && listener.bind(this));
    +  } else {
    +    return EventEmitter.prototype.on.call(this, event, listener);
    +  }
    +};
    + +
  • + + +
  • +
    + +
    + +
    +

    addContext is used to add Server Name Indication contexts

    + +
    + +
    Server.prototype.addContext = function addContext(hostname, credentials) {
    +  if (this._mode === 'tls') {
    +    this._server.addContext(hostname, credentials);
    +  }
    +};
    +
    +Server.prototype.address = function address() {
    +  return this._server.address()
    +};
    +
    +function createServerRaw(options, requestListener) {
    +  if (typeof options === 'function') {
    +    requestListener = options;
    +    options = {};
    +  }
    +
    +  if (options.pfx || (options.key && options.cert)) {
    +    throw new Error('options.pfx, options.key, and options.cert are nonsensical!');
    +  }
    +
    +  options.plain = true;
    +  var server = new Server(options);
    +
    +  if (requestListener) {
    +    server.on('request', requestListener);
    +  }
    +
    +  return server;
    +}
     
    -  this.emit('connection', socket, endpoint);
    -};
    +function createServerTLS(options, requestListener) {
    +  if (typeof options === 'function') {
    +    throw new Error('options are required!');
    +  }
    +  if (!options.pfx && !(options.key && options.cert)) {
    +    throw new Error('options.pfx or options.key and options.cert are required!');
    +  }
    +  options.plain = false;
     
    -Server.prototype._fallback = function _fallback(socket) {
    -  this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort,
    -                   protocol: socket[negotiatedProtocol],
    -                   SNI: socket.servername
    -                 }, 'Falling back to simple HTTPS');
    +  var server = new Server(options);
     
    -  for (var i = 0; i < this._originalSocketListeners.length; i++) {
    -    this._originalSocketListeners[i].call(this._server, socket);
    +  if (requestListener) {
    +    server.on('request', requestListener);
       }
     
    -  this.emit('connection', socket);
    -};
    + return server; +}
  • -
  • +
  • - +
    -

    There are 3 possible signatures of the listen function. Every arguments is forwarded to -the backing TCP or HTTPS server.

    +

    Exposed main interfaces for HTTPS connections (the default)

    -
    Server.prototype.listen = function listen(port, hostname) {
    -  this._log.info({ on: ((typeof hostname === 'string') ? (hostname + ':' + port) : port) },
    -                 'Listening for incoming connections');
    -  this._server.listen.apply(this._server, arguments);
    -};
    -
    -Server.prototype.close = function close(callback) {
    -  this._log.info('Closing server');
    -  this._server.close(callback);
    -};
    -
    -Server.prototype.setTimeout = function setTimeout(timeout, callback) {
    -  if (this._mode === 'tls') {
    -    this._server.setTimeout(timeout, callback);
    -  }
    -};
    -
    -Object.defineProperty(Server.prototype, 'timeout', {
    -  get: function getTimeout() {
    -    if (this._mode === 'tls') {
    -      return this._server.timeout;
    -    } else {
    -      return undefined;
    -    }
    -  },
    -  set: function setTimeout(timeout) {
    -    if (this._mode === 'tls') {
    -      this._server.timeout = timeout;
    -    }
    -  }
    -});
    +
    exports.https = {};
    +exports.createServer = exports.https.createServer = createServerTLS;
    +exports.request = exports.https.request = requestTLS;
    +exports.get = exports.https.get = getTLS;
  • -
  • +
  • - +
    -

    Overriding EventEmitter's on(event, listener) method to forward certain subscriptions to -server.There are events on the http.Server class where it makes difference whether someone is -listening on the event or not. In these cases, we can not simply forward the events from the -server to this since that means a listener. Instead, we forward the subscriptions.

    +

    Exposed main interfaces for raw TCP connections (not recommended)

    -
    Server.prototype.on = function on(event, listener) {
    -  if ((event === 'upgrade') || (event === 'timeout')) {
    -    this._server.on(event, listener && listener.bind(this));
    -  } else {
    -    EventEmitter.prototype.on.call(this, event, listener);
    -  }
    -};
    +
    exports.raw = {};
    +exports.raw.createServer = createServerRaw;
    +exports.raw.request = requestRaw;
    +exports.raw.get = getRaw;
  • -
  • +
  • - +
    -

    addContext is used to add Server Name Indication contexts

    +

    Exposed main interfaces for HTTP plaintext upgrade connections (not implemented)

    -
    Server.prototype.addContext = function addContext(hostname, credentials) {
    -  if (this._mode === 'tls') {
    -    this._server.addContext(hostname, credentials);
    -  }
    -};
    -
    -function createServer(options, requestListener) {
    -  if (typeof options === 'function') {
    -    requestListener = options;
    -    options = undefined;
    -  }
    -
    -  var server = new Server(options);
    -
    -  if (requestListener) {
    -    server.on('request', requestListener);
    -  }
    +            
    function notImplemented() {
    +    throw new Error('HTTP UPGRADE is not implemented!');
    +}
     
    -  return server;
    -}
    +exports.http = {}; +exports.http.createServer = exports.http.request = exports.http.get = notImplemented;
  • -
  • +
  • -
    - +
    +
    -

    IncomingRequest class

    +

    IncomingRequest class

  • -
  • +
  • - +
    -
    function IncomingRequest(stream) {
    -  IncomingMessage.call(this, stream);
    +            
    +function IncomingRequest(stream) {
    +  IncomingMessage.call(this, stream);
     }
    -IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } });
    +IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } });
  • -
  • +
  • - +
    -

    Request Header Fields -* headers argument: HTTP/2.0 request and response header fields carry information as a series - of key-value pairs. This includes the target URI for the request, the status code for the - response, as well as HTTP header fields.

    +

    Request Header Fields

    +
      +
    • headers argument: HTTP/2.0 request and response header fields carry information as a series +of key-value pairs. This includes the target URI for the request, the status code for the +response, as well as HTTP header fields.
    • +
    -
    IncomingRequest.prototype._onHeaders = function _onHeaders(headers) {
    +
    IncomingRequest.prototype._onHeaders = function _onHeaders(headers) {
  • -
  • +
  • - +
      -
    • The ":method" header field includes the HTTP method
    • -
    • The ":scheme" header field includes the scheme portion of the target URI
    • -
    • The ":host" header field includes the authority portion of the target URI
    • -
    • The ":path" header field includes the path and query parts of the target URI. +
    • The “:method” header field includes the HTTP method
    • +
    • The “:scheme” header field includes the scheme portion of the target URI
    • +
    • The “:authority” header field includes the authority portion of the target URI
    • +
    • The “:path” header field includes the path and query parts of the target URI. This field MUST NOT be empty; URIs that do not contain a path component MUST include a value -of '/', unless the request is an OPTIONS request for '', in which case the ":path" header -field MUST include ''.
    • +of ‘/‘, unless the request is an OPTIONS request for ‘‘, in which case the “:path” header +field MUST include ‘‘.
    • All HTTP/2.0 requests MUST include exactly one valid value for all of these header fields. A server MUST treat the absence of any of these header fields, presence of multiple values, or an invalid value as a stream error of type PROTOCOL_ERROR.
    • @@ -1092,19 +1392,36 @@

      IncomingRequest class

    -
      this.method = this._checkSpecialHeader(':method', headers[':method']);
    -  this.scheme = this._checkSpecialHeader(':scheme', headers[':scheme']);
    -  this.host   = this._checkSpecialHeader(':host'  , headers[':host']  );
    -  this.url    = this._checkSpecialHeader(':path'  , headers[':path']  );
    +
      this.method = this._checkSpecialHeader(':method'   , headers[':method']);
    +  this.scheme = this._checkSpecialHeader(':scheme'   , headers[':scheme']);
    +  this.host   = this._checkSpecialHeader(':authority', headers[':authority']  );
    +  this.url    = this._checkSpecialHeader(':path'     , headers[':path']  );
    +  if (!this.method || !this.scheme || !this.host || !this.url) {
  • -
  • +
  • - + +
    +

    This is invalid, and we’ve sent a RST_STREAM, so don’t continue processing

    + +
    + +
        return;
    +  }
    + +
  • + + +
  • +
    + +
    +
    • Host header is included in the headers object for backwards compatibility.
    • @@ -1112,16 +1429,16 @@

      IncomingRequest class

    -
      this.headers.host = this.host;
    +
      this.headers.host = this.host;
  • -
  • +
  • - +
    • Handling regular headers.
    • @@ -1129,16 +1446,16 @@

      IncomingRequest class

    -
      IncomingMessage.prototype._onHeaders.call(this, headers);
    +
      IncomingMessage.prototype._onHeaders.call(this, headers);
  • -
  • +
  • - +
    • Signaling that the headers arrived.
    • @@ -1146,218 +1463,297 @@

      IncomingRequest class

    -
      this._log.info({ method: this.method, scheme: this.scheme, host: this.host,
    -                   path: this.url, headers: this.headers }, 'Incoming request');
    -  this.emit('ready');
    +            
      this._log.info({ method: this.method, scheme: this.scheme, host: this.host,
    +                   path: this.url, headers: this.headers }, 'Incoming request');
    +  this.emit('ready');
     };
  • -
  • +
  • -
    - +
    +
    -

    OutgoingResponse class

    +

    OutgoingResponse class

  • -
  • +
  • - +
    -
    function OutgoingResponse(stream) {
    -  OutgoingMessage.call(this);
    +            
    +function OutgoingResponse(stream) {
    +  OutgoingMessage.call(this);
     
    -  this._log = stream._log.child({ component: 'http' });
    +  this._log = stream._log.child({ component: 'http' });
     
    -  this.stream = stream;
    -  this.statusCode = 200;
    -  this.sendDate = true;
    +  this.stream = stream;
    +  this.statusCode = 200;
    +  this.sendDate = true;
     
    -  this.stream.once('headers', this._onRequestHeaders.bind(this));
    +  this.stream.once('headers', this._onRequestHeaders.bind(this));
     }
    -OutgoingResponse.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingResponse } });
    +OutgoingResponse.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingResponse } });
    +
    +OutgoingResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase, headers) {
    +  if (this.headersSent) {
    +    return;
    +  }
     
    -OutgoingResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase, headers) {
    -  if (typeof reasonPhrase === 'string') {
    -    this._log.warn('Reason phrase argument was present but ignored by the writeHead method');
    -  } else {
    +  if (typeof reasonPhrase === 'string') {
    +    this._log.warn('Reason phrase argument was present but ignored by the writeHead method');
    +  } else {
         headers = reasonPhrase;
       }
     
    -  for (var name in headers) {
    -    this.setHeader(name, headers[name]);
    +  for (var name in headers) {
    +    this.setHeader(name, headers[name]);
       }
    -  headers = this._headers;
    +  headers = this._headers;
     
    -  if (this.sendDate && !('date' in this._headers)) {
    -    headers.date = (new Date()).toUTCString();
    +  if (this.sendDate && !('date' in this._headers)) {
    +    headers.date = (new Date()).toUTCString();
       }
     
    -  this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response');
    +  this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response');
     
    -  headers[':status'] = this.statusCode = statusCode;
    +  headers[':status'] = this.statusCode = statusCode;
     
    -  this.stream.headers(headers);
    -  this.headersSent = true;
    +  this.stream.headers(headers);
    +  this.headersSent = true;
     };
     
    -OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() {
    -  if (!this.headersSent) {
    -    this.writeHead(this.statusCode);
    +OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() {
    +  if (!this.headersSent) {
    +    this.writeHead(this.statusCode);
       }
     };
     
    -OutgoingResponse.prototype.write = function write() {
    -  this._implicitHeaders();
    -  return OutgoingMessage.prototype.write.apply(this, arguments);
    +OutgoingResponse.prototype._implicitHeader = function() {
    +  this._implicitHeaders();
    +};
    +
    +OutgoingResponse.prototype.write = function write() {
    +  this._implicitHeaders();
    +  return OutgoingMessage.prototype.write.apply(this, arguments);
     };
     
    -OutgoingResponse.prototype.end = function end() {
    -  this._implicitHeaders();
    -  return OutgoingMessage.prototype.end.apply(this, arguments);
    +OutgoingResponse.prototype.end = function end() {
    +  this.finshed = true;
    +  this._implicitHeaders();
    +  return OutgoingMessage.prototype.end.apply(this, arguments);
     };
     
    -OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) {
    -  this._requestHeaders = headers;
    +OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) {
    +  this._requestHeaders = headers;
     };
     
    -OutgoingResponse.prototype.push = function push(options) {
    -  if (typeof options === 'string') {
    +OutgoingResponse.prototype.push = function push(options) {
    +  if (typeof options === 'string') {
         options = url.parse(options);
       }
     
    -  if (!options.path) {
    -    throw new Error('`path` option is mandatory.');
    +  if (!options.path) {
    +    throw new Error('`path` option is mandatory.');
       }
     
    -  var promise = util._extend({
    -    ':method': (options.method || 'GET').toUpperCase(),
    -    ':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'],
    -    ':host': options.hostname || options.host || this._requestHeaders[':host'],
    -    ':path': options.path
    +  var promise = util._extend({
    +    ':method': (options.method || 'GET').toUpperCase(),
    +    ':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'],
    +    ':authority': options.hostname || options.host || this._requestHeaders[':authority'],
    +    ':path': options.path
       }, options.headers);
     
    -  this._log.info({ method: promise[':method'], scheme: promise[':scheme'], host: promise[':host'],
    -                   path: promise[':path'], headers: options.headers }, 'Promising push stream');
    +  this._log.info({ method: promise[':method'], scheme: promise[':scheme'],
    +                   authority: promise[':authority'], path: promise[':path'],
    +                   headers: options.headers }, 'Promising push stream');
    +
    +  var pushStream = this.stream.promise(promise);
     
    -  var pushStream = this.stream.promise(promise);
    +  return new OutgoingResponse(pushStream);
    +};
     
    -  return new OutgoingResponse(pushStream);
    +OutgoingResponse.prototype.altsvc = function altsvc(host, port, protocolID, maxAge, origin) {
    +    if (origin === undefined) {
    +        origin = "";
    +    }
    +    this.stream.altsvc(host, port, protocolID, maxAge, origin);
     };
  • -
  • +
  • - +
    -

    Overriding EventEmitter's on(event, listener) method to forward certain subscriptions to +

    Overriding EventEmitter‘s on(event, listener) method to forward certain subscriptions to request. See Server.prototype.on for explanation.

    -
    OutgoingResponse.prototype.on = function on(event, listener) {
    -  if (this.request && (event === 'timeout')) {
    -    this.request.on(event, listener && listener.bind(this));
    -  } else {
    -    OutgoingMessage.prototype.on.call(this, event, listener);
    +            
    OutgoingResponse.prototype.on = function on(event, listener) {
    +  if (this.request && (event === 'timeout')) {
    +    this.request.on(event, listener && listener.bind(this));
    +  } else {
    +    OutgoingMessage.prototype.on.call(this, event, listener);
       }
     };
  • -
  • +
  • -
    - +
    +
    -

    Client side

    +

    Client side

  • -
  • +
  • - +
    -
    exports.ClientRequest = OutgoingRequest; // for API compatibility
    +            
    +exports.ClientRequest = OutgoingRequest; // for API compatibility
     exports.OutgoingRequest = OutgoingRequest;
     exports.IncomingResponse = IncomingResponse;
     exports.Agent = Agent;
    -exports.globalAgent = undefined;
    -exports.request = function request(options, callback) {
    -  return (options.agent || exports.globalAgent).request(options, callback);
    -};
    -exports.get = function get(options, callback) {
    -  return (options.agent || exports.globalAgent).get(options, callback);
    -};
    +exports.globalAgent = undefined; + +function requestRaw(options, callback) { + if (typeof options === "string") { + options = url.parse(options); + } + options.plain = true; + if (options.protocol && options.protocol !== "http:") { + throw new Error('This interface only supports http-schemed URLs'); + } + if (options.agent && typeof(options.agent.request) === 'function') { + var agentOptions = util._extend({}, options); + delete agentOptions.agent; + return options.agent.request(agentOptions, callback); + } + return exports.globalAgent.request(options, callback); +} + +function requestTLS(options, callback) { + if (typeof options === "string") { + options = url.parse(options); + } + options.plain = false; + if (options.protocol && options.protocol !== "https:") { + throw new Error('This interface only supports https-schemed URLs'); + } + if (options.agent && typeof(options.agent.request) === 'function') { + var agentOptions = util._extend({}, options); + delete agentOptions.agent; + return options.agent.request(agentOptions, callback); + } + return exports.globalAgent.request(options, callback); +} + +function getRaw(options, callback) { + if (typeof options === "string") { + options = url.parse(options); + } + options.plain = true; + if (options.protocol && options.protocol !== "http:") { + throw new Error('This interface only supports http-schemed URLs'); + } + if (options.agent && typeof(options.agent.get) === 'function') { + var agentOptions = util._extend({}, options); + delete agentOptions.agent; + return options.agent.get(agentOptions, callback); + } + return exports.globalAgent.get(options, callback); +} + +function getTLS(options, callback) { + if (typeof options === "string") { + options = url.parse(options); + } + options.plain = false; + if (options.protocol && options.protocol !== "https:") { + throw new Error('This interface only supports https-schemed URLs'); + } + if (options.agent && typeof(options.agent.get) === 'function') { + var agentOptions = util._extend({}, options); + delete agentOptions.agent; + return options.agent.get(agentOptions, callback); + } + return exports.globalAgent.get(options, callback); +}
  • -
  • +
  • -
    - +
    +
    -

    Agent class

    +

    Agent class

  • -
  • +
  • - +
    -
    function Agent(options) {
    -  EventEmitter.call(this);
    +            
    +function Agent(options) {
    +  EventEmitter.call(this);
    +  this.setMaxListeners(0);
     
    -  options = options || {};
    +  options = util._extend({}, options);
     
    -  this._settings = options.settings;
    -  this._log = (options.log || defaultLogger).child({ component: 'http' });
    -  this.endpoints = {};
    + this._settings = options.settings; + this._log = (options.log || defaultLogger).child({ component: 'http' }); + this.endpoints = {};
  • -
  • +
  • - +
    • Using an own HTTPS agent, because the global agent does not look at NPN/ALPNProtocols when @@ -1368,71 +1764,74 @@

      Agent class

    -
      var agentOptions = {};
    -  agentOptions[protocolList] = supportedProtocols;
    -  this._httpsAgent = new https.Agent(agentOptions);
    +            
      options.ALPNProtocols = supportedProtocols;
    +  options.NPNProtocols = supportedProtocols;
    +  this._httpsAgent = new https.Agent(options);
     
    -  this.sockets = this._httpsAgent.sockets;
    -  this.requests = this._httpsAgent.requests;
    +  this.sockets = this._httpsAgent.sockets;
    +  this.requests = this._httpsAgent.requests;
     }
    -Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } });
    +Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } });
     
    -Agent.prototype.request = function request(options, callback) {
    -  if (typeof options === 'string') {
    +Agent.prototype.request = function request(options, callback) {
    +  if (typeof options === 'string') {
         options = url.parse(options);
    +  } else {
    +    options = util._extend({}, options);
       }
     
    -  options.method = (options.method || 'GET').toUpperCase();
    -  options.protocol = options.protocol || 'https:';
    -  options.host = options.hostname || options.host || 'localhost';
    -  options.port = options.port || 443;
    -  options.path = options.path || '/';
    +  options.method = (options.method || 'GET').toUpperCase();
    +  options.protocol = options.protocol || 'https:';
    +  options.host = options.hostname || options.host || 'localhost';
    +  options.port = options.port || 443;
    +  options.path = options.path || '/';
     
    -  if (!options.plain && options.protocol === 'http:') {
    -    this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1');
    -    throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.');
    +  if (!options.plain && options.protocol === 'http:') {
    +    this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1');
    +    this.emit('error', new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.'));
       }
     
    -  var request = new OutgoingRequest(this._log);
    +  var request = new OutgoingRequest(this._log);
     
    -  if (callback) {
    -    request.on('response', callback);
    +  if (callback) {
    +    request.on('response', callback);
       }
     
    -  var key = [
    +  var key = [
         !!options.plain,
         options.host,
         options.port
    -  ].join(':');
    + ].join(':'); + var self = this;
  • -
  • +
  • - +
      -
    • There's an existing HTTP/2 connection to this host
    • +
    • There’s an existing HTTP/2 connection to this host
    -
      if (key in this.endpoints) {
    -    var endpoint = this.endpoints[key];
    +            
      if (key in this.endpoints) {
    +    var endpoint = this.endpoints[key];
         request._start(endpoint.createStream(), options);
       }
  • -
  • +
  • - +
    • HTTP/2 over plain TCP
    • @@ -1440,13 +1839,25 @@

      Agent class

    -
      else if (options.plain) {
    -    endpoint = new Endpoint(this._log, 'CLIENT', this._settings);
    +            
      else if (options.plain) {
    +    endpoint = new Endpoint(this._log, 'CLIENT', this._settings);
         endpoint.socket = net.connect({
           host: options.host,
           port: options.port,
           localAddress: options.localAddress
         });
    +
    +    endpoint.socket.on('error', function (error) {
    +      self._log.error('Socket error: ' + error.toString());
    +      request.emit('error', error);
    +    });
    +
    +    endpoint.on('error', function(error){
    +      self._log.error('Connection error: ' + error.toString());
    +      request.emit('error', error);
    +    });
    +
    +    this.endpoints[key] = endpoint;
         endpoint.pipe(endpoint.socket).pipe(endpoint);
         request._start(endpoint.createStream(), options);
       }
    @@ -1454,286 +1865,363 @@

    Agent class

  • -
  • +
  • - +
      -
    • HTTP/2 over TLS negotiated using NPN or ALPN
    • +
    • HTTP/2 over TLS negotiated using NPN or ALPN, or fallback to HTTPS1
    -
      else {
    -    var started = false;
    -    options[protocolList] = supportedProtocols;
    -    options.servername = options.host; // Server Name Indication
    -    options.agent = this._httpsAgent;
    -    var httpsRequest = https.request(options);
    +            
      else {
    +    var started = false;
    +    var createAgent = hasAgentOptions(options);
    +    options.ALPNProtocols = supportedProtocols;
    +    options.NPNProtocols = supportedProtocols;
    +    options.servername = options.host; // Server Name Indication
    +    options.ciphers = options.ciphers || cipherSuites;
    +    if (createAgent) {
    +      options.agent = new https.Agent(options);
    +    } else if (options.agent == null) {
    +      options.agent = this._httpsAgent;
    +    }
    +    var httpsRequest = https.request(options);
     
    -    httpsRequest.on('socket', function(socket) {
    -      if (socket[negotiatedProtocol] !== undefined) {
    +    httpsRequest.on('error', function (error) {
    +      self._log.error('Socket error: ' + error.toString());
    +      self.removeAllListeners(key);
    +      request.emit('error', error);
    +    });
    +
    +    httpsRequest.on('socket', function(socket) {
    +      var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
    +      if (negotiatedProtocol != null) { // null in >=0.11.0, undefined in <0.11.0
             negotiated();
    -      } else {
    -        socket.on('secureConnect', negotiated);
    +      } else {
    +        socket.on('secureConnect', negotiated);
           }
         });
     
    -    var self = this;
    -    function negotiated() {
    -      var endpoint;
    -      if (httpsRequest.socket[negotiatedProtocol] === implementedVersion) {
    -        httpsRequest.socket.emit('agentRemove');
    +    function negotiated() {
    +      var endpoint;
    +      var negotiatedProtocol = httpsRequest.socket.alpnProtocol || httpsRequest.socket.npnProtocol;
    +      if (negotiatedProtocol === protocol.VERSION) {
    +        httpsRequest.socket.emit('agentRemove');
             unbundleSocket(httpsRequest.socket);
    -        endpoint = new Endpoint(self._log, 'CLIENT', self._settings);
    +        endpoint = new Endpoint(self._log, 'CLIENT', self._settings);
             endpoint.socket = httpsRequest.socket;
             endpoint.pipe(endpoint.socket).pipe(endpoint);
           }
    -      if (started) {
    -        if (endpoint) {
    -          endpoint.close();
    -        } else {
    -          httpsRequest.abort();
    -        }
    -      } else {
    -        if (endpoint) {
    -          self._log.info({ e: endpoint, server: options.host + ':' + options.port },
    -                         'New outgoing HTTP/2 connection');
    +      if (started) {
    + +
  • + + +
  • +
    + +
    + +
    +

    ** In the meantime, an other connection was made to the same host…

    + +
    + +
            if (endpoint) {
    + +
  • + + +
  • +
    + +
    + +
    +

    * and it turned out to be HTTP2 and the request was multiplexed on that one, so we should close this one

    + +
    + +
              endpoint.close();
    +        }
    + +
  • + + +
  • +
    + +
    + +
    +

    * otherwise, the fallback to HTTPS1 is already done.

    + +
    + +
          } else {
    +        if (endpoint) {
    +          self._log.info({ e: endpoint, server: options.host + ':' + options.port },
    +                         'New outgoing HTTP/2 connection');
               self.endpoints[key] = endpoint;
               self.emit(key, endpoint);
    -        } else {
    -          self.emit(key, undefined);
    +        } else {
    +          self.emit(key, undefined);
             }
           }
         }
     
    -    this.once(key, function(endpoint) {
    -      started = true;
    -      if (endpoint) {
    +    this.once(key, function(endpoint) {
    +      started = true;
    +      if (endpoint) {
             request._start(endpoint.createStream(), options);
    -      } else {
    +      } else {
             request._fallback(httpsRequest);
           }
         });
       }
     
    -  return request;
    +  return request;
     };
     
    -Agent.prototype.get = function get(options, callback) {
    -  var request = this.request(options, callback);
    +Agent.prototype.get = function get(options, callback) {
    +  var request = this.request(options, callback);
       request.end();
    -  return request;
    +  return request;
     };
     
    -function unbundleSocket(socket) {
    -  socket.removeAllListeners('data');
    -  socket.removeAllListeners('end');
    -  socket.removeAllListeners('readable');
    -  socket.removeAllListeners('close');
    -  socket.removeAllListeners('error');
    +Agent.prototype.destroy = function(error) {
    +  if (this._httpsAgent) {
    +    this._httpsAgent.destroy();
    +  }
    +  for (var key in this.endpoints) {
    +    this.endpoints[key].close(error);
    +  }
    +};
    +
    +function unbundleSocket(socket) {
    +  socket.removeAllListeners('data');
    +  socket.removeAllListeners('end');
    +  socket.removeAllListeners('readable');
    +  socket.removeAllListeners('close');
    +  socket.removeAllListeners('error');
       socket.unpipe();
    -  delete socket.ondata;
    -  delete socket.onend;
    +  delete socket.ondata;
    +  delete socket.onend;
     }
     
    -Object.defineProperty(Agent.prototype, 'maxSockets', {
    -  get: function getMaxSockets() {
    -    return this._httpsAgent.maxSockets;
    +function hasAgentOptions(options) {
    +  return options.pfx != null ||
    +    options.key != null ||
    +    options.passphrase != null ||
    +    options.cert != null ||
    +    options.ca != null ||
    +    options.ciphers != null ||
    +    options.rejectUnauthorized != null ||
    +    options.secureProtocol != null;
    +}
    +
    +Object.defineProperty(Agent.prototype, 'maxSockets', {
    +  get: function getMaxSockets() {
    +    return this._httpsAgent.maxSockets;
       },
    -  set: function setMaxSockets(value) {
    -    this._httpsAgent.maxSockets = value;
    +  set: function setMaxSockets(value) {
    +    this._httpsAgent.maxSockets = value;
       }
     });
     
    -exports.globalAgent = new Agent();
    +exports.globalAgent = new Agent();
  • -
  • +
  • -
    - +
    +
    -

    OutgoingRequest class

    +

    OutgoingRequest class

  • -
  • +
  • - +
    -
    function OutgoingRequest() {
    -  OutgoingMessage.call(this);
    +            
    +function OutgoingRequest() {
    +  OutgoingMessage.call(this);
     
    -  this._log = undefined;
    +  this._log = undefined;
     
    -  this.stream = undefined;
    +  this.stream = undefined;
     }
    -OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } });
    +OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } });
     
    -OutgoingRequest.prototype._start = function _start(stream, options) {
    -  this.stream = stream;
    +OutgoingRequest.prototype._start = function _start(stream, options) {
    +  this.stream = stream;
    +  this.options = options;
     
    -  this._log = stream._log.child({ component: 'http' });
    +  this._log = stream._log.child({ component: 'http' });
     
    -  for (var key in options.headers) {
    -    this.setHeader(key, options.headers[key]);
    +  for (var key in options.headers) {
    +    this.setHeader(key, options.headers[key]);
       }
    -  var headers = this._headers;
    -  delete headers.host;
    +  var headers = this._headers;
    +  delete headers.host;
     
    -  if (options.auth) {
    -    headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64');
    +  if (options.auth) {
    +    headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64');
       }
     
    -  headers[':scheme'] = options.protocol.slice(0, -1);
    -  headers[':method'] = options.method;
    -  headers[':host'] = options.host;
    -  headers[':path'] = options.path;
    -
    -  this._log.info({ scheme: headers[':scheme'], method: headers[':method'], host: headers[':host'],
    -                   path: headers[':path'], headers: (options.headers || {}) }, 'Sending request');
    -  this.stream.headers(headers);
    -  this.headersSent = true;
    +  headers[':scheme'] = options.protocol.slice(0, -1);
    +  headers[':method'] = options.method;
    +  headers[':authority'] = options.host;
    +  headers[':path'] = options.path;
     
    -  this.emit('socket', this.stream);
    +  this._log.info({ scheme: headers[':scheme'], method: headers[':method'],
    +                   authority: headers[':authority'], path: headers[':path'],
    +                   headers: (options.headers || {}) }, 'Sending request');
    +  this.stream.headers(headers);
    +  this.headersSent = true;
     
    -  var response = new IncomingResponse(this.stream);
    -  response.once('ready', this.emit.bind(this, 'response', response));
    +  this.emit('socket', this.stream);
    +  var response = new IncomingResponse(this.stream);
    +  response.req = this;
    +  response.once('ready', this.emit.bind(this, 'response', response));
     
    -  this.stream.on('promise', this._onPromise.bind(this));
    +  this.stream.on('promise', this._onPromise.bind(this));
     };
     
    -OutgoingRequest.prototype._fallback = function _fallback(request) {
    -  request.on('response', this.emit.bind(this, 'response'));
    -  this.stream = this.request = request;
    -  this.emit('socket', this.socket);
    +OutgoingRequest.prototype._fallback = function _fallback(request) {
    +  request.on('response', this.emit.bind(this, 'response'));
    +  this.stream = this.request = request;
    +  this.emit('socket', this.socket);
     };
     
    -OutgoingRequest.prototype.setPriority = function setPriority(priority) {
    -  if (this.stream) {
    -    this.stream.priority(priority);
    -  } else {
    -    this.once('socket', this.setPriority.bind(this, priority));
    +OutgoingRequest.prototype.setPriority = function setPriority(priority) {
    +  if (this.stream) {
    +    this.stream.priority(priority);
    +  } else {
    +    this.once('socket', this.setPriority.bind(this, priority));
       }
     };
  • -
  • +
  • - +
    -

    Overriding EventEmitter's on(event, listener) method to forward certain subscriptions to +

    Overriding EventEmitter‘s on(event, listener) method to forward certain subscriptions to request. See Server.prototype.on for explanation.

    -
    OutgoingRequest.prototype.on = function on(event, listener) {
    -  if (this.request && (event === 'upgrade')) {
    -    this.request.on(event, listener && listener.bind(this));
    -  } else {
    -    OutgoingMessage.prototype.on.call(this, event, listener);
    +            
    OutgoingRequest.prototype.on = function on(event, listener) {
    +  if (this.request && (event === 'upgrade')) {
    +    this.request.on(event, listener && listener.bind(this));
    +  } else {
    +    OutgoingMessage.prototype.on.call(this, event, listener);
       }
     };
  • -
  • +
  • - +

    Methods only in fallback mode

    -
    OutgoingRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
    -  if (this.request) {
    -    this.request.setNoDelay(noDelay);
    -  } else if (!this.stream) {
    -    this.on('socket', this.setNoDelay.bind(this, noDelay));
    +            
    OutgoingRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
    +  if (this.request) {
    +    this.request.setNoDelay(noDelay);
    +  } else if (!this.stream) {
    +    this.on('socket', this.setNoDelay.bind(this, noDelay));
       }
     };
     
    -OutgoingRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive(enable, initialDelay) {
    -  if (this.request) {
    -    this.request.setSocketKeepAlive(enable, initialDelay);
    -  } else if (!this.stream) {
    -    this.on('socket', this.setSocketKeepAlive.bind(this, enable, initialDelay));
    +OutgoingRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive(enable, initialDelay) {
    +  if (this.request) {
    +    this.request.setSocketKeepAlive(enable, initialDelay);
    +  } else if (!this.stream) {
    +    this.on('socket', this.setSocketKeepAlive.bind(this, enable, initialDelay));
       }
     };
     
    -OutgoingRequest.prototype.setTimeout = function setTimeout(timeout, callback) {
    -  if (this.request) {
    -    this.request.setTimeout(timeout, callback);
    -  } else if (!this.stream) {
    -    this.on('socket', this.setTimeout.bind(this, timeout, callback));
    +OutgoingRequest.prototype.setTimeout = function setTimeout(timeout, callback) {
    +  if (this.request) {
    +    this.request.setTimeout(timeout, callback);
    +  } else if (!this.stream) {
    +    this.on('socket', this.setTimeout.bind(this, timeout, callback));
       }
     };
  • -
  • +
  • - +

    Aborting the request

    -
    OutgoingRequest.prototype.abort = function abort() {
    -  if (this.request) {
    -    this.request.abort();
    -  } else if (this.stream) {
    -    this.stream.reset('CANCEL');
    -  } else {
    -    this.on('socket', this.abort.bind(this));
    +            
    OutgoingRequest.prototype.abort = function abort() {
    +  if (this.request) {
    +    this.request.abort();
    +  } else if (this.stream) {
    +    this.stream.reset('CANCEL');
    +  } else {
    +    this.on('socket', this.abort.bind(this));
       }
     };
  • -
  • +
  • - +

    Receiving push promises

    -
    OutgoingRequest.prototype._onPromise = function _onPromise(stream, headers) {
    -  this._log.info({ push_stream: stream.id }, 'Receiving push promise');
    +            
    OutgoingRequest.prototype._onPromise = function _onPromise(stream, headers) {
    +  this._log.info({ push_stream: stream.id }, 'Receiving push promise');
     
    -  var promise = new IncomingPromise(stream, headers);
    +  var promise = new IncomingPromise(stream, headers);
     
    -  if (this.listeners('push').length > 0) {
    -    this.emit('push', promise);
    -  } else {
    +  if (this.listeners('push').length > 0) {
    +    this.emit('push', promise);
    +  } else {
         promise.cancel();
       }
     };
    @@ -1741,64 +2229,67 @@

    OutgoingRequest class

  • -
  • +
  • -
    - +
    +
    -

    IncomingResponse class

    +

    IncomingResponse class

  • -
  • +
  • - +
    -
    function IncomingResponse(stream) {
    -  IncomingMessage.call(this, stream);
    +            
    +function IncomingResponse(stream) {
    +  IncomingMessage.call(this, stream);
     }
    -IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } });
    +IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } });
  • -
  • +
  • - +
    -

    Response Header Fields -* headers argument: HTTP/2.0 request and response header fields carry information as a series - of key-value pairs. This includes the target URI for the request, the status code for the - response, as well as HTTP header fields.

    +

    Response Header Fields

    +
      +
    • headers argument: HTTP/2.0 request and response header fields carry information as a series +of key-value pairs. This includes the target URI for the request, the status code for the +response, as well as HTTP header fields.
    • +
    -
    IncomingResponse.prototype._onHeaders = function _onHeaders(headers) {
    +
    IncomingResponse.prototype._onHeaders = function _onHeaders(headers) {
  • -
  • +
  • - +
      -
    • A single ":status" header field is defined that carries the HTTP status code field. This +
    • A single “:status” header field is defined that carries the HTTP status code field. This header field MUST be included in all responses.
    • -
    • A client MUST treat the absence of the ":status" header field, the presence of multiple +
    • A client MUST treat the absence of the “:status” header field, the presence of multiple values, or an invalid value as a stream error of type PROTOCOL_ERROR. Note: currently, we do not enforce it strictly: we accept any format, and parse it as int
    • HTTP/2.0 does not define a way to carry the reason phrase that is included in an HTTP/1.1 @@ -1807,16 +2298,16 @@

      IncomingResponse class

    -
      this.statusCode = parseInt(this._checkSpecialHeader(':status', headers[':status']));
    +
      this.statusCode = parseInt(this._checkSpecialHeader(':status', headers[':status']));
  • -
  • +
  • - +
    • Handling regular headers.
    • @@ -1824,16 +2315,16 @@

      IncomingResponse class

    -
      IncomingMessage.prototype._onHeaders.call(this, headers);
    +
      IncomingMessage.prototype._onHeaders.call(this, headers);
  • -
  • +
  • - +
    • Signaling that the headers arrived.
    • @@ -1841,60 +2332,61 @@

      IncomingResponse class

    -
      this._log.info({ status: this.statusCode, headers: this.headers}, 'Incoming response');
    -  this.emit('ready');
    +            
      this._log.info({ status: this.statusCode, headers: this.headers}, 'Incoming response');
    +  this.emit('ready');
     };
  • -
  • +
  • -
    - +
    +
    -

    IncomingPromise class

    +

    IncomingPromise class

  • -
  • +
  • - +
    -
    function IncomingPromise(responseStream, promiseHeaders) {
    -  var stream = new Readable();
    +            
    +function IncomingPromise(responseStream, promiseHeaders) {
    +  var stream = new Readable();
       stream._read = noop;
    -  stream.push(null);
    +  stream.push(null);
       stream._log = responseStream._log;
     
    -  IncomingRequest.call(this, stream);
    +  IncomingRequest.call(this, stream);
     
    -  this._onHeaders(promiseHeaders);
    +  this._onHeaders(promiseHeaders);
     
    -  this._responseStream = responseStream;
    +  this._responseStream = responseStream;
     
    -  var response = new IncomingResponse(this._responseStream);
    -  response.once('ready', this.emit.bind(this, 'response', response));
    +  var response = new IncomingResponse(this._responseStream);
    +  response.once('ready', this.emit.bind(this, 'response', response));
     
    -  this.stream.on('promise', this._onPromise.bind(this));
    +  this.stream.on('promise', this._onPromise.bind(this));
     }
    -IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } });
    +IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } });
     
    -IncomingPromise.prototype.cancel = function cancel() {
    -  this._responseStream.reset('CANCEL');
    +IncomingPromise.prototype.cancel = function cancel() {
    +  this._responseStream.reset('CANCEL');
     };
     
    -IncomingPromise.prototype.setPriority = function setPriority(priority) {
    -  this._responseStream.priority(priority);
    +IncomingPromise.prototype.setPriority = function setPriority(priority) {
    +  this._responseStream.priority(priority);
     };
     
     IncomingPromise.prototype._onPromise = OutgoingRequest.prototype._onPromise;
    diff --git a/doc/https.html b/doc/https.html deleted file mode 100644 index 10b7671a..00000000 --- a/doc/https.html +++ /dev/null @@ -1,208 +0,0 @@ - - - - - https.js - - - - - -
    -
    - - - -
      - -
    • -
      -

      https.js

      -
      -
    • - - - -
    • -
      - -
      - -
      -

      This is the main API that can be used to create HTTP/2 server that runs on top of TLS.

      - -
      - -
      var https2 = exports;
      - -
    • - - -
    • -
      - -
      - -
      -

      The main governing power behind the http2 API design is that it should look very similar to the -existing node.js HTTP/HTTPS -APIs. The additional features of HTTP/2 are exposed as extensions to these APIs. Furthermore, -node-http2 should fall back to using HTTP/1.1 if needed.

      - -
      - -
      var https = require('https');
      -
      -var default_settings = {};
      - -
    • - - -
    • -
      - -
      - -
      -

      Server

      - -
      - -
    • - - -
    • -
      - -
      - -
      - -
      - -
      https2.createServer = function createServer(options, requestListener) {
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      Client

      - -
      - -
    • - - -
    • -
      - -
      - -
      - -
      - -
      https2.request = function request(options, callback) {
      -};
      -
      -https2.get = function get(options, callback) {
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      Agent

      - -
      - -
    • - - -
    • -
      - -
      - -
      -

      HTTPS agents are not yet supported, -so every client request will create a new TCP stream.

      - -
      - -
    • - -
    -
    - - diff --git a/doc/index.html b/doc/index.html index b6eb9b1b..d79d63e0 100644 --- a/doc/index.html +++ b/doc/index.html @@ -16,48 +16,21 @@ Jump To … +
  • @@ -78,89 +51,54 @@

    index.js

    -

    node-http2 is an HTTP/2 (draft 04) implementation for node.js.

    -

    The main building blocks are mainly node.js streams that are connected through -pipes.

    -

    The main components are:

    +

    node-http2 is an HTTP/2 implementation for node.js.

    +

    The core of the protocol is implemented in the protocol sub-directory. This directory provides +two important features on top of the protocol:

      -
    • http.js: the top layer that presents an API very similar to the standard node.js -HTTPS module (which is in turn very similar to the HTTP module).

      -
    • -
    • Endpoint: represents an HTTP/2 endpoint (client or server). It's -responsible for the the first part of the handshake process (sending/receiving the -connection header) and manages other components (framer, compressor, -connection, streams) that make up a client or server.

      -
    • -
    • Connection: multiplexes the active HTTP/2 streams, manages connection -lifecycle and settings, and responsible for enforcing the connection level limits (flow -control, initiated stream limit)

      -
    • -
    • Stream: implementation of the HTTP/2 stream concept. -Implements the stream state machine defined by the standard, provides -management methods and events for using the stream (sending/receiving headers, data, etc.), -and enforces stream level constraints (flow control, sending only legal frames).

      -
    • -
    • Flow: implements flow control for Connection and Stream as parent class.

      -
    • -
    • Compressor and Decompressor: compression and decompression of HEADER and -PUSH_PROMISE frames

      +
    • Implementation of different negotiation schemes that can be used to start a HTTP2 connection. +These include TLS ALPN, Upgrade and Plain TCP.

    • -
    • Serializer and Deserializer: the lowest layer in the stack that transforms -between the binary and the JavaScript object representation of HTTP/2 frames

      +
    • Providing an API very similar to the standard node.js HTTPS module API +(which is in turn very similar to the HTTP module API).

    -
    module.exports   = require('./http');
    +            
    +module.exports   = require('./http');
     
    -/*
    -                    API user
    +/*
    +                  HTTP API
     
    -                 |            ^
    -                 |            |
    - +---------------|------------|--------------------------------------------------------+
    - |               |            |        Server/Agent                                    |
    - |               v            |                                                        |
    - |          +----------+ +----------+                                                  |
    - |          | Outgoing | | Incoming |                                                  |
    - |          | req/res. | | req/res. |                                                  |
    - |          +----------+ +----------+                                                  |
    - |               |            ^                                                        |
    - |   +-----------|------------|---------------------------------------+   +-----       |
    - |   |           |            |   Endpoint                            |   |            |
    - |   |           |            |                                       |   |            |
    - |   |   +-------|------------|-----------------------------------+   |   |            |
    - |   |   |       |            |  Connection                       |   |   |            |
    - |   |   |       v            |                                   |   |   |            |
    - |   |   |  +-----------------------+  +--------------------      |   |   |            |
    - |   |   |  |        Stream         |  |         Stream      ...  |   |   |            |
    - |   |   |  +-----------------------+  +--------------------      |   |   |            |
    - |   |   |       |            ^              |            ^       |   |   |            |
    - |   |   |       v            |              v            |       |   |   |            |
    - |   |   |       +------------+--+--------+--+------------+- ...  |   |   |            |
    - |   |   |                       |        ^                       |   |   |            |
    - |   |   |                       |        |                       |   |   |      ...   |
    - |   |   +-----------------------|--------|-----------------------+   |   |            |
    - |   |                           |        |                           |   |            |
    - |   |                           v        |                           |   |            |
    - |   |   +--------------------------+  +--------------------------+   |   |            |
    - |   |   |        Compressor        |  |       Decompressor       |   |   |            |
    - |   |   +--------------------------+  +--------------------------+   |   |            |
    - |   |                           |        ^                           |   |            |
    - |   |                           v        |                           |   |            |
    - |   |   +--------------------------+  +--------------------------+   |   |            |
    - |   |   |        Serializer        |  |       Deserializer       |   |   |            |
    - |   |   +--------------------------+  +--------------------------+   |   |            |
    - |   |                           |        ^                           |   |            |
    - |   +---------------------------|--------|---------------------------+   +-----       |
    - |                               |        |                                            |
    - |                               v        |                                            |
    - |   +----------------------------------------------------------------+   +-----       |
    - |   |                           TCP stream                           |   |      ...   |
    - |   +----------------------------------------------------------------+   +-----       |
    - |                                                                                     |
    - +-------------------------------------------------------------------------------------+
    +               |            ^
    +               |            |
    + +-------------|------------|------------------------------------------------------+
    + |             |            |        Server/Agent                                  |
    + |             v            |                                                      |
    + |        +----------+ +----------+                                                |
    + |        | Outgoing | | Incoming |                                                |
    + |        | req/res. | | req/res. |                                                |
    + |        +----------+ +----------+                                                |
    + |             |            ^                                                      |
    + |             |            |                                                      |
    + |   +---------|------------|-------------------------------------+   +-----       |
    + |   |         |            |   Endpoint                          |   |            |
    + |   |         |            |                                     |   |            |
    + |   |         v            |                                     |   |            |
    + |   |    +-----------------------+  +--------------------        |   |            |
    + |   |    |        Stream         |  |         Stream      ...    |   |            |
    + |   |    +-----------------------+  +--------------------        |   |            |
    + |   |                                                            |   |            |
    + |   +------------------------------------------------------------+   +-----       |
    + |                             |        |                                          |
    + |                             |        |                                          |
    + |                             v        |                                          |
    + |   +------------------------------------------------------------+   +-----       |
    + |   |                         TCP stream                         |   |      ...   |
    + |   +------------------------------------------------------------+   +-----       |
    + |                                                                                 |
    + +---------------------------------------------------------------------------------+
     
     */
    diff --git a/doc/protocol.jst b/doc/protocol.jst new file mode 100644 index 00000000..72ebeadf --- /dev/null +++ b/doc/protocol.jst @@ -0,0 +1,59 @@ + + + + + <%= title %> + + + + + +
    +
    + <% if (sources.length > 1) { %> + + <% } %> +
      + <% if (!hasTitle) { %> +
    • +
      +

      <%= title %>

      +
      +
    • + <% } %> + <% for (var i=0, l=sections.length; i + <% var section = sections[i]; %> +
    • +
      + <% heading = section.docsHtml.match(/^\s*<(h\d)>/) %> +
      + +
      + <%= section.docsHtml %> +
      + <% if (section.codeText.replace(/\s/gm, '') != '') { %> +
      <%= section.codeHtml %>
      + <% } %> +
    • + <% } %> +
    +
    + + diff --git a/doc/protocol/compressor.html b/doc/protocol/compressor.html new file mode 100644 index 00000000..f82b2fd9 --- /dev/null +++ b/doc/protocol/compressor.html @@ -0,0 +1,2217 @@ + + + + + compressor.js + + + + + +
    +
    + + + +
      + +
    • +
      +

      compressor.js

      +
      +
    • + + + +
    • +
      + +
      + +
      +

      The implementation of the HTTP/2 Header Compression spec is separated from +the ‘integration’ part which handles HEADERS and PUSH_PROMISE frames. The compression itself is +implemented in the first part of the file, and consists of three classes: HeaderTable, +HeaderSetDecompressor and HeaderSetCompressor. The two latter classes are +Transform Stream subclasses that operate in object mode. +These transform chunks of binary data into [name, value] pairs and vice versa, and store their +state in HeaderTable instances.

      +

      The ‘integration’ part is also implemented by two Transform Stream subclasses +that operate in object mode: the Compressor and the Decompressor. These +provide a layer between the framer and the +connection handling component.

      + +
      + +
      +exports.HeaderTable = HeaderTable;
      +exports.HuffmanTable = HuffmanTable;
      +exports.HeaderSetCompressor = HeaderSetCompressor;
      +exports.HeaderSetDecompressor = HeaderSetDecompressor;
      +exports.Compressor = Compressor;
      +exports.Decompressor = Decompressor;
      +
      +var TransformStream = require('stream').Transform;
      +var assert = require('assert');
      +var util = require('util');
      + +
    • + + +
    • +
      + +
      + +
      +

      Header compression

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The HeaderTable class

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The Header Table is a component used to associate headers to index values. It is basically an +ordered list of [name, value] pairs, so it’s implemented as a subclass of Array. +In this implementation, the Header Table and the Static Table are handled as a single table.

      + +
      + +
      function HeaderTable(log, limit) {
      +  var self = HeaderTable.staticTable.map(entryFromPair);
      +  self._log = log;
      +  self._limit = limit || DEFAULT_HEADER_TABLE_LIMIT;
      +  self._staticLength = self.length;
      +  self._size = 0;
      +  self._enforceLimit = HeaderTable.prototype._enforceLimit;
      +  self.add = HeaderTable.prototype.add;
      +  self.setSizeLimit = HeaderTable.prototype.setSizeLimit;
      +  return self;
      +}
      +
      +function entryFromPair(pair) {
      +  var entry = pair.slice();
      +  entry._size = size(entry);
      +  return entry;
      +}
      + +
    • + + +
    • +
      + +
      + +
      +

      The encoder decides how to update the header table and as such can control how much memory is +used by the header table. To limit the memory requirements on the decoder side, the header table +size is bounded.

      +
        +
      • The default header table size limit is 4096 bytes.
      • +
      • The size of an entry is defined as follows: the size of an entry is the sum of its name’s +length in bytes, of its value’s length in bytes and of 32 bytes.
      • +
      • The size of a header table is the sum of the size of its entries.
      • +
      + +
      + +
      var DEFAULT_HEADER_TABLE_LIMIT = 4096;
      +
      +function size(entry) {
      +  return (new Buffer(entry[0] + entry[1], 'utf8')).length + 32;
      +}
      + +
    • + + +
    • +
      + +
      + +
      +

      The add(index, entry) can be used to manage the header table:

      +
        +
      • it pushes the new entry at the beggining of the table
      • +
      • before doing such a modification, it has to be ensured that the header table size will stay +lower than or equal to the header table size limit. To achieve this, entries are evicted from +the end of the header table until the size of the header table is less than or equal to +(this._limit - entry.size), or until the table is empty.

        +
               <----------  Index Address Space ---------->
        +       <-- Static  Table -->  <-- Header  Table -->
        +       +---+-----------+---+  +---+-----------+---+
        +       | 0 |    ...    | k |  |k+1|    ...    | n |
        +       +---+-----------+---+  +---+-----------+---+
        +                              ^                   |
        +                              |                   V
        +                       Insertion Point       Drop Point
        +
      • +
      + +
      + +
      +HeaderTable.prototype._enforceLimit = function _enforceLimit(limit) {
      +  var droppedEntries = [];
      +  while ((this._size > 0) && (this._size > limit)) {
      +    var dropped = this.pop();
      +    this._size -= dropped._size;
      +    droppedEntries.unshift(dropped);
      +  }
      +  return droppedEntries;
      +};
      +
      +HeaderTable.prototype.add = function(entry) {
      +  var limit = this._limit - entry._size;
      +  var droppedEntries = this._enforceLimit(limit);
      +
      +  if (this._size <= limit) {
      +    this.splice(this._staticLength, 0, entry);
      +    this._size += entry._size;
      +  }
      +
      +  return droppedEntries;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The table size limit can be changed externally. In this case, the same eviction algorithm is used

      + +
      + +
      HeaderTable.prototype.setSizeLimit = function setSizeLimit(limit) {
      +  this._limit = limit;
      +  this._enforceLimit(this._limit);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The Static Table

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The table is generated with feeding the table from the spec to the following sed command:

      +
      sed -re "s/\s*\| [0-9]+\s*\| ([^ ]*)/  [ '\1'/g" -e "s/\|\s([^ ]*)/, '\1'/g" -e 's/ \|/],/g'
      +
      +
      + +
      +HeaderTable.staticTable  = [
      +  [ ':authority'                  , ''            ],
      +  [ ':method'                     , 'GET'         ],
      +  [ ':method'                     , 'POST'        ],
      +  [ ':path'                       , '/'           ],
      +  [ ':path'                       , '/index.html' ],
      +  [ ':scheme'                     , 'http'        ],
      +  [ ':scheme'                     , 'https'       ],
      +  [ ':status'                     , '200'         ],
      +  [ ':status'                     , '204'         ],
      +  [ ':status'                     , '206'         ],
      +  [ ':status'                     , '304'         ],
      +  [ ':status'                     , '400'         ],
      +  [ ':status'                     , '404'         ],
      +  [ ':status'                     , '500'         ],
      +  [ 'accept-charset'              , ''            ],
      +  [ 'accept-encoding'             , 'gzip, deflate'],
      +  [ 'accept-language'             , ''            ],
      +  [ 'accept-ranges'               , ''            ],
      +  [ 'accept'                      , ''            ],
      +  [ 'access-control-allow-origin' , ''            ],
      +  [ 'age'                         , ''            ],
      +  [ 'allow'                       , ''            ],
      +  [ 'authorization'               , ''            ],
      +  [ 'cache-control'               , ''            ],
      +  [ 'content-disposition'         , ''            ],
      +  [ 'content-encoding'            , ''            ],
      +  [ 'content-language'            , ''            ],
      +  [ 'content-length'              , ''            ],
      +  [ 'content-location'            , ''            ],
      +  [ 'content-range'               , ''            ],
      +  [ 'content-type'                , ''            ],
      +  [ 'cookie'                      , ''            ],
      +  [ 'date'                        , ''            ],
      +  [ 'etag'                        , ''            ],
      +  [ 'expect'                      , ''            ],
      +  [ 'expires'                     , ''            ],
      +  [ 'from'                        , ''            ],
      +  [ 'host'                        , ''            ],
      +  [ 'if-match'                    , ''            ],
      +  [ 'if-modified-since'           , ''            ],
      +  [ 'if-none-match'               , ''            ],
      +  [ 'if-range'                    , ''            ],
      +  [ 'if-unmodified-since'         , ''            ],
      +  [ 'last-modified'               , ''            ],
      +  [ 'link'                        , ''            ],
      +  [ 'location'                    , ''            ],
      +  [ 'max-forwards'                , ''            ],
      +  [ 'proxy-authenticate'          , ''            ],
      +  [ 'proxy-authorization'         , ''            ],
      +  [ 'range'                       , ''            ],
      +  [ 'referer'                     , ''            ],
      +  [ 'refresh'                     , ''            ],
      +  [ 'retry-after'                 , ''            ],
      +  [ 'server'                      , ''            ],
      +  [ 'set-cookie'                  , ''            ],
      +  [ 'strict-transport-security'   , ''            ],
      +  [ 'transfer-encoding'           , ''            ],
      +  [ 'user-agent'                  , ''            ],
      +  [ 'vary'                        , ''            ],
      +  [ 'via'                         , ''            ],
      +  [ 'www-authenticate'            , ''            ]
      +];
      + +
    • + + +
    • +
      + +
      + +
      +

      The HeaderSetDecompressor class

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      A HeaderSetDecompressor instance is a transform stream that can be used to decompress a +single header set. Its input is a stream of binary data chunks and its output is a stream of +[name, value] pairs.

      +

      Currently, it is not a proper streaming decompressor implementation, since it buffer its input +until the end os the stream, and then processes the whole header block at once.

      + +
      + +
      +util.inherits(HeaderSetDecompressor, TransformStream);
      +function HeaderSetDecompressor(log, table) {
      +  TransformStream.call(this, { objectMode: true });
      +
      +  this._log = log.child({ component: 'compressor' });
      +  this._table = table;
      +  this._chunks = [];
      +}
      + +
    • + + +
    • +
      + +
      + +
      +

      _transform is the implementation of the corresponding virtual function of the +TransformStream class. It collects the data chunks for later processing.

      + +
      + +
      HeaderSetDecompressor.prototype._transform = function _transform(chunk, encoding, callback) {
      +  this._chunks.push(chunk);
      +  callback();
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      execute(rep) executes the given header representation.

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The JavaScript object representation of a header representation:

      +
      {
      +  name: String || Integer,  // string literal or index
      +  value: String || Integer, // string literal or index
      +  index: Boolean            // with or without indexing
      +}
      +

      Important: to ease the indexing of the header table, indexes start at 0 instead of 1.

      +

      Examples:

      +
      Indexed:
      +{ name: 2  , value: 2  , index: false }
      +Literal:
      +{ name: 2  , value: 'X', index: false } // without indexing
      +{ name: 2  , value: 'Y', index: true  } // with indexing
      +{ name: 'A', value: 'Z', index: true  } // with indexing, literal name
      +
      +
      + +
      HeaderSetDecompressor.prototype._execute = function _execute(rep) {
      +  this._log.trace({ key: rep.name, value: rep.value, index: rep.index },
      +                  'Executing header representation');
      +
      +  var entry, pair;
      +
      +  if (rep.contextUpdate) {
      +    this._table.setSizeLimit(rep.newMaxSize);
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • An indexed representation entails the following actions:
          +
        • The header field corresponding to the referenced entry is emitted
        • +
        +
      • +
      + +
      + +
        else if (typeof rep.value === 'number') {
      +    var index = rep.value;
      +    entry = this._table[index];
      +
      +    pair = entry.slice();
      +    this.push(pair);
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • A literal representation that is not added to the header table entails the following +action:
          +
        • The header is emitted.
        • +
        +
      • +
      • A literal representation that is added to the header table entails the following further +actions:
          +
        • The header is added to the header table.
        • +
        • The header is emitted.
        • +
        +
      • +
      + +
      + +
        else {
      +    if (typeof rep.name === 'number') {
      +      pair = [this._table[rep.name][0], rep.value];
      +    } else {
      +      pair = [rep.name, rep.value];
      +    }
      +
      +    if (rep.index) {
      +      entry = entryFromPair(pair);
      +      this._table.add(entry);
      +    }
      +
      +    this.push(pair);
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      _flush is the implementation of the corresponding virtual function of the +TransformStream class. The whole decompressing process is done in _flush. It gets called when +the input stream is over.

      + +
      + +
      HeaderSetDecompressor.prototype._flush = function _flush(callback) {
      +  var buffer = concat(this._chunks);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • processes the header representations
      • +
      + +
      + +
        buffer.cursor = 0;
      +  while (buffer.cursor < buffer.length) {
      +    this._execute(HeaderSetDecompressor.header(buffer));
      +  }
      +
      +  callback();
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The HeaderSetCompressor class

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      A HeaderSetCompressor instance is a transform stream that can be used to compress a single +header set. Its input is a stream of [name, value] pairs and its output is a stream of +binary data chunks.

      +

      It is a real streaming compressor, since it does not wait until the header set is complete.

      +

      The compression algorithm is (intentionally) not specified by the spec. Therefore, the current +compression algorithm can probably be improved in the future.

      + +
      + +
      +util.inherits(HeaderSetCompressor, TransformStream);
      +function HeaderSetCompressor(log, table) {
      +  TransformStream.call(this, { objectMode: true });
      +
      +  this._log = log.child({ component: 'compressor' });
      +  this._table = table;
      +  this.push = TransformStream.prototype.push.bind(this);
      +}
      +
      +HeaderSetCompressor.prototype.send = function send(rep) {
      +  this._log.trace({ key: rep.name, value: rep.value, index: rep.index },
      +                  'Emitting header representation');
      +
      +  if (!rep.chunks) {
      +    rep.chunks = HeaderSetCompressor.header(rep);
      +  }
      +  rep.chunks.forEach(this.push);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      _transform is the implementation of the corresponding virtual function of the +TransformStream class. It processes the input headers one by one:

      + +
      + +
      HeaderSetCompressor.prototype._transform = function _transform(pair, encoding, callback) {
      +  var name = pair[0].toLowerCase();
      +  var value = pair[1];
      +  var entry, rep;
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • tries to find full (name, value) or name match in the header table
      • +
      + +
      + +
        var nameMatch = -1, fullMatch = -1;
      +  for (var droppedIndex = 0; droppedIndex < this._table.length; droppedIndex++) {
      +    entry = this._table[droppedIndex];
      +    if (entry[0] === name) {
      +      if (entry[1] === value) {
      +        fullMatch = droppedIndex;
      +        break;
      +      } else if (nameMatch === -1) {
      +        nameMatch = droppedIndex;
      +      }
      +    }
      +  }
      +
      +  var mustNeverIndex = ((name === 'cookie' && value.length < 20) ||
      +                        (name === 'set-cookie' && value.length < 20) ||
      +                        name === 'authorization');
      +
      +  if (fullMatch !== -1 && !mustNeverIndex) {
      +    this.send({ name: fullMatch, value: fullMatch, index: false });
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • otherwise, it will be a literal representation (with a name index if there’s a name match)
      • +
      + +
      + +
        else {
      +    entry = entryFromPair(pair);
      +
      +    var indexing = (entry._size < this._table._limit / 2) && !mustNeverIndex;
      +
      +    if (indexing) {
      +      this._table.add(entry);
      +    }
      +
      +    this.send({ name: (nameMatch !== -1) ? nameMatch : name, value: value, index: indexing, mustNeverIndex: mustNeverIndex, contextUpdate: false });
      +  }
      +
      +  callback();
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      _flush is the implementation of the corresponding virtual function of the +TransformStream class. It gets called when there’s no more header to compress. The final step:

      + +
      + +
      HeaderSetCompressor.prototype._flush = function _flush(callback) {
      +  callback();
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Detailed Format

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Integer representation

      +

      The algorithm to represent an integer I is as follows:

      +
        +
      1. If I < 2^N - 1, encode I on N bits
      2. +
      3. Else, encode 2^N - 1 on N bits and do the following steps:
          +
        1. Set I to (I - (2^N - 1)) and Q to 1
        2. +
        3. While Q > 0
            +
          1. Compute Q and R, quotient and remainder of I divided by 2^7
          2. +
          3. If Q is strictly greater than 0, write one 1 bit; otherwise, write one 0 bit
          4. +
          5. Encode R on the next 7 bits
          6. +
          7. I = Q
          8. +
          +
        4. +
        +
      4. +
      + +
      + +
      +HeaderSetCompressor.integer = function writeInteger(I, N) {
      +  var limit = Math.pow(2,N) - 1;
      +  if (I < limit) {
      +    return [new Buffer([I])];
      +  }
      +
      +  var bytes = [];
      +  if (N !== 0) {
      +    bytes.push(limit);
      +  }
      +  I -= limit;
      +
      +  var Q = 1, R;
      +  while (Q > 0) {
      +    Q = Math.floor(I / 128);
      +    R = I % 128;
      +
      +    if (Q > 0) {
      +      R += 128;
      +    }
      +    bytes.push(R);
      +
      +    I = Q;
      +  }
      +
      +  return [new Buffer(bytes)];
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The inverse algorithm:

      +
        +
      1. Set I to the number coded on the lower N bits of the first byte
      2. +
      3. If I is smaller than 2^N - 1 then return I
      4. +
      5. Else the number is encoded on more than one byte, so do the following steps:
          +
        1. Set M to 0
        2. +
        3. While returning with I
            +
          1. Let B be the next byte (the first byte if N is 0)
          2. +
          3. Read out the lower 7 bits of B and multiply it with 2^M
          4. +
          5. Increase I with this number
          6. +
          7. Increase M by 7
          8. +
          9. Return I if the most significant bit of B is 0
          10. +
          +
        4. +
        +
      6. +
      + +
      + +
      +HeaderSetDecompressor.integer = function readInteger(buffer, N) {
      +  var limit = Math.pow(2,N) - 1;
      +
      +  var I = buffer[buffer.cursor] & limit;
      +  if (N !== 0) {
      +    buffer.cursor += 1;
      +  }
      +
      +  if (I === limit) {
      +    var M = 0;
      +    do {
      +      I += (buffer[buffer.cursor] & 127) << M;
      +      M += 7;
      +      buffer.cursor += 1;
      +    } while (buffer[buffer.cursor - 1] & 128);
      +  }
      +
      +  return I;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Huffman Encoding

      + +
      + +
      +function HuffmanTable(table) {
      +  function createTree(codes, position) {
      +    if (codes.length === 1) {
      +      return [table.indexOf(codes[0])];
      +    }
      +
      +    else {
      +      position = position || 0;
      +      var zero = [];
      +      var one = [];
      +      for (var i = 0; i < codes.length; i++) {
      +        var string = codes[i];
      +        if (string[position] === '0') {
      +          zero.push(string);
      +        } else {
      +          one.push(string);
      +        }
      +      }
      +      return [createTree(zero, position + 1), createTree(one, position + 1)];
      +    }
      +  }
      +
      +  this.tree = createTree(table);
      +
      +  this.codes = table.map(function(bits) {
      +    return parseInt(bits, 2);
      +  });
      +  this.lengths = table.map(function(bits) {
      +    return bits.length;
      +  });
      +}
      +
      +HuffmanTable.prototype.encode = function encode(buffer) {
      +  var result = [];
      +  var space = 8;
      +
      +  function add(data) {
      +    if (space === 8) {
      +      result.push(data);
      +    } else {
      +      result[result.length - 1] |= data;
      +    }
      +  }
      +
      +  for (var i = 0; i < buffer.length; i++) {
      +    var byte = buffer[i];
      +    var code = this.codes[byte];
      +    var length = this.lengths[byte];
      +
      +    while (length !== 0) {
      +      if (space >= length) {
      +        add(code << (space - length));
      +        code = 0;
      +        space -= length;
      +        length = 0;
      +      } else {
      +        var shift = length - space;
      +        var msb = code >> shift;
      +        add(msb);
      +        code -= msb << shift;
      +        length -= space;
      +        space = 0;
      +      }
      +
      +      if (space === 0) {
      +        space = 8;
      +      }
      +    }
      +  }
      +
      +  if (space !== 8) {
      +    add(this.codes[256] >> (this.lengths[256] - space));
      +  }
      +
      +  return new Buffer(result);
      +};
      +
      +HuffmanTable.prototype.decode = function decode(buffer) {
      +  var result = [];
      +  var subtree = this.tree;
      +
      +  for (var i = 0; i < buffer.length; i++) {
      +    var byte = buffer[i];
      +
      +    for (var j = 0; j < 8; j++) {
      +      var bit = (byte & 128) ? 1 : 0;
      +      byte = byte << 1;
      +
      +      subtree = subtree[bit];
      +      if (subtree.length === 1) {
      +        result.push(subtree[0]);
      +        subtree = this.tree;
      +      }
      +    }
      +  }
      +
      +  return new Buffer(result);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The initializer arrays for the Huffman tables are generated with feeding the tables from the +spec to this sed command:

      +
      sed -e "s/^.* [|]//g" -e "s/|//g" -e "s/ .*//g" -e "s/^/  '/g" -e "s/$/',/g"
      +
      +
      + +
      +HuffmanTable.huffmanTable = new HuffmanTable([
      +  '1111111111000',
      +  '11111111111111111011000',
      +  '1111111111111111111111100010',
      +  '1111111111111111111111100011',
      +  '1111111111111111111111100100',
      +  '1111111111111111111111100101',
      +  '1111111111111111111111100110',
      +  '1111111111111111111111100111',
      +  '1111111111111111111111101000',
      +  '111111111111111111101010',
      +  '111111111111111111111111111100',
      +  '1111111111111111111111101001',
      +  '1111111111111111111111101010',
      +  '111111111111111111111111111101',
      +  '1111111111111111111111101011',
      +  '1111111111111111111111101100',
      +  '1111111111111111111111101101',
      +  '1111111111111111111111101110',
      +  '1111111111111111111111101111',
      +  '1111111111111111111111110000',
      +  '1111111111111111111111110001',
      +  '1111111111111111111111110010',
      +  '111111111111111111111111111110',
      +  '1111111111111111111111110011',
      +  '1111111111111111111111110100',
      +  '1111111111111111111111110101',
      +  '1111111111111111111111110110',
      +  '1111111111111111111111110111',
      +  '1111111111111111111111111000',
      +  '1111111111111111111111111001',
      +  '1111111111111111111111111010',
      +  '1111111111111111111111111011',
      +  '010100',
      +  '1111111000',
      +  '1111111001',
      +  '111111111010',
      +  '1111111111001',
      +  '010101',
      +  '11111000',
      +  '11111111010',
      +  '1111111010',
      +  '1111111011',
      +  '11111001',
      +  '11111111011',
      +  '11111010',
      +  '010110',
      +  '010111',
      +  '011000',
      +  '00000',
      +  '00001',
      +  '00010',
      +  '011001',
      +  '011010',
      +  '011011',
      +  '011100',
      +  '011101',
      +  '011110',
      +  '011111',
      +  '1011100',
      +  '11111011',
      +  '111111111111100',
      +  '100000',
      +  '111111111011',
      +  '1111111100',
      +  '1111111111010',
      +  '100001',
      +  '1011101',
      +  '1011110',
      +  '1011111',
      +  '1100000',
      +  '1100001',
      +  '1100010',
      +  '1100011',
      +  '1100100',
      +  '1100101',
      +  '1100110',
      +  '1100111',
      +  '1101000',
      +  '1101001',
      +  '1101010',
      +  '1101011',
      +  '1101100',
      +  '1101101',
      +  '1101110',
      +  '1101111',
      +  '1110000',
      +  '1110001',
      +  '1110010',
      +  '11111100',
      +  '1110011',
      +  '11111101',
      +  '1111111111011',
      +  '1111111111111110000',
      +  '1111111111100',
      +  '11111111111100',
      +  '100010',
      +  '111111111111101',
      +  '00011',
      +  '100011',
      +  '00100',
      +  '100100',
      +  '00101',
      +  '100101',
      +  '100110',
      +  '100111',
      +  '00110',
      +  '1110100',
      +  '1110101',
      +  '101000',
      +  '101001',
      +  '101010',
      +  '00111',
      +  '101011',
      +  '1110110',
      +  '101100',
      +  '01000',
      +  '01001',
      +  '101101',
      +  '1110111',
      +  '1111000',
      +  '1111001',
      +  '1111010',
      +  '1111011',
      +  '111111111111110',
      +  '11111111100',
      +  '11111111111101',
      +  '1111111111101',
      +  '1111111111111111111111111100',
      +  '11111111111111100110',
      +  '1111111111111111010010',
      +  '11111111111111100111',
      +  '11111111111111101000',
      +  '1111111111111111010011',
      +  '1111111111111111010100',
      +  '1111111111111111010101',
      +  '11111111111111111011001',
      +  '1111111111111111010110',
      +  '11111111111111111011010',
      +  '11111111111111111011011',
      +  '11111111111111111011100',
      +  '11111111111111111011101',
      +  '11111111111111111011110',
      +  '111111111111111111101011',
      +  '11111111111111111011111',
      +  '111111111111111111101100',
      +  '111111111111111111101101',
      +  '1111111111111111010111',
      +  '11111111111111111100000',
      +  '111111111111111111101110',
      +  '11111111111111111100001',
      +  '11111111111111111100010',
      +  '11111111111111111100011',
      +  '11111111111111111100100',
      +  '111111111111111011100',
      +  '1111111111111111011000',
      +  '11111111111111111100101',
      +  '1111111111111111011001',
      +  '11111111111111111100110',
      +  '11111111111111111100111',
      +  '111111111111111111101111',
      +  '1111111111111111011010',
      +  '111111111111111011101',
      +  '11111111111111101001',
      +  '1111111111111111011011',
      +  '1111111111111111011100',
      +  '11111111111111111101000',
      +  '11111111111111111101001',
      +  '111111111111111011110',
      +  '11111111111111111101010',
      +  '1111111111111111011101',
      +  '1111111111111111011110',
      +  '111111111111111111110000',
      +  '111111111111111011111',
      +  '1111111111111111011111',
      +  '11111111111111111101011',
      +  '11111111111111111101100',
      +  '111111111111111100000',
      +  '111111111111111100001',
      +  '1111111111111111100000',
      +  '111111111111111100010',
      +  '11111111111111111101101',
      +  '1111111111111111100001',
      +  '11111111111111111101110',
      +  '11111111111111111101111',
      +  '11111111111111101010',
      +  '1111111111111111100010',
      +  '1111111111111111100011',
      +  '1111111111111111100100',
      +  '11111111111111111110000',
      +  '1111111111111111100101',
      +  '1111111111111111100110',
      +  '11111111111111111110001',
      +  '11111111111111111111100000',
      +  '11111111111111111111100001',
      +  '11111111111111101011',
      +  '1111111111111110001',
      +  '1111111111111111100111',
      +  '11111111111111111110010',
      +  '1111111111111111101000',
      +  '1111111111111111111101100',
      +  '11111111111111111111100010',
      +  '11111111111111111111100011',
      +  '11111111111111111111100100',
      +  '111111111111111111111011110',
      +  '111111111111111111111011111',
      +  '11111111111111111111100101',
      +  '111111111111111111110001',
      +  '1111111111111111111101101',
      +  '1111111111111110010',
      +  '111111111111111100011',
      +  '11111111111111111111100110',
      +  '111111111111111111111100000',
      +  '111111111111111111111100001',
      +  '11111111111111111111100111',
      +  '111111111111111111111100010',
      +  '111111111111111111110010',
      +  '111111111111111100100',
      +  '111111111111111100101',
      +  '11111111111111111111101000',
      +  '11111111111111111111101001',
      +  '1111111111111111111111111101',
      +  '111111111111111111111100011',
      +  '111111111111111111111100100',
      +  '111111111111111111111100101',
      +  '11111111111111101100',
      +  '111111111111111111110011',
      +  '11111111111111101101',
      +  '111111111111111100110',
      +  '1111111111111111101001',
      +  '111111111111111100111',
      +  '111111111111111101000',
      +  '11111111111111111110011',
      +  '1111111111111111101010',
      +  '1111111111111111101011',
      +  '1111111111111111111101110',
      +  '1111111111111111111101111',
      +  '111111111111111111110100',
      +  '111111111111111111110101',
      +  '11111111111111111111101010',
      +  '11111111111111111110100',
      +  '11111111111111111111101011',
      +  '111111111111111111111100110',
      +  '11111111111111111111101100',
      +  '11111111111111111111101101',
      +  '111111111111111111111100111',
      +  '111111111111111111111101000',
      +  '111111111111111111111101001',
      +  '111111111111111111111101010',
      +  '111111111111111111111101011',
      +  '1111111111111111111111111110',
      +  '111111111111111111111101100',
      +  '111111111111111111111101101',
      +  '111111111111111111111101110',
      +  '111111111111111111111101111',
      +  '111111111111111111111110000',
      +  '11111111111111111111101110',
      +  '111111111111111111111111111111'
      +]);
      + +
    • + + +
    • +
      + +
      + +
      +

      String literal representation

      +

      Literal strings can represent header names or header values. There’s two variant of the +string encoding:

      +

      String literal with Huffman encoding:

      +
        0   1   2   3   4   5   6   7
      ++---+---+---+---+---+---+---+---+
      +| 1 |  Value Length Prefix (7)  |
      ++---+---+---+---+---+---+---+---+
      +|   Value Length (0-N bytes)    |
      ++---+---+---+---+---+---+---+---+
      +...
      ++---+---+---+---+---+---+---+---+
      +| Huffman Encoded Data  |Padding|
      ++---+---+---+---+---+---+---+---+
      +

      String literal without Huffman encoding:

      +
        0   1   2   3   4   5   6   7
      ++---+---+---+---+---+---+---+---+
      +| 0 |  Value Length Prefix (7)  |
      ++---+---+---+---+---+---+---+---+
      +|   Value Length (0-N bytes)    |
      ++---+---+---+---+---+---+---+---+
      +...
      ++---+---+---+---+---+---+---+---+
      +|  Field Bytes Without Encoding |
      ++---+---+---+---+---+---+---+---+
      +
      +
      + +
      +HeaderSetCompressor.string = function writeString(str) {
      +  str = new Buffer(str, 'utf8');
      +
      +  var huffman = HuffmanTable.huffmanTable.encode(str);
      +  if (huffman.length < str.length) {
      +    var length = HeaderSetCompressor.integer(huffman.length, 7);
      +    length[0][0] |= 128;
      +    return length.concat(huffman);
      +  }
      +
      +  else {
      +    length = HeaderSetCompressor.integer(str.length, 7);
      +    return length.concat(str);
      +  }
      +};
      +
      +HeaderSetDecompressor.string = function readString(buffer) {
      +  var huffman = buffer[buffer.cursor] & 128;
      +  var length = HeaderSetDecompressor.integer(buffer, 7);
      +  var encoded = buffer.slice(buffer.cursor, buffer.cursor + length);
      +  buffer.cursor += length;
      +  return (huffman ? HuffmanTable.huffmanTable.decode(encoded) : encoded).toString('utf8');
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Header represenations

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The JavaScript object representation is described near the +HeaderSetDecompressor.prototype._execute() method definition.

      +

      All binary header representations start with a prefix signaling the representation type and +an index represented using prefix coded integers:

      +
        0   1   2   3   4   5   6   7
      ++---+---+---+---+---+---+---+---+
      +| 1 |        Index (7+)         |  Indexed Representation
      ++---+---------------------------+
      +
      +  0   1   2   3   4   5   6   7
      ++---+---+---+---+---+---+---+---+
      +| 0 | 1 |      Index (6+)       |
      ++---+---+---+-------------------+  Literal w/ Indexing
      +|       Value Length (8+)       |
      ++-------------------------------+  w/ Indexed Name
      +| Value String (Length octets)  |
      ++-------------------------------+
      +
      +  0   1   2   3   4   5   6   7
      ++---+---+---+---+---+---+---+---+
      +| 0 | 1 |           0           |
      ++---+---+---+-------------------+
      +|       Name Length (8+)        |
      ++-------------------------------+  Literal w/ Indexing
      +|  Name String (Length octets)  |
      ++-------------------------------+  w/ New Name
      +|       Value Length (8+)       |
      ++-------------------------------+
      +| Value String (Length octets)  |
      ++-------------------------------+
      +
      +  0   1   2   3   4   5   6   7
      ++---+---+---+---+---+---+---+---+
      +| 0 | 0 | 0 | 0 |  Index (4+)   |
      ++---+---+---+-------------------+  Literal w/o Incremental Indexing
      +|       Value Length (8+)       |
      ++-------------------------------+  w/ Indexed Name
      +| Value String (Length octets)  |
      ++-------------------------------+
      +
      +  0   1   2   3   4   5   6   7
      ++---+---+---+---+---+---+---+---+
      +| 0 | 0 | 0 | 0 |       0       |
      ++---+---+---+-------------------+
      +|       Name Length (8+)        |
      ++-------------------------------+  Literal w/o Incremental Indexing
      +|  Name String (Length octets)  |
      ++-------------------------------+  w/ New Name
      +|       Value Length (8+)       |
      ++-------------------------------+
      +| Value String (Length octets)  |
      ++-------------------------------+
      +
      +  0   1   2   3   4   5   6   7
      ++---+---+---+---+---+---+---+---+
      +| 0 | 0 | 0 | 1 |  Index (4+)   |
      ++---+---+---+-------------------+  Literal never indexed
      +|       Value Length (8+)       |
      ++-------------------------------+  w/ Indexed Name
      +| Value String (Length octets)  |
      ++-------------------------------+
      +
      +  0   1   2   3   4   5   6   7
      ++---+---+---+---+---+---+---+---+
      +| 0 | 0 | 0 | 1 |       0       |
      ++---+---+---+-------------------+
      +|       Name Length (8+)        |
      ++-------------------------------+  Literal never indexed
      +|  Name String (Length octets)  |
      ++-------------------------------+  w/ New Name
      +|       Value Length (8+)       |
      ++-------------------------------+
      +| Value String (Length octets)  |
      ++-------------------------------+
      +

      The Indexed Representation consists of the 1-bit prefix and the Index that is represented as +a 7-bit prefix coded integer and nothing else.

      +

      After the first bits, all literal representations specify the header name, either as a +pointer to the Header Table (Index) or a string literal. When the string literal representation +is used, the Index is set to 0 and the string literal starts at the second byte.

      +

      For all literal representations, the specification of the header value comes next. It is +always represented as a string.

      + +
      + +
      +var representations = {
      +  indexed             : { prefix: 7, pattern: 0x80 },
      +  literalIncremental  : { prefix: 6, pattern: 0x40 },
      +  contextUpdate       : { prefix: 0, pattern: 0x20 },
      +  literalNeverIndexed : { prefix: 4, pattern: 0x10 },
      +  literal             : { prefix: 4, pattern: 0x00 }
      +};
      +
      +HeaderSetCompressor.header = function writeHeader(header) {
      +  var representation, buffers = [];
      +
      +  if (header.contextUpdate) {
      +    representation = representations.contextUpdate;
      +  } else if (typeof header.value === 'number') {
      +    representation = representations.indexed;
      +  } else if (header.index) {
      +    representation = representations.literalIncremental;
      +  } else if (header.mustNeverIndex) {
      +    representation = representations.literalNeverIndexed;
      +  } else {
      +    representation = representations.literal;
      +  }
      +
      +  if (representation === representations.contextUpdate) {
      +    buffers.push(HeaderSetCompressor.integer(header.newMaxSize, 5));
      +  }
      +
      +  else if (representation === representations.indexed) {
      +    buffers.push(HeaderSetCompressor.integer(header.value + 1, representation.prefix));
      +  }
      +
      +  else {
      +    if (typeof header.name === 'number') {
      +      buffers.push(HeaderSetCompressor.integer(header.name + 1, representation.prefix));
      +    } else {
      +      buffers.push(HeaderSetCompressor.integer(0, representation.prefix));
      +      buffers.push(HeaderSetCompressor.string(header.name));
      +    }
      +    buffers.push(HeaderSetCompressor.string(header.value));
      +  }
      +
      +  buffers[0][0][0] |= representation.pattern;
      +
      +  return Array.prototype.concat.apply([], buffers); // array of arrays of buffers -> array of buffers
      +};
      +
      +HeaderSetDecompressor.header = function readHeader(buffer) {
      +  var representation, header = {};
      +
      +  var firstByte = buffer[buffer.cursor];
      +  if (firstByte & 0x80) {
      +    representation = representations.indexed;
      +  } else if (firstByte & 0x40) {
      +    representation = representations.literalIncremental;
      +  } else if (firstByte & 0x20) {
      +    representation = representations.contextUpdate;
      +  } else if (firstByte & 0x10) {
      +    representation = representations.literalNeverIndexed;
      +  } else {
      +    representation = representations.literal;
      +  }
      +
      +  header.value = header.name = -1;
      +  header.index = false;
      +  header.contextUpdate = false;
      +  header.newMaxSize = 0;
      +  header.mustNeverIndex = false;
      +
      +  if (representation === representations.contextUpdate) {
      +    header.contextUpdate = true;
      +    header.newMaxSize = HeaderSetDecompressor.integer(buffer, 5);
      +  }
      +
      +  else if (representation === representations.indexed) {
      +    header.value = header.name = HeaderSetDecompressor.integer(buffer, representation.prefix) - 1;
      +  }
      +
      +  else {
      +    header.name = HeaderSetDecompressor.integer(buffer, representation.prefix) - 1;
      +    if (header.name === -1) {
      +      header.name = HeaderSetDecompressor.string(buffer);
      +    }
      +    header.value = HeaderSetDecompressor.string(buffer);
      +    header.index = (representation === representations.literalIncremental);
      +    header.mustNeverIndex = (representation === representations.literalNeverIndexed);
      +  }
      +
      +  return header;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Integration with HTTP/2

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      This section describes the interaction between the compressor/decompressor and the rest of the +HTTP/2 implementation. The Compressor and the Decompressor makes up a layer between the +framer and the connection handling component. They let most +frames pass through, except HEADERS and PUSH_PROMISE frames. They convert the frames between +these two representations:

      +
      {                                   {
      + type: 'HEADERS',                    type: 'HEADERS',
      + flags: {},                          flags: {},
      + stream: 1,               <===>      stream: 1,
      + headers: {                          data: Buffer
      +  N1: 'V1',                         }
      +  N2: ['V1', 'V2', ...],
      +  // ...
      + }
      +}
      +

      There are possibly several binary frame that belong to a single non-binary frame.

      + +
      + +
      +var MAX_HTTP_PAYLOAD_SIZE = 16384;
      + +
    • + + +
    • +
      + +
      + +
      +

      The Compressor class

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The Compressor transform stream is basically stateless.

      + +
      + +
      util.inherits(Compressor, TransformStream);
      +function Compressor(log, type) {
      +  TransformStream.call(this, { objectMode: true });
      +
      +  this._log = log.child({ component: 'compressor' });
      +
      +  assert((type === 'REQUEST') || (type === 'RESPONSE'));
      +  this._table = new HeaderTable(this._log);
      +}
      + +
    • + + +
    • +
      + +
      + +
      +

      Changing the header table size

      + +
      + +
      Compressor.prototype.setTableSizeLimit = function setTableSizeLimit(size) {
      +  this._table.setSizeLimit(size);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      compress takes a header set, and compresses it using a new HeaderSetCompressor stream +instance. This means that from now on, the advantages of streaming header encoding are lost, +but the API becomes simpler.

      + +
      + +
      Compressor.prototype.compress = function compress(headers) {
      +  var compressor = new HeaderSetCompressor(this._log, this._table);
      +  var colonHeaders = [];
      +  var nonColonHeaders = [];
      + +
    • + + +
    • +
      + +
      + +
      +

      To ensure we send colon headers first

      + +
      + +
        for (var name in headers) {
      +    if (name.trim()[0] === ':') {
      +      colonHeaders.push(name);
      +    } else {
      +      nonColonHeaders.push(name);
      +    }
      +  }
      +
      +  function compressHeader(name) {
      +    var value = headers[name];
      +    name = String(name).toLowerCase();
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • To allow for better compression efficiency, the Cookie header field MAY be split into +separate header fields, each with one or more cookie-pairs.
      • +
      + +
      + +
          if (name == 'cookie') {
      +      if (!(value instanceof Array)) {
      +        value = [value];
      +      }
      +      value = Array.prototype.concat.apply([], value.map(function(cookie) {
      +        return String(cookie).split(';').map(trim);
      +      }));
      +    }
      +
      +    if (value instanceof Array) {
      +      for (var i = 0; i < value.length; i++) {
      +        compressor.write([name, String(value[i])]);
      +      }
      +    } else {
      +      compressor.write([name, String(value)]);
      +    }
      +  }
      +
      +  colonHeaders.forEach(compressHeader);
      +  nonColonHeaders.forEach(compressHeader);
      +
      +  compressor.end();
      +
      +  var chunk, chunks = [];
      +  while (chunk = compressor.read()) {
      +    chunks.push(chunk);
      +  }
      +  return concat(chunks);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      When a frame arrives

      + +
      + +
      Compressor.prototype._transform = function _transform(frame, encoding, done) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • and it is a HEADERS or PUSH_PROMISE frame
          +
        • it generates a header block using the compress method
        • +
        • cuts the header block into chunks that are not larger than MAX_HTTP_PAYLOAD_SIZE
        • +
        • for each chunk, it pushes out a chunk frame that is identical to the original, except +the data property which holds the given chunk, the type of the frame which is always +CONTINUATION except for the first frame, and the END_HEADERS/END_PUSH_STREAM flag that +marks the last frame and the END_STREAM flag which is always false before the end
        • +
        +
      • +
      + +
      + +
        if (frame.type === 'HEADERS' || frame.type === 'PUSH_PROMISE') {
      +    var buffer = this.compress(frame.headers);
      + +
    • + + +
    • +
      + +
      + +
      +

      This will result in CONTINUATIONs from a PUSH_PROMISE being 4 bytes shorter than they could +be, but that’s not the end of the world, and it prevents us from going over MAX_HTTP_PAYLOAD_SIZE +on the initial PUSH_PROMISE frame.

      + +
      + +
          var adjustment = frame.type === 'PUSH_PROMISE' ? 4 : 0;
      +    var chunks = cut(buffer, MAX_HTTP_PAYLOAD_SIZE - adjustment);
      +
      +    for (var i = 0; i < chunks.length; i++) {
      +      var chunkFrame;
      +      var first = (i === 0);
      +      var last = (i === chunks.length - 1);
      +
      +      if (first) {
      +        chunkFrame = util._extend({}, frame);
      +        chunkFrame.flags = util._extend({}, frame.flags);
      +        chunkFrame.flags['END_' + frame.type] = last;
      +      } else {
      +        chunkFrame = {
      +          type: 'CONTINUATION',
      +          flags: { END_HEADERS: last },
      +          stream: frame.stream
      +        };
      +      }
      +      chunkFrame.data = chunks[i];
      +
      +      this.push(chunkFrame);
      +    }
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • otherwise, the frame is forwarded without taking any action
      • +
      + +
      + +
        else {
      +    this.push(frame);
      +  }
      +
      +  done();
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The Decompressor class

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The Decompressor is a stateful transform stream, since it has to collect multiple frames first, +and the decoding comes after unifying the payload of those frames.

      +

      If there’s a frame in progress, this._inProgress is true. The frames are collected in +this._frames, and the type of the frame and the stream identifier is stored in this._type +and this._stream respectively.

      + +
      + +
      util.inherits(Decompressor, TransformStream);
      +function Decompressor(log, type) {
      +  TransformStream.call(this, { objectMode: true });
      +
      +  this._log = log.child({ component: 'compressor' });
      +
      +  assert((type === 'REQUEST') || (type === 'RESPONSE'));
      +  this._table = new HeaderTable(this._log);
      +
      +  this._inProgress = false;
      +  this._base = undefined;
      +}
      + +
    • + + +
    • +
      + +
      + +
      +

      Changing the header table size

      + +
      + +
      Decompressor.prototype.setTableSizeLimit = function setTableSizeLimit(size) {
      +  this._table.setSizeLimit(size);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      decompress takes a full header block, and decompresses it using a new HeaderSetDecompressor +stream instance. This means that from now on, the advantages of streaming header decoding are +lost, but the API becomes simpler.

      + +
      + +
      Decompressor.prototype.decompress = function decompress(block) {
      +  var decompressor = new HeaderSetDecompressor(this._log, this._table);
      +  decompressor.end(block);
      +
      +  var seenNonColonHeader = false;
      +  var headers = {};
      +  var pair;
      +  while (pair = decompressor.read()) {
      +    var name = pair[0];
      +    var value = pair[1];
      +    var isColonHeader = (name.trim()[0] === ':');
      +    if (seenNonColonHeader && isColonHeader) {
      +        this.emit('error', 'PROTOCOL_ERROR');
      +        return headers;
      +    }
      +    seenNonColonHeader = !isColonHeader;
      +    if (name in headers) {
      +      if (headers[name] instanceof Array) {
      +        headers[name].push(value);
      +      } else {
      +        headers[name] = [headers[name], value];
      +      }
      +    } else {
      +      headers[name] = value;
      +    }
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • If there are multiple Cookie header fields after decompression, these MUST be concatenated +into a single octet string using the two octet delimiter of 0x3B, 0x20 (the ASCII +string “; “).
      • +
      + +
      + +
        if (('cookie' in headers) && (headers['cookie'] instanceof Array)) {
      +    headers['cookie'] = headers['cookie'].join('; ');
      +  }
      +
      +  return headers;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      When a frame arrives

      + +
      + +
      Decompressor.prototype._transform = function _transform(frame, encoding, done) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • and the collection process is already _inProgress, the frame is simply stored, except if +it’s an illegal frame
      • +
      + +
      + +
        if (this._inProgress) {
      +    if ((frame.type !== 'CONTINUATION') || (frame.stream !== this._base.stream)) {
      +      this._log.error('A series of HEADER frames were not continuous');
      +      this.emit('error', 'PROTOCOL_ERROR');
      +      return;
      +    }
      +    this._frames.push(frame);
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • and the collection process is not _inProgress, but the new frame’s type is HEADERS or +PUSH_PROMISE, a new collection process begins
      • +
      + +
      + +
        else if ((frame.type === 'HEADERS') || (frame.type === 'PUSH_PROMISE')) {
      +    this._inProgress = true;
      +    this._base = util._extend({}, frame);
      +    this._frames = [frame];
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • otherwise, the frame is forwarded without taking any action
      • +
      + +
      + +
        else {
      +    this.push(frame);
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • When the frame signals that it’s the last in the series, the header block chunks are +concatenated, the headers are decompressed, and a new frame gets pushed out with the +decompressed headers.
      • +
      + +
      + +
        if (this._inProgress && (frame.flags.END_HEADERS || frame.flags.END_PUSH_PROMISE)) {
      +    var buffer = concat(this._frames.map(function(frame) {
      +      return frame.data;
      +    }));
      +    try {
      +      var headers = this.decompress(buffer);
      +    } catch(error) {
      +      this._log.error({ err: error }, 'Header decompression error');
      +      this.emit('error', 'COMPRESSION_ERROR');
      +      return;
      +    }
      +    this.push(util._extend(this._base, { headers: headers }));
      +    this._inProgress = false;
      +  }
      +
      +  done();
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Helper functions

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Concatenate an array of buffers into a new buffer

      + +
      + +
      function concat(buffers) {
      +  var size = 0;
      +  for (var i = 0; i < buffers.length; i++) {
      +    size += buffers[i].length;
      +  }
      +
      +  var concatenated = new Buffer(size);
      +  for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) {
      +    buffers[j].copy(concatenated, cursor);
      +  }
      +
      +  return concatenated;
      +}
      + +
    • + + +
    • +
      + +
      + +
      +

      Cut buffer into chunks not larger than size

      + +
      + +
      function cut(buffer, size) {
      +  var chunks = [];
      +  var cursor = 0;
      +  do {
      +    var chunkSize = Math.min(size, buffer.length - cursor);
      +    chunks.push(buffer.slice(cursor, cursor + chunkSize));
      +    cursor += chunkSize;
      +  } while(cursor < buffer.length);
      +  return chunks;
      +}
      +
      +function trim(string) {
      +  return string.trim();
      +}
      + +
    • + +
    +
    + + diff --git a/doc/protocol/connection.html b/doc/protocol/connection.html new file mode 100644 index 00000000..39a4a78a --- /dev/null +++ b/doc/protocol/connection.html @@ -0,0 +1,1759 @@ + + + + + The Connection class + + + + + +
    +
    + + + +
      + + + +
    • +
      + +
      + +
      + +
      + +
      var assert = require('assert');
      + +
    • + + +
    • +
      + +
      + +
      +

      The Connection class

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The Connection class manages HTTP/2 connections. Each instance corresponds to one transport +stream (TCP stream). It operates by sending and receiving frames and is implemented as a +Flow subclass.

      + +
      + +
      +var Flow = require('./flow').Flow;
      +
      +exports.Connection = Connection;
      + +
    • + + +
    • +
      + +
      + +
      +

      Public API

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • new Connection(log, firstStreamId, settings): create a new Connection

        +
      • +
      • Event: ‘error’ (type): signals a connection level error made by the other end

        +
      • +
      • Event: ‘peerError’ (type): signals the receipt of a GOAWAY frame that contains an error +code other than NO_ERROR

        +
      • +
      • Event: ‘stream’ (stream): signals that there’s an incoming stream

        +
      • +
      • createStream(): stream: initiate a new stream

        +
      • +
      • set(settings, callback): change the value of one or more settings according to the +key-value pairs of settings. The callback is called after the peer acknowledged the changes.

        +
      • +
      • ping([callback]): send a ping and call callback when the answer arrives

        +
      • +
      • close([error]): close the stream with an error code

        +
      • +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Constructor

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The main aspects of managing the connection are:

      + +
      + +
      function Connection(log, firstStreamId, settings) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • initializing the base class
      • +
      + +
      + +
        Flow.call(this, 0);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • logging: every method uses the common logger object
      • +
      + +
      + +
        this._log = log.child({ component: 'connection' });
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • stream management
      • +
      + +
      + +
        this._initializeStreamManagement(firstStreamId);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • lifecycle management
      • +
      + +
      + +
        this._initializeLifecycleManagement();
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • flow control
      • +
      + +
      + +
        this._initializeFlowControl();
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • settings management
      • +
      + +
      + +
        this._initializeSettingsManagement(settings);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • multiplexing
      • +
      + +
      + +
        this._initializeMultiplexing();
      +}
      +Connection.prototype = Object.create(Flow.prototype, { constructor: { value: Connection } });
      + +
    • + + +
    • +
      + +
      + +
      +

      Overview

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +
               |    ^             |    ^
      +         v    |             v    |
      +    +--------------+   +--------------+
      ++---|   stream1    |---|   stream2    |----      ....      ---+
      +|   | +----------+ |   | +----------+ |                       |
      +|   | | stream1. | |   | | stream2. | |                       |
      +|   +-| upstream |-+   +-| upstream |-+                       |
      +|     +----------+       +----------+                         |
      +|       |     ^             |     ^                           |
      +|       v     |             v     |                           |
      +|       +-----+-------------+-----+--------      ....         |
      +|       ^     |             |     |                           |
      +|       |     v             |     |                           |
      +|   +--------------+        |     |                           |
      +|   |   stream0    |        |     |                           |
      +|   |  connection  |        |     |                           |
      +|   |  management  |     multiplexing                         |
      +|   +--------------+     flow control                         |
      +|                           |     ^                           |
      +|                   _read() |     | _write()                  |
      +|                           v     |                           |
      +|                +------------+ +-----------+                 |
      +|                |output queue| |input queue|                 |
      ++----------------+------------+-+-----------+-----------------+
      +                            |     ^
      +                     read() |     | write()
      +                            v     |
      +
      +
      + +
    • + + +
    • +
      + +
      + +
      +

      Stream management

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
      +var Stream  = require('./stream').Stream;
      + +
    • + + +
    • +
      + +
      + +
      +

      Initialization:

      + +
      + +
      Connection.prototype._initializeStreamManagement = function _initializeStreamManagement(firstStreamId) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • streams are stored in two data structures:
          +
        • _streamIds is an id -> stream map of the streams that are allowed to receive frames.
        • +
        • _streamPriorities is a priority -> [stream] map of stream that allowed to send frames.
        • +
        +
      • +
      + +
      + +
        this._streamIds = [];
      +  this._streamPriorities = [];
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • The next outbound stream ID and the last inbound stream id
      • +
      + +
      + +
        this._nextStreamId = firstStreamId;
      +  this._lastIncomingStream = 0;
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Calling _writeControlFrame when there’s an incoming stream with 0 as stream ID
      • +
      + +
      + +
        this._streamIds[0] = { upstream: { write: this._writeControlFrame.bind(this) } };
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • By default, the number of concurrent outbound streams is not limited. The _streamLimit can +be set by the SETTINGS_MAX_CONCURRENT_STREAMS setting.
      • +
      + +
      + +
        this._streamSlotsFree = Infinity;
      +  this._streamLimit = Infinity;
      +  this.on('RECEIVING_SETTINGS_MAX_CONCURRENT_STREAMS', this._updateStreamLimit);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      _writeControlFrame is called when there’s an incoming frame in the _control stream. It +broadcasts the message by creating an event on it.

      + +
      + +
      Connection.prototype._writeControlFrame = function _writeControlFrame(frame) {
      +  if ((frame.type === 'SETTINGS') || (frame.type === 'PING') ||
      +      (frame.type === 'GOAWAY') || (frame.type === 'WINDOW_UPDATE') ||
      +      (frame.type === 'ALTSVC')) {
      +    this._log.debug({ frame: frame }, 'Receiving connection level frame');
      +    this.emit(frame.type, frame);
      +  } else {
      +    this._log.error({ frame: frame }, 'Invalid connection level frame');
      +    this.emit('error', 'PROTOCOL_ERROR');
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Methods to manage the stream slot pool:

      + +
      + +
      Connection.prototype._updateStreamLimit = function _updateStreamLimit(newStreamLimit) {
      +  var wakeup = (this._streamSlotsFree === 0) && (newStreamLimit > this._streamLimit);
      +  this._streamSlotsFree += newStreamLimit - this._streamLimit;
      +  this._streamLimit = newStreamLimit;
      +  if (wakeup) {
      +    this.emit('wakeup');
      +  }
      +};
      +
      +Connection.prototype._changeStreamCount = function _changeStreamCount(change) {
      +  if (change) {
      +    this._log.trace({ free: this._streamSlotsFree, change: change },
      +                    'Changing active stream count.');
      +    var wakeup = (this._streamSlotsFree === 0) && (change < 0);
      +    this._streamSlotsFree -= change;
      +    if (wakeup) {
      +      this.emit('wakeup');
      +    }
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Creating a new inbound or outbound stream with the given id (which is undefined in case of +an outbound stream) consists of three steps:

      +
        +
      1. var stream = new Stream(this._log, this);
      2. +
      3. this._allocateId(stream, id);
      4. +
      5. this._allocatePriority(stream);
      6. +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Allocating an ID to a stream

      + +
      + +
      Connection.prototype._allocateId = function _allocateId(stream, id) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • initiated stream without definite ID
      • +
      + +
      + +
        if (id === undefined) {
      +    id = this._nextStreamId;
      +    this._nextStreamId += 2;
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • incoming stream with a legitim ID (larger than any previous and different parity than ours)
      • +
      + +
      + +
        else if ((id > this._lastIncomingStream) && ((id - this._nextStreamId) % 2 !== 0)) {
      +    this._lastIncomingStream = id;
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • incoming stream with invalid ID
      • +
      + +
      + +
        else {
      +    this._log.error({ stream_id: id, lastIncomingStream: this._lastIncomingStream },
      +                    'Invalid incoming stream ID.');
      +    this.emit('error', 'PROTOCOL_ERROR');
      +    return undefined;
      +  }
      +
      +  assert(!(id in this._streamIds));
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • adding to this._streamIds
      • +
      + +
      + +
        this._log.trace({ s: stream, stream_id: id }, 'Allocating ID for stream.');
      +  this._streamIds[id] = stream;
      +  stream.id = id;
      +  this.emit('new_stream', stream, id);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • forwarding connection errors from streams
      • +
      + +
      + +
        stream.on('connectionError', this.emit.bind(this, 'error'));
      +
      +  return id;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Allocating a priority to a stream, and managing priority changes

      + +
      + +
      Connection.prototype._allocatePriority = function _allocatePriority(stream) {
      +  this._log.trace({ s: stream }, 'Allocating priority for stream.');
      +  this._insert(stream, stream._priority);
      +  stream.on('priority', this._reprioritize.bind(this, stream));
      +  stream.upstream.on('readable', this.emit.bind(this, 'wakeup'));
      +  this.emit('wakeup');
      +};
      +
      +Connection.prototype._insert = function _insert(stream, priority) {
      +  if (priority in this._streamPriorities) {
      +    this._streamPriorities[priority].push(stream);
      +  } else {
      +    this._streamPriorities[priority] = [stream];
      +  }
      +};
      +
      +Connection.prototype._reprioritize = function _reprioritize(stream, priority) {
      +  var bucket = this._streamPriorities[stream._priority];
      +  var index = bucket.indexOf(stream);
      +  assert(index !== -1);
      +  bucket.splice(index, 1);
      +  if (bucket.length === 0) {
      +    delete this._streamPriorities[stream._priority];
      +  }
      +
      +  this._insert(stream, priority);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Creating an inbound stream with the given ID. It is called when there’s an incoming frame to +a previously nonexistent stream.

      + +
      + +
      Connection.prototype._createIncomingStream = function _createIncomingStream(id) {
      +  this._log.debug({ stream_id: id }, 'New incoming stream.');
      +
      +  var stream = new Stream(this._log, this);
      +  this._allocateId(stream, id);
      +  this._allocatePriority(stream);
      +  this.emit('stream', stream, id);
      +
      +  return stream;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Creating an outbound stream

      + +
      + +
      Connection.prototype.createStream = function createStream() {
      +  this._log.trace('Creating new outbound stream.');
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Receiving is enabled immediately, and an ID gets assigned to the stream
      • +
      + +
      + +
        var stream = new Stream(this._log, this);
      +  this._allocatePriority(stream);
      +
      +  return stream;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Multiplexing

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
      +Connection.prototype._initializeMultiplexing = function _initializeMultiplexing() {
      +  this.on('window_update', this.emit.bind(this, 'wakeup'));
      +  this._sendScheduled = false;
      +  this._firstFrameReceived = false;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The _send method is a virtual method of the Flow class that has to be implemented +by child classes. It reads frames from streams and pushes them to the output buffer.

      + +
      + +
      Connection.prototype._send = function _send(immediate) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Do not do anything if the connection is already closed
      • +
      + +
      + +
        if (this._closed) {
      +    return;
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Collapsing multiple calls in a turn into a single deferred call
      • +
      + +
      + +
        if (immediate) {
      +    this._sendScheduled = false;
      +  } else {
      +    if (!this._sendScheduled) {
      +      this._sendScheduled = true;
      +      setImmediate(this._send.bind(this, true));
      +    }
      +    return;
      +  }
      +
      +  this._log.trace('Starting forwarding frames from streams.');
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Looping through priority buckets in priority order.
      • +
      + +
      + +
      priority_loop:
      +  for (var priority in this._streamPriorities) {
      +    var bucket = this._streamPriorities[priority];
      +    var nextBucket = [];
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Forwarding frames from buckets with round-robin scheduling.
          +
        1. pulling out frame
        2. +
        3. if there’s no frame, skip this stream
        4. +
        5. if forwarding this frame would make streamCount greater than streamLimit, skip +this stream
        6. +
        7. adding stream to the bucket of the next round
        8. +
        9. assigning an ID to the frame (allocating an ID to the stream if there isn’t already)
        10. +
        11. if forwarding a PUSH_PROMISE, allocate ID to the promised stream
        12. +
        13. forwarding the frame, changing streamCount as appropriate
        14. +
        15. stepping to the next stream if there’s still more frame needed in the output buffer
        16. +
        17. switching to the bucket of the next round
        18. +
        +
      • +
      + +
      + +
          while (bucket.length > 0) {
      +      for (var index = 0; index < bucket.length; index++) {
      +        var stream = bucket[index];
      +        var frame = stream.upstream.read((this._window > 0) ? this._window : -1);
      +
      +        if (!frame) {
      +          continue;
      +        } else if (frame.count_change > this._streamSlotsFree) {
      +          stream.upstream.unshift(frame);
      +          continue;
      +        }
      +
      +        nextBucket.push(stream);
      +
      +        if (frame.stream === undefined) {
      +          frame.stream = stream.id || this._allocateId(stream);
      +        }
      +
      +        if (frame.type === 'PUSH_PROMISE') {
      +          this._allocatePriority(frame.promised_stream);
      +          frame.promised_stream = this._allocateId(frame.promised_stream);
      +        }
      +
      +        this._log.trace({ s: stream, frame: frame }, 'Forwarding outgoing frame');
      +        var moreNeeded = this.push(frame);
      +        this._changeStreamCount(frame.count_change);
      +
      +        assert(moreNeeded !== null); // The frame shouldn't be unforwarded
      +        if (moreNeeded === false) {
      +          break priority_loop;
      +        }
      +      }
      +
      +      bucket = nextBucket;
      +      nextBucket = [];
      +    }
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • if we couldn’t forward any frame, then sleep until window update, or some other wakeup event
      • +
      + +
      + +
        if (moreNeeded === undefined) {
      +    this.once('wakeup', this._send.bind(this));
      +  }
      +
      +  this._log.trace({ moreNeeded: moreNeeded }, 'Stopping forwarding frames from streams.');
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The _receive method is another virtual method of the Flow class that has to be +implemented by child classes. It forwards the given frame to the appropriate stream:

      + +
      + +
      Connection.prototype._receive = function _receive(frame, done) {
      +  this._log.trace({ frame: frame }, 'Forwarding incoming frame');
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • first frame needs to be checked by the _onFirstFrameReceived method
      • +
      + +
      + +
        if (!this._firstFrameReceived) {
      +    this._firstFrameReceived = true;
      +    this._onFirstFrameReceived(frame);
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +

      Do some sanity checking here before we create a stream

      + +
      + +
        if ((frame.type == 'SETTINGS' ||
      +       frame.type == 'PING' ||
      +       frame.type == 'GOAWAY') &&
      +      frame.stream != 0) {
      + +
    • + + +
    • +
      + +
      + +
      +

      Got connection-level frame on a stream - EEP!

      + +
      + +
          this.close('PROTOCOL_ERROR');
      +    return;
      +  } else if ((frame.type == 'DATA' ||
      +              frame.type == 'HEADERS' ||
      +              frame.type == 'PRIORITY' ||
      +              frame.type == 'RST_STREAM' ||
      +              frame.type == 'PUSH_PROMISE' ||
      +              frame.type == 'CONTINUATION') &&
      +             frame.stream == 0) {
      + +
    • + + +
    • +
      + +
      + +
      +

      Got stream-level frame on connection - EEP!

      + +
      + +
          this.close('PROTOCOL_ERROR');
      +    return;
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +

      WINDOW_UPDATE can be on either stream or connection

      + +
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • gets the appropriate stream from the stream registry
      • +
      + +
      + +
        var stream = this._streamIds[frame.stream];
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • or creates one if it’s not in this.streams
      • +
      + +
      + +
        if (!stream) {
      +    stream = this._createIncomingStream(frame.stream);
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • in case of PUSH_PROMISE, replaces the promised stream id with a new incoming stream
      • +
      + +
      + +
        if (frame.type === 'PUSH_PROMISE') {
      +    frame.promised_stream = this._createIncomingStream(frame.promised_stream);
      +  }
      +
      +  frame.count_change = this._changeStreamCount.bind(this);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • and writes it to the stream‘s upstream
      • +
      + +
      + +
        stream.upstream.write(frame);
      +
      +  done();
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Settings management

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
      +var defaultSettings = {
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Settings management initialization:

      + +
      + +
      Connection.prototype._initializeSettingsManagement = function _initializeSettingsManagement(settings) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Setting up the callback queue for setting acknowledgements
      • +
      + +
      + +
        this._settingsAckCallbacks = [];
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Sending the initial settings.
      • +
      + +
      + +
        this._log.debug({ settings: settings },
      +                  'Sending the first SETTINGS frame as part of the connection header.');
      +  this.set(settings || defaultSettings);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Forwarding SETTINGS frames to the _receiveSettings method
      • +
      + +
      + +
        this.on('SETTINGS', this._receiveSettings);
      +  this.on('RECEIVING_SETTINGS_MAX_FRAME_SIZE', this._sanityCheckMaxFrameSize);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Checking that the first frame the other endpoint sends is SETTINGS
      • +
      + +
      + +
      Connection.prototype._onFirstFrameReceived = function _onFirstFrameReceived(frame) {
      +  if ((frame.stream === 0) && (frame.type === 'SETTINGS')) {
      +    this._log.debug('Receiving the first SETTINGS frame as part of the connection header.');
      +  } else {
      +    this._log.fatal({ frame: frame }, 'Invalid connection header: first frame is not SETTINGS.');
      +    this.emit('error', 'PROTOCOL_ERROR');
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Handling of incoming SETTINGS frames.

      + +
      + +
      Connection.prototype._receiveSettings = function _receiveSettings(frame) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • If it’s an ACK, call the appropriate callback
      • +
      + +
      + +
        if (frame.flags.ACK) {
      +    var callback = this._settingsAckCallbacks.shift();
      +    if (callback) {
      +      callback();
      +    }
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • If it’s a setting change request, then send an ACK and change the appropriate settings
      • +
      + +
      + +
        else {
      +    if (!this._closed) {
      +      this.push({
      +        type: 'SETTINGS',
      +        flags: { ACK: true },
      +        stream: 0,
      +        settings: {}
      +      });
      +    }
      +    for (var name in frame.settings) {
      +      this.emit('RECEIVING_' + name, frame.settings[name]);
      +    }
      +  }
      +};
      +
      +Connection.prototype._sanityCheckMaxFrameSize = function _sanityCheckMaxFrameSize(value) {
      +  if ((value < 0x4000) || (value >= 0x01000000)) {
      +    this._log.fatal('Received invalid value for max frame size: ' + value);
      +    this.emit('error');
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Changing one or more settings value and sending out a SETTINGS frame

      + +
      + +
      Connection.prototype.set = function set(settings, callback) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Calling the callback and emitting event when the change is acknowledges
      • +
      + +
      + +
        var self = this;
      +  this._settingsAckCallbacks.push(function() {
      +    for (var name in settings) {
      +      self.emit('ACKNOWLEDGED_' + name, settings[name]);
      +    }
      +    if (callback) {
      +      callback();
      +    }
      +  });
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Sending out the SETTINGS frame
      • +
      + +
      + +
        this.push({
      +    type: 'SETTINGS',
      +    flags: { ACK: false },
      +    stream: 0,
      +    settings: settings
      +  });
      +  for (var name in settings) {
      +    this.emit('SENDING_' + name, settings[name]);
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Lifecycle management

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The main responsibilities of lifecycle management code:

      +
        +
      • keeping the connection alive by
          +
        • sending PINGs when the connection is idle
        • +
        • answering PINGs
        • +
        +
      • +
      • ending the connection
      • +
      + +
      + +
      +Connection.prototype._initializeLifecycleManagement = function _initializeLifecycleManagement() {
      +  this._pings = {};
      +  this.on('PING', this._receivePing);
      +  this.on('GOAWAY', this._receiveGoaway);
      +  this._closed = false;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Generating a string of length 16 with random hexadecimal digits

      + +
      + +
      Connection.prototype._generatePingId = function _generatePingId() {
      +  do {
      +    var id = '';
      +    for (var i = 0; i < 16; i++) {
      +      id += Math.floor(Math.random()*16).toString(16);
      +    }
      +  } while(id in this._pings);
      +  return id;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Sending a ping and calling callback when the answer arrives

      + +
      + +
      Connection.prototype.ping = function ping(callback) {
      +  var id = this._generatePingId();
      +  var data = new Buffer(id, 'hex');
      +  this._pings[id] = callback;
      +
      +  this._log.debug({ data: data }, 'Sending PING.');
      +  this.push({
      +    type: 'PING',
      +    flags: {
      +      ACK: false
      +    },
      +    stream: 0,
      +    data: data
      +  });
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Answering pings

      + +
      + +
      Connection.prototype._receivePing = function _receivePing(frame) {
      +  if (frame.flags.ACK) {
      +    var id = frame.data.toString('hex');
      +    if (id in this._pings) {
      +      this._log.debug({ data: frame.data }, 'Receiving answer for a PING.');
      +      var callback = this._pings[id];
      +      if (callback) {
      +        callback();
      +      }
      +      delete this._pings[id];
      +    } else {
      +      this._log.warn({ data: frame.data }, 'Unsolicited PING answer.');
      +    }
      +
      +  } else {
      +    this._log.debug({ data: frame.data }, 'Answering PING.');
      +    this.push({
      +      type: 'PING',
      +      flags: {
      +        ACK: true
      +      },
      +      stream: 0,
      +      data: frame.data
      +    });
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Terminating the connection

      + +
      + +
      Connection.prototype.close = function close(error) {
      +  if (this._closed) {
      +    this._log.warn('Trying to close an already closed connection');
      +    return;
      +  }
      +
      +  this._log.debug({ error: error }, 'Closing the connection');
      +  this.push({
      +    type: 'GOAWAY',
      +    flags: {},
      +    stream: 0,
      +    last_stream: this._lastIncomingStream,
      +    error: error || 'NO_ERROR'
      +  });
      +  this.push(null);
      +  this._closed = true;
      +};
      +
      +Connection.prototype._receiveGoaway = function _receiveGoaway(frame) {
      +  this._log.debug({ error: frame.error }, 'Other end closed the connection');
      +  this.push(null);
      +  this._closed = true;
      +  if (frame.error !== 'NO_ERROR') {
      +    this.emit('peerError', frame.error);
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Flow control

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
      +Connection.prototype._initializeFlowControl = function _initializeFlowControl() {
      + +
    • + + +
    • +
      + +
      + +
      +

      Handling of initial window size of individual streams.

      + +
      + +
        this._initialStreamWindowSize = INITIAL_STREAM_WINDOW_SIZE;
      +  this.on('new_stream', function(stream) {
      +    stream.upstream.setInitialWindow(this._initialStreamWindowSize);
      +  });
      +  this.on('RECEIVING_SETTINGS_INITIAL_WINDOW_SIZE', this._setInitialStreamWindowSize);
      +  this._streamIds[0].upstream.setInitialWindow = function noop() {};
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The initial connection flow control window is 65535 bytes.

      + +
      + +
      var INITIAL_STREAM_WINDOW_SIZE = 65535;
      + +
    • + + +
    • +
      + +
      + +
      +

      A SETTINGS frame can alter the initial flow control window size for all current streams. When the +value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the window size of all +stream by calling the setInitialStreamWindowSize method. The window size has to be modified by +the difference between the new value and the old value.

      + +
      + +
      Connection.prototype._setInitialStreamWindowSize = function _setInitialStreamWindowSize(size) {
      +  if ((this._initialStreamWindowSize === Infinity) && (size !== Infinity)) {
      +    this._log.error('Trying to manipulate initial flow control window size after flow control was turned off.');
      +    this.emit('error', 'FLOW_CONTROL_ERROR');
      +  } else {
      +    this._log.debug({ size: size }, 'Changing stream initial window size.');
      +    this._initialStreamWindowSize = size;
      +    this._streamIds.forEach(function(stream) {
      +      stream.upstream.setInitialWindow(size);
      +    });
      +  }
      +};
      + +
    • + +
    +
    + + diff --git a/doc/protocol/docco.css b/doc/protocol/docco.css new file mode 100644 index 00000000..8910977e --- /dev/null +++ b/doc/protocol/docco.css @@ -0,0 +1,511 @@ +/*-------------- node-http2 customizations --------------------*/ + +ul.sections > li > div.annotation { + min-width: 35em !important; + max-width: 35em !important; +} + +#background { + width: 35em !important; +} + +/*--------------------- Typography ----------------------------*/ + +@font-face { + font-family: 'aller-light'; + src: url('public/fonts/aller-light.eot'); + src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'), + url('public/fonts/aller-light.woff') format('woff'), + url('public/fonts/aller-light.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'aller-bold'; + src: url('public/fonts/aller-bold.eot'); + src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'), + url('public/fonts/aller-bold.woff') format('woff'), + url('public/fonts/aller-bold.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'novecento-bold'; + src: url('public/fonts/novecento-bold.eot'); + src: url('public/fonts/novecento-bold.eot?#iefix') format('embedded-opentype'), + url('public/fonts/novecento-bold.woff') format('woff'), + url('public/fonts/novecento-bold.ttf') format('truetype'); + font-weight: normal; + font-style: normal; +} + +/*--------------------- Layout ----------------------------*/ +html { height: 100%; } +body { + font-family: "aller-light"; + font-size: 14px; + line-height: 18px; + color: #30404f; + margin: 0; padding: 0; + height:100%; +} +#container { min-height: 100%; } + +a { + color: #000; +} + +b, strong { + font-weight: normal; + font-family: "aller-bold"; +} + +p, ul, ol { + margin: 15px 0 0px; +} + +h1, h2, h3, h4, h5, h6 { + color: #112233; + line-height: 1em; + font-weight: normal; + font-family: "novecento-bold"; + text-transform: uppercase; + margin: 30px 0 15px 0; +} + +h1 { + margin-top: 40px; +} + +hr { + border: 0; + background: 1px solid #ddd; + height: 1px; + margin: 20px 0; +} + +pre, tt, code { + font-size: 12px; line-height: 16px; + font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; + margin: 0; padding: 0; +} + .annotation pre { + display: block; + margin: 0; + padding: 7px 10px; + background: #fcfcfc; + -moz-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); + -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1); + box-shadow: inset 0 0 10px rgba(0,0,0,0.1); + overflow-x: auto; + } + .annotation pre code { + border: 0; + padding: 0; + background: transparent; + } + + +blockquote { + border-left: 5px solid #ccc; + margin: 0; + padding: 1px 0 1px 1em; +} + .sections blockquote p { + font-family: Menlo, Consolas, Monaco, monospace; + font-size: 12px; line-height: 16px; + color: #999; + margin: 10px 0 0; + white-space: pre-wrap; + } + +ul.sections { + list-style: none; + padding:0 0 5px 0;; + margin:0; +} + +/* + Force border-box so that % widths fit the parent + container without overlap because of margin/padding. + + More Info : http://www.quirksmode.org/css/box.html +*/ +ul.sections > li > div { + -moz-box-sizing: border-box; /* firefox */ + -ms-box-sizing: border-box; /* ie */ + -webkit-box-sizing: border-box; /* webkit */ + -khtml-box-sizing: border-box; /* konqueror */ + box-sizing: border-box; /* css3 */ +} + + +/*---------------------- Jump Page -----------------------------*/ +#jump_to, #jump_page { + margin: 0; + background: white; + -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777; + -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px; + font: 16px Arial; + cursor: pointer; + text-align: right; + list-style: none; +} + +#jump_to a { + text-decoration: none; +} + +#jump_to a.large { + display: none; +} +#jump_to a.small { + font-size: 22px; + font-weight: bold; + color: #676767; +} + +#jump_to, #jump_wrapper { + position: fixed; + right: 0; top: 0; + padding: 10px 15px; + margin:0; +} + +#jump_wrapper { + display: none; + padding:0; +} + +#jump_to:hover #jump_wrapper { + display: block; +} + +#jump_page { + padding: 5px 0 3px; + margin: 0 0 25px 25px; +} + +#jump_page .source { + display: block; + padding: 15px; + text-decoration: none; + border-top: 1px solid #eee; +} + +#jump_page .source:hover { + background: #f5f5ff; +} + +#jump_page .source:first-child { +} + +/*---------------------- Low resolutions (> 320px) ---------------------*/ +@media only screen and (min-width: 320px) { + .pilwrap { display: none; } + + ul.sections > li > div { + display: block; + padding:5px 10px 0 10px; + } + + ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { + padding-left: 30px; + } + + ul.sections > li > div.content { + background: #f5f5ff; + overflow-x:auto; + -webkit-box-shadow: inset 0 0 5px #e5e5ee; + box-shadow: inset 0 0 5px #e5e5ee; + border: 1px solid #dedede; + margin:5px 10px 5px 10px; + padding-bottom: 5px; + } + + ul.sections > li > div.annotation pre { + margin: 7px 0 7px; + padding-left: 15px; + } + + ul.sections > li > div.annotation p tt, .annotation code { + background: #f8f8ff; + border: 1px solid #dedede; + font-size: 12px; + padding: 0 0.2em; + } +} + +/*---------------------- (> 481px) ---------------------*/ +@media only screen and (min-width: 481px) { + #container { + position: relative; + } + body { + background-color: #F5F5FF; + font-size: 15px; + line-height: 21px; + } + pre, tt, code { + line-height: 18px; + } + p, ul, ol { + margin: 0 0 15px; + } + + + #jump_to { + padding: 5px 10px; + } + #jump_wrapper { + padding: 0; + } + #jump_to, #jump_page { + font: 10px Arial; + text-transform: uppercase; + } + #jump_page .source { + padding: 5px 10px; + } + #jump_to a.large { + display: inline-block; + } + #jump_to a.small { + display: none; + } + + + + #background { + position: absolute; + top: 0; bottom: 0; + width: 350px; + background: #fff; + border-right: 1px solid #e5e5ee; + z-index: -1; + } + + ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol { + padding-left: 40px; + } + + ul.sections > li { + white-space: nowrap; + } + + ul.sections > li > div { + display: inline-block; + } + + ul.sections > li > div.annotation { + max-width: 350px; + min-width: 350px; + min-height: 5px; + padding: 13px; + overflow-x: hidden; + white-space: normal; + vertical-align: top; + text-align: left; + } + ul.sections > li > div.annotation pre { + margin: 15px 0 15px; + padding-left: 15px; + } + + ul.sections > li > div.content { + padding: 13px; + vertical-align: top; + background: #f5f5ff; + border: none; + -webkit-box-shadow: none; + box-shadow: none; + } + + .pilwrap { + position: relative; + display: inline; + } + + .pilcrow { + font: 12px Arial; + text-decoration: none; + color: #454545; + position: absolute; + top: 3px; left: -20px; + padding: 1px 2px; + opacity: 0; + -webkit-transition: opacity 0.2s linear; + } + .for-h1 .pilcrow { + top: 47px; + } + .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow { + top: 35px; + } + + ul.sections > li > div.annotation:hover .pilcrow { + opacity: 1; + } +} + +/*---------------------- (> 1025px) ---------------------*/ +@media only screen and (min-width: 1025px) { + + body { + font-size: 16px; + line-height: 24px; + } + + #background { + width: 525px; + } + ul.sections > li > div.annotation { + max-width: 525px; + min-width: 525px; + padding: 10px 25px 1px 50px; + } + ul.sections > li > div.content { + padding: 9px 15px 16px 25px; + } +} + +/*---------------------- Syntax Highlighting -----------------------------*/ + +td.linenos { background-color: #f0f0f0; padding-right: 10px; } +span.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; } +/* + +github.com style (c) Vasily Polovnyov + +*/ + +pre code { + display: block; padding: 0.5em; + color: #000; + background: #f8f8ff +} + +pre .comment, +pre .template_comment, +pre .diff .header, +pre .javadoc { + color: #408080; + font-style: italic +} + +pre .keyword, +pre .assignment, +pre .literal, +pre .css .rule .keyword, +pre .winutils, +pre .javascript .title, +pre .lisp .title, +pre .subst { + color: #954121; + /*font-weight: bold*/ +} + +pre .number, +pre .hexcolor { + color: #40a070 +} + +pre .string, +pre .tag .value, +pre .phpdoc, +pre .tex .formula { + color: #219161; +} + +pre .title, +pre .id { + color: #19469D; +} +pre .params { + color: #00F; +} + +pre .javascript .title, +pre .lisp .title, +pre .subst { + font-weight: normal +} + +pre .class .title, +pre .haskell .label, +pre .tex .command { + color: #458; + font-weight: bold +} + +pre .tag, +pre .tag .title, +pre .rules .property, +pre .django .tag .keyword { + color: #000080; + font-weight: normal +} + +pre .attribute, +pre .variable, +pre .instancevar, +pre .lisp .body { + color: #008080 +} + +pre .regexp { + color: #B68 +} + +pre .class { + color: #458; + font-weight: bold +} + +pre .symbol, +pre .ruby .symbol .string, +pre .ruby .symbol .keyword, +pre .ruby .symbol .keymethods, +pre .lisp .keyword, +pre .tex .special, +pre .input_number { + color: #990073 +} + +pre .builtin, +pre .constructor, +pre .built_in, +pre .lisp .title { + color: #0086b3 +} + +pre .preprocessor, +pre .pi, +pre .doctype, +pre .shebang, +pre .cdata { + color: #999; + font-weight: bold +} + +pre .deletion { + background: #fdd +} + +pre .addition { + background: #dfd +} + +pre .diff .change { + background: #0086b3 +} + +pre .chunk { + color: #aaa +} + +pre .tex .formula { + opacity: 0.5; +} diff --git a/doc/protocol/endpoint.html b/doc/protocol/endpoint.html new file mode 100644 index 00000000..e9ca8bd8 --- /dev/null +++ b/doc/protocol/endpoint.html @@ -0,0 +1,718 @@ + + + + + The Endpoint class + + + + + +
    +
    + + + +
      + + + +
    • +
      + +
      + +
      + +
      + +
      var assert = require('assert');
      +
      +var Serializer   = require('./framer').Serializer;
      +var Deserializer = require('./framer').Deserializer;
      +var Compressor   = require('./compressor').Compressor;
      +var Decompressor = require('./compressor').Decompressor;
      +var Connection   = require('./connection').Connection;
      +var Duplex       = require('stream').Duplex;
      +var Transform    = require('stream').Transform;
      +
      +exports.Endpoint = Endpoint;
      + +
    • + + +
    • +
      + +
      + +
      +

      The Endpoint class

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Public API

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • new Endpoint(log, role, settings, filters): create a new Endpoint.

        +
          +
        • log: bunyan logger of the parent
        • +
        • role: ‘CLIENT’ or ‘SERVER’
        • +
        • settings: initial HTTP/2 settings
        • +
        • filters: a map of functions that filter the traffic between components (for debugging or +intentional failure injection).

          +

          Filter functions get three arguments:

          +
            +
          1. frame: the current frame
          2. +
          3. forward(frame): function that can be used to forward a frame to the next component
          4. +
          5. done(): callback to signal the end of the filter process
          6. +
          +

          Valid filter names and their position in the stack:

          +
            +
          • beforeSerialization: after compression, before serialization
          • +
          • beforeCompression: after multiplexing, before compression
          • +
          • afterDeserialization: after deserialization, before decompression
          • +
          • afterDecompression: after decompression, before multiplexing
          • +
          +
        • +
        +
      • +
      • Event: ‘stream’ (Stream): ‘stream’ event forwarded from the underlying Connection

        +
      • +
      • Event: ‘error’ (type): signals an error

        +
      • +
      • createStream(): Stream: initiate a new stream (forwarded to the underlying Connection)

        +
      • +
      • close([error]): close the connection with an error code

        +
      • +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Constructor

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The process of initialization:

      + +
      + +
      function Endpoint(log, role, settings, filters) {
      +  Duplex.call(this);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Initializing logging infrastructure
      • +
      + +
      + +
        this._log = log.child({ component: 'endpoint', e: this });
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • First part of the handshake process: sending and receiving the client connection header +prelude.
      • +
      + +
      + +
        assert((role === 'CLIENT') || role === 'SERVER');
      +  if (role === 'CLIENT') {
      +    this._writePrelude();
      +  } else {
      +    this._readPrelude();
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Initialization of component. This includes the second part of the handshake process: +sending the first SETTINGS frame. This is done by the connection class right after +initialization.
      • +
      + +
      + +
        this._initializeDataFlow(role, settings, filters || {});
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Initialization of management code.
      • +
      + +
      + +
        this._initializeManagement();
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Initializing error handling.
      • +
      + +
      + +
        this._initializeErrorHandling();
      +}
      +Endpoint.prototype = Object.create(Duplex.prototype, { constructor: { value: Endpoint } });
      + +
    • + + +
    • +
      + +
      + +
      +

      Handshake

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
      +var CLIENT_PRELUDE = new Buffer('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');
      + +
    • + + +
    • +
      + +
      + +
      +

      Writing the client header is simple and synchronous.

      + +
      + +
      Endpoint.prototype._writePrelude = function _writePrelude() {
      +  this._log.debug('Sending the client connection header prelude.');
      +  this.push(CLIENT_PRELUDE);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The asynchronous process of reading the client header:

      + +
      + +
      Endpoint.prototype._readPrelude = function _readPrelude() {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • progress in the header is tracker using a cursor
      • +
      + +
      + +
        var cursor = 0;
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • _write is temporarily replaced by the comparator function
      • +
      + +
      + +
        this._write = function _temporalWrite(chunk, encoding, done) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • which compares the stored header with the current chunk byte by byte and emits the +‘error’ event if there’s a byte that doesn’t match
      • +
      + +
      + +
          var offset = cursor;
      +    while(cursor < CLIENT_PRELUDE.length && (cursor - offset) < chunk.length) {
      +      if (CLIENT_PRELUDE[cursor] !== chunk[cursor - offset]) {
      +        this._log.fatal({ cursor: cursor, offset: offset, chunk: chunk },
      +                        'Client connection header prelude does not match.');
      +        this._error('handshake', 'PROTOCOL_ERROR');
      +        return;
      +      }
      +      cursor += 1;
      +    }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • if the whole header is over, and there were no error then restore the original _write +and call it with the remaining part of the current chunk
      • +
      + +
      + +
          if (cursor === CLIENT_PRELUDE.length) {
      +      this._log.debug('Successfully received the client connection header prelude.');
      +      delete this._write;
      +      chunk = chunk.slice(cursor - offset);
      +      this._write(chunk, encoding, done);
      +    }
      +  };
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Data flow

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +
      +---------------------------------------------+
      +|                                             |
      +|   +-------------------------------------+   |
      +|   | +---------+ +---------+ +---------+ |   |
      +|   | | stream1 | | stream2 | |   ...   | |   |
      +|   | +---------+ +---------+ +---------+ |   |
      +|   |             connection              |   |
      +|   +-------------------------------------+   |
      +|             |                 ^             |
      +|        pipe |                 | pipe        |
      +|             v                 |             |
      +|   +------------------+------------------+   |
      +|   |    compressor    |   decompressor   |   |
      +|   +------------------+------------------+   |
      +|             |                 ^             |
      +|        pipe |                 | pipe        |
      +|             v                 |             |
      +|   +------------------+------------------+   |
      +|   |    serializer    |   deserializer   |   |
      +|   +------------------+------------------+   |
      +|             |                 ^             |
      +|     _read() |                 | _write()    |
      +|             v                 |             |
      +|      +------------+     +-----------+       |
      +|      |output queue|     |input queue|       |
      ++------+------------+-----+-----------+-------+
      +              |                 ^
      +       read() |                 | write()
      +              v                 |
      +
      +
      + +
      +function createTransformStream(filter) {
      +  var transform = new Transform({ objectMode: true });
      +  var push = transform.push.bind(transform);
      +  transform._transform = function(frame, encoding, done) {
      +    filter(frame, push, done);
      +  };
      +  return transform;
      +}
      +
      +function pipeAndFilter(stream1, stream2, filter) {
      +  if (filter) {
      +    stream1.pipe(createTransformStream(filter)).pipe(stream2);
      +  } else {
      +    stream1.pipe(stream2);
      +  }
      +}
      +
      +Endpoint.prototype._initializeDataFlow = function _initializeDataFlow(role, settings, filters) {
      +  var firstStreamId, compressorRole, decompressorRole;
      +  if (role === 'CLIENT') {
      +    firstStreamId = 1;
      +    compressorRole = 'REQUEST';
      +    decompressorRole = 'RESPONSE';
      +  } else {
      +    firstStreamId = 2;
      +    compressorRole = 'RESPONSE';
      +    decompressorRole = 'REQUEST';
      +  }
      +
      +  this._serializer   = new Serializer(this._log);
      +  this._deserializer = new Deserializer(this._log);
      +  this._compressor   = new Compressor(this._log, compressorRole);
      +  this._decompressor = new Decompressor(this._log, decompressorRole);
      +  this._connection   = new Connection(this._log, firstStreamId, settings);
      +
      +  pipeAndFilter(this._connection, this._compressor, filters.beforeCompression);
      +  pipeAndFilter(this._compressor, this._serializer, filters.beforeSerialization);
      +  pipeAndFilter(this._deserializer, this._decompressor, filters.afterDeserialization);
      +  pipeAndFilter(this._decompressor, this._connection, filters.afterDecompression);
      +
      +  this._connection.on('ACKNOWLEDGED_SETTINGS_HEADER_TABLE_SIZE',
      +                      this._decompressor.setTableSizeLimit.bind(this._decompressor));
      +  this._connection.on('RECEIVING_SETTINGS_HEADER_TABLE_SIZE',
      +                      this._compressor.setTableSizeLimit.bind(this._compressor));
      +};
      +
      +var noread = {};
      +Endpoint.prototype._read = function _read() {
      +  this._readableState.sync = true;
      +  var moreNeeded = noread, chunk;
      +  while (moreNeeded && (chunk = this._serializer.read())) {
      +    moreNeeded = this.push(chunk);
      +  }
      +  if (moreNeeded === noread) {
      +    this._serializer.once('readable', this._read.bind(this));
      +  }
      +  this._readableState.sync = false;
      +};
      +
      +Endpoint.prototype._write = function _write(chunk, encoding, done) {
      +  this._deserializer.write(chunk, encoding, done);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Management

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
      +Endpoint.prototype._initializeManagement = function _initializeManagement() {
      +  this._connection.on('stream', this.emit.bind(this, 'stream'));
      +};
      +
      +Endpoint.prototype.createStream = function createStream() {
      +  return this._connection.createStream();
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Error handling

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
      +Endpoint.prototype._initializeErrorHandling = function _initializeErrorHandling() {
      +  this._serializer.on('error', this._error.bind(this, 'serializer'));
      +  this._deserializer.on('error', this._error.bind(this, 'deserializer'));
      +  this._compressor.on('error', this._error.bind(this, 'compressor'));
      +  this._decompressor.on('error', this._error.bind(this, 'decompressor'));
      +  this._connection.on('error', this._error.bind(this, 'connection'));
      +
      +  this._connection.on('peerError', this.emit.bind(this, 'peerError'));
      +};
      +
      +Endpoint.prototype._error = function _error(component, error) {
      +  this._log.fatal({ source: component, message: error }, 'Fatal error, closing connection');
      +  this.close(error);
      +  setImmediate(this.emit.bind(this, 'error', error));
      +};
      +
      +Endpoint.prototype.close = function close(error) {
      +  this._connection.close(error);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Bunyan serializers

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
      +exports.serializers = {};
      +
      +var nextId = 0;
      +exports.serializers.e = function(endpoint) {
      +  if (!('id' in endpoint)) {
      +    endpoint.id = nextId;
      +    nextId += 1;
      +  }
      +  return endpoint.id;
      +};
      + +
    • + +
    +
    + + diff --git a/doc/protocol/flow.html b/doc/protocol/flow.html new file mode 100644 index 00000000..18aa5624 --- /dev/null +++ b/doc/protocol/flow.html @@ -0,0 +1,889 @@ + + + + + The Flow class + + + + + +
    +
    + + + +
      + + + +
    • +
      + +
      + +
      + +
      + +
      var assert = require('assert');
      + +
    • + + +
    • +
      + +
      + +
      +

      The Flow class

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Flow is a Duplex stream subclass which implements HTTP/2 flow control. It is designed to be +subclassed by Connection and the upstream component of Stream.

      + +
      + +
      +var Duplex  = require('stream').Duplex;
      +
      +exports.Flow = Flow;
      + +
    • + + +
    • +
      + +
      + +
      +

      Public API

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Event: ‘error’ (type): signals an error

        +
      • +
      • setInitialWindow(size): the initial flow control window size can be changed any time +(as described in the standard) using this method

        +
      • +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      API for child classes

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • new Flow([flowControlId]): creating a new flow that will listen for WINDOW_UPDATES frames +with the given flowControlId (or every update frame if not given)

        +
      • +
      • _send(): called when more frames should be pushed. The child class is expected to override +this (instead of the _read method of the Duplex class).

        +
      • +
      • _receive(frame, readyCallback): called when there’s an incoming frame. The child class is +expected to override this (instead of the _write method of the Duplex class).

        +
      • +
      • push(frame): bool: schedules frame for sending.

        +

        Returns true if it needs more frames in the output queue, false if the output queue is +full, and null if did not push the frame into the output queue (instead, it pushed it into +the flow control queue).

        +
      • +
      • read(limit): frame: like the regular read, but the ‘flow control size’ (0 for non-DATA +frames, length of the payload for DATA frames) of the returned frame will be under limit. +Small exception: pass -1 as limit if the max. flow control size is 0. read(0) means the +same thing as in the original API.

        +
      • +
      • getLastQueuedFrame(): frame: returns the last frame in output buffers

        +
      • +
      • _log: the Flow class uses the _log object of the parent

        +
      • +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Constructor

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      When a HTTP/2.0 connection is first established, new streams are created with an initial flow +control window size of 65535 bytes.

      + +
      + +
      var INITIAL_WINDOW_SIZE = 65535;
      + +
    • + + +
    • +
      + +
      + +
      +

      flowControlId is needed if only specific WINDOW_UPDATEs should be watched.

      + +
      + +
      function Flow(flowControlId) {
      +  Duplex.call(this, { objectMode: true });
      +
      +  this._window = this._initialWindow = INITIAL_WINDOW_SIZE;
      +  this._flowControlId = flowControlId;
      +  this._queue = [];
      +  this._ended = false;
      +  this._received = 0;
      +  this._blocked = false;
      +}
      +Flow.prototype = Object.create(Duplex.prototype, { constructor: { value: Flow } });
      + +
    • + + +
    • +
      + +
      + +
      +

      Incoming frames

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      _receive is called when there’s an incoming frame.

      + +
      + +
      Flow.prototype._receive = function _receive(frame, callback) {
      +  throw new Error('The _receive(frame, callback) method has to be overridden by the child class!');
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      _receive is called by _write which in turn is called by Duplex when someone write()s +to the flow. It emits the ‘receiving’ event and notifies the window size tracking code if the +incoming frame is a WINDOW_UPDATE.

      + +
      + +
      Flow.prototype._write = function _write(frame, encoding, callback) {
      +  var sentToUs = (this._flowControlId === undefined) || (frame.stream === this._flowControlId);
      +
      +  if (sentToUs && (frame.flags.END_STREAM || (frame.type === 'RST_STREAM'))) {
      +    this._ended = true;
      +  }
      +
      +  if ((frame.type === 'DATA') && (frame.data.length > 0)) {
      +    this._receive(frame, function() {
      +      this._received += frame.data.length;
      +      if (!this._restoreWindowTimer) {
      +        this._restoreWindowTimer = setImmediate(this._restoreWindow.bind(this));
      +      }
      +      callback();
      +    }.bind(this));
      +  }
      +
      +  else {
      +    this._receive(frame, callback);
      +  }
      +
      +  if (sentToUs && (frame.type === 'WINDOW_UPDATE')) {
      +    this._updateWindow(frame);
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      _restoreWindow basically acknowledges the DATA frames received since it’s last call. It sends +a WINDOW_UPDATE that restores the flow control window of the remote end. +TODO: push this directly into the output queue. No need to wait for DATA frames in the queue.

      + +
      + +
      Flow.prototype._restoreWindow = function _restoreWindow() {
      +  delete this._restoreWindowTimer;
      +  if (!this._ended && (this._received > 0)) {
      +    this.push({
      +      type: 'WINDOW_UPDATE',
      +      flags: {},
      +      stream: this._flowControlId,
      +      window_size: this._received
      +    });
      +    this._received = 0;
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Outgoing frames - sending procedure

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +
                                          flow
      +           +-------------------------------------------------+
      +           |                                                 |
      +           +--------+           +---------+                  |
      +   read()  | output |  _read()  | flow    |  _send()         |
      +<----------|        |<----------| control |<-------------    |
      +           | buffer |           | buffer  |                  |
      +           +--------+           +---------+                  |
      +           | input  |                                        |
      +---------->|        |----------------------------------->    |
      +  write()  | buffer |  _write()              _receive()      |
      +           +--------+                                        |
      +           |                                                 |
      +           +-------------------------------------------------+
      +
      +
      + +
    • + + +
    • +
      + +
      + +
      +

      _send is called when more frames should be pushed to the output buffer.

      + +
      + +
      Flow.prototype._send = function _send() {
      +  throw new Error('The _send() method has to be overridden by the child class!');
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      _send is called by _read which is in turn called by Duplex when it wants to have more +items in the output queue.

      + +
      + +
      Flow.prototype._read = function _read() {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • if the flow control queue is empty, then let the user push more frames
      • +
      + +
      + +
        if (this._queue.length === 0) {
      +    this._send();
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • if there are items in the flow control queue, then let’s put them into the output queue (to +the extent it is possible with respect to the window size and output queue feedback)
      • +
      + +
      + +
        else if (this._window > 0) {
      +    this._blocked = false;
      +    this._readableState.sync = true; // to avoid reentrant calls
      +    do {
      +      var moreNeeded = this._push(this._queue[0]);
      +      if (moreNeeded !== null) {
      +        this._queue.shift();
      +      }
      +    } while (moreNeeded && (this._queue.length > 0));
      +    this._readableState.sync = false;
      +
      +    assert((moreNeeded == false) ||                              // * output queue is full
      +           (this._queue.length === 0) ||                         // * flow control queue is empty
      +           (!this._window && (this._queue[0].type === 'DATA'))); // * waiting for window update
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • otherwise, come back when the flow control window is positive
      • +
      + +
      + +
        else if (!this._blocked) {
      +    this._parentPush({
      +      type: 'BLOCKED',
      +      flags: {},
      +      stream: this._flowControlId
      +    });
      +    this.once('window_update', this._read);
      +    this._blocked = true;
      +  }
      +};
      +
      +var MAX_PAYLOAD_SIZE = 4096; // Must not be greater than MAX_HTTP_PAYLOAD_SIZE which is 16383
      + +
    • + + +
    • +
      + +
      + +
      +

      read(limit) is like the read of the Readable class, but it guarantess that the ‘flow control +size’ (0 for non-DATA frames, length of the payload for DATA frames) of the returned frame will +be under limit.

      + +
      + +
      Flow.prototype.read = function read(limit) {
      +  if (limit === 0) {
      +    return Duplex.prototype.read.call(this, 0);
      +  } else if (limit === -1) {
      +    limit = 0;
      +  } else if ((limit === undefined) || (limit > MAX_PAYLOAD_SIZE)) {
      +    limit = MAX_PAYLOAD_SIZE;
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Looking at the first frame in the queue without pulling it out if possible.
      • +
      + +
      + +
        var frame = this._readableState.buffer[0];
      +  if (!frame && !this._readableState.ended) {
      +    this._read();
      +    frame = this._readableState.buffer[0];
      +  }
      +
      +  if (frame && (frame.type === 'DATA')) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • If the frame is DATA, then there’s two special cases:
          +
        • if the limit is 0, we shouldn’t return anything
        • +
        • if the size of the frame is larger than limit, then the frame should be split
        • +
        +
      • +
      + +
      + +
          if (limit === 0) {
      +      return Duplex.prototype.read.call(this, 0);
      +    }
      +
      +    else if (frame.data.length > limit) {
      +      this._log.trace({ frame: frame, size: frame.data.length, forwardable: limit },
      +        'Splitting out forwardable part of a DATA frame.');
      +      this.unshift({
      +        type: 'DATA',
      +        flags: {},
      +        stream: frame.stream,
      +        data: frame.data.slice(0, limit)
      +      });
      +      frame.data = frame.data.slice(limit);
      +    }
      +  }
      +
      +  return Duplex.prototype.read.call(this);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      _parentPush pushes the given frame into the output queue

      + +
      + +
      Flow.prototype._parentPush = function _parentPush(frame) {
      +  this._log.trace({ frame: frame }, 'Pushing frame into the output queue');
      +
      +  if (frame && (frame.type === 'DATA') && (this._window !== Infinity)) {
      +    this._log.trace({ window: this._window, by: frame.data.length },
      +                    'Decreasing flow control window size.');
      +    this._window -= frame.data.length;
      +    assert(this._window >= 0);
      +  }
      +
      +  return Duplex.prototype.push.call(this, frame);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      _push(frame) pushes frame into the output queue and decreases the flow control window size. +It is capable of splitting DATA frames into smaller parts, if the window size is not enough to +push the whole frame. The return value is similar to push except that it returns null if it +did not push the whole frame to the output queue (but maybe it did push part of the frame).

      + +
      + +
      Flow.prototype._push = function _push(frame) {
      +  var data = frame && (frame.type === 'DATA') && frame.data;
      +
      +  if (!data || (data.length <= this._window)) {
      +    return this._parentPush(frame);
      +  }
      +
      +  else if (this._window <= 0) {
      +    return null;
      +  }
      +
      +  else {
      +    this._log.trace({ frame: frame, size: frame.data.length, forwardable: this._window },
      +                    'Splitting out forwardable part of a DATA frame.');
      +    frame.data = data.slice(this._window);
      +    this._parentPush({
      +      type: 'DATA',
      +      flags: {},
      +      stream: frame.stream,
      +      data: data.slice(0, this._window)
      +    });
      +    return null;
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Push frame into the flow control queue, or if it’s empty, then directly into the output queue

      + +
      + +
      Flow.prototype.push = function push(frame) {
      +  if (frame === null) {
      +    this._log.debug('Enqueueing outgoing End Of Stream');
      +  } else {
      +    this._log.debug({ frame: frame }, 'Enqueueing outgoing frame');
      +  }
      +
      +  var moreNeeded = null;
      +  if (this._queue.length === 0) {
      +    moreNeeded = this._push(frame);
      +  }
      +
      +  if (moreNeeded === null) {
      +    this._queue.push(frame);
      +  }
      +
      +  return moreNeeded;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      getLastQueuedFrame returns the last frame in output buffers. This is primarily used by the +Stream class to mark the last frame with END_STREAM flag.

      + +
      + +
      Flow.prototype.getLastQueuedFrame = function getLastQueuedFrame() {
      +  var readableQueue = this._readableState.buffer;
      +  return this._queue[this._queue.length - 1] || readableQueue[readableQueue.length - 1];
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Outgoing frames - managing the window size

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Flow control window size is manipulated using the _increaseWindow method.

      +
        +
      • Invoking it with Infinite means turning off flow control. Flow control cannot be enabled +again once disabled. Any attempt to re-enable flow control MUST be rejected with a +FLOW_CONTROL_ERROR error code.
      • +
      • A sender MUST NOT allow a flow control window to exceed 2^31 - 1 bytes. The action taken +depends on it being a stream or the connection itself.
      • +
      + +
      + +
      +var WINDOW_SIZE_LIMIT = Math.pow(2, 31) - 1;
      +
      +Flow.prototype._increaseWindow = function _increaseWindow(size) {
      +  if ((this._window === Infinity) && (size !== Infinity)) {
      +    this._log.error('Trying to increase flow control window after flow control was turned off.');
      +    this.emit('error', 'FLOW_CONTROL_ERROR');
      +  } else {
      +    this._log.trace({ window: this._window, by: size }, 'Increasing flow control window size.');
      +    this._window += size;
      +    if ((this._window !== Infinity) && (this._window > WINDOW_SIZE_LIMIT)) {
      +      this._log.error('Flow control window grew too large.');
      +      this.emit('error', 'FLOW_CONTROL_ERROR');
      +    } else {
      +      if (size != 0) {
      +        this.emit('window_update');
      +      }
      +    }
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The _updateWindow method gets called every time there’s an incoming WINDOW_UPDATE frame. It +modifies the flow control window:

      +
        +
      • Flow control can be disabled for an individual stream by sending a WINDOW_UPDATE with the +END_FLOW_CONTROL flag set. The payload of a WINDOW_UPDATE frame that has the END_FLOW_CONTROL +flag set is ignored.
      • +
      • A sender that receives a WINDOW_UPDATE frame updates the corresponding window by the amount +specified in the frame.
      • +
      + +
      + +
      Flow.prototype._updateWindow = function _updateWindow(frame) {
      +  this._increaseWindow(frame.flags.END_FLOW_CONTROL ? Infinity : frame.window_size);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      A SETTINGS frame can alter the initial flow control window size for all current streams. When the +value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the size of all stream by +calling the setInitialWindow method. The window size has to be modified by the difference +between the new value and the old value.

      + +
      + +
      Flow.prototype.setInitialWindow = function setInitialWindow(initialWindow) {
      +  this._increaseWindow(initialWindow - this._initialWindow);
      +  this._initialWindow = initialWindow;
      +};
      + +
    • + +
    +
    + + diff --git a/doc/protocol/framer.html b/doc/protocol/framer.html new file mode 100644 index 00000000..773b267d --- /dev/null +++ b/doc/protocol/framer.html @@ -0,0 +1,2196 @@ + + + + + framer.js + + + + + +
    +
    + + + +
      + +
    • +
      +

      framer.js

      +
      +
    • + + + +
    • +
      + +
      + +
      +

      The framer consists of two Transform Stream subclasses that operate in object mode: +the Serializer and the Deserializer

      + +
      + +
      var assert = require('assert');
      +
      +var Transform = require('stream').Transform;
      +
      +exports.Serializer = Serializer;
      +exports.Deserializer = Deserializer;
      +
      +var logData = Boolean(process.env.HTTP2_LOG_DATA);
      +
      +var MAX_PAYLOAD_SIZE = 16384;
      +var WINDOW_UPDATE_PAYLOAD_SIZE = 4;
      + +
    • + + +
    • +
      + +
      + +
      +

      Serializer

      + +
      + +
    • + + +
    • +
      + +
      + +
      +
      Frame Objects
      +* * * * * * * --+---------------------------
      +                |                          |
      +                v                          v           Buffers
      + [] -----> Payload Ser. --[buffers]--> Header Ser. --> * * * *
      +empty      adds payload                adds header
      +array        buffers                     buffer
      +
      +
      + +
      +function Serializer(log) {
      +  this._log = log.child({ component: 'serializer' });
      +  Transform.call(this, { objectMode: true });
      +}
      +Serializer.prototype = Object.create(Transform.prototype, { constructor: { value: Serializer } });
      + +
    • + + +
    • +
      + +
      + +
      +

      When there’s an incoming frame object, it first generates the frame type specific part of the +frame (payload), and then then adds the header part which holds fields that are common to all +frame types (like the length of the payload).

      + +
      + +
      Serializer.prototype._transform = function _transform(frame, encoding, done) {
      +  this._log.trace({ frame: frame }, 'Outgoing frame');
      +
      +  assert(frame.type in Serializer, 'Unknown frame type: ' + frame.type);
      +
      +  var buffers = [];
      +  Serializer[frame.type](frame, buffers);
      +  var length = Serializer.commonHeader(frame, buffers);
      +
      +  assert(length <= MAX_PAYLOAD_SIZE, 'Frame too large!');
      +
      +  for (var i = 0; i < buffers.length; i++) {
      +    if (logData) {
      +      this._log.trace({ data: buffers[i] }, 'Outgoing data');
      +    }
      +    this.push(buffers[i]);
      +  }
      +
      +  done();
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Deserializer

      + +
      + +
    • + + +
    • +
      + +
      + +
      +
      Buffers
      +* * * * --------+-------------------------
      +                |                        |
      +                v                        v           Frame Objects
      + {} -----> Header Des. --{frame}--> Payload Des. --> * * * * * * *
      +empty      adds parsed              adds parsed
      +object  header properties        payload properties
      +
      +
      + +
      +function Deserializer(log, role) {
      +  this._role = role;
      +  this._log = log.child({ component: 'deserializer' });
      +  Transform.call(this, { objectMode: true });
      +  this._next(COMMON_HEADER_SIZE);
      +}
      +Deserializer.prototype = Object.create(Transform.prototype, { constructor: { value: Deserializer } });
      + +
    • + + +
    • +
      + +
      + +
      +

      The Deserializer is stateful, and it’s two main alternating states are: waiting for header and +waiting for payload. The state is stored in the boolean property _waitingForHeader.

      +

      When entering a new state, a _buffer is created that will hold the accumulated data (header or +payload). The _cursor is used to track the progress.

      + +
      + +
      Deserializer.prototype._next = function(size) {
      +  this._cursor = 0;
      +  this._buffer = new Buffer(size);
      +  this._waitingForHeader = !this._waitingForHeader;
      +  if (this._waitingForHeader) {
      +    this._frame = {};
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Parsing an incoming buffer is an iterative process because it can hold multiple frames if it’s +large enough. A cursor is used to track the progress in parsing the incoming chunk.

      + +
      + +
      Deserializer.prototype._transform = function _transform(chunk, encoding, done) {
      +  var cursor = 0;
      +
      +  if (logData) {
      +    this._log.trace({ data: chunk }, 'Incoming data');
      +  }
      +
      +  while(cursor < chunk.length) {
      + +
    • + + +
    • +
      + +
      + +
      +

      The content of an incoming buffer is first copied to _buffer. If it can’t hold the full +chunk, then only a part of it is copied.

      + +
      + +
          var toCopy = Math.min(chunk.length - cursor, this._buffer.length - this._cursor);
      +    chunk.copy(this._buffer, this._cursor, cursor, cursor + toCopy);
      +    this._cursor += toCopy;
      +    cursor += toCopy;
      + +
    • + + +
    • +
      + +
      + +
      +

      When _buffer is full, it’s content gets parsed either as header or payload depending on +the actual state.

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      If it’s header then the parsed data is stored in a temporary variable and then the +deserializer waits for the specified length payload.

      + +
      + +
          if ((this._cursor === this._buffer.length) && this._waitingForHeader) {
      +      var payloadSize = Deserializer.commonHeader(this._buffer, this._frame);
      +      if (payloadSize <= MAX_PAYLOAD_SIZE) {
      +        this._next(payloadSize);
      +      } else {
      +        this.emit('error', 'FRAME_SIZE_ERROR');
      +        return;
      +      }
      +    }
      + +
    • + + +
    • +
      + +
      + +
      +

      If it’s payload then the the frame object is finalized and then gets pushed out. +Unknown frame types are ignored.

      +

      Note: If we just finished the parsing of a header and the payload length is 0, this branch +will also run.

      + +
      + +
          if ((this._cursor === this._buffer.length) && !this._waitingForHeader) {
      +      if (this._frame.type) {
      +        var error = Deserializer[this._frame.type](this._buffer, this._frame, this._role);
      +        if (error) {
      +          this._log.error('Incoming frame parsing error: ' + error);
      +          this.emit('error', error);
      +        } else {
      +          this._log.trace({ frame: this._frame }, 'Incoming frame');
      +          this.push(this._frame);
      +        }
      +      } else {
      +        this._log.error('Unknown type incoming frame');
      + +
    • + + +
    • +
      + +
      + +
      +

      Ignore it other than logging

      + +
      + +
            }
      +      this._next(COMMON_HEADER_SIZE);
      +    }
      +  }
      +
      +  done();
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Frame Header

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      HTTP/2 frames share a common base format consisting of a 9-byte header followed by 0 to 2^24 - 1 +bytes of data.

      +

      Additional size limits can be set by specific application uses. HTTP limits the frame size to +16,384 octets by default, though this can be increased by a receiver.

      +
       0                   1                   2                   3
      + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      +|                 Length (24)                   |
      ++---------------+---------------+---------------+
      +|   Type (8)    |   Flags (8)   |
      ++-+-----------------------------+---------------+---------------+
      +|R|                 Stream Identifier (31)                      |
      ++-+-------------------------------------------------------------+
      +|                     Frame Data (0...)                       ...
      ++---------------------------------------------------------------+
      +

      The fields of the frame header are defined as:

      +
        +
      • Length: +The length of the frame data expressed as an unsigned 24-bit integer. The 9 bytes of the frame +header are not included in this value.

        +
      • +
      • Type: +The 8-bit type of the frame. The frame type determines how the remainder of the frame header +and data are interpreted. Implementations MUST ignore unsupported and unrecognized frame types.

        +
      • +
      • Flags: +An 8-bit field reserved for frame-type specific boolean flags.

        +

        Flags are assigned semantics specific to the indicated frame type. Flags that have no defined +semantics for a particular frame type MUST be ignored, and MUST be left unset (0) when sending.

        +
      • +
      • R: +A reserved 1-bit field. The semantics of this bit are undefined and the bit MUST remain unset +(0) when sending and MUST be ignored when receiving.

        +
      • +
      • Stream Identifier: +A 31-bit stream identifier. The value 0 is reserved for frames that are associated with the +connection as a whole as opposed to an individual stream.

        +
      • +
      +

      The structure and content of the remaining frame data is dependent entirely on the frame type.

      + +
      + +
      +var COMMON_HEADER_SIZE = 9;
      +
      +var frameTypes = [];
      +
      +var frameFlags = {};
      +
      +var genericAttributes = ['type', 'flags', 'stream'];
      +
      +var typeSpecificAttributes = {};
      +
      +Serializer.commonHeader = function writeCommonHeader(frame, buffers) {
      +  var headerBuffer = new Buffer(COMMON_HEADER_SIZE);
      +
      +  var size = 0;
      +  for (var i = 0; i < buffers.length; i++) {
      +    size += buffers[i].length;
      +  }
      +  headerBuffer.writeUInt8(0, 0);
      +  headerBuffer.writeUInt16BE(size, 1);
      +
      +  var typeId = frameTypes.indexOf(frame.type);  // If we are here then the type is valid for sure
      +  headerBuffer.writeUInt8(typeId, 3);
      +
      +  var flagByte = 0;
      +  for (var flag in frame.flags) {
      +    var position = frameFlags[frame.type].indexOf(flag);
      +    assert(position !== -1, 'Unknown flag for frame type ' + frame.type + ': ' + flag);
      +    if (frame.flags[flag]) {
      +      flagByte |= (1 << position);
      +    }
      +  }
      +  headerBuffer.writeUInt8(flagByte, 4);
      +
      +  assert((0 <= frame.stream) && (frame.stream < 0x7fffffff), frame.stream);
      +  headerBuffer.writeUInt32BE(frame.stream || 0, 5);
      +
      +  buffers.unshift(headerBuffer);
      +
      +  return size;
      +};
      +
      +Deserializer.commonHeader = function readCommonHeader(buffer, frame) {
      +  if (buffer.length < 9) {
      +    return 'FRAME_SIZE_ERROR';
      +  }
      +
      +  var totallyWastedByte = buffer.readUInt8(0);
      +  var length = buffer.readUInt16BE(1);
      + +
    • + + +
    • +
      + +
      + +
      +

      We do this just for sanity checking later on, to make sure no one sent us a +frame that’s super large.

      + +
      + +
        length += totallyWastedByte << 16;
      +
      +  frame.type = frameTypes[buffer.readUInt8(3)];
      +  if (!frame.type) {
      + +
    • + + +
    • +
      + +
      + +
      +

      We are required to ignore unknown frame types

      + +
      + +
          return length;
      +  }
      +
      +  frame.flags = {};
      +  var flagByte = buffer.readUInt8(4);
      +  var definedFlags = frameFlags[frame.type];
      +  for (var i = 0; i < definedFlags.length; i++) {
      +    frame.flags[definedFlags[i]] = Boolean(flagByte & (1 << i));
      +  }
      +
      +  frame.stream = buffer.readUInt32BE(5) & 0x7fffffff;
      +
      +  return length;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Frame types

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Every frame type is registered in the following places:

      +
        +
      • frameTypes: a register of frame type codes (used by commonHeader())
      • +
      • frameFlags: a register of valid flags for frame types (used by commonHeader())
      • +
      • typeSpecificAttributes: a register of frame specific frame object attributes (used by +logging code and also serves as documentation for frame objects)
      • +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      DATA Frames

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      DATA frames (type=0x0) convey arbitrary, variable-length sequences of octets associated with a +stream.

      +

      The DATA frame defines the following flags:

      +
        +
      • END_STREAM (0x1): +Bit 1 being set indicates that this frame is the last that the endpoint will send for the +identified stream.
      • +
      • PADDED (0x08): +Bit 4 being set indicates that the Pad Length field is present.
      • +
      + +
      + +
      +frameTypes[0x0] = 'DATA';
      +
      +frameFlags.DATA = ['END_STREAM', 'RESERVED2', 'RESERVED4', 'PADDED'];
      +
      +typeSpecificAttributes.DATA = ['data'];
      +
      +Serializer.DATA = function writeData(frame, buffers) {
      +  buffers.push(frame.data);
      +};
      +
      +Deserializer.DATA = function readData(buffer, frame) {
      +  var dataOffset = 0;
      +  var paddingLength = 0;
      +  if (frame.flags.PADDED) {
      +    if (buffer.length < 1) {
      + +
    • + + +
    • +
      + +
      + +
      +

      We must have at least one byte for padding control, but we don’t. Bad peer!

      + +
      + +
            return 'FRAME_SIZE_ERROR';
      +    }
      +    paddingLength = (buffer.readUInt8(dataOffset) & 0xff);
      +    dataOffset = 1;
      +  }
      +
      +  if (paddingLength) {
      +    if (paddingLength >= (buffer.length - 1)) {
      + +
    • + + +
    • +
      + +
      + +
      +

      We don’t have enough room for the padding advertised - bad peer!

      + +
      + +
            return 'FRAME_SIZE_ERROR';
      +    }
      +    frame.data = buffer.slice(dataOffset, -1 * paddingLength);
      +  } else {
      +    frame.data = buffer.slice(dataOffset);
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      HEADERS

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The HEADERS frame (type=0x1) allows the sender to create a stream.

      +

      The HEADERS frame defines the following flags:

      +
        +
      • END_STREAM (0x1): +Bit 1 being set indicates that this frame is the last that the endpoint will send for the +identified stream.
      • +
      • END_HEADERS (0x4): +The END_HEADERS bit indicates that this frame contains the entire payload necessary to provide +a complete set of headers.
      • +
      • PADDED (0x08): +Bit 4 being set indicates that the Pad Length field is present.
      • +
      • PRIORITY (0x20): +Bit 6 being set indicates that the Exlusive Flag (E), Stream Dependency, and Weight fields are +present.
      • +
      + +
      + +
      +frameTypes[0x1] = 'HEADERS';
      +
      +frameFlags.HEADERS = ['END_STREAM', 'RESERVED2', 'END_HEADERS', 'PADDED', 'RESERVED5', 'PRIORITY'];
      +
      +typeSpecificAttributes.HEADERS = ['priorityDependency', 'priorityWeight', 'exclusiveDependency', 'headers', 'data'];
      + +
    • + + +
    • +
      + +
      + +
      +
       0                   1                   2                   3
      + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      +|Pad Length? (8)|
      ++-+-------------+---------------+-------------------------------+
      +|E|                 Stream Dependency? (31)                     |
      ++-+-------------+-----------------------------------------------+
      +|  Weight? (8)  |
      ++-+-------------+-----------------------------------------------+
      +|                   Header Block Fragment (*)                 ...
      ++---------------------------------------------------------------+
      +|                           Padding (*)                       ...
      ++---------------------------------------------------------------+
      +

      The payload of a HEADERS frame contains a Headers Block

      + +
      + +
      +Serializer.HEADERS = function writeHeadersPriority(frame, buffers) {
      +  if (frame.flags.PRIORITY) {
      +    var buffer = new Buffer(5);
      +    assert((0 <= frame.priorityDependency) && (frame.priorityDependency <= 0x7fffffff), frame.priorityDependency);
      +    buffer.writeUInt32BE(frame.priorityDependency, 0);
      +    if (frame.exclusiveDependency) {
      +      buffer[0] |= 0x80;
      +    }
      +    assert((0 <= frame.priorityWeight) && (frame.priorityWeight <= 0xff), frame.priorityWeight);
      +    buffer.writeUInt8(frame.priorityWeight, 4);
      +    buffers.push(buffer);
      +  }
      +  buffers.push(frame.data);
      +};
      +
      +Deserializer.HEADERS = function readHeadersPriority(buffer, frame) {
      +  var minFrameLength = 0;
      +  if (frame.flags.PADDED) {
      +    minFrameLength += 1;
      +  }
      +  if (frame.flags.PRIORITY) {
      +    minFrameLength += 5;
      +  }
      +  if (buffer.length < minFrameLength) {
      + +
    • + + +
    • +
      + +
      + +
      +

      Peer didn’t send enough data - bad peer!

      + +
      + +
          return 'FRAME_SIZE_ERROR';
      +  }
      +
      +  var dataOffset = 0;
      +  var paddingLength = 0;
      +  if (frame.flags.PADDED) {
      +    paddingLength = (buffer.readUInt8(dataOffset) & 0xff);
      +    dataOffset = 1;
      +  }
      +
      +  if (frame.flags.PRIORITY) {
      +    var dependencyData = new Buffer(4);
      +    buffer.copy(dependencyData, 0, dataOffset, dataOffset + 4);
      +    dataOffset += 4;
      +    frame.exclusiveDependency = !!(dependencyData[0] & 0x80);
      +    dependencyData[0] &= 0x7f;
      +    frame.priorityDependency = dependencyData.readUInt32BE(0);
      +    frame.priorityWeight = buffer.readUInt8(dataOffset);
      +    dataOffset += 1;
      +  }
      +
      +  if (paddingLength) {
      +    if ((buffer.length - dataOffset) < paddingLength) {
      + +
    • + + +
    • +
      + +
      + +
      +

      Not enough data left to satisfy the advertised padding - bad peer!

      + +
      + +
            return 'FRAME_SIZE_ERROR';
      +    }
      +    frame.data = buffer.slice(dataOffset, -1 * paddingLength);
      +  } else {
      +    frame.data = buffer.slice(dataOffset);
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      PRIORITY

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The PRIORITY frame (type=0x2) specifies the sender-advised priority of a stream.

      +

      The PRIORITY frame does not define any flags.

      + +
      + +
      +frameTypes[0x2] = 'PRIORITY';
      +
      +frameFlags.PRIORITY = [];
      +
      +typeSpecificAttributes.PRIORITY = ['priorityDependency', 'priorityWeight', 'exclusiveDependency'];
      + +
    • + + +
    • +
      + +
      + +
      +
       0                   1                   2                   3
      + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      +|E|                 Stream Dependency? (31)                     |
      ++-+-------------+-----------------------------------------------+
      +|  Weight? (8)  |
      ++-+-------------+
      +

      The payload of a PRIORITY frame contains an exclusive bit, a 31-bit dependency, and an 8-bit weight

      + +
      + +
      +Serializer.PRIORITY = function writePriority(frame, buffers) {
      +  var buffer = new Buffer(5);
      +  assert((0 <= frame.priorityDependency) && (frame.priorityDependency <= 0x7fffffff), frame.priorityDependency);
      +  buffer.writeUInt32BE(frame.priorityDependency, 0);
      +  if (frame.exclusiveDependency) {
      +    buffer[0] |= 0x80;
      +  }
      +  assert((0 <= frame.priorityWeight) && (frame.priorityWeight <= 0xff), frame.priorityWeight);
      +  buffer.writeUInt8(frame.priorityWeight, 4);
      +
      +  buffers.push(buffer);
      +};
      +
      +Deserializer.PRIORITY = function readPriority(buffer, frame) {
      +  if (buffer.length < 5) {
      + +
    • + + +
    • +
      + +
      + +
      +

      PRIORITY frames are 5 bytes long. Bad peer!

      + +
      + +
          return 'FRAME_SIZE_ERROR';
      +  }
      +  var dependencyData = new Buffer(4);
      +  buffer.copy(dependencyData, 0, 0, 4);
      +  frame.exclusiveDependency = !!(dependencyData[0] & 0x80);
      +  dependencyData[0] &= 0x7f;
      +  frame.priorityDependency = dependencyData.readUInt32BE(0);
      +  frame.priorityWeight = buffer.readUInt8(4);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      RST_STREAM

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The RST_STREAM frame (type=0x3) allows for abnormal termination of a stream.

      +

      No type-flags are defined.

      + +
      + +
      +frameTypes[0x3] = 'RST_STREAM';
      +
      +frameFlags.RST_STREAM = [];
      +
      +typeSpecificAttributes.RST_STREAM = ['error'];
      + +
    • + + +
    • +
      + +
      + +
      +
       0                   1                   2                   3
      + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      +|                         Error Code (32)                       |
      ++---------------------------------------------------------------+
      +

      The RST_STREAM frame contains a single unsigned, 32-bit integer identifying the error +code (see Error Codes). The error code indicates why the stream is being terminated.

      + +
      + +
      +Serializer.RST_STREAM = function writeRstStream(frame, buffers) {
      +  var buffer = new Buffer(4);
      +  var code = errorCodes.indexOf(frame.error);
      +  assert((0 <= code) && (code <= 0xffffffff), code);
      +  buffer.writeUInt32BE(code, 0);
      +  buffers.push(buffer);
      +};
      +
      +Deserializer.RST_STREAM = function readRstStream(buffer, frame) {
      +  if (buffer.length < 4) {
      + +
    • + + +
    • +
      + +
      + +
      +

      RST_STREAM is 4 bytes long. Bad peer!

      + +
      + +
          return 'FRAME_SIZE_ERROR';
      +  }
      +  frame.error = errorCodes[buffer.readUInt32BE(0)];
      +  if (!frame.error) {
      + +
    • + + +
    • +
      + +
      + +
      +

      Unknown error codes are considered equivalent to INTERNAL_ERROR

      + +
      + +
          frame.error = 'INTERNAL_ERROR';
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      SETTINGS

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The SETTINGS frame (type=0x4) conveys configuration parameters that affect how endpoints +communicate.

      +

      The SETTINGS frame defines the following flag:

      + +
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • ACK (0x1): +Bit 1 being set indicates that this frame acknowledges receipt and application of the peer’s +SETTINGS frame.
      • +
      + +
      + +
      frameTypes[0x4] = 'SETTINGS';
      +
      +frameFlags.SETTINGS = ['ACK'];
      +
      +typeSpecificAttributes.SETTINGS = ['settings'];
      + +
    • + + +
    • +
      + +
      + +
      +

      The payload of a SETTINGS frame consists of zero or more settings. Each setting consists of a +16-bit identifier, and an unsigned 32-bit value.

      +
       0                   1                   2                   3
      + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      +|         Identifier(16)          |        Value (32)           |
      ++-----------------+---------------------------------------------+
      +...Value                          |
      ++---------------------------------+
      +

      Each setting in a SETTINGS frame replaces the existing value for that setting. Settings are +processed in the order in which they appear, and a receiver of a SETTINGS frame does not need to +maintain any state other than the current value of settings. Therefore, the value of a setting +is the last value that is seen by a receiver. This permits the inclusion of the same settings +multiple times in the same SETTINGS frame, though doing so does nothing other than waste +connection capacity.

      + +
      + +
      +Serializer.SETTINGS = function writeSettings(frame, buffers) {
      +  var settings = [], settingsLeft = Object.keys(frame.settings);
      +  definedSettings.forEach(function(setting, id) {
      +    if (setting.name in frame.settings) {
      +      settingsLeft.splice(settingsLeft.indexOf(setting.name), 1);
      +      var value = frame.settings[setting.name];
      +      settings.push({ id: id, value: setting.flag ? Boolean(value) : value });
      +    }
      +  });
      +  assert(settingsLeft.length === 0, 'Unknown settings: ' + settingsLeft.join(', '));
      +
      +  var buffer = new Buffer(settings.length * 6);
      +  for (var i = 0; i < settings.length; i++) {
      +    buffer.writeUInt16BE(settings[i].id & 0xffff, i*6);
      +    buffer.writeUInt32BE(settings[i].value, i*6 + 2);
      +  }
      +
      +  buffers.push(buffer);
      +};
      +
      +Deserializer.SETTINGS = function readSettings(buffer, frame, role) {
      +  frame.settings = {};
      + +
    • + + +
    • +
      + +
      + +
      +

      Receipt of a SETTINGS frame with the ACK flag set and a length +field value other than 0 MUST be treated as a connection error +(Section 5.4.1) of type FRAME_SIZE_ERROR.

      + +
      + +
        if(frame.flags.ACK && buffer.length != 0) {
      +    return 'FRAME_SIZE_ERROR';
      +  }
      +
      +  if (buffer.length % 6 !== 0) {
      +    return 'PROTOCOL_ERROR';
      +  }
      +  for (var i = 0; i < buffer.length / 6; i++) {
      +    var id = buffer.readUInt16BE(i*6) & 0xffff;
      +    var setting = definedSettings[id];
      +    if (setting) {
      +      if (role == 'CLIENT' && setting.name == 'SETTINGS_ENABLE_PUSH') {
      +        return 'SETTINGS frame on client got SETTINGS_ENABLE_PUSH';
      +      }
      +      var value = buffer.readUInt32BE(i*6 + 2);
      +      frame.settings[setting.name] = setting.flag ? Boolean(value & 0x1) : value;
      +    }
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The following settings are defined:

      + +
      + +
      var definedSettings = [];
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • SETTINGS_HEADER_TABLE_SIZE (1): +Allows the sender to inform the remote endpoint of the size of the header compression table +used to decode header blocks.
      • +
      + +
      + +
      definedSettings[1] = { name: 'SETTINGS_HEADER_TABLE_SIZE', flag: false };
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • SETTINGS_ENABLE_PUSH (2): +This setting can be use to disable server push. An endpoint MUST NOT send a PUSH_PROMISE frame +if it receives this setting set to a value of 0. The default value is 1, which indicates that +push is permitted.
      • +
      + +
      + +
      definedSettings[2] = { name: 'SETTINGS_ENABLE_PUSH', flag: true };
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • SETTINGS_MAX_CONCURRENT_STREAMS (3): +indicates the maximum number of concurrent streams that the sender will allow.
      • +
      + +
      + +
      definedSettings[3] = { name: 'SETTINGS_MAX_CONCURRENT_STREAMS', flag: false };
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • SETTINGS_INITIAL_WINDOW_SIZE (4): +indicates the sender’s initial stream window size (in bytes) for new streams.
      • +
      + +
      + +
      definedSettings[4] = { name: 'SETTINGS_INITIAL_WINDOW_SIZE', flag: false };
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • SETTINGS_MAX_FRAME_SIZE (5): +indicates the maximum size of a frame the receiver will allow.
      • +
      + +
      + +
      definedSettings[5] = { name: 'SETTINGS_MAX_FRAME_SIZE', flag: false };
      + +
    • + + +
    • +
      + +
      + +
      +

      PUSH_PROMISE

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The PUSH_PROMISE frame (type=0x5) is used to notify the peer endpoint in advance of streams the +sender intends to initiate.

      +

      The PUSH_PROMISE frame defines the following flags:

      +
        +
      • END_PUSH_PROMISE (0x4): +The END_PUSH_PROMISE bit indicates that this frame contains the entire payload necessary to +provide a complete set of headers.
      • +
      + +
      + +
      +frameTypes[0x5] = 'PUSH_PROMISE';
      +
      +frameFlags.PUSH_PROMISE = ['RESERVED1', 'RESERVED2', 'END_PUSH_PROMISE', 'PADDED'];
      +
      +typeSpecificAttributes.PUSH_PROMISE = ['promised_stream', 'headers', 'data'];
      + +
    • + + +
    • +
      + +
      + +
      +
       0                   1                   2                   3
      + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      +|Pad Length? (8)|
      ++-+-------------+-----------------------------------------------+
      +|X|                Promised-Stream-ID (31)                      |
      ++-+-------------------------------------------------------------+
      +|                 Header Block Fragment (*)                   ...
      ++---------------------------------------------------------------+
      +|                         Padding (*)                         ...
      ++---------------------------------------------------------------+
      +

      The PUSH_PROMISE frame includes the unsigned 31-bit identifier of +the stream the endpoint plans to create along with a minimal set of headers that provide +additional context for the stream.

      + +
      + +
      +Serializer.PUSH_PROMISE = function writePushPromise(frame, buffers) {
      +  var buffer = new Buffer(4);
      +
      +  var promised_stream = frame.promised_stream;
      +  assert((0 <= promised_stream) && (promised_stream <= 0x7fffffff), promised_stream);
      +  buffer.writeUInt32BE(promised_stream, 0);
      +
      +  buffers.push(buffer);
      +  buffers.push(frame.data);
      +};
      +
      +Deserializer.PUSH_PROMISE = function readPushPromise(buffer, frame) {
      +  if (buffer.length < 4) {
      +    return 'FRAME_SIZE_ERROR';
      +  }
      +  var dataOffset = 0;
      +  var paddingLength = 0;
      +  if (frame.flags.PADDED) {
      +    if (buffer.length < 5) {
      +      return 'FRAME_SIZE_ERROR';
      +    }
      +    paddingLength = (buffer.readUInt8(dataOffset) & 0xff);
      +    dataOffset = 1;
      +  }
      +  frame.promised_stream = buffer.readUInt32BE(dataOffset) & 0x7fffffff;
      +  dataOffset += 4;
      +  if (paddingLength) {
      +    if ((buffer.length - dataOffset) < paddingLength) {
      +      return 'FRAME_SIZE_ERROR';
      +    }
      +    frame.data = buffer.slice(dataOffset, -1 * paddingLength);
      +  } else {
      +    frame.data = buffer.slice(dataOffset);
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      PING

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The PING frame (type=0x6) is a mechanism for measuring a minimal round-trip time from the +sender, as well as determining whether an idle connection is still functional.

      +

      The PING frame defines one type-specific flag:

      +
        +
      • ACK (0x1): +Bit 1 being set indicates that this PING frame is a PING response.
      • +
      + +
      + +
      +frameTypes[0x6] = 'PING';
      +
      +frameFlags.PING = ['ACK'];
      +
      +typeSpecificAttributes.PING = ['data'];
      + +
    • + + +
    • +
      + +
      + +
      +

      In addition to the frame header, PING frames MUST contain 8 additional octets of opaque data.

      + +
      + +
      +Serializer.PING = function writePing(frame, buffers) {
      +  buffers.push(frame.data);
      +};
      +
      +Deserializer.PING = function readPing(buffer, frame) {
      +  if (buffer.length !== 8) {
      +    return 'FRAME_SIZE_ERROR';
      +  }
      +  frame.data = buffer;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      GOAWAY

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The GOAWAY frame (type=0x7) informs the remote peer to stop creating streams on this connection.

      +

      The GOAWAY frame does not define any flags.

      + +
      + +
      +frameTypes[0x7] = 'GOAWAY';
      +
      +frameFlags.GOAWAY = [];
      +
      +typeSpecificAttributes.GOAWAY = ['last_stream', 'error'];
      + +
    • + + +
    • +
      + +
      + +
      +
       0                   1                   2                   3
      + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      ++-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
      +|X|                  Last-Stream-ID (31)                        |
      ++-+-------------------------------------------------------------+
      +|                      Error Code (32)                          |
      ++---------------------------------------------------------------+
      +

      The last stream identifier in the GOAWAY frame contains the highest numbered stream identifier +for which the sender of the GOAWAY frame has received frames on and might have taken some action +on.

      +

      The GOAWAY frame also contains a 32-bit error code (see Error Codes) that contains the reason for +closing the connection.

      + +
      + +
      +Serializer.GOAWAY = function writeGoaway(frame, buffers) {
      +  var buffer = new Buffer(8);
      +
      +  var last_stream = frame.last_stream;
      +  assert((0 <= last_stream) && (last_stream <= 0x7fffffff), last_stream);
      +  buffer.writeUInt32BE(last_stream, 0);
      +
      +  var code = errorCodes.indexOf(frame.error);
      +  assert((0 <= code) && (code <= 0xffffffff), code);
      +  buffer.writeUInt32BE(code, 4);
      +
      +  buffers.push(buffer);
      +};
      +
      +Deserializer.GOAWAY = function readGoaway(buffer, frame) {
      +  if (buffer.length !== 8) {
      + +
    • + + +
    • +
      + +
      + +
      +

      GOAWAY must have 8 bytes

      + +
      + +
          return 'FRAME_SIZE_ERROR';
      +  }
      +  frame.last_stream = buffer.readUInt32BE(0) & 0x7fffffff;
      +  frame.error = errorCodes[buffer.readUInt32BE(4)];
      +  if (!frame.error) {
      + +
    • + + +
    • +
      + +
      + +
      +

      Unknown error types are to be considered equivalent to INTERNAL ERROR

      + +
      + +
          frame.error = 'INTERNAL_ERROR';
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      WINDOW_UPDATE

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The WINDOW_UPDATE frame (type=0x8) is used to implement flow control.

      +

      The WINDOW_UPDATE frame does not define any flags.

      + +
      + +
      +frameTypes[0x8] = 'WINDOW_UPDATE';
      +
      +frameFlags.WINDOW_UPDATE = [];
      +
      +typeSpecificAttributes.WINDOW_UPDATE = ['window_size'];
      + +
    • + + +
    • +
      + +
      + +
      +

      The payload of a WINDOW_UPDATE frame is a 32-bit value indicating the additional number of bytes +that the sender can transmit in addition to the existing flow control window. The legal range +for this field is 1 to 2^31 - 1 (0x7fffffff) bytes; the most significant bit of this value is +reserved.

      + +
      + +
      +Serializer.WINDOW_UPDATE = function writeWindowUpdate(frame, buffers) {
      +  var buffer = new Buffer(4);
      +
      +  var window_size = frame.window_size;
      +  assert((0 < window_size) && (window_size <= 0x7fffffff), window_size);
      +  buffer.writeUInt32BE(window_size, 0);
      +
      +  buffers.push(buffer);
      +};
      +
      +Deserializer.WINDOW_UPDATE = function readWindowUpdate(buffer, frame) {
      +  if (buffer.length !== WINDOW_UPDATE_PAYLOAD_SIZE) {
      +    return 'FRAME_SIZE_ERROR';
      +  }
      +  frame.window_size = buffer.readUInt32BE(0) & 0x7fffffff;
      +  if (frame.window_size === 0) {
      +    return 'PROTOCOL_ERROR';
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      CONTINUATION

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The CONTINUATION frame (type=0x9) is used to continue a sequence of header block fragments.

      +

      The CONTINUATION frame defines the following flag:

      +
        +
      • END_HEADERS (0x4): +The END_HEADERS bit indicates that this frame ends the sequence of header block fragments +necessary to provide a complete set of headers.
      • +
      + +
      + +
      +frameTypes[0x9] = 'CONTINUATION';
      +
      +frameFlags.CONTINUATION = ['RESERVED1', 'RESERVED2', 'END_HEADERS'];
      +
      +typeSpecificAttributes.CONTINUATION = ['headers', 'data'];
      +
      +Serializer.CONTINUATION = function writeContinuation(frame, buffers) {
      +  buffers.push(frame.data);
      +};
      +
      +Deserializer.CONTINUATION = function readContinuation(buffer, frame) {
      +  frame.data = buffer;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      ALTSVC

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The ALTSVC frame (type=0xA) advertises the availability of an alternative service to the client.

      +

      The ALTSVC frame does not define any flags.

      + +
      + +
      +frameTypes[0xA] = 'ALTSVC';
      +
      +frameFlags.ALTSVC = [];
      + +
    • + + +
    • +
      + +
      + +
      +
      0                   1                   2                   3
      +0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
      +

      +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Origin-Len (16) | Origin? () … + +——————————-+—————-+————–+ + | Alt-Svc-Field-Value () … + +—————————————————————+

      +

      The ALTSVC frame contains the following fields:

      +

      Origin-Len: An unsigned, 16-bit integer indicating the length, in + octets, of the Origin field.

      +

      Origin: An OPTIONAL sequence of characters containing ASCII + serialisation of an origin (RFC6454, + Section 6.2) that the alternate service is applicable to.

      +

      Alt-Svc-Field-Value: A sequence of octets (length determined by + subtracting the length of all preceding fields from the frame + length) containing a value identical to the Alt-Svc field value + defined in (Section 3)[https://tools.ietf.org/html/rfc7838#section-3] + (ABNF production “Alt-Svc”).

      + +
      + +
      +typeSpecificAttributes.ALTSVC = ['maxAge', 'port', 'protocolID', 'host',
      +                                 'origin'];
      +
      +function istchar(c) {
      +  return ('!#$&\'*+-.^_`|~1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.indexOf(c) > -1);
      +}
      +
      +function hexencode(s) {
      +  var t = '';
      +  for (var i = 0; i < s.length; i++) {
      +    if (!istchar(s[i])) {
      +      t += '%';
      +      t += new Buffer(s[i]).toString('hex');
      +    } else {
      +      t += s[i];
      +    }
      +  }
      +  return t;
      +}
      +
      +Serializer.ALTSVC = function writeAltSvc(frame, buffers) {
      +  var buffer = new Buffer(2);
      +  buffer.writeUInt16BE(frame.origin.length, 0);
      +  buffers.push(buffer);
      +  buffers.push(new Buffer(frame.origin, 'ascii'));
      +
      +  var fieldValue = hexencode(frame.protocolID) + '="' + frame.host + ':' + frame.port + '"';
      +  if (frame.maxAge !== 86400) { // 86400 is the default
      +    fieldValue += "; ma=" + frame.maxAge;
      +  }
      +
      +  buffers.push(new Buffer(fieldValue, 'ascii'));
      +};
      +
      +function stripquotes(s) {
      +  var start = 0;
      +  var end = s.length;
      +  while ((start < end) && (s[start] === '"')) {
      +    start++;
      +  }
      +  while ((end > start) && (s[end - 1] === '"')) {
      +    end--;
      +  }
      +  if (start >= end) {
      +    return "";
      +  }
      +  return s.substring(start, end);
      +}
      +
      +function splitNameValue(nvpair) {
      +  var eq = -1;
      +  var inQuotes = false;
      +
      +  for (var i = 0; i < nvpair.length; i++) {
      +    if (nvpair[i] === '"') {
      +      inQuotes = !inQuotes;
      +      continue;
      +    }
      +    if (inQuotes) {
      +      continue;
      +    }
      +    if (nvpair[i] === '=') {
      +      eq = i;
      +      break;
      +    }
      +  }
      +
      +  if (eq === -1) {
      +    return {'name': nvpair, 'value': null};
      +  }
      +
      +  var name = stripquotes(nvpair.substring(0, eq).trim());
      +  var value = stripquotes(nvpair.substring(eq + 1).trim());
      +  return {'name': name, 'value': value};
      +}
      +
      +function splitHeaderParameters(hv) {
      +  return parseHeaderValue(hv, ';', splitNameValue);
      +}
      +
      +function parseHeaderValue(hv, separator, callback) {
      +  var start = 0;
      +  var inQuotes = false;
      +  var values = [];
      +
      +  for (var i = 0; i < hv.length; i++) {
      +    if (hv[i] === '"') {
      +      inQuotes = !inQuotes;
      +      continue;
      +    }
      +    if (inQuotes) {
      + +
    • + + +
    • +
      + +
      + +
      +

      Just skip this

      + +
      + +
            continue;
      +    }
      +    if (hv[i] === separator) {
      +      var newValue = hv.substring(start, i).trim();
      +      if (newValue.length > 0) {
      +        newValue = callback(newValue);
      +        values.push(newValue);
      +      }
      +      start = i + 1;
      +    }
      +  }
      +
      +  var newValue = hv.substring(start).trim();
      +  if (newValue.length > 0) {
      +    newValue = callback(newValue);
      +    values.push(newValue);
      +  }
      +
      +  return values;
      +}
      +
      +function rsplit(s, delim, count) {
      +  var nsplits = 0;
      +  var end = s.length;
      +  var rval = [];
      +  for (var i = s.length - 1; i >= 0; i--) {
      +    if (s[i] === delim) {
      +      var t = s.substring(i + 1, end);
      +      end = i;
      +      rval.unshift(t);
      +      nsplits++;
      +      if (nsplits === count) {
      +        break;
      +      }
      +    }
      +  }
      +  if (end !== 0) {
      +    rval.unshift(s.substring(0, end));
      +  }
      +  return rval;
      +}
      +
      +function ishex(c) {
      +  return ('0123456789ABCDEFabcdef'.indexOf(c) > -1);
      +}
      +
      +function unescape(s) {
      +  var i = 0;
      +  var t = '';
      +  while (i < s.length) {
      +    if (s[i] != '%' || !ishex(s[i + 1]) || !ishex(s[i + 2])) {
      +      t += s[i];
      +    } else {
      +      ++i;
      +      var hexvalue = '';
      +      if (i < s.length) {
      +        hexvalue += s[i];
      +        ++i;
      +      }
      +      if (i < s.length) {
      +        hexvalue += s[i];
      +      }
      +      if (hexvalue.length > 0) {
      +        t += new Buffer(hexvalue, 'hex').toString();
      +      } else {
      +        t += '%';
      +      }
      +    }
      +
      +    ++i;
      +  }
      +  return t;
      +}
      +
      +Deserializer.ALTSVC = function readAltSvc(buffer, frame) {
      +  if (buffer.length < 2) {
      +    return 'FRAME_SIZE_ERROR';
      +  }
      +  var originLength = buffer.readUInt16BE(0);
      +  if ((buffer.length - 2) < originLength) {
      +    return 'FRAME_SIZE_ERROR';
      +  }
      +  frame.origin = buffer.toString('ascii', 2, 2 + originLength);
      +  var fieldValue = buffer.toString('ascii', 2 + originLength);
      +  var values = parseHeaderValue(fieldValue, ',', splitHeaderParameters);
      +  if (values.length > 1) {
      + +
    • + + +
    • +
      + +
      + +
      +

      TODO - warn that we only use one here

      + +
      + +
        }
      +  if (values.length === 0) {
      + +
    • + + +
    • +
      + +
      + +
      +

      Well that’s a malformed frame. Just ignore it.

      + +
      + +
          return;
      +  }
      +
      +  var chosenAltSvc = values[0];
      +  frame.maxAge = 86400; // Default
      +  for (var i = 0; i < chosenAltSvc.length; i++) {
      +    if (i === 0) {
      + +
    • + + +
    • +
      + +
      + +
      +

      This corresponds to the protocolID=”:“ item

      + +
      + +
            frame.protocolID = unescape(chosenAltSvc[i].name);
      +      var hostport = rsplit(chosenAltSvc[i].value, ':', 1);
      +      frame.host = hostport[0];
      +      frame.port = parseInt(hostport[1], 10);
      +    } else if (chosenAltSvc[i].name == 'ma') {
      +      frame.maxAge = parseInt(chosenAltSvc[i].value, 10);
      +    }
      + +
    • + + +
    • +
      + +
      + +
      +

      Otherwise, we just ignore this

      + +
      + +
        }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      BLOCKED

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The BLOCKED frame (type=0xB) indicates that the sender is unable to send data +due to a closed flow control window.

      +

      The BLOCKED frame does not define any flags and contains no payload.

      + +
      + +
      +frameTypes[0xB] = 'BLOCKED';
      +
      +frameFlags.BLOCKED = [];
      +
      +typeSpecificAttributes.BLOCKED = [];
      +
      +Serializer.BLOCKED = function writeBlocked(frame, buffers) {
      +};
      +
      +Deserializer.BLOCKED = function readBlocked(buffer, frame) {
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Error Codes

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
      +var errorCodes = [
      +  'NO_ERROR',
      +  'PROTOCOL_ERROR',
      +  'INTERNAL_ERROR',
      +  'FLOW_CONTROL_ERROR',
      +  'SETTINGS_TIMEOUT',
      +  'STREAM_CLOSED',
      +  'FRAME_SIZE_ERROR',
      +  'REFUSED_STREAM',
      +  'CANCEL',
      +  'COMPRESSION_ERROR',
      +  'CONNECT_ERROR',
      +  'ENHANCE_YOUR_CALM',
      +  'INADEQUATE_SECURITY',
      +  'HTTP_1_1_REQUIRED'
      +];
      + +
    • + + +
    • +
      + +
      + +
      +

      Logging

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Bunyan serializers to improve logging output +for debug messages emitted in this component.

      + +
      + +
      exports.serializers = {};
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • frame serializer: it transforms data attributes from Buffers to hex strings and filters out +flags that are not present.
      • +
      + +
      + +
      var frameCounter = 0;
      +exports.serializers.frame = function(frame) {
      +  if (!frame) {
      +    return null;
      +  }
      +
      +  if ('id' in frame) {
      +    return frame.id;
      +  }
      +
      +  frame.id = frameCounter;
      +  frameCounter += 1;
      +
      +  var logEntry = { id: frame.id };
      +  genericAttributes.concat(typeSpecificAttributes[frame.type]).forEach(function(name) {
      +    logEntry[name] = frame[name];
      +  });
      +
      +  if (frame.data instanceof Buffer) {
      +    if (logEntry.data.length > 50) {
      +      logEntry.data = frame.data.slice(0, 47).toString('hex') + '...';
      +    } else {
      +      logEntry.data = frame.data.toString('hex');
      +    }
      +
      +    if (!('length' in logEntry)) {
      +      logEntry.length = frame.data.length;
      +    }
      +  }
      +
      +  if (frame.promised_stream instanceof Object) {
      +    logEntry.promised_stream = 'stream-' + frame.promised_stream.id;
      +  }
      +
      +  logEntry.flags = Object.keys(frame.flags || {}).filter(function(name) {
      +    return frame.flags[name] === true;
      +  });
      +
      +  return logEntry;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • data serializer: it simply transforms a buffer to a hex string.
      • +
      + +
      + +
      exports.serializers.data = function(data) {
      +  return data.toString('hex');
      +};
      + +
    • + +
    +
    + + diff --git a/doc/protocol/index.html b/doc/protocol/index.html new file mode 100644 index 00000000..5d61ce48 --- /dev/null +++ b/doc/protocol/index.html @@ -0,0 +1,170 @@ + + + + + index.js + + + + + +
    +
    + + + +
      + +
    • +
      +

      index.js

      +
      +
    • + + + +
    • +
      + +
      + +
      +

      This is an implementation of the HTTP/2 +framing layer for node.js.

      +

      The main building blocks are node.js streams that are connected through pipes.

      +

      The main components are:

      +
        +
      • Endpoint: represents an HTTP/2 endpoint (client or server). It’s +responsible for the the first part of the handshake process (sending/receiving the +connection header) and manages other components (framer, compressor, +connection, streams) that make up a client or server.

        +
      • +
      • Connection: multiplexes the active HTTP/2 streams, manages connection +lifecycle and settings, and responsible for enforcing the connection level limits (flow +control, initiated stream limit)

        +
      • +
      • Stream: implementation of the HTTP/2 stream concept. +Implements the stream state machine defined by the standard, provides +management methods and events for using the stream (sending/receiving headers, data, etc.), +and enforces stream level constraints (flow control, sending only legal frames).

        +
      • +
      • Flow: implements flow control for Connection and Stream as parent class.

        +
      • +
      • Compressor and Decompressor: compression and decompression of HEADER and +PUSH_PROMISE frames

        +
      • +
      • Serializer and Deserializer: the lowest layer in the stack that transforms +between the binary and the JavaScript object representation of HTTP/2 frames

        +
      • +
      + +
      + +
      +exports.VERSION = 'h2';
      +
      +exports.Endpoint = require('./endpoint').Endpoint;
      +
      +/* Bunyan serializers exported by submodules that are worth adding when creating a logger. */
      +exports.serializers = {};
      +var modules = ['./framer', './compressor', './flow', './connection', './stream', './endpoint'];
      +modules.map(require).forEach(function(module) {
      +  for (var name in module.serializers) {
      +    exports.serializers[name] = module.serializers[name];
      +  }
      +});
      +
      +/*
      +              Stream API            Endpoint API
      +              Stream data
      +
      +             |            ^        |            ^
      +             |            |        |            |
      +             |            |        |            |
      + +-----------|------------|---------------------------------------+
      + |           |            |   Endpoint                            |
      + |           |            |                                       |
      + |   +-------|------------|-----------------------------------+   |
      + |   |       |            |  Connection                       |   |
      + |   |       v            |                                   |   |
      + |   |  +-----------------------+  +--------------------      |   |
      + |   |  |        Stream         |  |         Stream      ...  |   |
      + |   |  +-----------------------+  +--------------------      |   |
      + |   |       |            ^              |            ^       |   |
      + |   |       v            |              v            |       |   |
      + |   |       +------------+--+--------+--+------------+- ...  |   |
      + |   |                       |        ^                       |   |
      + |   |                       |        |                       |   |
      + |   +-----------------------|--------|-----------------------+   |
      + |                           |        |                           |
      + |                           v        |                           |
      + |   +--------------------------+  +--------------------------+   |
      + |   |        Compressor        |  |       Decompressor       |   |
      + |   +--------------------------+  +--------------------------+   |
      + |                           |        ^                           |
      + |                           v        |                           |
      + |   +--------------------------+  +--------------------------+   |
      + |   |        Serializer        |  |       Deserializer       |   |
      + |   +--------------------------+  +--------------------------+   |
      + |                           |        ^                           |
      + +---------------------------|--------|---------------------------+
      +                             |        |
      +                             v        |
      +
      +                              Raw data
      +
      +*/
      + +
    • + +
    +
    + + diff --git a/doc/protocol/public/fonts/aller-bold.eot b/doc/protocol/public/fonts/aller-bold.eot new file mode 100755 index 00000000..1b32532a Binary files /dev/null and b/doc/protocol/public/fonts/aller-bold.eot differ diff --git a/doc/protocol/public/fonts/aller-bold.ttf b/doc/protocol/public/fonts/aller-bold.ttf new file mode 100755 index 00000000..dc4cc9c2 Binary files /dev/null and b/doc/protocol/public/fonts/aller-bold.ttf differ diff --git a/doc/protocol/public/fonts/aller-bold.woff b/doc/protocol/public/fonts/aller-bold.woff new file mode 100755 index 00000000..fa16fd0a Binary files /dev/null and b/doc/protocol/public/fonts/aller-bold.woff differ diff --git a/doc/protocol/public/fonts/aller-light.eot b/doc/protocol/public/fonts/aller-light.eot new file mode 100755 index 00000000..40bd654b Binary files /dev/null and b/doc/protocol/public/fonts/aller-light.eot differ diff --git a/doc/protocol/public/fonts/aller-light.ttf b/doc/protocol/public/fonts/aller-light.ttf new file mode 100755 index 00000000..c2c72902 Binary files /dev/null and b/doc/protocol/public/fonts/aller-light.ttf differ diff --git a/doc/protocol/public/fonts/aller-light.woff b/doc/protocol/public/fonts/aller-light.woff new file mode 100755 index 00000000..81a09d18 Binary files /dev/null and b/doc/protocol/public/fonts/aller-light.woff differ diff --git a/doc/protocol/public/fonts/novecento-bold.eot b/doc/protocol/public/fonts/novecento-bold.eot new file mode 100755 index 00000000..98a9a7fb Binary files /dev/null and b/doc/protocol/public/fonts/novecento-bold.eot differ diff --git a/doc/protocol/public/fonts/novecento-bold.ttf b/doc/protocol/public/fonts/novecento-bold.ttf new file mode 100755 index 00000000..2af39b08 Binary files /dev/null and b/doc/protocol/public/fonts/novecento-bold.ttf differ diff --git a/doc/protocol/public/fonts/novecento-bold.woff b/doc/protocol/public/fonts/novecento-bold.woff new file mode 100755 index 00000000..de558b5a Binary files /dev/null and b/doc/protocol/public/fonts/novecento-bold.woff differ diff --git a/doc/protocol/public/stylesheets/normalize.css b/doc/protocol/public/stylesheets/normalize.css new file mode 100644 index 00000000..73abb76f --- /dev/null +++ b/doc/protocol/public/stylesheets/normalize.css @@ -0,0 +1,375 @@ +/*! normalize.css v2.0.1 | MIT License | git.io/normalize */ + +/* ========================================================================== + HTML5 display definitions + ========================================================================== */ + +/* + * Corrects `block` display not defined in IE 8/9. + */ + +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +nav, +section, +summary { + display: block; +} + +/* + * Corrects `inline-block` display not defined in IE 8/9. + */ + +audio, +canvas, +video { + display: inline-block; +} + +/* + * Prevents modern browsers from displaying `audio` without controls. + * Remove excess height in iOS 5 devices. + */ + +audio:not([controls]) { + display: none; + height: 0; +} + +/* + * Addresses styling for `hidden` attribute not present in IE 8/9. + */ + +[hidden] { + display: none; +} + +/* ========================================================================== + Base + ========================================================================== */ + +/* + * 1. Sets default font family to sans-serif. + * 2. Prevents iOS text size adjust after orientation change, without disabling + * user zoom. + */ + +html { + font-family: sans-serif; /* 1 */ + -webkit-text-size-adjust: 100%; /* 2 */ + -ms-text-size-adjust: 100%; /* 2 */ +} + +/* + * Removes default margin. + */ + +body { + margin: 0; +} + +/* ========================================================================== + Links + ========================================================================== */ + +/* + * Addresses `outline` inconsistency between Chrome and other browsers. + */ + +a:focus { + outline: thin dotted; +} + +/* + * Improves readability when focused and also mouse hovered in all browsers. + */ + +a:active, +a:hover { + outline: 0; +} + +/* ========================================================================== + Typography + ========================================================================== */ + +/* + * Addresses `h1` font sizes within `section` and `article` in Firefox 4+, + * Safari 5, and Chrome. + */ + +h1 { + font-size: 2em; +} + +/* + * Addresses styling not present in IE 8/9, Safari 5, and Chrome. + */ + +abbr[title] { + border-bottom: 1px dotted; +} + +/* + * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome. + */ + +b, +strong { + font-weight: bold; +} + +/* + * Addresses styling not present in Safari 5 and Chrome. + */ + +dfn { + font-style: italic; +} + +/* + * Addresses styling not present in IE 8/9. + */ + +mark { + background: #ff0; + color: #000; +} + + +/* + * Corrects font family set oddly in Safari 5 and Chrome. + */ + +code, +kbd, +pre, +samp { + font-family: monospace, serif; + font-size: 1em; +} + +/* + * Improves readability of pre-formatted text in all browsers. + */ + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +/* + * Sets consistent quote types. + */ + +q { + quotes: "\201C" "\201D" "\2018" "\2019"; +} + +/* + * Addresses inconsistent and variable font size in all browsers. + */ + +small { + font-size: 80%; +} + +/* + * Prevents `sub` and `sup` affecting `line-height` in all browsers. + */ + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +/* ========================================================================== + Embedded content + ========================================================================== */ + +/* + * Removes border when inside `a` element in IE 8/9. + */ + +img { + border: 0; +} + +/* + * Corrects overflow displayed oddly in IE 9. + */ + +svg:not(:root) { + overflow: hidden; +} + +/* ========================================================================== + Figures + ========================================================================== */ + +/* + * Addresses margin not present in IE 8/9 and Safari 5. + */ + +figure { + margin: 0; +} + +/* ========================================================================== + Forms + ========================================================================== */ + +/* + * Define consistent border, margin, and padding. + */ + +fieldset { + border: 1px solid #c0c0c0; + margin: 0 2px; + padding: 0.35em 0.625em 0.75em; +} + +/* + * 1. Corrects color not being inherited in IE 8/9. + * 2. Remove padding so people aren't caught out if they zero out fieldsets. + */ + +legend { + border: 0; /* 1 */ + padding: 0; /* 2 */ +} + +/* + * 1. Corrects font family not being inherited in all browsers. + * 2. Corrects font size not being inherited in all browsers. + * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome + */ + +button, +input, +select, +textarea { + font-family: inherit; /* 1 */ + font-size: 100%; /* 2 */ + margin: 0; /* 3 */ +} + +/* + * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in + * the UA stylesheet. + */ + +button, +input { + line-height: normal; +} + +/* + * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` + * and `video` controls. + * 2. Corrects inability to style clickable `input` types in iOS. + * 3. Improves usability and consistency of cursor style between image-type + * `input` and others. + */ + +button, +html input[type="button"], /* 1 */ +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; /* 2 */ + cursor: pointer; /* 3 */ +} + +/* + * Re-set default cursor for disabled elements. + */ + +button[disabled], +input[disabled] { + cursor: default; +} + +/* + * 1. Addresses box sizing set to `content-box` in IE 8/9. + * 2. Removes excess padding in IE 8/9. + */ + +input[type="checkbox"], +input[type="radio"] { + box-sizing: border-box; /* 1 */ + padding: 0; /* 2 */ +} + +/* + * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome. + * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome + * (include `-moz` to future-proof). + */ + +input[type="search"] { + -webkit-appearance: textfield; /* 1 */ + -moz-box-sizing: content-box; + -webkit-box-sizing: content-box; /* 2 */ + box-sizing: content-box; +} + +/* + * Removes inner padding and search cancel button in Safari 5 and Chrome + * on OS X. + */ + +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} + +/* + * Removes inner padding and border in Firefox 4+. + */ + +button::-moz-focus-inner, +input::-moz-focus-inner { + border: 0; + padding: 0; +} + +/* + * 1. Removes default vertical scrollbar in IE 8/9. + * 2. Improves readability and alignment in all browsers. + */ + +textarea { + overflow: auto; /* 1 */ + vertical-align: top; /* 2 */ +} + +/* ========================================================================== + Tables + ========================================================================== */ + +/* + * Remove most spacing between table cells. + */ + +table { + border-collapse: collapse; + border-spacing: 0; +} \ No newline at end of file diff --git a/doc/protocol/stream.html b/doc/protocol/stream.html new file mode 100644 index 00000000..69c6f813 --- /dev/null +++ b/doc/protocol/stream.html @@ -0,0 +1,1485 @@ + + + + + The Stream class + + + + + +
    +
    + + + +
      + + + +
    • +
      + +
      + +
      + +
      + +
      var assert = require('assert');
      + +
    • + + +
    • +
      + +
      + +
      +

      The Stream class

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Stream is a Duplex stream +subclass that implements the HTTP/2 Stream +concept. It has two ‘sides’: one that is used by the user to send/receive data (the stream +object itself) and one that is used by a Connection to read/write frames to/from the other peer +(stream.upstream).

      + +
      + +
      +var Duplex = require('stream').Duplex;
      +
      +exports.Stream = Stream;
      + +
    • + + +
    • +
      + +
      + +
      +

      Public API

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • new Stream(log, connection): create a new Stream

        +
      • +
      • Event: ‘headers’ (headers): signals incoming headers

        +
      • +
      • Event: ‘promise’ (stream, headers): signals an incoming push promise

        +
      • +
      • Event: ‘priority’ (priority): signals a priority change. priority is a number between 0 + (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.

        +
      • +
      • Event: ‘error’ (type): signals an error

        +
      • +
      • headers(headers): send headers

        +
      • +
      • promise(headers): Stream: promise a stream

        +
      • +
      • priority(priority): set the priority of the stream. Priority can be changed by the peer +too, but once it is set locally, it can not be changed remotely.

        +
      • +
      • reset(error): reset the stream with an error code

        +
      • +
      • upstream: a Flow that is used by the parent connection to write/read frames +that are to be sent/arrived to/from the peer and are related to this stream.

        +
      • +
      +

      Headers are always in the regular node.js header format.

      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      Constructor

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The main aspects of managing the stream are:

      + +
      + +
      function Stream(log, connection) {
      +  Duplex.call(this);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • logging
      • +
      + +
      + +
        this._log = log.child({ component: 'stream', s: this });
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • receiving and sending stream management commands
      • +
      + +
      + +
        this._initializeManagement();
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • sending and receiving frames to/from the upstream connection
      • +
      + +
      + +
        this._initializeDataFlow();
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • maintaining the state of the stream (idle, open, closed, etc.) and error detection
      • +
      + +
      + +
        this._initializeState();
      +
      +  this.connection = connection;
      +}
      +
      +Stream.prototype = Object.create(Duplex.prototype, { constructor: { value: Stream } });
      + +
    • + + +
    • +
      + +
      + +
      +

      Managing the stream

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      the default stream priority is 2^30

      + +
      + +
      var DEFAULT_PRIORITY = Math.pow(2, 30);
      +var MAX_PRIORITY = Math.pow(2, 31) - 1;
      + +
    • + + +
    • +
      + +
      + +
      +

      PUSH_PROMISE and HEADERS are forwarded to the user through events.

      + +
      + +
      Stream.prototype._initializeManagement = function _initializeManagement() {
      +  this._resetSent = false;
      +  this._priority = DEFAULT_PRIORITY;
      +  this._letPeerPrioritize = true;
      +};
      +
      +Stream.prototype.promise = function promise(headers) {
      +  var stream = new Stream(this._log, this.connection);
      +  stream._priority = Math.min(this._priority + 1, MAX_PRIORITY);
      +  this._pushUpstream({
      +    type: 'PUSH_PROMISE',
      +    flags: {},
      +    stream: this.id,
      +    promised_stream: stream,
      +    headers: headers
      +  });
      +  return stream;
      +};
      +
      +Stream.prototype._onPromise = function _onPromise(frame) {
      +  this.emit('promise', frame.promised_stream, frame.headers);
      +};
      +
      +Stream.prototype.headers = function headers(headers) {
      +  this._pushUpstream({
      +    type: 'HEADERS',
      +    flags: {},
      +    stream: this.id,
      +    headers: headers
      +  });
      +};
      +
      +Stream.prototype._onHeaders = function _onHeaders(frame) {
      +  if (frame.priority !== undefined) {
      +    this.priority(frame.priority, true);
      +  }
      +  this.emit('headers', frame.headers);
      +};
      +
      +Stream.prototype.priority = function priority(priority, peer) {
      +  if ((peer && this._letPeerPrioritize) || !peer) {
      +    if (!peer) {
      +      this._letPeerPrioritize = false;
      +
      +      var lastFrame = this.upstream.getLastQueuedFrame();
      +      if (lastFrame && ((lastFrame.type === 'HEADERS') || (lastFrame.type === 'PRIORITY'))) {
      +        lastFrame.priority = priority;
      +      } else {
      +        this._pushUpstream({
      +          type: 'PRIORITY',
      +          flags: {},
      +          stream: this.id,
      +          priority: priority
      +        });
      +      }
      +    }
      +
      +    this._log.debug({ priority: priority }, 'Changing priority');
      +    this.emit('priority', priority);
      +    this._priority = priority;
      +  }
      +};
      +
      +Stream.prototype._onPriority = function _onPriority(frame) {
      +  this.priority(frame.priority, true);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Resetting the stream. Normally, an endpoint SHOULD NOT send more than one RST_STREAM frame for +any stream.

      + +
      + +
      Stream.prototype.reset = function reset(error) {
      +  if (!this._resetSent) {
      +    this._resetSent = true;
      +    this._pushUpstream({
      +      type: 'RST_STREAM',
      +      flags: {},
      +      stream: this.id,
      +      error: error
      +    });
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Specify an alternate service for the origin of this stream

      + +
      + +
      Stream.prototype.altsvc = function altsvc(host, port, protocolID, maxAge, origin) {
      +    var stream;
      +    if (origin) {
      +        stream = 0;
      +    } else {
      +        stream = this.id;
      +    }
      +    this._pushUpstream({
      +        type: 'ALTSVC',
      +        flags: {},
      +        stream: stream,
      +        host: host,
      +        port: port,
      +        protocolID: protocolID,
      +        origin: origin,
      +        maxAge: maxAge
      +    });
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Data flow

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
    • + + +
    • +
      + +
      + +
      +

      The incoming and the generated outgoing frames are received/transmitted on the this.upstream +Flow. The Connection object instantiating the stream will read +and write frames to/from it. The stream itself is a regular Duplex stream, and is used by +the user to write or read the body of the request.

      + +
      + +
    • + + +
    • +
      + +
      + +
      +
      upstream side                  stream                  user side
      +
      +               +------------------------------------+
      +               |                                    |
      +               +------------------+                 |
      +               |     upstream     |                 |
      +               |                  |                 |
      +               +--+               |              +--|
      +       read()  |  |  _send()      |    _write()  |  |  write(buf)
      +<--------------|B |<--------------|--------------| B|<------------
      +               |  |               |              |  |
      +       frames  +--+               |              +--|  buffers
      +               |  |               |              |  |
      +-------------->|B |---------------|------------->| B|------------>
      + write(frame)  |  |  _receive()   |     _read()  |  |  read()
      +               +--+               |              +--|
      +               |                  |                 |
      +               |                  |                 |
      +               +------------------+                 |
      +               |                                    |
      +               +------------------------------------+
      +
      +B: input or output buffer
      +
      +
      + +
      +var Flow = require('./flow').Flow;
      +
      +Stream.prototype._initializeDataFlow = function _initializeDataFlow() {
      +  this.id = undefined;
      +
      +  this._ended = false;
      +
      +  this.upstream = new Flow();
      +  this.upstream._log = this._log;
      +  this.upstream._send = this._send.bind(this);
      +  this.upstream._receive = this._receive.bind(this);
      +  this.upstream.write = this._writeUpstream.bind(this);
      +  this.upstream.on('error', this.emit.bind(this, 'error'));
      +
      +  this.on('finish', this._finishing);
      +};
      +
      +Stream.prototype._pushUpstream = function _pushUpstream(frame) {
      +  this.upstream.push(frame);
      +  this._transition(true, frame);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Overriding the upstream’s write allows us to act immediately instead of waiting for the input +queue to empty. This is important in case of control frames.

      + +
      + +
      Stream.prototype._writeUpstream = function _writeUpstream(frame) {
      +  this._log.debug({ frame: frame }, 'Receiving frame');
      +
      +  var moreNeeded = Flow.prototype.write.call(this.upstream, frame);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Transition to a new state if that’s the effect of receiving the frame
      • +
      + +
      + +
        this._transition(false, frame);
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • If it’s a control frame. Call the appropriate handler method.
      • +
      + +
      + +
        if (frame.type === 'HEADERS') {
      +    if (this._processedHeaders && !frame.flags['END_STREAM']) {
      +      this.emit('error', 'PROTOCOL_ERROR');
      +    }
      +    this._processedHeaders = true;
      +    this._onHeaders(frame);
      +  } else if (frame.type === 'PUSH_PROMISE') {
      +    this._onPromise(frame);
      +  } else if (frame.type === 'PRIORITY') {
      +    this._onPriority(frame);
      +  } else if (frame.type === 'ALTSVC') {
      + +
    • + + +
    • +
      + +
      + +
      +

      TODO

      + +
      + +
        } else if (frame.type === 'BLOCKED') {
      + +
    • + + +
    • +
      + +
      + +
      +

      TODO

      + +
      + +
        }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • If it’s an invalid stream level frame, emit error
      • +
      + +
      + +
        else if ((frame.type !== 'DATA') &&
      +           (frame.type !== 'WINDOW_UPDATE') &&
      +           (frame.type !== 'RST_STREAM')) {
      +    this._log.error({ frame: frame }, 'Invalid stream level frame');
      +    this.emit('error', 'PROTOCOL_ERROR');
      +  }
      +
      +  return moreNeeded;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The _receive method (= upstream._receive) gets called when there’s an incoming frame.

      + +
      + +
      Stream.prototype._receive = function _receive(frame, ready) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • If it’s a DATA frame, then push the payload into the output buffer on the other side. +Call ready when the other side is ready to receive more.
      • +
      + +
      + +
        if (!this._ended && (frame.type === 'DATA')) {
      +    var moreNeeded = this.push(frame.data);
      +    if (!moreNeeded) {
      +      this._receiveMore = ready;
      +    }
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Any frame may signal the end of the stream with the END_STREAM flag
      • +
      + +
      + +
        if (!this._ended && (frame.flags.END_STREAM || (frame.type === 'RST_STREAM'))) {
      +    this.push(null);
      +    this._ended = true;
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Postpone calling ready if push() returned a falsy value
      • +
      + +
      + +
        if (this._receiveMore !== ready) {
      +    ready();
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The _read method is called when the user side is ready to receive more data. If there’s a +pending write on the upstream, then call its pending ready callback to receive more frames.

      + +
      + +
      Stream.prototype._read = function _read() {
      +  if (this._receiveMore) {
      +    var receiveMore = this._receiveMore;
      +    delete this._receiveMore;
      +    receiveMore();
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The write method gets called when there’s a write request from the user.

      + +
      + +
      Stream.prototype._write = function _write(buffer, encoding, ready) {
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Chunking is done by the upstream Flow.
      • +
      + +
      + +
        var moreNeeded = this._pushUpstream({
      +    type: 'DATA',
      +    flags: {},
      +    stream: this.id,
      +    data: buffer
      +  });
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • Call ready when upstream is ready to receive more frames.
      • +
      + +
      + +
        if (moreNeeded) {
      +    ready();
      +  } else {
      +    this._sendMore = ready;
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      The _send (= upstream._send) method is called when upstream is ready to receive more frames. +If there’s a pending write on the user side, then call its pending ready callback to receive more +writes.

      + +
      + +
      Stream.prototype._send = function _send() {
      +  if (this._sendMore) {
      +    var sendMore = this._sendMore;
      +    delete this._sendMore;
      +    sendMore();
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      When the stream is finishing (the user calls end() on it), then we have to set the END_STREAM +flag on the last frame. If there’s no frame in the queue, or if it doesn’t support this flag, +then we create a 0 length DATA frame. We could do this all the time, but putting the flag on an +existing frame is a nice optimization.

      + +
      + +
      var emptyBuffer = new Buffer(0);
      +Stream.prototype._finishing = function _finishing() {
      +  var endFrame = {
      +    type: 'DATA',
      +    flags: { END_STREAM: true },
      +    stream: this.id,
      +    data: emptyBuffer
      +  };
      +  var lastFrame = this.upstream.getLastQueuedFrame();
      +  if (lastFrame && ((lastFrame.type === 'DATA') || (lastFrame.type === 'HEADERS'))) {
      +    this._log.debug({ frame: lastFrame }, 'Marking last frame with END_STREAM flag.');
      +    lastFrame.flags.END_STREAM = true;
      +    this._transition(true, endFrame);
      +  } else {
      +    this._pushUpstream(endFrame);
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Stream States

      + +
      + +
    • + + +
    • +
      + +
      + +
      +
                            +--------+
      +                PP    |        |    PP
      +             ,--------|  idle  |--------.
      +            /         |        |         \
      +           v          +--------+          v
      +    +----------+          |           +----------+
      +    |          |          | H         |          |
      +,---| reserved |          |           | reserved |---.
      +|   | (local)  |          v           | (remote) |   |
      +|   +----------+      +--------+      +----------+   |
      +|      |          ES  |        |  ES          |      |
      +|      | H    ,-------|  open  |-------.      | H    |
      +|      |     /        |        |        \     |      |
      +|      v    v         +--------+         v    v      |
      +|   +----------+          |           +----------+   |
      +|   |   half   |          |           |   half   |   |
      +|   |  closed  |          | R         |  closed  |   |
      +|   | (remote) |          |           | (local)  |   |
      +|   +----------+          |           +----------+   |
      +|        |                v                 |        |
      +|        |  ES / R    +--------+  ES / R    |        |
      +|        `----------->|        |<-----------'        |
      +|  R                  | closed |                  R  |
      +`-------------------->|        |<--------------------'
      +                      +--------+
      +
      +
      + +
    • + + +
    • +
      + +
      + +
      +

      Streams begin in the IDLE state and transitions happen when there’s an incoming or outgoing frame

      + +
      + +
      Stream.prototype._initializeState = function _initializeState() {
      +  this.state = 'IDLE';
      +  this._initiated = undefined;
      +  this._closedByUs = undefined;
      +  this._closedWithRst = undefined;
      +  this._processedHeaders = false;
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Only _setState should change this.state directly. It also logs the state change and notifies +interested parties using the ‘state’ event.

      + +
      + +
      Stream.prototype._setState = function transition(state) {
      +  assert(this.state !== state);
      +  this._log.debug({ from: this.state, to: state }, 'State transition');
      +  this.state = state;
      +  this.emit('state', state);
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      A state is ‘active’ if the stream in that state counts towards the concurrency limit. Streams +that are in the “open” state, or either of the “half closed” states count toward this limit.

      + +
      + +
      function activeState(state) {
      +  return ((state === 'HALF_CLOSED_LOCAL') || (state === 'HALF_CLOSED_REMOTE') || (state === 'OPEN'));
      +}
      + +
    • + + +
    • +
      + +
      + +
      +

      _transition is called every time there’s an incoming or outgoing frame. It manages state +transitions, and detects stream errors. A stream error is always caused by a frame that is not +allowed in the current state.

      + +
      + +
      Stream.prototype._transition = function transition(sending, frame) {
      +  var receiving = !sending;
      +  var connectionError;
      +  var streamError;
      +
      +  var DATA = false, HEADERS = false, PRIORITY = false, ALTSVC = false, BLOCKED = false;
      +  var RST_STREAM = false, PUSH_PROMISE = false, WINDOW_UPDATE = false;
      +  switch(frame.type) {
      +    case 'DATA'         : DATA          = true; break;
      +    case 'HEADERS'      : HEADERS       = true; break;
      +    case 'PRIORITY'     : PRIORITY      = true; break;
      +    case 'RST_STREAM'   : RST_STREAM    = true; break;
      +    case 'PUSH_PROMISE' : PUSH_PROMISE  = true; break;
      +    case 'WINDOW_UPDATE': WINDOW_UPDATE = true; break;
      +    case 'ALTSVC'       : ALTSVC        = true; break;
      +    case 'BLOCKED'      : BLOCKED       = true; break;
      +  }
      +
      +  var previousState = this.state;
      +
      +  switch (this.state) {
      + +
    • + + +
    • +
      + +
      + +
      +

      All streams start in the idle state. In this state, no frames have been exchanged.

      +
        +
      • Sending or receiving a HEADERS frame causes the stream to become “open”.
      • +
      +

      When the HEADERS frame contains the END_STREAM flags, then two state transitions happen.

      + +
      + +
          case 'IDLE':
      +      if (HEADERS) {
      +        this._setState('OPEN');
      +        if (frame.flags.END_STREAM) {
      +          this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE');
      +        }
      +        this._initiated = sending;
      +      } else if (sending && RST_STREAM) {
      +        this._setState('CLOSED');
      +      } else if (PRIORITY) {
      +        /* No state change */
      +      } else {
      +        connectionError = 'PROTOCOL_ERROR';
      +      }
      +      break;
      + +
    • + + +
    • +
      + +
      + +
      +

      A stream in the reserved (local) state is one that has been promised by sending a +PUSH_PROMISE frame.

      +
        +
      • The endpoint can send a HEADERS frame. This causes the stream to open in a “half closed +(remote)” state.
      • +
      • Either endpoint can send a RST_STREAM frame to cause the stream to become “closed”. This +releases the stream reservation.
      • +
      • An endpoint may receive PRIORITY frame in this state.
      • +
      • An endpoint MUST NOT send any other type of frame in this state.
      • +
      + +
      + +
          case 'RESERVED_LOCAL':
      +      if (sending && HEADERS) {
      +        this._setState('HALF_CLOSED_REMOTE');
      +      } else if (RST_STREAM) {
      +        this._setState('CLOSED');
      +      } else if (PRIORITY) {
      +        /* No state change */
      +      } else {
      +        connectionError = 'PROTOCOL_ERROR';
      +      }
      +      break;
      + +
    • + + +
    • +
      + +
      + +
      +

      A stream in the reserved (remote) state has been reserved by a remote peer.

      +
        +
      • Either endpoint can send a RST_STREAM frame to cause the stream to become “closed”. This +releases the stream reservation.
      • +
      • Receiving a HEADERS frame causes the stream to transition to “half closed (local)”.
      • +
      • An endpoint MAY send PRIORITY frames in this state to reprioritize the stream.
      • +
      • Receiving any other type of frame MUST be treated as a stream error of type PROTOCOL_ERROR.
      • +
      + +
      + +
          case 'RESERVED_REMOTE':
      +      if (RST_STREAM) {
      +        this._setState('CLOSED');
      +      } else if (receiving && HEADERS) {
      +        this._setState('HALF_CLOSED_LOCAL');
      +      } else if (BLOCKED || PRIORITY) {
      +        /* No state change */
      +      } else {
      +        connectionError = 'PROTOCOL_ERROR';
      +      }
      +      break;
      + +
    • + + +
    • +
      + +
      + +
      +

      The open state is where both peers can send frames. In this state, sending peers observe +advertised stream level flow control limits.

      +
        +
      • From this state either endpoint can send a frame with a END_STREAM flag set, which causes +the stream to transition into one of the “half closed” states: an endpoint sending a +END_STREAM flag causes the stream state to become “half closed (local)”; an endpoint +receiving a END_STREAM flag causes the stream state to become “half closed (remote)”.
      • +
      • Either endpoint can send a RST_STREAM frame from this state, causing it to transition +immediately to “closed”.
      • +
      + +
      + +
          case 'OPEN':
      +      if (frame.flags.END_STREAM) {
      +        this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE');
      +      } else if (RST_STREAM) {
      +        this._setState('CLOSED');
      +      } else {
      +        /* No state change */
      +      }
      +      break;
      + +
    • + + +
    • +
      + +
      + +
      +

      A stream that is half closed (local) cannot be used for sending frames.

      +
        +
      • A stream transitions from this state to “closed” when a frame that contains a END_STREAM +flag is received, or when either peer sends a RST_STREAM frame.
      • +
      • An endpoint MAY send or receive PRIORITY frames in this state to reprioritize the stream.
      • +
      • WINDOW_UPDATE can be sent by a peer that has sent a frame bearing the END_STREAM flag.
      • +
      + +
      + +
          case 'HALF_CLOSED_LOCAL':
      +      if (RST_STREAM || (receiving && frame.flags.END_STREAM)) {
      +        this._setState('CLOSED');
      +      } else if (BLOCKED || ALTSVC || receiving || PRIORITY || (sending && WINDOW_UPDATE)) {
      +        /* No state change */
      +      } else {
      +        connectionError = 'PROTOCOL_ERROR';
      +      }
      +      break;
      + +
    • + + +
    • +
      + +
      + +
      +

      A stream that is half closed (remote) is no longer being used by the peer to send frames. +In this state, an endpoint is no longer obligated to maintain a receiver flow control window +if it performs flow control.

      +
        +
      • If an endpoint receives additional frames for a stream that is in this state it MUST +respond with a stream error of type STREAM_CLOSED.
      • +
      • A stream can transition from this state to “closed” by sending a frame that contains a +END_STREAM flag, or when either peer sends a RST_STREAM frame.
      • +
      • An endpoint MAY send or receive PRIORITY frames in this state to reprioritize the stream.
      • +
      • A receiver MAY receive a WINDOW_UPDATE frame on a “half closed (remote)” stream.
      • +
      + +
      + +
          case 'HALF_CLOSED_REMOTE':
      +      if (RST_STREAM || (sending && frame.flags.END_STREAM)) {
      +        this._setState('CLOSED');
      +      } else if (BLOCKED || ALTSVC || sending || PRIORITY || (receiving && WINDOW_UPDATE)) {
      +        /* No state change */
      +      } else {
      +        connectionError = 'PROTOCOL_ERROR';
      +      }
      +      break;
      + +
    • + + +
    • +
      + +
      + +
      +

      The closed state is the terminal state.

      +
        +
      • An endpoint MUST NOT send frames on a closed stream. An endpoint that receives a frame +after receiving a RST_STREAM or a frame containing a END_STREAM flag on that stream MUST +treat that as a stream error of type STREAM_CLOSED.
      • +
      • WINDOW_UPDATE, PRIORITY or RST_STREAM frames can be received in this state for a short +period after a frame containing an END_STREAM flag is sent. Until the remote peer receives +and processes the frame bearing the END_STREAM flag, it might send either frame type. +Endpoints MUST ignore WINDOW_UPDATE frames received in this state, though endpoints MAY +choose to treat WINDOW_UPDATE frames that arrive a significant time after sending +END_STREAM as a connection error of type PROTOCOL_ERROR.
      • +
      • If this state is reached as a result of sending a RST_STREAM frame, the peer that receives +the RST_STREAM might have already sent - or enqueued for sending - frames on the stream +that cannot be withdrawn. An endpoint that sends a RST_STREAM frame MUST ignore frames that +it receives on closed streams after it has sent a RST_STREAM frame. An endpoint MAY choose +to limit the period over which it ignores frames and treat frames that arrive after this +time as being in error.
      • +
      • An endpoint might receive a PUSH_PROMISE frame after it sends RST_STREAM. PUSH_PROMISE +causes a stream to become “reserved”. If promised streams are not desired, a RST_STREAM +can be used to close any of those streams.
      • +
      + +
      + +
          case 'CLOSED':
      +      if (PRIORITY || (sending && RST_STREAM) ||
      +          (receiving && WINDOW_UPDATE) ||
      +          (receiving && this._closedByUs &&
      +           (this._closedWithRst || RST_STREAM || ALTSVC))) {
      +        /* No state change */
      +      } else {
      +        streamError = 'STREAM_CLOSED';
      +      }
      +      break;
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +

      Noting that the connection was closed by the other endpoint. It may be important in edge cases. +For example, when the peer tries to cancel a promised stream, but we already sent every data +on it, then the stream is in CLOSED state, yet we want to ignore the incoming RST_STREAM.

      + +
      + +
        if ((this.state === 'CLOSED') && (previousState !== 'CLOSED')) {
      +    this._closedByUs = sending;
      +    this._closedWithRst = RST_STREAM;
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +

      Sending/receiving a PUSH_PROMISE

      +
        +
      • Sending a PUSH_PROMISE frame marks the associated stream for later use. The stream state +for the reserved stream transitions to “reserved (local)”.
      • +
      • Receiving a PUSH_PROMISE frame marks the associated stream as reserved by the remote peer. +The state of the stream becomes “reserved (remote)”.
      • +
      + +
      + +
        if (PUSH_PROMISE && !connectionError && !streamError) {
      +    /* This assertion must hold, because _transition is called immediately when a frame is written
      +       to the stream. If it would be called when a frame gets out of the input queue, the state
      +       of the reserved could have been changed by then. */
      +    assert(frame.promised_stream.state === 'IDLE', frame.promised_stream.state);
      +    frame.promised_stream._setState(sending ? 'RESERVED_LOCAL' : 'RESERVED_REMOTE');
      +    frame.promised_stream._initiated = sending;
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +

      Signaling how sending/receiving this frame changes the active stream count (-1, 0 or +1)

      + +
      + +
        if (this._initiated) {
      +    var change = (activeState(this.state) - activeState(previousState));
      +    if (sending) {
      +      frame.count_change = change;
      +    } else {
      +      frame.count_change(change);
      +    }
      +  } else if (sending) {
      +    frame.count_change = 0;
      +  }
      + +
    • + + +
    • +
      + +
      + +
      +

      Common error handling.

      + +
      + +
        if (connectionError || streamError) {
      +    var info = {
      +      error: connectionError,
      +      frame: frame,
      +      state: this.state,
      +      closedByUs: this._closedByUs,
      +      closedWithRst: this._closedWithRst
      +    };
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • When sending something invalid, throwing an exception, since it is probably a bug.
      • +
      + +
      + +
          if (sending) {
      +      this._log.error(info, 'Sending illegal frame.');
      +      return this.emit('error', new Error('Sending illegal frame (' + frame.type + ') in ' + this.state + ' state.'));
      +    }
      + +
    • + + +
    • +
      + +
      + +
      +
        +
      • In case of a serious problem, emitting and error and letting someone else handle it +(e.g. closing the connection)
      • +
      • When receiving something invalid, sending an RST_STREAM using the reset method. +This will automatically cause a transition to the CLOSED state.
      • +
      + +
      + +
          else {
      +      this._log.error(info, 'Received illegal frame.');
      +      if (connectionError) {
      +        this.emit('connectionError', connectionError);
      +      } else {
      +        this.reset(streamError);
      +        this.emit('error', streamError);
      +      }
      +    }
      +  }
      +};
      + +
    • + + +
    • +
      + +
      + +
      +

      Bunyan serializers

      + +
      + +
    • + + +
    • +
      + +
      + +
      + +
      + +
      +exports.serializers = {};
      +
      +var nextId = 0;
      +exports.serializers.s = function(stream) {
      +  if (!('_id' in stream)) {
      +    stream._id = nextId;
      +    nextId += 1;
      +  }
      +  return stream._id;
      +};
      + +
    • + +
    +
    + + diff --git a/doc/root.jst b/doc/root.jst new file mode 100644 index 00000000..e1d8c9d0 --- /dev/null +++ b/doc/root.jst @@ -0,0 +1,59 @@ + + + + + <%= title %> + + + + + +
    +
    + <% if (sources.length > 1) { %> + + <% } %> +
      + <% if (!hasTitle) { %> +
    • +
      +

      <%= title %>

      +
      +
    • + <% } %> + <% for (var i=0, l=sections.length; i + <% var section = sections[i]; %> +
    • +
      + <% heading = section.docsHtml.match(/^\s*<(h\d)>/) %> +
      + +
      + <%= section.docsHtml %> +
      + <% if (section.codeText.replace(/\s/gm, '') != '') { %> +
      <%= section.codeHtml %>
      + <% } %> +
    • + <% } %> +
    +
    + + diff --git a/doc/stream.html b/doc/stream.html deleted file mode 100644 index 7dcf715e..00000000 --- a/doc/stream.html +++ /dev/null @@ -1,1312 +0,0 @@ - - - - - stream.js - - - - - -
    -
    - - - -
      - -
    • -
      -

      stream.js

      -
      -
    • - - - -
    • -
      - -
      - -
      - -
      - -
      var assert = require('assert');
      - -
    • - - -
    • -
      - -
      - -
      -

      The Stream class

      - -
      - -
    • - - -
    • -
      - -
      - -
      -

      Stream is a Duplex stream -subclass that implements the HTTP/2 Stream -concept. It has two 'sides': one that is used by the user to send/receive data (the stream -object itself) and one that is used by a Connection to read/write frames to/from the other peer -(stream.upstream).

      - -
      - -
      var Duplex = require('stream').Duplex;
      -
      -exports.Stream = Stream;
      - -
    • - - -
    • -
      - -
      - -
      -

      Public API

      - -
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • new Stream(log): create a new Stream

        -
      • -
      • Event: 'headers' (headers): signals incoming headers

        -
      • -
      • Event: 'promise' (stream, headers): signals an incoming push promise

        -
      • -
      • Event: 'priority' (priority): signals a priority change. priority is a number between 0 - (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.

        -
      • -
      • Event: 'error' (type): signals an error

        -
      • -
      • headers(headers): send headers

        -
      • -
      • promise(headers): Stream: promise a stream

        -
      • -
      • priority(priority): set the priority of the stream. Priority can be changed by the peer -too, but once it is set locally, it can not be changed remotely.

        -
      • -
      • reset(error): reset the stream with an error code

        -
      • -
      • upstream: a Flow that is used by the parent connection to write/read frames -that are to be sent/arrived to/from the peer and are related to this stream.

        -
      • -
      -

      Headers are always in the regular node.js header format.

      -

      Constructor

      - -
      - -
    • - - -
    • -
      - -
      - -
      -

      The main aspects of managing the stream are:

      - -
      - -
      function Stream(log) {
      -  Duplex.call(this);
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • logging
      • -
      - -
      - -
        this._log = log.child({ component: 'stream', s: this });
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • receiving and sending stream management commands
      • -
      - -
      - -
        this._initializeManagement();
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • sending and receiving frames to/from the upstream connection
      • -
      - -
      - -
        this._initializeDataFlow();
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • maintaining the state of the stream (idle, open, closed, etc.) and error detection
      • -
      - -
      - -
        this._initializeState();
      -}
      -
      -Stream.prototype = Object.create(Duplex.prototype, { constructor: { value: Stream } });
      - -
    • - - -
    • -
      - -
      - -
      -

      Managing the stream

      - -
      - -
    • - - -
    • -
      - -
      - -
      -

      the default stream priority is 2^30

      - -
      - -
      var DEFAULT_PRIORITY = Math.pow(2, 30);
      -var MAX_PRIORITY = Math.pow(2, 31) - 1;
      - -
    • - - -
    • -
      - -
      - -
      -

      PUSH_PROMISE and HEADERS are forwarded to the user through events.

      - -
      - -
      Stream.prototype._initializeManagement = function _initializeManagement() {
      -  this._resetSent = false;
      -  this._priority = DEFAULT_PRIORITY;
      -  this._letPeerPrioritize = true;
      -};
      -
      -Stream.prototype.promise = function promise(headers) {
      -  var stream = new Stream(this._log);
      -  stream._priority = Math.min(this._priority + 1, MAX_PRIORITY);
      -  this._pushUpstream({
      -    type: 'PUSH_PROMISE',
      -    flags: {},
      -    stream: this.id,
      -    promised_stream: stream,
      -    headers: headers
      -  });
      -  return stream;
      -};
      -
      -Stream.prototype._onPromise = function _onPromise(frame) {
      -  this.emit('promise', frame.promised_stream, frame.headers);
      -};
      -
      -Stream.prototype.headers = function headers(headers) {
      -  this._pushUpstream({
      -    type: 'HEADERS',
      -    flags: {},
      -    stream: this.id,
      -    headers: headers
      -  });
      -};
      -
      -Stream.prototype._onHeaders = function _onHeaders(frame) {
      -  if (frame.priority !== undefined) {
      -    this.priority(frame.priority, true);
      -  }
      -  this.emit('headers', frame.headers);
      -};
      -
      -Stream.prototype.priority = function priority(priority, peer) {
      -  if ((peer && this._letPeerPrioritize) || !peer) {
      -    if (!peer) {
      -      this._letPeerPrioritize = false;
      -
      -      var lastFrame = this.upstream.getLastQueuedFrame();
      -      if (lastFrame && ((lastFrame.type === 'HEADERS') || (lastFrame.type === 'PRIORITY'))) {
      -        lastFrame.priority = priority;
      -      } else {
      -        this._pushUpstream({
      -          type: 'PRIORITY',
      -          flags: {},
      -          stream: this.id,
      -          priority: priority
      -        });
      -      }
      -    }
      -
      -    this._log.debug({ priority: priority }, 'Changing priority');
      -    this.emit('priority', priority);
      -    this._priority = priority;
      -  }
      -};
      -
      -Stream.prototype._onPriority = function _onPriority(frame) {
      -  this.priority(frame.priority, true);
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      Resetting the stream. Normally, an endpoint SHOULD NOT send more than one RST_STREAM frame for -any stream.

      - -
      - -
      Stream.prototype.reset = function reset(error) {
      -  if (!this._resetSent) {
      -    this._resetSent = true;
      -    this._pushUpstream({
      -      type: 'RST_STREAM',
      -      flags: {},
      -      stream: this.id,
      -      error: error
      -    });
      -  }
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      Data flow

      - -
      - -
    • - - -
    • -
      - -
      - -
      -

      The incoming and the generated outgoing frames are received/transmitted on the this.upsteam -Flow. The Connection object instantiating the stream will read -and write frames to/from it. The stream itself is a regular Duplex stream, and is used by -the user to write or read the body of the request.

      -
      upstream side                  stream                  user side
      -
      -               +------------------------------------+
      -               |                                    |
      -               +------------------+                 |
      -               |     upstream     |                 |
      -               |                  |                 |
      -               +--+               |              +--|
      -       read()  |  |  _send()      |    _write()  |  |  write(buf)
      -<--------------|B |<--------------|--------------| B|<------------
      -               |  |               |              |  |
      -       frames  +--+               |              +--|  buffers
      -               |  |               |              |  |
      --------------->|B |---------------|------------->| B|------------>
      - write(frame)  |  |  _receive()   |     _read()  |  |  read()
      -               +--+               |              +--|
      -               |                  |                 |
      -               |                  |                 |
      -               +------------------+                 |
      -               |                                    |
      -               +------------------------------------+
      -
      -B: input or output buffer
      - -
      - -
      var Flow = require('./flow').Flow;
      -
      -Stream.prototype._initializeDataFlow = function _initializeDataFlow() {
      -  this.id = undefined;
      -
      -  this._ended = false;
      -
      -  this.upstream = new Flow();
      -  this.upstream._log = this._log;
      -  this.upstream._send = this._send.bind(this);
      -  this.upstream._receive = this._receive.bind(this);
      -  this.upstream.write = this._writeUpstream.bind(this);
      -  this.upstream.on('error', this.emit.bind(this, 'error'));
      -
      -  this.on('finish', this._finishing);
      -};
      -
      -Stream.prototype._pushUpstream = function _pushUpstream(frame) {
      -  this.upstream.push(frame);
      -  this._transition(true, frame);
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      Overriding the upstream's write allows us to act immediately instead of waiting for the input -queue to empty. This is important in case of control frames.

      - -
      - -
      Stream.prototype._writeUpstream = function _writeUpstream(frame) {
      -  this._log.debug({ frame: frame }, 'Receiving frame');
      -
      -  var moreNeeded = Flow.prototype.write.call(this.upstream, frame);
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • Transition to a new state if that's the effect of receiving the frame
      • -
      - -
      - -
        this._transition(false, frame);
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • If it's a control frame. Call the appropriate handler method.
      • -
      - -
      - -
        if (frame.type === 'HEADERS') {
      -    this._onHeaders(frame);
      -  } else if (frame.type === 'PUSH_PROMISE') {
      -    this._onPromise(frame);
      -  } else if (frame.type === 'PRIORITY') {
      -    this._onPriority(frame);
      -  }
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • If it's an invalid stream level frame, emit error
      • -
      - -
      - -
        else if ((frame.type !== 'DATA') &&
      -           (frame.type !== 'WINDOW_UPDATE') &&
      -           (frame.type !== 'RST_STREAM')) {
      -    this._log.error({ frame: frame }, 'Invalid stream level frame');
      -    this.emit('error', 'PROTOCOL_ERROR');
      -  }
      -
      -  return moreNeeded;
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      The _receive method (= upstream._receive) gets called when there's an incoming frame.

      - -
      - -
      Stream.prototype._receive = function _receive(frame, ready) {
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • If it's a DATA frame, then push the payload into the output buffer on the other side. -Call ready when the other side is ready to receive more.
      • -
      - -
      - -
        if (!this._ended && (frame.type === 'DATA')) {
      -    var moreNeeded = this.push(frame.data);
      -    if (!moreNeeded) {
      -      this._receiveMore = ready;
      -    }
      -  }
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • Any frame may signal the end of the stream with the END_STREAM flag
      • -
      - -
      - -
        if (!this._ended && (frame.flags.END_STREAM || (frame.type === 'RST_STREAM'))) {
      -    this.push(null);
      -    this._ended = true;
      -  }
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • Postpone calling ready if push() returned a falsy value
      • -
      - -
      - -
        if (this._receiveMore !== ready) {
      -    ready();
      -  }
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      The _read method is called when the user side is ready to receive more data. If there's a -pending write on the upstream, then call its pending ready callback to receive more frames.

      - -
      - -
      Stream.prototype._read = function _read() {
      -  if (this._receiveMore) {
      -    var receiveMore = this._receiveMore;
      -    delete this._receiveMore;
      -    receiveMore();
      -  }
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      The write method gets called when there's a write request from the user.

      - -
      - -
      Stream.prototype._write = function _write(buffer, encoding, ready) {
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • Chunking is done by the upstream Flow.
      • -
      - -
      - -
        var moreNeeded = this._pushUpstream({
      -    type: 'DATA',
      -    flags: {},
      -    stream: this.id,
      -    data: buffer
      -  });
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • Call ready when upstream is ready to receive more frames.
      • -
      - -
      - -
        if (moreNeeded) {
      -    ready();
      -  } else {
      -    this._sendMore = ready;
      -  }
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      The _send (= upstream._send) method is called when upstream is ready to receive more frames. -If there's a pending write on the user side, then call its pending ready callback to receive more -writes.

      - -
      - -
      Stream.prototype._send = function _send() {
      -  if (this._sendMore) {
      -    var sendMore = this._sendMore;
      -    delete this._sendMore;
      -    sendMore();
      -  }
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      When the stream is finishing (the user calls end() on it), then we have to set the END_STREAM -flag on the last frame. If there's no frame in the queue, or if it doesn't support this flag, -then we create a 0 length DATA frame. We could do this all the time, but putting the flag on an -existing frame is a nice optimization.

      - -
      - -
      var emptyBuffer = new Buffer(0);
      -Stream.prototype._finishing = function _finishing() {
      -  var endFrame = {
      -    type: 'DATA',
      -    flags: { END_STREAM: true },
      -    stream: this.id,
      -    data: emptyBuffer
      -  };
      -  var lastFrame = this.upstream.getLastQueuedFrame();
      -  if (lastFrame && ((lastFrame.type === 'DATA') || (lastFrame.type === 'HEADERS'))) {
      -    this._log.debug({ frame: lastFrame }, 'Marking last frame with END_STREAM flag.');
      -    lastFrame.flags.END_STREAM = true;
      -    this._transition(true, endFrame);
      -  } else {
      -    this._pushUpstream(endFrame);
      -  }
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      Stream States

      - -
      - -
    • - - -
    • -
      - -
      - -
      -
                            +--------+
      -                PP    |        |    PP
      -             ,--------|  idle  |--------.
      -            /         |        |         \
      -           v          +--------+          v
      -    +----------+          |           +----------+
      -    |          |          | H         |          |
      -,---| reserved |          |           | reserved |---.
      -|   | (local)  |          v           | (remote) |   |
      -|   +----------+      +--------+      +----------+   |
      -|      |          ES  |        |  ES          |      |
      -|      | H    ,-------|  open  |-------.      | H    |
      -|      |     /        |        |        \     |      |
      -|      v    v         +--------+         v    v      |
      -|   +----------+          |           +----------+   |
      -|   |   half   |          |           |   half   |   |
      -|   |  closed  |          | R         |  closed  |   |
      -|   | (remote) |          |           | (local)  |   |
      -|   +----------+          |           +----------+   |
      -|        |                v                 |        |
      -|        |  ES / R    +--------+  ES / R    |        |
      -|        `----------->|        |<-----------'        |
      -|  R                  | closed |                  R  |
      -`-------------------->|        |<--------------------'
      -                      +--------+
      -

      Streams begin in the IDLE state and transitions happen when there's an incoming or outgoing frame

      - -
      - -
      Stream.prototype._initializeState = function _initializeState() {
      -  this.state = 'IDLE';
      -  this._initiated = undefined;
      -  this._closedByUs = undefined;
      -  this._closedWithRst = undefined;
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      Only _setState should change this.state directly. It also logs the state change and notifies -interested parties using the 'state' event.

      - -
      - -
      Stream.prototype._setState = function transition(state) {
      -  assert(this.state !== state);
      -  this._log.debug({ from: this.state, to: state }, 'State transition');
      -  this.state = state;
      -  this.emit('state', state);
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      A state is 'active' if the stream in that state counts towards the concurrency limit. Streams -that are in the "open" state, or either of the "half closed" states count toward this limit.

      - -
      - -
      function activeState(state) {
      -  return ((state === 'HALF_CLOSED_LOCAL') || (state === 'HALF_CLOSED_REMOTE') || (state === 'OPEN'));
      -}
      - -
    • - - -
    • -
      - -
      - -
      -

      _transition is called every time there's an incoming or outgoing frame. It manages state -transitions, and detects stream errors. A stream error is always caused by a frame that is not -allowed in the current state.

      - -
      - -
      Stream.prototype._transition = function transition(sending, frame) {
      -  var receiving = !sending;
      -  var error = undefined;
      -
      -  var DATA = false, HEADERS = false, PRIORITY = false;
      -  var RST_STREAM = false, PUSH_PROMISE = false, WINDOW_UPDATE = false;
      -  switch(frame.type) {
      -    case 'DATA'         : DATA          = true; break;
      -    case 'HEADERS'      : HEADERS       = true; break;
      -    case 'PRIORITY'     : PRIORITY      = true; break;
      -    case 'RST_STREAM'   : RST_STREAM    = true; break;
      -    case 'PUSH_PROMISE' : PUSH_PROMISE  = true; break;
      -    case 'WINDOW_UPDATE': WINDOW_UPDATE = true; break;
      -  }
      -
      -  var previousState = this.state;
      -
      -  switch (this.state) {
      - -
    • - - -
    • -
      - -
      - -
      -

      All streams start in the idle state. In this state, no frames have been exchanged.

      -
        -
      • Sending or receiving a HEADERS frame causes the stream to become "open".
      • -
      -

      When the HEADERS frame contains the END_STREAM flags, then two state transitions happen.

      - -
      - -
          case 'IDLE':
      -      if (HEADERS) {
      -        this._setState('OPEN');
      -        if (frame.flags.END_STREAM) {
      -          this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE');
      -        }
      -        this._initiated = sending;
      -      } else if (sending && RST_STREAM) {
      -        this._setState('CLOSED');
      -      } else {
      -        error = 'PROTOCOL_ERROR';
      -      }
      -      break;
      - -
    • - - -
    • -
      - -
      - -
      -

      A stream in the reserved (local) state is one that has been promised by sending a -PUSH_PROMISE frame.

      -
        -
      • The endpoint can send a HEADERS frame. This causes the stream to open in a "half closed -(remote)" state.
      • -
      • Either endpoint can send a RST_STREAM frame to cause the stream to become "closed". This -releases the stream reservation.
      • -
      • An endpoint may receive PRIORITY frame in this state.
      • -
      • An endpoint MUST NOT send any other type of frame in this state.
      • -
      - -
      - -
          case 'RESERVED_LOCAL':
      -      if (sending && HEADERS) {
      -        this._setState('HALF_CLOSED_REMOTE');
      -      } else if (RST_STREAM) {
      -        this._setState('CLOSED');
      -      } else if (receiving && PRIORITY) {
      -        /* No state change */
      -      } else {
      -        error = 'PROTOCOL_ERROR';
      -      }
      -      break;
      - -
    • - - -
    • -
      - -
      - -
      -

      A stream in the reserved (remote) state has been reserved by a remote peer.

      -
        -
      • Either endpoint can send a RST_STREAM frame to cause the stream to become "closed". This -releases the stream reservation.
      • -
      • Receiving a HEADERS frame causes the stream to transition to "half closed (local)".
      • -
      • An endpoint MAY send PRIORITY frames in this state to reprioritize the stream.
      • -
      • Receiving any other type of frame MUST be treated as a stream error of type PROTOCOL_ERROR.
      • -
      - -
      - -
          case 'RESERVED_REMOTE':
      -      if (RST_STREAM) {
      -        this._setState('CLOSED');
      -      } else if (receiving && HEADERS) {
      -        this._setState('HALF_CLOSED_LOCAL');
      -      } else if (sending && PRIORITY) {
      -        /* No state change */
      -      } else {
      -        error = 'PROTOCOL_ERROR';
      -      }
      -      break;
      - -
    • - - -
    • -
      - -
      - -
      -

      The open state is where both peers can send frames. In this state, sending peers observe -advertised stream level flow control limits.

      -
        -
      • From this state either endpoint can send a frame with a END_STREAM flag set, which causes -the stream to transition into one of the "half closed" states: an endpoint sending a -END_STREAM flag causes the stream state to become "half closed (local)"; an endpoint -receiving a END_STREAM flag causes the stream state to become "half closed (remote)".
      • -
      • Either endpoint can send a RST_STREAM frame from this state, causing it to transition -immediately to "closed".
      • -
      - -
      - -
          case 'OPEN':
      -      if (frame.flags.END_STREAM) {
      -        this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE');
      -      } else if (RST_STREAM) {
      -        this._setState('CLOSED');
      -      } else {
      -        /* No state change */
      -      }
      -      break;
      - -
    • - - -
    • -
      - -
      - -
      -

      A stream that is half closed (local) cannot be used for sending frames.

      -
        -
      • A stream transitions from this state to "closed" when a frame that contains a END_STREAM -flag is received, or when either peer sends a RST_STREAM frame.
      • -
      • An endpoint MAY send or receive PRIORITY frames in this state to reprioritize the stream.
      • -
      • WINDOW_UPDATE can be sent by a peer that has sent a frame bearing the END_STREAM flag.
      • -
      - -
      - -
          case 'HALF_CLOSED_LOCAL':
      -      if (RST_STREAM || (receiving && frame.flags.END_STREAM)) {
      -        this._setState('CLOSED');
      -      } else if (receiving || (sending && (PRIORITY || WINDOW_UPDATE))) {
      -        /* No state change */
      -      } else {
      -        error = 'PROTOCOL_ERROR';
      -      }
      -      break;
      - -
    • - - -
    • -
      - -
      - -
      -

      A stream that is half closed (remote) is no longer being used by the peer to send frames. -In this state, an endpoint is no longer obligated to maintain a receiver flow control window -if it performs flow control.

      -
        -
      • If an endpoint receives additional frames for a stream that is in this state it MUST -respond with a stream error of type STREAM_CLOSED.
      • -
      • A stream can transition from this state to "closed" by sending a frame that contains a -END_STREAM flag, or when either peer sends a RST_STREAM frame.
      • -
      • An endpoint MAY send or receive PRIORITY frames in this state to reprioritize the stream.
      • -
      • A receiver MAY receive a WINDOW_UPDATE frame on a "half closed (remote)" stream.
      • -
      - -
      - -
          case 'HALF_CLOSED_REMOTE':
      -      if (RST_STREAM || (sending && frame.flags.END_STREAM)) {
      -        this._setState('CLOSED');
      -      } else if (sending || (receiving && (WINDOW_UPDATE || PRIORITY))) {
      -        /* No state change */
      -      } else {
      -        error = 'PROTOCOL_ERROR';
      -      }
      -      break;
      - -
    • - - -
    • -
      - -
      - -
      -

      The closed state is the terminal state.

      -
        -
      • An endpoint MUST NOT send frames on a closed stream. An endpoint that receives a frame -after receiving a RST_STREAM or a frame containing a END_STREAM flag on that stream MUST -treat that as a stream error of type STREAM_CLOSED.
      • -
      • WINDOW_UPDATE, PRIORITY or RST_STREAM frames can be received in this state for a short -period after a frame containing an END_STREAM flag is sent. Until the remote peer receives -and processes the frame bearing the END_STREAM flag, it might send either frame type. -Endpoints MUST ignore WINDOW_UPDATE frames received in this state, though endpoints MAY -choose to treat WINDOW_UPDATE frames that arrive a significant time after sending -END_STREAM as a connection error of type PROTOCOL_ERROR.
      • -
      • If this state is reached as a result of sending a RST_STREAM frame, the peer that receives -the RST_STREAM might have already sent - or enqueued for sending - frames on the stream -that cannot be withdrawn. An endpoint that sends a RST_STREAM frame MUST ignore frames that -it receives on closed streams after it has sent a RST_STREAM frame. An endpoint MAY choose -to limit the period over which it ignores frames and treat frames that arrive after this -time as being in error.
      • -
      • An endpoint might receive a PUSH_PROMISE frame after it sends RST_STREAM. PUSH_PROMISE -causes a stream to become "reserved". If promised streams are not desired, a RST_STREAM -can be used to close any of those streams.
      • -
      - -
      - -
          case 'CLOSED':
      -      if ((sending && RST_STREAM) ||
      -          (receiving && this._closedByUs &&
      -           (this._closedWithRst || WINDOW_UPDATE || PRIORITY || RST_STREAM))) {
      -        /* No state change */
      -      } else {
      -        error = 'STREAM_CLOSED';
      -      }
      -      break;
      -  }
      - -
    • - - -
    • -
      - -
      - -
      -

      Noting that the connection was closed by the other endpoint. It may be important in edge cases. -For example, when the peer tries to cancel a promised stream, but we already sent every data -on it, then the stream is in CLOSED state, yet we want to ignore the incoming RST_STREAM.

      - -
      - -
        if ((this.state === 'CLOSED') && (previousState !== 'CLOSED')) {
      -    this._closedByUs = sending;
      -    this._closedWithRst = RST_STREAM;
      -  }
      - -
    • - - -
    • -
      - -
      - -
      -

      Sending/receiving a PUSH_PROMISE

      -
        -
      • Sending a PUSH_PROMISE frame marks the associated stream for later use. The stream state -for the reserved stream transitions to "reserved (local)".
      • -
      • Receiving a PUSH_PROMISE frame marks the associated stream as reserved by the remote peer. -The state of the stream becomes "reserved (remote)".
      • -
      - -
      - -
        if (PUSH_PROMISE && !error) {
      -    /* This assertion must hold, because _transition is called immediately when a frame is written
      -       to the stream. If it would be called when a frame gets out of the input queue, the state
      -       of the reserved could have been changed by then. */
      -    assert(frame.promised_stream.state === 'IDLE', frame.promised_stream.state);
      -    frame.promised_stream._setState(sending ? 'RESERVED_LOCAL' : 'RESERVED_REMOTE');
      -    frame.promised_stream._initiated = sending;
      -  }
      - -
    • - - -
    • -
      - -
      - -
      -

      Signaling how sending/receiving this frame changes the active stream count (-1, 0 or +1)

      - -
      - -
        if (this._initiated) {
      -    var change = (activeState(this.state) - activeState(previousState));
      -    if (sending) {
      -      frame.count_change = change;
      -    } else {
      -      frame.count_change(change);
      -    }
      -  } else if (sending) {
      -    frame.count_change = 0;
      -  }
      - -
    • - - -
    • -
      - -
      - -
      -

      Common error handling.

      - -
      - -
        if (error) {
      -    var info = {
      -      error: error,
      -      frame: frame,
      -      state: this.state,
      -      closedByUs: this._closedByUs,
      -      closedWithRst: this._closedWithRst
      -    };
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • When sending something invalid, throwing an exception, since it is probably a bug.
      • -
      - -
      - -
          if (sending) {
      -      this._log.error(info, 'Sending illegal frame.');
      -      throw new Error('Sending illegal frame (' + frame.type + ') in ' + this.state + ' state.');
      -    }
      - -
    • - - -
    • -
      - -
      - -
      -
        -
      • When receiving something invalid, sending an RST_STREAM using the reset method. -This will automatically cause a transition to the CLOSED state.
      • -
      - -
      - -
          else {
      -      this._log.error(info, 'Received illegal frame.');
      -      this.emit('error', error);
      -    }
      -  }
      -};
      - -
    • - - -
    • -
      - -
      - -
      -

      Bunyan serializers

      - -
      - -
    • - - -
    • -
      - -
      - -
      - -
      - -
      exports.serializers = {};
      -
      -var nextId = 0;
      -exports.serializers.s = function(stream) {
      -  if (!('_id' in stream)) {
      -    stream._id = nextId;
      -    nextId += 1;
      -  }
      -  return stream._id;
      -};
      - -
    • - -
    -
    - - diff --git a/example/client.js b/example/client.js deleted file mode 100644 index 89ad8a45..00000000 --- a/example/client.js +++ /dev/null @@ -1,42 +0,0 @@ -var fs = require('fs'); -var path = require('path'); -var http2 = require('..'); - -http2.globalAgent = new http2.Agent({ - log: require('../test/util').createLogger('client') -}); - -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - -// Sending the request -// It would be `var request = http2.get(process.argv.pop());` if we wouldn't care about plain mode -var options = require('url').parse(process.argv.pop()); -options.plain = Boolean(process.env.HTTP2_PLAIN); -var request = http2.request(options); -request.end(); - -// Receiving the response -request.on('response', function(response) { - response.pipe(process.stdout); - response.on('end', finish); -}); - -// Receiving push streams -request.on('push', function(pushRequest) { - var filename = path.join(__dirname, '/push-' + push_count); - push_count += 1; - console.error('Receiving pushed resource: ' + pushRequest.url + ' -> ' + filename); - pushRequest.on('response', function(pushResponse) { - pushResponse.pipe(fs.createWriteStream(filename)).on('finish', finish); - }); -}); - -// Quitting after both the response and the associated pushed resources have arrived -var push_count = 0; -var finished = 0; -function finish() { - finished += 1; - if (finished === (1 + push_count)) { - process.exit(); - } -} diff --git a/example/localhost.crt b/example/localhost.crt deleted file mode 100644 index e5c5b383..00000000 --- a/example/localhost.crt +++ /dev/null @@ -1,14 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICIzCCAYwCCQCsvG34Az33qTANBgkqhkiG9w0BAQUFADBWMQswCQYDVQQGEwJY -WDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBh -bnkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTMwODAyMTMwODQzWhcNMTMw -OTAxMTMwODQzWjBWMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5 -MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMRIwEAYDVQQDDAlsb2NhbGhv -c3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM8D4tgE1cdI9uLo4N9AL8Ck -ogREH5LSm3SsRGFdUu5b2Nx63K/qwtTUbtUlISZBI+KESkwQXcf1ErwXUDnbTtk/ -VpLJ+gfIN18e9LAdiZgAMEWlitiLhR+D17w4NzHYOpWy1YzgOckukPy1ZfTH9e7j -tEH9+7c4mpv7QMkFdw4hAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAP+ZFskjJtNxY -c+5JfMjEgSHEIy+AJ5/vXIspNYKMb7l0gYDvmFm8QTKChKTYvJmepBrIdL7MjXCX -SWiPz05ch99c84yOx5qVpcPd0y2fjO8xn2NCLfWdP7iSVYmpftwzjqFzPc4EkAny -NOpbnw9iM4JXsZNFtPTvSp+8StPGWzU= ------END CERTIFICATE----- diff --git a/example/localhost.key b/example/localhost.key deleted file mode 100644 index 391f82b6..00000000 --- a/example/localhost.key +++ /dev/null @@ -1,15 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQDPA+LYBNXHSPbi6ODfQC/ApKIERB+S0pt0rERhXVLuW9jcetyv -6sLU1G7VJSEmQSPihEpMEF3H9RK8F1A5207ZP1aSyfoHyDdfHvSwHYmYADBFpYrY -i4Ufg9e8ODcx2DqVstWM4DnJLpD8tWX0x/Xu47RB/fu3OJqb+0DJBXcOIQIDAQAB -AoGAHtRVVBZkP+l92w0TcCv+8JGUD06V5Se4Pwfopxde4mCLS0qA0zIDEe8REm0V -Ir1Quss4xVsqnDzDLX/LUtJ2S1+seWcoLdDV/wSDiM2CLS7KauUazrTWHLNId/lu -/VombYWK10uNiDZZJ8xwEaKt+ZptC2kK8/yi0aX0PrGhAIECQQDsD8A64BBrWCrb -7PrJt04CAcM3uBUzS6ausiJKw9IEktnvcnsN9kZazcAW86WDFsXI5oPubmgHhQ/s -m9iIrbMPAkEA4IAUWi5mVuWAyUIc9YbjJdnmvkAykSxr/vp/26RMSDmUAAUlYNNc -HZbM1uVZsFForKza28Px01Ga728ZdhRrzwJBAIrwNlcwu9lCWm95Cp6hGfPKb8ki -uq+nTiKyS8avfLQebtElE1JDamNViEK6AuemBqFZM7upFeefJKFBlO/VNHcCQCXN -CyBALdU14aCBtFSXOMoXzaV9M8aD/084qKy4FmwW3de/BhMuo5UL3kPU7Gwm2QQy -OsvES4S0ee0U/OmH+LsCQAnNdxNPgzJDTx7wOTFhHIBr4mtepLiaRXIdkLEsR9Kb -vcK6BwUfomM29eGOXtUAU7sJ5xnyKkSuNN7fxIWjzPI= ------END RSA PRIVATE KEY----- diff --git a/example/server.js b/example/server.js deleted file mode 100644 index 73cdd6b4..00000000 --- a/example/server.js +++ /dev/null @@ -1,49 +0,0 @@ -var fs = require('fs'); -var path = require('path'); -var http2 = require('..'); - -var options = process.env.HTTP2_PLAIN ? { - plain: true -} : { - key: fs.readFileSync(path.join(__dirname, '/localhost.key')), - cert: fs.readFileSync(path.join(__dirname, '/localhost.crt')) -}; - -// Passing bunyan logger (optional) -options.log = require('../test/util').createLogger('server'); - -// We cache one file to be able to do simple performance tests without waiting for the disk -var cachedFile = fs.readFileSync(path.join(__dirname, './server.js')); -var cachedUrl = '/server.js'; - -// Creating the server -var server = http2.createServer(options, function(request, response) { - var filename = path.join(__dirname, request.url); - - // Serving server.js from cache. Useful for microbenchmarks. - if (request.url === cachedUrl) { - response.end(cachedFile); - } - - // Reading file from disk if it exists and is safe. - else if ((filename.indexOf(__dirname) === 0) && fs.existsSync(filename) && fs.statSync(filename).isFile()) { - response.writeHead('200'); - - // If they download the certificate, push the private key too, they might need it. - if (response.push && request.url === '/localhost.crt') { - var push = response.push('/localhost.key'); - push.writeHead(200); - fs.createReadStream(path.join(__dirname, '/localhost.key')).pipe(push); - } - - fs.createReadStream(filename).pipe(response); - } - - // Otherwise responding with 404. - else { - response.writeHead('404'); - response.end(); - } -}); - -server.listen(process.env.HTTP2_PORT || 8080); diff --git a/lib/compressor.js b/lib/compressor.js deleted file mode 100644 index e66fe5ae..00000000 --- a/lib/compressor.js +++ /dev/null @@ -1,944 +0,0 @@ -// The implementation of the [HTTP/2 Header Compression][http2-compression] spec is separated from -// the 'integration' part which handles HEADERS and PUSH_PROMISE frames. The compression itself is -// implemented in the first part of the file, and consists of three classes: `HeaderTable`, -// `HeaderSetDecompressor` and `HeaderSetCompressor`. The two latter classes are -// [Transform Stream][node-transform] subclasses that operate in [object mode][node-objectmode]. -// These transform chunks of binary data into `[name, value]` pairs and vice versa, and store their -// state in `HeaderTable` instances. -// -// The 'integration' part is also implemented by two [Transform Stream][node-transform] subclasses -// that operate in [object mode][node-objectmode]: the `Compressor` and the `Decompressor`. These -// provide a layer between the [framer](framer.html) and the -// [connection handling component](connection.html). -// -// [node-transform]: http://nodejs.org/api/stream.html#stream_class_stream_transform -// [node-objectmode]: http://nodejs.org/api/stream.html#stream_new_stream_readable_options -// [http2-compression]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03 - -exports.HeaderTable = HeaderTable; -exports.HeaderSetCompressor = HeaderSetCompressor; -exports.HeaderSetDecompressor = HeaderSetDecompressor; -exports.Compressor = Compressor; -exports.Decompressor = Decompressor; - -var TransformStream = require('stream').Transform; -var assert = require('assert'); -var util = require('util'); - -// Header compression -// ================== - -// The HeaderTable class -// --------------------- - -// The [Header Table][headertable] is a component used to associate headers to index values. It is -// basically an ordered list of `[name, value]` pairs, so it's implemented as a subclass of `Array`. -// [headertable]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-3.1.2 -function HeaderTable(log, table, limit) { - var self = table.map(entryFromPair); - self._log = log; - self._limit = limit || DEFAULT_HEADER_TABLE_LIMIT; - self._size = tableSize(self); - self.add = HeaderTable.prototype.add; - return self; -} - -// There are few more sets that are needed for the compression/decompression process that are all -// subsets of the Header Table, and are implemented as flags on header table entries: -// -// * [Reference Set][referenceset]: contains a group of headers used as a reference for the -// differential encoding of a new set of headers. (`reference` flag) -// * Emitted headers: the headers that are already emitted as part of the current decompression -// process (not part of the spec, `emitted` flag) -// * Headers to be kept: headers that should not be removed as the last step of the encoding process -// (not part of the spec, `keep` flag) -// -// [referenceset]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-3.1.3 -// -// Relations of the sets: -// -// ,----------------------------------. -// | Header Table | -// | | -// | ,----------------------------. | -// | | Reference Set | | -// | | | | -// | | ,---------. ,---------. | | -// | | | Keep | | Emitted | | | -// | | | | | | | | -// | | `---------' `---------' | | -// | `----------------------------' | -// `----------------------------------' -function entryFromPair(pair) { - var entry = pair.slice(); - entry.reference = false; - entry.emitted = false; - entry.keep = false; - entry._size = size(entry); - return entry; -} - -// The encoder decides how to update the header table and as such can control how much memory is -// used by the header table. To limit the memory requirements on the decoder side, the header table -// size is bounded. -// -// * The default header table size limit is 4096 bytes. -// * The size of an entry is defined as follows: the size of an entry is the sum of its name's -// length in bytes, of its value's length in bytes and of 32 bytes. -// * The size of a header table is the sum of the size of its entries. -var DEFAULT_HEADER_TABLE_LIMIT = 4096; - -function size(entry) { - return (new Buffer(entry[0] + entry[1], 'utf8')).length + 32; -} - -function tableSize(table) { - var size = 0; - for (var i = 0; i < table.length; i++) { - size += table[i]._size; - } - return size; -} - -// The `add(index, entry)` can be used to [manage the header table][tablemgmt]: -// [tablemgmt]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-3.2.4 -// -// * if `index` is `Infinite` it pushes the new `entry` at the end of the table -// * otherwise, it replaces the entry with the given `index` with the new `entry` -// * before doing such a modification, it has to be ensured that the header table size will stay -// lower than or equal to the header table size limit. To achieve this, repeatedly, the first -// entry of the header table is removed, until enough space is available for the modification. -HeaderTable.prototype.add = function(index, entry) { - var limit = this._limit - entry._size; - var droppedEntries = []; - - while ((this._size > limit) && (this.length > 0)) { - var dropped = this.shift(); - this._size -= dropped._size; - droppedEntries.push(dropped); - } - - if (this._size <= limit) { - index -= droppedEntries.length; - if (index < 0) { - this.unshift(entry); - } else { - this.splice(index, 1, entry); // this is like push() if index is Infinity - } - this._size += entry._size; - } - - return droppedEntries; -}; - -// Initial header tables -// --------------------- - -// ### [Initial request table][requesttable] ### -// [requesttable]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#appendix-B.1 -HeaderTable.initialRequestTable = [ - [ ':scheme' , 'http' ], - [ ':scheme' , 'https' ], - [ ':host' , '' ], - [ ':path' , '/' ], - [ ':method' , 'get' ], - [ 'accept' , '' ], - [ 'accept-charset' , '' ], - [ 'accept-encoding' , '' ], - [ 'accept-language' , '' ], - [ 'cookie' , '' ], - [ 'if-modified-since' , '' ], - [ 'user-agent' , '' ], - [ 'referer' , '' ], - [ 'authorization' , '' ], - [ 'allow' , '' ], - [ 'cache-control' , '' ], - [ 'connection' , '' ], - [ 'content-length' , '' ], - [ 'content-type' , '' ], - [ 'date' , '' ], - [ 'expect' , '' ], - [ 'from' , '' ], - [ 'if-match' , '' ], - [ 'if-none-match' , '' ], - [ 'if-range' , '' ], - [ 'if-unmodified-since' , '' ], - [ 'max-forwards' , '' ], - [ 'proxy-authorization' , '' ], - [ 'range' , '' ], - [ 'via' , '' ] -]; - -// ### [Initial response table][responsetable] ### -// [responsetable]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#appendix-B.2 -HeaderTable.initialResponseTable = [ - [ ':status' , '200' ], - [ 'age' , '' ], - [ 'cache-control' , '' ], - [ 'content-length' , '' ], - [ 'content-type' , '' ], - [ 'date' , '' ], - [ 'etag' , '' ], - [ 'expires' , '' ], - [ 'last-modified' , '' ], - [ 'server' , '' ], - [ 'set-cookie' , '' ], - [ 'vary' , '' ], - [ 'via' , '' ], - [ 'access-control-allow-origin' , '' ], - [ 'accept-ranges' , '' ], - [ 'allow' , '' ], - [ 'connection' , '' ], - [ 'content-disposition' , '' ], - [ 'content-encoding' , '' ], - [ 'content-language' , '' ], - [ 'content-location' , '' ], - [ 'content-range' , '' ], - [ 'link' , '' ], - [ 'location' , '' ], - [ 'proxy-authenticate' , '' ], - [ 'refresh' , '' ], - [ 'retry-after' , '' ], - [ 'strict-transport-security' , '' ], - [ 'transfer-encoding' , '' ], - [ 'www-authenticate' , '' ] -]; - -// The HeaderSetDecompressor class -// ------------------------------- - -// A `HeaderSetDecompressor` instance is a transform stream that can be used to *decompress a -// single header set*. Its input is a stream of binary data chunks and its output is a stream of -// `[name, value]` pairs. -// -// Currently, it is not a proper streaming decompressor implementation, since it buffer its input -// until the end os the stream, and then processes the whole header block at once. - -util.inherits(HeaderSetDecompressor, TransformStream); -function HeaderSetDecompressor(log, table) { - TransformStream.call(this, { objectMode: true }); - - this._log = log.child({ component: 'compressor' }); - this._table = table; - this._chunks = []; -} - -// `_transform` is the implementation of the [corresponding virtual function][_transform] of the -// TransformStream class. It collects the data chunks for later processing. -// [_transform]: http://nodejs.org/api/stream.html#stream_transform_transform_chunk_encoding_callback -HeaderSetDecompressor.prototype._transform = function _transform(chunk, encoding, callback) { - this._chunks.push(chunk); - callback(); -}; - -// `execute(rep)` executes the given [header representation][representation]. -// [representation]: http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-3.1.5 - -// The *JavaScript object representation* of a header representation: -// -// { -// name: String || Integer, // string literal or index -// value: String || Integer, // string literal or index -// index: Integer // -1 : no indexing -// // 0 - ... : substitution indexing -// // Infinity : incremental indexing -// } -// -// Examples: -// -// Indexed: -// { name: 2 , value: 2 , index: -1 } -// Literal: -// { name: 2 , value: 'X', index: -1 } // without indexing -// { name: 2 , value: 'Y', index: Infinity } // incremental indexing -// { name: 'A', value: 'Z', index: 123 } // substitution indexing -HeaderSetDecompressor.prototype._execute = function _execute(rep) { - this._log.trace({ key: rep.name, value: rep.value, index: rep.index }, - 'Executing header representation'); - - var index, entry, pair; - - // * An _indexed representation_ corresponding to an entry _present_ in the reference set - // entails the following actions: - // * The entry is removed from the reference set. - // * An _indexed representation_ corresponding to an entry _not present_ in the reference set - // entails the following actions: - // * The header corresponding to the entry is emitted. - // * The entry is added to the reference set. - if (typeof rep.value === 'number') { - index = rep.value; - entry = this._table[index]; - - if (entry.reference) { - entry.reference = false; - } else { - entry.reference = true; - entry.emitted = true; - pair = entry.slice(); - this.push(pair); - } - } - - // * A _literal representation_ that is _not added_ to the header table entails the following - // action: - // * The header is emitted. - // * A _literal representation_ that is _added_ to the header table entails the following further - // actions: - // * The header is added to the header table, at the location defined by the representation. - // * The new entry is added to the reference set. - else { - if (typeof rep.name === 'number') { - pair = [this._table[rep.name][0], rep.value]; - } else { - pair = [rep.name, rep.value]; - } - - index = rep.index; - if (index !== -1) { - entry = entryFromPair(pair); - entry.reference = true; - entry.emitted = true; - this._table.add(index, entry); - } - - this.push(pair); - } -}; - -// `_flush` is the implementation of the [corresponding virtual function][_flush] of the -// TransformStream class. The whole decompressing process is done in `_flush`. It gets called when -// the input stream is over. -// [_flush]: http://nodejs.org/api/stream.html#stream_transform_flush_callback -HeaderSetDecompressor.prototype._flush = function _flush(callback) { - var buffer = concat(this._chunks); - - // * processes the header representations - buffer.cursor = 0; - while (buffer.cursor < buffer.length) { - this._execute(HeaderSetDecompressor.header(buffer)); - } - - // * [emits the reference set](http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-3.2.2) - for (var index = 0; index < this._table.length; index++) { - var entry = this._table[index]; - if (entry.reference && !entry.emitted) { - this.push(entry.slice()); - } - entry.emitted = false; - } - - callback(); -}; - -// The HeaderSetCompressor class -// ----------------------------- - -// A `HeaderSetCompressor` instance is a transform stream that can be used to *compress a single -// header set*. Its input is a stream of `[name, value]` pairs and its output is a stream of -// binary data chunks. -// -// It is a real streaming compressor, since it does not wait until the header set is complete. -// -// The compression algorithm is (intentionally) not specified by the spec. Therefore, the current -// compression algorithm can probably be improved in the future. - -util.inherits(HeaderSetCompressor, TransformStream); -function HeaderSetCompressor(log, table) { - TransformStream.call(this, { objectMode: true }); - - this._log = log.child({ component: 'compressor' }); - this._table = table; - this.push = TransformStream.prototype.push.bind(this); -} - -HeaderSetCompressor.prototype.send = function send(rep) { - this._log.trace({ key: rep.name, value: rep.value, index: rep.index }, - 'Emitting header representation'); - - if (!rep.chunks) { - rep.chunks = HeaderSetCompressor.header(rep); - } - rep.chunks.forEach(this.push); -}; - -// `_transform` is the implementation of the [corresponding virtual function][_transform] of the -// TransformStream class. It processes the input headers one by one: -// [_transform]: http://nodejs.org/api/stream.html#stream_transform_transform_chunk_encoding_callback -HeaderSetCompressor.prototype._transform = function _transform(pair, encoding, callback) { - var name = pair[0].toLowerCase(); - var value = pair[1]; - var entry, rep; - - // * tries to find full (name, value) or name match in the header table - var nameMatch = -1, fullMatch = -1; - for (var index = 0; index < this._table.length; index++) { - entry = this._table[index]; - if (entry[0] === name) { - if (entry[1] === value) { - fullMatch = index; - break; - } else if (nameMatch === -1) { - nameMatch = index; - } - } - } - - // * if there's full match, it will be an indexed representation (or more than one) depending - // on its presence in the reference, the emitted and the keep set: - // - // * If the entry is outside the reference set, then a single indexed representation puts the - // entry into it and emits the header. - // - // * If it's already in the keep set, then 4 indexed representations are needed: - // - // 1. removes it from the reference set - // 2. puts it back in the reference set and emits the header once - // 3. removes it again - // 4. puts it back and emits it again for the second time - // - // It won't be emitted at the end of the decoding process since it's now in the emitted set. - // - // * If it's in the emitted set, then 2 indexed representations are needed: - // - // 1. removes it from the reference set - // 2. puts it back in the reference set and emits the header once - // - // * If it's in the reference set, but outside the keep set and the emitted set, then this - // header is common with the previous header set, and is still untouched. We mark it to keep - // in the reference set (that means don't remove at the end of the encoding process). - if (fullMatch !== -1) { - rep = { name: fullMatch, value: fullMatch, index: -1 }; - - if (!entry.reference) { - this.send(rep); - entry.reference = true; - entry.emitted = true; - } - - else if (entry.keep) { - this.send(rep); - this.send(rep); - this.send(rep); - this.send(rep); - entry.keep = false; - entry.emitted = true; - } - - else if (entry.emitted) { - this.send(rep); - this.send(rep); - } - - else { - entry.keep = true; - } - } - - // * otherwise, it will be a literal representation (with a name index if there's a name match) - else { - entry = entryFromPair(pair); - entry.emitted = true; - - var insertIndex; - if (entry._size > this._table._limit / 2) { - insertIndex = -1; - } else if (nameMatch !== -1) { - insertIndex = nameMatch; - } else { - insertIndex = Infinity; - } - - if (insertIndex !== -1) { - entry.reference = true; - var droppedEntries = this._table.add(insertIndex, entry); - for (index = 0; index < droppedEntries.length; index++) { - var dropped = droppedEntries[index]; - if (dropped.keep) { - rep = { name: index, value: index, index: -1 }; - this.send(rep); - this.send(rep); - } - } - } - - this.send({ name: (nameMatch !== -1) ? nameMatch : name, value: value, index: insertIndex }); - } - - callback(); -}; - -// `_flush` is the implementation of the [corresponding virtual function][_flush] of the -// TransformStream class. It gets called when there's no more header to compress. The final step: -// [_flush]: http://nodejs.org/api/stream.html#stream_transform_flush_callback -HeaderSetCompressor.prototype._flush = function _flush(callback) { - // * removing entries from the header set that are not marked to be kept or emitted - for (var index = 0; index < this._table.length; index++) { - var entry = this._table[index]; - if (entry.reference && !entry.keep && !entry.emitted) { - this.send({ name: index, value: index, index: -1 }); - entry.reference = false; - } - entry.keep = false; - entry.emitted = false; - } - - callback(); -}; - -// [Detailed Format](http://tools.ietf.org/html/draft-ietf-httpbis-header-compression-03#section-4) -// ----------------- - -// ### Integer representation ### -// -// The algorithm to represent an integer I is as follows: -// -// 1. If I < 2^N - 1, encode I on N bits -// 2. Else, encode 2^N - 1 on N bits and do the following steps: -// 1. Set I to (I - (2^N - 1)) and Q to 1 -// 2. While Q > 0 -// 1. Compute Q and R, quotient and remainder of I divided by 2^7 -// 2. If Q is strictly greater than 0, write one 1 bit; otherwise, write one 0 bit -// 3. Encode R on the next 7 bits -// 4. I = Q - -HeaderSetCompressor.integer = function writeInteger(I, N) { - var limit = Math.pow(2,N) - 1; - if (I < limit) { - return [new Buffer([I])]; - } - - var bytes = []; - if (N !== 0) { - bytes.push(limit); - } - I -= limit; - - var Q = 1, R; - while (Q > 0) { - Q = Math.floor(I / 128); - R = I % 128; - - if (Q > 0) { - R += 128; - } - bytes.push(R); - - I = Q; - } - - return [new Buffer(bytes)]; -}; - -// The inverse algorithm: -// -// 1. Set I to the number coded on the lower N bits of the first byte -// 2. If I is smaller than 2^N - 1 then return I -// 2. Else the number is encoded on more than one byte, so do the following steps: -// 1. Set M to 0 -// 2. While returning with I -// 1. Let B be the next byte (the first byte if N is 0) -// 2. Read out the lower 7 bits of B and multiply it with 2^M -// 3. Increase I with this number -// 4. Increase M by 7 -// 5. Return I if the most significant bit of B is 0 - -HeaderSetDecompressor.integer = function readInteger(buffer, N) { - var limit = Math.pow(2,N) - 1; - - var I = buffer[buffer.cursor] & limit; - if (N !== 0) { - buffer.cursor += 1; - } - - if (I === limit) { - var M = 0; - do { - I += (buffer[buffer.cursor] & 127) << M; - M += 7; - buffer.cursor += 1; - } while (buffer[buffer.cursor - 1] & 128); - } - - return I; -}; - -// ### String literal representation ### -// -// Literal **strings** can represent header names or header values. They are encoded in two parts: -// -// 1. The string length, defined as the number of bytes needed to store its UTF-8 representation, -// is represented as an integer with a zero bits prefix. If the string length is strictly less -// than 128, it is represented as one byte. -// 2. The string value represented as a list of UTF-8 characters. - -HeaderSetCompressor.string = function writeString(str) { - var encodedString = new Buffer(str, 'utf8'); - var encodedLength = HeaderSetCompressor.integer(encodedString.length, 0); - return encodedLength.concat(encodedString); -}; - -HeaderSetDecompressor.string = function readString(buffer) { - var length = HeaderSetDecompressor.integer(buffer, 0); - var str = buffer.toString('utf8', buffer.cursor, buffer.cursor + length); - buffer.cursor += length; - return str; -}; - -// ### Header represenations ### - -// The JavaScript object representation is described near the -// `HeaderTable.prototype.execute()` method definition. -// -// **All binary header representations** start with a prefix signaling the representation type and -// an index represented using prefix coded integers: -// -// 0 1 2 3 4 5 6 7 -// +---+---+---+---+---+---+---+---+ -// | 1 | Index (7+) | Indexed Representation -// +---+---------------------------+ -// -// +---+---+---+---+---+---+---+---+ -// | 0 | 1 | 1 | Index (5+) | Literal w/o Indexing -// +---+---+---+-------------------+ -// -// +---+---+---+---+---+---+---+---+ -// | 0 | 1 | 0 | Index (5+) | Literal w/ Incremental Indexing -// +---+---+---+-------------------+ -// -// +---+---+---+---+---+---+---+---+ -// | 0 | 0 | Index (6+) | Literal w/ Substitution Indexing -// +---+---+-----------------------+ -// -// The **Indexed Representation** consists of the 1-bit prefix and the Index that is represented as -// a 7-bit prefix coded integer and nothing else. -// -// After the first bits, **all literal representations** specify the header name, either as a -// pointer to the Header Table (Index) or a string literal. When the string literal representation -// is used, the Index is set to 0 and the string literal starts at the second byte. -// -// When using **Substitution Indexing**, a new index comes next represented as a 0-bit prefix -// integer, specifying the record in the Header Table that needs to be replaced. -// -// For **all literal representations**, the specification of the header value comes next. It is -// always represented as a string. - -var representations = { - indexed : { prefix: 7, pattern: 0x80 }, - literal : { prefix: 5, pattern: 0x60 }, - literalIncremental : { prefix: 5, pattern: 0x40 }, - literalSubstitution : { prefix: 6, pattern: 0x00 } -}; - -HeaderSetCompressor.header = function writeHeader(header) { - var representation, buffers = []; - - if (typeof header.value === 'number') { - representation = representations.indexed; - } else if (header.index === -1) { - representation = representations.literal; - } else if (header.index === Infinity) { - representation = representations.literalIncremental; - } else { - representation = representations.literalSubstitution; - } - - if (representation === representations.indexed) { - buffers.push(HeaderSetCompressor.integer(header.value, representation.prefix)); - - } else { - if (typeof header.name === 'number') { - buffers.push(HeaderSetCompressor.integer(header.name + 1, representation.prefix)); - } else { - buffers.push(HeaderSetCompressor.integer(0, representation.prefix)); - buffers.push(HeaderSetCompressor.string(header.name)); - } - - if (representation === representations.literalSubstitution) { - buffers.push(HeaderSetCompressor.integer(header.index, 0)); - } - - buffers.push(HeaderSetCompressor.string(header.value)); - } - - buffers[0][0][0] |= representation.pattern; - - return Array.prototype.concat.apply([], buffers); // array of arrays of buffers -> array of buffers -}; - -HeaderSetDecompressor.header = function readHeader(buffer) { - var representation, header = {}; - - var firstByte = buffer[buffer.cursor]; - if (firstByte & 0x80) { - representation = representations.indexed; - } else if (firstByte & 0x40) { - if (firstByte & 0x20) { - representation = representations.literal; - } else { - representation = representations.literalIncremental; - } - } else { - representation = representations.literalSubstitution; - } - - if (representation === representations.indexed) { - header.value = header.name = HeaderSetDecompressor.integer(buffer, representation.prefix); - header.index = -1; - - } else { - header.name = HeaderSetDecompressor.integer(buffer, representation.prefix) - 1; - if (header.name === -1) { - header.name = HeaderSetDecompressor.string(buffer); - } - - if (representation === representations.literalSubstitution) { - header.index = HeaderSetDecompressor.integer(buffer, 0); - } else if (representation === representations.literalIncremental) { - header.index = Infinity; - } else { - header.index = -1; - } - - header.value = HeaderSetDecompressor.string(buffer); - } - - return header; -}; - -// Integration with HTTP/2 -// ======================= - -// This section describes the interaction between the compressor/decompressor and the rest of the -// HTTP/2 implementation. The `Compressor` and the `Decompressor` makes up a layer between the -// [framer](framer.html) and the [connection handling component](connection.html). They let most -// frames pass through, except HEADERS and PUSH_PROMISE frames. They convert the frames between -// these two representations: -// -// { { -// type: 'HEADERS', type: 'HEADERS', -// flags: {}, flags: {}, -// stream: 1, <===> stream: 1, -// headers: { data: Buffer -// N1: 'V1', } -// N2: ['V1', 'V2', ...], -// // ... -// } -// } -// -// There are possibly several binary frame that belong to a single non-binary frame. - -var MAX_HTTP_PAYLOAD_SIZE = 16383; - -// The Compressor class -// -------------------- - -// The Compressor transform stream is basically stateless. -util.inherits(Compressor, TransformStream); -function Compressor(log, type) { - TransformStream.call(this, { objectMode: true }); - - this._log = log.child({ component: 'compressor' }); - - assert((type === 'REQUEST') || (type === 'RESPONSE')); - var initialTable = (type === 'REQUEST') ? HeaderTable.initialRequestTable - : HeaderTable.initialResponseTable; - this._table = new HeaderTable(this._log, initialTable); -} - -// `compress` takes a header set, and compresses it using a new `HeaderSetCompressor` stream -// instance. This means that from now on, the advantages of streaming header encoding are lost, -// but the API becomes simpler. -Compressor.prototype.compress = function compress(headers) { - var compressor = new HeaderSetCompressor(this._log, this._table); - for (var name in headers) { - var value = headers[name]; - if (value instanceof Array) { - for (var i = 0; i< value.length; i++) { - compressor.write([String(name), String(value[i])]); - } - } else { - compressor.write([String(name), String(value)]); - } - } - compressor.end(); - - var chunk, chunks = []; - while (chunk = compressor.read()) { - chunks.push(chunk); - } - return concat(chunks); -}; - -// When a `frame` arrives -Compressor.prototype._transform = function _transform(frame, encoding, done) { - // * and it is a HEADERS or PUSH_PROMISE frame - // * it generates a header block using the compress method - // * cuts the header block into `chunks` that are not larger than `MAX_HTTP_PAYLOAD_SIZE` - // * for each chunk, it pushes out a chunk frame that is identical to the original, except - // the `data` property which holds the given chunk, the type of the frame which is always - // CONTINUATION except for the first frame, and the END_HEADERS/END_PUSH_STREAM flag that - // marks the last frame and the END_STREAM flag which is always false before the end - if (frame.type === 'HEADERS' || frame.type === 'PUSH_PROMISE') { - var buffer = this.compress(frame.headers); - - var chunks = cut(buffer, MAX_HTTP_PAYLOAD_SIZE); - - for (var i = 0; i < chunks.length; i++) { - var chunkFrame; - var first = (i === 0); - var last = (i === chunks.length - 1); - - if (first) { - chunkFrame = util._extend({}, frame); - chunkFrame.flags = util._extend({}, frame.flags); - chunkFrame.flags['END_' + frame.type] = last; - } else { - chunkFrame = { - type: 'CONTINUATION', - flags: { END_HEADERS: last }, - stream: frame.stream - }; - } - if (chunkFrame.type !== 'PUSH_PROMISE') { - chunkFrame.flags.END_STREAM = last && frame.flags.END_STREAM; - } - chunkFrame.data = chunks[i]; - - this.push(chunkFrame); - } - } - - // * otherwise, the frame is forwarded without taking any action - else { - this.push(frame); - } - - done(); -}; - -// The Decompressor class -// ---------------------- - -// The Decompressor is a stateful transform stream, since it has to collect multiple frames first, -// and the decoding comes after unifying the payload of those frames. -// -// If there's a frame in progress, `this._inProgress` is `true`. The frames are collected in -// `this._frames`, and the type of the frame and the stream identifier is stored in `this._type` -// and `this._stream` respectively. -util.inherits(Decompressor, TransformStream); -function Decompressor(log, type) { - TransformStream.call(this, { objectMode: true }); - - this._log = log.child({ component: 'compressor' }); - - assert((type === 'REQUEST') || (type === 'RESPONSE')); - var initialTable = (type === 'REQUEST') ? HeaderTable.initialRequestTable - : HeaderTable.initialResponseTable; - this._table = new HeaderTable(this._log, initialTable); - - this._inProgress = false; - this._base = undefined; -} - -// `decompress` takes a full header block, and decompresses it using a new `HeaderSetDecompressor` -// stream instance. This means that from now on, the advantages of streaming header decoding are -// lost, but the API becomes simpler. -Decompressor.prototype.decompress = function decompress(block) { - var decompressor = new HeaderSetDecompressor(this._log, this._table); - decompressor.end(block); - - var headers = {}; - var pair; - while (pair = decompressor.read()) { - var name = pair[0]; - var value = pair[1]; - if (name in headers) { - if (headers[name] instanceof Array) { - headers[name].push(value); - } else { - headers[name] = [headers[name], value]; - } - } else { - headers[name] = value; - } - } - - return headers; -}; - -// When a `frame` arrives -Decompressor.prototype._transform = function _transform(frame, encoding, done) { - // * and the collection process is already `_inProgress`, the frame is simply stored, except if - // it's an illegal frame - if (this._inProgress) { - if ((frame.type !== 'CONTINUATION') || (frame.stream !== this._base.stream)) { - this._log.error('A series of HEADER frames were not continuous'); - this.emit('error', 'PROTOCOL_ERROR'); - return; - } - this._frames.push(frame); - } - - // * and the collection process is not `_inProgress`, but the new frame's type is HEADERS or - // PUSH_PROMISE, a new collection process begins - else if ((frame.type === 'HEADERS') || (frame.type === 'PUSH_PROMISE')) { - this._inProgress = true; - this._base = frame; - this._frames = [frame]; - } - - // * otherwise, the frame is forwarded without taking any action - else { - this.push(frame); - } - - // * When the frame signals that it's the last in the series, the header block chunks are - // concatenated, the headers are decompressed, and a new frame gets pushed out with the - // decompressed headers. - if (this._inProgress && (frame.flags.END_HEADERS || frame.flags.END_PUSH_PROMISE)) { - var buffer = concat(this._frames.map(function(frame) { - return frame.data; - })); - try { - var headers = this.decompress(buffer); - } catch(error) { - this._log.error({ err: error }, 'Header decompression error'); - this.emit('error', 'COMPRESSION_ERROR'); - return; - } - this.push(util._extend({ headers: headers }, this._base)); - this._inProgress = false; - } - - done(); -}; - -// Helper functions -// ================ - -// Concatenate an array of buffers into a new buffer -function concat(buffers) { - var size = 0; - for (var i = 0; i < buffers.length; i++) { - size += buffers[i].length; - } - - var concatenated = new Buffer(size); - for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) { - buffers[j].copy(concatenated, cursor); - } - - return concatenated; -} - -// Cut `buffer` into chunks not larger than `size` -function cut(buffer, size) { - var chunks = []; - var cursor = 0; - do { - var chunkSize = Math.min(size, buffer.length - cursor); - chunks.push(buffer.slice(cursor, cursor + chunkSize)); - cursor += chunkSize; - } while(cursor < buffer.length); - return chunks; -} diff --git a/lib/connection.js b/lib/connection.js deleted file mode 100644 index 024c3678..00000000 --- a/lib/connection.js +++ /dev/null @@ -1,585 +0,0 @@ -var assert = require('assert'); - -// The Connection class -// ==================== - -// The Connection class manages HTTP/2 connections. Each instance corresponds to one transport -// stream (TCP stream). It operates by sending and receiving frames and is implemented as a -// [Flow](flow.html) subclass. - -var Flow = require('./flow').Flow; - -exports.Connection = Connection; - -// Public API -// ---------- - -// * **new Connection(log, firstStreamId, settings)**: create a new Connection -// -// * **Event: 'error' (type)**: signals a connection level error made by the other end -// -// * **Event: 'peerError' (type)**: signals the receipt of a GOAWAY frame that contains an error -// code other than NO_ERROR -// -// * **Event: 'stream' (stream)**: signals that there's an incoming stream -// -// * **createStream(): stream**: initiate a new stream -// -// * **set(settings)**: change the value of one or more settings according to the key-value pairs -// of `settings` -// -// * **ping([callback])**: send a ping and call callback when the answer arrives -// -// * **close([error])**: close the stream with an error code - -// Constructor -// ----------- - -// The main aspects of managing the connection are: -function Connection(log, firstStreamId, settings) { - // * initializing the base class - Flow.call(this, 0); - - // * logging: every method uses the common logger object - this._log = log.child({ component: 'connection' }); - - // * stream management - this._initializeStreamManagement(firstStreamId); - - // * lifecycle management - this._initializeLifecycleManagement(); - - // * flow control - this._initializeFlowControl(); - - // * settings management - this._initializeSettingsManagement(settings); - - // * multiplexing - this._initializeMultiplexing(); -} -Connection.prototype = Object.create(Flow.prototype, { constructor: { value: Connection } }); - -// Overview -// -------- - -// | ^ | ^ -// v | v | -// +--------------+ +--------------+ -// +---| stream1 |---| stream2 |---- .... ---+ -// | | +----------+ | | +----------+ | | -// | | | stream1. | | | | stream2. | | | -// | +-| upstream |-+ +-| upstream |-+ | -// | +----------+ +----------+ | -// | | ^ | ^ | -// | v | v | | -// | +-----+-------------+-----+-------- .... | -// | ^ | | | | -// | | v | | | -// | +--------------+ | | | -// | | stream0 | | | | -// | | connection | | | | -// | | management | multiplexing | -// | +--------------+ flow control | -// | | ^ | -// | _read() | | _write() | -// | v | | -// | +------------+ +-----------+ | -// | |output queue| |input queue| | -// +----------------+------------+-+-----------+-----------------+ -// | ^ -// read() | | write() -// v | - -// Stream management -// ----------------- - -var Stream = require('./stream').Stream; - -// Initialization: -Connection.prototype._initializeStreamManagement = function _initializeStreamManagement(firstStreamId) { - // * streams are stored in two data structures: - // * `_streamIds` is an id -> stream map of the streams that are allowed to receive frames. - // * `_streamPriorities` is a priority -> [stream] map of stream that allowed to send frames. - this._streamIds = []; - this._streamPriorities = []; - - // * The next outbound stream ID and the last inbound stream id - this._nextStreamId = firstStreamId; - this._lastIncomingStream = 0; - - // * Calling `_writeControlFrame` when there's an incoming stream with 0 as stream ID - this._streamIds[0] = { upstream: { write: this._writeControlFrame.bind(this) } }; - - // * By default, the number of concurrent outbound streams is not limited. The `_streamLimit` can - // be set by the SETTINGS_MAX_CONCURRENT_STREAMS setting. - this._streamSlotsFree = Infinity; - this._streamLimit = Infinity; - this.on('RECEIVING_SETTINGS_MAX_CONCURRENT_STREAMS', this._updateStreamLimit); -}; - -// `_writeControlFrame` is called when there's an incoming frame in the `_control` stream. It -// broadcasts the message by creating an event on it. -Connection.prototype._writeControlFrame = function _writeControlFrame(frame) { - if ((frame.type === 'SETTINGS') || (frame.type === 'PING') || - (frame.type === 'GOAWAY') || (frame.type === 'WINDOW_UPDATE')) { - this._log.debug({ frame: frame }, 'Receiving connection level frame'); - this.emit(frame.type, frame); - } else { - this._log.error({ frame: frame }, 'Invalid connection level frame'); - this.emit('error', 'PROTOCOL_ERROR'); - } -}; - -// Methods to manage the stream slot pool: -Connection.prototype._updateStreamLimit = function _updateStreamLimit(newStreamLimit) { - var wakeup = (this._streamSlotsFree === 0) && (newStreamLimit > this._streamLimit); - this._streamSlotsFree += newStreamLimit - this._streamLimit; - this._streamLimit = newStreamLimit; - if (wakeup) { - this.emit('wakeup'); - } -}; - -Connection.prototype._changeStreamCount = function _changeStreamCount(change) { - if (change) { - this._log.trace({ free: this._streamSlotsFree, change: change }, - 'Changing active stream count.'); - var wakeup = (this._streamSlotsFree === 0) && (change < 0); - this._streamSlotsFree -= change; - if (wakeup) { - this.emit('wakeup'); - } - } -}; - -// Creating a new *inbound or outbound* stream with the given `id` (which is undefined in case of -// an outbound stream) consists of three steps: -// -// 1. var stream = new Stream(this._log); -// 2. this._allocateId(stream, id); -// 2. this._allocatePriority(stream); - -// Allocating an ID to a stream -Connection.prototype._allocateId = function _allocateId(stream, id) { - // * initiated stream without definite ID - if (id === undefined) { - id = this._nextStreamId; - this._nextStreamId += 2; - } - - // * incoming stream with a legitim ID (larger than any previous and different parity than ours) - else if ((id > this._lastIncomingStream) && ((id - this._nextStreamId) % 2 !== 0)) { - this._lastIncomingStream = id; - } - - // * incoming stream with invalid ID - else { - this._log.error({ stream_id: id, lastIncomingStream: this._lastIncomingStream }, - 'Invalid incoming stream ID.'); - this.emit('error', 'PROTOCOL_ERROR'); - return undefined; - } - - assert(!(id in this._streamIds)); - - // * adding to `this._streamIds` - this._log.trace({ s: stream, stream_id: id }, 'Allocating ID for stream.'); - this._streamIds[id] = stream; - stream.id = id; - this.emit('new_stream', stream, id); - - // * handling stream errors as connection errors - stream.on('error', this.emit.bind(this, 'error')); - - return id; -}; - -// Allocating a priority to a stream, and managing priority changes -Connection.prototype._allocatePriority = function _allocatePriority(stream) { - this._log.trace({ s: stream }, 'Allocating priority for stream.'); - this._insert(stream, stream._priority); - stream.on('priority', this._reprioritize.bind(this, stream)); - stream.upstream.on('readable', this.emit.bind(this, 'wakeup')); - this.emit('wakeup'); -}; - -Connection.prototype._insert = function _insert(stream, priority) { - if (priority in this._streamPriorities) { - this._streamPriorities[priority].push(stream); - } else { - this._streamPriorities[priority] = [stream]; - } -}; - -Connection.prototype._reprioritize = function _reprioritize(stream, priority) { - var bucket = this._streamPriorities[stream._priority]; - var index = bucket.indexOf(stream); - assert(index !== -1); - bucket.splice(index, 1); - if (bucket.length === 0) { - delete this._streamPriorities[stream._priority]; - } - - this._insert(stream, priority); -}; - -// Creating an *inbound* stream with the given ID. It is called when there's an incoming frame to -// a previously nonexistent stream. -Connection.prototype._createIncomingStream = function _createIncomingStream(id) { - this._log.debug({ stream_id: id }, 'New incoming stream.'); - - var stream = new Stream(this._log); - this._allocateId(stream, id); - this._allocatePriority(stream); - this.emit('stream', stream, id); - - return stream; -}; - -// Creating an *outbound* stream -Connection.prototype.createStream = function createStream() { - this._log.trace('Creating new outbound stream.'); - - // * Receiving is enabled immediately, and an ID gets assigned to the stream - var stream = new Stream(this._log); - this._allocatePriority(stream); - - return stream; -}; - -// Multiplexing -// ------------ - -Connection.prototype._initializeMultiplexing = function _initializeMultiplexing() { - this.on('window_update', this.emit.bind(this, 'wakeup')); - this._sendScheduled = false; - this._firstFrameReceived = false; -}; - -// The `_send` method is a virtual method of the [Flow class](flow.html) that has to be implemented -// by child classes. It reads frames from streams and pushes them to the output buffer. -Connection.prototype._send = function _send(immediate) { - // * Do not do anything if the connectionis already closed - if (this._closed) { - return; - } - - // * Collapsing multiple calls in a turn into a single deferred call - if (immediate) { - this._sendScheduled = false; - } else { - if (!this._sendScheduled) { - this._sendScheduled = true; - setImmediate(this._send.bind(this, true)); - } - return; - } - - this._log.trace('Starting forwarding frames from streams.'); - - // * Looping through priority `bucket`s in priority order. -priority_loop: - for (var priority in this._streamPriorities) { - var bucket = this._streamPriorities[priority]; - var nextBucket = []; - - // * Forwarding frames from buckets with round-robin scheduling. - // 1. pulling out frame - // 2. if there's no frame, skip this stream - // 3. if forwarding this frame would make `streamCount` greater than `streamLimit`, skip - // this stream - // 4. adding stream to the bucket of the next round - // 5. assigning an ID to the frame (allocating an ID to the stream if there isn't already) - // 6. if forwarding a PUSH_PROMISE, allocate ID to the promised stream - // 7. forwarding the frame, changing `streamCount` as appropriate - // 8. stepping to the next stream if there's still more frame needed in the output buffer - // 9. switching to the bucket of the next round - while (bucket.length > 0) { - for (var index = 0; index < bucket.length; index++) { - var stream = bucket[index]; - var frame = stream.upstream.read((this._window > 0) ? this._window : -1); - - if (!frame) { - continue; - } else if (frame.count_change > this._streamSlotsFree) { - stream.upstream.unshift(frame); - continue; - } - - nextBucket.push(stream); - - if (frame.stream === undefined) { - frame.stream = stream.id || this._allocateId(stream); - } - - if (frame.type === 'PUSH_PROMISE') { - this._allocatePriority(frame.promised_stream); - frame.promised_stream = this._allocateId(frame.promised_stream); - } - - this._log.trace({ s: stream, frame: frame }, 'Forwarding outgoing frame'); - var moreNeeded = this.push(frame); - this._changeStreamCount(frame.count_change); - - assert(moreNeeded !== null); // The frame shouldn't be unforwarded - if (moreNeeded === false) { - break priority_loop; - } - } - - bucket = nextBucket; - nextBucket = []; - } - } - - // * if we couldn't forward any frame, then sleep until window update, or some other wakeup event - if (moreNeeded === undefined) { - this.once('wakeup', this._send.bind(this)); - } - - this._log.trace({ moreNeeded: moreNeeded }, 'Stopping forwarding frames from streams.'); -}; - -// The `_receive` method is another virtual method of the [Flow class](flow.html) that has to be -// implemented by child classes. It forwards the given frame to the appropriate stream: -Connection.prototype._receive = function _receive(frame, done) { - this._log.trace({ frame: frame }, 'Forwarding incoming frame'); - - // * first frame needs to be checked by the `_onFirstFrameReceived` method - if (!this._firstFrameReceived) { - this._firstFrameReceived = true; - this._onFirstFrameReceived(frame); - } - - // * gets the appropriate stream from the stream registry - var stream = this._streamIds[frame.stream]; - - // * or creates one if it's not in `this.streams` - if (!stream) { - stream = this._createIncomingStream(frame.stream); - } - - // * in case of PUSH_PROMISE, replaces the promised stream id with a new incoming stream - if (frame.type === 'PUSH_PROMISE') { - frame.promised_stream = this._createIncomingStream(frame.promised_stream); - } - - frame.count_change = this._changeStreamCount.bind(this); - - // * and writes it to the `stream`'s `upstream` - stream.upstream.write(frame); - - done(); -}; - -// Settings management -// ------------------- - -var defaultSettings = { - SETTINGS_FLOW_CONTROL_OPTIONS: true -}; - -// Settings management initialization: -Connection.prototype._initializeSettingsManagement = function _initializeSettingsManagement(settings) { - // * Sending the initial settings. - this._log.debug('Sending the first SETTINGS frame as part of the connection header.'); - this.set(settings || defaultSettings); - - // * Forwarding SETTINGS frames to the `_receiveSettings` method - this.on('SETTINGS', this._receiveSettings); -}; - -// * Checking that the first frame the other endpoint sends is SETTINGS -Connection.prototype._onFirstFrameReceived = function _onFirstFrameReceived(frame) { - if ((frame.stream === 0) && (frame.type === 'SETTINGS')) { - this._log.debug('Receiving the first SETTINGS frame as part of the connection header.'); - } else { - this._log.fatal({ frame: frame }, 'Invalid connection header: first frame is not SETTINGS.'); - this.emit('error'); - } -}; - -// Handling of incoming SETTINGS frames. -Connection.prototype._receiveSettings = function _receiveSettings(frame) { - for (var name in frame.settings) { - this.emit('RECEIVING_' + name, frame.settings[name]); - } -}; - -// Changing one or more settings value and sending out a SETTINGS frame -Connection.prototype.set = function set(settings) { - this.push({ - type: 'SETTINGS', - flags: {}, - stream: 0, - settings: settings - }); - for (var name in settings) { - this.emit('SENDING_' + name, settings[name]); - } -}; - -// Lifecycle management -// -------------------- - -// The main responsibilities of lifecycle management code: -// -// * keeping the connection alive by -// * sending PINGs when the connection is idle -// * answering PINGs -// * ending the connection - -Connection.prototype._initializeLifecycleManagement = function _initializeLifecycleManagement() { - this._pings = {}; - this.on('PING', this._receivePing); - this.on('GOAWAY', this._receiveGoaway); - this._closed = false; -}; - -// Generating a string of length 16 with random hexadecimal digits -Connection.prototype._generatePingId = function _generatePingId() { - do { - var id = ''; - for (var i = 0; i < 16; i++) { - id += Math.floor(Math.random()*16).toString(16); - } - } while(id in this._pings); - return id; -}; - -// Sending a ping and calling `callback` when the answer arrives -Connection.prototype.ping = function ping(callback) { - var id = this._generatePingId(); - var data = new Buffer(id, 'hex'); - this._pings[id] = callback; - - this._log.debug({ data: data }, 'Sending PING.'); - this.push({ - type: 'PING', - flags: { - PONG: false - }, - stream: 0, - data: data - }); -}; - -// Answering pings -Connection.prototype._receivePing = function _receivePing(frame) { - if (frame.flags.PONG) { - var id = frame.data.toString('hex'); - if (id in this._pings) { - this._log.debug({ data: frame.data }, 'Receiving answer for a PING.'); - var callback = this._pings[id]; - if (callback) { - callback(); - } - delete this._pings[id]; - } else { - this._log.warn({ data: frame.data }, 'Unsolicited PING answer.'); - } - - } else { - this._log.debug({ data: frame.data }, 'Answering PING.'); - this.push({ - type: 'PING', - flags: { - PONG: true - }, - stream: 0, - data: frame.data - }); - } -}; - -// Terminating the connection -Connection.prototype.close = function close(error) { - if (this._closed) { - this._log.warn('Trying to close an already closed connection'); - return; - } - - this._log.debug({ error: error }, 'Closing the connection'); - this.push({ - type: 'GOAWAY', - flags: {}, - stream: 0, - last_stream: this._lastIncomingStream, - error: error || 'NO_ERROR' - }); - this.push(null); - this._closed = true; -}; - -Connection.prototype._receiveGoaway = function _receiveGoaway(frame) { - this._log.debug({ error: frame.error }, 'Other end closed the connection'); - this.push(null); - this._closed = true; - if (frame.error !== 'NO_ERROR') { - this.emit('peerError', frame.error); - } -}; - -// Flow control -// ------------ - -Connection.prototype._initializeFlowControl = function _initializeFlowControl() { - // Handling of initial window size of individual streams. - this._initialStreamWindowSize = INITIAL_STREAM_WINDOW_SIZE; - this.on('new_stream', function(stream) { - stream.upstream.setInitialWindow(this._initialStreamWindowSize); - if (this._remoteFlowControlDisabled) { - stream.upstream.disableRemoteFlowControl(); - } - }); - this.on('RECEIVING_SETTINGS_INITIAL_WINDOW_SIZE', this._setInitialStreamWindowSize); - this.on('RECEIVING_SETTINGS_FLOW_CONTROL_OPTIONS', this._setLocalFlowControl); - this.on('SENDING_SETTINGS_FLOW_CONTROL_OPTIONS', this._setRemoteFlowControl); - this._streamIds[0].upstream.setInitialWindow = function noop() {}; - this._streamIds[0].upstream.disableRemoteFlowControl = function noop() {}; -}; - -// The initial connection flow control window is 65535 bytes. -var INITIAL_STREAM_WINDOW_SIZE = 65535; - -// A SETTINGS frame can alter the initial flow control window size for all current streams. When the -// value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the window size of all -// stream by calling the `setInitialStreamWindowSize` method. The window size has to be modified by -// the difference between the new value and the old value. -Connection.prototype._setInitialStreamWindowSize = function _setInitialStreamWindowSize(size) { - if ((this._initialStreamWindowSize === Infinity) && (size !== Infinity)) { - this._log.error('Trying to manipulate initial flow control window size after flow control was turned off.'); - this.emit('error', 'FLOW_CONTROL_ERROR'); - } else { - this._log.debug({ size: size }, 'Changing stream initial window size.'); - this._initialStreamWindowSize = size; - this._streamIds.forEach(function(stream) { - stream.upstream.setInitialWindow(size); - }); - } -}; - -// `_setStreamFlowControl()` may be used to disable/enable flow control. In practice, it is just -// for turning off flow control since it can not be turned on. -Connection.prototype._setLocalFlowControl = function _setLocalFlowControl(disable) { - if (disable) { - this._increaseWindow(Infinity); - this._setInitialStreamWindowSize(Infinity); - } else if (this._initialStreamWindowSize === Infinity) { - this._log.error('Trying to re-enable flow control after it was turned off.'); - this.emit('error', 'FLOW_CONTROL_ERROR'); - } -}; - -Connection.prototype._setRemoteFlowControl = function _setRemoteFlowControl(disable) { - if (disable) { - this.disableRemoteFlowControl(); - this._streamIds.forEach(function(stream) { - stream.upstream.disableRemoteFlowControl(); - }); - } else if (this._remoteFlowControlDisabled) { - this._log.error('Trying to re-enable flow control after it was turned off.'); - throw new Error('Trying to re-enable flow control after it was turned off.'); - } -}; diff --git a/lib/endpoint.js b/lib/endpoint.js deleted file mode 100644 index 512be36e..00000000 --- a/lib/endpoint.js +++ /dev/null @@ -1,259 +0,0 @@ -var assert = require('assert'); - -var Serializer = require('./framer').Serializer; -var Deserializer = require('./framer').Deserializer; -var Compressor = require('./compressor').Compressor; -var Decompressor = require('./compressor').Decompressor; -var Connection = require('./connection').Connection; -var Duplex = require('stream').Duplex; -var Transform = require('stream').Transform; - -exports.Endpoint = Endpoint; - -// The Endpoint class -// ================== - -// Public API -// ---------- - -// - **new Endpoint(log, role, settings, filters)**: create a new Endpoint. -// -// - `log`: bunyan logger of the parent -// - `role`: 'CLIENT' or 'SERVER' -// - `settings`: initial HTTP/2 settings -// - `filters`: a map of functions that filter the traffic between components (for debugging or -// intentional failure injection). -// -// Filter functions get three arguments: -// 1. `frame`: the current frame -// 2. `forward(frame)`: function that can be used to forward a frame to the next component -// 3. `done()`: callback to signal the end of the filter process -// -// Valid filter names and their position in the stack: -// - `beforeSerialization`: after compression, before serialization -// - `beforeCompression`: after multiplexing, before compression -// - `afterDeserialization`: after deserialization, before decompression -// - `afterDecompression`: after decompression, before multiplexing -// -// * **Event: 'stream' (Stream)**: 'stream' event forwarded from the underlying Connection -// -// * **Event: 'error' (type)**: signals an error -// -// * **createStream(): Stream**: initiate a new stream (forwarded to the underlying Connection) -// -// * **close([error])**: close the connection with an error code - -// Constructor -// ----------- - -// The process of initialization: -function Endpoint(log, role, settings, filters) { - Duplex.call(this); - - // * Initializing logging infrastructure - this._log = log.child({ component: 'endpoint', e: this }); - - // * First part of the handshake process: sending and receiving the client connection header - // prelude. - assert((role === 'CLIENT') || role === 'SERVER'); - if (role === 'CLIENT') { - this._writePrelude(); - } else { - this._readPrelude(); - } - - // * Initialization of componenet. This includes the second part of the handshake process: - // sending the first SETTINGS frame. This is done by the connection class right after - // initialization. - this._initializeDataFlow(role, settings, filters || {}); - - // * Initialization of management code. - this._initializeManagement(); - - // * Initializing error handling. - this._initializeErrorHandling(); -} -Endpoint.prototype = Object.create(Duplex.prototype, { constructor: { value: Endpoint } }); - -// Handshake -// --------- - -var CLIENT_PRELUDE = new Buffer('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'); - -// Writing the client header is simple and synchronous. -Endpoint.prototype._writePrelude = function _writePrelude() { - this._log.debug('Sending the client connection header prelude.'); - this.push(CLIENT_PRELUDE); -}; - -// The asynchronous process of reading the client header: -Endpoint.prototype._readPrelude = function _readPrelude() { - // * progress in the header is tracker using a `cursor` - var cursor = 0; - - // * `_write` is temporarily replaced by the comparator function - this._write = function _temporalWrite(chunk, encoding, done) { - // * which compares the stored header with the current `chunk` byte by byte and emits the - // 'error' event if there's a byte that doesn't match - var offset = cursor; - while(cursor < CLIENT_PRELUDE.length && (cursor - offset) < chunk.length) { - if (CLIENT_PRELUDE[cursor] !== chunk[cursor - offset]) { - this._log.fatal({ cursor: cursor, offset: offset, chunk: chunk }, - 'Client connection header prelude does not match.'); - this._error('handshake', 'PROTOCOL_ERROR'); - return; - } - cursor += 1; - } - - // * if the whole header is over, and there were no error then restore the original `_write` - // and call it with the remaining part of the current chunk - if (cursor === CLIENT_PRELUDE.length) { - this._log.debug('Successfully received the client connection header prelude.'); - delete this._write; - chunk = chunk.slice(cursor - offset); - this._write(chunk, encoding, done); - } - }; -}; - -// Data flow -// --------- - -// +---------------------------------------------+ -// | | -// | +-------------------------------------+ | -// | | +---------+ +---------+ +---------+ | | -// | | | stream1 | | stream2 | | ... | | | -// | | +---------+ +---------+ +---------+ | | -// | | connection | | -// | +-------------------------------------+ | -// | | ^ | -// | pipe | | pipe | -// | v | | -// | +------------------+------------------+ | -// | | compressor | decompressor | | -// | +------------------+------------------+ | -// | | ^ | -// | pipe | | pipe | -// | v | | -// | +------------------+------------------+ | -// | | serializer | deserializer | | -// | +------------------+------------------+ | -// | | ^ | -// | _read() | | _write() | -// | v | | -// | +------------+ +-----------+ | -// | |output queue| |input queue| | -// +------+------------+-----+-----------+-------+ -// | ^ -// read() | | write() -// v | - -function createTransformStream(filter) { - var transform = new Transform({ objectMode: true }); - var push = transform.push.bind(transform); - transform._transform = function(frame, encoding, done) { - filter(frame, push, done); - }; - return transform; -} - -function pipeAndFilter(stream1, stream2, filter) { - if (filter) { - stream1.pipe(createTransformStream(filter)).pipe(stream2); - } else { - stream1.pipe(stream2); - } -} - -var MAX_HTTP_PAYLOAD_SIZE = 16383; - -Endpoint.prototype._initializeDataFlow = function _initializeDataFlow(role, settings, filters) { - var firstStreamId, compressorRole, decompressorRole; - if (role === 'CLIENT') { - firstStreamId = 1; - compressorRole = 'REQUEST'; - decompressorRole = 'RESPONSE'; - } else { - firstStreamId = 2; - compressorRole = 'RESPONSE'; - decompressorRole = 'REQUEST'; - } - - this._serializer = new Serializer(this._log, MAX_HTTP_PAYLOAD_SIZE); - this._deserializer = new Deserializer(this._log, MAX_HTTP_PAYLOAD_SIZE); - this._compressor = new Compressor(this._log, compressorRole); - this._decompressor = new Decompressor(this._log, decompressorRole); - this._connection = new Connection(this._log, firstStreamId, settings); - - pipeAndFilter(this._connection, this._compressor, filters.beforeCompression); - pipeAndFilter(this._compressor, this._serializer, filters.beforeSerialization); - pipeAndFilter(this._deserializer, this._decompressor, filters.afterDeserialization); - pipeAndFilter(this._decompressor, this._connection, filters.afterDecompression); -}; - -var noread = {}; -Endpoint.prototype._read = function _read() { - this._readableState.sync = true; - var moreNeeded = noread, chunk; - while (moreNeeded && (chunk = this._serializer.read())) { - moreNeeded = this.push(chunk); - } - if (moreNeeded === noread) { - this._serializer.once('readable', this._read.bind(this)); - } - this._readableState.sync = false; -}; - -Endpoint.prototype._write = function _write(chunk, encoding, done) { - this._deserializer.write(chunk, encoding, done); -}; - -// Management -// -------------- - -Endpoint.prototype._initializeManagement = function _initializeManagement() { - this._connection.on('stream', this.emit.bind(this, 'stream')); -}; - -Endpoint.prototype.createStream = function createStream() { - return this._connection.createStream(); -}; - -// Error handling -// -------------- - -Endpoint.prototype._initializeErrorHandling = function _initializeErrorHandling() { - this._serializer.on('error', this._error.bind(this, 'serializer')); - this._deserializer.on('error', this._error.bind(this, 'deserializer')); - this._compressor.on('error', this._error.bind(this, 'compressor')); - this._decompressor.on('error', this._error.bind(this, 'decompressor')); - this._connection.on('error', this._error.bind(this, 'connection')); - - this._connection.on('peerError', this.emit.bind(this, 'peerError')); -}; - -Endpoint.prototype._error = function _error(component, error) { - this._log.fatal({ source: component, message: error }, 'Fatal error, closing connection'); - this.close(error); - setImmediate(this.emit.bind(this, 'error', error)); -}; - -Endpoint.prototype.close = function close(error) { - this._connection.close(error); -}; - -// Bunyan serializers -// ------------------ - -exports.serializers = {}; - -var nextId = 0; -exports.serializers.e = function(endpoint) { - if (!('id' in endpoint)) { - endpoint.id = nextId; - nextId += 1; - } - return endpoint.id; -}; diff --git a/lib/flow.js b/lib/flow.js deleted file mode 100644 index f5b19af6..00000000 --- a/lib/flow.js +++ /dev/null @@ -1,362 +0,0 @@ -var assert = require('assert'); - -// The Flow class -// ============== - -// Flow is a [Duplex stream][1] subclass which implements HTTP/2 flow control. It is designed to be -// subclassed by [Connection](connection.html) and the `upstream` component of [Stream](stream.html). -// [1]: http://nodejs.org/api/stream.html#stream_class_stream_duplex - -var Duplex = require('stream').Duplex; - -exports.Flow = Flow; - -// Public API -// ---------- - -// * **Event: 'error' (type)**: signals an error -// -// * **setInitialWindow(size)**: the initial flow control window size can be changed *any time* -// ([as described in the standard][1]) using this method -// -// * **disableRemoteFlowControl()**: sends a WINDOW_UPDATE signaling that we don't want flow control -// -// * **disableLocalFlowControl()**: disables flow control for outgoing frames -// -// [1]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-6.9.2 - -// API for child classes -// --------------------- - -// * **new Flow([flowControlId])**: creating a new flow that will listen for WINDOW_UPDATES frames -// with the given `flowControlId` (or every update frame if not given) -// -// * **_send()**: called when more frames should be pushed. The child class is expected to override -// this (instead of the `_read` method of the Duplex class). -// -// * **_receive(frame, readyCallback)**: called when there's an incoming frame. The child class is -// expected to override this (instead of the `_write` method of the Duplex class). -// -// * **push(frame): bool**: schedules `frame` for sending. -// -// Returns `true` if it needs more frames in the output queue, `false` if the output queue is -// full, and `null` if did not push the frame into the output queue (instead, it pushed it into -// the flow control queue). -// -// * **read(limit): frame**: like the regular `read`, but the 'flow control size' (0 for non-DATA -// frames, length of the payload for DATA frames) of the returned frame will be under `limit`. -// Small exception: pass -1 as `limit` if the max. flow control size is 0. `read(0)` means the -// same thing as [in the original API](http://nodejs.org/api/stream.html#stream_stream_read_0). -// -// * **getLastQueuedFrame(): frame**: returns the last frame in output buffers -// -// * **_log**: the Flow class uses the `_log` object of the parent - -// Constructor -// ----------- - -// When a HTTP/2.0 connection is first established, new streams are created with an initial flow -// control window size of 65535 bytes. -var INITIAL_WINDOW_SIZE = 65535; - -// `flowControlId` is needed if only specific WINDOW_UPDATEs should be watched. -function Flow(flowControlId) { - Duplex.call(this, { objectMode: true }); - - this._window = this._initialWindow = INITIAL_WINDOW_SIZE; - this._flowControlId = flowControlId; - this._queue = []; - this._ended = false; - this._received = 0; - this._remoteFlowControlDisabled = false; -} -Flow.prototype = Object.create(Duplex.prototype, { constructor: { value: Flow } }); - -// Incoming frames -// --------------- - -// `_receive` is called when there's an incoming frame. -Flow.prototype._receive = function _receive(frame, callback) { - throw new Error('The _receive(frame, callback) method has to be overridden by the child class!'); -}; - -// `_receive` is called by `_write` which in turn is [called by Duplex][1] when someone `write()`s -// to the flow. It emits the 'receiving' event and notifies the window size tracking code if the -// incoming frame is a WINDOW_UPDATE. -// [1]: http://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1 -Flow.prototype._write = function _write(frame, encoding, callback) { - if (frame.flags.END_STREAM || (frame.type === 'RST_STREAM')) { - this._ended = true; - } - - if ((frame.type === 'DATA') && (frame.data.length > 0) && !this._remoteFlowControlDisabled) { - this._receive(frame, function() { - this._received += frame.data.length; - if (!this._restoreWindowTimer) { - this._restoreWindowTimer = setImmediate(this._restoreWindow.bind(this)); - } - callback(); - }.bind(this)); - } - - else { - this._receive(frame, callback); - } - - if ((frame.type === 'WINDOW_UPDATE') && - ((this._flowControlId === undefined) || (frame.stream === this._flowControlId))) { - this._updateWindow(frame); - } -}; - -// `_restoreWindow` basically acknowledges the DATA frames received since it's last call. It sends -// a WINDOW_UPDATE that restores the flow control window of the remote end. -Flow.prototype._restoreWindow = function _restoreWindow() { - delete this._restoreWindowTimer; - if (!this._ended && !this._remoteFlowControlDisabled && (this._received > 0)) { - this.push({ - type: 'WINDOW_UPDATE', - flags: {}, - stream: this._flowControlId, - window_size: this._received - }); - this._received = 0; - } -}; - -// Must be called after sending a SETTINGS frame that turns off flow control on the remote side. -Flow.prototype.disableRemoteFlowControl = function disableRemoteFlowControl() { - this._log.debug('Turning off remote flow control'); - this._remoteFlowControlDisabled = true; -}; - -// Outgoing frames - sending procedure -// ----------------------------------- - -// flow -// +-------------------------------------------------+ -// | | -// +--------+ +---------+ | -// read() | output | _read() | flow | _send() | -// <----------| |<----------| control |<------------- | -// | buffer | | buffer | | -// +--------+ +---------+ | -// | input | | -// ---------->| |-----------------------------------> | -// write() | buffer | _write() _receive() | -// +--------+ | -// | | -// +-------------------------------------------------+ - -// `_send` is called when more frames should be pushed to the output buffer. -Flow.prototype._send = function _send() { - throw new Error('The _send() method has to be overridden by the child class!'); -}; - -// `_send` is called by `_read` which is in turn [called by Duplex][1] when it wants to have more -// items in the output queue. -// [1]: http://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1 -Flow.prototype._read = function _read() { - // * if the flow control queue is empty, then let the user push more frames - if (this._queue.length === 0) { - this._send(); - } - - // * if there are items in the flow control queue, then let's put them into the output queue (to - // the extent it is possible with respect to the window size and output queue feedback) - else if (this._window > 0) { - this._readableState.sync = true; // to avoid reentrant calls - do { - var moreNeeded = this._push(this._queue[0]); - if (moreNeeded !== null) { - this._queue.shift(); - } - } while (moreNeeded && (this._queue.length > 0)); - this._readableState.sync = false; - - assert((moreNeeded == false) || // * output queue is full - (this._queue.length === 0) || // * flow control queue is empty - (!this._window && (this._queue[0].type === 'DATA'))); // * waiting for window update - } - - // * otherwise, come back when the flow control window is positive - else { - this.once('window_update', this._read); - } -}; - -var MAX_PAYLOAD_SIZE = 4096; // Must not be greater than MAX_HTTP_PAYLOAD_SIZE which is 16383 - -// `read(limit)` is like the `read` of the Readable class, but it guarantess that the 'flow control -// size' (0 for non-DATA frames, length of the payload for DATA frames) of the returned frame will -// be under `limit`. -Flow.prototype.read = function read(limit) { - if (limit === 0) { - return Duplex.prototype.read.call(this, 0); - } else if (limit === -1) { - limit = 0; - } else if ((limit === undefined) || (limit > MAX_PAYLOAD_SIZE)) { - limit = MAX_PAYLOAD_SIZE; - } - - // * Looking at the first frame in the queue without pulling it out if possible. This will save - // a costly unshift if the frame proves to be too large to return. - var firstInQueue = this._readableState.buffer[0]; - var frame = firstInQueue || Duplex.prototype.read.call(this); - - if ((frame === null) || (frame.type !== 'DATA') || (frame.data.length <= limit)) { - if (firstInQueue) { - Duplex.prototype.read.call(this); - } - return frame; - } - - else if (limit <= 0) { - if (!firstInQueue) { - this.unshift(frame); - } - return null; - } - - else { - this._log.trace({ frame: frame, size: frame.data.length, forwardable: limit }, - 'Splitting out forwardable part of a DATA frame.'); - var forwardable = { - type: 'DATA', - flags: {}, - stream: frame.stream, - data: frame.data.slice(0, limit) - }; - frame.data = frame.data.slice(limit); - - if (!firstInQueue) { - this.unshift(frame); - } - return forwardable; - } -}; - -// `_parentPush` pushes the given `frame` into the output queue -Flow.prototype._parentPush = function _parentPush(frame) { - this._log.trace({ frame: frame }, 'Pushing frame into the output queue'); - - if (frame && (frame.type === 'DATA') && (this._window !== Infinity)) { - this._log.trace({ window: this._window, by: frame.data.length }, - 'Decreasing flow control window size.'); - this._window -= frame.data.length; - assert(this._window >= 0); - } - - return Duplex.prototype.push.call(this, frame); -}; - -// `_push(frame)` pushes `frame` into the output queue and decreases the flow control window size. -// It is capable of splitting DATA frames into smaller parts, if the window size is not enough to -// push the whole frame. The return value is similar to `push` except that it returns `null` if it -// did not push the whole frame to the output queue (but maybe it did push part of the frame). -Flow.prototype._push = function _push(frame) { - var data = frame && (frame.type === 'DATA') && frame.data; - - if (!data || (data.length <= this._window)) { - return this._parentPush(frame); - } - - else if (this._window <= 0) { - return null; - } - - else { - this._log.trace({ frame: frame, size: frame.data.length, forwardable: this._window }, - 'Splitting out forwardable part of a DATA frame.'); - frame.data = data.slice(this._window); - this._parentPush({ - type: 'DATA', - flags: {}, - stream: frame.stream, - data: data.slice(0, this._window) - }); - return null; - } -}; - -// Push `frame` into the flow control queue, or if it's empty, then directly into the output queue -Flow.prototype.push = function push(frame) { - if (frame === null) { - this._log.debug('Enqueueing outgoing End Of Stream'); - } else { - this._log.debug({ frame: frame }, 'Enqueueing outgoing frame'); - } - - var moreNeeded = null; - if (this._queue.length === 0) { - moreNeeded = this._push(frame); - } - - if (moreNeeded === null) { - this._queue.push(frame); - } - - return moreNeeded; -}; - -// `getLastQueuedFrame` returns the last frame in output buffers. This is primarily used by the -// [Stream](stream.html) class to mark the last frame with END_STREAM flag. -Flow.prototype.getLastQueuedFrame = function getLastQueuedFrame() { - var readableQueue = this._readableState.buffer; - return this._queue[this._queue.length - 1] || readableQueue[readableQueue.length - 1]; -}; - -// Outgoing frames - managing the window size -// ------------------------------------------ - -// Flow control window size is manipulated using the `_increaseWindow` method. -// -// * Invoking it with `Infinite` means turning off flow control. Flow control cannot be enabled -// again once disabled. Any attempt to re-enable flow control MUST be rejected with a -// FLOW_CONTROL_ERROR error code. -// * A sender MUST NOT allow a flow control window to exceed 2^31 - 1 bytes. The action taken -// depends on it being a stream or the connection itself. - -var WINDOW_SIZE_LIMIT = Math.pow(2, 31) - 1; - -Flow.prototype._increaseWindow = function _increaseWindow(size) { - if ((this._window === Infinity) && (size !== Infinity)) { - this._log.error('Trying to increase flow control window after flow control was turned off.'); - this.emit('error', 'FLOW_CONTROL_ERROR'); - } else { - this._log.trace({ window: this._window, by: size }, 'Increasing flow control window size.'); - this._window += size; - if ((this._window !== Infinity) && (this._window > WINDOW_SIZE_LIMIT)) { - this._log.error('Flow control window grew too large.'); - this.emit('error', 'FLOW_CONTROL_ERROR'); - } else { - this.emit('window_update'); - } - } -}; - -// The `_updateWindow` method gets called every time there's an incoming WINDOW_UPDATE frame. It -// modifies the flow control window: -// -// * Flow control can be disabled for an individual stream by sending a WINDOW_UPDATE with the -// END_FLOW_CONTROL flag set. The payload of a WINDOW_UPDATE frame that has the END_FLOW_CONTROL -// flag set is ignored. -// * A sender that receives a WINDOW_UPDATE frame updates the corresponding window by the amount -// specified in the frame. -Flow.prototype._updateWindow = function _updateWindow(frame) { - this._increaseWindow(frame.flags.END_FLOW_CONTROL ? Infinity : frame.window_size); -}; - -// A SETTINGS frame can alter the initial flow control window size for all current streams. When the -// value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the size of all stream by -// calling the `setInitialWindow` method. The window size has to be modified by the difference -// between the new value and the old value. -Flow.prototype.setInitialWindow = function setInitialWindow(initialWindow) { - this._increaseWindow(initialWindow - this._initialWindow); - this._initialWindow = initialWindow; -}; - -// Flow control for outgoing frames can be disabled by the peer with various methods. -Flow.prototype.disableLocalFlowControl = function disableLocalFlowControl() { - this._increaseWindow(Infinity); -}; diff --git a/lib/framer.js b/lib/framer.js deleted file mode 100644 index 4e0b2178..00000000 --- a/lib/framer.js +++ /dev/null @@ -1,746 +0,0 @@ -// The framer consists of two [Transform Stream][1] subclasses that operate in [object mode][2]: -// the Serializer and the Deserializer -// [1]: http://nodejs.org/api/stream.html#stream_class_stream_transform -// [2]: http://nodejs.org/api/stream.html#stream_new_stream_readable_options -var assert = require('assert'); - -var Transform = require('stream').Transform; - -exports.Serializer = Serializer; -exports.Deserializer = Deserializer; - -var logData = Boolean(process.env.HTTP2_LOG_DATA); - -// Serializer -// ---------- -// -// Frame Objects -// * * * * * * * --+--------------------------- -// | | -// v v Buffers -// [] -----> Payload Ser. --[buffers]--> Header Ser. --> * * * * -// empty adds payload adds header -// array buffers buffer - -function Serializer(log, sizeLimit) { - this._log = log.child({ component: 'serializer' }); - this._sizeLimit = sizeLimit || MAX_PAYLOAD_SIZE; - Transform.call(this, { objectMode: true }); -} -Serializer.prototype = Object.create(Transform.prototype, { constructor: { value: Serializer } }); - -// When there's an incoming frame object, it first generates the frame type specific part of the -// frame (payload), and then then adds the header part which holds fields that are common to all -// frame types (like the length of the payload). -Serializer.prototype._transform = function _transform(frame, encoding, done) { - this._log.trace({ frame: frame }, 'Outgoing frame'); - - assert(frame.type in Serializer, 'Unknown frame type: ' + frame.type); - - var buffers = []; - Serializer[frame.type](frame, buffers); - Serializer.commonHeader(frame, buffers); - - assert(buffers[0].readUInt16BE(0) <= this._sizeLimit, 'Frame too large!'); - - for (var i = 0; i < buffers.length; i++) { - if (logData) { - this._log.trace({ data: buffers[i] }, 'Outgoing data'); - } - this.push(buffers[i]); - } - - done(); -}; - -// Deserializer -// ------------ -// -// Buffers -// * * * * --------+------------------------- -// | | -// v v Frame Objects -// {} -----> Header Des. --{frame}--> Payload Des. --> * * * * * * * -// empty adds parsed adds parsed -// object header properties payload properties - -function Deserializer(log, sizeLimit) { - this._log = log.child({ component: 'deserializer' }); - this._sizeLimit = sizeLimit || MAX_PAYLOAD_SIZE; - Transform.call(this, { objectMode: true }); - this._next(COMMON_HEADER_SIZE); -} -Deserializer.prototype = Object.create(Transform.prototype, { constructor: { value: Deserializer } }); - -// The Deserializer is stateful, and it's two main alternating states are: *waiting for header* and -// *waiting for payload*. The state is stored in the boolean property `_waitingForHeader`. -// -// When entering a new state, a `_buffer` is created that will hold the accumulated data (header or -// payload). The `_cursor` is used to track the progress. -Deserializer.prototype._next = function(size) { - this._cursor = 0; - this._buffer = new Buffer(size); - this._waitingForHeader = !this._waitingForHeader; - if (this._waitingForHeader) { - this._frame = {}; - } -}; - -// Parsing an incoming buffer is an iterative process because it can hold multiple frames if it's -// large enough. A `cursor` is used to track the progress in parsing the incoming `chunk`. -Deserializer.prototype._transform = function _transform(chunk, encoding, done) { - var cursor = 0; - - if (logData) { - this._log.trace({ data: chunk }, 'Incoming data'); - } - - while(cursor < chunk.length) { - // The content of an incoming buffer is first copied to `_buffer`. If it can't hold the full - // chunk, then only a part of it is copied. - var toCopy = Math.min(chunk.length - cursor, this._buffer.length - this._cursor); - chunk.copy(this._buffer, this._cursor, cursor, cursor + toCopy); - this._cursor += toCopy; - cursor += toCopy; - - // When `_buffer` is full, it's content gets parsed either as header or payload depending on - // the actual state. - - // If it's header then the parsed data is stored in a temporary variable and then the - // deserializer waits for the specified length payload. - if ((this._cursor === this._buffer.length) && this._waitingForHeader) { - var payloadSize = Deserializer.commonHeader(this._buffer, this._frame); - if (payloadSize <= this._sizeLimit) { - this._next(payloadSize); - } else { - this.emit('error', 'FRAME_TOO_LARGE'); - return; - } - } - - // If it's payload then the the frame object is finalized and then gets pushed out. - // Unknown frame types are ignored. - // - // Note: If we just finished the parsing of a header and the payload length is 0, this branch - // will also run. - if ((this._cursor === this._buffer.length) && !this._waitingForHeader) { - if (this._frame.type) { - var error = Deserializer[this._frame.type](this._buffer, this._frame); - if (error) { - this._log.error('Incoming frame parsing error: ' + error); - this.emit('error', 'PROTOCOL_ERROR'); - } else { - this._log.trace({ frame: this._frame }, 'Incoming frame'); - this.push(this._frame); - } - } else { - this._log.warn({ frame: this._frame }, 'Unknown type incoming frame'); - } - this._next(COMMON_HEADER_SIZE); - } - } - - done(); -}; - -// [Frame Header](http://http2.github.io/http2-spec/#FrameHeader) -// -------------------------------------------------------------- -// -// HTTP/2.0 frames share a common base format consisting of an 8-byte header followed by 0 to 65535 -// bytes of data. -// -// Additional size limits can be set by specific application uses. HTTP limits the frame size to -// 16,383 octets. -// -// 0 1 2 3 -// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | Length (16) | Type (8) | Flags (8) | -// +-+-------------+---------------+-------------------------------+ -// |R| Stream Identifier (31) | -// +-+-------------------------------------------------------------+ -// | Frame Data (0...) ... -// +---------------------------------------------------------------+ -// -// The fields of the frame header are defined as: -// -// * Length: -// The length of the frame data expressed as an unsigned 16-bit integer. The 8 bytes of the frame -// header are not included in this value. -// -// * Type: -// The 8-bit type of the frame. The frame type determines how the remainder of the frame header -// and data are interpreted. Implementations MUST ignore unsupported and unrecognized frame types. -// -// * Flags: -// An 8-bit field reserved for frame-type specific boolean flags. -// -// Flags are assigned semantics specific to the indicated frame type. Flags that have no defined -// semantics for a particular frame type MUST be ignored, and MUST be left unset (0) when sending. -// -// * R: -// A reserved 1-bit field. The semantics of this bit are undefined and the bit MUST remain unset -// (0) when sending and MUST be ignored when receiving. -// -// * Stream Identifier: -// A 31-bit stream identifier (see Section 3.4.1). A value 0 is reserved for frames that are -// associated with the connection as a whole as opposed to an individual stream. -// -// The structure and content of the remaining frame data is dependent entirely on the frame type. - -var COMMON_HEADER_SIZE = 8; -var MAX_PAYLOAD_SIZE = 65535; - -var frameTypes = []; - -var frameFlags = {}; - -var genericAttributes = ['type', 'flags', 'stream']; - -var typeSpecificAttributes = {}; - -Serializer.commonHeader = function writeCommonHeader(frame, buffers) { - var headerBuffer = new Buffer(COMMON_HEADER_SIZE); - - var size = 0; - for (var i = 0; i < buffers.length; i++) { - size += buffers[i].length; - } - assert(size <= MAX_PAYLOAD_SIZE, size); - headerBuffer.writeUInt16BE(size, 0); - - var typeId = frameTypes.indexOf(frame.type); // If we are here then the type is valid for sure - headerBuffer.writeUInt8(typeId, 2); - - var flagByte = 0; - for (var flag in frame.flags) { - var position = frameFlags[frame.type].indexOf(flag); - assert(position !== -1, 'Unknown flag for frame type ' + frame.type + ': ' + flag); - if (frame.flags[flag]) { - flagByte |= (1 << position); - } - } - headerBuffer.writeUInt8(flagByte, 3); - - assert((0 <= frame.stream) && (frame.stream < 0x7fffffff), frame.stream); - headerBuffer.writeUInt32BE(frame.stream || 0, 4); - - buffers.unshift(headerBuffer); -}; - -Deserializer.commonHeader = function readCommonHeader(buffer, frame) { - var length = buffer.readUInt16BE(0); - - frame.type = frameTypes[buffer.readUInt8(2)]; - - frame.flags = {}; - var flagByte = buffer.readUInt8(3); - var definedFlags = frameFlags[frame.type]; - for (var i = 0; i < definedFlags.length; i++) { - frame.flags[definedFlags[i]] = Boolean(flagByte & (1 << i)); - } - - frame.stream = buffer.readUInt32BE(4) & 0x7fffffff; - - return length; -}; - -// Frame types -// =========== - -// Every frame type is registered in the following places: -// -// * `frameTypes`: a register of frame type codes (used by `commonHeader()`) -// * `frameFlags`: a register of valid flags for frame types (used by `commonHeader()`) -// * `typeSpecificAttributes`: a register of frame specific frame object attributes (used by -// logging code and also serves as documentation for frame objects) - -// [DATA Frames](http://http2.github.io/http2-spec/#DataFrames) -// ------------------------------------------------------------ -// -// DATA frames (type=0x0) convey arbitrary, variable-length sequences of octets associated with a -// stream. -// -// The DATA frame defines the following flags: -// -// * END_STREAM (0x1): -// Bit 1 being set indicates that this frame is the last that the endpoint will send for the -// identified stream. -// * RESERVED (0x2): -// Bit 2 is reserved for future use. - -frameTypes[0x0] = 'DATA'; - -frameFlags.DATA = ['END_STREAM', 'RESERVED']; - -typeSpecificAttributes.DATA = ['data']; - -Serializer.DATA = function writeData(frame, buffers) { - buffers.push(frame.data); -}; - -Deserializer.DATA = function readData(buffer, frame) { - frame.data = buffer; -}; - -// [HEADERS](http://http2.github.io/http2-spec/#HEADERS) -// -------------------------------------------------------------- -// -// The HEADERS frame (type=0x1) allows the sender to create a stream. -// -// The HEADERS frame defines the following flags: -// -// * END_STREAM (0x1): -// Bit 1 being set indicates that this frame is the last that the endpoint will send for the -// identified stream. -// * RESERVED (0x2): -// Bit 2 is reserved for future use. -// * END_HEADERS (0x4): -// The END_HEADERS bit indicates that this frame contains the entire payload necessary to provide -// a complete set of headers. -// * PRIORITY (0x8): -// Bit 4 being set indicates that the first four octets of this frame contain a single reserved -// bit and a 31-bit priority. - -frameTypes[0x1] = 'HEADERS'; - -frameFlags.HEADERS = ['END_STREAM', 'RESERVED', 'END_HEADERS', 'PRIORITY']; - -typeSpecificAttributes.HEADERS = ['priority', 'headers', 'data']; - -// 0 1 2 3 -// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// |X| (Optional) Priority (31) | -// +-+-------------------------------------------------------------+ -// | Header Block (*) ... -// +---------------------------------------------------------------+ -// -// The payload of a HEADERS frame contains a Headers Block - -Serializer.HEADERS = function writeHeadersPriority(frame, buffers) { - if (frame.flags.PRIORITY) { - var buffer = new Buffer(4); - assert((0 <= frame.priority) && (frame.priority <= 0xffffffff), frame.priority); - buffer.writeUInt32BE(frame.priority, 0); - buffers.push(buffer); - } - buffers.push(frame.data); -}; - -Deserializer.HEADERS = function readHeadersPriority(buffer, frame) { - if (frame.flags.PRIORITY) { - frame.priority = buffer.readUInt32BE(0) & 0x7fffffff; - frame.data = buffer.slice(4); - } else { - frame.data = buffer; - } -}; - -// [PRIORITY](http://http2.github.io/http2-spec/#PRIORITY) -// ------------------------------------------------------- -// -// The PRIORITY frame (type=0x2) specifies the sender-advised priority of a stream. -// -// The PRIORITY frame does not define any flags. - -frameTypes[0x2] = 'PRIORITY'; - -frameFlags.PRIORITY = []; - -typeSpecificAttributes.PRIORITY = ['priority']; - -// 0 1 2 3 -// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// |X| Priority (31) | -// +-+-------------------------------------------------------------+ -// -// The payload of a PRIORITY frame contains a single reserved bit and a 31-bit priority. - -Serializer.PRIORITY = function writePriority(frame, buffers) { - var buffer = new Buffer(4); - buffer.writeUInt32BE(frame.priority, 0); - buffers.push(buffer); -}; - -Deserializer.PRIORITY = function readPriority(buffer, frame) { - frame.priority = buffer.readUInt32BE(0); -}; - -// [RST_STREAM](http://http2.github.io/http2-spec/#RST_STREAM) -// ----------------------------------------------------------- -// -// The RST_STREAM frame (type=0x3) allows for abnormal termination of a stream. -// -// No type-flags are defined. - -frameTypes[0x3] = 'RST_STREAM'; - -frameFlags.RST_STREAM = []; - -typeSpecificAttributes.RST_STREAM = ['error']; - -// 0 1 2 3 -// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | Error Code (32) | -// +---------------------------------------------------------------+ -// -// The RST_STREAM frame contains a single unsigned, 32-bit integer identifying the error -// code (see Error Codes). The error code indicates why the stream is being terminated. - -Serializer.RST_STREAM = function writeRstStream(frame, buffers) { - var buffer = new Buffer(4); - var code = errorCodes.indexOf(frame.error); - assert((0 <= code) && (code <= 0xffffffff), code); - buffer.writeUInt32BE(code, 0); - buffers.push(buffer); -}; - -Deserializer.RST_STREAM = function readRstStream(buffer, frame) { - frame.error = errorCodes[buffer.readUInt32BE(0)]; -}; - -// [SETTINGS](http://http2.github.io/http2-spec/#SETTINGS) -// ------------------------------------------------------- -// -// The SETTINGS frame (type=0x4) conveys configuration parameters that affect how endpoints -// communicate. -// -// The SETTINGS frame does not define any flags. - -frameTypes[0x4] = 'SETTINGS'; - -frameFlags.SETTINGS = []; - -typeSpecificAttributes.SETTINGS = ['settings']; - -// The payload of a SETTINGS frame consists of zero or more settings. Each setting consists of an -// 8-bit reserved field, an unsigned 24-bit setting identifier, and an unsigned 32-bit value. -// -// 0 1 2 3 -// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// | Reserved(8) | Setting Identifier (24) | -// +---------------+-----------------------------------------------+ -// | Value (32) | -// +---------------------------------------------------------------+ -// -// Each setting in a SETTINGS frame replaces the existing value for that setting. Settings are -// processed in the order in which they appear, and a receiver of a SETTINGS frame does not need to -// maintain any state other than the current value of settings. Therefore, the value of a setting -// is the last value that is seen by a receiver. This permits the inclusion of the same settings -// multiple times in the same SETTINGS frame, though doing so does nothing other than waste -// connection capacity. - -Serializer.SETTINGS = function writeSettings(frame, buffers) { - var settings = [], settingsLeft = Object.keys(frame.settings); - definedSettings.forEach(function(setting, id) { - if (setting.name in frame.settings) { - settingsLeft.splice(settingsLeft.indexOf(setting.name), 1); - var value = frame.settings[setting.name]; - settings.push({ id: id, value: setting.flag ? Boolean(value) : value }); - } - }); - assert(settingsLeft.length === 0, 'Unknown settings: ' + settingsLeft.join(', ')); - - var buffer = new Buffer(settings.length * 8); - for (var i = 0; i < settings.length; i++) { - buffer.writeUInt32BE(settings[i].id & 0xffffff, i*8); - buffer.writeUInt32BE(settings[i].value, i*8 + 4); - } - - buffers.push(buffer); -}; - -Deserializer.SETTINGS = function readSettings(buffer, frame) { - frame.settings = {}; - - if (buffer.length % 8 !== 0) { - return 'Invalid SETTINGS frame'; - } - for (var i = 0; i < buffer.length / 8; i++) { - var id = buffer.readUInt32BE(i*8) & 0xffffff; - var setting = definedSettings[id]; - if (setting) { - var value = buffer.readUInt32BE(i*8 + 4); - frame.settings[setting.name] = setting.flag ? Boolean(value & 0x1) : value; - } else { - /* Unknown setting, ignoring */ - } - } -}; - -// The following settings are defined: -var definedSettings = []; - -// * SETTINGS_MAX_CONCURRENT_STREAMS (4): -// indicates the maximum number of concurrent streams that the sender will allow. -definedSettings[4] = { name: 'SETTINGS_MAX_CONCURRENT_STREAMS', flag: false }; - -// * SETTINGS_INITIAL_WINDOW_SIZE (7): -// indicates the sender's initial stream window size (in bytes) for new streams. -definedSettings[7] = { name: 'SETTINGS_INITIAL_WINDOW_SIZE', flag: false }; - -// * SETTINGS_FLOW_CONTROL_OPTIONS (10): -// indicates that streams directed to the sender will not be subject to flow control. The least -// significant bit (0x1) is set to indicate that new streams are not flow controlled. All other -// bits are reserved. -definedSettings[10] = { name: 'SETTINGS_FLOW_CONTROL_OPTIONS', flag: true }; - -// [PUSH_PROMISE](http://http2.github.io/http2-spec/#PUSH_PROMISE) -// --------------------------------------------------------------- -// -// The PUSH_PROMISE frame (type=0x5) is used to notify the peer endpoint in advance of streams the -// sender intends to initiate. -// -// The PUSH_PROMISE frame defines the following flags: -// -// * END_PUSH_PROMISE (0x1): -// The END_PUSH_PROMISE bit indicates that this frame contains the entire payload necessary to -// provide a complete set of headers. - -frameTypes[0x5] = 'PUSH_PROMISE'; - -frameFlags.PUSH_PROMISE = ['END_PUSH_PROMISE']; - -typeSpecificAttributes.PUSH_PROMISE = ['promised_stream', 'headers', 'data']; - -// 0 1 2 3 -// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// |X| Promised-Stream-ID (31) | -// +-+-------------------------------------------------------------+ -// | Header Block (*) ... -// +---------------------------------------------------------------+ -// -// The PUSH_PROMISE frame includes the unsigned 31-bit identifier of -// the stream the endpoint plans to create along with a minimal set of headers that provide -// additional context for the stream. - -Serializer.PUSH_PROMISE = function writePushPromise(frame, buffers) { - var buffer = new Buffer(4); - - var promised_stream = frame.promised_stream; - assert((0 <= promised_stream) && (promised_stream <= 0x7fffffff), promised_stream); - buffer.writeUInt32BE(promised_stream, 0); - - buffers.push(buffer); - buffers.push(frame.data); -}; - -Deserializer.PUSH_PROMISE = function readPushPromise(buffer, frame) { - frame.promised_stream = buffer.readUInt32BE(0) & 0x7fffffff; - frame.data = buffer.slice(4); -}; - -// [PING](http://http2.github.io/http2-spec/#PING) -// ----------------------------------------------- -// -// The PING frame (type=0x6) is a mechanism for measuring a minimal round-trip time from the -// sender, as well as determining whether an idle connection is still functional. -// -// The PING frame defines one type-specific flag: -// -// * PONG (0x2): -// Bit 2 being set indicates that this PING frame is a PING response. - -frameTypes[0x6] = 'PING'; - -frameFlags.PING = ['PONG']; - -typeSpecificAttributes.PING = ['data']; - -// In addition to the frame header, PING frames MUST contain 8 additional octets of opaque data. - -Serializer.PING = function writePing(frame, buffers) { - buffers.push(frame.data); -}; - -Deserializer.PING = function readPing(buffer, frame) { - if (buffer.length !== 8) { - return 'Invalid size PING frame'; - } - frame.data = buffer; -}; - -// [GOAWAY](http://http2.github.io/http2-spec/#GOAWAY) -// --------------------------------------------------- -// -// The GOAWAY frame (type=0x7) informs the remote peer to stop creating streams on this connection. -// -// The GOAWAY frame does not define any flags. - -frameTypes[0x7] = 'GOAWAY'; - -frameFlags.GOAWAY = []; - -typeSpecificAttributes.GOAWAY = ['last_stream', 'error']; - -// 0 1 2 3 -// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 -// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -// |X| Last-Stream-ID (31) | -// +-+-------------------------------------------------------------+ -// | Error Code (32) | -// +---------------------------------------------------------------+ -// -// The last stream identifier in the GOAWAY frame contains the highest numbered stream identifier -// for which the sender of the GOAWAY frame has received frames on and might have taken some action -// on. -// -// The GOAWAY frame also contains a 32-bit error code (see Error Codes) that contains the reason for -// closing the connection. - -Serializer.GOAWAY = function writeGoaway(frame, buffers) { - var buffer = new Buffer(8); - - var last_stream = frame.last_stream; - assert((0 <= last_stream) && (last_stream <= 0x7fffffff), last_stream); - buffer.writeUInt32BE(last_stream, 0); - - var code = errorCodes.indexOf(frame.error); - assert((0 <= code) && (code <= 0xffffffff), code); - buffer.writeUInt32BE(code, 4); - - buffers.push(buffer); -}; - -Deserializer.GOAWAY = function readGoaway(buffer, frame) { - frame.last_stream = buffer.readUInt32BE(0) & 0x7fffffff; - frame.error = errorCodes[buffer.readUInt32BE(4)]; -}; - -// [WINDOW_UPDATE](http://http2.github.io/http2-spec/#WINDOW_UPDATE) -// ----------------------------------------------------------------- -// -// The WINDOW_UPDATE frame (type=0x9) is used to implement flow control. -// -// The WINDOW_UPDATE frame does not define any flags. - -frameTypes[0x9] = 'WINDOW_UPDATE'; - -frameFlags.WINDOW_UPDATE = []; - -typeSpecificAttributes.WINDOW_UPDATE = ['window_size']; - -// The payload of a WINDOW_UPDATE frame is a 32-bit value indicating the additional number of bytes -// that the sender can transmit in addition to the existing flow control window. The legal range -// for this field is 1 to 2^31 - 1 (0x7fffffff) bytes; the most significant bit of this value is -// reserved. - -Serializer.WINDOW_UPDATE = function writeWindowUpdate(frame, buffers) { - var buffer = new Buffer(4); - - var window_size = frame.window_size; - assert((0 <= window_size) && (window_size <= 0x7fffffff), window_size); - buffer.writeUInt32BE(window_size, 0); - - buffers.push(buffer); -}; - -Deserializer.WINDOW_UPDATE = function readWindowUpdate(buffer, frame) { - frame.window_size = buffer.readUInt32BE(0) & 0x7fffffff; -}; - -// [CONTINUATION](http://http2.github.io/http2-spec/#CONTINUATION) -// ------------------------------------------------------------ -// -// The CONTINUATION frame (type=0xA) is used to continue a sequence of header block fragments. -// -// The CONTINUATION frame defines the following flags: -// -// * END_STREAM (0x1): -// Bit 1 being set indicates that this frame is the last that the endpoint will send for the -// identified stream. -// * RESERVED (0x2): -// Bit 2 is reserved for future use. -// * END_HEADERS (0x4): -// The END_HEADERS bit indicates that this frame ends the sequence of header block fragments -// necessary to provide a complete set of headers. - -frameTypes[0xA] = 'CONTINUATION'; - -frameFlags.CONTINUATION = ['END_STREAM', 'RESERVED', 'END_HEADERS']; - -typeSpecificAttributes.CONTINUATION = ['headers', 'data']; - -Serializer.CONTINUATION = function writeContinuation(frame, buffers) { - buffers.push(frame.data); -}; - -Deserializer.CONTINUATION = function readContinuation(buffer, frame) { - frame.data = buffer; -}; - -// [Error Codes](http://http2.github.io/http2-spec/#ErrorCodes) -// ------------------------------------------------------------ - -var errorCodes = [ - 'NO_ERROR', - 'PROTOCOL_ERROR', - 'INTERNAL_ERROR', - 'FLOW_CONTROL_ERROR', - , - 'STREAM_CLOSED', - 'FRAME_TOO_LARGE', - 'REFUSED_STREAM', - 'CANCEL', - 'COMPRESSION_ERROR' -]; - -// Logging -// ------- - -// [Bunyan serializers](https://github.com/trentm/node-bunyan#serializers) to improve logging output -// for debug messages emitted in this component. -exports.serializers = {}; - -// * `frame` serializer: it transforms data attributes from Buffers to hex strings and filters out -// flags that are not present. -var frameCounter = 0; -exports.serializers.frame = function(frame) { - if (!frame) { - return null; - } - - if ('id' in frame) { - return frame.id; - } - - frame.id = frameCounter; - frameCounter += 1; - - var logEntry = { id: frame.id }; - genericAttributes.concat(typeSpecificAttributes[frame.type]).forEach(function(name) { - logEntry[name] = frame[name]; - }); - - if (frame.data instanceof Buffer) { - if (logEntry.data.length > 50) { - logEntry.data = frame.data.slice(0, 47).toString('hex') + '...'; - } else { - logEntry.data = frame.data.toString('hex'); - } - - if (!('length' in logEntry)) { - logEntry.length = frame.data.length; - } - } - - if (frame.promised_stream instanceof Object) { - logEntry.promised_stream = 'stream-' + frame.promised_stream.id; - } - - logEntry.flags = Object.keys(frame.flags || {}).filter(function(name) { - return frame.flags[name] === true; - }); - - return logEntry; -}; - -// * `data` serializer: it simply transforms a buffer to a hex string. -exports.serializers.data = function(data) { - return data.toString('hex'); -}; diff --git a/lib/http.js b/lib/http.js deleted file mode 100644 index 127f1162..00000000 --- a/lib/http.js +++ /dev/null @@ -1,983 +0,0 @@ -// Public API -// ========== - -// The main governing power behind the http2 API design is that it should look very similar to the -// existing node.js [HTTPS API][1] (which is, in turn, almost identical to the [HTTP API][2]). The -// additional features of HTTP/2 are exposed as extensions to this API. Furthermore, node-http2 -// should fall back to using HTTP/1.1 if needed. Compatibility with undocumented or deprecated -// elements of the node.js HTTP/HTTPS API is a non-goal. -// -// Additional and modified API elements -// ------------------------------------ -// -// - **Class: http2.Endpoint**: an API for using the raw HTTP/2 framing layer. For documentation -// see the [lib/endpoint.js](endpoint.html) file. -// -// - **Class: http2.Server** -// - **Event: 'connection' (socket, [endpoint])**: there's a second argument if the negotiation of -// HTTP/2 was successful: the reference to the [Endpoint](endpoint.html) object tied to the -// socket. -// -// - **http2.createServer(options, [requestListener])**: additional option: -// - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object -// - **plain**: if `true`, the server will accept HTTP/2 connections over plain TCP instead of -// TLS -// -// - **Class: http2.ServerResponse** -// - **response.push(options)**: initiates a server push. `options` describes the 'imaginary' -// request to which the push stream is a response; the possible options are identical to the -// ones accepted by `http2.request`. Returns a ServerResponse object that can be used to send -// the response headers and content. -// -// - **Class: http2.Agent** -// - **new Agent(options)**: additional option: -// - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object -// - **agent.sockets**: only contains TCP sockets that corresponds to HTTP/1 requests. -// - **agent.endpoints**: contains [Endpoint](endpoint.html) objects for HTTP/2 connections. -// -// - **http2.request(options, [callback])**: additional option: -// - **plain**: if `true`, the client will not try to build a TLS tunnel, instead it will use -// the raw TCP stream for HTTP/2 -// -// - **Class: http2.ClientRequest** -// - **Event: 'socket' (socket)**: in case of an HTTP/2 incoming message, `socket` is a reference -// to the associated [HTTP/2 Stream](stream.html) object (and not to the TCP socket). -// - **Event: 'push' (promise)**: signals the intention of a server push associated to this -// request. `promise` is an IncomingPromise. If there's no listener for this event, the server -// push is cancelled. -// - **request.setPriority(priority)**: assign a priority to this request. `priority` is a number -// between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30. -// -// - **Class: http2.IncomingMessage** -// - has two subclasses for easier interface description: **IncomingRequest** and -// **IncomingResponse** -// - **message.socket**: in case of an HTTP/2 incoming message, it's a reference to the associated -// [HTTP/2 Stream](stream.html) object (and not to the TCP socket). -// -// - **Class: http2.IncomingRequest (IncomingMessage)** -// - **message.url**: in case of an HTTP/2 incoming request, the `url` field always contains the -// path, and never a full url (it contains the path in most cases in the HTTPS api as well). -// - **message.scheme**: additional field. Mandatory HTTP/2 request metadata. -// - **message.host**: additional field. Mandatory HTTP/2 request metadata. Note that this -// replaces the old Host header field, but node-http2 will add Host to the `message.headers` for -// backwards compatibility. -// -// - **Class: http2.IncomingPromise (IncomingRequest)** -// - contains the metadata of the 'imaginary' request to which the server push is an answer. -// - **Event: 'response' (response)**: signals the arrival of the actual push stream. `response` -// is an IncomingResponse. -// - **Event: 'push' (promise)**: signals the intention of a server push associated to this -// request. `promise` is an IncomingPromise. If there's no listener for this event, the server -// push is cancelled. -// - **promise.cancel()**: cancels the promised server push. -// - **promise.setPriority(priority)**: assign a priority to this push stream. `priority` is a -// number between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30. -// -// API elements not yet implemented -// -------------------------------- -// -// - **Class: http2.Server** -// - **server.maxHeadersCount** -// -// API elements that are not applicable to HTTP/2 -// ---------------------------------------------- -// -// The reason may be deprecation of certain HTTP/1.1 features, or that some API elements simply -// don't make sense when using HTTP/2. These will not be present when a request is done with HTTP/2, -// but will function normally when falling back to using HTTP/1.1. -// -// - **Class: http2.Server** -// - **Event: 'checkContinue'**: not in the spec, yet (see [http-spec#18][expect-continue]) -// - **Event: 'upgrade'**: upgrade is deprecated in HTTP/2 -// - **Event: 'timeout'**: HTTP/2 sockets won't timeout because of application level keepalive -// (PING frames) -// - **Event: 'connect'**: not in the spec, yet (see [http-spec#230][connect]) -// - **server.setTimeout(msecs, [callback])** -// - **server.timeout** -// -// - **Class: http2.ServerResponse** -// - **Event: 'close'** -// - **Event: 'timeout'** -// - **response.writeContinue()** -// - **response.writeHead(statusCode, [reasonPhrase], [headers])**: reasonPhrase will always be -// ignored since [it's not supported in HTTP/2][3] -// - **response.setTimeout(timeout, [callback])** -// -// - **Class: http2.Agent** -// - **agent.maxSockets**: only affects HTTP/1 connection pool. When using HTTP/2, there's always -// one connection per host. -// -// - **Class: http2.ClientRequest** -// - **Event: 'upgrade'** -// - **Event: 'connect'** -// - **Event: 'continue'** -// - **request.setTimeout(timeout, [callback])** -// - **request.setNoDelay([noDelay])** -// - **request.setSocketKeepAlive([enable], [initialDelay])** -// -// - **Class: http2.IncomingMessage** -// - **Event: 'close'** -// - **message.setTimeout(timeout, [callback])** -// -// [1]: http://nodejs.org/api/https.html -// [2]: http://nodejs.org/api/http.html -// [3]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-8.1.3 -// [expect-continue]: https://github.com/http2/http2-spec/issues/18 -// [connect]: https://github.com/http2/http2-spec/issues/230 - -// Common server and client side code -// ================================== - -var net = require('net'); -var url = require('url'); -var util = require('util'); -var EventEmitter = require('events').EventEmitter; -var PassThrough = require('stream').PassThrough; -var Readable = require('stream').Readable; -var Writable = require('stream').Writable; -var Endpoint = require('./endpoint').Endpoint; -var http = require('http'); -var https = require('https'); - -exports.STATUS_CODES = http.STATUS_CODES; -exports.IncomingMessage = IncomingMessage; -exports.OutgoingMessage = OutgoingMessage; -exports.Endpoint = Endpoint; - -var deprecatedHeaders = [ - 'connection', - 'host', - 'keep-alive', - 'proxy-connection', - 'te', - 'transfer-encoding', - 'upgrade' -]; - -// The implemented version of the HTTP/2 specification is [draft 04][1]. -// [1]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04 -var implementedVersion = 'HTTP-draft-06/2.0'; - -// When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback -var supportedProtocols = [implementedVersion, 'http/1.1', 'http/1.0']; - -// Using ALPN or NPN depending on node.js support (preferring ALPN) -var negotiationMethod = process.features.tls_alpn ? 'ALPN' : 'NPN'; -var protocolList = process.features.tls_alpn ? 'ALPNProtocols' : 'NPNProtocols'; -var negotiatedProtocol = process.features.tls_alpn ? 'alpnProtocol' : 'npnProtocol'; - -// Logging -// ------- - -// Logger shim, used when no logger is provided by the user. -function noop() {} -var defaultLogger = { - fatal: noop, - error: noop, - warn : noop, - info : noop, - debug: noop, - trace: noop, - - child: function() { return this; } -}; - -// Bunyan serializers exported by submodules that are worth adding when creating a logger. -exports.serializers = {}; -var modules = ['./framer', './compressor', './flow', './connection', './stream', './endpoint']; -modules.forEach(function(module) { - util._extend(exports.serializers, require(module).serializers); -}); - -// IncomingMessage class -// --------------------- - -function IncomingMessage(stream) { - // * This is basically a read-only wrapper for the [Stream](stream.html) class. - PassThrough.call(this); - stream.pipe(this); - this.socket = this.stream = stream; - - this._log = stream._log.child({ component: 'http' }); - - // * HTTP/2.0 does not define a way to carry the version identifier that is included in the - // HTTP/1.1 request/status line. Version is always 2.0. - this.httpVersion = '2.0'; - this.httpVersionMajor = 2; - this.httpVersionMinor = 0; - - // * `this.headers` will store the regular headers (and none of the special colon headers) - this.headers = {}; - this.trailers = undefined; - this._lastHeadersSeen = undefined; - - // * Other metadata is filled in when the headers arrive. - stream.once('headers', this._onHeaders.bind(this)); - stream.once('end', this._onEnd.bind(this)); -} -IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } }); - -// [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-05#section-8.1.2.1) -// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series -// of key-value pairs. This includes the target URI for the request, the status code for the -// response, as well as HTTP header fields. -IncomingMessage.prototype._onHeaders = function _onHeaders(headers) { - // * An HTTP/2.0 request or response MUST NOT include any of the following header fields: - // Connection, Host, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, and Upgrade. A server - // MUST treat the presence of any of these header fields as a stream error of type - // PROTOCOL_ERROR. - for (var i = 0; i < deprecatedHeaders.length; i++) { - var key = deprecatedHeaders[i]; - if (key in headers) { - this._log.error({ key: key, value: headers[key] }, 'Deprecated header found'); - this.stream.emit('error', 'PROTOCOL_ERROR'); - return; - } - } - - // * Store the _regular_ headers in `this.headers` - for (var name in headers) { - if (name[0] !== ':') { - this.headers[name] = headers[name]; - } - } - - // * The last header block, if it's not the first, will represent the trailers - var self = this; - this.stream.on('headers', function(headers) { - self._lastHeadersSeen = headers; - }); -}; - -IncomingMessage.prototype._onEnd = function _onEnd() { - this.trailers = this._lastHeadersSeen; -}; - -IncomingMessage.prototype.setTimeout = noop; - -IncomingMessage.prototype._checkSpecialHeader = function _checkSpecialHeader(key, value) { - if ((typeof value !== 'string') || (value.length === 0)) { - this._log.error({ key: key, value: value }, 'Invalid or missing special header field'); - this.stream.emit('error', 'PROTOCOL_ERROR'); - } - - return value; -} -; - -// OutgoingMessage class -// --------------------- - -function OutgoingMessage() { - // * This is basically a read-only wrapper for the [Stream](stream.html) class. - Writable.call(this); - - this._headers = {}; - this._trailers = undefined; - this.headersSent = false; - - this.on('finish', this._finish); -} -OutgoingMessage.prototype = Object.create(Writable.prototype, { constructor: { value: OutgoingMessage } }); - -OutgoingMessage.prototype._write = function _write(chunk, encoding, callback) { - if (this.stream) { - this.stream.write(chunk, encoding, callback); - } else { - this.once('socket', this._write.bind(this, chunk, encoding, callback)); - } -}; - -OutgoingMessage.prototype._finish = function _finish() { - if (this.stream) { - if (this._trailers) { - if (this.request) { - this.request.addTrailers(this._trailers); - } else { - this.stream.headers(this._trailers); - } - } - this.stream.end(); - } else { - this.once('socket', this._finish.bind(this)); - } -}; - -OutgoingMessage.prototype.setHeader = function setHeader(name, value) { - if (this.headersSent) { - throw new Error('Can\'t set headers after they are sent.'); - } else { - name = name.toLowerCase(); - if (deprecatedHeaders.indexOf(name) !== -1) { - throw new Error('Cannot set deprecated header: ' + name); - } - this._headers[name] = value; - } -}; - -OutgoingMessage.prototype.removeHeader = function removeHeader(name) { - if (this.headersSent) { - throw new Error('Can\'t remove headers after they are sent.'); - } else { - delete this._headers[name.toLowerCase()]; - } -}; - -OutgoingMessage.prototype.getHeader = function getHeader(name) { - return this._headers[name.toLowerCase()]; -}; - -OutgoingMessage.prototype.addTrailers = function addTrailers(trailers) { - this._trailers = trailers; -}; - -OutgoingMessage.prototype.setTimeout = noop; - -OutgoingMessage.prototype._checkSpecialHeader = IncomingMessage.prototype._checkSpecialHeader; - -// Server side -// =========== - -exports.createServer = createServer; -exports.Server = Server; -exports.IncomingRequest = IncomingRequest; -exports.OutgoingResponse = OutgoingResponse; -exports.ServerResponse = OutgoingResponse; // for API compatibility - -// Server class -// ------------ - -function Server(options) { - options = options || {}; - - this._log = (options.log || defaultLogger).child({ component: 'http' }); - this._settings = options.settings; - - var start = this._start.bind(this); - var fallback = this._fallback.bind(this); - - // HTTP2 over TLS (using NPN or ALPN) - if ((options.key && options.cert) || options.pfx) { - this._log.info('Creating HTTP/2 server over TLS/' + negotiationMethod); - this._mode = 'tls'; - options[protocolList] = supportedProtocols; - this._server = https.createServer(options); - this._originalSocketListeners = this._server.listeners('secureConnection'); - this._server.removeAllListeners('secureConnection'); - this._server.on('secureConnection', function(socket) { - if (socket[negotiatedProtocol] === implementedVersion && socket.servername) { - start(socket); - } else { - fallback(socket); - } - }); - this._server.on('request', this.emit.bind(this, 'request')); - } - - // HTTP2 over plain TCP - else if (options.plain) { - this._log.info('Creating HTTP/2 server over plain TCP'); - this._mode = 'plain'; - this._server = net.createServer(start); - } - - // HTTP/2 with HTTP/1.1 upgrade - else { - this._log.error('Trying to create HTTP/2 server with Upgrade from HTTP/1.1'); - throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported. Please provide TLS keys.'); - } - - this._server.on('close', this.emit.bind(this, 'close')); -} -Server.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Server } }); - -// Starting HTTP/2 -Server.prototype._start = function _start(socket) { - var endpoint = new Endpoint(this._log, 'SERVER', this._settings); - - this._log.info({ e: endpoint, - client: socket.remoteAddress + ':' + socket.remotePort, - SNI: socket.servername - }, 'New incoming HTTP/2 connection'); - - endpoint.pipe(socket).pipe(endpoint); - - var self = this; - endpoint.on('stream', function _onStream(stream) { - var response = new OutgoingResponse(stream); - var request = new IncomingRequest(stream); - - request.once('ready', self.emit.bind(self, 'request', request, response)); - }); - - endpoint.on('error', this.emit.bind(this, 'clientError')); - - this.emit('connection', socket, endpoint); -}; - -Server.prototype._fallback = function _fallback(socket) { - this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort, - protocol: socket[negotiatedProtocol], - SNI: socket.servername - }, 'Falling back to simple HTTPS'); - - for (var i = 0; i < this._originalSocketListeners.length; i++) { - this._originalSocketListeners[i].call(this._server, socket); - } - - this.emit('connection', socket); -}; - -// There are [3 possible signatures][1] of the `listen` function. Every arguments is forwarded to -// the backing TCP or HTTPS server. -// [1]: http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback -Server.prototype.listen = function listen(port, hostname) { - this._log.info({ on: ((typeof hostname === 'string') ? (hostname + ':' + port) : port) }, - 'Listening for incoming connections'); - this._server.listen.apply(this._server, arguments); -}; - -Server.prototype.close = function close(callback) { - this._log.info('Closing server'); - this._server.close(callback); -}; - -Server.prototype.setTimeout = function setTimeout(timeout, callback) { - if (this._mode === 'tls') { - this._server.setTimeout(timeout, callback); - } -}; - -Object.defineProperty(Server.prototype, 'timeout', { - get: function getTimeout() { - if (this._mode === 'tls') { - return this._server.timeout; - } else { - return undefined; - } - }, - set: function setTimeout(timeout) { - if (this._mode === 'tls') { - this._server.timeout = timeout; - } - } -}); - -// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to -// `server`.There are events on the `http.Server` class where it makes difference whether someone is -// listening on the event or not. In these cases, we can not simply forward the events from the -// `server` to `this` since that means a listener. Instead, we forward the subscriptions. -Server.prototype.on = function on(event, listener) { - if ((event === 'upgrade') || (event === 'timeout')) { - this._server.on(event, listener && listener.bind(this)); - } else { - EventEmitter.prototype.on.call(this, event, listener); - } -}; - -// `addContext` is used to add Server Name Indication contexts -Server.prototype.addContext = function addContext(hostname, credentials) { - if (this._mode === 'tls') { - this._server.addContext(hostname, credentials); - } -}; - -function createServer(options, requestListener) { - if (typeof options === 'function') { - requestListener = options; - options = undefined; - } - - var server = new Server(options); - - if (requestListener) { - server.on('request', requestListener); - } - - return server; -} - -// IncomingRequest class -// --------------------- - -function IncomingRequest(stream) { - IncomingMessage.call(this, stream); -} -IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } }); - -// [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-05#section-8.1.2.1) -// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series -// of key-value pairs. This includes the target URI for the request, the status code for the -// response, as well as HTTP header fields. -IncomingRequest.prototype._onHeaders = function _onHeaders(headers) { - // * The ":method" header field includes the HTTP method - // * The ":scheme" header field includes the scheme portion of the target URI - // * The ":host" header field includes the authority portion of the target URI - // * The ":path" header field includes the path and query parts of the target URI. - // This field MUST NOT be empty; URIs that do not contain a path component MUST include a value - // of '/', unless the request is an OPTIONS request for '*', in which case the ":path" header - // field MUST include '*'. - // * All HTTP/2.0 requests MUST include exactly one valid value for all of these header fields. A - // server MUST treat the absence of any of these header fields, presence of multiple values, or - // an invalid value as a stream error of type PROTOCOL_ERROR. - this.method = this._checkSpecialHeader(':method', headers[':method']); - this.scheme = this._checkSpecialHeader(':scheme', headers[':scheme']); - this.host = this._checkSpecialHeader(':host' , headers[':host'] ); - this.url = this._checkSpecialHeader(':path' , headers[':path'] ); - - // * Host header is included in the headers object for backwards compatibility. - this.headers.host = this.host; - - // * Handling regular headers. - IncomingMessage.prototype._onHeaders.call(this, headers); - - // * Signaling that the headers arrived. - this._log.info({ method: this.method, scheme: this.scheme, host: this.host, - path: this.url, headers: this.headers }, 'Incoming request'); - this.emit('ready'); -}; - -// OutgoingResponse class -// ---------------------- - -function OutgoingResponse(stream) { - OutgoingMessage.call(this); - - this._log = stream._log.child({ component: 'http' }); - - this.stream = stream; - this.statusCode = 200; - this.sendDate = true; - - this.stream.once('headers', this._onRequestHeaders.bind(this)); -} -OutgoingResponse.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingResponse } }); - -OutgoingResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase, headers) { - if (typeof reasonPhrase === 'string') { - this._log.warn('Reason phrase argument was present but ignored by the writeHead method'); - } else { - headers = reasonPhrase; - } - - for (var name in headers) { - this.setHeader(name, headers[name]); - } - headers = this._headers; - - if (this.sendDate && !('date' in this._headers)) { - headers.date = (new Date()).toUTCString(); - } - - this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response'); - - headers[':status'] = this.statusCode = statusCode; - - this.stream.headers(headers); - this.headersSent = true; -}; - -OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() { - if (!this.headersSent) { - this.writeHead(this.statusCode); - } -}; - -OutgoingResponse.prototype.write = function write() { - this._implicitHeaders(); - return OutgoingMessage.prototype.write.apply(this, arguments); -}; - -OutgoingResponse.prototype.end = function end() { - this._implicitHeaders(); - return OutgoingMessage.prototype.end.apply(this, arguments); -}; - -OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) { - this._requestHeaders = headers; -}; - -OutgoingResponse.prototype.push = function push(options) { - if (typeof options === 'string') { - options = url.parse(options); - } - - if (!options.path) { - throw new Error('`path` option is mandatory.'); - } - - var promise = util._extend({ - ':method': (options.method || 'GET').toUpperCase(), - ':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'], - ':host': options.hostname || options.host || this._requestHeaders[':host'], - ':path': options.path - }, options.headers); - - this._log.info({ method: promise[':method'], scheme: promise[':scheme'], host: promise[':host'], - path: promise[':path'], headers: options.headers }, 'Promising push stream'); - - var pushStream = this.stream.promise(promise); - - return new OutgoingResponse(pushStream); -}; - -// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to -// `request`. See `Server.prototype.on` for explanation. -OutgoingResponse.prototype.on = function on(event, listener) { - if (this.request && (event === 'timeout')) { - this.request.on(event, listener && listener.bind(this)); - } else { - OutgoingMessage.prototype.on.call(this, event, listener); - } -}; - -// Client side -// =========== - -exports.ClientRequest = OutgoingRequest; // for API compatibility -exports.OutgoingRequest = OutgoingRequest; -exports.IncomingResponse = IncomingResponse; -exports.Agent = Agent; -exports.globalAgent = undefined; -exports.request = function request(options, callback) { - return (options.agent || exports.globalAgent).request(options, callback); -}; -exports.get = function get(options, callback) { - return (options.agent || exports.globalAgent).get(options, callback); -}; - -// Agent class -// ----------- - -function Agent(options) { - EventEmitter.call(this); - - options = options || {}; - - this._settings = options.settings; - this._log = (options.log || defaultLogger).child({ component: 'http' }); - this.endpoints = {}; - - // * Using an own HTTPS agent, because the global agent does not look at `NPN/ALPNProtocols` when - // generating the key identifying the connection, so we may get useless non-negotiated TLS - // channels even if we ask for a negotiated one. This agent will contain only negotiated - // channels. - var agentOptions = {}; - agentOptions[protocolList] = supportedProtocols; - this._httpsAgent = new https.Agent(agentOptions); - - this.sockets = this._httpsAgent.sockets; - this.requests = this._httpsAgent.requests; -} -Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } }); - -Agent.prototype.request = function request(options, callback) { - if (typeof options === 'string') { - options = url.parse(options); - } - - options.method = (options.method || 'GET').toUpperCase(); - options.protocol = options.protocol || 'https:'; - options.host = options.hostname || options.host || 'localhost'; - options.port = options.port || 443; - options.path = options.path || '/'; - - if (!options.plain && options.protocol === 'http:') { - this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1'); - throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.'); - } - - var request = new OutgoingRequest(this._log); - - if (callback) { - request.on('response', callback); - } - - var key = [ - !!options.plain, - options.host, - options.port - ].join(':'); - - // * There's an existing HTTP/2 connection to this host - if (key in this.endpoints) { - var endpoint = this.endpoints[key]; - request._start(endpoint.createStream(), options); - } - - // * HTTP/2 over plain TCP - else if (options.plain) { - endpoint = new Endpoint(this._log, 'CLIENT', this._settings); - endpoint.socket = net.connect({ - host: options.host, - port: options.port, - localAddress: options.localAddress - }); - endpoint.pipe(endpoint.socket).pipe(endpoint); - request._start(endpoint.createStream(), options); - } - - // * HTTP/2 over TLS negotiated using NPN or ALPN - else { - var started = false; - options[protocolList] = supportedProtocols; - options.servername = options.host; // Server Name Indication - options.agent = this._httpsAgent; - var httpsRequest = https.request(options); - - httpsRequest.on('socket', function(socket) { - if (socket[negotiatedProtocol] !== undefined) { - negotiated(); - } else { - socket.on('secureConnect', negotiated); - } - }); - - var self = this; - function negotiated() { - var endpoint; - if (httpsRequest.socket[negotiatedProtocol] === implementedVersion) { - httpsRequest.socket.emit('agentRemove'); - unbundleSocket(httpsRequest.socket); - endpoint = new Endpoint(self._log, 'CLIENT', self._settings); - endpoint.socket = httpsRequest.socket; - endpoint.pipe(endpoint.socket).pipe(endpoint); - } - if (started) { - if (endpoint) { - endpoint.close(); - } else { - httpsRequest.abort(); - } - } else { - if (endpoint) { - self._log.info({ e: endpoint, server: options.host + ':' + options.port }, - 'New outgoing HTTP/2 connection'); - self.endpoints[key] = endpoint; - self.emit(key, endpoint); - } else { - self.emit(key, undefined); - } - } - } - - this.once(key, function(endpoint) { - started = true; - if (endpoint) { - request._start(endpoint.createStream(), options); - } else { - request._fallback(httpsRequest); - } - }); - } - - return request; -}; - -Agent.prototype.get = function get(options, callback) { - var request = this.request(options, callback); - request.end(); - return request; -}; - -function unbundleSocket(socket) { - socket.removeAllListeners('data'); - socket.removeAllListeners('end'); - socket.removeAllListeners('readable'); - socket.removeAllListeners('close'); - socket.removeAllListeners('error'); - socket.unpipe(); - delete socket.ondata; - delete socket.onend; -} - -Object.defineProperty(Agent.prototype, 'maxSockets', { - get: function getMaxSockets() { - return this._httpsAgent.maxSockets; - }, - set: function setMaxSockets(value) { - this._httpsAgent.maxSockets = value; - } -}); - -exports.globalAgent = new Agent(); - -// OutgoingRequest class -// --------------------- - -function OutgoingRequest() { - OutgoingMessage.call(this); - - this._log = undefined; - - this.stream = undefined; -} -OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } }); - -OutgoingRequest.prototype._start = function _start(stream, options) { - this.stream = stream; - - this._log = stream._log.child({ component: 'http' }); - - for (var key in options.headers) { - this.setHeader(key, options.headers[key]); - } - var headers = this._headers; - delete headers.host; - - if (options.auth) { - headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64'); - } - - headers[':scheme'] = options.protocol.slice(0, -1); - headers[':method'] = options.method; - headers[':host'] = options.host; - headers[':path'] = options.path; - - this._log.info({ scheme: headers[':scheme'], method: headers[':method'], host: headers[':host'], - path: headers[':path'], headers: (options.headers || {}) }, 'Sending request'); - this.stream.headers(headers); - this.headersSent = true; - - this.emit('socket', this.stream); - - var response = new IncomingResponse(this.stream); - response.once('ready', this.emit.bind(this, 'response', response)); - - this.stream.on('promise', this._onPromise.bind(this)); -}; - -OutgoingRequest.prototype._fallback = function _fallback(request) { - request.on('response', this.emit.bind(this, 'response')); - this.stream = this.request = request; - this.emit('socket', this.socket); -}; - -OutgoingRequest.prototype.setPriority = function setPriority(priority) { - if (this.stream) { - this.stream.priority(priority); - } else { - this.once('socket', this.setPriority.bind(this, priority)); - } -}; - -// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to -// `request`. See `Server.prototype.on` for explanation. -OutgoingRequest.prototype.on = function on(event, listener) { - if (this.request && (event === 'upgrade')) { - this.request.on(event, listener && listener.bind(this)); - } else { - OutgoingMessage.prototype.on.call(this, event, listener); - } -}; - -// Methods only in fallback mode -OutgoingRequest.prototype.setNoDelay = function setNoDelay(noDelay) { - if (this.request) { - this.request.setNoDelay(noDelay); - } else if (!this.stream) { - this.on('socket', this.setNoDelay.bind(this, noDelay)); - } -}; - -OutgoingRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive(enable, initialDelay) { - if (this.request) { - this.request.setSocketKeepAlive(enable, initialDelay); - } else if (!this.stream) { - this.on('socket', this.setSocketKeepAlive.bind(this, enable, initialDelay)); - } -}; - -OutgoingRequest.prototype.setTimeout = function setTimeout(timeout, callback) { - if (this.request) { - this.request.setTimeout(timeout, callback); - } else if (!this.stream) { - this.on('socket', this.setTimeout.bind(this, timeout, callback)); - } -}; - -// Aborting the request -OutgoingRequest.prototype.abort = function abort() { - if (this.request) { - this.request.abort(); - } else if (this.stream) { - this.stream.reset('CANCEL'); - } else { - this.on('socket', this.abort.bind(this)); - } -}; - -// Receiving push promises -OutgoingRequest.prototype._onPromise = function _onPromise(stream, headers) { - this._log.info({ push_stream: stream.id }, 'Receiving push promise'); - - var promise = new IncomingPromise(stream, headers); - - if (this.listeners('push').length > 0) { - this.emit('push', promise); - } else { - promise.cancel(); - } -}; - -// IncomingResponse class -// ---------------------- - -function IncomingResponse(stream) { - IncomingMessage.call(this, stream); -} -IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } }); - -// [Response Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-05#section-8.1.2.2) -// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series -// of key-value pairs. This includes the target URI for the request, the status code for the -// response, as well as HTTP header fields. -IncomingResponse.prototype._onHeaders = function _onHeaders(headers) { - // * A single ":status" header field is defined that carries the HTTP status code field. This - // header field MUST be included in all responses. - // * A client MUST treat the absence of the ":status" header field, the presence of multiple - // values, or an invalid value as a stream error of type PROTOCOL_ERROR. - // Note: currently, we do not enforce it strictly: we accept any format, and parse it as int - // * HTTP/2.0 does not define a way to carry the reason phrase that is included in an HTTP/1.1 - // status line. - this.statusCode = parseInt(this._checkSpecialHeader(':status', headers[':status'])); - - // * Handling regular headers. - IncomingMessage.prototype._onHeaders.call(this, headers); - - // * Signaling that the headers arrived. - this._log.info({ status: this.statusCode, headers: this.headers}, 'Incoming response'); - this.emit('ready'); -}; - -// IncomingPromise class -// ------------------------- - -function IncomingPromise(responseStream, promiseHeaders) { - var stream = new Readable(); - stream._read = noop; - stream.push(null); - stream._log = responseStream._log; - - IncomingRequest.call(this, stream); - - this._onHeaders(promiseHeaders); - - this._responseStream = responseStream; - - var response = new IncomingResponse(this._responseStream); - response.once('ready', this.emit.bind(this, 'response', response)); - - this.stream.on('promise', this._onPromise.bind(this)); -} -IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } }); - -IncomingPromise.prototype.cancel = function cancel() { - this._responseStream.reset('CANCEL'); -}; - -IncomingPromise.prototype.setPriority = function setPriority(priority) { - this._responseStream.priority(priority); -}; - -IncomingPromise.prototype._onPromise = OutgoingRequest.prototype._onPromise; diff --git a/lib/index.js b/lib/index.js deleted file mode 100644 index 0c6783bd..00000000 --- a/lib/index.js +++ /dev/null @@ -1,93 +0,0 @@ -// [node-http2][homepage] is an [HTTP/2 (draft 04)][http2] implementation for [node.js][node]. -// -// The main building blocks are mainly [node.js streams][node-stream] that are connected through -// pipes. -// -// The main components are: -// -// * [http.js](http.html): the top layer that presents an API very similar to the standard node.js -// [HTTPS module][node-https] (which is in turn very similar to the [HTTP module][node-http]). -// -// * [Endpoint](endpoint.html): represents an HTTP/2 endpoint (client or server). It's -// responsible for the the first part of the handshake process (sending/receiving the -// [connection header][http2-connheader]) and manages other components (framer, compressor, -// connection, streams) that make up a client or server. -// -// * [Connection](connection.html): multiplexes the active HTTP/2 streams, manages connection -// lifecycle and settings, and responsible for enforcing the connection level limits (flow -// control, initiated stream limit) -// -// * [Stream](stream.html): implementation of the [HTTP/2 stream concept](http2-stream). -// Implements the [stream state machine][http2-streamstate] defined by the standard, provides -// management methods and events for using the stream (sending/receiving headers, data, etc.), -// and enforces stream level constraints (flow control, sending only legal frames). -// -// * [Flow](flow.html): implements flow control for Connection and Stream as parent class. -// -// * [Compressor and Decompressor](compressor.html): compression and decompression of HEADER and -// PUSH_PROMISE frames -// -// * [Serializer and Deserializer](framer.html): the lowest layer in the stack that transforms -// between the binary and the JavaScript object representation of HTTP/2 frames -// -// [homepage]: https://github.com/molnarg/node-http2 -// [http2]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04 -// [http2-connheader]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-3.5 -// [http2-stream]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-5 -// [http2-streamstate]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-5.1 -// [node]: http://nodejs.org/ -// [node-stream]: http://nodejs.org/api/stream.html -// [node-https]: http://nodejs.org/api/https.html -// [node-http]: http://nodejs.org/api/http.html - -module.exports = require('./http'); - -/* - API user - - | ^ - | | - +---------------|------------|--------------------------------------------------------+ - | | | Server/Agent | - | v | | - | +----------+ +----------+ | - | | Outgoing | | Incoming | | - | | req/res. | | req/res. | | - | +----------+ +----------+ | - | | ^ | - | +-----------|------------|---------------------------------------+ +----- | - | | | | Endpoint | | | - | | | | | | | - | | +-------|------------|-----------------------------------+ | | | - | | | | | Connection | | | | - | | | v | | | | | - | | | +-----------------------+ +-------------------- | | | | - | | | | Stream | | Stream ... | | | | - | | | +-----------------------+ +-------------------- | | | | - | | | | ^ | ^ | | | | - | | | v | v | | | | | - | | | +------------+--+--------+--+------------+- ... | | | | - | | | | ^ | | | | - | | | | | | | | ... | - | | +-----------------------|--------|-----------------------+ | | | - | | | | | | | - | | v | | | | - | | +--------------------------+ +--------------------------+ | | | - | | | Compressor | | Decompressor | | | | - | | +--------------------------+ +--------------------------+ | | | - | | | ^ | | | - | | v | | | | - | | +--------------------------+ +--------------------------+ | | | - | | | Serializer | | Deserializer | | | | - | | +--------------------------+ +--------------------------+ | | | - | | | ^ | | | - | +---------------------------|--------|---------------------------+ +----- | - | | | | - | v | | - | +----------------------------------------------------------------+ +----- | - | | TCP stream | | ... | - | +----------------------------------------------------------------+ +----- | - | | - +-------------------------------------------------------------------------------------+ - -*/ diff --git a/lib/stream.js b/lib/stream.js deleted file mode 100644 index e5b72110..00000000 --- a/lib/stream.js +++ /dev/null @@ -1,615 +0,0 @@ -var assert = require('assert'); - -// The Stream class -// ================ - -// Stream is a [Duplex stream](http://nodejs.org/api/stream.html#stream_class_stream_duplex) -// subclass that implements the [HTTP/2 Stream](http://http2.github.io/http2-spec/#rfc.section.3.4) -// concept. It has two 'sides': one that is used by the user to send/receive data (the `stream` -// object itself) and one that is used by a Connection to read/write frames to/from the other peer -// (`stream.upstream`). - -var Duplex = require('stream').Duplex; - -exports.Stream = Stream; - -// Public API -// ---------- - -// * **new Stream(log)**: create a new Stream -// -// * **Event: 'headers' (headers)**: signals incoming headers -// -// * **Event: 'promise' (stream, headers)**: signals an incoming push promise -// -// * **Event: 'priority' (priority)**: signals a priority change. `priority` is a number between 0 -// (highest priority) and 2^31-1 (lowest priority). Default value is 2^30. -// -// * **Event: 'error' (type)**: signals an error -// -// * **headers(headers)**: send headers -// -// * **promise(headers): Stream**: promise a stream -// -// * **priority(priority)**: set the priority of the stream. Priority can be changed by the peer -// too, but once it is set locally, it can not be changed remotely. -// -// * **reset(error)**: reset the stream with an error code -// -// * **upstream**: a [Flow](flow.js) that is used by the parent connection to write/read frames -// that are to be sent/arrived to/from the peer and are related to this stream. -// -// Headers are always in the [regular node.js header format][1]. -// [1]: http://nodejs.org/api/http.html#http_message_headers - -// Constructor -// ----------- - -// The main aspects of managing the stream are: -function Stream(log) { - Duplex.call(this); - - // * logging - this._log = log.child({ component: 'stream', s: this }); - - // * receiving and sending stream management commands - this._initializeManagement(); - - // * sending and receiving frames to/from the upstream connection - this._initializeDataFlow(); - - // * maintaining the state of the stream (idle, open, closed, etc.) and error detection - this._initializeState(); -} - -Stream.prototype = Object.create(Duplex.prototype, { constructor: { value: Stream } }); - -// Managing the stream -// ------------------- - -// the default stream priority is 2^30 -var DEFAULT_PRIORITY = Math.pow(2, 30); -var MAX_PRIORITY = Math.pow(2, 31) - 1; - -// PUSH_PROMISE and HEADERS are forwarded to the user through events. -Stream.prototype._initializeManagement = function _initializeManagement() { - this._resetSent = false; - this._priority = DEFAULT_PRIORITY; - this._letPeerPrioritize = true; -}; - -Stream.prototype.promise = function promise(headers) { - var stream = new Stream(this._log); - stream._priority = Math.min(this._priority + 1, MAX_PRIORITY); - this._pushUpstream({ - type: 'PUSH_PROMISE', - flags: {}, - stream: this.id, - promised_stream: stream, - headers: headers - }); - return stream; -}; - -Stream.prototype._onPromise = function _onPromise(frame) { - this.emit('promise', frame.promised_stream, frame.headers); -}; - -Stream.prototype.headers = function headers(headers) { - this._pushUpstream({ - type: 'HEADERS', - flags: {}, - stream: this.id, - headers: headers - }); -}; - -Stream.prototype._onHeaders = function _onHeaders(frame) { - if (frame.priority !== undefined) { - this.priority(frame.priority, true); - } - this.emit('headers', frame.headers); -}; - -Stream.prototype.priority = function priority(priority, peer) { - if ((peer && this._letPeerPrioritize) || !peer) { - if (!peer) { - this._letPeerPrioritize = false; - - var lastFrame = this.upstream.getLastQueuedFrame(); - if (lastFrame && ((lastFrame.type === 'HEADERS') || (lastFrame.type === 'PRIORITY'))) { - lastFrame.priority = priority; - } else { - this._pushUpstream({ - type: 'PRIORITY', - flags: {}, - stream: this.id, - priority: priority - }); - } - } - - this._log.debug({ priority: priority }, 'Changing priority'); - this.emit('priority', priority); - this._priority = priority; - } -}; - -Stream.prototype._onPriority = function _onPriority(frame) { - this.priority(frame.priority, true); -}; - -// Resetting the stream. Normally, an endpoint SHOULD NOT send more than one RST_STREAM frame for -// any stream. -Stream.prototype.reset = function reset(error) { - if (!this._resetSent) { - this._resetSent = true; - this._pushUpstream({ - type: 'RST_STREAM', - flags: {}, - stream: this.id, - error: error - }); - } -}; - -// Data flow -// --------- - -// The incoming and the generated outgoing frames are received/transmitted on the `this.upsteam` -// [Flow](flow.html). The [Connection](connection.html) object instantiating the stream will read -// and write frames to/from it. The stream itself is a regular [Duplex stream][1], and is used by -// the user to write or read the body of the request. -// [1]: http://nodejs.org/api/stream.html#stream_class_stream_duplex - -// upstream side stream user side -// -// +------------------------------------+ -// | | -// +------------------+ | -// | upstream | | -// | | | -// +--+ | +--| -// read() | | _send() | _write() | | write(buf) -// <--------------|B |<--------------|--------------| B|<------------ -// | | | | | -// frames +--+ | +--| buffers -// | | | | | -// -------------->|B |---------------|------------->| B|------------> -// write(frame) | | _receive() | _read() | | read() -// +--+ | +--| -// | | | -// | | | -// +------------------+ | -// | | -// +------------------------------------+ -// -// B: input or output buffer - -var Flow = require('./flow').Flow; - -Stream.prototype._initializeDataFlow = function _initializeDataFlow() { - this.id = undefined; - - this._ended = false; - - this.upstream = new Flow(); - this.upstream._log = this._log; - this.upstream._send = this._send.bind(this); - this.upstream._receive = this._receive.bind(this); - this.upstream.write = this._writeUpstream.bind(this); - this.upstream.on('error', this.emit.bind(this, 'error')); - - this.on('finish', this._finishing); -}; - -Stream.prototype._pushUpstream = function _pushUpstream(frame) { - this.upstream.push(frame); - this._transition(true, frame); -}; - -// Overriding the upstream's `write` allows us to act immediately instead of waiting for the input -// queue to empty. This is important in case of control frames. -Stream.prototype._writeUpstream = function _writeUpstream(frame) { - this._log.debug({ frame: frame }, 'Receiving frame'); - - var moreNeeded = Flow.prototype.write.call(this.upstream, frame); - - // * Transition to a new state if that's the effect of receiving the frame - this._transition(false, frame); - - // * If it's a control frame. Call the appropriate handler method. - if (frame.type === 'HEADERS') { - this._onHeaders(frame); - } else if (frame.type === 'PUSH_PROMISE') { - this._onPromise(frame); - } else if (frame.type === 'PRIORITY') { - this._onPriority(frame); - } - - // * If it's an invalid stream level frame, emit error - else if ((frame.type !== 'DATA') && - (frame.type !== 'WINDOW_UPDATE') && - (frame.type !== 'RST_STREAM')) { - this._log.error({ frame: frame }, 'Invalid stream level frame'); - this.emit('error', 'PROTOCOL_ERROR'); - } - - return moreNeeded; -}; - -// The `_receive` method (= `upstream._receive`) gets called when there's an incoming frame. -Stream.prototype._receive = function _receive(frame, ready) { - // * If it's a DATA frame, then push the payload into the output buffer on the other side. - // Call ready when the other side is ready to receive more. - if (!this._ended && (frame.type === 'DATA')) { - var moreNeeded = this.push(frame.data); - if (!moreNeeded) { - this._receiveMore = ready; - } - } - - // * Any frame may signal the end of the stream with the END_STREAM flag - if (!this._ended && (frame.flags.END_STREAM || (frame.type === 'RST_STREAM'))) { - this.push(null); - this._ended = true; - } - - // * Postpone calling `ready` if `push()` returned a falsy value - if (this._receiveMore !== ready) { - ready(); - } -}; - -// The `_read` method is called when the user side is ready to receive more data. If there's a -// pending write on the upstream, then call its pending ready callback to receive more frames. -Stream.prototype._read = function _read() { - if (this._receiveMore) { - var receiveMore = this._receiveMore; - delete this._receiveMore; - receiveMore(); - } -}; - -// The `write` method gets called when there's a write request from the user. -Stream.prototype._write = function _write(buffer, encoding, ready) { - // * Chunking is done by the upstream Flow. - var moreNeeded = this._pushUpstream({ - type: 'DATA', - flags: {}, - stream: this.id, - data: buffer - }); - - // * Call ready when upstream is ready to receive more frames. - if (moreNeeded) { - ready(); - } else { - this._sendMore = ready; - } -}; - -// The `_send` (= `upstream._send`) method is called when upstream is ready to receive more frames. -// If there's a pending write on the user side, then call its pending ready callback to receive more -// writes. -Stream.prototype._send = function _send() { - if (this._sendMore) { - var sendMore = this._sendMore; - delete this._sendMore; - sendMore(); - } -}; - -// When the stream is finishing (the user calls `end()` on it), then we have to set the `END_STREAM` -// flag on the last frame. If there's no frame in the queue, or if it doesn't support this flag, -// then we create a 0 length DATA frame. We could do this all the time, but putting the flag on an -// existing frame is a nice optimization. -var emptyBuffer = new Buffer(0); -Stream.prototype._finishing = function _finishing() { - var endFrame = { - type: 'DATA', - flags: { END_STREAM: true }, - stream: this.id, - data: emptyBuffer - }; - var lastFrame = this.upstream.getLastQueuedFrame(); - if (lastFrame && ((lastFrame.type === 'DATA') || (lastFrame.type === 'HEADERS'))) { - this._log.debug({ frame: lastFrame }, 'Marking last frame with END_STREAM flag.'); - lastFrame.flags.END_STREAM = true; - this._transition(true, endFrame); - } else { - this._pushUpstream(endFrame); - } -}; - -// [Stream States](http://tools.ietf.org/id/draft-unicorn-httpbis-http2-01.html#StreamStates) -// ---------------- -// -// +--------+ -// PP | | PP -// ,--------| idle |--------. -// / | | \ -// v +--------+ v -// +----------+ | +----------+ -// | | | H | | -// ,---| reserved | | | reserved |---. -// | | (local) | v | (remote) | | -// | +----------+ +--------+ +----------+ | -// | | ES | | ES | | -// | | H ,-------| open |-------. | H | -// | | / | | \ | | -// | v v +--------+ v v | -// | +----------+ | +----------+ | -// | | half | | | half | | -// | | closed | | R | closed | | -// | | (remote) | | | (local) | | -// | +----------+ | +----------+ | -// | | v | | -// | | ES / R +--------+ ES / R | | -// | `----------->| |<-----------' | -// | R | closed | R | -// `-------------------->| |<--------------------' -// +--------+ - -// Streams begin in the IDLE state and transitions happen when there's an incoming or outgoing frame -Stream.prototype._initializeState = function _initializeState() { - this.state = 'IDLE'; - this._initiated = undefined; - this._closedByUs = undefined; - this._closedWithRst = undefined; -}; - -// Only `_setState` should change `this.state` directly. It also logs the state change and notifies -// interested parties using the 'state' event. -Stream.prototype._setState = function transition(state) { - assert(this.state !== state); - this._log.debug({ from: this.state, to: state }, 'State transition'); - this.state = state; - this.emit('state', state); -}; - -// A state is 'active' if the stream in that state counts towards the concurrency limit. Streams -// that are in the "open" state, or either of the "half closed" states count toward this limit. -function activeState(state) { - return ((state === 'HALF_CLOSED_LOCAL') || (state === 'HALF_CLOSED_REMOTE') || (state === 'OPEN')); -} - -// `_transition` is called every time there's an incoming or outgoing frame. It manages state -// transitions, and detects stream errors. A stream error is always caused by a frame that is not -// allowed in the current state. -Stream.prototype._transition = function transition(sending, frame) { - var receiving = !sending; - var error = undefined; - - var DATA = false, HEADERS = false, PRIORITY = false; - var RST_STREAM = false, PUSH_PROMISE = false, WINDOW_UPDATE = false; - switch(frame.type) { - case 'DATA' : DATA = true; break; - case 'HEADERS' : HEADERS = true; break; - case 'PRIORITY' : PRIORITY = true; break; - case 'RST_STREAM' : RST_STREAM = true; break; - case 'PUSH_PROMISE' : PUSH_PROMISE = true; break; - case 'WINDOW_UPDATE': WINDOW_UPDATE = true; break; - } - - var previousState = this.state; - - switch (this.state) { - // All streams start in the **idle** state. In this state, no frames have been exchanged. - // - // * Sending or receiving a HEADERS frame causes the stream to become "open". - // - // When the HEADERS frame contains the END_STREAM flags, then two state transitions happen. - case 'IDLE': - if (HEADERS) { - this._setState('OPEN'); - if (frame.flags.END_STREAM) { - this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE'); - } - this._initiated = sending; - } else if (sending && RST_STREAM) { - this._setState('CLOSED'); - } else { - error = 'PROTOCOL_ERROR'; - } - break; - - // A stream in the **reserved (local)** state is one that has been promised by sending a - // PUSH_PROMISE frame. - // - // * The endpoint can send a HEADERS frame. This causes the stream to open in a "half closed - // (remote)" state. - // * Either endpoint can send a RST_STREAM frame to cause the stream to become "closed". This - // releases the stream reservation. - // * An endpoint may receive PRIORITY frame in this state. - // * An endpoint MUST NOT send any other type of frame in this state. - case 'RESERVED_LOCAL': - if (sending && HEADERS) { - this._setState('HALF_CLOSED_REMOTE'); - } else if (RST_STREAM) { - this._setState('CLOSED'); - } else if (receiving && PRIORITY) { - /* No state change */ - } else { - error = 'PROTOCOL_ERROR'; - } - break; - - // A stream in the **reserved (remote)** state has been reserved by a remote peer. - // - // * Either endpoint can send a RST_STREAM frame to cause the stream to become "closed". This - // releases the stream reservation. - // * Receiving a HEADERS frame causes the stream to transition to "half closed (local)". - // * An endpoint MAY send PRIORITY frames in this state to reprioritize the stream. - // * Receiving any other type of frame MUST be treated as a stream error of type PROTOCOL_ERROR. - case 'RESERVED_REMOTE': - if (RST_STREAM) { - this._setState('CLOSED'); - } else if (receiving && HEADERS) { - this._setState('HALF_CLOSED_LOCAL'); - } else if (sending && PRIORITY) { - /* No state change */ - } else { - error = 'PROTOCOL_ERROR'; - } - break; - - // The **open** state is where both peers can send frames. In this state, sending peers observe - // advertised stream level flow control limits. - // - // * From this state either endpoint can send a frame with a END_STREAM flag set, which causes - // the stream to transition into one of the "half closed" states: an endpoint sending a - // END_STREAM flag causes the stream state to become "half closed (local)"; an endpoint - // receiving a END_STREAM flag causes the stream state to become "half closed (remote)". - // * Either endpoint can send a RST_STREAM frame from this state, causing it to transition - // immediately to "closed". - case 'OPEN': - if (frame.flags.END_STREAM) { - this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE'); - } else if (RST_STREAM) { - this._setState('CLOSED'); - } else { - /* No state change */ - } - break; - - // A stream that is **half closed (local)** cannot be used for sending frames. - // - // * A stream transitions from this state to "closed" when a frame that contains a END_STREAM - // flag is received, or when either peer sends a RST_STREAM frame. - // * An endpoint MAY send or receive PRIORITY frames in this state to reprioritize the stream. - // * WINDOW_UPDATE can be sent by a peer that has sent a frame bearing the END_STREAM flag. - case 'HALF_CLOSED_LOCAL': - if (RST_STREAM || (receiving && frame.flags.END_STREAM)) { - this._setState('CLOSED'); - } else if (receiving || (sending && (PRIORITY || WINDOW_UPDATE))) { - /* No state change */ - } else { - error = 'PROTOCOL_ERROR'; - } - break; - - // A stream that is **half closed (remote)** is no longer being used by the peer to send frames. - // In this state, an endpoint is no longer obligated to maintain a receiver flow control window - // if it performs flow control. - // - // * If an endpoint receives additional frames for a stream that is in this state it MUST - // respond with a stream error of type STREAM_CLOSED. - // * A stream can transition from this state to "closed" by sending a frame that contains a - // END_STREAM flag, or when either peer sends a RST_STREAM frame. - // * An endpoint MAY send or receive PRIORITY frames in this state to reprioritize the stream. - // * A receiver MAY receive a WINDOW_UPDATE frame on a "half closed (remote)" stream. - case 'HALF_CLOSED_REMOTE': - if (RST_STREAM || (sending && frame.flags.END_STREAM)) { - this._setState('CLOSED'); - } else if (sending || (receiving && (WINDOW_UPDATE || PRIORITY))) { - /* No state change */ - } else { - error = 'PROTOCOL_ERROR'; - } - break; - - // The **closed** state is the terminal state. - // - // * An endpoint MUST NOT send frames on a closed stream. An endpoint that receives a frame - // after receiving a RST_STREAM or a frame containing a END_STREAM flag on that stream MUST - // treat that as a stream error of type STREAM_CLOSED. - // * WINDOW_UPDATE, PRIORITY or RST_STREAM frames can be received in this state for a short - // period after a frame containing an END_STREAM flag is sent. Until the remote peer receives - // and processes the frame bearing the END_STREAM flag, it might send either frame type. - // Endpoints MUST ignore WINDOW_UPDATE frames received in this state, though endpoints MAY - // choose to treat WINDOW_UPDATE frames that arrive a significant time after sending - // END_STREAM as a connection error of type PROTOCOL_ERROR. - // * If this state is reached as a result of sending a RST_STREAM frame, the peer that receives - // the RST_STREAM might have already sent - or enqueued for sending - frames on the stream - // that cannot be withdrawn. An endpoint that sends a RST_STREAM frame MUST ignore frames that - // it receives on closed streams after it has sent a RST_STREAM frame. An endpoint MAY choose - // to limit the period over which it ignores frames and treat frames that arrive after this - // time as being in error. - // * An endpoint might receive a PUSH_PROMISE frame after it sends RST_STREAM. PUSH_PROMISE - // causes a stream to become "reserved". If promised streams are not desired, a RST_STREAM - // can be used to close any of those streams. - case 'CLOSED': - if ((sending && RST_STREAM) || - (receiving && this._closedByUs && - (this._closedWithRst || WINDOW_UPDATE || PRIORITY || RST_STREAM))) { - /* No state change */ - } else { - error = 'STREAM_CLOSED'; - } - break; - } - - // Noting that the connection was closed by the other endpoint. It may be important in edge cases. - // For example, when the peer tries to cancel a promised stream, but we already sent every data - // on it, then the stream is in CLOSED state, yet we want to ignore the incoming RST_STREAM. - if ((this.state === 'CLOSED') && (previousState !== 'CLOSED')) { - this._closedByUs = sending; - this._closedWithRst = RST_STREAM; - } - - // Sending/receiving a PUSH_PROMISE - // - // * Sending a PUSH_PROMISE frame marks the associated stream for later use. The stream state - // for the reserved stream transitions to "reserved (local)". - // * Receiving a PUSH_PROMISE frame marks the associated stream as reserved by the remote peer. - // The state of the stream becomes "reserved (remote)". - if (PUSH_PROMISE && !error) { - /* This assertion must hold, because _transition is called immediately when a frame is written - to the stream. If it would be called when a frame gets out of the input queue, the state - of the reserved could have been changed by then. */ - assert(frame.promised_stream.state === 'IDLE', frame.promised_stream.state); - frame.promised_stream._setState(sending ? 'RESERVED_LOCAL' : 'RESERVED_REMOTE'); - frame.promised_stream._initiated = sending; - } - - // Signaling how sending/receiving this frame changes the active stream count (-1, 0 or +1) - if (this._initiated) { - var change = (activeState(this.state) - activeState(previousState)); - if (sending) { - frame.count_change = change; - } else { - frame.count_change(change); - } - } else if (sending) { - frame.count_change = 0; - } - - // Common error handling. - if (error) { - var info = { - error: error, - frame: frame, - state: this.state, - closedByUs: this._closedByUs, - closedWithRst: this._closedWithRst - }; - - // * When sending something invalid, throwing an exception, since it is probably a bug. - if (sending) { - this._log.error(info, 'Sending illegal frame.'); - throw new Error('Sending illegal frame (' + frame.type + ') in ' + this.state + ' state.'); - } - - // * When receiving something invalid, sending an RST_STREAM using the `reset` method. - // This will automatically cause a transition to the CLOSED state. - else { - this._log.error(info, 'Received illegal frame.'); - this.emit('error', error); - } - } -}; - -// Bunyan serializers -// ------------------ - -exports.serializers = {}; - -var nextId = 0; -exports.serializers.s = function(stream) { - if (!('_id' in stream)) { - stream._id = nextId; - nextId += 1; - } - return stream._id; -}; diff --git a/package.json b/package.json deleted file mode 100644 index a8ccb803..00000000 --- a/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "http2", - "version": "1.0.1", - "description": "An HTTP/2 client and server implementation", - "main": "lib/index.js", - "engines" : { - "node" : ">=0.10.0" - }, - "devDependencies": { - "istanbul": "*", - "chai": "*", - "mocha": "*", - "docco": "*", - "bunyan": "*" - }, - "scripts": { - "test": "istanbul test _mocha -- --reporter spec --slow 200", - "prepublish": "docco lib/* --output doc --layout parallel --css doc/docco.css" - }, - "repository": { - "type": "git", - "url": "git://github.com/molnarg/node-http2.git" - }, - "homepage": "https://github.com/molnarg/node-http2", - "bugs": { - "url": "https://github.com/molnarg/node-http2/issues" - }, - "keywords": [ - "http", - "http2", - "client", - "server" - ], - "author": "Gábor Molnár (http://gabor.molnar.es)", - "contributors": [ - "Nick Hurley", - "Mike Belshe" - ], - "license": "MIT", - "readmeFilename": "README.md" -} diff --git a/test/compressor.js b/test/compressor.js deleted file mode 100644 index 1f168d49..00000000 --- a/test/compressor.js +++ /dev/null @@ -1,259 +0,0 @@ -var expect = require('chai').expect; -var util = require('./util'); - -var compressor = require('../lib/compressor'); -var HeaderTable = compressor.HeaderTable; -var HeaderSetCompressor = compressor.HeaderSetCompressor; -var HeaderSetDecompressor = compressor.HeaderSetDecompressor; -var Compressor = compressor.Compressor; -var Decompressor = compressor.Decompressor; - -var test_integers = [{ - N: 5, - I: 10, - buffer: new Buffer([10]) -}, { - N: 0, - I: 10, - buffer: new Buffer([10]) -}, { - N: 5, - I: 1337, - buffer: new Buffer([31, 128 + 26, 10]) -}, { - N: 0, - I: 1337, - buffer: new Buffer([128 + 57, 10]) -}]; - -var test_strings = [{ - string: 'abcdefghij', - buffer: new Buffer('0A6162636465666768696A', 'hex') -}, { - string: 'éáűőúöüó€', - buffer: new Buffer('13C3A9C3A1C5B1C591C3BAC3B6C3BCC3B3E282AC', 'hex') -}]; - -var test_headers = [{ - header: { - name: 3, - value: '/my-example/index.html', - index: Infinity - }, - buffer: new Buffer('44' + '162F6D792D6578616D706C652F696E6465782E68746D6C', 'hex') -}, { - header: { - name: 11, - value: 'my-user-agent', - index: Infinity - }, - buffer: new Buffer('4C' + '0D6D792D757365722D6167656E74', 'hex') -}, { - header: { - name: 'x-my-header', - value: 'first', - index: Infinity - }, - buffer: new Buffer('40' + '0B782D6D792D686561646572' + '056669727374', 'hex') -}, { - header: { - name: 30, - value: 30, - index: -1 - }, - buffer: new Buffer('9e', 'hex') -}, { - header: { - name: 32, - value: 32, - index: -1 - }, - buffer: new Buffer('a0', 'hex') -}, { - header: { - name: 3, - value: '/my-example/resources/script.js', - index: 30 - }, - buffer: new Buffer('041e' + '1F2F6D792D6578616D706C652F7265736F75726365732F7363726970742E6A73', 'hex') -}, { - header: { - name: 32, - value: 'second', - index: Infinity - }, - buffer: new Buffer('5F02' + '067365636F6E64', 'hex') -}, { - header: { - name: 32, - value: 'third', - index: -1 - }, - buffer: new Buffer('7F02' + '057468697264', 'hex') -}]; - -var test_header_sets = [{ - headers: { - ':path': '/my-example/index.html', - 'user-agent': 'my-user-agent', - 'x-my-header': 'first' - }, - buffer: util.concat(test_headers.slice(0, 3).map(function(test) { return test.buffer; })) -}, { - headers: { - ':path': '/my-example/resources/script.js', - 'user-agent': 'my-user-agent', - 'x-my-header': 'second' - }, - buffer: util.concat(test_headers.slice(3, 7).map(function(test) { return test.buffer; })) -}, { - headers: { - ':path': '/my-example/resources/script.js', - 'user-agent': 'my-user-agent', - 'x-my-header': ['third', 'second'] - }, - buffer: test_headers[7].buffer -}, { - headers: { - ':status': '200', - 'user-agent': 'my-user-agent', - 'cookie': ['first', 'second', 'third', 'third'], - 'verylong': (new Buffer(9000)).toString('hex') - } -}]; - -describe('compressor.js', function() { - describe('HeaderTable', function() { - }); - - describe('HeaderSetCompressor', function() { - describe('static method .integer(I, N)', function() { - it('should return an array of buffers that represent the N-prefix coded form of the integer I', function() { - for (var i = 0; i < test_strings.length; i++) { - var test = test_strings[i]; - expect(util.concat(HeaderSetCompressor.string(test.string))).to.deep.equal(test.buffer); - } - }); - }); - describe('static method .string(string)', function() { - it('should return an array of buffers that represent the encoded form of the string', function() { - for (var i = 0; i < test_strings.length; i++) { - var test = test_strings[i]; - expect(util.concat(HeaderSetCompressor.string(test.string))).to.deep.equal(test.buffer); - } - }); - }); - describe('static method .header({ name, value, indexing, substitution })', function() { - it('should return an array of buffers that represent the encoded form of the header', function() { - for (var i = 0; i < test_headers.length; i++) { - var test = test_headers[i]; - expect(util.concat(HeaderSetCompressor.header(test.header))).to.deep.equal(test.buffer); - } - }); - }); - }); - - describe('HeaderSetDecompressor', function() { - describe('static method .integer(buffer, N)', function() { - it('should return the parsed N-prefix coded number and increase the cursor property of buffer', function() { - for (var i = 0; i < test_integers.length; i++) { - var test = test_integers[i]; - test.buffer.cursor = 0; - expect(HeaderSetDecompressor.integer(test.buffer, test.N)).to.equal(test.I); - expect(test.buffer.cursor).to.equal(test.buffer.length); - } - }); - }); - describe('static method .string(buffer)', function() { - it('should return the parsed string and increase the cursor property of buffer', function() { - for (var i = 0; i < test_strings.length; i++) { - var test = test_strings[i]; - test.buffer.cursor = 0; - expect(HeaderSetDecompressor.string(test.buffer)).to.equal(test.string); - expect(test.buffer.cursor).to.equal(test.buffer.length); - } - }); - }); - describe('static method .header(buffer)', function() { - it('should return the parsed header and increase the cursor property of buffer', function() { - for (var i = 0; i < test_headers.length; i++) { - var test = test_headers[i]; - test.buffer.cursor = 0; - expect(HeaderSetDecompressor.header(test.buffer)).to.deep.equal(test.header); - expect(test.buffer.cursor).to.equal(test.buffer.length); - } - }); - }); - }); - describe('Decompressor', function() { - describe('method decompress(buffer)', function() { - it('should return the parsed header set in { name1: value1, name2: [value2, value3], ... } format', function() { - var decompressor = new Decompressor(util.log, 'REQUEST'); - var header_set = test_header_sets[0]; - expect(decompressor.decompress(header_set.buffer)).to.deep.equal(header_set.headers); - header_set = test_header_sets[1]; - expect(decompressor.decompress(header_set.buffer)).to.deep.equal(header_set.headers); - header_set = test_header_sets[2]; - expect(decompressor.decompress(header_set.buffer)).to.deep.equal(header_set.headers); - }); - }); - describe('transform stream', function() { - it('should emit an error event if a series of header frames is interleaved with other frames', function() { - var decompressor = new Decompressor(util.log, 'REQUEST'); - var error_occured = false; - decompressor.on('error', function() { - error_occured = true; - }); - decompressor.write({ - type: 'HEADERS', - flags: { - END_HEADERS: false - }, - data: new Buffer(5) - }); - decompressor.write({ - type: 'DATA', - flags: {}, - data: new Buffer(5) - }); - expect(error_occured).to.be.equal(true); - }); - }); - }); - - describe('invariant', function() { - describe('decompressor.decompress(compressor.compress(headerset)) === headerset', function() { - it('should be true for any header set if the states are synchronized', function() { - var compressor = new Compressor(util.log, 'REQUEST'); - var decompressor = new Decompressor(util.log, 'REQUEST'); - for (var i = 0; i < 10; i++) { - var headers = test_header_sets[i%4].headers; - var compressed = compressor.compress(headers); - var decompressed = decompressor.decompress(compressed); - expect(decompressed).to.deep.equal(headers); - expect(compressor._table).to.deep.equal(decompressor._table); - } - }); - }); - describe('source.pipe(compressor).pipe(decompressor).pipe(destination)', function() { - it('should behave like source.pipe(destination) for a stream of frames', function(done) { - var compressor = new Compressor(util.log, 'RESPONSE'); - var decompressor = new Decompressor(util.log, 'RESPONSE'); - compressor.pipe(decompressor); - for (var i = 0; i < 10; i++) { - compressor.write({ - type: i%2 ? 'HEADERS' : 'PUSH_PROMISE', - flags: {}, - headers: test_header_sets[i%4].headers - }); - } - setTimeout(function() { - for (var j = 0; j < 10; j++) { - expect(decompressor.read().headers).to.deep.equal(test_header_sets[j%4].headers); - } - done(); - }, 10); - }); - }); - }); -}); diff --git a/test/connection.js b/test/connection.js deleted file mode 100644 index 90ab5fc3..00000000 --- a/test/connection.js +++ /dev/null @@ -1,280 +0,0 @@ -var expect = require('chai').expect; -var util = require('./util'); - -var Connection = require('../lib/connection').Connection; - -var settings = { - SETTINGS_MAX_CONCURRENT_STREAMS: 100, - SETTINGS_INITIAL_WINDOW_SIZE: 100000 -}; - -var MAX_PRIORITY = Math.pow(2, 31) - 1; -var MAX_RANDOM_PRIORITY = 10; - -function randomPriority() { - return Math.floor(Math.random() * (MAX_RANDOM_PRIORITY + 1)); -} - -function expectPriorityOrder(priorities) { - priorities.forEach(function(bucket, priority) { - bucket.forEach(function(stream) { - expect(stream._priority).to.be.equal(priority); - }); - }); -} - -describe('connection.js', function() { - describe('Connection class', function() { - describe('method ._insert(stream)', function() { - it('should insert the stream in _streamPriorities in a place determined by stream._priority', function() { - var streams = []; - var connection = Object.create(Connection.prototype, { _streamPriorities: { value: streams }}); - var streamCount = 10; - - for (var i = 0; i < streamCount; i++) { - var stream = { _priority: randomPriority() }; - connection._insert(stream, stream._priority); - expect(connection._streamPriorities[stream._priority]).to.include(stream); - } - - expectPriorityOrder(connection._streamPriorities); - }); - }); - describe('method ._reprioritize(stream)', function() { - it('should eject and then insert the stream in _streamPriorities in a place determined by stream._priority', function() { - var streams = []; - var connection = Object.create(Connection.prototype, { _streamPriorities: { value: streams }}); - var streamCount = 10; - var oldPriority, newPriority, stream; - - for (var i = 0; i < streamCount; i++) { - oldPriority = randomPriority(); - while ((newPriority = randomPriority()) === oldPriority); - stream = { _priority: oldPriority }; - connection._insert(stream, oldPriority); - connection._reprioritize(stream, newPriority); - stream._priority = newPriority; - - expect(connection._streamPriorities[newPriority]).to.include(stream); - expect(connection._streamPriorities[oldPriority] || []).to.not.include(stream); - } - - expectPriorityOrder(streams); - }); - }); - describe('invalid operation', function() { - describe('disabling and the re-enabling flow control', function() { - it('should result in an error event with type "FLOW_CONTROL_ERROR"', function(done) { - var connection = new Connection(util.log, 1, settings); - - connection.on('error', function(error) { - expect(error).to.equal('FLOW_CONTROL_ERROR'); - done(); - }); - - connection._setLocalFlowControl(true); - connection._setLocalFlowControl(false); - }); - }); - describe('manipulating flow control window after flow control was turned off', function() { - it('should result in an error event with type "FLOW_CONTROL_ERROR"', function(done) { - var connection = new Connection(util.log, 1, settings); - - connection.on('error', function(error) { - expect(error).to.equal('FLOW_CONTROL_ERROR'); - done(); - }); - - connection._setLocalFlowControl(true); - connection._setInitialStreamWindowSize(10); - }); - }); - describe('disabling flow control twice', function() { - it('should be ignored', function() { - var connection = new Connection(util.log, 1, settings); - - connection._setLocalFlowControl(true); - connection._setLocalFlowControl(true); - }); - }); - describe('enabling flow control when already enabled', function() { - it('should be ignored', function() { - var connection = new Connection(util.log, 1, settings); - - connection._setLocalFlowControl(false); - }); - }); - describe('unsolicited ping answer', function() { - it('should be ignored', function() { - var connection = new Connection(util.log, 1, settings); - - connection._receivePing({ - stream: 0, - type: 'PING', - flags: { - 'PONG': true - }, - data: new Buffer(8) - }); - }); - }); - }); - }); - describe('test scenario', function() { - var c, s; - beforeEach(function() { - c = new Connection(util.log.child({ role: 'client' }), 1, settings); - s = new Connection(util.log.child({ role: 'client' }), 2, settings); - c.pipe(s).pipe(c); - }); - - describe('connection setup', function() { - it('should work as expected', function(done) { - setTimeout(function() { - // If there are no exception until this, then we're done - done(); - }, 10); - }); - }); - describe('sending/receiving a request', function() { - it('should work as expected', function(done) { - // Request and response data - var request_headers = { - ':method': 'GET', - ':path': '/' - }; - var request_data = new Buffer(0); - var response_headers = { - ':status': '200' - }; - var response_data = new Buffer('12345678', 'hex'); - - // Setting up server - s.on('stream', function(server_stream) { - server_stream.on('headers', function(headers) { - expect(headers).to.deep.equal(request_headers); - server_stream.headers(response_headers); - server_stream.end(response_data); - }); - }); - - // Sending request - var client_stream = c.createStream(); - client_stream.headers(request_headers); - client_stream.end(request_data); - - // Waiting for answer - done = util.callNTimes(2, done); - client_stream.on('headers', function(headers) { - expect(headers).to.deep.equal(response_headers); - done(); - }); - client_stream.on('readable', function() { - expect(client_stream.read()).to.deep.equal(response_data); - done(); - }); - }); - }); - describe('server push', function() { - it('should work as expected', function(done) { - var request_headers = { ':method': 'get', ':path': '/' }; - var response_headers = { ':status': '200' }; - var push_request_headers = { ':method': 'get', ':path': '/x' }; - var push_response_headers = { ':status': '200' }; - var response_content = new Buffer(10); - var push_content = new Buffer(10); - - done = util.callNTimes(5, done); - - s.on('stream', function(response) { - response.headers(response_headers); - - var pushed = response.promise(push_request_headers); - pushed.headers(push_response_headers); - pushed.end(push_content); - - response.end(response_content); - }); - - var request = c.createStream(); - request.headers(request_headers); - request.end(); - request.on('headers', function(headers) { - expect(headers).to.deep.equal(response_headers); - done(); - }); - request.on('readable', function() { - expect(request.read()).to.deep.equal(response_content); - done(); - }); - request.on('promise', function(pushed, headers) { - expect(headers).to.deep.equal(push_request_headers); - pushed.on('headers', function(headers) { - expect(headers).to.deep.equal(response_headers); - done(); - }); - pushed.on('readable', function() { - expect(pushed.read()).to.deep.equal(push_content); - done(); - }); - pushed.on('end', function() { - done(); - }); - }); - }); - }); - describe('ping from client', function() { - it('should work as expected', function(done) { - c.ping(function() { - done(); - }); - }); - }); - describe('ping from server', function() { - it('should work as expected', function(done) { - s.ping(function() { - done(); - }); - }); - }); - describe('creating two streams and then using them in reverse order', function() { - it('should not result in non-monotonous local ID ordering', function() { - var s1 = c.createStream(); - var s2 = c.createStream(); - s2.headers({ ':method': 'get', ':path': '/' }); - s1.headers({ ':method': 'get', ':path': '/' }); - }); - }); - describe('creating two promises and then using them in reverse order', function() { - it('should not result in non-monotonous local ID ordering', function(done) { - s.on('stream', function(response) { - response.headers({ ':status': '200' }); - - var p1 = s.createStream(); - var p2 = s.createStream(); - response.promise(p2, { ':method': 'get', ':path': '/p2' }); - response.promise(p1, { ':method': 'get', ':path': '/p1' }); - p2.headers({ ':status': '200' }); - p1.headers({ ':status': '200' }); - }); - - var request = c.createStream(); - request.headers({ ':method': 'get', ':path': '/' }); - - done = util.callNTimes(2, done); - request.on('promise', function() { - done(); - }); - }); - }); - describe('closing the connection on one end', function() { - it('should result in closed streams on both ends', function(done) { - done = util.callNTimes(2, done); - c.on('end', done); - s.on('end', done); - - c.close(); - }); - }); - }); -}); diff --git a/test/endpoint.js b/test/endpoint.js deleted file mode 100644 index 79097ff5..00000000 --- a/test/endpoint.js +++ /dev/null @@ -1,41 +0,0 @@ -var expect = require('chai').expect; -var util = require('./util'); - -var endpoint = require('../lib/endpoint'); -var Endpoint = endpoint.Endpoint; - -var settings = { - SETTINGS_MAX_CONCURRENT_STREAMS: 100, - SETTINGS_INITIAL_WINDOW_SIZE: 100000 -}; - -describe('endpoint.js', function() { - describe('scenario', function() { - describe('connection setup', function() { - it('should work as expected', function(done) { - var c = new Endpoint(util.log.child({ role: 'client' }), 'CLIENT', settings); - var s = new Endpoint(util.log.child({ role: 'client' }), 'SERVER', settings); - - util.log.debug('Test initialization over, starting piping.'); - c.pipe(s).pipe(c); - - setTimeout(function() { - // If there are no exception until this, then we're done - done(); - }, 10); - }); - }); - }); - describe('bunyan serializer', function() { - describe('`e`', function() { - var format = endpoint.serializers.e; - it('should assign a unique ID to each endpoint', function() { - var c = new Endpoint(util.log.child({ role: 'client' }), 'CLIENT', settings); - var s = new Endpoint(util.log.child({ role: 'client' }), 'SERVER', settings); - expect(format(c)).to.not.equal(format(s)); - expect(format(c)).to.equal(format(c)); - expect(format(s)).to.equal(format(s)); - }); - }); - }); -}); diff --git a/test/flow.js b/test/flow.js deleted file mode 100644 index ba5c1967..00000000 --- a/test/flow.js +++ /dev/null @@ -1,210 +0,0 @@ -var expect = require('chai').expect; -var util = require('./util'); - -var Flow = require('../lib/flow').Flow; - -function createFlow(log) { - var flowControlId = util.random(10, 100); - var flow = new Flow(flowControlId); - flow._log = util.log.child(log || {}); - return flow; -} - -describe('flow.js', function() { - describe('Flow class', function() { - var flow; - beforeEach(function() { - flow = createFlow(); - }); - - describe('._receive(frame, callback) method', function() { - it('is called when there\'s a frame in the input buffer to be consumed', function(done) { - var frame = { type: 'PRIORITY', flags: {}, priority: 1 }; - flow._receive = function _receive(receivedFrame, callback) { - expect(receivedFrame).to.equal(frame); - callback(); - }; - flow.write(frame, done); - }); - it('has to be overridden by the child class, otherwise it throws', function() { - expect(flow._receive.bind(flow)).to.throw(Error); - }); - }); - describe('._send() method', function() { - it('is called when the output buffer should be filled with more frames and the flow' + - 'control queue is empty', function() { - var sendCalled = 0; - var notFlowControlledFrame = { type: 'PRIORITY', flags: {}, priority: 1 }; - flow._send = function _send() { - sendCalled += 1; - this.push(notFlowControlledFrame); - }; - expect(flow.read()).to.equal(notFlowControlledFrame); - - flow._window = 0; - flow._queue.push({ type: 'DATA', flags: {}, data: { length: 1 } }); - expect(flow.read()).to.equal(null); - - expect(sendCalled).to.equal(1); - }); - it('has to be overridden by the child class, otherwise it throws', function() { - expect(flow._send.bind(flow)).to.throw(Error); - }); - }); - describe('._increaseWindow(size) method', function() { - it('should increase `this._window` by `size`', function() { - flow._send = util.noop; - flow._window = 0; - - var increase1 = util.random(0,100); - var increase2 = util.random(0,100); - flow._increaseWindow(increase1); - flow._increaseWindow(increase2); - expect(flow._window).to.equal(increase1 + increase2); - - flow._increaseWindow(Infinity); - expect(flow._window).to.equal(Infinity); - }); - it('should emit error when increasing with a finite `size` when `_window` is infinite', function() { - flow._send = util.noop; - flow._increaseWindow(Infinity); - var increase = util.random(1,100); - - expect(flow._increaseWindow.bind(flow, increase)).to.throw('Uncaught, unspecified "error" event.'); - }); - it('should emit error when `_window` grows over the window limit', function() { - var WINDOW_SIZE_LIMIT = Math.pow(2, 31) - 1; - flow._send = util.noop; - flow._window = 0; - - flow._increaseWindow(WINDOW_SIZE_LIMIT); - expect(flow._increaseWindow.bind(flow, 1)).to.throw('Uncaught, unspecified "error" event.'); - - }); - }); - describe('.disableLocalFlowControl() method', function() { - it('should increase `this._window` by Infinity', function() { - flow._send = util.noop; - flow.disableLocalFlowControl(); - expect(flow._window).to.equal(Infinity); - }); - }); - describe('.read() method', function() { - describe('when the flow control queue is not empty', function() { - it('should return the first item in the queue if the window is enough', function() { - var priorityFrame = { type: 'PRIORITY', flags: {}, priority: 1 }; - var dataFrame = { type: 'DATA', flags: {}, data: { length: 10 } }; - flow._send = util.noop; - flow._window = 10; - flow._queue = [priorityFrame, dataFrame]; - - expect(flow.read()).to.equal(priorityFrame); - expect(flow.read()).to.equal(dataFrame); - }); - it('should also split DATA frames when needed', function() { - var buffer = new Buffer(10); - var dataFrame = { type: 'DATA', flags: {}, stream: util.random(0, 100), data: buffer }; - flow._send = util.noop; - flow._window = 5; - flow._queue = [dataFrame]; - - var expectedFragment = { flags: {}, type: 'DATA', stream: dataFrame.stream, data: buffer.slice(0,5) }; - expect(flow.read()).to.deep.equal(expectedFragment); - expect(dataFrame.data).to.deep.equal(buffer.slice(5)); - }); - }); - }); - describe('.push(frame) method', function() { - it('should push `frame` into the output queue or the flow control queue', function() { - var priorityFrame = { type: 'PRIORITY', flags: {}, priority: 1 }; - var dataFrame = { type: 'DATA', flags: {}, data: { length: 10 } }; - flow._window = 10; - - flow.push(dataFrame); // output queue - flow.push(dataFrame); // flow control queue, because of depleted window - flow.push(priorityFrame); // flow control queue, because it's not empty - - expect(flow.read()).to.be.equal(dataFrame); - expect(flow._queue[0]).to.be.equal(dataFrame); - expect(flow._queue[1]).to.be.equal(priorityFrame); - }); - }); - describe('.write() method', function() { - it('call with a DATA frame should trigger sending WINDOW_UPDATE if remote flow control is not' + - 'disabled', function(done) { - flow._remoteFlowControlDisabled = false; - flow._window = 100; - flow._send = util.noop; - flow._receive = function(frame, callback) { - callback(); - }; - - var buffer = new Buffer(util.random(10, 100)); - flow.write({ type: 'DATA', flags: {}, data: buffer }); - flow.once('readable', function() { - expect(flow.read()).to.be.deep.equal({ - type: 'WINDOW_UPDATE', - flags: {}, - stream: flow._flowControlId, - window_size: buffer.length - }); - done(); - }); - }); - }); - }); - describe('test scenario', function() { - var flow1, flow2; - beforeEach(function() { - flow1 = createFlow({ flow: 1 }); - flow2 = createFlow({ flow: 2 }); - flow1._flowControlId = flow2._flowControlId; - flow1._remoteFlowControlDisabled = flow2._remoteFlowControlDisabled = false; - flow1._send = flow2._send = util.noop; - flow1._receive = flow2._receive = function(frame, callback) { callback(); }; - }); - - describe('sending a large data stream', function() { - it('should work as expected', function(done) { - // Sender side - var frameNumber = util.random(5, 8); - var input = []; - flow1._send = function _send() { - if (input.length >= frameNumber) { - this.push({ type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(0) }); - this.push(null); - } else { - var buffer = new Buffer(util.random(1000, 100000)); - input.push(buffer); - this.push({ type: 'DATA', flags: {}, data: buffer }); - } - }; - - // Receiver side - var output = []; - flow2._receive = function _receive(frame, callback) { - if (frame.type === 'DATA') { - output.push(frame.data); - } - if (frame.flags.END_STREAM) { - this.emit('end_stream'); - } - callback(); - }; - - // Checking results - flow2.on('end_stream', function() { - input = util.concat(input); - output = util.concat(output); - - expect(input).to.deep.equal(output); - - done(); - }); - - // Start piping - flow1.pipe(flow2).pipe(flow1); - }); - }); - }); -}); diff --git a/test/framer.js b/test/framer.js deleted file mode 100644 index a6323fd6..00000000 --- a/test/framer.js +++ /dev/null @@ -1,248 +0,0 @@ -var expect = require('chai').expect; -var util = require('./util'); - -var framer = require('../lib/framer'); -var Serializer = framer.Serializer; -var Deserializer = framer.Deserializer; - -var frame_types = { - DATA: ['data'], - HEADERS: ['priority', 'data'], - PRIORITY: ['priority'], - RST_STREAM: ['error'], - SETTINGS: ['settings'], - PUSH_PROMISE: ['promised_stream', 'data'], - PING: ['data'], - GOAWAY: ['last_stream', 'error'], - WINDOW_UPDATE: ['window_size'], - CONTINUATION: ['data'] -}; - -var test_frames = [{ - frame: { - type: 'DATA', - flags: { END_STREAM: false, RESERVED: false }, - stream: 10, - - data: new Buffer('12345678', 'hex') - }, - // length + type + flags + stream + content - buffer: new Buffer('0004' + '00' + '00' + '0000000A' + '12345678', 'hex') - -}, { - frame: { - type: 'HEADERS', - flags: { END_STREAM: false, RESERVED: false, END_HEADERS: false, PRIORITY: false }, - stream: 15, - - data: new Buffer('12345678', 'hex') - }, - buffer: new Buffer('0004' + '01' + '00' + '0000000F' + '12345678', 'hex') - -}, { - frame: { - type: 'HEADERS', - flags: { END_STREAM: false, RESERVED: false, END_HEADERS: false, PRIORITY: true }, - stream: 15, - - priority: 3, - data: new Buffer('12345678', 'hex') - }, - buffer: new Buffer('0008' + '01' + '08' + '0000000F' + '00000003' + '12345678', 'hex') - -}, { - frame: { - type: 'PRIORITY', - flags: { }, - stream: 10, - - priority: 3 - }, - buffer: new Buffer('0004' + '02' + '00' + '0000000A' + '00000003', 'hex') - -}, { - frame: { - type: 'RST_STREAM', - flags: { }, - stream: 10, - - error: 'INTERNAL_ERROR' - }, - buffer: new Buffer('0004' + '03' + '00' + '0000000A' + '00000002', 'hex') - -}, { - frame: { - type: 'SETTINGS', - flags: { }, - stream: 10, - - settings: { - SETTINGS_MAX_CONCURRENT_STREAMS: 0x01234567, - SETTINGS_INITIAL_WINDOW_SIZE: 0x89ABCDEF, - SETTINGS_FLOW_CONTROL_OPTIONS: true - } - }, - buffer: new Buffer('0018' + '04' + '00' + '0000000A' + '00' + '000004' + '01234567' + - '00' + '000007' + '89ABCDEF' + - '00' + '00000A' + '00000001', 'hex') - -}, { - frame: { - type: 'PUSH_PROMISE', - flags: { END_PUSH_PROMISE: false }, - stream: 15, - - promised_stream: 3, - data: new Buffer('12345678', 'hex') - }, - buffer: new Buffer('0008' + '05' + '00' + '0000000F' + '00000003' + '12345678', 'hex') - -}, { - frame: { - type: 'PING', - flags: { PONG: false }, - stream: 15, - - data: new Buffer('1234567887654321', 'hex') - }, - buffer: new Buffer('0008' + '06' + '00' + '0000000F' + '1234567887654321', 'hex') - -}, { - frame: { - type: 'GOAWAY', - flags: { }, - stream: 10, - - last_stream: 0x12345678, - error: 'PROTOCOL_ERROR' - }, - buffer: new Buffer('0008' + '07' + '00' + '0000000A' + '12345678' + '00000001', 'hex') - -}, { - frame: { - type: 'WINDOW_UPDATE', - flags: { }, - stream: 10, - - window_size: 0x12345678 - }, - buffer: new Buffer('0004' + '09' + '00' + '0000000A' + '12345678', 'hex') -}, { - frame: { - type: 'CONTINUATION', - flags: { END_STREAM: false, RESERVED: false, END_HEADERS: true }, - stream: 10, - - data: new Buffer('12345678', 'hex') - }, - // length + type + flags + stream + content - buffer: new Buffer('0004' + '0A' + '04' + '0000000A' + '12345678', 'hex') -}]; - -describe('framer.js', function() { - describe('Serializer', function() { - describe('static method .commonHeader({ type, flags, stream }, buffer_array)', function() { - it('should add the appropriate 8 byte header buffer in front of the others', function() { - for (var i = 0; i < test_frames.length; i++) { - var test = test_frames[i]; - var buffers = [test.buffer.slice(8)]; - var header_buffer = test.buffer.slice(0,8); - Serializer.commonHeader(test.frame, buffers); - expect(buffers[0]).to.deep.equal(header_buffer); - } - }); - }); - - Object.keys(frame_types).forEach(function(type) { - var tests = test_frames.filter(function(test) { return test.frame.type === type; }); - var frame_shape = '{ ' + frame_types[type].join(', ') + ' }'; - describe('static method .' + type + '(' + frame_shape + ', buffer_array)', function() { - it('should push buffers to the array that make up a ' + type + ' type payload', function() { - for (var i = 0; i < tests.length; i++) { - var test = tests[i]; - var buffers = []; - Serializer[type](test.frame, buffers); - expect(util.concat(buffers)).to.deep.equal(test.buffer.slice(8)); - } - }); - }); - }); - - describe('transform stream', function() { - it('should transform frame objects to appropriate buffers', function() { - var stream = new Serializer(util.log); - - for (var i = 0; i < test_frames.length; i++) { - var test = test_frames[i]; - stream.write(test.frame); - var chunk, buffer = new Buffer(0); - while (chunk = stream.read()) { - buffer = util.concat([buffer, chunk]); - } - expect(buffer).to.be.deep.equal(test.buffer); - } - }); - }); - }); - - describe('Deserializer', function() { - describe('static method .commonHeader(header_buffer, frame)', function() { - it('should augment the frame object with these properties: { type, flags, stream })', function() { - for (var i = 0; i < test_frames.length; i++) { - var test = test_frames[i], frame = {}; - Deserializer.commonHeader(test.buffer.slice(0,8), frame); - expect(frame).to.deep.equal({ - type: test.frame.type, - flags: test.frame.flags, - stream: test.frame.stream - }); - } - }); - }); - - Object.keys(frame_types).forEach(function(type) { - var tests = test_frames.filter(function(test) { return test.frame.type === type; }); - var frame_shape = '{ ' + frame_types[type].join(', ') + ' }'; - describe('static method .' + type + '(payload_buffer, frame)', function() { - it('should augment the frame object with these properties: ' + frame_shape, function() { - for (var i = 0; i < tests.length; i++) { - var test = tests[i]; - var frame = { - type: test.frame.type, - flags: test.frame.flags, - stream: test.frame.stream - }; - Deserializer[type](test.buffer.slice(8), frame); - expect(frame).to.deep.equal(test.frame); - } - }); - }); - }); - - describe('transform stream', function() { - it('should transform buffers to appropriate frame object', function() { - var stream = new Deserializer(util.log); - - var shuffled = util.shuffleBuffers(test_frames.map(function(test) { return test.buffer; })); - shuffled.forEach(stream.write.bind(stream)); - - for (var j = 0; j < test_frames.length; j++) { - expect(stream.read()).to.be.deep.equal(test_frames[j].frame); - } - }); - }); - }); - - describe('bunyan formatter', function() { - describe('`frame`', function() { - var format = framer.serializers.frame; - it('should assign a unique ID to each frame', function() { - var frame1 = { type: 'DATA', data: new Buffer(10) }; - var frame2 = { type: 'PRIORITY', priority: 1 }; - expect(format(frame1).id).to.be.equal(format(frame1)); - expect(format(frame2).id).to.be.equal(format(frame2)); - expect(format(frame1)).to.not.be.equal(format(frame2)); - }); - }); - }); -}); diff --git a/test/http.js b/test/http.js deleted file mode 100644 index a516b464..00000000 --- a/test/http.js +++ /dev/null @@ -1,422 +0,0 @@ -var expect = require('chai').expect; -var util = require('./util'); -var fs = require('fs'); -var path = require('path'); - -var http2 = require('../lib/http'); -var https = require('https'); - -process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; - -var options = { - key: fs.readFileSync(path.join(__dirname, '../example/localhost.key')), - cert: fs.readFileSync(path.join(__dirname, '../example/localhost.crt')), - log: util.log -}; - -http2.globalAgent = new http2.Agent({ log: util.log }); - -describe('http.js', function() { - describe('Server', function() { - describe('new Server(options)', function() { - it('should throw if called without \'plain\' or TLS options', function() { - expect(function() { - new http2.Server(); - }).to.throw(Error); - expect(function() { - http2.createServer(util.noop); - }).to.throw(Error); - }); - }); - describe('property `timeout`', function() { - it('should be a proxy for the backing HTTPS server\'s `timeout` property', function() { - var server = new http2.Server(options); - var backingServer = server._server; - var newTimeout = 10; - server.timeout = newTimeout; - expect(server.timeout).to.be.equal(newTimeout); - expect(backingServer.timeout).to.be.equal(newTimeout); - }); - }); - describe('method `setTimeout(timeout, [callback])`', function() { - it('should be a proxy for the backing HTTPS server\'s `setTimeout` method', function() { - var server = new http2.Server(options); - var backingServer = server._server; - var newTimeout = 10; - var newCallback = util.noop; - backingServer.setTimeout = function(timeout, callback) { - expect(timeout).to.be.equal(newTimeout); - expect(callback).to.be.equal(newCallback); - }; - server.setTimeout(newTimeout, newCallback); - }); - }); - }); - describe('Agent', function() { - describe('property `maxSockets`', function() { - it('should be a proxy for the backing HTTPS agent\'s `maxSockets` property', function() { - var agent = new http2.Agent({ log: util.log }); - var backingAgent = agent._httpsAgent; - var newMaxSockets = backingAgent.maxSockets + 1; - agent.maxSockets = newMaxSockets; - expect(agent.maxSockets).to.be.equal(newMaxSockets); - expect(backingAgent.maxSockets).to.be.equal(newMaxSockets); - }); - }); - describe('method `request(options, [callback])`', function() { - it('should throw when trying to use with \'http\' scheme', function() { - expect(function() { - var agent = new http2.Agent({ log: util.log }); - agent.request({ protocol: 'http:' }); - }).to.throw(Error); - }); - }); - }); - describe('OutgoingRequest', function() { - function testFallbackProxyMethod(name, originalArguments, done) { - var request = new http2.OutgoingRequest(); - - // When in HTTP/2 mode, this call should be ignored - request.stream = { reset: util.noop }; - request[name].apply(request, originalArguments); - delete request.stream; - - // When in fallback mode, this call should be forwarded - request[name].apply(request, originalArguments); - var mockFallbackRequest = { on: util.noop }; - mockFallbackRequest[name] = function() { - expect(arguments).to.deep.equal(originalArguments); - done(); - }; - request._fallback(mockFallbackRequest); - } - describe('method `setNoDelay(noDelay)`', function() { - it('should act as a proxy for the backing HTTPS agent\'s `setNoDelay` method', function(done) { - testFallbackProxyMethod('setNoDelay', [true], done); - }); - }); - describe('method `setSocketKeepAlive(enable, initialDelay)`', function() { - it('should act as a proxy for the backing HTTPS agent\'s `setSocketKeepAlive` method', function(done) { - testFallbackProxyMethod('setSocketKeepAlive', [true, util.random(10, 100)], done); - }); - }); - describe('method `setTimeout(timeout, [callback])`', function() { - it('should act as a proxy for the backing HTTPS agent\'s `setTimeout` method', function(done) { - testFallbackProxyMethod('setTimeout', [util.random(10, 100), util.noop], done); - }); - }); - describe('method `abort()`', function() { - it('should act as a proxy for the backing HTTPS agent\'s `abort` method', function(done) { - testFallbackProxyMethod('abort', [], done); - }); - }); - }); - describe('test scenario', function() { - describe('simple request', function() { - it('should work as expected', function(done) { - var path = '/x'; - var message = 'Hello world'; - - var server = http2.createServer(options, function(request, response) { - expect(request.url).to.equal(path); - response.end(message); - }); - - server.listen(1234, function() { - http2.get('https://localhost:1234' + path, function(response) { - response.on('readable', function() { - expect(response.read().toString()).to.equal(message); - server.close(); - done(); - }); - }); - }); - }); - }); - describe('request with payload', function() { - it('should work as expected', function(done) { - var path = '/x'; - var message = 'Hello world'; - - var server = http2.createServer(options, function(request, response) { - expect(request.url).to.equal(path); - request.once('readable', function() { - expect(request.read().toString()).to.equal(message); - response.end(); - }); - }); - - server.listen(1240, function() { - var request = http2.request({ - host: 'localhost', - port: 1240, - path: path - }); - request.write(message); - request.end(); - request.on('response', function() { - server.close(); - done(); - }); - }); - }); - }); - describe('request with custom status code and headers', function() { - it('should work as expected', function(done) { - var path = '/x'; - var message = 'Hello world'; - var headerName = 'name'; - var headerValue = 'value'; - - var server = http2.createServer(options, function(request, response) { - // Request URL and headers - expect(request.url).to.equal(path); - expect(request.headers[headerName]).to.equal(headerValue); - - // A header to be overwritten later - response.setHeader(headerName, 'to be overwritten'); - expect(response.getHeader(headerName)).to.equal('to be overwritten'); - - // A header to be deleted - response.setHeader('nonexistent', 'x'); - response.removeHeader('nonexistent'); - expect(response.getHeader('nonexistent')).to.equal(undefined); - - // Don't send date - response.sendDate = false; - - // Specifying more headers, the status code and a reason phrase with `writeHead` - var moreHeaders = {}; - moreHeaders[headerName] = headerValue; - response.writeHead(600, 'to be discarded', moreHeaders); - expect(response.getHeader(headerName)).to.equal(headerValue); - - // Empty response body - response.end(message); - }); - - server.listen(1239, function() { - var headers = {}; - headers[headerName] = headerValue; - var request = http2.request({ - host: 'localhost', - port: 1239, - path: path, - headers: headers - }); - request.end(); - request.on('response', function(response) { - expect(response.headers[headerName]).to.equal(headerValue); - expect(response.headers['nonexistent']).to.equal(undefined); - expect(response.headers['date']).to.equal(undefined); - response.on('readable', function() { - expect(response.read().toString()).to.equal(message); - server.close(); - done(); - }); - }); - }); - }); - }); - describe('request over plain TCP', function() { - it('should work as expected', function(done) { - var path = '/x'; - var message = 'Hello world'; - - var server = http2.createServer({ - plain: true, - log: util.log - }, function(request, response) { - expect(request.url).to.equal(path); - response.end(message); - }); - - server.listen(1237, function() { - var request = http2.request({ - plain: true, - host: 'localhost', - port: 1237, - path: path - }, function(response) { - response.on('readable', function() { - expect(response.read().toString()).to.equal(message); - server.close(); - done(); - }); - }); - request.end(); - }); - }); - }); - describe('request to an HTTPS/1 server', function() { - it('should fall back to HTTPS/1 successfully', function(done) { - var path = '/x'; - var message = 'Hello world'; - - var server = https.createServer(options, function(request, response) { - expect(request.url).to.equal(path); - response.end(message); - }); - - server.listen(5678, function() { - http2.get('https://localhost:5678' + path, function(response) { - response.on('readable', function() { - expect(response.read().toString()).to.equal(message); - done(); - }); - }); - }); - }); - }); - describe('HTTPS/1 request to a HTTP/2 server', function() { - it('should fall back to HTTPS/1 successfully', function(done) { - var path = '/x'; - var message = 'Hello world'; - - var server = http2.createServer(options, function(request, response) { - expect(request.url).to.equal(path); - response.end(message); - }); - - server.listen(1236, function() { - https.get('https://localhost:1236' + path, function(response) { - response.on('readable', function() { - expect(response.read().toString()).to.equal(message); - done(); - }); - }); - }); - }); - }); - describe('two parallel request', function() { - it('should work as expected', function(done) { - var path = '/x'; - var message = 'Hello world'; - - var server = http2.createServer(options, function(request, response) { - expect(request.url).to.equal(path); - response.end(message); - }); - - server.listen(1237, function() { - done = util.callNTimes(2, done); - // 1. request - http2.get('https://localhost:1237' + path, function(response) { - response.on('readable', function() { - expect(response.read().toString()).to.equal(message); - done(); - }); - }); - // 2. request - http2.get('https://localhost:1237' + path, function(response) { - response.on('readable', function() { - expect(response.read().toString()).to.equal(message); - done(); - }); - }); - }); - }); - }); - describe('two subsequent request', function() { - it('should use the same HTTP/2 connection', function(done) { - var path = '/x'; - var message = 'Hello world'; - - var server = http2.createServer(options, function(request, response) { - expect(request.url).to.equal(path); - response.end(message); - }); - - server.listen(1238, function() { - // 1. request - http2.get('https://localhost:1238' + path, function(response) { - response.on('readable', function() { - expect(response.read().toString()).to.equal(message); - - // 2. request - http2.get('https://localhost:1238' + path, function(response) { - response.on('readable', function() { - expect(response.read().toString()).to.equal(message); - done(); - }); - }); - }); - }); - }); - }); - }); - describe('request and response with trailers', function() { - it('should work as expected', function(done) { - var path = '/x'; - var message = 'Hello world'; - var requestTrailers = { 'content-md5': 'x' }; - var responseTrailers = { 'content-md5': 'y' }; - - var server = http2.createServer(options, function(request, response) { - expect(request.url).to.equal(path); - request.on('data', util.noop); - request.once('end', function() { - expect(request.trailers).to.deep.equal(requestTrailers); - response.write(message); - response.addTrailers(responseTrailers); - response.end(); - }); - }); - - server.listen(1241, function() { - var request = http2.request('https://localhost:1241' + path); - request.addTrailers(requestTrailers); - request.end(); - request.on('response', function(response) { - response.on('data', util.noop); - response.once('end', function() { - expect(response.trailers).to.deep.equal(responseTrailers); - done(); - }); - }); - }); - }); - }); - describe('server push', function() { - it('should work as expected', function(done) { - var path = '/x'; - var message = 'Hello world'; - var pushedPath = '/y'; - var pushedMessage = 'Hello world 2'; - - var server = http2.createServer(options, function(request, response) { - expect(request.url).to.equal(path); - var push1 = response.push('/y'); - push1.end(pushedMessage); - var push2 = response.push({ path: '/y', protocol: 'https:' }); - push2.end(pushedMessage); - response.end(message); - }); - - server.listen(1235, function() { - var request = http2.get('https://localhost:1235' + path); - done = util.callNTimes(5, done); - - request.on('response', function(response) { - response.on('readable', function() { - expect(response.read().toString()).to.equal(message); - done(); - }); - response.on('end', done); - }); - - request.on('push', function(promise) { - expect(promise.url).to.be.equal(pushedPath); - promise.on('response', function(pushStream) { - pushStream.on('readable', function() { - expect(pushStream.read().toString()).to.equal(pushedMessage); - done(); - }); - pushStream.on('end', done); - }); - }); - }); - }); - }); - }); -}); diff --git a/test/stream.js b/test/stream.js deleted file mode 100644 index efc583be..00000000 --- a/test/stream.js +++ /dev/null @@ -1,416 +0,0 @@ -var expect = require('chai').expect; -var util = require('./util'); - -var stream = require('../lib/stream'); -var Stream = stream.Stream; - -function createStream() { - var stream = new Stream(util.log); - stream.upstream._window = Infinity; - stream.upstream._remoteFlowControlDisabled = true; - return stream; -} - -// Execute a list of commands and assertions -var recorded_events = ['state', 'error', 'window_update', 'headers', 'promise']; -function execute_sequence(stream, sequence, done) { - if (!done) { - done = sequence; - sequence = stream; - stream = createStream(); - } - - var outgoing_frames = []; - - var emit = stream.emit, events = []; - stream.emit = function(name) { - if (recorded_events.indexOf(name) !== -1) { - events.push({ name: name, data: Array.prototype.slice.call(arguments, 1) }); - } - return emit.apply(this, arguments); - }; - - var commands = [], checks = []; - sequence.forEach(function(step) { - if ('method' in step || 'incoming' in step || 'outgoing' in step || 'wait' in step || 'set_state' in step) { - commands.push(step); - } - - if ('outgoing' in step || 'event' in step || 'active' in step) { - checks.push(step); - } - }); - - var activeCount = 0; - function count_change(change) { - activeCount += change; - } - - function execute(callback) { - var command = commands.shift(); - if (command) { - if ('method' in command) { - var value = stream[command.method.name].apply(stream, command.method.arguments); - if (command.method.ret) { - command.method.ret(value); - } - execute(callback); - } else if ('incoming' in command) { - command.incoming.count_change = count_change; - stream.upstream.write(command.incoming); - execute(callback); - } else if ('outgoing' in command) { - outgoing_frames.push(stream.upstream.read()); - execute(callback); - } else if ('set_state' in command) { - stream.state = command.set_state; - execute(callback); - } else if ('wait' in command) { - setTimeout(execute.bind(null, callback), command.wait); - } else { - throw new Error('Invalid command', command); - } - } else { - setTimeout(callback, 5); - } - } - - function check() { - checks.forEach(function(check) { - if ('outgoing' in check) { - var frame = outgoing_frames.shift(); - for (var key in check.outgoing) { - expect(frame).to.have.property(key).that.deep.equals(check.outgoing[key]); - } - count_change(frame.count_change); - } else if ('event' in check) { - var event = events.shift(); - expect(event.name).to.be.equal(check.event.name); - check.event.data.forEach(function(data, index) { - expect(event.data[index]).to.deep.equal(data); - }); - } else if ('active' in check) { - expect(activeCount).to.be.equal(check.active); - } else { - throw new Error('Invalid check', check); - } - }); - done(); - } - - setImmediate(execute.bind(null, check)); -} - -var example_frames = [ - { type: 'PRIORITY', flags: {}, priority: 1 }, - { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, - { type: 'RST_STREAM', flags: {}, error: 'CANCEL' }, - { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, - { type: 'DATA', flags: {}, data: new Buffer(5) }, - { type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log) } -]; - -var invalid_incoming_frames = { - IDLE: [ - { type: 'DATA', flags: {}, data: new Buffer(5) }, - { type: 'PRIORITY', flags: {}, priority: 1 }, - { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, - { type: 'PUSH_PROMISE', flags: {}, headers: {} }, - { type: 'RST_STREAM', flags: {}, error: 'CANCEL' } - ], - RESERVED_LOCAL: [ - { type: 'DATA', flags: {}, data: new Buffer(5) }, - { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, - { type: 'PUSH_PROMISE', flags: {}, headers: {} }, - { type: 'WINDOW_UPDATE', flags: {}, settings: {} } - ], - RESERVED_REMOTE: [ - { type: 'DATA', flags: {}, data: new Buffer(5) }, - { type: 'PRIORITY', flags: {}, priority: 1 }, - { type: 'PUSH_PROMISE', flags: {}, headers: {} }, - { type: 'WINDOW_UPDATE', flags: {}, settings: {} } - ], - OPEN: [ - ], - HALF_CLOSED_LOCAL: [ - ], - HALF_CLOSED_REMOTE: [ - { type: 'DATA', flags: {}, data: new Buffer(5) }, - { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, - { type: 'PUSH_PROMISE', flags: {}, headers: {} } - ] -}; - -var invalid_outgoing_frames = { - IDLE: [ - { type: 'DATA', flags: {}, data: new Buffer(5) }, - { type: 'PRIORITY', flags: {}, priority: 1 }, - { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, - { type: 'PUSH_PROMISE', flags: {}, headers: {} } - ], - RESERVED_LOCAL: [ - { type: 'DATA', flags: {}, data: new Buffer(5) }, - { type: 'PRIORITY', flags: {}, priority: 1 }, - { type: 'PUSH_PROMISE', flags: {}, headers: {} }, - { type: 'WINDOW_UPDATE', flags: {}, settings: {} } - ], - RESERVED_REMOTE: [ - { type: 'DATA', flags: {}, data: new Buffer(5) }, - { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, - { type: 'PUSH_PROMISE', flags: {}, headers: {} }, - { type: 'WINDOW_UPDATE', flags: {}, settings: {} } - ], - OPEN: [ - ], - HALF_CLOSED_LOCAL: [ - { type: 'DATA', flags: {}, data: new Buffer(5) }, - { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, - { type: 'PUSH_PROMISE', flags: {}, headers: {} } - ], - HALF_CLOSED_REMOTE: [ - ], - CLOSED: [ - { type: 'PRIORITY', flags: {}, priority: 1 }, - { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, - { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, - { type: 'DATA', flags: {}, data: new Buffer(5) }, - { type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log) } - ] -}; - -describe('stream.js', function() { - describe('Stream class', function() { - describe('._transition(sending, frame) method', function() { - it('should emit error, and answer RST_STREAM for invalid incoming frames', function() { - Object.keys(invalid_incoming_frames).forEach(function(state) { - invalid_incoming_frames[state].forEach(function(invalid_frame) { - var stream = createStream(); - stream.state = state; - expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); - }); - }); - - // CLOSED state as a result of incoming END_STREAM (or RST_STREAM) - var stream = createStream(); - stream.headers({}); - stream.end(); - stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop }); - example_frames.forEach(function(invalid_frame) { - invalid_frame.count_change = util.noop; - expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); - }); - - // CLOSED state as a result of outgoing END_STREAM - var stream = createStream(); - stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop }); - stream.headers({}); - stream.end(); - example_frames.slice(3).forEach(function(invalid_frame) { - invalid_frame.count_change = util.noop; - expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); - }); - }); - it('should throw exception for invalid outgoing frames', function() { - Object.keys(invalid_outgoing_frames).forEach(function(state) { - invalid_outgoing_frames[state].forEach(function(invalid_frame) { - var stream = createStream(); - stream.state = state; - expect(stream._transition.bind(stream, true, invalid_frame)).to.throw(Error); - }); - }); - }); - it('should close the stream when there\'s an incoming or outgoing RST_STREAM', function() { - [ - 'RESERVED_LOCAL', - 'RESERVED_REMOTE', - 'OPEN', - 'HALF_CLOSED_LOCAL', - 'HALF_CLOSED_REMOTE' - ].forEach(function(state) { - [true, false].forEach(function(sending) { - var stream = createStream(); - stream.state = state; - stream._transition(sending, { type: 'RST_STREAM', flags: {} }); - expect(stream.state).to.be.equal('CLOSED'); - }); - }); - }); - it('should ignore any incoming frame after sending reset', function() { - var stream = createStream(); - stream.reset(); - example_frames.forEach(stream._transition.bind(stream, false)); - }); - it('should ignore certain incoming frames after closing the stream with END_STREAM', function() { - var stream = createStream(); - stream.upstream.write({ type: 'HEADERS', flags: { END_STREAM: true }, headers:{} }); - stream.headers({}); - stream.end(); - example_frames.slice(0,3).forEach(function(frame) { - frame.count_change = util.noop; - stream._transition(false, frame); - }); - }); - }); - }); - describe('test scenario', function() { - describe('sending request', function() { - it('should trigger the appropriate state transitions and outgoing frames', function(done) { - execute_sequence([ - { method : { name: 'headers', arguments: [{ ':path': '/' }] } }, - { outgoing: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } }, - { event : { name: 'state', data: ['OPEN'] } }, - - { wait : 5 }, - { method : { name: 'end', arguments: [] } }, - { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, - { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(0) } }, - - { wait : 10 }, - { incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, - { incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: new Buffer(5) } }, - { event : { name: 'headers', data: [{ ':status': 200 }] } }, - { event : { name: 'state', data: ['CLOSED'] } }, - - { active : 0 } - ], done); - }); - }); - describe('answering request', function() { - it('should trigger the appropriate state transitions and outgoing frames', function(done) { - var payload = new Buffer(5); - execute_sequence([ - { incoming: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } }, - { event : { name: 'state', data: ['OPEN'] } }, - { event : { name: 'headers', data: [{ ':path': '/' }] } }, - - { wait : 5 }, - { incoming: { type: 'DATA', flags: { }, data: new Buffer(5) } }, - { incoming: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(10) } }, - { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, - - { wait : 5 }, - { method : { name: 'headers', arguments: [{ ':status': 200 }] } }, - { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, - - { wait : 5 }, - { method : { name: 'end', arguments: [payload] } }, - { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, - { event : { name: 'state', data: ['CLOSED'] } }, - - { active : 0 } - ], done); - }); - }); - describe('sending push stream', function() { - it('should trigger the appropriate state transitions and outgoing frames', function(done) { - var payload = new Buffer(5); - var pushStream; - - execute_sequence([ - // receiving request - { incoming: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } }, - { event : { name: 'state', data: ['OPEN'] } }, - { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, - { event : { name: 'headers', data: [{ ':path': '/' }] } }, - - // sending response headers - { wait : 5 }, - { method : { name: 'headers', arguments: [{ ':status': '200' }] } }, - { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } }, - - // sending push promise - { method : { name: 'promise', arguments: [{ ':path': '/' }], ret: function(str) { pushStream = str; } } }, - { outgoing: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/' } } }, - - // sending response data - { method : { name: 'end', arguments: [payload] } }, - { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, - { event : { name: 'state', data: ['CLOSED'] } }, - - { active : 0 } - ], function() { - // initial state of the promised stream - expect(pushStream.state).to.equal('RESERVED_LOCAL'); - - execute_sequence(pushStream, [ - // push headers - { wait : 5 }, - { method : { name: 'headers', arguments: [{ ':status': '200' }] } }, - { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } }, - { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, - - // push data - { method : { name: 'end', arguments: [payload] } }, - { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, - { event : { name: 'state', data: ['CLOSED'] } }, - - { active : 1 } - ], done); - }); - }); - }); - describe('receiving push stream', function() { - it('should trigger the appropriate state transitions and outgoing frames', function(done) { - var payload = new Buffer(5); - var original_stream = createStream(); - var promised_stream = createStream(); - - done = util.callNTimes(2, done); - - execute_sequence(original_stream, [ - // sending request headers - { method : { name: 'headers', arguments: [{ ':path': '/' }] } }, - { method : { name: 'end', arguments: [] } }, - { outgoing: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } }, - { event : { name: 'state', data: ['OPEN'] } }, - { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, - - // receiving response headers - { wait : 10 }, - { incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, - { event : { name: 'headers', data: [{ ':status': 200 }] } }, - - // receiving push promise - { incoming: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/2.html' }, promised_stream: promised_stream } }, - { event : { name: 'promise', data: [promised_stream, { ':path': '/2.html' }] } }, - - // receiving response data - { incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: payload } }, - { event : { name: 'state', data: ['CLOSED'] } }, - - { active : 0 } - ], done); - - execute_sequence(promised_stream, [ - // initial state of the promised stream - { event : { name: 'state', data: ['RESERVED_REMOTE'] } }, - - // push headers - { wait : 10 }, - { incoming: { type: 'HEADERS', flags: { END_STREAM: false }, headers: { ':status': 200 } } }, - { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, - { event : { name: 'headers', data: [{ ':status': 200 }] } }, - - // push data - { incoming: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, - { event : { name: 'state', data: ['CLOSED'] } }, - - { active : 0 } - ], done); - }); - }); - }); - - describe('bunyan formatter', function() { - describe('`s`', function() { - var format = stream.serializers.s; - it('should assign a unique ID to each frame', function() { - var stream1 = createStream(); - var stream2 = createStream(); - expect(format(stream1)).to.be.equal(format(stream1)); - expect(format(stream2)).to.be.equal(format(stream2)); - expect(format(stream1)).to.not.be.equal(format(stream2)); - }); - }); - }); -}); diff --git a/test/util.js b/test/util.js deleted file mode 100644 index 84a2dd17..00000000 --- a/test/util.js +++ /dev/null @@ -1,87 +0,0 @@ -var path = require('path'); -var fs = require('fs'); -var spawn = require('child_process').spawn; - -function noop() {} -exports.noop = noop; - -if (process.env.HTTP2_LOG) { - var logOutput = process.stderr; - if (process.stderr.isTTY) { - var bin = path.resolve(path.dirname(require.resolve('bunyan')), '..', 'bin', 'bunyan'); - if(bin && fs.existsSync(bin)) { - logOutput = spawn(bin, ['-o', 'short'], { - stdio: [null, process.stderr, process.stderr] - }).stdin; - } - } - exports.createLogger = function(name) { - return require('bunyan').createLogger({ - name: name, - stream: logOutput, - level: process.env.HTTP2_LOG, - serializers: require('../lib/http').serializers - }); - }; - exports.log = exports.createLogger('test'); -} else { - exports.createLogger = function() { - return exports.log; - }; - exports.log = { - fatal: noop, - error: noop, - warn : noop, - info : noop, - debug: noop, - trace: noop, - - child: function() { return this; } - }; -} - -exports.callNTimes = function callNTimes(limit, done) { - if (limit === 0) { - done(); - } else { - var i = 0; - return function() { - i += 1; - if (i === limit) { - done(); - } - }; - } -}; - -// Concatenate an array of buffers into a new buffer -exports.concat = function concat(buffers) { - var size = 0; - for (var i = 0; i < buffers.length; i++) { - size += buffers[i].length; - } - - var concatenated = new Buffer(size); - for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) { - buffers[j].copy(concatenated, cursor); - } - - return concatenated; -}; - -exports.random = function random(min, max) { - return min + Math.floor(Math.random() * (max - min + 1)); -}; - -// Concatenate an array of buffers and then cut them into random size buffers -exports.shuffleBuffers = function shuffleBuffers(buffers) { - var concatenated = exports.concat(buffers), output = [], written = 0; - - while (written < concatenated.length) { - var chunk_size = Math.min(concatenated.length - written, Math.ceil(Math.random()*20)); - output.push(concatenated.slice(written, written + chunk_size)); - written += chunk_size; - } - - return output; -}