Socket
Socket
Sign inDemoInstall

http2

Package Overview
Dependencies
0
Maintainers
1
Versions
44
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 0.1.1 to 0.2.0

test/http.js

33

example/client.js

@@ -1,19 +0,30 @@

var parse_url = require('url').parse;
var fs = require('fs');
var path = require('path');
var http2 = require('..');
var url = parse_url(process.argv.pop());
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
var request = http2.request({
method: 'get',
host: url.hostname,
port: url.port,
url: url.path,
rejectUnauthorized: false
});
request.end();
var request = http2.get(process.argv.pop());
var push_count = 0;
var finished = 0;
function finish() {
finished += 1;
if (finished === (1 + push_count)) {
process.exit();
}
}
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', process.exit);
response.on('end', finish);
});

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

var server = http2.createServer({
key: fs.readFileSync(path.join(__dirname, './localhost.key')),
var options = {
key: fs.readFileSync(path.join(__dirname, '/localhost.key')),
cert: fs.readFileSync(path.join(__dirname, '/localhost.crt'))
}, function(request, response) {
};
var server = http2.createServer(options, function(request, response) {
var filename = path.join(__dirname, request.url);
console.error('Incoming request:', request.url, '(' + filename + ')');
if (fs.existsSync(filename) && fs.statSync(filename).isFile()) {
if ((filename.indexOf(__dirname) === 0) && fs.existsSync(filename) && fs.statSync(filename).isFile()) {
console.error('Reading file from disk.');
var filestream = fs.createReadStream(filename);
response.writeHead('200');
// If they download the certificate, push the private key too, they might need it.
if (request.url === '/localhost.crt') {
var push = response.push('/localhost.key');
push.writeHead(200);
fs.createReadStream(path.join(__dirname, '/localhost.key')).pipe(push);
}
filestream.pipe(response);

@@ -26,7 +36,4 @@

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

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

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

function CompressionContext(table, limit) {
function CompressionContext(log, table, limit) {
this._log = log;
this._table = table.slice();

@@ -107,2 +108,5 @@ this._limit = limit || DEFAULT_HEADER_TABLE_LIMIT;

CompressionContext.prototype.execute = function execute(command) {
this._log.trace({ key: command.name, value: command.value, index: command.index },
'Executing a header representation');
var index, pair;

@@ -253,3 +257,3 @@

: CompressionContext.initialResponseTable;
this._context = new CompressionContext(initialTable);
this._context = new CompressionContext(this._log, initialTable);

@@ -299,3 +303,3 @@ this._initializeStream();

}
this._log.debug({ headers: headers }, 'Header decompression is done');
this._log.trace({ headers: headers }, 'Header decompression is done');
return headers;

@@ -316,3 +320,3 @@ };

: CompressionContext.initialResponseTable;
this._context = new CompressionContext(initialTable);
this._context = new CompressionContext(this._log, initialTable);

@@ -328,3 +332,3 @@ this._initializeStream();

Compressor.prototype.compress = function compress(headers) {
this._log.debug({ headers: headers }, 'Starting header compression');
this._log.trace({ headers: headers }, 'Starting header compression');
var i;

@@ -339,6 +343,6 @@

for (i = 0; i< value.length; i++) {
pairs.push([name, value[i]]);
pairs.push([String(name), String(value[i])]);
}
} else {
pairs.push([name, value]);
pairs.push([String(name), String(value)]);
}

@@ -660,2 +664,3 @@ }

priority: frame.priority,
promised_stream: frame.promised_stream,
data: chunks[i]

@@ -694,3 +699,5 @@ });

if ((frame.type !== this._type) || (frame.stream !== this._stream)) {
this.emit('error', 'A series of header frames must not be interleaved with other frames!');
this._log.error('A series of HEADER frames were not continuous');
this.emit('error', 'PROTOCOL_ERROR');
return;
}

@@ -721,3 +728,9 @@ this._frames.push(frame);

}));
var headers = this.decompress(buffer);
try {
var headers = this.decompress(buffer);
} catch(error) {
this._log.error({ err: error }, 'Header decompression error');
this.emit('error', 'COMPRESSION_ERROR');
return;
}
this.push({

@@ -728,2 +741,3 @@ type: frame.type,

priority: frame.priority,
promised_stream: frame.promised_stream,
headers: headers

@@ -730,0 +744,0 @@ });

@@ -17,3 +17,3 @@ var logging = require('./logging');

// * **new Flow(firstStreamId, settings, [log])**: create a new Connection
// * **new Connection(firstStreamId, settings, [log])**: create a new Connection
//

@@ -131,4 +131,6 @@ // * **Event: 'error' (type)**: signals a connection level error

(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');

@@ -159,3 +161,3 @@ }

Connection.prototype._newStream = function _newStream(id) {
this._log.trace({ id: id }, 'Adding new stream.');
this._log.trace({ stream_id: id }, 'Adding new stream.');
var stream = new Stream(this._log.child({ stream_id: id }));

@@ -168,3 +170,3 @@ this._streamsIds[id] = stream;

Connection.prototype._activateStream = function _activateStream(stream) {
this._log.trace({ id: this._getIdOf(stream) }, 'Activating stream.');
this._log.trace({ stream_id: this._getIdOf(stream) }, 'Activating stream.');
this._streamPriorities.push(stream);

@@ -182,6 +184,6 @@ stream.upstream.on('readable', this.read.bind(this, 0));

Connection.prototype._createIncomingStream = function _createIncomingStream(id) {
this._log.debug({ id: id }, 'New incoming stream.');
this._log.debug({ stream_id: id }, 'New incoming stream.');
if ((id <= this._lastIncomingStream) || ((id - this._nextStreamId) % 2 === 0)) {
this._log.error({ id: id, lastIncomingStream: this._lastIncomingStream }, 'Invalid incoming stream ID.');
this._log.error({ stream_id: id, lastIncomingStream: this._lastIncomingStream }, 'Invalid incoming stream ID.');
this.emit('error', 'PROTOCOL_ERROR');

@@ -204,3 +206,3 @@ return undefined;

this._log.trace({ id: id }, 'Creating new outbound stream.');
this._log.trace({ stream_id: id }, 'Creating new outbound stream.');

@@ -255,3 +257,3 @@ // * Creating a new Stream.

// * Looping through the active streams in priority order and forwarding frames from streams
stream_loop:
stream_loop:
for (var i = 0; i < this._streamPriorities.length; i++) {

@@ -269,2 +271,3 @@ var stream = this._streamPriorities[i];

this._log.trace({ stream_id: id, frame: frame }, 'Trying to forward outgoing frame');
var moreNeeded = this._push(frame, unshiftRemainder);

@@ -279,2 +282,4 @@

}
this._log.trace({ moreNeeded: moreNeeded }, 'Stopping forwarding frames from streams.');
};

@@ -285,2 +290,4 @@

Connection.prototype._receive = function _receive(frame, done) {
this._log.trace({ frame: frame }, 'Forwarding incoming frame');
// * gets the appropriate stream from the stream registry

@@ -308,2 +315,6 @@ var stream = this._streamsIds[frame.stream];

var defaultSettings = {
SETTINGS_FLOW_CONTROL_OPTIONS: true
};
// Settings management initialization:

@@ -313,3 +324,3 @@ Connection.prototype._initializeSettingsManagement = function _initializeSettingsManagement(settings) {

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

@@ -316,0 +327,0 @@ // * Checking that the first frame the other endpoint sends is SETTINGS

@@ -99,3 +99,2 @@ var assert = process.env.HTTP2_ASSERT ? require('assert') : function noop() {};

Flow.prototype._write = function _write(frame, encoding, callback) {
this._log.trace({ frame: frame }, 'Receiving frame');
this.emit('receiving', frame);

@@ -121,3 +120,4 @@

if ((frame.type === 'WINDOW_UPDATE') && (!this._flowControlId || (frame.stream === this._flowControlId))) {
if ((frame.type === 'WINDOW_UPDATE') &&
((this._flowControlId === undefined) || (frame.stream === this._flowControlId))) {
this._updateWindow(frame);

@@ -186,2 +186,5 @@ }

this._send();
} else {
assert(this._window === 0);
assert(this._queue[0].type === 'DATA');
}

@@ -213,34 +216,42 @@ this._readableState.reading = false;

Flow.prototype._push = function _push(frame, remainderCallback) {
var forwardable, remainder;
if ((frame === null) || (frame.type !== 'DATA') || (this._window >= frame.data.length)) {
forwardable = frame;
}
do {
var forwardable = undefined, remainder = undefined;
if ((frame === null) || (frame.type !== 'DATA') ||
((frame.data.length <= this._window) && (frame.data.length <= MAX_HTTP_PAYLOAD_SIZE))) {
forwardable = frame;
}
else if (this._window <= 0) {
remainder = frame;
}
else if (this._window <= 0) {
remainder = frame;
}
else {
var chunkSize = Math.min(this._window, MAX_HTTP_PAYLOAD_SIZE);
forwardable = {
stream: frame.stream,
type: 'DATA',
data: frame.data.slice(0, chunkSize)
};
else {
var chunkSize = Math.min(this._window, MAX_HTTP_PAYLOAD_SIZE);
forwardable = {
stream: frame.stream,
type: 'DATA',
data: frame.data.slice(0, chunkSize)
};
frame.data = frame.data.slice(chunkSize);
remainder = frame;
}
this._log.trace({ frame: frame, size: frame.data.length, forwardable: chunkSize },
'Splitting out forwardable part of a DATA frame.');
frame.data = frame.data.slice(chunkSize);
remainder = frame;
}
var moreNeeded = null;
if (forwardable !== undefined) {
if (forwardable && forwardable.type === 'DATA') {
this._log.trace({ window: this._window, by: forwardable.data.length },
'Decreasing flow control window size.');
this._window -= forwardable.data.length;
assert(this._window >= 0);
var moreNeeded = null;
if (forwardable !== undefined) {
this._log.trace({ frame: forwardable }, 'Pushing frame into the output queue');
if (forwardable && (forwardable.type === 'DATA') && (this._window !== Infinity)) {
this._log.trace({ window: this._window, by: forwardable.data.length },
'Decreasing flow control window size.');
this._window -= forwardable.data.length;
assert(this._window >= 0);
}
moreNeeded = Duplex.prototype.push.call(this, forwardable);
}
moreNeeded = Duplex.prototype.push.call(this, forwardable);
}
frame = remainder;
} while (remainder && moreNeeded);
if (remainder !== undefined) {

@@ -256,6 +267,6 @@ remainderCallback(remainder);

if (frame === null) {
this._log.trace('Enqueueing End Of Stream');
this._log.debug('Enqueueing outgoing End Of Stream');
} else {
frame.flags = frame.flags || {};
this._log.trace({ frame: frame }, 'Enqueueing frame');
this._log.debug({ frame: frame }, 'Enqueueing outgoing frame');
this.emit('sending', frame);

@@ -262,0 +273,0 @@ }

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

Serializer.prototype._transform = function _transform(frame, encoding, done) {
this._log.debug({ frame: frame }, 'Outgoing frame');
this._log.trace({ frame: frame }, 'Outgoing frame');

@@ -45,3 +45,3 @@ assert(frame.type in Serializer, 'Unknown frame type: ' + frame.type);

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

@@ -90,3 +90,3 @@ }

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

@@ -118,9 +118,9 @@ while(cursor < chunk.length) {

if (this._frame.type) {
try {
Deserializer[this._frame.type](this._buffer, this._frame);
this._log.debug({ frame: this._frame }, 'Incoming frame');
var error = Deserializer[this._frame.type](this._buffer, this._frame);
if (error) {
this._log.error('Incoming frame parsing error');
this.emit('error', 'PROTOCOL_ERROR');
} else {
this._log.trace({ frame: this._frame }, 'Incoming frame');
this.push(this._frame);
} catch(error) {
this._log.error({ err: error }, 'Incoming frame parsing error');
this.emit('error', 'PROTOCOL_ERROR');
}

@@ -450,3 +450,3 @@ } else {

if (buffer.length % 8 !== 0) {
throw new Error('Invalid SETTINGS frame.');
return 'Invalid SETTINGS frame';
}

@@ -544,3 +544,3 @@ for (var i = 0; i < buffer.length / 8; i++) {

Serializer.PING = function writePing(frame, buffers) {
assert(('data' in frame) && (frame.data.length === 8), 'PING frames must carry an 8 byte payload.');
assert(('data' in frame) && (frame.data.length === 8), 'PING frames must carry an 8 byte payload');
buffers.push(frame.data);

@@ -551,3 +551,3 @@ };

if (buffer.length !== 8) {
throw new Error('Invalid size PING frame.');
return 'Invalid size PING frame';
}

@@ -660,4 +660,16 @@ frame.data = buffer;

// flags that are not present.
var frameCounter = 0;
logging.serializers.frame = function(frame) {
var logEntry = {};
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) {

@@ -673,2 +685,6 @@ logEntry[name] = frame[name];

}
if (!('length' in logEntry)) {
logEntry.length = frame.data.length;
}
}

@@ -675,0 +691,0 @@

@@ -1,22 +0,122 @@

var tls = require('tls');
// 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.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.
//
// - **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.
// - **response.writeHead(statusCode, [reasonPhrase], [headers])**: reasonPhrase will always be
// ignored since [it's not supported in HTTP/2][3]
// - **response.setTimeout(timeout, [callback])**: will be ignored for HTTP/2 requests
//
// - **Class: http2.ClientRequest**
// - **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.
// - **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
//
// - **Class: http2.IncomingMessage**
// - has two subclasses for easier interface description: **IncomingRequest** and
// **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.
// - **message.setTimeout(timeout, [callback])**: will be ignored for HTTP/2 requests
//
// - **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.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)**
// - 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.
// - **promise.cancel()**: cancels the promised server push.
//
// API elements not yet implemented:
//
// - **Class: http2.Server**
// - **Event: 'checkContinue'**
// - **Event: 'connect'**
// - **Event: 'upgrade'**
// - **Event: 'clientError'**
// - **server.maxHeadersCount**
// - **server.setTimeout(msecs, callback)**
// - **server.timeout**
//
// - **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: 'upgrade'**
// - **Event: 'continue'**
// - **request.abort()**
//
// - **Class: http2.IncomingMessage**
// - **Event: 'close'**
// - **message.trailers**
//
// [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
// Common server and client side code
// ==================================
var net = require('net');
var url = require('url');
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var PassThrough = require('stream').PassThrough;
var Readable = require('stream').Readable;
var Writable = require('stream').Writable;
var Endpoint = require('./endpoint').Endpoint;
var logging = require('./logging');
var http = require('http');
var https = require('https');
// This is the main API that can be used to create HTTP/2 servers.
var http2 = exports;
exports.STATUS_CODES = http.STATUS_CODES;
exports.IncomingMessage = IncomingMessage;
exports.OutgoingMessage = OutgoingMessage;
// The implemented draft is [http2-04](http://tools.ietf.org/html/draft-ietf-httpbis-http2-04).
// 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-04/2.0';
// The main governing power behind the http2 API design is that it should look very similar to the
// existing node.js [HTTP](http://nodejs.org/api/http.html)/[HTTPS](http://nodejs.org/api/https.html)
// APIs. The additional features of HTTP/2 are exposed as extensions to these APIs. Furthermore,
// node-http2 should fall back to using HTTP/1.1 if needed.
var http = require('http');
var https = require('https');
http2.STATUS_CODES = http.STATUS_CODES;
// This should hold sane defaults. These can be overridden by the user using the options

@@ -28,32 +128,103 @@ // configuration object in client and server APIs.

// Server
// ------
// IncomingMessage class
// ---------------------
// Deviation from the original http API: there's and `options` optional argument. Values in it can
// override the default settings.
http2.createServer = createServer;
http2.Server = Server;
function IncomingMessage(stream, log) {
// * This is basically a read-only wrapper for the [Stream](stream.html) class.
PassThrough.call(this);
stream.pipe(this);
this.stream = stream;
function createServer(options, requestListener) {
if (typeof options === 'function') {
requestListener = options;
options = undefined;
this._log = log;
// * 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;
// * Other metadata is filled in when the headers arrive.
stream.once('headers', this._onHeaders.bind(this));
}
IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } });
IncomingMessage.prototype.setTimeout = function noop() {};
// OutgoingMessage class
// ---------------------
function OutgoingMessage(log) {
// * This is basically a read-only wrapper for the [Stream](stream.html) class.
Writable.call(this);
this._log = log;
this._headers = {};
this.headersSent = false;
this.on('finish', this._finish.bind(this));
}
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));
}
};
var server = new Server(options);
OutgoingMessage.prototype._finish = function _finish() {
if (this.stream) {
this.stream.end();
} else {
this.once('socket', this._finish.bind(this));
}
};
if (requestListener) {
server.on('request', requestListener);
OutgoingMessage.prototype.setHeader = function setHeader(name, value) {
if (this.headersSent) {
throw new Error('Can\'t set headers after they are sent.');
} else {
this._headers[name.toLowerCase()] = value;
}
};
return server;
}
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()];
};
IncomingMessage.prototype.setTimeout = function noop() {};
// Server side
// ===========
exports.createServer = createServer;
exports.Server = Server;
exports.IncomingRequest = IncomingRequest;
exports.OutgoingResponse = OutgoingResponse;
exports.ServerResponse = OutgoingResponse; // for API compatibility
// Server class
// ------------
function Server(options) {
options = options || {};
this._log = (options.log || logging.root).child({ component: 'http' });
this._settings = options.settings;
var start = this._start.bind(this);
var fallback = this._fallback.bind(this);
// HTTP2 over TLS (using NPN instean of ALPN)
if ((options.key && options.cert) || options.pfx) {
this._log.info('Creating HTTP/2 server over TLS/NPN');
options.NPNProtocols = [implementedVersion, 'http/1.1', 'http/1.0'];

@@ -63,3 +234,9 @@ this._server = https.createServer(options);

this._server.removeAllListeners('secureConnection');
this._server.on('secureConnection', this._onSecureConnection.bind(this));
this._server.on('secureConnection', function(socket) {
if (socket.npnProtocol === implementedVersion) {
start(socket);
} else {
fallback(socket);
}
});
this._server.on('request', this.emit.bind(this, 'request'));

@@ -70,3 +247,4 @@ }

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

@@ -76,167 +254,387 @@

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 } });
Server.prototype._onSecureConnection = function _onSecureConnection(socket) {
// Upgrading only if the NPN negotiation was successful
if (socket.npnProtocol === implementedVersion) {
this._start(socket);
}
// Fallback to https
else {
for (var i = 0; i < this._originalSocketListeners.length; i++) {
this._originalSocketListeners[i].call(this._server, socket);
}
}
};
// Starting HTTP/2
Server.prototype._start = function _start(socket) {
var endpoint = new Endpoint('SERVER', this._settings || default_settings);
var logger = this._log.child({ client: socket.remoteAddress + ':' + socket.remotePort });
logger.info('Incoming HTTP/2 connection');
var endpoint = new Endpoint('SERVER', this._settings, logger);
endpoint.pipe(socket).pipe(endpoint);
endpoint.on('stream', this._onStream.bind(this));
var self = this;
endpoint.on('stream', function _onStream(stream) {
var response = new OutgoingResponse(endpoint, stream, logger);
var request = new IncomingRequest(stream, logger);
request.once('ready', self.emit.bind(self, 'request', request, response));
});
this.emit('connection', socket, endpoint);
};
Server.prototype._onStream = function _onStream(stream) {
var request = new IncomingMessage(stream);
var response = new ServerResponse(stream);
Server.prototype._fallback = function _fallback(socket) {
this._log.info({ client: socket.remoteAddress + ':' + socket.remotePort, protocol: socket.npnProtocol },
'Falling back to simple HTTPS');
request.once('ready', this.emit.bind(this, 'request', request, response));
for (var i = 0; i < this._originalSocketListeners.length; i++) {
this._originalSocketListeners[i].call(this._server, socket);
}
this.emit('connection', socket);
};
Server.prototype.listen = function listen(port) {
this._server.listen(port);
// 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() {
this._server.close();
Server.prototype.close = function close(callback) {
this._log.info('Closing server');
this._server.close(callback);
};
// Client
// ------
function createServer(options, requestListener) {
if (typeof options === 'function') {
requestListener = options;
options = undefined;
}
http2.request = function request(options, callback) {
var request = new ClientRequest();
var server = new Server(options);
if (callback) {
request.on('response', callback);
if (requestListener) {
server.on('request', requestListener);
}
var tls_options = {
host: options.hostname || options.host,
port: options.port || 80,
NPNProtocols: [implementedVersion, 'http/1.1', 'http/1.0']
return server;
}
// IncomingRequest class
// ---------------------
function IncomingRequest(stream, log) {
IncomingMessage.call(this, stream, log);
}
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)
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]];
}
var options_to_forward = [
'pfx',
'key',
'passphrase',
'cert',
'ca',
'ciphers',
'rejectUnauthorized',
'secureProtocol'
// * An HTTP/2.0 request 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.
var deprecatedHeaders = [
'connection',
'host',
'keep-alive',
'proxy-connection',
'te',
'transfer-encoding',
'upgrade'
];
for (var i = 0; i < options_to_forward.length; i++) {
var key = options_to_forward[i];
if (key in options) {
tls_options[key] = options[key];
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;
}
}
var socket = tls.connect(tls_options, function() {
// HTTP2 is supported!
if (socket.npnProtocol === implementedVersion) {
var endpoint = new Endpoint('CLIENT', options._settings || default_settings);
endpoint.pipe(socket).pipe(endpoint);
request._start(endpoint.createStream(), options);
}
// * Host header is included in the headers object for backwards compatibility.
headers.host = this.host;
// Fallback
else {
socket.end();
request._fallback(https.request(options));
}
});
// * Signaling that the header arrived.
this._log.info({ method: this.method, scheme: this.scheme, host: this.host,
path: this.url, headers: headers}, 'Incoming request');
this.emit('ready');
};
return request;
// OutgoingResponse class
// ----------------------
function OutgoingResponse(endpoint, stream, log) {
OutgoingMessage.call(this, log);
this.endpoint = endpoint;
this.stream = stream;
this.statusCode = undefined;
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;
}
headers = headers || {};
for (var name in headers) {
this._headers[name.toLowerCase()] = headers[name];
}
if (this.sendDate && !('date' in this._headers)) {
this._headers.date = (new Date()).toUTCString();
}
this._log.info({ status: statusCode, headers: this._headers }, 'Sending server response');
this._headers[':status'] = this.statusCode = statusCode;
this.stream.headers(this._headers);
this.headersSent = true;
};
http2.get = function get(options, callback) {
OutgoingResponse.prototype._implicitHeaders = function _implicitHeaders() {
if (!this.headersSent) {
this.writeHead(this.statusCode);
}
};
// Common IncomingMessage class
// ----------------------------
OutgoingResponse.prototype.write = function write() {
this._implicitHeaders();
return OutgoingMessage.prototype.write.apply(this, arguments);
};
function IncomingMessage(stream) {
PassThrough.call(this);
OutgoingResponse.prototype.end = function end() {
this._implicitHeaders();
return OutgoingMessage.prototype.end.apply(this, arguments);
};
this._stream = stream;
OutgoingResponse.prototype._onRequestHeaders = function _onRequestHeaders(headers) {
this._requestHeaders = headers;
};
this.httpVersion = '2.0';
this.httpVersionMajor = 2;
this.httpVersionMinor = 0;
OutgoingResponse.prototype.push = function push(options) {
if (!this.headersSent) {
throw new Error('Initiating a server push is only possible after the head of the parent ' +
'request is sent.');
}
stream.pipe(this);
stream.once('headers', this._onHeaders.bind(this));
}
IncomingMessage.prototype = Object.create(PassThrough.prototype, { constructor: { value: IncomingMessage } });
if (typeof options === 'string') {
options = url.parse(options);
}
IncomingMessage.prototype._onHeaders = function _onHeaders(headers) {
this.statusCode = headers[':status'];
this.method = headers[':method'];
this.url = headers[':path'];
if (!options.path) {
throw new Error('`path` option is mandatory.');
}
this.headers = headers;
headers.host = headers[':host'];
delete headers[':scheme'];
delete headers[':method'];
delete headers[':host'];
delete headers[':path'];
var promiseHeaders = util._extend({
':method': (options.method || 'GET').toUpperCase(),
':scheme': options.protocol || this._requestHeaders[':scheme'],
':host': options.hostname || options.host || this._requestHeaders[':host'],
':path': options.path
}, options.headers);
this.emit('ready');
var pushStream = this.endpoint.createStream();
this.stream.promise(pushStream, promiseHeaders);
return new OutgoingResponse(this.endpoint, pushStream, this._log);
};
// ServerResponse
// --------------
// Client side
// ===========
function ServerResponse(stream) {
PassThrough.call(this);
exports.request = request;
exports.get = get;
exports.Agent = Agent;
exports.ClientRequest = OutgoingRequest; // for API compatibility
exports.OutgoingRequest = OutgoingRequest;
exports.IncomingResponse = IncomingResponse;
exports.globalAgent = undefined;
this._stream = stream;
// Agent class
// -----------
this.pipe(stream);
function Agent(options) {
EventEmitter.call(this);
options = options || {};
this._settings = options.settings;
this._log = (options.log || logging.root).child({ component: 'http' });
this._endpoints = {};
// * Using an own HTTPS agent, because the global agent does not look at `NPNProtocols` 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.
this._httpsAgent = new https.Agent({
NPNProtocols: [implementedVersion, 'http/1.1', 'http/1.0']
});
}
ServerResponse.prototype = Object.create(PassThrough.prototype, { constructor: { value: ServerResponse } });
Agent.prototype = Object.create(EventEmitter.prototype, { constructor: { value: Agent } });
ServerResponse.prototype.writeHead = function writeHead(statusCode, reasonPhrase, headers) {
if (!headers) {
headers = reasonPhrase;
Agent.prototype.request = function request(options, callback) {
if (typeof options === 'string') {
options = url.parse(options);
}
headers = headers || {};
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 || '/';
headers[':status'] = statusCode;
if (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.');
}
this._stream.headers(headers);
var request = new OutgoingRequest(logging.root);
if (callback) {
request.on('response', callback);
}
var key = [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 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 {
var started = false;
options.NPNProtocols = [implementedVersion, 'http/1.1', 'http/1.0'];
options.agent = this._httpsAgent;
var httpsRequest = https.request(options);
httpsRequest.on('socket', function(socket) {
if (socket.npnProtocol !== undefined) {
negotiated();
} else {
socket.on('secureConnect', negotiated);
}
});
var negotiated = function negotiated() {
if (!started) {
if (httpsRequest.socket.npnProtocol === implementedVersion) {
httpsRequest.socket.emit('agentRemove');
unbundleSocket(httpsRequest.socket);
var logger = this._log.child({ server: options.host + ':' + options.port });
var endpoint = new Endpoint('CLIENT', this._settings, logger);
endpoint.socket = httpsRequest.socket;
endpoint.pipe(endpoint.socket).pipe(endpoint);
this._endpoints[key] = endpoint;
this.emit(key, endpoint);
} else {
this.emit(key, undefined);
}
}
}.bind(this);
this.once(key, function(endpoint) {
started = true;
if (endpoint) {
request._start(endpoint.createStream(), options);
} else {
request._fallback(httpsRequest);
}
});
}
return request;
};
// ClientRequest
// -------------
Agent.prototype.get = function get(options, callback) {
var request = this.request(options, callback);
request.end();
return request;
};
function ClientRequest() {
PassThrough.call(this);
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;
}
this._stream = undefined;
this._request = undefined;
var globalAgent = exports.globalAgent = new Agent();
function request(options, callback) {
return globalAgent.request(options, callback);
}
ClientRequest.prototype = Object.create(PassThrough.prototype, { constructor: { value: ClientRequest } });
ClientRequest.prototype._start = function _start(stream, options) {
function get(options, callback) {
return globalAgent.get(options, callback);
}
// OutgoingRequest class
// ---------------------
function OutgoingRequest(log) {
OutgoingMessage.call(this, log);
this.socket = undefined;
this.stream = undefined;
this.request = undefined;
this.headersSent = true;
}
OutgoingRequest.prototype = Object.create(OutgoingMessage.prototype, { constructor: { value: OutgoingRequest } });
OutgoingRequest.prototype._start = function _start(stream, options) {
var logger = this._log.child({ server: (options.hostname || options.host) + ':' + (options.port || 80) });
logger.info('Successfully initiated HTTP/2 connection');
this.stream = stream;
var headers = {};

@@ -247,25 +645,114 @@ for (var key in options.headers) {

delete headers.host;
headers[':scheme'] = 'https';
headers[':scheme'] = options.protocol;
headers[':method'] = options.method;
headers[':host'] = options.hostname || options.host;
headers[':path'] = options.url;
headers[':host'] = options.hostname;
headers[':path'] = options.path;
this._stream = stream;
stream.headers(headers);
this.pipe(stream);
logger.info({ scheme: headers[':scheme'], method: headers[':method'], host: headers[':host'],
path: headers[':path'], headers: (options.headers || {}) }, 'Sending request');
this.stream.headers(headers);
var response = new IncomingMessage(stream);
this.emit('stream', this.stream);
var response = new IncomingResponse(this.stream, logger);
response.once('ready', this.emit.bind(this, 'response', response));
};
ClientRequest.prototype._fallback = function _fallback(request) {
this._request = request;
OutgoingRequest.prototype._fallback = function _fallback(request) {
this._log.info('Falling back to simple HTTPS');
this.request = request;
this.socket = request.socket;
this.emit('socket', request.socket);
this.pipe(request);
};
// Agent
// -----
// Methods only in fallback mode
OutgoingRequest.prototype.setNoDelay = function setNoDelay(noDelay) {
if (this.request) {
this.request.setNoDelay(noDelay);
}
};
// [HTTP agents](http://nodejs.org/api/http.html#http_class_http_agent) are not yet supported,
// so every client request will create a new TCP stream.
OutgoingRequest.prototype.setSocketKeepAlive = function setSocketKeepAlive(enable, initialDelay) {
if (this.request) {
this.request.setSocketKeepAlive(enable, initialDelay);
}
};
OutgoingRequest.prototype.setTimeout = function setTimeout(timeout, callback) {
if (this.request) {
this.request.setTimeout(timeout, callback);
}
};
// IncomingResponse class
// ----------------------
function IncomingResponse(stream, log) {
IncomingMessage.call(this, stream, log);
stream.on('promise', this._onPromise.bind(this));
}
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)
IncomingResponse.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;
// * 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.
// * HTTP/2.0 does not define a way to carry the reason phrase that is included in an HTTP/1.1
// status line.
var statusCode = headers[':status'];
if ((typeof statusCode !== 'string') || (statusCode.length === 0)) {
this._log.error({ key: ':status', value: statusCode }, 'Invalid header field');
this.stream.emit('error', 'PROTOCOL_ERROR');
return;
}
this.statusCode = statusCode;
delete headers[':status'];
// * Signaling that the header arrived.
this._log.info({ status: statusCode, headers: headers}, 'Incoming response');
this.emit('ready');
};
IncomingResponse.prototype._onPromise = function _onPromise(stream, headers) {
var promise = new IncomingPromise(stream, headers, this._log);
if (this.listeners('push').length > 0) {
this.emit('push', promise);
} else {
promise.cancel();
}
};
// IncomingPromise class
// -------------------------
function IncomingPromise(responseStream, promiseHeaders, log) {
var stream = new Readable();
stream._read = function noop() {};
stream.push(null);
IncomingRequest.call(this, stream, log);
stream.emit('headers', promiseHeaders);
this._responseStream = responseStream;
var response = new IncomingResponse(this._responseStream, log);
response.once('ready', this.emit.bind(this, 'response', response));
}
IncomingPromise.prototype = Object.create(IncomingRequest.prototype, { constructor: { value: IncomingPromise } });
IncomingPromise.prototype.cancel = function cancel() {
this._responseStream.reset('CANCEL');
};

@@ -5,3 +5,3 @@ // [node-http2](https://github.com/molnarg/node-http2) consists of the following components:

var http2 = require('./http');
module.exports = http2;
module.exports = http2;

@@ -18,2 +18,5 @@ // * [logging.js](logging.html): a default logger object and a registry of log formatter functions

// * [flow.js](flow.html): flow control
http2.flow = require('./flow');
// * [stream.js](stream.html): implementation of the HTTP/2 stream concept

@@ -20,0 +23,0 @@ http2.stream = require('./stream');

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

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

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

//
// * **Event: 'promise' (headers, stream)**: signals an incoming push promise
// * **Event: 'promise' (stream, headers)**: signals an incoming push promise
//

@@ -29,3 +29,3 @@ // * **Event: 'error' (type)**: signals an error

//
// * **promise(headers, stream)**: promise a stream
// * **promise(stream, headers)**: promise a stream
//

@@ -69,3 +69,3 @@ // * **reset(error)**: reset the stream with an error code

this.on('PUSH_PROMISE', function(frame) {
this.emit('promise', frame.headers, frame.promised_stream);
this.emit('promise', frame.promised_stream, frame.headers);
});

@@ -78,5 +78,5 @@ this.on('HEADERS', function(frame) {

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

@@ -156,2 +156,4 @@ stream.emit('promise_initiated');

Stream.prototype._receive = function _receive(frame, ready) {
this._log.debug({ frame: frame }, 'Receiving frame');
var callReady = true;

@@ -234,3 +236,3 @@

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

@@ -275,2 +277,3 @@ this._transition(true, endFrame);

this.state = 'IDLE';
this._closedByPeer = false;
this.on('sending', this._transition.bind(this, true));

@@ -308,2 +311,16 @@ this.on('receiving', this._transition.bind(this, false));

var DATA = false, HEADERS = false, PRIORITY = false, RST_STREAM = false, SETTINGS = false;
var PUSH_PROMISE = false, PING = false, GOAWAY = 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 'SETTINGS' : SETTINGS = true; break;
case 'PUSH_PROMISE' : PUSH_PROMISE = true; break;
case 'PING' : PING = true; break;
case 'GOAWAY' : GOAWAY = true; break;
case 'WINDOW_UPDATE': WINDOW_UPDATE = true; break;
}
switch (this.state) {

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

case 'IDLE':
if (frame.type === 'HEADERS') {
if (HEADERS) {
this._setState('OPEN');

@@ -337,7 +354,9 @@ if (frame.flags.END_STREAM) {

case 'RESERVED_LOCAL':
if (sending && (frame.type === 'HEADERS')) {
if (sending && HEADERS) {
this._setState('HALF_CLOSED_REMOTE');
} else if (sending && (frame.type === 'RST_STREAM')) {
} else if (RST_STREAM) {
this._setState('CLOSED');
} else if (!(receiving && (frame.type === 'PRIORITY'))) {
} else if (receiving && PRIORITY) {
/* No state change */
} else {
error = 'PROTOCOL_ERROR';

@@ -355,7 +374,9 @@ }

case 'RESERVED_REMOTE':
if (frame.type === 'RST_STREAM') {
if (RST_STREAM) {
this._setState('CLOSED');
} else if (receiving && (frame.type === 'HEADERS')) {
} else if (receiving && HEADERS) {
this._setState('HALF_CLOSED_LOCAL');
} else if (!(sending && (frame.type === 'PRIORITY'))) {
} else if (sending && PRIORITY) {
/* No state change */
} else {
error = 'PROTOCOL_ERROR';

@@ -377,5 +398,7 @@ }

this._setState(sending ? 'HALF_CLOSED_LOCAL' : 'HALF_CLOSED_REMOTE');
} else if (frame.type === 'RST_STREAM') {
} else if (RST_STREAM) {
this._setState('CLOSED');
} // Anything else is OK
} else {
/* No state change */
}
break;

@@ -390,7 +413,9 @@

case 'HALF_CLOSED_LOCAL':
if ((frame.type === 'RST_STREAM') || (receiving && frame.flags.END_STREAM)) {
if (RST_STREAM || (receiving && frame.flags.END_STREAM)) {
this._setState('CLOSED');
} else if (sending && !(frame.type === 'PRIORITY') && !(frame.type === 'WINDOW_UPDATE')) {
} else if (receiving || (sending && (PRIORITY || WINDOW_UPDATE))) {
/* No state change */
} else {
error = 'PROTOCOL_ERROR';
} // Receiving anything is OK
}
break;

@@ -409,7 +434,9 @@

case 'HALF_CLOSED_REMOTE':
if ((frame.type === 'RST_STREAM') || (sending && frame.flags.END_STREAM)) {
if (RST_STREAM || (sending && frame.flags.END_STREAM)) {
this._setState('CLOSED');
} else if (receiving && !(frame.type === 'PRIORITY') && !(frame.type === 'WINDOW_UPDATE')) {
} else if (sending || (receiving && (WINDOW_UPDATE || PRIORITY))) {
/* No state change */
} else {
error = 'PROTOCOL_ERROR';
} // Sending anything is OK
}
break;

@@ -438,12 +465,18 @@

case 'CLOSED':
if (receiving && (frame.type === 'PUSH_PROMISE')) {
this._setState('RESERVED_REMOTE');
} else if (!(sending && (frame.type === 'RST_STREAM')) &&
!(receiving && (frame.type === 'WINDOW_UPDATE')) &&
!(receiving && (frame.type === 'PRIORITY'))) {
error = 'PROTOCOL_ERROR';
} // TODO: act based on the reason for termination.
if ((sending && RST_STREAM) || (receiving && !this._closedByPeer) ||
(receiving && (WINDOW_UPDATE || PRIORITY))) {
/* 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 (receiving && (RST_STREAM || frame.flags.END_STREAM)) {
this._closedByPeer = true;
}
// Sending/receiving a PUSH_PROMISE

@@ -455,4 +488,5 @@ //

// The state of the stream becomes "reserved (remote)".
if (!error && (frame.type === 'PUSH_PROMISE')) {
assert(frame.promised_stream.state === 'IDLE');
if (PUSH_PROMISE && !error) {
assert(frame.promised_stream.state === 'IDLE', 'Promised stream is in invalid state (' +
frame.promised_stream.state + ')');
frame.promised_stream._setState(sending ? 'RESERVED_LOCAL' : 'RESERVED_REMOTE');

@@ -459,0 +493,0 @@ }

{
"name": "http2",
"version": "0.1.1",
"version": "0.2.0",
"description": "An HTTP/2 server implementation",

@@ -29,3 +29,6 @@ "main": "lib/index.js",

"keywords": [
"http2"
"http",
"http2",
"client",
"server"
],

@@ -32,0 +35,0 @@ "author": "Gábor Molnár <gabor@molnar.es> (http://gabor.molnar.es)",

node-http2
==========
An HTTP/2 server implementation for node.js, developed as a [Google Summer of Code project][1].
An HTTP/2 server implementation for node.js, developed as a [Google Summer of Code project]
(https://google-melange.appspot.com/gsoc/project/google/gsoc2013/molnarg/5001).
[1]: https://google-melange.appspot.com/gsoc/project/google/gsoc2013/molnarg/5001
Status
------
I post weekly status updates [on my blog][1]. Short version: the first version of the public API is
in place, server push is not exposed yet, prioritization and ALPN support is not yet done. Main
missing items will be tracked in the issue tracker under the label [feature][2].
[1]: http://gabor.molnar.es/blog/categories/google-summer-of-code/
[2]: https://github.com/molnarg/node-http2/issues?labels=feature&state=open
Installation
------------
Using npm:
```

@@ -32,9 +19,10 @@ npm install http2

goal is the perfect API compatibility, with additional HTTP2 related extensions (like server push).
Currently, basic operations work, server push is not yet exposed to the public API. See the examples
for more info.
Detailed API documentation is primarily maintained in the `lib/http.js` file and is [available in
the wiki](wiki/node-http2-API) as well.
Examples
--------
Using as a server:
### Using as a server ###

@@ -54,3 +42,3 @@ ```javascript

Using as a client:
### Using as a client ###

@@ -60,11 +48,6 @@ ```javascript

var request = http2.request({
method: 'get',
host: 'gabor.molnar.es',
port: 8080,
url: '/',
rejectUnauthorized: false
});
request.end();
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
var request = http2.get('https://gabor.molnar.es:8080/');
request.on('response', function(response) {

@@ -75,5 +58,7 @@ response.pipe(process.stdout);

An example server (serving up static files from its own directory) and client are available in the
example directory. Running the server:
### 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

@@ -84,9 +69,29 @@ $ node ./example/server.js

An example client is also available. Downloading the server's source code from the server (the
downloaded content gets pumped out to the standard error output):
### 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 'http://localhost:8080/server.js' 2>/tmp/server.js
$ node ./example/client.js 'https://localhost:8080/server.js' 2>/tmp/server.js
```
### Server push ###
For a server push example, see the source code of the example
[server](blob/master/example/server.js) and [client](blob/master/example/client.js).
Status
------
I post weekly status updates [on my blog](http://gabor.molnar.es/blog/categories/google-summer-of-code/).
Short version: main missing items are:
* prioritization
(issue [#19](https://github.com/molnarg/node-http2/issues/19)
and [#20](https://github.com/molnarg/node-http2/issues/20))
* ALPN support for negotiating HTTP/2 over TLS (it's done with NPN for now)
(issue [#5](https://github.com/molnarg/node-http2/issues/5))
* Upgrade mechanism to start HTTP/2 over unencrypted channel
(issue [#4](https://github.com/molnarg/node-http2/issues/4))
Development

@@ -98,18 +103,14 @@ -----------

There's a few library you will need to have installed to do anything described in the following
sections. After installing node-http2, run `npm install` in its directory to install development
dependencies.
sections. After installing/cloning node-http2, run `npm install` in its directory to install
development dependencies.
Used libraries:
* [mocha][1] for tests
* [chai][2] for assertions
* [istanbul][3] for code coverage analysis
* [docco][4] for developer documentation
* [bunyan][5] for logging
* [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
[1]: http://visionmedia.github.io/mocha/
[2]: http://chaijs.com/
[3]: https://github.com/gotwarlost/istanbul
[4]: http://jashkenas.github.io/docco/
[5]: https://github.com/trentm/node-bunyan
For pretty printing logs, you will also need a global install of bunyan (`npm install -g bunyan`).

@@ -129,15 +130,14 @@ ### Developer documentation ###

To generate a code coverage report, run `npm test --coverage`. Code coverage summary as of version
0.0.6:
To generate a code coverage report, run `npm test --coverage` (which runs very slowly, be patient).
Code coverage summary as of version 0.2.0:
```
Statements : 91.18% ( 775/850 )
Branches : 84.69% ( 249/294 )
Functions : 88.03% ( 103/117 )
Lines : 91.18% ( 775/850 )
Statements : 85.04% ( 1177/1384 )
Branches : 70.21% ( 370/527 )
Functions : 83.94% ( 162/193 )
Lines : 85.31% ( 1173/1375 )
```
There's a hosted version of the detailed (line-by-line) coverage report [here][1].
There's a hosted version of the detailed (line-by-line) coverage report
[here](http://molnarg.github.io/node-http2/coverage/lcov-report/lib/).
[1]: http://molnarg.github.io/node-http2/coverage/lcov-report/lib/
### Logging ###

@@ -147,3 +147,3 @@

`fatal`, `error`, `warn`, `info`, `debug` or `trace` (the logging level). Log output is in JSON
format, and can be pretty printed using the [bunyan][7] command line tool.
format, and can be pretty printed using the bunyan command line tool.

@@ -159,5 +159,11 @@ For example, running the test client with debug level logging output:

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/), and Nick Hurley of Mozilla, my GSoC
mentor, who helps with regular code review and technical advices.
License

@@ -164,0 +170,0 @@ -------

@@ -6,2 +6,12 @@ var expect = require('chai').expect;

function callNTimes(limit, done) {
var i = 0;
return function() {
i += 1;
if (i === limit) {
done();
}
};
}
var settings = {

@@ -13,3 +23,3 @@ SETTINGS_MAX_CONCURRENT_STREAMS: 100,

describe('connection.js', function() {
describe('scenario', function() {
describe('test scenario', function() {
describe('connection setup', function() {

@@ -61,21 +71,65 @@ it('should work as expected', function(done) {

// Waiting for answer
var headers_arrived = false;
var data_arrived = false;
done = callNTimes(2, done);
client_stream.on('headers', function(headers) {
expect(headers).to.deep.equal(response_headers);
headers_arrived = true;
done();
});
client_stream.on('data', function(chunk) {
expect(chunk).to.deep.equal(response_data);
data_arrived = true;
client_stream.on('readable', function() {
expect(client_stream.read()).to.deep.equal(response_data);
done();
});
client_stream.on('end', function() {
expect(headers_arrived).to.equal(true);
expect(data_arrived).to.equal(true);
});
});
describe('server push', function() {
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' }));
c.pipe(s).pipe(c);
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 = callNTimes(4, done);
s.on('stream', function(response) {
response.headers(response_headers);
var pushed = this.createStream();
response.promise(pushed, push_request_headers);
pushed.headers(push_response_headers);
pushed.write(push_content);
response.write(response_content);
});
var request = c.createStream();
request.headers(request_headers);
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();
});
});
});
});
describe('ping test', function() {
it('client ping', function(done) {
describe('ping from client', function() {
it('should work as expected', function(done) {
var c = new Connection(1, settings, log_root.child({ role: 'client' }));

@@ -85,7 +139,9 @@ var s = new Connection(2, settings, log_root.child({ role: 'server' }));

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

@@ -95,3 +151,3 @@ var s = new Connection(2, settings, log_root.child({ role: 'server' }));

c.pipe(s).pipe(c);
s.ping(function(id) {
s.ping(function() {
done();

@@ -101,3 +157,43 @@ });

});
describe('creating two streams and then using them in reverse order', function() {
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' }));
c.pipe(s).pipe(c);
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) {
var c = new Connection(1, settings, log_root.child({ role: 'client' }));
var s = new Connection(2, settings, log_root.child({ role: 'server' }));
c.pipe(s).pipe(c);
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 = callNTimes(2, done);
request.on('promise', function() {
done();
});
});
});
});
});

@@ -34,5 +34,5 @@ var expect = require('chai').expect;

var emit = stream.emit, events = [];
stream.emit = function(name, data) {
stream.emit = function(name) {
if (recorded_events.indexOf(name) !== -1) {
events.push({ name: name, data: data });
events.push({ name: name, data: Array.prototype.slice.call(arguments, 1) });
}

@@ -83,3 +83,7 @@ return emit.apply(this, arguments);

} else if ('event' in check) {
expect(events.shift()).to.deep.equal(check.event);
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 {

@@ -167,7 +171,7 @@ throw new Error('Invalid check', check);

{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':path': '/' }, priority: undefined } },
{ event : { name: 'state', data: 'OPEN' } },
{ event : { name: 'state', data: ['OPEN'] } },
{ wait : 5 },
{ method : { name: 'end', arguments: [] } },
{ event : { name: 'state', data: 'HALF_CLOSED_LOCAL' } },
{ event : { name: 'state', data: ['HALF_CLOSED_LOCAL'] } },
{ outgoing: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(0) } },

@@ -178,4 +182,4 @@

{ incoming: { type: 'DATA' , flags: { END_STREAM: true }, data: new Buffer(5) } },
{ event : { name: 'headers', data: { ':status': 200 } } },
{ event : { name: 'state', data: 'CLOSED' } }
{ event : { name: 'headers', data: [{ ':status': 200 }] } },
{ event : { name: 'state', data: ['CLOSED'] } }
], done);

@@ -189,4 +193,4 @@ });

{ incoming: { type: 'HEADERS', flags: { }, headers: { ':path': '/' } } },
{ event : { name: 'state', data: 'OPEN' } },
{ event : { name: 'headers', data: { ':path': '/' } } },
{ event : { name: 'state', data: ['OPEN'] } },
{ event : { name: 'headers', data: [{ ':path': '/' }] } },

@@ -196,3 +200,3 @@ { wait : 5 },

{ incoming: { type: 'DATA', flags: { END_STREAM: true }, data: new Buffer(10) } },
{ event : { name: 'state', data: 'HALF_CLOSED_REMOTE' } },
{ event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } },

@@ -206,3 +210,3 @@ { wait : 5 },

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

@@ -222,5 +226,5 @@ });

{ 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': '/' } } },
{ event : { name: 'state', data: ['OPEN'] } },
{ event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } },
{ event : { name: 'headers', data: [{ ':path': '/' }] } },

@@ -239,3 +243,3 @@ // sending response headers

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

@@ -245,3 +249,3 @@

// initial state of the promised stream
{ event : { name: 'state', data: 'RESERVED_LOCAL' } },
{ event : { name: 'state', data: ['RESERVED_LOCAL'] } },

@@ -252,3 +256,3 @@ // push headers

{ outgoing: { type: 'HEADERS', flags: { }, headers: { ':status': '200' }, priority: undefined } },
{ event : { name: 'state', data: 'HALF_CLOSED_REMOTE' } },
{ event : { name: 'state', data: ['HALF_CLOSED_REMOTE'] } },

@@ -258,3 +262,3 @@ // push data

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

@@ -276,4 +280,4 @@ });

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

@@ -283,11 +287,11 @@ // receiving response headers

{ incoming: { type: 'HEADERS', flags: { }, headers: { ':status': 200 } } },
{ event : { name: 'headers', data: { ':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: { ':path': '/2.html' } } },
{ 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' } }
{ event : { name: 'state', data: ['CLOSED'] } }
], done);

@@ -297,3 +301,3 @@

// initial state of the promised stream
{ event : { name: 'state', data: 'RESERVED_REMOTE' } },
{ event : { name: 'state', data: ['RESERVED_REMOTE'] } },

@@ -303,8 +307,8 @@ // push headers

{ incoming: { type: 'HEADERS', flags: { END_STREAM: false }, headers: { ':status': 200 } } },
{ event : { name: 'state', data: 'HALF_CLOSED_LOCAL' } },
{ event : { name: 'headers', data: { ':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' } }
{ event : { name: 'state', data: ['CLOSED'] } }
], done);

@@ -311,0 +315,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

SocketSocket SOC 2 Logo

Product

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

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc