Socket
Socket
Sign inDemoInstall

http2

Package Overview
Dependencies
Maintainers
1
Versions
44
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

http2 - npm Package Compare versions

Comparing version 0.2.1 to 0.3.0

test/util.js

43

example/client.js

@@ -5,6 +5,35 @@ var fs = require('fs');

if (process.env.HTTP2_LOG) {
http2.globalAgent = new http2.Agent({
log: require('bunyan').createLogger({
name: 'client',
stream: process.stderr,
level: process.env.HTTP2_LOG,
serializers: http2.serializers
})
});
}
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
// Sending the request
var request = http2.get(process.argv.pop());
// 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;

@@ -18,15 +47,1 @@ var finished = 0;

}
request.on('response', function(response) {
response.on('push', function(pushRequest) {
var filename = path.join(__dirname, '/push-' + (push_count));
push_count += 1;
console.log('Receiving pushed resource: ' + pushRequest.url + ' -> ' + filename);
pushRequest.on('response', function(pushResponse) {
pushResponse.pipe(fs.createWriteStream(filename)).on('finish', finish);
});
});
response.pipe(process.stderr);
response.on('end', finish);
});

@@ -5,5 +5,15 @@ var fs = require('fs');

if (process.env.HTTP2_LOG) {
var log = require('bunyan').createLogger({
name: 'server',
stream: process.stderr,
level: process.env.HTTP2_LOG,
serializers: http2.serializers
});
}
var options = {
key: fs.readFileSync(path.join(__dirname, '/localhost.key')),
cert: fs.readFileSync(path.join(__dirname, '/localhost.crt'))
cert: fs.readFileSync(path.join(__dirname, '/localhost.crt')),
log: log
};

@@ -13,6 +23,4 @@

