nstdlib-nightly
Version:
Node.js standard library converted to runtime-agnostic ES modules.
435 lines (395 loc) • 14.7 kB
JavaScript
// Source: https://github.com/nodejs/node/blob/65eff1eb/lib/_http_incoming.js
import { Readable, finished } from "nstdlib/lib/stream";
// 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.
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;
}
Object.setPrototypeOf(IncomingMessage.prototype, Readable.prototype);
Object.setPrototypeOf(IncomingMessage, Readable);
Object.defineProperty(IncomingMessage.prototype, "connection", {
__proto__: null,
get: function () {
return this.socket;
},
set: function (val) {
this.socket = val;
},
});
Object.defineProperty(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;
},
});
Object.defineProperty(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;
},
});
Object.defineProperty(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;
},
});
Object.defineProperty(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(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);
}
}
export { IncomingMessage };
export { readStart };
export { readStop };