diff --git a/benchmark/http/check_invalid_header_char.js b/benchmark/http/check_invalid_header_char.js index bd250cd5fe0206..62853272b34b50 100644 --- a/benchmark/http/check_invalid_header_char.js +++ b/benchmark/http/check_invalid_header_char.js @@ -1,6 +1,7 @@ 'use strict'; const common = require('../common.js'); +// TODO: benchmark has access to internal modules? const _checkInvalidHeaderChar = require('_http_common')._checkInvalidHeaderChar; const groupedInputs = { diff --git a/benchmark/http/check_is_http_token.js b/benchmark/http/check_is_http_token.js index 2137a73e547d60..0b743ad860e17c 100644 --- a/benchmark/http/check_is_http_token.js +++ b/benchmark/http/check_is_http_token.js @@ -1,6 +1,7 @@ 'use strict'; const common = require('../common.js'); +// TODO: benchmark has access to internal modules? const _checkIsHttpToken = require('_http_common')._checkIsHttpToken; const bench = common.createBenchmark(main, { diff --git a/benchmark/http/set_header.js b/benchmark/http/set_header.js index f2236696f1c7df..0938c1e9cc2198 100644 --- a/benchmark/http/set_header.js +++ b/benchmark/http/set_header.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common.js'); -const { OutgoingMessage } = require('_http_outgoing'); +const { OutgoingMessage } = require('http'); const bench = common.createBenchmark(main, { value: [ diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index c0f762c54860a6..fa27e5429523ca 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -3955,6 +3955,21 @@ Type: Documentation-only The support for priority signaling has been deprecated in the [RFC 9113][], and will be removed in future versions of Node.js. +### DEP0195: `require('node:_http_*')` + + + +Type: Runtime + +The `node:_http_agent`, `node:_http_client`, `node:_http_common`, `node:_http_incoming`, +`node:_http_outgoing` and `node:_http_server` modules are deprecated as they should be considered +an internal nodejs implementation rather than a public facing API, use `node:http` instead. + [DEP0142]: #dep0142-repl_builtinlibs [NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf [RFC 6066]: https://tools.ietf.org/html/rfc6066#section-3 diff --git a/lib/_http_agent.js b/lib/_http_agent.js index e8d31d83c26c51..f54b029cc267e0 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -21,527 +21,12 @@ 'use strict'; -const { - NumberParseInt, - ObjectKeys, - ObjectSetPrototypeOf, - ObjectValues, - Symbol, -} = primordials; - -const net = require('net'); -const EventEmitter = require('events'); -let debug = require('internal/util/debuglog').debuglog('http', (fn) => { - debug = fn; -}); -const { AsyncResource } = require('async_hooks'); -const { async_id_symbol } = require('internal/async_hooks').symbols; -const { - kEmptyObject, - once, -} = require('internal/util'); -const { - validateNumber, - validateOneOf, - validateString, -} = require('internal/validators'); - -const kOnKeylog = Symbol('onkeylog'); -const kRequestOptions = Symbol('requestOptions'); -const kRequestAsyncResource = Symbol('requestAsyncResource'); - -// TODO(jazelly): make this configurable -const HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER = 1000; -// New Agent code. - -// The largest departure from the previous implementation is that -// an Agent instance holds connections for a variable number of host:ports. -// Surprisingly, this is still API compatible as far as third parties are -// concerned. The only code that really notices the difference is the -// request object. - -// Another departure is that all code related to HTTP parsing is in -// ClientRequest.onSocket(). The Agent is now *strictly* -// concerned with managing a connection pool. - -class ReusedHandle { - constructor(type, handle) { - this.type = type; - this.handle = handle; - } -} - -function freeSocketErrorListener(err) { - const socket = this; - debug('SOCKET ERROR on FREE socket:', err.message, err.stack); - socket.destroy(); - socket.emit('agentRemove'); -} - -function Agent(options) { - if (!(this instanceof Agent)) - return new Agent(options); - - EventEmitter.call(this); - - this.defaultPort = 80; - this.protocol = 'http:'; - - this.options = { __proto__: null, ...options }; - - if (this.options.noDelay === undefined) - this.options.noDelay = true; - - // Don't confuse net and make it think that we're connecting to a pipe - this.options.path = null; - this.requests = { __proto__: null }; - this.sockets = { __proto__: null }; - this.freeSockets = { __proto__: null }; - this.keepAliveMsecs = this.options.keepAliveMsecs || 1000; - this.keepAlive = this.options.keepAlive || false; - this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets; - this.maxFreeSockets = this.options.maxFreeSockets || 256; - this.scheduling = this.options.scheduling || 'lifo'; - this.maxTotalSockets = this.options.maxTotalSockets; - this.totalSocketCount = 0; - - validateOneOf(this.scheduling, 'scheduling', ['fifo', 'lifo']); - - if (this.maxTotalSockets !== undefined) { - validateNumber(this.maxTotalSockets, 'maxTotalSockets', 1); - } else { - this.maxTotalSockets = Infinity; - } - - this.on('free', (socket, options) => { - const name = this.getName(options); - debug('agent.on(free)', name); - - // TODO(ronag): socket.destroy(err) might have been called - // before coming here and have an 'error' scheduled. In the - // case of socket.destroy() below this 'error' has no handler - // and could cause unhandled exception. - - if (!socket.writable) { - socket.destroy(); - return; - } - - const requests = this.requests[name]; - if (requests?.length) { - const req = requests.shift(); - const reqAsyncRes = req[kRequestAsyncResource]; - if (reqAsyncRes) { - // Run request within the original async context. - reqAsyncRes.runInAsyncScope(() => { - asyncResetHandle(socket); - setRequestSocket(this, req, socket); - }); - req[kRequestAsyncResource] = null; - } else { - setRequestSocket(this, req, socket); - } - if (requests.length === 0) { - delete this.requests[name]; - } - return; - } - - // If there are no pending requests, then put it in - // the freeSockets pool, but only if we're allowed to do so. - const req = socket._httpMessage; - if (!req || !req.shouldKeepAlive || !this.keepAlive) { - socket.destroy(); - return; - } - - const freeSockets = this.freeSockets[name] || []; - const freeLen = freeSockets.length; - let count = freeLen; - if (this.sockets[name]) - count += this.sockets[name].length; - - if (this.totalSocketCount > this.maxTotalSockets || - count > this.maxSockets || - freeLen >= this.maxFreeSockets || - !this.keepSocketAlive(socket)) { - socket.destroy(); - return; - } - - this.freeSockets[name] = freeSockets; - socket[async_id_symbol] = -1; - socket._httpMessage = null; - this.removeSocket(socket, options); - - socket.once('error', freeSocketErrorListener); - freeSockets.push(socket); - }); - - // Don't emit keylog events unless there is a listener for them. - this.on('newListener', maybeEnableKeylog); -} -ObjectSetPrototypeOf(Agent.prototype, EventEmitter.prototype); -ObjectSetPrototypeOf(Agent, EventEmitter); - -function maybeEnableKeylog(eventName) { - if (eventName === 'keylog') { - this.removeListener('newListener', maybeEnableKeylog); - // Future sockets will listen on keylog at creation. - const agent = this; - this[kOnKeylog] = function onkeylog(keylog) { - agent.emit('keylog', keylog, this); - }; - // Existing sockets will start listening on keylog now. - const sockets = ObjectValues(this.sockets); - for (let i = 0; i < sockets.length; i++) { - sockets[i].on('keylog', this[kOnKeylog]); - } - } -} - -Agent.defaultMaxSockets = Infinity; - -Agent.prototype.createConnection = net.createConnection; - -// Get the key for a given set of request options -Agent.prototype.getName = function getName(options = kEmptyObject) { - let name = options.host || 'localhost'; - - name += ':'; - if (options.port) - name += options.port; - - name += ':'; - if (options.localAddress) - name += options.localAddress; - - // Pacify parallel/test-http-agent-getname by only appending - // the ':' when options.family is set. - if (options.family === 4 || options.family === 6) - name += `:${options.family}`; - - if (options.socketPath) - name += `:${options.socketPath}`; - - return name; -}; - -Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, - localAddress/* legacy */) { - // Legacy API: addRequest(req, host, port, localAddress) - if (typeof options === 'string') { - options = { - __proto__: null, - host: options, - port, - localAddress, - }; - } - - options = { __proto__: null, ...options, ...this.options }; - if (options.socketPath) - options.path = options.socketPath; - - normalizeServerName(options, req); - - const name = this.getName(options); - this.sockets[name] ||= []; - - const freeSockets = this.freeSockets[name]; - let socket; - if (freeSockets) { - while (freeSockets.length && freeSockets[0].destroyed) { - freeSockets.shift(); - } - socket = this.scheduling === 'fifo' ? - freeSockets.shift() : - freeSockets.pop(); - if (!freeSockets.length) - delete this.freeSockets[name]; - } - - const freeLen = freeSockets ? freeSockets.length : 0; - const sockLen = freeLen + this.sockets[name].length; - - if (socket) { - asyncResetHandle(socket); - this.reuseSocket(socket, req); - setRequestSocket(this, req, socket); - this.sockets[name].push(socket); - } else if (sockLen < this.maxSockets && - this.totalSocketCount < this.maxTotalSockets) { - debug('call onSocket', sockLen, freeLen); - // If we are under maxSockets create a new one. - this.createSocket(req, options, (err, socket) => { - if (err) - req.onSocket(socket, err); - else - setRequestSocket(this, req, socket); - }); - } else { - debug('wait for socket'); - // We are over limit so we'll add it to the queue. - this.requests[name] ||= []; - - // Used to create sockets for pending requests from different origin - req[kRequestOptions] = options; - // Used to capture the original async context. - req[kRequestAsyncResource] = new AsyncResource('QueuedRequest'); - - this.requests[name].push(req); - } -}; - -Agent.prototype.createSocket = function createSocket(req, options, cb) { - options = { __proto__: null, ...options, ...this.options }; - if (options.socketPath) - options.path = options.socketPath; - - normalizeServerName(options, req); - - const name = this.getName(options); - options._agentKey = name; - - debug('createConnection', name, options); - options.encoding = null; - - const oncreate = once((err, s) => { - if (err) - return cb(err); - this.sockets[name] ||= []; - this.sockets[name].push(s); - this.totalSocketCount++; - debug('sockets', name, this.sockets[name].length, this.totalSocketCount); - installListeners(this, s, options); - cb(null, s); - }); - // When keepAlive is true, pass the related options to createConnection - if (this.keepAlive) { - options.keepAlive = this.keepAlive; - options.keepAliveInitialDelay = this.keepAliveMsecs; - } - const newSocket = this.createConnection(options, oncreate); - if (newSocket) - oncreate(null, newSocket); -}; - -function normalizeServerName(options, req) { - if (!options.servername && options.servername !== '') - options.servername = calculateServerName(options, req); -} - -function calculateServerName(options, req) { - let servername = options.host; - const hostHeader = req.getHeader('host'); - if (hostHeader) { - validateString(hostHeader, 'options.headers.host'); - - // abc => abc - // abc:123 => abc - // [::1] => ::1 - // [::1]:123 => ::1 - if (hostHeader[0] === '[') { - const index = hostHeader.indexOf(']'); - if (index === -1) { - // Leading '[', but no ']'. Need to do something... - servername = hostHeader; - } else { - servername = hostHeader.substring(1, index); - } - } else { - servername = hostHeader.split(':', 1)[0]; - } - } - // Don't implicitly set invalid (IP) servernames. - if (net.isIP(servername)) - servername = ''; - return servername; -} - -function installListeners(agent, s, options) { - function onFree() { - debug('CLIENT socket onFree'); - agent.emit('free', s, options); - } - s.on('free', onFree); - - function onClose(err) { - debug('CLIENT socket onClose'); - // This is the only place where sockets get removed from the Agent. - // If you want to remove a socket from the pool, just close it. - // All socket errors end in a close event anyway. - agent.totalSocketCount--; - agent.removeSocket(s, options); - } - s.on('close', onClose); - - function onTimeout() { - debug('CLIENT socket onTimeout'); - - // Destroy if in free list. - // TODO(ronag): Always destroy, even if not in free list. - const sockets = agent.freeSockets; - if (ObjectKeys(sockets).some((name) => sockets[name].includes(s))) { - return s.destroy(); - } - } - s.on('timeout', onTimeout); - - function onRemove() { - // We need this function for cases like HTTP 'upgrade' - // (defined by WebSockets) where we need to remove a socket from the - // pool because it'll be locked up indefinitely - debug('CLIENT socket onRemove'); - agent.totalSocketCount--; - agent.removeSocket(s, options); - s.removeListener('close', onClose); - s.removeListener('free', onFree); - s.removeListener('timeout', onTimeout); - s.removeListener('agentRemove', onRemove); - } - s.on('agentRemove', onRemove); - - if (agent[kOnKeylog]) { - s.on('keylog', agent[kOnKeylog]); - } -} - -Agent.prototype.removeSocket = function removeSocket(s, options) { - const name = this.getName(options); - debug('removeSocket', name, 'writable:', s.writable); - const sets = [this.sockets]; - - // If the socket was destroyed, remove it from the free buffers too. - if (!s.writable) - sets.push(this.freeSockets); - - for (let sk = 0; sk < sets.length; sk++) { - const sockets = sets[sk]; - - if (sockets[name]) { - const index = sockets[name].indexOf(s); - if (index !== -1) { - sockets[name].splice(index, 1); - // Don't leak - if (sockets[name].length === 0) - delete sockets[name]; - } - } - } - - let req; - if (this.requests[name]?.length) { - debug('removeSocket, have a request, make a socket'); - req = this.requests[name][0]; - } else { - // TODO(rickyes): this logic will not be FIFO across origins. - // There might be older requests in a different origin, but - // if the origin which releases the socket has pending requests - // that will be prioritized. - const keys = ObjectKeys(this.requests); - for (let i = 0; i < keys.length; i++) { - const prop = keys[i]; - // Check whether this specific origin is already at maxSockets - if (this.sockets[prop]?.length) break; - debug('removeSocket, have a request with different origin,' + - ' make a socket'); - req = this.requests[prop][0]; - options = req[kRequestOptions]; - break; - } - } - - if (req && options) { - req[kRequestOptions] = undefined; - // If we have pending requests and a socket gets closed make a new one - this.createSocket(req, options, (err, socket) => { - if (err) - req.onSocket(socket, err); - else - socket.emit('free'); - }); - } - -}; - -Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { - socket.setKeepAlive(true, this.keepAliveMsecs); - socket.unref(); - - let agentTimeout = this.options.timeout || 0; - let canKeepSocketAlive = true; - - if (socket._httpMessage?.res) { - const keepAliveHint = socket._httpMessage.res.headers['keep-alive']; - - if (keepAliveHint) { - const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1]; - - if (hint) { - // Let the timer expire before the announced timeout to reduce - // the likelihood of ECONNRESET errors - let serverHintTimeout = (NumberParseInt(hint) * 1000) - HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER; - serverHintTimeout = serverHintTimeout > 0 ? serverHintTimeout : 0; - if (serverHintTimeout === 0) { - // Cannot safely reuse the socket because the server timeout is - // too short - canKeepSocketAlive = false; - } else if (serverHintTimeout < agentTimeout) { - agentTimeout = serverHintTimeout; - } - } - } - } - - if (socket.timeout !== agentTimeout) { - socket.setTimeout(agentTimeout); - } - - return canKeepSocketAlive; -}; - -Agent.prototype.reuseSocket = function reuseSocket(socket, req) { - debug('have free socket'); - socket.removeListener('error', freeSocketErrorListener); - req.reusedSocket = true; - socket.ref(); -}; - -Agent.prototype.destroy = function destroy() { - const sets = [this.freeSockets, this.sockets]; - for (let s = 0; s < sets.length; s++) { - const set = sets[s]; - const keys = ObjectKeys(set); - for (let v = 0; v < keys.length; v++) { - const setName = set[keys[v]]; - for (let n = 0; n < setName.length; n++) { - setName[n].destroy(); - } - } - } -}; - -function setRequestSocket(agent, req, socket) { - req.onSocket(socket); - const agentTimeout = agent.options.timeout || 0; - if (req.timeout === undefined || req.timeout === agentTimeout) { - return; - } - socket.setTimeout(req.timeout); -} - -function asyncResetHandle(socket) { - // Guard against an uninitialized or user supplied Socket. - const handle = socket._handle; - if (handle && typeof handle.asyncReset === 'function') { - // Assign the handle a new asyncId and run any destroy()/init() hooks. - handle.asyncReset(new ReusedHandle(handle.getProviderType(), handle)); - socket[async_id_symbol] = handle.getAsyncId(); - } -} +const { Agent, globalAgent } = require('internal/http/agent'); module.exports = { Agent, - globalAgent: new Agent({ keepAlive: true, scheduling: 'lifo', timeout: 5000 }), + globalAgent, }; + +process.emitWarning('The _http_agent module is deprecated. Use `node:http` instead.', + 'DeprecationWarning', 'DEP0195'); diff --git a/lib/_http_client.js b/lib/_http_client.js index 00b59f357fa45d..5219d6e3ffce2a 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -21,976 +21,11 @@ 'use strict'; -const { - ArrayIsArray, - Boolean, - Error, - NumberIsFinite, - ObjectAssign, - ObjectKeys, - ObjectSetPrototypeOf, - ReflectApply, - String, - Symbol, -} = primordials; - -const net = require('net'); -const assert = require('internal/assert'); -const { - kEmptyObject, - once, -} = require('internal/util'); -const { - _checkIsHttpToken: checkIsHttpToken, - freeParser, - parsers, - HTTPParser, - isLenient, - prepareError, -} = require('_http_common'); -const { - kUniqueHeaders, - parseUniqueHeadersOption, - OutgoingMessage, -} = require('_http_outgoing'); -const Agent = require('_http_agent'); -const { Buffer } = require('buffer'); -const { defaultTriggerAsyncIdScope } = require('internal/async_hooks'); -const { URL, urlToHttpOptions, isURL } = require('internal/url'); -const { - kOutHeaders, - kNeedDrain, - isTraceHTTPEnabled, - traceBegin, - traceEnd, - getNextTraceEventId, -} = require('internal/http'); -const { - ConnResetException, - codes: { - ERR_HTTP_HEADERS_SENT, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_HTTP_TOKEN, - ERR_INVALID_PROTOCOL, - ERR_UNESCAPED_CHARACTERS, - }, -} = require('internal/errors'); -const { - validateInteger, - validateBoolean, -} = require('internal/validators'); -const { getTimerDuration } = require('internal/timers'); -const { - hasObserver, - startPerf, - stopPerf, -} = require('internal/perf/observe'); - -const kClientRequestStatistics = Symbol('ClientRequestStatistics'); - -const dc = require('diagnostics_channel'); -const onClientRequestCreatedChannel = dc.channel('http.client.request.created'); -const onClientRequestStartChannel = dc.channel('http.client.request.start'); -const onClientRequestErrorChannel = dc.channel('http.client.request.error'); -const onClientResponseFinishChannel = dc.channel('http.client.response.finish'); - -function emitErrorEvent(request, error) { - if (onClientRequestErrorChannel.hasSubscribers) { - onClientRequestErrorChannel.publish({ - request, - error, - }); - } - request.emit('error', error); -} - -const { addAbortSignal, finished } = require('stream'); - -let debug = require('internal/util/debuglog').debuglog('http', (fn) => { - debug = fn; -}); - -const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/; -const kError = Symbol('kError'); - -const kLenientAll = HTTPParser.kLenientAll | 0; -const kLenientNone = HTTPParser.kLenientNone | 0; - -const HTTP_CLIENT_TRACE_EVENT_NAME = 'http.client.request'; - -function validateHost(host, name) { - if (host !== null && host !== undefined && typeof host !== 'string') { - throw new ERR_INVALID_ARG_TYPE(`options.${name}`, - ['string', 'undefined', 'null'], - host); - } - return host; -} - -class HTTPClientAsyncResource { - constructor(type, req) { - this.type = type; - this.req = req; - } -} - -function ClientRequest(input, options, cb) { - OutgoingMessage.call(this); - - if (typeof input === 'string') { - const urlStr = input; - input = urlToHttpOptions(new URL(urlStr)); - } else if (isURL(input)) { - // url.URL instance - input = urlToHttpOptions(input); - } else { - cb = options; - options = input; - input = null; - } - - if (typeof options === 'function') { - cb = options; - options = input || kEmptyObject; - } else { - options = ObjectAssign(input || {}, options); - } - - let agent = options.agent; - const defaultAgent = options._defaultAgent || Agent.globalAgent; - if (agent === false) { - agent = new defaultAgent.constructor(); - } else if (agent === null || agent === undefined) { - if (typeof options.createConnection !== 'function') { - agent = defaultAgent; - } - // Explicitly pass through this statement as agent will not be used - // when createConnection is provided. - } else if (typeof agent.addRequest !== 'function') { - throw new ERR_INVALID_ARG_TYPE('options.agent', - ['Agent-like Object', 'undefined', 'false'], - agent); - } - this.agent = agent; - - const protocol = options.protocol || defaultAgent.protocol; - let expectedProtocol = defaultAgent.protocol; - if (this.agent?.protocol) - expectedProtocol = this.agent.protocol; - - if (options.path) { - const path = String(options.path); - if (INVALID_PATH_REGEX.test(path)) { - debug('Path contains unescaped characters: "%s"', path); - throw new ERR_UNESCAPED_CHARACTERS('Request path'); - } - } - - if (protocol !== expectedProtocol) { - throw new ERR_INVALID_PROTOCOL(protocol, expectedProtocol); - } - - const defaultPort = options.defaultPort || - (this.agent?.defaultPort); - - const optsWithoutSignal = { __proto__: null, ...options }; - - const port = optsWithoutSignal.port = options.port || defaultPort || 80; - const host = optsWithoutSignal.host = validateHost(options.hostname, 'hostname') || - validateHost(options.host, 'host') || 'localhost'; - - const setHost = options.setHost !== undefined ? - Boolean(options.setHost) : - options.setDefaultHeaders !== false; - - this._removedConnection = options.setDefaultHeaders === false; - this._removedContLen = options.setDefaultHeaders === false; - this._removedTE = options.setDefaultHeaders === false; - - this.socketPath = options.socketPath; - - if (options.timeout !== undefined) - this.timeout = getTimerDuration(options.timeout, 'timeout'); - - const signal = options.signal; - if (signal) { - addAbortSignal(signal, this); - delete optsWithoutSignal.signal; - } - let method = options.method; - const methodIsString = (typeof method === 'string'); - if (method !== null && method !== undefined && !methodIsString) { - throw new ERR_INVALID_ARG_TYPE('options.method', 'string', method); - } - - if (methodIsString && method) { - if (!checkIsHttpToken(method)) { - throw new ERR_INVALID_HTTP_TOKEN('Method', method); - } - method = this.method = method.toUpperCase(); - } else { - method = this.method = 'GET'; - } - - const maxHeaderSize = options.maxHeaderSize; - if (maxHeaderSize !== undefined) - validateInteger(maxHeaderSize, 'maxHeaderSize', 0); - this.maxHeaderSize = maxHeaderSize; - - const insecureHTTPParser = options.insecureHTTPParser; - if (insecureHTTPParser !== undefined) { - validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser'); - } - - this.insecureHTTPParser = insecureHTTPParser; - - if (options.joinDuplicateHeaders !== undefined) { - validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders'); - } - - this.joinDuplicateHeaders = options.joinDuplicateHeaders; - - this.path = options.path || '/'; - if (cb) { - this.once('response', cb); - } - - if (method === 'GET' || - method === 'HEAD' || - method === 'DELETE' || - method === 'OPTIONS' || - method === 'TRACE' || - method === 'CONNECT') { - this.useChunkedEncodingByDefault = false; - } else { - this.useChunkedEncodingByDefault = true; - } - - this._ended = false; - this.res = null; - this.aborted = false; - this.timeoutCb = null; - this.upgradeOrConnect = false; - this.parser = null; - this.maxHeadersCount = null; - this.reusedSocket = false; - this.host = host; - this.protocol = protocol; - - if (this.agent) { - // If there is an agent we should default to Connection:keep-alive, - // but only if the Agent will actually reuse the connection! - // If it's not a keepAlive agent, and the maxSockets==Infinity, then - // there's never a case where this socket will actually be reused - if (!this.agent.keepAlive && !NumberIsFinite(this.agent.maxSockets)) { - this._last = true; - this.shouldKeepAlive = false; - } else { - this._last = false; - this.shouldKeepAlive = true; - } - } - - const headersArray = ArrayIsArray(options.headers); - if (!headersArray) { - if (options.headers) { - const keys = ObjectKeys(options.headers); - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0; i < keys.length; i++) { - const key = keys[i]; - this.setHeader(key, options.headers[key]); - } - } - - if (host && !this.getHeader('host') && setHost) { - let hostHeader = host; - - // For the Host header, ensure that IPv6 addresses are enclosed - // in square brackets, as defined by URI formatting - // https://tools.ietf.org/html/rfc3986#section-3.2.2 - const posColon = hostHeader.indexOf(':'); - if (posColon !== -1 && - hostHeader.includes(':', posColon + 1) && - hostHeader.charCodeAt(0) !== 91/* '[' */) { - hostHeader = `[${hostHeader}]`; - } - - if (port && +port !== defaultPort) { - hostHeader += ':' + port; - } - this.setHeader('Host', hostHeader); - } - - if (options.auth && !this.getHeader('Authorization')) { - this.setHeader('Authorization', 'Basic ' + - Buffer.from(options.auth).toString('base64')); - } - - if (this.getHeader('expect')) { - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT('render'); - } - - this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', - this[kOutHeaders]); - } - } else { - this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', - options.headers); - } - - this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders); - - // initiate connection - if (this.agent) { - this.agent.addRequest(this, optsWithoutSignal); - } else { - // No agent, default to Connection:close. - this._last = true; - this.shouldKeepAlive = false; - let opts = optsWithoutSignal; - if (opts.path || opts.socketPath) { - opts = { ...optsWithoutSignal }; - if (opts.socketPath) { - opts.path = opts.socketPath; - } else { - opts.path &&= undefined; - } - } - if (typeof opts.createConnection === 'function') { - const oncreate = once((err, socket) => { - if (err) { - process.nextTick(() => emitErrorEvent(this, err)); - } else { - this.onSocket(socket); - } - }); - - try { - const newSocket = opts.createConnection(opts, oncreate); - if (newSocket) { - oncreate(null, newSocket); - } - } catch (err) { - oncreate(err); - } - } else { - debug('CLIENT use net.createConnection', opts); - this.onSocket(net.createConnection(opts)); - } - } - if (onClientRequestCreatedChannel.hasSubscribers) { - onClientRequestCreatedChannel.publish({ - request: this, - }); - } -} -ObjectSetPrototypeOf(ClientRequest.prototype, OutgoingMessage.prototype); -ObjectSetPrototypeOf(ClientRequest, OutgoingMessage); - -ClientRequest.prototype._finish = function _finish() { - OutgoingMessage.prototype._finish.call(this); - if (hasObserver('http')) { - startPerf(this, kClientRequestStatistics, { - type: 'http', - name: 'HttpClient', - detail: { - req: { - method: this.method, - url: `${this.protocol}//${this.host}${this.path}`, - headers: typeof this.getHeaders === 'function' ? this.getHeaders() : {}, - }, - }, - }); - } - if (onClientRequestStartChannel.hasSubscribers) { - onClientRequestStartChannel.publish({ - request: this, - }); - } - if (isTraceHTTPEnabled()) { - this._traceEventId = getNextTraceEventId(); - traceBegin(HTTP_CLIENT_TRACE_EVENT_NAME, this._traceEventId); - } -}; - -ClientRequest.prototype._implicitHeader = function _implicitHeader() { - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT('render'); - } - this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', - this[kOutHeaders]); -}; - -ClientRequest.prototype.abort = function abort() { - if (this.aborted) { - return; - } - this.aborted = true; - process.nextTick(emitAbortNT, this); - this.destroy(); -}; - -ClientRequest.prototype.destroy = function destroy(err) { - if (this.destroyed) { - return this; - } - this.destroyed = true; - - // If we're aborting, we don't care about any more response data. - if (this.res) { - this.res._dump(); - } - - this[kError] = err; - this.socket?.destroy(err); - - return this; -}; - -function emitAbortNT(req) { - req.emit('abort'); -} - -function ondrain() { - const msg = this._httpMessage; - if (msg && !msg.finished && msg[kNeedDrain]) { - msg[kNeedDrain] = false; - msg.emit('drain'); - } -} - -function socketCloseListener() { - const socket = this; - const req = socket._httpMessage; - debug('HTTP socket close'); - - // NOTE: It's important to get parser here, because it could be freed by - // the `socketOnData`. - const parser = socket.parser; - const res = req.res; - - req.destroyed = true; - if (res) { - // Socket closed before we emitted 'end' below. - if (!res.complete) { - res.destroy(new ConnResetException('aborted')); - } - req._closed = true; - req.emit('close'); - if (!res.aborted && res.readable) { - res.push(null); - } - } else { - if (!req.socket._hadError) { - // This socket error fired before we started to - // receive a response. The error needs to - // fire on the request. - req.socket._hadError = true; - emitErrorEvent(req, new ConnResetException('socket hang up')); - } - req._closed = true; - req.emit('close'); - } - - // Too bad. That output wasn't getting written. - // This is pretty terrible that it doesn't raise an error. - // Fixed better in v0.10 - if (req.outputData) - req.outputData.length = 0; - - if (parser) { - parser.finish(); - freeParser(parser, req, socket); - } -} - -function socketErrorListener(err) { - const socket = this; - const req = socket._httpMessage; - debug('SOCKET ERROR:', err.message, err.stack); - - if (req) { - // For Safety. Some additional errors might fire later on - // and we need to make sure we don't double-fire the error event. - req.socket._hadError = true; - emitErrorEvent(req, err); - } - - const parser = socket.parser; - if (parser) { - parser.finish(); - freeParser(parser, req, socket); - } - - // Ensure that no further data will come out of the socket - socket.removeListener('data', socketOnData); - socket.removeListener('end', socketOnEnd); - socket.destroy(); -} - -function socketOnEnd() { - const socket = this; - const req = this._httpMessage; - const parser = this.parser; - - if (!req.res && !req.socket._hadError) { - // If we don't have a response then we know that the socket - // ended prematurely and we need to emit an error on the request. - req.socket._hadError = true; - emitErrorEvent(req, new ConnResetException('socket hang up')); - } - if (parser) { - parser.finish(); - freeParser(parser, req, socket); - } - socket.destroy(); -} - -function socketOnData(d) { - const socket = this; - const req = this._httpMessage; - const parser = this.parser; - - assert(parser && parser.socket === socket); - - const ret = parser.execute(d); - if (ret instanceof Error) { - prepareError(ret, parser, d); - debug('parse error', ret); - freeParser(parser, req, socket); - socket.removeListener('data', socketOnData); - socket.removeListener('end', socketOnEnd); - socket.destroy(); - req.socket._hadError = true; - emitErrorEvent(req, ret); - } else if (parser.incoming?.upgrade) { - // Upgrade (if status code 101) or CONNECT - const bytesParsed = ret; - const res = parser.incoming; - req.res = res; - - socket.removeListener('data', socketOnData); - socket.removeListener('end', socketOnEnd); - socket.removeListener('drain', ondrain); - - if (req.timeoutCb) socket.removeListener('timeout', req.timeoutCb); - socket.removeListener('timeout', responseOnTimeout); - - parser.finish(); - freeParser(parser, req, socket); - - const bodyHead = d.slice(bytesParsed, d.length); - - const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; - if (req.listenerCount(eventName) > 0) { - req.upgradeOrConnect = true; - - // detach the socket - socket.emit('agentRemove'); - socket.removeListener('close', socketCloseListener); - socket.removeListener('error', socketErrorListener); - - socket._httpMessage = null; - socket.readableFlowing = null; - - req.emit(eventName, res, socket, bodyHead); - req.destroyed = true; - req._closed = true; - req.emit('close'); - } else { - // Requested Upgrade or used CONNECT method, but have no handler. - socket.destroy(); - } - } else if (parser.incoming?.complete && - // When the status code is informational (100, 102-199), - // the server will send a final response after this client - // sends a request body, so we must not free the parser. - // 101 (Switching Protocols) and all other status codes - // should be processed normally. - !statusIsInformational(parser.incoming.statusCode)) { - socket.removeListener('data', socketOnData); - socket.removeListener('end', socketOnEnd); - socket.removeListener('drain', ondrain); - freeParser(parser, req, socket); - } -} - -function statusIsInformational(status) { - // 100 (Continue) RFC7231 Section 6.2.1 - // 102 (Processing) RFC2518 - // 103 (Early Hints) RFC8297 - // 104-199 (Unassigned) - return (status < 200 && status >= 100 && status !== 101); -} - -// client -function parserOnIncomingClient(res, shouldKeepAlive) { - const socket = this.socket; - const req = socket._httpMessage; - - debug('AGENT incoming response!'); - - if (req.res) { - // We already have a response object, this means the server - // sent a double response. - socket.destroy(); - return 0; // No special treatment. - } - req.res = res; - - // Skip body and treat as Upgrade. - if (res.upgrade) - return 2; - - // Responses to CONNECT request is handled as Upgrade. - const method = req.method; - if (method === 'CONNECT') { - res.upgrade = true; - return 2; // Skip body and treat as Upgrade. - } - - if (statusIsInformational(res.statusCode)) { - // Restart the parser, as this is a 1xx informational message. - req.res = null; // Clear res so that we don't hit double-responses. - // Maintain compatibility by sending 100-specific events - if (res.statusCode === 100) { - req.emit('continue'); - } - // Send information events to all 1xx responses except 101 Upgrade. - req.emit('information', { - statusCode: res.statusCode, - statusMessage: res.statusMessage, - httpVersion: res.httpVersion, - httpVersionMajor: res.httpVersionMajor, - httpVersionMinor: res.httpVersionMinor, - headers: res.headers, - rawHeaders: res.rawHeaders, - }); - - return 1; // Skip body but don't treat as Upgrade. - } - - if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) { - // Server MUST respond with Connection:keep-alive for us to enable it. - // If we've been upgraded (via WebSockets) we also shouldn't try to - // keep the connection open. - req.shouldKeepAlive = false; - } - - if (req[kClientRequestStatistics] && hasObserver('http')) { - stopPerf(req, kClientRequestStatistics, { - detail: { - res: { - statusCode: res.statusCode, - statusMessage: res.statusMessage, - headers: res.headers, - }, - }, - }); - } - if (onClientResponseFinishChannel.hasSubscribers) { - onClientResponseFinishChannel.publish({ - request: req, - response: res, - }); - } - if (isTraceHTTPEnabled() && typeof req._traceEventId === 'number') { - traceEnd(HTTP_CLIENT_TRACE_EVENT_NAME, req._traceEventId, { - path: req.path, - statusCode: res.statusCode, - }); - } - req.res = res; - res.req = req; - - // Add our listener first, so that we guarantee socket cleanup - res.on('end', responseOnEnd); - req.on('finish', requestOnFinish); - socket.on('timeout', responseOnTimeout); - - // If the user did not listen for the 'response' event, then they - // can't possibly read the data, so we ._dump() it into the void - // so that the socket doesn't hang there in a paused state. - if (req.aborted || !req.emit('response', res)) - res._dump(); - - if (method === 'HEAD') - return 1; // Skip body but don't treat as Upgrade. - - if (res.statusCode === 304) { - res.complete = true; - return 1; // Skip body as there won't be any - } - - return 0; // No special treatment. -} - -// client -function responseKeepAlive(req) { - const socket = req.socket; - - debug('AGENT socket keep-alive'); - if (req.timeoutCb) { - socket.setTimeout(0, req.timeoutCb); - req.timeoutCb = null; - } - socket.removeListener('close', socketCloseListener); - socket.removeListener('error', socketErrorListener); - socket.removeListener('data', socketOnData); - socket.removeListener('end', socketOnEnd); - - // TODO(ronag): Between here and emitFreeNT the socket - // has no 'error' handler. - - // There are cases where _handle === null. Avoid those. Passing undefined to - // nextTick() will call getDefaultTriggerAsyncId() to retrieve the id. - const asyncId = socket._handle ? socket._handle.getAsyncId() : undefined; - // Mark this socket as available, AFTER user-added end - // handlers have a chance to run. - defaultTriggerAsyncIdScope(asyncId, process.nextTick, emitFreeNT, req); - - req.destroyed = true; - if (req.res) { - // Detach socket from IncomingMessage to avoid destroying the freed - // socket in IncomingMessage.destroy(). - req.res.socket = null; - } -} - -function responseOnEnd() { - const req = this.req; - const socket = req.socket; - - if (socket) { - if (req.timeoutCb) socket.removeListener('timeout', emitRequestTimeout); - socket.removeListener('timeout', responseOnTimeout); - } - - req._ended = true; - - if (!req.shouldKeepAlive) { - if (socket.writable) { - debug('AGENT socket.destroySoon()'); - if (typeof socket.destroySoon === 'function') - socket.destroySoon(); - else - socket.end(); - } - assert(!socket.writable); - } else if (req.writableFinished && !this.aborted) { - assert(req.finished); - // We can assume `req.finished` means all data has been written since: - // - `'responseOnEnd'` means we have been assigned a socket. - // - when we have a socket we write directly to it without buffering. - // - `req.finished` means `end()` has been called and no further data. - // can be written - // In addition, `req.writableFinished` means all data written has been - // accepted by the kernel. (i.e. the `req.socket` is drained).Without - // this constraint, we may assign a non drained socket to a request. - responseKeepAlive(req); - } -} - -function responseOnTimeout() { - const req = this._httpMessage; - if (!req) return; - const res = req.res; - if (!res) return; - res.emit('timeout'); -} - -// This function is necessary in the case where we receive the entire response -// from the server before we finish sending out the request. -function requestOnFinish() { - const req = this; - - if (req.shouldKeepAlive && req._ended) - responseKeepAlive(req); -} - -function emitFreeNT(req) { - req._closed = true; - req.emit('close'); - if (req.socket) { - req.socket.emit('free'); - } -} - -function tickOnSocket(req, socket) { - const parser = parsers.alloc(); - req.socket = socket; - const lenient = req.insecureHTTPParser === undefined ? - isLenient() : req.insecureHTTPParser; - parser.initialize(HTTPParser.RESPONSE, - new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req), - req.maxHeaderSize || 0, - lenient ? kLenientAll : kLenientNone); - parser.socket = socket; - parser.outgoing = req; - req.parser = parser; - - socket.parser = parser; - socket._httpMessage = req; - - // Propagate headers limit from request object to parser - if (typeof req.maxHeadersCount === 'number') { - parser.maxHeaderPairs = req.maxHeadersCount << 1; - } - - parser.joinDuplicateHeaders = req.joinDuplicateHeaders; - - parser.onIncoming = parserOnIncomingClient; - socket.on('error', socketErrorListener); - socket.on('data', socketOnData); - socket.on('end', socketOnEnd); - socket.on('close', socketCloseListener); - socket.on('drain', ondrain); - - if ( - req.timeout !== undefined || - (req.agent?.options?.timeout) - ) { - listenSocketTimeout(req); - } - req.emit('socket', socket); -} - -function emitRequestTimeout() { - const req = this._httpMessage; - if (req) { - req.emit('timeout'); - } -} - -function listenSocketTimeout(req) { - if (req.timeoutCb) { - return; - } - // Set timeoutCb so it will get cleaned up on request end. - req.timeoutCb = emitRequestTimeout; - // Delegate socket timeout event. - if (req.socket) { - req.socket.once('timeout', emitRequestTimeout); - } else { - req.on('socket', (socket) => { - socket.once('timeout', emitRequestTimeout); - }); - } -} - -ClientRequest.prototype.onSocket = function onSocket(socket, err) { - // TODO(ronag): Between here and onSocketNT the socket - // has no 'error' handler. - process.nextTick(onSocketNT, this, socket, err); -}; - -function onSocketNT(req, socket, err) { - if (req.destroyed || err) { - req.destroyed = true; - - function _destroy(req, err) { - if (!req.aborted && !err) { - err = new ConnResetException('socket hang up'); - } - if (err) { - emitErrorEvent(req, err); - } - req._closed = true; - req.emit('close'); - } - - if (socket) { - if (!err && req.agent && !socket.destroyed) { - socket.emit('free'); - } else { - finished(socket.destroy(err || req[kError]), (er) => { - if (er?.code === 'ERR_STREAM_PREMATURE_CLOSE') { - er = null; - } - _destroy(req, er || err); - }); - return; - } - } - - _destroy(req, err || req[kError]); - } else { - tickOnSocket(req, socket); - req._flush(); - } -} - -ClientRequest.prototype._deferToConnect = _deferToConnect; -function _deferToConnect(method, arguments_) { - // This function is for calls that need to happen once the socket is - // assigned to this request and writable. It's an important promisy - // thing for all the socket calls that happen either now - // (when a socket is assigned) or in the future (when a socket gets - // assigned out of the pool and is eventually writable). - - const callSocketMethod = () => { - if (method) - ReflectApply(this.socket[method], this.socket, arguments_); - }; - - const onSocket = () => { - if (this.socket.writable) { - callSocketMethod(); - } else { - this.socket.once('connect', callSocketMethod); - } - }; - - if (!this.socket) { - this.once('socket', onSocket); - } else { - onSocket(); - } -} - -ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) { - if (this._ended) { - return this; - } - - listenSocketTimeout(this); - msecs = getTimerDuration(msecs, 'msecs'); - if (callback) this.once('timeout', callback); - - if (this.socket) { - setSocketTimeout(this.socket, msecs); - } else { - this.once('socket', (sock) => setSocketTimeout(sock, msecs)); - } - - return this; -}; - -function setSocketTimeout(sock, msecs) { - if (sock.connecting) { - sock.once('connect', function() { - sock.setTimeout(msecs); - }); - } else { - sock.setTimeout(msecs); - } -} - -ClientRequest.prototype.setNoDelay = function setNoDelay(noDelay) { - this._deferToConnect('setNoDelay', [noDelay]); -}; - -ClientRequest.prototype.setSocketKeepAlive = - function setSocketKeepAlive(enable, initialDelay) { - this._deferToConnect('setKeepAlive', [enable, initialDelay]); - }; - -ClientRequest.prototype.clearTimeout = function clearTimeout(cb) { - this.setTimeout(0, cb); -}; +const { ClientRequest } = require('internal/http/client'); module.exports = { ClientRequest, }; + +process.emitWarning('The _http_client module is deprecated. Use `node:http` instead.', + 'DeprecationWarning', 'DEP0195'); diff --git a/lib/_http_common.js b/lib/_http_common.js index 96d9bdfc9fcbe5..f31f89a8d12e84 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -22,243 +22,27 @@ 'use strict'; const { - MathMin, - Symbol, -} = primordials; -const { setImmediate } = require('timers'); - -const { methods, allMethods, HTTPParser } = internalBinding('http_parser'); -const { getOptionValue } = require('internal/options'); -const insecureHTTPParser = getOptionValue('--insecure-http-parser'); - -const FreeList = require('internal/freelist'); -const incoming = require('_http_incoming'); -const { - IncomingMessage, - readStart, - readStop, -} = incoming; - -const kIncomingMessage = Symbol('IncomingMessage'); -const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0; -const kOnHeaders = HTTPParser.kOnHeaders | 0; -const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; -const kOnBody = HTTPParser.kOnBody | 0; -const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0; -const kOnExecute = HTTPParser.kOnExecute | 0; -const kOnTimeout = HTTPParser.kOnTimeout | 0; - -const MAX_HEADER_PAIRS = 2000; - -// Only called in the slow case where slow means -// that the request headers were either fragmented -// across multiple TCP packets or too large to be -// processed in a single run. This method is also -// called to process trailing HTTP headers. -function parserOnHeaders(headers, url) { - // Once we exceeded headers limit - stop collecting them - if (this.maxHeaderPairs <= 0 || - this._headers.length < this.maxHeaderPairs) { - this._headers.push(...headers); - } - this._url += url; -} - -// `headers` and `url` are set only if .onHeaders() has not been called for -// this request. -// `url` is not set for response parsers but that's not applicable here since -// all our parsers are request parsers. -function parserOnHeadersComplete(versionMajor, versionMinor, headers, method, - url, statusCode, statusMessage, upgrade, - shouldKeepAlive) { - const parser = this; - const { socket } = parser; - - if (headers === undefined) { - headers = parser._headers; - parser._headers = []; - } - - if (url === undefined) { - url = parser._url; - parser._url = ''; - } - - // Parser is also used by http client - const ParserIncomingMessage = (socket?.server?.[kIncomingMessage]) || - IncomingMessage; - - const incoming = parser.incoming = new ParserIncomingMessage(socket); - incoming.httpVersionMajor = versionMajor; - incoming.httpVersionMinor = versionMinor; - incoming.httpVersion = `${versionMajor}.${versionMinor}`; - incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders || - parser.joinDuplicateHeaders; - incoming.url = url; - incoming.upgrade = upgrade; - - let n = headers.length; - - // If parser.maxHeaderPairs <= 0 assume that there's no limit. - if (parser.maxHeaderPairs > 0) - n = MathMin(n, parser.maxHeaderPairs); - - incoming._addHeaderLines(headers, n); - - if (typeof method === 'number') { - // server only - incoming.method = allMethods[method]; - } else { - // client only - incoming.statusCode = statusCode; - incoming.statusMessage = statusMessage; - } - - return parser.onIncoming(incoming, shouldKeepAlive); -} - -function parserOnBody(b) { - const stream = this.incoming; - - // If the stream has already been removed, then drop it. - if (stream === null) - return; - - // Pretend this was the result of a stream._read call. - if (!stream._dumped) { - const ret = stream.push(b); - if (!ret) - readStop(this.socket); - } -} - -function parserOnMessageComplete() { - const parser = this; - const stream = parser.incoming; - - if (stream !== null) { - stream.complete = true; - // Emit any trailing headers. - const headers = parser._headers; - if (headers.length) { - stream._addHeaderLines(headers, headers.length); - parser._headers = []; - parser._url = ''; - } - - // For emit end event - stream.push(null); - } - - // Force to read the next incoming message - readStart(parser.socket); -} - - -const parsers = new FreeList('parsers', 1000, function parsersCb() { - const parser = new HTTPParser(); - - cleanParser(parser); - - parser[kOnHeaders] = parserOnHeaders; - parser[kOnHeadersComplete] = parserOnHeadersComplete; - parser[kOnBody] = parserOnBody; - parser[kOnMessageComplete] = parserOnMessageComplete; - - return parser; -}); - -function closeParserInstance(parser) { parser.close(); } - -// Free the parser and also break any links that it -// might have to any other things. -// TODO: All parser data should be attached to a -// single object, so that it can be easily cleaned -// up by doing `parser.data = {}`, which should -// be done in FreeList.free. `parsers.free(parser)` -// should be all that is needed. -function freeParser(parser, req, socket) { - if (parser) { - if (parser._consumed) - parser.unconsume(); - cleanParser(parser); - parser.remove(); - if (parsers.free(parser) === false) { - // Make sure the parser's stack has unwound before deleting the - // corresponding C++ object through .close(). - setImmediate(closeParserInstance, parser); - } else { - // Since the Parser destructor isn't going to run the destroy() callbacks - // it needs to be triggered manually. - parser.free(); - } - } - if (req) { - req.parser = null; - } - if (socket) { - socket.parser = null; - } -} - -const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; -/** - * Verifies that the given val is a valid HTTP token - * per the rules defined in RFC 7230 - * See https://tools.ietf.org/html/rfc7230#section-3.2.6 - */ -function checkIsHttpToken(val) { - return tokenRegExp.test(val); -} - -const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; -/** - * True if val contains an invalid field-vchar - * field-value = *( field-content / obs-fold ) - * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] - * field-vchar = VCHAR / obs-text - */ -function checkInvalidHeaderChar(val) { - return headerCharRegex.test(val); -} - -function cleanParser(parser) { - parser._headers = []; - parser._url = ''; - parser.socket = null; - parser.incoming = null; - parser.outgoing = null; - parser.maxHeaderPairs = MAX_HEADER_PAIRS; - parser[kOnMessageBegin] = null; - parser[kOnExecute] = null; - parser[kOnTimeout] = null; - parser._consumed = false; - parser.onIncoming = null; - parser.joinDuplicateHeaders = null; -} - -function prepareError(err, parser, rawPacket) { - err.rawPacket = rawPacket || parser.getCurrentBuffer(); - if (typeof err.reason === 'string') - err.message = `Parse Error: ${err.reason}`; -} - -let warnedLenient = false; + _checkInvalidHeaderChar, + _checkIsHttpToken, + chunkExpression, + continueExpression, + CRLF, + freeParser, + methods, + parsers, + kIncomingMessage, + HTTPParser, + isLenient, + prepareError, +} = require('internal/http/common'); -function isLenient() { - if (insecureHTTPParser && !warnedLenient) { - warnedLenient = true; - process.emitWarning('Using insecure HTTP parsing'); - } - return insecureHTTPParser; -} module.exports = { - _checkInvalidHeaderChar: checkInvalidHeaderChar, - _checkIsHttpToken: checkIsHttpToken, - chunkExpression: /(?:^|\W)chunked(?:$|\W)/i, - continueExpression: /(?:^|\W)100-continue(?:$|\W)/i, - CRLF: '\r\n', // TODO: Deprecate this. + _checkInvalidHeaderChar, + _checkIsHttpToken, + chunkExpression, + continueExpression, + CRLF, // TODO: Deprecate this. freeParser, methods, parsers, @@ -267,3 +51,6 @@ module.exports = { isLenient, prepareError, }; + +process.emitWarning('The _http_common module is deprecated.', + 'DeprecationWarning', 'DEP0195'); diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index c3e901e53e8b90..074d1e43613fef 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -21,433 +21,13 @@ 'use strict'; -const { - ObjectDefineProperty, - ObjectSetPrototypeOf, - Symbol, -} = primordials; - -const { Readable, finished } = require('stream'); - -const kHeaders = Symbol('kHeaders'); -const kHeadersDistinct = Symbol('kHeadersDistinct'); -const kHeadersCount = Symbol('kHeadersCount'); -const kTrailers = Symbol('kTrailers'); -const kTrailersDistinct = Symbol('kTrailersDistinct'); -const kTrailersCount = Symbol('kTrailersCount'); - -function readStart(socket) { - if (socket && !socket._paused && socket.readable) - socket.resume(); -} - -function readStop(socket) { - if (socket) - socket.pause(); -} - -/* Abstract base class for ServerRequest and ClientResponse. */ -function IncomingMessage(socket) { - let streamOptions; - - if (socket) { - streamOptions = { - highWaterMark: socket.readableHighWaterMark, - }; - } - - Readable.call(this, streamOptions); - - this._readableState.readingMore = true; - - this.socket = socket; - - this.httpVersionMajor = null; - this.httpVersionMinor = null; - this.httpVersion = null; - this.complete = false; - this[kHeaders] = null; - this[kHeadersCount] = 0; - this.rawHeaders = []; - this[kTrailers] = null; - this[kTrailersCount] = 0; - this.rawTrailers = []; - this.joinDuplicateHeaders = false; - this.aborted = false; - - this.upgrade = null; - - // request (server) only - this.url = ''; - this.method = null; - - // response (client) only - this.statusCode = null; - this.statusMessage = null; - this.client = socket; - - this._consuming = false; - // Flag for when we decide that this message cannot possibly be - // read by the user, so there's no point continuing to handle it. - this._dumped = false; -} -ObjectSetPrototypeOf(IncomingMessage.prototype, Readable.prototype); -ObjectSetPrototypeOf(IncomingMessage, Readable); - -ObjectDefineProperty(IncomingMessage.prototype, 'connection', { - __proto__: null, - get: function() { - return this.socket; - }, - set: function(val) { - this.socket = val; - }, -}); - -ObjectDefineProperty(IncomingMessage.prototype, 'headers', { - __proto__: null, - get: function() { - if (!this[kHeaders]) { - this[kHeaders] = {}; - - const src = this.rawHeaders; - const dst = this[kHeaders]; - - for (let n = 0; n < this[kHeadersCount]; n += 2) { - this._addHeaderLine(src[n + 0], src[n + 1], dst); - } - } - return this[kHeaders]; - }, - set: function(val) { - this[kHeaders] = val; - }, -}); - -ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', { - __proto__: null, - get: function() { - if (!this[kHeadersDistinct]) { - this[kHeadersDistinct] = {}; - - const src = this.rawHeaders; - const dst = this[kHeadersDistinct]; - - for (let n = 0; n < this[kHeadersCount]; n += 2) { - this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst); - } - } - return this[kHeadersDistinct]; - }, - set: function(val) { - this[kHeadersDistinct] = val; - }, -}); - -ObjectDefineProperty(IncomingMessage.prototype, 'trailers', { - __proto__: null, - get: function() { - if (!this[kTrailers]) { - this[kTrailers] = {}; - - const src = this.rawTrailers; - const dst = this[kTrailers]; - - for (let n = 0; n < this[kTrailersCount]; n += 2) { - this._addHeaderLine(src[n + 0], src[n + 1], dst); - } - } - return this[kTrailers]; - }, - set: function(val) { - this[kTrailers] = val; - }, -}); - -ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', { - __proto__: null, - get: function() { - if (!this[kTrailersDistinct]) { - this[kTrailersDistinct] = {}; - - const src = this.rawTrailers; - const dst = this[kTrailersDistinct]; - - for (let n = 0; n < this[kTrailersCount]; n += 2) { - this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst); - } - } - return this[kTrailersDistinct]; - }, - set: function(val) { - this[kTrailersDistinct] = val; - }, -}); - -IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { - if (callback) - this.on('timeout', callback); - this.socket.setTimeout(msecs); - return this; -}; - -// Argument n cannot be factored out due to the overhead of -// argument adaptor frame creation inside V8 in case that number of actual -// arguments is different from expected arguments. -// Ref: https://bugs.chromium.org/p/v8/issues/detail?id=10201 -// NOTE: Argument adapt frame issue might be solved in V8 engine v8.9. -// Refactoring `n` out might be possible when V8 is upgraded to that -// version. -// Ref: https://v8.dev/blog/v8-release-89 -IncomingMessage.prototype._read = function _read(n) { - if (!this._consuming) { - this._readableState.readingMore = false; - this._consuming = true; - } - - // We actually do almost nothing here, because the parserOnBody - // function fills up our internal buffer directly. However, we - // do need to unpause the underlying socket so that it flows. - if (this.socket.readable) - readStart(this.socket); -}; - -// It's possible that the socket will be destroyed, and removed from -// any messages, before ever calling this. In that case, just skip -// it, since something else is destroying this connection anyway. -IncomingMessage.prototype._destroy = function _destroy(err, cb) { - if (!this.readableEnded || !this.complete) { - this.aborted = true; - this.emit('aborted'); - } - - // If aborted and the underlying socket is not already destroyed, - // destroy it. - // We have to check if the socket is already destroyed because finished - // does not call the callback when this method is invoked from `_http_client` - // in `test/parallel/test-http-client-spurious-aborted.js` - if (this.socket && !this.socket.destroyed && this.aborted) { - this.socket.destroy(err); - const cleanup = finished(this.socket, (e) => { - if (e?.code === 'ERR_STREAM_PREMATURE_CLOSE') { - e = null; - } - cleanup(); - process.nextTick(onError, this, e || err, cb); - }); - } else { - process.nextTick(onError, this, err, cb); - } -}; - -IncomingMessage.prototype._addHeaderLines = _addHeaderLines; -function _addHeaderLines(headers, n) { - if (headers?.length) { - let dest; - if (this.complete) { - this.rawTrailers = headers; - this[kTrailersCount] = n; - dest = this[kTrailers]; - } else { - this.rawHeaders = headers; - this[kHeadersCount] = n; - dest = this[kHeaders]; - } - - if (dest) { - for (let i = 0; i < n; i += 2) { - this._addHeaderLine(headers[i], headers[i + 1], dest); - } - } - } -} - - -// This function is used to help avoid the lowercasing of a field name if it -// matches a 'traditional cased' version of a field name. It then returns the -// lowercased name to both avoid calling toLowerCase() a second time and to -// indicate whether the field was a 'no duplicates' field. If a field is not a -// 'no duplicates' field, a `0` byte is prepended as a flag. The one exception -// to this is the Set-Cookie header which is indicated by a `1` byte flag, since -// it is an 'array' field and thus is treated differently in _addHeaderLines(). -// TODO: perhaps http_parser could be returning both raw and lowercased versions -// of known header names to avoid us having to call toLowerCase() for those -// headers. -function matchKnownFields(field, lowercased) { - switch (field.length) { - case 3: - if (field === 'Age' || field === 'age') return 'age'; - break; - case 4: - if (field === 'Host' || field === 'host') return 'host'; - if (field === 'From' || field === 'from') return 'from'; - if (field === 'ETag' || field === 'etag') return 'etag'; - if (field === 'Date' || field === 'date') return '\u0000date'; - if (field === 'Vary' || field === 'vary') return '\u0000vary'; - break; - case 6: - if (field === 'Server' || field === 'server') return 'server'; - if (field === 'Cookie' || field === 'cookie') return '\u0002cookie'; - if (field === 'Origin' || field === 'origin') return '\u0000origin'; - if (field === 'Expect' || field === 'expect') return '\u0000expect'; - if (field === 'Accept' || field === 'accept') return '\u0000accept'; - break; - case 7: - if (field === 'Referer' || field === 'referer') return 'referer'; - if (field === 'Expires' || field === 'expires') return 'expires'; - if (field === 'Upgrade' || field === 'upgrade') return '\u0000upgrade'; - break; - case 8: - if (field === 'Location' || field === 'location') - return 'location'; - if (field === 'If-Match' || field === 'if-match') - return '\u0000if-match'; - break; - case 10: - if (field === 'User-Agent' || field === 'user-agent') - return 'user-agent'; - if (field === 'Set-Cookie' || field === 'set-cookie') - return '\u0001'; - if (field === 'Connection' || field === 'connection') - return '\u0000connection'; - break; - case 11: - if (field === 'Retry-After' || field === 'retry-after') - return 'retry-after'; - break; - case 12: - if (field === 'Content-Type' || field === 'content-type') - return 'content-type'; - if (field === 'Max-Forwards' || field === 'max-forwards') - return 'max-forwards'; - break; - case 13: - if (field === 'Authorization' || field === 'authorization') - return 'authorization'; - if (field === 'Last-Modified' || field === 'last-modified') - return 'last-modified'; - if (field === 'Cache-Control' || field === 'cache-control') - return '\u0000cache-control'; - if (field === 'If-None-Match' || field === 'if-none-match') - return '\u0000if-none-match'; - break; - case 14: - if (field === 'Content-Length' || field === 'content-length') - return 'content-length'; - break; - case 15: - if (field === 'Accept-Encoding' || field === 'accept-encoding') - return '\u0000accept-encoding'; - if (field === 'Accept-Language' || field === 'accept-language') - return '\u0000accept-language'; - if (field === 'X-Forwarded-For' || field === 'x-forwarded-for') - return '\u0000x-forwarded-for'; - break; - case 16: - if (field === 'Content-Encoding' || field === 'content-encoding') - return '\u0000content-encoding'; - if (field === 'X-Forwarded-Host' || field === 'x-forwarded-host') - return '\u0000x-forwarded-host'; - break; - case 17: - if (field === 'If-Modified-Since' || field === 'if-modified-since') - return 'if-modified-since'; - if (field === 'Transfer-Encoding' || field === 'transfer-encoding') - return '\u0000transfer-encoding'; - if (field === 'X-Forwarded-Proto' || field === 'x-forwarded-proto') - return '\u0000x-forwarded-proto'; - break; - case 19: - if (field === 'Proxy-Authorization' || field === 'proxy-authorization') - return 'proxy-authorization'; - if (field === 'If-Unmodified-Since' || field === 'if-unmodified-since') - return 'if-unmodified-since'; - break; - } - if (lowercased) { - return '\u0000' + field; - } - return matchKnownFields(field.toLowerCase(), true); -} -// Add the given (field, value) pair to the message -// -// Per RFC2616, section 4.2 it is acceptable to join multiple instances of the -// same header with a ', ' if the header in question supports specification of -// multiple values this way. The one exception to this is the Cookie header, -// which has multiple values joined with a '; ' instead. If a header's values -// cannot be joined in either of these ways, we declare the first instance the -// winner and drop the second. Extended header fields (those beginning with -// 'x-') are always joined. -IncomingMessage.prototype._addHeaderLine = _addHeaderLine; -function _addHeaderLine(field, value, dest) { - field = matchKnownFields(field); - const flag = field.charCodeAt(0); - if (flag === 0 || flag === 2) { - field = field.slice(1); - // Make a delimited list - if (typeof dest[field] === 'string') { - dest[field] += (flag === 0 ? ', ' : '; ') + value; - } else { - dest[field] = value; - } - } else if (flag === 1) { - // Array header -- only Set-Cookie at the moment - if (dest['set-cookie'] !== undefined) { - dest['set-cookie'].push(value); - } else { - dest['set-cookie'] = [value]; - } - } else if (this.joinDuplicateHeaders) { - // RFC 9110 https://www.rfc-editor.org/rfc/rfc9110#section-5.2 - // https://github.com/nodejs/node/issues/45699 - // allow authorization multiple fields - // Make a delimited list - if (dest[field] === undefined) { - dest[field] = value; - } else { - dest[field] += ', ' + value; - } - } else if (dest[field] === undefined) { - // Drop duplicates - dest[field] = value; - } -} - -IncomingMessage.prototype._addHeaderLineDistinct = _addHeaderLineDistinct; -function _addHeaderLineDistinct(field, value, dest) { - field = field.toLowerCase(); - if (!dest[field]) { - dest[field] = [value]; - } else { - dest[field].push(value); - } -} - - -// Call this instead of resume() if we want to just -// dump all the data to /dev/null -IncomingMessage.prototype._dump = function _dump() { - if (!this._dumped) { - this._dumped = true; - // If there is buffered data, it may trigger 'data' events. - // Remove 'data' event listeners explicitly. - this.removeAllListeners('data'); - this.resume(); - } -}; - -function onError(self, error, cb) { - // This is to keep backward compatible behavior. - // An error is emitted only if there are listeners attached to the event. - if (self.listenerCount('error') === 0) { - cb(); - } else { - cb(error); - } -} +const { IncomingMessage, readStart, readStop } = require('internal/http/incoming'); module.exports = { IncomingMessage, readStart, readStop, }; + +process.emitWarning('The _http_incoming module is deprecated. Use `node:http` instead.', + 'DeprecationWarning', 'DEP0194'); diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index fdd0f2f77eaac3..4609e6bfea32ca 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -22,1177 +22,13 @@ 'use strict'; const { - Array, - ArrayIsArray, - MathFloor, - ObjectDefineProperty, - ObjectHasOwn, - ObjectKeys, - ObjectSetPrototypeOf, - ObjectValues, - SafeSet, - Symbol, -} = primordials; - -const { getDefaultHighWaterMark } = require('internal/streams/state'); -const assert = require('internal/assert'); -const EE = require('events'); -const Stream = require('stream'); -const { kOutHeaders, utcDate, kNeedDrain } = require('internal/http'); -const { Buffer } = require('buffer'); -const { - _checkIsHttpToken: checkIsHttpToken, - _checkInvalidHeaderChar: checkInvalidHeaderChar, - chunkExpression: RE_TE_CHUNKED, -} = require('_http_common'); -const { - defaultTriggerAsyncIdScope, - symbols: { async_id_symbol }, -} = require('internal/async_hooks'); -const { - codes: { - ERR_HTTP_BODY_NOT_ALLOWED, - ERR_HTTP_CONTENT_LENGTH_MISMATCH, - ERR_HTTP_HEADERS_SENT, - ERR_HTTP_INVALID_HEADER_VALUE, - ERR_HTTP_TRAILER_INVALID, - ERR_INVALID_ARG_TYPE, - ERR_INVALID_ARG_VALUE, - ERR_INVALID_CHAR, - ERR_INVALID_HTTP_TOKEN, - ERR_METHOD_NOT_IMPLEMENTED, - ERR_STREAM_ALREADY_FINISHED, - ERR_STREAM_CANNOT_PIPE, - ERR_STREAM_DESTROYED, - ERR_STREAM_NULL_VALUES, - ERR_STREAM_WRITE_AFTER_END, - }, - hideStackFrames, -} = require('internal/errors'); -const { validateString } = require('internal/validators'); -const { assignFunctionName } = require('internal/util'); -const { isUint8Array } = require('internal/util/types'); - -let debug = require('internal/util/debuglog').debuglog('http', (fn) => { - debug = fn; -}); - -const kCorked = Symbol('corked'); -const kSocket = Symbol('kSocket'); -const kChunkedBuffer = Symbol('kChunkedBuffer'); -const kChunkedLength = Symbol('kChunkedLength'); -const kUniqueHeaders = Symbol('kUniqueHeaders'); -const kBytesWritten = Symbol('kBytesWritten'); -const kErrored = Symbol('errored'); -const kHighWaterMark = Symbol('kHighWaterMark'); -const kRejectNonStandardBodyWrites = Symbol('kRejectNonStandardBodyWrites'); - -const nop = () => {}; - -const RE_CONN_CLOSE = /(?:^|\W)close(?:$|\W)/i; - -// isCookieField performs a case-insensitive comparison of a provided string -// against the word "cookie." As of V8 6.6 this is faster than handrolling or -// using a case-insensitive RegExp. -function isCookieField(s) { - return s.length === 6 && s.toLowerCase() === 'cookie'; -} - -function isContentDispositionField(s) { - return s.length === 19 && s.toLowerCase() === 'content-disposition'; -} - -function OutgoingMessage(options) { - Stream.call(this); - - // Queue that holds all currently pending data, until the response will be - // assigned to the socket (until it will its turn in the HTTP pipeline). - this.outputData = []; - - // `outputSize` is an approximate measure of how much data is queued on this - // response. `_onPendingData` will be invoked to update similar global - // per-connection counter. That counter will be used to pause/unpause the - // TCP socket and HTTP Parser and thus handle the backpressure. - this.outputSize = 0; - - this.writable = true; - this.destroyed = false; - - this._last = false; - this.chunkedEncoding = false; - this.shouldKeepAlive = true; - this.maxRequestsOnConnectionReached = false; - this._defaultKeepAlive = true; - this.useChunkedEncodingByDefault = true; - this.sendDate = false; - this._removedConnection = false; - this._removedContLen = false; - this._removedTE = false; - - this.strictContentLength = false; - this[kBytesWritten] = 0; - this._contentLength = null; - this._hasBody = true; - this._trailer = ''; - this[kNeedDrain] = false; - - this.finished = false; - this._headerSent = false; - this[kCorked] = 0; - this[kChunkedBuffer] = []; - this[kChunkedLength] = 0; - this._closed = false; - - this[kSocket] = null; - this._header = null; - this[kOutHeaders] = null; - - this._keepAliveTimeout = 0; - - this._onPendingData = nop; - - this[kErrored] = null; - this[kHighWaterMark] = options?.highWaterMark ?? getDefaultHighWaterMark(); - this[kRejectNonStandardBodyWrites] = options?.rejectNonStandardBodyWrites ?? false; -} -ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype); -ObjectSetPrototypeOf(OutgoingMessage, Stream); - -ObjectDefineProperty(OutgoingMessage.prototype, 'errored', { - __proto__: null, - get() { - return this[kErrored]; - }, -}); - -ObjectDefineProperty(OutgoingMessage.prototype, 'closed', { - __proto__: null, - get() { - return this._closed; - }, -}); - -ObjectDefineProperty(OutgoingMessage.prototype, 'writableFinished', { - __proto__: null, - get() { - return ( - this.finished && - this.outputSize === 0 && - (!this[kSocket] || this[kSocket].writableLength === 0) - ); - }, -}); - -ObjectDefineProperty(OutgoingMessage.prototype, 'writableObjectMode', { - __proto__: null, - get() { - return false; - }, -}); - -ObjectDefineProperty(OutgoingMessage.prototype, 'writableLength', { - __proto__: null, - get() { - return this.outputSize + this[kChunkedLength] + (this[kSocket] ? this[kSocket].writableLength : 0); - }, -}); - -ObjectDefineProperty(OutgoingMessage.prototype, 'writableHighWaterMark', { - __proto__: null, - get() { - return this[kSocket] ? this[kSocket].writableHighWaterMark : this[kHighWaterMark]; - }, -}); - -ObjectDefineProperty(OutgoingMessage.prototype, 'writableCorked', { - __proto__: null, - get() { - return this[kCorked]; - }, -}); - -ObjectDefineProperty(OutgoingMessage.prototype, 'connection', { - __proto__: null, - get: function() { - return this[kSocket]; - }, - set: function(val) { - this.socket = val; - }, -}); - -ObjectDefineProperty(OutgoingMessage.prototype, 'socket', { - __proto__: null, - get: function() { - return this[kSocket]; - }, - set: function(val) { - for (let n = 0; n < this[kCorked]; n++) { - val?.cork(); - this[kSocket]?.uncork(); - } - this[kSocket] = val; - }, -}); - -OutgoingMessage.prototype._renderHeaders = function _renderHeaders() { - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT('render'); - } - - const headersMap = this[kOutHeaders]; - const headers = {}; - - if (headersMap !== null) { - const keys = ObjectKeys(headersMap); - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0, l = keys.length; i < l; i++) { - const key = keys[i]; - headers[headersMap[key][0]] = headersMap[key][1]; - } - } - return headers; -}; - -OutgoingMessage.prototype.cork = function cork() { - this[kCorked]++; - if (this[kSocket]) { - this[kSocket].cork(); - } -}; - -OutgoingMessage.prototype.uncork = function uncork() { - this[kCorked]--; - if (this[kSocket]) { - this[kSocket].uncork(); - } - - if (this[kCorked] || this[kChunkedBuffer].length === 0) { - return; - } - - const len = this[kChunkedLength]; - const buf = this[kChunkedBuffer]; - - assert(this.chunkedEncoding); - - let callbacks; - this._send(len.toString(16), 'latin1', null); - this._send(crlf_buf, null, null); - for (let n = 0; n < buf.length; n += 3) { - this._send(buf[n + 0], buf[n + 1], null); - if (buf[n + 2]) { - callbacks ??= []; - callbacks.push(buf[n + 2]); - } - } - this._send(crlf_buf, null, callbacks.length ? (err) => { - for (const callback of callbacks) { - callback(err); - } - } : null); - - this[kChunkedBuffer].length = 0; - this[kChunkedLength] = 0; -}; - -OutgoingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { - - if (callback) { - this.on('timeout', callback); - } - - if (!this[kSocket]) { - this.once('socket', function socketSetTimeoutOnConnect(socket) { - socket.setTimeout(msecs); - }); - } else { - this[kSocket].setTimeout(msecs); - } - return this; -}; - - -// It's possible that the socket will be destroyed, and removed from -// any messages, before ever calling this. In that case, just skip -// it, since something else is destroying this connection anyway. -OutgoingMessage.prototype.destroy = function destroy(error) { - if (this.destroyed) { - return this; - } - this.destroyed = true; - - this[kErrored] = error; - - if (this[kSocket]) { - this[kSocket].destroy(error); - } else { - this.once('socket', function socketDestroyOnConnect(socket) { - socket.destroy(error); - }); - } - - return this; -}; - - -// This abstract either writing directly to the socket or buffering it. -OutgoingMessage.prototype._send = function _send(data, encoding, callback, byteLength) { - // This is a shameful hack to get the headers and first body chunk onto - // the same packet. Future versions of Node are going to take care of - // this at a lower level and in a more general way. - if (!this._headerSent && this._header !== null) { - // `this._header` can be null if OutgoingMessage is used without a proper Socket - // See: /test/parallel/test-http-outgoing-message-inheritance.js - if (typeof data === 'string' && - (encoding === 'utf8' || encoding === 'latin1' || !encoding)) { - data = this._header + data; - } else { - const header = this._header; - this.outputData.unshift({ - data: header, - encoding: 'latin1', - callback: null, - }); - this.outputSize += header.length; - this._onPendingData(header.length); - } - this._headerSent = true; - } - return this._writeRaw(data, encoding, callback, byteLength); -}; - -OutgoingMessage.prototype._writeRaw = _writeRaw; -function _writeRaw(data, encoding, callback, size) { - const conn = this[kSocket]; - if (conn?.destroyed) { - // The socket was destroyed. If we're still trying to write to it, - // then we haven't gotten the 'close' event yet. - return false; - } - - if (typeof encoding === 'function') { - callback = encoding; - encoding = null; - } - - if (conn && conn._httpMessage === this && conn.writable) { - // There might be pending data in the this.output buffer. - if (this.outputData.length) { - this._flushOutput(conn); - } - // Directly write to socket. - return conn.write(data, encoding, callback); - } - // Buffer, as long as we're not destroyed. - this.outputData.push({ data, encoding, callback }); - this.outputSize += data.length; - this._onPendingData(data.length); - return this.outputSize < this[kHighWaterMark]; -} - - -OutgoingMessage.prototype._storeHeader = _storeHeader; -function _storeHeader(firstLine, headers) { - // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n' - // in the case of response it is: 'HTTP/1.1 200 OK\r\n' - const state = { - connection: false, - contLen: false, - te: false, - date: false, - expect: false, - trailer: false, - header: firstLine, - }; - - if (headers) { - if (headers === this[kOutHeaders]) { - for (const key in headers) { - const entry = headers[key]; - processHeader(this, state, entry[0], entry[1], false); - } - } else if (ArrayIsArray(headers)) { - if (headers.length && ArrayIsArray(headers[0])) { - for (let i = 0; i < headers.length; i++) { - const entry = headers[i]; - processHeader(this, state, entry[0], entry[1], true); - } - } else { - if (headers.length % 2 !== 0) { - throw new ERR_INVALID_ARG_VALUE('headers', headers); - } - - for (let n = 0; n < headers.length; n += 2) { - processHeader(this, state, headers[n + 0], headers[n + 1], true); - } - } - } else { - for (const key in headers) { - if (ObjectHasOwn(headers, key)) { - processHeader(this, state, key, headers[key], true); - } - } - } - } - - let { header } = state; - - // Date header - if (this.sendDate && !state.date) { - header += 'Date: ' + utcDate() + '\r\n'; - } - - // Force the connection to close when the response is a 204 No Content or - // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked" - // header. - // - // RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but - // node.js used to send out a zero chunk anyway to accommodate clients - // that don't have special handling for those responses. - // - // It was pointed out that this might confuse reverse proxies to the point - // of creating security liabilities, so suppress the zero chunk and force - // the connection to close. - if (this.chunkedEncoding && (this.statusCode === 204 || - this.statusCode === 304)) { - debug(this.statusCode + ' response should not use chunked encoding,' + - ' closing connection.'); - this.chunkedEncoding = false; - this.shouldKeepAlive = false; - } - - // keep-alive logic - if (this._removedConnection) { - // shouldKeepAlive is generally true for HTTP/1.1. In that common case, - // even if the connection header isn't sent, we still persist by default. - this._last = !this.shouldKeepAlive; - } else if (!state.connection) { - const shouldSendKeepAlive = this.shouldKeepAlive && - (state.contLen || this.useChunkedEncodingByDefault || this.agent); - if (shouldSendKeepAlive && this.maxRequestsOnConnectionReached) { - header += 'Connection: close\r\n'; - } else if (shouldSendKeepAlive) { - header += 'Connection: keep-alive\r\n'; - if (this._keepAliveTimeout && this._defaultKeepAlive) { - const timeoutSeconds = MathFloor(this._keepAliveTimeout / 1000); - let max = ''; - if (~~this._maxRequestsPerSocket > 0) { - max = `, max=${this._maxRequestsPerSocket}`; - } - header += `Keep-Alive: timeout=${timeoutSeconds}${max}\r\n`; - } - } else { - this._last = true; - header += 'Connection: close\r\n'; - } - } - - if (!state.contLen && !state.te) { - if (!this._hasBody) { - // Make sure we don't end the 0\r\n\r\n at the end of the message. - this.chunkedEncoding = false; - } else if (!this.useChunkedEncodingByDefault) { - this._last = true; - } else if (!state.trailer && - !this._removedContLen && - typeof this._contentLength === 'number') { - header += 'Content-Length: ' + this._contentLength + '\r\n'; - } else if (!this._removedTE) { - header += 'Transfer-Encoding: chunked\r\n'; - this.chunkedEncoding = true; - } else { - // We should only be able to get here if both Content-Length and - // Transfer-Encoding are removed by the user. - // See: test/parallel/test-http-remove-header-stays-removed.js - debug('Both Content-Length and Transfer-Encoding are removed'); - - // We can't keep alive in this case, because with no header info the body - // is defined as all data until the connection is closed. - this._last = true; - } - } - - // Test non-chunked message does not have trailer header set, - // message will be terminated by the first empty line after the - // header fields, regardless of the header fields present in the - // message, and thus cannot contain a message body or 'trailers'. - if (this.chunkedEncoding !== true && state.trailer) { - throw new ERR_HTTP_TRAILER_INVALID(); - } - - this._header = header + '\r\n'; - this._headerSent = false; - - // Wait until the first body chunk, or close(), is sent to flush, - // UNLESS we're sending Expect: 100-continue. - if (state.expect) this._send(''); -} - -function processHeader(self, state, key, value, validate) { - if (validate) - validateHeaderName(key); - - // If key is content-disposition and there is content-length - // encode the value in latin1 - // https://www.rfc-editor.org/rfc/rfc6266#section-4.3 - // Refs: https://github.com/nodejs/node/pull/46528 - if (isContentDispositionField(key) && self._contentLength) { - // The value could be an array here - if (ArrayIsArray(value)) { - for (let i = 0; i < value.length; i++) { - value[i] = Buffer.from(value[i], 'latin1'); - } - } else { - value = Buffer.from(value, 'latin1'); - } - } - - if (ArrayIsArray(value)) { - if ( - (value.length < 2 || !isCookieField(key)) && - (!self[kUniqueHeaders] || !self[kUniqueHeaders].has(key.toLowerCase())) - ) { - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0; i < value.length; i++) - storeHeader(self, state, key, value[i], validate); - return; - } - value = value.join('; '); - } - storeHeader(self, state, key, value, validate); -} - -function storeHeader(self, state, key, value, validate) { - if (validate) - validateHeaderValue(key, value); - state.header += key + ': ' + value + '\r\n'; - matchHeader(self, state, key, value); -} - -function matchHeader(self, state, field, value) { - if (field.length < 4 || field.length > 17) - return; - field = field.toLowerCase(); - switch (field) { - case 'connection': - state.connection = true; - self._removedConnection = false; - if (RE_CONN_CLOSE.test(value)) - self._last = true; - else - self.shouldKeepAlive = true; - break; - case 'transfer-encoding': - state.te = true; - self._removedTE = false; - if (RE_TE_CHUNKED.test(value)) - self.chunkedEncoding = true; - break; - case 'content-length': - state.contLen = true; - self._contentLength = +value; - self._removedContLen = false; - break; - case 'date': - case 'expect': - case 'trailer': - state[field] = true; - break; - case 'keep-alive': - self._defaultKeepAlive = false; - break; - } -} - -const validateHeaderName = assignFunctionName('validateHeaderName', hideStackFrames((name, label) => { - if (typeof name !== 'string' || !name || !checkIsHttpToken(name)) { - throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError(label || 'Header name', name); - } -})); - -const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value) => { - if (value === undefined) { - throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name); - } - if (checkInvalidHeaderChar(value)) { - debug('Header "%s" contains invalid characters', name); - throw new ERR_INVALID_CHAR.HideStackFramesError('header content', name); - } -})); - -function parseUniqueHeadersOption(headers) { - if (!ArrayIsArray(headers)) { - return null; - } - - const unique = new SafeSet(); - const l = headers.length; - for (let i = 0; i < l; i++) { - unique.add(headers[i].toLowerCase()); - } - - return unique; -} - -OutgoingMessage.prototype.setHeader = function setHeader(name, value) { - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT('set'); - } - validateHeaderName(name); - validateHeaderValue(name, value); - - let headers = this[kOutHeaders]; - if (headers === null) - this[kOutHeaders] = headers = { __proto__: null }; - - headers[name.toLowerCase()] = [name, value]; - return this; -}; - -OutgoingMessage.prototype.setHeaders = function setHeaders(headers) { - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT('set'); - } - - - if ( - !headers || - ArrayIsArray(headers) || - typeof headers.keys !== 'function' || - typeof headers.get !== 'function' - ) { - throw new ERR_INVALID_ARG_TYPE('headers', ['Headers', 'Map'], headers); - } - - // Headers object joins multiple cookies with a comma when using - // the getter to retrieve the value, - // unless iterating over the headers directly. - // We also cannot safely split by comma. - // To avoid setHeader overwriting the previous value we push - // set-cookie values in array and set them all at once. - const cookies = []; - - for (const { 0: key, 1: value } of headers) { - if (key === 'set-cookie') { - if (ArrayIsArray(value)) { - cookies.push(...value); - } else { - cookies.push(value); - } - continue; - } - this.setHeader(key, value); - } - if (cookies.length) { - this.setHeader('set-cookie', cookies); - } - - return this; -}; - -OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) { - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT('append'); - } - validateHeaderName(name); - validateHeaderValue(name, value); - - const field = name.toLowerCase(); - const headers = this[kOutHeaders]; - if (headers === null || !headers[field]) { - return this.setHeader(name, value); - } - - // Prepare the field for appending, if required - if (!ArrayIsArray(headers[field][1])) { - headers[field][1] = [headers[field][1]]; - } - - const existingValues = headers[field][1]; - if (ArrayIsArray(value)) { - for (let i = 0, length = value.length; i < length; i++) { - existingValues.push(value[i]); - } - } else { - existingValues.push(value); - } - - return this; -}; - - -OutgoingMessage.prototype.getHeader = function getHeader(name) { - validateString(name, 'name'); - - const headers = this[kOutHeaders]; - if (headers === null) - return; - - const entry = headers[name.toLowerCase()]; - return entry?.[1]; -}; - - -// Returns an array of the names of the current outgoing headers. -OutgoingMessage.prototype.getHeaderNames = function getHeaderNames() { - return this[kOutHeaders] !== null ? ObjectKeys(this[kOutHeaders]) : []; -}; - - -// Returns an array of the names of the current outgoing raw headers. -OutgoingMessage.prototype.getRawHeaderNames = function getRawHeaderNames() { - const headersMap = this[kOutHeaders]; - if (headersMap === null) return []; - - const values = ObjectValues(headersMap); - const headers = Array(values.length); - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0, l = values.length; i < l; i++) { - headers[i] = values[i][0]; - } - - return headers; -}; - - -// Returns a shallow copy of the current outgoing headers. -OutgoingMessage.prototype.getHeaders = function getHeaders() { - const headers = this[kOutHeaders]; - const ret = { __proto__: null }; - if (headers) { - const keys = ObjectKeys(headers); - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0; i < keys.length; ++i) { - const key = keys[i]; - const val = headers[key][1]; - ret[key] = val; - } - } - return ret; -}; - - -OutgoingMessage.prototype.hasHeader = function hasHeader(name) { - validateString(name, 'name'); - return this[kOutHeaders] !== null && - !!this[kOutHeaders][name.toLowerCase()]; -}; - - -OutgoingMessage.prototype.removeHeader = function removeHeader(name) { - validateString(name, 'name'); - - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT('remove'); - } - - const key = name.toLowerCase(); - - switch (key) { - case 'connection': - this._removedConnection = true; - break; - case 'content-length': - this._removedContLen = true; - break; - case 'transfer-encoding': - this._removedTE = true; - break; - case 'date': - this.sendDate = false; - break; - } - - if (this[kOutHeaders] !== null) { - delete this[kOutHeaders][key]; - } -}; - - -OutgoingMessage.prototype._implicitHeader = function _implicitHeader() { - throw new ERR_METHOD_NOT_IMPLEMENTED('_implicitHeader()'); -}; - -ObjectDefineProperty(OutgoingMessage.prototype, 'headersSent', { - __proto__: null, - configurable: true, - enumerable: true, - get: function() { return !!this._header; }, -}); - -ObjectDefineProperty(OutgoingMessage.prototype, 'writableEnded', { - __proto__: null, - get: function() { return this.finished; }, -}); - -ObjectDefineProperty(OutgoingMessage.prototype, 'writableNeedDrain', { - __proto__: null, - get: function() { - return !this.destroyed && !this.finished && this[kNeedDrain]; - }, -}); - -const crlf_buf = Buffer.from('\r\n'); -OutgoingMessage.prototype.write = function write(chunk, encoding, callback) { - if (typeof encoding === 'function') { - callback = encoding; - encoding = null; - } - - const ret = write_(this, chunk, encoding, callback, false); - if (!ret) - this[kNeedDrain] = true; - return ret; -}; - -function onError(msg, err, callback) { - if (msg.destroyed) { - return; - } - - const triggerAsyncId = msg.socket ? msg.socket[async_id_symbol] : undefined; - defaultTriggerAsyncIdScope(triggerAsyncId, - process.nextTick, - emitErrorNt, - msg, - err, - callback); -} - -function emitErrorNt(msg, err, callback) { - callback(err); - if (typeof msg.emit === 'function' && !msg.destroyed) { - msg.emit('error', err); - } -} - -function strictContentLength(msg) { - return ( - msg.strictContentLength && - msg._contentLength != null && - msg._hasBody && - !msg._removedContLen && - !msg.chunkedEncoding && - !msg.hasHeader('transfer-encoding') - ); -} - -function write_(msg, chunk, encoding, callback, fromEnd) { - if (typeof callback !== 'function') - callback = nop; - - if (chunk === null) { - throw new ERR_STREAM_NULL_VALUES(); - } else if (typeof chunk !== 'string' && !isUint8Array(chunk)) { - throw new ERR_INVALID_ARG_TYPE( - 'chunk', ['string', 'Buffer', 'Uint8Array'], chunk); - } - - let err; - if (msg.finished) { - err = new ERR_STREAM_WRITE_AFTER_END(); - } else if (msg.destroyed) { - err = new ERR_STREAM_DESTROYED('write'); - } - - if (err) { - if (!msg.destroyed) { - onError(msg, err, callback); - } else { - process.nextTick(callback, err); - } - return false; - } - - let len; - - if (msg.strictContentLength) { - len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength; - - if ( - strictContentLength(msg) && - (fromEnd ? msg[kBytesWritten] + len !== msg._contentLength : msg[kBytesWritten] + len > msg._contentLength) - ) { - throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(len + msg[kBytesWritten], msg._contentLength); - } - - msg[kBytesWritten] += len; - } - - if (!msg._header) { - if (fromEnd) { - len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength; - msg._contentLength = len; - } - msg._implicitHeader(); - } - - if (!msg._hasBody) { - if (msg[kRejectNonStandardBodyWrites]) { - throw new ERR_HTTP_BODY_NOT_ALLOWED(); - } else { - debug('This type of response MUST NOT have a body. ' + - 'Ignoring write() calls.'); - process.nextTick(callback); - return true; - } - } - - if (!fromEnd && msg.socket && !msg.socket.writableCorked) { - msg.socket.cork(); - process.nextTick(connectionCorkNT, msg.socket); - } - - let ret; - if (msg.chunkedEncoding && chunk.length !== 0) { - len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength; - if (msg[kCorked] && msg._headerSent) { - msg[kChunkedBuffer].push(chunk, encoding, callback); - msg[kChunkedLength] += len; - ret = msg[kChunkedLength] < msg[kHighWaterMark]; - } else { - msg._send(len.toString(16), 'latin1', null); - msg._send(crlf_buf, null, null); - msg._send(chunk, encoding, null, len); - ret = msg._send(crlf_buf, null, callback); - } - } else { - ret = msg._send(chunk, encoding, callback, len); - } - - debug('write ret = ' + ret); - return ret; -} - - -function connectionCorkNT(conn) { - conn.uncork(); -} - -OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { - this._trailer = ''; - const keys = ObjectKeys(headers); - const isArray = ArrayIsArray(headers); - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0, l = keys.length; i < l; i++) { - let field, value; - const key = keys[i]; - if (isArray) { - field = headers[key][0]; - value = headers[key][1]; - } else { - field = key; - value = headers[key]; - } - validateHeaderName(field, 'Trailer name'); - - // Check if the field must be sent several times - const isArrayValue = ArrayIsArray(value); - if ( - isArrayValue && value.length > 1 && - (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase())) - ) { - for (let j = 0, l = value.length; j < l; j++) { - if (checkInvalidHeaderChar(value[j])) { - debug('Trailer "%s"[%d] contains invalid characters', field, j); - throw new ERR_INVALID_CHAR('trailer content', field); - } - this._trailer += field + ': ' + value[j] + '\r\n'; - } - } else { - if (isArrayValue) { - value = value.join('; '); - } - - if (checkInvalidHeaderChar(value)) { - debug('Trailer "%s" contains invalid characters', field); - throw new ERR_INVALID_CHAR('trailer content', field); - } - this._trailer += field + ': ' + value + '\r\n'; - } - } -}; - -function onFinish(outmsg) { - if (outmsg?.socket?._hadError) return; - outmsg.emit('finish'); -} - -OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { - if (typeof chunk === 'function') { - callback = chunk; - chunk = null; - encoding = null; - } else if (typeof encoding === 'function') { - callback = encoding; - encoding = null; - } - - if (chunk) { - if (this.finished) { - onError(this, - new ERR_STREAM_WRITE_AFTER_END(), - typeof callback !== 'function' ? nop : callback); - return this; - } - - if (this[kSocket]) { - this[kSocket].cork(); - } - - write_(this, chunk, encoding, null, true); - } else if (this.finished) { - if (typeof callback === 'function') { - if (!this.writableFinished) { - this.on('finish', callback); - } else { - callback(new ERR_STREAM_ALREADY_FINISHED('end')); - } - } - return this; - } else if (!this._header) { - if (this[kSocket]) { - this[kSocket].cork(); - } - - this._contentLength = 0; - this._implicitHeader(); - } - - if (typeof callback === 'function') - this.once('finish', callback); - - if (strictContentLength(this) && this[kBytesWritten] !== this._contentLength) { - throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(this[kBytesWritten], this._contentLength); - } - - const finish = onFinish.bind(undefined, this); - - if (this._hasBody && this.chunkedEncoding) { - this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finish); - } else if (!this._headerSent || this.writableLength || chunk) { - this._send('', 'latin1', finish); - } else { - process.nextTick(finish); - } - - if (this[kSocket]) { - // Fully uncork connection on end(). - this[kSocket]._writableState.corked = 1; - this[kSocket].uncork(); - } - this[kCorked] = 1; - this.uncork(); - - this.finished = true; - - // There is the first message on the outgoing queue, and we've sent - // everything to the socket. - debug('outgoing message end.'); - if (this.outputData.length === 0 && - this[kSocket] && - this[kSocket]._httpMessage === this) { - this._finish(); - } - - return this; -}; - - -// This function is called once all user data are flushed to the socket. -// Note that it has a chance that the socket is not drained. -OutgoingMessage.prototype._finish = function _finish() { - assert(this[kSocket]); - this.emit('prefinish'); -}; - - -// This logic is probably a bit confusing. Let me explain a bit: -// -// In both HTTP servers and clients it is possible to queue up several -// outgoing messages. This is easiest to imagine in the case of a client. -// Take the following situation: -// -// req1 = client.request('GET', '/'); -// req2 = client.request('POST', '/'); -// -// When the user does -// -// req2.write('hello world\n'); -// -// it's possible that the first request has not been completely flushed to -// the socket yet. Thus the outgoing messages need to be prepared to queue -// up data internally before sending it on further to the socket's queue. -// -// This function, _flush(), is called by both the Server and Client -// to attempt to flush any pending messages out to the socket. -OutgoingMessage.prototype._flush = function _flush() { - const socket = this[kSocket]; - - if (socket?.writable) { - // There might be remaining data in this.output; write it out - const ret = this._flushOutput(socket); - - if (this.finished) { - // This is a queue to the server or client to bring in the next this. - this._finish(); - } else if (ret && this[kNeedDrain]) { - this[kNeedDrain] = false; - this.emit('drain'); - } - } -}; - -OutgoingMessage.prototype._flushOutput = function _flushOutput(socket) { - const outputLength = this.outputData.length; - if (outputLength <= 0) - return undefined; - - const outputData = this.outputData; - socket.cork(); - let ret; - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0; i < outputLength; i++) { - const { data, encoding, callback } = outputData[i]; - // Avoid any potential ref to Buffer in new generation from old generation - outputData[i].data = null; - ret = socket.write(data, encoding, callback); - } - socket.uncork(); - - this.outputData = []; - this._onPendingData(-this.outputSize); - this.outputSize = 0; - - return ret; -}; - - -OutgoingMessage.prototype.flushHeaders = function flushHeaders() { - if (!this._header) { - this._implicitHeader(); - } - - // Force-flush the headers. - this._send(''); -}; - -OutgoingMessage.prototype.pipe = function pipe() { - // OutgoingMessage should be write-only. Piping from it is disabled. - this.emit('error', new ERR_STREAM_CANNOT_PIPE()); -}; - -OutgoingMessage.prototype[EE.captureRejectionSymbol] = -function(err, event) { - this.destroy(err); -}; + kHighWaterMark, + kUniqueHeaders, + parseUniqueHeadersOption, + validateHeaderName, + validateHeaderValue, + OutgoingMessage, +} = require('internal/http/outgoing'); module.exports = { kHighWaterMark, @@ -1202,3 +38,6 @@ module.exports = { validateHeaderValue, OutgoingMessage, }; + +process.emitWarning('The _http_outgoing module is deprecated. Use `node:http` instead.', + 'DeprecationWarning', 'DEP0195'); diff --git a/lib/_http_server.js b/lib/_http_server.js index b728f73c419772..ab8a1563ed7899 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -22,1202 +22,17 @@ 'use strict'; const { - ArrayIsArray, - Error, - MathMin, - ObjectKeys, - ObjectSetPrototypeOf, - ReflectApply, - Symbol, - SymbolAsyncDispose, - SymbolFor, -} = primordials; - -const net = require('net'); -const EE = require('events'); -const assert = require('internal/assert'); -const { - parsers, - freeParser, - continueExpression, - chunkExpression, - kIncomingMessage, - HTTPParser, - isLenient, - _checkInvalidHeaderChar: checkInvalidHeaderChar, - prepareError, -} = require('_http_common'); -const { ConnectionsList } = internalBinding('http_parser'); -const { - kUniqueHeaders, - parseUniqueHeadersOption, - OutgoingMessage, -} = require('_http_outgoing'); -const { - kOutHeaders, - kNeedDrain, - isTraceHTTPEnabled, - traceBegin, - traceEnd, - getNextTraceEventId, -} = require('internal/http'); -const { - defaultTriggerAsyncIdScope, - getOrSetAsyncId, -} = require('internal/async_hooks'); -const { IncomingMessage } = require('_http_incoming'); -const { - ConnResetException, - codes: { - ERR_HTTP_HEADERS_SENT, - ERR_HTTP_INVALID_STATUS_CODE, - ERR_HTTP_REQUEST_TIMEOUT, - ERR_HTTP_SOCKET_ASSIGNED, - ERR_HTTP_SOCKET_ENCODING, - ERR_INVALID_ARG_VALUE, - ERR_INVALID_CHAR, - ERR_OUT_OF_RANGE, - }, -} = require('internal/errors'); -const { - assignFunctionName, - kEmptyObject, - promisify, -} = require('internal/util'); -const { - validateInteger, - validateBoolean, - validateLinkHeaderValue, - validateObject, -} = require('internal/validators'); -const Buffer = require('buffer').Buffer; -const { setInterval, clearInterval } = require('timers'); -let debug = require('internal/util/debuglog').debuglog('http', (fn) => { - debug = fn; -}); - -const dc = require('diagnostics_channel'); -const onRequestStartChannel = dc.channel('http.server.request.start'); -const onResponseCreatedChannel = dc.channel('http.server.response.created'); -const onResponseFinishChannel = dc.channel('http.server.response.finish'); - -const kServerResponse = Symbol('ServerResponse'); -const kServerResponseStatistics = Symbol('ServerResponseStatistics'); - -const { - hasObserver, - startPerf, - stopPerf, -} = require('internal/perf/observe'); - -const STATUS_CODES = { - 100: 'Continue', // RFC 7231 6.2.1 - 101: 'Switching Protocols', // RFC 7231 6.2.2 - 102: 'Processing', // RFC 2518 10.1 (obsoleted by RFC 4918) - 103: 'Early Hints', // RFC 8297 2 - 200: 'OK', // RFC 7231 6.3.1 - 201: 'Created', // RFC 7231 6.3.2 - 202: 'Accepted', // RFC 7231 6.3.3 - 203: 'Non-Authoritative Information', // RFC 7231 6.3.4 - 204: 'No Content', // RFC 7231 6.3.5 - 205: 'Reset Content', // RFC 7231 6.3.6 - 206: 'Partial Content', // RFC 7233 4.1 - 207: 'Multi-Status', // RFC 4918 11.1 - 208: 'Already Reported', // RFC 5842 7.1 - 226: 'IM Used', // RFC 3229 10.4.1 - 300: 'Multiple Choices', // RFC 7231 6.4.1 - 301: 'Moved Permanently', // RFC 7231 6.4.2 - 302: 'Found', // RFC 7231 6.4.3 - 303: 'See Other', // RFC 7231 6.4.4 - 304: 'Not Modified', // RFC 7232 4.1 - 305: 'Use Proxy', // RFC 7231 6.4.5 - 307: 'Temporary Redirect', // RFC 7231 6.4.7 - 308: 'Permanent Redirect', // RFC 7238 3 - 400: 'Bad Request', // RFC 7231 6.5.1 - 401: 'Unauthorized', // RFC 7235 3.1 - 402: 'Payment Required', // RFC 7231 6.5.2 - 403: 'Forbidden', // RFC 7231 6.5.3 - 404: 'Not Found', // RFC 7231 6.5.4 - 405: 'Method Not Allowed', // RFC 7231 6.5.5 - 406: 'Not Acceptable', // RFC 7231 6.5.6 - 407: 'Proxy Authentication Required', // RFC 7235 3.2 - 408: 'Request Timeout', // RFC 7231 6.5.7 - 409: 'Conflict', // RFC 7231 6.5.8 - 410: 'Gone', // RFC 7231 6.5.9 - 411: 'Length Required', // RFC 7231 6.5.10 - 412: 'Precondition Failed', // RFC 7232 4.2 - 413: 'Payload Too Large', // RFC 7231 6.5.11 - 414: 'URI Too Long', // RFC 7231 6.5.12 - 415: 'Unsupported Media Type', // RFC 7231 6.5.13 - 416: 'Range Not Satisfiable', // RFC 7233 4.4 - 417: 'Expectation Failed', // RFC 7231 6.5.14 - 418: 'I\'m a Teapot', // RFC 7168 2.3.3 - 421: 'Misdirected Request', // RFC 7540 9.1.2 - 422: 'Unprocessable Entity', // RFC 4918 11.2 - 423: 'Locked', // RFC 4918 11.3 - 424: 'Failed Dependency', // RFC 4918 11.4 - 425: 'Too Early', // RFC 8470 5.2 - 426: 'Upgrade Required', // RFC 2817 and RFC 7231 6.5.15 - 428: 'Precondition Required', // RFC 6585 3 - 429: 'Too Many Requests', // RFC 6585 4 - 431: 'Request Header Fields Too Large', // RFC 6585 5 - 451: 'Unavailable For Legal Reasons', // RFC 7725 3 - 500: 'Internal Server Error', // RFC 7231 6.6.1 - 501: 'Not Implemented', // RFC 7231 6.6.2 - 502: 'Bad Gateway', // RFC 7231 6.6.3 - 503: 'Service Unavailable', // RFC 7231 6.6.4 - 504: 'Gateway Timeout', // RFC 7231 6.6.5 - 505: 'HTTP Version Not Supported', // RFC 7231 6.6.6 - 506: 'Variant Also Negotiates', // RFC 2295 8.1 - 507: 'Insufficient Storage', // RFC 4918 11.5 - 508: 'Loop Detected', // RFC 5842 7.2 - 509: 'Bandwidth Limit Exceeded', - 510: 'Not Extended', // RFC 2774 7 - 511: 'Network Authentication Required', // RFC 6585 6 -}; - -const kOnExecute = HTTPParser.kOnExecute | 0; -const kOnTimeout = HTTPParser.kOnTimeout | 0; -const kLenientAll = HTTPParser.kLenientAll | 0; -const kLenientNone = HTTPParser.kLenientNone | 0; -const kConnections = Symbol('http.server.connections'); -const kConnectionsCheckingInterval = Symbol('http.server.connectionsCheckingInterval'); - -const HTTP_SERVER_TRACE_EVENT_NAME = 'http.server.request'; -// TODO(jazelly): make this configurable -const HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER = 1000; - -class HTTPServerAsyncResource { - constructor(type, socket) { - this.type = type; - this.socket = socket; - } -} - -function ServerResponse(req, options) { - OutgoingMessage.call(this, options); - - if (req.method === 'HEAD') this._hasBody = false; - - this.req = req; - this.sendDate = true; - this._sent100 = false; - this._expect_continue = false; - - if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { - this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te); - this.shouldKeepAlive = false; - } - - if (hasObserver('http')) { - startPerf(this, kServerResponseStatistics, { - type: 'http', - name: 'HttpRequest', - detail: { - req: { - method: req.method, - url: req.url, - headers: req.headers, - }, - }, - }); - } - if (isTraceHTTPEnabled()) { - this._traceEventId = getNextTraceEventId(); - traceBegin(HTTP_SERVER_TRACE_EVENT_NAME, this._traceEventId); - } - if (onResponseCreatedChannel.hasSubscribers) { - onResponseCreatedChannel.publish({ - request: req, - response: this, - }); - } -} -ObjectSetPrototypeOf(ServerResponse.prototype, OutgoingMessage.prototype); -ObjectSetPrototypeOf(ServerResponse, OutgoingMessage); - -ServerResponse.prototype._finish = function _finish() { - if (this[kServerResponseStatistics] && hasObserver('http')) { - stopPerf(this, kServerResponseStatistics, { - detail: { - res: { - statusCode: this.statusCode, - statusMessage: this.statusMessage, - headers: typeof this.getHeaders === 'function' ? this.getHeaders() : {}, - }, - }, - }); - } - OutgoingMessage.prototype._finish.call(this); - if (isTraceHTTPEnabled() && typeof this._traceEventId === 'number') { - const data = { - url: this.req?.url, - statusCode: this.statusCode, - }; - traceEnd(HTTP_SERVER_TRACE_EVENT_NAME, this._traceEventId, data); - } -}; - - -ServerResponse.prototype.statusCode = 200; -ServerResponse.prototype.statusMessage = undefined; - -function onServerResponseClose() { - // EventEmitter.emit makes a copy of the 'close' listeners array before - // calling the listeners. detachSocket() unregisters onServerResponseClose - // but if detachSocket() is called, directly or indirectly, by a 'close' - // listener, onServerResponseClose is still in that copy of the listeners - // array. That is, in the example below, b still gets called even though - // it's been removed by a: - // - // const EventEmitter = require('events'); - // const obj = new EventEmitter(); - // obj.on('event', a); - // obj.on('event', b); - // function a() { obj.removeListener('event', b) } - // function b() { throw "BAM!" } - // obj.emit('event'); // throws - // - // Ergo, we need to deal with stale 'close' events and handle the case - // where the ServerResponse object has already been deconstructed. - // Fortunately, that requires only a single if check. :-) - if (this._httpMessage) { - emitCloseNT(this._httpMessage); - } -} - -ServerResponse.prototype.assignSocket = function assignSocket(socket) { - if (socket._httpMessage) { - throw new ERR_HTTP_SOCKET_ASSIGNED(); - } - socket._httpMessage = this; - socket.on('close', onServerResponseClose); - this.socket = socket; - this.emit('socket', socket); - this._flush(); -}; - -ServerResponse.prototype.detachSocket = function detachSocket(socket) { - assert(socket._httpMessage === this); - socket.removeListener('close', onServerResponseClose); - socket._httpMessage = null; - this.socket = null; -}; - -ServerResponse.prototype.writeContinue = function writeContinue(cb) { - this._writeRaw('HTTP/1.1 100 Continue\r\n\r\n', 'ascii', cb); - this._sent100 = true; -}; - -ServerResponse.prototype.writeProcessing = function writeProcessing(cb) { - this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n', 'ascii', cb); -}; - -ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) { - let head = 'HTTP/1.1 103 Early Hints\r\n'; - - validateObject(hints, 'hints'); - - if (hints.link === null || hints.link === undefined) { - return; - } - - const link = validateLinkHeaderValue(hints.link); - - if (link.length === 0) { - return; - } - - head += 'Link: ' + link + '\r\n'; - - for (const key of ObjectKeys(hints)) { - if (key !== 'link') { - head += key + ': ' + hints[key] + '\r\n'; - } - } - - head += '\r\n'; - - this._writeRaw(head, 'ascii', cb); -}; - -ServerResponse.prototype._implicitHeader = function _implicitHeader() { - this.writeHead(this.statusCode); -}; - -ServerResponse.prototype.writeHead = writeHead; -function writeHead(statusCode, reason, obj) { - - if (this._header) { - throw new ERR_HTTP_HEADERS_SENT('write'); - } - - const originalStatusCode = statusCode; - - statusCode |= 0; - if (statusCode < 100 || statusCode > 999) { - throw new ERR_HTTP_INVALID_STATUS_CODE(originalStatusCode); - } - - - if (typeof reason === 'string') { - // writeHead(statusCode, reasonPhrase[, headers]) - this.statusMessage = reason; - } else { - // writeHead(statusCode[, headers]) - this.statusMessage ||= STATUS_CODES[statusCode] || 'unknown'; - obj ??= reason; - } - this.statusCode = statusCode; - - let headers; - if (this[kOutHeaders]) { - // Slow-case: when progressive API and header fields are passed. - let k; - if (ArrayIsArray(obj)) { - if (obj.length % 2 !== 0) { - throw new ERR_INVALID_ARG_VALUE('headers', obj); - } - - // Headers in obj should override previous headers but still - // allow explicit duplicates. To do so, we first remove any - // existing conflicts, then use appendHeader. - - for (let n = 0; n < obj.length; n += 2) { - k = obj[n + 0]; - this.removeHeader(k); - } - - for (let n = 0; n < obj.length; n += 2) { - k = obj[n + 0]; - if (k) this.appendHeader(k, obj[n + 1]); - } - } else if (obj) { - const keys = ObjectKeys(obj); - // Retain for(;;) loop for performance reasons - // Refs: https://github.com/nodejs/node/pull/30958 - for (let i = 0; i < keys.length; i++) { - k = keys[i]; - if (k) this.setHeader(k, obj[k]); - } - } - // Only progressive api is used - headers = this[kOutHeaders]; - } else { - // Only writeHead() called - headers = obj; - } - - if (checkInvalidHeaderChar(this.statusMessage)) - throw new ERR_INVALID_CHAR('statusMessage'); - - const statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`; - - if (statusCode === 204 || statusCode === 304 || - (statusCode >= 100 && statusCode <= 199)) { - // RFC 2616, 10.2.5: - // The 204 response MUST NOT include a message-body, and thus is always - // terminated by the first empty line after the header fields. - // RFC 2616, 10.3.5: - // The 304 response MUST NOT contain a message-body, and thus is always - // terminated by the first empty line after the header fields. - // RFC 2616, 10.1 Informational 1xx: - // This class of status code indicates a provisional response, - // consisting only of the Status-Line and optional headers, and is - // terminated by an empty line. - this._hasBody = false; - } - - // Don't keep alive connections where the client expects 100 Continue - // but we sent a final status; they may put extra bytes on the wire. - if (this._expect_continue && !this._sent100) { - this.shouldKeepAlive = false; - } - - this._storeHeader(statusLine, headers); - - return this; -} - -// Docs-only deprecated: DEP0063 -ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead; - -function storeHTTPOptions(options) { - this[kIncomingMessage] = options.IncomingMessage || IncomingMessage; - this[kServerResponse] = options.ServerResponse || ServerResponse; - - const maxHeaderSize = options.maxHeaderSize; - if (maxHeaderSize !== undefined) - validateInteger(maxHeaderSize, 'maxHeaderSize', 0); - this.maxHeaderSize = maxHeaderSize; - - const insecureHTTPParser = options.insecureHTTPParser; - if (insecureHTTPParser !== undefined) - validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser'); - this.insecureHTTPParser = insecureHTTPParser; - - const requestTimeout = options.requestTimeout; - if (requestTimeout !== undefined) { - validateInteger(requestTimeout, 'requestTimeout', 0); - this.requestTimeout = requestTimeout; - } else { - this.requestTimeout = 300_000; // 5 minutes - } - - const headersTimeout = options.headersTimeout; - if (headersTimeout !== undefined) { - validateInteger(headersTimeout, 'headersTimeout', 0); - this.headersTimeout = headersTimeout; - } else { - this.headersTimeout = MathMin(60_000, this.requestTimeout); // Minimum between 60 seconds or requestTimeout - } - - if (this.requestTimeout > 0 && this.headersTimeout > 0 && this.headersTimeout > this.requestTimeout) { - throw new ERR_OUT_OF_RANGE('headersTimeout', '<= requestTimeout', headersTimeout); - } - - const keepAliveTimeout = options.keepAliveTimeout; - if (keepAliveTimeout !== undefined) { - validateInteger(keepAliveTimeout, 'keepAliveTimeout', 0); - this.keepAliveTimeout = keepAliveTimeout; - } else { - this.keepAliveTimeout = 5_000; // 5 seconds; - } - - const connectionsCheckingInterval = options.connectionsCheckingInterval; - if (connectionsCheckingInterval !== undefined) { - validateInteger(connectionsCheckingInterval, 'connectionsCheckingInterval', 0); - this.connectionsCheckingInterval = connectionsCheckingInterval; - } else { - this.connectionsCheckingInterval = 30_000; // 30 seconds - } - - const requireHostHeader = options.requireHostHeader; - if (requireHostHeader !== undefined) { - validateBoolean(requireHostHeader, 'options.requireHostHeader'); - this.requireHostHeader = requireHostHeader; - } else { - this.requireHostHeader = true; - } - - const joinDuplicateHeaders = options.joinDuplicateHeaders; - if (joinDuplicateHeaders !== undefined) { - validateBoolean(joinDuplicateHeaders, 'options.joinDuplicateHeaders'); - } - this.joinDuplicateHeaders = joinDuplicateHeaders; - - const rejectNonStandardBodyWrites = options.rejectNonStandardBodyWrites; - if (rejectNonStandardBodyWrites !== undefined) { - validateBoolean(rejectNonStandardBodyWrites, 'options.rejectNonStandardBodyWrites'); - this.rejectNonStandardBodyWrites = rejectNonStandardBodyWrites; - } else { - this.rejectNonStandardBodyWrites = false; - } -} - -function setupConnectionsTracking() { - // Start connection handling - this[kConnections] ||= new ConnectionsList(); - - if (this[kConnectionsCheckingInterval]) { - clearInterval(this[kConnectionsCheckingInterval]); - } - // This checker is started without checking whether any headersTimeout or requestTimeout is non zero - // otherwise it would not be started if such timeouts are modified after createServer. - this[kConnectionsCheckingInterval] = - setInterval(checkConnections.bind(this), this.connectionsCheckingInterval).unref(); -} - -function httpServerPreClose(server) { - server.closeIdleConnections(); - clearInterval(server[kConnectionsCheckingInterval]); -} - -function Server(options, requestListener) { - if (!(this instanceof Server)) return new Server(options, requestListener); - - if (typeof options === 'function') { - requestListener = options; - options = kEmptyObject; - } else if (options == null) { - options = kEmptyObject; - } else { - validateObject(options, 'options'); - } - - storeHTTPOptions.call(this, options); - net.Server.call( - this, - { allowHalfOpen: true, noDelay: options.noDelay ?? true, - keepAlive: options.keepAlive, - keepAliveInitialDelay: options.keepAliveInitialDelay, - highWaterMark: options.highWaterMark }); - - if (requestListener) { - this.on('request', requestListener); - } - - // Similar option to this. Too lazy to write my own docs. - // http://www.squid-cache.org/Doc/config/half_closed_clients/ - // https://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F - this.httpAllowHalfOpen = false; - - this.on('connection', connectionListener); - this.on('listening', setupConnectionsTracking); - - this.timeout = 0; - this.maxHeadersCount = null; - this.maxRequestsPerSocket = 0; - - this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders); -} -ObjectSetPrototypeOf(Server.prototype, net.Server.prototype); -ObjectSetPrototypeOf(Server, net.Server); - -Server.prototype.close = function close() { - httpServerPreClose(this); - ReflectApply(net.Server.prototype.close, this, arguments); - return this; -}; - -Server.prototype[SymbolAsyncDispose] = assignFunctionName(SymbolAsyncDispose, async function() { - await promisify(this.close).call(this); -}); - -Server.prototype.closeAllConnections = function closeAllConnections() { - if (!this[kConnections]) { - return; - } - - const connections = this[kConnections].all(); - - for (let i = 0, l = connections.length; i < l; i++) { - connections[i].socket.destroy(); - } -}; - -Server.prototype.closeIdleConnections = function closeIdleConnections() { - if (!this[kConnections]) { - return; - } - - const connections = this[kConnections].idle(); - - for (let i = 0, l = connections.length; i < l; i++) { - if (connections[i].socket._httpMessage && !connections[i].socket._httpMessage.finished) { - continue; - } - - connections[i].socket.destroy(); - } -}; - -Server.prototype.setTimeout = function setTimeout(msecs, callback) { - this.timeout = msecs; - if (callback) - this.on('timeout', callback); - return this; -}; - -Server.prototype[EE.captureRejectionSymbol] = -assignFunctionName(EE.captureRejectionSymbol, function(err, event, ...args) { - switch (event) { - case 'request': { - const { 1: res } = args; - if (!res.headersSent && !res.writableEnded) { - // Don't leak headers. - const names = res.getHeaderNames(); - for (let i = 0; i < names.length; i++) { - res.removeHeader(names[i]); - } - res.statusCode = 500; - res.end(STATUS_CODES[500]); - } else { - res.destroy(); - } - break; - } - default: - net.Server.prototype[SymbolFor('nodejs.rejection')] - .apply(this, arguments); - } -}); - -function checkConnections() { - if (this.headersTimeout === 0 && this.requestTimeout === 0) { - return; - } - - const expired = this[kConnections].expired(this.headersTimeout, this.requestTimeout); - - for (let i = 0; i < expired.length; i++) { - const socket = expired[i].socket; - - if (socket) { - onRequestTimeout(socket); - } - } -} - -function connectionListener(socket) { - defaultTriggerAsyncIdScope( - getOrSetAsyncId(socket), connectionListenerInternal, this, socket, - ); -} - -function connectionListenerInternal(server, socket) { - debug('SERVER new http connection'); - - // Ensure that the server property of the socket is correctly set. - // See https://github.com/nodejs/node/issues/13435 - socket.server = server; - - // If the user has added a listener to the server, - // request, or response, then it's their responsibility. - // otherwise, destroy on timeout by default - if (server.timeout && typeof socket.setTimeout === 'function') - socket.setTimeout(server.timeout); - socket.on('timeout', socketOnTimeout); - - const parser = parsers.alloc(); - - const lenient = server.insecureHTTPParser === undefined ? - isLenient() : server.insecureHTTPParser; - - // TODO(addaleax): This doesn't play well with the - // `async_hooks.currentResource()` proposal, see - // https://github.com/nodejs/node/pull/21313 - parser.initialize( - HTTPParser.REQUEST, - new HTTPServerAsyncResource('HTTPINCOMINGMESSAGE', socket), - server.maxHeaderSize || 0, - lenient ? kLenientAll : kLenientNone, - server[kConnections], - ); - parser.socket = socket; - socket.parser = parser; - - // Propagate headers limit from server instance to parser - if (typeof server.maxHeadersCount === 'number') { - parser.maxHeaderPairs = server.maxHeadersCount << 1; - } - - const state = { - onData: null, - onEnd: null, - onClose: null, - onDrain: null, - outgoing: [], - incoming: [], - // `outgoingData` is an approximate amount of bytes queued through all - // inactive responses. If more data than the high watermark is queued - we - // need to pause TCP socket/HTTP parser, and wait until the data will be - // sent to the client. - outgoingData: 0, - requestsCount: 0, - keepAliveTimeoutSet: false, - }; - state.onData = socketOnData.bind(undefined, - server, socket, parser, state); - state.onEnd = socketOnEnd.bind(undefined, - server, socket, parser, state); - state.onClose = socketOnClose.bind(undefined, - socket, state); - state.onDrain = socketOnDrain.bind(undefined, - socket, state); - socket.on('data', state.onData); - socket.on('error', socketOnError); - socket.on('end', state.onEnd); - socket.on('close', state.onClose); - socket.on('drain', state.onDrain); - parser.onIncoming = parserOnIncoming.bind(undefined, - server, socket, state); - - // We are consuming socket, so it won't get any actual data - socket.on('resume', onSocketResume); - socket.on('pause', onSocketPause); - - // Overrides to unconsume on `data`, `readable` listeners - socket.on = generateSocketListenerWrapper('on'); - socket.addListener = generateSocketListenerWrapper('addListener'); - socket.prependListener = generateSocketListenerWrapper('prependListener'); - socket.setEncoding = socketSetEncoding; - - // We only consume the socket if it has never been consumed before. - if (socket._handle?.isStreamBase && - !socket._handle._consumed) { - parser._consumed = true; - socket._handle._consumed = true; - parser.consume(socket._handle); - } - parser[kOnExecute] = - onParserExecute.bind(undefined, - server, socket, parser, state); - - parser[kOnTimeout] = - onParserTimeout.bind(undefined, - server, socket); - - socket._paused = false; -} - -function socketSetEncoding() { - throw new ERR_HTTP_SOCKET_ENCODING(); -} - -function updateOutgoingData(socket, state, delta) { - state.outgoingData += delta; - socketOnDrain(socket, state); -} - -function socketOnDrain(socket, state) { - const needPause = state.outgoingData > socket.writableHighWaterMark; - - // If we previously paused, then start reading again. - if (socket._paused && !needPause) { - socket._paused = false; - if (socket.parser) - socket.parser.resume(); - socket.resume(); - } - - const msg = socket._httpMessage; - if (msg && !msg.finished && msg[kNeedDrain]) { - msg[kNeedDrain] = false; - msg.emit('drain'); - } -} - -function socketOnTimeout() { - const req = this.parser?.incoming; - const reqTimeout = req && !req.complete && req.emit('timeout', this); - const res = this._httpMessage; - const resTimeout = res && res.emit('timeout', this); - const serverTimeout = this.server.emit('timeout', this); - - if (!reqTimeout && !resTimeout && !serverTimeout) - this.destroy(); -} - -function socketOnClose(socket, state) { - debug('server socket close'); - freeParser(socket.parser, null, socket); - abortIncoming(state.incoming); -} - -function abortIncoming(incoming) { - while (incoming.length) { - const req = incoming.shift(); - req.destroy(new ConnResetException('aborted')); - } - // Abort socket._httpMessage ? -} - -function socketOnEnd(server, socket, parser, state) { - const ret = parser.finish(); - - if (ret instanceof Error) { - debug('parse error'); - // socketOnError has additional logic and will call socket.destroy(err). - socketOnError.call(socket, ret); - } else if (!server.httpAllowHalfOpen) { - socket.end(); - } else if (state.outgoing.length) { - state.outgoing[state.outgoing.length - 1]._last = true; - } else if (socket._httpMessage) { - socket._httpMessage._last = true; - } else { - socket.end(); - } -} - -function socketOnData(server, socket, parser, state, d) { - assert(!socket._paused); - debug('SERVER socketOnData %d', d.length); - - const ret = parser.execute(d); - onParserExecuteCommon(server, socket, parser, state, ret, d); -} - -function onRequestTimeout(socket) { - // socketOnError has additional logic and will call socket.destroy(err). - socketOnError.call(socket, new ERR_HTTP_REQUEST_TIMEOUT()); -} - -function onParserExecute(server, socket, parser, state, ret) { - // When underlying `net.Socket` instance is consumed - no - // `data` events are emitted, and thus `socket.setTimeout` fires the - // callback even if the data is constantly flowing into the socket. - // See, https://github.com/nodejs/node/commit/ec2822adaad76b126b5cccdeaa1addf2376c9aa6 - socket._unrefTimer(); - debug('SERVER socketOnParserExecute %d', ret); - onParserExecuteCommon(server, socket, parser, state, ret, undefined); -} - -function onParserTimeout(server, socket) { - const serverTimeout = server.emit('timeout', socket); - - if (!serverTimeout) - socket.destroy(); -} - -const noop = () => {}; -const badRequestResponse = Buffer.from( - `HTTP/1.1 400 ${STATUS_CODES[400]}\r\n` + - 'Connection: close\r\n\r\n', 'ascii', -); -const requestTimeoutResponse = Buffer.from( - `HTTP/1.1 408 ${STATUS_CODES[408]}\r\n` + - 'Connection: close\r\n\r\n', 'ascii', -); -const requestHeaderFieldsTooLargeResponse = Buffer.from( - `HTTP/1.1 431 ${STATUS_CODES[431]}\r\n` + - 'Connection: close\r\n\r\n', 'ascii', -); - -const requestChunkExtensionsTooLargeResponse = Buffer.from( - `HTTP/1.1 413 ${STATUS_CODES[413]}\r\n` + - 'Connection: close\r\n\r\n', 'ascii', -); - -function socketOnError(e) { - // Ignore further errors - this.removeListener('error', socketOnError); - - if (this.listenerCount('error', noop) === 0) { - this.on('error', noop); - } - - if (!this.server.emit('clientError', e, this)) { - // Caution must be taken to avoid corrupting the remote peer. - // Reply an error segment if there is no in-flight `ServerResponse`, - // or no data of the in-flight one has been written yet to this socket. - if (this.writable && - (!this._httpMessage || !this._httpMessage._headerSent)) { - let response; - - switch (e.code) { - case 'HPE_HEADER_OVERFLOW': - response = requestHeaderFieldsTooLargeResponse; - break; - case 'HPE_CHUNK_EXTENSIONS_OVERFLOW': - response = requestChunkExtensionsTooLargeResponse; - break; - case 'ERR_HTTP_REQUEST_TIMEOUT': - response = requestTimeoutResponse; - break; - default: - response = badRequestResponse; - break; - } - - this.write(response); - } - this.destroy(e); - } -} - -function onParserExecuteCommon(server, socket, parser, state, ret, d) { - resetSocketTimeout(server, socket, state); - - if (ret instanceof Error) { - prepareError(ret, parser, d); - debug('parse error', ret); - socketOnError.call(socket, ret); - } else if (parser.incoming?.upgrade) { - // Upgrade or CONNECT - const req = parser.incoming; - debug('SERVER upgrade or connect', req.method); - - d ||= parser.getCurrentBuffer(); - - socket.removeListener('data', state.onData); - socket.removeListener('end', state.onEnd); - socket.removeListener('close', state.onClose); - socket.removeListener('drain', state.onDrain); - socket.removeListener('error', socketOnError); - socket.removeListener('timeout', socketOnTimeout); - unconsume(parser, socket); - parser.finish(); - freeParser(parser, req, socket); - parser = null; - - const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; - if (eventName === 'upgrade' || server.listenerCount(eventName) > 0) { - debug('SERVER have listener for %s', eventName); - const bodyHead = d.slice(ret, d.length); - - socket.readableFlowing = null; - - server.emit(eventName, req, socket, bodyHead); - } else { - // Got CONNECT method, but have no handler. - socket.destroy(); - } - } else if (parser.incoming && parser.incoming.method === 'PRI') { - debug('SERVER got PRI request'); - socket.destroy(); - } - - if (socket._paused && socket.parser) { - // onIncoming paused the socket, we should pause the parser as well - debug('pause parser'); - socket.parser.pause(); - } -} - -function clearIncoming(req) { - req ||= this; - const parser = req.socket?.parser; - // Reset the .incoming property so that the request object can be gc'ed. - if (parser && parser.incoming === req) { - if (req.readableEnded) { - parser.incoming = null; - } else { - req.on('end', clearIncoming); - } - } -} - -function resOnFinish(req, res, socket, state, server) { - if (onResponseFinishChannel.hasSubscribers) { - onResponseFinishChannel.publish({ - request: req, - response: res, - socket, - server, - }); - } - - // Usually the first incoming element should be our request. it may - // be that in the case abortIncoming() was called that the incoming - // array will be empty. - assert(state.incoming.length === 0 || state.incoming[0] === req); - - state.incoming.shift(); - - // If the user never called req.read(), and didn't pipe() or - // .resume() or .on('data'), then we call req._dump() so that the - // bytes will be pulled off the wire. - if (!req._consuming && !req._readableState.resumeScheduled) - req._dump(); - - res.detachSocket(socket); - clearIncoming(req); - process.nextTick(emitCloseNT, res); - - if (res._last) { - if (typeof socket.destroySoon === 'function') { - socket.destroySoon(); - } else { - socket.end(); - } - } else if (state.outgoing.length === 0) { - if (server.keepAliveTimeout && typeof socket.setTimeout === 'function') { - // Increase the internal timeout wrt the advertised value to reduce - // the likelihood of ECONNRESET errors. - socket.setTimeout(server.keepAliveTimeout + HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER); - state.keepAliveTimeoutSet = true; - } - } else { - // Start sending the next message - const m = state.outgoing.shift(); - if (m) { - m.assignSocket(socket); - } - } -} - -function emitCloseNT(self) { - if (!self._closed) { - self.destroyed = true; - self._closed = true; - self.emit('close'); - } -} - -// The following callback is issued after the headers have been read on a -// new message. In this callback we setup the response object and pass it -// to the user. -function parserOnIncoming(server, socket, state, req, keepAlive) { - resetSocketTimeout(server, socket, state); - - if (req.upgrade) { - req.upgrade = req.method === 'CONNECT' || - server.listenerCount('upgrade') > 0; - if (req.upgrade) - return 2; - } - - state.incoming.push(req); - - // If the writable end isn't consuming, then stop reading - // so that we don't become overwhelmed by a flood of - // pipelined requests that may never be resolved. - if (!socket._paused) { - const ws = socket._writableState; - if (ws.needDrain || state.outgoingData >= socket.writableHighWaterMark) { - socket._paused = true; - // We also need to pause the parser, but don't do that until after - // the call to execute, because we may still be processing the last - // chunk. - socket.pause(); - } - } - - const res = new server[kServerResponse](req, - { - highWaterMark: socket.writableHighWaterMark, - rejectNonStandardBodyWrites: server.rejectNonStandardBodyWrites, - }); - res._keepAliveTimeout = server.keepAliveTimeout; - res._maxRequestsPerSocket = server.maxRequestsPerSocket; - res._onPendingData = updateOutgoingData.bind(undefined, - socket, state); - - res.shouldKeepAlive = keepAlive; - res[kUniqueHeaders] = server[kUniqueHeaders]; - - if (onRequestStartChannel.hasSubscribers) { - onRequestStartChannel.publish({ - request: req, - response: res, - socket, - server, - }); - } - - if (socket._httpMessage) { - // There are already pending outgoing res, append. - state.outgoing.push(res); - } else { - res.assignSocket(socket); - } - - // When we're finished writing the response, check if this is the last - // response, if so destroy the socket. - res.on('finish', - resOnFinish.bind(undefined, - req, res, socket, state, server)); - - let handled = false; - - - if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1) { - - // From RFC 7230 5.4 https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 - // A server MUST respond with a 400 (Bad Request) status code to any - // HTTP/1.1 request message that lacks a Host header field - if (server.requireHostHeader && req.headers.host === undefined) { - res.writeHead(400, ['Connection', 'close']); - res.end(); - return 0; - } - - const isRequestsLimitSet = ( - typeof server.maxRequestsPerSocket === 'number' && - server.maxRequestsPerSocket > 0 - ); - - if (isRequestsLimitSet) { - state.requestsCount++; - res.maxRequestsOnConnectionReached = ( - server.maxRequestsPerSocket <= state.requestsCount); - } - - if (isRequestsLimitSet && - (server.maxRequestsPerSocket < state.requestsCount)) { - handled = true; - server.emit('dropRequest', req, socket); - res.writeHead(503); - res.end(); - } else if (req.headers.expect !== undefined) { - handled = true; - - if (continueExpression.test(req.headers.expect)) { - res._expect_continue = true; - if (server.listenerCount('checkContinue') > 0) { - server.emit('checkContinue', req, res); - } else { - res.writeContinue(); - server.emit('request', req, res); - } - } else if (server.listenerCount('checkExpectation') > 0) { - server.emit('checkExpectation', req, res); - } else { - res.writeHead(417); - res.end(); - } - } - } - - if (!handled) { - server.emit('request', req, res); - } - - return 0; // No special treatment. -} - -function resetSocketTimeout(server, socket, state) { - if (!state.keepAliveTimeoutSet) - return; - - socket.setTimeout(server.timeout || 0); - state.keepAliveTimeoutSet = false; -} - -function onSocketResume() { - // It may seem that the socket is resumed, but this is an enemy's trick to - // deceive us! `resume` is emitted asynchronously, and may be called from - // `incoming.readStart()`. Stop the socket again here, just to preserve the - // state. - // - // We don't care about stream semantics for the consumed socket anyway. - if (this._paused) { - this.pause(); - return; - } - - if (this._handle && !this._handle.reading) { - this._handle.reading = true; - this._handle.readStart(); - } -} - -function onSocketPause() { - if (this._handle?.reading) { - this._handle.reading = false; - this._handle.readStop(); - } -} - -function unconsume(parser, socket) { - if (socket._handle) { - if (parser._consumed) - parser.unconsume(); - parser._consumed = false; - socket.removeListener('pause', onSocketPause); - socket.removeListener('resume', onSocketResume); - } -} - -function generateSocketListenerWrapper(originalFnName) { - return function socketListenerWrap(ev, fn) { - const res = net.Socket.prototype[originalFnName].call(this, - ev, fn); - if (!this.parser) { - this.on = net.Socket.prototype.on; - this.addListener = net.Socket.prototype.addListener; - this.prependListener = net.Socket.prototype.prependListener; - return res; - } - - if (ev === 'data' || ev === 'readable') - unconsume(this.parser, this); + STATUS_CODES, + Server, + ServerResponse, + setupConnectionsTracking, + storeHTTPOptions, + _connectionListener, + kServerResponse, + httpServerPreClose, + kConnectionsCheckingInterval, +} = require('internal/http/server'); - return res; - }; -} module.exports = { STATUS_CODES, @@ -1225,8 +40,11 @@ module.exports = { ServerResponse, setupConnectionsTracking, storeHTTPOptions, - _connectionListener: connectionListener, + _connectionListener, kServerResponse, httpServerPreClose, kConnectionsCheckingInterval, }; + +process.emitWarning('The _http_server module is deprecated. Use `node:http` instead.', + 'DeprecationWarning', 'DEP0195'); diff --git a/lib/http.js b/lib/http.js index 96ea32cf1b1034..873e00eed51d74 100644 --- a/lib/http.js +++ b/lib/http.js @@ -26,21 +26,21 @@ const { } = primordials; const { validateInteger } = require('internal/validators'); -const httpAgent = require('_http_agent'); -const { ClientRequest } = require('_http_client'); -const { methods, parsers } = require('_http_common'); -const { IncomingMessage } = require('_http_incoming'); +const httpAgent = require('internal/http/agent'); +const { ClientRequest } = require('internal/http/client'); +const { methods, parsers } = require('internal/http/common'); +const { IncomingMessage } = require('internal/http/incoming'); const { validateHeaderName, validateHeaderValue, OutgoingMessage, -} = require('_http_outgoing'); +} = require('internal/http/outgoing'); const { _connectionListener, STATUS_CODES, Server, ServerResponse, -} = require('_http_server'); +} = require('internal/http/server'); let maxHeaderSize; let undici; diff --git a/lib/https.js b/lib/https.js index acd3252b5cde66..1b3fb1804b9d85 100644 --- a/lib/https.js +++ b/lib/https.js @@ -44,15 +44,15 @@ const { assertCrypto(); const tls = require('tls'); -const { Agent: HttpAgent } = require('_http_agent'); +const { Agent: HttpAgent } = require('internal/http/agent'); const { httpServerPreClose, Server: HttpServer, setupConnectionsTracking, storeHTTPOptions, _connectionListener, -} = require('_http_server'); -const { ClientRequest } = require('_http_client'); +} = require('internal/http/server'); +const { ClientRequest } = require('internal/http/client'); let debug = require('internal/util/debuglog').debuglog('https', (fn) => { debug = fn; }); diff --git a/lib/internal/child_process.js b/lib/internal/child_process.js index dada6b8cc5a1fd..ba3404eb3ddab4 100644 --- a/lib/internal/child_process.js +++ b/lib/internal/child_process.js @@ -149,9 +149,9 @@ const handleConversion = { socket.setTimeout(0); if (freeParser === undefined) - freeParser = require('_http_common').freeParser; + freeParser = require('internal/http/common').freeParser; if (HTTPParser === undefined) - HTTPParser = require('_http_common').HTTPParser; + HTTPParser = require('internal/http/common').HTTPParser; // In case of an HTTP connection socket, release the associated // resources diff --git a/lib/internal/http/agent.js b/lib/internal/http/agent.js new file mode 100644 index 00000000000000..e8d31d83c26c51 --- /dev/null +++ b/lib/internal/http/agent.js @@ -0,0 +1,547 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + NumberParseInt, + ObjectKeys, + ObjectSetPrototypeOf, + ObjectValues, + Symbol, +} = primordials; + +const net = require('net'); +const EventEmitter = require('events'); +let debug = require('internal/util/debuglog').debuglog('http', (fn) => { + debug = fn; +}); +const { AsyncResource } = require('async_hooks'); +const { async_id_symbol } = require('internal/async_hooks').symbols; +const { + kEmptyObject, + once, +} = require('internal/util'); +const { + validateNumber, + validateOneOf, + validateString, +} = require('internal/validators'); + +const kOnKeylog = Symbol('onkeylog'); +const kRequestOptions = Symbol('requestOptions'); +const kRequestAsyncResource = Symbol('requestAsyncResource'); + +// TODO(jazelly): make this configurable +const HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER = 1000; +// New Agent code. + +// The largest departure from the previous implementation is that +// an Agent instance holds connections for a variable number of host:ports. +// Surprisingly, this is still API compatible as far as third parties are +// concerned. The only code that really notices the difference is the +// request object. + +// Another departure is that all code related to HTTP parsing is in +// ClientRequest.onSocket(). The Agent is now *strictly* +// concerned with managing a connection pool. + +class ReusedHandle { + constructor(type, handle) { + this.type = type; + this.handle = handle; + } +} + +function freeSocketErrorListener(err) { + const socket = this; + debug('SOCKET ERROR on FREE socket:', err.message, err.stack); + socket.destroy(); + socket.emit('agentRemove'); +} + +function Agent(options) { + if (!(this instanceof Agent)) + return new Agent(options); + + EventEmitter.call(this); + + this.defaultPort = 80; + this.protocol = 'http:'; + + this.options = { __proto__: null, ...options }; + + if (this.options.noDelay === undefined) + this.options.noDelay = true; + + // Don't confuse net and make it think that we're connecting to a pipe + this.options.path = null; + this.requests = { __proto__: null }; + this.sockets = { __proto__: null }; + this.freeSockets = { __proto__: null }; + this.keepAliveMsecs = this.options.keepAliveMsecs || 1000; + this.keepAlive = this.options.keepAlive || false; + this.maxSockets = this.options.maxSockets || Agent.defaultMaxSockets; + this.maxFreeSockets = this.options.maxFreeSockets || 256; + this.scheduling = this.options.scheduling || 'lifo'; + this.maxTotalSockets = this.options.maxTotalSockets; + this.totalSocketCount = 0; + + validateOneOf(this.scheduling, 'scheduling', ['fifo', 'lifo']); + + if (this.maxTotalSockets !== undefined) { + validateNumber(this.maxTotalSockets, 'maxTotalSockets', 1); + } else { + this.maxTotalSockets = Infinity; + } + + this.on('free', (socket, options) => { + const name = this.getName(options); + debug('agent.on(free)', name); + + // TODO(ronag): socket.destroy(err) might have been called + // before coming here and have an 'error' scheduled. In the + // case of socket.destroy() below this 'error' has no handler + // and could cause unhandled exception. + + if (!socket.writable) { + socket.destroy(); + return; + } + + const requests = this.requests[name]; + if (requests?.length) { + const req = requests.shift(); + const reqAsyncRes = req[kRequestAsyncResource]; + if (reqAsyncRes) { + // Run request within the original async context. + reqAsyncRes.runInAsyncScope(() => { + asyncResetHandle(socket); + setRequestSocket(this, req, socket); + }); + req[kRequestAsyncResource] = null; + } else { + setRequestSocket(this, req, socket); + } + if (requests.length === 0) { + delete this.requests[name]; + } + return; + } + + // If there are no pending requests, then put it in + // the freeSockets pool, but only if we're allowed to do so. + const req = socket._httpMessage; + if (!req || !req.shouldKeepAlive || !this.keepAlive) { + socket.destroy(); + return; + } + + const freeSockets = this.freeSockets[name] || []; + const freeLen = freeSockets.length; + let count = freeLen; + if (this.sockets[name]) + count += this.sockets[name].length; + + if (this.totalSocketCount > this.maxTotalSockets || + count > this.maxSockets || + freeLen >= this.maxFreeSockets || + !this.keepSocketAlive(socket)) { + socket.destroy(); + return; + } + + this.freeSockets[name] = freeSockets; + socket[async_id_symbol] = -1; + socket._httpMessage = null; + this.removeSocket(socket, options); + + socket.once('error', freeSocketErrorListener); + freeSockets.push(socket); + }); + + // Don't emit keylog events unless there is a listener for them. + this.on('newListener', maybeEnableKeylog); +} +ObjectSetPrototypeOf(Agent.prototype, EventEmitter.prototype); +ObjectSetPrototypeOf(Agent, EventEmitter); + +function maybeEnableKeylog(eventName) { + if (eventName === 'keylog') { + this.removeListener('newListener', maybeEnableKeylog); + // Future sockets will listen on keylog at creation. + const agent = this; + this[kOnKeylog] = function onkeylog(keylog) { + agent.emit('keylog', keylog, this); + }; + // Existing sockets will start listening on keylog now. + const sockets = ObjectValues(this.sockets); + for (let i = 0; i < sockets.length; i++) { + sockets[i].on('keylog', this[kOnKeylog]); + } + } +} + +Agent.defaultMaxSockets = Infinity; + +Agent.prototype.createConnection = net.createConnection; + +// Get the key for a given set of request options +Agent.prototype.getName = function getName(options = kEmptyObject) { + let name = options.host || 'localhost'; + + name += ':'; + if (options.port) + name += options.port; + + name += ':'; + if (options.localAddress) + name += options.localAddress; + + // Pacify parallel/test-http-agent-getname by only appending + // the ':' when options.family is set. + if (options.family === 4 || options.family === 6) + name += `:${options.family}`; + + if (options.socketPath) + name += `:${options.socketPath}`; + + return name; +}; + +Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, + localAddress/* legacy */) { + // Legacy API: addRequest(req, host, port, localAddress) + if (typeof options === 'string') { + options = { + __proto__: null, + host: options, + port, + localAddress, + }; + } + + options = { __proto__: null, ...options, ...this.options }; + if (options.socketPath) + options.path = options.socketPath; + + normalizeServerName(options, req); + + const name = this.getName(options); + this.sockets[name] ||= []; + + const freeSockets = this.freeSockets[name]; + let socket; + if (freeSockets) { + while (freeSockets.length && freeSockets[0].destroyed) { + freeSockets.shift(); + } + socket = this.scheduling === 'fifo' ? + freeSockets.shift() : + freeSockets.pop(); + if (!freeSockets.length) + delete this.freeSockets[name]; + } + + const freeLen = freeSockets ? freeSockets.length : 0; + const sockLen = freeLen + this.sockets[name].length; + + if (socket) { + asyncResetHandle(socket); + this.reuseSocket(socket, req); + setRequestSocket(this, req, socket); + this.sockets[name].push(socket); + } else if (sockLen < this.maxSockets && + this.totalSocketCount < this.maxTotalSockets) { + debug('call onSocket', sockLen, freeLen); + // If we are under maxSockets create a new one. + this.createSocket(req, options, (err, socket) => { + if (err) + req.onSocket(socket, err); + else + setRequestSocket(this, req, socket); + }); + } else { + debug('wait for socket'); + // We are over limit so we'll add it to the queue. + this.requests[name] ||= []; + + // Used to create sockets for pending requests from different origin + req[kRequestOptions] = options; + // Used to capture the original async context. + req[kRequestAsyncResource] = new AsyncResource('QueuedRequest'); + + this.requests[name].push(req); + } +}; + +Agent.prototype.createSocket = function createSocket(req, options, cb) { + options = { __proto__: null, ...options, ...this.options }; + if (options.socketPath) + options.path = options.socketPath; + + normalizeServerName(options, req); + + const name = this.getName(options); + options._agentKey = name; + + debug('createConnection', name, options); + options.encoding = null; + + const oncreate = once((err, s) => { + if (err) + return cb(err); + this.sockets[name] ||= []; + this.sockets[name].push(s); + this.totalSocketCount++; + debug('sockets', name, this.sockets[name].length, this.totalSocketCount); + installListeners(this, s, options); + cb(null, s); + }); + // When keepAlive is true, pass the related options to createConnection + if (this.keepAlive) { + options.keepAlive = this.keepAlive; + options.keepAliveInitialDelay = this.keepAliveMsecs; + } + const newSocket = this.createConnection(options, oncreate); + if (newSocket) + oncreate(null, newSocket); +}; + +function normalizeServerName(options, req) { + if (!options.servername && options.servername !== '') + options.servername = calculateServerName(options, req); +} + +function calculateServerName(options, req) { + let servername = options.host; + const hostHeader = req.getHeader('host'); + if (hostHeader) { + validateString(hostHeader, 'options.headers.host'); + + // abc => abc + // abc:123 => abc + // [::1] => ::1 + // [::1]:123 => ::1 + if (hostHeader[0] === '[') { + const index = hostHeader.indexOf(']'); + if (index === -1) { + // Leading '[', but no ']'. Need to do something... + servername = hostHeader; + } else { + servername = hostHeader.substring(1, index); + } + } else { + servername = hostHeader.split(':', 1)[0]; + } + } + // Don't implicitly set invalid (IP) servernames. + if (net.isIP(servername)) + servername = ''; + return servername; +} + +function installListeners(agent, s, options) { + function onFree() { + debug('CLIENT socket onFree'); + agent.emit('free', s, options); + } + s.on('free', onFree); + + function onClose(err) { + debug('CLIENT socket onClose'); + // This is the only place where sockets get removed from the Agent. + // If you want to remove a socket from the pool, just close it. + // All socket errors end in a close event anyway. + agent.totalSocketCount--; + agent.removeSocket(s, options); + } + s.on('close', onClose); + + function onTimeout() { + debug('CLIENT socket onTimeout'); + + // Destroy if in free list. + // TODO(ronag): Always destroy, even if not in free list. + const sockets = agent.freeSockets; + if (ObjectKeys(sockets).some((name) => sockets[name].includes(s))) { + return s.destroy(); + } + } + s.on('timeout', onTimeout); + + function onRemove() { + // We need this function for cases like HTTP 'upgrade' + // (defined by WebSockets) where we need to remove a socket from the + // pool because it'll be locked up indefinitely + debug('CLIENT socket onRemove'); + agent.totalSocketCount--; + agent.removeSocket(s, options); + s.removeListener('close', onClose); + s.removeListener('free', onFree); + s.removeListener('timeout', onTimeout); + s.removeListener('agentRemove', onRemove); + } + s.on('agentRemove', onRemove); + + if (agent[kOnKeylog]) { + s.on('keylog', agent[kOnKeylog]); + } +} + +Agent.prototype.removeSocket = function removeSocket(s, options) { + const name = this.getName(options); + debug('removeSocket', name, 'writable:', s.writable); + const sets = [this.sockets]; + + // If the socket was destroyed, remove it from the free buffers too. + if (!s.writable) + sets.push(this.freeSockets); + + for (let sk = 0; sk < sets.length; sk++) { + const sockets = sets[sk]; + + if (sockets[name]) { + const index = sockets[name].indexOf(s); + if (index !== -1) { + sockets[name].splice(index, 1); + // Don't leak + if (sockets[name].length === 0) + delete sockets[name]; + } + } + } + + let req; + if (this.requests[name]?.length) { + debug('removeSocket, have a request, make a socket'); + req = this.requests[name][0]; + } else { + // TODO(rickyes): this logic will not be FIFO across origins. + // There might be older requests in a different origin, but + // if the origin which releases the socket has pending requests + // that will be prioritized. + const keys = ObjectKeys(this.requests); + for (let i = 0; i < keys.length; i++) { + const prop = keys[i]; + // Check whether this specific origin is already at maxSockets + if (this.sockets[prop]?.length) break; + debug('removeSocket, have a request with different origin,' + + ' make a socket'); + req = this.requests[prop][0]; + options = req[kRequestOptions]; + break; + } + } + + if (req && options) { + req[kRequestOptions] = undefined; + // If we have pending requests and a socket gets closed make a new one + this.createSocket(req, options, (err, socket) => { + if (err) + req.onSocket(socket, err); + else + socket.emit('free'); + }); + } + +}; + +Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { + socket.setKeepAlive(true, this.keepAliveMsecs); + socket.unref(); + + let agentTimeout = this.options.timeout || 0; + let canKeepSocketAlive = true; + + if (socket._httpMessage?.res) { + const keepAliveHint = socket._httpMessage.res.headers['keep-alive']; + + if (keepAliveHint) { + const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1]; + + if (hint) { + // Let the timer expire before the announced timeout to reduce + // the likelihood of ECONNRESET errors + let serverHintTimeout = (NumberParseInt(hint) * 1000) - HTTP_AGENT_KEEP_ALIVE_TIMEOUT_BUFFER; + serverHintTimeout = serverHintTimeout > 0 ? serverHintTimeout : 0; + if (serverHintTimeout === 0) { + // Cannot safely reuse the socket because the server timeout is + // too short + canKeepSocketAlive = false; + } else if (serverHintTimeout < agentTimeout) { + agentTimeout = serverHintTimeout; + } + } + } + } + + if (socket.timeout !== agentTimeout) { + socket.setTimeout(agentTimeout); + } + + return canKeepSocketAlive; +}; + +Agent.prototype.reuseSocket = function reuseSocket(socket, req) { + debug('have free socket'); + socket.removeListener('error', freeSocketErrorListener); + req.reusedSocket = true; + socket.ref(); +}; + +Agent.prototype.destroy = function destroy() { + const sets = [this.freeSockets, this.sockets]; + for (let s = 0; s < sets.length; s++) { + const set = sets[s]; + const keys = ObjectKeys(set); + for (let v = 0; v < keys.length; v++) { + const setName = set[keys[v]]; + for (let n = 0; n < setName.length; n++) { + setName[n].destroy(); + } + } + } +}; + +function setRequestSocket(agent, req, socket) { + req.onSocket(socket); + const agentTimeout = agent.options.timeout || 0; + if (req.timeout === undefined || req.timeout === agentTimeout) { + return; + } + socket.setTimeout(req.timeout); +} + +function asyncResetHandle(socket) { + // Guard against an uninitialized or user supplied Socket. + const handle = socket._handle; + if (handle && typeof handle.asyncReset === 'function') { + // Assign the handle a new asyncId and run any destroy()/init() hooks. + handle.asyncReset(new ReusedHandle(handle.getProviderType(), handle)); + socket[async_id_symbol] = handle.getAsyncId(); + } +} + +module.exports = { + Agent, + globalAgent: new Agent({ keepAlive: true, scheduling: 'lifo', timeout: 5000 }), +}; diff --git a/lib/internal/http/client.js b/lib/internal/http/client.js new file mode 100644 index 00000000000000..89123deeb9806c --- /dev/null +++ b/lib/internal/http/client.js @@ -0,0 +1,996 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + ArrayIsArray, + Boolean, + Error, + NumberIsFinite, + ObjectAssign, + ObjectKeys, + ObjectSetPrototypeOf, + ReflectApply, + String, + Symbol, +} = primordials; + +const net = require('net'); +const assert = require('internal/assert'); +const { + kEmptyObject, + once, +} = require('internal/util'); +const { + _checkIsHttpToken: checkIsHttpToken, + freeParser, + parsers, + HTTPParser, + isLenient, + prepareError, +} = require('internal/http/common'); +const { + kUniqueHeaders, + parseUniqueHeadersOption, + OutgoingMessage, +} = require('internal/http/outgoing'); +const Agent = require('internal/http/agent'); +const { Buffer } = require('buffer'); +const { defaultTriggerAsyncIdScope } = require('internal/async_hooks'); +const { URL, urlToHttpOptions, isURL } = require('internal/url'); +const { + kOutHeaders, + kNeedDrain, + isTraceHTTPEnabled, + traceBegin, + traceEnd, + getNextTraceEventId, +} = require('internal/http'); +const { + ConnResetException, + codes: { + ERR_HTTP_HEADERS_SENT, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_HTTP_TOKEN, + ERR_INVALID_PROTOCOL, + ERR_UNESCAPED_CHARACTERS, + }, +} = require('internal/errors'); +const { + validateInteger, + validateBoolean, +} = require('internal/validators'); +const { getTimerDuration } = require('internal/timers'); +const { + hasObserver, + startPerf, + stopPerf, +} = require('internal/perf/observe'); + +const kClientRequestStatistics = Symbol('ClientRequestStatistics'); + +const dc = require('diagnostics_channel'); +const onClientRequestCreatedChannel = dc.channel('http.client.request.created'); +const onClientRequestStartChannel = dc.channel('http.client.request.start'); +const onClientRequestErrorChannel = dc.channel('http.client.request.error'); +const onClientResponseFinishChannel = dc.channel('http.client.response.finish'); + +function emitErrorEvent(request, error) { + if (onClientRequestErrorChannel.hasSubscribers) { + onClientRequestErrorChannel.publish({ + request, + error, + }); + } + request.emit('error', error); +} + +const { addAbortSignal, finished } = require('stream'); + +let debug = require('internal/util/debuglog').debuglog('http', (fn) => { + debug = fn; +}); + +const INVALID_PATH_REGEX = /[^\u0021-\u00ff]/; +const kError = Symbol('kError'); + +const kLenientAll = HTTPParser.kLenientAll | 0; +const kLenientNone = HTTPParser.kLenientNone | 0; + +const HTTP_CLIENT_TRACE_EVENT_NAME = 'http.client.request'; + +function validateHost(host, name) { + if (host !== null && host !== undefined && typeof host !== 'string') { + throw new ERR_INVALID_ARG_TYPE(`options.${name}`, + ['string', 'undefined', 'null'], + host); + } + return host; +} + +class HTTPClientAsyncResource { + constructor(type, req) { + this.type = type; + this.req = req; + } +} + +function ClientRequest(input, options, cb) { + OutgoingMessage.call(this); + + if (typeof input === 'string') { + const urlStr = input; + input = urlToHttpOptions(new URL(urlStr)); + } else if (isURL(input)) { + // url.URL instance + input = urlToHttpOptions(input); + } else { + cb = options; + options = input; + input = null; + } + + if (typeof options === 'function') { + cb = options; + options = input || kEmptyObject; + } else { + options = ObjectAssign(input || {}, options); + } + + let agent = options.agent; + const defaultAgent = options._defaultAgent || Agent.globalAgent; + if (agent === false) { + agent = new defaultAgent.constructor(); + } else if (agent === null || agent === undefined) { + if (typeof options.createConnection !== 'function') { + agent = defaultAgent; + } + // Explicitly pass through this statement as agent will not be used + // when createConnection is provided. + } else if (typeof agent.addRequest !== 'function') { + throw new ERR_INVALID_ARG_TYPE('options.agent', + ['Agent-like Object', 'undefined', 'false'], + agent); + } + this.agent = agent; + + const protocol = options.protocol || defaultAgent.protocol; + let expectedProtocol = defaultAgent.protocol; + if (this.agent?.protocol) + expectedProtocol = this.agent.protocol; + + if (options.path) { + const path = String(options.path); + if (INVALID_PATH_REGEX.test(path)) { + debug('Path contains unescaped characters: "%s"', path); + throw new ERR_UNESCAPED_CHARACTERS('Request path'); + } + } + + if (protocol !== expectedProtocol) { + throw new ERR_INVALID_PROTOCOL(protocol, expectedProtocol); + } + + const defaultPort = options.defaultPort || + (this.agent?.defaultPort); + + const optsWithoutSignal = { __proto__: null, ...options }; + + const port = optsWithoutSignal.port = options.port || defaultPort || 80; + const host = optsWithoutSignal.host = validateHost(options.hostname, 'hostname') || + validateHost(options.host, 'host') || 'localhost'; + + const setHost = options.setHost !== undefined ? + Boolean(options.setHost) : + options.setDefaultHeaders !== false; + + this._removedConnection = options.setDefaultHeaders === false; + this._removedContLen = options.setDefaultHeaders === false; + this._removedTE = options.setDefaultHeaders === false; + + this.socketPath = options.socketPath; + + if (options.timeout !== undefined) + this.timeout = getTimerDuration(options.timeout, 'timeout'); + + const signal = options.signal; + if (signal) { + addAbortSignal(signal, this); + delete optsWithoutSignal.signal; + } + let method = options.method; + const methodIsString = (typeof method === 'string'); + if (method !== null && method !== undefined && !methodIsString) { + throw new ERR_INVALID_ARG_TYPE('options.method', 'string', method); + } + + if (methodIsString && method) { + if (!checkIsHttpToken(method)) { + throw new ERR_INVALID_HTTP_TOKEN('Method', method); + } + method = this.method = method.toUpperCase(); + } else { + method = this.method = 'GET'; + } + + const maxHeaderSize = options.maxHeaderSize; + if (maxHeaderSize !== undefined) + validateInteger(maxHeaderSize, 'maxHeaderSize', 0); + this.maxHeaderSize = maxHeaderSize; + + const insecureHTTPParser = options.insecureHTTPParser; + if (insecureHTTPParser !== undefined) { + validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser'); + } + + this.insecureHTTPParser = insecureHTTPParser; + + if (options.joinDuplicateHeaders !== undefined) { + validateBoolean(options.joinDuplicateHeaders, 'options.joinDuplicateHeaders'); + } + + this.joinDuplicateHeaders = options.joinDuplicateHeaders; + + this.path = options.path || '/'; + if (cb) { + this.once('response', cb); + } + + if (method === 'GET' || + method === 'HEAD' || + method === 'DELETE' || + method === 'OPTIONS' || + method === 'TRACE' || + method === 'CONNECT') { + this.useChunkedEncodingByDefault = false; + } else { + this.useChunkedEncodingByDefault = true; + } + + this._ended = false; + this.res = null; + this.aborted = false; + this.timeoutCb = null; + this.upgradeOrConnect = false; + this.parser = null; + this.maxHeadersCount = null; + this.reusedSocket = false; + this.host = host; + this.protocol = protocol; + + if (this.agent) { + // If there is an agent we should default to Connection:keep-alive, + // but only if the Agent will actually reuse the connection! + // If it's not a keepAlive agent, and the maxSockets==Infinity, then + // there's never a case where this socket will actually be reused + if (!this.agent.keepAlive && !NumberIsFinite(this.agent.maxSockets)) { + this._last = true; + this.shouldKeepAlive = false; + } else { + this._last = false; + this.shouldKeepAlive = true; + } + } + + const headersArray = ArrayIsArray(options.headers); + if (!headersArray) { + if (options.headers) { + const keys = ObjectKeys(options.headers); + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0; i < keys.length; i++) { + const key = keys[i]; + this.setHeader(key, options.headers[key]); + } + } + + if (host && !this.getHeader('host') && setHost) { + let hostHeader = host; + + // For the Host header, ensure that IPv6 addresses are enclosed + // in square brackets, as defined by URI formatting + // https://tools.ietf.org/html/rfc3986#section-3.2.2 + const posColon = hostHeader.indexOf(':'); + if (posColon !== -1 && + hostHeader.includes(':', posColon + 1) && + hostHeader.charCodeAt(0) !== 91/* '[' */) { + hostHeader = `[${hostHeader}]`; + } + + if (port && +port !== defaultPort) { + hostHeader += ':' + port; + } + this.setHeader('Host', hostHeader); + } + + if (options.auth && !this.getHeader('Authorization')) { + this.setHeader('Authorization', 'Basic ' + + Buffer.from(options.auth).toString('base64')); + } + + if (this.getHeader('expect')) { + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT('render'); + } + + this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', + this[kOutHeaders]); + } + } else { + this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', + options.headers); + } + + this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders); + + // initiate connection + if (this.agent) { + this.agent.addRequest(this, optsWithoutSignal); + } else { + // No agent, default to Connection:close. + this._last = true; + this.shouldKeepAlive = false; + let opts = optsWithoutSignal; + if (opts.path || opts.socketPath) { + opts = { ...optsWithoutSignal }; + if (opts.socketPath) { + opts.path = opts.socketPath; + } else { + opts.path &&= undefined; + } + } + if (typeof opts.createConnection === 'function') { + const oncreate = once((err, socket) => { + if (err) { + process.nextTick(() => emitErrorEvent(this, err)); + } else { + this.onSocket(socket); + } + }); + + try { + const newSocket = opts.createConnection(opts, oncreate); + if (newSocket) { + oncreate(null, newSocket); + } + } catch (err) { + oncreate(err); + } + } else { + debug('CLIENT use net.createConnection', opts); + this.onSocket(net.createConnection(opts)); + } + } + if (onClientRequestCreatedChannel.hasSubscribers) { + onClientRequestCreatedChannel.publish({ + request: this, + }); + } +} +ObjectSetPrototypeOf(ClientRequest.prototype, OutgoingMessage.prototype); +ObjectSetPrototypeOf(ClientRequest, OutgoingMessage); + +ClientRequest.prototype._finish = function _finish() { + OutgoingMessage.prototype._finish.call(this); + if (hasObserver('http')) { + startPerf(this, kClientRequestStatistics, { + type: 'http', + name: 'HttpClient', + detail: { + req: { + method: this.method, + url: `${this.protocol}//${this.host}${this.path}`, + headers: typeof this.getHeaders === 'function' ? this.getHeaders() : {}, + }, + }, + }); + } + if (onClientRequestStartChannel.hasSubscribers) { + onClientRequestStartChannel.publish({ + request: this, + }); + } + if (isTraceHTTPEnabled()) { + this._traceEventId = getNextTraceEventId(); + traceBegin(HTTP_CLIENT_TRACE_EVENT_NAME, this._traceEventId); + } +}; + +ClientRequest.prototype._implicitHeader = function _implicitHeader() { + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT('render'); + } + this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', + this[kOutHeaders]); +}; + +ClientRequest.prototype.abort = function abort() { + if (this.aborted) { + return; + } + this.aborted = true; + process.nextTick(emitAbortNT, this); + this.destroy(); +}; + +ClientRequest.prototype.destroy = function destroy(err) { + if (this.destroyed) { + return this; + } + this.destroyed = true; + + // If we're aborting, we don't care about any more response data. + if (this.res) { + this.res._dump(); + } + + this[kError] = err; + this.socket?.destroy(err); + + return this; +}; + +function emitAbortNT(req) { + req.emit('abort'); +} + +function ondrain() { + const msg = this._httpMessage; + if (msg && !msg.finished && msg[kNeedDrain]) { + msg[kNeedDrain] = false; + msg.emit('drain'); + } +} + +function socketCloseListener() { + const socket = this; + const req = socket._httpMessage; + debug('HTTP socket close'); + + // NOTE: It's important to get parser here, because it could be freed by + // the `socketOnData`. + const parser = socket.parser; + const res = req.res; + + req.destroyed = true; + if (res) { + // Socket closed before we emitted 'end' below. + if (!res.complete) { + res.destroy(new ConnResetException('aborted')); + } + req._closed = true; + req.emit('close'); + if (!res.aborted && res.readable) { + res.push(null); + } + } else { + if (!req.socket._hadError) { + // This socket error fired before we started to + // receive a response. The error needs to + // fire on the request. + req.socket._hadError = true; + emitErrorEvent(req, new ConnResetException('socket hang up')); + } + req._closed = true; + req.emit('close'); + } + + // Too bad. That output wasn't getting written. + // This is pretty terrible that it doesn't raise an error. + // Fixed better in v0.10 + if (req.outputData) + req.outputData.length = 0; + + if (parser) { + parser.finish(); + freeParser(parser, req, socket); + } +} + +function socketErrorListener(err) { + const socket = this; + const req = socket._httpMessage; + debug('SOCKET ERROR:', err.message, err.stack); + + if (req) { + // For Safety. Some additional errors might fire later on + // and we need to make sure we don't double-fire the error event. + req.socket._hadError = true; + emitErrorEvent(req, err); + } + + const parser = socket.parser; + if (parser) { + parser.finish(); + freeParser(parser, req, socket); + } + + // Ensure that no further data will come out of the socket + socket.removeListener('data', socketOnData); + socket.removeListener('end', socketOnEnd); + socket.destroy(); +} + +function socketOnEnd() { + const socket = this; + const req = this._httpMessage; + const parser = this.parser; + + if (!req.res && !req.socket._hadError) { + // If we don't have a response then we know that the socket + // ended prematurely and we need to emit an error on the request. + req.socket._hadError = true; + emitErrorEvent(req, new ConnResetException('socket hang up')); + } + if (parser) { + parser.finish(); + freeParser(parser, req, socket); + } + socket.destroy(); +} + +function socketOnData(d) { + const socket = this; + const req = this._httpMessage; + const parser = this.parser; + + assert(parser && parser.socket === socket); + + const ret = parser.execute(d); + if (ret instanceof Error) { + prepareError(ret, parser, d); + debug('parse error', ret); + freeParser(parser, req, socket); + socket.removeListener('data', socketOnData); + socket.removeListener('end', socketOnEnd); + socket.destroy(); + req.socket._hadError = true; + emitErrorEvent(req, ret); + } else if (parser.incoming?.upgrade) { + // Upgrade (if status code 101) or CONNECT + const bytesParsed = ret; + const res = parser.incoming; + req.res = res; + + socket.removeListener('data', socketOnData); + socket.removeListener('end', socketOnEnd); + socket.removeListener('drain', ondrain); + + if (req.timeoutCb) socket.removeListener('timeout', req.timeoutCb); + socket.removeListener('timeout', responseOnTimeout); + + parser.finish(); + freeParser(parser, req, socket); + + const bodyHead = d.slice(bytesParsed, d.length); + + const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; + if (req.listenerCount(eventName) > 0) { + req.upgradeOrConnect = true; + + // detach the socket + socket.emit('agentRemove'); + socket.removeListener('close', socketCloseListener); + socket.removeListener('error', socketErrorListener); + + socket._httpMessage = null; + socket.readableFlowing = null; + + req.emit(eventName, res, socket, bodyHead); + req.destroyed = true; + req._closed = true; + req.emit('close'); + } else { + // Requested Upgrade or used CONNECT method, but have no handler. + socket.destroy(); + } + } else if (parser.incoming?.complete && + // When the status code is informational (100, 102-199), + // the server will send a final response after this client + // sends a request body, so we must not free the parser. + // 101 (Switching Protocols) and all other status codes + // should be processed normally. + !statusIsInformational(parser.incoming.statusCode)) { + socket.removeListener('data', socketOnData); + socket.removeListener('end', socketOnEnd); + socket.removeListener('drain', ondrain); + freeParser(parser, req, socket); + } +} + +function statusIsInformational(status) { + // 100 (Continue) RFC7231 Section 6.2.1 + // 102 (Processing) RFC2518 + // 103 (Early Hints) RFC8297 + // 104-199 (Unassigned) + return (status < 200 && status >= 100 && status !== 101); +} + +// client +function parserOnIncomingClient(res, shouldKeepAlive) { + const socket = this.socket; + const req = socket._httpMessage; + + debug('AGENT incoming response!'); + + if (req.res) { + // We already have a response object, this means the server + // sent a double response. + socket.destroy(); + return 0; // No special treatment. + } + req.res = res; + + // Skip body and treat as Upgrade. + if (res.upgrade) + return 2; + + // Responses to CONNECT request is handled as Upgrade. + const method = req.method; + if (method === 'CONNECT') { + res.upgrade = true; + return 2; // Skip body and treat as Upgrade. + } + + if (statusIsInformational(res.statusCode)) { + // Restart the parser, as this is a 1xx informational message. + req.res = null; // Clear res so that we don't hit double-responses. + // Maintain compatibility by sending 100-specific events + if (res.statusCode === 100) { + req.emit('continue'); + } + // Send information events to all 1xx responses except 101 Upgrade. + req.emit('information', { + statusCode: res.statusCode, + statusMessage: res.statusMessage, + httpVersion: res.httpVersion, + httpVersionMajor: res.httpVersionMajor, + httpVersionMinor: res.httpVersionMinor, + headers: res.headers, + rawHeaders: res.rawHeaders, + }); + + return 1; // Skip body but don't treat as Upgrade. + } + + if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) { + // Server MUST respond with Connection:keep-alive for us to enable it. + // If we've been upgraded (via WebSockets) we also shouldn't try to + // keep the connection open. + req.shouldKeepAlive = false; + } + + if (req[kClientRequestStatistics] && hasObserver('http')) { + stopPerf(req, kClientRequestStatistics, { + detail: { + res: { + statusCode: res.statusCode, + statusMessage: res.statusMessage, + headers: res.headers, + }, + }, + }); + } + if (onClientResponseFinishChannel.hasSubscribers) { + onClientResponseFinishChannel.publish({ + request: req, + response: res, + }); + } + if (isTraceHTTPEnabled() && typeof req._traceEventId === 'number') { + traceEnd(HTTP_CLIENT_TRACE_EVENT_NAME, req._traceEventId, { + path: req.path, + statusCode: res.statusCode, + }); + } + req.res = res; + res.req = req; + + // Add our listener first, so that we guarantee socket cleanup + res.on('end', responseOnEnd); + req.on('finish', requestOnFinish); + socket.on('timeout', responseOnTimeout); + + // If the user did not listen for the 'response' event, then they + // can't possibly read the data, so we ._dump() it into the void + // so that the socket doesn't hang there in a paused state. + if (req.aborted || !req.emit('response', res)) + res._dump(); + + if (method === 'HEAD') + return 1; // Skip body but don't treat as Upgrade. + + if (res.statusCode === 304) { + res.complete = true; + return 1; // Skip body as there won't be any + } + + return 0; // No special treatment. +} + +// client +function responseKeepAlive(req) { + const socket = req.socket; + + debug('AGENT socket keep-alive'); + if (req.timeoutCb) { + socket.setTimeout(0, req.timeoutCb); + req.timeoutCb = null; + } + socket.removeListener('close', socketCloseListener); + socket.removeListener('error', socketErrorListener); + socket.removeListener('data', socketOnData); + socket.removeListener('end', socketOnEnd); + + // TODO(ronag): Between here and emitFreeNT the socket + // has no 'error' handler. + + // There are cases where _handle === null. Avoid those. Passing undefined to + // nextTick() will call getDefaultTriggerAsyncId() to retrieve the id. + const asyncId = socket._handle ? socket._handle.getAsyncId() : undefined; + // Mark this socket as available, AFTER user-added end + // handlers have a chance to run. + defaultTriggerAsyncIdScope(asyncId, process.nextTick, emitFreeNT, req); + + req.destroyed = true; + if (req.res) { + // Detach socket from IncomingMessage to avoid destroying the freed + // socket in IncomingMessage.destroy(). + req.res.socket = null; + } +} + +function responseOnEnd() { + const req = this.req; + const socket = req.socket; + + if (socket) { + if (req.timeoutCb) socket.removeListener('timeout', emitRequestTimeout); + socket.removeListener('timeout', responseOnTimeout); + } + + req._ended = true; + + if (!req.shouldKeepAlive) { + if (socket.writable) { + debug('AGENT socket.destroySoon()'); + if (typeof socket.destroySoon === 'function') + socket.destroySoon(); + else + socket.end(); + } + assert(!socket.writable); + } else if (req.writableFinished && !this.aborted) { + assert(req.finished); + // We can assume `req.finished` means all data has been written since: + // - `'responseOnEnd'` means we have been assigned a socket. + // - when we have a socket we write directly to it without buffering. + // - `req.finished` means `end()` has been called and no further data. + // can be written + // In addition, `req.writableFinished` means all data written has been + // accepted by the kernel. (i.e. the `req.socket` is drained).Without + // this constraint, we may assign a non drained socket to a request. + responseKeepAlive(req); + } +} + +function responseOnTimeout() { + const req = this._httpMessage; + if (!req) return; + const res = req.res; + if (!res) return; + res.emit('timeout'); +} + +// This function is necessary in the case where we receive the entire response +// from the server before we finish sending out the request. +function requestOnFinish() { + const req = this; + + if (req.shouldKeepAlive && req._ended) + responseKeepAlive(req); +} + +function emitFreeNT(req) { + req._closed = true; + req.emit('close'); + if (req.socket) { + req.socket.emit('free'); + } +} + +function tickOnSocket(req, socket) { + const parser = parsers.alloc(); + req.socket = socket; + const lenient = req.insecureHTTPParser === undefined ? + isLenient() : req.insecureHTTPParser; + parser.initialize(HTTPParser.RESPONSE, + new HTTPClientAsyncResource('HTTPINCOMINGMESSAGE', req), + req.maxHeaderSize || 0, + lenient ? kLenientAll : kLenientNone); + parser.socket = socket; + parser.outgoing = req; + req.parser = parser; + + socket.parser = parser; + socket._httpMessage = req; + + // Propagate headers limit from request object to parser + if (typeof req.maxHeadersCount === 'number') { + parser.maxHeaderPairs = req.maxHeadersCount << 1; + } + + parser.joinDuplicateHeaders = req.joinDuplicateHeaders; + + parser.onIncoming = parserOnIncomingClient; + socket.on('error', socketErrorListener); + socket.on('data', socketOnData); + socket.on('end', socketOnEnd); + socket.on('close', socketCloseListener); + socket.on('drain', ondrain); + + if ( + req.timeout !== undefined || + (req.agent?.options?.timeout) + ) { + listenSocketTimeout(req); + } + req.emit('socket', socket); +} + +function emitRequestTimeout() { + const req = this._httpMessage; + if (req) { + req.emit('timeout'); + } +} + +function listenSocketTimeout(req) { + if (req.timeoutCb) { + return; + } + // Set timeoutCb so it will get cleaned up on request end. + req.timeoutCb = emitRequestTimeout; + // Delegate socket timeout event. + if (req.socket) { + req.socket.once('timeout', emitRequestTimeout); + } else { + req.on('socket', (socket) => { + socket.once('timeout', emitRequestTimeout); + }); + } +} + +ClientRequest.prototype.onSocket = function onSocket(socket, err) { + // TODO(ronag): Between here and onSocketNT the socket + // has no 'error' handler. + process.nextTick(onSocketNT, this, socket, err); +}; + +function onSocketNT(req, socket, err) { + if (req.destroyed || err) { + req.destroyed = true; + + function _destroy(req, err) { + if (!req.aborted && !err) { + err = new ConnResetException('socket hang up'); + } + if (err) { + emitErrorEvent(req, err); + } + req._closed = true; + req.emit('close'); + } + + if (socket) { + if (!err && req.agent && !socket.destroyed) { + socket.emit('free'); + } else { + finished(socket.destroy(err || req[kError]), (er) => { + if (er?.code === 'ERR_STREAM_PREMATURE_CLOSE') { + er = null; + } + _destroy(req, er || err); + }); + return; + } + } + + _destroy(req, err || req[kError]); + } else { + tickOnSocket(req, socket); + req._flush(); + } +} + +ClientRequest.prototype._deferToConnect = _deferToConnect; +function _deferToConnect(method, arguments_) { + // This function is for calls that need to happen once the socket is + // assigned to this request and writable. It's an important promisy + // thing for all the socket calls that happen either now + // (when a socket is assigned) or in the future (when a socket gets + // assigned out of the pool and is eventually writable). + + const callSocketMethod = () => { + if (method) + ReflectApply(this.socket[method], this.socket, arguments_); + }; + + const onSocket = () => { + if (this.socket.writable) { + callSocketMethod(); + } else { + this.socket.once('connect', callSocketMethod); + } + }; + + if (!this.socket) { + this.once('socket', onSocket); + } else { + onSocket(); + } +} + +ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) { + if (this._ended) { + return this; + } + + listenSocketTimeout(this); + msecs = getTimerDuration(msecs, 'msecs'); + if (callback) this.once('timeout', callback); + + if (this.socket) { + setSocketTimeout(this.socket, msecs); + } else { + this.once('socket', (sock) => setSocketTimeout(sock, msecs)); + } + + return this; +}; + +function setSocketTimeout(sock, msecs) { + if (sock.connecting) { + sock.once('connect', function() { + sock.setTimeout(msecs); + }); + } else { + sock.setTimeout(msecs); + } +} + +ClientRequest.prototype.setNoDelay = function setNoDelay(noDelay) { + this._deferToConnect('setNoDelay', [noDelay]); +}; + +ClientRequest.prototype.setSocketKeepAlive = + function setSocketKeepAlive(enable, initialDelay) { + this._deferToConnect('setKeepAlive', [enable, initialDelay]); + }; + +ClientRequest.prototype.clearTimeout = function clearTimeout(cb) { + this.setTimeout(0, cb); +}; + +module.exports = { + ClientRequest, +}; diff --git a/lib/internal/http/common.js b/lib/internal/http/common.js new file mode 100644 index 00000000000000..991b2a70015194 --- /dev/null +++ b/lib/internal/http/common.js @@ -0,0 +1,269 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + MathMin, + Symbol, +} = primordials; +const { setImmediate } = require('timers'); + +const { methods, allMethods, HTTPParser } = internalBinding('http_parser'); +const { getOptionValue } = require('internal/options'); +const insecureHTTPParser = getOptionValue('--insecure-http-parser'); + +const FreeList = require('internal/freelist'); +const incoming = require('internal/http/incoming'); +const { + IncomingMessage, + readStart, + readStop, +} = incoming; + +const kIncomingMessage = Symbol('IncomingMessage'); +const kOnMessageBegin = HTTPParser.kOnMessageBegin | 0; +const kOnHeaders = HTTPParser.kOnHeaders | 0; +const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; +const kOnBody = HTTPParser.kOnBody | 0; +const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0; +const kOnExecute = HTTPParser.kOnExecute | 0; +const kOnTimeout = HTTPParser.kOnTimeout | 0; + +const MAX_HEADER_PAIRS = 2000; + +// Only called in the slow case where slow means +// that the request headers were either fragmented +// across multiple TCP packets or too large to be +// processed in a single run. This method is also +// called to process trailing HTTP headers. +function parserOnHeaders(headers, url) { + // Once we exceeded headers limit - stop collecting them + if (this.maxHeaderPairs <= 0 || + this._headers.length < this.maxHeaderPairs) { + this._headers.push(...headers); + } + this._url += url; +} + +// `headers` and `url` are set only if .onHeaders() has not been called for +// this request. +// `url` is not set for response parsers but that's not applicable here since +// all our parsers are request parsers. +function parserOnHeadersComplete(versionMajor, versionMinor, headers, method, + url, statusCode, statusMessage, upgrade, + shouldKeepAlive) { + const parser = this; + const { socket } = parser; + + if (headers === undefined) { + headers = parser._headers; + parser._headers = []; + } + + if (url === undefined) { + url = parser._url; + parser._url = ''; + } + + // Parser is also used by http client + const ParserIncomingMessage = (socket?.server?.[kIncomingMessage]) || + IncomingMessage; + + const incoming = parser.incoming = new ParserIncomingMessage(socket); + incoming.httpVersionMajor = versionMajor; + incoming.httpVersionMinor = versionMinor; + incoming.httpVersion = `${versionMajor}.${versionMinor}`; + incoming.joinDuplicateHeaders = socket?.server?.joinDuplicateHeaders || + parser.joinDuplicateHeaders; + incoming.url = url; + incoming.upgrade = upgrade; + + let n = headers.length; + + // If parser.maxHeaderPairs <= 0 assume that there's no limit. + if (parser.maxHeaderPairs > 0) + n = MathMin(n, parser.maxHeaderPairs); + + incoming._addHeaderLines(headers, n); + + if (typeof method === 'number') { + // server only + incoming.method = allMethods[method]; + } else { + // client only + incoming.statusCode = statusCode; + incoming.statusMessage = statusMessage; + } + + return parser.onIncoming(incoming, shouldKeepAlive); +} + +function parserOnBody(b) { + const stream = this.incoming; + + // If the stream has already been removed, then drop it. + if (stream === null) + return; + + // Pretend this was the result of a stream._read call. + if (!stream._dumped) { + const ret = stream.push(b); + if (!ret) + readStop(this.socket); + } +} + +function parserOnMessageComplete() { + const parser = this; + const stream = parser.incoming; + + if (stream !== null) { + stream.complete = true; + // Emit any trailing headers. + const headers = parser._headers; + if (headers.length) { + stream._addHeaderLines(headers, headers.length); + parser._headers = []; + parser._url = ''; + } + + // For emit end event + stream.push(null); + } + + // Force to read the next incoming message + readStart(parser.socket); +} + + +const parsers = new FreeList('parsers', 1000, function parsersCb() { + const parser = new HTTPParser(); + + cleanParser(parser); + + parser[kOnHeaders] = parserOnHeaders; + parser[kOnHeadersComplete] = parserOnHeadersComplete; + parser[kOnBody] = parserOnBody; + parser[kOnMessageComplete] = parserOnMessageComplete; + + return parser; +}); + +function closeParserInstance(parser) { parser.close(); } + +// Free the parser and also break any links that it +// might have to any other things. +// TODO: All parser data should be attached to a +// single object, so that it can be easily cleaned +// up by doing `parser.data = {}`, which should +// be done in FreeList.free. `parsers.free(parser)` +// should be all that is needed. +function freeParser(parser, req, socket) { + if (parser) { + if (parser._consumed) + parser.unconsume(); + cleanParser(parser); + parser.remove(); + if (parsers.free(parser) === false) { + // Make sure the parser's stack has unwound before deleting the + // corresponding C++ object through .close(). + setImmediate(closeParserInstance, parser); + } else { + // Since the Parser destructor isn't going to run the destroy() callbacks + // it needs to be triggered manually. + parser.free(); + } + } + if (req) { + req.parser = null; + } + if (socket) { + socket.parser = null; + } +} + +const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; +/** + * Verifies that the given val is a valid HTTP token + * per the rules defined in RFC 7230 + * See https://tools.ietf.org/html/rfc7230#section-3.2.6 + */ +function checkIsHttpToken(val) { + return tokenRegExp.test(val); +} + +const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; +/** + * True if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + */ +function checkInvalidHeaderChar(val) { + return headerCharRegex.test(val); +} + +function cleanParser(parser) { + parser._headers = []; + parser._url = ''; + parser.socket = null; + parser.incoming = null; + parser.outgoing = null; + parser.maxHeaderPairs = MAX_HEADER_PAIRS; + parser[kOnMessageBegin] = null; + parser[kOnExecute] = null; + parser[kOnTimeout] = null; + parser._consumed = false; + parser.onIncoming = null; + parser.joinDuplicateHeaders = null; +} + +function prepareError(err, parser, rawPacket) { + err.rawPacket = rawPacket || parser.getCurrentBuffer(); + if (typeof err.reason === 'string') + err.message = `Parse Error: ${err.reason}`; +} + +let warnedLenient = false; + +function isLenient() { + if (insecureHTTPParser && !warnedLenient) { + warnedLenient = true; + process.emitWarning('Using insecure HTTP parsing'); + } + return insecureHTTPParser; +} + +module.exports = { + _checkInvalidHeaderChar: checkInvalidHeaderChar, + _checkIsHttpToken: checkIsHttpToken, + chunkExpression: /(?:^|\W)chunked(?:$|\W)/i, + continueExpression: /(?:^|\W)100-continue(?:$|\W)/i, + CRLF: '\r\n', // TODO: Deprecate this. + freeParser, + methods, + parsers, + kIncomingMessage, + HTTPParser, + isLenient, + prepareError, +}; diff --git a/lib/internal/http/incoming.js b/lib/internal/http/incoming.js new file mode 100644 index 00000000000000..c3e901e53e8b90 --- /dev/null +++ b/lib/internal/http/incoming.js @@ -0,0 +1,453 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + ObjectDefineProperty, + ObjectSetPrototypeOf, + Symbol, +} = primordials; + +const { Readable, finished } = require('stream'); + +const kHeaders = Symbol('kHeaders'); +const kHeadersDistinct = Symbol('kHeadersDistinct'); +const kHeadersCount = Symbol('kHeadersCount'); +const kTrailers = Symbol('kTrailers'); +const kTrailersDistinct = Symbol('kTrailersDistinct'); +const kTrailersCount = Symbol('kTrailersCount'); + +function readStart(socket) { + if (socket && !socket._paused && socket.readable) + socket.resume(); +} + +function readStop(socket) { + if (socket) + socket.pause(); +} + +/* Abstract base class for ServerRequest and ClientResponse. */ +function IncomingMessage(socket) { + let streamOptions; + + if (socket) { + streamOptions = { + highWaterMark: socket.readableHighWaterMark, + }; + } + + Readable.call(this, streamOptions); + + this._readableState.readingMore = true; + + this.socket = socket; + + this.httpVersionMajor = null; + this.httpVersionMinor = null; + this.httpVersion = null; + this.complete = false; + this[kHeaders] = null; + this[kHeadersCount] = 0; + this.rawHeaders = []; + this[kTrailers] = null; + this[kTrailersCount] = 0; + this.rawTrailers = []; + this.joinDuplicateHeaders = false; + this.aborted = false; + + this.upgrade = null; + + // request (server) only + this.url = ''; + this.method = null; + + // response (client) only + this.statusCode = null; + this.statusMessage = null; + this.client = socket; + + this._consuming = false; + // Flag for when we decide that this message cannot possibly be + // read by the user, so there's no point continuing to handle it. + this._dumped = false; +} +ObjectSetPrototypeOf(IncomingMessage.prototype, Readable.prototype); +ObjectSetPrototypeOf(IncomingMessage, Readable); + +ObjectDefineProperty(IncomingMessage.prototype, 'connection', { + __proto__: null, + get: function() { + return this.socket; + }, + set: function(val) { + this.socket = val; + }, +}); + +ObjectDefineProperty(IncomingMessage.prototype, 'headers', { + __proto__: null, + get: function() { + if (!this[kHeaders]) { + this[kHeaders] = {}; + + const src = this.rawHeaders; + const dst = this[kHeaders]; + + for (let n = 0; n < this[kHeadersCount]; n += 2) { + this._addHeaderLine(src[n + 0], src[n + 1], dst); + } + } + return this[kHeaders]; + }, + set: function(val) { + this[kHeaders] = val; + }, +}); + +ObjectDefineProperty(IncomingMessage.prototype, 'headersDistinct', { + __proto__: null, + get: function() { + if (!this[kHeadersDistinct]) { + this[kHeadersDistinct] = {}; + + const src = this.rawHeaders; + const dst = this[kHeadersDistinct]; + + for (let n = 0; n < this[kHeadersCount]; n += 2) { + this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst); + } + } + return this[kHeadersDistinct]; + }, + set: function(val) { + this[kHeadersDistinct] = val; + }, +}); + +ObjectDefineProperty(IncomingMessage.prototype, 'trailers', { + __proto__: null, + get: function() { + if (!this[kTrailers]) { + this[kTrailers] = {}; + + const src = this.rawTrailers; + const dst = this[kTrailers]; + + for (let n = 0; n < this[kTrailersCount]; n += 2) { + this._addHeaderLine(src[n + 0], src[n + 1], dst); + } + } + return this[kTrailers]; + }, + set: function(val) { + this[kTrailers] = val; + }, +}); + +ObjectDefineProperty(IncomingMessage.prototype, 'trailersDistinct', { + __proto__: null, + get: function() { + if (!this[kTrailersDistinct]) { + this[kTrailersDistinct] = {}; + + const src = this.rawTrailers; + const dst = this[kTrailersDistinct]; + + for (let n = 0; n < this[kTrailersCount]; n += 2) { + this._addHeaderLineDistinct(src[n + 0], src[n + 1], dst); + } + } + return this[kTrailersDistinct]; + }, + set: function(val) { + this[kTrailersDistinct] = val; + }, +}); + +IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { + if (callback) + this.on('timeout', callback); + this.socket.setTimeout(msecs); + return this; +}; + +// Argument n cannot be factored out due to the overhead of +// argument adaptor frame creation inside V8 in case that number of actual +// arguments is different from expected arguments. +// Ref: https://bugs.chromium.org/p/v8/issues/detail?id=10201 +// NOTE: Argument adapt frame issue might be solved in V8 engine v8.9. +// Refactoring `n` out might be possible when V8 is upgraded to that +// version. +// Ref: https://v8.dev/blog/v8-release-89 +IncomingMessage.prototype._read = function _read(n) { + if (!this._consuming) { + this._readableState.readingMore = false; + this._consuming = true; + } + + // We actually do almost nothing here, because the parserOnBody + // function fills up our internal buffer directly. However, we + // do need to unpause the underlying socket so that it flows. + if (this.socket.readable) + readStart(this.socket); +}; + +// It's possible that the socket will be destroyed, and removed from +// any messages, before ever calling this. In that case, just skip +// it, since something else is destroying this connection anyway. +IncomingMessage.prototype._destroy = function _destroy(err, cb) { + if (!this.readableEnded || !this.complete) { + this.aborted = true; + this.emit('aborted'); + } + + // If aborted and the underlying socket is not already destroyed, + // destroy it. + // We have to check if the socket is already destroyed because finished + // does not call the callback when this method is invoked from `_http_client` + // in `test/parallel/test-http-client-spurious-aborted.js` + if (this.socket && !this.socket.destroyed && this.aborted) { + this.socket.destroy(err); + const cleanup = finished(this.socket, (e) => { + if (e?.code === 'ERR_STREAM_PREMATURE_CLOSE') { + e = null; + } + cleanup(); + process.nextTick(onError, this, e || err, cb); + }); + } else { + process.nextTick(onError, this, err, cb); + } +}; + +IncomingMessage.prototype._addHeaderLines = _addHeaderLines; +function _addHeaderLines(headers, n) { + if (headers?.length) { + let dest; + if (this.complete) { + this.rawTrailers = headers; + this[kTrailersCount] = n; + dest = this[kTrailers]; + } else { + this.rawHeaders = headers; + this[kHeadersCount] = n; + dest = this[kHeaders]; + } + + if (dest) { + for (let i = 0; i < n; i += 2) { + this._addHeaderLine(headers[i], headers[i + 1], dest); + } + } + } +} + + +// This function is used to help avoid the lowercasing of a field name if it +// matches a 'traditional cased' version of a field name. It then returns the +// lowercased name to both avoid calling toLowerCase() a second time and to +// indicate whether the field was a 'no duplicates' field. If a field is not a +// 'no duplicates' field, a `0` byte is prepended as a flag. The one exception +// to this is the Set-Cookie header which is indicated by a `1` byte flag, since +// it is an 'array' field and thus is treated differently in _addHeaderLines(). +// TODO: perhaps http_parser could be returning both raw and lowercased versions +// of known header names to avoid us having to call toLowerCase() for those +// headers. +function matchKnownFields(field, lowercased) { + switch (field.length) { + case 3: + if (field === 'Age' || field === 'age') return 'age'; + break; + case 4: + if (field === 'Host' || field === 'host') return 'host'; + if (field === 'From' || field === 'from') return 'from'; + if (field === 'ETag' || field === 'etag') return 'etag'; + if (field === 'Date' || field === 'date') return '\u0000date'; + if (field === 'Vary' || field === 'vary') return '\u0000vary'; + break; + case 6: + if (field === 'Server' || field === 'server') return 'server'; + if (field === 'Cookie' || field === 'cookie') return '\u0002cookie'; + if (field === 'Origin' || field === 'origin') return '\u0000origin'; + if (field === 'Expect' || field === 'expect') return '\u0000expect'; + if (field === 'Accept' || field === 'accept') return '\u0000accept'; + break; + case 7: + if (field === 'Referer' || field === 'referer') return 'referer'; + if (field === 'Expires' || field === 'expires') return 'expires'; + if (field === 'Upgrade' || field === 'upgrade') return '\u0000upgrade'; + break; + case 8: + if (field === 'Location' || field === 'location') + return 'location'; + if (field === 'If-Match' || field === 'if-match') + return '\u0000if-match'; + break; + case 10: + if (field === 'User-Agent' || field === 'user-agent') + return 'user-agent'; + if (field === 'Set-Cookie' || field === 'set-cookie') + return '\u0001'; + if (field === 'Connection' || field === 'connection') + return '\u0000connection'; + break; + case 11: + if (field === 'Retry-After' || field === 'retry-after') + return 'retry-after'; + break; + case 12: + if (field === 'Content-Type' || field === 'content-type') + return 'content-type'; + if (field === 'Max-Forwards' || field === 'max-forwards') + return 'max-forwards'; + break; + case 13: + if (field === 'Authorization' || field === 'authorization') + return 'authorization'; + if (field === 'Last-Modified' || field === 'last-modified') + return 'last-modified'; + if (field === 'Cache-Control' || field === 'cache-control') + return '\u0000cache-control'; + if (field === 'If-None-Match' || field === 'if-none-match') + return '\u0000if-none-match'; + break; + case 14: + if (field === 'Content-Length' || field === 'content-length') + return 'content-length'; + break; + case 15: + if (field === 'Accept-Encoding' || field === 'accept-encoding') + return '\u0000accept-encoding'; + if (field === 'Accept-Language' || field === 'accept-language') + return '\u0000accept-language'; + if (field === 'X-Forwarded-For' || field === 'x-forwarded-for') + return '\u0000x-forwarded-for'; + break; + case 16: + if (field === 'Content-Encoding' || field === 'content-encoding') + return '\u0000content-encoding'; + if (field === 'X-Forwarded-Host' || field === 'x-forwarded-host') + return '\u0000x-forwarded-host'; + break; + case 17: + if (field === 'If-Modified-Since' || field === 'if-modified-since') + return 'if-modified-since'; + if (field === 'Transfer-Encoding' || field === 'transfer-encoding') + return '\u0000transfer-encoding'; + if (field === 'X-Forwarded-Proto' || field === 'x-forwarded-proto') + return '\u0000x-forwarded-proto'; + break; + case 19: + if (field === 'Proxy-Authorization' || field === 'proxy-authorization') + return 'proxy-authorization'; + if (field === 'If-Unmodified-Since' || field === 'if-unmodified-since') + return 'if-unmodified-since'; + break; + } + if (lowercased) { + return '\u0000' + field; + } + return matchKnownFields(field.toLowerCase(), true); +} +// Add the given (field, value) pair to the message +// +// Per RFC2616, section 4.2 it is acceptable to join multiple instances of the +// same header with a ', ' if the header in question supports specification of +// multiple values this way. The one exception to this is the Cookie header, +// which has multiple values joined with a '; ' instead. If a header's values +// cannot be joined in either of these ways, we declare the first instance the +// winner and drop the second. Extended header fields (those beginning with +// 'x-') are always joined. +IncomingMessage.prototype._addHeaderLine = _addHeaderLine; +function _addHeaderLine(field, value, dest) { + field = matchKnownFields(field); + const flag = field.charCodeAt(0); + if (flag === 0 || flag === 2) { + field = field.slice(1); + // Make a delimited list + if (typeof dest[field] === 'string') { + dest[field] += (flag === 0 ? ', ' : '; ') + value; + } else { + dest[field] = value; + } + } else if (flag === 1) { + // Array header -- only Set-Cookie at the moment + if (dest['set-cookie'] !== undefined) { + dest['set-cookie'].push(value); + } else { + dest['set-cookie'] = [value]; + } + } else if (this.joinDuplicateHeaders) { + // RFC 9110 https://www.rfc-editor.org/rfc/rfc9110#section-5.2 + // https://github.com/nodejs/node/issues/45699 + // allow authorization multiple fields + // Make a delimited list + if (dest[field] === undefined) { + dest[field] = value; + } else { + dest[field] += ', ' + value; + } + } else if (dest[field] === undefined) { + // Drop duplicates + dest[field] = value; + } +} + +IncomingMessage.prototype._addHeaderLineDistinct = _addHeaderLineDistinct; +function _addHeaderLineDistinct(field, value, dest) { + field = field.toLowerCase(); + if (!dest[field]) { + dest[field] = [value]; + } else { + dest[field].push(value); + } +} + + +// Call this instead of resume() if we want to just +// dump all the data to /dev/null +IncomingMessage.prototype._dump = function _dump() { + if (!this._dumped) { + this._dumped = true; + // If there is buffered data, it may trigger 'data' events. + // Remove 'data' event listeners explicitly. + this.removeAllListeners('data'); + this.resume(); + } +}; + +function onError(self, error, cb) { + // This is to keep backward compatible behavior. + // An error is emitted only if there are listeners attached to the event. + if (self.listenerCount('error') === 0) { + cb(); + } else { + cb(error); + } +} + +module.exports = { + IncomingMessage, + readStart, + readStop, +}; diff --git a/lib/internal/http/outgoing.js b/lib/internal/http/outgoing.js new file mode 100644 index 00000000000000..d094ca6bc975c8 --- /dev/null +++ b/lib/internal/http/outgoing.js @@ -0,0 +1,1204 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + Array, + ArrayIsArray, + MathFloor, + ObjectDefineProperty, + ObjectHasOwn, + ObjectKeys, + ObjectSetPrototypeOf, + ObjectValues, + SafeSet, + Symbol, +} = primordials; + +const { getDefaultHighWaterMark } = require('internal/streams/state'); +const assert = require('internal/assert'); +const EE = require('events'); +const Stream = require('stream'); +const { kOutHeaders, utcDate, kNeedDrain } = require('internal/http'); +const { Buffer } = require('buffer'); +const { + _checkIsHttpToken: checkIsHttpToken, + _checkInvalidHeaderChar: checkInvalidHeaderChar, + chunkExpression: RE_TE_CHUNKED, +} = require('internal/http/common'); +const { + defaultTriggerAsyncIdScope, + symbols: { async_id_symbol }, +} = require('internal/async_hooks'); +const { + codes: { + ERR_HTTP_BODY_NOT_ALLOWED, + ERR_HTTP_CONTENT_LENGTH_MISMATCH, + ERR_HTTP_HEADERS_SENT, + ERR_HTTP_INVALID_HEADER_VALUE, + ERR_HTTP_TRAILER_INVALID, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_CHAR, + ERR_INVALID_HTTP_TOKEN, + ERR_METHOD_NOT_IMPLEMENTED, + ERR_STREAM_ALREADY_FINISHED, + ERR_STREAM_CANNOT_PIPE, + ERR_STREAM_DESTROYED, + ERR_STREAM_NULL_VALUES, + ERR_STREAM_WRITE_AFTER_END, + }, + hideStackFrames, +} = require('internal/errors'); +const { validateString } = require('internal/validators'); +const { assignFunctionName } = require('internal/util'); +const { isUint8Array } = require('internal/util/types'); + +let debug = require('internal/util/debuglog').debuglog('http', (fn) => { + debug = fn; +}); + +const kCorked = Symbol('corked'); +const kSocket = Symbol('kSocket'); +const kChunkedBuffer = Symbol('kChunkedBuffer'); +const kChunkedLength = Symbol('kChunkedLength'); +const kUniqueHeaders = Symbol('kUniqueHeaders'); +const kBytesWritten = Symbol('kBytesWritten'); +const kErrored = Symbol('errored'); +const kHighWaterMark = Symbol('kHighWaterMark'); +const kRejectNonStandardBodyWrites = Symbol('kRejectNonStandardBodyWrites'); + +const nop = () => {}; + +const RE_CONN_CLOSE = /(?:^|\W)close(?:$|\W)/i; + +// isCookieField performs a case-insensitive comparison of a provided string +// against the word "cookie." As of V8 6.6 this is faster than handrolling or +// using a case-insensitive RegExp. +function isCookieField(s) { + return s.length === 6 && s.toLowerCase() === 'cookie'; +} + +function isContentDispositionField(s) { + return s.length === 19 && s.toLowerCase() === 'content-disposition'; +} + +function OutgoingMessage(options) { + Stream.call(this); + + // Queue that holds all currently pending data, until the response will be + // assigned to the socket (until it will its turn in the HTTP pipeline). + this.outputData = []; + + // `outputSize` is an approximate measure of how much data is queued on this + // response. `_onPendingData` will be invoked to update similar global + // per-connection counter. That counter will be used to pause/unpause the + // TCP socket and HTTP Parser and thus handle the backpressure. + this.outputSize = 0; + + this.writable = true; + this.destroyed = false; + + this._last = false; + this.chunkedEncoding = false; + this.shouldKeepAlive = true; + this.maxRequestsOnConnectionReached = false; + this._defaultKeepAlive = true; + this.useChunkedEncodingByDefault = true; + this.sendDate = false; + this._removedConnection = false; + this._removedContLen = false; + this._removedTE = false; + + this.strictContentLength = false; + this[kBytesWritten] = 0; + this._contentLength = null; + this._hasBody = true; + this._trailer = ''; + this[kNeedDrain] = false; + + this.finished = false; + this._headerSent = false; + this[kCorked] = 0; + this[kChunkedBuffer] = []; + this[kChunkedLength] = 0; + this._closed = false; + + this[kSocket] = null; + this._header = null; + this[kOutHeaders] = null; + + this._keepAliveTimeout = 0; + + this._onPendingData = nop; + + this[kErrored] = null; + this[kHighWaterMark] = options?.highWaterMark ?? getDefaultHighWaterMark(); + this[kRejectNonStandardBodyWrites] = options?.rejectNonStandardBodyWrites ?? false; +} +ObjectSetPrototypeOf(OutgoingMessage.prototype, Stream.prototype); +ObjectSetPrototypeOf(OutgoingMessage, Stream); + +ObjectDefineProperty(OutgoingMessage.prototype, 'errored', { + __proto__: null, + get() { + return this[kErrored]; + }, +}); + +ObjectDefineProperty(OutgoingMessage.prototype, 'closed', { + __proto__: null, + get() { + return this._closed; + }, +}); + +ObjectDefineProperty(OutgoingMessage.prototype, 'writableFinished', { + __proto__: null, + get() { + return ( + this.finished && + this.outputSize === 0 && + (!this[kSocket] || this[kSocket].writableLength === 0) + ); + }, +}); + +ObjectDefineProperty(OutgoingMessage.prototype, 'writableObjectMode', { + __proto__: null, + get() { + return false; + }, +}); + +ObjectDefineProperty(OutgoingMessage.prototype, 'writableLength', { + __proto__: null, + get() { + return this.outputSize + this[kChunkedLength] + (this[kSocket] ? this[kSocket].writableLength : 0); + }, +}); + +ObjectDefineProperty(OutgoingMessage.prototype, 'writableHighWaterMark', { + __proto__: null, + get() { + return this[kSocket] ? this[kSocket].writableHighWaterMark : this[kHighWaterMark]; + }, +}); + +ObjectDefineProperty(OutgoingMessage.prototype, 'writableCorked', { + __proto__: null, + get() { + return this[kCorked]; + }, +}); + +ObjectDefineProperty(OutgoingMessage.prototype, 'connection', { + __proto__: null, + get: function() { + return this[kSocket]; + }, + set: function(val) { + this.socket = val; + }, +}); + +ObjectDefineProperty(OutgoingMessage.prototype, 'socket', { + __proto__: null, + get: function() { + return this[kSocket]; + }, + set: function(val) { + for (let n = 0; n < this[kCorked]; n++) { + val?.cork(); + this[kSocket]?.uncork(); + } + this[kSocket] = val; + }, +}); + +OutgoingMessage.prototype._renderHeaders = function _renderHeaders() { + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT('render'); + } + + const headersMap = this[kOutHeaders]; + const headers = {}; + + if (headersMap !== null) { + const keys = ObjectKeys(headersMap); + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0, l = keys.length; i < l; i++) { + const key = keys[i]; + headers[headersMap[key][0]] = headersMap[key][1]; + } + } + return headers; +}; + +OutgoingMessage.prototype.cork = function cork() { + this[kCorked]++; + if (this[kSocket]) { + this[kSocket].cork(); + } +}; + +OutgoingMessage.prototype.uncork = function uncork() { + this[kCorked]--; + if (this[kSocket]) { + this[kSocket].uncork(); + } + + if (this[kCorked] || this[kChunkedBuffer].length === 0) { + return; + } + + const len = this[kChunkedLength]; + const buf = this[kChunkedBuffer]; + + assert(this.chunkedEncoding); + + let callbacks; + this._send(len.toString(16), 'latin1', null); + this._send(crlf_buf, null, null); + for (let n = 0; n < buf.length; n += 3) { + this._send(buf[n + 0], buf[n + 1], null); + if (buf[n + 2]) { + callbacks ??= []; + callbacks.push(buf[n + 2]); + } + } + this._send(crlf_buf, null, callbacks.length ? (err) => { + for (const callback of callbacks) { + callback(err); + } + } : null); + + this[kChunkedBuffer].length = 0; + this[kChunkedLength] = 0; +}; + +OutgoingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { + + if (callback) { + this.on('timeout', callback); + } + + if (!this[kSocket]) { + this.once('socket', function socketSetTimeoutOnConnect(socket) { + socket.setTimeout(msecs); + }); + } else { + this[kSocket].setTimeout(msecs); + } + return this; +}; + + +// It's possible that the socket will be destroyed, and removed from +// any messages, before ever calling this. In that case, just skip +// it, since something else is destroying this connection anyway. +OutgoingMessage.prototype.destroy = function destroy(error) { + if (this.destroyed) { + return this; + } + this.destroyed = true; + + this[kErrored] = error; + + if (this[kSocket]) { + this[kSocket].destroy(error); + } else { + this.once('socket', function socketDestroyOnConnect(socket) { + socket.destroy(error); + }); + } + + return this; +}; + + +// This abstract either writing directly to the socket or buffering it. +OutgoingMessage.prototype._send = function _send(data, encoding, callback, byteLength) { + // This is a shameful hack to get the headers and first body chunk onto + // the same packet. Future versions of Node are going to take care of + // this at a lower level and in a more general way. + if (!this._headerSent && this._header !== null) { + // `this._header` can be null if OutgoingMessage is used without a proper Socket + // See: /test/parallel/test-http-outgoing-message-inheritance.js + if (typeof data === 'string' && + (encoding === 'utf8' || encoding === 'latin1' || !encoding)) { + data = this._header + data; + } else { + const header = this._header; + this.outputData.unshift({ + data: header, + encoding: 'latin1', + callback: null, + }); + this.outputSize += header.length; + this._onPendingData(header.length); + } + this._headerSent = true; + } + return this._writeRaw(data, encoding, callback, byteLength); +}; + +OutgoingMessage.prototype._writeRaw = _writeRaw; +function _writeRaw(data, encoding, callback, size) { + const conn = this[kSocket]; + if (conn?.destroyed) { + // The socket was destroyed. If we're still trying to write to it, + // then we haven't gotten the 'close' event yet. + return false; + } + + if (typeof encoding === 'function') { + callback = encoding; + encoding = null; + } + + if (conn && conn._httpMessage === this && conn.writable) { + // There might be pending data in the this.output buffer. + if (this.outputData.length) { + this._flushOutput(conn); + } + // Directly write to socket. + return conn.write(data, encoding, callback); + } + // Buffer, as long as we're not destroyed. + this.outputData.push({ data, encoding, callback }); + this.outputSize += data.length; + this._onPendingData(data.length); + return this.outputSize < this[kHighWaterMark]; +} + + +OutgoingMessage.prototype._storeHeader = _storeHeader; +function _storeHeader(firstLine, headers) { + // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n' + // in the case of response it is: 'HTTP/1.1 200 OK\r\n' + const state = { + connection: false, + contLen: false, + te: false, + date: false, + expect: false, + trailer: false, + header: firstLine, + }; + + if (headers) { + if (headers === this[kOutHeaders]) { + for (const key in headers) { + const entry = headers[key]; + processHeader(this, state, entry[0], entry[1], false); + } + } else if (ArrayIsArray(headers)) { + if (headers.length && ArrayIsArray(headers[0])) { + for (let i = 0; i < headers.length; i++) { + const entry = headers[i]; + processHeader(this, state, entry[0], entry[1], true); + } + } else { + if (headers.length % 2 !== 0) { + throw new ERR_INVALID_ARG_VALUE('headers', headers); + } + + for (let n = 0; n < headers.length; n += 2) { + processHeader(this, state, headers[n + 0], headers[n + 1], true); + } + } + } else { + for (const key in headers) { + if (ObjectHasOwn(headers, key)) { + processHeader(this, state, key, headers[key], true); + } + } + } + } + + let { header } = state; + + // Date header + if (this.sendDate && !state.date) { + header += 'Date: ' + utcDate() + '\r\n'; + } + + // Force the connection to close when the response is a 204 No Content or + // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked" + // header. + // + // RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but + // node.js used to send out a zero chunk anyway to accommodate clients + // that don't have special handling for those responses. + // + // It was pointed out that this might confuse reverse proxies to the point + // of creating security liabilities, so suppress the zero chunk and force + // the connection to close. + if (this.chunkedEncoding && (this.statusCode === 204 || + this.statusCode === 304)) { + debug(this.statusCode + ' response should not use chunked encoding,' + + ' closing connection.'); + this.chunkedEncoding = false; + this.shouldKeepAlive = false; + } + + // keep-alive logic + if (this._removedConnection) { + // shouldKeepAlive is generally true for HTTP/1.1. In that common case, + // even if the connection header isn't sent, we still persist by default. + this._last = !this.shouldKeepAlive; + } else if (!state.connection) { + const shouldSendKeepAlive = this.shouldKeepAlive && + (state.contLen || this.useChunkedEncodingByDefault || this.agent); + if (shouldSendKeepAlive && this.maxRequestsOnConnectionReached) { + header += 'Connection: close\r\n'; + } else if (shouldSendKeepAlive) { + header += 'Connection: keep-alive\r\n'; + if (this._keepAliveTimeout && this._defaultKeepAlive) { + const timeoutSeconds = MathFloor(this._keepAliveTimeout / 1000); + let max = ''; + if (~~this._maxRequestsPerSocket > 0) { + max = `, max=${this._maxRequestsPerSocket}`; + } + header += `Keep-Alive: timeout=${timeoutSeconds}${max}\r\n`; + } + } else { + this._last = true; + header += 'Connection: close\r\n'; + } + } + + if (!state.contLen && !state.te) { + if (!this._hasBody) { + // Make sure we don't end the 0\r\n\r\n at the end of the message. + this.chunkedEncoding = false; + } else if (!this.useChunkedEncodingByDefault) { + this._last = true; + } else if (!state.trailer && + !this._removedContLen && + typeof this._contentLength === 'number') { + header += 'Content-Length: ' + this._contentLength + '\r\n'; + } else if (!this._removedTE) { + header += 'Transfer-Encoding: chunked\r\n'; + this.chunkedEncoding = true; + } else { + // We should only be able to get here if both Content-Length and + // Transfer-Encoding are removed by the user. + // See: test/parallel/test-http-remove-header-stays-removed.js + debug('Both Content-Length and Transfer-Encoding are removed'); + + // We can't keep alive in this case, because with no header info the body + // is defined as all data until the connection is closed. + this._last = true; + } + } + + // Test non-chunked message does not have trailer header set, + // message will be terminated by the first empty line after the + // header fields, regardless of the header fields present in the + // message, and thus cannot contain a message body or 'trailers'. + if (this.chunkedEncoding !== true && state.trailer) { + throw new ERR_HTTP_TRAILER_INVALID(); + } + + this._header = header + '\r\n'; + this._headerSent = false; + + // Wait until the first body chunk, or close(), is sent to flush, + // UNLESS we're sending Expect: 100-continue. + if (state.expect) this._send(''); +} + +function processHeader(self, state, key, value, validate) { + if (validate) + validateHeaderName(key); + + // If key is content-disposition and there is content-length + // encode the value in latin1 + // https://www.rfc-editor.org/rfc/rfc6266#section-4.3 + // Refs: https://github.com/nodejs/node/pull/46528 + if (isContentDispositionField(key) && self._contentLength) { + // The value could be an array here + if (ArrayIsArray(value)) { + for (let i = 0; i < value.length; i++) { + value[i] = Buffer.from(value[i], 'latin1'); + } + } else { + value = Buffer.from(value, 'latin1'); + } + } + + if (ArrayIsArray(value)) { + if ( + (value.length < 2 || !isCookieField(key)) && + (!self[kUniqueHeaders] || !self[kUniqueHeaders].has(key.toLowerCase())) + ) { + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0; i < value.length; i++) + storeHeader(self, state, key, value[i], validate); + return; + } + value = value.join('; '); + } + storeHeader(self, state, key, value, validate); +} + +function storeHeader(self, state, key, value, validate) { + if (validate) + validateHeaderValue(key, value); + state.header += key + ': ' + value + '\r\n'; + matchHeader(self, state, key, value); +} + +function matchHeader(self, state, field, value) { + if (field.length < 4 || field.length > 17) + return; + field = field.toLowerCase(); + switch (field) { + case 'connection': + state.connection = true; + self._removedConnection = false; + if (RE_CONN_CLOSE.test(value)) + self._last = true; + else + self.shouldKeepAlive = true; + break; + case 'transfer-encoding': + state.te = true; + self._removedTE = false; + if (RE_TE_CHUNKED.test(value)) + self.chunkedEncoding = true; + break; + case 'content-length': + state.contLen = true; + self._contentLength = +value; + self._removedContLen = false; + break; + case 'date': + case 'expect': + case 'trailer': + state[field] = true; + break; + case 'keep-alive': + self._defaultKeepAlive = false; + break; + } +} + +const validateHeaderName = assignFunctionName('validateHeaderName', hideStackFrames((name, label) => { + if (typeof name !== 'string' || !name || !checkIsHttpToken(name)) { + throw new ERR_INVALID_HTTP_TOKEN.HideStackFramesError(label || 'Header name', name); + } +})); + +const validateHeaderValue = assignFunctionName('validateHeaderValue', hideStackFrames((name, value) => { + if (value === undefined) { + throw new ERR_HTTP_INVALID_HEADER_VALUE.HideStackFramesError(value, name); + } + if (checkInvalidHeaderChar(value)) { + debug('Header "%s" contains invalid characters', name); + throw new ERR_INVALID_CHAR.HideStackFramesError('header content', name); + } +})); + +function parseUniqueHeadersOption(headers) { + if (!ArrayIsArray(headers)) { + return null; + } + + const unique = new SafeSet(); + const l = headers.length; + for (let i = 0; i < l; i++) { + unique.add(headers[i].toLowerCase()); + } + + return unique; +} + +OutgoingMessage.prototype.setHeader = function setHeader(name, value) { + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT('set'); + } + validateHeaderName(name); + validateHeaderValue(name, value); + + let headers = this[kOutHeaders]; + if (headers === null) + this[kOutHeaders] = headers = { __proto__: null }; + + headers[name.toLowerCase()] = [name, value]; + return this; +}; + +OutgoingMessage.prototype.setHeaders = function setHeaders(headers) { + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT('set'); + } + + + if ( + !headers || + ArrayIsArray(headers) || + typeof headers.keys !== 'function' || + typeof headers.get !== 'function' + ) { + throw new ERR_INVALID_ARG_TYPE('headers', ['Headers', 'Map'], headers); + } + + // Headers object joins multiple cookies with a comma when using + // the getter to retrieve the value, + // unless iterating over the headers directly. + // We also cannot safely split by comma. + // To avoid setHeader overwriting the previous value we push + // set-cookie values in array and set them all at once. + const cookies = []; + + for (const { 0: key, 1: value } of headers) { + if (key === 'set-cookie') { + if (ArrayIsArray(value)) { + cookies.push(...value); + } else { + cookies.push(value); + } + continue; + } + this.setHeader(key, value); + } + if (cookies.length) { + this.setHeader('set-cookie', cookies); + } + + return this; +}; + +OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) { + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT('append'); + } + validateHeaderName(name); + validateHeaderValue(name, value); + + const field = name.toLowerCase(); + const headers = this[kOutHeaders]; + if (headers === null || !headers[field]) { + return this.setHeader(name, value); + } + + // Prepare the field for appending, if required + if (!ArrayIsArray(headers[field][1])) { + headers[field][1] = [headers[field][1]]; + } + + const existingValues = headers[field][1]; + if (ArrayIsArray(value)) { + for (let i = 0, length = value.length; i < length; i++) { + existingValues.push(value[i]); + } + } else { + existingValues.push(value); + } + + return this; +}; + + +OutgoingMessage.prototype.getHeader = function getHeader(name) { + validateString(name, 'name'); + + const headers = this[kOutHeaders]; + if (headers === null) + return; + + const entry = headers[name.toLowerCase()]; + return entry?.[1]; +}; + + +// Returns an array of the names of the current outgoing headers. +OutgoingMessage.prototype.getHeaderNames = function getHeaderNames() { + return this[kOutHeaders] !== null ? ObjectKeys(this[kOutHeaders]) : []; +}; + + +// Returns an array of the names of the current outgoing raw headers. +OutgoingMessage.prototype.getRawHeaderNames = function getRawHeaderNames() { + const headersMap = this[kOutHeaders]; + if (headersMap === null) return []; + + const values = ObjectValues(headersMap); + const headers = Array(values.length); + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0, l = values.length; i < l; i++) { + headers[i] = values[i][0]; + } + + return headers; +}; + + +// Returns a shallow copy of the current outgoing headers. +OutgoingMessage.prototype.getHeaders = function getHeaders() { + const headers = this[kOutHeaders]; + const ret = { __proto__: null }; + if (headers) { + const keys = ObjectKeys(headers); + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0; i < keys.length; ++i) { + const key = keys[i]; + const val = headers[key][1]; + ret[key] = val; + } + } + return ret; +}; + + +OutgoingMessage.prototype.hasHeader = function hasHeader(name) { + validateString(name, 'name'); + return this[kOutHeaders] !== null && + !!this[kOutHeaders][name.toLowerCase()]; +}; + + +OutgoingMessage.prototype.removeHeader = function removeHeader(name) { + validateString(name, 'name'); + + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT('remove'); + } + + const key = name.toLowerCase(); + + switch (key) { + case 'connection': + this._removedConnection = true; + break; + case 'content-length': + this._removedContLen = true; + break; + case 'transfer-encoding': + this._removedTE = true; + break; + case 'date': + this.sendDate = false; + break; + } + + if (this[kOutHeaders] !== null) { + delete this[kOutHeaders][key]; + } +}; + + +OutgoingMessage.prototype._implicitHeader = function _implicitHeader() { + throw new ERR_METHOD_NOT_IMPLEMENTED('_implicitHeader()'); +}; + +ObjectDefineProperty(OutgoingMessage.prototype, 'headersSent', { + __proto__: null, + configurable: true, + enumerable: true, + get: function() { return !!this._header; }, +}); + +ObjectDefineProperty(OutgoingMessage.prototype, 'writableEnded', { + __proto__: null, + get: function() { return this.finished; }, +}); + +ObjectDefineProperty(OutgoingMessage.prototype, 'writableNeedDrain', { + __proto__: null, + get: function() { + return !this.destroyed && !this.finished && this[kNeedDrain]; + }, +}); + +const crlf_buf = Buffer.from('\r\n'); +OutgoingMessage.prototype.write = function write(chunk, encoding, callback) { + if (typeof encoding === 'function') { + callback = encoding; + encoding = null; + } + + const ret = write_(this, chunk, encoding, callback, false); + if (!ret) + this[kNeedDrain] = true; + return ret; +}; + +function onError(msg, err, callback) { + if (msg.destroyed) { + return; + } + + const triggerAsyncId = msg.socket ? msg.socket[async_id_symbol] : undefined; + defaultTriggerAsyncIdScope(triggerAsyncId, + process.nextTick, + emitErrorNt, + msg, + err, + callback); +} + +function emitErrorNt(msg, err, callback) { + callback(err); + if (typeof msg.emit === 'function' && !msg.destroyed) { + msg.emit('error', err); + } +} + +function strictContentLength(msg) { + return ( + msg.strictContentLength && + msg._contentLength != null && + msg._hasBody && + !msg._removedContLen && + !msg.chunkedEncoding && + !msg.hasHeader('transfer-encoding') + ); +} + +function write_(msg, chunk, encoding, callback, fromEnd) { + if (typeof callback !== 'function') + callback = nop; + + if (chunk === null) { + throw new ERR_STREAM_NULL_VALUES(); + } else if (typeof chunk !== 'string' && !isUint8Array(chunk)) { + throw new ERR_INVALID_ARG_TYPE( + 'chunk', ['string', 'Buffer', 'Uint8Array'], chunk); + } + + let err; + if (msg.finished) { + err = new ERR_STREAM_WRITE_AFTER_END(); + } else if (msg.destroyed) { + err = new ERR_STREAM_DESTROYED('write'); + } + + if (err) { + if (!msg.destroyed) { + onError(msg, err, callback); + } else { + process.nextTick(callback, err); + } + return false; + } + + let len; + + if (msg.strictContentLength) { + len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength; + + if ( + strictContentLength(msg) && + (fromEnd ? msg[kBytesWritten] + len !== msg._contentLength : msg[kBytesWritten] + len > msg._contentLength) + ) { + throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(len + msg[kBytesWritten], msg._contentLength); + } + + msg[kBytesWritten] += len; + } + + if (!msg._header) { + if (fromEnd) { + len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength; + msg._contentLength = len; + } + msg._implicitHeader(); + } + + if (!msg._hasBody) { + if (msg[kRejectNonStandardBodyWrites]) { + throw new ERR_HTTP_BODY_NOT_ALLOWED(); + } else { + debug('This type of response MUST NOT have a body. ' + + 'Ignoring write() calls.'); + process.nextTick(callback); + return true; + } + } + + if (!fromEnd && msg.socket && !msg.socket.writableCorked) { + msg.socket.cork(); + process.nextTick(connectionCorkNT, msg.socket); + } + + let ret; + if (msg.chunkedEncoding && chunk.length !== 0) { + len ??= typeof chunk === 'string' ? Buffer.byteLength(chunk, encoding) : chunk.byteLength; + if (msg[kCorked] && msg._headerSent) { + msg[kChunkedBuffer].push(chunk, encoding, callback); + msg[kChunkedLength] += len; + ret = msg[kChunkedLength] < msg[kHighWaterMark]; + } else { + msg._send(len.toString(16), 'latin1', null); + msg._send(crlf_buf, null, null); + msg._send(chunk, encoding, null, len); + ret = msg._send(crlf_buf, null, callback); + } + } else { + ret = msg._send(chunk, encoding, callback, len); + } + + debug('write ret = ' + ret); + return ret; +} + + +function connectionCorkNT(conn) { + conn.uncork(); +} + +OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { + this._trailer = ''; + const keys = ObjectKeys(headers); + const isArray = ArrayIsArray(headers); + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0, l = keys.length; i < l; i++) { + let field, value; + const key = keys[i]; + if (isArray) { + field = headers[key][0]; + value = headers[key][1]; + } else { + field = key; + value = headers[key]; + } + validateHeaderName(field, 'Trailer name'); + + // Check if the field must be sent several times + const isArrayValue = ArrayIsArray(value); + if ( + isArrayValue && value.length > 1 && + (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase())) + ) { + for (let j = 0, l = value.length; j < l; j++) { + if (checkInvalidHeaderChar(value[j])) { + debug('Trailer "%s"[%d] contains invalid characters', field, j); + throw new ERR_INVALID_CHAR('trailer content', field); + } + this._trailer += field + ': ' + value[j] + '\r\n'; + } + } else { + if (isArrayValue) { + value = value.join('; '); + } + + if (checkInvalidHeaderChar(value)) { + debug('Trailer "%s" contains invalid characters', field); + throw new ERR_INVALID_CHAR('trailer content', field); + } + this._trailer += field + ': ' + value + '\r\n'; + } + } +}; + +function onFinish(outmsg) { + if (outmsg?.socket?._hadError) return; + outmsg.emit('finish'); +} + +OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { + if (typeof chunk === 'function') { + callback = chunk; + chunk = null; + encoding = null; + } else if (typeof encoding === 'function') { + callback = encoding; + encoding = null; + } + + if (chunk) { + if (this.finished) { + onError(this, + new ERR_STREAM_WRITE_AFTER_END(), + typeof callback !== 'function' ? nop : callback); + return this; + } + + if (this[kSocket]) { + this[kSocket].cork(); + } + + write_(this, chunk, encoding, null, true); + } else if (this.finished) { + if (typeof callback === 'function') { + if (!this.writableFinished) { + this.on('finish', callback); + } else { + callback(new ERR_STREAM_ALREADY_FINISHED('end')); + } + } + return this; + } else if (!this._header) { + if (this[kSocket]) { + this[kSocket].cork(); + } + + this._contentLength = 0; + this._implicitHeader(); + } + + if (typeof callback === 'function') + this.once('finish', callback); + + if (strictContentLength(this) && this[kBytesWritten] !== this._contentLength) { + throw new ERR_HTTP_CONTENT_LENGTH_MISMATCH(this[kBytesWritten], this._contentLength); + } + + const finish = onFinish.bind(undefined, this); + + if (this._hasBody && this.chunkedEncoding) { + this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finish); + } else if (!this._headerSent || this.writableLength || chunk) { + this._send('', 'latin1', finish); + } else { + process.nextTick(finish); + } + + if (this[kSocket]) { + // Fully uncork connection on end(). + this[kSocket]._writableState.corked = 1; + this[kSocket].uncork(); + } + this[kCorked] = 1; + this.uncork(); + + this.finished = true; + + // There is the first message on the outgoing queue, and we've sent + // everything to the socket. + debug('outgoing message end.'); + if (this.outputData.length === 0 && + this[kSocket] && + this[kSocket]._httpMessage === this) { + this._finish(); + } + + return this; +}; + + +// This function is called once all user data are flushed to the socket. +// Note that it has a chance that the socket is not drained. +OutgoingMessage.prototype._finish = function _finish() { + assert(this[kSocket]); + this.emit('prefinish'); +}; + + +// This logic is probably a bit confusing. Let me explain a bit: +// +// In both HTTP servers and clients it is possible to queue up several +// outgoing messages. This is easiest to imagine in the case of a client. +// Take the following situation: +// +// req1 = client.request('GET', '/'); +// req2 = client.request('POST', '/'); +// +// When the user does +// +// req2.write('hello world\n'); +// +// it's possible that the first request has not been completely flushed to +// the socket yet. Thus the outgoing messages need to be prepared to queue +// up data internally before sending it on further to the socket's queue. +// +// This function, _flush(), is called by both the Server and Client +// to attempt to flush any pending messages out to the socket. +OutgoingMessage.prototype._flush = function _flush() { + const socket = this[kSocket]; + + if (socket?.writable) { + // There might be remaining data in this.output; write it out + const ret = this._flushOutput(socket); + + if (this.finished) { + // This is a queue to the server or client to bring in the next this. + this._finish(); + } else if (ret && this[kNeedDrain]) { + this[kNeedDrain] = false; + this.emit('drain'); + } + } +}; + +OutgoingMessage.prototype._flushOutput = function _flushOutput(socket) { + const outputLength = this.outputData.length; + if (outputLength <= 0) + return undefined; + + const outputData = this.outputData; + socket.cork(); + let ret; + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0; i < outputLength; i++) { + const { data, encoding, callback } = outputData[i]; + // Avoid any potential ref to Buffer in new generation from old generation + outputData[i].data = null; + ret = socket.write(data, encoding, callback); + } + socket.uncork(); + + this.outputData = []; + this._onPendingData(-this.outputSize); + this.outputSize = 0; + + return ret; +}; + + +OutgoingMessage.prototype.flushHeaders = function flushHeaders() { + if (!this._header) { + this._implicitHeader(); + } + + // Force-flush the headers. + this._send(''); +}; + +OutgoingMessage.prototype.pipe = function pipe() { + // OutgoingMessage should be write-only. Piping from it is disabled. + this.emit('error', new ERR_STREAM_CANNOT_PIPE()); +}; + +OutgoingMessage.prototype[EE.captureRejectionSymbol] = +function(err, event) { + this.destroy(err); +}; + +module.exports = { + kHighWaterMark, + kUniqueHeaders, + parseUniqueHeadersOption, + validateHeaderName, + validateHeaderValue, + OutgoingMessage, +}; diff --git a/lib/internal/http/server.js b/lib/internal/http/server.js new file mode 100644 index 00000000000000..a5f6811e24ab80 --- /dev/null +++ b/lib/internal/http/server.js @@ -0,0 +1,1232 @@ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +const { + ArrayIsArray, + Error, + MathMin, + ObjectKeys, + ObjectSetPrototypeOf, + ReflectApply, + Symbol, + SymbolAsyncDispose, + SymbolFor, +} = primordials; + +const net = require('net'); +const EE = require('events'); +const assert = require('internal/assert'); +const { + parsers, + freeParser, + continueExpression, + chunkExpression, + kIncomingMessage, + HTTPParser, + isLenient, + _checkInvalidHeaderChar: checkInvalidHeaderChar, + prepareError, +} = require('internal/http/common'); +const { ConnectionsList } = internalBinding('http_parser'); +const { + kUniqueHeaders, + parseUniqueHeadersOption, + OutgoingMessage, +} = require('internal/http/outgoing'); +const { + kOutHeaders, + kNeedDrain, + isTraceHTTPEnabled, + traceBegin, + traceEnd, + getNextTraceEventId, +} = require('internal/http'); +const { + defaultTriggerAsyncIdScope, + getOrSetAsyncId, +} = require('internal/async_hooks'); +const { IncomingMessage } = require('internal/http/incoming'); +const { + ConnResetException, + codes: { + ERR_HTTP_HEADERS_SENT, + ERR_HTTP_INVALID_STATUS_CODE, + ERR_HTTP_REQUEST_TIMEOUT, + ERR_HTTP_SOCKET_ASSIGNED, + ERR_HTTP_SOCKET_ENCODING, + ERR_INVALID_ARG_VALUE, + ERR_INVALID_CHAR, + ERR_OUT_OF_RANGE, + }, +} = require('internal/errors'); +const { + assignFunctionName, + kEmptyObject, + promisify, +} = require('internal/util'); +const { + validateInteger, + validateBoolean, + validateLinkHeaderValue, + validateObject, +} = require('internal/validators'); +const Buffer = require('buffer').Buffer; +const { setInterval, clearInterval } = require('timers'); +let debug = require('internal/util/debuglog').debuglog('http', (fn) => { + debug = fn; +}); + +const dc = require('diagnostics_channel'); +const onRequestStartChannel = dc.channel('http.server.request.start'); +const onResponseCreatedChannel = dc.channel('http.server.response.created'); +const onResponseFinishChannel = dc.channel('http.server.response.finish'); + +const kServerResponse = Symbol('ServerResponse'); +const kServerResponseStatistics = Symbol('ServerResponseStatistics'); + +const { + hasObserver, + startPerf, + stopPerf, +} = require('internal/perf/observe'); + +const STATUS_CODES = { + 100: 'Continue', // RFC 7231 6.2.1 + 101: 'Switching Protocols', // RFC 7231 6.2.2 + 102: 'Processing', // RFC 2518 10.1 (obsoleted by RFC 4918) + 103: 'Early Hints', // RFC 8297 2 + 200: 'OK', // RFC 7231 6.3.1 + 201: 'Created', // RFC 7231 6.3.2 + 202: 'Accepted', // RFC 7231 6.3.3 + 203: 'Non-Authoritative Information', // RFC 7231 6.3.4 + 204: 'No Content', // RFC 7231 6.3.5 + 205: 'Reset Content', // RFC 7231 6.3.6 + 206: 'Partial Content', // RFC 7233 4.1 + 207: 'Multi-Status', // RFC 4918 11.1 + 208: 'Already Reported', // RFC 5842 7.1 + 226: 'IM Used', // RFC 3229 10.4.1 + 300: 'Multiple Choices', // RFC 7231 6.4.1 + 301: 'Moved Permanently', // RFC 7231 6.4.2 + 302: 'Found', // RFC 7231 6.4.3 + 303: 'See Other', // RFC 7231 6.4.4 + 304: 'Not Modified', // RFC 7232 4.1 + 305: 'Use Proxy', // RFC 7231 6.4.5 + 307: 'Temporary Redirect', // RFC 7231 6.4.7 + 308: 'Permanent Redirect', // RFC 7238 3 + 400: 'Bad Request', // RFC 7231 6.5.1 + 401: 'Unauthorized', // RFC 7235 3.1 + 402: 'Payment Required', // RFC 7231 6.5.2 + 403: 'Forbidden', // RFC 7231 6.5.3 + 404: 'Not Found', // RFC 7231 6.5.4 + 405: 'Method Not Allowed', // RFC 7231 6.5.5 + 406: 'Not Acceptable', // RFC 7231 6.5.6 + 407: 'Proxy Authentication Required', // RFC 7235 3.2 + 408: 'Request Timeout', // RFC 7231 6.5.7 + 409: 'Conflict', // RFC 7231 6.5.8 + 410: 'Gone', // RFC 7231 6.5.9 + 411: 'Length Required', // RFC 7231 6.5.10 + 412: 'Precondition Failed', // RFC 7232 4.2 + 413: 'Payload Too Large', // RFC 7231 6.5.11 + 414: 'URI Too Long', // RFC 7231 6.5.12 + 415: 'Unsupported Media Type', // RFC 7231 6.5.13 + 416: 'Range Not Satisfiable', // RFC 7233 4.4 + 417: 'Expectation Failed', // RFC 7231 6.5.14 + 418: 'I\'m a Teapot', // RFC 7168 2.3.3 + 421: 'Misdirected Request', // RFC 7540 9.1.2 + 422: 'Unprocessable Entity', // RFC 4918 11.2 + 423: 'Locked', // RFC 4918 11.3 + 424: 'Failed Dependency', // RFC 4918 11.4 + 425: 'Too Early', // RFC 8470 5.2 + 426: 'Upgrade Required', // RFC 2817 and RFC 7231 6.5.15 + 428: 'Precondition Required', // RFC 6585 3 + 429: 'Too Many Requests', // RFC 6585 4 + 431: 'Request Header Fields Too Large', // RFC 6585 5 + 451: 'Unavailable For Legal Reasons', // RFC 7725 3 + 500: 'Internal Server Error', // RFC 7231 6.6.1 + 501: 'Not Implemented', // RFC 7231 6.6.2 + 502: 'Bad Gateway', // RFC 7231 6.6.3 + 503: 'Service Unavailable', // RFC 7231 6.6.4 + 504: 'Gateway Timeout', // RFC 7231 6.6.5 + 505: 'HTTP Version Not Supported', // RFC 7231 6.6.6 + 506: 'Variant Also Negotiates', // RFC 2295 8.1 + 507: 'Insufficient Storage', // RFC 4918 11.5 + 508: 'Loop Detected', // RFC 5842 7.2 + 509: 'Bandwidth Limit Exceeded', + 510: 'Not Extended', // RFC 2774 7 + 511: 'Network Authentication Required', // RFC 6585 6 +}; + +const kOnExecute = HTTPParser.kOnExecute | 0; +const kOnTimeout = HTTPParser.kOnTimeout | 0; +const kLenientAll = HTTPParser.kLenientAll | 0; +const kLenientNone = HTTPParser.kLenientNone | 0; +const kConnections = Symbol('http.server.connections'); +const kConnectionsCheckingInterval = Symbol('http.server.connectionsCheckingInterval'); + +const HTTP_SERVER_TRACE_EVENT_NAME = 'http.server.request'; +// TODO(jazelly): make this configurable +const HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER = 1000; + +class HTTPServerAsyncResource { + constructor(type, socket) { + this.type = type; + this.socket = socket; + } +} + +function ServerResponse(req, options) { + OutgoingMessage.call(this, options); + + if (req.method === 'HEAD') this._hasBody = false; + + this.req = req; + this.sendDate = true; + this._sent100 = false; + this._expect_continue = false; + + if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { + this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te); + this.shouldKeepAlive = false; + } + + if (hasObserver('http')) { + startPerf(this, kServerResponseStatistics, { + type: 'http', + name: 'HttpRequest', + detail: { + req: { + method: req.method, + url: req.url, + headers: req.headers, + }, + }, + }); + } + if (isTraceHTTPEnabled()) { + this._traceEventId = getNextTraceEventId(); + traceBegin(HTTP_SERVER_TRACE_EVENT_NAME, this._traceEventId); + } + if (onResponseCreatedChannel.hasSubscribers) { + onResponseCreatedChannel.publish({ + request: req, + response: this, + }); + } +} +ObjectSetPrototypeOf(ServerResponse.prototype, OutgoingMessage.prototype); +ObjectSetPrototypeOf(ServerResponse, OutgoingMessage); + +ServerResponse.prototype._finish = function _finish() { + if (this[kServerResponseStatistics] && hasObserver('http')) { + stopPerf(this, kServerResponseStatistics, { + detail: { + res: { + statusCode: this.statusCode, + statusMessage: this.statusMessage, + headers: typeof this.getHeaders === 'function' ? this.getHeaders() : {}, + }, + }, + }); + } + OutgoingMessage.prototype._finish.call(this); + if (isTraceHTTPEnabled() && typeof this._traceEventId === 'number') { + const data = { + url: this.req?.url, + statusCode: this.statusCode, + }; + traceEnd(HTTP_SERVER_TRACE_EVENT_NAME, this._traceEventId, data); + } +}; + + +ServerResponse.prototype.statusCode = 200; +ServerResponse.prototype.statusMessage = undefined; + +function onServerResponseClose() { + // EventEmitter.emit makes a copy of the 'close' listeners array before + // calling the listeners. detachSocket() unregisters onServerResponseClose + // but if detachSocket() is called, directly or indirectly, by a 'close' + // listener, onServerResponseClose is still in that copy of the listeners + // array. That is, in the example below, b still gets called even though + // it's been removed by a: + // + // const EventEmitter = require('events'); + // const obj = new EventEmitter(); + // obj.on('event', a); + // obj.on('event', b); + // function a() { obj.removeListener('event', b) } + // function b() { throw "BAM!" } + // obj.emit('event'); // throws + // + // Ergo, we need to deal with stale 'close' events and handle the case + // where the ServerResponse object has already been deconstructed. + // Fortunately, that requires only a single if check. :-) + if (this._httpMessage) { + emitCloseNT(this._httpMessage); + } +} + +ServerResponse.prototype.assignSocket = function assignSocket(socket) { + if (socket._httpMessage) { + throw new ERR_HTTP_SOCKET_ASSIGNED(); + } + socket._httpMessage = this; + socket.on('close', onServerResponseClose); + this.socket = socket; + this.emit('socket', socket); + this._flush(); +}; + +ServerResponse.prototype.detachSocket = function detachSocket(socket) { + assert(socket._httpMessage === this); + socket.removeListener('close', onServerResponseClose); + socket._httpMessage = null; + this.socket = null; +}; + +ServerResponse.prototype.writeContinue = function writeContinue(cb) { + this._writeRaw('HTTP/1.1 100 Continue\r\n\r\n', 'ascii', cb); + this._sent100 = true; +}; + +ServerResponse.prototype.writeProcessing = function writeProcessing(cb) { + this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n', 'ascii', cb); +}; + +ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) { + let head = 'HTTP/1.1 103 Early Hints\r\n'; + + validateObject(hints, 'hints'); + + if (hints.link === null || hints.link === undefined) { + return; + } + + const link = validateLinkHeaderValue(hints.link); + + if (link.length === 0) { + return; + } + + head += 'Link: ' + link + '\r\n'; + + for (const key of ObjectKeys(hints)) { + if (key !== 'link') { + head += key + ': ' + hints[key] + '\r\n'; + } + } + + head += '\r\n'; + + this._writeRaw(head, 'ascii', cb); +}; + +ServerResponse.prototype._implicitHeader = function _implicitHeader() { + this.writeHead(this.statusCode); +}; + +ServerResponse.prototype.writeHead = writeHead; +function writeHead(statusCode, reason, obj) { + + if (this._header) { + throw new ERR_HTTP_HEADERS_SENT('write'); + } + + const originalStatusCode = statusCode; + + statusCode |= 0; + if (statusCode < 100 || statusCode > 999) { + throw new ERR_HTTP_INVALID_STATUS_CODE(originalStatusCode); + } + + + if (typeof reason === 'string') { + // writeHead(statusCode, reasonPhrase[, headers]) + this.statusMessage = reason; + } else { + // writeHead(statusCode[, headers]) + this.statusMessage ||= STATUS_CODES[statusCode] || 'unknown'; + obj ??= reason; + } + this.statusCode = statusCode; + + let headers; + if (this[kOutHeaders]) { + // Slow-case: when progressive API and header fields are passed. + let k; + if (ArrayIsArray(obj)) { + if (obj.length % 2 !== 0) { + throw new ERR_INVALID_ARG_VALUE('headers', obj); + } + + // Headers in obj should override previous headers but still + // allow explicit duplicates. To do so, we first remove any + // existing conflicts, then use appendHeader. + + for (let n = 0; n < obj.length; n += 2) { + k = obj[n + 0]; + this.removeHeader(k); + } + + for (let n = 0; n < obj.length; n += 2) { + k = obj[n + 0]; + if (k) this.appendHeader(k, obj[n + 1]); + } + } else if (obj) { + const keys = ObjectKeys(obj); + // Retain for(;;) loop for performance reasons + // Refs: https://github.com/nodejs/node/pull/30958 + for (let i = 0; i < keys.length; i++) { + k = keys[i]; + if (k) this.setHeader(k, obj[k]); + } + } + // Only progressive api is used + headers = this[kOutHeaders]; + } else { + // Only writeHead() called + headers = obj; + } + + if (checkInvalidHeaderChar(this.statusMessage)) + throw new ERR_INVALID_CHAR('statusMessage'); + + const statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`; + + if (statusCode === 204 || statusCode === 304 || + (statusCode >= 100 && statusCode <= 199)) { + // RFC 2616, 10.2.5: + // The 204 response MUST NOT include a message-body, and thus is always + // terminated by the first empty line after the header fields. + // RFC 2616, 10.3.5: + // The 304 response MUST NOT contain a message-body, and thus is always + // terminated by the first empty line after the header fields. + // RFC 2616, 10.1 Informational 1xx: + // This class of status code indicates a provisional response, + // consisting only of the Status-Line and optional headers, and is + // terminated by an empty line. + this._hasBody = false; + } + + // Don't keep alive connections where the client expects 100 Continue + // but we sent a final status; they may put extra bytes on the wire. + if (this._expect_continue && !this._sent100) { + this.shouldKeepAlive = false; + } + + this._storeHeader(statusLine, headers); + + return this; +} + +// Docs-only deprecated: DEP0063 +ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead; + +function storeHTTPOptions(options) { + this[kIncomingMessage] = options.IncomingMessage || IncomingMessage; + this[kServerResponse] = options.ServerResponse || ServerResponse; + + const maxHeaderSize = options.maxHeaderSize; + if (maxHeaderSize !== undefined) + validateInteger(maxHeaderSize, 'maxHeaderSize', 0); + this.maxHeaderSize = maxHeaderSize; + + const insecureHTTPParser = options.insecureHTTPParser; + if (insecureHTTPParser !== undefined) + validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser'); + this.insecureHTTPParser = insecureHTTPParser; + + const requestTimeout = options.requestTimeout; + if (requestTimeout !== undefined) { + validateInteger(requestTimeout, 'requestTimeout', 0); + this.requestTimeout = requestTimeout; + } else { + this.requestTimeout = 300_000; // 5 minutes + } + + const headersTimeout = options.headersTimeout; + if (headersTimeout !== undefined) { + validateInteger(headersTimeout, 'headersTimeout', 0); + this.headersTimeout = headersTimeout; + } else { + this.headersTimeout = MathMin(60_000, this.requestTimeout); // Minimum between 60 seconds or requestTimeout + } + + if (this.requestTimeout > 0 && this.headersTimeout > 0 && this.headersTimeout > this.requestTimeout) { + throw new ERR_OUT_OF_RANGE('headersTimeout', '<= requestTimeout', headersTimeout); + } + + const keepAliveTimeout = options.keepAliveTimeout; + if (keepAliveTimeout !== undefined) { + validateInteger(keepAliveTimeout, 'keepAliveTimeout', 0); + this.keepAliveTimeout = keepAliveTimeout; + } else { + this.keepAliveTimeout = 5_000; // 5 seconds; + } + + const connectionsCheckingInterval = options.connectionsCheckingInterval; + if (connectionsCheckingInterval !== undefined) { + validateInteger(connectionsCheckingInterval, 'connectionsCheckingInterval', 0); + this.connectionsCheckingInterval = connectionsCheckingInterval; + } else { + this.connectionsCheckingInterval = 30_000; // 30 seconds + } + + const requireHostHeader = options.requireHostHeader; + if (requireHostHeader !== undefined) { + validateBoolean(requireHostHeader, 'options.requireHostHeader'); + this.requireHostHeader = requireHostHeader; + } else { + this.requireHostHeader = true; + } + + const joinDuplicateHeaders = options.joinDuplicateHeaders; + if (joinDuplicateHeaders !== undefined) { + validateBoolean(joinDuplicateHeaders, 'options.joinDuplicateHeaders'); + } + this.joinDuplicateHeaders = joinDuplicateHeaders; + + const rejectNonStandardBodyWrites = options.rejectNonStandardBodyWrites; + if (rejectNonStandardBodyWrites !== undefined) { + validateBoolean(rejectNonStandardBodyWrites, 'options.rejectNonStandardBodyWrites'); + this.rejectNonStandardBodyWrites = rejectNonStandardBodyWrites; + } else { + this.rejectNonStandardBodyWrites = false; + } +} + +function setupConnectionsTracking() { + // Start connection handling + this[kConnections] ||= new ConnectionsList(); + + if (this[kConnectionsCheckingInterval]) { + clearInterval(this[kConnectionsCheckingInterval]); + } + // This checker is started without checking whether any headersTimeout or requestTimeout is non zero + // otherwise it would not be started if such timeouts are modified after createServer. + this[kConnectionsCheckingInterval] = + setInterval(checkConnections.bind(this), this.connectionsCheckingInterval).unref(); +} + +function httpServerPreClose(server) { + server.closeIdleConnections(); + clearInterval(server[kConnectionsCheckingInterval]); +} + +function Server(options, requestListener) { + if (!(this instanceof Server)) return new Server(options, requestListener); + + if (typeof options === 'function') { + requestListener = options; + options = kEmptyObject; + } else if (options == null) { + options = kEmptyObject; + } else { + validateObject(options, 'options'); + } + + storeHTTPOptions.call(this, options); + net.Server.call( + this, + { allowHalfOpen: true, noDelay: options.noDelay ?? true, + keepAlive: options.keepAlive, + keepAliveInitialDelay: options.keepAliveInitialDelay, + highWaterMark: options.highWaterMark }); + + if (requestListener) { + this.on('request', requestListener); + } + + // Similar option to this. Too lazy to write my own docs. + // http://www.squid-cache.org/Doc/config/half_closed_clients/ + // https://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F + this.httpAllowHalfOpen = false; + + this.on('connection', connectionListener); + this.on('listening', setupConnectionsTracking); + + this.timeout = 0; + this.maxHeadersCount = null; + this.maxRequestsPerSocket = 0; + + this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders); +} +ObjectSetPrototypeOf(Server.prototype, net.Server.prototype); +ObjectSetPrototypeOf(Server, net.Server); + +Server.prototype.close = function close() { + httpServerPreClose(this); + ReflectApply(net.Server.prototype.close, this, arguments); + return this; +}; + +Server.prototype[SymbolAsyncDispose] = assignFunctionName(SymbolAsyncDispose, async function() { + await promisify(this.close).call(this); +}); + +Server.prototype.closeAllConnections = function closeAllConnections() { + if (!this[kConnections]) { + return; + } + + const connections = this[kConnections].all(); + + for (let i = 0, l = connections.length; i < l; i++) { + connections[i].socket.destroy(); + } +}; + +Server.prototype.closeIdleConnections = function closeIdleConnections() { + if (!this[kConnections]) { + return; + } + + const connections = this[kConnections].idle(); + + for (let i = 0, l = connections.length; i < l; i++) { + if (connections[i].socket._httpMessage && !connections[i].socket._httpMessage.finished) { + continue; + } + + connections[i].socket.destroy(); + } +}; + +Server.prototype.setTimeout = function setTimeout(msecs, callback) { + this.timeout = msecs; + if (callback) + this.on('timeout', callback); + return this; +}; + +Server.prototype[EE.captureRejectionSymbol] = +assignFunctionName(EE.captureRejectionSymbol, function(err, event, ...args) { + switch (event) { + case 'request': { + const { 1: res } = args; + if (!res.headersSent && !res.writableEnded) { + // Don't leak headers. + const names = res.getHeaderNames(); + for (let i = 0; i < names.length; i++) { + res.removeHeader(names[i]); + } + res.statusCode = 500; + res.end(STATUS_CODES[500]); + } else { + res.destroy(); + } + break; + } + default: + net.Server.prototype[SymbolFor('nodejs.rejection')] + .apply(this, arguments); + } +}); + +function checkConnections() { + if (this.headersTimeout === 0 && this.requestTimeout === 0) { + return; + } + + const expired = this[kConnections].expired(this.headersTimeout, this.requestTimeout); + + for (let i = 0; i < expired.length; i++) { + const socket = expired[i].socket; + + if (socket) { + onRequestTimeout(socket); + } + } +} + +function connectionListener(socket) { + defaultTriggerAsyncIdScope( + getOrSetAsyncId(socket), connectionListenerInternal, this, socket, + ); +} + +function connectionListenerInternal(server, socket) { + debug('SERVER new http connection'); + + // Ensure that the server property of the socket is correctly set. + // See https://github.com/nodejs/node/issues/13435 + socket.server = server; + + // If the user has added a listener to the server, + // request, or response, then it's their responsibility. + // otherwise, destroy on timeout by default + if (server.timeout && typeof socket.setTimeout === 'function') + socket.setTimeout(server.timeout); + socket.on('timeout', socketOnTimeout); + + const parser = parsers.alloc(); + + const lenient = server.insecureHTTPParser === undefined ? + isLenient() : server.insecureHTTPParser; + + // TODO(addaleax): This doesn't play well with the + // `async_hooks.currentResource()` proposal, see + // https://github.com/nodejs/node/pull/21313 + parser.initialize( + HTTPParser.REQUEST, + new HTTPServerAsyncResource('HTTPINCOMINGMESSAGE', socket), + server.maxHeaderSize || 0, + lenient ? kLenientAll : kLenientNone, + server[kConnections], + ); + parser.socket = socket; + socket.parser = parser; + + // Propagate headers limit from server instance to parser + if (typeof server.maxHeadersCount === 'number') { + parser.maxHeaderPairs = server.maxHeadersCount << 1; + } + + const state = { + onData: null, + onEnd: null, + onClose: null, + onDrain: null, + outgoing: [], + incoming: [], + // `outgoingData` is an approximate amount of bytes queued through all + // inactive responses. If more data than the high watermark is queued - we + // need to pause TCP socket/HTTP parser, and wait until the data will be + // sent to the client. + outgoingData: 0, + requestsCount: 0, + keepAliveTimeoutSet: false, + }; + state.onData = socketOnData.bind(undefined, + server, socket, parser, state); + state.onEnd = socketOnEnd.bind(undefined, + server, socket, parser, state); + state.onClose = socketOnClose.bind(undefined, + socket, state); + state.onDrain = socketOnDrain.bind(undefined, + socket, state); + socket.on('data', state.onData); + socket.on('error', socketOnError); + socket.on('end', state.onEnd); + socket.on('close', state.onClose); + socket.on('drain', state.onDrain); + parser.onIncoming = parserOnIncoming.bind(undefined, + server, socket, state); + + // We are consuming socket, so it won't get any actual data + socket.on('resume', onSocketResume); + socket.on('pause', onSocketPause); + + // Overrides to unconsume on `data`, `readable` listeners + socket.on = generateSocketListenerWrapper('on'); + socket.addListener = generateSocketListenerWrapper('addListener'); + socket.prependListener = generateSocketListenerWrapper('prependListener'); + socket.setEncoding = socketSetEncoding; + + // We only consume the socket if it has never been consumed before. + if (socket._handle?.isStreamBase && + !socket._handle._consumed) { + parser._consumed = true; + socket._handle._consumed = true; + parser.consume(socket._handle); + } + parser[kOnExecute] = + onParserExecute.bind(undefined, + server, socket, parser, state); + + parser[kOnTimeout] = + onParserTimeout.bind(undefined, + server, socket); + + socket._paused = false; +} + +function socketSetEncoding() { + throw new ERR_HTTP_SOCKET_ENCODING(); +} + +function updateOutgoingData(socket, state, delta) { + state.outgoingData += delta; + socketOnDrain(socket, state); +} + +function socketOnDrain(socket, state) { + const needPause = state.outgoingData > socket.writableHighWaterMark; + + // If we previously paused, then start reading again. + if (socket._paused && !needPause) { + socket._paused = false; + if (socket.parser) + socket.parser.resume(); + socket.resume(); + } + + const msg = socket._httpMessage; + if (msg && !msg.finished && msg[kNeedDrain]) { + msg[kNeedDrain] = false; + msg.emit('drain'); + } +} + +function socketOnTimeout() { + const req = this.parser?.incoming; + const reqTimeout = req && !req.complete && req.emit('timeout', this); + const res = this._httpMessage; + const resTimeout = res && res.emit('timeout', this); + const serverTimeout = this.server.emit('timeout', this); + + if (!reqTimeout && !resTimeout && !serverTimeout) + this.destroy(); +} + +function socketOnClose(socket, state) { + debug('server socket close'); + freeParser(socket.parser, null, socket); + abortIncoming(state.incoming); +} + +function abortIncoming(incoming) { + while (incoming.length) { + const req = incoming.shift(); + req.destroy(new ConnResetException('aborted')); + } + // Abort socket._httpMessage ? +} + +function socketOnEnd(server, socket, parser, state) { + const ret = parser.finish(); + + if (ret instanceof Error) { + debug('parse error'); + // socketOnError has additional logic and will call socket.destroy(err). + socketOnError.call(socket, ret); + } else if (!server.httpAllowHalfOpen) { + socket.end(); + } else if (state.outgoing.length) { + state.outgoing[state.outgoing.length - 1]._last = true; + } else if (socket._httpMessage) { + socket._httpMessage._last = true; + } else { + socket.end(); + } +} + +function socketOnData(server, socket, parser, state, d) { + assert(!socket._paused); + debug('SERVER socketOnData %d', d.length); + + const ret = parser.execute(d); + onParserExecuteCommon(server, socket, parser, state, ret, d); +} + +function onRequestTimeout(socket) { + // socketOnError has additional logic and will call socket.destroy(err). + socketOnError.call(socket, new ERR_HTTP_REQUEST_TIMEOUT()); +} + +function onParserExecute(server, socket, parser, state, ret) { + // When underlying `net.Socket` instance is consumed - no + // `data` events are emitted, and thus `socket.setTimeout` fires the + // callback even if the data is constantly flowing into the socket. + // See, https://github.com/nodejs/node/commit/ec2822adaad76b126b5cccdeaa1addf2376c9aa6 + socket._unrefTimer(); + debug('SERVER socketOnParserExecute %d', ret); + onParserExecuteCommon(server, socket, parser, state, ret, undefined); +} + +function onParserTimeout(server, socket) { + const serverTimeout = server.emit('timeout', socket); + + if (!serverTimeout) + socket.destroy(); +} + +const noop = () => {}; +const badRequestResponse = Buffer.from( + `HTTP/1.1 400 ${STATUS_CODES[400]}\r\n` + + 'Connection: close\r\n\r\n', 'ascii', +); +const requestTimeoutResponse = Buffer.from( + `HTTP/1.1 408 ${STATUS_CODES[408]}\r\n` + + 'Connection: close\r\n\r\n', 'ascii', +); +const requestHeaderFieldsTooLargeResponse = Buffer.from( + `HTTP/1.1 431 ${STATUS_CODES[431]}\r\n` + + 'Connection: close\r\n\r\n', 'ascii', +); + +const requestChunkExtensionsTooLargeResponse = Buffer.from( + `HTTP/1.1 413 ${STATUS_CODES[413]}\r\n` + + 'Connection: close\r\n\r\n', 'ascii', +); + +function socketOnError(e) { + // Ignore further errors + this.removeListener('error', socketOnError); + + if (this.listenerCount('error', noop) === 0) { + this.on('error', noop); + } + + if (!this.server.emit('clientError', e, this)) { + // Caution must be taken to avoid corrupting the remote peer. + // Reply an error segment if there is no in-flight `ServerResponse`, + // or no data of the in-flight one has been written yet to this socket. + if (this.writable && + (!this._httpMessage || !this._httpMessage._headerSent)) { + let response; + + switch (e.code) { + case 'HPE_HEADER_OVERFLOW': + response = requestHeaderFieldsTooLargeResponse; + break; + case 'HPE_CHUNK_EXTENSIONS_OVERFLOW': + response = requestChunkExtensionsTooLargeResponse; + break; + case 'ERR_HTTP_REQUEST_TIMEOUT': + response = requestTimeoutResponse; + break; + default: + response = badRequestResponse; + break; + } + + this.write(response); + } + this.destroy(e); + } +} + +function onParserExecuteCommon(server, socket, parser, state, ret, d) { + resetSocketTimeout(server, socket, state); + + if (ret instanceof Error) { + prepareError(ret, parser, d); + debug('parse error', ret); + socketOnError.call(socket, ret); + } else if (parser.incoming?.upgrade) { + // Upgrade or CONNECT + const req = parser.incoming; + debug('SERVER upgrade or connect', req.method); + + d ||= parser.getCurrentBuffer(); + + socket.removeListener('data', state.onData); + socket.removeListener('end', state.onEnd); + socket.removeListener('close', state.onClose); + socket.removeListener('drain', state.onDrain); + socket.removeListener('error', socketOnError); + socket.removeListener('timeout', socketOnTimeout); + unconsume(parser, socket); + parser.finish(); + freeParser(parser, req, socket); + parser = null; + + const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; + if (eventName === 'upgrade' || server.listenerCount(eventName) > 0) { + debug('SERVER have listener for %s', eventName); + const bodyHead = d.slice(ret, d.length); + + socket.readableFlowing = null; + + server.emit(eventName, req, socket, bodyHead); + } else { + // Got CONNECT method, but have no handler. + socket.destroy(); + } + } else if (parser.incoming && parser.incoming.method === 'PRI') { + debug('SERVER got PRI request'); + socket.destroy(); + } + + if (socket._paused && socket.parser) { + // onIncoming paused the socket, we should pause the parser as well + debug('pause parser'); + socket.parser.pause(); + } +} + +function clearIncoming(req) { + req ||= this; + const parser = req.socket?.parser; + // Reset the .incoming property so that the request object can be gc'ed. + if (parser && parser.incoming === req) { + if (req.readableEnded) { + parser.incoming = null; + } else { + req.on('end', clearIncoming); + } + } +} + +function resOnFinish(req, res, socket, state, server) { + if (onResponseFinishChannel.hasSubscribers) { + onResponseFinishChannel.publish({ + request: req, + response: res, + socket, + server, + }); + } + + // Usually the first incoming element should be our request. it may + // be that in the case abortIncoming() was called that the incoming + // array will be empty. + assert(state.incoming.length === 0 || state.incoming[0] === req); + + state.incoming.shift(); + + // If the user never called req.read(), and didn't pipe() or + // .resume() or .on('data'), then we call req._dump() so that the + // bytes will be pulled off the wire. + if (!req._consuming && !req._readableState.resumeScheduled) + req._dump(); + + res.detachSocket(socket); + clearIncoming(req); + process.nextTick(emitCloseNT, res); + + if (res._last) { + if (typeof socket.destroySoon === 'function') { + socket.destroySoon(); + } else { + socket.end(); + } + } else if (state.outgoing.length === 0) { + if (server.keepAliveTimeout && typeof socket.setTimeout === 'function') { + // Increase the internal timeout wrt the advertised value to reduce + // the likelihood of ECONNRESET errors. + socket.setTimeout(server.keepAliveTimeout + HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER); + state.keepAliveTimeoutSet = true; + } + } else { + // Start sending the next message + const m = state.outgoing.shift(); + if (m) { + m.assignSocket(socket); + } + } +} + +function emitCloseNT(self) { + if (!self._closed) { + self.destroyed = true; + self._closed = true; + self.emit('close'); + } +} + +// The following callback is issued after the headers have been read on a +// new message. In this callback we setup the response object and pass it +// to the user. +function parserOnIncoming(server, socket, state, req, keepAlive) { + resetSocketTimeout(server, socket, state); + + if (req.upgrade) { + req.upgrade = req.method === 'CONNECT' || + server.listenerCount('upgrade') > 0; + if (req.upgrade) + return 2; + } + + state.incoming.push(req); + + // If the writable end isn't consuming, then stop reading + // so that we don't become overwhelmed by a flood of + // pipelined requests that may never be resolved. + if (!socket._paused) { + const ws = socket._writableState; + if (ws.needDrain || state.outgoingData >= socket.writableHighWaterMark) { + socket._paused = true; + // We also need to pause the parser, but don't do that until after + // the call to execute, because we may still be processing the last + // chunk. + socket.pause(); + } + } + + const res = new server[kServerResponse](req, + { + highWaterMark: socket.writableHighWaterMark, + rejectNonStandardBodyWrites: server.rejectNonStandardBodyWrites, + }); + res._keepAliveTimeout = server.keepAliveTimeout; + res._maxRequestsPerSocket = server.maxRequestsPerSocket; + res._onPendingData = updateOutgoingData.bind(undefined, + socket, state); + + res.shouldKeepAlive = keepAlive; + res[kUniqueHeaders] = server[kUniqueHeaders]; + + if (onRequestStartChannel.hasSubscribers) { + onRequestStartChannel.publish({ + request: req, + response: res, + socket, + server, + }); + } + + if (socket._httpMessage) { + // There are already pending outgoing res, append. + state.outgoing.push(res); + } else { + res.assignSocket(socket); + } + + // When we're finished writing the response, check if this is the last + // response, if so destroy the socket. + res.on('finish', + resOnFinish.bind(undefined, + req, res, socket, state, server)); + + let handled = false; + + + if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1) { + + // From RFC 7230 5.4 https://datatracker.ietf.org/doc/html/rfc7230#section-5.4 + // A server MUST respond with a 400 (Bad Request) status code to any + // HTTP/1.1 request message that lacks a Host header field + if (server.requireHostHeader && req.headers.host === undefined) { + res.writeHead(400, ['Connection', 'close']); + res.end(); + return 0; + } + + const isRequestsLimitSet = ( + typeof server.maxRequestsPerSocket === 'number' && + server.maxRequestsPerSocket > 0 + ); + + if (isRequestsLimitSet) { + state.requestsCount++; + res.maxRequestsOnConnectionReached = ( + server.maxRequestsPerSocket <= state.requestsCount); + } + + if (isRequestsLimitSet && + (server.maxRequestsPerSocket < state.requestsCount)) { + handled = true; + server.emit('dropRequest', req, socket); + res.writeHead(503); + res.end(); + } else if (req.headers.expect !== undefined) { + handled = true; + + if (continueExpression.test(req.headers.expect)) { + res._expect_continue = true; + if (server.listenerCount('checkContinue') > 0) { + server.emit('checkContinue', req, res); + } else { + res.writeContinue(); + server.emit('request', req, res); + } + } else if (server.listenerCount('checkExpectation') > 0) { + server.emit('checkExpectation', req, res); + } else { + res.writeHead(417); + res.end(); + } + } + } + + if (!handled) { + server.emit('request', req, res); + } + + return 0; // No special treatment. +} + +function resetSocketTimeout(server, socket, state) { + if (!state.keepAliveTimeoutSet) + return; + + socket.setTimeout(server.timeout || 0); + state.keepAliveTimeoutSet = false; +} + +function onSocketResume() { + // It may seem that the socket is resumed, but this is an enemy's trick to + // deceive us! `resume` is emitted asynchronously, and may be called from + // `incoming.readStart()`. Stop the socket again here, just to preserve the + // state. + // + // We don't care about stream semantics for the consumed socket anyway. + if (this._paused) { + this.pause(); + return; + } + + if (this._handle && !this._handle.reading) { + this._handle.reading = true; + this._handle.readStart(); + } +} + +function onSocketPause() { + if (this._handle?.reading) { + this._handle.reading = false; + this._handle.readStop(); + } +} + +function unconsume(parser, socket) { + if (socket._handle) { + if (parser._consumed) + parser.unconsume(); + parser._consumed = false; + socket.removeListener('pause', onSocketPause); + socket.removeListener('resume', onSocketResume); + } +} + +function generateSocketListenerWrapper(originalFnName) { + return function socketListenerWrap(ev, fn) { + const res = net.Socket.prototype[originalFnName].call(this, + ev, fn); + if (!this.parser) { + this.on = net.Socket.prototype.on; + this.addListener = net.Socket.prototype.addListener; + this.prependListener = net.Socket.prototype.prependListener; + return res; + } + + if (ev === 'data' || ev === 'readable') + unconsume(this.parser, this); + + return res; + }; +} + +module.exports = { + STATUS_CODES, + Server, + ServerResponse, + setupConnectionsTracking, + storeHTTPOptions, + _connectionListener: connectionListener, + kServerResponse, + httpServerPreClose, + kConnectionsCheckingInterval, +}; diff --git a/lib/internal/http2/compat.js b/lib/internal/http2/compat.js index a02d3b0c5844fa..32fe9f57345ae8 100644 --- a/lib/internal/http2/compat.js +++ b/lib/internal/http2/compat.js @@ -59,7 +59,8 @@ const { assertValidPseudoHeader, getAuthority, } = require('internal/http2/util'); -const { _checkIsHttpToken: checkIsHttpToken } = require('_http_common'); + +const { _checkIsHttpToken: checkIsHttpToken } = require('internal/http/common'); const kBeginSend = Symbol('begin-send'); const kState = Symbol('state'); diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index 2cda2dabaabe78..c34c7a3384b8fb 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -46,8 +46,14 @@ const { Duplex } = require('stream'); const tls = require('tls'); const { setImmediate, setTimeout, clearTimeout } = require('timers'); -const { kIncomingMessage } = require('_http_common'); -const { kServerResponse, Server: HttpServer, httpServerPreClose, setupConnectionsTracking } = require('_http_server'); +const { kIncomingMessage } = require('internal/http/common'); +const { + kServerResponse, + Server: HttpServer, + httpServerPreClose, + setupConnectionsTracking, +} = require('internal/http/server'); + const JSStreamSocket = require('internal/js_stream_socket'); const { diff --git a/lib/internal/http2/util.js b/lib/internal/http2/util.js index 75312e5aa57c5f..af2dc5e0d1c754 100644 --- a/lib/internal/http2/util.js +++ b/lib/internal/http2/util.js @@ -16,7 +16,7 @@ const { const { _checkIsHttpToken: checkIsHttpToken, -} = require('_http_common'); +} = require('internal/http/common'); const binding = internalBinding('http2'); const { diff --git a/test/async-hooks/test-httpparser.request.js b/test/async-hooks/test-httpparser.request.js index 40c01bc8177d6e..6379bf330d23b2 100644 --- a/test/async-hooks/test-httpparser.request.js +++ b/test/async-hooks/test-httpparser.request.js @@ -1,3 +1,5 @@ +// Flags: --expose-internals --no-warnings + 'use strict'; const common = require('../common'); @@ -9,7 +11,7 @@ const { checkInvocations } = require('./hook-checks'); const hooks = initHooks(); hooks.enable(); -const { HTTPParser } = require('_http_common'); +const { HTTPParser } = require('internal/http/common'); const REQUEST = HTTPParser.REQUEST; diff --git a/test/async-hooks/test-httpparser.response.js b/test/async-hooks/test-httpparser.response.js index 64f0893c5a9aa8..2e600b127475b7 100644 --- a/test/async-hooks/test-httpparser.response.js +++ b/test/async-hooks/test-httpparser.response.js @@ -1,3 +1,5 @@ +// Flags: --expose-internals --no-warnings + 'use strict'; const common = require('../common'); @@ -10,7 +12,7 @@ const hooks = initHooks(); hooks.enable(); -const { HTTPParser } = require('_http_common'); +const { HTTPParser } = require('internal/http/common'); const RESPONSE = HTTPParser.RESPONSE; const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; diff --git a/test/parallel/test-http-agent-keepalive-delay.js b/test/parallel/test-http-agent-keepalive-delay.js index b5edd78b662709..11218520299fd7 100644 --- a/test/parallel/test-http-agent-keepalive-delay.js +++ b/test/parallel/test-http-agent-keepalive-delay.js @@ -3,7 +3,7 @@ const common = require('../common'); const assert = require('assert'); const http = require('http'); -const { Agent } = require('_http_agent'); +const { Agent } = require('http'); const agent = new Agent({ keepAlive: true, diff --git a/test/parallel/test-http-agent-keepalive.js b/test/parallel/test-http-agent-keepalive.js index f7424634df1bd5..14b12e027cc14b 100644 --- a/test/parallel/test-http-agent-keepalive.js +++ b/test/parallel/test-http-agent-keepalive.js @@ -23,7 +23,7 @@ const common = require('../common'); const assert = require('assert'); const http = require('http'); -const Agent = require('_http_agent').Agent; +const Agent = require('http').Agent; let name; diff --git a/test/parallel/test-http-common.js b/test/parallel/test-http-common.js index 1629856ce57d09..33b7d28ec134a9 100644 --- a/test/parallel/test-http-common.js +++ b/test/parallel/test-http-common.js @@ -1,7 +1,9 @@ +// Flags: --expose-internals --no-warnings + 'use strict'; require('../common'); const assert = require('assert'); -const httpCommon = require('_http_common'); +const httpCommon = require('internal/http/common'); const checkIsHttpToken = httpCommon._checkIsHttpToken; const checkInvalidHeaderChar = httpCommon._checkInvalidHeaderChar; diff --git a/test/parallel/test-http-invalidheaderfield2.js b/test/parallel/test-http-invalidheaderfield2.js index 1b4e9e6edb01f3..6c92b007cdbe8f 100644 --- a/test/parallel/test-http-invalidheaderfield2.js +++ b/test/parallel/test-http-invalidheaderfield2.js @@ -1,8 +1,10 @@ +// Flags: --expose-internals --no-warnings + 'use strict'; require('../common'); const assert = require('assert'); const inspect = require('util').inspect; -const { _checkIsHttpToken, _checkInvalidHeaderChar } = require('_http_common'); +const { _checkIsHttpToken, _checkInvalidHeaderChar } = require('internal/http/common'); // Good header field names [ diff --git a/test/parallel/test-http-parser-bad-ref.js b/test/parallel/test-http-parser-bad-ref.js index e34054eca67063..381f2cc2f99fde 100644 --- a/test/parallel/test-http-parser-bad-ref.js +++ b/test/parallel/test-http-parser-bad-ref.js @@ -2,11 +2,11 @@ // Run this program with valgrind or efence with --expose_gc to expose the // problem. -// Flags: --expose_gc +// Flags: --expose_gc --expose-internals --no-warnings require('../common'); const assert = require('assert'); -const { HTTPParser } = require('_http_common'); +const { HTTPParser } = require('internal/http/common'); const kOnHeaders = HTTPParser.kOnHeaders | 0; const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; diff --git a/test/parallel/test-http-parser-lazy-loaded.js b/test/parallel/test-http-parser-lazy-loaded.js index 44bb59f052e6ce..4b8d7b696cc1b2 100644 --- a/test/parallel/test-http-parser-lazy-loaded.js +++ b/test/parallel/test-http-parser-lazy-loaded.js @@ -20,7 +20,7 @@ binding.HTTPParser = DummyParser; const assert = require('assert'); const { spawn } = require('child_process'); -const { parsers } = require('_http_common'); +const { parsers } = require('internal/http/common'); // Test _http_common was not loaded before monkey patching const parser = parsers.alloc(); diff --git a/test/parallel/test-http-parser-memory-retention.js b/test/parallel/test-http-parser-memory-retention.js index 316797fec3eaaf..29d8dda98dd6ed 100644 --- a/test/parallel/test-http-parser-memory-retention.js +++ b/test/parallel/test-http-parser-memory-retention.js @@ -1,9 +1,10 @@ +// Flags: --expose-internals --no-warnings 'use strict'; const common = require('../common'); const assert = require('assert'); const http = require('http'); -const { HTTPParser } = require('_http_common'); +const { HTTPParser } = require('internal/http/common'); // Test that the `HTTPParser` instance is cleaned up before being returned to // the pool to avoid memory retention issues. diff --git a/test/parallel/test-http-parser.js b/test/parallel/test-http-parser.js index ad591319089c1a..fc364ad18b513f 100644 --- a/test/parallel/test-http-parser.js +++ b/test/parallel/test-http-parser.js @@ -1,3 +1,5 @@ +// Flags: --expose-internals --no-warnings + // Copyright Joyent, Inc. and other Node contributors. // // Permission is hereby granted, free of charge, to any person obtaining a @@ -23,7 +25,7 @@ const { mustCall, mustNotCall } = require('../common'); const assert = require('assert'); -const { methods, HTTPParser } = require('_http_common'); +const { methods, HTTPParser } = require('internal/http/common'); const { REQUEST, RESPONSE } = HTTPParser; const kOnHeaders = HTTPParser.kOnHeaders | 0; diff --git a/test/parallel/test-http-server-async-dispose.js b/test/parallel/test-http-server-async-dispose.js index 8af11dcb7b0c3c..29abde5bb8a8db 100644 --- a/test/parallel/test-http-server-async-dispose.js +++ b/test/parallel/test-http-server-async-dispose.js @@ -1,8 +1,10 @@ +// Flags: --expose-internals --no-warnings + 'use strict'; const common = require('../common'); const assert = require('assert'); const { createServer } = require('http'); -const { kConnectionsCheckingInterval } = require('_http_server'); +const { kConnectionsCheckingInterval } = require('internal/http/server'); const server = createServer(); diff --git a/test/parallel/test-http-server-clear-timer.js b/test/parallel/test-http-server-clear-timer.js index b56334860355ea..0121b22b825ae4 100644 --- a/test/parallel/test-http-server-clear-timer.js +++ b/test/parallel/test-http-server-clear-timer.js @@ -1,8 +1,10 @@ +// Flags: --expose-internals --no-warnings + 'use strict'; const common = require('../common'); const http = require('http'); const assert = require('assert'); -const { kConnectionsCheckingInterval } = require('_http_server'); +const { kConnectionsCheckingInterval } = require('internal/http/server'); let i = 0; let timer; diff --git a/test/parallel/test-http-server-close-destroy-timeout.js b/test/parallel/test-http-server-close-destroy-timeout.js index b1138ee36d5a90..b32d5fcdd6b891 100644 --- a/test/parallel/test-http-server-close-destroy-timeout.js +++ b/test/parallel/test-http-server-close-destroy-timeout.js @@ -1,8 +1,11 @@ +// Flags: --expose-internals --no-warnings + 'use strict'; + const common = require('../common'); const assert = require('assert'); const { createServer } = require('http'); -const { kConnectionsCheckingInterval } = require('_http_server'); +const { kConnectionsCheckingInterval } = require('internal/http/server'); const server = createServer(function(req, res) {}); server.listen(0, common.mustCall(function() { diff --git a/test/parallel/test-http-server-options-highwatermark.js b/test/parallel/test-http-server-options-highwatermark.js index 2b96b33f17c915..83ce8a0926cb02 100644 --- a/test/parallel/test-http-server-options-highwatermark.js +++ b/test/parallel/test-http-server-options-highwatermark.js @@ -3,7 +3,7 @@ const common = require('../common'); const assert = require('assert'); const http = require('http'); -const { kHighWaterMark } = require('_http_outgoing'); +const { kHighWaterMark } = require('internal/http/outgoing'); const { getDefaultHighWaterMark } = require('internal/streams/state'); diff --git a/test/parallel/test-http-set-max-idle-http-parser.js b/test/parallel/test-http-set-max-idle-http-parser.js index d935823a1ba946..1d196174304114 100644 --- a/test/parallel/test-http-set-max-idle-http-parser.js +++ b/test/parallel/test-http-set-max-idle-http-parser.js @@ -1,7 +1,9 @@ +// Flags: --expose-internals --no-warnings + 'use strict'; require('../common'); const assert = require('assert'); -const httpCommon = require('_http_common'); +const httpCommon = require('internal/http/common'); const http = require('http'); [Symbol(), {}, [], () => {}, 1n, true, '1', null, undefined].forEach((value) => { diff --git a/test/parallel/test-https-server-async-dispose.js b/test/parallel/test-https-server-async-dispose.js index 93b5cb74bbda11..1cb3748a028722 100644 --- a/test/parallel/test-https-server-async-dispose.js +++ b/test/parallel/test-https-server-async-dispose.js @@ -1,3 +1,4 @@ +// Flags: --expose-internals --no-warnings 'use strict'; const common = require('../common'); @@ -7,7 +8,7 @@ if (!common.hasCrypto) const assert = require('assert'); const { createServer } = require('https'); -const { kConnectionsCheckingInterval } = require('_http_server'); +const { kConnectionsCheckingInterval } = require('internal/http/server'); const server = createServer(); diff --git a/test/parallel/test-https-server-close-destroy-timeout.js b/test/parallel/test-https-server-close-destroy-timeout.js index e876721f610964..a99e6f837b7ca5 100644 --- a/test/parallel/test-https-server-close-destroy-timeout.js +++ b/test/parallel/test-https-server-close-destroy-timeout.js @@ -1,3 +1,4 @@ +// Flags: --expose-internals --no-warnings 'use strict'; const common = require('../common'); const assert = require('assert'); @@ -6,7 +7,7 @@ if (!common.hasCrypto) { } const { createServer } = require('https'); -const { kConnectionsCheckingInterval } = require('_http_server'); +const { kConnectionsCheckingInterval } = require('internal/http/server'); const fixtures = require('../common/fixtures'); diff --git a/test/parallel/test-outgoing-message-pipe.js b/test/parallel/test-outgoing-message-pipe.js index 049b41c9aaa903..8b4632ebeeb487 100644 --- a/test/parallel/test-outgoing-message-pipe.js +++ b/test/parallel/test-outgoing-message-pipe.js @@ -1,7 +1,7 @@ 'use strict'; require('../common'); const assert = require('assert'); -const OutgoingMessage = require('_http_outgoing').OutgoingMessage; +const OutgoingMessage = require('http').OutgoingMessage; // Verify that an error is thrown upon a call to `OutgoingMessage.pipe`. diff --git a/test/parallel/test-warn-http-agent-deprecation.js b/test/parallel/test-warn-http-agent-deprecation.js new file mode 100644 index 00000000000000..cdea144605a8d3 --- /dev/null +++ b/test/parallel/test-warn-http-agent-deprecation.js @@ -0,0 +1,10 @@ +'use strict'; + +const common = require('../common'); + +// _http_agent is deprecated. + +common.expectWarning('DeprecationWarning', + 'The _http_agent module is deprecated. Use `node:http` instead.', 'DEP0195'); + +require('_http_agent'); diff --git a/test/parallel/test-warn-http-client-deprecation.js b/test/parallel/test-warn-http-client-deprecation.js new file mode 100644 index 00000000000000..9b86949fd77528 --- /dev/null +++ b/test/parallel/test-warn-http-client-deprecation.js @@ -0,0 +1,10 @@ +'use strict'; + +const common = require('../common'); + +// _http_client is deprecated. + +common.expectWarning('DeprecationWarning', + 'The _http_client module is deprecated. Use `node:http` instead.', 'DEP0195'); + +require('_http_client'); diff --git a/test/parallel/test-warn-http-common-deprecation.js b/test/parallel/test-warn-http-common-deprecation.js new file mode 100644 index 00000000000000..d5203a308035cd --- /dev/null +++ b/test/parallel/test-warn-http-common-deprecation.js @@ -0,0 +1,10 @@ +'use strict'; + +const common = require('../common'); + +// _http_common is deprecated. + +common.expectWarning('DeprecationWarning', + 'The _http_common module is deprecated.', 'DEP0195'); + +require('_http_common'); diff --git a/test/parallel/test-warn-http-incoming-deprecation.js b/test/parallel/test-warn-http-incoming-deprecation.js new file mode 100644 index 00000000000000..f4d601bc5f1ca2 --- /dev/null +++ b/test/parallel/test-warn-http-incoming-deprecation.js @@ -0,0 +1,10 @@ +'use strict'; + +const common = require('../common'); + +// _http_incoming is deprecated. + +common.expectWarning('DeprecationWarning', + 'The _http_incoming module is deprecated. Use `node:http` instead.', 'DEP0195'); + +require('_http_incoming'); diff --git a/test/parallel/test-warn-http-outgoing-deprecation.js b/test/parallel/test-warn-http-outgoing-deprecation.js new file mode 100644 index 00000000000000..eaf09fddd1cf8d --- /dev/null +++ b/test/parallel/test-warn-http-outgoing-deprecation.js @@ -0,0 +1,10 @@ +'use strict'; + +const common = require('../common'); + +// _http_outgoing is deprecated. + +common.expectWarning('DeprecationWarning', + 'The _http_outgoing module is deprecated. Use `node:http` instead.', 'DEP0195'); + +require('_http_outgoing'); diff --git a/test/parallel/test-warn-http-server-deprecation.js b/test/parallel/test-warn-http-server-deprecation.js new file mode 100644 index 00000000000000..b904f77841982e --- /dev/null +++ b/test/parallel/test-warn-http-server-deprecation.js @@ -0,0 +1,10 @@ +'use strict'; + +const common = require('../common'); + +// _http_server is deprecated. + +common.expectWarning('DeprecationWarning', + 'The _http_server module is deprecated. Use `node:http` instead.', 'DEP0195'); + +require('_http_server'); diff --git a/test/sequential/test-async-wrap-getasyncid.js b/test/sequential/test-async-wrap-getasyncid.js index a75207b66e6633..d51d2c167286dd 100644 --- a/test/sequential/test-async-wrap-getasyncid.js +++ b/test/sequential/test-async-wrap-getasyncid.js @@ -172,7 +172,7 @@ if (common.hasCrypto) { // eslint-disable-line node-core/crypto-check { - const { HTTPParser } = require('_http_common'); + const { HTTPParser } = require('internal/http/common'); const parser = new HTTPParser(); testUninitialized(parser, 'HTTPParser'); parser.initialize(HTTPParser.REQUEST, {}); diff --git a/test/sequential/test-http-regr-gh-2928.js b/test/sequential/test-http-regr-gh-2928.js index f6a9e1603288a4..aa0f15167b66b2 100644 --- a/test/sequential/test-http-regr-gh-2928.js +++ b/test/sequential/test-http-regr-gh-2928.js @@ -1,11 +1,12 @@ +// Flags: --expose-internals --no-warnings // This test is designed to fail with a segmentation fault in Node.js 4.1.0 and // execute without issues in Node.js 4.1.1 and up. 'use strict'; const common = require('../common'); const assert = require('assert'); -const httpCommon = require('_http_common'); -const { HTTPParser } = require('_http_common'); +const httpCommon = require('internal/http/common'); +const { HTTPParser } = require('internal/http/common'); const net = require('net'); httpCommon.parsers.max = 50;