bug 950768 - http2 tests in node.js r=hurley r=mcmanus

This commit is contained in:
Gabor Molnar 2013-10-20 16:03:07 +02:00
parent 9af4381ba2
commit fab1e984e0
72 changed files with 21247 additions and 30 deletions

View File

@ -0,0 +1,429 @@
// test HTTP/2
var Ci = Components.interfaces;
var Cc = Components.classes;
// Generate a small and a large post with known pre-calculated md5 sums
function generateContent(size) {
var content = "";
for (var i = 0; i < size; i++) {
content += "0";
}
return content;
}
var posts = [];
posts.push(generateContent(10));
posts.push(generateContent(250000));
// pre-calculated md5sums (in hex) of the above posts
var md5s = ['f1b708bba17f1ce948dc979f4d7092bc',
'2ef8d3b6c8f329318eb1a119b12622b6'];
var bigListenerData = generateContent(128 * 1024);
var bigListenerMD5 = '8f607cfdd2c87d6a7eedb657dafbd836';
function checkIsHttp2(request) {
try {
if (request.getResponseHeader("X-Firefox-Spdy") == "HTTP-draft-08/2.0") {
if (request.getResponseHeader("X-Connection-Http2") == "yes") {
return true;
}
return false; // Weird case, but the server disagrees with us
}
} catch (e) {
// Nothing to do here
}
return false;
}
var Http2CheckListener = function() {};
Http2CheckListener.prototype = {
onStartRequestFired: false,
onDataAvailableFired: false,
isHttp2Connection: false,
onStartRequest: function testOnStartRequest(request, ctx) {
this.onStartRequestFired = true;
if (!Components.isSuccessCode(request.status))
do_throw("Channel should have a success code! (" + request.status + ")");
if (!(request instanceof Components.interfaces.nsIHttpChannel))
do_throw("Expecting an HTTP channel");
do_check_eq(request.responseStatus, 200);
do_check_eq(request.requestSucceeded, true);
},
onDataAvailable: function testOnDataAvailable(request, ctx, stream, off, cnt) {
this.onDataAvailableFired = true;
this.isHttp2Connection = checkIsHttp2(request);
read_stream(stream, cnt);
},
onStopRequest: function testOnStopRequest(request, ctx, status) {
do_check_true(this.onStartRequestFired);
do_check_true(this.onDataAvailableFired);
do_check_true(this.isHttp2Connection);
run_next_test();
do_test_finished();
}
};
/*
* Support for testing valid multiplexing of streams
*/
var multiplexContent = generateContent(30*1024);
var completed_channels = [];
function register_completed_channel(listener) {
completed_channels.push(listener);
if (completed_channels.length == 2) {
do_check_neq(completed_channels[0].streamID, completed_channels[1].streamID);
run_next_test();
do_test_finished();
}
}
/* Listener class to control the testing of multiplexing */
var Http2MultiplexListener = function() {};
Http2MultiplexListener.prototype = new Http2CheckListener();
Http2MultiplexListener.prototype.streamID = 0;
Http2MultiplexListener.prototype.buffer = "";
Http2MultiplexListener.prototype.onDataAvailable = function(request, ctx, stream, off, cnt) {
this.onDataAvailableFired = true;
this.isHttp2Connection = checkIsHttp2(request);
this.streamID = parseInt(request.getResponseHeader("X-Http2-StreamID"));
var data = read_stream(stream, cnt);
this.buffer = this.buffer.concat(data);
};
Http2MultiplexListener.prototype.onStopRequest = function(request, ctx, status) {
do_check_true(this.onStartRequestFired);
do_check_true(this.onDataAvailableFired);
do_check_true(this.isHttp2Connection);
do_check_true(this.buffer == multiplexContent);
// This is what does most of the hard work for us
register_completed_channel(this);
};
// Does the appropriate checks for header gatewaying
var Http2HeaderListener = function(value) {
this.value = value
};
Http2HeaderListener.prototype = new Http2CheckListener();
Http2HeaderListener.prototype.value = "";
Http2HeaderListener.prototype.onDataAvailable = function(request, ctx, stream, off, cnt) {
this.onDataAvailableFired = true;
this.isHttp2Connection = checkIsHttp2(request);
do_check_eq(request.getResponseHeader("X-Received-Test-Header"), this.value);
read_stream(stream, cnt);
};
var Http2PushListener = function() {};
Http2PushListener.prototype = new Http2CheckListener();
Http2PushListener.prototype.onDataAvailable = function(request, ctx, stream, off, cnt) {
this.onDataAvailableFired = true;
this.isHttp2Connection = checkIsHttp2(request);
if (ctx.originalURI.spec == "https://localhost:6944/push.js" ||
ctx.originalURI.spec == "https://localhost:6944/push2.js") {
do_check_eq(request.getResponseHeader("pushed"), "yes");
}
read_stream(stream, cnt);
};
// Does the appropriate checks for a large GET response
var Http2BigListener = function() {};
Http2BigListener.prototype = new Http2CheckListener();
Http2BigListener.prototype.buffer = "";
Http2BigListener.prototype.onDataAvailable = function(request, ctx, stream, off, cnt) {
this.onDataAvailableFired = true;
this.isHttp2Connection = checkIsHttp2(request);
this.buffer = this.buffer.concat(read_stream(stream, cnt));
// We know the server should send us the same data as our big post will be,
// so the md5 should be the same
do_check_eq(bigListenerMD5, request.getResponseHeader("X-Expected-MD5"));
};
Http2BigListener.prototype.onStopRequest = function(request, ctx, status) {
do_check_true(this.onStartRequestFired);
do_check_true(this.onDataAvailableFired);
do_check_true(this.isHttp2Connection);
// Don't want to flood output, so don't use do_check_eq
do_check_true(this.buffer == bigListenerData);
run_next_test();
do_test_finished();
};
// Does the appropriate checks for POSTs
var Http2PostListener = function(expected_md5) {
this.expected_md5 = expected_md5;
};
Http2PostListener.prototype = new Http2CheckListener();
Http2PostListener.prototype.expected_md5 = "";
Http2PostListener.prototype.onDataAvailable = function(request, ctx, stream, off, cnt) {
this.onDataAvailableFired = true;
this.isHttp2Connection = checkIsHttp2(request);
read_stream(stream, cnt);
do_check_eq(this.expected_md5, request.getResponseHeader("X-Calculated-MD5"));
};
function makeChan(url) {
var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
var chan = ios.newChannel(url, null, null).QueryInterface(Ci.nsIHttpChannel);
return chan;
}
// Make sure we make a HTTP2 connection and both us and the server mark it as such
function test_http2_basic() {
var chan = makeChan("https://localhost:6944/");
var listener = new Http2CheckListener();
chan.asyncOpen(listener, null);
}
// Support for making sure XHR works over SPDY
function checkXhr(xhr) {
if (xhr.readyState != 4) {
return;
}
do_check_eq(xhr.status, 200);
do_check_eq(checkIsHttp2(xhr), true);
run_next_test();
do_test_finished();
}
// Fires off an XHR request over SPDY
function test_http2_xhr() {
var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
req.open("GET", "https://localhost:6944/", true);
req.addEventListener("readystatechange", function (evt) { checkXhr(req); },
false);
req.send(null);
}
// Test to make sure we get multiplexing right
function test_http2_multiplex() {
var chan1 = makeChan("https://localhost:6944/multiplex1");
var chan2 = makeChan("https://localhost:6944/multiplex2");
var listener1 = new Http2MultiplexListener();
var listener2 = new Http2MultiplexListener();
chan1.asyncOpen(listener1, null);
chan2.asyncOpen(listener2, null);
}
// Test to make sure we gateway non-standard headers properly
function test_http2_header() {
var chan = makeChan("https://localhost:6944/header");
var hvalue = "Headers are fun";
var listener = new Http2HeaderListener(hvalue);
chan.setRequestHeader("X-Test-Header", hvalue, false);
chan.asyncOpen(listener, null);
}
function test_http2_push1() {
var chan = makeChan("https://localhost:6944/push");
chan.loadGroup = loadGroup;
var listener = new Http2PushListener();
chan.asyncOpen(listener, chan);
}
function test_http2_push2() {
var chan = makeChan("https://localhost:6944/push.js");
chan.loadGroup = loadGroup;
var listener = new Http2PushListener();
chan.asyncOpen(listener, chan);
}
function test_http2_push3() {
var chan = makeChan("https://localhost:6944/push2");
chan.loadGroup = loadGroup;
var listener = new Http2PushListener();
chan.asyncOpen(listener, chan);
}
function test_http2_push4() {
var chan = makeChan("https://localhost:6944/push2.js");
chan.loadGroup = loadGroup;
var listener = new Http2PushListener();
chan.asyncOpen(listener, chan);
}
// Make sure we handle GETs that cover more than 2 frames properly
function test_http2_big() {
var chan = makeChan("https://localhost:6944/big");
var listener = new Http2BigListener();
chan.asyncOpen(listener, null);
}
// Support for doing a POST
function do_post(content, chan, listener) {
var stream = Cc["@mozilla.org/io/string-input-stream;1"]
.createInstance(Ci.nsIStringInputStream);
stream.data = content;
var uchan = chan.QueryInterface(Ci.nsIUploadChannel);
uchan.setUploadStream(stream, "text/plain", stream.available());
chan.requestMethod = "POST";
chan.asyncOpen(listener, null);
}
// Make sure we can do a simple POST
function test_http2_post() {
var chan = makeChan("https://localhost:6944/post");
var listener = new Http2PostListener(md5s[0]);
do_post(posts[0], chan, listener);
}
// Make sure we can do a POST that covers more than 2 frames
function test_http2_post_big() {
var chan = makeChan("https://localhost:6944/post");
var listener = new Http2PostListener(md5s[1]);
do_post(posts[1], chan, listener);
}
// hack - the header test resets the multiplex object on the server,
// so make sure header is always run before the multiplex test.
//
// make sure post_big runs first to test race condition in restarting
// a stalled stream when a SETTINGS frame arrives
var tests = [ test_http2_post_big
, test_http2_basic
, test_http2_push1
, test_http2_push2
, test_http2_push3
, test_http2_push4
, test_http2_xhr
, test_http2_header
, test_http2_multiplex
, test_http2_big
, test_http2_post
];
var current_test = 0;
function run_next_test() {
if (current_test < tests.length) {
tests[current_test]();
current_test++;
do_test_pending();
}
}
// Support for making sure we can talk to the invalid cert the server presents
var CertOverrideListener = function(host, port, bits) {
this.host = host;
if (port) {
this.port = port;
}
this.bits = bits;
};
CertOverrideListener.prototype = {
host: null,
port: -1,
bits: null,
getInterface: function(aIID) {
return this.QueryInterface(aIID);
},
QueryInterface: function(aIID) {
if (aIID.equals(Ci.nsIBadCertListener2) ||
aIID.equals(Ci.nsIInterfaceRequestor) ||
aIID.equals(Ci.nsISupports))
return this;
throw Components.results.NS_ERROR_NO_INTERFACE;
},
notifyCertProblem: function(socketInfo, sslStatus, targetHost) {
var cert = sslStatus.QueryInterface(Ci.nsISSLStatus).serverCert;
var cos = Cc["@mozilla.org/security/certoverride;1"].
getService(Ci.nsICertOverrideService);
cos.rememberValidityOverride(this.host, this.port, cert, this.bits, false);
return true;
},
};
function addCertOverride(host, port, bits) {
var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
.createInstance(Ci.nsIXMLHttpRequest);
try {
var url;
if (port) {
url = "https://" + host + ":" + port + "/";
} else {
url = "https://" + host + "/";
}
req.open("GET", url, false);
req.channel.notificationCallbacks = new CertOverrideListener(host, port, bits);
req.send(null);
} catch (e) {
// This will fail since the server is not trusted yet
}
}
var prefs;
var spdypref;
var spdy3pref;
var spdypush;
var http2pref;
var loadGroup;
function resetPrefs() {
prefs.setBoolPref("network.http.spdy.enabled", spdypref);
prefs.setBoolPref("network.http.spdy.enabled.v3", spdy3pref);
prefs.setBoolPref("network.http.spdy.allow-push", spdypush);
prefs.setBoolPref("network.http.spdy.enabled.http2draft", http2pref);
}
function run_test() {
// Set to allow the cert presented by our SPDY server
do_get_profile();
var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
var oldPref = prefs.getIntPref("network.http.speculative-parallel-limit");
prefs.setIntPref("network.http.speculative-parallel-limit", 0);
addCertOverride("localhost", 6944,
Ci.nsICertOverrideService.ERROR_UNTRUSTED |
Ci.nsICertOverrideService.ERROR_MISMATCH |
Ci.nsICertOverrideService.ERROR_TIME);
prefs.setIntPref("network.http.speculative-parallel-limit", oldPref);
// Enable all versions of spdy to see that we auto negotiate spdy/3
spdypref = prefs.getBoolPref("network.http.spdy.enabled");
spdy3pref = prefs.getBoolPref("network.http.spdy.enabled.v3");
spdypush = prefs.getBoolPref("network.http.spdy.allow-push");
http2pref = prefs.getBoolPref("network.http.spdy.enabled.http2draft");
prefs.setBoolPref("network.http.spdy.enabled", true);
prefs.setBoolPref("network.http.spdy.enabled.v3", true);
prefs.setBoolPref("network.http.spdy.allow-push", true);
prefs.setBoolPref("network.http.spdy.enabled.http2draft", true);
loadGroup = Cc["@mozilla.org/network/load-group;1"].createInstance(Ci.nsILoadGroup);
// And make go!
run_next_test();
}

View File

@ -269,9 +269,11 @@ fail-if = os == "android"
[test_socks.js]
# Bug 675039: test fails consistently on Android
fail-if = os == "android"
# spdy unit tests require us to have node available to run the spdy server
# spdy and http2 unit tests require us to have node available to run the spdy and http2 server
[test_spdy.js]
run-if = hasNode
run-if = hasNode && processor == "x86_64"
[test_http2.js]
run-if = hasNode && processor == "x86_64"
[test_speculative_connect.js]
[test_standardurl.js]
[test_standardurl_port.js]

View File

@ -18,6 +18,9 @@ TEST_HARNESS_FILES := \
head.js \
node-spdy \
moz-spdy \
node-http2 \
moz-http2 \
node \
$(NULL)
# Extra files needed from $(topsrcdir)/build

View File

@ -0,0 +1,174 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
var http2 = require('../node-http2');
var fs = require('fs');
var url = require('url');
var crypto = require('crypto');
function getHttpContent(path) {
var content = '<!doctype html>' +
'<html>' +
'<head><title>HOORAY!</title></head>' +
'<body>You Win! (by requesting' + path + ')</body>' +
'</html>';
return content;
}
function generateContent(size) {
var content = '';
for (var i = 0; i < size; i++) {
content += '0';
}
return content;
}
/* This takes care of responding to the multiplexed request for us */
var m = {
mp1res: null,
mp2res: null,
buf: null,
mp1start: 0,
mp2start: 0,
checkReady: function() {
if (this.mp1res != null && this.mp2res != null) {
this.buf = generateContent(30*1024);
this.mp1start = 0;
this.mp2start = 0;
this.send(this.mp1res, 0);
setTimeout(this.send.bind(this, this.mp2res, 0), 5);
}
},
send: function(res, start) {
var end = Math.min(start + 1024, this.buf.length);
var content = this.buf.substring(start, end);
res.write(content);
if (end < this.buf.length) {
setTimeout(this.send.bind(this, res, end), 10);
} else {
res.end();
}
}
};
function handleRequest(req, res) {
var u = url.parse(req.url);
var content = getHttpContent(u.pathname);
var push;
if (req.httpVersionMajor === 2) {
res.setHeader('X-Connection-Http2', 'yes');
res.setHeader('X-Http2-StreamId', '' + req.stream.id);
} else {
res.setHeader('X-Connection-Http2', 'no');
}
if (u.pathname === '/exit') {
res.setHeader('Content-Type', 'text/plain');
res.writeHead(200);
res.end('ok');
process.exit();
}
else if ((u.pathname === '/multiplex1') && (req.httpVersionMajor === 2)) {
res.setHeader('Content-Type', 'text/plain');
res.writeHead(200);
m.mp1res = res;
m.checkReady();
return;
}
else if ((u.pathname === '/multiplex2') && (req.httpVersionMajor === 2)) {
res.setHeader('Content-Type', 'text/plain');
res.writeHead(200);
m.mp2res = res;
m.checkReady();
return;
}
else if (u.pathname === "/header") {
var val = req.headers["x-test-header"];
if (val) {
res.setHeader("X-Received-Test-Header", val);
}
}
else if (u.pathname === "/push") {
push = res.push('/push.js');
push.writeHead(200, {
'content-type': 'application/javascript',
'pushed' : 'yes',
'content-length' : 11,
'X-Connection-Http2': 'yes'
});
push.end('// comments');
content = '<head> <script src="push.js"/></head>body text';
}
else if (u.pathname === "/push2") {
push = res.push('/push2.js');
push.writeHead(200, {
'content-type': 'application/javascript',
'pushed' : 'yes',
// no content-length
'X-Connection-Http2': 'yes'
});
push.end('// comments');
content = '<head> <script src="push2.js"/></head>body text';
}
else if (u.pathname === "/big") {
content = generateContent(128 * 1024);
var hash = crypto.createHash('md5');
hash.update(content);
var md5 = hash.digest('hex');
res.setHeader("X-Expected-MD5", md5);
}
else if (u.pathname === "/post") {
if (req.method != "POST") {
res.writeHead(405);
res.end('Unexpected method: ' + req.method);
return;
}
var post_hash = crypto.createHash('md5');
req.on('data', function receivePostData(chunk) {
post_hash.update(chunk.toString());
});
req.on('end', function finishPost() {
var md5 = post_hash.digest('hex');
res.setHeader('X-Calculated-MD5', md5);
res.writeHead(200);
res.end(content);
});
return;
}
res.setHeader('Content-Type', 'text/html');
res.writeHead(200);
res.end(content);
}
// Set up the SSL certs for our server
var options = {
key: fs.readFileSync(__dirname + '/../moz-spdy/spdy-key.pem'),
cert: fs.readFileSync(__dirname + '/../moz-spdy/spdy-cert.pem'),
ca: fs.readFileSync(__dirname + '/../moz-spdy/spdy-ca.pem'),
//, log: require('../node-http2/test/util').createLogger('server')
};
var server = http2.createServer(options, handleRequest);
server.on('connection', function(socket) {
socket.on('error', function() {
// Ignoring SSL socket errors, since they usually represent a connection that was tore down
// by the browser because of an untrusted certificate. And this happens at least once, when
// the first test case if done.
});
});
server.listen(6944);
console.log('HTTP2 server listening on port 6944');

BIN
testing/xpcshell/node Executable file

Binary file not shown.

View File

