// 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, };