/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
ext/node/polyfills/internal/http2/util.ts
1 116 строк
35 KB
Kenta Moriuchi
chore: make prefer-primordials lint an internal plugin (#35978)
23 июл 2026, 11:53
Не верифицирован
23 июл 2026, 11:53
5b55d08
Код
Авторство
О чём код?
// deno-lint-ignore-file // Copyright 2018-2026 the Deno authors. MIT license. // Copyright Joyent and Node contributors. All rights reserved. MIT license. "use strict"; (function () { const { core, primordials } = __bootstrap; const { ArrayIsArray, ArrayPrototypeConcat, ArrayPrototypeIncludes, ArrayPrototypeMap, ArrayPrototypePush, Error, MathMax, Number, NumberIsNaN, ObjectAssign, ObjectKeys, SafeSet, String, StringFromCharCode, StringPrototypeCharCodeAt, StringPrototypeSlice, StringPrototypeToLowerCase, Symbol, } = primordials; const { op_http2_error_string, op_http2_http_state } = core.ops; const lazyHttpCommon = core.createLazyLoader("node:_http_common"); const lazyProcess = core.createLazyLoader("node:process"); const { codes, hideStackFrames } = core.loadExtScript( "ext:deno_node/internal/errors.ts", ); const { HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE, HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD, HTTP2_HEADER_AGE, HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_AUTHORIZATION, HTTP2_HEADER_CONNECTION, HTTP2_HEADER_CONTENT_ENCODING, HTTP2_HEADER_CONTENT_LANGUAGE, HTTP2_HEADER_CONTENT_LENGTH, HTTP2_HEADER_CONTENT_LOCATION, HTTP2_HEADER_CONTENT_MD5, HTTP2_HEADER_CONTENT_RANGE, HTTP2_HEADER_CONTENT_TYPE, HTTP2_HEADER_COOKIE, HTTP2_HEADER_DATE, HTTP2_HEADER_DNT, HTTP2_HEADER_ETAG, HTTP2_HEADER_EXPIRES, HTTP2_HEADER_FROM, HTTP2_HEADER_HOST, HTTP2_HEADER_HTTP2_SETTINGS, HTTP2_HEADER_IF_MATCH, HTTP2_HEADER_IF_MODIFIED_SINCE, HTTP2_HEADER_IF_NONE_MATCH, HTTP2_HEADER_IF_RANGE, HTTP2_HEADER_IF_UNMODIFIED_SINCE, HTTP2_HEADER_KEEP_ALIVE, HTTP2_HEADER_LAST_MODIFIED, HTTP2_HEADER_LOCATION, HTTP2_HEADER_MAX_FORWARDS, HTTP2_HEADER_METHOD, HTTP2_HEADER_PATH, HTTP2_HEADER_PROTOCOL, HTTP2_HEADER_PROXY_AUTHORIZATION, HTTP2_HEADER_PROXY_CONNECTION, HTTP2_HEADER_RANGE, HTTP2_HEADER_REFERER, HTTP2_HEADER_RETRY_AFTER, HTTP2_HEADER_SCHEME, HTTP2_HEADER_SET_COOKIE, HTTP2_HEADER_STATUS, HTTP2_HEADER_TE, HTTP2_HEADER_TK, HTTP2_HEADER_TRANSFER_ENCODING, HTTP2_HEADER_UPGRADE, HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS, HTTP2_HEADER_USER_AGENT, HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS, HTTP2_METHOD_CONNECT, HTTP2_METHOD_DELETE, HTTP2_METHOD_GET, HTTP2_METHOD_HEAD, NGHTTP2_NV_FLAG_NO_INDEX, NGHTTP2_NV_FLAG_NONE, NGHTTP2_SESSION_CLIENT, NGHTTP2_SESSION_SERVER, } = core.loadExtScript("ext:deno_node/internal/http2/constants.ts"); const { ERR_HTTP2_CONNECT_AUTHORITY, ERR_HTTP2_CONNECT_PATH, ERR_HTTP2_CONNECT_SCHEME, ERR_HTTP2_HEADER_SINGLE_VALUE, ERR_HTTP2_INVALID_CONNECTION_HEADERS, ERR_HTTP2_INVALID_PSEUDOHEADER, ERR_HTTP2_INVALID_SETTING_VALUE, ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS, ERR_INVALID_ARG_TYPE, ERR_INVALID_HTTP_TOKEN, } = codes; const kAuthority = Symbol("authority"); const kSensitiveHeaders = Symbol("sensitiveHeaders"); const kSocket = Symbol("socket"); const kProtocol = Symbol("protocol"); const kProxySocket = Symbol("proxySocket"); const kRequest = Symbol("request"); const kStrictSingleValueFields = Symbol("strictSingleValueFields"); // This set is defined strictly by the HTTP/2 specification. Only // :-prefixed headers defined by that specification may be added to // this set. const kValidPseudoHeaders = new SafeSet([ HTTP2_HEADER_STATUS, HTTP2_HEADER_METHOD, HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_SCHEME, HTTP2_HEADER_PATH, HTTP2_HEADER_PROTOCOL, ]); // This set contains headers that are permitted to have only a single // value. Multiple instances must not be specified. const kSingleValueHeaders = new SafeSet([ HTTP2_HEADER_STATUS, HTTP2_HEADER_METHOD, HTTP2_HEADER_AUTHORITY, HTTP2_HEADER_SCHEME, HTTP2_HEADER_PATH, HTTP2_HEADER_PROTOCOL, HTTP2_HEADER_ACCESS_CONTROL_ALLOW_CREDENTIALS, HTTP2_HEADER_ACCESS_CONTROL_MAX_AGE, HTTP2_HEADER_ACCESS_CONTROL_REQUEST_METHOD, HTTP2_HEADER_AGE, HTTP2_HEADER_AUTHORIZATION, HTTP2_HEADER_CONTENT_ENCODING, HTTP2_HEADER_CONTENT_LANGUAGE, HTTP2_HEADER_CONTENT_LENGTH, HTTP2_HEADER_CONTENT_LOCATION, HTTP2_HEADER_CONTENT_MD5, HTTP2_HEADER_CONTENT_RANGE, HTTP2_HEADER_CONTENT_TYPE, HTTP2_HEADER_DATE, HTTP2_HEADER_DNT, HTTP2_HEADER_ETAG, HTTP2_HEADER_EXPIRES, HTTP2_HEADER_FROM, HTTP2_HEADER_HOST, HTTP2_HEADER_IF_MATCH, HTTP2_HEADER_IF_MODIFIED_SINCE, HTTP2_HEADER_IF_NONE_MATCH, HTTP2_HEADER_IF_RANGE, HTTP2_HEADER_IF_UNMODIFIED_SINCE, HTTP2_HEADER_LAST_MODIFIED, HTTP2_HEADER_LOCATION, HTTP2_HEADER_MAX_FORWARDS, HTTP2_HEADER_PROXY_AUTHORIZATION, HTTP2_HEADER_RANGE, HTTP2_HEADER_REFERER, HTTP2_HEADER_RETRY_AFTER, HTTP2_HEADER_TK, HTTP2_HEADER_UPGRADE_INSECURE_REQUESTS, HTTP2_HEADER_USER_AGENT, HTTP2_HEADER_X_CONTENT_TYPE_OPTIONS, ]); // The HTTP methods in this set are specifically defined as assigning no // meaning to the request payload. By default, unless the user explicitly // overrides the endStream option on the request method, the endStream // option will be defaulted to true when these methods are used. const kNoPayloadMethods = new SafeSet([ HTTP2_METHOD_DELETE, HTTP2_METHOD_GET, HTTP2_METHOD_HEAD, ]); // The following ArrayBuffer instances are used to share memory more efficiently // with the native binding side for a number of methods. These are not intended // to be used directly by users in any way. The ArrayBuffers are created on // the native side with values that are filled in on demand, the js code then // reads those values out. The set of IDX constants that follow identify the // relevant data positions within these buffers. // NOTE: `op_http2_http_state()` returns Uint32Array/Float32Arrays that alias // thread-local Rust buffers used by the native HTTP/2 binding. When this // module is first evaluated during snapshot creation, the typed array's // external backing pointer captured at snapshot time no longer references the // runtime process's thread-local memory after deserialization, so writes from // JS never reach Rust. Re-bind the buffers on first runtime use to point at // the current process's thread-local memory. let { settingsBuffer, optionsBuffer, sessionState, streamState } = op_http2_http_state(); let httpStateRuntimeReady = false; function ensureHttpStateBuffers() { if (httpStateRuntimeReady) return; httpStateRuntimeReady = true; ({ settingsBuffer, optionsBuffer, sessionState, streamState } = op_http2_http_state()); } const IDX_SETTINGS_HEADER_TABLE_SIZE = 0; const IDX_SETTINGS_ENABLE_PUSH = 1; const IDX_SETTINGS_INITIAL_WINDOW_SIZE = 2; const IDX_SETTINGS_MAX_FRAME_SIZE = 3; const IDX_SETTINGS_MAX_CONCURRENT_STREAMS = 4; const IDX_SETTINGS_MAX_HEADER_LIST_SIZE = 5; const IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL = 6; const IDX_SETTINGS_FLAGS = 7; // Maximum number of allowed additional settings const MAX_ADDITIONAL_SETTINGS = 10; const IDX_SESSION_STATE_EFFECTIVE_LOCAL_WINDOW_SIZE = 0; const IDX_SESSION_STATE_EFFECTIVE_RECV_DATA_LENGTH = 1; const IDX_SESSION_STATE_NEXT_STREAM_ID = 2; const IDX_SESSION_STATE_LOCAL_WINDOW_SIZE = 3; const IDX_SESSION_STATE_LAST_PROC_STREAM_ID = 4; const IDX_SESSION_STATE_REMOTE_WINDOW_SIZE = 5; const IDX_SESSION_STATE_OUTBOUND_QUEUE_SIZE = 6; const IDX_SESSION_STATE_HD_DEFLATE_DYNAMIC_TABLE_SIZE = 7; const IDX_SESSION_STATE_HD_INFLATE_DYNAMIC_TABLE_SIZE = 8; const IDX_STREAM_STATE = 0; const IDX_STREAM_STATE_WEIGHT = 1; const IDX_STREAM_STATE_SUM_DEPENDENCY_WEIGHT = 2; const IDX_STREAM_STATE_LOCAL_CLOSE = 3; const IDX_STREAM_STATE_REMOTE_CLOSE = 4; const IDX_STREAM_STATE_LOCAL_WINDOW_SIZE = 5; const IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE = 0; const IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS = 1; const IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH = 2; const IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS = 3; const IDX_OPTIONS_PADDING_STRATEGY = 4; const IDX_OPTIONS_MAX_HEADER_LIST_PAIRS = 5; const IDX_OPTIONS_MAX_OUTSTANDING_PINGS = 6; const IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS = 7; const IDX_OPTIONS_MAX_SESSION_MEMORY = 8; const IDX_OPTIONS_MAX_SETTINGS = 9; const IDX_OPTIONS_STREAM_RESET_RATE = 10; const IDX_OPTIONS_STREAM_RESET_BURST = 11; const IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION = 12; const IDX_OPTIONS_FLAGS = 13; function updateOptionsBuffer(options) { ensureHttpStateBuffers(); let flags = 0; if (typeof options.maxDeflateDynamicTableSize === "number") { flags |= 1 << IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE; optionsBuffer[IDX_OPTIONS_MAX_DEFLATE_DYNAMIC_TABLE_SIZE] = options.maxDeflateDynamicTableSize; } if (typeof options.maxReservedRemoteStreams === "number") { flags |= 1 << IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS; optionsBuffer[IDX_OPTIONS_MAX_RESERVED_REMOTE_STREAMS] = options.maxReservedRemoteStreams; } if (typeof options.maxSendHeaderBlockLength === "number") { flags |= 1 << IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH; optionsBuffer[IDX_OPTIONS_MAX_SEND_HEADER_BLOCK_LENGTH] = options.maxSendHeaderBlockLength; } if (typeof options.peerMaxConcurrentStreams === "number") { flags |= 1 << IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS; optionsBuffer[IDX_OPTIONS_PEER_MAX_CONCURRENT_STREAMS] = options.peerMaxConcurrentStreams; } if (typeof options.paddingStrategy === "number") { flags |= 1 << IDX_OPTIONS_PADDING_STRATEGY; optionsBuffer[IDX_OPTIONS_PADDING_STRATEGY] = options.paddingStrategy; } if (typeof options.maxHeaderListPairs === "number") { flags |= 1 << IDX_OPTIONS_MAX_HEADER_LIST_PAIRS; optionsBuffer[IDX_OPTIONS_MAX_HEADER_LIST_PAIRS] = options.maxHeaderListPairs; } if (typeof options.maxOutstandingPings === "number") { flags |= 1 << IDX_OPTIONS_MAX_OUTSTANDING_PINGS; optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_PINGS] = options.maxOutstandingPings; } if (typeof options.maxOutstandingSettings === "number") { flags |= 1 << IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS; optionsBuffer[IDX_OPTIONS_MAX_OUTSTANDING_SETTINGS] = MathMax( 1, options.maxOutstandingSettings, ); } if (typeof options.maxSessionMemory === "number") { flags |= 1 << IDX_OPTIONS_MAX_SESSION_MEMORY; optionsBuffer[IDX_OPTIONS_MAX_SESSION_MEMORY] = MathMax( 1, options.maxSessionMemory, ); } if (typeof options.maxSettings === "number") { flags |= 1 << IDX_OPTIONS_MAX_SETTINGS; optionsBuffer[IDX_OPTIONS_MAX_SETTINGS] = MathMax(1, options.maxSettings); } if (typeof options.streamResetRate === "number") { flags |= 1 << IDX_OPTIONS_STREAM_RESET_RATE; optionsBuffer[IDX_OPTIONS_STREAM_RESET_RATE] = MathMax( 1, options.streamResetRate, ); } if (typeof options.streamResetBurst === "number") { flags |= 1 << IDX_OPTIONS_STREAM_RESET_BURST; optionsBuffer[IDX_OPTIONS_STREAM_RESET_BURST] = MathMax( 1, options.streamResetBurst, ); } if (typeof options.strictFieldWhitespaceValidation === "boolean") { flags |= 1 << IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION; optionsBuffer[IDX_OPTIONS_STRICT_HTTP_FIELD_WHITESPACE_VALIDATION] = options.strictFieldWhitespaceValidation === true ? 0 : 1; } optionsBuffer[IDX_OPTIONS_FLAGS] = flags; } function addCustomSettingsToObj() { ensureHttpStateBuffers(); const toRet = {}; const num = settingsBuffer[IDX_SETTINGS_FLAGS + 1]; for (let i = 0; i < num; i++) { // deno-lint-ignore deno-internal/prefer-primordials toRet[settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 1].toString()] = Number(settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 2]); } return toRet; } function getDefaultSettings() { // Return HTTP/2 default settings as defined in RFC 7540 // https://httpwg.org/specs/rfc7540.html#SettingValues return { __proto__: null, headerTableSize: 4096, enablePush: true, initialWindowSize: 65535, // 2^16 - 1 maxFrameSize: 16384, // 2^14 maxConcurrentStreams: 4294967295, // 2^32 - 1 (unlimited) maxHeaderListSize: 65535, // 2^16 - 1 maxHeaderSize: 65535, // alias for maxHeaderListSize enableConnectProtocol: false, }; } // Remote is a boolean. true to fetch remote settings, false to fetch local. // this is only called internally function getSettings(session, remote) { ensureHttpStateBuffers(); if (remote) { session.remoteSettings(); } else { session.localSettings(); } const toRet = { headerTableSize: settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE], enablePush: !!settingsBuffer[IDX_SETTINGS_ENABLE_PUSH], initialWindowSize: settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE], maxFrameSize: settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE], maxConcurrentStreams: settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS], maxHeaderListSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE], maxHeaderSize: settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE], enableConnectProtocol: !!settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL], }; if (settingsBuffer[IDX_SETTINGS_FLAGS + 1]) { toRet.customSettings = addCustomSettingsToObj(); } return toRet; } function updateSettingsBuffer(settings) { ensureHttpStateBuffers(); let flags = 0; let numCustomSettings = 0; if (typeof settings.customSettings === "object") { const customSettings = settings.customSettings; for (const setting in customSettings) { const val = customSettings[setting]; if (typeof val === "number") { let set = false; const nsetting = Number(setting); if ( NumberIsNaN(nsetting) || typeof nsetting !== "number" || 0 >= nsetting || nsetting > 0xffff ) { throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError( "Range Error", nsetting, 0, 0xffff, ); } if ( NumberIsNaN(val) || typeof val !== "number" || 0 >= val || val > 0xffffffff ) { throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError( "Range Error", val, 0, 0xffffffff, ); } if (nsetting < IDX_SETTINGS_FLAGS) { set = true; switch (nsetting) { case IDX_SETTINGS_HEADER_TABLE_SIZE: flags |= 1 << IDX_SETTINGS_HEADER_TABLE_SIZE; settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = val; break; case IDX_SETTINGS_ENABLE_PUSH: flags |= 1 << IDX_SETTINGS_ENABLE_PUSH; settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = val; break; case IDX_SETTINGS_INITIAL_WINDOW_SIZE: flags |= 1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE; settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] = val; break; case IDX_SETTINGS_MAX_FRAME_SIZE: flags |= 1 << IDX_SETTINGS_MAX_FRAME_SIZE; settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE] = val; break; case IDX_SETTINGS_MAX_CONCURRENT_STREAMS: flags |= 1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS; settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] = val; break; case IDX_SETTINGS_MAX_HEADER_LIST_SIZE: flags |= 1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE; settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] = val; break; case IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL: flags |= 1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL; settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] = val; break; default: set = false; break; } } if (!set) { // not supported let i = 0; while (i < numCustomSettings) { if ( settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 1] === nsetting ) { settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * i + 2] = val; break; } i++; } if (i === numCustomSettings) { if (numCustomSettings === MAX_ADDITIONAL_SETTINGS) { throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); } settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 1] = nsetting; settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 2] = val; numCustomSettings++; } } } } } settingsBuffer[IDX_SETTINGS_FLAGS + 1] = numCustomSettings; if (typeof settings.headerTableSize === "number") { flags |= 1 << IDX_SETTINGS_HEADER_TABLE_SIZE; settingsBuffer[IDX_SETTINGS_HEADER_TABLE_SIZE] = settings.headerTableSize; } if (typeof settings.maxConcurrentStreams === "number") { flags |= 1 << IDX_SETTINGS_MAX_CONCURRENT_STREAMS; settingsBuffer[IDX_SETTINGS_MAX_CONCURRENT_STREAMS] = settings.maxConcurrentStreams; } if (typeof settings.initialWindowSize === "number") { flags |= 1 << IDX_SETTINGS_INITIAL_WINDOW_SIZE; settingsBuffer[IDX_SETTINGS_INITIAL_WINDOW_SIZE] = settings.initialWindowSize; } if (typeof settings.maxFrameSize === "number") { flags |= 1 << IDX_SETTINGS_MAX_FRAME_SIZE; settingsBuffer[IDX_SETTINGS_MAX_FRAME_SIZE] = settings.maxFrameSize; } if ( typeof settings.maxHeaderListSize === "number" || typeof settings.maxHeaderSize === "number" ) { flags |= 1 << IDX_SETTINGS_MAX_HEADER_LIST_SIZE; if ( settings.maxHeaderSize !== undefined && (settings.maxHeaderSize !== settings.maxHeaderListSize) ) { lazyProcess().default.emitWarning( "settings.maxHeaderSize overwrite settings.maxHeaderListSize", ); settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] = settings.maxHeaderSize; } else { settingsBuffer[IDX_SETTINGS_MAX_HEADER_LIST_SIZE] = settings.maxHeaderListSize; } } if (typeof settings.enablePush === "boolean") { flags |= 1 << IDX_SETTINGS_ENABLE_PUSH; settingsBuffer[IDX_SETTINGS_ENABLE_PUSH] = Number(settings.enablePush); } if (typeof settings.enableConnectProtocol === "boolean") { flags |= 1 << IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL; settingsBuffer[IDX_SETTINGS_ENABLE_CONNECT_PROTOCOL] = Number( settings.enableConnectProtocol, ); } settingsBuffer[IDX_SETTINGS_FLAGS] = flags; } function remoteCustomSettingsToBuffer(remoteCustomSettings) { ensureHttpStateBuffers(); if (remoteCustomSettings.length > MAX_ADDITIONAL_SETTINGS) { throw new ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS(); } let numCustomSettings = 0; for (let i = 0; i < remoteCustomSettings.length; i++) { const nsetting = remoteCustomSettings[i]; if ( typeof nsetting === "number" && nsetting <= 0xffff && nsetting >= 0 ) { settingsBuffer[IDX_SETTINGS_FLAGS + 1 + 2 * numCustomSettings + 1] = nsetting; numCustomSettings++; } else { throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError( "Range Error", nsetting, 0, 0xffff, ); } } settingsBuffer[IDX_SETTINGS_FLAGS + 1] = numCustomSettings; } function getSessionState(session) { // Get state directly from session instead of using shared buffers // to avoid thread-local storage issues with snapshot serialization const state = session.getState(); return { effectiveLocalWindowSize: state.effectiveLocalWindowSize, effectiveRecvDataLength: state.effectiveRecvDataLength, nextStreamID: state.nextStreamId, localWindowSize: state.localWindowSize, lastProcStreamID: state.lastProcStreamId, remoteWindowSize: state.remoteWindowSize, outboundQueueSize: state.outboundQueueSize, deflateDynamicTableSize: state.hdDeflateDynamicTableSize, inflateDynamicTableSize: state.hdInflateDynamicTableSize, }; } function getStreamState(stream) { // Get state directly from stream instead of using shared buffers // to avoid thread-local storage issues with snapshot serialization const state = stream.getState(); return { state: state.state, weight: state.weight, sumDependencyWeight: state.sumDependencyWeight, localClose: state.localClose, remoteClose: state.remoteClose, localWindowSize: state.localWindowSize, }; } function isIllegalConnectionSpecificHeader(name, value) { switch (name) { case HTTP2_HEADER_CONNECTION: case HTTP2_HEADER_UPGRADE: case HTTP2_HEADER_HTTP2_SETTINGS: case HTTP2_HEADER_KEEP_ALIVE: case HTTP2_HEADER_PROXY_CONNECTION: case HTTP2_HEADER_TRANSFER_ENCODING: return true; case HTTP2_HEADER_TE: return value !== "trailers"; default: return false; } } const assertValidPseudoHeader = hideStackFrames((key) => { if (!kValidPseudoHeaders.has(key)) { throw new ERR_HTTP2_INVALID_PSEUDOHEADER(key); } }); const assertValidPseudoHeaderResponse = hideStackFrames((key) => { if (key !== ":status") { throw new ERR_HTTP2_INVALID_PSEUDOHEADER(key); } }); const assertValidPseudoHeaderTrailer = hideStackFrames((key) => { throw new ERR_HTTP2_INVALID_PSEUDOHEADER(key); }); /** * Takes a request headers array, validates it and sets defaults, and returns * the resulting headers in NgHeaders string list format. * @returns {object} */ function prepareRequestHeadersArray(headers, session) { let method; let scheme; let authority; let path; let protocol; // Extract the key psuedo header values from the headers array for (let i = 0; i < headers.length; i += 2) { if (headers[i][0] !== ":") { continue; } const header = StringPrototypeToLowerCase(headers[i]); const value = headers[i + 1]; if (header === HTTP2_HEADER_METHOD) { method = value; } else if (header === HTTP2_HEADER_SCHEME) { scheme = value; } else if (header === HTTP2_HEADER_AUTHORITY) { authority = value; } else if (header === HTTP2_HEADER_PATH) { path = value; } else if (header === HTTP2_HEADER_PROTOCOL) { protocol = value; } } // We then build an array of any missing pseudo headers, to prepend // default values to the given header array: const additionalPsuedoHeaders = []; if (method === undefined) { method = HTTP2_METHOD_GET; ArrayPrototypePush(additionalPsuedoHeaders, HTTP2_HEADER_METHOD, method); } const connect = method === HTTP2_METHOD_CONNECT; if (!connect || protocol !== undefined) { if (authority === undefined && headers[HTTP2_HEADER_HOST] === undefined) { authority = session[kAuthority]; ArrayPrototypePush( additionalPsuedoHeaders, HTTP2_HEADER_AUTHORITY, authority, ); } if (scheme === undefined) { scheme = StringPrototypeSlice(session[kProtocol], 0, -1); ArrayPrototypePush(additionalPsuedoHeaders, HTTP2_HEADER_SCHEME, scheme); } if (path === undefined) { ArrayPrototypePush(additionalPsuedoHeaders, HTTP2_HEADER_PATH, "/"); } } else { if (authority === undefined) { throw new ERR_HTTP2_CONNECT_AUTHORITY(); } if (scheme !== undefined) { throw new ERR_HTTP2_CONNECT_SCHEME(); } if (path !== undefined) { throw new ERR_HTTP2_CONNECT_PATH(); } } const rawHeaders = additionalPsuedoHeaders.length ? ArrayPrototypeConcat(additionalPsuedoHeaders, headers) : headers; if (headers[kSensitiveHeaders] !== undefined) { rawHeaders[kSensitiveHeaders] = headers[kSensitiveHeaders]; } const headersList = buildNgHeaderString( rawHeaders, assertValidPseudoHeader, session[kStrictSingleValueFields], ); return { rawHeaders, headersList, scheme, authority: authority ?? headers[HTTP2_HEADER_HOST], method, }; } /** * Takes a request headers object, validates it and sets defaults, and returns * the resulting headers in object format and NgHeaders string list format. * @returns {object} */ function prepareRequestHeadersObject(headers, session) { const headersObject = ObjectAssign({ __proto__: null }, headers); if (headersObject[HTTP2_HEADER_METHOD] === undefined) { headersObject[HTTP2_HEADER_METHOD] = HTTP2_METHOD_GET; } const connect = headersObject[HTTP2_HEADER_METHOD] === HTTP2_METHOD_CONNECT; if (!connect || headersObject[HTTP2_HEADER_PROTOCOL] !== undefined) { if (getAuthority(headersObject) === undefined) { headersObject[HTTP2_HEADER_AUTHORITY] = session[kAuthority]; } if (headersObject[HTTP2_HEADER_SCHEME] === undefined) { headersObject[HTTP2_HEADER_SCHEME] = StringPrototypeSlice( session[kProtocol], 0, -1, ); } if (headersObject[HTTP2_HEADER_PATH] === undefined) { headersObject[HTTP2_HEADER_PATH] = "/"; } } else { if (headersObject[HTTP2_HEADER_AUTHORITY] === undefined) { throw new ERR_HTTP2_CONNECT_AUTHORITY(); } if (headersObject[HTTP2_HEADER_SCHEME] !== undefined) { throw new ERR_HTTP2_CONNECT_SCHEME(); } if (headersObject[HTTP2_HEADER_PATH] !== undefined) { throw new ERR_HTTP2_CONNECT_PATH(); } } const headersList = buildNgHeaderString( headersObject, assertValidPseudoHeader, session[kStrictSingleValueFields], ); return { headersObject, headersList, scheme: headersObject[HTTP2_HEADER_SCHEME], authority: getAuthority(headersObject), method: headersObject[HTTP2_HEADER_METHOD], }; } const emptyArray = []; const kNeverIndexFlag = StringFromCharCode(NGHTTP2_NV_FLAG_NO_INDEX); const kNoHeaderFlags = StringFromCharCode(NGHTTP2_NV_FLAG_NONE); const kInvalidHeaderValueByte = StringFromCharCode(1); // The native binding converts this serialized string to Latin-1 and uses a // zero byte as the field delimiter. Replace value code units whose low byte is // zero with another invalid HTTP/2 header byte so nghttp2 still rejects or // drops the header through its normal path without interpreting value data as // another serialized field. function escapeNgHeaderValueZeroBytes(value) { let escaped = ""; let start = 0; let changed = false; for (let i = 0; i < value.length; ++i) { if ((StringPrototypeCharCodeAt(value, i) & 0xff) !== 0) { continue; } changed = true; escaped += StringPrototypeSlice(value, start, i) + kInvalidHeaderValueByte; start = i + 1; } return changed ? escaped + StringPrototypeSlice(value, start) : value; } /** * Builds an NgHeader string + header count value, validating the header key * format, rejecting illegal header configurations, and marking sensitive headers * that should not be indexed en route. This takes either a flat map of * raw headers ([k1, v1, k2, v2]) or a header object ({ k1: v1, k2: [v2, v3] }). * @returns {[string, number]} */ function buildNgHeaderString( arrayOrMap, assertValuePseudoHeader = assertValidPseudoHeader, strictSingleValueFields?, ) { const checkIsHttpToken = lazyHttpCommon()._checkIsHttpToken; let headers = ""; let pseudoHeaders = ""; let count = 0; const singles = new SafeSet(); const sensitiveHeaders = arrayOrMap[kSensitiveHeaders] || emptyArray; const neverIndex = ArrayPrototypeMap( sensitiveHeaders, (v) => StringPrototypeToLowerCase(v), ); function processHeader(key, value) { key = StringPrototypeToLowerCase(key); const isSingleValueHeader = strictSingleValueFields && kSingleValueHeaders.has(key); let isArray = ArrayIsArray(value); if (isArray) { switch (value.length) { case 0: return; case 1: value = String(value[0]); isArray = false; break; default: if (isSingleValueHeader) { throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key); } } } else { value = String(value); } if (isSingleValueHeader) { if (singles.has(key)) { throw new ERR_HTTP2_HEADER_SINGLE_VALUE(key); } singles.add(key); } const flags = ArrayPrototypeIncludes(neverIndex, key) ? kNeverIndexFlag : kNoHeaderFlags; if (key[0] === ":") { const err = assertValuePseudoHeader(key); if (err !== undefined) { throw err; } value = escapeNgHeaderValueZeroBytes(value); pseudoHeaders += `${key}\0${value}\0${flags}`; count++; return; } if (!checkIsHttpToken(key)) { throw new ERR_INVALID_HTTP_TOKEN("Header name", key); } if (isIllegalConnectionSpecificHeader(key, value)) { throw new ERR_HTTP2_INVALID_CONNECTION_HEADERS(key); } if (isArray) { for (let j = 0; j < value.length; ++j) { const val = escapeNgHeaderValueZeroBytes(String(value[j])); headers += `${key}\0${val}\0${flags}`; } count += value.length; return; } value = escapeNgHeaderValueZeroBytes(value); headers += `${key}\0${value}\0${flags}`; count++; } if (ArrayIsArray(arrayOrMap)) { for (let i = 0; i < arrayOrMap.length; i += 2) { const key = arrayOrMap[i]; const value = arrayOrMap[i + 1]; if (value === undefined || key === "") { continue; } processHeader(key, value); } } else { const keys = ObjectKeys(arrayOrMap); for (let i = 0; i < keys.length; ++i) { const key = keys[i]; const value = arrayOrMap[key]; if (value === undefined || key === "") { continue; } processHeader(key, value); } } return [pseudoHeaders + headers, count]; } // Messages for the custom error codes that nghttp2 callbacks can hand back // from the native binding (see custom_recv_error_code in ext/node/ops/http2). // Mirrors the templates in Node's lib/internal/errors.js. const kCustomErrorMessages = { __proto__: null, ERR_HTTP2_TOO_MANY_INVALID_FRAMES: "Too many invalid HTTP/2 frames", }; class NghttpError extends Error { constructor(integerCode, customErrorCode) { super( customErrorCode ? kCustomErrorMessages[customErrorCode] ?? customErrorCode : op_http2_error_string(integerCode), ); this.code = customErrorCode || "ERR_HTTP2_ERROR"; this.errno = integerCode; } toString() { return `${this.name} [${this.code}]: ${this.message}`; } } const assertIsObject = hideStackFrames((value, name, types) => { if ( value !== undefined && (value === null || typeof value !== "object" || ArrayIsArray(value)) ) { throw new ERR_INVALID_ARG_TYPE( name, types || "Object", value, ); } }); const assertIsArray = hideStackFrames((value, name, types) => { if ( value !== undefined && (value === null || !ArrayIsArray(value)) ) { throw new ERR_INVALID_ARG_TYPE( name, types || "Array", value, ); } }); const assertWithinRange = hideStackFrames( (name, value, min = 0, max = Infinity) => { if ( value !== undefined && (typeof value !== "number" || value < min || value > max) ) { throw new ERR_HTTP2_INVALID_SETTING_VALUE.RangeError.HideStackFramesError( name, value, min, max, ); } }, ); function toHeaderObject(headers, sensitiveHeaders) { const obj = { __proto__: null }; for (let n = 0; n < headers.length; n += 2) { const name = headers[n]; let value = headers[n + 1]; if (name === HTTP2_HEADER_STATUS) { value |= 0; } const existing = obj[name]; if (existing === undefined) { obj[name] = name === HTTP2_HEADER_SET_COOKIE ? [value] : value; } else if (!kSingleValueHeaders.has(name)) { switch (name) { case HTTP2_HEADER_COOKIE: // https://tools.ietf.org/html/rfc7540#section-8.1.2.5 // "...If there are multiple Cookie header fields after decompression, // these MUST be concatenated into a single octet string using the // two-octet delimiter of 0x3B, 0x20 (the ASCII string "; ") before // being passed into a non-HTTP/2 context." obj[name] = `${existing}; ${value}`; break; case HTTP2_HEADER_SET_COOKIE: // https://tools.ietf.org/html/rfc7230#section-3.2.2 // "Note: In practice, the "Set-Cookie" header field ([RFC6265]) often // appears multiple times in a response message and does not use the // list syntax, violating the above requirements on multiple header // fields with the same name. Since it cannot be combined into a // single field-value, recipients ought to handle "Set-Cookie" as a // special case while processing header fields." ArrayPrototypePush(existing, value); break; default: // https://tools.ietf.org/html/rfc7230#section-3.2.2 // "A recipient MAY combine multiple header fields with the same field // name into one "field-name: field-value" pair, without changing the // semantics of the message, by appending each subsequent field value // to the combined field value in order, separated by a comma." obj[name] = `${existing}, ${value}`; break; } } } obj[kSensitiveHeaders] = sensitiveHeaders; return obj; } function isPayloadMeaningless(method) { return kNoPayloadMethods.has(method); } function sessionName(type) { switch (type) { case NGHTTP2_SESSION_CLIENT: return "client"; case NGHTTP2_SESSION_SERVER: return "server"; default: return "<invalid>"; } } function getAuthority(headers) { // For non-CONNECT requests, HTTP/2 allows either :authority // or Host to be used equivalently. The first is preferred // when making HTTP/2 requests, and the latter is preferred // when converting from an HTTP/1 message. if (headers[HTTP2_HEADER_AUTHORITY] !== undefined) { return headers[HTTP2_HEADER_AUTHORITY]; } if (headers[HTTP2_HEADER_HOST] !== undefined) { return headers[HTTP2_HEADER_HOST]; } } return { assertIsArray, assertIsObject, assertValidPseudoHeader, assertValidPseudoHeaderResponse, assertValidPseudoHeaderTrailer, assertWithinRange, buildNgHeaderString, getAuthority, getDefaultSettings, getSessionState, getSettings, getStreamState, isPayloadMeaningless, kAuthority, kProtocol, kProxySocket, kRequest, kSensitiveHeaders, kSocket, kStrictSingleValueFields, MAX_ADDITIONAL_SETTINGS, NghttpError, prepareRequestHeadersArray, prepareRequestHeadersObject, remoteCustomSettingsToBuffer, sessionName, toHeaderObject, updateOptionsBuffer, updateSettingsBuffer, default: { assertIsObject, assertIsArray, assertValidPseudoHeader, assertValidPseudoHeaderResponse, assertValidPseudoHeaderTrailer, assertWithinRange, buildNgHeaderString, getAuthority, getDefaultSettings, getSessionState, getSettings, getStreamState, isPayloadMeaningless, kAuthority, kSensitiveHeaders, kSocket, kStrictSingleValueFields, kProtocol, kProxySocket, kRequest, MAX_ADDITIONAL_SETTINGS, NghttpError, prepareRequestHeadersArray, prepareRequestHeadersObject, remoteCustomSettingsToBuffer, sessionName, toHeaderObject, updateOptionsBuffer, updateSettingsBuffer, }, }; })();