@ -0,0 +1,136 @@
Version history
===============
### 2.1.0 (2013-11-10) ###
* Upgrade to the latest draft: [draft-ietf-httpbis-http2-07][draft-07]
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-2.1.0.tar.gz)
[draft-07]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-07
### 2.0.0 (2013-11-09) ###
* Splitting out everything that is not related to negotiating HTTP2 or the node-like HTTP API.
These live in separate module from now on:
[http2-protocol](https://github.com/molnarg/node-http2-protocol).
* The only backwards incompatible change: the `Endpoint` class is not exported anymore. Use the
http2-protocol module if you want to use this low level interface.
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-2.0.0.tar.gz)
### 1.0.1 (2013-10-14) ###
* Support for ALPN if node supports it (currently needs a custom build)
* Fix for a few small issues
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-1.0.1.tar.gz)
### 1.0.0 (2013-09-23) ###
* Exporting Endpoint class
* Support for 'filters' in Endpoint
* The last time-based release
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-1.0.0.tar.gz)
### 0.4.1 (2013-09-15) ###
* Major performance improvements
* Minor improvements to error handling
* [Blog post](http://gabor.molnar.es/blog/2013/09/15/gsoc-week-number-13/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.4.1.tar.gz)
### 0.4.0 (2013-09-09) ###
* Upgrade to the latest draft: [draft-ietf-httpbis-http2-06][draft-06]
* Support for HTTP trailers
* Support for TLS SNI (Server Name Indication)
* Improved stream scheduling algorithm
* [Blog post](http://gabor.molnar.es/blog/2013/09/09/gsoc-week-number-12/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.4.0.tar.gz)
[draft-06]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-06
### 0.3.1 (2013-09-03) ###
* Lot of testing, bugfixes
* [Blog post](http://gabor.molnar.es/blog/2013/09/03/gsoc-week-number-11/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.3.1.tar.gz)
### 0.3.0 (2013-08-27) ###
* Support for prioritization
* Small API compatibility improvements (compatibility with the standard node.js HTTP API)
* Minor push API change
* Ability to pass an external bunyan logger when creating a Server or Agent
* [Blog post](http://gabor.molnar.es/blog/2013/08/27/gsoc-week-number-10/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.3.0.tar.gz)
### 0.2.1 (2013-08-20) ###
* Fixing a flow control bug
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.2.1.tar.gz)
### 0.2.0 (2013-08-19) ###
* Exposing server push in the public API
* Connection pooling when operating as client
* Much better API compatibility with the standard node.js HTTPS module
* Logging improvements
* [Blog post](http://gabor.molnar.es/blog/2013/08/19/gsoc-week-number-9/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.2.0.tar.gz)
### 0.1.1 (2013-08-12) ###
* Lots of bugfixes
* Proper flow control for outgoing frames
* Basic flow control for incoming frames
* [Blog post](http://gabor.molnar.es/blog/2013/08/12/gsoc-week-number-8/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.1.1.tar.gz)
### 0.1.0 (2013-08-06) ###
* First release with public API (similar to the standard node HTTPS module)
* Support for NPN negotiation (no ALPN or Upgrade yet)
* Stream number limitation is in place
* Push streams works but not exposed yet in the public API
* [Blog post](http://gabor.molnar.es/blog/2013/08/05/gsoc-week-number-6-and-number-7/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.1.0.tar.gz)
### 0.0.6 (2013-07-19) ###
* `Connection` and `Endpoint` classes are usable, but not yet ready
* Addition of an exmaple server and client
* Using [istanbul](https://github.com/gotwarlost/istanbul) for measuring code coverage
* [Blog post](http://gabor.molnar.es/blog/2013/07/19/gsoc-week-number-5/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.6.tar.gz)
### 0.0.5 (2013-07-14) ###
* `Stream` class is done
* Public API stubs are in place
* [Blog post](http://gabor.molnar.es/blog/2013/07/14/gsoc-week-number-4/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.5.tar.gz)
### 0.0.4 (2013-07-08) ###
* Added logging
* Started `Stream` class implementation
* [Blog post](http://gabor.molnar.es/blog/2013/07/08/gsoc-week-number-3/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.4.tar.gz)
### 0.0.3 (2013-07-03) ###
* Header compression is ready
* [Blog post](http://gabor.molnar.es/blog/2013/07/03/the-http-slash-2-header-compression-implementation-of-node-http2/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.3.tar.gz)
### 0.0.2 (2013-07-01) ###
* Frame serialization and deserialization ready and updated to match the newest spec
* Header compression implementation started
* [Blog post](http://gabor.molnar.es/blog/2013/07/01/gsoc-week-number-2/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.2.tar.gz)
### 0.0.1 (2013-06-23) ###
* Frame serialization and deserialization largely done
* [Blog post](http://gabor.molnar.es/blog/2013/06/23/gsoc-week-number-1/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.1.tar.gz)

View File

@ -0,0 +1,23 @@
The MIT License
Copyright (C) 2013 Gábor Molnár <gabor@molnar.es>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,167 @@
node-http2
==========
An HTTP/2 ([draft-ietf-httpbis-http2-07](http://tools.ietf.org/html/draft-ietf-httpbis-http2-76))
client and server implementation for node.js.
Installation
------------
```
npm install http2
```
API
---
The API is very similar to the [standard node.js HTTPS API](http://nodejs.org/api/https.html). The
goal is the perfect API compatibility, with additional HTTP2 related extensions (like server push).
Detailed API documentation is primarily maintained in the `lib/http.js` file and is [available in
the wiki](https://github.com/molnarg/node-http2/wiki/Public-API) as well.
Examples
--------
### Using as a server ###
```javascript
var options = {
key: fs.readFileSync('./example/localhost.key'),
cert: fs.readFileSync('./example/localhost.crt')
};
require('http2').createServer(options, function(request, response) {
response.end('Hello world!');
}).listen(8080);
```
### Using as a client ###
```javascript
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
require('http2').get('https://localhost:8080/', function(response) {
response.pipe(process.stdout);
});
```
### Simple static file server ###
An simple static file server serving up content from its own directory is available in the `example`
directory. Running the server:
```bash
$ node ./example/server.js
```
### Simple command line client ###
An example client is also available. Downloading the server's own source code from the server:
```bash
$ node ./example/client.js 'https://localhost:8080/server.js' >/tmp/server.js
```
### Server push ###
For a server push example, see the source code of the example
[server](https://github.com/molnarg/node-http2/blob/master/example/server.js) and
[client](https://github.com/molnarg/node-http2/blob/master/example/client.js).
Status
------
* ALPN is not yet supported in node.js (see
[this issue](https://github.com/joyent/node/issues/5945)). For ALPN support, you will have to use
[Shigeki Ohtsu's node.js fork](https://github.com/shigeki/node/tree/alpn_support) until this code
gets merged upstream.
* Upgrade mechanism to start HTTP/2 over unencrypted channel is not implemented yet
(issue [#4](https://github.com/molnarg/node-http2/issues/4))
* Other minor features found in
[this list](https://github.com/molnarg/node-http2/issues?labels=feature) are not implemented yet
Development
-----------
### Development dependencies ###
There's a few library you will need to have installed to do anything described in the following
sections. After installing/cloning node-http2, run `npm install` in its directory to install
development dependencies.
Used libraries:
* [mocha](http://visionmedia.github.io/mocha/) for tests
* [chai](http://chaijs.com/) for assertions
* [istanbul](https://github.com/gotwarlost/istanbul) for code coverage analysis
* [docco](http://jashkenas.github.io/docco/) for developer documentation
* [bunyan](https://github.com/trentm/node-bunyan) for logging
For pretty printing logs, you will also need a global install of bunyan (`npm install -g bunyan`).
### Developer documentation ###
The developer documentation is located in the `doc` directory. The docs are usually updated only
before releasing a new version. To regenerate them manually, run `npm run-script prepublish`.
There's a hosted version which is located [here](http://molnarg.github.io/node-http2/doc/).
### Running the tests ###
It's easy, just run `npm test`. The tests are written in BDD style, so they are a good starting
point to understand the code.
### Test coverage ###
To generate a code coverage report, run `npm test --coverage` (which runs very slowly, be patient).
Code coverage summary as of version 1.0.1:
```
Statements : 93.26% ( 1563/1676 )
Branches : 84.85% ( 605/713 )
Functions : 94.81% ( 201/212 )
Lines : 93.23% ( 1557/1670 )
```
There's a hosted version of the detailed (line-by-line) coverage report
[here](http://molnarg.github.io/node-http2/coverage/lcov-report/lib/).
### Logging ###
Logging is turned off by default. You can turn it on by passing a bunyan logger as `log` option when
creating a server or agent.
When using the example server or client, it's very easy to turn logging on: set the `HTTP2_LOG`
environment variable to `fatal`, `error`, `warn`, `info`, `debug` or `trace` (the logging level).
To log every single incoming and outgoing data chunk, use `HTTP2_LOG_DATA=1` besides
`HTTP2_LOG=trace`. Log output goes to the standard error output. If the standard error is redirected
into a file, then the log output is in bunyan's JSON format for easier post-mortem analysis.
Running the example server and client with `info` level logging output:
```bash
$ HTTP2_LOG=info node ./example/server.js
```
```bash
$ HTTP2_LOG=info node ./example/client.js 'http://localhost:8080/server.js' >/dev/null
```
Contributors
------------
Code contributions are always welcome! People who contributed to node-http2 so far:
* Nick Hurley
* Mike Belshe
Special thanks to Google for financing the development of this module as part of their [Summer of
Code program](https://developers.google.com/open-source/soc/) (project: [HTTP/2 prototype server
implementation](https://google-melange.appspot.com/gsoc/project/google/gsoc2013/molnarg/5001)), and
Nick Hurley of Mozilla, my GSoC mentor, who helped with regular code review and technical advices.
License
-------
The MIT License
Copyright (C) 2013 Gábor Molnár <gabor@molnar.es>

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,106 @@
<!DOCTYPE html>
<html>
<head>
<title>index.js</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
<link rel="stylesheet" media="all" href="docco.css" />
</head>
<body>
<div id="container">
<div id="background"></div>
<ul id="jump_to">
<li>
<a class="large" href="javascript:void(0);">Jump To &hellip;</a>
<a class="small" href="javascript:void(0);">+</a>
<div id="jump_wrapper">
<div id="jump_page">
<a class="source" href="http.html">
http.js
</a>
<a class="source" href="index.html">
index.js
</a>
</div>
</li>
</ul>
<ul class="sections">
<li id="title">
<div class="annotation">
<h1>index.js</h1>
</div>
</li>
<li id="section-1">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-1">&#182;</a>
</div>
<p><a href="https://github.com/molnarg/node-http2">node-http2</a> is an <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-06">HTTP/2 (draft 06)</a> implementation for <a href="http://nodejs.org/">node.js</a>.</p>
<p>The core of the protocol is implemented by the <a href="https://github.com/molnarg/node-http2-protocol">http2-protocol</a> module. This module provides
two important features on top of http2-protocol:</p>
<ul>
<li><p>Implementation of different negotiation schemes that can be used to start a HTTP2 connection.
These include TLS ALPN, Upgrade and Plain TCP.</p>
</li>
<li><p>Providing an API very similar to the standard node.js <a href="http://nodejs.org/api/https.html">HTTPS module API</a>
(which is in turn very similar to the <a href="http://nodejs.org/api/http.html">HTTP module API</a>).</p>
</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre>module.exports = require(<span class="string">'./http'</span>);
<span class="comment">/*
HTTP API
| ^
| |
+-------------|------------|------------------------------------------------------+
| | | Server/Agent |
| v | |
| +----------+ +----------+ |
| | Outgoing | | Incoming | |
| | req/res. | | req/res. | |
| +----------+ +----------+ |
| | ^ |
| | | |
| +---------|------------|-------------------------------------+ +----- |
| | | | Endpoint | | |
| | | | | | |
| | v | | | |
| | +-----------------------+ +-------------------- | | |
| | | Stream | | Stream ... | | |
| | +-----------------------+ +-------------------- | | |
| | | | |
| +------------------------------------------------------------+ +----- |
| | | |
| | | |
| v | |
| +------------------------------------------------------------+ +----- |
| | TCP stream | | ... |
| +------------------------------------------------------------+ +----- |
| |
+---------------------------------------------------------------------------------+
*/</span></pre></div></div>
</li>
</ul>
</div>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,375 @@
/*! normalize.css v2.0.1 | MIT License | git.io/normalize */
/* ==========================================================================
HTML5 display definitions
========================================================================== */
/*
* Corrects `block` display not defined in IE 8/9.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
nav,
section,
summary {
display: block;
}
/*
* Corrects `inline-block` display not defined in IE 8/9.
*/
audio,
canvas,
video {
display: inline-block;
}
/*
* Prevents modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/*
* Addresses styling for `hidden` attribute not present in IE 8/9.
*/
[hidden] {
display: none;
}
/* ==========================================================================
Base
========================================================================== */
/*
* 1. Sets default font family to sans-serif.
* 2. Prevents iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-ms-text-size-adjust: 100%; /* 2 */
}
/*
* Removes default margin.
*/
body {
margin: 0;
}
/* ==========================================================================
Links
========================================================================== */
/*
* Addresses `outline` inconsistency between Chrome and other browsers.
*/
a:focus {
outline: thin dotted;
}
/*
* Improves readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* ==========================================================================
Typography
========================================================================== */
/*
* Addresses `h1` font sizes within `section` and `article` in Firefox 4+,
* Safari 5, and Chrome.
*/
h1 {
font-size: 2em;
}
/*
* Addresses styling not present in IE 8/9, Safari 5, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/*
* Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/*
* Addresses styling not present in Safari 5 and Chrome.
*/
dfn {
font-style: italic;
}
/*
* Addresses styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/*
* Corrects font family set oddly in Safari 5 and Chrome.
*/
code,
kbd,
pre,
samp {
font-family: monospace, serif;
font-size: 1em;
}
/*
* Improves readability of pre-formatted text in all browsers.
*/
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
/*
* Sets consistent quote types.
*/
q {
quotes: "\201C" "\201D" "\2018" "\2019";
}
/*
* Addresses inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/*
* Prevents `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* ==========================================================================
Embedded content
========================================================================== */
/*
* Removes border when inside `a` element in IE 8/9.
*/
img {
border: 0;
}
/*
* Corrects overflow displayed oddly in IE 9.
*/
svg:not(:root) {
overflow: hidden;
}
/* ==========================================================================
Figures
========================================================================== */
/*
* Addresses margin not present in IE 8/9 and Safari 5.
*/
figure {
margin: 0;
}
/* ==========================================================================
Forms
========================================================================== */
/*
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/*
* 1. Corrects color not being inherited in IE 8/9.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/*
* 1. Corrects font family not being inherited in all browsers.
* 2. Corrects font size not being inherited in all browsers.
* 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome
*/
button,
input,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 2 */
margin: 0; /* 3 */
}
/*
* Addresses Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
button,
input {
line-height: normal;
}
/*
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Corrects inability to style clickable `input` types in iOS.
* 3. Improves usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/*
* Re-set default cursor for disabled elements.
*/
button[disabled],
input[disabled] {
cursor: default;
}
/*
* 1. Addresses box sizing set to `content-box` in IE 8/9.
* 2. Removes excess padding in IE 8/9.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/*
* 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome.
* 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/*
* Removes inner padding and search cancel button in Safari 5 and Chrome
* on OS X.
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
* Removes inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/*
* 1. Removes default vertical scrollbar in IE 8/9.
* 2. Improves readability and alignment in all browsers.
*/
textarea {
overflow: auto; /* 1 */
vertical-align: top; /* 2 */
}
/* ==========================================================================
Tables
========================================================================== */
/*
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}

View File

@ -0,0 +1,42 @@
var fs = require('fs');
var path = require('path');
var http2 = require('..');
http2.globalAgent = new http2.Agent({
log: require('../test/util').createLogger('client')
});
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
// Sending the request
// It would be `var request = http2.get(process.argv.pop());` if we wouldn't care about plain mode
var options = require('url').parse(process.argv.pop());
options.plain = Boolean(process.env.HTTP2_PLAIN);
var request = http2.request(options);
request.end();
// Receiving the response
request.on('response', function(response) {
response.pipe(process.stdout);
response.on('end', finish);
});
// Receiving push streams
request.on('push', function(pushRequest) {
var filename = path.join(__dirname, '/push-' + push_count);
push_count += 1;
console.error('Receiving pushed resource: ' + pushRequest.url + ' -> ' + filename);
pushRequest.on('response', function(pushResponse) {
pushResponse.pipe(fs.createWriteStream(filename)).on('finish', finish);
});
});
// Quitting after both the response and the associated pushed resources have arrived
var push_count = 0;
var finished = 0;
function finish() {
finished += 1;
if (finished === (1 + push_count)) {
process.exit();
}
}

View File

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICIzCCAYwCCQCsvG34Az33qTANBgkqhkiG9w0BAQUFADBWMQswCQYDVQQGEwJY
WDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBh
bnkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTMwODAyMTMwODQzWhcNMTMw
OTAxMTMwODQzWjBWMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5
MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMRIwEAYDVQQDDAlsb2NhbGhv
c3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM8D4tgE1cdI9uLo4N9AL8Ck
ogREH5LSm3SsRGFdUu5b2Nx63K/qwtTUbtUlISZBI+KESkwQXcf1ErwXUDnbTtk/
VpLJ+gfIN18e9LAdiZgAMEWlitiLhR+D17w4NzHYOpWy1YzgOckukPy1ZfTH9e7j
tEH9+7c4mpv7QMkFdw4hAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAP+ZFskjJtNxY
c+5JfMjEgSHEIy+AJ5/vXIspNYKMb7l0gYDvmFm8QTKChKTYvJmepBrIdL7MjXCX
SWiPz05ch99c84yOx5qVpcPd0y2fjO8xn2NCLfWdP7iSVYmpftwzjqFzPc4EkAny
NOpbnw9iM4JXsZNFtPTvSp+8StPGWzU=
-----END CERTIFICATE-----

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDPA+LYBNXHSPbi6ODfQC/ApKIERB+S0pt0rERhXVLuW9jcetyv
6sLU1G7VJSEmQSPihEpMEF3H9RK8F1A5207ZP1aSyfoHyDdfHvSwHYmYADBFpYrY
i4Ufg9e8ODcx2DqVstWM4DnJLpD8tWX0x/Xu47RB/fu3OJqb+0DJBXcOIQIDAQAB
AoGAHtRVVBZkP+l92w0TcCv+8JGUD06V5Se4Pwfopxde4mCLS0qA0zIDEe8REm0V
Ir1Quss4xVsqnDzDLX/LUtJ2S1+seWcoLdDV/wSDiM2CLS7KauUazrTWHLNId/lu
/VombYWK10uNiDZZJ8xwEaKt+ZptC2kK8/yi0aX0PrGhAIECQQDsD8A64BBrWCrb
7PrJt04CAcM3uBUzS6ausiJKw9IEktnvcnsN9kZazcAW86WDFsXI5oPubmgHhQ/s
m9iIrbMPAkEA4IAUWi5mVuWAyUIc9YbjJdnmvkAykSxr/vp/26RMSDmUAAUlYNNc
HZbM1uVZsFForKza28Px01Ga728ZdhRrzwJBAIrwNlcwu9lCWm95Cp6hGfPKb8ki
uq+nTiKyS8avfLQebtElE1JDamNViEK6AuemBqFZM7upFeefJKFBlO/VNHcCQCXN
CyBALdU14aCBtFSXOMoXzaV9M8aD/084qKy4FmwW3de/BhMuo5UL3kPU7Gwm2QQy
OsvES4S0ee0U/OmH+LsCQAnNdxNPgzJDTx7wOTFhHIBr4mtepLiaRXIdkLEsR9Kb
vcK6BwUfomM29eGOXtUAU7sJ5xnyKkSuNN7fxIWjzPI=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,49 @@
var fs = require('fs');
var path = require('path');
var http2 = require('..');
var options = process.env.HTTP2_PLAIN ? {
plain: true
} : {
key: fs.readFileSync(path.join(__dirname, '/localhost.key')),
cert: fs.readFileSync(path.join(__dirname, '/localhost.crt'))
};
// Passing bunyan logger (optional)
options.log = require('../test/util').createLogger('server');
// We cache one file to be able to do simple performance tests without waiting for the disk
var cachedFile = fs.readFileSync(path.join(__dirname, './server.js'));
var cachedUrl = '/server.js';
// Creating the server
var server = http2.createServer(options, function(request, response) {
var filename = path.join(__dirname, request.url);
// Serving server.js from cache. Useful for microbenchmarks.
if (request.url === cachedUrl) {
response.end(cachedFile);
}
// Reading file from disk if it exists and is safe.
else if ((filename.indexOf(__dirname) === 0) && fs.existsSync(filename) && fs.statSync(filename).isFile()) {
response.writeHead('200');
// If they download the certificate, push the private key too, they might need it.
if (response.push && request.url === '/localhost.crt') {
var push = response.push('/localhost.key');
push.writeHead(200);
fs.createReadStream(path.join(__dirname, '/localhost.key')).pipe(push);
}
fs.createReadStream(filename).pipe(response);
}
// Otherwise responding with 404.
else {
response.writeHead('404');
response.end();
}
});
server.listen(process.env.HTTP2_PORT || 8080);

View File

@ -0,0 +1,985 @@
// Public API
// ==========
// The main governing power behind the http2 API design is that it should look very similar to the
// existing node.js [HTTPS API][1] (which is, in turn, almost identical to the [HTTP API][2]). The
// additional features of HTTP/2 are exposed as extensions to this API. Furthermore, node-http2
// should fall back to using HTTP/1.1 if needed. Compatibility with undocumented or deprecated
// elements of the node.js HTTP/HTTPS API is a non-goal.
//
// Additional and modified API elements
// ------------------------------------
//
// - **Class: http2.Endpoint**: an API for using the raw HTTP/2 framing layer. For documentation
// see the [lib/endpoint.js](endpoint.html) file.
//
// - **Class: http2.Server**
// - **Event: 'connection' (socket, [endpoint])**: there's a second argument if the negotiation of
// HTTP/2 was successful: the reference to the [Endpoint](endpoint.html) object tied to the
// socket.
//
// - **http2.createServer(options, [requestListener])**: additional option:
// - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object
// - **plain**: if `true`, the server will accept HTTP/2 connections over plain TCP instead of
// TLS
//
// - **Class: http2.ServerResponse**
// - **response.push(options)**: initiates a server push. `options` describes the 'imaginary'
// request to which the push stream is a response; the possible options are identical to the
// ones accepted by `http2.request`. Returns a ServerResponse object that can be used to send
// the response headers and content.
//
// - **Class: http2.Agent**
// - **new Agent(options)**: additional option:
// - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object
// - **agent.sockets**: only contains TCP sockets that corresponds to HTTP/1 requests.
// - **agent.endpoints**: contains [Endpoint](endpoint.html) objects for HTTP/2 connections.
//
// - **http2.request(options, [callback])**: additional option:
// - **plain**: if `true`, the client will not try to build a TLS tunnel, instead it will use
// the raw TCP stream for HTTP/2
//
// - **Class: http2.ClientRequest**
// - **Event: 'socket' (socket)**: in case of an HTTP/2 incoming message, `socket` is a reference
// to the associated [HTTP/2 Stream](stream.html) object (and not to the TCP socket).
// - **Event: 'push' (promise)**: signals the intention of a server push associated to this
// request. `promise` is an IncomingPromise. If there's no listener for this event, the server
// push is cancelled.
// - **request.setPriority(priority)**: assign a priority to this request. `priority` is a number
// between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.
//
// - **Class: http2.IncomingMessage**
// - has two subclasses for easier interface description: **IncomingRequest** and
// **IncomingResponse**
// - **message.socket**: in case of an HTTP/2 incoming message, it's a reference to the associated
// [HTTP/2 Stream](stream.html) object (and not to the TCP socket).
//
// - **Class: http2.IncomingRequest (IncomingMessage)**
// - **message.url**: in case of an HTTP/2 incoming request, the `url` field always contains the
// path, and never a full url (it contains the path in most cases in the HTTPS api as well).
// - **message.scheme**: additional field. Mandatory HTTP/2 request metadata.
// - **message.host**: additional field. Mandatory HTTP/2 request metadata. Note that this
// replaces the old Host header field, but node-http2 will add Host to the `message.headers` for
// backwards compatibility.
//
// - **Class: http2.IncomingPromise (IncomingRequest)**
// - contains the metadata of the 'imaginary' request to which the server push is an answer.
// - **Event: 'response' (response)**: signals the arrival of the actual push stream. `response`
// is an IncomingResponse.
// - **Event: 'push' (promise)**: signals the intention of a server push associated to this
// request. `promise` is an IncomingPromise. If there's no listener for this event, the server
// push is cancelled.
// - **promise.cancel()**: cancels the promised server push.
// - **promise.setPriority(priority)**: assign a priority to this push stream. `priority` is a
// number between 0 (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.
//
// API elements not yet implemented
// --------------------------------
//
// - **Class: http2.Server**
// - **server.maxHeadersCount**
//
// API elements that are not applicable to HTTP/2
// ----------------------------------------------
//
// The reason may be deprecation of certain HTTP/1.1 features, or that some API elements simply
// don't make sense when using HTTP/2. These will not be present when a request is done with HTTP/2,
// but will function normally when falling back to using HTTP/1.1.
//
// - **Class: http2.Server**
// - **Event: 'checkContinue'**: not in the spec, yet (see [http-spec#18][expect-continue])
// - **Event: 'upgrade'**: upgrade is deprecated in HTTP/2
// - **Event: 'timeout'**: HTTP/2 sockets won't timeout because of application level keepalive
// (PING frames)
// - **Event: 'connect'**: not in the spec, yet (see [http-spec#230][connect])
// - **server.setTimeout(msecs, [callback])**
// - **server.timeout**
//
// - **Class: http2.ServerResponse**
// - **Event: 'close'**
// - **Event: 'timeout'**
// - **response.writeContinue()**
// - **response.writeHead(statusCode, [reasonPhrase], [headers])**: reasonPhrase will always be
// ignored since [it's not supported in HTTP/2][3]
// - **response.setTimeout(timeout, [callback])**
//
// - **Class: http2.Agent**
// - **agent.maxSockets**: only affects HTTP/1 connection pool. When using HTTP/2, there's always
// one connection per host.
//
// - **Class: http2.ClientRequest**
// - **Event: 'upgrade'**
// - **Event: 'connect'**
// - **Event: 'continue'**
// - **request.setTimeout(timeout, [callback])**
// - **request.setNoDelay([noDelay])**
// - **request.setSocketKeepAlive([enable], [initialDelay])**
//
// - **Class: http2.IncomingMessage**
// - **Event: 'close'**
// - **message.setTimeout(timeout, [callback])**
//
// [1]: http://nodejs.org/api/https.html
// [2]: http://nodejs.org/api/http.html
// [3]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-8.1.3
// [expect-continue]: https://github.com/http2/http2-spec/issues/18
// [connect]: https://github.com/http2/http2-spec/issues/230
// Common server and client side code
// ==================================
var net = require('net');
var url = require('url');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var PassThrough = require('stream').PassThrough;
var Readable = require('stream').Readable;
var Writable = require('stream').Writable;
var Endpoint = require('http2-protocol').Endpoint;
var http = require('http');
var https = require('https');
exports.STATUS_CODES = http.STATUS_CODES;
exports.IncomingMessage = IncomingMessage;
exports.OutgoingMessage = OutgoingMessage;
var deprecatedHeaders = [
'connection',
'host',
'keep-alive',
'proxy-connection',
'te',
'transfer-encoding',
'upgrade'
];
// The implemented version of the HTTP/2 specification is [draft 04][1].
// [1]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04
var implementedVersion = 'HTTP-draft-08/2.0';
// When doing NPN/ALPN negotiation, HTTP/1.1 is used as fallback
var supportedProtocols = [implementedVersion, 'http/1.1', 'http/1.0'];
// Logging
// -------
// Logger shim, used when no logger is provided by the user.
function noop() {}
var defaultLogger = {
fatal: noop,
error: noop,
warn : noop,
info : noop,
debug: noop,
trace: noop,
child: function() { return this; }
};
// Bunyan serializers exported by submodules that are worth adding when creating a logger.
exports.serializers = require('http2-protocol').serializers;
// IncomingMessage class
// ---------------------
function IncomingMessage(stream) {
// * This is basically a read-only wrapper for the [Stream](stream.html) class.
PassThrough.call(this);
stream.pipe(this);
this.socket = this.stream = stream;
this._log = stream._log.child({ component: 'http' });
// * HTTP/2.0 does not define a way to carry the version identifier that is included in the
// HTTP/1.1 request/status line. Version is always 2.0.
this.httpVersion = '2.0';
this.httpVersionMajor = 2;
this.httpVersionMinor = 0;
// * `this.headers` will store the regular headers (and none of the special colon headers)
this.headers = {};
this.trailers = undefined;
this._lastHeadersSeen = undefined;
// * Other metadata is filled in when the headers arrive.
stream.once('headers', this._onHeaders.bind(this));
stream.once('end', this._onEnd.bind(this));
}
IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } });
// [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-05#section-8.1.2.1)
// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
// of key-value pairs. This includes the target URI for the request, the status code for the
// response, as well as HTTP header fields.
IncomingMessage.prototype._onHeaders = function _onHeaders(headers) {
// * An HTTP/2.0 request or response MUST NOT include any of the following header fields:
// Connection, Host, Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, and Upgrade. A server
// MUST treat the presence of any of these header fields as a stream error of type
// PROTOCOL_ERROR.
for (var i = 0; i < deprecatedHeaders.length; i++) {
var key = deprecatedHeaders[i];
if (key in headers) {
this._log.error({ key: key, value: headers[key] }, 'Deprecated header found');
this.stream.emit('error', 'PROTOCOL_ERROR');
return;
}
}
// * Store the _regular_ headers in `this.headers`
for (var name in headers) {
if (name[0] !== ':') {
this.headers[name] = headers[name];
}
}
// * The last header block, if it's not the first, will represent the trailers
var self = this;
this.stream.on('headers', function(headers) {
self._lastHeadersSeen = headers;
});
};
IncomingMessage.prototype._onEnd = function _onEnd() {
this.trailers = this._lastHeadersSeen;
};
IncomingMessage.prototype.setTimeout = noop;
IncomingMessage.prototype._checkSpecialHeader = function _checkSpecialHeader(key, value) {
if ((typeof value !== 'string') || (value.length === 0)) {
this._log.error({ key: key, value: value }, 'Invalid or missing special header field');
this.stream.emit('error', 'PROTOCOL_ERROR');
}
return value;
}
;
// OutgoingMessage class
// ---------------------
function OutgoingMessage() {
// * This is basically a read-only wrapper for the [Stream](stream.html) class.
Writable.call(this);
this._headers = {};
this._trailers = undefined;
this.headersSent = false;
this.on('finish', this._finish);
}
OutgoingMessage.prototype = Object.create(Writable.prototype, { constructor: { value: OutgoingMessage } });
OutgoingMessage.prototype._write = function _write(chunk, encoding, callback) {
if (this.stream) {
this.stream.write(chunk, encoding, callback);
} else {
this.once('socket', this._write.bind(this, chunk, encoding, callback));
}
};
OutgoingMessage.prototype._finish = function _finish() {
if (this.stream) {
if (this._trailers) {
if (this.request) {
this.request.addTrailers(this._trailers);
} else {
this.stream.headers(this._trailers);
}
}
this.stream.end();
} else {
this.once('socket', this._finish.bind(this));
}
};
OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
if (this.headersSent) {
throw new Error('Can\'t set headers after they are sent.');
} else {
name = name.toLowerCase();
if (deprecatedHeaders.indexOf(name) !== -1) {
throw new Error('Cannot set deprecated header: ' + name);
}
this._headers[name] = value;
}
};
OutgoingMessage.prototype.removeHeader = function removeHeader(name) {
if (this.headersSent) {
throw new Error('Can\'t remove headers after they are sent.');
} else {
delete this._headers[name.toLowerCase()];
}
};
OutgoingMessage.prototype.getHeader = function getHeader(name) {
return this._headers[name.toLowerCase()];
};
OutgoingMessage.prototype.addTrailers = function addTrailers(trailers) {
this._trailers = trailers;
};
OutgoingMessage.prototype.setTimeout = noop;
OutgoingMessage.prototype._checkSpecialHeader = IncomingMessage.prototype._checkSpecialHeader;
// Server side
// ===========
exports.createServer = createServer;
exports.Server = Server;
exports.IncomingRequest = IncomingRequest;
exports.OutgoingResponse = OutgoingResponse;
exports.ServerResponse = OutgoingResponse; // for API compatibility
// Server class
// ------------
function Server(options) {
options = util._extend({}, options);
this._log = (options.log || defaultLogger).child({ component: 'http' });
this._settings = options.settings;
var start = this._start.bind(this);
var fallback = this._fallback.bind(this);
// HTTP2 over TLS (using NPN or ALPN)
if ((options.key && options.cert) || options.pfx) {
this._log.info('Creating HTTP/2 server over TLS');
this._mode = 'tls';
options.ALPNProtocols = supportedProtocols;
options.NPNProtocols = supportedProtocols;
this._server = https.createServer(options);
this._originalSocketListeners = this._server.listeners('secureConnection');
this._server.removeAllListeners('secureConnection');
this._server.on('secureConnection', function(socket) {
var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
if ((negotiatedProtocol === implementedVersion) && socket.servername) {
start(socket);
} else {
fallback(socket);
}
});
this._server.on('request', this.emit.bind(this, 'request'));
}
// HTTP2 over plain TCP
else if (options.plain) {
this._log.info('Creating HTTP/2 server over plain TCP');
this._mode = 'plain';
this._server = net.createServer(start);
}
// HTTP/2 with HTTP/1.1 upgrade
else {
this._log.error('Trying to create HTTP/2 server with Upgrade from HTTP/1.1');
throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported. Please provide TLS keys.');
}
this._server.on('close', this.emit.bind(this, 'close'));
}
Server.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Server } });
// Starting HTTP/2
Server.prototype._start = function _start(socket) {
var endpoint = new Endpoint(this._log, 'SERVER', this._settings);
this._log.info({ e: endpoint,
client: socket.remoteAddress + ':' + socket.remotePort,
SNI: socket.servername
}, 'New incoming HTTP/2 connection');
endpoint.pipe(socket).pipe(endpoint);
var self = this;
endpoint.on('stream', function _onStream(stream) {
var response = new OutgoingResponse(stream);
var request = new IncomingRequest(stream);
request.once('ready', self.emit.bind(self, 'request', request, response));
});
endpoint.on('error', this.emit.bind(this, 'clientError'));
this.emit('connection', socket, endpoint);
};
Server.prototype._fallback = function _fallback(socket) {
var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort,
protocol: negotiatedProtocol,
SNI: socket.servername
}, 'Falling back to simple HTTPS');
for (var i = 0; i < this._originalSocketListeners.length; i++) {
this._originalSocketListeners[i].call(this._server, socket);
}
this.emit('connection', socket);
};
// There are [3 possible signatures][1] of the `listen` function. Every arguments is forwarded to
// the backing TCP or HTTPS server.
// [1]: http://nodejs.org/api/http.html#http_server_listen_port_hostname_backlog_callback
Server.prototype.listen = function listen(port, hostname) {
this._log.info({ on: ((typeof hostname === 'string') ? (hostname + ':' + port) : port) },
'Listening for incoming connections');
this._server.listen.apply(this._server, arguments);
};
Server.prototype.close = function close(callback) {
this._log.info('Closing server');
this._server.close(callback);
};
Server.prototype.setTimeout = function setTimeout(timeout, callback) {
if (this._mode === 'tls') {
this._server.setTimeout(timeout, callback);
}
};
Object.defineProperty(Server.prototype, 'timeout', {
get: function getTimeout() {
if (this._mode === 'tls') {
return this._server.timeout;
} else {
return undefined;
}
},
set: function setTimeout(timeout) {
if (this._mode === 'tls') {
this._server.timeout = timeout;
}
}
});
// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
// `server`.There are events on the `http.Server` class where it makes difference whether someone is
// listening on the event or not. In these cases, we can not simply forward the events from the
// `server` to `this` since that means a listener. Instead, we forward the subscriptions.
Server.prototype.on = function on(event, listener) {
if ((event === 'upgrade') || (event === 'timeout')) {
this._server.on(event, listener && listener.bind(this));
} else {
EventEmitter.prototype.on.call(this, event, listener);
}
};
// `addContext` is used to add Server Name Indication contexts
Server.prototype.addContext = function addContext(hostname, credentials) {
if (this._mode === 'tls') {
this._server.addContext(hostname, credentials);
}
};
function createServer(options, requestListener) {
if (typeof options === 'function') {
requestListener = options;
options = undefined;
}
var server = new Server(options);
if (requestListener) {
server.on('request', requestListener);
}
return server;
}
// IncomingRequest class
// ---------------------
function IncomingRequest(stream) {
IncomingMessage.call(this, stream);
}
IncomingRequest.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingRequest } });
// [Request Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-05#section-8.1.2.1)
// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
// of key-value pairs. This includes the target URI for the request, the status code for the
// response, as well as HTTP header fields.
IncomingRequest.prototype._onHeaders = function _onHeaders(headers) {
// * The ":method" header field includes the HTTP method
// * The ":scheme" header field includes the scheme portion of the target URI
// * The ":authority" header field includes the authority portion of the target URI
// * The ":path" header field includes the path and query parts of the target URI.
// This field MUST NOT be empty; URIs that do not contain a path component MUST include a value
// of '/', unless the request is an OPTIONS request for '*', in which case the ":path" header
// field MUST include '*'.
// * All HTTP/2.0 requests MUST include exactly one valid value for all of these header fields. A
// server MUST treat the absence of any of these header fields, presence of multiple values, or
// an invalid value as a stream error of type PROTOCOL_ERROR.
this.method = this._checkSpecialHeader(':method' , headers[':method']);
this.scheme = this._checkSpecialHeader(':scheme' , headers[':scheme']);
this.host = this._checkSpecialHeader(':authority', headers[':authority'] );
this.url = this._checkSpecialHeader(':path' , headers[':path'] );
// * Host header is included in the headers object for backwards compatibility.
this.headers.host = this.host;
// * Handling regular headers.
IncomingMessage.prototype._onHeaders.call(this, headers);
// * Signaling that the headers arrived.
this._log.info({ method: this.method, scheme: this.scheme, host: this.host,
path: this.url, headers: this.headers }, 'Incoming request');
this.emit('ready');
};
// OutgoingResponse class
// ----------------------
function OutgoingResponse(stream) {
OutgoingMessage.call(this);
this._log = stream._log.child({ component: 'http' });
this.stream = stream;
this.statusCode = 200;
this.sendDate = true;
this.stream.once('headers', this._onRequestHeaders.bind(this));
}
OutgoingResponse.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingResponse } });
OutgoingResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase, headers) {
if (typeof reasonPhrase === 'string') {
this._log.warn('Reason phrase argument was present but ignored by the writeHead method');
} else {
headers = reasonPhrase;
}
for (var name in headers) {
this.setHeader(name, headers[name]);
}
headers = this._headers;
if (this.sendDate && !('date' in this._headers)) {
headers.date = (new Date()).toUTCString();
}
this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response');
headers[':status'] = this.statusCode = statusCode;
this.stream.headers(headers);
this.headersSent = true;
};
OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() {
if (!this.headersSent) {
this.writeHead(this.statusCode);
}
};
OutgoingResponse.prototype.write = function write() {
this._implicitHeaders();
return OutgoingMessage.prototype.write.apply(this, arguments);
};
OutgoingResponse.prototype.end = function end() {
this._implicitHeaders();
return OutgoingMessage.prototype.end.apply(this, arguments);
};
OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) {
this._requestHeaders = headers;
};
OutgoingResponse.prototype.push = function push(options) {
if (typeof options === 'string') {
options = url.parse(options);
}
if (!options.path) {
throw new Error('`path` option is mandatory.');
}
var promise = util._extend({
':method': (options.method || 'GET').toUpperCase(),
':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'],
':authority': options.hostname || options.host || this._requestHeaders[':authority'],
':path': options.path
}, options.headers);
this._log.info({ method: promise[':method'], scheme: promise[':scheme'],
authority: promise[':authority'], path: promise[':path'],
headers: options.headers }, 'Promising push stream');
var pushStream = this.stream.promise(promise);
return new OutgoingResponse(pushStream);
};
// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
// `request`. See `Server.prototype.on` for explanation.
OutgoingResponse.prototype.on = function on(event, listener) {
if (this.request && (event === 'timeout')) {
this.request.on(event, listener && listener.bind(this));
} else {
OutgoingMessage.prototype.on.call(this, event, listener);
}
};
// Client side
// ===========
exports.ClientRequest = OutgoingRequest; // for API compatibility
exports.OutgoingRequest = OutgoingRequest;
exports.IncomingResponse = IncomingResponse;
exports.Agent = Agent;
exports.globalAgent = undefined;
exports.request = function request(options, callback) {
return (options.agent || exports.globalAgent).request(options, callback);
};
exports.get = function get(options, callback) {
return (options.agent || exports.globalAgent).get(options, callback);
};
// Agent class
// -----------
function Agent(options) {
EventEmitter.call(this);
options = util._extend({}, options);
this._settings = options.settings;
this._log = (options.log || defaultLogger).child({ component: 'http' });
this.endpoints = {};
// * Using an own HTTPS agent, because the global agent does not look at `NPN/ALPNProtocols` when
// generating the key identifying the connection, so we may get useless non-negotiated TLS
// channels even if we ask for a negotiated one. This agent will contain only negotiated
// channels.
var agentOptions = {};
agentOptions.ALPNProtocols = supportedProtocols;
agentOptions.NPNProtocols = supportedProtocols;
this._httpsAgent = new https.Agent(agentOptions);
this.sockets = this._httpsAgent.sockets;
this.requests = this._httpsAgent.requests;
}
Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } });
Agent.prototype.request = function request(options, callback) {
if (typeof options === 'string') {
options = url.parse(options);
} else {
options = util._extend({}, options);
}
options.method = (options.method || 'GET').toUpperCase();
options.protocol = options.protocol || 'https:';
options.host = options.hostname || options.host || 'localhost';
options.port = options.port || 443;
options.path = options.path || '/';
if (!options.plain && options.protocol === 'http:') {
this._log.error('Trying to negotiate client request with Upgrade from HTTP/1.1');
throw new Error('HTTP1.1 -> HTTP2 upgrade is not yet supported.');
}
var request = new OutgoingRequest(this._log);
if (callback) {
request.on('response', callback);
}
var key = [
!!options.plain,
options.host,
options.port
].join(':');
// * There's an existing HTTP/2 connection to this host
if (key in this.endpoints) {
var endpoint = this.endpoints[key];
request._start(endpoint.createStream(), options);
}
// * HTTP/2 over plain TCP
else if (options.plain) {
endpoint = new Endpoint(this._log, 'CLIENT', this._settings);
endpoint.socket = net.connect({
host: options.host,
port: options.port,
localAddress: options.localAddress
});
endpoint.pipe(endpoint.socket).pipe(endpoint);
request._start(endpoint.createStream(), options);
}
// * HTTP/2 over TLS negotiated using NPN or ALPN
else {
var started = false;
options.ALPNProtocols = supportedProtocols;
options.NPNProtocols = supportedProtocols;
options.servername = options.host; // Server Name Indication
options.agent = this._httpsAgent;
var httpsRequest = https.request(options);
httpsRequest.on('socket', function(socket) {
var negotiatedProtocol = socket.alpnProtocol || socket.npnProtocol;
if (negotiatedProtocol !== undefined) {
negotiated();
} else {
socket.on('secureConnect', negotiated);
}
});
var self = this;
function negotiated() {
var endpoint;
var negotiatedProtocol = httpsRequest.socket.alpnProtocol || httpsRequest.socket.npnProtocol;
if (negotiatedProtocol === implementedVersion) {
httpsRequest.socket.emit('agentRemove');
unbundleSocket(httpsRequest.socket);
endpoint = new Endpoint(self._log, 'CLIENT', self._settings);
endpoint.socket = httpsRequest.socket;
endpoint.pipe(endpoint.socket).pipe(endpoint);
}
if (started) {
if (endpoint) {
endpoint.close();
} else {
httpsRequest.abort();
}
} else {
if (endpoint) {
self._log.info({ e: endpoint, server: options.host + ':' + options.port },
'New outgoing HTTP/2 connection');
self.endpoints[key] = endpoint;
self.emit(key, endpoint);
} else {
self.emit(key, undefined);
}
}
}
this.once(key, function(endpoint) {
started = true;
if (endpoint) {
request._start(endpoint.createStream(), options);
} else {
request._fallback(httpsRequest);
}
});
}
return request;
};
Agent.prototype.get = function get(options, callback) {
var request = this.request(options, callback);
request.end();
return request;
};
function unbundleSocket(socket) {
socket.removeAllListeners('data');
socket.removeAllListeners('end');
socket.removeAllListeners('readable');
socket.removeAllListeners('close');
socket.removeAllListeners('error');
socket.unpipe();
delete socket.ondata;
delete socket.onend;
}
Object.defineProperty(Agent.prototype, 'maxSockets', {
get: function getMaxSockets() {
return this._httpsAgent.maxSockets;
},
set: function setMaxSockets(value) {
this._httpsAgent.maxSockets = value;
}
});
exports.globalAgent = new Agent();
// OutgoingRequest class
// ---------------------
function OutgoingRequest() {
OutgoingMessage.call(this);
this._log = undefined;
this.stream = undefined;
}
OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } });
OutgoingRequest.prototype._start = function _start(stream, options) {
this.stream = stream;
this._log = stream._log.child({ component: 'http' });
for (var key in options.headers) {
this.setHeader(key, options.headers[key]);
}
var headers = this._headers;
delete headers.host;
if (options.auth) {
headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64');
}
headers[':scheme'] = options.protocol.slice(0, -1);
headers[':method'] = options.method;
headers[':authority'] = options.host;
headers[':path'] = options.path;
this._log.info({ scheme: headers[':scheme'], method: headers[':method'],
authority: headers[':authority'], path: headers[':path'],
headers: (options.headers || {}) }, 'Sending request');
this.stream.headers(headers);
this.headersSent = true;
this.emit('socket', this.stream);
var response = new IncomingResponse(this.stream);
response.once('ready', this.emit.bind(this, 'response', response));
this.stream.on('promise', this._onPromise.bind(this));
};
OutgoingRequest.prototype._fallback = function _fallback(request) {
request.on('response', this.emit.bind(this, 'response'));
this.stream = this.request = request;
this.emit('socket', this.socket);
};
OutgoingRequest.prototype.setPriority = function setPriority(priority) {
if (this.stream) {
this.stream.priority(priority);
} else {
this.once('socket', this.setPriority.bind(this, priority));
}
};
// Overriding `EventEmitter`'s `on(event, listener)` method to forward certain subscriptions to
// `request`. See `Server.prototype.on` for explanation.
OutgoingRequest.prototype.on = function on(event, listener) {
if (this.request && (event === 'upgrade')) {
this.request.on(event, listener && listener.bind(this));
} else {
OutgoingMessage.prototype.on.call(this, event, listener);
}
};
// Methods only in fallback mode
OutgoingRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
if (this.request) {
this.request.setNoDelay(noDelay);
} else if (!this.stream) {
this.on('socket', this.setNoDelay.bind(this, noDelay));
}
};
OutgoingRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive(enable, initialDelay) {
if (this.request) {
this.request.setSocketKeepAlive(enable, initialDelay);
} else if (!this.stream) {
this.on('socket', this.setSocketKeepAlive.bind(this, enable, initialDelay));
}
};
OutgoingRequest.prototype.setTimeout = function setTimeout(timeout, callback) {
if (this.request) {
this.request.setTimeout(timeout, callback);
} else if (!this.stream) {
this.on('socket', this.setTimeout.bind(this, timeout, callback));
}
};
// Aborting the request
OutgoingRequest.prototype.abort = function abort() {
if (this.request) {
this.request.abort();
} else if (this.stream) {
this.stream.reset('CANCEL');
} else {
this.on('socket', this.abort.bind(this));
}
};
// Receiving push promises
OutgoingRequest.prototype._onPromise = function _onPromise(stream, headers) {
this._log.info({ push_stream: stream.id }, 'Receiving push promise');
var promise = new IncomingPromise(stream, headers);
if (this.listeners('push').length > 0) {
this.emit('push', promise);
} else {
promise.cancel();
}
};
// IncomingResponse class
// ----------------------
function IncomingResponse(stream) {
IncomingMessage.call(this, stream);
}
IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } });
// [Response Header Fields](http://tools.ietf.org/html/draft-ietf-httpbis-http2-05#section-8.1.2.2)
// * `headers` argument: HTTP/2.0 request and response header fields carry information as a series
// of key-value pairs. This includes the target URI for the request, the status code for the
// response, as well as HTTP header fields.
IncomingResponse.prototype._onHeaders = function _onHeaders(headers) {
// * A single ":status" header field is defined that carries the HTTP status code field. This
// header field MUST be included in all responses.
// * A client MUST treat the absence of the ":status" header field, the presence of multiple
// values, or an invalid value as a stream error of type PROTOCOL_ERROR.
// Note: currently, we do not enforce it strictly: we accept any format, and parse it as int
// * HTTP/2.0 does not define a way to carry the reason phrase that is included in an HTTP/1.1
// status line.
this.statusCode = parseInt(this._checkSpecialHeader(':status', headers[':status']));
// * Handling regular headers.
IncomingMessage.prototype._onHeaders.call(this, headers);
// * Signaling that the headers arrived.
this._log.info({ status: this.statusCode, headers: this.headers}, 'Incoming response');
this.emit('ready');
};
// IncomingPromise class
// -------------------------
function IncomingPromise(responseStream, promiseHeaders) {
var stream = new Readable();
stream._read = noop;
stream.push(null);
stream._log = responseStream._log;
IncomingRequest.call(this, stream);
this._onHeaders(promiseHeaders);
this._responseStream = responseStream;
var response = new IncomingResponse(this._responseStream);
response.once('ready', this.emit.bind(this, 'response', response));
this.stream.on('promise', this._onPromise.bind(this));
}
IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } });
IncomingPromise.prototype.cancel = function cancel() {
this._responseStream.reset('CANCEL');
};
IncomingPromise.prototype.setPriority = function setPriority(priority) {
this._responseStream.priority(priority);
};
IncomingPromise.prototype._onPromise = OutgoingRequest.prototype._onPromise;

View File

@ -0,0 +1,53 @@
// [node-http2][homepage] is an [HTTP/2 (draft 06)][http2] implementation for [node.js][node].
//
// The core of the protocol is implemented by the [http2-protocol] module. This module provides
// two important features on top of http2-protocol:
//
// * Implementation of different negotiation schemes that can be used to start a HTTP2 connection.
// These include TLS ALPN, Upgrade and Plain TCP.
//
// * Providing an API very similar to the standard node.js [HTTPS module API][node-https]
// (which is in turn very similar to the [HTTP module API][node-http]).
//
// [homepage]: https://github.com/molnarg/node-http2
// [http2-protocol]: https://github.com/molnarg/node-http2-protocol
// [http2]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-06
// [node]: http://nodejs.org/
// [node-https]: http://nodejs.org/api/https.html
// [node-http]: http://nodejs.org/api/http.html
module.exports = require('./http');
/*
HTTP API
| ^
| |
+-------------|------------|------------------------------------------------------+
| | | Server/Agent |
| v | |
| +----------+ +----------+ |
| | Outgoing | | Incoming | |
| | req/res. | | req/res. | |
| +----------+ +----------+ |
| | ^ |
| | | |
| +---------|------------|-------------------------------------+ +----- |
| | | | Endpoint | | |
| | | | | | |
| | v | | | |
| | +-----------------------+ +-------------------- | | |
| | | Stream | | Stream ... | | |
| | +-----------------------+ +-------------------- | | |
| | | | |
| +------------------------------------------------------------+ +----- |
| | | |
| | | |
| v | |
| +------------------------------------------------------------+ +----- |
| | TCP stream | | ... |
| +------------------------------------------------------------+ +----- |
| |
+---------------------------------------------------------------------------------+
*/

View File

@ -0,0 +1,137 @@
Version history
===============
### 0.7.0 (2013-11-10) ###
* Upgrade to the latest draft: [draft-ietf-httpbis-http2-07][draft-07]
* [Tarball](https://github.com/molnarg/node-http2-protocol/archive/node-http2-protocol-0.7.0.tar.gz)
[draft-07]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-07
### 0.6.0 (2013-11-09) ###
* Splitting out node-http2-protocol from node-http2
* The only exported class is `Endpoint`
* Versioning will be based on the implemented protocol version with a '0.' prefix
* [Tarball](https://github.com/molnarg/node-http2-protocol/archive/node-http2-protocol-0.6.0.tar.gz)
Version history as part of the [node-http](https://github.com/molnarg/node-http2) module
----------------------------------------------------------------------------------------
### 1.0.1 (2013-10-14) ###
* Support for ALPN if node supports it (currently needs a custom build)
* Fix for a few small issues
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-1.0.1.tar.gz)
### 1.0.0 (2013-09-23) ###
* Exporting Endpoint class
* Support for 'filters' in Endpoint
* The last time-based release
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-1.0.0.tar.gz)
### 0.4.1 (2013-09-15) ###
* Major performance improvements
* Minor improvements to error handling
* [Blog post](http://gabor.molnar.es/blog/2013/09/15/gsoc-week-number-13/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.4.1.tar.gz)
### 0.4.0 (2013-09-09) ###
* Upgrade to the latest draft: [draft-ietf-httpbis-http2-06][draft-06]
* Support for HTTP trailers
* Support for TLS SNI (Server Name Indication)
* Improved stream scheduling algorithm
* [Blog post](http://gabor.molnar.es/blog/2013/09/09/gsoc-week-number-12/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.4.0.tar.gz)
[draft-06]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-06
### 0.3.1 (2013-09-03) ###
* Lot of testing, bugfixes
* [Blog post](http://gabor.molnar.es/blog/2013/09/03/gsoc-week-number-11/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.3.1.tar.gz)
### 0.3.0 (2013-08-27) ###
* Support for prioritization
* Small API compatibility improvements (compatibility with the standard node.js HTTP API)
* Minor push API change
* Ability to pass an external bunyan logger when creating a Server or Agent
* [Blog post](http://gabor.molnar.es/blog/2013/08/27/gsoc-week-number-10/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.3.0.tar.gz)
### 0.2.1 (2013-08-20) ###
* Fixing a flow control bug
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.2.1.tar.gz)
### 0.2.0 (2013-08-19) ###
* Exposing server push in the public API
* Connection pooling when operating as client
* Much better API compatibility with the standard node.js HTTPS module
* Logging improvements
* [Blog post](http://gabor.molnar.es/blog/2013/08/19/gsoc-week-number-9/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.2.0.tar.gz)
### 0.1.1 (2013-08-12) ###
* Lots of bugfixes
* Proper flow control for outgoing frames
* Basic flow control for incoming frames
* [Blog post](http://gabor.molnar.es/blog/2013/08/12/gsoc-week-number-8/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.1.1.tar.gz)
### 0.1.0 (2013-08-06) ###
* First release with public API (similar to the standard node HTTPS module)
* Support for NPN negotiation (no ALPN or Upgrade yet)
* Stream number limitation is in place
* Push streams works but not exposed yet in the public API
* [Blog post](http://gabor.molnar.es/blog/2013/08/05/gsoc-week-number-6-and-number-7/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.1.0.tar.gz)
### 0.0.6 (2013-07-19) ###
* `Connection` and `Endpoint` classes are usable, but not yet ready
* Addition of an exmaple server and client
* Using [istanbul](https://github.com/gotwarlost/istanbul) for measuring code coverage
* [Blog post](http://gabor.molnar.es/blog/2013/07/19/gsoc-week-number-5/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.6.tar.gz)
### 0.0.5 (2013-07-14) ###
* `Stream` class is done
* Public API stubs are in place
* [Blog post](http://gabor.molnar.es/blog/2013/07/14/gsoc-week-number-4/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.5.tar.gz)
### 0.0.4 (2013-07-08) ###
* Added logging
* Started `Stream` class implementation
* [Blog post](http://gabor.molnar.es/blog/2013/07/08/gsoc-week-number-3/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.4.tar.gz)
### 0.0.3 (2013-07-03) ###
* Header compression is ready
* [Blog post](http://gabor.molnar.es/blog/2013/07/03/the-http-slash-2-header-compression-implementation-of-node-http2/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.3.tar.gz)
### 0.0.2 (2013-07-01) ###
* Frame serialization and deserialization ready and updated to match the newest spec
* Header compression implementation started
* [Blog post](http://gabor.molnar.es/blog/2013/07/01/gsoc-week-number-2/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.2.tar.gz)
### 0.0.1 (2013-06-23) ###
* Frame serialization and deserialization largely done
* [Blog post](http://gabor.molnar.es/blog/2013/06/23/gsoc-week-number-1/)
* [Tarball](https://github.com/molnarg/node-http2/archive/node-http2-0.0.1.tar.gz)

View File

@ -0,0 +1,23 @@
The MIT License
Copyright (C) 2013 Gábor Molnár <gabor@molnar.es>
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,84 @@
node-http2-protocol
===================
An HTTP/2 ([draft-ietf-httpbis-http2-07](http://tools.ietf.org/html/draft-ietf-httpbis-http2-07))
framing layer implementaion for node.js.
Installation
------------
```
npm install http2-protocol
```
Examples
--------
API
---
Development
-----------
### Development dependencies ###
There's a few library you will need to have installed to do anything described in the following
sections. After installing/cloning node-http2, run `npm install` in its directory to install
development dependencies.
Used libraries:
* [mocha](http://visionmedia.github.io/mocha/) for tests
* [chai](http://chaijs.com/) for assertions
* [istanbul](https://github.com/gotwarlost/istanbul) for code coverage analysis
* [docco](http://jashkenas.github.io/docco/) for developer documentation
* [bunyan](https://github.com/trentm/node-bunyan) for logging
For pretty printing logs, you will also need a global install of bunyan (`npm install -g bunyan`).
### Developer documentation ###
The developer documentation is located in the `doc` directory. The docs are usually updated only
before releasing a new version. To regenerate them manually, run `npm run-script prepublish`.
There's a hosted version which is located [here](http://molnarg.github.io/node-http2/doc/).
### Running the tests ###
It's easy, just run `npm test`. The tests are written in BDD style, so they are a good starting
point to understand the code.
### Test coverage ###
To generate a code coverage report, run `npm test --coverage` (it may be slow, be patient).
Code coverage summary as of version 0.6.0:
```
Statements : 92.39% ( 1165/1261 )
Branches : 86.57% ( 477/551 )
Functions : 91.22% ( 135/148 )
Lines : 92.35% ( 1159/1255 )
```
There's a hosted version of the detailed (line-by-line) coverage report
[here](http://molnarg.github.io/node-http2-protocol/coverage/lcov-report/lib/).
### Logging ###
Contributors
------------
Code contributions are always welcome! People who contributed to node-http2 so far:
* Nick Hurley
* Mike Belshe
Special thanks to Google for financing the development of this module as part of their [Summer of
Code program](https://developers.google.com/open-source/soc/) (project: [HTTP/2 prototype server
implementation](https://google-melange.appspot.com/gsoc/project/google/gsoc2013/molnarg/5001)), and
Nick Hurley of Mozilla, my GSoC mentor, who helped with regular code review and technical advices.
License
-------
The MIT License
Copyright (C) 2013 Gábor Molnár <gabor@molnar.es>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -0,0 +1,658 @@
<!DOCTYPE html>
<html>
<head>
<title>endpoint.js</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
<link rel="stylesheet" media="all" href="docco.css" />
</head>
<body>
<div id="container">
<div id="background"></div>
<ul id="jump_to">
<li>
<a class="large" href="javascript:void(0);">Jump To &hellip;</a>
<a class="small" href="javascript:void(0);">+</a>
<div id="jump_wrapper">
<div id="jump_page">
<a class="source" href="compressor.html">
compressor.js
</a>
<a class="source" href="connection.html">
connection.js
</a>
<a class="source" href="endpoint.html">
endpoint.js
</a>
<a class="source" href="flow.html">
flow.js
</a>
<a class="source" href="framer.html">
framer.js
</a>
<a class="source" href="index.html">
index.js
</a>
<a class="source" href="stream.html">
stream.js
</a>
</div>
</li>
</ul>
<ul class="sections">
<li id="title">
<div class="annotation">
<h1>endpoint.js</h1>
</div>
</li>
<li id="section-1">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-1">&#182;</a>
</div>
</div>
<div class="content"><div class='highlight'><pre><span class="keyword">var</span> assert = require(<span class="string">'assert'</span>);
<span class="keyword">var</span> Serializer = require(<span class="string">'./framer'</span>).Serializer;
<span class="keyword">var</span> Deserializer = require(<span class="string">'./framer'</span>).Deserializer;
<span class="keyword">var</span> Compressor = require(<span class="string">'./compressor'</span>).Compressor;
<span class="keyword">var</span> Decompressor = require(<span class="string">'./compressor'</span>).Decompressor;
<span class="keyword">var</span> Connection = require(<span class="string">'./connection'</span>).Connection;
<span class="keyword">var</span> Duplex = require(<span class="string">'stream'</span>).Duplex;
<span class="keyword">var</span> Transform = require(<span class="string">'stream'</span>).Transform;
exports.Endpoint = Endpoint;</pre></div></div>
</li>
<li id="section-2">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-2">&#182;</a>
</div>
<h1 id="the-endpoint-class">The Endpoint class</h1>
</div>
</li>
<li id="section-3">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-3">&#182;</a>
</div>
<h2 id="public-api">Public API</h2>
</div>
</li>
<li id="section-4">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-4">&#182;</a>
</div>
<ul>
<li><p><strong>new Endpoint(log, role, settings, filters)</strong>: create a new Endpoint.</p>
<ul>
<li><code>log</code>: bunyan logger of the parent</li>
<li><code>role</code>: &#39;CLIENT&#39; or &#39;SERVER&#39;</li>
<li><code>settings</code>: initial HTTP/2 settings</li>
<li><p><code>filters</code>: a map of functions that filter the traffic between components (for debugging or
intentional failure injection).</p>
<p>Filter functions get three arguments:</p>
<ol>
<li><code>frame</code>: the current frame</li>
<li><code>forward(frame)</code>: function that can be used to forward a frame to the next component</li>
<li><code>done()</code>: callback to signal the end of the filter process</li>
</ol>
<p>Valid filter names and their position in the stack:</p>
<ul>
<li><code>beforeSerialization</code>: after compression, before serialization</li>
<li><code>beforeCompression</code>: after multiplexing, before compression</li>
<li><code>afterDeserialization</code>: after deserialization, before decompression</li>
<li><code>afterDecompression</code>: after decompression, before multiplexing</li>
</ul>
</li>
</ul>
</li>
<li><p><strong>Event: &#39;stream&#39; (Stream)</strong>: &#39;stream&#39; event forwarded from the underlying Connection</p>
</li>
<li><p><strong>Event: &#39;error&#39; (type)</strong>: signals an error</p>
</li>
<li><p><strong>createStream(): Stream</strong>: initiate a new stream (forwarded to the underlying Connection)</p>
</li>
<li><p><strong>close([error])</strong>: close the connection with an error code</p>
</li>
</ul>
<h2 id="constructor">Constructor</h2>
</div>
</li>
<li id="section-5">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-5">&#182;</a>
</div>
<p>The process of initialization:</p>
</div>
<div class="content"><div class='highlight'><pre><span class="function"><span class="keyword">function</span> <span class="title">Endpoint</span><span class="params">(log, role, settings, filters)</span> {</span>
Duplex.call(<span class="keyword">this</span>);</pre></div></div>
</li>
<li id="section-6">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-6">&#182;</a>
</div>
<ul>
<li>Initializing logging infrastructure</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">this</span>._log = log.child({ component: <span class="string">'endpoint'</span>, e: <span class="keyword">this</span> });</pre></div></div>
</li>
<li id="section-7">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-7">&#182;</a>
</div>
<ul>
<li>First part of the handshake process: sending and receiving the client connection header
prelude.</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> assert((role === <span class="string">'CLIENT'</span>) || role === <span class="string">'SERVER'</span>);
<span class="keyword">if</span> (role === <span class="string">'CLIENT'</span>) {
<span class="keyword">this</span>._writePrelude();
} <span class="keyword">else</span> {
<span class="keyword">this</span>._readPrelude();
}</pre></div></div>
</li>
<li id="section-8">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-8">&#182;</a>
</div>
<ul>
<li>Initialization of component. This includes the second part of the handshake process:
sending the first SETTINGS frame. This is done by the connection class right after
initialization.</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">this</span>._initializeDataFlow(role, settings, filters || {});</pre></div></div>
</li>
<li id="section-9">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-9">&#182;</a>
</div>
<ul>
<li>Initialization of management code.</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">this</span>._initializeManagement();</pre></div></div>
</li>
<li id="section-10">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-10">&#182;</a>
</div>
<ul>
<li>Initializing error handling.</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">this</span>._initializeErrorHandling();
}
Endpoint.prototype = Object.create(Duplex.prototype, { constructor: { value: Endpoint } });</pre></div></div>
</li>
<li id="section-11">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-11">&#182;</a>
</div>
<h2 id="handshake">Handshake</h2>
</div>
</li>
<li id="section-12">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-12">&#182;</a>
</div>
</div>
<div class="content"><div class='highlight'><pre><span class="keyword">var</span> CLIENT_PRELUDE = <span class="keyword">new</span> Buffer(<span class="string">'PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n'</span>);</pre></div></div>
</li>
<li id="section-13">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-13">&#182;</a>
</div>
<p>Writing the client header is simple and synchronous.</p>
</div>
<div class="content"><div class='highlight'><pre>Endpoint.prototype._writePrelude = <span class="function"><span class="keyword">function</span> <span class="title">_writePrelude</span><span class="params">()</span> {</span>
<span class="keyword">this</span>._log.debug(<span class="string">'Sending the client connection header prelude.'</span>);
<span class="keyword">this</span>.push(CLIENT_PRELUDE);
};</pre></div></div>
</li>
<li id="section-14">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-14">&#182;</a>
</div>
<p>The asynchronous process of reading the client header:</p>
</div>
<div class="content"><div class='highlight'><pre>Endpoint.prototype._readPrelude = <span class="function"><span class="keyword">function</span> <span class="title">_readPrelude</span><span class="params">()</span> {</span></pre></div></div>
</li>
<li id="section-15">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-15">&#182;</a>
</div>
<ul>
<li>progress in the header is tracker using a <code>cursor</code></li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">var</span> cursor = <span class="number">0</span>;</pre></div></div>
</li>
<li id="section-16">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-16">&#182;</a>
</div>
<ul>
<li><code>_write</code> is temporarily replaced by the comparator function</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">this</span>._write = <span class="function"><span class="keyword">function</span> <span class="title">_temporalWrite</span><span class="params">(chunk, encoding, done)</span> {</span></pre></div></div>
</li>
<li id="section-17">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-17">&#182;</a>
</div>
<ul>
<li>which compares the stored header with the current <code>chunk</code> byte by byte and emits the
&#39;error&#39; event if there&#39;s a byte that doesn&#39;t match</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">var</span> offset = cursor;
<span class="keyword">while</span>(cursor &lt; CLIENT_PRELUDE.length &amp;&amp; (cursor - offset) &lt; chunk.length) {
<span class="keyword">if</span> (CLIENT_PRELUDE[cursor] !== chunk[cursor - offset]) {
<span class="keyword">this</span>._log.fatal({ cursor: cursor, offset: offset, chunk: chunk },
<span class="string">'Client connection header prelude does not match.'</span>);
<span class="keyword">this</span>._error(<span class="string">'handshake'</span>, <span class="string">'PROTOCOL_ERROR'</span>);
<span class="keyword">return</span>;
}
cursor += <span class="number">1</span>;
}</pre></div></div>
</li>
<li id="section-18">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-18">&#182;</a>
</div>
<ul>
<li>if the whole header is over, and there were no error then restore the original <code>_write</code>
and call it with the remaining part of the current chunk</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">if</span> (cursor === CLIENT_PRELUDE.length) {
<span class="keyword">this</span>._log.debug(<span class="string">'Successfully received the client connection header prelude.'</span>);
<span class="keyword">delete</span> <span class="keyword">this</span>._write;
chunk = chunk.slice(cursor - offset);
<span class="keyword">this</span>._write(chunk, encoding, done);
}
};
};</pre></div></div>
</li>
<li id="section-19">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-19">&#182;</a>
</div>
<h2 id="data-flow">Data flow</h2>
</div>
</li>
<li id="section-20">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-20">&#182;</a>
</div>
<pre><code>+---------------------------------------------+
| |
| +-------------------------------------+ |
| | +---------+ +---------+ +---------+ | |
| | | stream1 | | stream2 | | ... | | |
| | +---------+ +---------+ +---------+ | |
| | connection | |
| +-------------------------------------+ |
| | ^ |
| pipe | | pipe |
| v | |
| +------------------+------------------+ |
| | compressor | decompressor | |
| +------------------+------------------+ |
| | ^ |
| pipe | | pipe |
| v | |
| +------------------+------------------+ |
| | serializer | deserializer | |
| +------------------+------------------+ |
| | ^ |
| _read() | | _write() |
| v | |
| +------------+ +-----------+ |
| |output queue| |input queue| |
+------+------------+-----+-----------+-------+
| ^
read() | | write()
v |</code></pre>
</div>
<div class="content"><div class='highlight'><pre><span class="function"><span class="keyword">function</span> <span class="title">createTransformStream</span><span class="params">(filter)</span> {</span>
<span class="keyword">var</span> transform = <span class="keyword">new</span> Transform({ objectMode: <span class="literal">true</span> });
<span class="keyword">var</span> push = transform.push.bind(transform);
transform._transform = <span class="function"><span class="keyword">function</span><span class="params">(frame, encoding, done)</span> {</span>
filter(frame, push, done);
};
<span class="keyword">return</span> transform;
}
<span class="function"><span class="keyword">function</span> <span class="title">pipeAndFilter</span><span class="params">(stream1, stream2, filter)</span> {</span>
<span class="keyword">if</span> (filter) {
stream1.pipe(createTransformStream(filter)).pipe(stream2);
} <span class="keyword">else</span> {
stream1.pipe(stream2);
}
}
<span class="keyword">var</span> MAX_HTTP_PAYLOAD_SIZE = <span class="number">16383</span>;
Endpoint.prototype._initializeDataFlow = <span class="function"><span class="keyword">function</span> <span class="title">_initializeDataFlow</span><span class="params">(role, settings, filters)</span> {</span>
<span class="keyword">var</span> firstStreamId, compressorRole, decompressorRole;
<span class="keyword">if</span> (role === <span class="string">'CLIENT'</span>) {
firstStreamId = <span class="number">1</span>;
compressorRole = <span class="string">'REQUEST'</span>;
decompressorRole = <span class="string">'RESPONSE'</span>;
} <span class="keyword">else</span> {
firstStreamId = <span class="number">2</span>;
compressorRole = <span class="string">'RESPONSE'</span>;
decompressorRole = <span class="string">'REQUEST'</span>;
}
<span class="keyword">this</span>._serializer = <span class="keyword">new</span> Serializer(<span class="keyword">this</span>._log, MAX_HTTP_PAYLOAD_SIZE);
<span class="keyword">this</span>._deserializer = <span class="keyword">new</span> Deserializer(<span class="keyword">this</span>._log, MAX_HTTP_PAYLOAD_SIZE);
<span class="keyword">this</span>._compressor = <span class="keyword">new</span> Compressor(<span class="keyword">this</span>._log, compressorRole);
<span class="keyword">this</span>._decompressor = <span class="keyword">new</span> Decompressor(<span class="keyword">this</span>._log, decompressorRole);
<span class="keyword">this</span>._connection = <span class="keyword">new</span> Connection(<span class="keyword">this</span>._log, firstStreamId, settings);
pipeAndFilter(<span class="keyword">this</span>._connection, <span class="keyword">this</span>._compressor, filters.beforeCompression);
pipeAndFilter(<span class="keyword">this</span>._compressor, <span class="keyword">this</span>._serializer, filters.beforeSerialization);
pipeAndFilter(<span class="keyword">this</span>._deserializer, <span class="keyword">this</span>._decompressor, filters.afterDeserialization);
pipeAndFilter(<span class="keyword">this</span>._decompressor, <span class="keyword">this</span>._connection, filters.afterDecompression);
<span class="keyword">this</span>._connection.on(<span class="string">'ACKNOWLEDGED_SETTINGS_HEADER_TABLE_SIZE'</span>,
<span class="keyword">this</span>._decompressor.setTableSizeLimit.bind(<span class="keyword">this</span>._decompressor))
<span class="keyword">this</span>._connection.on(<span class="string">'RECEIVING_SETTINGS_HEADER_TABLE_SIZE'</span>,
<span class="keyword">this</span>._compressor.setTableSizeLimit.bind(<span class="keyword">this</span>._compressor))
};
<span class="keyword">var</span> noread = {};
Endpoint.prototype._read = <span class="function"><span class="keyword">function</span> <span class="title">_read</span><span class="params">()</span> {</span>
<span class="keyword">this</span>._readableState.sync = <span class="literal">true</span>;
<span class="keyword">var</span> moreNeeded = noread, chunk;
<span class="keyword">while</span> (moreNeeded &amp;&amp; (chunk = <span class="keyword">this</span>._serializer.read())) {
moreNeeded = <span class="keyword">this</span>.push(chunk);
}
<span class="keyword">if</span> (moreNeeded === noread) {
<span class="keyword">this</span>._serializer.once(<span class="string">'readable'</span>, <span class="keyword">this</span>._read.bind(<span class="keyword">this</span>));
}
<span class="keyword">this</span>._readableState.sync = <span class="literal">false</span>;
};
Endpoint.prototype._write = <span class="function"><span class="keyword">function</span> <span class="title">_write</span><span class="params">(chunk, encoding, done)</span> {</span>
<span class="keyword">this</span>._deserializer.write(chunk, encoding, done);
};</pre></div></div>
</li>
<li id="section-21">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-21">&#182;</a>
</div>
<h2 id="management">Management</h2>
</div>
</li>
<li id="section-22">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-22">&#182;</a>
</div>
</div>
<div class="content"><div class='highlight'><pre>Endpoint.prototype._initializeManagement = <span class="function"><span class="keyword">function</span> <span class="title">_initializeManagement</span><span class="params">()</span> {</span>
<span class="keyword">this</span>._connection.on(<span class="string">'stream'</span>, <span class="keyword">this</span>.emit.bind(<span class="keyword">this</span>, <span class="string">'stream'</span>));
};
Endpoint.prototype.createStream = <span class="function"><span class="keyword">function</span> <span class="title">createStream</span><span class="params">()</span> {</span>
<span class="keyword">return</span> <span class="keyword">this</span>._connection.createStream();
};</pre></div></div>
</li>
<li id="section-23">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-23">&#182;</a>
</div>
<h2 id="error-handling">Error handling</h2>
</div>
</li>
<li id="section-24">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-24">&#182;</a>
</div>
</div>
<div class="content"><div class='highlight'><pre>Endpoint.prototype._initializeErrorHandling = <span class="function"><span class="keyword">function</span> <span class="title">_initializeErrorHandling</span><span class="params">()</span> {</span>
<span class="keyword">this</span>._serializer.on(<span class="string">'error'</span>, <span class="keyword">this</span>._error.bind(<span class="keyword">this</span>, <span class="string">'serializer'</span>));
<span class="keyword">this</span>._deserializer.on(<span class="string">'error'</span>, <span class="keyword">this</span>._error.bind(<span class="keyword">this</span>, <span class="string">'deserializer'</span>));
<span class="keyword">this</span>._compressor.on(<span class="string">'error'</span>, <span class="keyword">this</span>._error.bind(<span class="keyword">this</span>, <span class="string">'compressor'</span>));
<span class="keyword">this</span>._decompressor.on(<span class="string">'error'</span>, <span class="keyword">this</span>._error.bind(<span class="keyword">this</span>, <span class="string">'decompressor'</span>));
<span class="keyword">this</span>._connection.on(<span class="string">'error'</span>, <span class="keyword">this</span>._error.bind(<span class="keyword">this</span>, <span class="string">'connection'</span>));
<span class="keyword">this</span>._connection.on(<span class="string">'peerError'</span>, <span class="keyword">this</span>.emit.bind(<span class="keyword">this</span>, <span class="string">'peerError'</span>));
};
Endpoint.prototype._error = <span class="function"><span class="keyword">function</span> <span class="title">_error</span><span class="params">(component, error)</span> {</span>
<span class="keyword">this</span>._log.fatal({ source: component, message: error }, <span class="string">'Fatal error, closing connection'</span>);
<span class="keyword">this</span>.close(error);
setImmediate(<span class="keyword">this</span>.emit.bind(<span class="keyword">this</span>, <span class="string">'error'</span>, error));
};
Endpoint.prototype.close = <span class="function"><span class="keyword">function</span> <span class="title">close</span><span class="params">(error)</span> {</span>
<span class="keyword">this</span>._connection.close(error);
};</pre></div></div>
</li>
<li id="section-25">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-25">&#182;</a>
</div>
<h2 id="bunyan-serializers">Bunyan serializers</h2>
</div>
</li>
<li id="section-26">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-26">&#182;</a>
</div>
</div>
<div class="content"><div class='highlight'><pre>exports.serializers = {};
<span class="keyword">var</span> nextId = <span class="number">0</span>;
exports.serializers.e = <span class="function"><span class="keyword">function</span><span class="params">(endpoint)</span> {</span>
<span class="keyword">if</span> (!(<span class="string">'id'</span> <span class="keyword">in</span> endpoint)) {
endpoint.id = nextId;
nextId += <span class="number">1</span>;
}
<span class="keyword">return</span> endpoint.id;
};</pre></div></div>
</li>
</ul>
</div>
</body>
</html>

View File

@ -0,0 +1,787 @@
<!DOCTYPE html>
<html>
<head>
<title>flow.js</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
<link rel="stylesheet" media="all" href="docco.css" />
</head>
<body>
<div id="container">
<div id="background"></div>
<ul id="jump_to">
<li>
<a class="large" href="javascript:void(0);">Jump To &hellip;</a>
<a class="small" href="javascript:void(0);">+</a>
<div id="jump_wrapper">
<div id="jump_page">
<a class="source" href="compressor.html">
compressor.js
</a>
<a class="source" href="connection.html">
connection.js
</a>
<a class="source" href="endpoint.html">
endpoint.js
</a>
<a class="source" href="flow.html">
flow.js
</a>
<a class="source" href="framer.html">
framer.js
</a>
<a class="source" href="index.html">
index.js
</a>
<a class="source" href="stream.html">
stream.js
</a>
</div>
</li>
</ul>
<ul class="sections">
<li id="title">
<div class="annotation">
<h1>flow.js</h1>
</div>
</li>
<li id="section-1">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-1">&#182;</a>
</div>
</div>
<div class="content"><div class='highlight'><pre><span class="keyword">var</span> assert = require(<span class="string">'assert'</span>);</pre></div></div>
</li>
<li id="section-2">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-2">&#182;</a>
</div>
<h1 id="the-flow-class">The Flow class</h1>
</div>
</li>
<li id="section-3">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-3">&#182;</a>
</div>
<p>Flow is a <a href="http://nodejs.org/api/stream.html#stream_class_stream_duplex">Duplex stream</a> subclass which implements HTTP/2 flow control. It is designed to be
subclassed by <a href="connection.html">Connection</a> and the <code>upstream</code> component of <a href="stream.html">Stream</a>.</p>
</div>
<div class="content"><div class='highlight'><pre><span class="keyword">var</span> Duplex = require(<span class="string">'stream'</span>).Duplex;
exports.Flow = Flow;</pre></div></div>
</li>
<li id="section-4">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-4">&#182;</a>
</div>
<h2 id="public-api">Public API</h2>
</div>
</li>
<li id="section-5">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-5">&#182;</a>
</div>
<ul>
<li><p><strong>Event: &#39;error&#39; (type)</strong>: signals an error</p>
</li>
<li><p><strong>setInitialWindow(size)</strong>: the initial flow control window size can be changed <em>any time</em>
(<a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-6.9.2">as described in the standard</a>) using this method</p>
</li>
<li><p><strong>disableRemoteFlowControl()</strong>: sends a WINDOW_UPDATE signaling that we don&#39;t want flow control</p>
</li>
<li><p><strong>disableLocalFlowControl()</strong>: disables flow control for outgoing frames</p>
</li>
</ul>
<h2 id="api-for-child-classes">API for child classes</h2>
</div>
</li>
<li id="section-6">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-6">&#182;</a>
</div>
<ul>
<li><p><strong>new Flow([flowControlId])</strong>: creating a new flow that will listen for WINDOW_UPDATES frames
with the given <code>flowControlId</code> (or every update frame if not given)</p>
</li>
<li><p><strong>_send()</strong>: called when more frames should be pushed. The child class is expected to override
this (instead of the <code>_read</code> method of the Duplex class).</p>
</li>
<li><p><strong>_receive(frame, readyCallback)</strong>: called when there&#39;s an incoming frame. The child class is
expected to override this (instead of the <code>_write</code> method of the Duplex class).</p>
</li>
<li><p><strong>push(frame): bool</strong>: schedules <code>frame</code> for sending.</p>
<p>Returns <code>true</code> if it needs more frames in the output queue, <code>false</code> if the output queue is
full, and <code>null</code> if did not push the frame into the output queue (instead, it pushed it into
the flow control queue).</p>
</li>
<li><p><strong>read(limit): frame</strong>: like the regular <code>read</code>, but the &#39;flow control size&#39; (0 for non-DATA
frames, length of the payload for DATA frames) of the returned frame will be under <code>limit</code>.
Small exception: pass -1 as <code>limit</code> if the max. flow control size is 0. <code>read(0)</code> means the
same thing as <a href="http://nodejs.org/api/stream.html#stream_stream_read_0">in the original API</a>.</p>
</li>
<li><p><strong>getLastQueuedFrame(): frame</strong>: returns the last frame in output buffers</p>
</li>
<li><p><strong>_log</strong>: the Flow class uses the <code>_log</code> object of the parent</p>
</li>
</ul>
<h2 id="constructor">Constructor</h2>
</div>
</li>
<li id="section-7">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-7">&#182;</a>
</div>
<p>When a HTTP/2.0 connection is first established, new streams are created with an initial flow
control window size of 65535 bytes.</p>
</div>
<div class="content"><div class='highlight'><pre><span class="keyword">var</span> INITIAL_WINDOW_SIZE = <span class="number">65535</span>;</pre></div></div>
</li>
<li id="section-8">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-8">&#182;</a>
</div>
<p><code>flowControlId</code> is needed if only specific WINDOW_UPDATEs should be watched.</p>
</div>
<div class="content"><div class='highlight'><pre><span class="function"><span class="keyword">function</span> <span class="title">Flow</span><span class="params">(flowControlId)</span> {</span>
Duplex.call(<span class="keyword">this</span>, { objectMode: <span class="literal">true</span> });
<span class="keyword">this</span>._window = <span class="keyword">this</span>._initialWindow = INITIAL_WINDOW_SIZE;
<span class="keyword">this</span>._flowControlId = flowControlId;
<span class="keyword">this</span>._queue = [];
<span class="keyword">this</span>._ended = <span class="literal">false</span>;
<span class="keyword">this</span>._received = <span class="number">0</span>;
<span class="keyword">this</span>._remoteFlowControlDisabled = <span class="literal">false</span>;
}
Flow.prototype = Object.create(Duplex.prototype, { constructor: { value: Flow } });</pre></div></div>
</li>
<li id="section-9">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-9">&#182;</a>
</div>
<h2 id="incoming-frames">Incoming frames</h2>
</div>
</li>
<li id="section-10">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-10">&#182;</a>
</div>
<p><code>_receive</code> is called when there&#39;s an incoming frame.</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype._receive = <span class="function"><span class="keyword">function</span> <span class="title">_receive</span><span class="params">(frame, callback)</span> {</span>
<span class="keyword">throw</span> <span class="keyword">new</span> Error(<span class="string">'The _receive(frame, callback) method has to be overridden by the child class!'</span>);
};</pre></div></div>
</li>
<li id="section-11">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-11">&#182;</a>
</div>
<p><code>_receive</code> is called by <code>_write</code> which in turn is <a href="http://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1">called by Duplex</a> when someone <code>write()</code>s
to the flow. It emits the &#39;receiving&#39; event and notifies the window size tracking code if the
incoming frame is a WINDOW_UPDATE.</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype._write = <span class="function"><span class="keyword">function</span> <span class="title">_write</span><span class="params">(frame, encoding, callback)</span> {</span>
<span class="keyword">if</span> (frame.flags.END_STREAM || (frame.type === <span class="string">'RST_STREAM'</span>)) {
<span class="keyword">this</span>._ended = <span class="literal">true</span>;
}
<span class="keyword">if</span> ((frame.type === <span class="string">'DATA'</span>) &amp;&amp; (frame.data.length &gt; <span class="number">0</span>) &amp;&amp; !<span class="keyword">this</span>._remoteFlowControlDisabled) {
<span class="keyword">this</span>._receive(frame, <span class="function"><span class="keyword">function</span><span class="params">()</span> {</span>
<span class="keyword">this</span>._received += frame.data.length;
<span class="keyword">if</span> (!<span class="keyword">this</span>._restoreWindowTimer) {
<span class="keyword">this</span>._restoreWindowTimer = setImmediate(<span class="keyword">this</span>._restoreWindow.bind(<span class="keyword">this</span>));
}
callback();
}.bind(<span class="keyword">this</span>));
}
<span class="keyword">else</span> {
<span class="keyword">this</span>._receive(frame, callback);
}
<span class="keyword">if</span> ((frame.type === <span class="string">'WINDOW_UPDATE'</span>) &amp;&amp;
((<span class="keyword">this</span>._flowControlId === <span class="literal">undefined</span>) || (frame.stream === <span class="keyword">this</span>._flowControlId))) {
<span class="keyword">this</span>._updateWindow(frame);
}
};</pre></div></div>
</li>
<li id="section-12">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-12">&#182;</a>
</div>
<p><code>_restoreWindow</code> basically acknowledges the DATA frames received since it&#39;s last call. It sends
a WINDOW_UPDATE that restores the flow control window of the remote end.</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype._restoreWindow = <span class="function"><span class="keyword">function</span> <span class="title">_restoreWindow</span><span class="params">()</span> {</span>
<span class="keyword">delete</span> <span class="keyword">this</span>._restoreWindowTimer;
<span class="keyword">if</span> (!<span class="keyword">this</span>._ended &amp;&amp; !<span class="keyword">this</span>._remoteFlowControlDisabled &amp;&amp; (<span class="keyword">this</span>._received &gt; <span class="number">0</span>)) {
<span class="keyword">this</span>.push({
type: <span class="string">'WINDOW_UPDATE'</span>,
flags: {},
stream: <span class="keyword">this</span>._flowControlId,
window_size: <span class="keyword">this</span>._received
});
<span class="keyword">this</span>._received = <span class="number">0</span>;
}
};</pre></div></div>
</li>
<li id="section-13">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-13">&#182;</a>
</div>
<p>Must be called after sending a SETTINGS frame that turns off flow control on the remote side.</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype.disableRemoteFlowControl = <span class="function"><span class="keyword">function</span> <span class="title">disableRemoteFlowControl</span><span class="params">()</span> {</span>
<span class="keyword">this</span>._log.debug(<span class="string">'Turning off remote flow control'</span>);
<span class="keyword">this</span>._remoteFlowControlDisabled = <span class="literal">true</span>;
};</pre></div></div>
</li>
<li id="section-14">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-14">&#182;</a>
</div>
<h2 id="outgoing-frames-sending-procedure">Outgoing frames - sending procedure</h2>
</div>
</li>
<li id="section-15">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-15">&#182;</a>
</div>
<pre><code> flow
+-------------------------------------------------+
| |
+--------+ +---------+ |
read() | output | _read() | flow | _send() |
&lt;----------| |&lt;----------| control |&lt;------------- |
| buffer | | buffer | |
+--------+ +---------+ |
| input | |
----------&gt;| |-----------------------------------&gt; |
write() | buffer | _write() _receive() |
+--------+ |
| |
+-------------------------------------------------+</code></pre>
<p><code>_send</code> is called when more frames should be pushed to the output buffer.</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype._send = <span class="function"><span class="keyword">function</span> <span class="title">_send</span><span class="params">()</span> {</span>
<span class="keyword">throw</span> <span class="keyword">new</span> Error(<span class="string">'The _send() method has to be overridden by the child class!'</span>);
};</pre></div></div>
</li>
<li id="section-16">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-16">&#182;</a>
</div>
<p><code>_send</code> is called by <code>_read</code> which is in turn <a href="http://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1">called by Duplex</a> when it wants to have more
items in the output queue.</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype._read = <span class="function"><span class="keyword">function</span> <span class="title">_read</span><span class="params">()</span> {</span></pre></div></div>
</li>
<li id="section-17">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-17">&#182;</a>
</div>
<ul>
<li>if the flow control queue is empty, then let the user push more frames</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">if</span> (<span class="keyword">this</span>._queue.length === <span class="number">0</span>) {
<span class="keyword">this</span>._send();
}</pre></div></div>
</li>
<li id="section-18">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-18">&#182;</a>
</div>
<ul>
<li>if there are items in the flow control queue, then let&#39;s put them into the output queue (to
the extent it is possible with respect to the window size and output queue feedback)</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">else</span> <span class="keyword">if</span> (<span class="keyword">this</span>._window &gt; <span class="number">0</span>) {
<span class="keyword">this</span>._readableState.sync = <span class="literal">true</span>; <span class="comment">// to avoid reentrant calls</span>
<span class="keyword">do</span> {
<span class="keyword">var</span> moreNeeded = <span class="keyword">this</span>._push(<span class="keyword">this</span>._queue[<span class="number">0</span>]);
<span class="keyword">if</span> (moreNeeded !== <span class="literal">null</span>) {
<span class="keyword">this</span>._queue.shift();
}
} <span class="keyword">while</span> (moreNeeded &amp;&amp; (<span class="keyword">this</span>._queue.length &gt; <span class="number">0</span>));
<span class="keyword">this</span>._readableState.sync = <span class="literal">false</span>;
assert((moreNeeded == <span class="literal">false</span>) || <span class="comment">// * output queue is full</span>
(<span class="keyword">this</span>._queue.length === <span class="number">0</span>) || <span class="comment">// * flow control queue is empty</span>
(!<span class="keyword">this</span>._window &amp;&amp; (<span class="keyword">this</span>._queue[<span class="number">0</span>].type === <span class="string">'DATA'</span>))); <span class="comment">// * waiting for window update</span>
}</pre></div></div>
</li>
<li id="section-19">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-19">&#182;</a>
</div>
<ul>
<li>otherwise, come back when the flow control window is positive</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">else</span> {
<span class="keyword">this</span>.once(<span class="string">'window_update'</span>, <span class="keyword">this</span>._read);
}
};
<span class="keyword">var</span> MAX_PAYLOAD_SIZE = <span class="number">4096</span>; <span class="comment">// Must not be greater than MAX_HTTP_PAYLOAD_SIZE which is 16383</span></pre></div></div>
</li>
<li id="section-20">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-20">&#182;</a>
</div>
<p><code>read(limit)</code> is like the <code>read</code> of the Readable class, but it guarantess that the &#39;flow control
size&#39; (0 for non-DATA frames, length of the payload for DATA frames) of the returned frame will
be under <code>limit</code>.</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype.read = <span class="function"><span class="keyword">function</span> <span class="title">read</span><span class="params">(limit)</span> {</span>
<span class="keyword">if</span> (limit === <span class="number">0</span>) {
<span class="keyword">return</span> Duplex.prototype.read.call(<span class="keyword">this</span>, <span class="number">0</span>);
} <span class="keyword">else</span> <span class="keyword">if</span> (limit === -<span class="number">1</span>) {
limit = <span class="number">0</span>;
} <span class="keyword">else</span> <span class="keyword">if</span> ((limit === <span class="literal">undefined</span>) || (limit &gt; MAX_PAYLOAD_SIZE)) {
limit = MAX_PAYLOAD_SIZE;
}</pre></div></div>
</li>
<li id="section-21">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-21">&#182;</a>
</div>
<ul>
<li>Looking at the first frame in the queue without pulling it out if possible. This will save
a costly unshift if the frame proves to be too large to return.</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre> <span class="keyword">var</span> firstInQueue = <span class="keyword">this</span>._readableState.buffer[<span class="number">0</span>];
<span class="keyword">var</span> frame = firstInQueue || Duplex.prototype.read.call(<span class="keyword">this</span>);
<span class="keyword">if</span> ((frame === <span class="literal">null</span>) || (frame.type !== <span class="string">'DATA'</span>) || (frame.data.length &lt;= limit)) {
<span class="keyword">if</span> (firstInQueue) {
Duplex.prototype.read.call(<span class="keyword">this</span>);
}
<span class="keyword">return</span> frame;
}
<span class="keyword">else</span> <span class="keyword">if</span> (limit &lt;= <span class="number">0</span>) {
<span class="keyword">if</span> (!firstInQueue) {
<span class="keyword">this</span>.unshift(frame);
}
<span class="keyword">return</span> <span class="literal">null</span>;
}
<span class="keyword">else</span> {
<span class="keyword">this</span>._log.trace({ frame: frame, size: frame.data.length, forwardable: limit },
<span class="string">'Splitting out forwardable part of a DATA frame.'</span>);
<span class="keyword">var</span> forwardable = {
type: <span class="string">'DATA'</span>,
flags: {},
stream: frame.stream,
data: frame.data.slice(<span class="number">0</span>, limit)
};
frame.data = frame.data.slice(limit);
<span class="keyword">if</span> (!firstInQueue) {
<span class="keyword">this</span>.unshift(frame);
}
<span class="keyword">return</span> forwardable;
}
};</pre></div></div>
</li>
<li id="section-22">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-22">&#182;</a>
</div>
<p><code>_parentPush</code> pushes the given <code>frame</code> into the output queue</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype._parentPush = <span class="function"><span class="keyword">function</span> <span class="title">_parentPush</span><span class="params">(frame)</span> {</span>
<span class="keyword">this</span>._log.trace({ frame: frame }, <span class="string">'Pushing frame into the output queue'</span>);
<span class="keyword">if</span> (frame &amp;&amp; (frame.type === <span class="string">'DATA'</span>) &amp;&amp; (<span class="keyword">this</span>._window !== <span class="literal">Infinity</span>)) {
<span class="keyword">this</span>._log.trace({ window: <span class="keyword">this</span>._window, by: frame.data.length },
<span class="string">'Decreasing flow control window size.'</span>);
<span class="keyword">this</span>._window -= frame.data.length;
assert(<span class="keyword">this</span>._window &gt;= <span class="number">0</span>);
}
<span class="keyword">return</span> Duplex.prototype.push.call(<span class="keyword">this</span>, frame);
};</pre></div></div>
</li>
<li id="section-23">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-23">&#182;</a>
</div>
<p><code>_push(frame)</code> pushes <code>frame</code> into the output queue and decreases the flow control window size.
It is capable of splitting DATA frames into smaller parts, if the window size is not enough to
push the whole frame. The return value is similar to <code>push</code> except that it returns <code>null</code> if it
did not push the whole frame to the output queue (but maybe it did push part of the frame).</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype._push = <span class="function"><span class="keyword">function</span> <span class="title">_push</span><span class="params">(frame)</span> {</span>
<span class="keyword">var</span> data = frame &amp;&amp; (frame.type === <span class="string">'DATA'</span>) &amp;&amp; frame.data;
<span class="keyword">if</span> (!data || (data.length &lt;= <span class="keyword">this</span>._window)) {
<span class="keyword">return</span> <span class="keyword">this</span>._parentPush(frame);
}
<span class="keyword">else</span> <span class="keyword">if</span> (<span class="keyword">this</span>._window &lt;= <span class="number">0</span>) {
<span class="keyword">return</span> <span class="literal">null</span>;
}
<span class="keyword">else</span> {
<span class="keyword">this</span>._log.trace({ frame: frame, size: frame.data.length, forwardable: <span class="keyword">this</span>._window },
<span class="string">'Splitting out forwardable part of a DATA frame.'</span>);
frame.data = data.slice(<span class="keyword">this</span>._window);
<span class="keyword">this</span>._parentPush({
type: <span class="string">'DATA'</span>,
flags: {},
stream: frame.stream,
data: data.slice(<span class="number">0</span>, <span class="keyword">this</span>._window)
});
<span class="keyword">return</span> <span class="literal">null</span>;
}
};</pre></div></div>
</li>
<li id="section-24">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-24">&#182;</a>
</div>
<p>Push <code>frame</code> into the flow control queue, or if it&#39;s empty, then directly into the output queue</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype.push = <span class="function"><span class="keyword">function</span> <span class="title">push</span><span class="params">(frame)</span> {</span>
<span class="keyword">if</span> (frame === <span class="literal">null</span>) {
<span class="keyword">this</span>._log.debug(<span class="string">'Enqueueing outgoing End Of Stream'</span>);
} <span class="keyword">else</span> {
<span class="keyword">this</span>._log.debug({ frame: frame }, <span class="string">'Enqueueing outgoing frame'</span>);
}
<span class="keyword">var</span> moreNeeded = <span class="literal">null</span>;
<span class="keyword">if</span> (<span class="keyword">this</span>._queue.length === <span class="number">0</span>) {
moreNeeded = <span class="keyword">this</span>._push(frame);
}
<span class="keyword">if</span> (moreNeeded === <span class="literal">null</span>) {
<span class="keyword">this</span>._queue.push(frame);
}
<span class="keyword">return</span> moreNeeded;
};</pre></div></div>
</li>
<li id="section-25">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-25">&#182;</a>
</div>
<p><code>getLastQueuedFrame</code> returns the last frame in output buffers. This is primarily used by the
<a href="stream.html">Stream</a> class to mark the last frame with END_STREAM flag.</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype.getLastQueuedFrame = <span class="function"><span class="keyword">function</span> <span class="title">getLastQueuedFrame</span><span class="params">()</span> {</span>
<span class="keyword">var</span> readableQueue = <span class="keyword">this</span>._readableState.buffer;
<span class="keyword">return</span> <span class="keyword">this</span>._queue[<span class="keyword">this</span>._queue.length - <span class="number">1</span>] || readableQueue[readableQueue.length - <span class="number">1</span>];
};</pre></div></div>
</li>
<li id="section-26">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-26">&#182;</a>
</div>
<h2 id="outgoing-frames-managing-the-window-size">Outgoing frames - managing the window size</h2>
</div>
</li>
<li id="section-27">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-27">&#182;</a>
</div>
<p>Flow control window size is manipulated using the <code>_increaseWindow</code> method.</p>
<ul>
<li>Invoking it with <code>Infinite</code> means turning off flow control. Flow control cannot be enabled
again once disabled. Any attempt to re-enable flow control MUST be rejected with a
FLOW_CONTROL_ERROR error code.</li>
<li>A sender MUST NOT allow a flow control window to exceed 2^31 - 1 bytes. The action taken
depends on it being a stream or the connection itself.</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre><span class="keyword">var</span> WINDOW_SIZE_LIMIT = Math.pow(<span class="number">2</span>, <span class="number">31</span>) - <span class="number">1</span>;
Flow.prototype._increaseWindow = <span class="function"><span class="keyword">function</span> <span class="title">_increaseWindow</span><span class="params">(size)</span> {</span>
<span class="keyword">if</span> ((<span class="keyword">this</span>._window === <span class="literal">Infinity</span>) &amp;&amp; (size !== <span class="literal">Infinity</span>)) {
<span class="keyword">this</span>._log.error(<span class="string">'Trying to increase flow control window after flow control was turned off.'</span>);
<span class="keyword">this</span>.emit(<span class="string">'error'</span>, <span class="string">'FLOW_CONTROL_ERROR'</span>);
} <span class="keyword">else</span> {
<span class="keyword">this</span>._log.trace({ window: <span class="keyword">this</span>._window, by: size }, <span class="string">'Increasing flow control window size.'</span>);
<span class="keyword">this</span>._window += size;
<span class="keyword">if</span> ((<span class="keyword">this</span>._window !== <span class="literal">Infinity</span>) &amp;&amp; (<span class="keyword">this</span>._window &gt; WINDOW_SIZE_LIMIT)) {
<span class="keyword">this</span>._log.error(<span class="string">'Flow control window grew too large.'</span>);
<span class="keyword">this</span>.emit(<span class="string">'error'</span>, <span class="string">'FLOW_CONTROL_ERROR'</span>);
} <span class="keyword">else</span> {
<span class="keyword">this</span>.emit(<span class="string">'window_update'</span>);
}
}
};</pre></div></div>
</li>
<li id="section-28">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-28">&#182;</a>
</div>
<p>The <code>_updateWindow</code> method gets called every time there&#39;s an incoming WINDOW_UPDATE frame. It
modifies the flow control window:</p>
<ul>
<li>Flow control can be disabled for an individual stream by sending a WINDOW_UPDATE with the
END_FLOW_CONTROL flag set. The payload of a WINDOW_UPDATE frame that has the END_FLOW_CONTROL
flag set is ignored.</li>
<li>A sender that receives a WINDOW_UPDATE frame updates the corresponding window by the amount
specified in the frame.</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype._updateWindow = <span class="function"><span class="keyword">function</span> <span class="title">_updateWindow</span><span class="params">(frame)</span> {</span>
<span class="keyword">this</span>._increaseWindow(frame.flags.END_FLOW_CONTROL ? <span class="literal">Infinity</span> : frame.window_size);
};</pre></div></div>
</li>
<li id="section-29">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-29">&#182;</a>
</div>
<p>A SETTINGS frame can alter the initial flow control window size for all current streams. When the
value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the size of all stream by
calling the <code>setInitialWindow</code> method. The window size has to be modified by the difference
between the new value and the old value.</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype.setInitialWindow = <span class="function"><span class="keyword">function</span> <span class="title">setInitialWindow</span><span class="params">(initialWindow)</span> {</span>
<span class="keyword">this</span>._increaseWindow(initialWindow - <span class="keyword">this</span>._initialWindow);
<span class="keyword">this</span>._initialWindow = initialWindow;
};</pre></div></div>
</li>
<li id="section-30">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-30">&#182;</a>
</div>
<p>Flow control for outgoing frames can be disabled by the peer with various methods.</p>
</div>
<div class="content"><div class='highlight'><pre>Flow.prototype.disableLocalFlowControl = <span class="function"><span class="keyword">function</span> <span class="title">disableLocalFlowControl</span><span class="params">()</span> {</span>
<span class="keyword">this</span>._increaseWindow(<span class="literal">Infinity</span>);
};</pre></div></div>
</li>
</ul>
</div>
</body>
</html>

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,164 @@
<!DOCTYPE html>
<html>
<head>
<title>index.js</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;">
<link rel="stylesheet" media="all" href="docco.css" />
</head>
<body>
<div id="container">
<div id="background"></div>
<ul id="jump_to">
<li>
<a class="large" href="javascript:void(0);">Jump To &hellip;</a>
<a class="small" href="javascript:void(0);">+</a>
<div id="jump_wrapper">
<div id="jump_page">
<a class="source" href="compressor.html">
compressor.js
</a>
<a class="source" href="connection.html">
connection.js
</a>
<a class="source" href="endpoint.html">
endpoint.js
</a>
<a class="source" href="flow.html">
flow.js
</a>
<a class="source" href="framer.html">
framer.js
</a>
<a class="source" href="index.html">
index.js
</a>
<a class="source" href="stream.html">
stream.js
</a>
</div>
</li>
</ul>
<ul class="sections">
<li id="title">
<div class="annotation">
<h1>index.js</h1>
</div>
</li>
<li id="section-1">
<div class="annotation">
<div class="pilwrap ">
<a class="pilcrow" href="#section-1">&#182;</a>
</div>
<p><a href="https://github.com/molnarg/node-http2">node-http2-protocol</a> is an implementation of the <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-06">HTTP/2 (draft 06)</a>
framing layer for <a href="http://nodejs.org/">node.js</a>.</p>
<p>The main building blocks are <a href="http://nodejs.org/api/stream.html">node.js streams</a> that are connected through pipes.</p>
<p>The main components are:</p>
<ul>
<li><p><a href="endpoint.html">Endpoint</a>: represents an HTTP/2 endpoint (client or server). It&#39;s
responsible for the the first part of the handshake process (sending/receiving the
<a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-06#section-3.5">connection header</a>) and manages other components (framer, compressor,
connection, streams) that make up a client or server.</p>
</li>
<li><p><a href="connection.html">Connection</a>: multiplexes the active HTTP/2 streams, manages connection
lifecycle and settings, and responsible for enforcing the connection level limits (flow
control, initiated stream limit)</p>
</li>
<li><p><a href="stream.html">Stream</a>: implementation of the <a href="http2-stream">HTTP/2 stream concept</a>.
Implements the <a href="http://tools.ietf.org/html/draft-ietf-httpbis-http2-06#section-5.1">stream state machine</a> defined by the standard, provides
management methods and events for using the stream (sending/receiving headers, data, etc.),
and enforces stream level constraints (flow control, sending only legal frames).</p>
</li>
<li><p><a href="flow.html">Flow</a>: implements flow control for Connection and Stream as parent class.</p>
</li>
<li><p><a href="compressor.html">Compressor and Decompressor</a>: compression and decompression of HEADER and
PUSH_PROMISE frames</p>
</li>
<li><p><a href="framer.html">Serializer and Deserializer</a>: the lowest layer in the stack that transforms
between the binary and the JavaScript object representation of HTTP/2 frames</p>
</li>
</ul>
</div>
<div class="content"><div class='highlight'><pre>exports.Endpoint = require(<span class="string">'./endpoint'</span>).Endpoint;
<span class="comment">/* Bunyan serializers exported by submodules that are worth adding when creating a logger. */</span>
exports.serializers = {};
<span class="keyword">var</span> modules = [<span class="string">'./framer'</span>, <span class="string">'./compressor'</span>, <span class="string">'./flow'</span>, <span class="string">'./connection'</span>, <span class="string">'./stream'</span>, <span class="string">'./endpoint'</span>];
modules.map(require).forEach(<span class="function"><span class="keyword">function</span><span class="params">(module)</span> {</span>
<span class="keyword">for</span> (<span class="keyword">var</span> name <span class="keyword">in</span> module.serializers) {
exports.serializers[name] = module.serializers[name];
}
});
<span class="comment">/*
Stream API Endpoint API
Stream data
| ^ | ^
| | | |
| | | |
+-----------|------------|---------------------------------------+
| | | Endpoint |
| | | |
| +-------|------------|-----------------------------------+ |
| | | | Connection | |
| | v | | |
| | +-----------------------+ +-------------------- | |
| | | Stream | | Stream ... | |
| | +-----------------------+ +-------------------- | |
| | | ^ | ^ | |
| | v | v | | |
| | +------------+--+--------+--+------------+- ... | |
| | | ^ | |
| | | | | |
| +-----------------------|--------|-----------------------+ |
| | | |
| v | |
| +--------------------------+ +--------------------------+ |
| | Compressor | | Decompressor | |
| +--------------------------+ +--------------------------+ |
| | ^ |
| v | |
| +--------------------------+ +--------------------------+ |
| | Serializer | | Deserializer | |
| +--------------------------+ +--------------------------+ |
| | ^ |
+---------------------------|--------|---------------------------+
| |
v |
Raw data
*/</span></pre></div></div>
</li>
</ul>
</div>
</body>
</html>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,375 @@
/*! normalize.css v2.0.1 | MIT License | git.io/normalize */
/* ==========================================================================
HTML5 display definitions
========================================================================== */
/*
* Corrects `block` display not defined in IE 8/9.
*/
article,
aside,
details,
figcaption,
figure,
footer,
header,
hgroup,
nav,
section,
summary {
display: block;
}
/*
* Corrects `inline-block` display not defined in IE 8/9.
*/
audio,
canvas,
video {
display: inline-block;
}
/*
* Prevents modern browsers from displaying `audio` without controls.
* Remove excess height in iOS 5 devices.
*/
audio:not([controls]) {
display: none;
height: 0;
}
/*
* Addresses styling for `hidden` attribute not present in IE 8/9.
*/
[hidden] {
display: none;
}
/* ==========================================================================
Base
========================================================================== */
/*
* 1. Sets default font family to sans-serif.
* 2. Prevents iOS text size adjust after orientation change, without disabling
* user zoom.
*/
html {
font-family: sans-serif; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
-ms-text-size-adjust: 100%; /* 2 */
}
/*
* Removes default margin.
*/
body {
margin: 0;
}
/* ==========================================================================
Links
========================================================================== */
/*
* Addresses `outline` inconsistency between Chrome and other browsers.
*/
a:focus {
outline: thin dotted;
}
/*
* Improves readability when focused and also mouse hovered in all browsers.
*/
a:active,
a:hover {
outline: 0;
}
/* ==========================================================================
Typography
========================================================================== */
/*
* Addresses `h1` font sizes within `section` and `article` in Firefox 4+,
* Safari 5, and Chrome.
*/
h1 {
font-size: 2em;
}
/*
* Addresses styling not present in IE 8/9, Safari 5, and Chrome.
*/
abbr[title] {
border-bottom: 1px dotted;
}
/*
* Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome.
*/
b,
strong {
font-weight: bold;
}
/*
* Addresses styling not present in Safari 5 and Chrome.
*/
dfn {
font-style: italic;
}
/*
* Addresses styling not present in IE 8/9.
*/
mark {
background: #ff0;
color: #000;
}
/*
* Corrects font family set oddly in Safari 5 and Chrome.
*/
code,
kbd,
pre,
samp {
font-family: monospace, serif;
font-size: 1em;
}
/*
* Improves readability of pre-formatted text in all browsers.
*/
pre {
white-space: pre;
white-space: pre-wrap;
word-wrap: break-word;
}
/*
* Sets consistent quote types.
*/
q {
quotes: "\201C" "\201D" "\2018" "\2019";
}
/*
* Addresses inconsistent and variable font size in all browsers.
*/
small {
font-size: 80%;
}
/*
* Prevents `sub` and `sup` affecting `line-height` in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sup {
top: -0.5em;
}
sub {
bottom: -0.25em;
}
/* ==========================================================================
Embedded content
========================================================================== */
/*
* Removes border when inside `a` element in IE 8/9.
*/
img {
border: 0;
}
/*
* Corrects overflow displayed oddly in IE 9.
*/
svg:not(:root) {
overflow: hidden;
}
/* ==========================================================================
Figures
========================================================================== */
/*
* Addresses margin not present in IE 8/9 and Safari 5.
*/
figure {
margin: 0;
}
/* ==========================================================================
Forms
========================================================================== */
/*
* Define consistent border, margin, and padding.
*/
fieldset {
border: 1px solid #c0c0c0;
margin: 0 2px;
padding: 0.35em 0.625em 0.75em;
}
/*
* 1. Corrects color not being inherited in IE 8/9.
* 2. Remove padding so people aren't caught out if they zero out fieldsets.
*/
legend {
border: 0; /* 1 */
padding: 0; /* 2 */
}
/*
* 1. Corrects font family not being inherited in all browsers.
* 2. Corrects font size not being inherited in all browsers.
* 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome
*/
button,
input,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 2 */
margin: 0; /* 3 */
}
/*
* Addresses Firefox 4+ setting `line-height` on `input` using `!important` in
* the UA stylesheet.
*/
button,
input {
line-height: normal;
}
/*
* 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`
* and `video` controls.
* 2. Corrects inability to style clickable `input` types in iOS.
* 3. Improves usability and consistency of cursor style between image-type
* `input` and others.
*/
button,
html input[type="button"], /* 1 */
input[type="reset"],
input[type="submit"] {
-webkit-appearance: button; /* 2 */
cursor: pointer; /* 3 */
}
/*
* Re-set default cursor for disabled elements.
*/
button[disabled],
input[disabled] {
cursor: default;
}
/*
* 1. Addresses box sizing set to `content-box` in IE 8/9.
* 2. Removes excess padding in IE 8/9.
*/
input[type="checkbox"],
input[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/*
* 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome.
* 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome
* (include `-moz` to future-proof).
*/
input[type="search"] {
-webkit-appearance: textfield; /* 1 */
-moz-box-sizing: content-box;
-webkit-box-sizing: content-box; /* 2 */
box-sizing: content-box;
}
/*
* Removes inner padding and search cancel button in Safari 5 and Chrome
* on OS X.
*/
input[type="search"]::-webkit-search-cancel-button,
input[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
* Removes inner padding and border in Firefox 4+.
*/
button::-moz-focus-inner,
input::-moz-focus-inner {
border: 0;
padding: 0;
}
/*
* 1. Removes default vertical scrollbar in IE 8/9.
* 2. Improves readability and alignment in all browsers.
*/
textarea {
overflow: auto; /* 1 */
vertical-align: top; /* 2 */
}
/* ==========================================================================
Tables
========================================================================== */
/*
* Remove most spacing between table cells.
*/
table {
border-collapse: collapse;
border-spacing: 0;
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,42 @@
var fs = require('fs');
var path = require('path');
var http2 = require('..');
http2.globalAgent = new http2.Agent({
log: require('../test/util').createLogger('client')
});
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
// Sending the request
// It would be `var request = http2.get(process.argv.pop());` if we wouldn't care about plain mode
var options = require('url').parse(process.argv.pop());
options.plain = Boolean(process.env.HTTP2_PLAIN);
var request = http2.request(options);
request.end();
// Receiving the response
request.on('response', function(response) {
response.pipe(process.stdout);
response.on('end', finish);
});
// Receiving push streams
request.on('push', function(pushRequest) {
var filename = path.join(__dirname, '/push-' + push_count);
push_count += 1;
console.error('Receiving pushed resource: ' + pushRequest.url + ' -> ' + filename);
pushRequest.on('response', function(pushResponse) {
pushResponse.pipe(fs.createWriteStream(filename)).on('finish', finish);
});
});
// Quitting after both the response and the associated pushed resources have arrived
var push_count = 0;
var finished = 0;
function finish() {
finished += 1;
if (finished === (1 + push_count)) {
process.exit();
}
}

View File

@ -0,0 +1,14 @@
-----BEGIN CERTIFICATE-----
MIICIzCCAYwCCQCsvG34Az33qTANBgkqhkiG9w0BAQUFADBWMQswCQYDVQQGEwJY
WDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBh
bnkgTHRkMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTMwODAyMTMwODQzWhcNMTMw
OTAxMTMwODQzWjBWMQswCQYDVQQGEwJYWDEVMBMGA1UEBwwMRGVmYXVsdCBDaXR5
MRwwGgYDVQQKDBNEZWZhdWx0IENvbXBhbnkgTHRkMRIwEAYDVQQDDAlsb2NhbGhv
c3QwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAM8D4tgE1cdI9uLo4N9AL8Ck
ogREH5LSm3SsRGFdUu5b2Nx63K/qwtTUbtUlISZBI+KESkwQXcf1ErwXUDnbTtk/
VpLJ+gfIN18e9LAdiZgAMEWlitiLhR+D17w4NzHYOpWy1YzgOckukPy1ZfTH9e7j
tEH9+7c4mpv7QMkFdw4hAgMBAAEwDQYJKoZIhvcNAQEFBQADgYEAP+ZFskjJtNxY
c+5JfMjEgSHEIy+AJ5/vXIspNYKMb7l0gYDvmFm8QTKChKTYvJmepBrIdL7MjXCX
SWiPz05ch99c84yOx5qVpcPd0y2fjO8xn2NCLfWdP7iSVYmpftwzjqFzPc4EkAny
NOpbnw9iM4JXsZNFtPTvSp+8StPGWzU=
-----END CERTIFICATE-----

View File

@ -0,0 +1,15 @@
-----BEGIN RSA PRIVATE KEY-----
MIICXAIBAAKBgQDPA+LYBNXHSPbi6ODfQC/ApKIERB+S0pt0rERhXVLuW9jcetyv
6sLU1G7VJSEmQSPihEpMEF3H9RK8F1A5207ZP1aSyfoHyDdfHvSwHYmYADBFpYrY
i4Ufg9e8ODcx2DqVstWM4DnJLpD8tWX0x/Xu47RB/fu3OJqb+0DJBXcOIQIDAQAB
AoGAHtRVVBZkP+l92w0TcCv+8JGUD06V5Se4Pwfopxde4mCLS0qA0zIDEe8REm0V
Ir1Quss4xVsqnDzDLX/LUtJ2S1+seWcoLdDV/wSDiM2CLS7KauUazrTWHLNId/lu
/VombYWK10uNiDZZJ8xwEaKt+ZptC2kK8/yi0aX0PrGhAIECQQDsD8A64BBrWCrb
7PrJt04CAcM3uBUzS6ausiJKw9IEktnvcnsN9kZazcAW86WDFsXI5oPubmgHhQ/s
m9iIrbMPAkEA4IAUWi5mVuWAyUIc9YbjJdnmvkAykSxr/vp/26RMSDmUAAUlYNNc
HZbM1uVZsFForKza28Px01Ga728ZdhRrzwJBAIrwNlcwu9lCWm95Cp6hGfPKb8ki
uq+nTiKyS8avfLQebtElE1JDamNViEK6AuemBqFZM7upFeefJKFBlO/VNHcCQCXN
CyBALdU14aCBtFSXOMoXzaV9M8aD/084qKy4FmwW3de/BhMuo5UL3kPU7Gwm2QQy
OsvES4S0ee0U/OmH+LsCQAnNdxNPgzJDTx7wOTFhHIBr4mtepLiaRXIdkLEsR9Kb
vcK6BwUfomM29eGOXtUAU7sJ5xnyKkSuNN7fxIWjzPI=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,49 @@
var fs = require('fs');
var path = require('path');
var http2 = require('..');
var options = process.env.HTTP2_PLAIN ? {
plain: true
} : {
key: fs.readFileSync(path.join(__dirname, '/localhost.key')),
cert: fs.readFileSync(path.join(__dirname, '/localhost.crt'))
};
// Passing bunyan logger (optional)
options.log = require('../test/util').createLogger('server');
// We cache one file to be able to do simple performance tests without waiting for the disk
var cachedFile = fs.readFileSync(path.join(__dirname, './server.js'));
var cachedUrl = '/server.js';
// Creating the server
var server = http2.createServer(options, function(request, response) {
var filename = path.join(__dirname, request.url);
// Serving server.js from cache. Useful for microbenchmarks.
if (request.url === cachedUrl) {
response.end(cachedFile);
}
// Reading file from disk if it exists and is safe.
else if ((filename.indexOf(__dirname) === 0) && fs.existsSync(filename) && fs.statSync(filename).isFile()) {
response.writeHead('200');
// If they download the certificate, push the private key too, they might need it.
if (response.push && request.url === '/localhost.crt') {
var push = response.push('/localhost.key');
push.writeHead(200);
fs.createReadStream(path.join(__dirname, '/localhost.key')).pipe(push);
}
fs.createReadStream(filename).pipe(response);
}
// Otherwise responding with 404.
else {
response.writeHead('404');
response.end();
}
});
server.listen(process.env.HTTP2_PORT || 8080);

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,619 @@
var assert = require('assert');
// The Connection class
// ====================
// The Connection class manages HTTP/2 connections. Each instance corresponds to one transport
// stream (TCP stream). It operates by sending and receiving frames and is implemented as a
// [Flow](flow.html) subclass.
var Flow = require('./flow').Flow;
exports.Connection = Connection;
// Public API
// ----------
// * **new Connection(log, firstStreamId, settings)**: create a new Connection
//
// * **Event: 'error' (type)**: signals a connection level error made by the other end
//
// * **Event: 'peerError' (type)**: signals the receipt of a GOAWAY frame that contains an error
// code other than NO_ERROR
//
// * **Event: 'stream' (stream)**: signals that there's an incoming stream
//
// * **createStream(): stream**: initiate a new stream
//
// * **set(settings, callback)**: change the value of one or more settings according to the
// key-value pairs of `settings`. The callback is called after the peer acknowledged the changes.
//
// * **ping([callback])**: send a ping and call callback when the answer arrives
//
// * **close([error])**: close the stream with an error code
// Constructor
// -----------
// The main aspects of managing the connection are:
function Connection(log, firstStreamId, settings) {
// * initializing the base class
Flow.call(this, 0);
// * logging: every method uses the common logger object
this._log = log.child({ component: 'connection' });
// * stream management
this._initializeStreamManagement(firstStreamId);
// * lifecycle management
this._initializeLifecycleManagement();
// * flow control
this._initializeFlowControl();
// * settings management
this._initializeSettingsManagement(settings);
// * multiplexing
this._initializeMultiplexing();
}
Connection.prototype = Object.create(Flow.prototype, { constructor: { value: Connection } });
// Overview
// --------
// | ^ | ^
// v | v |
// +--------------+ +--------------+
// +---| stream1 |---| stream2 |---- .... ---+
// | | +----------+ | | +----------+ | |
// | | | stream1. | | | | stream2. | | |
// | +-| upstream |-+ +-| upstream |-+ |
// | +----------+ +----------+ |
// | | ^ | ^ |
// | v | v | |
// | +-----+-------------+-----+-------- .... |
// | ^ | | | |
// | | v | | |
// | +--------------+ | | |
// | | stream0 | | | |
// | | connection | | | |
// | | management | multiplexing |
// | +--------------+ flow control |
// | | ^ |
// | _read() | | _write() |
// | v | |
// | +------------+ +-----------+ |
// | |output queue| |input queue| |
// +----------------+------------+-+-----------+-----------------+
// | ^
// read() | | write()
// v |
// Stream management
// -----------------
var Stream = require('./stream').Stream;
// Initialization:
Connection.prototype._initializeStreamManagement = function _initializeStreamManagement(firstStreamId) {
// * streams are stored in two data structures:
// * `_streamIds` is an id -> stream map of the streams that are allowed to receive frames.
// * `_streamPriorities` is a priority -> [stream] map of stream that allowed to send frames.
this._streamIds = [];
this._streamPriorities = [];
// * The next outbound stream ID and the last inbound stream id
this._nextStreamId = firstStreamId;
this._lastIncomingStream = 0;
// * Calling `_writeControlFrame` when there's an incoming stream with 0 as stream ID
this._streamIds[0] = { upstream: { write: this._writeControlFrame.bind(this) } };
// * By default, the number of concurrent outbound streams is not limited. The `_streamLimit` can
// be set by the SETTINGS_MAX_CONCURRENT_STREAMS setting.
this._streamSlotsFree = Infinity;
this._streamLimit = Infinity;
this.on('RECEIVING_SETTINGS_MAX_CONCURRENT_STREAMS', this._updateStreamLimit);
};
// `_writeControlFrame` is called when there's an incoming frame in the `_control` stream. It
// broadcasts the message by creating an event on it.
Connection.prototype._writeControlFrame = function _writeControlFrame(frame) {
if ((frame.type === 'SETTINGS') || (frame.type === 'PING') ||
(frame.type === 'GOAWAY') || (frame.type === 'WINDOW_UPDATE')) {
this._log.debug({ frame: frame }, 'Receiving connection level frame');
this.emit(frame.type, frame);
} else {
this._log.error({ frame: frame }, 'Invalid connection level frame');
this.emit('error', 'PROTOCOL_ERROR');
}
};
// Methods to manage the stream slot pool:
Connection.prototype._updateStreamLimit = function _updateStreamLimit(newStreamLimit) {
var wakeup = (this._streamSlotsFree === 0) && (newStreamLimit > this._streamLimit);
this._streamSlotsFree += newStreamLimit - this._streamLimit;
this._streamLimit = newStreamLimit;
if (wakeup) {
this.emit('wakeup');
}
};
Connection.prototype._changeStreamCount = function _changeStreamCount(change) {
if (change) {
this._log.trace({ free: this._streamSlotsFree, change: change },
'Changing active stream count.');
var wakeup = (this._streamSlotsFree === 0) && (change < 0);
this._streamSlotsFree -= change;
if (wakeup) {
this.emit('wakeup');
}
}
};
// Creating a new *inbound or outbound* stream with the given `id` (which is undefined in case of
// an outbound stream) consists of three steps:
//
// 1. var stream = new Stream(this._log);
// 2. this._allocateId(stream, id);
// 2. this._allocatePriority(stream);
// Allocating an ID to a stream
Connection.prototype._allocateId = function _allocateId(stream, id) {
// * initiated stream without definite ID
if (id === undefined) {
id = this._nextStreamId;
this._nextStreamId += 2;
}
// * incoming stream with a legitim ID (larger than any previous and different parity than ours)
else if ((id > this._lastIncomingStream) && ((id - this._nextStreamId) % 2 !== 0)) {
this._lastIncomingStream = id;
}
// * incoming stream with invalid ID
else {
this._log.error({ stream_id: id, lastIncomingStream: this._lastIncomingStream },
'Invalid incoming stream ID.');
this.emit('error', 'PROTOCOL_ERROR');
return undefined;
}
assert(!(id in this._streamIds));
// * adding to `this._streamIds`
this._log.trace({ s: stream, stream_id: id }, 'Allocating ID for stream.');
this._streamIds[id] = stream;
stream.id = id;
this.emit('new_stream', stream, id);
// * handling stream errors as connection errors
stream.on('error', this.emit.bind(this, 'error'));
return id;
};
// Allocating a priority to a stream, and managing priority changes
Connection.prototype._allocatePriority = function _allocatePriority(stream) {
this._log.trace({ s: stream }, 'Allocating priority for stream.');
this._insert(stream, stream._priority);
stream.on('priority', this._reprioritize.bind(this, stream));
stream.upstream.on('readable', this.emit.bind(this, 'wakeup'));
this.emit('wakeup');
};
Connection.prototype._insert = function _insert(stream, priority) {
if (priority in this._streamPriorities) {
this._streamPriorities[priority].push(stream);
} else {
this._streamPriorities[priority] = [stream];
}
};
Connection.prototype._reprioritize = function _reprioritize(stream, priority) {
var bucket = this._streamPriorities[stream._priority];
var index = bucket.indexOf(stream);
assert(index !== -1);
bucket.splice(index, 1);
if (bucket.length === 0) {
delete this._streamPriorities[stream._priority];
}
this._insert(stream, priority);
};
// Creating an *inbound* stream with the given ID. It is called when there's an incoming frame to
// a previously nonexistent stream.
Connection.prototype._createIncomingStream = function _createIncomingStream(id) {
this._log.debug({ stream_id: id }, 'New incoming stream.');
var stream = new Stream(this._log);
this._allocateId(stream, id);
this._allocatePriority(stream);
this.emit('stream', stream, id);
return stream;
};
// Creating an *outbound* stream
Connection.prototype.createStream = function createStream() {
this._log.trace('Creating new outbound stream.');
// * Receiving is enabled immediately, and an ID gets assigned to the stream
var stream = new Stream(this._log);
this._allocatePriority(stream);
return stream;
};
// Multiplexing
// ------------
Connection.prototype._initializeMultiplexing = function _initializeMultiplexing() {
this.on('window_update', this.emit.bind(this, 'wakeup'));
this._sendScheduled = false;
this._firstFrameReceived = false;
};
// The `_send` method is a virtual method of the [Flow class](flow.html) that has to be implemented
// by child classes. It reads frames from streams and pushes them to the output buffer.
Connection.prototype._send = function _send(immediate) {
// * Do not do anything if the connection is already closed
if (this._closed) {
return;
}
// * Collapsing multiple calls in a turn into a single deferred call
if (immediate) {
this._sendScheduled = false;
} else {
if (!this._sendScheduled) {
this._sendScheduled = true;
setImmediate(this._send.bind(this, true));
}
return;
}
this._log.trace('Starting forwarding frames from streams.');
// * Looping through priority `bucket`s in priority order.
priority_loop:
for (var priority in this._streamPriorities) {
var bucket = this._streamPriorities[priority];
var nextBucket = [];
// * Forwarding frames from buckets with round-robin scheduling.
// 1. pulling out frame
// 2. if there's no frame, skip this stream
// 3. if forwarding this frame would make `streamCount` greater than `streamLimit`, skip
// this stream
// 4. adding stream to the bucket of the next round
// 5. assigning an ID to the frame (allocating an ID to the stream if there isn't already)
// 6. if forwarding a PUSH_PROMISE, allocate ID to the promised stream
// 7. forwarding the frame, changing `streamCount` as appropriate
// 8. stepping to the next stream if there's still more frame needed in the output buffer
// 9. switching to the bucket of the next round
while (bucket.length > 0) {
for (var index = 0; index < bucket.length; index++) {
var stream = bucket[index];
var frame = stream.upstream.read((this._window > 0) ? this._window : -1);
if (!frame) {
continue;
} else if (frame.count_change > this._streamSlotsFree) {
stream.upstream.unshift(frame);
continue;
}
nextBucket.push(stream);
if (frame.stream === undefined) {
frame.stream = stream.id || this._allocateId(stream);
}
if (frame.type === 'PUSH_PROMISE') {
this._allocatePriority(frame.promised_stream);
frame.promised_stream = this._allocateId(frame.promised_stream);
}
this._log.trace({ s: stream, frame: frame }, 'Forwarding outgoing frame');
var moreNeeded = this.push(frame);
this._changeStreamCount(frame.count_change);
assert(moreNeeded !== null); // The frame shouldn't be unforwarded
if (moreNeeded === false) {
break priority_loop;
}
}
bucket = nextBucket;
nextBucket = [];
}
}
// * if we couldn't forward any frame, then sleep until window update, or some other wakeup event
if (moreNeeded === undefined) {
this.once('wakeup', this._send.bind(this));
}
this._log.trace({ moreNeeded: moreNeeded }, 'Stopping forwarding frames from streams.');
};
// The `_receive` method is another virtual method of the [Flow class](flow.html) that has to be
// implemented by child classes. It forwards the given frame to the appropriate stream:
Connection.prototype._receive = function _receive(frame, done) {
this._log.trace({ frame: frame }, 'Forwarding incoming frame');
// * first frame needs to be checked by the `_onFirstFrameReceived` method
if (!this._firstFrameReceived) {
this._firstFrameReceived = true;
this._onFirstFrameReceived(frame);
}
// * gets the appropriate stream from the stream registry
var stream = this._streamIds[frame.stream];
// * or creates one if it's not in `this.streams`
if (!stream) {
stream = this._createIncomingStream(frame.stream);
}
// * in case of PUSH_PROMISE, replaces the promised stream id with a new incoming stream
if (frame.type === 'PUSH_PROMISE') {
frame.promised_stream = this._createIncomingStream(frame.promised_stream);
}
frame.count_change = this._changeStreamCount.bind(this);
// * and writes it to the `stream`'s `upstream`
stream.upstream.write(frame);
done();
};
// Settings management
// -------------------
var defaultSettings = {
SETTINGS_FLOW_CONTROL_OPTIONS: true
};
// Settings management initialization:
Connection.prototype._initializeSettingsManagement = function _initializeSettingsManagement(settings) {
// * Setting up the callback queue for setting acknowledgements
this._settingsAckCallbacks = [];
// * Sending the initial settings.
this._log.debug({ settings: settings },
'Sending the first SETTINGS frame as part of the connection header.');
this.set(settings || defaultSettings);
// * Forwarding SETTINGS frames to the `_receiveSettings` method
this.on('SETTINGS', this._receiveSettings);
};
// * Checking that the first frame the other endpoint sends is SETTINGS
Connection.prototype._onFirstFrameReceived = function _onFirstFrameReceived(frame) {
if ((frame.stream === 0) && (frame.type === 'SETTINGS')) {
this._log.debug('Receiving the first SETTINGS frame as part of the connection header.');
} else {
this._log.fatal({ frame: frame }, 'Invalid connection header: first frame is not SETTINGS.');
this.emit('error');
}
};
// Handling of incoming SETTINGS frames.
Connection.prototype._receiveSettings = function _receiveSettings(frame) {
// * If it's an ACK, call the appropriate callback
if (frame.flags.ACK) {
var callback = this._settingsAckCallbacks.shift();
if (callback) {
callback();
}
}
// * If it's a setting change request, then send an ACK and change the appropriate settings
else {
if (!this._closed) {
this.push({
type: 'SETTINGS',
flags: { ACK: true },
stream: 0,
settings: {}
});
}
for (var name in frame.settings) {
this.emit('RECEIVING_' + name, frame.settings[name]);
}
}
};
// Changing one or more settings value and sending out a SETTINGS frame
Connection.prototype.set = function set(settings, callback) {
// * Calling the callback and emitting event when the change is acknowledges
callback = callback || function noop() {};
var self = this;
this._settingsAckCallbacks.push(function() {
for (var name in settings) {
self.emit('ACKNOWLEDGED_' + name, settings[name]);
}
callback();
});
// * Sending out the SETTINGS frame
this.push({
type: 'SETTINGS',
flags: { ACK: false },
stream: 0,
settings: settings
});
for (var name in settings) {
this.emit('SENDING_' + name, settings[name]);
}
};
// Lifecycle management
// --------------------
// The main responsibilities of lifecycle management code:
//
// * keeping the connection alive by
// * sending PINGs when the connection is idle
// * answering PINGs
// * ending the connection
Connection.prototype._initializeLifecycleManagement = function _initializeLifecycleManagement() {
this._pings = {};
this.on('PING', this._receivePing);
this.on('GOAWAY', this._receiveGoaway);
this._closed = false;
};
// Generating a string of length 16 with random hexadecimal digits
Connection.prototype._generatePingId = function _generatePingId() {
do {
var id = '';
for (var i = 0; i < 16; i++) {
id += Math.floor(Math.random()*16).toString(16);
}
} while(id in this._pings);
return id;
};
// Sending a ping and calling `callback` when the answer arrives
Connection.prototype.ping = function ping(callback) {
var id = this._generatePingId();
var data = new Buffer(id, 'hex');
this._pings[id] = callback;
this._log.debug({ data: data }, 'Sending PING.');
this.push({
type: 'PING',
flags: {
ACK: false
},
stream: 0,
data: data
});
};
// Answering pings
Connection.prototype._receivePing = function _receivePing(frame) {
if (frame.flags.ACK) {
var id = frame.data.toString('hex');
if (id in this._pings) {
this._log.debug({ data: frame.data }, 'Receiving answer for a PING.');
var callback = this._pings[id];
if (callback) {
callback();
}
delete this._pings[id];
} else {
this._log.warn({ data: frame.data }, 'Unsolicited PING answer.');
}
} else {
this._log.debug({ data: frame.data }, 'Answering PING.');
this.push({
type: 'PING',
flags: {
ACK: true
},
stream: 0,
data: frame.data
});
}
};
// Terminating the connection
Connection.prototype.close = function close(error) {
if (this._closed) {
this._log.warn('Trying to close an already closed connection');
return;
}
this._log.debug({ error: error }, 'Closing the connection');
this.push({
type: 'GOAWAY',
flags: {},
stream: 0,
last_stream: this._lastIncomingStream,
error: error || 'NO_ERROR'
});
this.push(null);
this._closed = true;
};
Connection.prototype._receiveGoaway = function _receiveGoaway(frame) {
this._log.debug({ error: frame.error }, 'Other end closed the connection');
this.push(null);
this._closed = true;
if (frame.error !== 'NO_ERROR') {
this.emit('peerError', frame.error);
}
};
// Flow control
// ------------
Connection.prototype._initializeFlowControl = function _initializeFlowControl() {
// Handling of initial window size of individual streams.
this._initialStreamWindowSize = INITIAL_STREAM_WINDOW_SIZE;
this.on('new_stream', function(stream) {
stream.upstream.setInitialWindow(this._initialStreamWindowSize);
if (this._remoteFlowControlDisabled) {
stream.upstream.disableRemoteFlowControl();
}
});
this.on('RECEIVING_SETTINGS_INITIAL_WINDOW_SIZE', this._setInitialStreamWindowSize);
this.on('RECEIVING_SETTINGS_FLOW_CONTROL_OPTIONS', this._setLocalFlowControl);
this.on('SENDING_SETTINGS_FLOW_CONTROL_OPTIONS', this._setRemoteFlowControl);
this._streamIds[0].upstream.setInitialWindow = function noop() {};
this._streamIds[0].upstream.disableRemoteFlowControl = function noop() {};
};
// The initial connection flow control window is 65535 bytes.
var INITIAL_STREAM_WINDOW_SIZE = 65535;
// A SETTINGS frame can alter the initial flow control window size for all current streams. When the
// value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the window size of all
// stream by calling the `setInitialStreamWindowSize` method. The window size has to be modified by
// the difference between the new value and the old value.
Connection.prototype._setInitialStreamWindowSize = function _setInitialStreamWindowSize(size) {
if ((this._initialStreamWindowSize === Infinity) && (size !== Infinity)) {
this._log.error('Trying to manipulate initial flow control window size after flow control was turned off.');
this.emit('error', 'FLOW_CONTROL_ERROR');
} else {
this._log.debug({ size: size }, 'Changing stream initial window size.');
this._initialStreamWindowSize = size;
this._streamIds.forEach(function(stream) {
stream.upstream.setInitialWindow(size);
});
}
};
// `_setStreamFlowControl()` may be used to disable/enable flow control. In practice, it is just
// for turning off flow control since it can not be turned on.
Connection.prototype._setLocalFlowControl = function _setLocalFlowControl(disable) {
if (disable) {
this._increaseWindow(Infinity);
this._setInitialStreamWindowSize(Infinity);
} else if (this._initialStreamWindowSize === Infinity) {
this._log.error('Trying to re-enable flow control after it was turned off.');
this.emit('error', 'FLOW_CONTROL_ERROR');
}
};
Connection.prototype._setRemoteFlowControl = function _setRemoteFlowControl(disable) {
if (disable) {
this.disableRemoteFlowControl();
this._streamIds.forEach(function(stream) {
stream.upstream.disableRemoteFlowControl();
});
} else if (this._remoteFlowControlDisabled) {
this._log.error('Trying to re-enable flow control after it was turned off.');
throw new Error('Trying to re-enable flow control after it was turned off.');
}
};

View File

@ -0,0 +1,264 @@
var assert = require('assert');
var Serializer = require('./framer').Serializer;
var Deserializer = require('./framer').Deserializer;
var Compressor = require('./compressor').Compressor;
var Decompressor = require('./compressor').Decompressor;
var Connection = require('./connection').Connection;
var Duplex = require('stream').Duplex;
var Transform = require('stream').Transform;
exports.Endpoint = Endpoint;
// The Endpoint class
// ==================
// Public API
// ----------
// - **new Endpoint(log, role, settings, filters)**: create a new Endpoint.
//
// - `log`: bunyan logger of the parent
// - `role`: 'CLIENT' or 'SERVER'
// - `settings`: initial HTTP/2 settings
// - `filters`: a map of functions that filter the traffic between components (for debugging or
// intentional failure injection).
//
// Filter functions get three arguments:
// 1. `frame`: the current frame
// 2. `forward(frame)`: function that can be used to forward a frame to the next component
// 3. `done()`: callback to signal the end of the filter process
//
// Valid filter names and their position in the stack:
// - `beforeSerialization`: after compression, before serialization
// - `beforeCompression`: after multiplexing, before compression
// - `afterDeserialization`: after deserialization, before decompression
// - `afterDecompression`: after decompression, before multiplexing
//
// * **Event: 'stream' (Stream)**: 'stream' event forwarded from the underlying Connection
//
// * **Event: 'error' (type)**: signals an error
//
// * **createStream(): Stream**: initiate a new stream (forwarded to the underlying Connection)
//
// * **close([error])**: close the connection with an error code
// Constructor
// -----------
// The process of initialization:
function Endpoint(log, role, settings, filters) {
Duplex.call(this);
// * Initializing logging infrastructure
this._log = log.child({ component: 'endpoint', e: this });
// * First part of the handshake process: sending and receiving the client connection header
// prelude.
assert((role === 'CLIENT') || role === 'SERVER');
if (role === 'CLIENT') {
this._writePrelude();
} else {
this._readPrelude();
}
// * Initialization of component. This includes the second part of the handshake process:
// sending the first SETTINGS frame. This is done by the connection class right after
// initialization.
this._initializeDataFlow(role, settings, filters || {});
// * Initialization of management code.
this._initializeManagement();
// * Initializing error handling.
this._initializeErrorHandling();
}
Endpoint.prototype = Object.create(Duplex.prototype, { constructor: { value: Endpoint } });
// Handshake
// ---------
var CLIENT_PRELUDE = new Buffer('PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n');
// Writing the client header is simple and synchronous.
Endpoint.prototype._writePrelude = function _writePrelude() {
this._log.debug('Sending the client connection header prelude.');
this.push(CLIENT_PRELUDE);
};
// The asynchronous process of reading the client header:
Endpoint.prototype._readPrelude = function _readPrelude() {
// * progress in the header is tracker using a `cursor`
var cursor = 0;
// * `_write` is temporarily replaced by the comparator function
this._write = function _temporalWrite(chunk, encoding, done) {
// * which compares the stored header with the current `chunk` byte by byte and emits the
// 'error' event if there's a byte that doesn't match
var offset = cursor;
while(cursor < CLIENT_PRELUDE.length && (cursor - offset) < chunk.length) {
if (CLIENT_PRELUDE[cursor] !== chunk[cursor - offset]) {
this._log.fatal({ cursor: cursor, offset: offset, chunk: chunk },
'Client connection header prelude does not match.');
this._error('handshake', 'PROTOCOL_ERROR');
return;
}
cursor += 1;
}
// * if the whole header is over, and there were no error then restore the original `_write`
// and call it with the remaining part of the current chunk
if (cursor === CLIENT_PRELUDE.length) {
this._log.debug('Successfully received the client connection header prelude.');
delete this._write;
chunk = chunk.slice(cursor - offset);
this._write(chunk, encoding, done);
}
};
};
// Data flow
// ---------
// +---------------------------------------------+
// | |
// | +-------------------------------------+ |
// | | +---------+ +---------+ +---------+ | |
// | | | stream1 | | stream2 | | ... | | |
// | | +---------+ +---------+ +---------+ | |
// | | connection | |
// | +-------------------------------------+ |
// | | ^ |
// | pipe | | pipe |
// | v | |
// | +------------------+------------------+ |
// | | compressor | decompressor | |
// | +------------------+------------------+ |
// | | ^ |
// | pipe | | pipe |
// | v | |
// | +------------------+------------------+ |
// | | serializer | deserializer | |
// | +------------------+------------------+ |
// | | ^ |
// | _read() | | _write() |
// | v | |
// | +------------+ +-----------+ |
// | |output queue| |input queue| |
// +------+------------+-----+-----------+-------+
// | ^
// read() | | write()
// v |
function createTransformStream(filter) {
var transform = new Transform({ objectMode: true });
var push = transform.push.bind(transform);
transform._transform = function(frame, encoding, done) {
filter(frame, push, done);
};
return transform;
}
function pipeAndFilter(stream1, stream2, filter) {
if (filter) {
stream1.pipe(createTransformStream(filter)).pipe(stream2);
} else {
stream1.pipe(stream2);
}
}
var MAX_HTTP_PAYLOAD_SIZE = 16383;
Endpoint.prototype._initializeDataFlow = function _initializeDataFlow(role, settings, filters) {
var firstStreamId, compressorRole, decompressorRole;
if (role === 'CLIENT') {
firstStreamId = 1;
compressorRole = 'REQUEST';
decompressorRole = 'RESPONSE';
} else {
firstStreamId = 2;
compressorRole = 'RESPONSE';
decompressorRole = 'REQUEST';
}
this._serializer = new Serializer(this._log, MAX_HTTP_PAYLOAD_SIZE);
this._deserializer = new Deserializer(this._log, MAX_HTTP_PAYLOAD_SIZE);
this._compressor = new Compressor(this._log, compressorRole);
this._decompressor = new Decompressor(this._log, decompressorRole);
this._connection = new Connection(this._log, firstStreamId, settings);
pipeAndFilter(this._connection, this._compressor, filters.beforeCompression);
pipeAndFilter(this._compressor, this._serializer, filters.beforeSerialization);
pipeAndFilter(this._deserializer, this._decompressor, filters.afterDeserialization);
pipeAndFilter(this._decompressor, this._connection, filters.afterDecompression);
this._connection.on('ACKNOWLEDGED_SETTINGS_HEADER_TABLE_SIZE',
this._decompressor.setTableSizeLimit.bind(this._decompressor))
this._connection.on('RECEIVING_SETTINGS_HEADER_TABLE_SIZE',
this._compressor.setTableSizeLimit.bind(this._compressor))
};
var noread = {};
Endpoint.prototype._read = function _read() {
this._readableState.sync = true;
var moreNeeded = noread, chunk;
while (moreNeeded && (chunk = this._serializer.read())) {
moreNeeded = this.push(chunk);
}
if (moreNeeded === noread) {
this._serializer.once('readable', this._read.bind(this));
}
this._readableState.sync = false;
};
Endpoint.prototype._write = function _write(chunk, encoding, done) {
this._deserializer.write(chunk, encoding, done);
};
// Management
// --------------
Endpoint.prototype._initializeManagement = function _initializeManagement() {
this._connection.on('stream', this.emit.bind(this, 'stream'));
};
Endpoint.prototype.createStream = function createStream() {
return this._connection.createStream();
};
// Error handling
// --------------
Endpoint.prototype._initializeErrorHandling = function _initializeErrorHandling() {
this._serializer.on('error', this._error.bind(this, 'serializer'));
this._deserializer.on('error', this._error.bind(this, 'deserializer'));
this._compressor.on('error', this._error.bind(this, 'compressor'));
this._decompressor.on('error', this._error.bind(this, 'decompressor'));
this._connection.on('error', this._error.bind(this, 'connection'));
this._connection.on('peerError', this.emit.bind(this, 'peerError'));
};
Endpoint.prototype._error = function _error(component, error) {
this._log.fatal({ source: component, message: error }, 'Fatal error, closing connection');
this.close(error);
setImmediate(this.emit.bind(this, 'error', error));
};
Endpoint.prototype.close = function close(error) {
this._connection.close(error);
};
// Bunyan serializers
// ------------------
exports.serializers = {};
var nextId = 0;
exports.serializers.e = function(endpoint) {
if (!('id' in endpoint)) {
endpoint.id = nextId;
nextId += 1;
}
return endpoint.id;
};

View File

@ -0,0 +1,362 @@
var assert = require('assert');
// The Flow class
// ==============
// Flow is a [Duplex stream][1] subclass which implements HTTP/2 flow control. It is designed to be
// subclassed by [Connection](connection.html) and the `upstream` component of [Stream](stream.html).
// [1]: http://nodejs.org/api/stream.html#stream_class_stream_duplex
var Duplex = require('stream').Duplex;
exports.Flow = Flow;
// Public API
// ----------
// * **Event: 'error' (type)**: signals an error
//
// * **setInitialWindow(size)**: the initial flow control window size can be changed *any time*
// ([as described in the standard][1]) using this method
//
// * **disableRemoteFlowControl()**: sends a WINDOW_UPDATE signaling that we don't want flow control
//
// * **disableLocalFlowControl()**: disables flow control for outgoing frames
//
// [1]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-6.9.2
// API for child classes
// ---------------------
// * **new Flow([flowControlId])**: creating a new flow that will listen for WINDOW_UPDATES frames
// with the given `flowControlId` (or every update frame if not given)
//
// * **_send()**: called when more frames should be pushed. The child class is expected to override
// this (instead of the `_read` method of the Duplex class).
//
// * **_receive(frame, readyCallback)**: called when there's an incoming frame. The child class is
// expected to override this (instead of the `_write` method of the Duplex class).
//
// * **push(frame): bool**: schedules `frame` for sending.
//
// Returns `true` if it needs more frames in the output queue, `false` if the output queue is
// full, and `null` if did not push the frame into the output queue (instead, it pushed it into
// the flow control queue).
//
// * **read(limit): frame**: like the regular `read`, but the 'flow control size' (0 for non-DATA
// frames, length of the payload for DATA frames) of the returned frame will be under `limit`.
// Small exception: pass -1 as `limit` if the max. flow control size is 0. `read(0)` means the
// same thing as [in the original API](http://nodejs.org/api/stream.html#stream_stream_read_0).
//
// * **getLastQueuedFrame(): frame**: returns the last frame in output buffers
//
// * **_log**: the Flow class uses the `_log` object of the parent
// Constructor
// -----------
// When a HTTP/2.0 connection is first established, new streams are created with an initial flow
// control window size of 65535 bytes.
var INITIAL_WINDOW_SIZE = 65535;
// `flowControlId` is needed if only specific WINDOW_UPDATEs should be watched.
function Flow(flowControlId) {
Duplex.call(this, { objectMode: true });
this._window = this._initialWindow = INITIAL_WINDOW_SIZE;
this._flowControlId = flowControlId;
this._queue = [];
this._ended = false;
this._received = 0;
this._remoteFlowControlDisabled = false;
}
Flow.prototype = Object.create(Duplex.prototype, { constructor: { value: Flow } });
// Incoming frames
// ---------------
// `_receive` is called when there's an incoming frame.
Flow.prototype._receive = function _receive(frame, callback) {
throw new Error('The _receive(frame, callback) method has to be overridden by the child class!');
};
// `_receive` is called by `_write` which in turn is [called by Duplex][1] when someone `write()`s
// to the flow. It emits the 'receiving' event and notifies the window size tracking code if the
// incoming frame is a WINDOW_UPDATE.
// [1]: http://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1
Flow.prototype._write = function _write(frame, encoding, callback) {
if (frame.flags.END_STREAM || (frame.type === 'RST_STREAM')) {
this._ended = true;
}
if ((frame.type === 'DATA') && (frame.data.length > 0) && !this._remoteFlowControlDisabled) {
this._receive(frame, function() {
this._received += frame.data.length;
if (!this._restoreWindowTimer) {
this._restoreWindowTimer = setImmediate(this._restoreWindow.bind(this));
}
callback();
}.bind(this));
}
else {
this._receive(frame, callback);
}
if ((frame.type === 'WINDOW_UPDATE') &&
((this._flowControlId === undefined) || (frame.stream === this._flowControlId))) {
this._updateWindow(frame);
}
};
// `_restoreWindow` basically acknowledges the DATA frames received since it's last call. It sends
// a WINDOW_UPDATE that restores the flow control window of the remote end.
Flow.prototype._restoreWindow = function _restoreWindow() {
delete this._restoreWindowTimer;
if (!this._ended && !this._remoteFlowControlDisabled && (this._received > 0)) {
this.push({
type: 'WINDOW_UPDATE',
flags: {},
stream: this._flowControlId,
window_size: this._received
});
this._received = 0;
}
};
// Must be called after sending a SETTINGS frame that turns off flow control on the remote side.
Flow.prototype.disableRemoteFlowControl = function disableRemoteFlowControl() {
this._log.debug('Turning off remote flow control');
this._remoteFlowControlDisabled = true;
};
// Outgoing frames - sending procedure
// -----------------------------------
// flow
// +-------------------------------------------------+
// | |
// +--------+ +---------+ |
// read() | output | _read() | flow | _send() |
// <----------| |<----------| control |<------------- |
// | buffer | | buffer | |
// +--------+ +---------+ |
// | input | |
// ---------->| |-----------------------------------> |
// write() | buffer | _write() _receive() |
// +--------+ |
// | |
// +-------------------------------------------------+
// `_send` is called when more frames should be pushed to the output buffer.
Flow.prototype._send = function _send() {
throw new Error('The _send() method has to be overridden by the child class!');
};
// `_send` is called by `_read` which is in turn [called by Duplex][1] when it wants to have more
// items in the output queue.
// [1]: http://nodejs.org/api/stream.html#stream_writable_write_chunk_encoding_callback_1
Flow.prototype._read = function _read() {
// * if the flow control queue is empty, then let the user push more frames
if (this._queue.length === 0) {
this._send();
}
// * if there are items in the flow control queue, then let's put them into the output queue (to
// the extent it is possible with respect to the window size and output queue feedback)
else if (this._window > 0) {
this._readableState.sync = true; // to avoid reentrant calls
do {
var moreNeeded = this._push(this._queue[0]);
if (moreNeeded !== null) {
this._queue.shift();
}
} while (moreNeeded && (this._queue.length > 0));
this._readableState.sync = false;
assert((moreNeeded == false) || // * output queue is full
(this._queue.length === 0) || // * flow control queue is empty
(!this._window && (this._queue[0].type === 'DATA'))); // * waiting for window update
}
// * otherwise, come back when the flow control window is positive
else {
this.once('window_update', this._read);
}
};
var MAX_PAYLOAD_SIZE = 4096; // Must not be greater than MAX_HTTP_PAYLOAD_SIZE which is 16383
// `read(limit)` is like the `read` of the Readable class, but it guarantess that the 'flow control
// size' (0 for non-DATA frames, length of the payload for DATA frames) of the returned frame will
// be under `limit`.
Flow.prototype.read = function read(limit) {
if (limit === 0) {
return Duplex.prototype.read.call(this, 0);
} else if (limit === -1) {
limit = 0;
} else if ((limit === undefined) || (limit > MAX_PAYLOAD_SIZE)) {
limit = MAX_PAYLOAD_SIZE;
}
// * Looking at the first frame in the queue without pulling it out if possible. This will save
// a costly unshift if the frame proves to be too large to return.
var firstInQueue = this._readableState.buffer[0];
var frame = firstInQueue || Duplex.prototype.read.call(this);
if ((frame === null) || (frame.type !== 'DATA') || (frame.data.length <= limit)) {
if (firstInQueue) {
Duplex.prototype.read.call(this);
}
return frame;
}
else if (limit <= 0) {
if (!firstInQueue) {
this.unshift(frame);
}
return null;
}
else {
this._log.trace({ frame: frame, size: frame.data.length, forwardable: limit },
'Splitting out forwardable part of a DATA frame.');
var forwardable = {
type: 'DATA',
flags: {},
stream: frame.stream,
data: frame.data.slice(0, limit)
};
frame.data = frame.data.slice(limit);
if (!firstInQueue) {
this.unshift(frame);
}
return forwardable;
}
};
// `_parentPush` pushes the given `frame` into the output queue
Flow.prototype._parentPush = function _parentPush(frame) {
this._log.trace({ frame: frame }, 'Pushing frame into the output queue');
if (frame && (frame.type === 'DATA') && (this._window !== Infinity)) {
this._log.trace({ window: this._window, by: frame.data.length },
'Decreasing flow control window size.');
this._window -= frame.data.length;
assert(this._window >= 0);
}
return Duplex.prototype.push.call(this, frame);
};
// `_push(frame)` pushes `frame` into the output queue and decreases the flow control window size.
// It is capable of splitting DATA frames into smaller parts, if the window size is not enough to
// push the whole frame. The return value is similar to `push` except that it returns `null` if it
// did not push the whole frame to the output queue (but maybe it did push part of the frame).
Flow.prototype._push = function _push(frame) {
var data = frame && (frame.type === 'DATA') && frame.data;
if (!data || (data.length <= this._window)) {
return this._parentPush(frame);
}
else if (this._window <= 0) {
return null;
}
else {
this._log.trace({ frame: frame, size: frame.data.length, forwardable: this._window },
'Splitting out forwardable part of a DATA frame.');
frame.data = data.slice(this._window);
this._parentPush({
type: 'DATA',
flags: {},
stream: frame.stream,
data: data.slice(0, this._window)
});
return null;
}
};
// Push `frame` into the flow control queue, or if it's empty, then directly into the output queue
Flow.prototype.push = function push(frame) {
if (frame === null) {
this._log.debug('Enqueueing outgoing End Of Stream');
} else {
this._log.debug({ frame: frame }, 'Enqueueing outgoing frame');
}
var moreNeeded = null;
if (this._queue.length === 0) {
moreNeeded = this._push(frame);
}
if (moreNeeded === null) {
this._queue.push(frame);
}
return moreNeeded;
};
// `getLastQueuedFrame` returns the last frame in output buffers. This is primarily used by the
// [Stream](stream.html) class to mark the last frame with END_STREAM flag.
Flow.prototype.getLastQueuedFrame = function getLastQueuedFrame() {
var readableQueue = this._readableState.buffer;
return this._queue[this._queue.length - 1] || readableQueue[readableQueue.length - 1];
};
// Outgoing frames - managing the window size
// ------------------------------------------
// Flow control window size is manipulated using the `_increaseWindow` method.
//
// * Invoking it with `Infinite` means turning off flow control. Flow control cannot be enabled
// again once disabled. Any attempt to re-enable flow control MUST be rejected with a
// FLOW_CONTROL_ERROR error code.
// * A sender MUST NOT allow a flow control window to exceed 2^31 - 1 bytes. The action taken
// depends on it being a stream or the connection itself.
var WINDOW_SIZE_LIMIT = Math.pow(2, 31) - 1;
Flow.prototype._increaseWindow = function _increaseWindow(size) {
if ((this._window === Infinity) && (size !== Infinity)) {
this._log.error('Trying to increase flow control window after flow control was turned off.');
this.emit('error', 'FLOW_CONTROL_ERROR');
} else {
this._log.trace({ window: this._window, by: size }, 'Increasing flow control window size.');
this._window += size;
if ((this._window !== Infinity) && (this._window > WINDOW_SIZE_LIMIT)) {
this._log.error('Flow control window grew too large.');
this.emit('error', 'FLOW_CONTROL_ERROR');
} else {
this.emit('window_update');
}
}
};
// The `_updateWindow` method gets called every time there's an incoming WINDOW_UPDATE frame. It
// modifies the flow control window:
//
// * Flow control can be disabled for an individual stream by sending a WINDOW_UPDATE with the
// END_FLOW_CONTROL flag set. The payload of a WINDOW_UPDATE frame that has the END_FLOW_CONTROL
// flag set is ignored.
// * A sender that receives a WINDOW_UPDATE frame updates the corresponding window by the amount
// specified in the frame.
Flow.prototype._updateWindow = function _updateWindow(frame) {
this._increaseWindow(frame.flags.END_FLOW_CONTROL ? Infinity : frame.window_size);
};
// A SETTINGS frame can alter the initial flow control window size for all current streams. When the
// value of SETTINGS_INITIAL_WINDOW_SIZE changes, a receiver MUST adjust the size of all stream by
// calling the `setInitialWindow` method. The window size has to be modified by the difference
// between the new value and the old value.
Flow.prototype.setInitialWindow = function setInitialWindow(initialWindow) {
this._increaseWindow(initialWindow - this._initialWindow);
this._initialWindow = initialWindow;
};
// Flow control for outgoing frames can be disabled by the peer with various methods.
Flow.prototype.disableLocalFlowControl = function disableLocalFlowControl() {
this._increaseWindow(Infinity);
};

View File

@ -0,0 +1,761 @@
// The framer consists of two [Transform Stream][1] subclasses that operate in [object mode][2]:
// the Serializer and the Deserializer
// [1]: http://nodejs.org/api/stream.html#stream_class_stream_transform
// [2]: http://nodejs.org/api/stream.html#stream_new_stream_readable_options
var assert = require('assert');
var Transform = require('stream').Transform;
exports.Serializer = Serializer;
exports.Deserializer = Deserializer;
var logData = Boolean(process.env.HTTP2_LOG_DATA);
// Serializer
// ----------
//
// Frame Objects
// * * * * * * * --+---------------------------
// | |
// v v Buffers
// [] -----> Payload Ser. --[buffers]--> Header Ser. --> * * * *
// empty adds payload adds header
// array buffers buffer
function Serializer(log, sizeLimit) {
this._log = log.child({ component: 'serializer' });
this._sizeLimit = sizeLimit || MAX_PAYLOAD_SIZE;
Transform.call(this, { objectMode: true });
}
Serializer.prototype = Object.create(Transform.prototype, { constructor: { value: Serializer } });
// When there's an incoming frame object, it first generates the frame type specific part of the
// frame (payload), and then then adds the header part which holds fields that are common to all
// frame types (like the length of the payload).
Serializer.prototype._transform = function _transform(frame, encoding, done) {
this._log.trace({ frame: frame }, 'Outgoing frame');
assert(frame.type in Serializer, 'Unknown frame type: ' + frame.type);
var buffers = [];
Serializer[frame.type](frame, buffers);
Serializer.commonHeader(frame, buffers);
assert(buffers[0].readUInt16BE(0) <= this._sizeLimit, 'Frame too large!');
for (var i = 0; i < buffers.length; i++) {
if (logData) {
this._log.trace({ data: buffers[i] }, 'Outgoing data');
}
this.push(buffers[i]);
}
done();
};
// Deserializer
// ------------
//
// Buffers
// * * * * --------+-------------------------
// | |
// v v Frame Objects
// {} -----> Header Des. --{frame}--> Payload Des. --> * * * * * * *
// empty adds parsed adds parsed
// object header properties payload properties
function Deserializer(log, sizeLimit) {
this._log = log.child({ component: 'deserializer' });
this._sizeLimit = sizeLimit || MAX_PAYLOAD_SIZE;
Transform.call(this, { objectMode: true });
this._next(COMMON_HEADER_SIZE);
}
Deserializer.prototype = Object.create(Transform.prototype, { constructor: { value: Deserializer } });
// The Deserializer is stateful, and it's two main alternating states are: *waiting for header* and
// *waiting for payload*. The state is stored in the boolean property `_waitingForHeader`.
//
// When entering a new state, a `_buffer` is created that will hold the accumulated data (header or
// payload). The `_cursor` is used to track the progress.
Deserializer.prototype._next = function(size) {
this._cursor = 0;
this._buffer = new Buffer(size);
this._waitingForHeader = !this._waitingForHeader;
if (this._waitingForHeader) {
this._frame = {};
}
};
// Parsing an incoming buffer is an iterative process because it can hold multiple frames if it's
// large enough. A `cursor` is used to track the progress in parsing the incoming `chunk`.
Deserializer.prototype._transform = function _transform(chunk, encoding, done) {
var cursor = 0;
if (logData) {
this._log.trace({ data: chunk }, 'Incoming data');
}
while(cursor < chunk.length) {
// The content of an incoming buffer is first copied to `_buffer`. If it can't hold the full
// chunk, then only a part of it is copied.
var toCopy = Math.min(chunk.length - cursor, this._buffer.length - this._cursor);
chunk.copy(this._buffer, this._cursor, cursor, cursor + toCopy);
this._cursor += toCopy;
cursor += toCopy;
// When `_buffer` is full, it's content gets parsed either as header or payload depending on
// the actual state.
// If it's header then the parsed data is stored in a temporary variable and then the
// deserializer waits for the specified length payload.
if ((this._cursor === this._buffer.length) && this._waitingForHeader) {
var payloadSize = Deserializer.commonHeader(this._buffer, this._frame);
if (payloadSize <= this._sizeLimit) {
this._next(payloadSize);
} else {
this.emit('error', 'FRAME_SIZE_ERROR');
return;
}
}
// If it's payload then the the frame object is finalized and then gets pushed out.
// Unknown frame types are ignored.
//
// Note: If we just finished the parsing of a header and the payload length is 0, this branch
// will also run.
if ((this._cursor === this._buffer.length) && !this._waitingForHeader) {
if (this._frame.type) {
var error = Deserializer[this._frame.type](this._buffer, this._frame);
if (error) {
this._log.error('Incoming frame parsing error: ' + error);
this.emit('error', 'PROTOCOL_ERROR');
} else {
this._log.trace({ frame: this._frame }, 'Incoming frame');
this.push(this._frame);
}
} else {
this._log.warn({ frame: this._frame }, 'Unknown type incoming frame');
}
this._next(COMMON_HEADER_SIZE);
}
}
done();
};
// [Frame Header](http://http2.github.io/http2-spec/#FrameHeader)
// --------------------------------------------------------------
//
// HTTP/2.0 frames share a common base format consisting of an 8-byte header followed by 0 to 65535
// bytes of data.
//
// Additional size limits can be set by specific application uses. HTTP limits the frame size to
// 16,383 octets.
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | R | Length (14) | Type (8) | Flags (8) |
// +-+-+---------------------------+---------------+---------------+
// |R| Stream Identifier (31) |
// +-+-------------------------------------------------------------+
// | Frame Data (0...) ...
// +---------------------------------------------------------------+
//
// The fields of the frame header are defined as:
//
// * R:
// A reserved 2-bit field. The semantics of these bits are undefined and the bits MUST remain
// unset (0) when sending and MUST be ignored when receiving.
//
// * Length:
// The length of the frame data expressed as an unsigned 14-bit integer. The 8 bytes of the frame
// header are not included in this value.
//
// * Type:
// The 8-bit type of the frame. The frame type determines how the remainder of the frame header
// and data are interpreted. Implementations MUST ignore unsupported and unrecognized frame types.
//
// * Flags:
// An 8-bit field reserved for frame-type specific boolean flags.
//
// Flags are assigned semantics specific to the indicated frame type. Flags that have no defined
// semantics for a particular frame type MUST be ignored, and MUST be left unset (0) when sending.
//
// * R:
// A reserved 1-bit field. The semantics of this bit are undefined and the bit MUST remain unset
// (0) when sending and MUST be ignored when receiving.
//
// * Stream Identifier:
// A 31-bit stream identifier. The value 0 is reserved for frames that are associated with the
// connection as a whole as opposed to an individual stream.
//
// The structure and content of the remaining frame data is dependent entirely on the frame type.
var COMMON_HEADER_SIZE = 8;
var MAX_PAYLOAD_SIZE = 16383;
var frameTypes = [];
var frameFlags = {};
var genericAttributes = ['type', 'flags', 'stream'];
var typeSpecificAttributes = {};
Serializer.commonHeader = function writeCommonHeader(frame, buffers) {
var headerBuffer = new Buffer(COMMON_HEADER_SIZE);
var size = 0;
for (var i = 0; i < buffers.length; i++) {
size += buffers[i].length;
}
assert(size <= MAX_PAYLOAD_SIZE, size);
headerBuffer.writeUInt16BE(size, 0);
var typeId = frameTypes.indexOf(frame.type); // If we are here then the type is valid for sure
headerBuffer.writeUInt8(typeId, 2);
var flagByte = 0;
for (var flag in frame.flags) {
var position = frameFlags[frame.type].indexOf(flag);
assert(position !== -1, 'Unknown flag for frame type ' + frame.type + ': ' + flag);
if (frame.flags[flag]) {
flagByte |= (1 << position);
}
}
headerBuffer.writeUInt8(flagByte, 3);
assert((0 <= frame.stream) && (frame.stream < 0x7fffffff), frame.stream);
headerBuffer.writeUInt32BE(frame.stream || 0, 4);
buffers.unshift(headerBuffer);
};
Deserializer.commonHeader = function readCommonHeader(buffer, frame) {
var length = buffer.readUInt16BE(0);
frame.type = frameTypes[buffer.readUInt8(2)];
frame.flags = {};
var flagByte = buffer.readUInt8(3);
var definedFlags = frameFlags[frame.type];
for (var i = 0; i < definedFlags.length; i++) {
frame.flags[definedFlags[i]] = Boolean(flagByte & (1 << i));
}
frame.stream = buffer.readUInt32BE(4) & 0x7fffffff;
return length;
};
// Frame types
// ===========
// Every frame type is registered in the following places:
//
// * `frameTypes`: a register of frame type codes (used by `commonHeader()`)
// * `frameFlags`: a register of valid flags for frame types (used by `commonHeader()`)
// * `typeSpecificAttributes`: a register of frame specific frame object attributes (used by
// logging code and also serves as documentation for frame objects)
// [DATA Frames](http://http2.github.io/http2-spec/#DataFrames)
// ------------------------------------------------------------
//
// DATA frames (type=0x0) convey arbitrary, variable-length sequences of octets associated with a
// stream.
//
// The DATA frame defines the following flags:
//
// * END_STREAM (0x1):
// Bit 1 being set indicates that this frame is the last that the endpoint will send for the
// identified stream.
// * RESERVED (0x2):
// Bit 2 is reserved for future use.
frameTypes[0x0] = 'DATA';
frameFlags.DATA = ['END_STREAM', 'RESERVED'];
typeSpecificAttributes.DATA = ['data'];
Serializer.DATA = function writeData(frame, buffers) {
buffers.push(frame.data);
};
Deserializer.DATA = function readData(buffer, frame) {
frame.data = buffer;
};
// [HEADERS](http://http2.github.io/http2-spec/#HEADERS)
// --------------------------------------------------------------
//
// The HEADERS frame (type=0x1) allows the sender to create a stream.
//
// The HEADERS frame defines the following flags:
//
// * END_STREAM (0x1):
// Bit 1 being set indicates that this frame is the last that the endpoint will send for the
// identified stream.
// * RESERVED (0x2):
// Bit 2 is reserved for future use.
// * END_HEADERS (0x4):
// The END_HEADERS bit indicates that this frame contains the entire payload necessary to provide
// a complete set of headers.
// * PRIORITY (0x8):
// Bit 4 being set indicates that the first four octets of this frame contain a single reserved
// bit and a 31-bit priority.
frameTypes[0x1] = 'HEADERS';
frameFlags.HEADERS = ['END_STREAM', 'RESERVED', 'END_HEADERS', 'PRIORITY'];
typeSpecificAttributes.HEADERS = ['priority', 'headers', 'data'];
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |X| (Optional) Priority (31) |
// +-+-------------------------------------------------------------+
// | Header Block (*) ...
// +---------------------------------------------------------------+
//
// The payload of a HEADERS frame contains a Headers Block
Serializer.HEADERS = function writeHeadersPriority(frame, buffers) {
if (frame.flags.PRIORITY) {
var buffer = new Buffer(4);
assert((0 <= frame.priority) && (frame.priority <= 0xffffffff), frame.priority);
buffer.writeUInt32BE(frame.priority, 0);
buffers.push(buffer);
}
buffers.push(frame.data);
};
Deserializer.HEADERS = function readHeadersPriority(buffer, frame) {
if (frame.flags.PRIORITY) {
frame.priority = buffer.readUInt32BE(0) & 0x7fffffff;
frame.data = buffer.slice(4);
} else {
frame.data = buffer;
}
};
// [PRIORITY](http://http2.github.io/http2-spec/#PRIORITY)
// -------------------------------------------------------
//
// The PRIORITY frame (type=0x2) specifies the sender-advised priority of a stream.
//
// The PRIORITY frame does not define any flags.
frameTypes[0x2] = 'PRIORITY';
frameFlags.PRIORITY = [];
typeSpecificAttributes.PRIORITY = ['priority'];
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |X| Priority (31) |
// +-+-------------------------------------------------------------+
//
// The payload of a PRIORITY frame contains a single reserved bit and a 31-bit priority.
Serializer.PRIORITY = function writePriority(frame, buffers) {
var buffer = new Buffer(4);
buffer.writeUInt32BE(frame.priority, 0);
buffers.push(buffer);
};
Deserializer.PRIORITY = function readPriority(buffer, frame) {
frame.priority = buffer.readUInt32BE(0);
};
// [RST_STREAM](http://http2.github.io/http2-spec/#RST_STREAM)
// -----------------------------------------------------------
//
// The RST_STREAM frame (type=0x3) allows for abnormal termination of a stream.
//
// No type-flags are defined.
frameTypes[0x3] = 'RST_STREAM';
frameFlags.RST_STREAM = [];
typeSpecificAttributes.RST_STREAM = ['error'];
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Error Code (32) |
// +---------------------------------------------------------------+
//
// The RST_STREAM frame contains a single unsigned, 32-bit integer identifying the error
// code (see Error Codes). The error code indicates why the stream is being terminated.
Serializer.RST_STREAM = function writeRstStream(frame, buffers) {
var buffer = new Buffer(4);
var code = errorCodes.indexOf(frame.error);
assert((0 <= code) && (code <= 0xffffffff), code);
buffer.writeUInt32BE(code, 0);
buffers.push(buffer);
};
Deserializer.RST_STREAM = function readRstStream(buffer, frame) {
frame.error = errorCodes[buffer.readUInt32BE(0)];
};
// [SETTINGS](http://http2.github.io/http2-spec/#SETTINGS)
// -------------------------------------------------------
//
// The SETTINGS frame (type=0x4) conveys configuration parameters that affect how endpoints
// communicate.
//
// The SETTINGS frame defines the following flag:
// * ACK (0x1):
// Bit 1 being set indicates that this frame acknowledges receipt and application of the peer's
// SETTINGS frame.
frameTypes[0x4] = 'SETTINGS';
frameFlags.SETTINGS = ['ACK'];
typeSpecificAttributes.SETTINGS = ['settings'];
// The payload of a SETTINGS frame consists of zero or more settings. Each setting consists of an
// 8-bit reserved field, an unsigned 24-bit setting identifier, and an unsigned 32-bit value.
//
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// | Reserved(8) | Setting Identifier (24) |
// +---------------+-----------------------------------------------+
// | Value (32) |
// +---------------------------------------------------------------+
//
// Each setting in a SETTINGS frame replaces the existing value for that setting. Settings are
// processed in the order in which they appear, and a receiver of a SETTINGS frame does not need to
// maintain any state other than the current value of settings. Therefore, the value of a setting
// is the last value that is seen by a receiver. This permits the inclusion of the same settings
// multiple times in the same SETTINGS frame, though doing so does nothing other than waste
// connection capacity.
Serializer.SETTINGS = function writeSettings(frame, buffers) {
var settings = [], settingsLeft = Object.keys(frame.settings);
definedSettings.forEach(function(setting, id) {
if (setting.name in frame.settings) {
settingsLeft.splice(settingsLeft.indexOf(setting.name), 1);
var value = frame.settings[setting.name];
settings.push({ id: id, value: setting.flag ? Boolean(value) : value });
}
});
assert(settingsLeft.length === 0, 'Unknown settings: ' + settingsLeft.join(', '));
var buffer = new Buffer(settings.length * 8);
for (var i = 0; i < settings.length; i++) {
buffer.writeUInt32BE(settings[i].id & 0xffffff, i*8);
buffer.writeUInt32BE(settings[i].value, i*8 + 4);
}
buffers.push(buffer);
};
Deserializer.SETTINGS = function readSettings(buffer, frame) {
frame.settings = {};
if (buffer.length % 8 !== 0) {
return 'Invalid SETTINGS frame';
}
for (var i = 0; i < buffer.length / 8; i++) {
var id = buffer.readUInt32BE(i*8) & 0xffffff;
var setting = definedSettings[id];
if (setting) {
var value = buffer.readUInt32BE(i*8 + 4);
frame.settings[setting.name] = setting.flag ? Boolean(value & 0x1) : value;
} else {
/* Unknown setting, ignoring */
}
}
};
// The following settings are defined:
var definedSettings = [];
// * SETTINGS_HEADER_TABLE_SIZE (1):
// Allows the sender to inform the remote endpoint of the size of the header compression table
// used to decode header blocks.
definedSettings[1] = { name: 'SETTINGS_HEADER_TABLE_SIZE', flag: false };
// * SETTINGS_ENABLE_PUSH (2):
// This setting can be use to disable server push. An endpoint MUST NOT send a PUSH_PROMISE frame
// if it receives this setting set to a value of 0. The default value is 1, which indicates that
// push is permitted.
definedSettings[2] = { name: 'SETTINGS_ENABLE_PUSH', flag: true };
// * SETTINGS_MAX_CONCURRENT_STREAMS (4):
// indicates the maximum number of concurrent streams that the sender will allow.
definedSettings[4] = { name: 'SETTINGS_MAX_CONCURRENT_STREAMS', flag: false };
// * SETTINGS_INITIAL_WINDOW_SIZE (7):
// indicates the sender's initial stream window size (in bytes) for new streams.
definedSettings[7] = { name: 'SETTINGS_INITIAL_WINDOW_SIZE', flag: false };
// * SETTINGS_FLOW_CONTROL_OPTIONS (10):
// indicates that streams directed to the sender will not be subject to flow control. The least
// significant bit (0x1) is set to indicate that new streams are not flow controlled. All other
// bits are reserved.
definedSettings[10] = { name: 'SETTINGS_FLOW_CONTROL_OPTIONS', flag: true };
// [PUSH_PROMISE](http://http2.github.io/http2-spec/#PUSH_PROMISE)
// ---------------------------------------------------------------
//
// The PUSH_PROMISE frame (type=0x5) is used to notify the peer endpoint in advance of streams the
// sender intends to initiate.
//
// The PUSH_PROMISE frame defines the following flags:
//
// * END_PUSH_PROMISE (0x4):
// The END_PUSH_PROMISE bit indicates that this frame contains the entire payload necessary to
// provide a complete set of headers.
frameTypes[0x5] = 'PUSH_PROMISE';
frameFlags.PUSH_PROMISE = ['RESERVED1', 'RESERVED2', 'END_PUSH_PROMISE'];
typeSpecificAttributes.PUSH_PROMISE = ['promised_stream', 'headers', 'data'];
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |X| Promised-Stream-ID (31) |
// +-+-------------------------------------------------------------+
// | Header Block (*) ...
// +---------------------------------------------------------------+
//
// The PUSH_PROMISE frame includes the unsigned 31-bit identifier of
// the stream the endpoint plans to create along with a minimal set of headers that provide
// additional context for the stream.
Serializer.PUSH_PROMISE = function writePushPromise(frame, buffers) {
var buffer = new Buffer(4);
var promised_stream = frame.promised_stream;
assert((0 <= promised_stream) && (promised_stream <= 0x7fffffff), promised_stream);
buffer.writeUInt32BE(promised_stream, 0);
buffers.push(buffer);
buffers.push(frame.data);
};
Deserializer.PUSH_PROMISE = function readPushPromise(buffer, frame) {
frame.promised_stream = buffer.readUInt32BE(0) & 0x7fffffff;
frame.data = buffer.slice(4);
};
// [PING](http://http2.github.io/http2-spec/#PING)
// -----------------------------------------------
//
// The PING frame (type=0x6) is a mechanism for measuring a minimal round-trip time from the
// sender, as well as determining whether an idle connection is still functional.
//
// The PING frame defines one type-specific flag:
//
// * ACK (0x1):
// Bit 1 being set indicates that this PING frame is a PING response.
frameTypes[0x6] = 'PING';
frameFlags.PING = ['ACK'];
typeSpecificAttributes.PING = ['data'];
// In addition to the frame header, PING frames MUST contain 8 additional octets of opaque data.
Serializer.PING = function writePing(frame, buffers) {
buffers.push(frame.data);
};
Deserializer.PING = function readPing(buffer, frame) {
if (buffer.length !== 8) {
return 'Invalid size PING frame';
}
frame.data = buffer;
};
// [GOAWAY](http://http2.github.io/http2-spec/#GOAWAY)
// ---------------------------------------------------
//
// The GOAWAY frame (type=0x7) informs the remote peer to stop creating streams on this connection.
//
// The GOAWAY frame does not define any flags.
frameTypes[0x7] = 'GOAWAY';
frameFlags.GOAWAY = [];
typeSpecificAttributes.GOAWAY = ['last_stream', 'error'];
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// |X| Last-Stream-ID (31) |
// +-+-------------------------------------------------------------+
// | Error Code (32) |
// +---------------------------------------------------------------+
//
// The last stream identifier in the GOAWAY frame contains the highest numbered stream identifier
// for which the sender of the GOAWAY frame has received frames on and might have taken some action
// on.
//
// The GOAWAY frame also contains a 32-bit error code (see Error Codes) that contains the reason for
// closing the connection.
Serializer.GOAWAY = function writeGoaway(frame, buffers) {
var buffer = new Buffer(8);
var last_stream = frame.last_stream;
assert((0 <= last_stream) && (last_stream <= 0x7fffffff), last_stream);
buffer.writeUInt32BE(last_stream, 0);
var code = errorCodes.indexOf(frame.error);
assert((0 <= code) && (code <= 0xffffffff), code);
buffer.writeUInt32BE(code, 4);
buffers.push(buffer);
};
Deserializer.GOAWAY = function readGoaway(buffer, frame) {
frame.last_stream = buffer.readUInt32BE(0) & 0x7fffffff;
frame.error = errorCodes[buffer.readUInt32BE(4)];
};
// [WINDOW_UPDATE](http://http2.github.io/http2-spec/#WINDOW_UPDATE)
// -----------------------------------------------------------------
//
// The WINDOW_UPDATE frame (type=0x9) is used to implement flow control.
//
// The WINDOW_UPDATE frame does not define any flags.
frameTypes[0x9] = 'WINDOW_UPDATE';
frameFlags.WINDOW_UPDATE = [];
typeSpecificAttributes.WINDOW_UPDATE = ['window_size'];
// The payload of a WINDOW_UPDATE frame is a 32-bit value indicating the additional number of bytes
// that the sender can transmit in addition to the existing flow control window. The legal range
// for this field is 1 to 2^31 - 1 (0x7fffffff) bytes; the most significant bit of this value is
// reserved.
Serializer.WINDOW_UPDATE = function writeWindowUpdate(frame, buffers) {
var buffer = new Buffer(4);
var window_size = frame.window_size;
assert((0 <= window_size) && (window_size <= 0x7fffffff), window_size);
buffer.writeUInt32BE(window_size, 0);
buffers.push(buffer);
};
Deserializer.WINDOW_UPDATE = function readWindowUpdate(buffer, frame) {
frame.window_size = buffer.readUInt32BE(0) & 0x7fffffff;
};
// [CONTINUATION](http://http2.github.io/http2-spec/#CONTINUATION)
// ------------------------------------------------------------
//
// The CONTINUATION frame (type=0xA) is used to continue a sequence of header block fragments.
//
// The CONTINUATION frame defines the following flag:
//
// * END_HEADERS (0x4):
// The END_HEADERS bit indicates that this frame ends the sequence of header block fragments
// necessary to provide a complete set of headers.
frameTypes[0xA] = 'CONTINUATION';
frameFlags.CONTINUATION = ['RESERVED1', 'RESERVED2', 'END_HEADERS'];
typeSpecificAttributes.CONTINUATION = ['headers', 'data'];
Serializer.CONTINUATION = function writeContinuation(frame, buffers) {
buffers.push(frame.data);
};
Deserializer.CONTINUATION = function readContinuation(buffer, frame) {
frame.data = buffer;
};
// [Error Codes](http://http2.github.io/http2-spec/#ErrorCodes)
// ------------------------------------------------------------
var errorCodes = [
'NO_ERROR',
'PROTOCOL_ERROR',
'INTERNAL_ERROR',
'FLOW_CONTROL_ERROR',
'SETTINGS_TIMEOUT',
'STREAM_CLOSED',
'FRAME_SIZE_ERROR',
'REFUSED_STREAM',
'CANCEL',
'COMPRESSION_ERROR',
'CONNECT_ERROR'
];
errorCodes[420] = 'ENHANCE_YOUR_CALM';
// Logging
// -------
// [Bunyan serializers](https://github.com/trentm/node-bunyan#serializers) to improve logging output
// for debug messages emitted in this component.
exports.serializers = {};
// * `frame` serializer: it transforms data attributes from Buffers to hex strings and filters out
// flags that are not present.
var frameCounter = 0;
exports.serializers.frame = function(frame) {
if (!frame) {
return null;
}
if ('id' in frame) {
return frame.id;
}
frame.id = frameCounter;
frameCounter += 1;
var logEntry = { id: frame.id };
genericAttributes.concat(typeSpecificAttributes[frame.type]).forEach(function(name) {
logEntry[name] = frame[name];
});
if (frame.data instanceof Buffer) {
if (logEntry.data.length > 50) {
logEntry.data = frame.data.slice(0, 47).toString('hex') + '...';
} else {
logEntry.data = frame.data.toString('hex');
}
if (!('length' in logEntry)) {
logEntry.length = frame.data.length;
}
}
if (frame.promised_stream instanceof Object) {
logEntry.promised_stream = 'stream-' + frame.promised_stream.id;
}
logEntry.flags = Object.keys(frame.flags || {}).filter(function(name) {
return frame.flags[name] === true;
});
return logEntry;
};
// * `data` serializer: it simply transforms a buffer to a hex string.
exports.serializers.data = function(data) {
return data.toString('hex');
};

View File

@ -0,0 +1,90 @@
// [node-http2-protocol][homepage] is an implementation of the [HTTP/2 (draft 06)][http2]
// framing layer for [node.js][node].
//
// The main building blocks are [node.js streams][node-stream] that are connected through pipes.
//
// The main components are:
//
// * [Endpoint](endpoint.html): represents an HTTP/2 endpoint (client or server). It's
// responsible for the the first part of the handshake process (sending/receiving the
// [connection header][http2-connheader]) and manages other components (framer, compressor,
// connection, streams) that make up a client or server.
//
// * [Connection](connection.html): multiplexes the active HTTP/2 streams, manages connection
// lifecycle and settings, and responsible for enforcing the connection level limits (flow
// control, initiated stream limit)
//
// * [Stream](stream.html): implementation of the [HTTP/2 stream concept](http2-stream).
// Implements the [stream state machine][http2-streamstate] defined by the standard, provides
// management methods and events for using the stream (sending/receiving headers, data, etc.),
// and enforces stream level constraints (flow control, sending only legal frames).
//
// * [Flow](flow.html): implements flow control for Connection and Stream as parent class.
//
// * [Compressor and Decompressor](compressor.html): compression and decompression of HEADER and
// PUSH_PROMISE frames
//
// * [Serializer and Deserializer](framer.html): the lowest layer in the stack that transforms
// between the binary and the JavaScript object representation of HTTP/2 frames
//
// [homepage]: https://github.com/molnarg/node-http2
// [http2]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-06
// [http2-connheader]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-06#section-3.5
// [http2-stream]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-06#section-5
// [http2-streamstate]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-06#section-5.1
// [node]: http://nodejs.org/
// [node-stream]: http://nodejs.org/api/stream.html
// [node-https]: http://nodejs.org/api/https.html
// [node-http]: http://nodejs.org/api/http.html
exports.Endpoint = require('./endpoint').Endpoint;
/* Bunyan serializers exported by submodules that are worth adding when creating a logger. */
exports.serializers = {};
var modules = ['./framer', './compressor', './flow', './connection', './stream', './endpoint'];
modules.map(require).forEach(function(module) {
for (var name in module.serializers) {
exports.serializers[name] = module.serializers[name];
}
});
/*
Stream API Endpoint API
Stream data
| ^ | ^
| | | |
| | | |
+-----------|------------|---------------------------------------+
| | | Endpoint |
| | | |
| +-------|------------|-----------------------------------+ |
| | | | Connection | |
| | v | | |
| | +-----------------------+ +-------------------- | |
| | | Stream | | Stream ... | |
| | +-----------------------+ +-------------------- | |
| | | ^ | ^ | |
| | v | v | | |
| | +------------+--+--------+--+------------+- ... | |
| | | ^ | |
| | | | | |
| +-----------------------|--------|-----------------------+ |
| | | |
| v | |
| +--------------------------+ +--------------------------+ |
| | Compressor | | Decompressor | |
| +--------------------------+ +--------------------------+ |
| | ^ |
| v | |
| +--------------------------+ +--------------------------+ |
| | Serializer | | Deserializer | |
| +--------------------------+ +--------------------------+ |
| | ^ |
+---------------------------|--------|---------------------------+
| |
v |
Raw data
*/

View File

@ -0,0 +1,615 @@
var assert = require('assert');
// The Stream class
// ================
// Stream is a [Duplex stream](http://nodejs.org/api/stream.html#stream_class_stream_duplex)
// subclass that implements the [HTTP/2 Stream](http://http2.github.io/http2-spec/#rfc.section.3.4)
// concept. It has two 'sides': one that is used by the user to send/receive data (the `stream`
// object itself) and one that is used by a Connection to read/write frames to/from the other peer
// (`stream.upstream`).
var Duplex = require('stream').Duplex;
exports.Stream = Stream;
// Public API
// ----------
// * **new Stream(log)**: create a new Stream
//
// * **Event: 'headers' (headers)**: signals incoming headers
//
// * **Event: 'promise' (stream, headers)**: signals an incoming push promise
//
// * **Event: 'priority' (priority)**: signals a priority change. `priority` is a number between 0
// (highest priority) and 2^31-1 (lowest priority). Default value is 2^30.
//
// * **Event: 'error' (type)**: signals an error
//
// * **headers(headers)**: send headers
//
// * **promise(headers): Stream**: promise a stream
//
// * **priority(priority)**: set the priority of the stream. Priority can be changed by the peer
// too, but once it is set locally, it can not be changed remotely.
//
// * **reset(error)**: reset the stream with an error code
//
// * **upstream**: a [Flow](flow.js) that is used by the parent connection to write/read frames
// that are to be sent/arrived to/from the peer and are related to this stream.
//
// Headers are always in the [regular node.js header format][1].
// [1]: http://nodejs.org/api/http.html#http_message_headers
// Constructor
// -----------
// The main aspects of managing the stream are:
function Stream(log) {
Duplex.call(this);
// * logging
this._log = log.child({ component: 'stream', s: this });
// * receiving and sending stream management commands
this._initializeManagement();
// * sending and receiving frames to/from the upstream connection
this._initializeDataFlow();
// * maintaining the state of the stream (idle, open, closed, etc.) and error detection
this._initializeState();
}
Stream.prototype = Object.create(Duplex.prototype, { constructor: { value: Stream } });
// Managing the stream
// -------------------
// the default stream priority is 2^30
var DEFAULT_PRIORITY = Math.pow(2, 30);
var MAX_PRIORITY = Math.pow(2, 31) - 1;
// PUSH_PROMISE and HEADERS are forwarded to the user through events.
Stream.prototype._initializeManagement = function _initializeManagement() {
this._resetSent = false;
this._priority = DEFAULT_PRIORITY;
this._letPeerPrioritize = true;
};
Stream.prototype.promise = function promise(headers) {
var stream = new Stream(this._log);
stream._priority = Math.min(this._priority + 1, MAX_PRIORITY);
this._pushUpstream({
type: 'PUSH_PROMISE',
flags: {},
stream: this.id,
promised_stream: stream,
headers: headers
});
return stream;
};
Stream.prototype._onPromise = function _onPromise(frame) {
this.emit('promise', frame.promised_stream, frame.headers);
};
Stream.prototype.headers = function headers(headers) {
this._pushUpstream({
type: 'HEADERS',
flags: {},
stream: this.id,
headers: headers
});
};
Stream.prototype._onHeaders = function _onHeaders(frame) {
if (frame.priority !== undefined) {
this.priority(frame.priority, true);
}
this.emit('headers', frame.headers);
};
Stream.prototype.priority = function priority(priority, peer) {
if ((peer && this._letPeerPrioritize) || !peer) {
if (!peer) {
this._letPeerPrioritize = false;
var lastFrame = this.upstream.getLastQueuedFrame();
if (lastFrame && ((lastFrame.type === 'HEADERS') || (lastFrame.type === 'PRIORITY'))) {
lastFrame.priority = priority;
} else {
this._pushUpstream({
type: 'PRIORITY',
flags: {},
stream: this.id,
priority: priority
});
}
}
this._log.debug({ priority: priority }, 'Changing priority');
this.emit('priority', priority);
this._priority = priority;
}
};
Stream.prototype._onPriority = function _onPriority(frame) {
this.priority(frame.priority, true);
};
// Resetting the stream. Normally, an endpoint SHOULD NOT send more than one RST_STREAM frame for
// any stream.
Stream.prototype.reset = function reset(error) {
if (!this._resetSent) {
this._resetSent = true;
this._pushUpstream({
type: 'RST_STREAM',
flags: {},
stream: this.id,
error: error
});
}
};
// Data flow
// ---------
// The incoming and the generated outgoing frames are received/transmitted on the `this.upstream`
// [Flow](flow.html). The [Connection](connection.html) object instantiating the stream will read
// and write frames to/from it. The stream itself is a regular [Duplex stream][1], and is used by
// the user to write or read the body of the request.
// [1]: http://nodejs.org/api/stream.html#stream_class_stream_duplex
// upstream side stream user side
//
// +------------------------------------+
// | |
// +------------------+ |
// | upstream | |
// | | |
// +--+ | +--|
// read() | | _send() | _write() | | write(buf)
// <--------------|B |<--------------|--------------| B|<------------
// | | | | |
// frames +--+ | +--| buffers
// | | | | |
// -------------->|B |---------------|------------->| B|------------>
// write(frame) | | _receive() | _read() | | read()
// +--+ | +--|
// | | |
// | | |
// +------------------+ |
// | |
// +------------------------------------+
//
// B: input or output buffer
var Flow = require('./flow').Flow;
Stream.prototype._initializeDataFlow = function _initializeDataFlow() {
this.id = undefined;
this._ended = false;
this.upstream = new Flow();
this.upstream._log = this._log;
this.upstream._send = this._send.bind(this);
this.upstream._receive = this._receive.bind(this);
this.upstream.write = this._writeUpstream.bind(this);
this.upstream.on('error', this.emit.bind(this, 'error'));
this.on('finish', this._finishing);
};
Stream.prototype._pushUpstream = function _pushUpstream(frame) {
this.upstream.push(frame);
this._transition(true, frame);
};
// Overriding the upstream's `write` allows us to act immediately instead of waiting for the input
// queue to empty. This is important in case of control frames.
Stream.prototype._writeUpstream = function _writeUpstream(frame) {
this._log.debug({ frame: frame }, 'Receiving frame');
var moreNeeded = Flow.prototype.write.call(this.upstream, frame);
// * Transition to a new state if that's the effect of receiving the frame
this._transition(false, frame);
// * If it's a control frame. Call the appropriate handler method.
if (frame.type === 'HEADERS') {
this._onHeaders(frame);
} else if (frame.type === 'PUSH_PROMISE') {
this._onPromise(frame);
} else if (frame.type === 'PRIORITY') {
this._onPriority(frame);
}
// * If it's an invalid stream level frame, emit error
else if ((frame.type !== 'DATA') &&
(frame.type !== 'WINDOW_UPDATE') &&
(frame.type !== 'RST_STREAM')) {
this._log.error({ frame: frame }, 'Invalid stream level frame');
this.emit('error', 'PROTOCOL_ERROR');
}
return moreNeeded;
};
// The `_receive` method (= `upstream._receive`) gets called when there's an incoming frame.
Stream.prototype._receive = function _receive(frame, ready) {
// * If it's a DATA frame, then push the payload into the output buffer on the other side.
// Call ready when the other side is ready to receive more.
if (!this._ended && (frame.type === 'DATA')) {
var moreNeeded = this.push(frame.data);
if (!moreNeeded) {
this._receiveMore = ready;
}
}
// * Any frame may signal the end of the stream with the END_STREAM flag
if (!this._ended && (frame.flags.END_STREAM || (frame.type === 'RST_STREAM'))) {
this.push(null);
this._ended = true;
}
// * Postpone calling `ready` if `push()` returned a falsy value
if (this._receiveMore !== ready) {
ready();
}
};
// The `_read` method is called when the user side is ready to receive more data. If there's a
// pending write on the upstream, then call its pending ready callback to receive more frames.
Stream.prototype._read = function _read() {
if (this._receiveMore) {
var receiveMore = this._receiveMore;
delete this._receiveMore;
receiveMore();
}
};
// The `write` method gets called when there's a write request from the user.
Stream.prototype._write = function _write(buffer, encoding, ready) {
// * Chunking is done by the upstream Flow.
var moreNeeded = this._pushUpstream({
type: 'DATA',
flags: {},
stream: this.id,
data: buffer
});
// * Call ready when upstream is ready to receive more frames.
if (moreNeeded) {
ready();
} else {
this._sendMore = ready;
}
};
// The `_send` (= `upstream._send`) method is called when upstream is ready to receive more frames.
// If there's a pending write on the user side, then call its pending ready callback to receive more
// writes.
Stream.prototype._send = function _send() {
if (this._sendMore) {
var sendMore = this._sendMore;
delete this._sendMore;
sendMore();
}
};
// When the stream is finishing (the user calls `end()` on it), then we have to set the `END_STREAM`
// flag on the last frame. If there's no frame in the queue, or if it doesn't support this flag,
// then we create a 0 length DATA frame. We could do this all the time, but putting the flag on an
// existing frame is a nice optimization.
var emptyBuffer = new Buffer(0);
Stream.prototype._finishing = function _finishing() {
var endFrame = {
type: 'DATA',
flags: { END_STREAM: true },
stream: this.id,
data: emptyBuffer
};
var lastFrame = this.upstream.getLastQueuedFrame();
if (lastFrame && ((lastFrame.type === 'DATA') || (lastFrame.type === 'HEADERS'))) {
this._log.debug({ frame: lastFrame }, 'Marking last frame with END_STREAM flag.');
lastFrame.flags.END_STREAM = true;
this._transition(true, endFrame);
} else {
this._pushUpstream(endFrame);
}
};
// [Stream States](http://tools.ietf.org/id/draft-unicorn-httpbis-http2-01.html#StreamStates)
// ----------------
//
// +--------+
// PP | | PP
// ,--------| idle |--------.
// / | | \
// v +--------+ v
// +----------+ | +----------+
// | | | H | |
// ,---| reserved | | | reserved |---.
// | | (local) | v | (remote) | |
// | +----------+ +--------+ +----------+ |
// | | ES | | ES | |
// | | H ,-------| open |-------. | H |
// | | / | | \ | |
// | v v +--------+ v v |
// | +----------+ | +----------+ |
// | | half | | | half | |
// | | closed | | R | closed | |
// | | (remote) | | | (local) | |
// | +----------+ | +----------+ |
// | | v | |
// | | ES / R +--------+ ES / R | |
// | `----------->| |<-----------' |
// | R | closed | R |
// `-------------------->| |<--------------------'
// +--------+
// Streams begin in the IDLE state and transitions happen when there's an incoming or outgoing frame
Stream.prototype._initializeState = function _initializeState() {
this.state = 'IDLE';
this._initiated = undefined;
this._closedByUs = undefined;
this._closedWithRst = undefined;
};
// Only `_setState` should change `this.state` directly. It also logs the state change and notifies
// interested parties using the 'state' event.
Stream.prototype._setState = function transition(state) {
assert(this.state !== state);
this._log.debug({ from: this.state, to: state }, 'State transition');
this.state = state;
this.emit('state', state);
};
// A state is 'active' if the stream in that state counts towards the concurrency limit. Streams
// that are in the "open" state, or either of the "half closed" states count toward this limit.
function activeState(state) {
return ((state === 'HALF_CLOSED_LOCAL') || (state === 'HALF_CLOSED_REMOTE') || (state === 'OPEN'));
}
// `_transition` is called every time there's an incoming or outgoing frame. It manages state
// transitions, and detects stream errors. A stream error is always caused by a frame that is not
// allowed in the current state.
Stream.prototype._transition = function transition(sending, frame) {
var receiving = !sending;
var error = undefined;
var DATA = false, HEADERS = false, PRIORITY = false;
var RST_STREAM = false, PUSH_PROMISE = false, WINDOW_UPDATE = false;
switch(frame.type) {
case 'DATA' : DATA = true; break;
case 'HEADERS' : HEADERS = true; break;
case 'PRIORITY' : PRIORITY = true; break;
case 'RST_STREAM' : RST_STREAM = true; break;
case 'PUSH_PROMISE' : PUSH_PROMISE = true; break;
case 'WINDOW_UPDATE': WINDOW_UPDATE = true; break;
}
var previousState = this.state;
switch (this.state) {
// All streams start in the **idle** state. In this state, no frames have been exchanged.
//
// * Sending or receiving a HEADERS frame causes the stream to become "open".
//
// When the HEADERS frame contains the END_STREAM flags, then two state transitions happen.
case 'IDLE':
if (HEADERS) {
this._setState('OPEN');
if (frame.flags.END_STREAM) {
this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE');
}
this._initiated = sending;
} else if (sending && RST_STREAM) {
this._setState('CLOSED');
} else {
error = 'PROTOCOL_ERROR';
}
break;
// A stream in the **reserved (local)** state is one that has been promised by sending a
// PUSH_PROMISE frame.
//
// * The endpoint can send a HEADERS frame. This causes the stream to open in a "half closed
// (remote)" state.
// * Either endpoint can send a RST_STREAM frame to cause the stream to become "closed". This
// releases the stream reservation.
// * An endpoint may receive PRIORITY frame in this state.
// * An endpoint MUST NOT send any other type of frame in this state.
case 'RESERVED_LOCAL':
if (sending && HEADERS) {
this._setState('HALF_CLOSED_REMOTE');
} else if (RST_STREAM) {
this._setState('CLOSED');
} else if (receiving && PRIORITY) {
/* No state change */
} else {
error = 'PROTOCOL_ERROR';
}
break;
// A stream in the **reserved (remote)** state has been reserved by a remote peer.
//
// * Either endpoint can send a RST_STREAM frame to cause the stream to become "closed". This
// releases the stream reservation.
// * Receiving a HEADERS frame causes the stream to transition to "half closed (local)".
// * An endpoint MAY send PRIORITY frames in this state to reprioritize the stream.
// * Receiving any other type of frame MUST be treated as a stream error of type PROTOCOL_ERROR.
case 'RESERVED_REMOTE':
if (RST_STREAM) {
this._setState('CLOSED');
} else if (receiving && HEADERS) {
this._setState('HALF_CLOSED_LOCAL');
} else if (sending && PRIORITY) {
/* No state change */
} else {
error = 'PROTOCOL_ERROR';
}
break;
// The **open** state is where both peers can send frames. In this state, sending peers observe
// advertised stream level flow control limits.
//
// * From this state either endpoint can send a frame with a END_STREAM flag set, which causes
// the stream to transition into one of the "half closed" states: an endpoint sending a
// END_STREAM flag causes the stream state to become "half closed (local)"; an endpoint
// receiving a END_STREAM flag causes the stream state to become "half closed (remote)".
// * Either endpoint can send a RST_STREAM frame from this state, causing it to transition
// immediately to "closed".
case 'OPEN':
if (frame.flags.END_STREAM) {
this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE');
} else if (RST_STREAM) {
this._setState('CLOSED');
} else {
/* No state change */
}
break;
// A stream that is **half closed (local)** cannot be used for sending frames.
//
// * A stream transitions from this state to "closed" when a frame that contains a END_STREAM
// flag is received, or when either peer sends a RST_STREAM frame.
// * An endpoint MAY send or receive PRIORITY frames in this state to reprioritize the stream.
// * WINDOW_UPDATE can be sent by a peer that has sent a frame bearing the END_STREAM flag.
case 'HALF_CLOSED_LOCAL':
if (RST_STREAM || (receiving && frame.flags.END_STREAM)) {
this._setState('CLOSED');
} else if (receiving || (sending && (PRIORITY || WINDOW_UPDATE))) {
/* No state change */
} else {
error = 'PROTOCOL_ERROR';
}
break;
// A stream that is **half closed (remote)** is no longer being used by the peer to send frames.
// In this state, an endpoint is no longer obligated to maintain a receiver flow control window
// if it performs flow control.
//
// * If an endpoint receives additional frames for a stream that is in this state it MUST
// respond with a stream error of type STREAM_CLOSED.
// * A stream can transition from this state to "closed" by sending a frame that contains a
// END_STREAM flag, or when either peer sends a RST_STREAM frame.
// * An endpoint MAY send or receive PRIORITY frames in this state to reprioritize the stream.
// * A receiver MAY receive a WINDOW_UPDATE frame on a "half closed (remote)" stream.
case 'HALF_CLOSED_REMOTE':
if (RST_STREAM || (sending && frame.flags.END_STREAM)) {
this._setState('CLOSED');
} else if (sending || (receiving && (WINDOW_UPDATE || PRIORITY))) {
/* No state change */
} else {
error = 'PROTOCOL_ERROR';
}
break;
// The **closed** state is the terminal state.
//
// * An endpoint MUST NOT send frames on a closed stream. An endpoint that receives a frame
// after receiving a RST_STREAM or a frame containing a END_STREAM flag on that stream MUST
// treat that as a stream error of type STREAM_CLOSED.
// * WINDOW_UPDATE, PRIORITY or RST_STREAM frames can be received in this state for a short
// period after a frame containing an END_STREAM flag is sent. Until the remote peer receives
// and processes the frame bearing the END_STREAM flag, it might send either frame type.
// Endpoints MUST ignore WINDOW_UPDATE frames received in this state, though endpoints MAY
// choose to treat WINDOW_UPDATE frames that arrive a significant time after sending
// END_STREAM as a connection error of type PROTOCOL_ERROR.
// * If this state is reached as a result of sending a RST_STREAM frame, the peer that receives
// the RST_STREAM might have already sent - or enqueued for sending - frames on the stream
// that cannot be withdrawn. An endpoint that sends a RST_STREAM frame MUST ignore frames that
// it receives on closed streams after it has sent a RST_STREAM frame. An endpoint MAY choose
// to limit the period over which it ignores frames and treat frames that arrive after this
// time as being in error.
// * An endpoint might receive a PUSH_PROMISE frame after it sends RST_STREAM. PUSH_PROMISE
// causes a stream to become "reserved". If promised streams are not desired, a RST_STREAM
// can be used to close any of those streams.
case 'CLOSED':
if ((sending && RST_STREAM) ||
(receiving && this._closedByUs &&
(this._closedWithRst || WINDOW_UPDATE || PRIORITY || RST_STREAM))) {
/* No state change */
} else {
error = 'STREAM_CLOSED';
}
break;
}
// Noting that the connection was closed by the other endpoint. It may be important in edge cases.
// For example, when the peer tries to cancel a promised stream, but we already sent every data
// on it, then the stream is in CLOSED state, yet we want to ignore the incoming RST_STREAM.
if ((this.state === 'CLOSED') && (previousState !== 'CLOSED')) {
this._closedByUs = sending;
this._closedWithRst = RST_STREAM;
}
// Sending/receiving a PUSH_PROMISE
//
// * Sending a PUSH_PROMISE frame marks the associated stream for later use. The stream state
// for the reserved stream transitions to "reserved (local)".
// * Receiving a PUSH_PROMISE frame marks the associated stream as reserved by the remote peer.
// The state of the stream becomes "reserved (remote)".
if (PUSH_PROMISE && !error) {
/* This assertion must hold, because _transition is called immediately when a frame is written
to the stream. If it would be called when a frame gets out of the input queue, the state
of the reserved could have been changed by then. */
assert(frame.promised_stream.state === 'IDLE', frame.promised_stream.state);
frame.promised_stream._setState(sending ? 'RESERVED_LOCAL' : 'RESERVED_REMOTE');
frame.promised_stream._initiated = sending;
}
// Signaling how sending/receiving this frame changes the active stream count (-1, 0 or +1)
if (this._initiated) {
var change = (activeState(this.state) - activeState(previousState));
if (sending) {
frame.count_change = change;
} else {
frame.count_change(change);
}
} else if (sending) {
frame.count_change = 0;
}
// Common error handling.
if (error) {
var info = {
error: error,
frame: frame,
state: this.state,
closedByUs: this._closedByUs,
closedWithRst: this._closedWithRst
};
// * When sending something invalid, throwing an exception, since it is probably a bug.
if (sending) {
this._log.error(info, 'Sending illegal frame.');
throw new Error('Sending illegal frame (' + frame.type + ') in ' + this.state + ' state.');
}
// * When receiving something invalid, sending an RST_STREAM using the `reset` method.
// This will automatically cause a transition to the CLOSED state.
else {
this._log.error(info, 'Received illegal frame.');
this.emit('error', error);
}
}
};
// Bunyan serializers
// ------------------
exports.serializers = {};
var nextId = 0;
exports.serializers.s = function(stream) {
if (!('_id' in stream)) {
stream._id = nextId;
nextId += 1;
}
return stream._id;
};

View File

@ -0,0 +1,52 @@
{
"name": "http2-protocol",
"version": "0.7.0",
"description": "A JavaScript implementation of the HTTP/2 framing layer",
"main": "lib/index.js",
"engines": {
"node": "0.10.x"
},
"devDependencies": {
"istanbul": "*",
"chai": "*",
"mocha": "*",
"docco": "*",
"bunyan": "*"
},
"scripts": {
"test": "istanbul test _mocha -- --reporter spec --slow 200",
"prepublish": "docco lib/* --output doc --layout parallel --css doc/docco.css"
},
"repository": {
"type": "git",
"url": "https://github.com/molnarg/node-http2-protocol.git"
},
"homepage": "https://github.com/molnarg/node-http2-protocol",
"bugs": {
"url": "https://github.com/molnarg/node-http2-protocol/issues"
},
"keywords": [
"http",
"http2",
"client",
"server"
],
"author": {
"name": "Gábor Molnár",
"email": "gabor@molnar.es",
"url": "http://gabor.molnar.es"
},
"contributors": [
{
"name": "Nick Hurley"
},
{
"name": "Mike Belshe"
}
],
"license": "MIT",
"readmeFilename": "README.md",
"readme": "node-http2-protocol\n===================\n\nAn HTTP/2 ([draft-ietf-httpbis-http2-07](http://tools.ietf.org/html/draft-ietf-httpbis-http2-07))\nframing layer implementaion for node.js.\n\nInstallation\n------------\n\n```\nnpm install http2-protocol\n```\n\nExamples\n--------\n\nAPI\n---\n\nDevelopment\n-----------\n\n### Development dependencies ###\n\nThere's a few library you will need to have installed to do anything described in the following\nsections. After installing/cloning node-http2, run `npm install` in its directory to install\ndevelopment dependencies.\n\nUsed libraries:\n\n* [mocha](http://visionmedia.github.io/mocha/) for tests\n* [chai](http://chaijs.com/) for assertions\n* [istanbul](https://github.com/gotwarlost/istanbul) for code coverage analysis\n* [docco](http://jashkenas.github.io/docco/) for developer documentation\n* [bunyan](https://github.com/trentm/node-bunyan) for logging\n\nFor pretty printing logs, you will also need a global install of bunyan (`npm install -g bunyan`).\n\n### Developer documentation ###\n\nThe developer documentation is located in the `doc` directory. The docs are usually updated only\nbefore releasing a new version. To regenerate them manually, run `npm run-script prepublish`.\nThere's a hosted version which is located [here](http://molnarg.github.io/node-http2/doc/).\n\n### Running the tests ###\n\nIt's easy, just run `npm test`. The tests are written in BDD style, so they are a good starting\npoint to understand the code.\n\n### Test coverage ###\n\nTo generate a code coverage report, run `npm test --coverage` (it may be slow, be patient).\nCode coverage summary as of version 0.6.0:\n```\nStatements : 92.39% ( 1165/1261 )\nBranches : 86.57% ( 477/551 )\nFunctions : 91.22% ( 135/148 )\nLines : 92.35% ( 1159/1255 )\n```\n\nThere's a hosted version of the detailed (line-by-line) coverage report\n[here](http://molnarg.github.io/node-http2-protocol/coverage/lcov-report/lib/).\n\n### Logging ###\n\nContributors\n------------\n\nCode contributions are always welcome! People who contributed to node-http2 so far:\n\n* Nick Hurley\n* Mike Belshe\n\nSpecial thanks to Google for financing the development of this module as part of their [Summer of\nCode program](https://developers.google.com/open-source/soc/) (project: [HTTP/2 prototype server\nimplementation](https://google-melange.appspot.com/gsoc/project/google/gsoc2013/molnarg/5001)), and\nNick Hurley of Mozilla, my GSoC mentor, who helped with regular code review and technical advices.\n\nLicense\n-------\n\nThe MIT License\n\nCopyright (C) 2013 Gábor Molnár <gabor@molnar.es>\n",
"_id": "http2-protocol@0.7.0",
"_from": "http2-protocol@0.7.x"
}

View File

@ -0,0 +1,427 @@
var expect = require('chai').expect;
var util = require('./util');
var compressor = require('../lib/compressor');
var HeaderTable = compressor.HeaderTable;
var HuffmanTable = compressor.HuffmanTable;
var HeaderSetCompressor = compressor.HeaderSetCompressor;
var HeaderSetDecompressor = compressor.HeaderSetDecompressor;
var Compressor = compressor.Compressor;
var Decompressor = compressor.Decompressor;
var test_integers = [{
N: 5,
I: 10,
buffer: new Buffer([10])
}, {
N: 0,
I: 10,
buffer: new Buffer([10])
}, {
N: 5,
I: 1337,
buffer: new Buffer([31, 128 + 26, 10])
}, {
N: 0,
I: 1337,
buffer: new Buffer([128 + 57, 10])
}];
var test_strings = [{
string: 'www.foo.com',
buffer: new Buffer('88db6d898b5a44b74f', 'hex')
}, {
string: 'éáűőúöüó€',
buffer: new Buffer('13C3A9C3A1C5B1C591C3BAC3B6C3BCC3B3E282AC', 'hex')
}];
test_huffman_request = {
'GET': 'f77778ff',
'http': 'ce3177',
'/': '0f',
'www.foo.com': 'db6d898b5a44b74f',
'https': 'ce31743f',
'www.bar.com': 'db6d897a1e44b74f',
'no-cache': '63654a1398ff',
'/custom-path.css': '04eb08b7495c88e644c21f',
'custom-key': '4eb08b749790fa7f',
'custom-value': '4eb08b74979a17a8ff'
};
test_huffman_response = {
'302': '409f',
'private': 'c31b39bf387f',
'Mon, 21 OCt 2013 20:13:21 GMT': 'a2fba20320f2ebcc0c490062d2434c827a1d',
': https://www.bar.com': '6871cf3c326ebd7e9e9e926e7e32557dbf',
'200': '311f',
'Mon, 21 OCt 2013 20:13:22 GMT': 'a2fba20320f2ebcc0c490062d2434cc27a1d',
'https://www.bar.com': 'e39e7864dd7afd3d3d24dcfc64aafb7f',
'gzip': 'e1fbb30f',
'foo=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\
AAAAAAAAAAAAAAAAAAAAAAAAAALASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKHQWOEIUAL\
QWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKH\
QWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEO\
IUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOP\
IUAXQWEOIUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234ZZZZZZZZZZ\
ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ1234 m\
ax-age=3600; version=1': 'df7dfb36eddbb76eddbb76eddbb76eddbb76eddbb76eddbb76\
eddbb76eddbb76eddbb76eddbb76eddbb76eddbb76eddbb76eddbb76eddbb76eddbb\
76eddbb76eddbb7e3b69ecf0fe7e1fd7f3d5fe7f7e5fd79f6f97cbbfe9b7fbfebcfb\
7cbbfe9b7fbf8f87f3f0febcfcbb7bfe9b7e3fd79f6f977fd36ff7f1febb7e9b7fbf\
8fc7f9f0db4f67f5e7dbe5f4efdbfdf891a13f1db4f6787f3f0febf9eaff3fbf2feb\
cfb7cbe5dff4dbfdff5e7dbe5dff4dbfdfc7c3f9f87f5e7e5dbdff4dbf1febcfb7cb\
bfe9b7fbf8ff5dbf4dbfdfc7e3fcf86da7b3faf3edf2fa77edfefc48d09f8eda7b3c\
3f9f87f5fcf57f9fdf97f5e7dbe5f2effa6dfeffaf3edf2effa6dfefe3e1fcfc3faf\
3f2edeffa6df8ff5e7dbe5dff4dbfdfc7faedfa6dfefe3f1fe7c36d3d9fd79f6f97d\
3bf6ff7e24684fc76d3d9e1fcfc3fafe7abfcfefcbfaf3edf2f977fd36ff7fd79f6f\
977fd36ff7f1f0fe7e1fd79f976f7fd36fc7faf3edf2effa6dfefe3fd76fd36ff7f1\
f8ff3e1b69ecfebcfb7cbe9dfb7fbf123427fcff3fcff3fcff3fcff3fcff3fcff3fc\
ff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3\
fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcf\
f3fcff3fcff08d090b5fd237f086c44a23ef0e70c72b2fbb617f',
'foo=ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ\
ZZZZZZZZZZZZZZZZZZZZZZZZZZLASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKHQWOEIUAL\
QWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEOIUAXLJKH\
QWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOPIUAXQWEO\
IUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234LASDJKHQKBZXOQWEOP\
IUAXQWEOIUAXLJKHQWOEIUALQWEOIUAXLQEUAXLLKJASDQWEOUIAXN1234AAAAAAAAAA\
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA1234 m\
ax-age=3600; version=1': 'df7dfb3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcf\
f3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3f\
cff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff3fcff\
3e3b69ecf0fe7e1fd7f3d5fe7f7e5fd79f6f97cbbfe9b7fbfebcfb7cbbfe9b7fbf8f\
87f3f0febcfcbb7bfe9b7e3fd79f6f977fd36ff7f1febb7e9b7fbf8fc7f9f0db4f67\
f5e7dbe5f4efdbfdf891a13f1db4f6787f3f0febf9eaff3fbf2febcfb7cbe5dff4db\
fdff5e7dbe5dff4dbfdfc7c3f9f87f5e7e5dbdff4dbf1febcfb7cbbfe9b7fbf8ff5d\
bf4dbfdfc7e3fcf86da7b3faf3edf2fa77edfefc48d09f8eda7b3c3f9f87f5fcf57f\
9fdf97f5e7dbe5f2effa6dfeffaf3edf2effa6dfefe3e1fcfc3faf3f2edeffa6df8f\
f5e7dbe5dff4dbfdfc7faedfa6dfefe3f1fe7c36d3d9fd79f6f97d3bf6ff7e24684f\
c76d3d9e1fcfc3fafe7abfcfefcbfaf3edf2f977fd36ff7fd79f6f977fd36ff7f1f0\
fe7e1fd79f976f7fd36fc7faf3edf2effa6dfefe3fd76fd36ff7f1f8ff3e1b69ecfe\
bcfb7cbe9dfb7fbf1234276eddbb76eddbb76eddbb76eddbb76eddbb76eddbb76edd\
bb76eddbb76eddbb76eddbb76eddbb76eddbb76eddbb76eddbb76eddbb76eddbb76e\
ddbb76eddbb48d090b5fd237f086c44a23ef0e70c72b2fbb617f'
};
var test_headers = [{
header: {
name: 1,
value: 'GET',
index: true
},
buffer: new Buffer('02' + '03474554', 'hex')
}, {
header: {
name: 6,
value: 'http',
index: true
},
buffer: new Buffer('07' + '83ce3177', 'hex')
}, {
header: {
name: 5,
value: '/',
index: true
},
buffer: new Buffer('06' + '012f', 'hex')
}, {
header: {
name: 3,
value: 'www.foo.com',
index: true
},
buffer: new Buffer('04' + '88db6d898b5a44b74f', 'hex')
}, {
header: {
name: 2,
value: 'https',
index: true
},
buffer: new Buffer('03' + '84ce31743f', 'hex')
}, {
header: {
name: 1,
value: 'www.bar.com',
index: true
},
buffer: new Buffer('02' + '88db6d897a1e44b74f', 'hex')
}, {
header: {
name: 28,
value: 'no-cache',
index: true
},
buffer: new Buffer('1d' + '8663654a1398ff', 'hex')
}, {
header: {
name: 3,
value: 3,
index: false
},
buffer: new Buffer('83', 'hex')
}, {
header: {
name: 5,
value: 5,
index: false
},
buffer: new Buffer('85', 'hex')
}, {
header: {
name: 4,
value: '/custom-path.css',
index: true
},
buffer: new Buffer('05' + '8b04eb08b7495c88e644c21f', 'hex')
}, {
header: {
name: 'custom-key',
value: 'custom-value',
index: true
},
buffer: new Buffer('00' + '884eb08b749790fa7f' + '894eb08b74979a17a8ff', 'hex')
}, {
header: {
name: 2,
value: 2,
index: false
},
buffer: new Buffer('82', 'hex')
}, {
header: {
name: 6,
value: 6,
index: false
},
buffer: new Buffer('86', 'hex')
}];
var test_header_sets = [{
headers: {
':method': 'GET',
':scheme': 'http',
':path': '/',
':authority': 'www.foo.com'
},
buffer: util.concat(test_headers.slice(0, 4).map(function(test) { return test.buffer; }))
}, {
headers: {
':method': 'GET',
':scheme': 'https',
':path': '/',
':authority': 'www.bar.com',
'cache-control': 'no-cache'
},
buffer: util.concat(test_headers.slice(4, 9).map(function(test) { return test.buffer; }))
}, {
headers: {
':method': 'GET',
':scheme': 'https',
':path': '/custom-path.css',
':authority': 'www.bar.com',
'custom-key': 'custom-value'
},
buffer: util.concat(test_headers.slice(9, 13).map(function(test) { return test.buffer; }))
}, {
headers: {
':method': 'GET',
':scheme': 'https',
':path': '/custom-path.css',
':authority': ['www.foo.com', 'www.bar.com'],
'custom-key': 'custom-value'
},
buffer: test_headers[3].buffer
}, {
headers: {
':status': '200',
'user-agent': 'my-user-agent',
'cookie': ['first', 'second', 'third', 'third'],
'verylong': (new Buffer(9000)).toString('hex')
}
}];
describe('compressor.js', function() {
describe('HeaderTable', function() {
});
describe('HuffmanTable', function() {
describe('method encode(buffer)', function() {
it('should return the Huffman encoded version of the input buffer', function() {
var table = HuffmanTable.requestHuffmanTable;
for (var decoded in test_huffman_request) {
var encoded = test_huffman_request[decoded];
expect(table.encode(new Buffer(decoded)).toString('hex')).to.equal(encoded);
}
table = HuffmanTable.responseHuffmanTable;
for (decoded in test_huffman_response) {
encoded = test_huffman_response[decoded];
expect(table.encode(new Buffer(decoded)).toString('hex')).to.equal(encoded);
}
});
})
describe('method decode(buffer)', function() {
it('should return the Huffman decoded version of the input buffer', function() {
var table = HuffmanTable.requestHuffmanTable;
for (var decoded in test_huffman_request) {
var encoded = test_huffman_request[decoded];
expect(table.decode(new Buffer(encoded, 'hex')).toString()).to.equal(decoded)
}
table = HuffmanTable.responseHuffmanTable;
for (decoded in test_huffman_response) {
encoded = test_huffman_response[decoded];
expect(table.decode(new Buffer(encoded, 'hex')).toString()).to.equal(decoded)
}
});
})
});
describe('HeaderSetCompressor', function() {
describe('static method .integer(I, N)', function() {
it('should return an array of buffers that represent the N-prefix coded form of the integer I', function() {
for (var i = 0; i < test_integers.length; i++) {
var test = test_integers[i];
test.buffer.cursor = 0;
expect(util.concat(HeaderSetCompressor.integer(test.I, test.N))).to.deep.equal(test.buffer);
}
});
});
describe('static method .string(string)', function() {
it('should return an array of buffers that represent the encoded form of the string', function() {
var table = HuffmanTable.requestHuffmanTable;
for (var i = 0; i < test_strings.length; i++) {
var test = test_strings[i];
expect(util.concat(HeaderSetCompressor.string(test.string, table))).to.deep.equal(test.buffer);
}
});
});
describe('static method .header({ name, value, index })', function() {
it('should return an array of buffers that represent the encoded form of the header', function() {
var table = HuffmanTable.requestHuffmanTable;
for (var i = 0; i < test_headers.length; i++) {
var test = test_headers[i];
expect(util.concat(HeaderSetCompressor.header(test.header, table))).to.deep.equal(test.buffer);
}
});
});
});
describe('HeaderSetDecompressor', function() {
describe('static method .integer(buffer, N)', function() {
it('should return the parsed N-prefix coded number and increase the cursor property of buffer', function() {
for (var i = 0; i < test_integers.length; i++) {
var test = test_integers[i];
test.buffer.cursor = 0;
expect(HeaderSetDecompressor.integer(test.buffer, test.N)).to.equal(test.I);
expect(test.buffer.cursor).to.equal(test.buffer.length);
}
});
});
describe('static method .string(buffer)', function() {
it('should return the parsed string and increase the cursor property of buffer', function() {
var table = HuffmanTable.requestHuffmanTable;
for (var i = 0; i < test_strings.length; i++) {
var test = test_strings[i];
test.buffer.cursor = 0;
expect(HeaderSetDecompressor.string(test.buffer, table)).to.equal(test.string);
expect(test.buffer.cursor).to.equal(test.buffer.length);
}
});
});
describe('static method .header(buffer)', function() {
it('should return the parsed header and increase the cursor property of buffer', function() {
var table = HuffmanTable.requestHuffmanTable;
for (var i = 0; i < test_headers.length; i++) {
var test = test_headers[i];
test.buffer.cursor = 0;
expect(HeaderSetDecompressor.header(test.buffer, table)).to.deep.equal(test.header);
expect(test.buffer.cursor).to.equal(test.buffer.length);
}
});
});
});
describe('Decompressor', function() {
describe('method decompress(buffer)', function() {
it('should return the parsed header set in { name1: value1, name2: [value2, value3], ... } format', function() {
var decompressor = new Decompressor(util.log, 'REQUEST');
for (var i = 0; i < 4; i++) {
var header_set = test_header_sets[i];
expect(decompressor.decompress(header_set.buffer)).to.deep.equal(header_set.headers);
}
});
});
describe('transform stream', function() {
it('should emit an error event if a series of header frames is interleaved with other frames', function() {
var decompressor = new Decompressor(util.log, 'REQUEST');
var error_occured = false;
decompressor.on('error', function() {
error_occured = true;
});
decompressor.write({
type: 'HEADERS',
flags: {
END_HEADERS: false
},
data: new Buffer(5)
});
decompressor.write({
type: 'DATA',
flags: {},
data: new Buffer(5)
});
expect(error_occured).to.be.equal(true);
});
});
});
describe('invariant', function() {
describe('decompressor.decompress(compressor.compress(headerset)) === headerset', function() {
it('should be true for any header set if the states are synchronized', function() {
var compressor = new Compressor(util.log, 'REQUEST');
var decompressor = new Decompressor(util.log, 'REQUEST');
var n = test_header_sets.length;
for (var i = 0; i < 10; i++) {
var headers = test_header_sets[i%n].headers;
var compressed = compressor.compress(headers);
var decompressed = decompressor.decompress(compressed);
expect(decompressed).to.deep.equal(headers);
expect(compressor._table).to.deep.equal(decompressor._table);
}
});
});
describe('source.pipe(compressor).pipe(decompressor).pipe(destination)', function() {
it('should behave like source.pipe(destination) for a stream of frames', function(done) {
var compressor = new Compressor(util.log, 'RESPONSE');
var decompressor = new Decompressor(util.log, 'RESPONSE');
var n = test_header_sets.length;
compressor.pipe(decompressor);
for (var i = 0; i < 10; i++) {
compressor.write({
type: i%2 ? 'HEADERS' : 'PUSH_PROMISE',
flags: {},
headers: test_header_sets[i%n].headers
});
}
setTimeout(function() {
for (var j = 0; j < 10; j++) {
expect(decompressor.read().headers).to.deep.equal(test_header_sets[j%n].headers);
}
done();
}, 10);
});
});
describe('huffmanTable.decompress(huffmanTable.compress(buffer)) === buffer', function() {
it('should be true for any buffer', function() {
for (var i = 0; i < 10; i++) {
var buffer = [];
while (Math.random() > 0.1) {
buffer.push(Math.floor(Math.random() * 256))
}
buffer = new Buffer(buffer);
var table = HuffmanTable.requestHuffmanTable;
var result = table.decode(table.encode(buffer));
expect(result).to.deep.equal(buffer);
}
})
})
});
});

View File

@ -0,0 +1,280 @@
var expect = require('chai').expect;
var util = require('./util');
var Connection = require('../lib/connection').Connection;
var settings = {
SETTINGS_MAX_CONCURRENT_STREAMS: 100,
SETTINGS_INITIAL_WINDOW_SIZE: 100000
};
var MAX_PRIORITY = Math.pow(2, 31) - 1;
var MAX_RANDOM_PRIORITY = 10;
function randomPriority() {
return Math.floor(Math.random() * (MAX_RANDOM_PRIORITY + 1));
}
function expectPriorityOrder(priorities) {
priorities.forEach(function(bucket, priority) {
bucket.forEach(function(stream) {
expect(stream._priority).to.be.equal(priority);
});
});
}
describe('connection.js', function() {
describe('Connection class', function() {
describe('method ._insert(stream)', function() {
it('should insert the stream in _streamPriorities in a place determined by stream._priority', function() {
var streams = [];
var connection = Object.create(Connection.prototype, { _streamPriorities: { value: streams }});
var streamCount = 10;
for (var i = 0; i < streamCount; i++) {
var stream = { _priority: randomPriority() };
connection._insert(stream, stream._priority);
expect(connection._streamPriorities[stream._priority]).to.include(stream);
}
expectPriorityOrder(connection._streamPriorities);
});
});
describe('method ._reprioritize(stream)', function() {
it('should eject and then insert the stream in _streamPriorities in a place determined by stream._priority', function() {
var streams = [];
var connection = Object.create(Connection.prototype, { _streamPriorities: { value: streams }});
var streamCount = 10;
var oldPriority, newPriority, stream;
for (var i = 0; i < streamCount; i++) {
oldPriority = randomPriority();
while ((newPriority = randomPriority()) === oldPriority);
stream = { _priority: oldPriority };
connection._insert(stream, oldPriority);
connection._reprioritize(stream, newPriority);
stream._priority = newPriority;
expect(connection._streamPriorities[newPriority]).to.include(stream);
expect(connection._streamPriorities[oldPriority] || []).to.not.include(stream);
}
expectPriorityOrder(streams);
});
});
describe('invalid operation', function() {
describe('disabling and the re-enabling flow control', function() {
it('should result in an error event with type "FLOW_CONTROL_ERROR"', function(done) {
var connection = new Connection(util.log, 1, settings);
connection.on('error', function(error) {
expect(error).to.equal('FLOW_CONTROL_ERROR');
done();
});
connection._setLocalFlowControl(true);
connection._setLocalFlowControl(false);
});
});
describe('manipulating flow control window after flow control was turned off', function() {
it('should result in an error event with type "FLOW_CONTROL_ERROR"', function(done) {
var connection = new Connection(util.log, 1, settings);
connection.on('error', function(error) {
expect(error).to.equal('FLOW_CONTROL_ERROR');
done();
});
connection._setLocalFlowControl(true);
connection._setInitialStreamWindowSize(10);
});
});
describe('disabling flow control twice', function() {
it('should be ignored', function() {
var connection = new Connection(util.log, 1, settings);
connection._setLocalFlowControl(true);
connection._setLocalFlowControl(true);
});
});
describe('enabling flow control when already enabled', function() {
it('should be ignored', function() {
var connection = new Connection(util.log, 1, settings);
connection._setLocalFlowControl(false);
});
});
describe('unsolicited ping answer', function() {
it('should be ignored', function() {
var connection = new Connection(util.log, 1, settings);
connection._receivePing({
stream: 0,
type: 'PING',
flags: {
'PONG': true
},
data: new Buffer(8)
});
});
});
});
});
describe('test scenario', function() {
var c, s;
beforeEach(function() {
c = new Connection(util.log.child({ role: 'client' }), 1, settings);
s = new Connection(util.log.child({ role: 'client' }), 2, settings);
c.pipe(s).pipe(c);
});
describe('connection setup', function() {
it('should work as expected', function(done) {
setTimeout(function() {
// If there are no exception until this, then we're done
done();
}, 10);
});
});
describe('sending/receiving a request', function() {
it('should work as expected', function(done) {
// Request and response data
var request_headers = {
':method': 'GET',
':path': '/'
};
var request_data = new Buffer(0);
var response_headers = {
':status': '200'
};
var response_data = new Buffer('12345678', 'hex');
// Setting up server
s.on('stream', function(server_stream) {
server_stream.on('headers', function(headers) {
expect(headers).to.deep.equal(request_headers);
server_stream.headers(response_headers);
server_stream.end(response_data);
});
});
// Sending request
var client_stream = c.createStream();
client_stream.headers(request_headers);
client_stream.end(request_data);
// Waiting for answer
done = util.callNTimes(2, done);
client_stream.on('headers', function(headers) {
expect(headers).to.deep.equal(response_headers);
done();
});
client_stream.on('readable', function() {
expect(client_stream.read()).to.deep.equal(response_data);
done();
});
});
});
describe('server push', function() {
it('should work as expected', function(done) {
var request_headers = { ':method': 'get', ':path': '/' };
var response_headers = { ':status': '200' };
var push_request_headers = { ':method': 'get', ':path': '/x' };
var push_response_headers = { ':status': '200' };
var response_content = new Buffer(10);
var push_content = new Buffer(10);
done = util.callNTimes(5, done);
s.on('stream', function(response) {
response.headers(response_headers);
var pushed = response.promise(push_request_headers);
pushed.headers(push_response_headers);
pushed.end(push_content);
response.end(response_content);
});
var request = c.createStream();
request.headers(request_headers);
request.end();
request.on('headers', function(headers) {
expect(headers).to.deep.equal(response_headers);
done();
});
request.on('readable', function() {
expect(request.read()).to.deep.equal(response_content);
done();
});
request.on('promise', function(pushed, headers) {
expect(headers).to.deep.equal(push_request_headers);
pushed.on('headers', function(headers) {
expect(headers).to.deep.equal(response_headers);
done();
});
pushed.on('readable', function() {
expect(pushed.read()).to.deep.equal(push_content);
done();
});
pushed.on('end', function() {
done();
});
});
});
});
describe('ping from client', function() {
it('should work as expected', function(done) {
c.ping(function() {
done();
});
});
});
describe('ping from server', function() {
it('should work as expected', function(done) {
s.ping(function() {
done();
});
});
});
describe('creating two streams and then using them in reverse order', function() {
it('should not result in non-monotonous local ID ordering', function() {
var s1 = c.createStream();
var s2 = c.createStream();
s2.headers({ ':method': 'get', ':path': '/' });
s1.headers({ ':method': 'get', ':path': '/' });
});
});
describe('creating two promises and then using them in reverse order', function() {
it('should not result in non-monotonous local ID ordering', function(done) {
s.on('stream', function(response) {
response.headers({ ':status': '200' });
var p1 = s.createStream();
var p2 = s.createStream();
response.promise(p2, { ':method': 'get', ':path': '/p2' });
response.promise(p1, { ':method': 'get', ':path': '/p1' });
p2.headers({ ':status': '200' });
p1.headers({ ':status': '200' });
});
var request = c.createStream();
request.headers({ ':method': 'get', ':path': '/' });
done = util.callNTimes(2, done);
request.on('promise', function() {
done();
});
});
});
describe('closing the connection on one end', function() {
it('should result in closed streams on both ends', function(done) {
done = util.callNTimes(2, done);
c.on('end', done);
s.on('end', done);
c.close();
});
});
});
});

View File

@ -0,0 +1,41 @@
var expect = require('chai').expect;
var util = require('./util');
var endpoint = require('../lib/endpoint');
var Endpoint = endpoint.Endpoint;
var settings = {
SETTINGS_MAX_CONCURRENT_STREAMS: 100,
SETTINGS_INITIAL_WINDOW_SIZE: 100000
};
describe('endpoint.js', function() {
describe('scenario', function() {
describe('connection setup', function() {
it('should work as expected', function(done) {
var c = new Endpoint(util.log.child({ role: 'client' }), 'CLIENT', settings);
var s = new Endpoint(util.log.child({ role: 'client' }), 'SERVER', settings);
util.log.debug('Test initialization over, starting piping.');
c.pipe(s).pipe(c);
setTimeout(function() {
// If there are no exception until this, then we're done
done();
}, 10);
});
});
});
describe('bunyan serializer', function() {
describe('`e`', function() {
var format = endpoint.serializers.e;
it('should assign a unique ID to each endpoint', function() {
var c = new Endpoint(util.log.child({ role: 'client' }), 'CLIENT', settings);
var s = new Endpoint(util.log.child({ role: 'client' }), 'SERVER', settings);
expect(format(c)).to.not.equal(format(s));
expect(format(c)).to.equal(format(c));
expect(format(s)).to.equal(format(s));
});
});
});
});

View File

@ -0,0 +1,210 @@
var expect = require('chai').expect;
var util = require('./util');
var Flow = require('../lib/flow').Flow;
function createFlow(log) {
var flowControlId = util.random(10, 100);
var flow = new Flow(flowControlId);
flow._log = util.log.child(log || {});
return flow;
}
describe('flow.js', function() {
describe('Flow class', function() {
var flow;
beforeEach(function() {
flow = createFlow();
});
describe('._receive(frame, callback) method', function() {
it('is called when there\'s a frame in the input buffer to be consumed', function(done) {
var frame = { type: 'PRIORITY', flags: {}, priority: 1 };
flow._receive = function _receive(receivedFrame, callback) {
expect(receivedFrame).to.equal(frame);
callback();
};
flow.write(frame, done);
});
it('has to be overridden by the child class, otherwise it throws', function() {
expect(flow._receive.bind(flow)).to.throw(Error);
});
});
describe('._send() method', function() {
it('is called when the output buffer should be filled with more frames and the flow' +
'control queue is empty', function() {
var sendCalled = 0;
var notFlowControlledFrame = { type: 'PRIORITY', flags: {}, priority: 1 };
flow._send = function _send() {
sendCalled += 1;
this.push(notFlowControlledFrame);
};
expect(flow.read()).to.equal(notFlowControlledFrame);
flow._window = 0;
flow._queue.push({ type: 'DATA', flags: {}, data: { length: 1 } });
expect(flow.read()).to.equal(null);
expect(sendCalled).to.equal(1);
});
it('has to be overridden by the child class, otherwise it throws', function() {
expect(flow._send.bind(flow)).to.throw(Error);
});
});
describe('._increaseWindow(size) method', function() {
it('should increase `this._window` by `size`', function() {
flow._send = util.noop;
flow._window = 0;
var increase1 = util.random(0,100);
var increase2 = util.random(0,100);
flow._increaseWindow(increase1);
flow._increaseWindow(increase2);
expect(flow._window).to.equal(increase1 + increase2);
flow._increaseWindow(Infinity);
expect(flow._window).to.equal(Infinity);
});
it('should emit error when increasing with a finite `size` when `_window` is infinite', function() {
flow._send = util.noop;
flow._increaseWindow(Infinity);
var increase = util.random(1,100);
expect(flow._increaseWindow.bind(flow, increase)).to.throw('Uncaught, unspecified "error" event.');
});
it('should emit error when `_window` grows over the window limit', function() {
var WINDOW_SIZE_LIMIT = Math.pow(2, 31) - 1;
flow._send = util.noop;
flow._window = 0;
flow._increaseWindow(WINDOW_SIZE_LIMIT);
expect(flow._increaseWindow.bind(flow, 1)).to.throw('Uncaught, unspecified "error" event.');
});
});
describe('.disableLocalFlowControl() method', function() {
it('should increase `this._window` by Infinity', function() {
flow._send = util.noop;
flow.disableLocalFlowControl();
expect(flow._window).to.equal(Infinity);
});
});
describe('.read() method', function() {
describe('when the flow control queue is not empty', function() {
it('should return the first item in the queue if the window is enough', function() {
var priorityFrame = { type: 'PRIORITY', flags: {}, priority: 1 };
var dataFrame = { type: 'DATA', flags: {}, data: { length: 10 } };
flow._send = util.noop;
flow._window = 10;
flow._queue = [priorityFrame, dataFrame];
expect(flow.read()).to.equal(priorityFrame);
expect(flow.read()).to.equal(dataFrame);
});
it('should also split DATA frames when needed', function() {
var buffer = new Buffer(10);
var dataFrame = { type: 'DATA', flags: {}, stream: util.random(0, 100), data: buffer };
flow._send = util.noop;
flow._window = 5;
flow._queue = [dataFrame];
var expectedFragment = { flags: {}, type: 'DATA', stream: dataFrame.stream, data: buffer.slice(0,5) };
expect(flow.read()).to.deep.equal(expectedFragment);
expect(dataFrame.data).to.deep.equal(buffer.slice(5));
});
});
});
describe('.push(frame) method', function() {
it('should push `frame` into the output queue or the flow control queue', function() {
var priorityFrame = { type: 'PRIORITY', flags: {}, priority: 1 };
var dataFrame = { type: 'DATA', flags: {}, data: { length: 10 } };
flow._window = 10;
flow.push(dataFrame); // output queue
flow.push(dataFrame); // flow control queue, because of depleted window
flow.push(priorityFrame); // flow control queue, because it's not empty
expect(flow.read()).to.be.equal(dataFrame);
expect(flow._queue[0]).to.be.equal(dataFrame);
expect(flow._queue[1]).to.be.equal(priorityFrame);
});
});
describe('.write() method', function() {
it('call with a DATA frame should trigger sending WINDOW_UPDATE if remote flow control is not' +
'disabled', function(done) {
flow._remoteFlowControlDisabled = false;
flow._window = 100;
flow._send = util.noop;
flow._receive = function(frame, callback) {
callback();
};
var buffer = new Buffer(util.random(10, 100));
flow.write({ type: 'DATA', flags: {}, data: buffer });
flow.once('readable', function() {
expect(flow.read()).to.be.deep.equal({
type: 'WINDOW_UPDATE',
flags: {},
stream: flow._flowControlId,
window_size: buffer.length
});
done();
});
});
});
});
describe('test scenario', function() {
var flow1, flow2;
beforeEach(function() {
flow1 = createFlow({ flow: 1 });
flow2 = createFlow({ flow: 2 });
flow1._flowControlId = flow2._flowControlId;
flow1._remoteFlowControlDisabled = flow2._remoteFlowControlDisabled = false;
flow1._send = flow2._send = util.noop;
flow1._receive = flow2._receive = function(frame, callback) { callback(); };
});
describe('sending a large data stream', function() {
it('should work as expected', function(done) {
// Sender side
var frameNumber = util.random(5, 8);
var input = [];
flow1._send = function _send() {
if (input.length >= frameNumber) {
this.push({ type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(0) });
this.push(null);
} else {
var buffer = new Buffer(util.random(1000, 100000));
input.push(buffer);
this.push({ type: 'DATA', flags: {}, data: buffer });
}
};
// Receiver side
var output = [];
flow2._receive = function _receive(frame, callback) {
if (frame.type === 'DATA') {
output.push(frame.data);
}
if (frame.flags.END_STREAM) {
this.emit('end_stream');
}
callback();
};
// Checking results
flow2.on('end_stream', function() {
input = util.concat(input);
output = util.concat(output);
expect(input).to.deep.equal(output);
done();
});
// Start piping
flow1.pipe(flow2).pipe(flow1);
});
});
});
});

View File

@ -0,0 +1,252 @@
var expect = require('chai').expect;
var util = require('./util');
var framer = require('../lib/framer');
var Serializer = framer.Serializer;
var Deserializer = framer.Deserializer;
var frame_types = {
DATA: ['data'],
HEADERS: ['priority', 'data'],
PRIORITY: ['priority'],
RST_STREAM: ['error'],
SETTINGS: ['settings'],
PUSH_PROMISE: ['promised_stream', 'data'],
PING: ['data'],
GOAWAY: ['last_stream', 'error'],
WINDOW_UPDATE: ['window_size'],
CONTINUATION: ['data']
};
var test_frames = [{
frame: {
type: 'DATA',
flags: { END_STREAM: false, RESERVED: false },
stream: 10,
data: new Buffer('12345678', 'hex')
},
// length + type + flags + stream + content
buffer: new Buffer('0004' + '00' + '00' + '0000000A' + '12345678', 'hex')
}, {
frame: {
type: 'HEADERS',
flags: { END_STREAM: false, RESERVED: false, END_HEADERS: false, PRIORITY: false },
stream: 15,
data: new Buffer('12345678', 'hex')
},
buffer: new Buffer('0004' + '01' + '00' + '0000000F' + '12345678', 'hex')
}, {
frame: {
type: 'HEADERS',
flags: { END_STREAM: false, RESERVED: false, END_HEADERS: false, PRIORITY: true },
stream: 15,
priority: 3,
data: new Buffer('12345678', 'hex')
},
buffer: new Buffer('0008' + '01' + '08' + '0000000F' + '00000003' + '12345678', 'hex')
}, {
frame: {
type: 'PRIORITY',
flags: { },
stream: 10,
priority: 3
},
buffer: new Buffer('0004' + '02' + '00' + '0000000A' + '00000003', 'hex')
}, {
frame: {
type: 'RST_STREAM',
flags: { },
stream: 10,
error: 'INTERNAL_ERROR'
},
buffer: new Buffer('0004' + '03' + '00' + '0000000A' + '00000002', 'hex')
}, {
frame: {
type: 'SETTINGS',
flags: { ACK: false },
stream: 10,
settings: {
SETTINGS_HEADER_TABLE_SIZE: 0x12345678,
SETTINGS_ENABLE_PUSH: true,
SETTINGS_MAX_CONCURRENT_STREAMS: 0x01234567,
SETTINGS_INITIAL_WINDOW_SIZE: 0x89ABCDEF,
SETTINGS_FLOW_CONTROL_OPTIONS: true
}
},
buffer: new Buffer('0028' + '04' + '00' + '0000000A' + '00' + '000001' + '12345678' +
'00' + '000002' + '00000001' +
'00' + '000004' + '01234567' +
'00' + '000007' + '89ABCDEF' +
'00' + '00000A' + '00000001', 'hex')
}, {
frame: {
type: 'PUSH_PROMISE',
flags: { RESERVED1: false, RESERVED2: false, END_PUSH_PROMISE: false },
stream: 15,
promised_stream: 3,
data: new Buffer('12345678', 'hex')
},
buffer: new Buffer('0008' + '05' + '00' + '0000000F' + '00000003' + '12345678', 'hex')
}, {
frame: {
type: 'PING',
flags: { ACK: false },
stream: 15,
data: new Buffer('1234567887654321', 'hex')
},
buffer: new Buffer('0008' + '06' + '00' + '0000000F' + '1234567887654321', 'hex')
}, {
frame: {
type: 'GOAWAY',
flags: { },
stream: 10,
last_stream: 0x12345678,
error: 'PROTOCOL_ERROR'
},
buffer: new Buffer('0008' + '07' + '00' + '0000000A' + '12345678' + '00000001', 'hex')
}, {
frame: {
type: 'WINDOW_UPDATE',
flags: { },
stream: 10,
window_size: 0x12345678
},
buffer: new Buffer('0004' + '09' + '00' + '0000000A' + '12345678', 'hex')
}, {
frame: {
type: 'CONTINUATION',
flags: { RESERVED1: false, RESERVED2: false, END_HEADERS: true },
stream: 10,
data: new Buffer('12345678', 'hex')
},
// length + type + flags + stream + content
buffer: new Buffer('0004' + '0A' + '04' + '0000000A' + '12345678', 'hex')
}];
describe('framer.js', function() {
describe('Serializer', function() {
describe('static method .commonHeader({ type, flags, stream }, buffer_array)', function() {
it('should add the appropriate 8 byte header buffer in front of the others', function() {
for (var i = 0; i < test_frames.length; i++) {
var test = test_frames[i];
var buffers = [test.buffer.slice(8)];
var header_buffer = test.buffer.slice(0,8);
Serializer.commonHeader(test.frame, buffers);
expect(buffers[0]).to.deep.equal(header_buffer);
}
});
});
Object.keys(frame_types).forEach(function(type) {
var tests = test_frames.filter(function(test) { return test.frame.type === type; });
var frame_shape = '{ ' + frame_types[type].join(', ') + ' }';
describe('static method .' + type + '(' + frame_shape + ', buffer_array)', function() {
it('should push buffers to the array that make up a ' + type + ' type payload', function() {
for (var i = 0; i < tests.length; i++) {
var test = tests[i];
var buffers = [];
Serializer[type](test.frame, buffers);
expect(util.concat(buffers)).to.deep.equal(test.buffer.slice(8));
}
});
});
});
describe('transform stream', function() {
it('should transform frame objects to appropriate buffers', function() {
var stream = new Serializer(util.log);
for (var i = 0; i < test_frames.length; i++) {
var test = test_frames[i];
stream.write(test.frame);
var chunk, buffer = new Buffer(0);
while (chunk = stream.read()) {
buffer = util.concat([buffer, chunk]);
}
expect(buffer).to.be.deep.equal(test.buffer);
}
});
});
});
describe('Deserializer', function() {
describe('static method .commonHeader(header_buffer, frame)', function() {
it('should augment the frame object with these properties: { type, flags, stream })', function() {
for (var i = 0; i < test_frames.length; i++) {
var test = test_frames[i], frame = {};
Deserializer.commonHeader(test.buffer.slice(0,8), frame);
expect(frame).to.deep.equal({
type: test.frame.type,
flags: test.frame.flags,
stream: test.frame.stream
});
}
});
});
Object.keys(frame_types).forEach(function(type) {
var tests = test_frames.filter(function(test) { return test.frame.type === type; });
var frame_shape = '{ ' + frame_types[type].join(', ') + ' }';
describe('static method .' + type + '(payload_buffer, frame)', function() {
it('should augment the frame object with these properties: ' + frame_shape, function() {
for (var i = 0; i < tests.length; i++) {
var test = tests[i];
var frame = {
type: test.frame.type,
flags: test.frame.flags,
stream: test.frame.stream
};
Deserializer[type](test.buffer.slice(8), frame);
expect(frame).to.deep.equal(test.frame);
}
});
});
});
describe('transform stream', function() {
it('should transform buffers to appropriate frame object', function() {
var stream = new Deserializer(util.log);
var shuffled = util.shuffleBuffers(test_frames.map(function(test) { return test.buffer; }));
shuffled.forEach(stream.write.bind(stream));
for (var j = 0; j < test_frames.length; j++) {
expect(stream.read()).to.be.deep.equal(test_frames[j].frame);
}
});
});
});
describe('bunyan formatter', function() {
describe('`frame`', function() {
var format = framer.serializers.frame;
it('should assign a unique ID to each frame', function() {
var frame1 = { type: 'DATA', data: new Buffer(10) };
var frame2 = { type: 'PRIORITY', priority: 1 };
expect(format(frame1).id).to.be.equal(format(frame1));
expect(format(frame2).id).to.be.equal(format(frame2));
expect(format(frame1)).to.not.be.equal(format(frame2));
});
});
});
});

View File

@ -0,0 +1,416 @@
var expect = require('chai').expect;
var util = require('./util');
var stream = require('../lib/stream');
var Stream = stream.Stream;
function createStream() {
var stream = new Stream(util.log);
stream.upstream._window = Infinity;
stream.upstream._remoteFlowControlDisabled = true;
return stream;
}
// Execute a list of commands and assertions
var recorded_events = ['state', 'error', 'window_update', 'headers', 'promise'];
function execute_sequence(stream, sequence, done) {
if (!done) {
done = sequence;
sequence = stream;
stream = createStream();
}
var outgoing_frames = [];
var emit = stream.emit, events = [];
stream.emit = function(name) {
if (recorded_events.indexOf(name) !== -1) {
events.push({ name: name, data: Array.prototype.slice.call(arguments, 1) });
}
return emit.apply(this, arguments);
};
var commands = [], checks = [];
sequence.forEach(function(step) {
if ('method' in step || 'incoming' in step || 'outgoing' in step || 'wait' in step || 'set_state' in step) {
commands.push(step);
}
if ('outgoing' in step || 'event' in step || 'active' in step) {
checks.push(step);
}
});
var activeCount = 0;
function count_change(change) {
activeCount += change;
}
function execute(callback) {
var command = commands.shift();
if (command) {
if ('method' in command) {
var value = stream[command.method.name].apply(stream, command.method.arguments);
if (command.method.ret) {
command.method.ret(value);
}
execute(callback);
} else if ('incoming' in command) {
command.incoming.count_change = count_change;
stream.upstream.write(command.incoming);
execute(callback);
} else if ('outgoing' in command) {
outgoing_frames.push(stream.upstream.read());
execute(callback);
} else if ('set_state' in command) {
stream.state = command.set_state;
execute(callback);
} else if ('wait' in command) {
setTimeout(execute.bind(null, callback), command.wait);
} else {
throw new Error('Invalid command', command);
}
} else {
setTimeout(callback, 5);
}
}
function check() {
checks.forEach(function(check) {
if ('outgoing' in check) {
var frame = outgoing_frames.shift();
for (var key in check.outgoing) {
expect(frame).to.have.property(key).that.deep.equals(check.outgoing[key]);
}
count_change(frame.count_change);
} else if ('event' in check) {
var event = events.shift();
expect(event.name).to.be.equal(check.event.name);
check.event.data.forEach(function(data, index) {
expect(event.data[index]).to.deep.equal(data);
});
} else if ('active' in check) {
expect(activeCount).to.be.equal(check.active);
} else {
throw new Error('Invalid check', check);
}
});
done();
}
setImmediate(execute.bind(null, check));
}
var example_frames = [
{ type: 'PRIORITY', flags: {}, priority: 1 },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} },
{ type: 'RST_STREAM', flags: {}, error: 'CANCEL' },
{ type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log) }
];
var invalid_incoming_frames = {
IDLE: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'PRIORITY', flags: {}, priority: 1 },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} },
{ type: 'RST_STREAM', flags: {}, error: 'CANCEL' }
],
RESERVED_LOCAL: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} }
],
RESERVED_REMOTE: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'PRIORITY', flags: {}, priority: 1 },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} }
],
OPEN: [
],
HALF_CLOSED_LOCAL: [
],
HALF_CLOSED_REMOTE: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} }
]
};
var invalid_outgoing_frames = {
IDLE: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'PRIORITY', flags: {}, priority: 1 },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} }
],
RESERVED_LOCAL: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'PRIORITY', flags: {}, priority: 1 },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} }
],
RESERVED_REMOTE: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} }
],
OPEN: [
],
HALF_CLOSED_LOCAL: [
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
{ type: 'PUSH_PROMISE', flags: {}, headers: {} }
],
HALF_CLOSED_REMOTE: [
],
CLOSED: [
{ type: 'PRIORITY', flags: {}, priority: 1 },
{ type: 'WINDOW_UPDATE', flags: {}, settings: {} },
{ type: 'HEADERS', flags: {}, headers: {}, priority: undefined },
{ type: 'DATA', flags: {}, data: new Buffer(5) },
{ type: 'PUSH_PROMISE', flags: {}, headers: {}, promised_stream: new Stream(util.log) }
]
};
describe('stream.js', function() {
describe('Stream class', function() {
describe('._transition(sending, frame) method', function() {
it('should emit error, and answer RST_STREAM for invalid incoming frames', function() {
Object.keys(invalid_incoming_frames).forEach(function(state) {
invalid_incoming_frames[state].forEach(function(invalid_frame) {
var stream = createStream();
stream.state = state;
expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.');
});
});
// CLOSED state as a result of incoming END_STREAM (or RST_STREAM)
var stream = createStream();
stream.headers({});
stream.end();
stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop });
example_frames.forEach(function(invalid_frame) {
invalid_frame.count_change = util.noop;
expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.');
});
// CLOSED state as a result of outgoing END_STREAM
var stream = createStream();
stream.upstream.write({ type: 'HEADERS', headers:{}, flags: { END_STREAM: true }, count_change: util.noop });
stream.headers({});
stream.end();
example_frames.slice(3).forEach(function(invalid_frame) {
invalid_frame.count_change = util.noop;
expect(stream._transition.bind(stream, false, invalid_frame)).to.throw('Uncaught, unspecified "error" event.');
});
});
it('should throw exception for invalid outgoing frames', function() {
Object.keys(invalid_outgoing_frames).forEach(function(state) {
invalid_outgoing_frames[state].forEach(function(invalid_frame) {
var stream = createStream();
stream.state = state;
expect(stream._transition.bind(stream, true, invalid_frame)).to.throw(Error);
});
});
});
it('should close the stream when there\'s an incoming or outgoing RST_STREAM', function() {
[
'RESERVED_LOCAL',
'RESERVED_REMOTE',
'OPEN',
'HALF_CLOSED_LOCAL',
'HALF_CLOSED_REMOTE'
].forEach(function(state) {
[true, false].forEach(function(sending) {
var stream = createStream();
stream.state = state;
stream._transition(sending, { type: 'RST_STREAM', flags: {} });
expect(stream.state).to.be.equal('CLOSED');
});
});
});
it('should ignore any incoming frame after sending reset', function() {
var stream = createStream();
stream.reset();
example_frames.forEach(stream._transition.bind(stream, false));
});
it('should ignore certain incoming frames after closing the stream with END_STREAM', function() {
var stream = createStream();
stream.upstream.write({ type: 'HEADERS', flags: { END_STREAM: true }, headers:{} });
stream.headers({});
stream.end();
example_frames.slice(0,3).forEach(function(frame) {
frame.count_change = util.noop;
stream._transition(false, frame);
});
});
});
});
describe('test scenario', function() {
describe('sending request', function() {
it('should trigger the appropriate state transitions and outgoing frames', function(done) {
execute_sequence([
{ method : { name: 'headers', arguments: [{ ':path': '/' }] } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } },
{ event : { name: 'state', data: ['OPEN'] } },
{ wait : 5 },
{ method : { name: 'end', arguments: [] } },
{ event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } },
{ outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(0) } },
{ wait : 10 },
{ incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } },
{ incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: new Buffer(5) } },
{ event : { name: 'headers', data: [{ ':status': 200 }] } },
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 0 }
], done);
});
});
describe('answering request', function() {
it('should trigger the appropriate state transitions and outgoing frames', function(done) {
var payload = new Buffer(5);
execute_sequence([
{ incoming: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } },
{ event : { name: 'state', data: ['OPEN'] } },
{ event : { name: 'headers', data: [{ ':path': '/' }] } },
{ wait : 5 },
{ incoming: { type: 'DATA', flags: { }, data: new Buffer(5) } },
{ incoming: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(10) } },
{ event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } },
{ wait : 5 },
{ method : { name: 'headers', arguments: [{ ':status': 200 }] } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } },
{ wait : 5 },
{ method : { name: 'end', arguments: [payload] } },
{ outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 0 }
], done);
});
});
describe('sending push stream', function() {
it('should trigger the appropriate state transitions and outgoing frames', function(done) {
var payload = new Buffer(5);
var pushStream;
execute_sequence([
// receiving request
{ incoming: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } },
{ event : { name: 'state', data: ['OPEN'] } },
{ event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } },
{ event : { name: 'headers', data: [{ ':path': '/' }] } },
// sending response headers
{ wait : 5 },
{ method : { name: 'headers', arguments: [{ ':status': '200' }] } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } },
// sending push promise
{ method : { name: 'promise', arguments: [{ ':path': '/' }], ret: function(str) { pushStream = str; } } },
{ outgoing: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/' } } },
// sending response data
{ method : { name: 'end', arguments: [payload] } },
{ outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 0 }
], function() {
// initial state of the promised stream
expect(pushStream.state).to.equal('RESERVED_LOCAL');
execute_sequence(pushStream, [
// push headers
{ wait : 5 },
{ method : { name: 'headers', arguments: [{ ':status': '200' }] } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } },
{ event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } },
// push data
{ method : { name: 'end', arguments: [payload] } },
{ outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 1 }
], done);
});
});
});
describe('receiving push stream', function() {
it('should trigger the appropriate state transitions and outgoing frames', function(done) {
var payload = new Buffer(5);
var original_stream = createStream();
var promised_stream = createStream();
done = util.callNTimes(2, done);
execute_sequence(original_stream, [
// sending request headers
{ method : { name: 'headers', arguments: [{ ':path': '/' }] } },
{ method : { name: 'end', arguments: [] } },
{ outgoing: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } },
{ event : { name: 'state', data: ['OPEN'] } },
{ event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } },
// receiving response headers
{ wait : 10 },
{ incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } },
{ event : { name: 'headers', data: [{ ':status': 200 }] } },
// receiving push promise
{ incoming: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/2.html' }, promised_stream: promised_stream } },
{ event : { name: 'promise', data: [promised_stream, { ':path': '/2.html' }] } },
// receiving response data
{ incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: payload } },
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 0 }
], done);
execute_sequence(promised_stream, [
// initial state of the promised stream
{ event : { name: 'state', data: ['RESERVED_REMOTE'] } },
// push headers
{ wait : 10 },
{ incoming: { type: 'HEADERS', flags: { END_STREAM: false }, headers: { ':status': 200 } } },
{ event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } },
{ event : { name: 'headers', data: [{ ':status': 200 }] } },
// push data
{ incoming: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 0 }
], done);
});
});
});
describe('bunyan formatter', function() {
describe('`s`', function() {
var format = stream.serializers.s;
it('should assign a unique ID to each frame', function() {
var stream1 = createStream();
var stream2 = createStream();
expect(format(stream1)).to.be.equal(format(stream1));
expect(format(stream2)).to.be.equal(format(stream2));
expect(format(stream1)).to.not.be.equal(format(stream2));
});
});
});
});

