mirror of
https://github.com/nodejs/node.git
synced 2024-11-27 22:16:50 +01:00
574f2dd517
PR-URL: https://github.com/nodejs/node/pull/55045 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: Chemi Atlow <chemi@atlow.co.il> Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com> Reviewed-By: Paolo Insogna <paolo@cowtech.it> Reviewed-By: Marco Ippolito <marcoippolito54@gmail.com> Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
454 lines
14 KiB
JavaScript
454 lines
14 KiB
JavaScript
// 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,
|
|
};
|