| // 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, |
| StringPrototypeCharCodeAt, |
| StringPrototypeSlice, |
| StringPrototypeToLowerCase, |
| 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.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 && 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(StringPrototypeToLowerCase(field), 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 = StringPrototypeCharCodeAt(field, 0); |
| if (flag === 0 || flag === 2) { |
| field = StringPrototypeSlice(field, 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 (dest[field] === undefined) { |
| // Drop duplicates |
| dest[field] = value; |
| } |
| } |
| |
| IncomingMessage.prototype._addHeaderLineDistinct = _addHeaderLineDistinct; |
| function _addHeaderLineDistinct(field, value, dest) { |
| field = StringPrototypeToLowerCase(field); |
| 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 |
| }; |