mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 23:38:38 +09:00
import FIREFOX_52_6_0esr_RELEASE from mozilla-esr52 hg repo
This commit is contained in:
commit
dcd9973243
150858 changed files with 23884658 additions and 0 deletions
1366
testing/xpcshell/node-http2/lib/protocol/compressor.js
Normal file
1366
testing/xpcshell/node-http2/lib/protocol/compressor.js
Normal file
File diff suppressed because it is too large
Load diff
619
testing/xpcshell/node-http2/lib/protocol/connection.js
Normal file
619
testing/xpcshell/node-http2/lib/protocol/connection.js
Normal 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);
|
||||
});
|
||||
}
|
||||
};
|
||||
262
testing/xpcshell/node-http2/lib/protocol/endpoint.js
Normal file
262
testing/xpcshell/node-http2/lib/protocol/endpoint.js
Normal 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;
|
||||
};
|
||||
353
testing/xpcshell/node-http2/lib/protocol/flow.js
Normal file
353
testing/xpcshell/node-http2/lib/protocol/flow.js
Normal 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;
|
||||
};
|
||||
1165
testing/xpcshell/node-http2/lib/protocol/framer.js
Normal file
1165
testing/xpcshell/node-http2/lib/protocol/framer.js
Normal file
File diff suppressed because it is too large
Load diff
91
testing/xpcshell/node-http2/lib/protocol/index.js
Normal file
91
testing/xpcshell/node-http2/lib/protocol/index.js
Normal 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
|
||||
|
||||
*/
|
||||
659
testing/xpcshell/node-http2/lib/protocol/stream.js
Normal file
659
testing/xpcshell/node-http2/lib/protocol/stream.js
Normal 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;
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue