mirror of https://github.com/lukechilds/node.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2552 lines
81 KiB
2552 lines
81 KiB
'use strict';
|
|
|
|
/* eslint-disable no-use-before-define */
|
|
|
|
require('internal/util').assertCrypto();
|
|
|
|
const binding = process.binding('http2');
|
|
const assert = require('assert');
|
|
const Buffer = require('buffer').Buffer;
|
|
const EventEmitter = require('events');
|
|
const net = require('net');
|
|
const tls = require('tls');
|
|
const util = require('util');
|
|
const fs = require('fs');
|
|
const errors = require('internal/errors');
|
|
const { Duplex } = require('stream');
|
|
const { URL } = require('url');
|
|
const { onServerStream } = require('internal/http2/compat');
|
|
const { utcDate } = require('internal/http');
|
|
const { _connectionListener: httpConnectionListener } = require('http');
|
|
const { isUint8Array } = process.binding('util');
|
|
const debug = util.debuglog('http2');
|
|
|
|
|
|
const {
|
|
assertIsObject,
|
|
assertValidPseudoHeaderResponse,
|
|
assertValidPseudoHeaderTrailer,
|
|
assertWithinRange,
|
|
getDefaultSettings,
|
|
getSessionState,
|
|
getSettings,
|
|
getStreamState,
|
|
isPayloadMeaningless,
|
|
mapToHeaders,
|
|
NghttpError,
|
|
toHeaderObject,
|
|
updateOptionsBuffer,
|
|
updateSettingsBuffer
|
|
} = require('internal/http2/util');
|
|
|
|
const {
|
|
_unrefActive,
|
|
enroll,
|
|
unenroll
|
|
} = require('timers');
|
|
|
|
const { WriteWrap } = process.binding('stream_wrap');
|
|
const { constants } = binding;
|
|
|
|
const NETServer = net.Server;
|
|
const TLSServer = tls.Server;
|
|
|
|
const kInspect = require('internal/util').customInspectSymbol;
|
|
|
|
const kAuthority = Symbol('authority');
|
|
const kDestroySocket = Symbol('destroy-socket');
|
|
const kHandle = Symbol('handle');
|
|
const kID = Symbol('id');
|
|
const kInit = Symbol('init');
|
|
const kLocalSettings = Symbol('local-settings');
|
|
const kOptions = Symbol('options');
|
|
const kOwner = Symbol('owner');
|
|
const kProceed = Symbol('proceed');
|
|
const kProtocol = Symbol('protocol');
|
|
const kRemoteSettings = Symbol('remote-settings');
|
|
const kServer = Symbol('server');
|
|
const kSession = Symbol('session');
|
|
const kSocket = Symbol('socket');
|
|
const kState = Symbol('state');
|
|
const kType = Symbol('type');
|
|
|
|
const kDefaultSocketTimeout = 2 * 60 * 1000;
|
|
const kRenegTest = /TLS session renegotiation disabled for this socket/;
|
|
|
|
const paddingBuffer = new Uint32Array(binding.paddingArrayBuffer);
|
|
|
|
const {
|
|
NGHTTP2_CANCEL,
|
|
NGHTTP2_DEFAULT_WEIGHT,
|
|
NGHTTP2_FLAG_END_STREAM,
|
|
NGHTTP2_HCAT_HEADERS,
|
|
NGHTTP2_HCAT_PUSH_RESPONSE,
|
|
NGHTTP2_HCAT_RESPONSE,
|
|
NGHTTP2_INTERNAL_ERROR,
|
|
NGHTTP2_NO_ERROR,
|
|
NGHTTP2_PROTOCOL_ERROR,
|
|
NGHTTP2_REFUSED_STREAM,
|
|
NGHTTP2_SESSION_CLIENT,
|
|
NGHTTP2_SESSION_SERVER,
|
|
NGHTTP2_ERR_NOMEM,
|
|
NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE,
|
|
NGHTTP2_ERR_INVALID_ARGUMENT,
|
|
NGHTTP2_ERR_STREAM_CLOSED,
|
|
|
|
HTTP2_HEADER_AUTHORITY,
|
|
HTTP2_HEADER_DATE,
|
|
HTTP2_HEADER_METHOD,
|
|
HTTP2_HEADER_PATH,
|
|
HTTP2_HEADER_SCHEME,
|
|
HTTP2_HEADER_STATUS,
|
|
HTTP2_HEADER_CONTENT_LENGTH,
|
|
|
|
NGHTTP2_SETTINGS_HEADER_TABLE_SIZE,
|
|
NGHTTP2_SETTINGS_ENABLE_PUSH,
|
|
NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS,
|
|
NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE,
|
|
NGHTTP2_SETTINGS_MAX_FRAME_SIZE,
|
|
NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE,
|
|
|
|
HTTP2_METHOD_GET,
|
|
HTTP2_METHOD_HEAD,
|
|
HTTP2_METHOD_CONNECT,
|
|
|
|
HTTP_STATUS_CONTENT_RESET,
|
|
HTTP_STATUS_OK,
|
|
HTTP_STATUS_NO_CONTENT,
|
|
HTTP_STATUS_NOT_MODIFIED,
|
|
HTTP_STATUS_SWITCHING_PROTOCOLS
|
|
} = constants;
|
|
|
|
function sessionName(type) {
|
|
switch (type) {
|
|
case NGHTTP2_SESSION_CLIENT:
|
|
return 'client';
|
|
case NGHTTP2_SESSION_SERVER:
|
|
return 'server';
|
|
default:
|
|
return '<invalid>';
|
|
}
|
|
}
|
|
|
|
// Top level to avoid creating a closure
|
|
function emit() {
|
|
this.emit.apply(this, arguments);
|
|
}
|
|
|
|
// Called when a new block of headers has been received for a given
|
|
// stream. The stream may or may not be new. If the stream is new,
|
|
// create the associated Http2Stream instance and emit the 'stream'
|
|
// event. If the stream is not new, emit the 'headers' event to pass
|
|
// the block of headers on.
|
|
function onSessionHeaders(id, cat, flags, headers) {
|
|
_unrefActive(this);
|
|
const owner = this[kOwner];
|
|
debug(`[${sessionName(owner[kType])}] headers were received on ` +
|
|
`stream ${id}: ${cat}`);
|
|
const streams = owner[kState].streams;
|
|
|
|
const endOfStream = !!(flags & NGHTTP2_FLAG_END_STREAM);
|
|
let stream = streams.get(id);
|
|
|
|
// Convert the array of header name value pairs into an object
|
|
const obj = toHeaderObject(headers);
|
|
|
|
if (stream === undefined) {
|
|
switch (owner[kType]) {
|
|
case NGHTTP2_SESSION_SERVER:
|
|
stream = new ServerHttp2Stream(owner, id,
|
|
{ readable: !endOfStream },
|
|
obj);
|
|
if (obj[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) {
|
|
// For head requests, there must not be a body...
|
|
// end the writable side immediately.
|
|
stream.end();
|
|
const state = stream[kState];
|
|
state.headRequest = true;
|
|
}
|
|
break;
|
|
case NGHTTP2_SESSION_CLIENT:
|
|
stream = new ClientHttp2Stream(owner, id, { readable: !endOfStream });
|
|
break;
|
|
default:
|
|
assert.fail(null, null,
|
|
'Internal HTTP/2 Error. Invalid session type. Please ' +
|
|
'report this as a bug in Node.js');
|
|
}
|
|
streams.set(id, stream);
|
|
process.nextTick(emit.bind(owner, 'stream', stream, obj, flags));
|
|
} else {
|
|
let event;
|
|
let status;
|
|
switch (cat) {
|
|
case NGHTTP2_HCAT_RESPONSE:
|
|
status = obj[HTTP2_HEADER_STATUS];
|
|
if (!endOfStream &&
|
|
status !== undefined &&
|
|
status >= 100 &&
|
|
status < 200) {
|
|
event = 'headers';
|
|
} else {
|
|
event = 'response';
|
|
}
|
|
break;
|
|
case NGHTTP2_HCAT_PUSH_RESPONSE:
|
|
event = 'push';
|
|
break;
|
|
case NGHTTP2_HCAT_HEADERS:
|
|
status = obj[HTTP2_HEADER_STATUS];
|
|
if (!endOfStream && status !== undefined && status >= 200) {
|
|
event = 'response';
|
|
} else {
|
|
event = endOfStream ? 'trailers' : 'headers';
|
|
}
|
|
break;
|
|
default:
|
|
assert.fail(null, null,
|
|
'Internal HTTP/2 Error. Invalid headers category. Please ' +
|
|
'report this as a bug in Node.js');
|
|
}
|
|
debug(`[${sessionName(owner[kType])}] emitting stream '${event}' event`);
|
|
process.nextTick(emit.bind(stream, event, obj, flags));
|
|
}
|
|
}
|
|
|
|
// Called to determine if there are trailers to be sent at the end of a
|
|
// Stream. The 'getTrailers' callback is invoked and passed a holder object.
|
|
// The trailers to return are set on that object by the handler. Once the
|
|
// event handler returns, those are sent off for processing. Note that this
|
|
// is a necessarily synchronous operation. We need to know immediately if
|
|
// there are trailing headers to send.
|
|
function onSessionTrailers(id) {
|
|
const owner = this[kOwner];
|
|
debug(`[${sessionName(owner[kType])}] checking for trailers`);
|
|
const streams = owner[kState].streams;
|
|
const stream = streams.get(id);
|
|
// It should not be possible for the stream not to exist at this point.
|
|
// If it does not exist, there is something very very wrong.
|
|
assert(stream !== undefined,
|
|
'Internal HTTP/2 Failure. Stream does not exist. Please ' +
|
|
'report this as a bug in Node.js');
|
|
const getTrailers = stream[kState].getTrailers;
|
|
if (typeof getTrailers !== 'function')
|
|
return [];
|
|
const trailers = Object.create(null);
|
|
getTrailers.call(stream, trailers);
|
|
const headersList = mapToHeaders(trailers, assertValidPseudoHeaderTrailer);
|
|
if (!Array.isArray(headersList)) {
|
|
process.nextTick(() => stream.emit('error', headersList));
|
|
return;
|
|
}
|
|
return headersList;
|
|
}
|
|
|
|
// Called when the stream is closed. The streamClosed event is emitted on the
|
|
// Http2Stream instance. Note that this event is distinctly different than the
|
|
// require('stream') interface 'close' event which deals with the state of the
|
|
// Readable and Writable sides of the Duplex.
|
|
function onSessionStreamClose(id, code) {
|
|
const owner = this[kOwner];
|
|
debug(`[${sessionName(owner[kType])}] session is closing the stream ` +
|
|
`${id}: ${code}`);
|
|
const stream = owner[kState].streams.get(id);
|
|
if (stream === undefined)
|
|
return;
|
|
_unrefActive(this);
|
|
// Set the rst state for the stream
|
|
abort(stream);
|
|
const state = stream[kState];
|
|
state.rst = true;
|
|
state.rstCode = code;
|
|
|
|
if (state.fd !== undefined) {
|
|
debug(`Closing fd ${state.fd} for stream ${id}`);
|
|
fs.close(state.fd, afterFDClose.bind(stream));
|
|
}
|
|
|
|
setImmediate(stream.destroy.bind(stream));
|
|
}
|
|
|
|
function afterFDClose(err) {
|
|
if (err)
|
|
process.nextTick(() => this.emit('error', err));
|
|
}
|
|
|
|
// Called when an error event needs to be triggered
|
|
function onSessionError(error) {
|
|
_unrefActive(this);
|
|
process.nextTick(() => this[kOwner].emit('error', error));
|
|
}
|
|
|
|
// Receives a chunk of data for a given stream and forwards it on
|
|
// to the Http2Stream Duplex for processing.
|
|
function onSessionRead(nread, buf, handle) {
|
|
const streams = this[kOwner][kState].streams;
|
|
const id = handle.id;
|
|
const stream = streams.get(id);
|
|
// It should not be possible for the stream to not exist at this point.
|
|
// If it does not, something is very very wrong
|
|
assert(stream !== undefined,
|
|
'Internal HTTP/2 Failure. Stream does not exist. Please ' +
|
|
'report this as a bug in Node.js');
|
|
const state = stream[kState];
|
|
_unrefActive(this); // Reset the session timeout timer
|
|
_unrefActive(stream); // Reset the stream timeout timer
|
|
|
|
if (nread >= 0 && !stream.destroyed) {
|
|
if (!stream.push(buf)) {
|
|
this.streamReadStop(id);
|
|
state.reading = false;
|
|
}
|
|
} else {
|
|
// Last chunk was received. End the readable side.
|
|
stream.push(null);
|
|
}
|
|
}
|
|
|
|
// Called when the remote peer settings have been updated.
|
|
// Resets the cached settings.
|
|
function onSettings(ack) {
|
|
const owner = this[kOwner];
|
|
debug(`[${sessionName(owner[kType])}] new settings received`);
|
|
_unrefActive(this);
|
|
let event = 'remoteSettings';
|
|
if (ack) {
|
|
if (owner[kState].pendingAck > 0)
|
|
owner[kState].pendingAck--;
|
|
owner[kLocalSettings] = undefined;
|
|
event = 'localSettings';
|
|
} else {
|
|
owner[kRemoteSettings] = undefined;
|
|
}
|
|
// Only emit the event if there are listeners registered
|
|
if (owner.listenerCount(event) > 0) {
|
|
const settings = event === 'localSettings' ?
|
|
owner.localSettings : owner.remoteSettings;
|
|
process.nextTick(emit.bind(owner, event, settings));
|
|
}
|
|
}
|
|
|
|
// If the stream exists, an attempt will be made to emit an event
|
|
// on the stream object itself. Otherwise, forward it on to the
|
|
// session (which may, in turn, forward it on to the server)
|
|
function onPriority(id, parent, weight, exclusive) {
|
|
const owner = this[kOwner];
|
|
debug(`[${sessionName(owner[kType])}] priority advisement for stream ` +
|
|
`${id}: \n parent: ${parent},\n weight: ${weight},\n` +
|
|
` exclusive: ${exclusive}`);
|
|
_unrefActive(this);
|
|
const streams = owner[kState].streams;
|
|
const stream = streams.get(id);
|
|
const emitter = stream === undefined ? owner : stream;
|
|
process.nextTick(
|
|
emit.bind(emitter, 'priority', id, parent, weight, exclusive));
|
|
}
|
|
|
|
function emitFrameError(id, type, code) {
|
|
if (!this.emit('frameError', type, code, id)) {
|
|
const err = new errors.Error('ERR_HTTP2_FRAME_ERROR', type, code, id);
|
|
err.errno = code;
|
|
this.emit('error', err);
|
|
}
|
|
}
|
|
|
|
// Called by the native layer when an error has occurred sending a
|
|
// frame. This should be exceedingly rare.
|
|
function onFrameError(id, type, code) {
|
|
const owner = this[kOwner];
|
|
debug(`[${sessionName(owner[kType])}] error sending frame type ` +
|
|
`${type} on stream ${id}, code: ${code}`);
|
|
_unrefActive(this);
|
|
const streams = owner[kState].streams;
|
|
const stream = streams.get(id);
|
|
const emitter = stream !== undefined ? stream : owner;
|
|
process.nextTick(emitFrameError.bind(emitter, id, type, code));
|
|
}
|
|
|
|
function emitGoaway(state, code, lastStreamID, buf) {
|
|
this.emit('goaway', code, lastStreamID, buf);
|
|
// Tear down the session or destroy
|
|
if (!state.shuttingDown && !state.shutdown) {
|
|
this.shutdown({}, this.destroy.bind(this));
|
|
} else {
|
|
this.destroy();
|
|
}
|
|
}
|
|
|
|
// Called by the native layer when a goaway frame has been received
|
|
function onGoawayData(code, lastStreamID, buf) {
|
|
const owner = this[kOwner];
|
|
const state = owner[kState];
|
|
debug(`[${sessionName(owner[kType])}] goaway data received`);
|
|
process.nextTick(emitGoaway.bind(owner, state, code, lastStreamID, buf));
|
|
}
|
|
|
|
// Returns the padding to use per frame. The selectPadding callback is set
|
|
// on the options. It is invoked with two arguments, the frameLen, and the
|
|
// maxPayloadLen. The method must return a numeric value within the range
|
|
// frameLen <= n <= maxPayloadLen.
|
|
function onSelectPadding(fn) {
|
|
assert(typeof fn === 'function',
|
|
'options.selectPadding must be a function. Please report this as a ' +
|
|
'bug in Node.js');
|
|
return function getPadding() {
|
|
debug('fetching padding for frame');
|
|
const frameLen = paddingBuffer[0];
|
|
const maxFramePayloadLen = paddingBuffer[1];
|
|
paddingBuffer[2] = Math.min(maxFramePayloadLen,
|
|
Math.max(frameLen,
|
|
fn(frameLen,
|
|
maxFramePayloadLen) | 0));
|
|
};
|
|
}
|
|
|
|
// When a ClientHttp2Session is first created, the socket may not yet be
|
|
// connected. If request() is called during this time, the actual request
|
|
// will be deferred until the socket is ready to go.
|
|
function requestOnConnect(headers, options) {
|
|
const session = this[kSession];
|
|
debug(`[${sessionName(session[kType])}] connected.. initializing request`);
|
|
const streams = session[kState].streams;
|
|
|
|
validatePriorityOptions(options);
|
|
const handle = session[kHandle];
|
|
|
|
const headersList = mapToHeaders(headers);
|
|
if (!Array.isArray(headersList)) {
|
|
process.nextTick(() => this.emit('error', headersList));
|
|
return;
|
|
}
|
|
|
|
let getTrailers = false;
|
|
if (typeof options.getTrailers === 'function') {
|
|
getTrailers = true;
|
|
this[kState].getTrailers = options.getTrailers;
|
|
}
|
|
|
|
// ret will be either the reserved stream ID (if positive)
|
|
// or an error code (if negative)
|
|
const ret = handle.submitRequest(headersList,
|
|
!!options.endStream,
|
|
options.parent | 0,
|
|
options.weight | 0,
|
|
!!options.exclusive,
|
|
getTrailers);
|
|
|
|
// In an error condition, one of three possible response codes will be
|
|
// possible:
|
|
// * NGHTTP2_ERR_NOMEM - Out of memory, this should be fatal to the process.
|
|
// * NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE - Maximum stream ID is reached, this
|
|
// is fatal for the session
|
|
// * NGHTTP2_ERR_INVALID_ARGUMENT - Stream was made dependent on itself, this
|
|
// impacts on this stream.
|
|
// For the first two, emit the error on the session,
|
|
// For the third, emit the error on the stream, it will bubble up to the
|
|
// session if not handled.
|
|
let err;
|
|
switch (ret) {
|
|
case NGHTTP2_ERR_NOMEM:
|
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
|
process.nextTick(() => session.emit('error', err));
|
|
break;
|
|
case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
|
|
err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
|
|
process.nextTick(() => this.emit('error', err));
|
|
break;
|
|
case NGHTTP2_ERR_INVALID_ARGUMENT:
|
|
err = new errors.Error('ERR_HTTP2_STREAM_SELF_DEPENDENCY');
|
|
process.nextTick(() => this.emit('error', err));
|
|
break;
|
|
default:
|
|
// Some other, unexpected error was returned. Emit on the session.
|
|
if (ret < 0) {
|
|
err = new NghttpError(ret);
|
|
process.nextTick(() => session.emit('error', err));
|
|
break;
|
|
}
|
|
debug(`[${sessionName(session[kType])}] stream ${ret} initialized`);
|
|
this[kInit](ret);
|
|
streams.set(ret, this);
|
|
}
|
|
}
|
|
|
|
function validatePriorityOptions(options) {
|
|
if (options.weight === undefined) {
|
|
options.weight = NGHTTP2_DEFAULT_WEIGHT;
|
|
} else if (typeof options.weight !== 'number') {
|
|
const err = new errors.RangeError('ERR_INVALID_OPT_VALUE',
|
|
'weight',
|
|
options.weight);
|
|
Error.captureStackTrace(err, validatePriorityOptions);
|
|
throw err;
|
|
}
|
|
|
|
if (options.parent === undefined) {
|
|
options.parent = 0;
|
|
} else if (typeof options.parent !== 'number' || options.parent < 0) {
|
|
const err = new errors.RangeError('ERR_INVALID_OPT_VALUE',
|
|
'parent',
|
|
options.parent);
|
|
Error.captureStackTrace(err, validatePriorityOptions);
|
|
throw err;
|
|
}
|
|
|
|
if (options.exclusive === undefined) {
|
|
options.exclusive = false;
|
|
} else if (typeof options.exclusive !== 'boolean') {
|
|
const err = new errors.RangeError('ERR_INVALID_OPT_VALUE',
|
|
'exclusive',
|
|
options.exclusive);
|
|
Error.captureStackTrace(err, validatePriorityOptions);
|
|
throw err;
|
|
}
|
|
|
|
if (options.silent === undefined) {
|
|
options.silent = false;
|
|
} else if (typeof options.silent !== 'boolean') {
|
|
const err = new errors.RangeError('ERR_INVALID_OPT_VALUE',
|
|
'silent',
|
|
options.silent);
|
|
Error.captureStackTrace(err, validatePriorityOptions);
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
// Creates the internal binding.Http2Session handle for an Http2Session
|
|
// instance. This occurs only after the socket connection has been
|
|
// established. Note: the binding.Http2Session will take over ownership
|
|
// of the socket. No other code should read from or write to the socket.
|
|
function setupHandle(session, socket, type, options) {
|
|
return function() {
|
|
debug(`[${sessionName(type)}] setting up session handle`);
|
|
session[kState].connecting = false;
|
|
|
|
updateOptionsBuffer(options);
|
|
const handle = new binding.Http2Session(type);
|
|
handle[kOwner] = session;
|
|
handle.onpriority = onPriority;
|
|
handle.onsettings = onSettings;
|
|
handle.onheaders = onSessionHeaders;
|
|
handle.ontrailers = onSessionTrailers;
|
|
handle.onstreamclose = onSessionStreamClose;
|
|
handle.onerror = onSessionError;
|
|
handle.onread = onSessionRead;
|
|
handle.onframeerror = onFrameError;
|
|
handle.ongoawaydata = onGoawayData;
|
|
|
|
if (typeof options.selectPadding === 'function')
|
|
handle.ongetpadding = onSelectPadding(options.selectPadding);
|
|
|
|
assert(socket._handle !== undefined,
|
|
'Internal HTTP/2 Failure. The socket is not connected. Please ' +
|
|
'report this as a bug in Node.js');
|
|
handle.consume(socket._handle._externalStream);
|
|
|
|
session[kHandle] = handle;
|
|
|
|
const settings = typeof options.settings === 'object' ?
|
|
options.settings : Object.create(null);
|
|
|
|
session.settings(settings);
|
|
process.nextTick(emit.bind(session, 'connect', session, socket));
|
|
};
|
|
}
|
|
|
|
// Submits a SETTINGS frame to be sent to the remote peer.
|
|
function submitSettings(settings) {
|
|
debug(`[${sessionName(this[kType])}] submitting actual settings`);
|
|
_unrefActive(this);
|
|
this[kLocalSettings] = undefined;
|
|
|
|
updateSettingsBuffer(settings);
|
|
const handle = this[kHandle];
|
|
const ret = handle.submitSettings();
|
|
let err;
|
|
switch (ret) {
|
|
case NGHTTP2_ERR_NOMEM:
|
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
|
process.nextTick(() => this.emit('error', err));
|
|
break;
|
|
default:
|
|
// Some other unexpected error was reported.
|
|
if (ret < 0) {
|
|
err = new NghttpError(ret);
|
|
process.nextTick(() => this.emit('error', err));
|
|
}
|
|
}
|
|
debug(`[${sessionName(this[kType])}] settings complete`);
|
|
}
|
|
|
|
// Submits a PRIORITY frame to be sent to the remote peer
|
|
// Note: If the silent option is true, the change will be made
|
|
// locally with no PRIORITY frame sent.
|
|
function submitPriority(stream, options) {
|
|
debug(`[${sessionName(this[kType])}] submitting actual priority`);
|
|
_unrefActive(this);
|
|
|
|
const handle = this[kHandle];
|
|
const ret =
|
|
handle.submitPriority(
|
|
stream[kID],
|
|
options.parent | 0,
|
|
options.weight | 0,
|
|
!!options.exclusive,
|
|
!!options.silent);
|
|
|
|
let err;
|
|
switch (ret) {
|
|
case NGHTTP2_ERR_NOMEM:
|
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
|
process.nextTick(() => this.emit('error', err));
|
|
break;
|
|
default:
|
|
// Some other unexpected error was reported.
|
|
if (ret < 0) {
|
|
err = new NghttpError(ret);
|
|
process.nextTick(() => this.emit('error', err));
|
|
}
|
|
}
|
|
debug(`[${sessionName(this[kType])}] priority complete`);
|
|
}
|
|
|
|
// Submit an RST-STREAM frame to be sent to the remote peer.
|
|
// This will cause the Http2Stream to be closed.
|
|
function submitRstStream(stream, code) {
|
|
debug(`[${sessionName(this[kType])}] submit actual rststream`);
|
|
_unrefActive(this);
|
|
const id = stream[kID];
|
|
const handle = this[kHandle];
|
|
const ret = handle.submitRstStream(id, code);
|
|
let err;
|
|
switch (ret) {
|
|
case NGHTTP2_ERR_NOMEM:
|
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
|
process.nextTick(() => this.emit('error', err));
|
|
break;
|
|
default:
|
|
// Some other unexpected error was reported.
|
|
if (ret < 0) {
|
|
err = new NghttpError(ret);
|
|
process.nextTick(() => this.emit('error', err));
|
|
break;
|
|
}
|
|
stream.destroy();
|
|
}
|
|
debug(`[${sessionName(this[kType])}] rststream complete`);
|
|
}
|
|
|
|
function doShutdown(options) {
|
|
const handle = this[kHandle];
|
|
const state = this[kState];
|
|
if (handle === undefined || state.shutdown)
|
|
return; // Nothing to do, possibly because the session shutdown already.
|
|
const ret = handle.submitGoaway(options.errorCode | 0,
|
|
options.lastStreamID | 0,
|
|
options.opaqueData);
|
|
state.shuttingDown = false;
|
|
state.shutdown = true;
|
|
if (ret < 0) {
|
|
debug(`[${sessionName(this[kType])}] shutdown failed! code: ${ret}`);
|
|
const err = new NghttpError(ret);
|
|
process.nextTick(() => this.emit('error', err));
|
|
return;
|
|
}
|
|
process.nextTick(emit.bind(this, 'shutdown', options));
|
|
debug(`[${sessionName(this[kType])}] shutdown is complete`);
|
|
}
|
|
|
|
// Submit a graceful or immediate shutdown request for the Http2Session.
|
|
function submitShutdown(options) {
|
|
debug(`[${sessionName(this[kType])}] submitting actual shutdown request`);
|
|
const handle = this[kHandle];
|
|
const type = this[kType];
|
|
if (type === NGHTTP2_SESSION_SERVER &&
|
|
options.graceful === true) {
|
|
// first send a shutdown notice
|
|
handle.submitShutdownNotice();
|
|
// then, on flip of the event loop, do the actual shutdown
|
|
setImmediate(doShutdown.bind(this, options));
|
|
} else {
|
|
doShutdown.call(this, options);
|
|
}
|
|
}
|
|
|
|
function finishSessionDestroy(socket) {
|
|
const state = this[kState];
|
|
if (state.destroyed)
|
|
return;
|
|
|
|
if (!socket.destroyed)
|
|
socket.destroy();
|
|
|
|
state.destroying = false;
|
|
state.destroyed = true;
|
|
|
|
// Destroy the handle
|
|
const handle = this[kHandle];
|
|
if (handle !== undefined) {
|
|
handle.destroy(state.skipUnconsume);
|
|
debug(`[${sessionName(this[kType])}] nghttp2session handle destroyed`);
|
|
}
|
|
this[kHandle] = undefined;
|
|
|
|
process.nextTick(emit.bind(this, 'close'));
|
|
debug(`[${sessionName(this[kType])}] nghttp2session destroyed`);
|
|
}
|
|
|
|
// Upon creation, the Http2Session takes ownership of the socket. The session
|
|
// may not be ready to use immediately if the socket is not yet fully connected.
|
|
class Http2Session extends EventEmitter {
|
|
|
|
// type { number } either NGHTTP2_SESSION_SERVER or NGHTTP2_SESSION_CLIENT
|
|
// options { Object }
|
|
// socket { net.Socket | tls.TLSSocket }
|
|
constructor(type, options, socket) {
|
|
super();
|
|
|
|
// No validation is performed on the input parameters because this
|
|
// constructor is not exported directly for users.
|
|
|
|
// If the session property already exists on the socket,
|
|
// then it has already been bound to an Http2Session instance
|
|
// and cannot be attached again.
|
|
if (socket[kSession] !== undefined)
|
|
throw new errors.Error('ERR_HTTP2_SOCKET_BOUND');
|
|
|
|
socket[kSession] = this;
|
|
|
|
this[kState] = {
|
|
streams: new Map(),
|
|
destroyed: false,
|
|
shutdown: false,
|
|
shuttingDown: false,
|
|
pendingAck: 0,
|
|
maxPendingAck: Math.max(1, (options.maxPendingAck | 0) || 10)
|
|
};
|
|
|
|
this[kType] = type;
|
|
this[kSocket] = socket;
|
|
|
|
// Do not use nagle's algorithm
|
|
socket.setNoDelay();
|
|
|
|
// Disable TLS renegotiation on the socket
|
|
if (typeof socket.disableRenegotiation === 'function')
|
|
socket.disableRenegotiation();
|
|
|
|
socket[kDestroySocket] = socket.destroy;
|
|
socket.destroy = socketDestroy;
|
|
|
|
const setupFn = setupHandle(this, socket, type, options);
|
|
if (socket.connecting) {
|
|
this[kState].connecting = true;
|
|
socket.once('connect', setupFn);
|
|
} else {
|
|
setupFn();
|
|
}
|
|
|
|
// Any individual session can have any number of active open
|
|
// streams, these may all need to be made aware of changes
|
|
// in state that occur -- such as when the associated socket
|
|
// is closed. To do so, we need to set the max listener count
|
|
// to something more reasonable because we may have any number
|
|
// of concurrent streams (2^31-1 is the upper limit on the number
|
|
// of streams)
|
|
this.setMaxListeners((2 ** 31) - 1);
|
|
debug(`[${sessionName(type)}] http2session created`);
|
|
}
|
|
|
|
[kInspect](depth, opts) {
|
|
const state = this[kState];
|
|
const obj = {
|
|
type: this[kType],
|
|
destroyed: state.destroyed,
|
|
destroying: state.destroying,
|
|
shutdown: state.shutdown,
|
|
shuttingDown: state.shuttingDown,
|
|
state: this.state,
|
|
localSettings: this.localSettings,
|
|
remoteSettings: this.remoteSettings
|
|
};
|
|
return `Http2Session ${util.format(obj)}`;
|
|
}
|
|
|
|
// The socket owned by this session
|
|
get socket() {
|
|
return this[kSocket];
|
|
}
|
|
|
|
// The session type
|
|
get type() {
|
|
return this[kType];
|
|
}
|
|
|
|
// true if the Http2Session is waiting for a settings acknowledgement
|
|
get pendingSettingsAck() {
|
|
return this[kState].pendingAck > 0;
|
|
}
|
|
|
|
// true if the Http2Session has been destroyed
|
|
get destroyed() {
|
|
return this[kState].destroyed;
|
|
}
|
|
|
|
// Retrieves state information for the Http2Session
|
|
get state() {
|
|
const handle = this[kHandle];
|
|
return handle !== undefined ?
|
|
getSessionState(handle) :
|
|
Object.create(null);
|
|
}
|
|
|
|
// The settings currently in effect for the local peer. These will
|
|
// be updated only when a settings acknowledgement has been received.
|
|
get localSettings() {
|
|
let settings = this[kLocalSettings];
|
|
if (settings !== undefined)
|
|
return settings;
|
|
|
|
const handle = this[kHandle];
|
|
if (handle === undefined)
|
|
return Object.create(null);
|
|
|
|
settings = getSettings(handle, false); // Local
|
|
this[kLocalSettings] = settings;
|
|
return settings;
|
|
}
|
|
|
|
// The settings currently in effect for the remote peer.
|
|
get remoteSettings() {
|
|
let settings = this[kRemoteSettings];
|
|
if (settings !== undefined)
|
|
return settings;
|
|
|
|
const handle = this[kHandle];
|
|
if (handle === undefined)
|
|
return Object.create(null);
|
|
|
|
settings = getSettings(handle, true); // Remote
|
|
this[kRemoteSettings] = settings;
|
|
return settings;
|
|
}
|
|
|
|
// Submits a SETTINGS frame to be sent to the remote peer.
|
|
settings(settings) {
|
|
if (this[kState].destroyed || this[kState].destroying)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
|
|
|
|
// Validate the input first
|
|
assertIsObject(settings, 'settings');
|
|
settings = Object.assign(Object.create(null), settings);
|
|
assertWithinRange('headerTableSize',
|
|
settings.headerTableSize,
|
|
0, 2 ** 32 - 1);
|
|
assertWithinRange('initialWindowSize',
|
|
settings.initialWindowSize,
|
|
0, 2 ** 32 - 1);
|
|
assertWithinRange('maxFrameSize',
|
|
settings.maxFrameSize,
|
|
16384, 2 ** 24 - 1);
|
|
assertWithinRange('maxConcurrentStreams',
|
|
settings.maxConcurrentStreams,
|
|
0, 2 ** 31 - 1);
|
|
assertWithinRange('maxHeaderListSize',
|
|
settings.maxHeaderListSize,
|
|
0, 2 ** 32 - 1);
|
|
if (settings.enablePush !== undefined &&
|
|
typeof settings.enablePush !== 'boolean') {
|
|
const err = new errors.TypeError('ERR_HTTP2_INVALID_SETTING_VALUE',
|
|
'enablePush', settings.enablePush);
|
|
err.actual = settings.enablePush;
|
|
throw err;
|
|
}
|
|
if (this[kState].pendingAck === this[kState].maxPendingAck) {
|
|
throw new errors.Error('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK',
|
|
this[kState].pendingAck);
|
|
}
|
|
debug(`[${sessionName(this[kType])}] sending settings`);
|
|
|
|
this[kState].pendingAck++;
|
|
if (this[kState].connecting) {
|
|
debug(`[${sessionName(this[kType])}] session still connecting, ` +
|
|
'queue settings');
|
|
this.once('connect', submitSettings.bind(this, settings));
|
|
return;
|
|
}
|
|
submitSettings.call(this, settings);
|
|
}
|
|
|
|
// Submits a PRIORITY frame to be sent to the remote peer.
|
|
priority(stream, options) {
|
|
if (this[kState].destroyed || this[kState].destroying)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
|
|
|
|
if (!(stream instanceof Http2Stream)) {
|
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
|
|
'stream',
|
|
'Http2Stream');
|
|
}
|
|
assertIsObject(options, 'options');
|
|
options = Object.assign(Object.create(null), options);
|
|
validatePriorityOptions(options);
|
|
|
|
debug(`[${sessionName(this[kType])}] sending priority for stream ` +
|
|
`${stream[kID]}`);
|
|
|
|
// A stream cannot be made to depend on itself
|
|
if (options.parent === stream[kID]) {
|
|
debug(`[${sessionName(this[kType])}] session still connecting. queue ` +
|
|
'priority');
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'parent',
|
|
options.parent);
|
|
}
|
|
|
|
if (stream[kID] === undefined) {
|
|
stream.once('ready', submitPriority.bind(this, stream, options));
|
|
return;
|
|
}
|
|
submitPriority.call(this, stream, options);
|
|
}
|
|
|
|
// Submits an RST-STREAM frame to be sent to the remote peer. This will
|
|
// cause the stream to be closed.
|
|
rstStream(stream, code = NGHTTP2_NO_ERROR) {
|
|
// Do not check destroying here, as the rstStream may be sent while
|
|
// the session is in the middle of being destroyed.
|
|
if (this[kState].destroyed)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
|
|
|
|
if (!(stream instanceof Http2Stream)) {
|
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
|
|
'stream',
|
|
'Http2Stream');
|
|
}
|
|
|
|
if (typeof code !== 'number') {
|
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
|
|
'code',
|
|
'number');
|
|
}
|
|
|
|
if (this[kState].rst) {
|
|
// rst has already been called, do not call again,
|
|
// skip straight to destroy
|
|
stream.destroy();
|
|
return;
|
|
}
|
|
stream[kState].rst = true;
|
|
stream[kState].rstCode = code;
|
|
|
|
debug(`[${sessionName(this[kType])}] initiating rststream for stream ` +
|
|
`${stream[kID]}: ${code}`);
|
|
|
|
if (stream[kID] === undefined) {
|
|
debug(`[${sessionName(this[kType])}] session still connecting, queue ` +
|
|
'rststream');
|
|
stream.once('ready', submitRstStream.bind(this, stream, code));
|
|
return;
|
|
}
|
|
submitRstStream.call(this, stream, code);
|
|
}
|
|
|
|
// Destroy the Http2Session
|
|
destroy() {
|
|
const state = this[kState];
|
|
if (state.destroyed || state.destroying)
|
|
return;
|
|
debug(`[${sessionName(this[kType])}] destroying nghttp2session`);
|
|
state.destroying = true;
|
|
|
|
// Unenroll the timer
|
|
unenroll(this);
|
|
|
|
// Shut down any still open streams
|
|
const streams = state.streams;
|
|
streams.forEach((stream) => stream.destroy());
|
|
|
|
// Disassociate from the socket and server
|
|
const socket = this[kSocket];
|
|
// socket.pause();
|
|
delete this[kSocket];
|
|
delete this[kServer];
|
|
|
|
state.destroyed = false;
|
|
state.destroying = true;
|
|
|
|
if (this[kHandle] !== undefined)
|
|
this[kHandle].destroying();
|
|
|
|
setImmediate(finishSessionDestroy.bind(this, socket));
|
|
}
|
|
|
|
// Graceful or immediate shutdown of the Http2Session. Graceful shutdown
|
|
// is only supported on the server-side
|
|
shutdown(options, callback) {
|
|
if (this[kState].destroyed || this[kState].destroying)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
|
|
|
|
if (this[kState].shutdown || this[kState].shuttingDown)
|
|
return;
|
|
|
|
debug(`[${sessionName(this[kType])}] initiating shutdown`);
|
|
this[kState].shuttingDown = true;
|
|
|
|
const type = this[kType];
|
|
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
|
|
assertIsObject(options, 'options');
|
|
options = Object.assign(Object.create(null), options);
|
|
|
|
if (options.opaqueData !== undefined &&
|
|
!isUint8Array(options.opaqueData)) {
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'opaqueData',
|
|
options.opaqueData);
|
|
}
|
|
if (type === NGHTTP2_SESSION_SERVER &&
|
|
options.graceful !== undefined &&
|
|
typeof options.graceful !== 'boolean') {
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'graceful',
|
|
options.graceful);
|
|
}
|
|
if (options.errorCode !== undefined &&
|
|
typeof options.errorCode !== 'number') {
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'errorCode',
|
|
options.errorCode);
|
|
}
|
|
if (options.lastStreamID !== undefined &&
|
|
(typeof options.lastStreamID !== 'number' ||
|
|
options.lastStreamID < 0)) {
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'lastStreamID',
|
|
options.lastStreamID);
|
|
}
|
|
|
|
if (callback) {
|
|
this.on('shutdown', callback);
|
|
}
|
|
|
|
if (this[kState].connecting) {
|
|
debug(`[${sessionName(this[kType])}] session still connecting, queue ` +
|
|
'shutdown');
|
|
this.once('connect', submitShutdown.bind(this, options));
|
|
return;
|
|
}
|
|
|
|
debug(`[${sessionName(this[kType])}] sending shutdown`);
|
|
submitShutdown.call(this, options);
|
|
}
|
|
|
|
_onTimeout() {
|
|
process.nextTick(emit.bind(this, 'timeout'));
|
|
}
|
|
}
|
|
|
|
class ServerHttp2Session extends Http2Session {
|
|
constructor(options, socket, server) {
|
|
super(NGHTTP2_SESSION_SERVER, options, socket);
|
|
this[kServer] = server;
|
|
}
|
|
|
|
get server() {
|
|
return this[kServer];
|
|
}
|
|
}
|
|
|
|
class ClientHttp2Session extends Http2Session {
|
|
constructor(options, socket) {
|
|
super(NGHTTP2_SESSION_CLIENT, options, socket);
|
|
debug(`[${sessionName(this[kType])}] clienthttp2session created`);
|
|
}
|
|
|
|
// Submits a new HTTP2 request to the connected peer. Returns the
|
|
// associated Http2Stream instance.
|
|
request(headers, options) {
|
|
if (this[kState].destroyed || this[kState].destroying)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_SESSION');
|
|
debug(`[${sessionName(this[kType])}] initiating request`);
|
|
_unrefActive(this);
|
|
assertIsObject(headers, 'headers');
|
|
assertIsObject(options, 'options');
|
|
|
|
headers = Object.assign(Object.create(null), headers);
|
|
options = Object.assign(Object.create(null), options);
|
|
|
|
if (headers[HTTP2_HEADER_METHOD] === undefined)
|
|
headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET;
|
|
|
|
const connect = headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT;
|
|
|
|
if (!connect) {
|
|
if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
|
|
headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
|
|
if (headers[HTTP2_HEADER_SCHEME] === undefined)
|
|
headers[HTTP2_HEADER_SCHEME] = this[kProtocol].slice(0, -1);
|
|
if (headers[HTTP2_HEADER_PATH] === undefined)
|
|
headers[HTTP2_HEADER_PATH] = '/';
|
|
} else {
|
|
if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
|
|
throw new errors.Error('ERR_HTTP2_CONNECT_AUTHORITY');
|
|
if (headers[HTTP2_HEADER_SCHEME] !== undefined)
|
|
throw new errors.Error('ERR_HTTP2_CONNECT_SCHEME');
|
|
if (headers[HTTP2_HEADER_PATH] !== undefined)
|
|
throw new errors.Error('ERR_HTTP2_CONNECT_PATH');
|
|
}
|
|
|
|
validatePriorityOptions(options);
|
|
|
|
if (options.endStream === undefined) {
|
|
// For some methods, we know that a payload is meaningless, so end the
|
|
// stream by default if the user has not specifically indicated a
|
|
// preference.
|
|
options.endStream = isPayloadMeaningless(headers[HTTP2_HEADER_METHOD]);
|
|
} else if (typeof options.endStream !== 'boolean') {
|
|
throw new errors.RangeError('ERR_INVALID_OPT_VALUE',
|
|
'endStream',
|
|
options.endStream);
|
|
}
|
|
|
|
if (options.getTrailers !== undefined &&
|
|
typeof options.getTrailers !== 'function') {
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'getTrailers',
|
|
options.getTrailers);
|
|
}
|
|
|
|
const stream = new ClientHttp2Stream(this, undefined, {});
|
|
|
|
const onConnect = requestOnConnect.bind(stream, headers, options);
|
|
|
|
// Close the writable side of the stream if options.endStream is set.
|
|
if (options.endStream)
|
|
stream.end();
|
|
|
|
if (this[kState].connecting) {
|
|
debug(`[${sessionName(this[kType])}] session still connecting, queue ` +
|
|
'stream init');
|
|
stream.on('connect', onConnect);
|
|
} else {
|
|
debug(`[${sessionName(this[kType])}] session connected, immediate ` +
|
|
'stream init');
|
|
onConnect();
|
|
}
|
|
return stream;
|
|
}
|
|
}
|
|
|
|
function createWriteReq(req, handle, data, encoding) {
|
|
switch (encoding) {
|
|
case 'latin1':
|
|
case 'binary':
|
|
return handle.writeLatin1String(req, data);
|
|
case 'buffer':
|
|
return handle.writeBuffer(req, data);
|
|
case 'utf8':
|
|
case 'utf-8':
|
|
return handle.writeUtf8String(req, data);
|
|
case 'ascii':
|
|
return handle.writeAsciiString(req, data);
|
|
case 'ucs2':
|
|
case 'ucs-2':
|
|
case 'utf16le':
|
|
case 'utf-16le':
|
|
return handle.writeUcs2String(req, data);
|
|
default:
|
|
return handle.writeBuffer(req, Buffer.from(data, encoding));
|
|
}
|
|
}
|
|
|
|
function afterDoStreamWrite(status, handle, req) {
|
|
_unrefActive(handle[kOwner]);
|
|
if (typeof req.callback === 'function')
|
|
req.callback();
|
|
this.handle = undefined;
|
|
}
|
|
|
|
function onHandleFinish() {
|
|
const session = this[kSession];
|
|
if (session === undefined) return;
|
|
if (this[kID] === undefined) {
|
|
this.once('ready', onHandleFinish.bind(this));
|
|
} else {
|
|
const handle = session[kHandle];
|
|
if (handle !== undefined) {
|
|
// Shutdown on the next tick of the event loop just in case there is
|
|
// still data pending in the outbound queue.
|
|
assert(handle.shutdownStream(this[kID]) === undefined,
|
|
`The stream ${this[kID]} does not exist. Please report this as ` +
|
|
'a bug in Node.js');
|
|
}
|
|
}
|
|
}
|
|
|
|
function onSessionClose(hadError, code) {
|
|
abort(this);
|
|
// Close the readable side
|
|
this.push(null);
|
|
// Close the writable side
|
|
this.end();
|
|
}
|
|
|
|
function onStreamClosed(code) {
|
|
abort(this);
|
|
// Close the readable side
|
|
this.push(null);
|
|
// Close the writable side
|
|
this.end();
|
|
}
|
|
|
|
function streamOnResume() {
|
|
if (this._paused)
|
|
return this.pause();
|
|
if (this[kID] === undefined) {
|
|
this.once('ready', streamOnResume.bind(this));
|
|
return;
|
|
}
|
|
const session = this[kSession];
|
|
const state = this[kState];
|
|
if (session && !state.reading) {
|
|
state.reading = true;
|
|
assert(session[kHandle].streamReadStart(this[kID]) === undefined,
|
|
'HTTP/2 Stream #{this[kID]} does not exist. Please report this as ' +
|
|
'a bug in Node.js');
|
|
}
|
|
}
|
|
|
|
function streamOnPause() {
|
|
const session = this[kSession];
|
|
const state = this[kState];
|
|
if (session && state.reading) {
|
|
state.reading = false;
|
|
assert(session[kHandle].streamReadStop(this[kID]) === undefined,
|
|
`HTTP/2 Stream ${this[kID]} does not exist. Please report this as ' +
|
|
'a bug in Node.js`);
|
|
}
|
|
}
|
|
|
|
function streamOnDrain() {
|
|
const needPause = 0 > this._writableState.highWaterMark;
|
|
if (this._paused && !needPause) {
|
|
this._paused = false;
|
|
this.resume();
|
|
}
|
|
}
|
|
|
|
function streamOnSessionConnect() {
|
|
const session = this[kSession];
|
|
debug(`[${sessionName(session[kType])}] session connected. emiting stream ` +
|
|
'connect');
|
|
this[kState].connecting = false;
|
|
process.nextTick(emit.bind(this, 'connect'));
|
|
}
|
|
|
|
function streamOnceReady() {
|
|
const session = this[kSession];
|
|
debug(`[${sessionName(session[kType])}] stream ${this[kID]} is ready`);
|
|
this.uncork();
|
|
}
|
|
|
|
function abort(stream) {
|
|
if (!stream[kState].aborted &&
|
|
!(stream._writableState.ended || stream._writableState.ending)) {
|
|
stream.emit('aborted');
|
|
stream[kState].aborted = true;
|
|
}
|
|
}
|
|
|
|
// An Http2Stream is a Duplex stream. On the server-side, the Readable side
|
|
// provides access to the received request data. On the client-side, the
|
|
// Readable side provides access to the received response data. On the
|
|
// server side, the writable side is used to transmit response data, while
|
|
// on the client side it is used to transmit request data.
|
|
class Http2Stream extends Duplex {
|
|
constructor(session, options) {
|
|
options.allowHalfOpen = true;
|
|
super(options);
|
|
this.cork();
|
|
this[kSession] = session;
|
|
|
|
const state = this[kState] = {
|
|
rst: false,
|
|
rstCode: NGHTTP2_NO_ERROR,
|
|
headersSent: false,
|
|
aborted: false,
|
|
closeHandler: onSessionClose.bind(this)
|
|
};
|
|
|
|
this.once('ready', streamOnceReady);
|
|
this.once('streamClosed', onStreamClosed);
|
|
this.once('finish', onHandleFinish);
|
|
this.on('resume', streamOnResume);
|
|
this.on('pause', streamOnPause);
|
|
this.on('drain', streamOnDrain);
|
|
session.once('close', state.closeHandler);
|
|
|
|
if (session[kState].connecting) {
|
|
debug(`[${sessionName(session[kType])}] session is still connecting, ` +
|
|
'queuing stream init');
|
|
state.connecting = true;
|
|
session.once('connect', streamOnSessionConnect.bind(this));
|
|
}
|
|
debug(`[${sessionName(session[kType])}] http2stream created`);
|
|
}
|
|
|
|
[kInit](id) {
|
|
this[kID] = id;
|
|
this.emit('ready');
|
|
}
|
|
|
|
[kInspect](depth, opts) {
|
|
const obj = {
|
|
id: this[kID],
|
|
state: this.state,
|
|
readableState: this._readableState,
|
|
writeableSate: this._writableState
|
|
};
|
|
return `Http2Stream ${util.format(obj)}`;
|
|
}
|
|
|
|
// The id of the Http2Stream, will be undefined if the socket is not
|
|
// yet connected.
|
|
get id() {
|
|
return this[kID];
|
|
}
|
|
|
|
// The Http2Session that owns this Http2Stream.
|
|
get session() {
|
|
return this[kSession];
|
|
}
|
|
|
|
_onTimeout() {
|
|
process.nextTick(emit.bind(this, 'timeout'));
|
|
}
|
|
|
|
// true if the Http2Stream was aborted abornomally.
|
|
get aborted() {
|
|
return this[kState].aborted;
|
|
}
|
|
|
|
// The error code reported when this Http2Stream was closed.
|
|
get rstCode() {
|
|
return this[kState].rst ? this[kState].rstCode : undefined;
|
|
}
|
|
|
|
// State information for the Http2Stream
|
|
get state() {
|
|
const id = this[kID];
|
|
if (this.destroyed || id === undefined)
|
|
return Object.create(null);
|
|
return getStreamState(this[kSession][kHandle], id);
|
|
}
|
|
|
|
[kProceed]() {
|
|
assert.fail(null, null,
|
|
'Implementors MUST implement this. Please report this as a ' +
|
|
'bug in Node.js');
|
|
}
|
|
|
|
_write(data, encoding, cb) {
|
|
if (this[kID] === undefined) {
|
|
this.once('ready', this._write.bind(this, data, encoding, cb));
|
|
return;
|
|
}
|
|
_unrefActive(this);
|
|
if (!this[kState].headersSent)
|
|
this[kProceed]();
|
|
const session = this[kSession];
|
|
const handle = session[kHandle];
|
|
const req = new WriteWrap();
|
|
req.stream = this[kID];
|
|
req.handle = handle;
|
|
req.callback = cb;
|
|
req.oncomplete = afterDoStreamWrite;
|
|
req.async = false;
|
|
const err = createWriteReq(req, handle, data, encoding);
|
|
if (err)
|
|
throw util._errnoException(err, 'write', req.error);
|
|
this._bytesDispatched += req.bytes;
|
|
}
|
|
|
|
_writev(data, cb) {
|
|
if (this[kID] === undefined) {
|
|
this.once('ready', this._writev.bind(this, data, cb));
|
|
return;
|
|
}
|
|
_unrefActive(this);
|
|
if (!this[kState].headersSent)
|
|
this[kProceed]();
|
|
const session = this[kSession];
|
|
const handle = session[kHandle];
|
|
const req = new WriteWrap();
|
|
req.stream = this[kID];
|
|
req.handle = handle;
|
|
req.callback = cb;
|
|
req.oncomplete = afterDoStreamWrite;
|
|
req.async = false;
|
|
const chunks = new Array(data.length << 1);
|
|
for (var i = 0; i < data.length; i++) {
|
|
const entry = data[i];
|
|
chunks[i * 2] = entry.chunk;
|
|
chunks[i * 2 + 1] = entry.encoding;
|
|
}
|
|
const err = handle.writev(req, chunks);
|
|
if (err)
|
|
throw util._errnoException(err, 'write', req.error);
|
|
}
|
|
|
|
_read(nread) {
|
|
if (this[kID] === undefined) {
|
|
this.once('ready', this._read.bind(this, nread));
|
|
return;
|
|
}
|
|
if (this.destroyed) {
|
|
this.push(null);
|
|
return;
|
|
}
|
|
_unrefActive(this);
|
|
const state = this[kState];
|
|
if (state.reading)
|
|
return;
|
|
state.reading = true;
|
|
assert(this[kSession][kHandle].streamReadStart(this[kID]) === undefined,
|
|
'HTTP/2 Stream #{this[kID]} does not exist. Please report this as ' +
|
|
'a bug in Node.js');
|
|
}
|
|
|
|
// Submits an RST-STREAM frame to shutdown this stream.
|
|
// If the stream ID has not yet been allocated, the action will
|
|
// defer until the ready event is emitted.
|
|
// After sending the rstStream, this.destroy() will be called making
|
|
// the stream object no longer usable.
|
|
rstStream(code = NGHTTP2_NO_ERROR) {
|
|
if (this.destroyed)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
|
|
const session = this[kSession];
|
|
if (this[kID] === undefined) {
|
|
debug(
|
|
`[${sessionName(session[kType])}] queuing rstStream for new stream`);
|
|
this.once('ready', this.rstStream.bind(this, code));
|
|
return;
|
|
}
|
|
debug(`[${sessionName(session[kType])}] sending rstStream for stream ` +
|
|
`${this[kID]}: ${code}`);
|
|
_unrefActive(this);
|
|
this[kSession].rstStream(this, code);
|
|
}
|
|
|
|
rstWithNoError() {
|
|
this.rstStream(NGHTTP2_NO_ERROR);
|
|
}
|
|
|
|
rstWithProtocolError() {
|
|
this.rstStream(NGHTTP2_PROTOCOL_ERROR);
|
|
}
|
|
|
|
rstWithCancel() {
|
|
this.rstStream(NGHTTP2_CANCEL);
|
|
}
|
|
|
|
rstWithRefuse() {
|
|
this.rstStream(NGHTTP2_REFUSED_STREAM);
|
|
}
|
|
|
|
rstWithInternalError() {
|
|
this.rstStream(NGHTTP2_INTERNAL_ERROR);
|
|
}
|
|
|
|
// Note that this (and other methods like additionalHeaders and rstStream)
|
|
// cause nghttp to queue frames up in its internal buffer that are not
|
|
// actually sent on the wire until the next tick of the event loop. The
|
|
// semantics of this method then are: queue a priority frame to be sent and
|
|
// not immediately send the priority frame. There is current no callback
|
|
// triggered when the data is actually sent.
|
|
priority(options) {
|
|
if (this.destroyed)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
|
|
const session = this[kSession];
|
|
if (this[kID] === undefined) {
|
|
debug(`[${sessionName(session[kType])}] queuing priority for new stream`);
|
|
this.once('ready', this.priority.bind(this, options));
|
|
return;
|
|
}
|
|
debug(`[${sessionName(session[kType])}] sending priority for stream ` +
|
|
`${this[kID]}`);
|
|
_unrefActive(this);
|
|
this[kSession].priority(this, options);
|
|
}
|
|
|
|
// Called by this.destroy().
|
|
// * If called before the stream is allocated, will defer until the
|
|
// ready event is emitted.
|
|
// * Will submit an RST stream to shutdown the stream if necessary.
|
|
// This will cause the internal resources to be released.
|
|
// * Then cleans up the resources on the js side
|
|
_destroy(err, callback) {
|
|
const session = this[kSession];
|
|
const handle = session[kHandle];
|
|
if (this[kID] === undefined) {
|
|
debug(`[${sessionName(session[kType])}] queuing destroy for new stream`);
|
|
this.once('ready', this._destroy.bind(this, err, callback));
|
|
return;
|
|
}
|
|
process.nextTick(() => {
|
|
debug(`[${sessionName(session[kType])}] destroying stream ${this[kID]}`);
|
|
|
|
// Submit RST-STREAM frame if one hasn't been sent already and the
|
|
// stream hasn't closed normally...
|
|
if (!this[kState].rst && !session.destroyed) {
|
|
const code =
|
|
err instanceof Error ?
|
|
NGHTTP2_INTERNAL_ERROR : NGHTTP2_NO_ERROR;
|
|
this[kSession].rstStream(this, code);
|
|
}
|
|
|
|
// Remove the close handler on the session
|
|
session.removeListener('close', this[kState].closeHandler);
|
|
|
|
// Unenroll the timer
|
|
unenroll(this);
|
|
|
|
setImmediate(finishStreamDestroy.bind(this, handle));
|
|
|
|
// All done
|
|
const rst = this[kState].rst;
|
|
const code = rst ? this[kState].rstCode : NGHTTP2_NO_ERROR;
|
|
if (code !== NGHTTP2_NO_ERROR) {
|
|
const err = new errors.Error('ERR_HTTP2_STREAM_ERROR', code);
|
|
process.nextTick(() => this.emit('error', err));
|
|
}
|
|
process.nextTick(emit.bind(this, 'streamClosed', code));
|
|
debug(`[${sessionName(session[kType])}] stream ${this[kID]} destroyed`);
|
|
callback(err);
|
|
});
|
|
}
|
|
}
|
|
|
|
function finishStreamDestroy(handle) {
|
|
const id = this[kID];
|
|
const session = this[kSession];
|
|
session[kState].streams.delete(id);
|
|
delete this[kSession];
|
|
if (handle !== undefined)
|
|
handle.destroyStream(id);
|
|
this.emit('destroy');
|
|
}
|
|
|
|
function processHeaders(headers) {
|
|
assertIsObject(headers, 'headers');
|
|
headers = Object.assign(Object.create(null), headers);
|
|
if (headers[HTTP2_HEADER_STATUS] == null)
|
|
headers[HTTP2_HEADER_STATUS] = HTTP_STATUS_OK;
|
|
headers[HTTP2_HEADER_DATE] = utcDate();
|
|
|
|
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
|
|
// This is intentionally stricter than the HTTP/1 implementation, which
|
|
// allows values between 100 and 999 (inclusive) in order to allow for
|
|
// backwards compatibility with non-spec compliant code. With HTTP/2,
|
|
// we have the opportunity to start fresh with stricter spec copmliance.
|
|
// This will have an impact on the compatibility layer for anyone using
|
|
// non-standard, non-compliant status codes.
|
|
if (statusCode < 200 || statusCode > 599)
|
|
throw new errors.RangeError('ERR_HTTP2_STATUS_INVALID',
|
|
headers[HTTP2_HEADER_STATUS]);
|
|
|
|
return headers;
|
|
}
|
|
|
|
function processRespondWithFD(fd, headers, offset = 0, length = -1,
|
|
getTrailers = false) {
|
|
const session = this[kSession];
|
|
const state = this[kState];
|
|
state.headersSent = true;
|
|
|
|
// Close the writable side of the stream
|
|
this.end();
|
|
|
|
const handle = session[kHandle];
|
|
const ret =
|
|
handle.submitFile(this[kID], fd, headers, offset, length, getTrailers);
|
|
let err;
|
|
switch (ret) {
|
|
case NGHTTP2_ERR_NOMEM:
|
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
|
process.nextTick(() => session.emit('error', err));
|
|
break;
|
|
default:
|
|
if (ret < 0) {
|
|
err = new NghttpError(ret);
|
|
process.nextTick(() => this.emit('error', err));
|
|
}
|
|
}
|
|
}
|
|
|
|
function doSendFD(session, options, fd, headers, getTrailers, err, stat) {
|
|
if (this.destroyed || session.destroyed) {
|
|
abort(this);
|
|
return;
|
|
}
|
|
if (err) {
|
|
process.nextTick(() => this.emit('error', err));
|
|
return;
|
|
}
|
|
|
|
const statOptions = {
|
|
offset: options.offset !== undefined ? options.offset : 0,
|
|
length: options.length !== undefined ? options.length : -1
|
|
};
|
|
|
|
if (typeof options.statCheck === 'function' &&
|
|
options.statCheck.call(this, stat, headers, statOptions) === false) {
|
|
return;
|
|
}
|
|
|
|
const headersList = mapToHeaders(headers,
|
|
assertValidPseudoHeaderResponse);
|
|
if (!Array.isArray(headersList)) {
|
|
process.nextTick(() => this.emit('error', headersList));
|
|
}
|
|
|
|
processRespondWithFD.call(this, fd, headersList,
|
|
statOptions.offset,
|
|
statOptions.length,
|
|
getTrailers);
|
|
}
|
|
|
|
function doSendFileFD(session, options, fd, headers, getTrailers, err, stat) {
|
|
if (this.destroyed || session.destroyed) {
|
|
abort(this);
|
|
return;
|
|
}
|
|
if (err) {
|
|
process.nextTick(() => this.emit('error', err));
|
|
return;
|
|
}
|
|
if (!stat.isFile()) {
|
|
err = new errors.Error('ERR_HTTP2_SEND_FILE');
|
|
process.nextTick(() => this.emit('error', err));
|
|
return;
|
|
}
|
|
|
|
const statOptions = {
|
|
offset: options.offset !== undefined ? options.offset : 0,
|
|
length: options.length !== undefined ? options.length : -1
|
|
};
|
|
|
|
// Set the content-length by default
|
|
if (typeof options.statCheck === 'function' &&
|
|
options.statCheck.call(this, stat, headers) === false) {
|
|
return;
|
|
}
|
|
|
|
statOptions.length =
|
|
statOptions.length < 0 ? stat.size - (+statOptions.offset) :
|
|
Math.min(stat.size - (+statOptions.offset),
|
|
statOptions.length);
|
|
|
|
if (headers[HTTP2_HEADER_CONTENT_LENGTH] === undefined)
|
|
headers[HTTP2_HEADER_CONTENT_LENGTH] = statOptions.length;
|
|
|
|
const headersList = mapToHeaders(headers,
|
|
assertValidPseudoHeaderResponse);
|
|
if (!Array.isArray(headersList)) {
|
|
process.nextTick(() => this.emit('error', headersList));
|
|
}
|
|
|
|
processRespondWithFD.call(this, fd, headersList,
|
|
options.offset,
|
|
options.length,
|
|
getTrailers);
|
|
}
|
|
|
|
function afterOpen(session, options, headers, getTrailers, err, fd) {
|
|
const state = this[kState];
|
|
if (this.destroyed || session.destroyed) {
|
|
abort(this);
|
|
return;
|
|
}
|
|
if (err) {
|
|
process.nextTick(() => this.emit('error', err));
|
|
return;
|
|
}
|
|
state.fd = fd;
|
|
|
|
fs.fstat(fd,
|
|
doSendFileFD.bind(this, session, options, fd, headers, getTrailers));
|
|
}
|
|
|
|
|
|
class ServerHttp2Stream extends Http2Stream {
|
|
constructor(session, id, options, headers) {
|
|
super(session, options);
|
|
this[kInit](id);
|
|
this[kProtocol] = headers[HTTP2_HEADER_SCHEME];
|
|
this[kAuthority] = headers[HTTP2_HEADER_AUTHORITY];
|
|
debug(`[${sessionName(session[kType])}] created serverhttp2stream`);
|
|
}
|
|
|
|
// true if the HEADERS frame has been sent
|
|
get headersSent() {
|
|
return this[kState].headersSent;
|
|
}
|
|
|
|
// true if the remote peer accepts push streams
|
|
get pushAllowed() {
|
|
return this[kSession].remoteSettings.enablePush;
|
|
}
|
|
|
|
// create a push stream, call the given callback with the created
|
|
// Http2Stream for the push stream.
|
|
pushStream(headers, options, callback) {
|
|
if (this.destroyed)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
|
|
|
|
const session = this[kSession];
|
|
debug(`[${sessionName(session[kType])}] initiating push stream for stream` +
|
|
` ${this[kID]}`);
|
|
|
|
_unrefActive(this);
|
|
const state = session[kState];
|
|
const streams = state.streams;
|
|
const handle = session[kHandle];
|
|
|
|
if (!this[kSession].remoteSettings.enablePush)
|
|
throw new errors.Error('ERR_HTTP2_PUSH_DISABLED');
|
|
|
|
if (typeof options === 'function') {
|
|
callback = options;
|
|
options = undefined;
|
|
}
|
|
|
|
if (typeof callback !== 'function')
|
|
throw new errors.TypeError('ERR_INVALID_CALLBACK');
|
|
|
|
assertIsObject(options, 'options');
|
|
options = Object.assign(Object.create(null), options);
|
|
options.endStream = !!options.endStream;
|
|
|
|
assertIsObject(headers, 'headers');
|
|
headers = Object.assign(Object.create(null), headers);
|
|
|
|
if (headers[HTTP2_HEADER_METHOD] === undefined)
|
|
headers[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET;
|
|
if (headers[HTTP2_HEADER_AUTHORITY] === undefined)
|
|
headers[HTTP2_HEADER_AUTHORITY] = this[kAuthority];
|
|
if (headers[HTTP2_HEADER_SCHEME] === undefined)
|
|
headers[HTTP2_HEADER_SCHEME] = this[kProtocol];
|
|
if (headers[HTTP2_HEADER_PATH] === undefined)
|
|
headers[HTTP2_HEADER_PATH] = '/';
|
|
|
|
let headRequest = false;
|
|
if (headers[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) {
|
|
headRequest = true;
|
|
options.endStream = true;
|
|
}
|
|
|
|
const headersList = mapToHeaders(headers);
|
|
if (!Array.isArray(headersList)) {
|
|
// An error occurred!
|
|
throw headersList;
|
|
}
|
|
|
|
const ret = handle.submitPushPromise(this[kID],
|
|
headersList,
|
|
options.endStream);
|
|
let err;
|
|
switch (ret) {
|
|
case NGHTTP2_ERR_NOMEM:
|
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
|
process.nextTick(() => session.emit('error', err));
|
|
break;
|
|
case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE:
|
|
err = new errors.Error('ERR_HTTP2_OUT_OF_STREAMS');
|
|
process.nextTick(() => this.emit('error', err));
|
|
break;
|
|
case NGHTTP2_ERR_STREAM_CLOSED:
|
|
err = new errors.Error('ERR_HTTP2_STREAM_CLOSED');
|
|
process.nextTick(() => this.emit('error', err));
|
|
break;
|
|
default:
|
|
if (ret <= 0) {
|
|
err = new NghttpError(ret);
|
|
process.nextTick(() => this.emit('error', err));
|
|
break;
|
|
}
|
|
debug(`[${sessionName(session[kType])}] push stream ${ret} created`);
|
|
options.readable = !options.endStream;
|
|
|
|
const stream = new ServerHttp2Stream(session, ret, options, headers);
|
|
|
|
// If the push stream is a head request, close the writable side of
|
|
// the stream immediately as there won't be any data sent.
|
|
if (headRequest) {
|
|
stream.end();
|
|
const state = stream[kState];
|
|
state.headRequest = true;
|
|
}
|
|
|
|
streams.set(ret, stream);
|
|
process.nextTick(callback, stream, headers, 0);
|
|
}
|
|
}
|
|
|
|
// Initiate a response on this Http2Stream
|
|
respond(headers, options) {
|
|
const session = this[kSession];
|
|
if (this.destroyed)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
|
|
debug(`[${sessionName(session[kType])}] initiating response for stream ` +
|
|
`${this[kID]}`);
|
|
_unrefActive(this);
|
|
const state = this[kState];
|
|
|
|
if (state.headersSent)
|
|
throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
|
|
|
|
assertIsObject(options, 'options');
|
|
options = Object.assign(Object.create(null), options);
|
|
options.endStream = !!options.endStream;
|
|
|
|
let getTrailers = false;
|
|
if (options.getTrailers !== undefined) {
|
|
if (typeof options.getTrailers !== 'function') {
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'getTrailers',
|
|
options.getTrailers);
|
|
}
|
|
getTrailers = true;
|
|
state.getTrailers = options.getTrailers;
|
|
}
|
|
|
|
headers = processHeaders(headers);
|
|
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
|
|
|
|
// Payload/DATA frames are not permitted in these cases so set
|
|
// the options.endStream option to true so that the underlying
|
|
// bits do not attempt to send any.
|
|
if (statusCode === HTTP_STATUS_NO_CONTENT ||
|
|
statusCode === HTTP_STATUS_CONTENT_RESET ||
|
|
statusCode === HTTP_STATUS_NOT_MODIFIED ||
|
|
state.headRequest === true) {
|
|
options.endStream = true;
|
|
}
|
|
|
|
const headersList = mapToHeaders(headers, assertValidPseudoHeaderResponse);
|
|
if (!Array.isArray(headersList)) {
|
|
// An error occurred!
|
|
throw headersList;
|
|
}
|
|
state.headersSent = true;
|
|
|
|
// Close the writable side if the endStream option is set
|
|
if (options.endStream)
|
|
this.end();
|
|
|
|
const handle = session[kHandle];
|
|
const ret =
|
|
handle.submitResponse(this[kID],
|
|
headersList,
|
|
options.endStream,
|
|
getTrailers);
|
|
let err;
|
|
switch (ret) {
|
|
case NGHTTP2_ERR_NOMEM:
|
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
|
process.nextTick(() => session.emit('error', err));
|
|
break;
|
|
default:
|
|
if (ret < 0) {
|
|
err = new NghttpError(ret);
|
|
process.nextTick(() => this.emit('error', err));
|
|
}
|
|
}
|
|
}
|
|
|
|
// Initiate a response using an open FD. Note that there are fewer
|
|
// protections with this approach. For one, the fd is not validated by
|
|
// default. In respondWithFile, the file is checked to make sure it is a
|
|
// regular file, here the fd is passed directly. If the underlying
|
|
// mechanism is not able to read from the fd, then the stream will be
|
|
// reset with an error code.
|
|
respondWithFD(fd, headers, options) {
|
|
const session = this[kSession];
|
|
if (this.destroyed)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
|
|
debug(`[${sessionName(session[kType])}] initiating response for stream ` +
|
|
`${this[kID]}`);
|
|
_unrefActive(this);
|
|
const state = this[kState];
|
|
|
|
if (state.headersSent)
|
|
throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
|
|
|
|
assertIsObject(options, 'options');
|
|
options = Object.assign(Object.create(null), options);
|
|
|
|
if (options.offset !== undefined && typeof options.offset !== 'number')
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'offset',
|
|
options.offset);
|
|
|
|
if (options.length !== undefined && typeof options.length !== 'number')
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'length',
|
|
options.length);
|
|
|
|
if (options.statCheck !== undefined &&
|
|
typeof options.statCheck !== 'function') {
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'statCheck',
|
|
options.statCheck);
|
|
}
|
|
|
|
let getTrailers = false;
|
|
if (options.getTrailers !== undefined) {
|
|
if (typeof options.getTrailers !== 'function') {
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'getTrailers',
|
|
options.getTrailers);
|
|
}
|
|
getTrailers = true;
|
|
state.getTrailers = options.getTrailers;
|
|
}
|
|
|
|
if (typeof fd !== 'number')
|
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
|
|
'fd', 'number');
|
|
|
|
headers = processHeaders(headers);
|
|
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
|
|
// Payload/DATA frames are not permitted in these cases
|
|
if (statusCode === HTTP_STATUS_NO_CONTENT ||
|
|
statusCode === HTTP_STATUS_CONTENT_RESET ||
|
|
statusCode === HTTP_STATUS_NOT_MODIFIED) {
|
|
throw new errors.Error('ERR_HTTP2_PAYLOAD_FORBIDDEN', statusCode);
|
|
}
|
|
|
|
if (options.statCheck !== undefined) {
|
|
fs.fstat(fd,
|
|
doSendFD.bind(this, session, options, fd, headers, getTrailers));
|
|
return;
|
|
}
|
|
|
|
const headersList = mapToHeaders(headers,
|
|
assertValidPseudoHeaderResponse);
|
|
if (!Array.isArray(headersList)) {
|
|
process.nextTick(() => this.emit('error', headersList));
|
|
}
|
|
|
|
processRespondWithFD.call(this, fd, headersList,
|
|
options.offset,
|
|
options.length,
|
|
getTrailers);
|
|
}
|
|
|
|
// Initiate a file response on this Http2Stream. The path is passed to
|
|
// fs.open() to acquire the fd with mode 'r', then the fd is passed to
|
|
// fs.fstat(). Assuming fstat is successful, a check is made to ensure
|
|
// that the file is a regular file, then options.statCheck is called,
|
|
// giving the user an opportunity to verify the details and set additional
|
|
// headers. If statCheck returns false, the operation is aborted and no
|
|
// file details are sent.
|
|
respondWithFile(path, headers, options) {
|
|
const session = this[kSession];
|
|
if (this.destroyed)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
|
|
debug(`[${sessionName(session[kType])}] initiating response for stream ` +
|
|
`${this[kID]}`);
|
|
_unrefActive(this);
|
|
const state = this[kState];
|
|
|
|
if (state.headersSent)
|
|
throw new errors.Error('ERR_HTTP2_HEADERS_SENT');
|
|
|
|
assertIsObject(options, 'options');
|
|
options = Object.assign(Object.create(null), options);
|
|
|
|
if (options.offset !== undefined && typeof options.offset !== 'number')
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'offset',
|
|
options.offset);
|
|
|
|
if (options.length !== undefined && typeof options.length !== 'number')
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'length',
|
|
options.length);
|
|
|
|
if (options.statCheck !== undefined &&
|
|
typeof options.statCheck !== 'function') {
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'statCheck',
|
|
options.statCheck);
|
|
}
|
|
|
|
let getTrailers = false;
|
|
if (options.getTrailers !== undefined) {
|
|
if (typeof options.getTrailers !== 'function') {
|
|
throw new errors.TypeError('ERR_INVALID_OPT_VALUE',
|
|
'getTrailers',
|
|
options.getTrailers);
|
|
}
|
|
getTrailers = true;
|
|
state.getTrailers = options.getTrailers;
|
|
}
|
|
|
|
headers = processHeaders(headers);
|
|
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
|
|
// Payload/DATA frames are not permitted in these cases
|
|
if (statusCode === HTTP_STATUS_NO_CONTENT ||
|
|
statusCode === HTTP_STATUS_CONTENT_RESET ||
|
|
statusCode === HTTP_STATUS_NOT_MODIFIED) {
|
|
throw new errors.Error('ERR_HTTP2_PAYLOAD_FORBIDDEN', statusCode);
|
|
}
|
|
|
|
fs.open(path, 'r',
|
|
afterOpen.bind(this, session, options, headers, getTrailers));
|
|
}
|
|
|
|
// Sends a block of informational headers. In theory, the HTTP/2 spec
|
|
// allows sending a HEADER block at any time during a streams lifecycle,
|
|
// but the HTTP request/response semantics defined in HTTP/2 places limits
|
|
// such that HEADERS may only be sent *before* or *after* DATA frames.
|
|
// If the block of headers being sent includes a status code, it MUST be
|
|
// a 1xx informational code and it MUST be sent before the request/response
|
|
// headers are sent, or an error will be thrown.
|
|
additionalHeaders(headers) {
|
|
if (this.destroyed)
|
|
throw new errors.Error('ERR_HTTP2_INVALID_STREAM');
|
|
|
|
if (this[kState].headersSent)
|
|
throw new errors.Error('ERR_HTTP2_HEADERS_AFTER_RESPOND');
|
|
|
|
const session = this[kSession];
|
|
debug(`[${sessionName(session[kType])}] sending additional headers`);
|
|
|
|
assertIsObject(headers, 'headers');
|
|
headers = Object.assign(Object.create(null), headers);
|
|
if (headers[HTTP2_HEADER_STATUS] != null) {
|
|
const statusCode = headers[HTTP2_HEADER_STATUS] |= 0;
|
|
if (statusCode === HTTP_STATUS_SWITCHING_PROTOCOLS)
|
|
throw new errors.Error('ERR_HTTP2_STATUS_101');
|
|
if (statusCode < 100 || statusCode >= 200) {
|
|
throw new errors.RangeError('ERR_HTTP2_INVALID_INFO_STATUS',
|
|
headers[HTTP2_HEADER_STATUS]);
|
|
}
|
|
}
|
|
|
|
_unrefActive(this);
|
|
const handle = this[kSession][kHandle];
|
|
|
|
const headersList = mapToHeaders(headers,
|
|
assertValidPseudoHeaderResponse);
|
|
if (!Array.isArray(headersList)) {
|
|
throw headersList;
|
|
}
|
|
|
|
const ret =
|
|
handle.sendHeaders(this[kID], headersList);
|
|
let err;
|
|
switch (ret) {
|
|
case NGHTTP2_ERR_NOMEM:
|
|
err = new errors.Error('ERR_OUTOFMEMORY');
|
|
process.nextTick(() => this[kSession].emit('error', err));
|
|
break;
|
|
default:
|
|
if (ret < 0) {
|
|
err = new NghttpError(ret);
|
|
process.nextTick(() => this.emit('error', err));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
ServerHttp2Stream.prototype[kProceed] = ServerHttp2Stream.prototype.respond;
|
|
|
|
class ClientHttp2Stream extends Http2Stream {
|
|
constructor(session, id, options) {
|
|
super(session, options);
|
|
this[kState].headersSent = true;
|
|
if (id !== undefined)
|
|
this[kInit](id);
|
|
debug(`[${sessionName(session[kType])}] clienthttp2stream created`);
|
|
}
|
|
}
|
|
|
|
const setTimeout = {
|
|
configurable: true,
|
|
enumerable: true,
|
|
writable: true,
|
|
value: function(msecs, callback) {
|
|
if (typeof msecs !== 'number') {
|
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE',
|
|
'msecs',
|
|
'number');
|
|
}
|
|
if (msecs === 0) {
|
|
unenroll(this);
|
|
if (callback !== undefined) {
|
|
if (typeof callback !== 'function')
|
|
throw new errors.TypeError('ERR_INVALID_CALLBACK');
|
|
this.removeListener('timeout', callback);
|
|
}
|
|
} else {
|
|
enroll(this, msecs);
|
|
_unrefActive(this);
|
|
if (callback !== undefined) {
|
|
if (typeof callback !== 'function')
|
|
throw new errors.TypeError('ERR_INVALID_CALLBACK');
|
|
this.once('timeout', callback);
|
|
}
|
|
}
|
|
return this;
|
|
}
|
|
};
|
|
|
|
const onTimeout = {
|
|
configurable: false,
|
|
enumerable: false,
|
|
value: function() {
|
|
process.nextTick(emit.bind(this, 'timeout'));
|
|
}
|
|
};
|
|
|
|
Object.defineProperties(Http2Stream.prototype, {
|
|
setTimeout,
|
|
onTimeout
|
|
});
|
|
Object.defineProperties(Http2Session.prototype, {
|
|
setTimeout,
|
|
onTimeout
|
|
});
|
|
|
|
// --------------------------------------------------------------------
|
|
|
|
// Set as a replacement for socket.prototype.destroy upon the
|
|
// establishment of a new connection.
|
|
function socketDestroy(error) {
|
|
const type = this[kSession][kType];
|
|
debug(`[${sessionName(type)}] socket destroy called`);
|
|
delete this[kServer];
|
|
this.removeListener('timeout', socketOnTimeout);
|
|
// destroy the session first so that it will stop trying to
|
|
// send data while we close the socket.
|
|
this[kSession].destroy();
|
|
this.destroy = this[kDestroySocket];
|
|
debug(`[${sessionName(type)}] destroying the socket`);
|
|
this.destroy(error);
|
|
}
|
|
|
|
function socketOnResume() {
|
|
if (this._paused)
|
|
return this.pause();
|
|
if (this._handle && !this._handle.reading) {
|
|
this._handle.reading = true;
|
|
this._handle.readStart();
|
|
}
|
|
}
|
|
|
|
function socketOnPause() {
|
|
if (this._handle && this._handle.reading) {
|
|
this._handle.reading = false;
|
|
this._handle.readStop();
|
|
}
|
|
}
|
|
|
|
function socketOnDrain() {
|
|
const needPause = 0 > this._writableState.highWaterMark;
|
|
if (this._paused && !needPause) {
|
|
this._paused = false;
|
|
this.resume();
|
|
}
|
|
}
|
|
|
|
// When an Http2Session emits an error, first try to forward it to the
|
|
// server as a sessionError; failing that, forward it to the socket as
|
|
// a sessionError; failing that, destroy, remove the error listener, and
|
|
// re-emit the error event
|
|
function sessionOnError(error) {
|
|
debug(`[${sessionName(this[kType])}] server session error: ${error.message}`);
|
|
if (this[kServer] !== undefined && this[kServer].emit('sessionError', error))
|
|
return;
|
|
if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error))
|
|
return;
|
|
this.destroy();
|
|
this.removeListener('error', sessionOnError);
|
|
this.emit('error', error);
|
|
}
|
|
|
|
// When a Socket emits an error, forward it to the session as a
|
|
// socketError; failing that, remove the listener and call destroy
|
|
function socketOnError(error) {
|
|
const type = this[kSession] && this[kSession][kType];
|
|
debug(`[${sessionName(type)}] server socket error: ${error.message}`);
|
|
if (kRenegTest.test(error.message))
|
|
return this.destroy();
|
|
if (this[kSession] !== undefined &&
|
|
this[kSession].emit('socketError', error, this))
|
|
return;
|
|
this.removeListener('error', socketOnError);
|
|
this.destroy(error);
|
|
}
|
|
|
|
// When the socket times out on the server, attempt a graceful shutdown
|
|
// of the session.
|
|
function socketOnTimeout() {
|
|
debug('socket timeout');
|
|
process.nextTick(() => {
|
|
const server = this[kServer];
|
|
const session = this[kSession];
|
|
// If server or session are undefined, then we're already in the process of
|
|
// shutting down, do nothing.
|
|
if (server === undefined || session === undefined)
|
|
return;
|
|
if (!server.emit('timeout', session, this)) {
|
|
session.shutdown(
|
|
{
|
|
graceful: true,
|
|
errorCode: NGHTTP2_NO_ERROR
|
|
},
|
|
this.destroy.bind(this));
|
|
}
|
|
});
|
|
}
|
|
|
|
// Handles the on('stream') event for a session and forwards
|
|
// it on to the server object.
|
|
function sessionOnStream(stream, headers, flags) {
|
|
debug(`[${sessionName(this[kType])}] emit server stream event`);
|
|
this[kServer].emit('stream', stream, headers, flags);
|
|
}
|
|
|
|
function sessionOnPriority(stream, parent, weight, exclusive) {
|
|
debug(`[${sessionName(this[kType])}] priority change received`);
|
|
this[kServer].emit('priority', stream, parent, weight, exclusive);
|
|
}
|
|
|
|
function sessionOnSocketError(error, socket) {
|
|
if (this.listenerCount('socketError') <= 1 && this[kServer] !== undefined)
|
|
this[kServer].emit('socketError', error, socket, this);
|
|
}
|
|
|
|
function connectionListener(socket) {
|
|
debug('[server] received a connection');
|
|
const options = this[kOptions] || {};
|
|
|
|
if (this.timeout) {
|
|
socket.setTimeout(this.timeout);
|
|
socket.on('timeout', socketOnTimeout);
|
|
}
|
|
|
|
if (socket.alpnProtocol === false || socket.alpnProtocol === 'http/1.1') {
|
|
if (options.allowHTTP1 === true) {
|
|
// Fallback to HTTP/1.1
|
|
return httpConnectionListener.call(this, socket);
|
|
} else if (this.emit('unknownProtocol', socket)) {
|
|
// Let event handler deal with the socket
|
|
return;
|
|
} else {
|
|
// Reject non-HTTP/2 client
|
|
return socket.destroy();
|
|
}
|
|
}
|
|
|
|
socket.on('error', socketOnError);
|
|
socket.on('resume', socketOnResume);
|
|
socket.on('pause', socketOnPause);
|
|
socket.on('drain', socketOnDrain);
|
|
socket.on('close', socketOnClose);
|
|
|
|
// Set up the Session
|
|
const session = new ServerHttp2Session(options, socket, this);
|
|
|
|
session.on('error', sessionOnError);
|
|
session.on('stream', sessionOnStream);
|
|
session.on('priority', sessionOnPriority);
|
|
session.on('socketError', sessionOnSocketError);
|
|
|
|
socket[kServer] = this;
|
|
|
|
process.nextTick(emit.bind(this, 'session', session));
|
|
}
|
|
|
|
function initializeOptions(options) {
|
|
assertIsObject(options, 'options');
|
|
options = Object.assign(Object.create(null), options);
|
|
options.allowHalfOpen = true;
|
|
assertIsObject(options.settings, 'options.settings');
|
|
options.settings = Object.assign(Object.create(null), options.settings);
|
|
return options;
|
|
}
|
|
|
|
function initializeTLSOptions(options, servername) {
|
|
options = initializeOptions(options);
|
|
options.ALPNProtocols = ['h2'];
|
|
if (options.allowHTTP1 === true)
|
|
options.ALPNProtocols.push('http/1.1');
|
|
if (servername !== undefined && options.servername === undefined)
|
|
options.servername = servername;
|
|
return options;
|
|
}
|
|
|
|
function onErrorSecureServerSession(err, conn) {
|
|
if (!this.emit('clientError', err, conn))
|
|
conn.destroy(err);
|
|
}
|
|
|
|
class Http2SecureServer extends TLSServer {
|
|
constructor(options, requestListener) {
|
|
options = initializeTLSOptions(options);
|
|
super(options, connectionListener);
|
|
this[kOptions] = options;
|
|
this.timeout = kDefaultSocketTimeout;
|
|
this.on('newListener', setupCompat);
|
|
if (typeof requestListener === 'function')
|
|
this.on('request', requestListener);
|
|
this.on('tlsClientError', onErrorSecureServerSession);
|
|
debug('http2secureserver created');
|
|
}
|
|
|
|
setTimeout(msecs, callback) {
|
|
this.timeout = msecs;
|
|
if (callback !== undefined) {
|
|
if (typeof callback !== 'function')
|
|
throw new errors.TypeError('ERR_INVALID_CALLBACK');
|
|
this.on('timeout', callback);
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
|
|
class Http2Server extends NETServer {
|
|
constructor(options, requestListener) {
|
|
super(connectionListener);
|
|
this[kOptions] = initializeOptions(options);
|
|
this.timeout = kDefaultSocketTimeout;
|
|
this.on('newListener', setupCompat);
|
|
if (typeof requestListener === 'function')
|
|
this.on('request', requestListener);
|
|
debug('http2server created');
|
|
}
|
|
|
|
setTimeout(msecs, callback) {
|
|
this.timeout = msecs;
|
|
if (callback !== undefined) {
|
|
if (typeof callback !== 'function')
|
|
throw new errors.TypeError('ERR_INVALID_CALLBACK');
|
|
this.on('timeout', callback);
|
|
}
|
|
return this;
|
|
}
|
|
}
|
|
|
|
function setupCompat(ev) {
|
|
if (ev === 'request') {
|
|
debug('setting up compatibility handler');
|
|
this.removeListener('newListener', setupCompat);
|
|
this.on('stream', onServerStream);
|
|
}
|
|
}
|
|
|
|
function socketOnClose(hadError) {
|
|
const session = this[kSession];
|
|
if (session !== undefined && !session.destroyed) {
|
|
// Skip unconsume because the socket is destroyed.
|
|
session[kState].skipUnconsume = true;
|
|
session.destroy();
|
|
}
|
|
}
|
|
|
|
// If the session emits an error, forward it to the socket as a sessionError;
|
|
// failing that, destroy the session, remove the listener and re-emit the error
|
|
function clientSessionOnError(error) {
|
|
debug(`client session error: ${error.message}`);
|
|
if (this[kSocket] !== undefined && this[kSocket].emit('sessionError', error))
|
|
return;
|
|
this.destroy();
|
|
this.removeListener('error', socketOnError);
|
|
this.removeListener('error', clientSessionOnError);
|
|
}
|
|
|
|
function connect(authority, options, listener) {
|
|
if (typeof options === 'function') {
|
|
listener = options;
|
|
options = undefined;
|
|
}
|
|
|
|
assertIsObject(options, 'options');
|
|
options = Object.assign(Object.create(null), options);
|
|
|
|
if (typeof authority === 'string')
|
|
authority = new URL(authority);
|
|
|
|
assertIsObject(authority, 'authority', ['string', 'object', 'URL']);
|
|
|
|
debug(`connecting to ${authority}`);
|
|
|
|
const protocol = authority.protocol || options.protocol || 'https:';
|
|
const port = '' + (authority.port !== '' ? authority.port : 443);
|
|
const host = authority.hostname || authority.host || 'localhost';
|
|
|
|
let socket;
|
|
switch (protocol) {
|
|
case 'http:':
|
|
socket = net.connect(port, host);
|
|
break;
|
|
case 'https:':
|
|
socket = tls.connect(port, host, initializeTLSOptions(options, host));
|
|
break;
|
|
default:
|
|
throw new errors.Error('ERR_HTTP2_UNSUPPORTED_PROTOCOL', protocol);
|
|
}
|
|
|
|
socket.on('error', socketOnError);
|
|
socket.on('resume', socketOnResume);
|
|
socket.on('pause', socketOnPause);
|
|
socket.on('drain', socketOnDrain);
|
|
socket.on('close', socketOnClose);
|
|
|
|
const session = new ClientHttp2Session(options, socket);
|
|
|
|
session.on('error', clientSessionOnError);
|
|
|
|
session[kAuthority] = `${options.servername || host}:${port}`;
|
|
session[kProtocol] = protocol;
|
|
|
|
if (typeof listener === 'function')
|
|
session.once('connect', listener);
|
|
return session;
|
|
}
|
|
|
|
function createSecureServer(options, handler) {
|
|
if (typeof options === 'function') {
|
|
handler = options;
|
|
options = Object.create(null);
|
|
}
|
|
debug('creating http2secureserver');
|
|
return new Http2SecureServer(options, handler);
|
|
}
|
|
|
|
function createServer(options, handler) {
|
|
if (typeof options === 'function') {
|
|
handler = options;
|
|
options = Object.create(null);
|
|
}
|
|
debug('creating htt2pserver');
|
|
return new Http2Server(options, handler);
|
|
}
|
|
|
|
// Returns a Base64 encoded settings frame payload from the given
|
|
// object. The value is suitable for passing as the value of the
|
|
// HTTP2-Settings header frame.
|
|
function getPackedSettings(settings) {
|
|
assertIsObject(settings, 'settings');
|
|
settings = settings || Object.create(null);
|
|
assertWithinRange('headerTableSize',
|
|
settings.headerTableSize,
|
|
0, 2 ** 32 - 1);
|
|
assertWithinRange('initialWindowSize',
|
|
settings.initialWindowSize,
|
|
0, 2 ** 32 - 1);
|
|
assertWithinRange('maxFrameSize',
|
|
settings.maxFrameSize,
|
|
16384, 2 ** 24 - 1);
|
|
assertWithinRange('maxConcurrentStreams',
|
|
settings.maxConcurrentStreams,
|
|
0, 2 ** 31 - 1);
|
|
assertWithinRange('maxHeaderListSize',
|
|
settings.maxHeaderListSize,
|
|
0, 2 ** 32 - 1);
|
|
if (settings.enablePush !== undefined &&
|
|
typeof settings.enablePush !== 'boolean') {
|
|
const err = new errors.TypeError('ERR_HTTP2_INVALID_SETTING_VALUE',
|
|
'enablePush', settings.enablePush);
|
|
err.actual = settings.enablePush;
|
|
throw err;
|
|
}
|
|
updateSettingsBuffer(settings);
|
|
return binding.packSettings();
|
|
}
|
|
|
|
function getUnpackedSettings(buf, options = {}) {
|
|
if (!isUint8Array(buf)) {
|
|
throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'buf',
|
|
['Buffer', 'Uint8Array']);
|
|
}
|
|
if (buf.length % 6 !== 0)
|
|
throw new errors.RangeError('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH');
|
|
const settings = Object.create(null);
|
|
let offset = 0;
|
|
while (offset < buf.length) {
|
|
const id = buf.readUInt16BE(offset);
|
|
offset += 2;
|
|
const value = buf.readUInt32BE(offset);
|
|
switch (id) {
|
|
case NGHTTP2_SETTINGS_HEADER_TABLE_SIZE:
|
|
settings.headerTableSize = value;
|
|
break;
|
|
case NGHTTP2_SETTINGS_ENABLE_PUSH:
|
|
settings.enablePush = Boolean(value);
|
|
break;
|
|
case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS:
|
|
settings.maxConcurrentStreams = value;
|
|
break;
|
|
case NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE:
|
|
settings.initialWindowSize = value;
|
|
break;
|
|
case NGHTTP2_SETTINGS_MAX_FRAME_SIZE:
|
|
settings.maxFrameSize = value;
|
|
break;
|
|
case NGHTTP2_SETTINGS_MAX_HEADER_LIST_SIZE:
|
|
settings.maxHeaderListSize = value;
|
|
break;
|
|
}
|
|
offset += 4;
|
|
}
|
|
|
|
if (options != null && options.validate) {
|
|
assertWithinRange('headerTableSize',
|
|
settings.headerTableSize,
|
|
0, 2 ** 32 - 1);
|
|
assertWithinRange('initialWindowSize',
|
|
settings.initialWindowSize,
|
|
0, 2 ** 32 - 1);
|
|
assertWithinRange('maxFrameSize',
|
|
settings.maxFrameSize,
|
|
16384, 2 ** 24 - 1);
|
|
assertWithinRange('maxConcurrentStreams',
|
|
settings.maxConcurrentStreams,
|
|
0, 2 ** 31 - 1);
|
|
assertWithinRange('maxHeaderListSize',
|
|
settings.maxHeaderListSize,
|
|
0, 2 ** 32 - 1);
|
|
if (settings.enablePush !== undefined &&
|
|
typeof settings.enablePush !== 'boolean') {
|
|
const err = new errors.TypeError('ERR_HTTP2_INVALID_SETTING_VALUE',
|
|
'enablePush', settings.enablePush);
|
|
err.actual = settings.enablePush;
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
return settings;
|
|
}
|
|
|
|
// Exports
|
|
module.exports = {
|
|
constants,
|
|
getDefaultSettings,
|
|
getPackedSettings,
|
|
getUnpackedSettings,
|
|
createServer,
|
|
createSecureServer,
|
|
connect
|
|
};
|
|
|
|
/* eslint-enable no-use-before-define */
|
|
|