View File

@ -0,0 +1,87 @@
var path = require('path');
var fs = require('fs');
var spawn = require('child_process').spawn;
function noop() {}
exports.noop = noop;
if (process.env.HTTP2_LOG) {
var logOutput = process.stderr;
if (process.stderr.isTTY) {
var bin = path.resolve(path.dirname(require.resolve('bunyan')), '..', 'bin', 'bunyan');
if(bin && fs.existsSync(bin)) {
logOutput = spawn(bin, ['-o', 'short'], {
stdio: [null, process.stderr, process.stderr]
}).stdin;
}
}
exports.createLogger = function(name) {
return require('bunyan').createLogger({
name: name,
stream: logOutput,
level: process.env.HTTP2_LOG,
serializers: require('../lib/http').serializers
});
};
exports.log = exports.createLogger('test');
} else {
exports.createLogger = function() {
return exports.log;
};
exports.log = {
fatal: noop,
error: noop,
warn : noop,
info : noop,
debug: noop,
trace: noop,
child: function() { return this; }
};
}
exports.callNTimes = function callNTimes(limit, done) {
if (limit === 0) {
done();
} else {
var i = 0;
return function() {
i += 1;
if (i === limit) {
done();
}
};
}
};
// Concatenate an array of buffers into a new buffer
exports.concat = function concat(buffers) {
var size = 0;
for (var i = 0; i < buffers.length; i++) {
size += buffers[i].length;
}
var concatenated = new Buffer(size);
for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) {
buffers[j].copy(concatenated, cursor);
}
return concatenated;
};
exports.random = function random(min, max) {
return min + Math.floor(Math.random() * (max - min + 1));
};
// Concatenate an array of buffers and then cut them into random size buffers
exports.shuffleBuffers = function shuffleBuffers(buffers) {
var concatenated = exports.concat(buffers), output = [], written = 0;
while (written < concatenated.length) {
var chunk_size = Math.min(concatenated.length - written, Math.ceil(Math.random()*20));
output.push(concatenated.slice(written, written + chunk_size));
written += chunk_size;
}
return output;
}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,422 @@
var expect = require('chai').expect;
var util = require('./util');
var fs = require('fs');
var path = require('path');
var http2 = require('../lib/http');
var https = require('https');
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
var options = {
key: fs.readFileSync(path.join(__dirname, '../example/localhost.key')),
cert: fs.readFileSync(path.join(__dirname, '../example/localhost.crt')),
log: util.log
};
http2.globalAgent = new http2.Agent({ log: util.log });
describe('http.js', function() {
describe('Server', function() {
describe('new Server(options)', function() {
it('should throw if called without \'plain\' or TLS options', function() {
expect(function() {
new http2.Server();
}).to.throw(Error);
expect(function() {
http2.createServer(util.noop);
}).to.throw(Error);
});
});
describe('property `timeout`', function() {
it('should be a proxy for the backing HTTPS server\'s `timeout` property', function() {
var server = new http2.Server(options);
var backingServer = server._server;
var newTimeout = 10;
server.timeout = newTimeout;
expect(server.timeout).to.be.equal(newTimeout);
expect(backingServer.timeout).to.be.equal(newTimeout);
});
});
describe('method `setTimeout(timeout, [callback])`', function() {
it('should be a proxy for the backing HTTPS server\'s `setTimeout` method', function() {
var server = new http2.Server(options);
var backingServer = server._server;
var newTimeout = 10;
var newCallback = util.noop;
backingServer.setTimeout = function(timeout, callback) {
expect(timeout).to.be.equal(newTimeout);
expect(callback).to.be.equal(newCallback);
};
server.setTimeout(newTimeout, newCallback);
});
});
});
describe('Agent', function() {
describe('property `maxSockets`', function() {
it('should be a proxy for the backing HTTPS agent\'s `maxSockets` property', function() {
var agent = new http2.Agent({ log: util.log });
var backingAgent = agent._httpsAgent;
var newMaxSockets = backingAgent.maxSockets + 1;
agent.maxSockets = newMaxSockets;
expect(agent.maxSockets).to.be.equal(newMaxSockets);
expect(backingAgent.maxSockets).to.be.equal(newMaxSockets);
});
});
describe('method `request(options, [callback])`', function() {
it('should throw when trying to use with \'http\' scheme', function() {
expect(function() {
var agent = new http2.Agent({ log: util.log });
agent.request({ protocol: 'http:' });
}).to.throw(Error);
});
});
});
describe('OutgoingRequest', function() {
function testFallbackProxyMethod(name, originalArguments, done) {
var request = new http2.OutgoingRequest();
// When in HTTP/2 mode, this call should be ignored
request.stream = { reset: util.noop };
request[name].apply(request, originalArguments);
delete request.stream;
// When in fallback mode, this call should be forwarded
request[name].apply(request, originalArguments);
var mockFallbackRequest = { on: util.noop };
mockFallbackRequest[name] = function() {
expect(arguments).to.deep.equal(originalArguments);
done();
};
request._fallback(mockFallbackRequest);
}
describe('method `setNoDelay(noDelay)`', function() {
it('should act as a proxy for the backing HTTPS agent\'s `setNoDelay` method', function(done) {
testFallbackProxyMethod('setNoDelay', [true], done);
});
});
describe('method `setSocketKeepAlive(enable, initialDelay)`', function() {
it('should act as a proxy for the backing HTTPS agent\'s `setSocketKeepAlive` method', function(done) {
testFallbackProxyMethod('setSocketKeepAlive', [true, util.random(10, 100)], done);
});
});
describe('method `setTimeout(timeout, [callback])`', function() {
it('should act as a proxy for the backing HTTPS agent\'s `setTimeout` method', function(done) {
testFallbackProxyMethod('setTimeout', [util.random(10, 100), util.noop], done);
});
});
describe('method `abort()`', function() {
it('should act as a proxy for the backing HTTPS agent\'s `abort` method', function(done) {
testFallbackProxyMethod('abort', [], done);
});
});
});
describe('test scenario', function() {
describe('simple request', function() {
it('should work as expected', function(done) {
var path = '/x';
var message = 'Hello world';
var server = http2.createServer(options, function(request, response) {
expect(request.url).to.equal(path);
response.end(message);
});
server.listen(1234, function() {
http2.get('https://localhost:1234' + path, function(response) {
response.on('readable', function() {
expect(response.read().toString()).to.equal(message);
server.close();
done();
});
});
});
});
});
describe('request with payload', function() {
it('should work as expected', function(done) {
var path = '/x';
var message = 'Hello world';
var server = http2.createServer(options, function(request, response) {
expect(request.url).to.equal(path);
request.once('readable', function() {
expect(request.read().toString()).to.equal(message);
response.end();
});
});
server.listen(1240, function() {
var request = http2.request({
host: 'localhost',
port: 1240,
path: path
});
request.write(message);
request.end();
request.on('response', function() {
server.close();
done();
});
});
});
});
describe('request with custom status code and headers', function() {
it('should work as expected', function(done) {
var path = '/x';
var message = 'Hello world';
var headerName = 'name';
var headerValue = 'value';
var server = http2.createServer(options, function(request, response) {
// Request URL and headers
expect(request.url).to.equal(path);
expect(request.headers[headerName]).to.equal(headerValue);
// A header to be overwritten later
response.setHeader(headerName, 'to be overwritten');
expect(response.getHeader(headerName)).to.equal('to be overwritten');
// A header to be deleted
response.setHeader('nonexistent', 'x');
response.removeHeader('nonexistent');
expect(response.getHeader('nonexistent')).to.equal(undefined);
// Don't send date
response.sendDate = false;
// Specifying more headers, the status code and a reason phrase with `writeHead`
var moreHeaders = {};
moreHeaders[headerName] = headerValue;
response.writeHead(600, 'to be discarded', moreHeaders);
expect(response.getHeader(headerName)).to.equal(headerValue);
// Empty response body
response.end(message);
});
server.listen(1239, function() {
var headers = {};
headers[headerName] = headerValue;
var request = http2.request({
host: 'localhost',
port: 1239,
path: path,
headers: headers
});
request.end();
request.on('response', function(response) {
expect(response.headers[headerName]).to.equal(headerValue);
expect(response.headers['nonexistent']).to.equal(undefined);
expect(response.headers['date']).to.equal(undefined);
response.on('readable', function() {
expect(response.read().toString()).to.equal(message);
server.close();
done();
});
});
});
});
});
describe('request over plain TCP', function() {
it('should work as expected', function(done) {
var path = '/x';
var message = 'Hello world';
var server = http2.createServer({
plain: true,
log: util.log
}, function(request, response) {
expect(request.url).to.equal(path);
response.end(message);
});
server.listen(1237, function() {
var request = http2.request({
plain: true,
host: 'localhost',
port: 1237,
path: path
}, function(response) {
response.on('readable', function() {
expect(response.read().toString()).to.equal(message);
server.close();
done();
});
});
request.end();
});
});
});
describe('request to an HTTPS/1 server', function() {
it('should fall back to HTTPS/1 successfully', function(done) {
var path = '/x';
var message = 'Hello world';
var server = https.createServer(options, function(request, response) {
expect(request.url).to.equal(path);
response.end(message);
});
server.listen(5678, function() {
http2.get('https://localhost:5678' + path, function(response) {
response.on('readable', function() {
expect(response.read().toString()).to.equal(message);
done();
});
});
});
});
});
describe('HTTPS/1 request to a HTTP/2 server', function() {
it('should fall back to HTTPS/1 successfully', function(done) {
var path = '/x';
var message = 'Hello world';
var server = http2.createServer(options, function(request, response) {
expect(request.url).to.equal(path);
response.end(message);
});
server.listen(1236, function() {
https.get('https://localhost:1236' + path, function(response) {
response.on('readable', function() {
expect(response.read().toString()).to.equal(message);
done();
});
});
});
});
});
describe('two parallel request', function() {
it('should work as expected', function(done) {
var path = '/x';
var message = 'Hello world';
var server = http2.createServer(options, function(request, response) {
expect(request.url).to.equal(path);
response.end(message);
});
server.listen(1237, function() {
done = util.callNTimes(2, done);
// 1. request
http2.get('https://localhost:1237' + path, function(response) {
response.on('readable', function() {
expect(response.read().toString()).to.equal(message);
done();
});
});
// 2. request
http2.get('https://localhost:1237' + path, function(response) {
response.on('readable', function() {
expect(response.read().toString()).to.equal(message);
done();
});
});
});
});
});
describe('two subsequent request', function() {
it('should use the same HTTP/2 connection', function(done) {
var path = '/x';
var message = 'Hello world';
var server = http2.createServer(options, function(request, response) {
expect(request.url).to.equal(path);
response.end(message);
});
server.listen(1238, function() {
// 1. request
http2.get('https://localhost:1238' + path, function(response) {
response.on('readable', function() {
expect(response.read().toString()).to.equal(message);
// 2. request
http2.get('https://localhost:1238' + path, function(response) {
response.on('readable', function() {
expect(response.read().toString()).to.equal(message);
done();
});
});
});
});
});
});
});
describe('request and response with trailers', function() {
it('should work as expected', function(done) {
var path = '/x';
var message = 'Hello world';
var requestTrailers = { 'content-md5': 'x' };
var responseTrailers = { 'content-md5': 'y' };
var server = http2.createServer(options, function(request, response) {
expect(request.url).to.equal(path);
request.on('data', util.noop);
request.once('end', function() {
expect(request.trailers).to.deep.equal(requestTrailers);
response.write(message);
response.addTrailers(responseTrailers);
response.end();
});
});
server.listen(1241, function() {
var request = http2.request('https://localhost:1241' + path);
request.addTrailers(requestTrailers);
request.end();
request.on('response', function(response) {
response.on('data', util.noop);
response.once('end', function() {
expect(response.trailers).to.deep.equal(responseTrailers);
done();
});
});
});
});
});
describe('server push', function() {
it('should work as expected', function(done) {
var path = '/x';
var message = 'Hello world';
var pushedPath = '/y';
var pushedMessage = 'Hello world 2';
var server = http2.createServer(options, function(request, response) {
expect(request.url).to.equal(path);
var push1 = response.push('/y');
push1.end(pushedMessage);
var push2 = response.push({ path: '/y', protocol: 'https:' });
push2.end(pushedMessage);
response.end(message);
});
server.listen(1235, function() {
var request = http2.get('https://localhost:1235' + path);
done = util.callNTimes(5, done);
request.on('response', function(response) {
response.on('readable', function() {
expect(response.read().toString()).to.equal(message);
done();
});
response.on('end', done);
});
request.on('push', function(promise) {
expect(promise.url).to.be.equal(pushedPath);
promise.on('response', function(pushStream) {
pushStream.on('readable', function() {
expect(pushStream.read().toString()).to.equal(pushedMessage);
done();
});
pushStream.on('end', done);
});
});
});
});
});
});
});

View File

@ -0,0 +1,87 @@
var path = require('path');
var fs = require('fs');
var spawn = require('child_process').spawn;
function noop() {}
exports.noop = noop;
if (process.env.HTTP2_LOG) {
var logOutput = process.stderr;
if (process.stderr.isTTY) {
var bin = path.resolve(path.dirname(require.resolve('bunyan')), '..', 'bin', 'bunyan');
if(bin && fs.existsSync(bin)) {
logOutput = spawn(bin, ['-o', 'short'], {
stdio: [null, process.stderr, process.stderr]
}).stdin;
}
}
exports.createLogger = function(name) {
return require('bunyan').createLogger({
name: name,
stream: logOutput,
level: process.env.HTTP2_LOG,
serializers: require('../lib/http').serializers
});
};
exports.log = exports.createLogger('test');
} else {
exports.createLogger = function() {
return exports.log;
};
exports.log = {
fatal: noop,
error: noop,
warn : noop,
info : noop,
debug: noop,
trace: noop,
child: function() { return this; }
};
}
exports.callNTimes = function callNTimes(limit, done) {
if (limit === 0) {
done();
} else {
var i = 0;
return function() {
i += 1;
if (i === limit) {
done();
}
};
}
};
// Concatenate an array of buffers into a new buffer
exports.concat = function concat(buffers) {
var size = 0;
for (var i = 0; i < buffers.length; i++) {
size += buffers[i].length;
}
var concatenated = new Buffer(size);
for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) {
buffers[j].copy(concatenated, cursor);
}
return concatenated;
};
exports.random = function random(min, max) {
return min + Math.floor(Math.random() * (max - min + 1));
};
// Concatenate an array of buffers and then cut them into random size buffers
exports.shuffleBuffers = function shuffleBuffers(buffers) {
var concatenated = exports.concat(buffers), output = [], written = 0;
while (written < concatenated.length) {
var chunk_size = Math.min(concatenated.length - written, Math.ceil(Math.random()*20));
output.push(concatenated.slice(written, written + chunk_size));
written += chunk_size;
}
return output;
}

