import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo

This commit is contained in:
Roy Tam 2018-01-19 03:59:58 +08:00
commit dcd9973243
150858 changed files with 23884658 additions and 0 deletions

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

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

File diff suppressed because it is too large Load diff

View file

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

View file

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