UNPKG

nstdlib-nightly

Version:

Node.js standard library converted to runtime-agnostic ES modules.

1,607 lines (1,446 loc) 108 kB
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/internal/http2/core.js import { assertCrypto, customInspectSymbol as kInspect, kEmptyObject, promisify, } from "nstdlib/lib/internal/util"; import * as assert from "nstdlib/lib/assert"; import EventEmitter from "nstdlib/lib/events"; import { addAbortListener } from "nstdlib/lib/internal/events/abort_listener"; import * as fs from "nstdlib/lib/fs"; import * as http from "nstdlib/lib/http"; import { readUInt16BE, readUInt32BE } from "nstdlib/lib/internal/buffer"; import { URL, getURLOrigin } from "nstdlib/lib/internal/url"; import * as net from "nstdlib/lib/net"; import { Duplex } from "nstdlib/lib/stream"; import * as tls from "nstdlib/lib/tls"; import { setImmediate, setTimeout, clearTimeout } from "nstdlib/lib/timers"; import { kIncomingMessage, _checkIsHttpToken as checkIsHttpToken, } from "nstdlib/lib/_http_common"; import { kServerResponse, Server as HttpServer, httpServerPreClose, setupConnectionsTracking, } from "nstdlib/lib/_http_server"; import * as JSStreamSocket from "nstdlib/lib/internal/js_stream_socket"; import { defaultTriggerAsyncIdScope, symbols as __symbols__, } from "nstdlib/lib/internal/async_hooks"; import { AbortError, aggregateTwoErrors, codes as __codes__, hideStackFrames, } from "nstdlib/lib/internal/errors"; import { isUint32, validateAbortSignal, validateBoolean, validateBuffer, validateFunction, validateInt32, validateInteger, validateNumber, validateString, validateUint32, } from "nstdlib/lib/internal/validators"; import * as fsPromisesInternal from "nstdlib/lib/internal/fs/promises"; import { utcDate } from "nstdlib/lib/internal/http"; import { Http2ServerRequest, Http2ServerResponse, onServerStream, } from "nstdlib/lib/internal/http2/compat"; import { assertIsObject, assertIsArray, assertValidPseudoHeader, assertValidPseudoHeaderResponse, assertValidPseudoHeaderTrailer, assertWithinRange, getAuthority, getDefaultSettings, getSessionState, getSettings, getStreamState, isPayloadMeaningless, kSensitiveHeaders, kSocket, kRequest, kProxySocket, mapToHeaders, MAX_ADDITIONAL_SETTINGS, NghttpError, remoteCustomSettingsToBuffer, sessionName, toHeaderObject, updateOptionsBuffer, updateSettingsBuffer, } from "nstdlib/lib/internal/http2/util"; import { writeGeneric, writevGeneric, onStreamRead, kAfterAsyncWrite, kMaybeDestroy, kUpdateTimer, kHandle, kSession, kBoundSession, setStreamTimeout, } from "nstdlib/lib/internal/stream_base_commons"; import { kTimeout } from "nstdlib/lib/internal/timers"; import { isArrayBufferView } from "nstdlib/lib/internal/util/types"; import { format } from "nstdlib/lib/internal/util/inspect"; import { FileHandle } from "nstdlib/stub/binding/fs"; import * as binding from "nstdlib/stub/binding/http2"; import { ShutdownWrap, kReadBytesOrError, streamBaseState, } from "nstdlib/stub/binding/stream_wrap"; import { UV_EOF } from "nstdlib/stub/binding/uv"; import { StreamPipe } from "nstdlib/stub/binding/stream_pipe"; assertCrypto(); const { async_id_symbol, owner_symbol } = __symbols__; const { ERR_HTTP2_ALTSVC_INVALID_ORIGIN, ERR_HTTP2_ALTSVC_LENGTH, ERR_HTTP2_CONNECT_AUTHORITY, ERR_HTTP2_CONNECT_PATH, ERR_HTTP2_CONNECT_SCHEME, ERR_HTTP2_GOAWAY_SESSION, ERR_HTTP2_HEADERS_AFTER_RESPOND, ERR_HTTP2_HEADERS_SENT, ERR_HTTP2_INVALID_INFO_STATUS, ERR_HTTP2_INVALID_ORIGIN, ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH, ERR_HTTP2_INVALID_SESSION, ERR_HTTP2_INVALID_SETTING_VALUE, ERR_HTTP2_INVALID_STREAM, ERR_HTTP2_MAX_PENDING_SETTINGS_ACK, ERR_HTTP2_NESTED_PUSH, ERR_HTTP2_NO_MEM, ERR_HTTP2_NO_SOCKET_MANIPULATION, ERR_HTTP2_ORIGIN_LENGTH, ERR_HTTP2_OUT_OF_STREAMS, ERR_HTTP2_PAYLOAD_FORBIDDEN, ERR_HTTP2_PING_CANCEL, ERR_HTTP2_PING_LENGTH, ERR_HTTP2_PUSH_DISABLED, ERR_HTTP2_SEND_FILE, ERR_HTTP2_SEND_FILE_NOSEEK, ERR_HTTP2_SESSION_ERROR, ERR_HTTP2_SETTINGS_CANCEL, ERR_HTTP2_SOCKET_BOUND, ERR_HTTP2_SOCKET_UNBOUND, ERR_HTTP2_STATUS_101, ERR_HTTP2_STATUS_INVALID, ERR_HTTP2_STREAM_CANCEL, ERR_HTTP2_STREAM_ERROR, ERR_HTTP2_STREAM_SELF_DEPENDENCY, ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS, ERR_HTTP2_TRAILERS_ALREADY_SENT, ERR_HTTP2_TRAILERS_NOT_READY, ERR_HTTP2_UNSUPPORTED_PROTOCOL, ERR_INVALID_ARG_TYPE, ERR_INVALID_ARG_VALUE, ERR_INVALID_CHAR, ERR_INVALID_HTTP_TOKEN, ERR_OUT_OF_RANGE, ERR_SOCKET_CLOSED, } = __codes__; const { _connectionListener: httpConnectionListener } = http; const debugEnabled = debug.enabled; function debugStream(id, sessionType, message, ...args) { if (!debugEnabled) { return; } { /* debug */ } } function debugStreamObj(stream, message, ...args) { const session = stream[kSession]; const type = session ? session[kType] : undefined; debugStream(stream[kID], type, message, ...args); } function debugSession(sessionType, message, ...args) { { /* debug */ } } function debugSessionObj(session, message, ...args) { debugSession(session[kType], message, ...args); } const kMaxFrameSize = 2 ** 24 - 1; const kMaxInt = 2 ** 32 - 1; const kMaxStreams = 2 ** 32 - 1; const kMaxALTSVC = 2 ** 14 - 2; // eslint-disable-next-line no-control-regex const kQuotedString = /^[\x09\x20-\x5b\x5d-\x7e\x80-\xff]*$/; const { constants, nameForErrorCode } = binding; const NETServer = net.Server; const TLSServer = tls.Server; const kAlpnProtocol = Symbol("alpnProtocol"); const kAuthority = Symbol("authority"); const kEncrypted = Symbol("encrypted"); const kID = Symbol("id"); const kInit = Symbol("init"); const kInfoHeaders = Symbol("sent-info-headers"); const kLocalSettings = Symbol("local-settings"); const kNativeFields = Symbol("kNativeFields"); const kOptions = Symbol("options"); const kOwner = owner_symbol; const kOrigin = Symbol("origin"); const kPendingRequestCalls = Symbol("kPendingRequestCalls"); const kProceed = Symbol("proceed"); const kProtocol = Symbol("protocol"); const kRemoteSettings = Symbol("remote-settings"); const kSelectPadding = Symbol("select-padding"); const kSentHeaders = Symbol("sent-headers"); const kSentTrailers = Symbol("sent-trailers"); const kServer = Symbol("server"); const kState = Symbol("state"); const kType = Symbol("type"); const kWriteGeneric = Symbol("write-generic"); const { kBitfield, kSessionPriorityListenerCount, kSessionFrameErrorListenerCount, kSessionMaxInvalidFrames, kSessionMaxRejectedStreams, kSessionUint8FieldCount, kSessionHasRemoteSettingsListeners, kSessionRemoteSettingsIsUpToDate, kSessionHasPingListeners, kSessionHasAltsvcListeners, } = binding; const { NGHTTP2_CANCEL, NGHTTP2_REFUSED_STREAM, NGHTTP2_DEFAULT_WEIGHT, NGHTTP2_FLAG_END_STREAM, NGHTTP2_HCAT_PUSH_RESPONSE, NGHTTP2_HCAT_RESPONSE, NGHTTP2_INTERNAL_ERROR, NGHTTP2_NO_ERROR, NGHTTP2_SESSION_CLIENT, NGHTTP2_SESSION_SERVER, NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE, NGHTTP2_ERR_INVALID_ARGUMENT, NGHTTP2_ERR_STREAM_CLOSED, NGHTTP2_ERR_NOMEM, HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_DATE, HTTP2_HEADER_METHOD, HTTP2_HEADER_PATH, HTTP2_HEADER_PROTOCOL, 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, NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL, HTTP2_METHOD_GET, HTTP2_METHOD_HEAD, HTTP2_METHOD_CONNECT, HTTP_STATUS_CONTINUE, HTTP_STATUS_RESET_CONTENT, HTTP_STATUS_OK, HTTP_STATUS_NO_CONTENT, HTTP_STATUS_NOT_MODIFIED, HTTP_STATUS_SWITCHING_PROTOCOLS, HTTP_STATUS_MISDIRECTED_REQUEST, STREAM_OPTION_EMPTY_PAYLOAD, STREAM_OPTION_GET_TRAILERS, } = constants; const STREAM_FLAGS_PENDING = 0x0; const STREAM_FLAGS_READY = 0x1; const STREAM_FLAGS_CLOSED = 0x2; const STREAM_FLAGS_HEADERS_SENT = 0x4; const STREAM_FLAGS_HEAD_REQUEST = 0x8; const STREAM_FLAGS_ABORTED = 0x10; const STREAM_FLAGS_HAS_TRAILERS = 0x20; const SESSION_FLAGS_PENDING = 0x0; const SESSION_FLAGS_READY = 0x1; const SESSION_FLAGS_CLOSED = 0x2; const SESSION_FLAGS_DESTROYED = 0x4; // Top level to avoid creating a closure function emit(self, ...args) { ReflectApply(self.emit, self, args); } // 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(handle, id, cat, flags, headers, sensitiveHeaders) { const session = this[kOwner]; if (session.destroyed) return; const type = session[kType]; session[kUpdateTimer](); debugStream(id, type, "headers received"); const streams = session[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, sensitiveHeaders); if (stream === undefined) { if (session.closed) { // We are not accepting any new streams at this point. This callback // should not be invoked at this point in time, but just in case it is, // refuse the stream using an RST_STREAM and destroy the handle. handle.rstStream(NGHTTP2_REFUSED_STREAM); handle.destroy(); return; } // session[kType] can be only one of two possible values if (type === NGHTTP2_SESSION_SERVER) { // eslint-disable-next-line no-use-before-define stream = new ServerHttp2Stream(session, handle, id, {}, obj); if (endOfStream) { stream.push(null); } if (obj[HTTP2_HEADER_METHOD] === HTTP2_METHOD_HEAD) { // For head requests, there must not be a body... // end the writable side immediately. stream.end(); stream[kState].flags |= STREAM_FLAGS_HEAD_REQUEST; } } else { // eslint-disable-next-line no-use-before-define stream = new ClientHttp2Stream(session, handle, id, {}); if (endOfStream) { stream.push(null); } stream.end(); } if (endOfStream) stream[kState].endAfterHeaders = true; process.nextTick(emit, session, "stream", stream, obj, flags, headers); } else { let event; const status = obj[HTTP2_HEADER_STATUS]; if (cat === NGHTTP2_HCAT_RESPONSE) { if ( !endOfStream && status !== undefined && status >= 100 && status < 200 ) { event = "headers"; } else { event = "response"; } } else if (cat === NGHTTP2_HCAT_PUSH_RESPONSE) { event = "push"; } else if (status !== undefined && status >= 200) { event = "response"; } else { event = endOfStream ? "trailers" : "headers"; } const session = stream.session; if (status === HTTP_STATUS_MISDIRECTED_REQUEST) { const originSet = (session[kState].originSet = initOriginSet(session)); originSet.delete(stream[kOrigin]); } debugStream(id, type, "emitting stream '%s' event", event); process.nextTick(emit, stream, event, obj, flags, headers); } if (endOfStream) { stream.push(null); } } function tryClose(fd) { // Try to close the file descriptor. If closing fails, assert because // an error really should not happen at this point. fs.close(fd, assert.ifError); } // Called when the Http2Stream has finished sending data and is ready for // trailers to be sent. This will only be called if the { hasOptions: true } // option is set. function onStreamTrailers() { const stream = this[kOwner]; stream[kState].trailersReady = true; if (stream.destroyed || stream.closed) return; if (!stream.emit("wantTrailers")) { // There are no listeners, send empty trailing HEADERS frame and close. stream.sendTrailers({}); } } // Submit an RST-STREAM frame to be sent to the remote peer. // This will cause the Http2Stream to be closed. function submitRstStream(code) { if (this[kHandle] !== undefined) { this[kHandle].rstStream(code); } } // Keep track of the number/presence of JS event listeners. Knowing that there // are no listeners allows the C++ code to skip calling into JS for an event. function sessionListenerAdded(name) { switch (name) { case "ping": this[kNativeFields][kBitfield] |= 1 << kSessionHasPingListeners; break; case "altsvc": this[kNativeFields][kBitfield] |= 1 << kSessionHasAltsvcListeners; break; case "remoteSettings": this[kNativeFields][kBitfield] |= 1 << kSessionHasRemoteSettingsListeners; break; case "priority": this[kNativeFields][kSessionPriorityListenerCount]++; break; case "frameError": this[kNativeFields][kSessionFrameErrorListenerCount]++; break; } } function sessionListenerRemoved(name) { switch (name) { case "ping": if (this.listenerCount(name) > 0) return; this[kNativeFields][kBitfield] &= ~(1 << kSessionHasPingListeners); break; case "altsvc": if (this.listenerCount(name) > 0) return; this[kNativeFields][kBitfield] &= ~(1 << kSessionHasAltsvcListeners); break; case "remoteSettings": if (this.listenerCount(name) > 0) return; this[kNativeFields][kBitfield] &= ~( 1 << kSessionHasRemoteSettingsListeners ); break; case "priority": this[kNativeFields][kSessionPriorityListenerCount]--; break; case "frameError": this[kNativeFields][kSessionFrameErrorListenerCount]--; break; } } // Also keep track of listeners for the Http2Stream instances, as some events // are emitted on those objects. function streamListenerAdded(name) { const session = this[kSession]; if (!session) return; switch (name) { case "priority": session[kNativeFields][kSessionPriorityListenerCount]++; break; case "frameError": session[kNativeFields][kSessionFrameErrorListenerCount]++; break; } } function streamListenerRemoved(name) { const session = this[kSession]; if (!session) return; switch (name) { case "priority": session[kNativeFields][kSessionPriorityListenerCount]--; break; case "frameError": session[kNativeFields][kSessionFrameErrorListenerCount]--; break; } } function onPing(payload) { const session = this[kOwner]; if (session.destroyed) return; session[kUpdateTimer](); debugSessionObj(session, "new ping received"); session.emit("ping", payload); } // Called when the stream is closed either by sending or receiving an // RST_STREAM frame, or through a natural end-of-stream. // If the writable and readable sides of the stream are still open at this // point, close them. If there is an open fd for file send, close that also. // At this point the underlying node::http2:Http2Stream handle is no // longer usable so destroy it also. function onStreamClose(code) { const stream = this[kOwner]; if (!stream || stream.destroyed) return false; debugStreamObj( stream, "closed with code %d, closed %s, readable %s", code, stream.closed, stream.readable, ); if (!stream.closed) closeStream(stream, code, kNoRstStream); stream[kState].fd = -1; // Defer destroy we actually emit end. if (!stream.readable || code !== NGHTTP2_NO_ERROR) { // If errored or ended, we can destroy immediately. stream.destroy(); } else { // Wait for end to destroy. stream.on("end", stream[kMaybeDestroy]); // Push a null so the stream can end whenever the client consumes // it completely. stream.push(null); // If the user hasn't tried to consume the stream (and this is a server // session) then just dump the incoming data so that the stream can // be destroyed. if ( stream[kSession][kType] === NGHTTP2_SESSION_SERVER && !stream[kState].didRead && stream.readableFlowing === null ) stream.resume(); else stream.read(0); } return true; } // Called when the remote peer settings have been updated. // Resets the cached settings. function onSettings() { const session = this[kOwner]; if (session.destroyed) return; session[kUpdateTimer](); debugSessionObj(session, "new settings received"); session[kRemoteSettings] = undefined; session.emit("remoteSettings", session.remoteSettings); } // 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 session = this[kOwner]; if (session.destroyed) return; debugStream( id, session[kType], "priority [parent: %d, weight: %d, exclusive: %s]", parent, weight, exclusive, ); const emitter = session[kState].streams.get(id) || session; if (!emitter.destroyed) { emitter[kUpdateTimer](); emitter.emit("priority", id, parent, weight, exclusive); } } // Called by the native layer when an error has occurred sending a // frame. This should be exceedingly rare. function onFrameError(id, type, code) { const session = this[kOwner]; if (session.destroyed) return; debugSessionObj( session, "error sending frame type %d on stream %d, code: %d", type, id, code, ); const emitter = session[kState].streams.get(id) || session; emitter[kUpdateTimer](); emitter.emit("frameError", type, code, id); session[kState].streams.get(id).close(code); session.close(); } function onAltSvc(stream, origin, alt) { const session = this[kOwner]; if (session.destroyed) return; debugSessionObj( session, "altsvc received: stream: %d, origin: %s, alt: %s", stream, origin, alt, ); session[kUpdateTimer](); session.emit("altsvc", alt, origin, stream); } function initOriginSet(session) { let originSet = session[kState].originSet; if (originSet === undefined) { const socket = session[kSocket]; session[kState].originSet = originSet = new Set(); if (socket.servername != null) { let originString = `https://${socket.servername}`; if (socket.remotePort != null) originString += `:${socket.remotePort}`; // We have to ensure that it is a properly serialized // ASCII origin string. The socket.servername might not // be properly ASCII encoded. originSet.add(getURLOrigin(originString)); } } return originSet; } function onOrigin(origins) { const session = this[kOwner]; if (session.destroyed) return; debugSessionObj(session, "origin received: %j", origins); session[kUpdateTimer](); if (!session.encrypted || session.destroyed) return undefined; const originSet = initOriginSet(session); for (let n = 0; n < origins.length; n++) originSet.add(origins[n]); session.emit("origin", origins); } // Receiving a GOAWAY frame from the connected peer is a signal that no // new streams should be created. If the code === NGHTTP2_NO_ERROR, we // are going to send our close, but allow existing frames to close // normally. If code !== NGHTTP2_NO_ERROR, we are going to send our own // close using the same code then destroy the session with an error. // The goaway event will be emitted on next tick. function onGoawayData(code, lastStreamID, buf) { const session = this[kOwner]; if (session.destroyed) return; debugSessionObj( session, "goaway %d received [last stream id: %d]", code, lastStreamID, ); const state = session[kState]; state.goawayCode = code; state.goawayLastStreamID = lastStreamID; session.emit("goaway", code, lastStreamID, buf); if (code === NGHTTP2_NO_ERROR) { // If this is a no error goaway, begin shutting down. // No new streams permitted, but existing streams may // close naturally on their own. session.close(); } else { // However, if the code is not NGHTTP_NO_ERROR, destroy the // session immediately. We destroy with an error but send a // goaway using NGHTTP2_NO_ERROR because there was no error // condition on this side of the session that caused the // shutdown. session.destroy(new ERR_HTTP2_SESSION_ERROR(code), NGHTTP2_NO_ERROR); } } // 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]; // At this point, the stream should have already been destroyed during // the session.destroy() method. Do nothing else. if (session === undefined || session.destroyed) return; // If the session was closed while waiting for the connect, destroy // the stream and do not continue with the request. if (session.closed) { const err = new ERR_HTTP2_GOAWAY_SESSION(); this.destroy(err); return; } debugSessionObj(session, "connected, initializing request"); let streamOptions = 0; if (options.endStream) streamOptions |= STREAM_OPTION_EMPTY_PAYLOAD; if (options.waitForTrailers) streamOptions |= STREAM_OPTION_GET_TRAILERS; // `ret` will be either the reserved stream ID (if positive) // or an error code (if negative) const ret = session[kHandle].request( headers, streamOptions, options.parent | 0, options.weight | 0, !!options.exclusive, ); // In an error condition, one of three possible response codes will be // possible: // * 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. if (typeof ret === "number") { let err; switch (ret) { case NGHTTP2_ERR_STREAM_ID_NOT_AVAILABLE: err = new ERR_HTTP2_OUT_OF_STREAMS(); this.destroy(err); break; case NGHTTP2_ERR_INVALID_ARGUMENT: err = new ERR_HTTP2_STREAM_SELF_DEPENDENCY(); this.destroy(err); break; default: session.destroy(new NghttpError(ret)); } return; } this[kInit](ret.id(), ret); } // Validates that priority options are correct, specifically: // 1. options.weight must be a number // 2. options.parent must be a positive number // 3. options.exclusive must be a boolean // 4. if specified, options.silent must be a boolean // // Also sets the default priority options if they are not set. const setAndValidatePriorityOptions = hideStackFrames((options) => { if (options.weight === undefined) { options.weight = NGHTTP2_DEFAULT_WEIGHT; } else { validateNumber.withoutStackTrace(options.weight, "options.weight"); } if (options.parent === undefined) { options.parent = 0; } else { validateNumber.withoutStackTrace(options.parent, "options.parent", 0); } if (options.exclusive === undefined) { options.exclusive = false; } else { validateBoolean.withoutStackTrace(options.exclusive, "options.exclusive"); } if (options.silent === undefined) { options.silent = false; } else { validateBoolean.withoutStackTrace(options.silent, "options.silent"); } }); // When an error occurs internally at the binding level, immediately // destroy the session. function onSessionInternalError(integerCode, customErrorCode) { if (this[kOwner] !== undefined) this[kOwner].destroy(new NghttpError(integerCode, customErrorCode)); } function settingsCallback(cb, ack, duration) { this[kState].pendingAck--; this[kLocalSettings] = undefined; if (ack) { debugSessionObj(this, "settings received"); const settings = this.localSettings; if (typeof cb === "function") cb(null, settings, duration); this.emit("localSettings", settings); } else { debugSessionObj(this, "settings canceled"); if (typeof cb === "function") cb(new ERR_HTTP2_SETTINGS_CANCEL()); } } // Submits a SETTINGS frame to be sent to the remote peer. function submitSettings(settings, callback) { if (this.destroyed) return; debugSessionObj(this, "submitting settings"); this[kUpdateTimer](); updateSettingsBuffer(settings); if (!this[kHandle].settings(settingsCallback.bind(this, callback))) { this.destroy(new ERR_HTTP2_MAX_PENDING_SETTINGS_ACK()); } } // 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(options) { if (this.destroyed) return; this[kUpdateTimer](); // If the parent is the id, do nothing because a // stream cannot be made to depend on itself. if (options.parent === this[kID]) return; this[kHandle].priority( options.parent | 0, options.weight | 0, !!options.exclusive, !!options.silent, ); } // Submit a GOAWAY frame to be sent to the remote peer. // If the lastStreamID is set to <= 0, then the lastProcStreamID will // be used. The opaqueData must either be a typed array or undefined // (which will be checked elsewhere). function submitGoaway(code, lastStreamID, opaqueData) { if (this.destroyed) return; debugSessionObj(this, "submitting goaway"); this[kUpdateTimer](); this[kHandle].goaway(code, lastStreamID, opaqueData); } const proxySocketHandler = { get(session, prop) { switch (prop) { case "setTimeout": case "ref": case "unref": return session[prop].bind(session); case "destroy": case "emit": case "end": case "pause": case "read": case "resume": case "write": case "setEncoding": case "setKeepAlive": case "setNoDelay": throw new ERR_HTTP2_NO_SOCKET_MANIPULATION(); default: { const socket = session[kSocket]; if (socket === undefined) throw new ERR_HTTP2_SOCKET_UNBOUND(); const value = socket[prop]; return typeof value === "function" ? value.bind(socket) : value; } } }, getPrototypeOf(session) { const socket = session[kSocket]; if (socket === undefined) throw new ERR_HTTP2_SOCKET_UNBOUND(); return Reflect.getPrototypeOf(socket); }, set(session, prop, value) { switch (prop) { case "setTimeout": case "ref": case "unref": session[prop] = value; return true; case "destroy": case "emit": case "end": case "pause": case "read": case "resume": case "write": case "setEncoding": case "setKeepAlive": case "setNoDelay": throw new ERR_HTTP2_NO_SOCKET_MANIPULATION(); default: { const socket = session[kSocket]; if (socket === undefined) throw new ERR_HTTP2_SOCKET_UNBOUND(); socket[prop] = value; return true; } } }, }; // pingCallback() returns a function that is invoked when an HTTP2 PING // frame acknowledgement is received. The ack is either true or false to // indicate if the ping was successful or not. The duration indicates the // number of milliseconds elapsed since the ping was sent and the ack // received. The payload is a Buffer containing the 8 bytes of payload // data received on the PING acknowledgement. function pingCallback(cb) { return function pingCallback(ack, duration, payload) { if (ack) { cb(null, duration, payload); } else { cb(new ERR_HTTP2_PING_CANCEL()); } }; } // Validates the values in a settings object. Specifically: // 1. headerTableSize must be a number in the range 0 <= n <= kMaxInt // 2. initialWindowSize must be a number in the range 0 <= n <= kMaxInt // 3. maxFrameSize must be a number in the range 16384 <= n <= kMaxFrameSize // 4. maxConcurrentStreams must be a number in the range 0 <= n <= kMaxStreams // 5. maxHeaderListSize must be a number in the range 0 <= n <= kMaxInt // 6. enablePush must be a boolean // 7. enableConnectProtocol must be a boolean // All settings are optional and may be left undefined const validateSettings = hideStackFrames((settings) => { if (settings === undefined) return; assertIsObject.withoutStackTrace( settings.customSettings, "customSettings", "Number", ); if (settings.customSettings) { const entries = Object.entries(settings.customSettings); if (entries.length > MAX_ADDITIONAL_SETTINGS) throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); for (const { 0: key, 1: value } of entries) { assertWithinRange.withoutStackTrace( "customSettings:id", Number(key), 0, 0xffff, ); assertWithinRange.withoutStackTrace( "customSettings:value", Number(value), 0, kMaxInt, ); } } assertWithinRange.withoutStackTrace( "headerTableSize", settings.headerTableSize, 0, kMaxInt, ); assertWithinRange.withoutStackTrace( "initialWindowSize", settings.initialWindowSize, 0, kMaxInt, ); assertWithinRange.withoutStackTrace( "maxFrameSize", settings.maxFrameSize, 16384, kMaxFrameSize, ); assertWithinRange.withoutStackTrace( "maxConcurrentStreams", settings.maxConcurrentStreams, 0, kMaxStreams, ); assertWithinRange.withoutStackTrace( "maxHeaderListSize", settings.maxHeaderListSize, 0, kMaxInt, ); assertWithinRange.withoutStackTrace( "maxHeaderSize", settings.maxHeaderSize, 0, kMaxInt, ); if ( settings.enablePush !== undefined && typeof settings.enablePush !== "boolean" ) { throw new ERR_HTTP2_INVALID_SETTING_VALUE.HideStackFramesError( "enablePush", settings.enablePush, ); } if ( settings.enableConnectProtocol !== undefined && typeof settings.enableConnectProtocol !== "boolean" ) { throw new ERR_HTTP2_INVALID_SETTING_VALUE.HideStackFramesError( "enableConnectProtocol", settings.enableConnectProtocol, ); } }); // Wrap a typed array in a proxy, and allow selectively copying the entries // that have explicitly been set to another typed array. function trackAssignmentsTypedArray(typedArray) { const typedArrayLength = typedArray.length; const modifiedEntries = new Uint8Array(typedArrayLength); function copyAssigned(target) { for (let i = 0; i < typedArrayLength; i++) { if (modifiedEntries[i]) { target[i] = typedArray[i]; } } } return new Proxy(typedArray, { __proto__: null, get(obj, prop, receiver) { if (prop === "copyAssigned") { return copyAssigned; } return ReflectGet(obj, prop, receiver); }, set(obj, prop, value) { if (`${+prop}` === prop) { modifiedEntries[prop] = 1; } return ReflectSet(obj, prop, value); }, }); } // 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(socket, type, options) { // If the session has been destroyed, go ahead and emit 'connect', // but do nothing else. The various on('connect') handlers set by // core will check for session.destroyed before progressing, this // ensures that those at least get cleared out. if (this.destroyed) { process.nextTick(emit, this, "connect", this, socket); return; } assert( socket._handle !== undefined, "Internal HTTP/2 Failure. The socket is not connected. Please " + "report this as a bug in Node.js", ); debugSession(type, "setting up session handle"); this[kState].flags |= SESSION_FLAGS_READY; updateOptionsBuffer(options); if (options.remoteCustomSettings) { remoteCustomSettingsToBuffer(options.remoteCustomSettings); } const handle = new binding.Http2Session(type); handle[kOwner] = this; if (typeof options.selectPadding === "function") this[kSelectPadding] = options.selectPadding; handle.consume(socket._handle); this[kHandle] = handle; if (this[kNativeFields]) { // If some options have already been set before the handle existed, copy // those (and only those) that have manually been set over. this[kNativeFields].copyAssigned(handle.fields); } this[kNativeFields] = handle.fields; if (socket.encrypted) { this[kAlpnProtocol] = socket.alpnProtocol; this[kEncrypted] = true; } else { // 'h2c' is the protocol identifier for HTTP/2 over plain-text. We use // it here to identify any session that is not explicitly using an // encrypted socket. this[kAlpnProtocol] = "h2c"; this[kEncrypted] = false; } if (isUint32(options.maxSessionInvalidFrames)) { const uint32 = new Uint32Array( this[kNativeFields].buffer, kSessionMaxInvalidFrames, 1, ); uint32[0] = options.maxSessionInvalidFrames; } if (isUint32(options.maxSessionRejectedStreams)) { const uint32 = new Uint32Array( this[kNativeFields].buffer, kSessionMaxRejectedStreams, 1, ); uint32[0] = options.maxSessionRejectedStreams; } const settings = typeof options.settings === "object" ? options.settings : {}; this.settings(settings); if (type === NGHTTP2_SESSION_SERVER && Array.isArray(options.origins)) { ReflectApply(this.origin, this, options.origins); } process.nextTick(emit, this, "connect", this, socket); } // Emits a close event followed by an error event if err is truthy. Used // by Http2Session.prototype.destroy() function emitClose(self, error) { if (error) self.emit("error", error); self.emit("close"); } function cleanupSession(session) { const socket = session[kSocket]; const handle = session[kHandle]; session[kProxySocket] = undefined; session[kSocket] = undefined; session[kHandle] = undefined; session[kNativeFields] = trackAssignmentsTypedArray( new Uint8Array(kSessionUint8FieldCount), ); if (handle) handle.ondone = null; if (socket) { socket[kBoundSession] = undefined; socket[kServer] = undefined; } } function finishSessionClose(session, error) { debugSessionObj(session, "finishSessionClose"); const socket = session[kSocket]; cleanupSession(session); if (socket && !socket.destroyed) { socket.on("close", () => { emitClose(session, error); }); if (session.closed) { // If we're gracefully closing the socket, call resume() so we can detect // the peer closing in case binding.Http2Session is already gone. socket.resume(); } // Always wait for writable side to finish. socket.end((err) => { debugSessionObj(session, "finishSessionClose socket end", err, error); // If session.destroy() was called, destroy the underlying socket. Delay // it a bit to try to avoid ECONNRESET on Windows. if (!session.closed) { setImmediate(() => { socket.destroy(error); }); } }); } else { process.nextTick(emitClose, session, error); } } function closeSession(session, code, error) { debugSessionObj(session, "start closing/destroying", error); const state = session[kState]; state.flags |= SESSION_FLAGS_DESTROYED; state.destroyCode = code; // Clear timeout and remove timeout listeners. session.setTimeout(0); session.removeAllListeners("timeout"); // Destroy any pending and open streams if (state.pendingStreams.size > 0 || state.streams.size > 0) { const cancel = new ERR_HTTP2_STREAM_CANCEL(error); state.pendingStreams.forEach((stream) => stream.destroy(cancel)); state.streams.forEach((stream) => stream.destroy(error)); } // Disassociate from the socket and server. const socket = session[kSocket]; const handle = session[kHandle]; // Destroy the handle if it exists at this point. if (handle !== undefined) { handle.ondone = finishSessionClose.bind(null, session, error); handle.destroy(code, socket.destroyed); } else { finishSessionClose(session, error); } } // 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. // In that case, the Http2Session will wait for the socket to connect. Once // the Http2Session is ready, it will emit its own 'connect' event. // // The Http2Session.goaway() method will send a GOAWAY frame, signalling // to the connected peer that a shutdown is in progress. Sending a goaway // frame has no other effect, however. // // Receiving a GOAWAY frame will cause the Http2Session to first emit a 'goaway' // event notifying the user that a shutdown is in progress. If the goaway // error code equals 0 (NGHTTP2_NO_ERROR), session.close() will be called, // causing the Http2Session to send its own GOAWAY frame and switch itself // into a graceful closing state. In this state, new inbound or outbound // Http2Streams will be rejected. Existing *pending* streams (those created // but without an assigned stream ID or handle) will be destroyed with a // cancel error. Existing open streams will be permitted to complete on their // own. Once all existing streams close, session.destroy() will be called // automatically. // // Calling session.destroy() will tear down the Http2Session immediately, // making it no longer usable. Pending and existing streams will be destroyed. // The bound socket will be destroyed. Once all resources have been freed up, // the 'close' event will be emitted. Note that pending streams will be // destroyed using a specific "ERR_HTTP2_STREAM_CANCEL" error. Existing open // streams will be destroyed using the same error passed to session.destroy() // // If destroy is called with an error, an 'error' event will be emitted // immediately following the 'close' event. // // The socket and Http2Session lifecycles are tightly bound. Once one is // destroyed, the other should also be destroyed. When the socket is destroyed // with an error, session.destroy() will be called with that same error. // Likewise, when session.destroy() is called with an error, the same error // will be sent to the socket. class Http2Session extends EventEmitter { 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[kBoundSession] !== undefined) throw new ERR_HTTP2_SOCKET_BOUND(); socket[kBoundSession] = this; if (!socket._handle || !socket._handle.isStreamBase) { socket = new JSStreamSocket(socket); } socket.on("error", socketOnError); socket.on("close", socketOnClose); this[kState] = { destroyCode: NGHTTP2_NO_ERROR, flags: SESSION_FLAGS_PENDING, goawayCode: null, goawayLastStreamID: null, streams: new Map(), pendingStreams: new Set(), pendingAck: 0, shutdownWritableCalled: false, writeQueueSize: 0, originSet: undefined, }; this[kEncrypted] = undefined; this[kAlpnProtocol] = undefined; this[kType] = type; this[kProxySocket] = null; this[kSocket] = socket; this[kTimeout] = null; this[kHandle] = undefined; // Do not use nagle's algorithm if (typeof socket.setNoDelay === "function") socket.setNoDelay(); // Disable TLS renegotiation on the socket if (typeof socket.disableRenegotiation === "function") socket.disableRenegotiation(); const setupFn = setupHandle.bind(this, socket, type, options); if (socket.connecting || socket.secureConnecting) { const connectEvent = socket instanceof tls.TLSSocket ? "secureConnect" : "connect"; socket.once(connectEvent, () => { try { setupFn(); } catch (error) { socket.destroy(error); } }); } else { setupFn(); } if (!this[kNativeFields]) { this[kNativeFields] = trackAssignmentsTypedArray( new Uint8Array(kSessionUint8FieldCount), ); } this.on("newListener", sessionListenerAdded); this.on("removeListener", sessionListenerRemoved); // Process data on the next tick - a remoteSettings handler may be attached. // https://github.com/nodejs/node/issues/35981 process.nextTick(() => { // Socket already has some buffered data - emulate receiving it // https://github.com/nodejs/node/issues/35475 // https://github.com/nodejs/node/issues/34532 if (socket.readableLength) { let buf; while ((buf = socket.read()) !== null) { debugSession(type, `${buf.length} bytes already in buffer`); this[kHandle].receive(buf); } } }); debugSession(type, "created"); } // Returns undefined if the socket is not yet connected, true if the // socket is a TLSSocket, and false if it is not. get encrypted() { return this[kEncrypted]; } // Returns undefined if the socket is not yet connected, `h2` if the // socket is a TLSSocket and the alpnProtocol is `h2`, or `h2c` if the // socket is not a TLSSocket. get alpnProtocol() { return this[kAlpnProtocol]; } // TODO(jasnell): originSet is being added in preparation for ORIGIN frame // support. At the current time, the ORIGIN frame specification is awaiting // publication as an RFC and is awaiting implementation in nghttp2. Once // added, an ORIGIN frame will add to the origins included in the origin // set. 421 responses will remove origins from the set. get originSet() { if (!this.encrypted || this.destroyed) return undefined; return Array.from(initOriginSet(this)); } // True if the Http2Session is still waiting for the socket to connect get connecting() { return (this[kState].flags & SESSION_FLAGS_READY) === 0; } // True if Http2Session.prototype.close() has been called get closed() { return !!(this[kState].flags & SESSION_FLAGS_CLOSED); } // True if Http2Session.prototype.destroy() has been called get destroyed() { return !!(this[kState].flags & SESSION_FLAGS_DESTROYED); } // Resets the timeout counter [kUpdateTimer]() { if (this.destroyed) return; if (this[kTimeout]) this[kTimeout].refresh(); } // Sets the id of the next stream to be created by this Http2Session. // The value must be a number in the range 0 <= n <= kMaxStreams. The // value also needs to be larger than the current next stream ID. setNextStreamID(id) { if (this.destroyed) throw new ERR_HTTP2_INVALID_SESSION(); validateNumber(id, "id"); if (id <= 0 || id > kMaxStreams) throw new ERR_OUT_OF_RANGE("id", `> 0 and <= ${kMaxStreams}`, id); this[kHandle].setNextStreamID(id); } // Sets the local window size (local endpoints's window size) // Returns 0 if success or throw an exception if NGHTTP2_ERR_NOMEM // if the window allocation fails setLocalWindowSize(windowSize) { if (this.destroyed) throw new ERR_HTTP2_INVALID_SESSION(); validateInt32(windowSize, "windowSize", 0); const ret = this[kHandle].setLocalWindowSize(windowSize); if (ret === NGHTTP2_ERR_NOMEM) { this.destroy(new ERR_HTTP2_NO_MEM()); } } // If ping is called while we are still connecting, or after close() has // been called, the ping callback will be invoked immediately with a ping // cancelled error and a duration of 0.0. ping(payload, callback) { if (this.destroyed) throw new ERR_HTTP2_INVALID_SESSION(); if (typeof payload === "function") { callback = payload; payload = undefined; } if (payload) { validateBuffer(payload, "payload"); } if (payload && payload.length !== 8) { throw new ERR_HTTP2_PING_LENGTH(); } validateFunction(callback, "callback"); const cb = pingCallback(callback); if (this.connecting || this.closed) { process.nextTick(cb, false, 0.0, payload); return; } return this[kHandle].ping(payload, cb); } [kInspect](depth, opts) { if (typeof depth === "number" && depth < 0) return this; const obj = { type: this[kType], closed: this.closed, destroyed: this.destroyed, state: this.state, localSettings: this.localSettings, remoteSettings: this.remoteSettings, }; return `Http2Session ${format(obj)}`; } // The socket owned by this session get socket() { const proxySocket = this[kProxySocket]; if (proxySocket === null) return (this[kProxySocket] = new Proxy(this, proxySocketHandler)); return proxySocket; } // The session type get type() { return this[kType]; } // If a GOAWAY frame has been received, gives the error code specified get goawayCode() { return this[kState].goawayCode || NGHTTP2_NO_ERROR; } // If a GOAWAY frame has been received, gives the last stream ID reported get goawayLastStreamID() { return this[kState].goawayLastStreamID || 0; } // True if the Http2Session is waiting for a settings acknowledgement get pendingSettingsAck() { return this[kState].pendingAck > 0; } // Retrieves state information for the Http2Session get state() { return this.connecting || this.destroyed ? {} : getSessionState(this[kHandle]); } // The settings currently in effect for the local peer. These will // be updated only when a settings acknowledgement has been received. get localSettings() { const settings = this[kLocalSettings]; if (settings !== undefined) return settings; if (this.destroyed || this.connecting) return {}; return (this[kLocalSettings] = getSettings(this[kHandle], false)); // Local } // The settings currently in effect for the remote peer. get remoteSettings() { if ( this[kNativeFields][kBitfield] & (1 << kSessionRemoteSettingsIsUpToDate) ) { const settings = this[kRemoteSettings]; if (settings !== undefined) { return settings; } } if (this.destroyed || this.connecting) return {}; this[kNativeFields][kBitfield] |= 1 << kSessionRemoteSettingsIsUpToDate; return (this[kRemoteSettings] = getSettings(this[kHandle], true)); // Remote } // Submits a SETTINGS frame to be sent to the remote peer. settings(settings, callback) { if (this.destroyed) throw new ERR_HTTP2_INVALID_SESSION(); assertIsObject(settings, "settings"); validateSettings(settings); if (callback) { validateFunction(callback, "callback"); } debugSessionObj(this, "sending settings"); this[kState].pendingAck++; const settingsFn = submitSettings.bind(this, { ...settings }, callback); if (this.connecting) { this.once("connect", settingsFn); return; } settingsFn(); } // Submits a GOAWAY frame to be sent to the remote peer. Note that this // is only a notification, and does not affect the usable state of the // session with the notable exception that new incoming streams will // be rejected automatically. goaway(code = NGHTTP2_NO_ERROR, lastStreamID = 0, opaqueData) { if (this.destroyed) throw new ERR_HTTP2_INVALID_SESSION(); if (opaqueData !== undefined) { validateBuffer(opaqueData, "opaqueData"); } validateNumber(code, "code"); validateNumber(lastStreamID, "lastStreamID"); const goawayFn = submitGoaway.bind(this, code, lastStreamID, opaqueData); if (this.connecting) { this.once("connect", goawayFn); return; } goawayFn(); } // Destroy the Http2Session, making it no longer usable and cancelling // any pending activity. destroy(error = NGHTTP2_NO_ERROR, code) { if (this.destroyed) return; debugSessionObj(this, "destroying"); if (typeof error === "number") { code = error; error = code !== NGHTTP2_NO_ERROR ? new ERR_HTTP2_SESSION_ERROR(code) : undefined; } if (code === undefined && error != null) code = NGHTTP2_INTERNAL_ERROR; closeSession(this, code, error); } // Closing the session will: // 1. Send a goaway frame // 2. Mark the session as closed // 3. Prevent new inbound or outbound streams from being opened // 4. Optionally register a 'close' event handler // 5. Will cause the session to automatically destroy after the // last currently open Http2Stream closes. // // Close always assumes a good, non-error shutdown (NGHTTP_NO_ERROR) // // If the session has not connected yet, the closed flag will still be // set but the goaway will not be sent until after the connect event // is emitted. close(callback) { if (this.closed || this.destroyed) return; debugSessionObj(this, "marking session closed"); this[kState].flags |= SESSION_FLAGS_CLOSED; if (typeof callback === "function") this.once("close", callback); this.goaway(); this[kMaybeDestroy](); } [EventEmitter.captureRejectionSymbol](err, event, ...args) { switch (event) { case "stream": { const stream = args[0]; stream.destroy(err); break; } default: this.destroy(err); } } // Destroy the session if: // * error is not undefined/null // * session is closed and there are no more pending or open streams [kMaybeDestroy](error) { if (error == null) { const state = this[kState]; // Do not destroy if we're not closed and there are pending/open streams if ( !this.closed || state.streams