├── .npmignore ├── .gitignore ├── .travis.yml ├── example ├── localhost.crt ├── localhost.key ├── client.js └── server.js ├── LICENSE ├── package.json ├── test ├── endpoint.js ├── util.js ├── connection.js ├── flow.js ├── framer.js ├── stream.js ├── compressor.js └── http.js ├── lib ├── index.js └── protocol │ ├── index.js │ ├── endpoint.js │ ├── flow.js │ ├── connection.js │ └── stream.js ├── README.md └── HISTORY.md /.npmignore: -------------------------------------------------------------------------------- 1 | test 2 | .travis.yml 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea 3 | coverage 4 | doc 5 | .vscode 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "iojs" 4 | - "0.12" 5 | 6 | -------------------------------------------------------------------------------- /example/localhost.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIICDTCCAXYCCQC7iiBVXeTv1DANBgkqhkiG9w0BAQUFADBLMQswCQYDVQQGEwJI 3 | VTETMBEGA1UECBMKU29tZS1TdGF0ZTETMBEGA1UEChMKbm9kZS1odHRwMjESMBAG 4 | A1UEAxMJbG9jYWxob3N0MB4XDTE0MTIwMjE4NDcwNFoXDTI0MTEyOTE4NDcwNFow 5 | SzELMAkGA1UEBhMCSFUxEzARBgNVBAgTClNvbWUtU3RhdGUxEzARBgNVBAoTCm5v 6 | ZGUtaHR0cDIxEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOB 7 | jQAwgYkCgYEA8As7rj7xdD+RuAmORju9NI+jtOScGgiAbfovaFyzTu0O0H9SCExi 8 | u6e2iXMRfzomTix/yjRvbdHEXfgONG1MnKUc0oC4GxHXshyMDEXq9LadgAmR/nDL 9 | UVT0eo7KqC21ufaca2nVS9qOdlSCE/p7IJdb2+BF1RmuC9pHpXvFW20CAwEAATAN 10 | BgkqhkiG9w0BAQUFAAOBgQDn8c/9ho9L08dOqEJ2WTBmv4dfRC3oTWR/0oIGsaXb 11 | RhQONy5CJv/ymPYE7nCFWTMaia+w8oFqMie/aNZ7VK6L+hafuUS93IjuTXVN++JP 12 | 4948B0BBagvXGTwNtvm/1sZHLrXTkH1dbRUEF8M+KUSRUu2zJgm+e1bD8WTKQOIL 13 | NA== 14 | -----END CERTIFICATE----- 15 | -------------------------------------------------------------------------------- /example/localhost.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIICXQIBAAKBgQDwCzuuPvF0P5G4CY5GO700j6O05JwaCIBt+i9oXLNO7Q7Qf1II 3 | TGK7p7aJcxF/OiZOLH/KNG9t0cRd+A40bUycpRzSgLgbEdeyHIwMRer0tp2ACZH+ 4 | cMtRVPR6jsqoLbW59pxradVL2o52VIIT+nsgl1vb4EXVGa4L2kele8VbbQIDAQAB 5 | AoGAKKB+FVup2hb4PsG/RrvNphu5hWA721wdAIAbjfpCjtUocLlb1PO4sjIMfu7u 6 | wy3AVfLKHhsJ0Phz18OoA8+L65NMoMRsHOGaLEnGIJzJcnDLT5+uTFN5di0a1+UK 7 | BzB828rlHBNoQisogVCoKTYlCPJAZuI3trEzupWAV28XjTECQQD5LUEwYq4xr62L 8 | dEq5Qj/+c5paK/jrEBY83VZUmWzYsFgUwmpdku2ITRILQlOM33j6rk8krZZb93sb 9 | 38ydmfwjAkEA9p30zyjOI9kKqTl9WdYNYtIXpyNGYa+Pga33o9pawTewiyS2uCYs 10 | wnQQV26bQ0YwQqLQhtIbo4fzCO6Ex0w7LwJBANHNbd8cp4kEX35U+3nDM3i+w477 11 | CUp6sA6tWrw+tqw4xuEr1T1WshOauP+r6AdsPkPsMo0yb7CdzxVoObPVbLsCQQCc 12 | sx0cjEb/TCeUAy186Z+zzN6umqFb7Jt4wLt7Z4EHCIWqw/c95zPFks3XYDZTdsOv 13 | c5igMdzR+c4ZPMUthWiNAkByx7If12G1Z/R2Y0vIB0WJq4BJnZCZ0mRR0oAmPoA+ 14 | sZbmwctZ3IU+68Rgr4EAhrU04ygjF67IiNyXX0qqu3VH 15 | -----END RSA PRIVATE KEY----- 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (C) 2013 Gábor Molnár , Google Inc 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "http2", 3 | "version": "3.3.4", 4 | "description": "An HTTP/2 client and server implementation", 5 | "main": "lib/index.js", 6 | "engines" : { 7 | "node" : ">=0.12.0" 8 | }, 9 | "devDependencies": { 10 | "istanbul": "*", 11 | "chai": "*", 12 | "mocha": "*", 13 | "docco": "*", 14 | "bunyan": "*" 15 | }, 16 | "scripts": { 17 | "test": "istanbul test _mocha -- --reporter spec --slow 500 --timeout 15000", 18 | "doc": "docco lib/* --output doc --layout parallel --template root.jst --css doc/docco.css && docco lib/protocol/* --output doc/protocol --layout parallel --template protocol.jst --css doc/docco.css" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/molnarg/node-http2.git" 23 | }, 24 | "homepage": "https://github.com/molnarg/node-http2", 25 | "bugs": { 26 | "url": "https://github.com/molnarg/node-http2/issues" 27 | }, 28 | "keywords": [ 29 | "http", 30 | "http2", 31 | "client", 32 | "server" 33 | ], 34 | "author": "Gábor Molnár (http://gabor.molnar.es)", 35 | "contributors": [ 36 | "Nick Hurley", 37 | "Mike Belshe", 38 | "Yoshihiro Iwanaga", 39 | "Igor Novikov", 40 | "James Willcox", 41 | "David Björklund", 42 | "Patrick McManus" 43 | ], 44 | "license": "MIT", 45 | "readmeFilename": "README.md" 46 | } 47 | -------------------------------------------------------------------------------- /test/endpoint.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var util = require('./util'); 3 | 4 | var endpoint = require('../lib/protocol/endpoint'); 5 | var Endpoint = endpoint.Endpoint; 6 | 7 | var settings = { 8 | SETTINGS_MAX_CONCURRENT_STREAMS: 100, 9 | SETTINGS_INITIAL_WINDOW_SIZE: 100000 10 | }; 11 | 12 | describe('endpoint.js', function() { 13 | describe('scenario', function() { 14 | describe('connection setup', function() { 15 | it('should work as expected', function(done) { 16 | var c = new Endpoint(util.log.child({ role: 'client' }), 'CLIENT', settings); 17 | var s = new Endpoint(util.log.child({ role: 'client' }), 'SERVER', settings); 18 | 19 | util.log.debug('Test initialization over, starting piping.'); 20 | c.pipe(s).pipe(c); 21 | 22 | setTimeout(function() { 23 | // If there are no exception until this, then we're done 24 | done(); 25 | }, 10); 26 | }); 27 | }); 28 | }); 29 | describe('bunyan serializer', function() { 30 | describe('`e`', function() { 31 | var format = endpoint.serializers.e; 32 | it('should assign a unique ID to each endpoint', function() { 33 | var c = new Endpoint(util.log.child({ role: 'client' }), 'CLIENT', settings); 34 | var s = new Endpoint(util.log.child({ role: 'client' }), 'SERVER', settings); 35 | expect(format(c)).to.not.equal(format(s)); 36 | expect(format(c)).to.equal(format(c)); 37 | expect(format(s)).to.equal(format(s)); 38 | }); 39 | }); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /example/client.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var http2 = require('..'); 4 | var urlParse = require('url').parse; 5 | 6 | // Setting the global logger (optional) 7 | http2.globalAgent = new http2.Agent({ 8 | rejectUnauthorized: true, 9 | log: require('../test/util').createLogger('client') 10 | }); 11 | 12 | // Sending the request 13 | var url = process.argv.pop(); 14 | var options = urlParse(url); 15 | 16 | // Optionally verify self-signed certificates. 17 | if (options.hostname == 'localhost') { 18 | options.key = fs.readFileSync(path.join(__dirname, '/localhost.key')); 19 | options.ca = fs.readFileSync(path.join(__dirname, '/localhost.crt')); 20 | } 21 | 22 | var request = process.env.HTTP2_PLAIN ? http2.raw.get(options) : http2.get(options); 23 | 24 | // Receiving the response 25 | request.on('response', function(response) { 26 | response.pipe(process.stdout); 27 | response.on('end', finish); 28 | }); 29 | 30 | // Receiving push streams 31 | request.on('push', function(pushRequest) { 32 | var filename = path.join(__dirname, '/push-' + push_count); 33 | push_count += 1; 34 | console.error('Receiving pushed resource: ' + pushRequest.url + ' -> ' + filename); 35 | pushRequest.on('response', function(pushResponse) { 36 | pushResponse.pipe(fs.createWriteStream(filename)).on('finish', finish); 37 | }); 38 | }); 39 | 40 | // Quitting after both the response and the associated pushed resources have arrived 41 | var push_count = 0; 42 | var finished = 0; 43 | function finish() { 44 | finished += 1; 45 | if (finished === (1 + push_count)) { 46 | process.exit(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /example/server.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var http2 = require('..'); 4 | 5 | // We cache one file to be able to do simple performance tests without waiting for the disk 6 | var cachedFile = fs.readFileSync(path.join(__dirname, './server.js')); 7 | var cachedUrl = '/server.js'; 8 | 9 | // The callback to handle requests 10 | function onRequest(request, response) { 11 | var filename = path.join(__dirname, request.url); 12 | 13 | // Serving server.js from cache. Useful for microbenchmarks. 14 | if (request.url === cachedUrl) { 15 | if (response.push) { 16 | // Also push down the client js, since it's possible if the requester wants 17 | // one, they want both. 18 | var push = response.push('/client.js'); 19 | push.writeHead(200); 20 | fs.createReadStream(path.join(__dirname, '/client.js')).pipe(push); 21 | } 22 | response.end(cachedFile); 23 | } 24 | 25 | // Reading file from disk if it exists and is safe. 26 | else if ((filename.indexOf(__dirname) === 0) && fs.existsSync(filename) && fs.statSync(filename).isFile()) { 27 | response.writeHead(200); 28 | var fileStream = fs.createReadStream(filename); 29 | fileStream.pipe(response); 30 | fileStream.on('finish',response.end); 31 | } 32 | 33 | // Otherwise responding with 404. 34 | else { 35 | response.writeHead(404); 36 | response.end(); 37 | } 38 | } 39 | 40 | // Creating a bunyan logger (optional) 41 | var log = require('../test/util').createLogger('server'); 42 | 43 | // Creating the server in plain or TLS mode (TLS mode is the default) 44 | var server; 45 | if (process.env.HTTP2_PLAIN) { 46 | server = http2.raw.createServer({ 47 | log: log 48 | }, onRequest); 49 | } else { 50 | server = http2.createServer({ 51 | log: log, 52 | key: fs.readFileSync(path.join(__dirname, '/localhost.key')), 53 | cert: fs.readFileSync(path.join(__dirname, '/localhost.crt')) 54 | }, onRequest); 55 | } 56 | server.listen(process.env.HTTP2_PORT || 8080); 57 | -------------------------------------------------------------------------------- /test/util.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | var spawn = require('child_process').spawn; 4 | 5 | function noop() {} 6 | exports.noop = noop; 7 | 8 | if (process.env.HTTP2_LOG) { 9 | var logOutput = process.stderr; 10 | if (process.stderr.isTTY) { 11 | var bin = path.resolve(path.dirname(require.resolve('bunyan')), '..', 'bin', 'bunyan'); 12 | if(bin && fs.existsSync(bin)) { 13 | logOutput = spawn(bin, ['-o', 'short'], { 14 | stdio: [null, process.stderr, process.stderr] 15 | }).stdin; 16 | } 17 | } 18 | exports.createLogger = function(name) { 19 | return require('bunyan').createLogger({ 20 | name: name, 21 | stream: logOutput, 22 | level: process.env.HTTP2_LOG, 23 | serializers: require('../lib/http').serializers 24 | }); 25 | }; 26 | exports.log = exports.createLogger('test'); 27 | exports.clientLog = exports.createLogger('client'); 28 | exports.serverLog = exports.createLogger('server'); 29 | } else { 30 | exports.createLogger = function() { 31 | return exports.log; 32 | }; 33 | exports.log = exports.clientLog = exports.serverLog = { 34 | fatal: noop, 35 | error: noop, 36 | warn : noop, 37 | info : noop, 38 | debug: noop, 39 | trace: noop, 40 | 41 | child: function() { return this; } 42 | }; 43 | } 44 | 45 | exports.callNTimes = function callNTimes(limit, done) { 46 | if (limit === 0) { 47 | done(); 48 | } else { 49 | var i = 0; 50 | return function() { 51 | i += 1; 52 | if (i === limit) { 53 | done(); 54 | } 55 | }; 56 | } 57 | }; 58 | 59 | // Concatenate an array of buffers into a new buffer 60 | exports.concat = function concat(buffers) { 61 | var size = 0; 62 | for (var i = 0; i < buffers.length; i++) { 63 | size += buffers[i].length; 64 | } 65 | 66 | var concatenated = new Buffer(size); 67 | for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) { 68 | buffers[j].copy(concatenated, cursor); 69 | } 70 | 71 | return concatenated; 72 | }; 73 | 74 | exports.random = function random(min, max) { 75 | return min + Math.floor(Math.random() * (max - min + 1)); 76 | }; 77 | 78 | // Concatenate an array of buffers and then cut them into random size buffers 79 | exports.shuffleBuffers = function shuffleBuffers(buffers) { 80 | var concatenated = exports.concat(buffers), output = [], written = 0; 81 | 82 | while (written < concatenated.length) { 83 | var chunk_size = Math.min(concatenated.length - written, Math.ceil(Math.random()*20)); 84 | output.push(concatenated.slice(written, written + chunk_size)); 85 | written += chunk_size; 86 | } 87 | 88 | return output; 89 | }; 90 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | // [node-http2][homepage] is an [HTTP/2][http2] implementation for [node.js][node]. 2 | // 3 | // The core of the protocol is implemented in the protocol sub-directory. This directory provides 4 | // two important features on top of the protocol: 5 | // 6 | // * Implementation of different negotiation schemes that can be used to start a HTTP2 connection. 7 | // These include TLS ALPN, Upgrade and Plain TCP. 8 | // 9 | // * Providing an API very similar to the standard node.js [HTTPS module API][node-https] 10 | // (which is in turn very similar to the [HTTP module API][node-http]). 11 | // 12 | // [homepage]: https://github.com/molnarg/node-http2 13 | // [http2]: https://tools.ietf.org/html/rfc7540 14 | // [node]: https://nodejs.org/ 15 | // [node-https]: https://nodejs.org/api/https.html 16 | // [node-http]: https://nodejs.org/api/http.html 17 | 18 | module.exports = require('./http'); 19 | 20 | /* 21 | HTTP API 22 | 23 | | ^ 24 | | | 25 | +-------------|------------|------------------------------------------------------+ 26 | | | | Server/Agent | 27 | | v | | 28 | | +----------+ +----------+ | 29 | | | Outgoing | | Incoming | | 30 | | | req/res. | | req/res. | | 31 | | +----------+ +----------+ | 32 | | | ^ | 33 | | | | | 34 | | +---------|------------|-------------------------------------+ +----- | 35 | | | | | Endpoint | | | 36 | | | | | | | | 37 | | | v | | | | 38 | | | +-----------------------+ +-------------------- | | | 39 | | | | Stream | | Stream ... | | | 40 | | | +-----------------------+ +-------------------- | | | 41 | | | | | | 42 | | +------------------------------------------------------------+ +----- | 43 | | | | | 44 | | | | | 45 | | v | | 46 | | +------------------------------------------------------------+ +----- | 47 | | | TCP stream | | ... | 48 | | +------------------------------------------------------------+ +----- | 49 | | | 50 | +---------------------------------------------------------------------------------+ 51 | 52 | */ 53 | -------------------------------------------------------------------------------- /lib/protocol/index.js: -------------------------------------------------------------------------------- 1 | // This is an implementation of the [HTTP/2][http2] 2 | // framing layer for [node.js][node]. 3 | // 4 | // The main building blocks are [node.js streams][node-stream] that are connected through pipes. 5 | // 6 | // The main components are: 7 | // 8 | // * [Endpoint](endpoint.html): represents an HTTP/2 endpoint (client or server). It's 9 | // responsible for the the first part of the handshake process (sending/receiving the 10 | // [connection header][http2-connheader]) and manages other components (framer, compressor, 11 | // connection, streams) that make up a client or server. 12 | // 13 | // * [Connection](connection.html): multiplexes the active HTTP/2 streams, manages connection 14 | // lifecycle and settings, and responsible for enforcing the connection level limits (flow 15 | // control, initiated stream limit) 16 | // 17 | // * [Stream](stream.html): implementation of the [HTTP/2 stream concept][http2-stream]. 18 | // Implements the [stream state machine][http2-streamstate] defined by the standard, provides 19 | // management methods and events for using the stream (sending/receiving headers, data, etc.), 20 | // and enforces stream level constraints (flow control, sending only legal frames). 21 | // 22 | // * [Flow](flow.html): implements flow control for Connection and Stream as parent class. 23 | // 24 | // * [Compressor and Decompressor](compressor.html): compression and decompression of HEADER and 25 | // PUSH_PROMISE frames 26 | // 27 | // * [Serializer and Deserializer](framer.html): the lowest layer in the stack that transforms 28 | // between the binary and the JavaScript object representation of HTTP/2 frames 29 | // 30 | // [http2]: https://tools.ietf.org/html/rfc7540 31 | // [http2-connheader]: https://tools.ietf.org/html/rfc7540#section-3.5 32 | // [http2-stream]: https://tools.ietf.org/html/rfc7540#section-5 33 | // [http2-streamstate]: https://tools.ietf.org/html/rfc7540#section-5.1 34 | // [node]: https://nodejs.org/ 35 | // [node-stream]: https://nodejs.org/api/stream.html 36 | // [node-https]: https://nodejs.org/api/https.html 37 | // [node-http]: https://nodejs.org/api/http.html 38 | 39 | exports.VERSION = 'h2'; 40 | 41 | exports.Endpoint = require('./endpoint').Endpoint; 42 | 43 | /* Bunyan serializers exported by submodules that are worth adding when creating a logger. */ 44 | exports.serializers = {}; 45 | var modules = ['./framer', './compressor', './flow', './connection', './stream', './endpoint']; 46 | modules.map(require).forEach(function(module) { 47 | for (var name in module.serializers) { 48 | exports.serializers[name] = module.serializers[name]; 49 | } 50 | }); 51 | 52 | /* 53 | Stream API Endpoint API 54 | Stream data 55 | 56 | | ^ | ^ 57 | | | | | 58 | | | | | 59 | +-----------|------------|---------------------------------------+ 60 | | | | Endpoint | 61 | | | | | 62 | | +-------|------------|-----------------------------------+ | 63 | | | | | Connection | | 64 | | | v | | | 65 | | | +-----------------------+ +-------------------- | | 66 | | | | Stream | | Stream ... | | 67 | | | +-----------------------+ +-------------------- | | 68 | | | | ^ | ^ | | 69 | | | v | v | | | 70 | | | +------------+--+--------+--+------------+- ... | | 71 | | | | ^ | | 72 | | | | | | | 73 | | +-----------------------|--------|-----------------------+ | 74 | | | | | 75 | | v | | 76 | | +--------------------------+ +--------------------------+ | 77 | | | Compressor | | Decompressor | | 78 | | +--------------------------+ +--------------------------+ | 79 | | | ^ | 80 | | v | | 81 | | +--------------------------+ +--------------------------+ | 82 | | | Serializer | | Deserializer | | 83 | | +--------------------------+ +--------------------------+ | 84 | | | ^ | 85 | +---------------------------|--------|---------------------------+ 86 | | | 87 | v | 88 | 89 | Raw data 90 | 91 | */ 92 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | node-http2 2 | ========== 3 | 4 | An HTTP/2 ([RFC 7540](http://tools.ietf.org/html/rfc7540)) 5 | client and server implementation for node.js. 6 | 7 | ![Travis CI status](https://travis-ci.org/molnarg/node-http2.svg?branch=master) 8 | 9 | Installation 10 | ------------ 11 | 12 | ``` 13 | npm install http2 14 | ``` 15 | 16 | API 17 | --- 18 | 19 | The API is very similar to the [standard node.js HTTPS API](http://nodejs.org/api/https.html). The 20 | goal is the perfect API compatibility, with additional HTTP2 related extensions (like server push). 21 | 22 | Detailed API documentation is primarily maintained in the `lib/http.js` file and is [available in 23 | the wiki](https://github.com/molnarg/node-http2/wiki/Public-API) as well. 24 | 25 | Examples 26 | -------- 27 | 28 | ### Using as a server ### 29 | 30 | ```javascript 31 | var options = { 32 | key: fs.readFileSync('./example/localhost.key'), 33 | cert: fs.readFileSync('./example/localhost.crt') 34 | }; 35 | 36 | require('http2').createServer(options, function(request, response) { 37 | response.end('Hello world!'); 38 | }).listen(8080); 39 | ``` 40 | 41 | ### Using as a client ### 42 | 43 | ```javascript 44 | require('http2').get('https://localhost:8080/', function(response) { 45 | response.pipe(process.stdout); 46 | }); 47 | ``` 48 | 49 | ### Simple static file server ### 50 | 51 | An simple static file server serving up content from its own directory is available in the `example` 52 | directory. Running the server: 53 | 54 | ```bash 55 | $ node ./example/server.js 56 | ``` 57 | 58 | ### Simple command line client ### 59 | 60 | An example client is also available. Downloading the server's own source code from the server: 61 | 62 | ```bash 63 | $ node ./example/client.js 'https://localhost:8080/server.js' >/tmp/server.js 64 | ``` 65 | 66 | ### Server push ### 67 | 68 | For a server push example, see the source code of the example 69 | [server](https://github.com/molnarg/node-http2/blob/master/example/server.js) and 70 | [client](https://github.com/molnarg/node-http2/blob/master/example/client.js). 71 | 72 | Status 73 | ------ 74 | 75 | * ALPN is only supported in node.js >= 5.0 76 | * Upgrade mechanism to start HTTP/2 over unencrypted channel is not implemented yet 77 | (issue [#4](https://github.com/molnarg/node-http2/issues/4)) 78 | * Other minor features found in 79 | [this list](https://github.com/molnarg/node-http2/issues?labels=feature) are not implemented yet 80 | 81 | Development 82 | ----------- 83 | 84 | ### Development dependencies ### 85 | 86 | There's a few library you will need to have installed to do anything described in the following 87 | sections. After installing/cloning node-http2, run `npm install` in its directory to install 88 | development dependencies. 89 | 90 | Used libraries: 91 | 92 | * [mocha](http://visionmedia.github.io/mocha/) for tests 93 | * [chai](http://chaijs.com/) for assertions 94 | * [istanbul](https://github.com/gotwarlost/istanbul) for code coverage analysis 95 | * [docco](http://jashkenas.github.io/docco/) for developer documentation 96 | * [bunyan](https://github.com/trentm/node-bunyan) for logging 97 | 98 | For pretty printing logs, you will also need a global install of bunyan (`npm install -g bunyan`). 99 | 100 | ### Developer documentation ### 101 | 102 | The developer documentation is generated from the source code using docco and can be viewed online 103 | [here](http://molnarg.github.io/node-http2/doc/). If you'd like to have an offline copy, just run 104 | `npm run-script doc`. 105 | 106 | ### Running the tests ### 107 | 108 | It's easy, just run `npm test`. The tests are written in BDD style, so they are a good starting 109 | point to understand the code. 110 | 111 | ### Test coverage ### 112 | 113 | To generate a code coverage report, run `npm test --coverage` (which runs very slowly, be patient). 114 | Code coverage summary as of version 3.0.1: 115 | ``` 116 | Statements : 92.09% ( 1759/1910 ) 117 | Branches : 82.56% ( 696/843 ) 118 | Functions : 91.38% ( 212/232 ) 119 | Lines : 92.17% ( 1753/1902 ) 120 | ``` 121 | 122 | There's a hosted version of the detailed (line-by-line) coverage report 123 | [here](http://molnarg.github.io/node-http2/coverage/lcov-report/lib/). 124 | 125 | ### Logging ### 126 | 127 | Logging is turned off by default. You can turn it on by passing a bunyan logger as `log` option when 128 | creating a server or agent. 129 | 130 | When using the example server or client, it's very easy to turn logging on: set the `HTTP2_LOG` 131 | environment variable to `fatal`, `error`, `warn`, `info`, `debug` or `trace` (the logging level). 132 | To log every single incoming and outgoing data chunk, use `HTTP2_LOG_DATA=1` besides 133 | `HTTP2_LOG=trace`. Log output goes to the standard error output. If the standard error is redirected 134 | into a file, then the log output is in bunyan's JSON format for easier post-mortem analysis. 135 | 136 | Running the example server and client with `info` level logging output: 137 | 138 | ```bash 139 | $ HTTP2_LOG=info node ./example/server.js 140 | ``` 141 | 142 | ```bash 143 | $ HTTP2_LOG=info node ./example/client.js 'https://localhost:8080/server.js' >/dev/null 144 | ``` 145 | 146 | Contributors 147 | ------------ 148 | 149 | The co-maintainer of the project is [Nick Hurley](https://github.com/todesschaf). 150 | 151 | Code contributions are always welcome! People who contributed to node-http2 so far: 152 | 153 | * [Nick Hurley](https://github.com/todesschaf) 154 | * [Mike Belshe](https://github.com/mbelshe) 155 | * [Yoshihiro Iwanaga](https://github.com/iwanaga) 156 | * [Igor Novikov](https://github.com/vsemogutor) 157 | * [James Willcox](https://github.com/snorp) 158 | * [David Björklund](https://github.com/kesla) 159 | * [Patrick McManus](https://github.com/mcmanus) 160 | 161 | Special thanks to Google for financing the development of this module as part of their [Summer of 162 | Code program](https://developers.google.com/open-source/soc/) (project: [HTTP/2 prototype server 163 | implementation](https://google-melange.appspot.com/gsoc/project/details/google/gsoc2013/molnarg/5818821692620800)), and 164 | Nick Hurley of Mozilla, my GSoC mentor, who helped with regular code review and technical advices. 165 | 166 | License 167 | ------- 168 | 169 | The MIT License 170 | 171 | Copyright (C) 2013 Gábor Molnár 172 | -------------------------------------------------------------------------------- /test/connection.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var util = require('./util'); 3 | 4 | var Connection = require('../lib/protocol/connection').Connection; 5 | 6 | var settings = { 7 | SETTINGS_MAX_CONCURRENT_STREAMS: 100, 8 | SETTINGS_INITIAL_WINDOW_SIZE: 100000 9 | }; 10 | 11 | var MAX_PRIORITY = Math.pow(2, 31) - 1; 12 | var MAX_RANDOM_PRIORITY = 10; 13 | 14 | function randomPriority() { 15 | return Math.floor(Math.random() * (MAX_RANDOM_PRIORITY + 1)); 16 | } 17 | 18 | function expectPriorityOrder(priorities) { 19 | priorities.forEach(function(bucket, priority) { 20 | bucket.forEach(function(stream) { 21 | expect(stream._priority).to.be.equal(priority); 22 | }); 23 | }); 24 | } 25 | 26 | describe('connection.js', function() { 27 | describe('Connection class', function() { 28 | describe('method ._insert(stream)', function() { 29 | it('should insert the stream in _streamPriorities in a place determined by stream._priority', function() { 30 | var streams = []; 31 | var connection = Object.create(Connection.prototype, { _streamPriorities: { value: streams }}); 32 | var streamCount = 10; 33 | 34 | for (var i = 0; i < streamCount; i++) { 35 | var stream = { _priority: randomPriority() }; 36 | connection._insert(stream, stream._priority); 37 | expect(connection._streamPriorities[stream._priority]).to.include(stream); 38 | } 39 | 40 | expectPriorityOrder(connection._streamPriorities); 41 | }); 42 | }); 43 | describe('method ._reprioritize(stream)', function() { 44 | it('should eject and then insert the stream in _streamPriorities in a place determined by stream._priority', function() { 45 | var streams = []; 46 | var connection = Object.create(Connection.prototype, { _streamPriorities: { value: streams }}); 47 | var streamCount = 10; 48 | var oldPriority, newPriority, stream; 49 | 50 | for (var i = 0; i < streamCount; i++) { 51 | oldPriority = randomPriority(); 52 | while ((newPriority = randomPriority()) === oldPriority); 53 | stream = { _priority: oldPriority }; 54 | connection._insert(stream, oldPriority); 55 | connection._reprioritize(stream, newPriority); 56 | stream._priority = newPriority; 57 | 58 | expect(connection._streamPriorities[newPriority]).to.include(stream); 59 | expect(connection._streamPriorities[oldPriority] || []).to.not.include(stream); 60 | } 61 | 62 | expectPriorityOrder(streams); 63 | }); 64 | }); 65 | describe('invalid operation', function() { 66 | describe('unsolicited ping answer', function() { 67 | it('should be ignored', function() { 68 | var connection = new Connection(util.log, 1, settings); 69 | 70 | connection._receivePing({ 71 | stream: 0, 72 | type: 'PING', 73 | flags: { 74 | 'PONG': true 75 | }, 76 | data: new Buffer(8) 77 | }); 78 | }); 79 | }); 80 | }); 81 | }); 82 | describe('test scenario', function() { 83 | var c, s; 84 | beforeEach(function() { 85 | c = new Connection(util.log.child({ role: 'client' }), 1, settings); 86 | s = new Connection(util.log.child({ role: 'client' }), 2, settings); 87 | c.pipe(s).pipe(c); 88 | }); 89 | 90 | describe('connection setup', function() { 91 | it('should work as expected', function(done) { 92 | setTimeout(function() { 93 | // If there are no exception until this, then we're done 94 | done(); 95 | }, 10); 96 | }); 97 | }); 98 | describe('sending/receiving a request', function() { 99 | it('should work as expected', function(done) { 100 | // Request and response data 101 | var request_headers = { 102 | ':method': 'GET', 103 | ':path': '/' 104 | }; 105 | var request_data = new Buffer(0); 106 | var response_headers = { 107 | ':status': '200' 108 | }; 109 | var response_data = new Buffer('12345678', 'hex'); 110 | 111 | // Setting up server 112 | s.on('stream', function(server_stream) { 113 | server_stream.on('headers', function(headers) { 114 | expect(headers).to.deep.equal(request_headers); 115 | server_stream.headers(response_headers); 116 | server_stream.end(response_data); 117 | }); 118 | }); 119 | 120 | // Sending request 121 | var client_stream = c.createStream(); 122 | client_stream.headers(request_headers); 123 | client_stream.end(request_data); 124 | 125 | // Waiting for answer 126 | done = util.callNTimes(2, done); 127 | client_stream.on('headers', function(headers) { 128 | expect(headers).to.deep.equal(response_headers); 129 | done(); 130 | }); 131 | client_stream.on('data', function(data) { 132 | expect(data).to.deep.equal(response_data); 133 | done(); 134 | }); 135 | }); 136 | }); 137 | describe('server push', function() { 138 | it('should work as expected', function(done) { 139 | var request_headers = { ':method': 'get', ':path': '/' }; 140 | var response_headers = { ':status': '200' }; 141 | var push_request_headers = { ':method': 'get', ':path': '/x' }; 142 | var push_response_headers = { ':status': '200' }; 143 | var response_content = new Buffer(10); 144 | var push_content = new Buffer(10); 145 | 146 | done = util.callNTimes(5, done); 147 | 148 | s.on('stream', function(response) { 149 | response.headers(response_headers); 150 | 151 | var pushed = response.promise(push_request_headers); 152 | pushed.headers(push_response_headers); 153 | pushed.end(push_content); 154 | 155 | response.end(response_content); 156 | }); 157 | 158 | var request = c.createStream(); 159 | request.headers(request_headers); 160 | request.end(); 161 | request.on('headers', function(headers) { 162 | expect(headers).to.deep.equal(response_headers); 163 | done(); 164 | }); 165 | request.on('data', function(data) { 166 | expect(data).to.deep.equal(response_content); 167 | done(); 168 | }); 169 | request.on('promise', function(pushed, headers) { 170 | expect(headers).to.deep.equal(push_request_headers); 171 | pushed.on('headers', function(headers) { 172 | expect(headers).to.deep.equal(response_headers); 173 | done(); 174 | }); 175 | pushed.on('data', function(data) { 176 | expect(data).to.deep.equal(push_content); 177 | done(); 178 | }); 179 | pushed.on('end', done); 180 | }); 181 | }); 182 | }); 183 | describe('ping from client', function() { 184 | it('should work as expected', function(done) { 185 | c.ping(function() { 186 | done(); 187 | }); 188 | }); 189 | }); 190 | describe('ping from server', function() { 191 | it('should work as expected', function(done) { 192 | s.ping(function() { 193 | done(); 194 | }); 195 | }); 196 | }); 197 | describe('creating two streams and then using them in reverse order', function() { 198 | it('should not result in non-monotonous local ID ordering', function() { 199 | var s1 = c.createStream(); 200 | var s2 = c.createStream(); 201 | s2.headers({ ':method': 'get', ':path': '/' }); 202 | s1.headers({ ':method': 'get', ':path': '/' }); 203 | }); 204 | }); 205 | describe('creating two promises and then using them in reverse order', function() { 206 | it('should not result in non-monotonous local ID ordering', function(done) { 207 | s.on('stream', function(response) { 208 | response.headers({ ':status': '200' }); 209 | 210 | var p1 = s.createStream(); 211 | var p2 = s.createStream(); 212 | response.promise(p2, { ':method': 'get', ':path': '/p2' }); 213 | response.promise(p1, { ':method': 'get', ':path': '/p1' }); 214 | p2.headers({ ':status': '200' }); 215 | p1.headers({ ':status': '200' }); 216 | }); 217 | 218 | var request = c.createStream(); 219 | request.headers({ ':method': 'get', ':path': '/' }); 220 | 221 | done = util.callNTimes(2, done); 222 | request.on('promise', function() { 223 | done(); 224 | }); 225 | }); 226 | }); 227 | describe('closing the connection on one end', function() { 228 | it('should result in closed streams on both ends', function(done) { 229 | done = util.callNTimes(2, done); 230 | c.on('end', done); 231 | s.on('end', done); 232 | 233 | c.close(); 234 | }); 235 | }); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | Version history 2 | =============== 3 | 4 | ### 3.3.4 (2016-04-22) ### 5 | * More PR bugfixes (https://github.com/molnarg/node-http2/issues?q=milestone%3Av3.3.4) 6 | 7 | ### 3.3.3 (2016-04-21) ### 8 | 9 | * Bugfixes from pull requests (https://github.com/molnarg/node-http2/search?q=milestone%3Av3.3.3&type=Issues&utf8=%E2%9C%93) 10 | 11 | ### 3.3.2 (2016-01-11) ### 12 | 13 | * Fix an incompatibility with Firefox (issue 167) 14 | 15 | ### 3.3.1 (2016-01-11) ### 16 | 17 | * Fix some DoS bugs (issues 145, 146, 147, and 148) 18 | 19 | ### 3.3.0 (2016-01-10) ### 20 | 21 | * Bugfix updates from pull requests 22 | 23 | ### 3.2.0 (2015-02-19) ### 24 | 25 | * Update ALPN token to final RFC version (h2). 26 | * Update altsvc implementation to draft 06: [draft-ietf-httpbis-alt-svc-06] 27 | 28 | [draft-ietf-httpbis-altsvc-06]: http://tools.ietf.org/html/draft-ietf-httpbis-alt-svc-06 29 | 30 | ### 3.1.2 (2015-02-17) ### 31 | 32 | * Update the example server to have a safe push example. 33 | 34 | ### 3.1.1 (2015-01-29) ### 35 | 36 | * Bugfix release. 37 | * Fixes an issue sending a push promise that is large enough to fill the frame (#93). 38 | 39 | ### 3.1.0 (2014-12-11) ### 40 | 41 | * Upgrade to the latest draft: [draft-ietf-httpbis-http2-16] 42 | * This involves some state transition changes that are technically incompatible with draft-14. If you need to be assured to interop on -14, continue using 3.0.1 43 | 44 | [draft-ietf-httpbis-http2-16]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-16 45 | 46 | ### 3.0.1 (2014-11-20) ### 47 | 48 | * Bugfix release. 49 | * Fixed #81 and #87 50 | * Fixed a bug in flow control (without GitHub issue) 51 | 52 | ### 3.0.0 (2014-08-25) ### 53 | 54 | * Re-join node-http2 and node-http2-protocol into one repository 55 | * API Changes 56 | * The default versions of createServer, request, and get now enforce TLS-only 57 | * The raw versions of createServer, request, and get are now under http2.raw instead of http2 58 | * What was previously in the http2-protocol repository/module is now available under http2.protocol from this repo/module 59 | * http2-protocol.ImplementedVersion is now http2.protocol.VERSION (the ALPN token) 60 | 61 | ### 2.7.1 (2014-08-01) ### 62 | 63 | * Require protocol 0.14.1 (bugfix release) 64 | 65 | ### 2.7.0 (2014-07-31) ### 66 | 67 | * Upgrade to the latest draft: [draft-ietf-httpbis-http2-14] 68 | 69 | [draft-ietf-httpbis-http2-14]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-14 70 | 71 | ### 2.6.0 (2014-06-18) ### 72 | 73 | * Upgrade to the latest draft: [draft-ietf-httpbis-http2-13] 74 | 75 | [draft-ietf-httpbis-http2-13]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-13 76 | 77 | ### 2.5.3 (2014-06-15) ### 78 | 79 | * Exposing API to send ALTSVC frames 80 | 81 | ### 2.5.2 (2014-05-25) ### 82 | 83 | * Fix a bug that occurs when the ALPN negotiation is unsuccessful 84 | 85 | ### 2.5.1 (2014-05-25) ### 86 | 87 | * Support for node 0.11.x 88 | * New cipher suite priority list with comformant ciphers on the top (only available in node >=0.11.x) 89 | 90 | ### 2.5.0 (2014-04-24) ### 91 | 92 | * Upgrade to the latest draft: [draft-ietf-httpbis-http2-12] 93 | 94 | [draft-ietf-httpbis-http2-12]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-12 95 | 96 | ### 2.4.0 (2014-04-16) ### 97 | 98 | * Upgrade to the latest draft: [draft-ietf-httpbis-http2-11] 99 | 100 | [draft-ietf-httpbis-http2-11]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-11 101 | 102 | ### 2.3.0 (2014-03-12) ### 103 | 104 | * Upgrade to the latest draft: [draft-ietf-httpbis-http2-10] 105 | 106 | [draft-ietf-httpbis-http2-10]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-10 107 | 108 | ### 2.2.0 (2013-12-25) ### 109 | 110 | * Upgrade to the latest draft: [draft-ietf-httpbis-http2-09] 111 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-2.2.0.tar.gz) 112 | 113 | [draft-ietf-httpbis-http2-09]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-09 114 | 115 | ### 2.1.1 (2013-12-21) ### 116 | 117 | * Minor bugfix 118 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-2.1.1.tar.gz) 119 | 120 | ### 2.1.0 (2013-11-10) ### 121 | 122 | * Upgrade to the latest draft: [draft-ietf-httpbis-http2-07][draft-07] 123 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-2.1.0.tar.gz) 124 | 125 | [draft-07]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-07 126 | 127 | ### 2.0.0 (2013-11-09) ### 128 | 129 | * Splitting out everything that is not related to negotiating HTTP2 or the node-like HTTP API. 130 | These live in separate module from now on: 131 | [http2-protocol](https://github.com/molnarg/node-http2-protocol). 132 | * The only backwards incompatible change: the `Endpoint` class is not exported anymore. Use the 133 | http2-protocol module if you want to use this low level interface. 134 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-2.0.0.tar.gz) 135 | 136 | ### 1.0.1 (2013-10-14) ### 137 | 138 | * Support for ALPN if node supports it (currently needs a custom build) 139 | * Fix for a few small issues 140 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-1.0.1.tar.gz) 141 | 142 | ### 1.0.0 (2013-09-23) ### 143 | 144 | * Exporting Endpoint class 145 | * Support for 'filters' in Endpoint 146 | * The last time-based release 147 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-1.0.0.tar.gz) 148 | 149 | ### 0.4.1 (2013-09-15) ### 150 | 151 | * Major performance improvements 152 | * Minor improvements to error handling 153 | * [Blog post](http://gabor.molnar.es/blog/2013/09/15/gsoc-week-number-13/) 154 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.4.1.tar.gz) 155 | 156 | ### 0.4.0 (2013-09-09) ### 157 | 158 | * Upgrade to the latest draft: [draft-ietf-httpbis-http2-06][draft-06] 159 | * Support for HTTP trailers 160 | * Support for TLS SNI (Server Name Indication) 161 | * Improved stream scheduling algorithm 162 | * [Blog post](http://gabor.molnar.es/blog/2013/09/09/gsoc-week-number-12/) 163 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.4.0.tar.gz) 164 | 165 | [draft-06]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-06 166 | 167 | ### 0.3.1 (2013-09-03) ### 168 | 169 | * Lot of testing, bugfixes 170 | * [Blog post](http://gabor.molnar.es/blog/2013/09/03/gsoc-week-number-11/) 171 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.3.1.tar.gz) 172 | 173 | ### 0.3.0 (2013-08-27) ### 174 | 175 | * Support for prioritization 176 | * Small API compatibility improvements (compatibility with the standard node.js HTTP API) 177 | * Minor push API change 178 | * Ability to pass an external bunyan logger when creating a Server or Agent 179 | * [Blog post](http://gabor.molnar.es/blog/2013/08/27/gsoc-week-number-10/) 180 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.3.0.tar.gz) 181 | 182 | ### 0.2.1 (2013-08-20) ### 183 | 184 | * Fixing a flow control bug 185 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.2.1.tar.gz) 186 | 187 | ### 0.2.0 (2013-08-19) ### 188 | 189 | * Exposing server push in the public API 190 | * Connection pooling when operating as client 191 | * Much better API compatibility with the standard node.js HTTPS module 192 | * Logging improvements 193 | * [Blog post](http://gabor.molnar.es/blog/2013/08/19/gsoc-week-number-9/) 194 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.2.0.tar.gz) 195 | 196 | ### 0.1.1 (2013-08-12) ### 197 | 198 | * Lots of bugfixes 199 | * Proper flow control for outgoing frames 200 | * Basic flow control for incoming frames 201 | * [Blog post](http://gabor.molnar.es/blog/2013/08/12/gsoc-week-number-8/) 202 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.1.1.tar.gz) 203 | 204 | ### 0.1.0 (2013-08-06) ### 205 | 206 | * First release with public API (similar to the standard node HTTPS module) 207 | * Support for NPN negotiation (no ALPN or Upgrade yet) 208 | * Stream number limitation is in place 209 | * Push streams works but not exposed yet in the public API 210 | * [Blog post](http://gabor.molnar.es/blog/2013/08/05/gsoc-week-number-6-and-number-7/) 211 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.1.0.tar.gz) 212 | 213 | ### 0.0.6 (2013-07-19) ### 214 | 215 | * `Connection` and `Endpoint` classes are usable, but not yet ready 216 | * Addition of an exmaple server and client 217 | * Using [istanbul](https://github.com/gotwarlost/istanbul) for measuring code coverage 218 | * [Blog post](http://gabor.molnar.es/blog/2013/07/19/gsoc-week-number-5/) 219 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.6.tar.gz) 220 | 221 | ### 0.0.5 (2013-07-14) ### 222 | 223 | * `Stream` class is done 224 | * Public API stubs are in place 225 | * [Blog post](http://gabor.molnar.es/blog/2013/07/14/gsoc-week-number-4/) 226 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.5.tar.gz) 227 | 228 | ### 0.0.4 (2013-07-08) ### 229 | 230 | * Added logging 231 | * Started `Stream` class implementation 232 | * [Blog post](http://gabor.molnar.es/blog/2013/07/08/gsoc-week-number-3/) 233 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.4.tar.gz) 234 | 235 | ### 0.0.3 (2013-07-03) ### 236 | 237 | * Header compression is ready 238 | * [Blog post](http://gabor.molnar.es/blog/2013/07/03/the-http-slash-2-header-compression-implementation-of-node-http2/) 239 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.3.tar.gz) 240 | 241 | ### 0.0.2 (2013-07-01) ### 242 | 243 | * Frame serialization and deserialization ready and updated to match the newest spec 244 | * Header compression implementation started 245 | * [Blog post](http://gabor.molnar.es/blog/2013/07/01/gsoc-week-number-2/) 246 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.2.tar.gz) 247 | 248 | ### 0.0.1 (2013-06-23) ### 249 | 250 | * Frame serialization and deserialization largely done 251 | * [Blog post](http://gabor.molnar.es/blog/2013/06/23/gsoc-week-number-1/) 252 | * [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.1.tar.gz) 253 | -------------------------------------------------------------------------------- /test/flow.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var util = require('./util'); 3 | 4 | var Flow = require('../lib/protocol/flow').Flow; 5 | 6 | var MAX_PAYLOAD_SIZE = 4096; 7 | 8 | function createFlow(log) { 9 | var flowControlId = util.random(10, 100); 10 | var flow = new Flow(flowControlId); 11 | flow._log = util.log.child(log || {}); 12 | return flow; 13 | } 14 | 15 | describe('flow.js', function() { 16 | describe('Flow class', function() { 17 | var flow; 18 | beforeEach(function() { 19 | flow = createFlow(); 20 | }); 21 | 22 | describe('._receive(frame, callback) method', function() { 23 | it('is called when there\'s a frame in the input buffer to be consumed', function(done) { 24 | var frame = { type: 'PRIORITY', flags: {}, priority: 1 }; 25 | flow._receive = function _receive(receivedFrame, callback) { 26 | expect(receivedFrame).to.equal(frame); 27 | callback(); 28 | }; 29 | flow.write(frame, done); 30 | }); 31 | it('has to be overridden by the child class, otherwise it throws', function() { 32 | expect(flow._receive.bind(flow)).to.throw(Error); 33 | }); 34 | }); 35 | describe('._send() method', function() { 36 | it('is called when the output buffer should be filled with more frames and the flow' + 37 | 'control queue is empty', function() { 38 | var notFlowControlledFrame = { type: 'PRIORITY', flags: {}, priority: 1 }; 39 | flow._send = function _send() { 40 | this.push(notFlowControlledFrame); 41 | }; 42 | expect(flow.read()).to.equal(notFlowControlledFrame); 43 | 44 | flow._window = 0; 45 | flow._queue.push({ type: 'DATA', flags: {}, data: { length: 1 } }); 46 | var frame = flow.read(); 47 | while (frame.type === notFlowControlledFrame.type) frame = flow.read(); 48 | expect(frame.type).to.equal('BLOCKED'); 49 | expect(flow.read()).to.equal(null); 50 | }); 51 | it('has to be overridden by the child class, otherwise it throws', function() { 52 | expect(flow._send.bind(flow)).to.throw(Error); 53 | }); 54 | }); 55 | describe('._increaseWindow(size) method', function() { 56 | it('should increase `this._window` by `size`', function() { 57 | flow._send = util.noop; 58 | flow._window = 0; 59 | 60 | var increase1 = util.random(0,100); 61 | var increase2 = util.random(0,100); 62 | flow._increaseWindow(increase1); 63 | flow._increaseWindow(increase2); 64 | expect(flow._window).to.equal(increase1 + increase2); 65 | 66 | flow._increaseWindow(Infinity); 67 | expect(flow._window).to.equal(Infinity); 68 | }); 69 | it('should emit error when increasing with a finite `size` when `_window` is infinite', function() { 70 | flow._send = util.noop; 71 | flow._increaseWindow(Infinity); 72 | var increase = util.random(1,100); 73 | 74 | expect(flow._increaseWindow.bind(flow, increase)).to.throw('Uncaught, unspecified "error" event.'); 75 | }); 76 | it('should emit error when `_window` grows over the window limit', function() { 77 | var WINDOW_SIZE_LIMIT = Math.pow(2, 31) - 1; 78 | flow._send = util.noop; 79 | flow._window = 0; 80 | 81 | flow._increaseWindow(WINDOW_SIZE_LIMIT); 82 | expect(flow._increaseWindow.bind(flow, 1)).to.throw('Uncaught, unspecified "error" event.'); 83 | 84 | }); 85 | }); 86 | describe('.read() method', function() { 87 | describe('when the flow control queue is not empty', function() { 88 | it('should return the first item in the queue if the window is enough', function() { 89 | var priorityFrame = { type: 'PRIORITY', flags: {}, priority: 1 }; 90 | var dataFrame = { type: 'DATA', flags: {}, data: { length: 10 } }; 91 | flow._send = util.noop; 92 | flow._window = 10; 93 | flow._queue = [priorityFrame, dataFrame]; 94 | 95 | expect(flow.read()).to.equal(priorityFrame); 96 | expect(flow.read()).to.equal(dataFrame); 97 | }); 98 | it('should also split DATA frames when needed', function() { 99 | var buffer = new Buffer(10); 100 | var dataFrame = { type: 'DATA', flags: {}, stream: util.random(0, 100), data: buffer }; 101 | flow._send = util.noop; 102 | flow._window = 5; 103 | flow._queue = [dataFrame]; 104 | 105 | var expectedFragment = { flags: {}, type: 'DATA', stream: dataFrame.stream, data: buffer.slice(0,5) }; 106 | expect(flow.read()).to.deep.equal(expectedFragment); 107 | expect(dataFrame.data).to.deep.equal(buffer.slice(5)); 108 | }); 109 | }); 110 | }); 111 | describe('.push(frame) method', function() { 112 | it('should push `frame` into the output queue or the flow control queue', function() { 113 | var priorityFrame = { type: 'PRIORITY', flags: {}, priority: 1 }; 114 | var dataFrame = { type: 'DATA', flags: {}, data: { length: 10 } }; 115 | flow._window = 10; 116 | 117 | flow.push(dataFrame); // output queue 118 | flow.push(dataFrame); // flow control queue, because of depleted window 119 | flow.push(priorityFrame); // flow control queue, because it's not empty 120 | 121 | expect(flow.read()).to.be.equal(dataFrame); 122 | expect(flow._queue[0]).to.be.equal(dataFrame); 123 | expect(flow._queue[1]).to.be.equal(priorityFrame); 124 | }); 125 | }); 126 | describe('.write() method', function() { 127 | it('call with a DATA frame should trigger sending WINDOW_UPDATE if remote flow control is not' + 128 | 'disabled', function(done) { 129 | flow._window = 100; 130 | flow._send = util.noop; 131 | flow._receive = function(frame, callback) { 132 | callback(); 133 | }; 134 | 135 | var buffer = new Buffer(util.random(10, 100)); 136 | flow.write({ type: 'DATA', flags: {}, data: buffer }); 137 | flow.once('readable', function() { 138 | expect(flow.read()).to.be.deep.equal({ 139 | type: 'WINDOW_UPDATE', 140 | flags: {}, 141 | stream: flow._flowControlId, 142 | window_size: buffer.length 143 | }); 144 | done(); 145 | }); 146 | }); 147 | }); 148 | }); 149 | describe('test scenario', function() { 150 | var flow1, flow2; 151 | beforeEach(function() { 152 | flow1 = createFlow({ flow: 1 }); 153 | flow2 = createFlow({ flow: 2 }); 154 | flow1._flowControlId = flow2._flowControlId; 155 | flow1._send = flow2._send = util.noop; 156 | flow1._receive = flow2._receive = function(frame, callback) { callback(); }; 157 | }); 158 | 159 | describe('sending a large data stream', function() { 160 | it('should work as expected', function(done) { 161 | // Sender side 162 | var frameNumber = util.random(5, 8); 163 | var input = []; 164 | flow1._send = function _send() { 165 | if (input.length >= frameNumber) { 166 | this.push({ type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(0) }); 167 | this.push(null); 168 | } else { 169 | var buffer = new Buffer(util.random(1000, 100000)); 170 | input.push(buffer); 171 | this.push({ type: 'DATA', flags: {}, data: buffer }); 172 | } 173 | }; 174 | 175 | // Receiver side 176 | var output = []; 177 | flow2._receive = function _receive(frame, callback) { 178 | if (frame.type === 'DATA') { 179 | expect(frame.data.length).to.be.lte(MAX_PAYLOAD_SIZE); 180 | output.push(frame.data); 181 | } 182 | if (frame.flags.END_STREAM) { 183 | this.emit('end_stream'); 184 | } 185 | callback(); 186 | }; 187 | 188 | // Checking results 189 | flow2.on('end_stream', function() { 190 | input = util.concat(input); 191 | output = util.concat(output); 192 | 193 | expect(input).to.deep.equal(output); 194 | 195 | done(); 196 | }); 197 | 198 | // Start piping 199 | flow1.pipe(flow2).pipe(flow1); 200 | }); 201 | }); 202 | 203 | describe('when running out of window', function() { 204 | it('should send a BLOCKED frame', function(done) { 205 | // Sender side 206 | var frameNumber = util.random(5, 8); 207 | var input = []; 208 | flow1._send = function _send() { 209 | if (input.length >= frameNumber) { 210 | this.push({ type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(0) }); 211 | this.push(null); 212 | } else { 213 | var buffer = new Buffer(util.random(1000, 100000)); 214 | input.push(buffer); 215 | this.push({ type: 'DATA', flags: {}, data: buffer }); 216 | } 217 | }; 218 | 219 | // Receiver side 220 | // Do not send WINDOW_UPDATESs except when the other side sends BLOCKED 221 | var output = []; 222 | flow2._restoreWindow = util.noop; 223 | flow2._receive = function _receive(frame, callback) { 224 | if (frame.type === 'DATA') { 225 | expect(frame.data.length).to.be.lte(MAX_PAYLOAD_SIZE); 226 | output.push(frame.data); 227 | } 228 | if (frame.flags.END_STREAM) { 229 | this.emit('end_stream'); 230 | } 231 | if (frame.type === 'BLOCKED') { 232 | setTimeout(function() { 233 | this._push({ 234 | type: 'WINDOW_UPDATE', 235 | flags: {}, 236 | stream: this._flowControlId, 237 | window_size: this._received 238 | }); 239 | this._received = 0; 240 | }.bind(this), 20); 241 | } 242 | callback(); 243 | }; 244 | 245 | // Checking results 246 | flow2.on('end_stream', function() { 247 | input = util.concat(input); 248 | output = util.concat(output); 249 | 250 | expect(input).to.deep.equal(output); 251 | 252 | done(); 253 | }); 254 | 255 | // Start piping 256 | flow1.pipe(flow2).pipe(flow1); 257 | }); 258 | }); 259 | }); 260 | }); 261 | -------------------------------------------------------------------------------- /lib/protocol/endpoint.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | var Serializer = require('./framer').Serializer; 4 | var Deserializer = require('./framer').Deserializer; 5 | var Compressor = require('./compressor').Compressor; 6 | var Decompressor = require('./compressor').Decompressor; 7 | var Connection = require('./connection').Connection; 8 | var Duplex = require('stream').Duplex; 9 | var Transform = require('stream').Transform; 10 | 11 | exports.Endpoint = Endpoint; 12 | 13 | // The Endpoint class 14 | // ================== 15 | 16 | // Public API 17 | // ---------- 18 | 19 | // - **new Endpoint(log, role, settings, filters)**: create a new Endpoint. 20 | // 21 | // - `log`: bunyan logger of the parent 22 | // - `role`: 'CLIENT' or 'SERVER' 23 | // - `settings`: initial HTTP/2 settings 24 | // - `filters`: a map of functions that filter the traffic between components (for debugging or 25 | // intentional failure injection). 26 | // 27 | // Filter functions get three arguments: 28 | // 1. `frame`: the current frame 29 | // 2. `forward(frame)`: function that can be used to forward a frame to the next component 30 | // 3. `done()`: callback to signal the end of the filter process 31 | // 32 | // Valid filter names and their position in the stack: 33 | // - `beforeSerialization`: after compression, before serialization 34 | // - `beforeCompression`: after multiplexing, before compression 35 | // - `afterDeserialization`: after deserialization, before decompression 36 | // - `afterDecompression`: after decompression, before multiplexing 37 | // 38 | // * **Event: 'stream' (Stream)**: 'stream' event forwarded from the underlying Connection 39 | // 40 | // * **Event: 'error' (type)**: signals an error 41 | // 42 | // * **createStream(): Stream**: initiate a new stream (forwarded to the underlying Connection) 43 | // 44 | // * **close([error])**: close the connection with an error code 45 | 46 | // Constructor 47 | // ----------- 48 | 49 | // The process of initialization: 50 | function Endpoint(log, role, settings, filters) { 51 | Duplex.call(this); 52 | 53 | // * Initializing logging infrastructure 54 | this._log = log.child({ component: 'endpoint', e: this }); 55 | 56 | // * First part of the handshake process: sending and receiving the client connection header 57 | // prelude. 58 | assert((role === 'CLIENT') || role === 'SERVER'); 59 | if (role === 'CLIENT') { 60 | this._writePrelude(); 61 | } else { 62 | this._readPrelude(); 63 | } 64 | 65 | // * Initialization of component. This includes the second part of the handshake process: 66 | // sending the first SETTINGS frame. This is done by the connection class right after 67 | // initialization. 68 | this._initializeDataFlow(role, settings, filters || {}); 69 | 70 | // * Initialization of management code. 71 | this._initializeManagement(); 72 | 73 | // * Initializing error handling. 74 | this._initializeErrorHandling(); 75 | } 76 | Endpoint.prototype = Object.create(Duplex.prototype, { constructor: { value: Endpoint } }); 77 | 78 | // Handshake 79 | // --------- 80 | 81 | var CLIENT_PRELUDE = new Buffer('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'); 82 | 83 | // Writing the client header is simple and synchronous. 84 | Endpoint.prototype._writePrelude = function _writePrelude() { 85 | this._log.debug('Sending the client connection header prelude.'); 86 | this.push(CLIENT_PRELUDE); 87 | }; 88 | 89 | // The asynchronous process of reading the client header: 90 | Endpoint.prototype._readPrelude = function _readPrelude() { 91 | // * progress in the header is tracker using a `cursor` 92 | var cursor = 0; 93 | 94 | // * `_write` is temporarily replaced by the comparator function 95 | this._write = function _temporalWrite(chunk, encoding, done) { 96 | // * which compares the stored header with the current `chunk` byte by byte and emits the 97 | // 'error' event if there's a byte that doesn't match 98 | var offset = cursor; 99 | while(cursor < CLIENT_PRELUDE.length && (cursor - offset) < chunk.length) { 100 | if (CLIENT_PRELUDE[cursor] !== chunk[cursor - offset]) { 101 | this._log.fatal({ cursor: cursor, offset: offset, chunk: chunk }, 102 | 'Client connection header prelude does not match.'); 103 | this._error('handshake', 'PROTOCOL_ERROR'); 104 | return; 105 | } 106 | cursor += 1; 107 | } 108 | 109 | // * if the whole header is over, and there were no error then restore the original `_write` 110 | // and call it with the remaining part of the current chunk 111 | if (cursor === CLIENT_PRELUDE.length) { 112 | this._log.debug('Successfully received the client connection header prelude.'); 113 | delete this._write; 114 | chunk = chunk.slice(cursor - offset); 115 | this._write(chunk, encoding, done); 116 | } 117 | }; 118 | }; 119 | 120 | // Data flow 121 | // --------- 122 | 123 | // +---------------------------------------------+ 124 | // | | 125 | // | +-------------------------------------+ | 126 | // | | +---------+ +---------+ +---------+ | | 127 | // | | | stream1 | | stream2 | | ... | | | 128 | // | | +---------+ +---------+ +---------+ | | 129 | // | | connection | | 130 | // | +-------------------------------------+ | 131 | // | | ^ | 132 | // | pipe | | pipe | 133 | // | v | | 134 | // | +------------------+------------------+ | 135 | // | | compressor | decompressor | | 136 | // | +------------------+------------------+ | 137 | // | | ^ | 138 | // | pipe | | pipe | 139 | // | v | | 140 | // | +------------------+------------------+ | 141 | // | | serializer | deserializer | | 142 | // | +------------------+------------------+ | 143 | // | | ^ | 144 | // | _read() | | _write() | 145 | // | v | | 146 | // | +------------+ +-----------+ | 147 | // | |output queue| |input queue| | 148 | // +------+------------+-----+-----------+-------+ 149 | // | ^ 150 | // read() | | write() 151 | // v | 152 | 153 | function createTransformStream(filter) { 154 | var transform = new Transform({ objectMode: true }); 155 | var push = transform.push.bind(transform); 156 | transform._transform = function(frame, encoding, done) { 157 | filter(frame, push, done); 158 | }; 159 | return transform; 160 | } 161 | 162 | function pipeAndFilter(stream1, stream2, filter) { 163 | if (filter) { 164 | stream1.pipe(createTransformStream(filter)).pipe(stream2); 165 | } else { 166 | stream1.pipe(stream2); 167 | } 168 | } 169 | 170 | Endpoint.prototype._initializeDataFlow = function _initializeDataFlow(role, settings, filters) { 171 | var firstStreamId, compressorRole, decompressorRole; 172 | if (role === 'CLIENT') { 173 | firstStreamId = 1; 174 | compressorRole = 'REQUEST'; 175 | decompressorRole = 'RESPONSE'; 176 | } else { 177 | firstStreamId = 2; 178 | compressorRole = 'RESPONSE'; 179 | decompressorRole = 'REQUEST'; 180 | } 181 | 182 | this._serializer = new Serializer(this._log); 183 | this._deserializer = new Deserializer(this._log); 184 | this._compressor = new Compressor(this._log, compressorRole); 185 | this._decompressor = new Decompressor(this._log, decompressorRole); 186 | this._connection = new Connection(this._log, firstStreamId, settings); 187 | 188 | pipeAndFilter(this._connection, this._compressor, filters.beforeCompression); 189 | pipeAndFilter(this._compressor, this._serializer, filters.beforeSerialization); 190 | pipeAndFilter(this._deserializer, this._decompressor, filters.afterDeserialization); 191 | pipeAndFilter(this._decompressor, this._connection, filters.afterDecompression); 192 | 193 | this._connection.on('ACKNOWLEDGED_SETTINGS_HEADER_TABLE_SIZE', 194 | this._decompressor.setTableSizeLimit.bind(this._decompressor)); 195 | this._connection.on('RECEIVING_SETTINGS_HEADER_TABLE_SIZE', 196 | this._compressor.setTableSizeLimit.bind(this._compressor)); 197 | }; 198 | 199 | var noread = {}; 200 | Endpoint.prototype._read = function _read() { 201 | this._readableState.sync = true; 202 | var moreNeeded = noread, chunk; 203 | while (moreNeeded && (chunk = this._serializer.read())) { 204 | moreNeeded = this.push(chunk); 205 | } 206 | if (moreNeeded === noread) { 207 | this._serializer.once('readable', this._read.bind(this)); 208 | } 209 | this._readableState.sync = false; 210 | }; 211 | 212 | Endpoint.prototype._write = function _write(chunk, encoding, done) { 213 | this._deserializer.write(chunk, encoding, done); 214 | }; 215 | 216 | // Management 217 | // -------------- 218 | 219 | Endpoint.prototype._initializeManagement = function _initializeManagement() { 220 | this._connection.on('stream', this.emit.bind(this, 'stream')); 221 | }; 222 | 223 | Endpoint.prototype.createStream = function createStream() { 224 | return this._connection.createStream(); 225 | }; 226 | 227 | // Error handling 228 | // -------------- 229 | 230 | Endpoint.prototype._initializeErrorHandling = function _initializeErrorHandling() { 231 | this._serializer.on('error', this._error.bind(this, 'serializer')); 232 | this._deserializer.on('error', this._error.bind(this, 'deserializer')); 233 | this._compressor.on('error', this._error.bind(this, 'compressor')); 234 | this._decompressor.on('error', this._error.bind(this, 'decompressor')); 235 | this._connection.on('error', this._error.bind(this, 'connection')); 236 | 237 | this._connection.on('peerError', this.emit.bind(this, 'peerError')); 238 | }; 239 | 240 | Endpoint.prototype._error = function _error(component, error) { 241 | this._log.fatal({ source: component, message: error }, 'Fatal error, closing connection'); 242 | this.close(error); 243 | setImmediate(this.emit.bind(this, 'error', error)); 244 | }; 245 | 246 | Endpoint.prototype.close = function close(error) { 247 | this._connection.close(error); 248 | }; 249 | 250 | // Bunyan serializers 251 | // ------------------ 252 | 253 | exports.serializers = {}; 254 | 255 | var nextId = 0; 256 | exports.serializers.e = function(endpoint) { 257 | if (!('id' in endpoint)) { 258 | endpoint.id = nextId; 259 | nextId += 1; 260 | } 261 | return endpoint.id; 262 | }; 263 | -------------------------------------------------------------------------------- /test/framer.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var util = require('./util'); 3 | 4 | var framer = require('../lib/protocol/framer'); 5 | var Serializer = framer.Serializer; 6 | var Deserializer = framer.Deserializer; 7 | 8 | var frame_types = { 9 | DATA: ['data'], 10 | HEADERS: ['priority_information', 'data'], 11 | PRIORITY: ['priority_information'], 12 | RST_STREAM: ['error'], 13 | SETTINGS: ['settings'], 14 | PUSH_PROMISE: ['promised_stream', 'data'], 15 | PING: ['data'], 16 | GOAWAY: ['last_stream', 'error'], 17 | WINDOW_UPDATE: ['window_size'], 18 | CONTINUATION: ['data'], 19 | ALTSVC: ['protocolID', 'host', 'port', 'origin', 'maxAge'] 20 | }; 21 | 22 | var test_frames = [{ 23 | frame: { 24 | type: 'DATA', 25 | flags: { END_STREAM: false, RESERVED2: false, RESERVED4: false, 26 | PADDED: false }, 27 | stream: 10, 28 | 29 | data: new Buffer('12345678', 'hex') 30 | }, 31 | // length + type + flags + stream + content 32 | buffer: new Buffer('000004' + '00' + '00' + '0000000A' + '12345678', 'hex') 33 | 34 | }, { 35 | frame: { 36 | type: 'HEADERS', 37 | flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, 38 | PADDED: false, RESERVED5: false, PRIORITY: false }, 39 | stream: 15, 40 | 41 | data: new Buffer('12345678', 'hex') 42 | }, 43 | buffer: new Buffer('000004' + '01' + '00' + '0000000F' + '12345678', 'hex') 44 | 45 | }, { 46 | frame: { 47 | type: 'HEADERS', 48 | flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, 49 | PADDED: false, RESERVED5: false, PRIORITY: true }, 50 | stream: 15, 51 | priorityDependency: 10, 52 | priorityWeight: 5, 53 | exclusiveDependency: false, 54 | 55 | data: new Buffer('12345678', 'hex') 56 | }, 57 | buffer: new Buffer('000009' + '01' + '20' + '0000000F' + '0000000A' + '05' + '12345678', 'hex') 58 | 59 | 60 | }, { 61 | frame: { 62 | type: 'HEADERS', 63 | flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, 64 | PADDED: false, RESERVED5: false, PRIORITY: true }, 65 | stream: 15, 66 | priorityDependency: 10, 67 | priorityWeight: 5, 68 | exclusiveDependency: true, 69 | 70 | data: new Buffer('12345678', 'hex') 71 | }, 72 | buffer: new Buffer('000009' + '01' + '20' + '0000000F' + '8000000A' + '05' + '12345678', 'hex') 73 | 74 | }, { 75 | frame: { 76 | type: 'PRIORITY', 77 | flags: { }, 78 | stream: 10, 79 | 80 | priorityDependency: 9, 81 | priorityWeight: 5, 82 | exclusiveDependency: false 83 | }, 84 | buffer: new Buffer('000005' + '02' + '00' + '0000000A' + '00000009' + '05', 'hex') 85 | 86 | }, { 87 | frame: { 88 | type: 'PRIORITY', 89 | flags: { }, 90 | stream: 10, 91 | 92 | priorityDependency: 9, 93 | priorityWeight: 5, 94 | exclusiveDependency: true 95 | }, 96 | buffer: new Buffer('000005' + '02' + '00' + '0000000A' + '80000009' + '05', 'hex') 97 | 98 | }, { 99 | frame: { 100 | type: 'RST_STREAM', 101 | flags: { }, 102 | stream: 10, 103 | 104 | error: 'INTERNAL_ERROR' 105 | }, 106 | buffer: new Buffer('000004' + '03' + '00' + '0000000A' + '00000002', 'hex') 107 | 108 | }, { 109 | frame: { 110 | type: 'SETTINGS', 111 | flags: { ACK: false }, 112 | stream: 10, 113 | 114 | settings: { 115 | SETTINGS_HEADER_TABLE_SIZE: 0x12345678, 116 | SETTINGS_ENABLE_PUSH: true, 117 | SETTINGS_MAX_CONCURRENT_STREAMS: 0x01234567, 118 | SETTINGS_INITIAL_WINDOW_SIZE: 0x89ABCDEF, 119 | SETTINGS_MAX_FRAME_SIZE: 0x00010000 120 | } 121 | }, 122 | buffer: new Buffer('00001E' + '04' + '00' + '0000000A' + '0001' + '12345678' + 123 | '0002' + '00000001' + 124 | '0003' + '01234567' + 125 | '0004' + '89ABCDEF' + 126 | '0005' + '00010000', 'hex') 127 | 128 | }, { 129 | frame: { 130 | type: 'PUSH_PROMISE', 131 | flags: { RESERVED1: false, RESERVED2: false, END_PUSH_PROMISE: false, 132 | PADDED: false }, 133 | stream: 15, 134 | 135 | promised_stream: 3, 136 | data: new Buffer('12345678', 'hex') 137 | }, 138 | buffer: new Buffer('000008' + '05' + '00' + '0000000F' + '00000003' + '12345678', 'hex') 139 | 140 | }, { 141 | frame: { 142 | type: 'PING', 143 | flags: { ACK: false }, 144 | stream: 15, 145 | 146 | data: new Buffer('1234567887654321', 'hex') 147 | }, 148 | buffer: new Buffer('000008' + '06' + '00' + '0000000F' + '1234567887654321', 'hex') 149 | 150 | }, { 151 | frame: { 152 | type: 'GOAWAY', 153 | flags: { }, 154 | stream: 10, 155 | 156 | last_stream: 0x12345678, 157 | error: 'PROTOCOL_ERROR' 158 | }, 159 | buffer: new Buffer('000008' + '07' + '00' + '0000000A' + '12345678' + '00000001', 'hex') 160 | 161 | }, { 162 | frame: { 163 | type: 'WINDOW_UPDATE', 164 | flags: { }, 165 | stream: 10, 166 | 167 | window_size: 0x12345678 168 | }, 169 | buffer: new Buffer('000004' + '08' + '00' + '0000000A' + '12345678', 'hex') 170 | }, { 171 | frame: { 172 | type: 'CONTINUATION', 173 | flags: { RESERVED1: false, RESERVED2: false, END_HEADERS: true }, 174 | stream: 10, 175 | 176 | data: new Buffer('12345678', 'hex') 177 | }, 178 | // length + type + flags + stream + content 179 | buffer: new Buffer('000004' + '09' + '04' + '0000000A' + '12345678', 'hex') 180 | }, { 181 | frame: { 182 | type: 'ALTSVC', 183 | flags: { }, 184 | stream: 0, 185 | 186 | maxAge: 31536000, 187 | port: 4443, 188 | protocolID: "h2", 189 | host: "altsvc.example.com", 190 | origin: "" 191 | }, 192 | buffer: new Buffer(new Buffer('00002B' + '0A' + '00' + '00000000' + '0000', 'hex') + new Buffer('h2="altsvc.example.com:4443"; ma=31536000', 'ascii')) 193 | }, { 194 | frame: { 195 | type: 'ALTSVC', 196 | flags: { }, 197 | stream: 0, 198 | 199 | maxAge: 31536000, 200 | port: 4443, 201 | protocolID: "h2", 202 | host: "altsvc.example.com", 203 | origin: "https://onlyme.example.com" 204 | }, 205 | buffer: new Buffer(new Buffer('000045' + '0A' + '00' + '00000000' + '001A', 'hex') + new Buffer('https://onlyme.example.comh2="altsvc.example.com:4443"; ma=31536000', 'ascii')) 206 | 207 | }, { 208 | frame: { 209 | type: 'BLOCKED', 210 | flags: { }, 211 | stream: 10 212 | }, 213 | buffer: new Buffer('000000' + '0B' + '00' + '0000000A', 'hex') 214 | }]; 215 | 216 | var deserializer_test_frames = test_frames.slice(0); 217 | var padded_test_frames = [{ 218 | frame: { 219 | type: 'DATA', 220 | flags: { END_STREAM: false, RESERVED2: false, RESERVED4: false, 221 | PADDED: true }, 222 | stream: 10, 223 | data: new Buffer('12345678', 'hex') 224 | }, 225 | // length + type + flags + stream + pad length + content + padding 226 | buffer: new Buffer('00000B' + '00' + '08' + '0000000A' + '06' + '12345678' + '000000000000', 'hex') 227 | 228 | }, { 229 | frame: { 230 | type: 'HEADERS', 231 | flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, 232 | PADDED: true, RESERVED5: false, PRIORITY: false }, 233 | stream: 15, 234 | 235 | data: new Buffer('12345678', 'hex') 236 | }, 237 | // length + type + flags + stream + pad length + data + padding 238 | buffer: new Buffer('00000B' + '01' + '08' + '0000000F' + '06' + '12345678' + '000000000000', 'hex') 239 | 240 | }, { 241 | frame: { 242 | type: 'HEADERS', 243 | flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, 244 | PADDED: true, RESERVED5: false, PRIORITY: true }, 245 | stream: 15, 246 | priorityDependency: 10, 247 | priorityWeight: 5, 248 | exclusiveDependency: false, 249 | 250 | data: new Buffer('12345678', 'hex') 251 | }, 252 | // length + type + flags + stream + pad length + priority dependency + priority weight + data + padding 253 | buffer: new Buffer('000010' + '01' + '28' + '0000000F' + '06' + '0000000A' + '05' + '12345678' + '000000000000', 'hex') 254 | 255 | }, { 256 | frame: { 257 | type: 'HEADERS', 258 | flags: { END_STREAM: false, RESERVED2: false, END_HEADERS: false, 259 | PADDED: true, RESERVED5: false, PRIORITY: true }, 260 | stream: 15, 261 | priorityDependency: 10, 262 | priorityWeight: 5, 263 | exclusiveDependency: true, 264 | 265 | data: new Buffer('12345678', 'hex') 266 | }, 267 | // length + type + flags + stream + pad length + priority dependency + priority weight + data + padding 268 | buffer: new Buffer('000010' + '01' + '28' + '0000000F' + '06' + '8000000A' + '05' + '12345678' + '000000000000', 'hex') 269 | 270 | }, { 271 | frame: { 272 | type: 'PUSH_PROMISE', 273 | flags: { RESERVED1: false, RESERVED2: false, END_PUSH_PROMISE: false, 274 | PADDED: true }, 275 | stream: 15, 276 | 277 | promised_stream: 3, 278 | data: new Buffer('12345678', 'hex') 279 | }, 280 | // length + type + flags + stream + pad length + promised stream + data + padding 281 | buffer: new Buffer('00000F' + '05' + '08' + '0000000F' + '06' + '00000003' + '12345678' + '000000000000', 'hex') 282 | 283 | }]; 284 | for (var idx = 0; idx < padded_test_frames.length; idx++) { 285 | deserializer_test_frames.push(padded_test_frames[idx]); 286 | } 287 | 288 | 289 | describe('framer.js', function() { 290 | describe('Serializer', function() { 291 | describe('static method .commonHeader({ type, flags, stream }, buffer_array)', function() { 292 | it('should add the appropriate 9 byte header buffer in front of the others', function() { 293 | for (var i = 0; i < test_frames.length; i++) { 294 | var test = test_frames[i]; 295 | var buffers = [test.buffer.slice(9)]; 296 | var header_buffer = test.buffer.slice(0,9); 297 | Serializer.commonHeader(test.frame, buffers); 298 | expect(buffers[0]).to.deep.equal(header_buffer); 299 | } 300 | }); 301 | }); 302 | 303 | Object.keys(frame_types).forEach(function(type) { 304 | var tests = test_frames.filter(function(test) { return test.frame.type === type; }); 305 | var frame_shape = '{ ' + frame_types[type].join(', ') + ' }'; 306 | describe('static method .' + type + '(' + frame_shape + ', buffer_array)', function() { 307 | it('should push buffers to the array that make up a ' + type + ' type payload', function() { 308 | for (var i = 0; i < tests.length; i++) { 309 | var test = tests[i]; 310 | var buffers = []; 311 | Serializer[type](test.frame, buffers); 312 | expect(util.concat(buffers)).to.deep.equal(test.buffer.slice(9)); 313 | } 314 | }); 315 | }); 316 | }); 317 | 318 | describe('transform stream', function() { 319 | it('should transform frame objects to appropriate buffers', function() { 320 | var stream = new Serializer(util.log); 321 | 322 | for (var i = 0; i < test_frames.length; i++) { 323 | var test = test_frames[i]; 324 | stream.write(test.frame); 325 | var chunk, buffer = new Buffer(0); 326 | while (chunk = stream.read()) { 327 | buffer = util.concat([buffer, chunk]); 328 | } 329 | expect(buffer).to.be.deep.equal(test.buffer); 330 | } 331 | }); 332 | }); 333 | }); 334 | 335 | describe('Deserializer', function() { 336 | describe('static method .commonHeader(header_buffer, frame)', function() { 337 | it('should augment the frame object with these properties: { type, flags, stream })', function() { 338 | for (var i = 0; i < deserializer_test_frames.length; i++) { 339 | var test = deserializer_test_frames[i], frame = {}; 340 | Deserializer.commonHeader(test.buffer.slice(0,9), frame); 341 | expect(frame).to.deep.equal({ 342 | type: test.frame.type, 343 | flags: test.frame.flags, 344 | stream: test.frame.stream 345 | }); 346 | } 347 | }); 348 | }); 349 | 350 | Object.keys(frame_types).forEach(function(type) { 351 | var tests = deserializer_test_frames.filter(function(test) { return test.frame.type === type; }); 352 | var frame_shape = '{ ' + frame_types[type].join(', ') + ' }'; 353 | describe('static method .' + type + '(payload_buffer, frame)', function() { 354 | it('should augment the frame object with these properties: ' + frame_shape, function() { 355 | for (var i = 0; i < tests.length; i++) { 356 | var test = tests[i]; 357 | var frame = { 358 | type: test.frame.type, 359 | flags: test.frame.flags, 360 | stream: test.frame.stream 361 | }; 362 | Deserializer[type](test.buffer.slice(9), frame); 363 | expect(frame).to.deep.equal(test.frame); 364 | } 365 | }); 366 | }); 367 | }); 368 | 369 | describe('transform stream', function() { 370 | it('should transform buffers to appropriate frame object', function() { 371 | var stream = new Deserializer(util.log); 372 | 373 | var shuffled = util.shuffleBuffers(deserializer_test_frames.map(function(test) { return test.buffer; })); 374 | shuffled.forEach(stream.write.bind(stream)); 375 | 376 | for (var j = 0; j < deserializer_test_frames.length; j++) { 377 | expect(stream.read()).to.be.deep.equal(deserializer_test_frames[j].frame); 378 | } 379 | }); 380 | }); 381 | }); 382 | 383 | describe('bunyan formatter', function() { 384 | describe('`frame`', function() { 385 | var format = framer.serializers.frame; 386 | it('should assign a unique ID to each frame', function() { 387 | var frame1 = { type: 'DATA', data: new Buffer(10) }; 388 | var frame2 = { type: 'PRIORITY', priority: 1 }; 389 | expect(format(frame1).id).to.be.equal(format(frame1)); 390 | expect(format(frame2).id).to.be.equal(format(frame2)); 391 | expect(format(frame1)).to.not.be.equal(format(frame2)); 392 | }); 393 | }); 394 | }); 395 | }); 396 | -------------------------------------------------------------------------------- /lib/protocol/flow.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | // The Flow class 4 | // ============== 5 | 6 | // Flow is a [Duplex stream][1] subclass which implements HTTP/2 flow control. It is designed to be 7 | // subclassed by [Connection](connection.html) and the `upstream` component of [Stream](stream.html). 8 | // [1]: https://nodejs.org/api/stream.html#stream_class_stream_duplex 9 | 10 | var Duplex = require('stream').Duplex; 11 | 12 | exports.Flow = Flow; 13 | 14 | // Public API 15 | // ---------- 16 | 17 | // * **Event: 'error' (type)**: signals an error 18 | // 19 | // * **setInitialWindow(size)**: the initial flow control window size can be changed *any time* 20 | // ([as described in the standard][1]) using this method 21 | // 22 | // [1]: https://tools.ietf.org/html/rfc7540#section-6.9.2 23 | 24 | // API for child classes 25 | // --------------------- 26 | 27 | // * **new Flow([flowControlId])**: creating a new flow that will listen for WINDOW_UPDATES frames 28 | // with the given `flowControlId` (or every update frame if not given) 29 | // 30 | // * **_send()**: called when more frames should be pushed. The child class is expected to override 31 | // this (instead of the `_read` method of the Duplex class). 32 | // 33 | // * **_receive(frame, readyCallback)**: called when there's an incoming frame. The child class is 34 | // expected to override this (instead of the `_write` method of the Duplex class). 35 | // 36 | // * **push(frame): bool**: schedules `frame` for sending. 37 | // 38 | // Returns `true` if it needs more frames in the output queue, `false` if the output queue is 39 | // full, and `null` if did not push the frame into the output queue (instead, it pushed it into 40 | // the flow control queue). 41 | // 42 | // * **read(limit): frame**: like the regular `read`, but the 'flow control size' (0 for non-DATA 43 | // frames, length of the payload for DATA frames) of the returned frame will be under `limit`. 44 | // Small exception: pass -1 as `limit` if the max. flow control size is 0. `read(0)` means the 45 | // same thing as [in the original API](https://nodejs.org/api/stream.html#stream_stream_read_0). 46 | // 47 | // * **getLastQueuedFrame(): frame**: returns the last frame in output buffers 48 | // 49 | // * **_log**: the Flow class uses the `_log` object of the parent 50 | 51 | // Constructor 52 | // ----------- 53 | 54 | // When a HTTP/2.0 connection is first established, new streams are created with an initial flow 55 | // control window size of 65535 bytes. 56 | var INITIAL_WINDOW_SIZE = 65535; 57 | 58 | // `flowControlId` is needed if only specific WINDOW_UPDATEs should be watched. 59 | function Flow(flowControlId) { 60 | Duplex.call(this, { objectMode: true }); 61 | 62 | this._window = this._initialWindow = INITIAL_WINDOW_SIZE; 63 | this._flowControlId = flowControlId; 64 | this._queue = []; 65 | this._ended = false; 66 | this._received = 0; 67 | this._blocked = false; 68 | } 69 | Flow.prototype = Object.create(Duplex.prototype, { constructor: { value: Flow } }); 70 | 71 | // Incoming frames 72 | // --------------- 73 | 74 | // `_receive` is called when there's an incoming frame. 75 | Flow.prototype._receive = function _receive(frame, callback) { 76 | throw new Error('The _receive(frame, callback) method has to be overridden by the child class!'); 77 | }; 78 | 79 | // `_receive` is called by `_write` which in turn is [called by Duplex][1] when someone `write()`s 80 | // to the flow. It emits the 'receiving' event and notifies the window size tracking code if the 81 | // incoming frame is a WINDOW_UPDATE. 82 | // [1]: https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1 83 | Flow.prototype._write = function _write(frame, encoding, callback) { 84 | var sentToUs = (this._flowControlId === undefined) || (frame.stream === this._flowControlId); 85 | 86 | if (sentToUs && (frame.flags.END_STREAM || (frame.type === 'RST_STREAM'))) { 87 | this._ended = true; 88 | } 89 | 90 | if ((frame.type === 'DATA') && (frame.data.length > 0)) { 91 | this._receive(frame, function() { 92 | this._received += frame.data.length; 93 | if (!this._restoreWindowTimer) { 94 | this._restoreWindowTimer = setImmediate(this._restoreWindow.bind(this)); 95 | } 96 | callback(); 97 | }.bind(this)); 98 | } 99 | 100 | else { 101 | this._receive(frame, callback); 102 | } 103 | 104 | if (sentToUs && (frame.type === 'WINDOW_UPDATE')) { 105 | this._updateWindow(frame); 106 | } 107 | }; 108 | 109 | // `_restoreWindow` basically acknowledges the DATA frames received since it's last call. It sends 110 | // a WINDOW_UPDATE that restores the flow control window of the remote end. 111 | // TODO: push this directly into the output queue. No need to wait for DATA frames in the queue. 112 | Flow.prototype._restoreWindow = function _restoreWindow() { 113 | delete this._restoreWindowTimer; 114 | if (!this._ended && (this._received > 0)) { 115 | this.push({ 116 | type: 'WINDOW_UPDATE', 117 | flags: {}, 118 | stream: this._flowControlId, 119 | window_size: this._received 120 | }); 121 | this._received = 0; 122 | } 123 | }; 124 | 125 | // Outgoing frames - sending procedure 126 | // ----------------------------------- 127 | 128 | // flow 129 | // +-------------------------------------------------+ 130 | // | | 131 | // +--------+ +---------+ | 132 | // read() | output | _read() | flow | _send() | 133 | // <----------| |<----------| control |<------------- | 134 | // | buffer | | buffer | | 135 | // +--------+ +---------+ | 136 | // | input | | 137 | // ---------->| |-----------------------------------> | 138 | // write() | buffer | _write() _receive() | 139 | // +--------+ | 140 | // | | 141 | // +-------------------------------------------------+ 142 | 143 | // `_send` is called when more frames should be pushed to the output buffer. 144 | Flow.prototype._send = function _send() { 145 | throw new Error('The _send() method has to be overridden by the child class!'); 146 | }; 147 | 148 | // `_send` is called by `_read` which is in turn [called by Duplex][1] when it wants to have more 149 | // items in the output queue. 150 | // [1]: https://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1 151 | Flow.prototype._read = function _read() { 152 | // * if the flow control queue is empty, then let the user push more frames 153 | if (this._queue.length === 0) { 154 | this._send(); 155 | } 156 | 157 | // * if there are items in the flow control queue, then let's put them into the output queue (to 158 | // the extent it is possible with respect to the window size and output queue feedback) 159 | else if (this._window > 0) { 160 | this._blocked = false; 161 | this._readableState.sync = true; // to avoid reentrant calls 162 | do { 163 | var moreNeeded = this._push(this._queue[0]); 164 | if (moreNeeded !== null) { 165 | this._queue.shift(); 166 | } 167 | } while (moreNeeded && (this._queue.length > 0)); 168 | this._readableState.sync = false; 169 | 170 | assert((moreNeeded == false) || // * output queue is full 171 | (this._queue.length === 0) || // * flow control queue is empty 172 | (!this._window && (this._queue[0].type === 'DATA'))); // * waiting for window update 173 | } 174 | 175 | // * otherwise, come back when the flow control window is positive 176 | else if (!this._blocked) { 177 | this._parentPush({ 178 | type: 'BLOCKED', 179 | flags: {}, 180 | stream: this._flowControlId 181 | }); 182 | this.once('window_update', this._read); 183 | this._blocked = true; 184 | } 185 | }; 186 | 187 | var MAX_PAYLOAD_SIZE = 4096; // Must not be greater than MAX_HTTP_PAYLOAD_SIZE which is 16383 188 | 189 | // `read(limit)` is like the `read` of the Readable class, but it guarantess that the 'flow control 190 | // size' (0 for non-DATA frames, length of the payload for DATA frames) of the returned frame will 191 | // be under `limit`. 192 | Flow.prototype.read = function read(limit) { 193 | if (limit === 0) { 194 | return Duplex.prototype.read.call(this, 0); 195 | } else if (limit === -1) { 196 | limit = 0; 197 | } else if ((limit === undefined) || (limit > MAX_PAYLOAD_SIZE)) { 198 | limit = MAX_PAYLOAD_SIZE; 199 | } 200 | 201 | // * Looking at the first frame in the queue without pulling it out if possible. 202 | var frame = this._readableState.buffer[0]; 203 | if (!frame && !this._readableState.ended) { 204 | this._read(); 205 | frame = this._readableState.buffer[0]; 206 | } 207 | 208 | if (frame && (frame.type === 'DATA')) { 209 | // * If the frame is DATA, then there's two special cases: 210 | // * if the limit is 0, we shouldn't return anything 211 | // * if the size of the frame is larger than limit, then the frame should be split 212 | if (limit === 0) { 213 | return Duplex.prototype.read.call(this, 0); 214 | } 215 | 216 | else if (frame.data.length > limit) { 217 | this._log.trace({ frame: frame, size: frame.data.length, forwardable: limit }, 218 | 'Splitting out forwardable part of a DATA frame.'); 219 | this.unshift({ 220 | type: 'DATA', 221 | flags: {}, 222 | stream: frame.stream, 223 | data: frame.data.slice(0, limit) 224 | }); 225 | frame.data = frame.data.slice(limit); 226 | } 227 | } 228 | 229 | return Duplex.prototype.read.call(this); 230 | }; 231 | 232 | // `_parentPush` pushes the given `frame` into the output queue 233 | Flow.prototype._parentPush = function _parentPush(frame) { 234 | this._log.trace({ frame: frame }, 'Pushing frame into the output queue'); 235 | 236 | if (frame && (frame.type === 'DATA') && (this._window !== Infinity)) { 237 | this._log.trace({ window: this._window, by: frame.data.length }, 238 | 'Decreasing flow control window size.'); 239 | this._window -= frame.data.length; 240 | assert(this._window >= 0); 241 | } 242 | 243 | return Duplex.prototype.push.call(this, frame); 244 | }; 245 | 246 | // `_push(frame)` pushes `frame` into the output queue and decreases the flow control window size. 247 | // It is capable of splitting DATA frames into smaller parts, if the window size is not enough to 248 | // push the whole frame. The return value is similar to `push` except that it returns `null` if it 249 | // did not push the whole frame to the output queue (but maybe it did push part of the frame). 250 | Flow.prototype._push = function _push(frame) { 251 | var data = frame && (frame.type === 'DATA') && frame.data; 252 | 253 | if (!data || (data.length <= this._window)) { 254 | return this._parentPush(frame); 255 | } 256 | 257 | else if (this._window <= 0) { 258 | return null; 259 | } 260 | 261 | else { 262 | this._log.trace({ frame: frame, size: frame.data.length, forwardable: this._window }, 263 | 'Splitting out forwardable part of a DATA frame.'); 264 | frame.data = data.slice(this._window); 265 | this._parentPush({ 266 | type: 'DATA', 267 | flags: {}, 268 | stream: frame.stream, 269 | data: data.slice(0, this._window) 270 | }); 271 | return null; 272 | } 273 | }; 274 | 275 | // Push `frame` into the flow control queue, or if it's empty, then directly into the output queue 276 | Flow.prototype.push = function push(frame) { 277 | if (frame === null) { 278 | this._log.debug('Enqueueing outgoing End Of Stream'); 279 | } else { 280 | this._log.debug({ frame: frame }, 'Enqueueing outgoing frame'); 281 | } 282 | 283 | var moreNeeded = null; 284 | if (this._queue.length === 0) { 285 | moreNeeded = this._push(frame); 286 | } 287 | 288 | if (moreNeeded === null) { 289 | this._queue.push(frame); 290 | } 291 | 292 | return moreNeeded; 293 | }; 294 | 295 | // `getLastQueuedFrame` returns the last frame in output buffers. This is primarily used by the 296 | // [Stream](stream.html) class to mark the last frame with END_STREAM flag. 297 | Flow.prototype.getLastQueuedFrame = function getLastQueuedFrame() { 298 | var readableQueue = this._readableState.buffer; 299 | return this._queue[this._queue.length - 1] || readableQueue[readableQueue.length - 1]; 300 | }; 301 | 302 | // Outgoing frames - managing the window size 303 | // ------------------------------------------ 304 | 305 | // Flow control window size is manipulated using the `_increaseWindow` method. 306 | // 307 | // * Invoking it with `Infinite` means turning off flow control. Flow control cannot be enabled 308 | // again once disabled. Any attempt to re-enable flow control MUST be rejected with a 309 | // FLOW_CONTROL_ERROR error code. 310 | // * A sender MUST NOT allow a flow control window to exceed 2^31 - 1 bytes. The action taken 311 | // depends on it being a stream or the connection itself. 312 | 313 | var WINDOW_SIZE_LIMIT = Math.pow(2, 31) - 1; 314 | 315 | Flow.prototype._increaseWindow = function _increaseWindow(size) { 316 | if ((this._window === Infinity) && (size !== Infinity)) { 317 | this._log.error('Trying to increase flow control window after flow control was turned off.'); 318 | this.emit('error', 'FLOW_CONTROL_ERROR'); 319 | } else { 320 | this._log.trace({ window: this._window, by: size }, 'Increasing flow control window size.'); 321 | this._window += size; 322 | if ((this._window !== Infinity) && (this._window > WINDOW_SIZE_LIMIT)) { 323 | this._log.error('Flow control window grew too large.'); 324 | this.emit('error', 'FLOW_CONTROL_ERROR'); 325 | } else { 326 | if (size != 0) { 327 | this.emit('window_update'); 328 | } 329 | } 330 | } 331 | }; 332 | 333 | // The `_updateWindow` method gets called every time there's an incoming WINDOW_UPDATE frame. It 334 | // modifies the flow control window: 335 | // 336 | // * Flow control can be disabled for an individual stream by sending a WINDOW_UPDATE with the 337 | // END_FLOW_CONTROL flag set. The payload of a WINDOW_UPDATE frame that has the END_FLOW_CONTROL 338 | // flag set is ignored. 339 | // * A sender that receives a WINDOW_UPDATE frame updates the corresponding window by the amount 340 | // specified in the frame. 341 | Flow.prototype._updateWindow = function _updateWindow(frame) { 342 | this._increaseWindow(frame.flags.END_FLOW_CONTROL ? Infinity : frame.window_size); 343 | }; 344 | 345 | // A SETTINGS frame can alter the initial flow control window size for all current streams. When the 346 | // value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the size of all stream by 347 | // calling the `setInitialWindow` method. The window size has to be modified by the difference 348 | // between the new value and the old value. 349 | Flow.prototype.setInitialWindow = function setInitialWindow(initialWindow) { 350 | this._increaseWindow(initialWindow - this._initialWindow); 351 | this._initialWindow = initialWindow; 352 | }; 353 | -------------------------------------------------------------------------------- /test/stream.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var util = require('./util'); 3 | 4 | var stream = require('../lib/protocol/stream'); 5 | var Stream = stream.Stream; 6 | 7 | function createStream() { 8 | var stream = new Stream(util.log, null); 9 | stream.upstream._window = Infinity; 10 | return stream; 11 | } 12 | 13 | // Execute a list of commands and assertions 14 | var recorded_events = ['state', 'error', 'window_update', 'headers', 'promise']; 15 | function execute_sequence(stream, sequence, done) { 16 | if (!done) { 17 | done = sequence; 18 | sequence = stream; 19 | stream = createStream(); 20 | } 21 | 22 | var outgoing_frames = []; 23 | 24 | var emit = stream.emit, events = []; 25 | stream.emit = function(name) { 26 | if (recorded_events.indexOf(name) !== -1) { 27 | events.push({ name: name, data: Array.prototype.slice.call(arguments, 1) }); 28 | } 29 | return emit.apply(this, arguments); 30 | }; 31 | 32 | var commands = [], checks = []; 33 | sequence.forEach(function(step) { 34 | if ('method' in step || 'incoming' in step || 'outgoing' in step || 'wait' in step || 'set_state' in step) { 35 | commands.push(step); 36 | } 37 | 38 | if ('outgoing' in step || 'event' in step || 'active' in step) { 39 | checks.push(step); 40 | } 41 | }); 42 | 43 | var activeCount = 0; 44 | function count_change(change) { 45 | activeCount += change; 46 | } 47 | 48 | function execute(callback) { 49 | var command = commands.shift(); 50 | if (command) { 51 | if ('method' in command) { 52 | var value = stream[command.method.name].apply(stream, command.method.arguments); 53 | if (command.method.ret) { 54 | command.method.ret(value); 55 | } 56 | execute(callback); 57 | } else if ('incoming' in command) { 58 | command.incoming.count_change = count_change; 59 | stream.upstream.write(command.incoming); 60 | execute(callback); 61 | } else if ('outgoing' in command) { 62 | outgoing_frames.push(stream.upstream.read()); 63 | execute(callback); 64 | } else if ('set_state' in command) { 65 | stream.state = command.set_state; 66 | execute(callback); 67 | } else if ('wait' in command) { 68 | setTimeout(execute.bind(null, callback), command.wait); 69 | } else { 70 | throw new Error('Invalid command', command); 71 | } 72 | } else { 73 | setTimeout(callback, 5); 74 | } 75 | } 76 | 77 | function check() { 78 | checks.forEach(function(check) { 79 | if ('outgoing' in check) { 80 | var frame = outgoing_frames.shift(); 81 | for (var key in check.outgoing) { 82 | expect(frame).to.have.property(key).that.deep.equals(check.outgoing[key]); 83 | } 84 | count_change(frame.count_change); 85 | } else if ('event' in check) { 86 | var event = events.shift(); 87 | expect(event.name).to.be.equal(check.event.name); 88 | check.event.data.forEach(function(data, index) { 89 | expect(event.data[index]).to.deep.equal(data); 90 | }); 91 | } else if ('active' in check) { 92 | expect(activeCount).to.be.equal(check.active); 93 | } else { 94 | throw new Error('Invalid check', check); 95 | } 96 | }); 97 | done(); 98 | } 99 | 100 | setImmediate(execute.bind(null, check)); 101 | } 102 | 103 | var example_frames = [ 104 | { type: 'PRIORITY', flags: {}, priority: 1 }, 105 | { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, 106 | { type: 'RST_STREAM', flags: {}, error: 'CANCEL' }, 107 | { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 108 | { type: 'DATA', flags: {}, data: new Buffer(5) }, 109 | { type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log, null) } 110 | ]; 111 | 112 | var invalid_incoming_frames = { 113 | IDLE: [ 114 | { type: 'DATA', flags: {}, data: new Buffer(5) }, 115 | { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, 116 | { type: 'PUSH_PROMISE', flags: {}, headers: {} }, 117 | { type: 'RST_STREAM', flags: {}, error: 'CANCEL' } 118 | ], 119 | RESERVED_LOCAL: [ 120 | { type: 'DATA', flags: {}, data: new Buffer(5) }, 121 | { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 122 | { type: 'PUSH_PROMISE', flags: {}, headers: {} }, 123 | { type: 'WINDOW_UPDATE', flags: {}, settings: {} } 124 | ], 125 | RESERVED_REMOTE: [ 126 | { type: 'DATA', flags: {}, data: new Buffer(5) }, 127 | { type: 'PUSH_PROMISE', flags: {}, headers: {} }, 128 | { type: 'WINDOW_UPDATE', flags: {}, settings: {} } 129 | ], 130 | OPEN: [ 131 | ], 132 | HALF_CLOSED_LOCAL: [ 133 | ], 134 | HALF_CLOSED_REMOTE: [ 135 | { type: 'DATA', flags: {}, data: new Buffer(5) }, 136 | { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 137 | { type: 'PUSH_PROMISE', flags: {}, headers: {} } 138 | ] 139 | }; 140 | 141 | var invalid_outgoing_frames = { 142 | IDLE: [ 143 | { type: 'DATA', flags: {}, data: new Buffer(5) }, 144 | { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, 145 | { type: 'PUSH_PROMISE', flags: {}, headers: {} } 146 | ], 147 | RESERVED_LOCAL: [ 148 | { type: 'DATA', flags: {}, data: new Buffer(5) }, 149 | { type: 'PUSH_PROMISE', flags: {}, headers: {} }, 150 | { type: 'WINDOW_UPDATE', flags: {}, settings: {} } 151 | ], 152 | RESERVED_REMOTE: [ 153 | { type: 'DATA', flags: {}, data: new Buffer(5) }, 154 | { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 155 | { type: 'PUSH_PROMISE', flags: {}, headers: {} }, 156 | { type: 'WINDOW_UPDATE', flags: {}, settings: {} } 157 | ], 158 | OPEN: [ 159 | ], 160 | HALF_CLOSED_LOCAL: [ 161 | { type: 'DATA', flags: {}, data: new Buffer(5) }, 162 | { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 163 | { type: 'PUSH_PROMISE', flags: {}, headers: {} } 164 | ], 165 | HALF_CLOSED_REMOTE: [ 166 | ], 167 | CLOSED: [ 168 | { type: 'WINDOW_UPDATE', flags: {}, settings: {} }, 169 | { type: 'HEADERS', flags: {}, headers: {}, priority: undefined }, 170 | { type: 'DATA', flags: {}, data: new Buffer(5) }, 171 | { type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log, null) } 172 | ] 173 | }; 174 | 175 | describe('stream.js', function() { 176 | describe('Stream class', function() { 177 | describe('._transition(sending, frame) method', function() { 178 | it('should emit error, and answer RST_STREAM for invalid incoming frames', function() { 179 | Object.keys(invalid_incoming_frames).forEach(function(state) { 180 | invalid_incoming_frames[state].forEach(function(invalid_frame) { 181 | var stream = createStream(); 182 | var connectionErrorHappened = false; 183 | stream.state = state; 184 | stream.once('connectionError', function() { connectionErrorHappened = true; }); 185 | stream._transition(false, invalid_frame); 186 | expect(connectionErrorHappened); 187 | }); 188 | }); 189 | 190 | // CLOSED state as a result of incoming END_STREAM (or RST_STREAM) 191 | var stream = createStream(); 192 | stream.headers({}); 193 | stream.end(); 194 | stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop }); 195 | example_frames.slice(2).forEach(function(invalid_frame) { 196 | invalid_frame.count_change = util.noop; 197 | expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); 198 | }); 199 | 200 | // CLOSED state as a result of outgoing END_STREAM 201 | stream = createStream(); 202 | stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop }); 203 | stream.headers({}); 204 | stream.end(); 205 | example_frames.slice(3).forEach(function(invalid_frame) { 206 | invalid_frame.count_change = util.noop; 207 | expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.'); 208 | }); 209 | }); 210 | it('should throw exception for invalid outgoing frames', function() { 211 | Object.keys(invalid_outgoing_frames).forEach(function(state) { 212 | invalid_outgoing_frames[state].forEach(function(invalid_frame) { 213 | var stream = createStream(); 214 | stream.state = state; 215 | expect(stream._transition.bind(stream, true, invalid_frame)).to.throw(Error); 216 | }); 217 | }); 218 | }); 219 | it('should close the stream when there\'s an incoming or outgoing RST_STREAM', function() { 220 | [ 221 | 'RESERVED_LOCAL', 222 | 'RESERVED_REMOTE', 223 | 'OPEN', 224 | 'HALF_CLOSED_LOCAL', 225 | 'HALF_CLOSED_REMOTE' 226 | ].forEach(function(state) { 227 | [true, false].forEach(function(sending) { 228 | var stream = createStream(); 229 | stream.state = state; 230 | stream._transition(sending, { type: 'RST_STREAM', flags: {} }); 231 | expect(stream.state).to.be.equal('CLOSED'); 232 | }); 233 | }); 234 | }); 235 | it('should ignore any incoming frame after sending reset', function() { 236 | var stream = createStream(); 237 | stream.reset(); 238 | example_frames.forEach(stream._transition.bind(stream, false)); 239 | }); 240 | it('should ignore certain incoming frames after closing the stream with END_STREAM', function() { 241 | var stream = createStream(); 242 | stream.upstream.write({ type: 'HEADERS', flags: { END_STREAM: true }, headers:{} }); 243 | stream.headers({}); 244 | stream.end(); 245 | example_frames.slice(0,3).forEach(function(frame) { 246 | frame.count_change = util.noop; 247 | stream._transition(false, frame); 248 | }); 249 | }); 250 | }); 251 | }); 252 | describe('test scenario', function() { 253 | describe('sending request', function() { 254 | it('should trigger the appropriate state transitions and outgoing frames', function(done) { 255 | execute_sequence([ 256 | { method : { name: 'headers', arguments: [{ ':path': '/' }] } }, 257 | { outgoing: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } }, 258 | { event : { name: 'state', data: ['OPEN'] } }, 259 | 260 | { wait : 5 }, 261 | { method : { name: 'end', arguments: [] } }, 262 | { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, 263 | { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(0) } }, 264 | 265 | { wait : 10 }, 266 | { incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, 267 | { incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: new Buffer(5) } }, 268 | { event : { name: 'headers', data: [{ ':status': 200 }] } }, 269 | { event : { name: 'state', data: ['CLOSED'] } }, 270 | 271 | { active : 0 } 272 | ], done); 273 | }); 274 | }); 275 | describe('answering request', function() { 276 | it('should trigger the appropriate state transitions and outgoing frames', function(done) { 277 | var payload = new Buffer(5); 278 | execute_sequence([ 279 | { incoming: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } }, 280 | { event : { name: 'state', data: ['OPEN'] } }, 281 | { event : { name: 'headers', data: [{ ':path': '/' }] } }, 282 | 283 | { wait : 5 }, 284 | { incoming: { type: 'DATA', flags: { }, data: new Buffer(5) } }, 285 | { incoming: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(10) } }, 286 | { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, 287 | 288 | { wait : 5 }, 289 | { method : { name: 'headers', arguments: [{ ':status': 200 }] } }, 290 | { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, 291 | 292 | { wait : 5 }, 293 | { method : { name: 'end', arguments: [payload] } }, 294 | { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, 295 | { event : { name: 'state', data: ['CLOSED'] } }, 296 | 297 | { active : 0 } 298 | ], done); 299 | }); 300 | }); 301 | describe('sending push stream', function() { 302 | it('should trigger the appropriate state transitions and outgoing frames', function(done) { 303 | var payload = new Buffer(5); 304 | var pushStream; 305 | 306 | execute_sequence([ 307 | // receiving request 308 | { incoming: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } }, 309 | { event : { name: 'state', data: ['OPEN'] } }, 310 | { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, 311 | { event : { name: 'headers', data: [{ ':path': '/' }] } }, 312 | 313 | // sending response headers 314 | { wait : 5 }, 315 | { method : { name: 'headers', arguments: [{ ':status': '200' }] } }, 316 | { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } }, 317 | 318 | // sending push promise 319 | { method : { name: 'promise', arguments: [{ ':path': '/' }], ret: function(str) { pushStream = str; } } }, 320 | { outgoing: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/' } } }, 321 | 322 | // sending response data 323 | { method : { name: 'end', arguments: [payload] } }, 324 | { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, 325 | { event : { name: 'state', data: ['CLOSED'] } }, 326 | 327 | { active : 0 } 328 | ], function() { 329 | // initial state of the promised stream 330 | expect(pushStream.state).to.equal('RESERVED_LOCAL'); 331 | 332 | execute_sequence(pushStream, [ 333 | // push headers 334 | { wait : 5 }, 335 | { method : { name: 'headers', arguments: [{ ':status': '200' }] } }, 336 | { outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } }, 337 | { event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } }, 338 | 339 | // push data 340 | { method : { name: 'end', arguments: [payload] } }, 341 | { outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, 342 | { event : { name: 'state', data: ['CLOSED'] } }, 343 | 344 | { active : 1 } 345 | ], done); 346 | }); 347 | }); 348 | }); 349 | describe('receiving push stream', function() { 350 | it('should trigger the appropriate state transitions and outgoing frames', function(done) { 351 | var payload = new Buffer(5); 352 | var original_stream = createStream(); 353 | var promised_stream = createStream(); 354 | 355 | done = util.callNTimes(2, done); 356 | 357 | execute_sequence(original_stream, [ 358 | // sending request headers 359 | { method : { name: 'headers', arguments: [{ ':path': '/' }] } }, 360 | { method : { name: 'end', arguments: [] } }, 361 | { outgoing: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } }, 362 | { event : { name: 'state', data: ['OPEN'] } }, 363 | { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, 364 | 365 | // receiving response headers 366 | { wait : 10 }, 367 | { incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } }, 368 | { event : { name: 'headers', data: [{ ':status': 200 }] } }, 369 | 370 | // receiving push promise 371 | { incoming: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/2.html' }, promised_stream: promised_stream } }, 372 | { event : { name: 'promise', data: [promised_stream, { ':path': '/2.html' }] } }, 373 | 374 | // receiving response data 375 | { incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: payload } }, 376 | { event : { name: 'state', data: ['CLOSED'] } }, 377 | 378 | { active : 0 } 379 | ], done); 380 | 381 | execute_sequence(promised_stream, [ 382 | // initial state of the promised stream 383 | { event : { name: 'state', data: ['RESERVED_REMOTE'] } }, 384 | 385 | // push headers 386 | { wait : 10 }, 387 | { incoming: { type: 'HEADERS', flags: { END_STREAM: false }, headers: { ':status': 200 } } }, 388 | { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } }, 389 | { event : { name: 'headers', data: [{ ':status': 200 }] } }, 390 | 391 | // push data 392 | { incoming: { type: 'DATA', flags: { END_STREAM: true }, data: payload } }, 393 | { event : { name: 'state', data: ['CLOSED'] } }, 394 | 395 | { active : 0 } 396 | ], done); 397 | }); 398 | }); 399 | }); 400 | 401 | describe('bunyan formatter', function() { 402 | describe('`s`', function() { 403 | var format = stream.serializers.s; 404 | it('should assign a unique ID to each frame', function() { 405 | var stream1 = createStream(); 406 | var stream2 = createStream(); 407 | expect(format(stream1)).to.be.equal(format(stream1)); 408 | expect(format(stream2)).to.be.equal(format(stream2)); 409 | expect(format(stream1)).to.not.be.equal(format(stream2)); 410 | }); 411 | }); 412 | }); 413 | }); 414 | -------------------------------------------------------------------------------- /test/compressor.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var util = require('./util'); 3 | 4 | var compressor = require('../lib/protocol/compressor'); 5 | var HeaderTable = compressor.HeaderTable; 6 | var HuffmanTable = compressor.HuffmanTable; 7 | var HeaderSetCompressor = compressor.HeaderSetCompressor; 8 | var HeaderSetDecompressor = compressor.HeaderSetDecompressor; 9 | var Compressor = compressor.Compressor; 10 | var Decompressor = compressor.Decompressor; 11 | 12 | var test_integers = [{ 13 | N: 5, 14 | I: 10, 15 | buffer: new Buffer([10]) 16 | }, { 17 | N: 0, 18 | I: 10, 19 | buffer: new Buffer([10]) 20 | }, { 21 | N: 5, 22 | I: 1337, 23 | buffer: new Buffer([31, 128 + 26, 10]) 24 | }, { 25 | N: 0, 26 | I: 1337, 27 | buffer: new Buffer([128 + 57, 10]) 28 | }]; 29 | 30 | var test_strings = [{ 31 | string: 'www.foo.com', 32 | buffer: new Buffer('89f1e3c2f29ceb90f4ff', 'hex') 33 | }, { 34 | string: 'éáűőúöüó€', 35 | buffer: new Buffer('13c3a9c3a1c5b1c591c3bac3b6c3bcc3b3e282ac', 'hex') 36 | }]; 37 | 38 | test_huffman_request = { 39 | 'GET': 'c5837f', 40 | 'http': '9d29af', 41 | '/': '63', 42 | 'www.foo.com': 'f1e3c2f29ceb90f4ff', 43 | 'https': '9d29ad1f', 44 | 'www.bar.com': 'f1e3c2f18ec5c87a7f', 45 | 'no-cache': 'a8eb10649cbf', 46 | '/custom-path.css': '6096a127a56ac699d72211', 47 | 'custom-key': '25a849e95ba97d7f', 48 | 'custom-value': '25a849e95bb8e8b4bf' 49 | }; 50 | 51 | test_huffman_response = { 52 | '302': '6402', 53 | 'private': 'aec3771a4b', 54 | 'Mon, 21 OCt 2013 20:13:21 GMT': 'd07abe941054d5792a0801654102e059b820a98b46ff', 55 | ': https://www.bar.com': 'b8a4e94d68b8c31e3c785e31d8b90f4f', 56 | '200': '1001', 57 | 'Mon, 21 OCt 2013 20:13:22 GMT': 'd07abe941054d5792a0801654102e059b821298b46ff', 58 | 'https://www.bar.com': '9d29ad171863c78f0bc63b1721e9', 59 | 'gzip': '9bd9ab', 60 | 'foo=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\ 61 | AAAAAAAAAAAAAAAAAAAAAAAAAALASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKHQWOEIUAL\ 62 | QWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKH\ 63 | QWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEO\ 64 | IUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOP\ 65 | IUAXQWEOIUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234ZZZZZZZZZZ\ 66 | ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ1234 m\ 67 | ax-age=3600; version=1': '94e7821861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861873c3bafe5cd8f666bbfbf9ab672c1ab5e4e10fe6ce583564e10fe67cb9b1ece5ab064e10e7d9cb06ac9c21fccfb307087f33e7cd961dd7f672c1ab86487f34844cb59e1dd7f2e6c7b335dfdfcd5b3960d5af27087f3672c1ab27087f33e5cd8f672d583270873ece583564e10fe67d983843f99f3e6cb0eebfb3960d5c3243f9a42265acf0eebf97363d99aefefe6ad9cb06ad793843f9b3960d593843f99f2e6c7b396ac1938439f672c1ab27087f33ecc1c21fccf9f3658775fd9cb06ae1921fcd21132d678775fcb9b1eccd77f7f356ce58356bc9c21fcd9cb06ac9c21fccf97363d9cb560c9c21cfb3960d593843f99f660e10fe67cf9b2c3bafece583570c90fe6908996bf7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f42265a5291f9587316065c003ed4ee5b1063d5007f', 68 | 'foo=ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ\ 69 | ZZZZZZZZZZZZZZZZZZZZZZZZZZLASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKHQWOEIUAL\ 70 | QWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKH\ 71 | QWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEO\ 72 | IUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOP\ 73 | IUAXQWEOIUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234AAAAAAAAAA\ 74 | AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234 m\ 75 | ax-age=3600; version=1': '94e783f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f73c3bafe5cd8f666bbfbf9ab672c1ab5e4e10fe6ce583564e10fe67cb9b1ece5ab064e10e7d9cb06ac9c21fccfb307087f33e7cd961dd7f672c1ab86487f34844cb59e1dd7f2e6c7b335dfdfcd5b3960d5af27087f3672c1ab27087f33e5cd8f672d583270873ece583564e10fe67d983843f99f3e6cb0eebfb3960d5c3243f9a42265acf0eebf97363d99aefefe6ad9cb06ad793843f9b3960d593843f99f2e6c7b396ac1938439f672c1ab27087f33ecc1c21fccf9f3658775fd9cb06ae1921fcd21132d678775fcb9b1eccd77f7f356ce58356bc9c21fcd9cb06ac9c21fccf97363d9cb560c9c21cfb3960d593843f99f660e10fe67cf9b2c3bafece583570c90fe6908996a1861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861861842265a5291f9587316065c003ed4ee5b1063d5007f' 76 | }; 77 | 78 | var test_headers = [{ 79 | // index 80 | header: { 81 | name: 1, 82 | value: 1, 83 | index: false, 84 | mustNeverIndex: false, 85 | contextUpdate: false, 86 | newMaxSize: 0 87 | }, 88 | buffer: new Buffer('82', 'hex') 89 | }, { 90 | // index 91 | header: { 92 | name: 5, 93 | value: 5, 94 | index: false, 95 | mustNeverIndex: false, 96 | contextUpdate: false, 97 | newMaxSize: 0 98 | }, 99 | buffer: new Buffer('86', 'hex') 100 | }, { 101 | // index 102 | header: { 103 | name: 3, 104 | value: 3, 105 | index: false, 106 | mustNeverIndex: false, 107 | contextUpdate: false, 108 | newMaxSize: 0 109 | }, 110 | buffer: new Buffer('84', 'hex') 111 | }, { 112 | // literal w/index, name index 113 | header: { 114 | name: 0, 115 | value: 'www.foo.com', 116 | index: true, 117 | mustNeverIndex: false, 118 | contextUpdate: false, 119 | newMaxSize: 0 120 | }, 121 | buffer: new Buffer('41' + '89f1e3c2f29ceb90f4ff', 'hex') 122 | }, { 123 | // indexed 124 | header: { 125 | name: 1, 126 | value: 1, 127 | index: false, 128 | mustNeverIndex: false, 129 | contextUpdate: false, 130 | newMaxSize: 0 131 | }, 132 | buffer: new Buffer('82', 'hex') 133 | }, { 134 | // indexed 135 | header: { 136 | name: 6, 137 | value: 6, 138 | index: false, 139 | mustNeverIndex: false, 140 | contextUpdate: false, 141 | newMaxSize: 0 142 | }, 143 | buffer: new Buffer('87', 'hex') 144 | }, { 145 | // indexed 146 | header: { 147 | name: 3, 148 | value: 3, 149 | index: false, 150 | mustNeverIndex: false, 151 | contextUpdate: false, 152 | newMaxSize: 0 153 | }, 154 | buffer: new Buffer('84', 'hex') 155 | }, { 156 | // literal w/index, name index 157 | header: { 158 | name: 0, 159 | value: 'www.bar.com', 160 | index: true, 161 | mustNeverIndex: false, 162 | contextUpdate: false, 163 | newMaxSize: 0 164 | }, 165 | buffer: new Buffer('41' + '89f1e3c2f18ec5c87a7f', 'hex') 166 | }, { 167 | // literal w/index, name index 168 | header: { 169 | name: 23, 170 | value: 'no-cache', 171 | index: true, 172 | mustNeverIndex: false, 173 | contextUpdate: false, 174 | newMaxSize: 0 175 | }, 176 | buffer: new Buffer('58' + '86a8eb10649cbf', 'hex') 177 | }, { 178 | // index 179 | header: { 180 | name: 1, 181 | value: 1, 182 | index: false, 183 | mustNeverIndex: false, 184 | contextUpdate: false, 185 | newMaxSize: 0 186 | }, 187 | buffer: new Buffer('82', 'hex') 188 | }, { 189 | // index 190 | header: { 191 | name: 6, 192 | value: 6, 193 | index: false, 194 | mustNeverIndex: false, 195 | contextUpdate: false, 196 | newMaxSize: 0 197 | }, 198 | buffer: new Buffer('87', 'hex') 199 | }, { 200 | // literal w/index, name index 201 | header: { 202 | name: 3, 203 | value: '/custom-path.css', 204 | index: true, 205 | mustNeverIndex: false, 206 | contextUpdate: false, 207 | newMaxSize: 0 208 | }, 209 | buffer: new Buffer('44' + '8b6096a127a56ac699d72211', 'hex') 210 | }, { 211 | // index 212 | header: { 213 | name: 63, 214 | value: 63, 215 | index: false, 216 | mustNeverIndex: false, 217 | contextUpdate: false, 218 | newMaxSize: 0 219 | }, 220 | buffer: new Buffer('C0', 'hex') 221 | }, { 222 | // literal w/index, new name & value 223 | header: { 224 | name: 'custom-key', 225 | value: 'custom-value', 226 | index: true, 227 | mustNeverIndex: false, 228 | contextUpdate: false, 229 | newMaxSize: 0 230 | }, 231 | buffer: new Buffer('40' + '8825a849e95ba97d7f' + '8925a849e95bb8e8b4bf', 'hex') 232 | }, { 233 | // index 234 | header: { 235 | name: 1, 236 | value: 1, 237 | index: false, 238 | mustNeverIndex: false, 239 | contextUpdate: false, 240 | newMaxSize: 0 241 | }, 242 | buffer: new Buffer('82', 'hex') 243 | }, { 244 | // index 245 | header: { 246 | name: 6, 247 | value: 6, 248 | index: false, 249 | mustNeverIndex: false, 250 | contextUpdate: false, 251 | newMaxSize: 0 252 | }, 253 | buffer: new Buffer('87', 'hex') 254 | }, { 255 | // index 256 | header: { 257 | name: 62, 258 | value: 62, 259 | index: false, 260 | mustNeverIndex: false, 261 | contextUpdate: false, 262 | newMaxSize: 0 263 | }, 264 | buffer: new Buffer('BF', 'hex') 265 | }, { 266 | // index 267 | header: { 268 | name: 65, 269 | value: 65, 270 | index: false, 271 | mustNeverIndex: false, 272 | contextUpdate: false, 273 | newMaxSize: 0 274 | }, 275 | buffer: new Buffer('C2', 'hex') 276 | }, { 277 | // index 278 | header: { 279 | name: 64, 280 | value: 64, 281 | index: false, 282 | mustNeverIndex: false, 283 | contextUpdate: false, 284 | newMaxSize: 0 285 | }, 286 | buffer: new Buffer('C1', 'hex') 287 | }, { 288 | // index 289 | header: { 290 | name: 61, 291 | value: 61, 292 | index: false, 293 | mustNeverIndex: false, 294 | contextUpdate: false, 295 | newMaxSize: 0 296 | }, 297 | buffer: new Buffer('BE', 'hex') 298 | }, { 299 | // Literal w/o index, name index 300 | header: { 301 | name: 6, 302 | value: "whatever", 303 | index: false, 304 | mustNeverIndex: false, 305 | contextUpdate: false, 306 | newMaxSize: 0 307 | }, 308 | buffer: new Buffer('07' + '86f138d25ee5b3', 'hex') 309 | }, { 310 | // Literal w/o index, new name & value 311 | header: { 312 | name: "foo", 313 | value: "bar", 314 | index: false, 315 | mustNeverIndex: false, 316 | contextUpdate: false, 317 | newMaxSize: 0 318 | }, 319 | buffer: new Buffer('00' + '8294e7' + '03626172', 'hex') 320 | }, { 321 | // Literal never indexed, name index 322 | header: { 323 | name: 6, 324 | value: "whatever", 325 | index: false, 326 | mustNeverIndex: true, 327 | contextUpdate: false, 328 | newMaxSize: 0 329 | }, 330 | buffer: new Buffer('17' + '86f138d25ee5b3', 'hex') 331 | }, { 332 | // Literal never indexed, new name & value 333 | header: { 334 | name: "foo", 335 | value: "bar", 336 | index: false, 337 | mustNeverIndex: true, 338 | contextUpdate: false, 339 | newMaxSize: 0 340 | }, 341 | buffer: new Buffer('10' + '8294e7' + '03626172', 'hex') 342 | }, { 343 | header: { 344 | name: -1, 345 | value: -1, 346 | index: false, 347 | mustNeverIndex: false, 348 | contextUpdate: true, 349 | newMaxSize: 100 350 | }, 351 | buffer: new Buffer('3F45', 'hex') 352 | }]; 353 | 354 | var test_header_sets = [{ 355 | headers: { 356 | ':method': 'GET', 357 | ':scheme': 'http', 358 | ':path': '/', 359 | ':authority': 'www.foo.com' 360 | }, 361 | buffer: util.concat(test_headers.slice(0, 4).map(function(test) { return test.buffer; })) 362 | }, { 363 | headers: { 364 | ':method': 'GET', 365 | ':scheme': 'https', 366 | ':path': '/', 367 | ':authority': 'www.bar.com', 368 | 'cache-control': 'no-cache' 369 | }, 370 | buffer: util.concat(test_headers.slice(4, 9).map(function(test) { return test.buffer; })) 371 | }, { 372 | headers: { 373 | ':method': 'GET', 374 | ':scheme': 'https', 375 | ':path': '/custom-path.css', 376 | ':authority': 'www.bar.com', 377 | 'custom-key': 'custom-value' 378 | }, 379 | buffer: util.concat(test_headers.slice(9, 14).map(function(test) { return test.buffer; })) 380 | }, { 381 | headers: { 382 | ':method': 'GET', 383 | ':scheme': 'https', 384 | ':path': '/custom-path.css', 385 | ':authority': ['www.foo.com', 'www.bar.com'], 386 | 'custom-key': 'custom-value' 387 | }, 388 | buffer: util.concat(test_headers.slice(14, 19).map(function(test) { return test.buffer; })) 389 | }]; 390 | 391 | describe('compressor.js', function() { 392 | describe('HeaderTable', function() { 393 | }); 394 | 395 | describe('HuffmanTable', function() { 396 | describe('method encode(buffer)', function() { 397 | it('should return the Huffman encoded version of the input buffer', function() { 398 | var table = HuffmanTable.huffmanTable; 399 | for (var decoded in test_huffman_request) { 400 | var encoded = test_huffman_request[decoded]; 401 | expect(table.encode(new Buffer(decoded)).toString('hex')).to.equal(encoded); 402 | } 403 | table = HuffmanTable.huffmanTable; 404 | for (decoded in test_huffman_response) { 405 | encoded = test_huffman_response[decoded]; 406 | expect(table.encode(new Buffer(decoded)).toString('hex')).to.equal(encoded); 407 | } 408 | }); 409 | }); 410 | describe('method decode(buffer)', function() { 411 | it('should return the Huffman decoded version of the input buffer', function() { 412 | var table = HuffmanTable.huffmanTable; 413 | for (var decoded in test_huffman_request) { 414 | var encoded = test_huffman_request[decoded]; 415 | expect(table.decode(new Buffer(encoded, 'hex')).toString()).to.equal(decoded); 416 | } 417 | table = HuffmanTable.huffmanTable; 418 | for (decoded in test_huffman_response) { 419 | encoded = test_huffman_response[decoded]; 420 | expect(table.decode(new Buffer(encoded, 'hex')).toString()).to.equal(decoded); 421 | } 422 | }); 423 | }); 424 | }); 425 | 426 | describe('HeaderSetCompressor', function() { 427 | describe('static method .integer(I, N)', function() { 428 | it('should return an array of buffers that represent the N-prefix coded form of the integer I', function() { 429 | for (var i = 0; i < test_integers.length; i++) { 430 | var test = test_integers[i]; 431 | test.buffer.cursor = 0; 432 | expect(util.concat(HeaderSetCompressor.integer(test.I, test.N))).to.deep.equal(test.buffer); 433 | } 434 | }); 435 | }); 436 | describe('static method .string(string)', function() { 437 | it('should return an array of buffers that represent the encoded form of the string', function() { 438 | var table = HuffmanTable.huffmanTable; 439 | for (var i = 0; i < test_strings.length; i++) { 440 | var test = test_strings[i]; 441 | expect(util.concat(HeaderSetCompressor.string(test.string, table))).to.deep.equal(test.buffer); 442 | } 443 | }); 444 | }); 445 | describe('static method .header({ name, value, index })', function() { 446 | it('should return an array of buffers that represent the encoded form of the header', function() { 447 | var table = HuffmanTable.huffmanTable; 448 | for (var i = 0; i < test_headers.length; i++) { 449 | var test = test_headers[i]; 450 | expect(util.concat(HeaderSetCompressor.header(test.header, table))).to.deep.equal(test.buffer); 451 | } 452 | }); 453 | }); 454 | }); 455 | 456 | describe('HeaderSetDecompressor', function() { 457 | describe('static method .integer(buffer, N)', function() { 458 | it('should return the parsed N-prefix coded number and increase the cursor property of buffer', function() { 459 | for (var i = 0; i < test_integers.length; i++) { 460 | var test = test_integers[i]; 461 | test.buffer.cursor = 0; 462 | expect(HeaderSetDecompressor.integer(test.buffer, test.N)).to.equal(test.I); 463 | expect(test.buffer.cursor).to.equal(test.buffer.length); 464 | } 465 | }); 466 | }); 467 | describe('static method .string(buffer)', function() { 468 | it('should return the parsed string and increase the cursor property of buffer', function() { 469 | var table = HuffmanTable.huffmanTable; 470 | for (var i = 0; i < test_strings.length; i++) { 471 | var test = test_strings[i]; 472 | test.buffer.cursor = 0; 473 | expect(HeaderSetDecompressor.string(test.buffer, table)).to.equal(test.string); 474 | expect(test.buffer.cursor).to.equal(test.buffer.length); 475 | } 476 | }); 477 | }); 478 | describe('static method .header(buffer)', function() { 479 | it('should return the parsed header and increase the cursor property of buffer', function() { 480 | var table = HuffmanTable.huffmanTable; 481 | for (var i = 0; i < test_headers.length; i++) { 482 | var test = test_headers[i]; 483 | test.buffer.cursor = 0; 484 | expect(HeaderSetDecompressor.header(test.buffer, table)).to.deep.equal(test.header); 485 | expect(test.buffer.cursor).to.equal(test.buffer.length); 486 | } 487 | }); 488 | }); 489 | }); 490 | describe('Decompressor', function() { 491 | describe('method decompress(buffer)', function() { 492 | it('should return the parsed header set in { name1: value1, name2: [value2, value3], ... } format', function() { 493 | var decompressor = new Decompressor(util.log, 'REQUEST'); 494 | for (var i = 0; i < test_header_sets.length - 1; i++) { 495 | var header_set = test_header_sets[i]; 496 | expect(decompressor.decompress(header_set.buffer)).to.deep.equal(header_set.headers); 497 | } 498 | }); 499 | }); 500 | describe('transform stream', function() { 501 | it('should emit an error event if a series of header frames is interleaved with other frames', function() { 502 | var decompressor = new Decompressor(util.log, 'REQUEST'); 503 | var error_occured = false; 504 | decompressor.on('error', function() { 505 | error_occured = true; 506 | }); 507 | decompressor.write({ 508 | type: 'HEADERS', 509 | flags: { 510 | END_HEADERS: false 511 | }, 512 | data: new Buffer(5) 513 | }); 514 | decompressor.write({ 515 | type: 'DATA', 516 | flags: {}, 517 | data: new Buffer(5) 518 | }); 519 | expect(error_occured).to.be.equal(true); 520 | }); 521 | }); 522 | }); 523 | 524 | describe('invariant', function() { 525 | describe('decompressor.decompress(compressor.compress(headerset)) === headerset', function() { 526 | it('should be true for any header set if the states are synchronized', function() { 527 | var compressor = new Compressor(util.log, 'REQUEST'); 528 | var decompressor = new Decompressor(util.log, 'REQUEST'); 529 | var n = test_header_sets.length; 530 | for (var i = 0; i < 10; i++) { 531 | var headers = test_header_sets[i%n].headers; 532 | var compressed = compressor.compress(headers); 533 | var decompressed = decompressor.decompress(compressed); 534 | expect(decompressed).to.deep.equal(headers); 535 | expect(compressor._table).to.deep.equal(decompressor._table); 536 | } 537 | }); 538 | }); 539 | describe('source.pipe(compressor).pipe(decompressor).pipe(destination)', function() { 540 | it('should behave like source.pipe(destination) for a stream of frames', function(done) { 541 | var compressor = new Compressor(util.log, 'RESPONSE'); 542 | var decompressor = new Decompressor(util.log, 'RESPONSE'); 543 | var n = test_header_sets.length; 544 | compressor.pipe(decompressor); 545 | for (var i = 0; i < 10; i++) { 546 | compressor.write({ 547 | type: i%2 ? 'HEADERS' : 'PUSH_PROMISE', 548 | flags: {}, 549 | headers: test_header_sets[i%n].headers 550 | }); 551 | } 552 | setTimeout(function() { 553 | for (var j = 0; j < 10; j++) { 554 | expect(decompressor.read().headers).to.deep.equal(test_header_sets[j%n].headers); 555 | } 556 | done(); 557 | }, 10); 558 | }); 559 | }); 560 | describe('huffmanTable.decompress(huffmanTable.compress(buffer)) === buffer', function() { 561 | it('should be true for any buffer', function() { 562 | for (var i = 0; i < 10; i++) { 563 | var buffer = []; 564 | while (Math.random() > 0.1) { 565 | buffer.push(Math.floor(Math.random() * 256)) 566 | } 567 | buffer = new Buffer(buffer); 568 | var table = HuffmanTable.huffmanTable; 569 | var result = table.decode(table.encode(buffer)); 570 | expect(result).to.deep.equal(buffer); 571 | } 572 | }); 573 | }); 574 | }); 575 | }); 576 | -------------------------------------------------------------------------------- /lib/protocol/connection.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | // The Connection class 4 | // ==================== 5 | 6 | // The Connection class manages HTTP/2 connections. Each instance corresponds to one transport 7 | // stream (TCP stream). It operates by sending and receiving frames and is implemented as a 8 | // [Flow](flow.html) subclass. 9 | 10 | var Flow = require('./flow').Flow; 11 | 12 | exports.Connection = Connection; 13 | 14 | // Public API 15 | // ---------- 16 | 17 | // * **new Connection(log, firstStreamId, settings)**: create a new Connection 18 | // 19 | // * **Event: 'error' (type)**: signals a connection level error made by the other end 20 | // 21 | // * **Event: 'peerError' (type)**: signals the receipt of a GOAWAY frame that contains an error 22 | // code other than NO_ERROR 23 | // 24 | // * **Event: 'stream' (stream)**: signals that there's an incoming stream 25 | // 26 | // * **createStream(): stream**: initiate a new stream 27 | // 28 | // * **set(settings, callback)**: change the value of one or more settings according to the 29 | // key-value pairs of `settings`. The callback is called after the peer acknowledged the changes. 30 | // 31 | // * **ping([callback])**: send a ping and call callback when the answer arrives 32 | // 33 | // * **close([error])**: close the stream with an error code 34 | 35 | // Constructor 36 | // ----------- 37 | 38 | // The main aspects of managing the connection are: 39 | function Connection(log, firstStreamId, settings) { 40 | // * initializing the base class 41 | Flow.call(this, 0); 42 | 43 | // * logging: every method uses the common logger object 44 | this._log = log.child({ component: 'connection' }); 45 | 46 | // * stream management 47 | this._initializeStreamManagement(firstStreamId); 48 | 49 | // * lifecycle management 50 | this._initializeLifecycleManagement(); 51 | 52 | // * flow control 53 | this._initializeFlowControl(); 54 | 55 | // * settings management 56 | this._initializeSettingsManagement(settings); 57 | 58 | // * multiplexing 59 | this._initializeMultiplexing(); 60 | } 61 | Connection.prototype = Object.create(Flow.prototype, { constructor: { value: Connection } }); 62 | 63 | // Overview 64 | // -------- 65 | 66 | // | ^ | ^ 67 | // v | v | 68 | // +--------------+ +--------------+ 69 | // +---| stream1 |---| stream2 |---- .... ---+ 70 | // | | +----------+ | | +----------+ | | 71 | // | | | stream1. | | | | stream2. | | | 72 | // | +-| upstream |-+ +-| upstream |-+ | 73 | // | +----------+ +----------+ | 74 | // | | ^ | ^ | 75 | // | v | v | | 76 | // | +-----+-------------+-----+-------- .... | 77 | // | ^ | | | | 78 | // | | v | | | 79 | // | +--------------+ | | | 80 | // | | stream0 | | | | 81 | // | | connection | | | | 82 | // | | management | multiplexing | 83 | // | +--------------+ flow control | 84 | // | | ^ | 85 | // | _read() | | _write() | 86 | // | v | | 87 | // | +------------+ +-----------+ | 88 | // | |output queue| |input queue| | 89 | // +----------------+------------+-+-----------+-----------------+ 90 | // | ^ 91 | // read() | | write() 92 | // v | 93 | 94 | // Stream management 95 | // ----------------- 96 | 97 | var Stream = require('./stream').Stream; 98 | 99 | // Initialization: 100 | Connection.prototype._initializeStreamManagement = function _initializeStreamManagement(firstStreamId) { 101 | // * streams are stored in two data structures: 102 | // * `_streamIds` is an id -> stream map of the streams that are allowed to receive frames. 103 | // * `_streamPriorities` is a priority -> [stream] map of stream that allowed to send frames. 104 | this._streamIds = []; 105 | this._streamPriorities = []; 106 | 107 | // * The next outbound stream ID and the last inbound stream id 108 | this._nextStreamId = firstStreamId; 109 | this._lastIncomingStream = 0; 110 | 111 | // * Calling `_writeControlFrame` when there's an incoming stream with 0 as stream ID 112 | this._streamIds[0] = { upstream: { write: this._writeControlFrame.bind(this) } }; 113 | 114 | // * By default, the number of concurrent outbound streams is not limited. The `_streamLimit` can 115 | // be set by the SETTINGS_MAX_CONCURRENT_STREAMS setting. 116 | this._streamSlotsFree = Infinity; 117 | this._streamLimit = Infinity; 118 | this.on('RECEIVING_SETTINGS_MAX_CONCURRENT_STREAMS', this._updateStreamLimit); 119 | }; 120 | 121 | // `_writeControlFrame` is called when there's an incoming frame in the `_control` stream. It 122 | // broadcasts the message by creating an event on it. 123 | Connection.prototype._writeControlFrame = function _writeControlFrame(frame) { 124 | if ((frame.type === 'SETTINGS') || (frame.type === 'PING') || 125 | (frame.type === 'GOAWAY') || (frame.type === 'WINDOW_UPDATE') || 126 | (frame.type === 'ALTSVC')) { 127 | this._log.debug({ frame: frame }, 'Receiving connection level frame'); 128 | this.emit(frame.type, frame); 129 | } else { 130 | this._log.error({ frame: frame }, 'Invalid connection level frame'); 131 | this.emit('error', 'PROTOCOL_ERROR'); 132 | } 133 | }; 134 | 135 | // Methods to manage the stream slot pool: 136 | Connection.prototype._updateStreamLimit = function _updateStreamLimit(newStreamLimit) { 137 | var wakeup = (this._streamSlotsFree === 0) && (newStreamLimit > this._streamLimit); 138 | this._streamSlotsFree += newStreamLimit - this._streamLimit; 139 | this._streamLimit = newStreamLimit; 140 | if (wakeup) { 141 | this.emit('wakeup'); 142 | } 143 | }; 144 | 145 | Connection.prototype._changeStreamCount = function _changeStreamCount(change) { 146 | if (change) { 147 | this._log.trace({ free: this._streamSlotsFree, change: change }, 148 | 'Changing active stream count.'); 149 | var wakeup = (this._streamSlotsFree === 0) && (change < 0); 150 | this._streamSlotsFree -= change; 151 | if (wakeup) { 152 | this.emit('wakeup'); 153 | } 154 | } 155 | }; 156 | 157 | // Creating a new *inbound or outbound* stream with the given `id` (which is undefined in case of 158 | // an outbound stream) consists of three steps: 159 | // 160 | // 1. var stream = new Stream(this._log, this); 161 | // 2. this._allocateId(stream, id); 162 | // 2. this._allocatePriority(stream); 163 | 164 | // Allocating an ID to a stream 165 | Connection.prototype._allocateId = function _allocateId(stream, id) { 166 | // * initiated stream without definite ID 167 | if (id === undefined) { 168 | id = this._nextStreamId; 169 | this._nextStreamId += 2; 170 | } 171 | 172 | // * incoming stream with a legitim ID (larger than any previous and different parity than ours) 173 | else if ((id > this._lastIncomingStream) && ((id - this._nextStreamId) % 2 !== 0)) { 174 | this._lastIncomingStream = id; 175 | } 176 | 177 | // * incoming stream with invalid ID 178 | else { 179 | this._log.error({ stream_id: id, lastIncomingStream: this._lastIncomingStream }, 180 | 'Invalid incoming stream ID.'); 181 | this.emit('error', 'PROTOCOL_ERROR'); 182 | return undefined; 183 | } 184 | 185 | assert(!(id in this._streamIds)); 186 | 187 | // * adding to `this._streamIds` 188 | this._log.trace({ s: stream, stream_id: id }, 'Allocating ID for stream.'); 189 | this._streamIds[id] = stream; 190 | stream.id = id; 191 | this.emit('new_stream', stream, id); 192 | 193 | // * forwarding connection errors from streams 194 | stream.on('connectionError', this.emit.bind(this, 'error')); 195 | 196 | return id; 197 | }; 198 | 199 | // Allocating a priority to a stream, and managing priority changes 200 | Connection.prototype._allocatePriority = function _allocatePriority(stream) { 201 | this._log.trace({ s: stream }, 'Allocating priority for stream.'); 202 | this._insert(stream, stream._priority); 203 | stream.on('priority', this._reprioritize.bind(this, stream)); 204 | stream.upstream.on('readable', this.emit.bind(this, 'wakeup')); 205 | this.emit('wakeup'); 206 | }; 207 | 208 | Connection.prototype._insert = function _insert(stream, priority) { 209 | if (priority in this._streamPriorities) { 210 | this._streamPriorities[priority].push(stream); 211 | } else { 212 | this._streamPriorities[priority] = [stream]; 213 | } 214 | }; 215 | 216 | Connection.prototype._reprioritize = function _reprioritize(stream, priority) { 217 | var bucket = this._streamPriorities[stream._priority]; 218 | var index = bucket.indexOf(stream); 219 | assert(index !== -1); 220 | bucket.splice(index, 1); 221 | if (bucket.length === 0) { 222 | delete this._streamPriorities[stream._priority]; 223 | } 224 | 225 | this._insert(stream, priority); 226 | }; 227 | 228 | // Creating an *inbound* stream with the given ID. It is called when there's an incoming frame to 229 | // a previously nonexistent stream. 230 | Connection.prototype._createIncomingStream = function _createIncomingStream(id) { 231 | this._log.debug({ stream_id: id }, 'New incoming stream.'); 232 | 233 | var stream = new Stream(this._log, this); 234 | this._allocateId(stream, id); 235 | this._allocatePriority(stream); 236 | this.emit('stream', stream, id); 237 | 238 | return stream; 239 | }; 240 | 241 | // Creating an *outbound* stream 242 | Connection.prototype.createStream = function createStream() { 243 | this._log.trace('Creating new outbound stream.'); 244 | 245 | // * Receiving is enabled immediately, and an ID gets assigned to the stream 246 | var stream = new Stream(this._log, this); 247 | this._allocatePriority(stream); 248 | 249 | return stream; 250 | }; 251 | 252 | // Multiplexing 253 | // ------------ 254 | 255 | Connection.prototype._initializeMultiplexing = function _initializeMultiplexing() { 256 | this.on('window_update', this.emit.bind(this, 'wakeup')); 257 | this._sendScheduled = false; 258 | this._firstFrameReceived = false; 259 | }; 260 | 261 | // The `_send` method is a virtual method of the [Flow class](flow.html) that has to be implemented 262 | // by child classes. It reads frames from streams and pushes them to the output buffer. 263 | Connection.prototype._send = function _send(immediate) { 264 | // * Do not do anything if the connection is already closed 265 | if (this._closed) { 266 | return; 267 | } 268 | 269 | // * Collapsing multiple calls in a turn into a single deferred call 270 | if (immediate) { 271 | this._sendScheduled = false; 272 | } else { 273 | if (!this._sendScheduled) { 274 | this._sendScheduled = true; 275 | setImmediate(this._send.bind(this, true)); 276 | } 277 | return; 278 | } 279 | 280 | this._log.trace('Starting forwarding frames from streams.'); 281 | 282 | // * Looping through priority `bucket`s in priority order. 283 | priority_loop: 284 | for (var priority in this._streamPriorities) { 285 | var bucket = this._streamPriorities[priority]; 286 | var nextBucket = []; 287 | 288 | // * Forwarding frames from buckets with round-robin scheduling. 289 | // 1. pulling out frame 290 | // 2. if there's no frame, skip this stream 291 | // 3. if forwarding this frame would make `streamCount` greater than `streamLimit`, skip 292 | // this stream 293 | // 4. adding stream to the bucket of the next round 294 | // 5. assigning an ID to the frame (allocating an ID to the stream if there isn't already) 295 | // 6. if forwarding a PUSH_PROMISE, allocate ID to the promised stream 296 | // 7. forwarding the frame, changing `streamCount` as appropriate 297 | // 8. stepping to the next stream if there's still more frame needed in the output buffer 298 | // 9. switching to the bucket of the next round 299 | while (bucket.length > 0) { 300 | for (var index = 0; index < bucket.length; index++) { 301 | var stream = bucket[index]; 302 | var frame = stream.upstream.read((this._window > 0) ? this._window : -1); 303 | 304 | if (!frame) { 305 | continue; 306 | } else if (frame.count_change > this._streamSlotsFree) { 307 | stream.upstream.unshift(frame); 308 | continue; 309 | } 310 | 311 | nextBucket.push(stream); 312 | 313 | if (frame.stream === undefined) { 314 | frame.stream = stream.id || this._allocateId(stream); 315 | } 316 | 317 | if (frame.type === 'PUSH_PROMISE') { 318 | this._allocatePriority(frame.promised_stream); 319 | frame.promised_stream = this._allocateId(frame.promised_stream); 320 | } 321 | 322 | this._log.trace({ s: stream, frame: frame }, 'Forwarding outgoing frame'); 323 | var moreNeeded = this.push(frame); 324 | this._changeStreamCount(frame.count_change); 325 | 326 | assert(moreNeeded !== null); // The frame shouldn't be unforwarded 327 | if (moreNeeded === false) { 328 | break priority_loop; 329 | } 330 | } 331 | 332 | bucket = nextBucket; 333 | nextBucket = []; 334 | } 335 | } 336 | 337 | // * if we couldn't forward any frame, then sleep until window update, or some other wakeup event 338 | if (moreNeeded === undefined) { 339 | this.once('wakeup', this._send.bind(this)); 340 | } 341 | 342 | this._log.trace({ moreNeeded: moreNeeded }, 'Stopping forwarding frames from streams.'); 343 | }; 344 | 345 | // The `_receive` method is another virtual method of the [Flow class](flow.html) that has to be 346 | // implemented by child classes. It forwards the given frame to the appropriate stream: 347 | Connection.prototype._receive = function _receive(frame, done) { 348 | this._log.trace({ frame: frame }, 'Forwarding incoming frame'); 349 | 350 | // * first frame needs to be checked by the `_onFirstFrameReceived` method 351 | if (!this._firstFrameReceived) { 352 | this._firstFrameReceived = true; 353 | this._onFirstFrameReceived(frame); 354 | } 355 | 356 | // Do some sanity checking here before we create a stream 357 | if ((frame.type == 'SETTINGS' || 358 | frame.type == 'PING' || 359 | frame.type == 'GOAWAY') && 360 | frame.stream != 0) { 361 | // Got connection-level frame on a stream - EEP! 362 | this.close('PROTOCOL_ERROR'); 363 | return; 364 | } else if ((frame.type == 'DATA' || 365 | frame.type == 'HEADERS' || 366 | frame.type == 'PRIORITY' || 367 | frame.type == 'RST_STREAM' || 368 | frame.type == 'PUSH_PROMISE' || 369 | frame.type == 'CONTINUATION') && 370 | frame.stream == 0) { 371 | // Got stream-level frame on connection - EEP! 372 | this.close('PROTOCOL_ERROR'); 373 | return; 374 | } 375 | // WINDOW_UPDATE can be on either stream or connection 376 | 377 | // * gets the appropriate stream from the stream registry 378 | var stream = this._streamIds[frame.stream]; 379 | 380 | // * or creates one if it's not in `this.streams` 381 | if (!stream) { 382 | stream = this._createIncomingStream(frame.stream); 383 | } 384 | 385 | // * in case of PUSH_PROMISE, replaces the promised stream id with a new incoming stream 386 | if (frame.type === 'PUSH_PROMISE') { 387 | frame.promised_stream = this._createIncomingStream(frame.promised_stream); 388 | } 389 | 390 | frame.count_change = this._changeStreamCount.bind(this); 391 | 392 | // * and writes it to the `stream`'s `upstream` 393 | stream.upstream.write(frame); 394 | 395 | done(); 396 | }; 397 | 398 | // Settings management 399 | // ------------------- 400 | 401 | var defaultSettings = { 402 | }; 403 | 404 | // Settings management initialization: 405 | Connection.prototype._initializeSettingsManagement = function _initializeSettingsManagement(settings) { 406 | // * Setting up the callback queue for setting acknowledgements 407 | this._settingsAckCallbacks = []; 408 | 409 | // * Sending the initial settings. 410 | this._log.debug({ settings: settings }, 411 | 'Sending the first SETTINGS frame as part of the connection header.'); 412 | this.set(settings || defaultSettings); 413 | 414 | // * Forwarding SETTINGS frames to the `_receiveSettings` method 415 | this.on('SETTINGS', this._receiveSettings); 416 | this.on('RECEIVING_SETTINGS_MAX_FRAME_SIZE', this._sanityCheckMaxFrameSize); 417 | }; 418 | 419 | // * Checking that the first frame the other endpoint sends is SETTINGS 420 | Connection.prototype._onFirstFrameReceived = function _onFirstFrameReceived(frame) { 421 | if ((frame.stream === 0) && (frame.type === 'SETTINGS')) { 422 | this._log.debug('Receiving the first SETTINGS frame as part of the connection header.'); 423 | } else { 424 | this._log.fatal({ frame: frame }, 'Invalid connection header: first frame is not SETTINGS.'); 425 | this.emit('error', 'PROTOCOL_ERROR'); 426 | } 427 | }; 428 | 429 | // Handling of incoming SETTINGS frames. 430 | Connection.prototype._receiveSettings = function _receiveSettings(frame) { 431 | // * If it's an ACK, call the appropriate callback 432 | if (frame.flags.ACK) { 433 | var callback = this._settingsAckCallbacks.shift(); 434 | if (callback) { 435 | callback(); 436 | } 437 | } 438 | 439 | // * If it's a setting change request, then send an ACK and change the appropriate settings 440 | else { 441 | if (!this._closed) { 442 | this.push({ 443 | type: 'SETTINGS', 444 | flags: { ACK: true }, 445 | stream: 0, 446 | settings: {} 447 | }); 448 | } 449 | for (var name in frame.settings) { 450 | this.emit('RECEIVING_' + name, frame.settings[name]); 451 | } 452 | } 453 | }; 454 | 455 | Connection.prototype._sanityCheckMaxFrameSize = function _sanityCheckMaxFrameSize(value) { 456 | if ((value < 0x4000) || (value >= 0x01000000)) { 457 | this._log.fatal('Received invalid value for max frame size: ' + value); 458 | this.emit('error'); 459 | } 460 | }; 461 | 462 | // Changing one or more settings value and sending out a SETTINGS frame 463 | Connection.prototype.set = function set(settings, callback) { 464 | // * Calling the callback and emitting event when the change is acknowledges 465 | var self = this; 466 | this._settingsAckCallbacks.push(function() { 467 | for (var name in settings) { 468 | self.emit('ACKNOWLEDGED_' + name, settings[name]); 469 | } 470 | if (callback) { 471 | callback(); 472 | } 473 | }); 474 | 475 | // * Sending out the SETTINGS frame 476 | this.push({ 477 | type: 'SETTINGS', 478 | flags: { ACK: false }, 479 | stream: 0, 480 | settings: settings 481 | }); 482 | for (var name in settings) { 483 | this.emit('SENDING_' + name, settings[name]); 484 | } 485 | }; 486 | 487 | // Lifecycle management 488 | // -------------------- 489 | 490 | // The main responsibilities of lifecycle management code: 491 | // 492 | // * keeping the connection alive by 493 | // * sending PINGs when the connection is idle 494 | // * answering PINGs 495 | // * ending the connection 496 | 497 | Connection.prototype._initializeLifecycleManagement = function _initializeLifecycleManagement() { 498 | this._pings = {}; 499 | this.on('PING', this._receivePing); 500 | this.on('GOAWAY', this._receiveGoaway); 501 | this._closed = false; 502 | }; 503 | 504 | // Generating a string of length 16 with random hexadecimal digits 505 | Connection.prototype._generatePingId = function _generatePingId() { 506 | do { 507 | var id = ''; 508 | for (var i = 0; i < 16; i++) { 509 | id += Math.floor(Math.random()*16).toString(16); 510 | } 511 | } while(id in this._pings); 512 | return id; 513 | }; 514 | 515 | // Sending a ping and calling `callback` when the answer arrives 516 | Connection.prototype.ping = function ping(callback) { 517 | var id = this._generatePingId(); 518 | var data = new Buffer(id, 'hex'); 519 | this._pings[id] = callback; 520 | 521 | this._log.debug({ data: data }, 'Sending PING.'); 522 | this.push({ 523 | type: 'PING', 524 | flags: { 525 | ACK: false 526 | }, 527 | stream: 0, 528 | data: data 529 | }); 530 | }; 531 | 532 | // Answering pings 533 | Connection.prototype._receivePing = function _receivePing(frame) { 534 | if (frame.flags.ACK) { 535 | var id = frame.data.toString('hex'); 536 | if (id in this._pings) { 537 | this._log.debug({ data: frame.data }, 'Receiving answer for a PING.'); 538 | var callback = this._pings[id]; 539 | if (callback) { 540 | callback(); 541 | } 542 | delete this._pings[id]; 543 | } else { 544 | this._log.warn({ data: frame.data }, 'Unsolicited PING answer.'); 545 | } 546 | 547 | } else { 548 | this._log.debug({ data: frame.data }, 'Answering PING.'); 549 | this.push({ 550 | type: 'PING', 551 | flags: { 552 | ACK: true 553 | }, 554 | stream: 0, 555 | data: frame.data 556 | }); 557 | } 558 | }; 559 | 560 | // Terminating the connection 561 | Connection.prototype.close = function close(error) { 562 | if (this._closed) { 563 | this._log.warn('Trying to close an already closed connection'); 564 | return; 565 | } 566 | 567 | this._log.debug({ error: error }, 'Closing the connection'); 568 | this.push({ 569 | type: 'GOAWAY', 570 | flags: {}, 571 | stream: 0, 572 | last_stream: this._lastIncomingStream, 573 | error: error || 'NO_ERROR' 574 | }); 575 | this.push(null); 576 | this._closed = true; 577 | }; 578 | 579 | Connection.prototype._receiveGoaway = function _receiveGoaway(frame) { 580 | this._log.debug({ error: frame.error }, 'Other end closed the connection'); 581 | this.push(null); 582 | this._closed = true; 583 | if (frame.error !== 'NO_ERROR') { 584 | this.emit('peerError', frame.error); 585 | } 586 | }; 587 | 588 | // Flow control 589 | // ------------ 590 | 591 | Connection.prototype._initializeFlowControl = function _initializeFlowControl() { 592 | // Handling of initial window size of individual streams. 593 | this._initialStreamWindowSize = INITIAL_STREAM_WINDOW_SIZE; 594 | this.on('new_stream', function(stream) { 595 | stream.upstream.setInitialWindow(this._initialStreamWindowSize); 596 | }); 597 | this.on('RECEIVING_SETTINGS_INITIAL_WINDOW_SIZE', this._setInitialStreamWindowSize); 598 | this._streamIds[0].upstream.setInitialWindow = function noop() {}; 599 | }; 600 | 601 | // The initial connection flow control window is 65535 bytes. 602 | var INITIAL_STREAM_WINDOW_SIZE = 65535; 603 | 604 | // A SETTINGS frame can alter the initial flow control window size for all current streams. When the 605 | // value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the window size of all 606 | // stream by calling the `setInitialStreamWindowSize` method. The window size has to be modified by 607 | // the difference between the new value and the old value. 608 | Connection.prototype._setInitialStreamWindowSize = function _setInitialStreamWindowSize(size) { 609 | if ((this._initialStreamWindowSize === Infinity) && (size !== Infinity)) { 610 | this._log.error('Trying to manipulate initial flow control window size after flow control was turned off.'); 611 | this.emit('error', 'FLOW_CONTROL_ERROR'); 612 | } else { 613 | this._log.debug({ size: size }, 'Changing stream initial window size.'); 614 | this._initialStreamWindowSize = size; 615 | this._streamIds.forEach(function(stream) { 616 | stream.upstream.setInitialWindow(size); 617 | }); 618 | } 619 | }; 620 | -------------------------------------------------------------------------------- /lib/protocol/stream.js: -------------------------------------------------------------------------------- 1 | var assert = require('assert'); 2 | 3 | // The Stream class 4 | // ================ 5 | 6 | // Stream is a [Duplex stream](https://nodejs.org/api/stream.html#stream_class_stream_duplex) 7 | // subclass that implements the [HTTP/2 Stream](https://tools.ietf.org/html/rfc7540#section-5) 8 | // concept. It has two 'sides': one that is used by the user to send/receive data (the `stream` 9 | // object itself) and one that is used by a Connection to read/write frames to/from the other peer 10 | // (`stream.upstream`). 11 | 12 | var Duplex = require('stream').Duplex; 13 | 14 | exports.Stream = Stream; 15 | 16 | // Public API 17 | // ---------- 18 | 19 | // * **new Stream(log, connection)**: create a new Stream 20 | // 21 | // * **Event: 'headers' (headers)**: signals incoming headers 22 | // 23 | // * **Event: 'promise' (stream, headers)**: signals an incoming push promise 24 | // 25 | // * **Event: 'priority' (priority)**: signals a priority change. `priority` is a number between 0 26 | // (highest priority) and 2^31-1 (lowest priority). Default value is 2^30. 27 | // 28 | // * **Event: 'error' (type)**: signals an error 29 | // 30 | // * **headers(headers)**: send headers 31 | // 32 | // * **promise(headers): Stream**: promise a stream 33 | // 34 | // * **priority(priority)**: set the priority of the stream. Priority can be changed by the peer 35 | // too, but once it is set locally, it can not be changed remotely. 36 | // 37 | // * **reset(error)**: reset the stream with an error code 38 | // 39 | // * **upstream**: a [Flow](flow.js) that is used by the parent connection to write/read frames 40 | // that are to be sent/arrived to/from the peer and are related to this stream. 41 | // 42 | // Headers are always in the [regular node.js header format][1]. 43 | // [1]: https://nodejs.org/api/http.html#http_message_headers 44 | 45 | // Constructor 46 | // ----------- 47 | 48 | // The main aspects of managing the stream are: 49 | function Stream(log, connection) { 50 | Duplex.call(this); 51 | 52 | // * logging 53 | this._log = log.child({ component: 'stream', s: this }); 54 | 55 | // * receiving and sending stream management commands 56 | this._initializeManagement(); 57 | 58 | // * sending and receiving frames to/from the upstream connection 59 | this._initializeDataFlow(); 60 | 61 | // * maintaining the state of the stream (idle, open, closed, etc.) and error detection 62 | this._initializeState(); 63 | 64 | this.connection = connection; 65 | } 66 | 67 | Stream.prototype = Object.create(Duplex.prototype, { constructor: { value: Stream } }); 68 | 69 | // Managing the stream 70 | // ------------------- 71 | 72 | // the default stream priority is 2^30 73 | var DEFAULT_PRIORITY = Math.pow(2, 30); 74 | var MAX_PRIORITY = Math.pow(2, 31) - 1; 75 | 76 | // PUSH_PROMISE and HEADERS are forwarded to the user through events. 77 | Stream.prototype._initializeManagement = function _initializeManagement() { 78 | this._resetSent = false; 79 | this._priority = DEFAULT_PRIORITY; 80 | this._letPeerPrioritize = true; 81 | }; 82 | 83 | Stream.prototype.promise = function promise(headers) { 84 | var stream = new Stream(this._log, this.connection); 85 | stream._priority = Math.min(this._priority + 1, MAX_PRIORITY); 86 | this._pushUpstream({ 87 | type: 'PUSH_PROMISE', 88 | flags: {}, 89 | stream: this.id, 90 | promised_stream: stream, 91 | headers: headers 92 | }); 93 | return stream; 94 | }; 95 | 96 | Stream.prototype._onPromise = function _onPromise(frame) { 97 | this.emit('promise', frame.promised_stream, frame.headers); 98 | }; 99 | 100 | Stream.prototype.headers = function headers(headers) { 101 | this._pushUpstream({ 102 | type: 'HEADERS', 103 | flags: {}, 104 | stream: this.id, 105 | headers: headers 106 | }); 107 | }; 108 | 109 | Stream.prototype._onHeaders = function _onHeaders(frame) { 110 | if (frame.priority !== undefined) { 111 | this.priority(frame.priority, true); 112 | } 113 | this.emit('headers', frame.headers); 114 | }; 115 | 116 | Stream.prototype.priority = function priority(priority, peer) { 117 | if ((peer && this._letPeerPrioritize) || !peer) { 118 | if (!peer) { 119 | this._letPeerPrioritize = false; 120 | 121 | var lastFrame = this.upstream.getLastQueuedFrame(); 122 | if (lastFrame && ((lastFrame.type === 'HEADERS') || (lastFrame.type === 'PRIORITY'))) { 123 | lastFrame.priority = priority; 124 | } else { 125 | this._pushUpstream({ 126 | type: 'PRIORITY', 127 | flags: {}, 128 | stream: this.id, 129 | priority: priority 130 | }); 131 | } 132 | } 133 | 134 | this._log.debug({ priority: priority }, 'Changing priority'); 135 | this.emit('priority', priority); 136 | this._priority = priority; 137 | } 138 | }; 139 | 140 | Stream.prototype._onPriority = function _onPriority(frame) { 141 | this.priority(frame.priority, true); 142 | }; 143 | 144 | // Resetting the stream. Normally, an endpoint SHOULD NOT send more than one RST_STREAM frame for 145 | // any stream. 146 | Stream.prototype.reset = function reset(error) { 147 | if (!this._resetSent) { 148 | this._resetSent = true; 149 | this._pushUpstream({ 150 | type: 'RST_STREAM', 151 | flags: {}, 152 | stream: this.id, 153 | error: error 154 | }); 155 | } 156 | }; 157 | 158 | // Specify an alternate service for the origin of this stream 159 | Stream.prototype.altsvc = function altsvc(host, port, protocolID, maxAge, origin) { 160 | var stream; 161 | if (origin) { 162 | stream = 0; 163 | } else { 164 | stream = this.id; 165 | } 166 | this._pushUpstream({ 167 | type: 'ALTSVC', 168 | flags: {}, 169 | stream: stream, 170 | host: host, 171 | port: port, 172 | protocolID: protocolID, 173 | origin: origin, 174 | maxAge: maxAge 175 | }); 176 | }; 177 | 178 | // Data flow 179 | // --------- 180 | 181 | // The incoming and the generated outgoing frames are received/transmitted on the `this.upstream` 182 | // [Flow](flow.html). The [Connection](connection.html) object instantiating the stream will read 183 | // and write frames to/from it. The stream itself is a regular [Duplex stream][1], and is used by 184 | // the user to write or read the body of the request. 185 | // [1]: https://nodejs.org/api/stream.html#stream_class_stream_duplex 186 | 187 | // upstream side stream user side 188 | // 189 | // +------------------------------------+ 190 | // | | 191 | // +------------------+ | 192 | // | upstream | | 193 | // | | | 194 | // +--+ | +--| 195 | // read() | | _send() | _write() | | write(buf) 196 | // <--------------|B |<--------------|--------------| B|<------------ 197 | // | | | | | 198 | // frames +--+ | +--| buffers 199 | // | | | | | 200 | // -------------->|B |---------------|------------->| B|------------> 201 | // write(frame) | | _receive() | _read() | | read() 202 | // +--+ | +--| 203 | // | | | 204 | // | | | 205 | // +------------------+ | 206 | // | | 207 | // +------------------------------------+ 208 | // 209 | // B: input or output buffer 210 | 211 | var Flow = require('./flow').Flow; 212 | 213 | Stream.prototype._initializeDataFlow = function _initializeDataFlow() { 214 | this.id = undefined; 215 | 216 | this._ended = false; 217 | 218 | this.upstream = new Flow(); 219 | this.upstream._log = this._log; 220 | this.upstream._send = this._send.bind(this); 221 | this.upstream._receive = this._receive.bind(this); 222 | this.upstream.write = this._writeUpstream.bind(this); 223 | this.upstream.on('error', this.emit.bind(this, 'error')); 224 | 225 | this.on('finish', this._finishing); 226 | }; 227 | 228 | Stream.prototype._pushUpstream = function _pushUpstream(frame) { 229 | this.upstream.push(frame); 230 | this._transition(true, frame); 231 | }; 232 | 233 | // Overriding the upstream's `write` allows us to act immediately instead of waiting for the input 234 | // queue to empty. This is important in case of control frames. 235 | Stream.prototype._writeUpstream = function _writeUpstream(frame) { 236 | this._log.debug({ frame: frame }, 'Receiving frame'); 237 | 238 | var moreNeeded = Flow.prototype.write.call(this.upstream, frame); 239 | 240 | // * Transition to a new state if that's the effect of receiving the frame 241 | this._transition(false, frame); 242 | 243 | // * If it's a control frame. Call the appropriate handler method. 244 | if (frame.type === 'HEADERS') { 245 | if (this._processedHeaders && !frame.flags['END_STREAM']) { 246 | this.emit('error', 'PROTOCOL_ERROR'); 247 | } 248 | this._processedHeaders = true; 249 | this._onHeaders(frame); 250 | } else if (frame.type === 'PUSH_PROMISE') { 251 | this._onPromise(frame); 252 | } else if (frame.type === 'PRIORITY') { 253 | this._onPriority(frame); 254 | } else if (frame.type === 'ALTSVC') { 255 | // TODO 256 | } else if (frame.type === 'BLOCKED') { 257 | // TODO 258 | } 259 | 260 | // * If it's an invalid stream level frame, emit error 261 | else if ((frame.type !== 'DATA') && 262 | (frame.type !== 'WINDOW_UPDATE') && 263 | (frame.type !== 'RST_STREAM')) { 264 | this._log.error({ frame: frame }, 'Invalid stream level frame'); 265 | this.emit('error', 'PROTOCOL_ERROR'); 266 | } 267 | 268 | return moreNeeded; 269 | }; 270 | 271 | // The `_receive` method (= `upstream._receive`) gets called when there's an incoming frame. 272 | Stream.prototype._receive = function _receive(frame, ready) { 273 | // * If it's a DATA frame, then push the payload into the output buffer on the other side. 274 | // Call ready when the other side is ready to receive more. 275 | if (!this._ended && (frame.type === 'DATA')) { 276 | var moreNeeded = this.push(frame.data); 277 | if (!moreNeeded) { 278 | this._receiveMore = ready; 279 | } 280 | } 281 | 282 | // * Any frame may signal the end of the stream with the END_STREAM flag 283 | if (!this._ended && (frame.flags.END_STREAM || (frame.type === 'RST_STREAM'))) { 284 | this.push(null); 285 | this._ended = true; 286 | } 287 | 288 | // * Postpone calling `ready` if `push()` returned a falsy value 289 | if (this._receiveMore !== ready) { 290 | ready(); 291 | } 292 | }; 293 | 294 | // The `_read` method is called when the user side is ready to receive more data. If there's a 295 | // pending write on the upstream, then call its pending ready callback to receive more frames. 296 | Stream.prototype._read = function _read() { 297 | if (this._receiveMore) { 298 | var receiveMore = this._receiveMore; 299 | delete this._receiveMore; 300 | receiveMore(); 301 | } 302 | }; 303 | 304 | // The `write` method gets called when there's a write request from the user. 305 | Stream.prototype._write = function _write(buffer, encoding, ready) { 306 | // * Chunking is done by the upstream Flow. 307 | var moreNeeded = this._pushUpstream({ 308 | type: 'DATA', 309 | flags: {}, 310 | stream: this.id, 311 | data: buffer 312 | }); 313 | 314 | // * Call ready when upstream is ready to receive more frames. 315 | if (moreNeeded) { 316 | ready(); 317 | } else { 318 | this._sendMore = ready; 319 | } 320 | }; 321 | 322 | // The `_send` (= `upstream._send`) method is called when upstream is ready to receive more frames. 323 | // If there's a pending write on the user side, then call its pending ready callback to receive more 324 | // writes. 325 | Stream.prototype._send = function _send() { 326 | if (this._sendMore) { 327 | var sendMore = this._sendMore; 328 | delete this._sendMore; 329 | sendMore(); 330 | } 331 | }; 332 | 333 | // When the stream is finishing (the user calls `end()` on it), then we have to set the `END_STREAM` 334 | // flag on the last frame. If there's no frame in the queue, or if it doesn't support this flag, 335 | // then we create a 0 length DATA frame. We could do this all the time, but putting the flag on an 336 | // existing frame is a nice optimization. 337 | var emptyBuffer = new Buffer(0); 338 | Stream.prototype._finishing = function _finishing() { 339 | var endFrame = { 340 | type: 'DATA', 341 | flags: { END_STREAM: true }, 342 | stream: this.id, 343 | data: emptyBuffer 344 | }; 345 | var lastFrame = this.upstream.getLastQueuedFrame(); 346 | if (lastFrame && ((lastFrame.type === 'DATA') || (lastFrame.type === 'HEADERS'))) { 347 | this._log.debug({ frame: lastFrame }, 'Marking last frame with END_STREAM flag.'); 348 | lastFrame.flags.END_STREAM = true; 349 | this._transition(true, endFrame); 350 | } else { 351 | this._pushUpstream(endFrame); 352 | } 353 | }; 354 | 355 | // [Stream States](https://tools.ietf.org/html/rfc7540#section-5.1) 356 | // ---------------- 357 | // 358 | // +--------+ 359 | // PP | | PP 360 | // ,--------| idle |--------. 361 | // / | | \ 362 | // v +--------+ v 363 | // +----------+ | +----------+ 364 | // | | | H | | 365 | // ,---| reserved | | | reserved |---. 366 | // | | (local) | v | (remote) | | 367 | // | +----------+ +--------+ +----------+ | 368 | // | | ES | | ES | | 369 | // | | H ,-------| open |-------. | H | 370 | // | | / | | \ | | 371 | // | v v +--------+ v v | 372 | // | +----------+ | +----------+ | 373 | // | | half | | | half | | 374 | // | | closed | | R | closed | | 375 | // | | (remote) | | | (local) | | 376 | // | +----------+ | +----------+ | 377 | // | | v | | 378 | // | | ES / R +--------+ ES / R | | 379 | // | `----------->| |<-----------' | 380 | // | R | closed | R | 381 | // `-------------------->| |<--------------------' 382 | // +--------+ 383 | 384 | // Streams begin in the IDLE state and transitions happen when there's an incoming or outgoing frame 385 | Stream.prototype._initializeState = function _initializeState() { 386 | this.state = 'IDLE'; 387 | this._initiated = undefined; 388 | this._closedByUs = undefined; 389 | this._closedWithRst = undefined; 390 | this._processedHeaders = false; 391 | }; 392 | 393 | // Only `_setState` should change `this.state` directly. It also logs the state change and notifies 394 | // interested parties using the 'state' event. 395 | Stream.prototype._setState = function transition(state) { 396 | assert(this.state !== state); 397 | this._log.debug({ from: this.state, to: state }, 'State transition'); 398 | this.state = state; 399 | this.emit('state', state); 400 | }; 401 | 402 | // A state is 'active' if the stream in that state counts towards the concurrency limit. Streams 403 | // that are in the "open" state, or either of the "half closed" states count toward this limit. 404 | function activeState(state) { 405 | return ((state === 'HALF_CLOSED_LOCAL') || (state === 'HALF_CLOSED_REMOTE') || (state === 'OPEN')); 406 | } 407 | 408 | // `_transition` is called every time there's an incoming or outgoing frame. It manages state 409 | // transitions, and detects stream errors. A stream error is always caused by a frame that is not 410 | // allowed in the current state. 411 | Stream.prototype._transition = function transition(sending, frame) { 412 | var receiving = !sending; 413 | var connectionError; 414 | var streamError; 415 | 416 | var DATA = false, HEADERS = false, PRIORITY = false, ALTSVC = false, BLOCKED = false; 417 | var RST_STREAM = false, PUSH_PROMISE = false, WINDOW_UPDATE = false; 418 | switch(frame.type) { 419 | case 'DATA' : DATA = true; break; 420 | case 'HEADERS' : HEADERS = true; break; 421 | case 'PRIORITY' : PRIORITY = true; break; 422 | case 'RST_STREAM' : RST_STREAM = true; break; 423 | case 'PUSH_PROMISE' : PUSH_PROMISE = true; break; 424 | case 'WINDOW_UPDATE': WINDOW_UPDATE = true; break; 425 | case 'ALTSVC' : ALTSVC = true; break; 426 | case 'BLOCKED' : BLOCKED = true; break; 427 | } 428 | 429 | var previousState = this.state; 430 | 431 | switch (this.state) { 432 | // All streams start in the **idle** state. In this state, no frames have been exchanged. 433 | // 434 | // * Sending or receiving a HEADERS frame causes the stream to become "open". 435 | // 436 | // When the HEADERS frame contains the END_STREAM flags, then two state transitions happen. 437 | case 'IDLE': 438 | if (HEADERS) { 439 | this._setState('OPEN'); 440 | if (frame.flags.END_STREAM) { 441 | this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE'); 442 | } 443 | this._initiated = sending; 444 | } else if (sending && RST_STREAM) { 445 | this._setState('CLOSED'); 446 | } else if (PRIORITY) { 447 | /* No state change */ 448 | } else { 449 | connectionError = 'PROTOCOL_ERROR'; 450 | } 451 | break; 452 | 453 | // A stream in the **reserved (local)** state is one that has been promised by sending a 454 | // PUSH_PROMISE frame. 455 | // 456 | // * The endpoint can send a HEADERS frame. This causes the stream to open in a "half closed 457 | // (remote)" state. 458 | // * Either endpoint can send a RST_STREAM frame to cause the stream to become "closed". This 459 | // releases the stream reservation. 460 | // * An endpoint may receive PRIORITY frame in this state. 461 | // * An endpoint MUST NOT send any other type of frame in this state. 462 | case 'RESERVED_LOCAL': 463 | if (sending && HEADERS) { 464 | this._setState('HALF_CLOSED_REMOTE'); 465 | } else if (RST_STREAM) { 466 | this._setState('CLOSED'); 467 | } else if (PRIORITY) { 468 | /* No state change */ 469 | } else { 470 | connectionError = 'PROTOCOL_ERROR'; 471 | } 472 | break; 473 | 474 | // A stream in the **reserved (remote)** state has been reserved by a remote peer. 475 | // 476 | // * Either endpoint can send a RST_STREAM frame to cause the stream to become "closed". This 477 | // releases the stream reservation. 478 | // * Receiving a HEADERS frame causes the stream to transition to "half closed (local)". 479 | // * An endpoint MAY send PRIORITY frames in this state to reprioritize the stream. 480 | // * Receiving any other type of frame MUST be treated as a stream error of type PROTOCOL_ERROR. 481 | case 'RESERVED_REMOTE': 482 | if (RST_STREAM) { 483 | this._setState('CLOSED'); 484 | } else if (receiving && HEADERS) { 485 | this._setState('HALF_CLOSED_LOCAL'); 486 | } else if (BLOCKED || PRIORITY) { 487 | /* No state change */ 488 | } else { 489 | connectionError = 'PROTOCOL_ERROR'; 490 | } 491 | break; 492 | 493 | // The **open** state is where both peers can send frames. In this state, sending peers observe 494 | // advertised stream level flow control limits. 495 | // 496 | // * From this state either endpoint can send a frame with a END_STREAM flag set, which causes 497 | // the stream to transition into one of the "half closed" states: an endpoint sending a 498 | // END_STREAM flag causes the stream state to become "half closed (local)"; an endpoint 499 | // receiving a END_STREAM flag causes the stream state to become "half closed (remote)". 500 | // * Either endpoint can send a RST_STREAM frame from this state, causing it to transition 501 | // immediately to "closed". 502 | case 'OPEN': 503 | if (frame.flags.END_STREAM) { 504 | this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE'); 505 | } else if (RST_STREAM) { 506 | this._setState('CLOSED'); 507 | } else { 508 | /* No state change */ 509 | } 510 | break; 511 | 512 | // A stream that is **half closed (local)** cannot be used for sending frames. 513 | // 514 | // * A stream transitions from this state to "closed" when a frame that contains a END_STREAM 515 | // flag is received, or when either peer sends a RST_STREAM frame. 516 | // * An endpoint MAY send or receive PRIORITY frames in this state to reprioritize the stream. 517 | // * WINDOW_UPDATE can be sent by a peer that has sent a frame bearing the END_STREAM flag. 518 | case 'HALF_CLOSED_LOCAL': 519 | if (RST_STREAM || (receiving && frame.flags.END_STREAM)) { 520 | this._setState('CLOSED'); 521 | } else if (BLOCKED || ALTSVC || receiving || PRIORITY || (sending && WINDOW_UPDATE)) { 522 | /* No state change */ 523 | } else { 524 | connectionError = 'PROTOCOL_ERROR'; 525 | } 526 | break; 527 | 528 | // A stream that is **half closed (remote)** is no longer being used by the peer to send frames. 529 | // In this state, an endpoint is no longer obligated to maintain a receiver flow control window 530 | // if it performs flow control. 531 | // 532 | // * If an endpoint receives additional frames for a stream that is in this state it MUST 533 | // respond with a stream error of type STREAM_CLOSED. 534 | // * A stream can transition from this state to "closed" by sending a frame that contains a 535 | // END_STREAM flag, or when either peer sends a RST_STREAM frame. 536 | // * An endpoint MAY send or receive PRIORITY frames in this state to reprioritize the stream. 537 | // * A receiver MAY receive a WINDOW_UPDATE frame on a "half closed (remote)" stream. 538 | case 'HALF_CLOSED_REMOTE': 539 | if (RST_STREAM || (sending && frame.flags.END_STREAM)) { 540 | this._setState('CLOSED'); 541 | } else if (BLOCKED || ALTSVC || sending || PRIORITY || (receiving && WINDOW_UPDATE)) { 542 | /* No state change */ 543 | } else { 544 | connectionError = 'PROTOCOL_ERROR'; 545 | } 546 | break; 547 | 548 | // The **closed** state is the terminal state. 549 | // 550 | // * An endpoint MUST NOT send frames on a closed stream. An endpoint that receives a frame 551 | // after receiving a RST_STREAM or a frame containing a END_STREAM flag on that stream MUST 552 | // treat that as a stream error of type STREAM_CLOSED. 553 | // * WINDOW_UPDATE, PRIORITY or RST_STREAM frames can be received in this state for a short 554 | // period after a frame containing an END_STREAM flag is sent. Until the remote peer receives 555 | // and processes the frame bearing the END_STREAM flag, it might send either frame type. 556 | // Endpoints MUST ignore WINDOW_UPDATE frames received in this state, though endpoints MAY 557 | // choose to treat WINDOW_UPDATE frames that arrive a significant time after sending 558 | // END_STREAM as a connection error of type PROTOCOL_ERROR. 559 | // * If this state is reached as a result of sending a RST_STREAM frame, the peer that receives 560 | // the RST_STREAM might have already sent - or enqueued for sending - frames on the stream 561 | // that cannot be withdrawn. An endpoint that sends a RST_STREAM frame MUST ignore frames that 562 | // it receives on closed streams after it has sent a RST_STREAM frame. An endpoint MAY choose 563 | // to limit the period over which it ignores frames and treat frames that arrive after this 564 | // time as being in error. 565 | // * An endpoint might receive a PUSH_PROMISE frame after it sends RST_STREAM. PUSH_PROMISE 566 | // causes a stream to become "reserved". If promised streams are not desired, a RST_STREAM 567 | // can be used to close any of those streams. 568 | case 'CLOSED': 569 | if (PRIORITY || (sending && RST_STREAM) || 570 | (receiving && WINDOW_UPDATE) || 571 | (receiving && this._closedByUs && 572 | (this._closedWithRst || RST_STREAM || ALTSVC))) { 573 | /* No state change */ 574 | } else { 575 | streamError = 'STREAM_CLOSED'; 576 | } 577 | break; 578 | } 579 | 580 | // Noting that the connection was closed by the other endpoint. It may be important in edge cases. 581 | // For example, when the peer tries to cancel a promised stream, but we already sent every data 582 | // on it, then the stream is in CLOSED state, yet we want to ignore the incoming RST_STREAM. 583 | if ((this.state === 'CLOSED') && (previousState !== 'CLOSED')) { 584 | this._closedByUs = sending; 585 | this._closedWithRst = RST_STREAM; 586 | } 587 | 588 | // Sending/receiving a PUSH_PROMISE 589 | // 590 | // * Sending a PUSH_PROMISE frame marks the associated stream for later use. The stream state 591 | // for the reserved stream transitions to "reserved (local)". 592 | // * Receiving a PUSH_PROMISE frame marks the associated stream as reserved by the remote peer. 593 | // The state of the stream becomes "reserved (remote)". 594 | if (PUSH_PROMISE && !connectionError && !streamError) { 595 | /* This assertion must hold, because _transition is called immediately when a frame is written 596 | to the stream. If it would be called when a frame gets out of the input queue, the state 597 | of the reserved could have been changed by then. */ 598 | assert(frame.promised_stream.state === 'IDLE', frame.promised_stream.state); 599 | frame.promised_stream._setState(sending ? 'RESERVED_LOCAL' : 'RESERVED_REMOTE'); 600 | frame.promised_stream._initiated = sending; 601 | } 602 | 603 | // Signaling how sending/receiving this frame changes the active stream count (-1, 0 or +1) 604 | if (this._initiated) { 605 | var change = (activeState(this.state) - activeState(previousState)); 606 | if (sending) { 607 | frame.count_change = change; 608 | } else { 609 | frame.count_change(change); 610 | } 611 | } else if (sending) { 612 | frame.count_change = 0; 613 | } 614 | 615 | // Common error handling. 616 | if (connectionError || streamError) { 617 | var info = { 618 | error: connectionError, 619 | frame: frame, 620 | state: this.state, 621 | closedByUs: this._closedByUs, 622 | closedWithRst: this._closedWithRst 623 | }; 624 | 625 | // * When sending something invalid, throwing an exception, since it is probably a bug. 626 | if (sending) { 627 | this._log.error(info, 'Sending illegal frame.'); 628 | return this.emit('error', new Error('Sending illegal frame (' + frame.type + ') in ' + this.state + ' state.')); 629 | } 630 | 631 | // * In case of a serious problem, emitting and error and letting someone else handle it 632 | // (e.g. closing the connection) 633 | // * When receiving something invalid, sending an RST_STREAM using the `reset` method. 634 | // This will automatically cause a transition to the CLOSED state. 635 | else { 636 | this._log.error(info, 'Received illegal frame.'); 637 | if (connectionError) { 638 | this.emit('connectionError', connectionError); 639 | } else { 640 | this.reset(streamError); 641 | this.emit('error', streamError); 642 | } 643 | } 644 | } 645 | }; 646 | 647 | // Bunyan serializers 648 | // ------------------ 649 | 650 | exports.serializers = {}; 651 | 652 | var nextId = 0; 653 | exports.serializers.s = function(stream) { 654 | if (!('_id' in stream)) { 655 | stream._id = nextId; 656 | nextId += 1; 657 | } 658 | return stream._id; 659 | }; 660 | -------------------------------------------------------------------------------- /test/http.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect; 2 | var util = require('./util'); 3 | var fs = require('fs'); 4 | var path = require('path'); 5 | var url = require('url'); 6 | var net = require('net'); 7 | 8 | var http2 = require('../lib/http'); 9 | var https = require('https'); 10 | 11 | var serverOptions = { 12 | key: fs.readFileSync(path.join(__dirname, '../example/localhost.key')), 13 | cert: fs.readFileSync(path.join(__dirname, '../example/localhost.crt')), 14 | rejectUnauthorized: true, 15 | log: util.serverLog 16 | }; 17 | 18 | var agentOptions = { 19 | key: serverOptions.key, 20 | ca: serverOptions.cert, 21 | rejectUnauthorized: true, 22 | log: util.clientLog 23 | }; 24 | 25 | var globalAgent = new http2.Agent(agentOptions); 26 | 27 | describe('http.js', function() { 28 | beforeEach(function() { 29 | http2.globalAgent = globalAgent; 30 | }); 31 | describe('Server', function() { 32 | describe('new Server(options)', function() { 33 | it('should throw if called without \'plain\' or TLS options', function() { 34 | expect(function() { 35 | new http2.Server(); 36 | }).to.throw(Error); 37 | expect(function() { 38 | http2.createServer(util.noop); 39 | }).to.throw(Error); 40 | }); 41 | }); 42 | describe('method `listen()`', function () { 43 | it('should emit `listening` event', function (done) { 44 | var server = http2.createServer(serverOptions); 45 | 46 | server.on('listening', function () { 47 | server.close(); 48 | 49 | done(); 50 | }) 51 | 52 | server.listen(0); 53 | }); 54 | it('should emit `error` on failure', function (done) { 55 | var server = http2.createServer(serverOptions); 56 | 57 | // This TCP server is used to explicitly take a port to make 58 | // server.listen() fails. 59 | var net = require('net').createServer(); 60 | 61 | server.on('error', function () { 62 | net.close() 63 | 64 | done(); 65 | }); 66 | 67 | net.listen(0, function () { 68 | server.listen(this.address().port); 69 | }); 70 | }); 71 | }); 72 | describe('property `timeout`', function() { 73 | it('should be a proxy for the backing HTTPS server\'s `timeout` property', function() { 74 | var server = new http2.Server(serverOptions); 75 | var backingServer = server._server; 76 | var newTimeout = 10; 77 | server.timeout = newTimeout; 78 | expect(server.timeout).to.be.equal(newTimeout); 79 | expect(backingServer.timeout).to.be.equal(newTimeout); 80 | }); 81 | }); 82 | describe('method `setTimeout(timeout, [callback])`', function() { 83 | it('should be a proxy for the backing HTTPS server\'s `setTimeout` method', function() { 84 | var server = new http2.Server(serverOptions); 85 | var backingServer = server._server; 86 | var newTimeout = 10; 87 | var newCallback = util.noop; 88 | backingServer.setTimeout = function(timeout, callback) { 89 | expect(timeout).to.be.equal(newTimeout); 90 | expect(callback).to.be.equal(newCallback); 91 | }; 92 | server.setTimeout(newTimeout, newCallback); 93 | }); 94 | }); 95 | }); 96 | describe('Agent', function() { 97 | describe('property `maxSockets`', function() { 98 | it('should be a proxy for the backing HTTPS agent\'s `maxSockets` property', function() { 99 | var agent = new http2.Agent({ log: util.clientLog }); 100 | var backingAgent = agent._httpsAgent; 101 | var newMaxSockets = backingAgent.maxSockets + 1; 102 | agent.maxSockets = newMaxSockets; 103 | expect(agent.maxSockets).to.be.equal(newMaxSockets); 104 | expect(backingAgent.maxSockets).to.be.equal(newMaxSockets); 105 | }); 106 | }); 107 | describe('method `request(options, [callback])`', function() { 108 | it('should use a new agent for request-specific TLS settings', function(done) { 109 | var path = '/x'; 110 | var message = 'Hello world'; 111 | 112 | var server = http2.createServer(serverOptions, function(request, response) { 113 | expect(request.url).to.equal(path); 114 | response.end(message); 115 | }); 116 | 117 | server.listen(1234, function() { 118 | var options = url.parse('https://localhost:1234' + path); 119 | options.key = agentOptions.key; 120 | options.ca = agentOptions.ca; 121 | options.rejectUnauthorized = true; 122 | 123 | http2.globalAgent = new http2.Agent({ log: util.clientLog }); 124 | http2.get(options, function(response) { 125 | response.on('data', function(data) { 126 | expect(data.toString()).to.equal(message); 127 | server.close(); 128 | done(); 129 | }); 130 | }); 131 | }); 132 | }); 133 | it('should throw when trying to use with \'http\' scheme', function() { 134 | expect(function() { 135 | var agent = new http2.Agent({ log: util.clientLog }); 136 | agent.request({ protocol: 'http:' }); 137 | }).to.throw(Error); 138 | }); 139 | }); 140 | }); 141 | describe('OutgoingRequest', function() { 142 | function testFallbackProxyMethod(name, originalArguments, done) { 143 | var request = new http2.OutgoingRequest(); 144 | 145 | // When in HTTP/2 mode, this call should be ignored 146 | request.stream = { reset: util.noop }; 147 | request[name].apply(request, originalArguments); 148 | delete request.stream; 149 | 150 | // When in fallback mode, this call should be forwarded 151 | request[name].apply(request, originalArguments); 152 | var mockFallbackRequest = { on: util.noop }; 153 | mockFallbackRequest[name] = function() { 154 | expect(Array.prototype.slice.call(arguments)).to.deep.equal(originalArguments); 155 | done(); 156 | }; 157 | request._fallback(mockFallbackRequest); 158 | } 159 | describe('method `setNoDelay(noDelay)`', function() { 160 | it('should act as a proxy for the backing HTTPS agent\'s `setNoDelay` method', function(done) { 161 | testFallbackProxyMethod('setNoDelay', [true], done); 162 | }); 163 | }); 164 | describe('method `setSocketKeepAlive(enable, initialDelay)`', function() { 165 | it('should act as a proxy for the backing HTTPS agent\'s `setSocketKeepAlive` method', function(done) { 166 | testFallbackProxyMethod('setSocketKeepAlive', [true, util.random(10, 100)], done); 167 | }); 168 | }); 169 | describe('method `setTimeout(timeout, [callback])`', function() { 170 | it('should act as a proxy for the backing HTTPS agent\'s `setTimeout` method', function(done) { 171 | testFallbackProxyMethod('setTimeout', [util.random(10, 100), util.noop], done); 172 | }); 173 | }); 174 | describe('method `abort()`', function() { 175 | it('should act as a proxy for the backing HTTPS agent\'s `abort` method', function(done) { 176 | testFallbackProxyMethod('abort', [], done); 177 | }); 178 | }); 179 | }); 180 | describe('OutgoingResponse', function() { 181 | it('should throw error when writeHead is called multiple times on it', function() { 182 | var called = false; 183 | var stream = { _log: util.log, headers: function () { 184 | if (called) { 185 | throw new Error('Should not send headers twice'); 186 | } else { 187 | called = true; 188 | } 189 | }, once: util.noop }; 190 | var response = new http2.OutgoingResponse(stream); 191 | 192 | response.writeHead(200); 193 | response.writeHead(404); 194 | }); 195 | it('field finished should be Boolean', function(){ 196 | var stream = { _log: util.log, headers: function () {}, once: util.noop }; 197 | var response = new http2.OutgoingResponse(stream); 198 | expect(response.finished).to.be.a('Boolean'); 199 | }); 200 | it('field finished should initially be false and then go to true when response completes',function(done){ 201 | var res; 202 | var server = http2.createServer(serverOptions, function(request, response) { 203 | res = response; 204 | expect(res.finished).to.be.false; 205 | response.end('HiThere'); 206 | }); 207 | server.listen(1236, function() { 208 | http2.get('https://localhost:1236/finished-test', function(response) { 209 | response.on('data', function(data){ 210 | var sink = data; // 211 | }); 212 | response.on('end',function(){ 213 | expect(res.finished).to.be.true; 214 | server.close(); 215 | done(); 216 | }); 217 | }); 218 | }); 219 | }); 220 | }); 221 | describe('test scenario', function() { 222 | describe('simple request', function() { 223 | it('should work as expected', function(done) { 224 | var path = '/x'; 225 | var message = 'Hello world'; 226 | 227 | var server = http2.createServer(serverOptions, function(request, response) { 228 | expect(request.url).to.equal(path); 229 | response.end(message); 230 | }); 231 | 232 | server.listen(1234, function() { 233 | http2.get('https://localhost:1234' + path, function(response) { 234 | response.on('data', function(data) { 235 | expect(data.toString()).to.equal(message); 236 | server.close(); 237 | done(); 238 | }); 239 | }); 240 | }); 241 | }); 242 | }); 243 | describe('2 simple request in parallel', function() { 244 | it('should work as expected', function(originalDone) { 245 | var path = '/x'; 246 | var message = 'Hello world'; 247 | var done = util.callNTimes(2, function() { 248 | server.close(); 249 | originalDone(); 250 | }); 251 | 252 | var server = http2.createServer(serverOptions, function(request, response) { 253 | expect(request.url).to.equal(path); 254 | response.end(message); 255 | }); 256 | 257 | server.listen(1234, function() { 258 | http2.get('https://localhost:1234' + path, function(response) { 259 | response.on('data', function(data) { 260 | expect(data.toString()).to.equal(message); 261 | done(); 262 | }); 263 | }); 264 | http2.get('https://localhost:1234' + path, function(response) { 265 | response.on('data', function(data) { 266 | expect(data.toString()).to.equal(message); 267 | done(); 268 | }); 269 | }); 270 | }); 271 | }); 272 | }); 273 | describe('100 simple request in a series', function() { 274 | it('should work as expected', function(done) { 275 | var path = '/x'; 276 | var message = 'Hello world'; 277 | 278 | var server = http2.createServer(serverOptions, function(request, response) { 279 | expect(request.url).to.equal(path); 280 | response.end(message); 281 | }); 282 | 283 | var n = 100; 284 | server.listen(1242, function() { 285 | doRequest(); 286 | function doRequest() { 287 | http2.get('https://localhost:1242' + path, function(response) { 288 | response.on('data', function(data) { 289 | expect(data.toString()).to.equal(message); 290 | if (n) { 291 | n -= 1; 292 | doRequest(); 293 | } else { 294 | server.close(); 295 | done(); 296 | } 297 | }); 298 | }); 299 | } 300 | }); 301 | }); 302 | }); 303 | describe('request with payload', function() { 304 | it('should work as expected', function(done) { 305 | var path = '/x'; 306 | var message = 'Hello world'; 307 | 308 | var server = http2.createServer(serverOptions, function(request, response) { 309 | expect(request.url).to.equal(path); 310 | request.once('data', function(data) { 311 | expect(data.toString()).to.equal(message); 312 | response.end(); 313 | }); 314 | }); 315 | 316 | server.listen(1240, function() { 317 | var request = http2.request({ 318 | host: 'localhost', 319 | port: 1240, 320 | path: path 321 | }); 322 | request.write(message); 323 | request.end(); 324 | request.on('response', function() { 325 | server.close(); 326 | done(); 327 | }); 328 | }); 329 | }); 330 | }); 331 | describe('request with custom status code and headers', function() { 332 | it('should work as expected', function(done) { 333 | var path = '/x'; 334 | var message = 'Hello world'; 335 | var headerName = 'name'; 336 | var headerValue = 'value'; 337 | 338 | var server = http2.createServer(serverOptions, function(request, response) { 339 | // Request URL and headers 340 | expect(request.url).to.equal(path); 341 | expect(request.headers[headerName]).to.equal(headerValue); 342 | 343 | // A header to be overwritten later 344 | response.setHeader(headerName, 'to be overwritten'); 345 | expect(response.getHeader(headerName)).to.equal('to be overwritten'); 346 | 347 | // A header to be deleted 348 | response.setHeader('nonexistent', 'x'); 349 | response.removeHeader('nonexistent'); 350 | expect(response.getHeader('nonexistent')).to.equal(undefined); 351 | 352 | // A set-cookie header which should always be an array 353 | response.setHeader('set-cookie', 'foo'); 354 | 355 | // Don't send date 356 | response.sendDate = false; 357 | 358 | // Specifying more headers, the status code and a reason phrase with `writeHead` 359 | var moreHeaders = {}; 360 | moreHeaders[headerName] = headerValue; 361 | response.writeHead(600, 'to be discarded', moreHeaders); 362 | expect(response.getHeader(headerName)).to.equal(headerValue); 363 | 364 | // Empty response body 365 | response.end(message); 366 | }); 367 | 368 | server.listen(1239, function() { 369 | var headers = {}; 370 | headers[headerName] = headerValue; 371 | var request = http2.request({ 372 | host: 'localhost', 373 | port: 1239, 374 | path: path, 375 | headers: headers 376 | }); 377 | request.end(); 378 | request.on('response', function(response) { 379 | expect(response.headers[headerName]).to.equal(headerValue); 380 | expect(response.headers['nonexistent']).to.equal(undefined); 381 | expect(response.headers['set-cookie']).to.an.instanceof(Array) 382 | expect(response.headers['set-cookie']).to.deep.equal(['foo']) 383 | expect(response.headers['date']).to.equal(undefined); 384 | response.on('data', function(data) { 385 | expect(data.toString()).to.equal(message); 386 | server.close(); 387 | done(); 388 | }); 389 | }); 390 | }); 391 | }); 392 | }); 393 | describe('request over plain TCP', function() { 394 | it('should work as expected', function(done) { 395 | var path = '/x'; 396 | var message = 'Hello world'; 397 | 398 | var server = http2.raw.createServer({ 399 | log: util.serverLog 400 | }, function(request, response) { 401 | expect(request.url).to.equal(path); 402 | response.end(message); 403 | }); 404 | 405 | server.listen(1237, function() { 406 | var request = http2.raw.request({ 407 | plain: true, 408 | host: 'localhost', 409 | port: 1237, 410 | path: path 411 | }, function(response) { 412 | response.on('data', function(data) { 413 | expect(data.toString()).to.equal(message); 414 | server.close(); 415 | done(); 416 | }); 417 | }); 418 | request.end(); 419 | }); 420 | }); 421 | }); 422 | describe('get over plain TCP', function() { 423 | it('should work as expected', function(done) { 424 | var path = '/x'; 425 | var message = 'Hello world'; 426 | 427 | var server = http2.raw.createServer({ 428 | log: util.serverLog 429 | }, function(request, response) { 430 | expect(request.url).to.equal(path); 431 | response.end(message); 432 | }); 433 | 434 | server.listen(1237, function() { 435 | var request = http2.raw.get('http://localhost:1237/x', function(response) { 436 | response.on('data', function(data) { 437 | expect(data.toString()).to.equal(message); 438 | server.close(); 439 | done(); 440 | }); 441 | }); 442 | request.end(); 443 | }); 444 | }); 445 | }); 446 | describe('request to an HTTPS/1 server', function() { 447 | it('should fall back to HTTPS/1 successfully', function(done) { 448 | var path = '/x'; 449 | var message = 'Hello world'; 450 | 451 | var server = https.createServer(serverOptions, function(request, response) { 452 | expect(request.url).to.equal(path); 453 | response.end(message); 454 | }); 455 | 456 | server.listen(5678, function() { 457 | http2.get('https://localhost:5678' + path, function(response) { 458 | response.on('data', function(data) { 459 | expect(data.toString()).to.equal(message); 460 | done(); 461 | }); 462 | }); 463 | }); 464 | }); 465 | }); 466 | describe('2 parallel request to an HTTPS/1 server', function() { 467 | it('should fall back to HTTPS/1 successfully', function(originalDone) { 468 | var path = '/x'; 469 | var message = 'Hello world'; 470 | var done = util.callNTimes(2, function() { 471 | server.close(); 472 | originalDone(); 473 | }); 474 | 475 | var server = https.createServer(serverOptions, function(request, response) { 476 | expect(request.url).to.equal(path); 477 | response.end(message); 478 | }); 479 | 480 | server.listen(6789, function() { 481 | http2.get('https://localhost:6789' + path, function(response) { 482 | response.on('data', function(data) { 483 | expect(data.toString()).to.equal(message); 484 | done(); 485 | }); 486 | }); 487 | http2.get('https://localhost:6789' + path, function(response) { 488 | response.on('data', function(data) { 489 | expect(data.toString()).to.equal(message); 490 | done(); 491 | }); 492 | }); 493 | }); 494 | }); 495 | }); 496 | describe('HTTPS/1 request to a HTTP/2 server', function() { 497 | it('should fall back to HTTPS/1 successfully', function(done) { 498 | var path = '/x'; 499 | var message = 'Hello world'; 500 | 501 | var server = http2.createServer(serverOptions, function(request, response) { 502 | expect(request.url).to.equal(path); 503 | response.end(message); 504 | }); 505 | 506 | server.listen(1236, function() { 507 | var options = url.parse('https://localhost:1236' + path); 508 | options.agent = new https.Agent(agentOptions); 509 | https.get(options, function(response) { 510 | response.on('data', function(data) { 511 | expect(data.toString()).to.equal(message); 512 | done(); 513 | }); 514 | }); 515 | }); 516 | }); 517 | }); 518 | describe('two parallel request', function() { 519 | it('should work as expected', function(done) { 520 | var path = '/x'; 521 | var message = 'Hello world'; 522 | 523 | var server = http2.createServer(serverOptions, function(request, response) { 524 | expect(request.url).to.equal(path); 525 | response.end(message); 526 | }); 527 | 528 | server.listen(1237, function() { 529 | done = util.callNTimes(2, done); 530 | // 1. request 531 | http2.get('https://localhost:1237' + path, function(response) { 532 | response.on('data', function(data) { 533 | expect(data.toString()).to.equal(message); 534 | done(); 535 | }); 536 | }); 537 | // 2. request 538 | http2.get('https://localhost:1237' + path, function(response) { 539 | response.on('data', function(data) { 540 | expect(data.toString()).to.equal(message); 541 | done(); 542 | }); 543 | }); 544 | }); 545 | }); 546 | }); 547 | describe('two subsequent request', function() { 548 | it('should use the same HTTP/2 connection', function(done) { 549 | var path = '/x'; 550 | var message = 'Hello world'; 551 | 552 | var server = http2.createServer(serverOptions, function(request, response) { 553 | expect(request.url).to.equal(path); 554 | response.end(message); 555 | }); 556 | 557 | server.listen(1238, function() { 558 | // 1. request 559 | http2.get('https://localhost:1238' + path, function(response) { 560 | response.on('data', function(data) { 561 | expect(data.toString()).to.equal(message); 562 | 563 | // 2. request 564 | http2.get('https://localhost:1238' + path, function(response) { 565 | response.on('data', function(data) { 566 | expect(data.toString()).to.equal(message); 567 | done(); 568 | }); 569 | }); 570 | }); 571 | }); 572 | }); 573 | }); 574 | }); 575 | describe('https server node module specification conformance', function() { 576 | it('should provide API for remote HTTP 1.1 client address', function(done) { 577 | var remoteAddress = null; 578 | var remotePort = null; 579 | 580 | var server = http2.createServer(serverOptions, function(request, response) { 581 | // HTTPS 1.1 client with Node 0.10 server 582 | if (!request.remoteAddress) { 583 | if (request.socket.socket) { 584 | remoteAddress = request.socket.socket.remoteAddress; 585 | remotePort = request.socket.socket.remotePort; 586 | } else { 587 | remoteAddress = request.socket.remoteAddress; 588 | remotePort = request.socket.remotePort; 589 | } 590 | } else { 591 | // HTTPS 1.1/2.0 client with Node 0.12 server 592 | remoteAddress = request.remoteAddress; 593 | remotePort = request.remotePort; 594 | } 595 | response.write('Pong'); 596 | response.end(); 597 | }); 598 | 599 | server.listen(1259, 'localhost', function() { 600 | var request = https.request({ 601 | host: 'localhost', 602 | port: 1259, 603 | path: '/', 604 | ca: serverOptions.cert 605 | }); 606 | request.write('Ping'); 607 | request.end(); 608 | request.on('response', function(response) { 609 | response.on('data', function(data) { 610 | var localAddress = response.socket.address(); 611 | expect(remoteAddress).to.equal(localAddress.address); 612 | expect(remotePort).to.equal(localAddress.port); 613 | server.close(); 614 | done(); 615 | }); 616 | }); 617 | }); 618 | }); 619 | it('should provide API for remote HTTP 2.0 client address', function(done) { 620 | var remoteAddress = null; 621 | var remotePort = null; 622 | var localAddress = null; 623 | 624 | var server = http2.createServer(serverOptions, function(request, response) { 625 | remoteAddress = request.remoteAddress; 626 | remotePort = request.remotePort; 627 | response.write('Pong'); 628 | response.end(); 629 | }); 630 | 631 | server.listen(1258, 'localhost', function() { 632 | var request = http2.request({ 633 | host: 'localhost', 634 | port: 1258, 635 | path: '/' 636 | }); 637 | request.write('Ping'); 638 | globalAgent.on('false:localhost:1258', function(endpoint) { 639 | localAddress = endpoint.socket.address(); 640 | }); 641 | request.end(); 642 | request.on('response', function(response) { 643 | response.on('data', function(data) { 644 | expect(remoteAddress).to.equal(localAddress.address); 645 | expect(remotePort).to.equal(localAddress.port); 646 | server.close(); 647 | done(); 648 | }); 649 | }); 650 | }); 651 | }); 652 | it('should expose net.Socket as .socket and .connection', function(done) { 653 | var server = http2.createServer(serverOptions, function(request, response) { 654 | expect(request.socket).to.equal(request.connection); 655 | expect(request.socket).to.be.instanceof(net.Socket); 656 | response.write('Pong'); 657 | response.end(); 658 | done(); 659 | }); 660 | 661 | server.listen(1248, 'localhost', function() { 662 | var request = https.request({ 663 | host: 'localhost', 664 | port: 1248, 665 | path: '/', 666 | ca: serverOptions.cert 667 | }); 668 | request.write('Ping'); 669 | request.end(); 670 | }); 671 | }); 672 | }); 673 | describe('request and response with trailers', function() { 674 | it('should work as expected', function(done) { 675 | var path = '/x'; 676 | var message = 'Hello world'; 677 | var requestTrailers = { 'content-md5': 'x' }; 678 | var responseTrailers = { 'content-md5': 'y' }; 679 | 680 | var server = http2.createServer(serverOptions, function(request, response) { 681 | expect(request.url).to.equal(path); 682 | request.on('data', util.noop); 683 | request.once('end', function() { 684 | expect(request.trailers).to.deep.equal(requestTrailers); 685 | response.write(message); 686 | response.addTrailers(responseTrailers); 687 | response.end(); 688 | }); 689 | }); 690 | 691 | server.listen(1241, function() { 692 | var request = http2.request('https://localhost:1241' + path); 693 | request.addTrailers(requestTrailers); 694 | request.end(); 695 | request.on('response', function(response) { 696 | response.on('data', util.noop); 697 | response.once('end', function() { 698 | expect(response.trailers).to.deep.equal(responseTrailers); 699 | done(); 700 | }); 701 | }); 702 | }); 703 | }); 704 | }); 705 | describe('Handle socket error', function () { 706 | it('HTTPS on Connection Refused error', function (done) { 707 | var path = '/x'; 708 | var request = http2.request('https://127.0.0.1:6666' + path); 709 | 710 | request.on('error', function (err) { 711 | expect(err.errno).to.equal('ECONNREFUSED'); 712 | done(); 713 | }); 714 | 715 | request.on('response', function (response) { 716 | server._server._handle.destroy(); 717 | 718 | response.on('data', util.noop); 719 | 720 | response.once('end', function () { 721 | done(new Error('Request should have failed')); 722 | }); 723 | }); 724 | 725 | request.end(); 726 | 727 | }); 728 | it('HTTP on Connection Refused error', function (done) { 729 | var path = '/x'; 730 | 731 | var request = http2.raw.request('http://127.0.0.1:6666' + path); 732 | 733 | request.on('error', function (err) { 734 | expect(err.errno).to.equal('ECONNREFUSED'); 735 | done(); 736 | }); 737 | 738 | request.on('response', function (response) { 739 | server._server._handle.destroy(); 740 | 741 | response.on('data', util.noop); 742 | 743 | response.once('end', function () { 744 | done(new Error('Request should have failed')); 745 | }); 746 | }); 747 | 748 | request.end(); 749 | }); 750 | }); 751 | describe('server push', function() { 752 | it('should work as expected', function(done) { 753 | var path = '/x'; 754 | var message = 'Hello world'; 755 | var pushedPath = '/y'; 756 | var pushedMessage = 'Hello world 2'; 757 | 758 | var server = http2.createServer(serverOptions, function(request, response) { 759 | expect(request.url).to.equal(path); 760 | var push1 = response.push('/y'); 761 | push1.end(pushedMessage); 762 | var push2 = response.push({ path: '/y', protocol: 'https:' }); 763 | push2.end(pushedMessage); 764 | response.end(message); 765 | }); 766 | 767 | server.listen(1235, function() { 768 | var request = http2.get('https://localhost:1235' + path); 769 | done = util.callNTimes(5, done); 770 | 771 | request.on('response', function(response) { 772 | response.on('data', function(data) { 773 | expect(data.toString()).to.equal(message); 774 | done(); 775 | }); 776 | response.on('end', done); 777 | }); 778 | 779 | request.on('push', function(promise) { 780 | expect(promise.url).to.be.equal(pushedPath); 781 | promise.on('response', function(pushStream) { 782 | pushStream.on('data', function(data) { 783 | expect(data.toString()).to.equal(pushedMessage); 784 | done(); 785 | }); 786 | pushStream.on('end', done); 787 | }); 788 | }); 789 | }); 790 | }); 791 | }); 792 | }); 793 | }); 794 | --------------------------------------------------------------------------------