View File

@ -69,6 +69,7 @@ proto._init = function _init(base, options, handler) {
}
options.NPNProtocols = ['spdy/3', 'spdy/2', 'http/1.1', 'http/1.0'];
options.ALPNProtocols = ['spdy/3', 'spdy/2', 'http/1.1', 'http/1.0'];
state.options = options;
state.reqHandler = handler;
@ -79,7 +80,7 @@ proto._init = function _init(base, options, handler) {
}
// Use https if NPN is not supported
if (!process.features.tls_npn && !options.debug && !options.plain) {
if (!process.features.tls_npn && !process.features.tls_alpn && !options.debug && !options.plain) {
return;
}
};
@ -226,7 +227,8 @@ proto._onConnection = function _onConnection(socket) {
state = this._spdyState;
// Fallback to HTTPS if needed
if ((!socket.npnProtocol || !socket.npnProtocol.match(/spdy/)) &&
var selectedProtocol = socket.npnProtocol || socket.alpnProtocol;
if ((!selectedProtocol || !selectedProtocol.match(/spdy/)) &&
!state.options.debug && !state.options.plain) {
return state.handler.call(this, socket);
}

View File

@ -753,7 +753,7 @@ class XPCShellTests(object):
return wrapped
setattr(self.log, fun_name, wrap(unwrapped))
self.nodeProc = None
self.nodeProc = {}
def buildTestList(self):
"""
@ -940,33 +940,38 @@ class XPCShellTests(object):
# We try to find the node executable in the path given to us by the user in
# the MOZ_NODE_PATH environment variable
localPath = os.getenv('MOZ_NODE_PATH', None)
# localPath = os.getenv('MOZ_NODE_PATH', None)
# Temporarily, we use the node binary in this directory
localPath = os.path.join(os.path.split(os.path.abspath(__file__))[0], 'node')
if localPath and os.path.exists(localPath) and os.path.isfile(localPath):
nodeBin = localPath
if nodeBin:
self.log.info('Found node at %s' % (nodeBin,))
def startServer(name, serverJs):
if os.path.exists(serverJs):
# OK, we found our SPDY server, let's try to get it running
self.log.info('Found %s at %s' % (name, serverJs))
try:
# We pipe stdin to node because the spdy server will exit when its
# stdin reaches EOF
process = Popen([nodeBin, serverJs], stdin=PIPE, stdout=PIPE,
stderr=STDOUT, env=self.env, cwd=os.getcwd())
self.nodeProc[name] = process
# Check to make sure the server starts properly by waiting for it to
# tell us it's started
msg = process.stdout.readline()
if 'server listening' in msg:
nodeMozInfo['hasNode'] = True # Todo: refactor this
except OSError, e:
# This occurs if the subprocess couldn't be started
self.log.error('Could not run %s server: %s' % (name, str(e)))
myDir = os.path.split(os.path.abspath(__file__))[0]
mozSpdyJs = os.path.join(myDir, 'moz-spdy', 'moz-spdy.js')
if os.path.exists(mozSpdyJs):
# OK, we found our SPDY server, let's try to get it running
self.log.info('Found moz-spdy at %s' % (mozSpdyJs,))
stdout, stderr = self.getPipes()
try:
# We pipe stdin to node because the spdy server will exit when its
# stdin reaches EOF
self.nodeProc = Popen([nodeBin, mozSpdyJs], stdin=PIPE, stdout=PIPE,
stderr=STDOUT, env=self.env, cwd=os.getcwd())
# Check to make sure the server starts properly by waiting for it to
# tell us it's started
msg = self.nodeProc.stdout.readline()
if msg.startswith('SPDY server listening'):
nodeMozInfo['hasNode'] = True
except OSError, e:
# This occurs if the subprocess couldn't be started
self.log.error('Could not run node SPDY server: %s' % (str(e),))
startServer('moz-spdy', os.path.join(myDir, 'moz-spdy', 'moz-spdy.js'))
startServer('moz-http2', os.path.join(myDir, 'moz-http2', 'moz-http2.js'))
mozinfo.update(nodeMozInfo)
@ -974,10 +979,9 @@ class XPCShellTests(object):
"""
Shut down our node process, if it exists
"""
if self.nodeProc:
self.log.info('Node SPDY server shutting down ...')
# moz-spdy exits when its stdin reaches EOF, so force that to happen here
self.nodeProc.communicate()
for name, proc in self.nodeProc.iteritems():
self.log.info('Node %s server shutting down ...' % name)
proc.terminate()
def writeXunitResults(self, results, name=None, filename=None, fh=None):
"""