From cbd2c387ce29175b9c273541ea2eacc316da0a1f Mon Sep 17 00:00:00 2001 From: Antoine du Hamel Date: Sat, 6 Jul 2024 10:33:16 +0200 Subject: [PATCH] http: remove prototype primordials Co-authored-by: Yagiz Nizipli PR-URL: https://github.com/nodejs/node/pull/53698 Reviewed-By: Yagiz Nizipli Reviewed-By: Robert Nagy Reviewed-By: Moshe Atlow Reviewed-By: Marco Ippolito Reviewed-By: Luigi Pinca Reviewed-By: Matteo Collina Reviewed-By: Benjamin Gruenbaum Reviewed-By: James M Snell Reviewed-By: Rafael Gonzaga --- doc/contributing/primordials.md | 1 + lib/_http_agent.js | 51 ++++++++++++--------------------- lib/_http_client.js | 23 ++++++--------- lib/_http_common.js | 5 ++-- lib/_http_incoming.js | 11 +++---- lib/_http_outgoing.js | 44 +++++++++++++--------------- lib/_http_server.js | 9 ++---- lib/eslint.config_partial.mjs | 5 +++- lib/http.js | 4 +-- lib/internal/http.js | 6 ++-- 10 files changed, 63 insertions(+), 96 deletions(-) diff --git a/doc/contributing/primordials.md b/doc/contributing/primordials.md index 2dfe183b89..a3362c71de 100644 --- a/doc/contributing/primordials.md +++ b/doc/contributing/primordials.md @@ -7,6 +7,7 @@ later look these up from the global proxy, which can be mutated by users. For some area of the codebase, performance and code readability are deemed more important than reliability against prototype pollution: +* `node:http` * `node:http2` Usage of primordials should be preferred for new code in other areas, but diff --git a/lib/_http_agent.js b/lib/_http_agent.js index a4829526f6..ad8eb227f6 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -22,23 +22,10 @@ 'use strict'; const { - ArrayPrototypeIncludes, - ArrayPrototypeIndexOf, - ArrayPrototypePop, - ArrayPrototypePush, - ArrayPrototypeShift, - ArrayPrototypeSome, - ArrayPrototypeSplice, - FunctionPrototypeCall, NumberParseInt, ObjectKeys, ObjectSetPrototypeOf, ObjectValues, - RegExpPrototypeExec, - StringPrototypeIndexOf, - StringPrototypeSplit, - StringPrototypeStartsWith, - StringPrototypeSubstring, Symbol, } = primordials; @@ -92,7 +79,7 @@ function Agent(options) { if (!(this instanceof Agent)) return new Agent(options); - FunctionPrototypeCall(EventEmitter, this); + EventEmitter.call(this); this.defaultPort = 80; this.protocol = 'http:'; @@ -139,7 +126,7 @@ function Agent(options) { const requests = this.requests[name]; if (requests && requests.length) { - const req = ArrayPrototypeShift(requests); + const req = requests.shift(); const reqAsyncRes = req[kRequestAsyncResource]; if (reqAsyncRes) { // Run request within the original async context. @@ -185,7 +172,7 @@ function Agent(options) { this.removeSocket(socket, options); socket.once('error', freeSocketErrorListener); - ArrayPrototypePush(freeSockets, socket); + freeSockets.push(socket); }); // Don't emit keylog events unless there is a listener for them. @@ -264,11 +251,11 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, let socket; if (freeSockets) { while (freeSockets.length && freeSockets[0].destroyed) { - ArrayPrototypeShift(freeSockets); + freeSockets.shift(); } socket = this.scheduling === 'fifo' ? - ArrayPrototypeShift(freeSockets) : - ArrayPrototypePop(freeSockets); + freeSockets.shift() : + freeSockets.pop(); if (!freeSockets.length) delete this.freeSockets[name]; } @@ -280,7 +267,7 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, asyncResetHandle(socket); this.reuseSocket(socket, req); setRequestSocket(this, req, socket); - ArrayPrototypePush(this.sockets[name], socket); + this.sockets[name].push(socket); } else if (sockLen < this.maxSockets && this.totalSocketCount < this.maxTotalSockets) { debug('call onSocket', sockLen, freeLen); @@ -303,7 +290,7 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, // Used to capture the original async context. req[kRequestAsyncResource] = new AsyncResource('QueuedRequest'); - ArrayPrototypePush(this.requests[name], req); + this.requests[name].push(req); } }; @@ -326,7 +313,7 @@ Agent.prototype.createSocket = function createSocket(req, options, cb) { if (!this.sockets[name]) { this.sockets[name] = []; } - ArrayPrototypePush(this.sockets[name], s); + this.sockets[name].push(s); this.totalSocketCount++; debug('sockets', name, this.sockets[name].length, this.totalSocketCount); installListeners(this, s, options); @@ -357,16 +344,16 @@ function calculateServerName(options, req) { // abc:123 => abc // [::1] => ::1 // [::1]:123 => ::1 - if (StringPrototypeStartsWith(hostHeader, '[')) { - const index = StringPrototypeIndexOf(hostHeader, ']'); + if (hostHeader.startsWith('[')) { + const index = hostHeader.indexOf(']'); if (index === -1) { // Leading '[', but no ']'. Need to do something... servername = hostHeader; } else { - servername = StringPrototypeSubstring(hostHeader, 1, index); + servername = hostHeader.substring(1, index); } } else { - servername = StringPrototypeSplit(hostHeader, ':', 1)[0]; + servername = hostHeader.split(':', 1)[0]; } } // Don't implicitly set invalid (IP) servernames. @@ -398,9 +385,7 @@ function installListeners(agent, s, options) { // Destroy if in free list. // TODO(ronag): Always destroy, even if not in free list. const sockets = agent.freeSockets; - if (ArrayPrototypeSome(ObjectKeys(sockets), (name) => - ArrayPrototypeIncludes(sockets[name], s), - )) { + if (ObjectKeys(sockets).some((name) => sockets[name].includes(s))) { return s.destroy(); } } @@ -432,15 +417,15 @@ Agent.prototype.removeSocket = function removeSocket(s, options) { // If the socket was destroyed, remove it from the free buffers too. if (!s.writable) - ArrayPrototypePush(sets, this.freeSockets); + sets.push(this.freeSockets); for (let sk = 0; sk < sets.length; sk++) { const sockets = sets[sk]; if (sockets[name]) { - const index = ArrayPrototypeIndexOf(sockets[name], s); + const index = sockets[name].indexOf(s); if (index !== -1) { - ArrayPrototypeSplice(sockets[name], index, 1); + sockets[name].splice(index, 1); // Don't leak if (sockets[name].length === 0) delete sockets[name]; @@ -493,7 +478,7 @@ Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { const keepAliveHint = socket._httpMessage.res.headers['keep-alive']; if (keepAliveHint) { - const hint = RegExpPrototypeExec(/^timeout=(\d+)/, keepAliveHint)?.[1]; + const hint = /^timeout=(\d+)/.exec(keepAliveHint)?.[1]; if (hint) { const serverHintTimeout = NumberParseInt(hint) * 1000; diff --git a/lib/_http_client.js b/lib/_http_client.js index 09ccd2e4db..0ad234044f 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -25,20 +25,13 @@ const { ArrayIsArray, Boolean, Error, - FunctionPrototypeCall, NumberIsFinite, ObjectAssign, ObjectKeys, ObjectSetPrototypeOf, ReflectApply, - RegExpPrototypeExec, String, - StringPrototypeCharCodeAt, - StringPrototypeIncludes, - StringPrototypeIndexOf, - StringPrototypeToUpperCase, Symbol, - TypedArrayPrototypeSlice, } = primordials; const net = require('net'); @@ -130,7 +123,7 @@ class HTTPClientAsyncResource { } function ClientRequest(input, options, cb) { - FunctionPrototypeCall(OutgoingMessage, this); + OutgoingMessage.call(this); if (typeof input === 'string') { const urlStr = input; @@ -175,7 +168,7 @@ function ClientRequest(input, options, cb) { if (options.path) { const path = String(options.path); - if (RegExpPrototypeExec(INVALID_PATH_REGEX, path) !== null) { + if (INVALID_PATH_REGEX.test(path)) { debug('Path contains unescaped characters: "%s"', path); throw new ERR_UNESCAPED_CHARACTERS('Request path'); } @@ -216,7 +209,7 @@ function ClientRequest(input, options, cb) { if (!checkIsHttpToken(method)) { throw new ERR_INVALID_HTTP_TOKEN('Method', method); } - method = this.method = StringPrototypeToUpperCase(method); + method = this.method = method.toUpperCase(); } else { method = this.method = 'GET'; } @@ -298,10 +291,10 @@ function ClientRequest(input, options, cb) { // 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 = StringPrototypeIndexOf(hostHeader, ':'); + const posColon = hostHeader.indexOf(':'); if (posColon !== -1 && - StringPrototypeIncludes(hostHeader, ':', posColon + 1) && - StringPrototypeCharCodeAt(hostHeader, 0) !== 91/* '[' */) { + hostHeader.includes(':', posColon + 1) && + hostHeader.charCodeAt(0) !== 91/* '[' */) { hostHeader = `[${hostHeader}]`; } @@ -374,7 +367,7 @@ ObjectSetPrototypeOf(ClientRequest.prototype, OutgoingMessage.prototype); ObjectSetPrototypeOf(ClientRequest, OutgoingMessage); ClientRequest.prototype._finish = function _finish() { - FunctionPrototypeCall(OutgoingMessage.prototype._finish, this); + OutgoingMessage.prototype._finish.call(this); if (hasObserver('http')) { startPerf(this, kClientRequestStatistics, { type: 'http', @@ -565,7 +558,7 @@ function socketOnData(d) { parser.finish(); freeParser(parser, req, socket); - const bodyHead = TypedArrayPrototypeSlice(d, bytesParsed, d.length); + const bodyHead = d.slice(bytesParsed, d.length); const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; if (req.listenerCount(eventName) > 0) { diff --git a/lib/_http_common.js b/lib/_http_common.js index b56d351b69..5ccf43e1d6 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -23,7 +23,6 @@ const { MathMin, - RegExpPrototypeExec, Symbol, } = primordials; const { setImmediate } = require('timers'); @@ -210,7 +209,7 @@ const tokenRegExp = /^[\^_`a-zA-Z\-0-9!#$%&'*+.|~]+$/; * See https://tools.ietf.org/html/rfc7230#section-3.2.6 */ function checkIsHttpToken(val) { - return RegExpPrototypeExec(tokenRegExp, val) !== null; + return tokenRegExp.test(val); } const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; @@ -221,7 +220,7 @@ const headerCharRegex = /[^\t\x20-\x7e\x80-\xff]/; * field-vchar = VCHAR / obs-text */ function checkInvalidHeaderChar(val) { - return RegExpPrototypeExec(headerCharRegex, val) !== null; + return headerCharRegex.test(val); } function cleanParser(parser) { diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index e45ae8190e..1dd04fdf3e 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -24,9 +24,6 @@ const { ObjectDefineProperty, ObjectSetPrototypeOf, - StringPrototypeCharCodeAt, - StringPrototypeSlice, - StringPrototypeToLowerCase, Symbol, } = primordials; @@ -370,7 +367,7 @@ function matchKnownFields(field, lowercased) { if (lowercased) { return '\u0000' + field; } - return matchKnownFields(StringPrototypeToLowerCase(field), true); + return matchKnownFields(field.toLowerCase(), true); } // Add the given (field, value) pair to the message // @@ -384,9 +381,9 @@ function matchKnownFields(field, lowercased) { IncomingMessage.prototype._addHeaderLine = _addHeaderLine; function _addHeaderLine(field, value, dest) { field = matchKnownFields(field); - const flag = StringPrototypeCharCodeAt(field, 0); + const flag = field.charCodeAt(0); if (flag === 0 || flag === 2) { - field = StringPrototypeSlice(field, 1); + field = field.slice(1); // Make a delimited list if (typeof dest[field] === 'string') { dest[field] += (flag === 0 ? ', ' : '; ') + value; @@ -418,7 +415,7 @@ function _addHeaderLine(field, value, dest) { IncomingMessage.prototype._addHeaderLineDistinct = _addHeaderLineDistinct; function _addHeaderLineDistinct(field, value, dest) { - field = StringPrototypeToLowerCase(field); + field = field.toLowerCase(); if (!dest[field]) { dest[field] = [value]; } else { diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 0b7ed15622..658ad80f99 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -24,17 +24,13 @@ const { Array, ArrayIsArray, - ArrayPrototypeJoin, MathFloor, - NumberPrototypeToString, ObjectDefineProperty, + ObjectHasOwn, ObjectKeys, - ObjectPrototypeHasOwnProperty, ObjectSetPrototypeOf, ObjectValues, - RegExpPrototypeExec, SafeSet, - StringPrototypeToLowerCase, Symbol, } = primordials; @@ -99,11 +95,11 @@ const RE_CONN_CLOSE = /(?:^|\W)close(?:$|\W)/i; // 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 && StringPrototypeToLowerCase(s) === 'cookie'; + return s.length === 6 && s.toLowerCase() === 'cookie'; } function isContentDispositionField(s) { - return s.length === 19 && StringPrototypeToLowerCase(s) === 'content-disposition'; + return s.length === 19 && s.toLowerCase() === 'content-disposition'; } function OutgoingMessage(options) { @@ -230,7 +226,7 @@ ObjectDefineProperty(OutgoingMessage.prototype, '_headers', { // Refs: https://github.com/nodejs/node/pull/30958 for (let i = 0; i < keys.length; ++i) { const name = keys[i]; - headers[StringPrototypeToLowerCase(name)] = [name, val[name]]; + headers[name.toLowerCase()] = [name, val[name]]; } } }, 'OutgoingMessage.prototype._headers is deprecated', 'DEP0066'), @@ -339,7 +335,7 @@ OutgoingMessage.prototype.uncork = function() { assert(this.chunkedEncoding); let callbacks; - this._send(NumberPrototypeToString(len, 16), 'latin1', null); + 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); @@ -491,7 +487,7 @@ function _storeHeader(firstLine, headers) { } } else { for (const key in headers) { - if (ObjectPrototypeHasOwnProperty(headers, key)) { + if (ObjectHasOwn(headers, key)) { processHeader(this, state, key, headers[key], true); } } @@ -613,7 +609,7 @@ function processHeader(self, state, key, value, validate) { if (ArrayIsArray(value)) { if ( (value.length < 2 || !isCookieField(key)) && - (!self[kUniqueHeaders] || !self[kUniqueHeaders].has(StringPrototypeToLowerCase(key))) + (!self[kUniqueHeaders] || !self[kUniqueHeaders].has(key.toLowerCase())) ) { // Retain for(;;) loop for performance reasons // Refs: https://github.com/nodejs/node/pull/30958 @@ -621,7 +617,7 @@ function processHeader(self, state, key, value, validate) { storeHeader(self, state, key, value[i], validate); return; } - value = ArrayPrototypeJoin(value, '; '); + value = value.join('; '); } storeHeader(self, state, key, value, validate); } @@ -636,12 +632,12 @@ function storeHeader(self, state, key, value, validate) { function matchHeader(self, state, field, value) { if (field.length < 4 || field.length > 17) return; - field = StringPrototypeToLowerCase(field); + field = field.toLowerCase(); switch (field) { case 'connection': state.connection = true; self._removedConnection = false; - if (RegExpPrototypeExec(RE_CONN_CLOSE, value) !== null) + if (RE_CONN_CLOSE.test(value)) self._last = true; else self.shouldKeepAlive = true; @@ -649,7 +645,7 @@ function matchHeader(self, state, field, value) { case 'transfer-encoding': state.te = true; self._removedTE = false; - if (RegExpPrototypeExec(RE_TE_CHUNKED, value) !== null) + if (RE_TE_CHUNKED.test(value)) self.chunkedEncoding = true; break; case 'content-length': @@ -692,7 +688,7 @@ function parseUniqueHeadersOption(headers) { const unique = new SafeSet(); const l = headers.length; for (let i = 0; i < l; i++) { - unique.add(StringPrototypeToLowerCase(headers[i])); + unique.add(headers[i].toLowerCase()); } return unique; @@ -709,7 +705,7 @@ OutgoingMessage.prototype.setHeader = function setHeader(name, value) { if (headers === null) this[kOutHeaders] = headers = { __proto__: null }; - headers[StringPrototypeToLowerCase(name)] = [name, value]; + headers[name.toLowerCase()] = [name, value]; return this; }; @@ -761,7 +757,7 @@ OutgoingMessage.prototype.appendHeader = function appendHeader(name, value) { validateHeaderName(name); validateHeaderValue(name, value); - const field = StringPrototypeToLowerCase(name); + const field = name.toLowerCase(); const headers = this[kOutHeaders]; if (headers === null || !headers[field]) { return this.setHeader(name, value); @@ -792,7 +788,7 @@ OutgoingMessage.prototype.getHeader = function getHeader(name) { if (headers === null) return; - const entry = headers[StringPrototypeToLowerCase(name)]; + const entry = headers[name.toLowerCase()]; return entry && entry[1]; }; @@ -841,7 +837,7 @@ OutgoingMessage.prototype.getHeaders = function getHeaders() { OutgoingMessage.prototype.hasHeader = function hasHeader(name) { validateString(name, 'name'); return this[kOutHeaders] !== null && - !!this[kOutHeaders][StringPrototypeToLowerCase(name)]; + !!this[kOutHeaders][name.toLowerCase()]; }; @@ -852,7 +848,7 @@ OutgoingMessage.prototype.removeHeader = function removeHeader(name) { throw new ERR_HTTP_HEADERS_SENT('remove'); } - const key = StringPrototypeToLowerCase(name); + const key = name.toLowerCase(); switch (key) { case 'connection': @@ -1013,7 +1009,7 @@ function write_(msg, chunk, encoding, callback, fromEnd) { msg[kChunkedLength] += len; ret = msg[kChunkedLength] < msg[kHighWaterMark]; } else { - msg._send(NumberPrototypeToString(len, 16), 'latin1', null); + 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); @@ -1053,7 +1049,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { const isArrayValue = ArrayIsArray(value); if ( isArrayValue && value.length > 1 && - (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(StringPrototypeToLowerCase(field))) + (!this[kUniqueHeaders] || !this[kUniqueHeaders].has(field.toLowerCase())) ) { for (let j = 0, l = value.length; j < l; j++) { if (checkInvalidHeaderChar(value[j])) { @@ -1064,7 +1060,7 @@ OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { } } else { if (isArrayValue) { - value = ArrayPrototypeJoin(value, '; '); + value = value.join('; '); } if (checkInvalidHeaderChar(value)) { diff --git a/lib/_http_server.js b/lib/_http_server.js index 420c3c338e..b6e2ba69bc 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -24,12 +24,10 @@ const { ArrayIsArray, Error, - FunctionPrototypeCall, MathMin, ObjectKeys, ObjectSetPrototypeOf, ReflectApply, - RegExpPrototypeExec, Symbol, SymbolAsyncDispose, SymbolFor, @@ -203,8 +201,7 @@ function ServerResponse(req, options) { this._expect_continue = false; if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) { - this.useChunkedEncodingByDefault = RegExpPrototypeExec(chunkExpression, - req.headers.te) !== null; + this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te); this.shouldKeepAlive = false; } @@ -577,7 +574,7 @@ Server.prototype.close = function() { }; Server.prototype[SymbolAsyncDispose] = async function() { - return FunctionPrototypeCall(promisify(this.close), this); + return promisify(this.close).call(this); }; Server.prototype.closeAllConnections = function() { @@ -1128,7 +1125,7 @@ function parserOnIncoming(server, socket, state, req, keepAlive) { } else if (req.headers.expect !== undefined) { handled = true; - if (RegExpPrototypeExec(continueExpression, req.headers.expect) !== null) { + if (continueExpression.test(req.headers.expect)) { res._expect_continue = true; if (server.listenerCount('checkContinue') > 0) { server.emit('checkContinue', req, res); diff --git a/lib/eslint.config_partial.mjs b/lib/eslint.config_partial.mjs index b0abd1b026..26ee6f5e1b 100644 --- a/lib/eslint.config_partial.mjs +++ b/lib/eslint.config_partial.mjs @@ -491,8 +491,11 @@ export default [ }, { files: [ - 'lib/internal/http2/*.js', + 'lib/_http_*.js', + 'lib/http.js', 'lib/http2.js', + 'lib/internal/http.js', + 'lib/internal/http2/*.js', ], rules: { 'no-restricted-syntax': [ diff --git a/lib/http.js b/lib/http.js index 9fce02d6e3..684bc2ebc0 100644 --- a/lib/http.js +++ b/lib/http.js @@ -22,8 +22,6 @@ 'use strict'; const { - ArrayPrototypeSlice, - ArrayPrototypeSort, ObjectDefineProperty, } = primordials; @@ -118,7 +116,7 @@ function get(url, options, cb) { module.exports = { _connectionListener, - METHODS: ArrayPrototypeSort(ArrayPrototypeSlice(methods)), + METHODS: methods.toSorted(), STATUS_CODES, Agent: httpAgent.Agent, ClientRequest, diff --git a/lib/internal/http.js b/lib/internal/http.js index da1e8d3332..6fc0156bf5 100644 --- a/lib/internal/http.js +++ b/lib/internal/http.js @@ -2,8 +2,6 @@ const { Date, - DatePrototypeGetMilliseconds, - DatePrototypeToUTCString, Symbol, } = primordials; @@ -23,8 +21,8 @@ function utcDate() { function cache() { const d = new Date(); - utcCache = DatePrototypeToUTCString(d); - setUnrefTimeout(resetCache, 1000 - DatePrototypeGetMilliseconds(d)); + utcCache = d.toUTCString(); + setUnrefTimeout(resetCache, 1000 - d.getMilliseconds()); } function resetCache() {