var filename = path.join(__dirname, request.url);
console.error('Incoming request:', request.url, '(' + filename + ')');
if ((filename.indexOf(__dirname) === 0) && fs.existsSync(filename) && fs.statSync(filename).isFile()) {
console.error('Reading file from disk.');
var filestream = fs.createReadStream(filename);

@@ -31,3 +39,2 @@ response.writeHead('200');

} else {
console.error('File not found.');
response.writeHead('404');

@@ -38,4 +45,2 @@ response.end();

var port = process.env.HTTP2_PORT || 8080;
server.listen(port);
console.error('Listening on localhost:' + port + ', serving up files from', __dirname);
server.listen(process.env.HTTP2_PORT || 8080);
Version history
===============
### 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) ###

@@ -5,0 +14,0 @@

@@ -16,3 +16,2 @@ // HTTP/2 compression is implemented by two [Transform Stream][1] subclasses that operate in

var logging = require('./logging');
var Transform = require('stream').Transform;

@@ -252,3 +251,3 @@

function Decompressor(type, log) {
this._log = (log || logging.root).child({ component: 'decompressor' });
this._log = log.child({ component: 'decompressor' });

@@ -314,3 +313,3 @@ var initialTable = (type === 'REQUEST') ? CompressionContext.initialRequestTable

function Compressor(type, log) {
this._log = (log || logging.root).child({ component: 'compressor' });
this._log = log.child({ component: 'compressor' });

@@ -317,0 +316,0 @@ var initialTable = (type === 'REQUEST') ? CompressionContext.initialRequestTable

@@ -1,2 +0,2 @@

var logging = require('./logging');
var assert = process.env.HTTP2_ASSERT ? require('assert') : function noop() {};

@@ -41,3 +41,3 @@ // The Connection class

// * logging: every method uses the common logger object
this._log = (log || logging.root).child({ component: 'connection' });
this._log = log.child({ component: 'connection' });

@@ -99,11 +99,7 @@ // * stream management

// * streams are stored in two data structures:
// * `_streamsIds` is the primary registry of streams. It's a sparse array that serves as an
// id -> stream map.
//
// * `_streamPriorities` is an ordered set of streams that are allowed to send data. The order
// is determined by stream priorities. (currently, it's order of creation)
this._streamsIds = [];
// * `_streamsIn` is an id -> stream map of the streams that are allowed to receive frames.
// * `_streamsOut` is the list of all streams ordered by priority.
this._streamsIn = [];
this._streamsOut = [];
this._streamPriorities = [];
// * The next outbound stream ID and the last inbound stream id

@@ -114,3 +110,3 @@ this._nextStreamId = firstStreamId;

// * Calling `_writeControlFrame` when there's an incoming stream with 0 as stream ID
this._streamsIds[0] = { upstream: { write: this._writeControlFrame.bind(this) } };
this._streamsIn[0] = { upstream: { write: this._writeControlFrame.bind(this) } };

@@ -125,3 +121,3 @@ // * By default, the number of concurrent outbound streams is not limited. The `_streamLimit` can

Connection.prototype._getIdOf = function _getIdOf(stream) {
return this._streamsIds.indexOf(stream);
return this._streamsIn.indexOf(stream);
};

@@ -142,103 +138,103 @@

// Changing the stream count limit
// Method to manage the stream slot pool:
Connection.prototype._updateStreamLimit = function _updateStreamLimit(newStreamLimit) {
this._streamLimit = newStreamLimit;
this.emit('stream_slot_change');
this.read(0);
};
// Freeing a slot in the stream pool
Connection.prototype._decreaseStreamCount = function _decreaseStreamCount() {
this._streamCount -= 1;
this.emit('stream_slot_change');
Connection.prototype._changeStreamCount = function _changeStreamCount(change) {
if (change) {
this._log.trace({ count: this._streamCount, change: change }, 'Changing active stream count.');
this._streamCount += change;
this.read(0);
}
};
// Creating a new *inbound or outbound* stream with the given `id` consists of two steps:
// 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 newstream = this._newStream(id);`
// * creates the new stream and registers it in `this._streamsIds`
// 2. `this._activateStream(newstream);`
// * adds it to `_streamPriorities` (in the appropriate position)
// * transforms 'readable' events on the stream to 'stream_readable' events on the connection
// 1. var stream = new Stream(this._log);
// 2. this._enableReceiving(stream, id);
// 2. this._enableSending(stream);
Connection.prototype._newStream = function _newStream(id) {
var stream = new Stream(this._log);
this._log.debug({ s: stream.id, stream_id: id }, 'Created new stream.');
this._streamsIds[id] = stream;
// Enabling receiving
Connection.prototype._enableReceiving = function _enableReceiving(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._streamsIn));
// * adding to `this._streamsIn`
this._log.trace({ s: stream, stream_id: id }, 'Enabling receiving for a stream.');
this._streamsIn[id] = stream;
this.emit('new_stream', stream, id);
return stream;
return id;
};
Connection.prototype._activateStream = function _activateStream(stream) {
this._log.trace({ s: stream.id }, 'Activating stream.');
this._streamPriorities.push(stream);
// Enabling sending
Connection.prototype._enableSending = function _enableSending(stream) {
this._log.trace({ s: stream }, 'Enabling sending for a stream.');
this._insert(stream);
stream.on('priority', this._reprioritize.bind(this, stream));
stream.upstream.on('readable', this.read.bind(this, 0));
this.read(0);
};
// `_insert(stream)` inserts `stream` in `_streamsOut` in a place determined by `stream._priority`
Connection.prototype._insert = function _insert(stream) {
var streams = this._streamsOut;
var index = 0;
while ((index < streams.length) && (streams[index]._priority <= stream._priority)) {
index += 1;
}
streams.splice(index, 0, stream);
};
// `_reprioritize(stream)` moves `stream` to the apprioriate place in `_streamsOut` (according to
// its `_priority`)
Connection.prototype._reprioritize = function _reprioritize(stream) {
var index = this._streamsOut.indexOf(stream);
assert(index !== -1);
this._streamsOut.splice(index, 1);
this._insert(stream);
};
// Creating an *inbound* stream with the given ID. It is called when there's an incoming frame to
// a previously nonexistent stream.
//
// * Incoming stream IDs have to be greater than any previous incoming stream ID, and have to be of
// different parity than IDs used for outbound streams.
// * It creates and activates the stream.
// * Emits 'stream' event with the new stream.
Connection.prototype._createIncomingStream = function _createIncomingStream(id) {
this._log.debug({ stream_id: id }, 'New incoming stream.');
if ((id <= this._lastIncomingStream) || ((id - this._nextStreamId) % 2 === 0)) {
this._log.error({ stream_id: id, lastIncomingStream: this._lastIncomingStream }, 'Invalid incoming stream ID.');
this.emit('error', 'PROTOCOL_ERROR');
return undefined;
}
var stream = new Stream(this._log);
this._enableReceiving(stream, id);
this._enableSending(stream);
this.emit('stream', stream, id);
this._lastIncomingStream = id;
var stream = this._newStream(id);
this._activateStream(stream);
this.emit('stream', stream, id);
return stream;
};
// Creating an *outbound* stream with the next available ID
// Creating an *outbound* stream
Connection.prototype.createStream = function createStream() {
// * Allocating a new ID with the appropriate parity.
var id = this._nextStreamId;
this._nextStreamId += 2;
this._log.trace('Creating new outbound stream.');
this._log.trace({ stream_id: id }, 'Creating new outbound stream.');
// * Receiving is enabled immediately, and an ID gets assigned to the stream
var stream = new Stream(this._log);
this._enableSending(stream);
// * Creating a new Stream.
var stream = this._newStream(id);
// * Activating the created stream is only possible when there's enough space in the stream pool.
// `tryToActivate` tries to activate the stream until it finally succeeds.
var self = this;
function tryToActivate() {
if (self._streamCount >= self._streamLimit) {
self.once('stream_slot_change', tryToActivate);
} else {
self._activateStream(stream);
}
}
// * Starting activation process when
// * it becomes 'active' (tries to send a frame)
// * and if it is a promised stream, the PUSH_PROMISE is sent
var promisePending = false;
stream.once('promise_initiated', function() {
promisePending = true;
stream.once('promise_sent', function() {
promisePending = false;
});
});
stream.once('active', function() {
if (promisePending) {
stream.once('promise_sent', tryToActivate);
} else {
tryToActivate();
}
});
// * When the stream becomes inactive, decreasing the `_streamCount`
stream.once('inactive', this._decreaseStreamCount.bind(this));
return stream;

@@ -257,17 +253,32 @@ };

stream_loop:
for (var i = 0; i < this._streamPriorities.length; i++) {
var stream = this._streamPriorities[i];
for (var i = 0; i < this._streamsOut.length; i++) {
var stream = this._streamsOut[i];
var id = this._getIdOf(stream);
var frame;
var unshiftRemainder = stream.upstream.unshift.bind(stream.upstream);
while (frame = stream.upstream.read()) {
if (this._streamCount + frame.count_change > this._streamLimit) {
stream.upstream.unshift(frame);
continue stream_loop;
}
if (id === -1) {
id = this._enableReceiving(stream);
}
frame.stream = id;
if (frame.type === 'PUSH_PROMISE') {
frame.promised_stream.emit('promise_sent');
frame.promised_stream = this._getIdOf(frame.promised_stream);
setImmediate(this._enableSending.bind(this, frame.promised_stream));
frame.promised_stream = this._enableReceiving(frame.promised_stream);
}
this._log.trace({ s: stream.id, frame: frame }, 'Trying to forward outgoing frame');
var moreNeeded = this._push(frame, unshiftRemainder);
this._log.trace({ s: stream, frame: frame }, 'Trying to forward outgoing frame');
var remainder = null;
var moreNeeded = this._push(frame, function(remainderFrame) {
stream.upstream.unshift(remainder = remainderFrame);
});
if (!remainder) {
this._changeStreamCount(frame.count_change);
}
if (moreNeeded === null) {

@@ -290,3 +301,3 @@ continue stream_loop;

// * gets the appropriate stream from the stream registry
var stream = this._streamsIds[frame.stream];
var stream = this._streamsIn[frame.stream];

@@ -303,2 +314,4 @@ // * or creates one if it's not in `this.streams`

frame.count_change = this._changeStreamCount.bind(this);
// * and writes it to the `stream`'s `upstream`

@@ -320,3 +333,3 @@ stream.upstream.write(frame);

// * Sending the initial settings.
this._log.info('Sending the first SETTINGS frame as part of the connection header.');
this._log.debug('Sending the first SETTINGS frame as part of the connection header.');
this.set(settings || defaultSettings);

@@ -327,3 +340,3 @@

if ((frame.stream === 0) && (frame.type === 'SETTINGS')) {
this._log.info('Receiving the first SETTINGS frame as part of the connection header.');
this._log.debug('Receiving the first SETTINGS frame as part of the connection header.');
} else {

@@ -452,3 +465,3 @@ this._log.fatal({ frame: frame }, 'Invalid connection header: first frame is not SETTINGS.');

this.on('SETTINGS_FLOW_CONTROL_OPTIONS', this._setStreamFlowControl);
this._streamsIds[0].upstream.setInitialWindow = function noop() {};
this._streamsIn[0].upstream.setInitialWindow = function noop() {};

@@ -473,3 +486,3 @@ // Flow control for incoming frames is not yet supported, and is turned off in the initial

this._initialStreamWindowSize = size;
this._streamsIds.forEach(function(stream) {
this._streamsIn.forEach(function(stream) {
stream.upstream.setInitialWindow(size);

@@ -476,0 +489,0 @@ });

@@ -1,2 +0,1 @@

var logging = require('./logging');
var Serializer = require('./framer').Serializer;

@@ -14,5 +13,2 @@ var Deserializer = require('./framer').Deserializer;

// Counter for globally unique endpoint ID generation
var nextId = 0;
// The process of initialization:

@@ -23,4 +19,3 @@ function Endpoint(role, settings, log) {

// * Initializing logging infrastructure
this.id = nextId++;
this._log = (log || logging.root).child({ component: 'endpoint', e: this.id });
this._log = log.child({ component: 'endpoint', e: this });

@@ -57,3 +52,3 @@ // * First part of the handshake process: sending and receiving the client connection header

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

@@ -85,3 +80,3 @@ };

if (cursor === CLIENT_PRELUDE.length) {
this._log.info('Successfully received the client connection header prelude.');
this._log.debug('Successfully received the client connection header prelude.');
delete this._write;

@@ -192,1 +187,15 @@ chunk = chunk.slice(cursor - offset);

};
// Bunyan serializers
// ------------------
exports.serializers = {};
var nextId = 0;
exports.serializers.e = function(endpoint) {
if (!('id' in endpoint)) {
endpoint.id = nextId;
nextId += 1;
}
return endpoint.id;
};
var assert = process.env.HTTP2_ASSERT ? require('assert') : function noop() {};
var logging = require('./logging');

@@ -170,4 +169,2 @@ // The Flow class

var MAX_HTTP_PAYLOAD_SIZE = 16383; // TODO: this is repeated in multiple files
// `_send` is called when more frames should be pushed to the output buffer.

@@ -213,2 +210,5 @@ Flow.prototype._send = function _send() {

// similar to `push` except that it returns `null` if it did not push anything to the output queue.
var MAX_PAYLOAD_SIZE = 4096; // Must not be greater than MAX_HTTP_PAYLOAD_SIZE which is 16383
Flow.prototype._push = function _push(frame, remainderCallback) {

@@ -218,3 +218,3 @@ do {

if ((frame === null) || (frame.type !== 'DATA') ||
((frame.data.length <= this._window) && (frame.data.length <= MAX_HTTP_PAYLOAD_SIZE))) {
((frame.data.length <= this._window) && (frame.data.length <= MAX_PAYLOAD_SIZE))) {
forwardable = frame;

@@ -228,3 +228,3 @@ }

else {
var chunkSize = Math.min(this._window, MAX_HTTP_PAYLOAD_SIZE);
var chunkSize = Math.min(this._window, MAX_PAYLOAD_SIZE);
forwardable = {

@@ -231,0 +231,0 @@ stream: frame.stream,

@@ -6,3 +6,2 @@ // The framer consists of two [Transform Stream][1] subclasses that operate in [object mode][2]:

var assert = process.env.HTTP2_ASSERT ? require('assert') : function noop() {};
var logging = require('./logging');

@@ -14,2 +13,3 @@ var Transform = require('stream').Transform;

var logData = Boolean(process.env.HTTP2_LOG_DATA);

@@ -28,3 +28,3 @@ // Serializer

function Serializer(log) {
this._log = (log || logging.root).child({ component: 'serializer' });
this._log = log.child({ component: 'serializer' });
Transform.call(this, { objectMode: true });

@@ -47,3 +47,5 @@ }

for (var i = 0; i < buffers.length; i++) {
this._log.trace({ data: buffers[i] }, 'Outgoing data');
if (logData) {
this._log.trace({ data: buffers[i] }, 'Outgoing data');
}
this.push(buffers[i]);

@@ -67,3 +69,3 @@ }

function Deserializer(log) {
this._log = (log || logging.root).child({ component: 'deserializer' });
this._log = log.child({ component: 'deserializer' });
Transform.call(this, { objectMode: true });

@@ -93,3 +95,5 @@ this._next(COMMON_HEADER_SIZE);

this._log.trace({ data: chunk }, 'Incoming data');
if (logData) {
this._log.trace({ data: chunk }, 'Incoming data');
}

@@ -656,2 +660,3 @@ while(cursor < chunk.length) {

// for debug messages emitted in this component.
exports.serializers = {};

@@ -661,3 +666,3 @@ // * `frame` serializer: it transforms data attributes from Buffers to hex strings and filters out

var frameCounter = 0;
logging.serializers.frame = function(frame) {
exports.serializers.frame = function(frame) {
if (!frame) {

@@ -703,4 +708,4 @@ return null;

// * `data` serializer: it simply transforms a buffer to a hex string.
logging.serializers.data = function(data) {
exports.serializers.data = function(data) {
return data.toString('hex');
};

@@ -22,3 +22,7 @@ // Public API

//
// - **http2.createServer(options, [requestListener])**: additional option:
// - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object
//
// - **Class: http2.ServerResponse**
// - **Event: 'close'**: only emitted for HTTP/1 responses.
// - **Event: 'timeout'**: only emitted for HTTP/1 responses.

@@ -33,11 +37,23 @@ // - **response.push(options)**: initiates a server push. `options` describes the 'imaginary'

//
// - **Class: http2.Agent**
// - **new Agent(options)**: additional option:
// - **log**: an optional [bunyan](https://github.com/trentm/node-bunyan) logger object
// - **agent.maxSockets**: only affects HTTP/1 connection pool. For HTTP/2, there's always one
// connection per host.
// - **agent.sockets**: only contains TCP sockets that corresponds to HTTP/1 requests.
// - **agent.endpoints**: contains [Endpoint](endpoint.html) objects for HTTP/2 connections.
//
// - **Class: http2.ClientRequest**
// - **Event: 'upgrade'**: upgrade is deprecated in HTTP/2 so it will never be emitted for HTTP/2
// requests.
// - **Event: 'socket' (socket)**: it's not emitted in case of an HTTP/2 incoming message.
// - **Event: 'stream' (stream)**: in case of an HTTP/2 incoming message, a reference to the
// associated [HTTP/2 Stream](stream.html) object is emitted.
// - **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.setTimeout(timeout, [callback])**: will be ignored for HTTP/2 requests
// - **request.setNoDelay([noDelay])**: will be ignored for HTTP/2 requests
// - **request.setSocketKeepAlive([enable], [initialDelay])**: will be ignored for HTTP/2 requests
// - **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.
//

@@ -47,5 +63,5 @@ // - **Class: http2.IncomingMessage**

// **IncomingResponse**
// - **message.socket**: it's not present in case of an HTTP/2 incoming message.
// - **message.stream**: in case of an HTTP/2 incoming message, it's a reference to the associated
// [HTTP/2 Stream](stream.html) object.
// - **Event: 'close'**: only emitted for HTTP/1 messages.
// - **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).
// - **message.setTimeout(timeout, [callback])**: will be ignored for HTTP/2 requests

@@ -61,6 +77,2 @@ //

//
// - **Class: http2.IncomingResponse (IncomingMessage)**
// - **Event: 'push' (promise)**: signals the intention of a server push. `promise` is an
// IncomingPromise. If there's no listener for this event, the server push is cancelled.
//
// - **Class: http2.IncomingPromise (IncomingRequest)**

@@ -70,3 +82,8 @@ // - contains the metadata of the 'imaginary' request to which the server push is an answer.

// 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.
//

@@ -78,26 +95,13 @@ // API elements not yet implemented:

// - **Event: 'connect'**
// - **Event: 'clientError'**
// - **server.maxHeadersCount**
//
// - **Class: http2.ServerResponse**
// - **Event: 'close'**
// - **response.writeContinue()**
// - **response.addTrailers(headers)**
//
// - **http.request(options, callback)**: not implemented options:
// - **auth**
// - **agent**
//
// - **Class: http2.Agent**
// - **agent.maxSockets**
// - **agent.sockets**
// - **agent.requests**
//
// - **Class: http2.ClientRequest**
// - **Event: 'connect'**
// - **Event: 'continue'**
// - **request.abort()**
//
// - **Class: http2.IncomingMessage**
// - **Event: 'close'**
// - **message.trailers**

@@ -120,3 +124,2 @@ //

var Endpoint = require('./endpoint').Endpoint;
var logging = require('./logging');
var http = require('http');

@@ -133,2 +136,25 @@ var https = require('https');

// Logging
// -------
// Logger shim, used when no logger is provided by the user.
function noop() {}
var defaultLogger = {
fatal: noop,
error: noop,
warn : noop,
info : noop,
debug: noop,
trace: noop,
child: function() { return this; }
};
// Bunyan serializers exported by submodules that are worth adding when creating a logger.
exports.serializers = {};
var modules = ['./framer', './compressor', './flow', './connection', './stream', './endpoint'];
modules.forEach(function(module) {
util._extend(exports.serializers, require(module).serializers);
});
// IncomingMessage class

@@ -141,3 +167,3 @@ // ---------------------

stream.pipe(this);
this.stream = stream;
this.socket = this.stream = stream;

@@ -226,3 +252,3 @@ this._log = stream._log.child({ component: 'http' });

this._log = (options.log || logging.root).child({ component: 'http' });
this._log = (options.log || defaultLogger).child({ component: 'http' });
this._settings = options.settings;

@@ -272,3 +298,3 @@

this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort, endpoint: endpoint.id },
this._log.info({ e: endpoint, client: socket.remoteAddress + ':' + socket.remotePort },
'New incoming HTTP/2 connection');

@@ -280,3 +306,3 @@

endpoint.on('stream', function _onStream(stream) {
var response = new OutgoingResponse(endpoint, stream);
var response = new OutgoingResponse(stream);
var request = new IncomingRequest(stream);

@@ -287,2 +313,4 @@

endpoint.on('error', this.emit.bind(this, 'clientError'));
this.emit('connection', socket, endpoint);

@@ -326,2 +354,4 @@ };

return this._server.timeout;
} else {
return undefined;
}

@@ -372,35 +402,6 @@ },

// [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) {
// * 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.
this.headers = headers;
// * The ":method" header field includes the HTTP method
// * The ":scheme" header field includes the scheme portion of the target URI
// * The ":host" header field includes the authority portion of the target URI
// * The ":path" header field includes the path and query parts of the target URI.
// This field MUST NOT be empty; URIs that do not contain a path component MUST include a value
// of '/', unless the request is an OPTIONS request for '*', in which case the ":path" header
// field MUST include '*'.
// * All HTTP/2.0 requests MUST include exactly one valid value for all of these header fields. A
// server MUST treat the absence of any of these header fields, presence of multiple values, or
// an invalid value as a stream error of type PROTOCOL_ERROR.
var mapping = {
method: ':method',
scheme: ':scheme',
host: ':host',
url: ':path'
};
for (var property in mapping) {
var value = headers[mapping[property]];
if ((typeof value !== 'string') || (value.length === 0)) {
this._log.error({ key: mapping[property], value: value }, 'Invalid header field');
this.stream.emit('error', 'PROTOCOL_ERROR');
return;
}
this[property] = value;
delete headers[mapping[property]];
}
// * An HTTP/2.0 request MUST NOT include any of the following header fields: Connection, Host,

@@ -427,8 +428,41 @@ // Keep-Alive, Proxy-Connection, TE, Transfer-Encoding, and Upgrade. A server MUST treat the

// `this.headers` will store the regular headers (and none of the special colon headers)
this.headers = {};
// * The ":method" header field includes the HTTP method
// * The ":scheme" header field includes the scheme portion of the target URI
// * The ":host" header field includes the authority portion of the target URI
// * The ":path" header field includes the path and query parts of the target URI.
// This field MUST NOT be empty; URIs that do not contain a path component MUST include a value
// of '/', unless the request is an OPTIONS request for '*', in which case the ":path" header
// field MUST include '*'.
// * All HTTP/2.0 requests MUST include exactly one valid value for all of these header fields. A
// server MUST treat the absence of any of these header fields, presence of multiple values, or
// an invalid value as a stream error of type PROTOCOL_ERROR.
var mapping = {
':method': 'method',
':scheme': 'scheme',
':host': 'host',
':path': 'url'
};
for (var name in headers) {
if (name in mapping) {
var value = headers[name];
if ((typeof value !== 'string') || (value.length === 0)) {
this._log.error({ key: name, value: value }, 'Invalid header field');
this.stream.emit('error', 'PROTOCOL_ERROR');
return;
}
this[mapping[name]] = value;
} else {
this.headers[name] = headers[name];
}
}
// * Host header is included in the headers object for backwards compatibility.
headers.host = this.host;
this.headers.host = this.host;
// * Signaling that the header arrived.
this._log.info({ method: this.method, scheme: this.scheme, host: this.host,
path: this.url, headers: headers}, 'Incoming request');
path: this.url, headers: this.headers }, 'Incoming request');
this.emit('ready');

@@ -440,3 +474,3 @@ };

function OutgoingResponse(endpoint, stream) {
function OutgoingResponse(stream) {
OutgoingMessage.call(this);

@@ -446,3 +480,2 @@

this.endpoint = endpoint;
this.stream = stream;

@@ -516,5 +549,5 @@ this.statusCode = undefined;

var promiseHeaders = util._extend({
var promise = util._extend({
':method': (options.method || 'GET').toUpperCase(),
':scheme': options.protocol || this._requestHeaders[':scheme'],
':scheme': (options.protocol && options.protocol.slice(0, -1)) || this._requestHeaders[':scheme'],
':host': options.hostname || options.host || this._requestHeaders[':host'],

@@ -524,6 +557,8 @@ ':path': options.path

var pushStream = this.endpoint.createStream();
this.stream.promise(pushStream, promiseHeaders);
this._log.info({ method: promise[':method'], scheme: promise[':scheme'], host: promise[':host'],
path: promise[':path'], headers: options.headers }, 'Promising push stream');
return new OutgoingResponse(this.endpoint, pushStream);
var pushStream = this.stream.promise(promise);
return new OutgoingResponse(pushStream);
};

@@ -544,9 +579,13 @@

exports.request = request;
exports.get = get;
exports.Agent = Agent;
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);
};

@@ -562,4 +601,4 @@ // Agent class

this._settings = options.settings;
this._log = (options.log || logging.root).child({ component: 'http' });
this._endpoints = {};
this._log = (options.log || defaultLogger).child({ component: 'http' });
this.endpoints = {};

@@ -573,2 +612,5 @@ // * Using an own HTTPS agent, because the global agent does not look at `NPNProtocols` when

});
this.sockets = this._httpsAgent.sockets;
this.requests = this._httpsAgent.requests;
}

@@ -583,3 +625,3 @@ Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } });

options.method = (options.method || 'GET').toUpperCase();
options.protocol = options.protocol || 'https';
options.protocol = options.protocol || 'https:';
options.host = options.hostname || options.host || 'localhost';

@@ -603,4 +645,4 @@ options.port = options.port || 443;

// * There's an existing HTTP/2 connection to this host
if (key in this._endpoints) {
var endpoint = this._endpoints[key];
if (key in this.endpoints) {
var endpoint = this.endpoints[key];
request._start(endpoint.createStream(), options);

@@ -610,6 +652,2 @@ }

// * HTTP/2 over TLS negotiated using NPN (or later ALPN)
// * if the negotiation is unsuccessful
// * adding socket to the HTTPS agent's socket pool
// * initiating a request with the HTTPS agent
// * calling request's fallback() to fall back to use the new request object
else {

@@ -620,2 +658,3 @@ var started = false;

var httpsRequest = https.request(options);
httpsRequest.on('socket', function(socket) {

@@ -629,3 +668,4 @@ if (socket.npnProtocol !== undefined) {

var negotiated = function negotiated() {
var self = this;
function negotiated() {
if (!started) {

@@ -635,14 +675,14 @@ if (httpsRequest.socket.npnProtocol === implementedVersion) {

unbundleSocket(httpsRequest.socket);
var endpoint = new Endpoint('CLIENT', this._settings, this._log);
this._log.info({ server: options.host + ':' + options.port, endpoint: endpoint.id },
'New incoming HTTP/2 connection');
var endpoint = new Endpoint('CLIENT', self._settings, self._log);
self._log.info({ e: endpoint, server: options.host + ':' + options.port },
'New outgoing HTTP/2 connection');
endpoint.socket = httpsRequest.socket;
endpoint.pipe(endpoint.socket).pipe(endpoint);
this._endpoints[key] = endpoint;
this.emit(key, endpoint);
self.endpoints[key] = endpoint;
self.emit(key, endpoint);
} else {
this.emit(key, undefined);
self.emit(key, undefined);
}
}
}.bind(this);
}

@@ -679,12 +719,13 @@ this.once(key, function(endpoint) {

var globalAgent = exports.globalAgent = new Agent();
Object.defineProperty(Agent.prototype, 'maxSockets', {
get: function getMaxSockets() {
return this._httpsAgent.maxSockets;
},
set: function setMaxSockets(value) {
this._httpsAgent.maxSockets = value;
}
});
function request(options, callback) {
return globalAgent.request(options, callback);
}
exports.globalAgent = new Agent();
function get(options, callback) {
return globalAgent.get(options, callback);
}
// OutgoingRequest class

@@ -698,5 +739,3 @@ // ---------------------

this.socket = undefined;
this.stream = undefined;
this.request = undefined;
this.headersSent = true;

@@ -716,5 +755,10 @@ }

delete headers.host;
headers[':scheme'] = options.protocol;
if (options.auth) {
headers.authorization = 'Basic ' + new Buffer(options.auth).toString('base64');
}
headers[':scheme'] = options.protocol.slice(0, -1);
headers[':method'] = options.method;
headers[':host'] = options.hostname;
headers[':host'] = options.host;
headers[':path'] = options.path;

@@ -726,12 +770,13 @@

this.emit('stream', this.stream);
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) {
this.request = request;
this.socket = request.socket;
this.emit('socket', request.socket);
this.stream = this.request = request;
this.emit('socket', this.socket);

@@ -741,2 +786,10 @@ this.pipe(request);

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

@@ -756,2 +809,4 @@ // `request`. See `Server.prototype.on` for explanation.

this.request.setNoDelay(noDelay);
} else if (!this.stream) {
this.on('socket', this.setNoDelay.bind(this, noDelay));
}

@@ -763,2 +818,4 @@ };

this.request.setSocketKeepAlive(enable, initialDelay);
} else if (!this.stream) {
this.on('socket', this.setSocketKeepAlive.bind(this, enable, initialDelay));
}

@@ -770,5 +827,31 @@ };

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

@@ -779,3 +862,2 @@ // ----------------------

IncomingMessage.call(this, stream);
stream.on('promise', this._onPromise.bind(this));
}

@@ -811,12 +893,2 @@ IncomingResponse.prototype = Object.create(IncomingMessage.prototype, { constructor: { value: IncomingResponse } });

IncomingResponse.prototype._onPromise = function _onPromise(stream, headers) {
var promise = new IncomingPromise(stream, headers);
if (this.listeners('push').length > 0) {
this.emit('push', promise);
} else {
promise.cancel();
}
};
// IncomingPromise class

@@ -833,3 +905,3 @@ // -------------------------

stream.emit('headers', promiseHeaders);
this._onHeaders(promiseHeaders);

@@ -840,2 +912,4 @@ this._responseStream = responseStream;

response.once('ready', this.emit.bind(this, 'response', response));
this.stream.on('promise', this._onPromise.bind(this));
}

@@ -847,1 +921,7 @@ IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } });

};
IncomingPromise.prototype.setPriority = function setPriority(priority) {
this._responseStream.priority(priority);
};
IncomingPromise.prototype._onPromise = OutgoingRequest.prototype._onPromise;

@@ -1,29 +0,93 @@

// [node-http2](https://github.com/molnarg/node-http2) consists of the following components:
// [node-http2][homepage] is an [HTTP/2 (draft 04)][http2] implementation for [node.js][node].
//
// The main building blocks are mainly [node.js streams][node-stream] that are connected through
// pipes.
//
// The main components are:
//
// * [http.js](http.html): the top layer that presents an API very similar to the standard node.js
// [HTTPS module][node-https] (which is in turn very similar to the [HTTP module][node-http]).
//
// * [Endpoint](endpoint.html): represents an HTTP/2 endpoint (client or server). It's
// responsible for the the first part of the handshake process (sending/receiving the
// [connection header][http2-connheader]) and manages other components (framer, compressor,
// connection, streams) that make up a client or server.
//
// * [Connection](connection.html): multiplexes the active HTTP/2 streams, manages connection
// lifecycle and settings, and responsible for enforcing the connection level limits (flow
// control, initiated stream limit)
//
// * [Stream](stream.html): implementation of the [HTTP/2 stream concept](http2-stream).
// Implements the [stream state machine][http2-streamstate] defined by the standard, provides
// management methods and events for using the stream (sending/receiving headers, data, etc.),
// and enforces stream level constraints (flow control, sending only legal frames).
//
// * [Flow](flow.html): implements flow control for Connection and Stream as parent class.
//
// * [Compressor and Decompressor](compressor.html): compression and decompression of HEADER and
// PUSH_PROMISE frames
//
// * [Serializer and Deserializer](framer.html): the lowest layer in the stack that transforms
// between the binary and the JavaScript object representation of HTTP/2 frames
//
// [homepage]: https://github.com/molnarg/node-http2
// [http2]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04
// [http2-connheader]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-3.5
// [http2-stream]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-5
// [http2-streamstate]: http://tools.ietf.org/html/draft-ietf-httpbis-http2-04#section-5.1
// [node]: http://nodejs.org/
// [node-stream]: http://nodejs.org/api/stream.html
// [node-https]: http://nodejs.org/api/https.html
// [node-http]: http://nodejs.org/api/http.html
// * [http.js](http.html): public node-http2 API
var http2 = require('./http');
module.exports = http2;
module.exports = require('./http');
// * [logging.js](logging.html): a default logger object and a registry of log formatter functions
http2.logging = require('./logging');
/*
API user
// * [framer.js](framer.html): the lowest layer in the stack that transforms between the binary and
// the JavaScript object representation of HTTP/2 frames
http2.framer = require('./framer');
| ^
| |
+---------------|------------|--------------------------------------------------------+
| | | Server/Agent |
| v | |
| +----------+ +----------+ |
| | Outgoing | | Incoming | |
| | req/res. | | req/res. | |
| +----------+ +----------+ |
| | ^ |
| +-----------|------------|---------------------------------------+ +----- |
| | | | Endpoint | | |
| | | | | | |
| | +-------|------------|-----------------------------------+ | | |
| | | | | Connection | | | |
| | | v | | | | |
| | | +-----------------------+ +-----------------------+ | | | |
| | | | Stream | | Stream | | | | |
| | | +-----------------------+ +-----------------------+ | | | |
| | | | ^ | ^ | | | |
| | | v | v | | | | |
| | | +------------+--+--------+--+------------+- ... | | | |
| | | | ^ | | | ... |
| | | | | | | | ... |
| | +-----------------------|--------|-----------------------+ | | |
| | | | | | |
| | v | | | |
| | +--------------------------+ +--------------------------+ | | |
| | | Compressor | | Decompressor | | | |
| | +--------------------------+ +--------------------------+ | | |
| | | ^ | | |
| | v | | | |
| | +--------------------------+ +--------------------------+ | | |
| | | Serializer | | Deserializer | | | |
| | +--------------------------+ +--------------------------+ | | |
| | | ^ | | |
| +---------------------------|--------|---------------------------+ +----- |
| | | |
| v | |
| +----------------------------------------------------------------+ +----- |
| | TCP stream | | ... |
| +----------------------------------------------------------------+ +----- |
| |
+-------------------------------------------------------------------------------------+
// * [compressor.js](compressor.html): compression and decompression of HEADER frames
http2.compressor = require('./compressor');
// * [flow.js](flow.html): flow control
http2.flow = require('./flow');
// * [stream.js](stream.html): implementation of the HTTP/2 stream concept
http2.stream = require('./stream');
// * [connection.js](connection.html): multiplexes streams, manages the identifiers of them and
// repsonsible for connection level flow control
http2.connection = require('./connection');
// * [endpoint.js](endpoint.html): manages other components (framer, compressor, connection,
// streams) and part of the handshake process
http2.endpoint = require('./endpoint');
*/
var assert = process.env.HTTP2_ASSERT ? require('assert') : function noop() {};
var logging = require('./logging');

@@ -24,8 +23,14 @@ // The Stream class

//
// * **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, [priority])**: send headers
// * **headers(headers)**: send headers
//
// * **promise(stream, headers)**: promise a stream
// * **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

@@ -36,4 +41,2 @@ //

//
// * **id**: a globally unique, public ID for easier logging
//
// Headers are always in the [regular node.js header format][1].

@@ -45,5 +48,2 @@ // [1]: http://nodejs.org/api/http.html#http_message_headers

// Counter for globally unique stream ID generation
var nextId = 0;
// The main aspects of managing the stream are:

@@ -54,4 +54,3 @@ function Stream(log) {

// * logging
this.id = nextId++;
this._log = (log || logging.root).child({ component: 'stream', s: this.id });
this._log = log.child({ component: 'stream', s: this });

@@ -73,5 +72,10 @@ // * receiving and sending stream management commands

// PUSH_PROMISE and HEADERS are forwarded to the user through events. When error happens, we first
// close 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._priority = DEFAULT_PRIORITY;
this._letPeerPrioritize = true;
this.on('PUSH_PROMISE', function(frame) {

@@ -81,12 +85,15 @@ this.emit('promise', frame.promised_stream, frame.headers);

this.on('HEADERS', function(frame) {
this.priority = frame.priority;
if (frame.priority !== undefined) {
this.priority(frame.priority, true);
}
this.emit('headers', frame.headers);
});
this.on('PRIORITY', function(frame) {
this.priority(frame.priority, true);
});
};
// For sending management frames, the `this.upstream.push(frame)` method is used. It notifies the
// state management code about the sent frames (using the 'sending' event) so we don't have to
// manage state transitions here.
Stream.prototype.promise = function promise(stream, headers) {
stream.emit('promise_initiated');
Stream.prototype.promise = function promise(headers) {
var stream = new Stream(this._log);
stream._priority = Math.min(this._priority + 1, MAX_PRIORITY);
this.upstream.push({

@@ -97,13 +104,34 @@ type: 'PUSH_PROMISE',

});
return stream;
};
Stream.prototype.headers = function headers(headers, priority) {
Stream.prototype.headers = function headers(headers) {
this.upstream.push({
type: 'HEADERS',
priority: priority,
headers: headers
});
this.priority = priority;
};
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.upstream.push({
type: 'PRIORITY',
priority: priority
});
}
}
this._log.debug({ priority: priority }, 'Changing priority');
this._priority = priority;
this.emit('priority', priority);
}
};
Stream.prototype.reset = function reset(error) {

@@ -244,3 +272,4 @@ this.upstream.push({

if (lastFrame && ((lastFrame.type === 'DATA') || (lastFrame.type === 'HEADERS'))) {
this._log.trace({ frame: lastFrame }, 'Marking last frame with END_STREAM flag.');
this._log.debug({ frame: lastFrame }, 'Marking last frame with END_STREAM flag.');
lastFrame.flags = lastFrame.flags || {};
lastFrame.flags.END_STREAM = true;

@@ -285,2 +314,3 @@ this._transition(true, endFrame);

this.state = 'IDLE';
this._initiated = undefined;
this._closedByPeer = false;

@@ -292,22 +322,17 @@ this.on('sending', this._transition.bind(this, true));

// Only `_setState` should change `this.state` directly. It also logs the state change and notifies
// interested parties using the 'state', 'active' and 'inactive' event.
var ACTIVE_STATES = ['HALF_CLOSED_LOCAL', 'HALF_CLOSED_REMOTE', 'OPEN'];
// interested parties using the 'state' event.
Stream.prototype._setState = function transition(state) {
if (this.state !== state) {
this._log.debug({ from: this.state, to: state }, 'State transition');
var wasActive = (ACTIVE_STATES.indexOf(this.state) !== -1);
var isActive = (ACTIVE_STATES.indexOf(state) !== -1);
this.state = state;
this.emit('state', state);
if (!wasActive && isActive) {
this.emit('active');
} else if (wasActive && !isActive) {
this.emit('inactive');
}
}
};
// 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

@@ -334,2 +359,4 @@ // transitions, and detects stream errors. A stream error is always caused by a frame that is not

var previousState = this.state;
switch (this.state) {

@@ -347,2 +374,3 @@ // All streams start in the **idle** state. In this state, no frames have been exchanged.

}
this._initiated = sending;
} else {

@@ -495,4 +523,17 @@ error = 'PROTOCOL_ERROR';

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.

@@ -518,1 +559,15 @@ if (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;
};
{
"name": "http2",
"version": "0.2.1",
"description": "An HTTP/2 server implementation",
"version": "0.3.0",
"description": "An HTTP/2 client and server implementation",
"main": "lib/index.js",

@@ -17,3 +17,3 @@ "engines" : {

"scripts": {
"test": "istanbul test _mocha -- --reporter spec",
"test": "HTTP2_ASSERT=1 istanbul test _mocha -- --reporter spec",
"prepublish": "docco lib/* --output doc --layout parallel --css doc/docco.css"

@@ -20,0 +20,0 @@ },

@@ -48,3 +48,3 @@ node-http2

var request = http2.get('https://gabor.molnar.es:8080/');
var request = http2.get('https://localhost:8080/');

@@ -63,3 +63,2 @@ request.on('response', function(response) {

$ node ./example/server.js
Listening on localhost:8080, serving up files from ./example
```

@@ -72,3 +71,3 @@

```bash
$ node ./example/client.js 'https://localhost:8080/server.js' 2>/tmp/server.js
$ node ./example/client.js 'https://localhost:8080/server.js' >/tmp/server.js
```

@@ -142,10 +141,20 @@

Logging is turned off by default. To turn it on, set the `HTTP2_LOG` environment variable to
`fatal`, `error`, `warn`, `info`, `debug` or `trace` (the logging level). Log output is in JSON
format, and can be pretty printed using the bunyan command line tool.
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.
For example, running the test client with debug level logging output:
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 stdout, and is in JSON format. It can be pretty printed using
the bunyan command line tool.
Running the example server and client with `info` level logging output:
```bash
$ HTTP2_LOG=info node ./example/server.js 2> >(bunyan -o short)
```
HTTP2_LOG=debug node ./example/client.js 'http://localhost:8080/server.js' 2>/tmp/server.js | bunyan
```bash
$ HTTP2_LOG=info node ./example/client.js 'http://localhost:8080/server.js' \
>/dev/null 2> >(bunyan -o short)
```

@@ -152,0 +161,0 @@

var expect = require('chai').expect;
var util = require('./util');

@@ -8,17 +9,2 @@ var compressor = require('../lib/compressor');

// Concatenate an array of buffers into a new buffer
function concat(buffers) {
var size = 0;
for (var i = 0; i < buffers.length; i++) {
size += buffers[i].length;
}
var concatenated = new Buffer(size);
for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) {
buffers[j].copy(concatenated, cursor);
}
return concatenated;
}
var test_integers = [{

@@ -114,3 +100,3 @@ N: 5,

},
buffer: concat(test_headers.slice(0, 3).map(function(test) { return test.buffer; }))
buffer: util.concat(test_headers.slice(0, 3).map(function(test) { return test.buffer; }))
}, {

@@ -122,3 +108,3 @@ headers: {

},
buffer: concat(test_headers.slice(3, 7).map(function(test) { return test.buffer; }))
buffer: util.concat(test_headers.slice(3, 7).map(function(test) { return test.buffer; }))
}, {

@@ -159,3 +145,3 @@ headers: {

var test = test_strings[i];
expect(concat(Compressor.string(test.string))).to.deep.equal(test.buffer);
expect(util.concat(Compressor.string(test.string))).to.deep.equal(test.buffer);
}

@@ -168,3 +154,3 @@ });

var test = test_strings[i];
expect(concat(Compressor.string(test.string))).to.deep.equal(test.buffer);
expect(util.concat(Compressor.string(test.string))).to.deep.equal(test.buffer);
}

@@ -177,3 +163,3 @@ });

var test = test_headers[i];
expect(concat(Compressor.header(test.header))).to.deep.equal(test.buffer);
expect(util.concat(Compressor.header(test.header))).to.deep.equal(test.buffer);
}

@@ -217,3 +203,3 @@ });

it('should return the parsed header set in { name1: value1, name2: [value2, value3], ... } format', function() {
var decompressor = new Decompressor('REQUEST');
var decompressor = new Decompressor('REQUEST', util.log);
var header_set = test_header_sets[0];

@@ -229,3 +215,3 @@ expect(decompressor.decompress(header_set.buffer)).to.deep.equal(header_set.headers);

it('should emit an error event if a series of header frames is interleaved with other frames', function() {
var decompressor = new Decompressor('REQUEST');
var decompressor = new Decompressor('REQUEST', util.log);
var error_occured = false;

@@ -255,4 +241,4 @@ decompressor.on('error', function() {

it('should be true for any header set if the states are synchronized', function() {
var compressor = new Compressor('REQUEST');
var decompressor = new Decompressor('REQUEST');
var compressor = new Compressor('REQUEST', util.log);
var decompressor = new Decompressor('REQUEST', util.log);
for (var i = 0; i < 10; i++) {

@@ -271,4 +257,4 @@ var headers = test_header_sets[i%4].headers;

it('should behave like source.pipe(destination) for a stream of frames', function(done) {
var compressor = new Compressor('RESPONSE');
var decompressor = new Decompressor('RESPONSE');
var compressor = new Compressor('RESPONSE', util.log);
var decompressor = new Decompressor('RESPONSE', util.log);
compressor.pipe(decompressor);

@@ -275,0 +261,0 @@ for (var i = 0; i < 10; i++) {

var expect = require('chai').expect;
var log_root = require('../lib/logging').root;
var util = require('./util');
var Connection = require('../lib/connection').Connection;
function callNTimes(limit, done) {
var i = 0;
return function() {
i += 1;
if (i === limit) {
done();
}
};
}
var settings = {

@@ -21,9 +11,121 @@ SETTINGS_MAX_CONCURRENT_STREAMS: 100,

var MAX_PRIORITY = Math.pow(2, 31) - 1;
function randomPriority() {
return Math.floor(Math.random() * (MAX_PRIORITY + 1));
}
function expectPriorityOrder(streams) {
var previousPriority = -1;
for (var j = 0; j < streams.length; j++) {
var priority = streams[j]._priority;
expect(priority).to.be.at.least(previousPriority);
previousPriority = priority;
}
}
describe('connection.js', function() {
describe('Connection class', function() {
describe('method ._insert(stream)', function() {
it('should insert the stream in _streamsOut in a place determined by stream._priority', function() {
var streams = [];
var connection = Object.create(Connection.prototype, { _streamsOut: { value: streams }});
var streamCount = 10;
// Inserting streams with random priority
for (var i = 0; i < streamCount; i++) {
var stream = { _priority: randomPriority() };
connection._insert(stream);
}
// Resulting _streamsOut should be ordered by priority
expect(streams.length).to.equal(streamCount);
expectPriorityOrder(streams);
});
});
describe('method ._reprioritize(stream)', function() {
it('should eject and then insert the stream in _streamsOut in a place determined by stream._priority', function() {
var streams = [];
var connection = Object.create(Connection.prototype, { _streamsOut: { value: streams }});
var streamCount = 10;
// Inserting streams with random priority
for (var i = 0; i < streamCount; i++) {
var stream = { _priority: randomPriority() };
connection._insert(stream);
}
// Reprioritizing stream
stream = streams[Math.floor(Math.random() * streamCount)];
stream._priority = randomPriority();
connection._reprioritize(stream);
// Resulting _streamsOut should be ordered by priority
expect(streams.length).to.equal(streamCount);
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(1, settings, util.log);
connection.on('error', function(error) {
expect(error).to.equal('FLOW_CONTROL_ERROR');
done();
});
connection._setStreamFlowControl(true);
connection._setStreamFlowControl(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(1, settings, util.log);
connection.on('error', function(error) {
expect(error).to.equal('FLOW_CONTROL_ERROR');
done();
});
connection._setStreamFlowControl(true);
connection._setInitialStreamWindowSize(10);
});
});
describe('disabling flow control twice', function() {
it('should be ignored', function() {
var connection = new Connection(1, settings, util.log);
connection._setStreamFlowControl(true);
connection._setStreamFlowControl(true);
});
});
describe('enabling flow control when already enabled', function() {
it('should be ignored', function() {
var connection = new Connection(1, settings, util.log);
connection._setStreamFlowControl(false);
});
});
describe('unsolicited ping answer', function() {
it('should be ignored', function() {
var connection = new Connection(1, settings, util.log);
connection._receivePing({
stream: 0,
type: 'PING',
flags: {
'PONG': true
},
data: new Buffer(8)
});
});
});
});
});
describe('test scenario', function() {
describe('connection setup', function() {
it('should work as expected', function(done) {
var c = new Connection(1, settings);
var s = new Connection(2, settings);
var c = new Connection(1, settings, util.log.child({ role: 'client' }));
var s = new Connection(2, settings, util.log.child({ role: 'server' }));
c.pipe(s).pipe(c);

@@ -39,4 +141,4 @@

it('should work as expected', function(done) {
var c = new Connection(1, settings, log_root.child({ role: 'client' }));
var s = new Connection(2, settings, log_root.child({ role: 'server' }));
var c = new Connection(1, settings, util.log.child({ role: 'client' }));
var s = new Connection(2, settings, util.log.child({ role: 'server' }));

@@ -71,3 +173,3 @@ c.pipe(s).pipe(c);

// Waiting for answer
done = callNTimes(2, done);
done = util.callNTimes(2, done);
client_stream.on('headers', function(headers) {

@@ -85,4 +187,4 @@ expect(headers).to.deep.equal(response_headers);

it('should work as expected', function(done) {
var c = new Connection(1, settings, log_root.child({ role: 'client' }));
var s = new Connection(2, settings, log_root.child({ role: 'server' }));
var c = new Connection(1, settings, util.log.child({ role: 'client' }));
var s = new Connection(2, settings, util.log.child({ role: 'server' }));

@@ -98,3 +200,3 @@ c.pipe(s).pipe(c);

done = callNTimes(4, done);
done = util.callNTimes(5, done);

@@ -104,8 +206,7 @@ s.on('stream', function(response) {

var pushed = this.createStream();
response.promise(pushed, push_request_headers);
var pushed = response.promise(push_request_headers);
pushed.headers(push_response_headers);
pushed.write(push_content);
pushed.end(push_content);
response.write(response_content);
response.end(response_content);
});

@@ -115,2 +216,3 @@

request.headers(request_headers);
request.end();
request.on('headers', function(headers) {

@@ -134,2 +236,5 @@ expect(headers).to.deep.equal(response_headers);

});
pushed.on('end', function() {
done();
});
});

@@ -140,6 +245,6 @@ });

it('should work as expected', function(done) {
var c = new Connection(1, settings, log_root.child({ role: 'client' }));
var s = new Connection(2, settings, log_root.child({ role: 'server' }));
var c = new Connection(1, settings, util.log.child({ role: 'client' }));
var s = new Connection(2, settings, util.log.child({ role: 'server' }));
c.pipe(s).pipe(c);
c.pipe(s).pipe(c);
c.ping(function() {

@@ -152,6 +257,6 @@ done();

it('should work as expected', function(done) {
var c = new Connection(1, settings, log_root.child({ role: 'client' }));
var s = new Connection(2, settings, log_root.child({ role: 'server' }));
var c = new Connection(1, settings, util.log.child({ role: 'client' }));
var s = new Connection(2, settings, util.log.child({ role: 'server' }));
c.pipe(s).pipe(c);
c.pipe(s).pipe(c);
s.ping(function() {

@@ -164,4 +269,4 @@ done();

it('should not result in non-monotonous local ID ordering', function() {
var c = new Connection(1, settings, log_root.child({ role: 'client' }));
var s = new Connection(2, settings, log_root.child({ role: 'server' }));
var c = new Connection(1, settings, util.log.child({ role: 'client' }));
var s = new Connection(2, settings, util.log.child({ role: 'server' }));

@@ -178,4 +283,4 @@ c.pipe(s).pipe(c);

it('should not result in non-monotonous local ID ordering', function(done) {
var c = new Connection(1, settings, log_root.child({ role: 'client' }));
var s = new Connection(2, settings, log_root.child({ role: 'server' }));
var c = new Connection(1, settings, util.log.child({ role: 'client' }));
var s = new Connection(2, settings, util.log.child({ role: 'server' }));

@@ -198,3 +303,3 @@ c.pipe(s).pipe(c);

done = callNTimes(2, done);
done = util.callNTimes(2, done);
request.on('promise', function() {

@@ -205,3 +310,16 @@ done();

});
describe('closing the connection on one end', function() {
it('should result in closed streams on both ends', function(done) {
var c = new Connection(1, settings, util.log.child({ role: 'client' }));
var s = new Connection(2, settings, util.log.child({ role: 'server' }));
c.pipe(s).pipe(c);
done = util.callNTimes(2, done);
c.on('end', done);
s.on('end', done);
c.close();
});
});
});
});
var expect = require('chai').expect;
var log_root = require('../lib/logging').root;
var util = require('./util');

@@ -16,6 +16,6 @@ var endpoint = require('../lib/endpoint');

it('should work as expected', function(done) {
var c = new Endpoint('CLIENT', settings, log_root.child({ role: 'client' }));
var s = new Endpoint('SERVER', settings, log_root.child({ role: 'server' }));
var c = new Endpoint('CLIENT', settings, util.log.child({ role: 'client' }));
var s = new Endpoint('SERVER', settings, util.log.child({ role: 'server' }));
log_root.debug('Test initialization over, starting piping.');
util.log.debug('Test initialization over, starting piping.');
c.pipe(s).pipe(c);

@@ -22,0 +22,0 @@

var expect = require('chai').expect;
var util = require('./util');

@@ -7,17 +8,2 @@ var framer = require('../lib/framer');

// Concatenate an array of buffers into a new buffer
function concat(buffers) {
var size = 0;
for (var i = 0; i < buffers.length; i++) {
size += buffers[i].length;
}
var concatenated = new Buffer(size);
for (var cursor = 0, j = 0; j < buffers.length; cursor += buffers[j].length, j++) {
buffers[j].copy(concatenated, cursor);
}
return concatenated;
}
var frame_types = {

@@ -158,3 +144,3 @@ DATA: ['data'],

function shuffle_buffers(buffers) {
var concatenated = concat(buffers), output = [], written = 0;
var concatenated = util.concat(buffers), output = [], written = 0;

@@ -193,3 +179,3 @@ while (written < concatenated.length) {

Serializer[type](test.frame, buffers);
expect(concat(buffers)).to.deep.equal(test.buffer.slice(8));
expect(util.concat(buffers)).to.deep.equal(test.buffer.slice(8));
}

@@ -202,3 +188,3 @@ });

it('should transform frame objects to appropriate buffers', function() {
var stream = new Serializer();
var stream = new Serializer(util.log);

@@ -210,3 +196,3 @@ for (var i = 0; i < test_frames.length; i++) {

while (chunk = stream.read()) {
buffer = concat([buffer, chunk]);
buffer = util.concat([buffer, chunk]);
}

@@ -257,3 +243,3 @@ expect(buffer).to.be.deep.equal(test.buffer);

it('should transform buffers to appropriate frame object', function() {
var stream = new Deserializer();
var stream = new Deserializer(util.log);

@@ -260,0 +246,0 @@ var shuffled = shuffle_buffers(test_frames.map(function(test) { return test.buffer; }));

var expect = require('chai').expect;
var util = require('./util');
var fs = require('fs');

@@ -9,5 +10,6 @@ var path = require('path');

var tls = {
var options = {
key: fs.readFileSync(path.join(__dirname, '../example/localhost.key')),
cert: fs.readFileSync(path.join(__dirname, '../example/localhost.crt'))
cert: fs.readFileSync(path.join(__dirname, '../example/localhost.crt')),
log: util.log
};

@@ -22,3 +24,3 @@

var server = http2.createServer(tls, function(request, response) {
var server = http2.createServer(options, function(request, response) {
expect(request.url).to.equal(path);

@@ -25,0 +27,0 @@ response.end(message);

var expect = require('chai').expect;
var util = require('./util');
var Stream = require('../lib/stream').Stream;
function callNTimes(limit, done) {
var i = 0;
return function() {
i += 1;
if (i === limit) {
done();
}
};
}
function createStream() {
var stream = new Stream();
var stream = new Stream(util.log);
stream.upstream._window = Infinity;

@@ -47,3 +38,3 @@ stream.upstream._remoteFlowControlDisabled = true;

if ('outgoing' in step || 'event' in step) {
if ('outgoing' in step || 'event' in step || 'active' in step) {
checks.push(step);

@@ -53,2 +44,7 @@ }

var activeCount = 0;
function count_change(change) {
activeCount += change;
}
function execute(callback) {

@@ -58,5 +54,9 @@ var command = commands.shift();

if ('method' in command) {
stream[command.method.name].apply(stream, command.method.arguments);
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);

@@ -83,3 +83,7 @@ execute(callback);

if ('outgoing' in check) {
expect(outgoing_frames.shift()).to.deep.equal(check.outgoing);
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) {

@@ -91,2 +95,4 @@ var event = events.shift();

});
} else if ('active' in check) {
expect(activeCount).to.be.equal(check.active);
} else {

@@ -139,10 +145,3 @@ throw new Error('Invalid check', check);

it('should emit error, and answer RST_STREAM for invalid incoming frames in ' + state + ' state', function(done) {
var left = invalid_frames[state].length + 1;
function one_done() {
left -= 1;
if (!left) {
done();
}
}
one_done();
done = util.callNTimes(invalid_frames[state].length, done);

@@ -162,3 +161,3 @@ invalid_frames[state].forEach(function(invalid_frame) {

expect(error_emitted).to.equal(true);
one_done();
done();
});

@@ -175,3 +174,3 @@ });

{ method : { name: 'headers', arguments: [{ ':path': '/' }] } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':path': '/' }, priority: undefined } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } },
{ event : { name: 'state', data: ['OPEN'] } },

@@ -188,3 +187,5 @@

{ event : { name: 'headers', data: [{ ':status': 200 }] } },
{ event : { name: 'state', data: ['CLOSED'] } }
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 0 }
], done);

@@ -208,3 +209,3 @@ });

{ method : { name: 'headers', arguments: [{ ':status': 200 }] } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': 200 }, priority: undefined } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } },

@@ -214,3 +215,5 @@ { wait : 5 },

{ outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
{ event : { name: 'state', data: ['CLOSED'] } }
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 0 }
], done);

@@ -222,8 +225,5 @@ });

var payload = new Buffer(5);
var original_stream = createStream();
var promised_stream = createStream();
var pushStream;
done = callNTimes(2, done);
execute_sequence(original_stream, [
execute_sequence([
// receiving request

@@ -238,7 +238,7 @@ { incoming: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } },

{ method : { name: 'headers', arguments: [{ ':status': '200' }] } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' }, priority: undefined } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' } } },
// sending push promise
{ method : { name: 'promise', arguments: [promised_stream, { ':path': '/' }] } },
{ outgoing: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/' }, promised_stream: promised_stream } },
{ method : { name: 'promise', arguments: [{ ':path': '/' }], ret: function(str) { pushStream = str; } } },
{ outgoing: { type: 'PUSH_PROMISE', flags: { }, headers: { ':path': '/' } } },

@@ -248,20 +248,24 @@ // sending response data

{ outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
{ event : { name: 'state', data: ['CLOSED'] } }
], done);
{ event : { name: 'state', data: ['CLOSED'] } },
execute_sequence(promised_stream, [
{ active : 0 }
], function() {
// initial state of the promised stream
{ event : { name: 'state', data: ['RESERVED_LOCAL'] } },
expect(pushStream.state).to.equal('RESERVED_LOCAL');
// push headers
{ wait : 5 },
{ method : { name: 'headers', arguments: [{ ':status': '200' }] } },
{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' }, priority: undefined } },
{ event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } },
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'] } }
], done);
// push data
{ method : { name: 'end', arguments: [payload] } },
{ outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 1 }
], done);
});
});

@@ -275,3 +279,3 @@ });

done = callNTimes(2, done);
done = util.callNTimes(2, done);

@@ -282,3 +286,3 @@ execute_sequence(original_stream, [

{ method : { name: 'end', arguments: [] } },
{ outgoing: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' }, priority: undefined } },
{ outgoing: { type: 'HEADERS', flags: { END_STREAM: true }, headers: { ':path': '/' } } },
{ event : { name: 'state', data: ['OPEN'] } },

@@ -298,3 +302,5 @@ { event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } },

{ incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: payload } },
{ event : { name: 'state', data: ['CLOSED'] } }
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 0 }
], done);

@@ -314,3 +320,5 @@

{ incoming: { type: 'DATA', flags: { END_STREAM: true }, data: payload } },
{ event : { name: 'state', data: ['CLOSED'] } }
{ event : { name: 'state', data: ['CLOSED'] } },
{ active : 0 }
], done);

@@ -317,0 +325,0 @@ });

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc