mirror of https://github.com/nodejs/node.git
1228 lines
38 KiB
JavaScript
1228 lines
38 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 {
|
|
ArrayIsArray,
|
|
Error,
|
|
MathMin,
|
|
ObjectKeys,
|
|
ObjectSetPrototypeOf,
|
|
ReflectApply,
|
|
Symbol,
|
|
SymbolFor,
|
|
} = primordials;
|
|
|
|
const net = require('net');
|
|
const EE = require('events');
|
|
const assert = require('internal/assert');
|
|
const {
|
|
parsers,
|
|
freeParser,
|
|
continueExpression,
|
|
chunkExpression,
|
|
kIncomingMessage,
|
|
HTTPParser,
|
|
isLenient,
|
|
_checkInvalidHeaderChar: checkInvalidHeaderChar,
|
|
prepareError,
|
|
} = require('_http_common');
|
|
const { ConnectionsList } = internalBinding('http_parser');
|
|
const {
|
|
kUniqueHeaders,
|
|
parseUniqueHeadersOption,
|
|
OutgoingMessage,
|
|
} = require('_http_outgoing');
|
|
const {
|
|
kOutHeaders,
|
|
kNeedDrain,
|
|
isTraceHTTPEnabled,
|
|
traceBegin,
|
|
traceEnd,
|
|
getNextTraceEventId,
|
|
} = require('internal/http');
|
|
const {
|
|
defaultTriggerAsyncIdScope,
|
|
getOrSetAsyncId,
|
|
} = require('internal/async_hooks');
|
|
const { IncomingMessage } = require('_http_incoming');
|
|
const {
|
|
ConnResetException,
|
|
codes: {
|
|
ERR_HTTP_HEADERS_SENT,
|
|
ERR_HTTP_INVALID_STATUS_CODE,
|
|
ERR_HTTP_REQUEST_TIMEOUT,
|
|
ERR_HTTP_SOCKET_ASSIGNED,
|
|
ERR_HTTP_SOCKET_ENCODING,
|
|
ERR_INVALID_ARG_VALUE,
|
|
ERR_INVALID_CHAR,
|
|
ERR_OUT_OF_RANGE,
|
|
},
|
|
} = require('internal/errors');
|
|
const {
|
|
kEmptyObject,
|
|
promisify,
|
|
SymbolAsyncDispose,
|
|
} = require('internal/util');
|
|
const {
|
|
validateInteger,
|
|
validateBoolean,
|
|
validateLinkHeaderValue,
|
|
validateObject,
|
|
} = require('internal/validators');
|
|
const Buffer = require('buffer').Buffer;
|
|
const { setInterval, clearInterval } = require('timers');
|
|
let debug = require('internal/util/debuglog').debuglog('http', (fn) => {
|
|
debug = fn;
|
|
});
|
|
|
|
const dc = require('diagnostics_channel');
|
|
const onRequestStartChannel = dc.channel('http.server.request.start');
|
|
const onResponseFinishChannel = dc.channel('http.server.response.finish');
|
|
|
|
const kServerResponse = Symbol('ServerResponse');
|
|
const kServerResponseStatistics = Symbol('ServerResponseStatistics');
|
|
|
|
const {
|
|
hasObserver,
|
|
startPerf,
|
|
stopPerf,
|
|
} = require('internal/perf/observe');
|
|
|
|
const STATUS_CODES = {
|
|
100: 'Continue', // RFC 7231 6.2.1
|
|
101: 'Switching Protocols', // RFC 7231 6.2.2
|
|
102: 'Processing', // RFC 2518 10.1 (obsoleted by RFC 4918)
|
|
103: 'Early Hints', // RFC 8297 2
|
|
200: 'OK', // RFC 7231 6.3.1
|
|
201: 'Created', // RFC 7231 6.3.2
|
|
202: 'Accepted', // RFC 7231 6.3.3
|
|
203: 'Non-Authoritative Information', // RFC 7231 6.3.4
|
|
204: 'No Content', // RFC 7231 6.3.5
|
|
205: 'Reset Content', // RFC 7231 6.3.6
|
|
206: 'Partial Content', // RFC 7233 4.1
|
|
207: 'Multi-Status', // RFC 4918 11.1
|
|
208: 'Already Reported', // RFC 5842 7.1
|
|
226: 'IM Used', // RFC 3229 10.4.1
|
|
300: 'Multiple Choices', // RFC 7231 6.4.1
|
|
301: 'Moved Permanently', // RFC 7231 6.4.2
|
|
302: 'Found', // RFC 7231 6.4.3
|
|
303: 'See Other', // RFC 7231 6.4.4
|
|
304: 'Not Modified', // RFC 7232 4.1
|
|
305: 'Use Proxy', // RFC 7231 6.4.5
|
|
307: 'Temporary Redirect', // RFC 7231 6.4.7
|
|
308: 'Permanent Redirect', // RFC 7238 3
|
|
400: 'Bad Request', // RFC 7231 6.5.1
|
|
401: 'Unauthorized', // RFC 7235 3.1
|
|
402: 'Payment Required', // RFC 7231 6.5.2
|
|
403: 'Forbidden', // RFC 7231 6.5.3
|
|
404: 'Not Found', // RFC 7231 6.5.4
|
|
405: 'Method Not Allowed', // RFC 7231 6.5.5
|
|
406: 'Not Acceptable', // RFC 7231 6.5.6
|
|
407: 'Proxy Authentication Required', // RFC 7235 3.2
|
|
408: 'Request Timeout', // RFC 7231 6.5.7
|
|
409: 'Conflict', // RFC 7231 6.5.8
|
|
410: 'Gone', // RFC 7231 6.5.9
|
|
411: 'Length Required', // RFC 7231 6.5.10
|
|
412: 'Precondition Failed', // RFC 7232 4.2
|
|
413: 'Payload Too Large', // RFC 7231 6.5.11
|
|
414: 'URI Too Long', // RFC 7231 6.5.12
|
|
415: 'Unsupported Media Type', // RFC 7231 6.5.13
|
|
416: 'Range Not Satisfiable', // RFC 7233 4.4
|
|
417: 'Expectation Failed', // RFC 7231 6.5.14
|
|
418: 'I\'m a Teapot', // RFC 7168 2.3.3
|
|
421: 'Misdirected Request', // RFC 7540 9.1.2
|
|
422: 'Unprocessable Entity', // RFC 4918 11.2
|
|
423: 'Locked', // RFC 4918 11.3
|
|
424: 'Failed Dependency', // RFC 4918 11.4
|
|
425: 'Too Early', // RFC 8470 5.2
|
|
426: 'Upgrade Required', // RFC 2817 and RFC 7231 6.5.15
|
|
428: 'Precondition Required', // RFC 6585 3
|
|
429: 'Too Many Requests', // RFC 6585 4
|
|
431: 'Request Header Fields Too Large', // RFC 6585 5
|
|
451: 'Unavailable For Legal Reasons', // RFC 7725 3
|
|
500: 'Internal Server Error', // RFC 7231 6.6.1
|
|
501: 'Not Implemented', // RFC 7231 6.6.2
|
|
502: 'Bad Gateway', // RFC 7231 6.6.3
|
|
503: 'Service Unavailable', // RFC 7231 6.6.4
|
|
504: 'Gateway Timeout', // RFC 7231 6.6.5
|
|
505: 'HTTP Version Not Supported', // RFC 7231 6.6.6
|
|
506: 'Variant Also Negotiates', // RFC 2295 8.1
|
|
507: 'Insufficient Storage', // RFC 4918 11.5
|
|
508: 'Loop Detected', // RFC 5842 7.2
|
|
509: 'Bandwidth Limit Exceeded',
|
|
510: 'Not Extended', // RFC 2774 7
|
|
511: 'Network Authentication Required', // RFC 6585 6
|
|
};
|
|
|
|
const kOnExecute = HTTPParser.kOnExecute | 0;
|
|
const kOnTimeout = HTTPParser.kOnTimeout | 0;
|
|
const kLenientAll = HTTPParser.kLenientAll | 0;
|
|
const kLenientNone = HTTPParser.kLenientNone | 0;
|
|
const kConnections = Symbol('http.server.connections');
|
|
const kConnectionsCheckingInterval = Symbol('http.server.connectionsCheckingInterval');
|
|
|
|
const HTTP_SERVER_TRACE_EVENT_NAME = 'http.server.request';
|
|
// TODO(jazelly): make this configurable
|
|
const HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER = 1000;
|
|
|
|
class HTTPServerAsyncResource {
|
|
constructor(type, socket) {
|
|
this.type = type;
|
|
this.socket = socket;
|
|
}
|
|
}
|
|
|
|
function ServerResponse(req, options) {
|
|
OutgoingMessage.call(this, options);
|
|
|
|
if (req.method === 'HEAD') this._hasBody = false;
|
|
|
|
this.req = req;
|
|
this.sendDate = true;
|
|
this._sent100 = false;
|
|
this._expect_continue = false;
|
|
|
|
if (req.httpVersionMajor < 1 || req.httpVersionMinor < 1) {
|
|
this.useChunkedEncodingByDefault = chunkExpression.test(req.headers.te);
|
|
this.shouldKeepAlive = false;
|
|
}
|
|
|
|
if (hasObserver('http')) {
|
|
startPerf(this, kServerResponseStatistics, {
|
|
type: 'http',
|
|
name: 'HttpRequest',
|
|
detail: {
|
|
req: {
|
|
method: req.method,
|
|
url: req.url,
|
|
headers: req.headers,
|
|
},
|
|
},
|
|
});
|
|
}
|
|
if (isTraceHTTPEnabled()) {
|
|
this._traceEventId = getNextTraceEventId();
|
|
traceBegin(HTTP_SERVER_TRACE_EVENT_NAME, this._traceEventId);
|
|
}
|
|
}
|
|
ObjectSetPrototypeOf(ServerResponse.prototype, OutgoingMessage.prototype);
|
|
ObjectSetPrototypeOf(ServerResponse, OutgoingMessage);
|
|
|
|
ServerResponse.prototype._finish = function _finish() {
|
|
if (this[kServerResponseStatistics] && hasObserver('http')) {
|
|
stopPerf(this, kServerResponseStatistics, {
|
|
detail: {
|
|
res: {
|
|
statusCode: this.statusCode,
|
|
statusMessage: this.statusMessage,
|
|
headers: typeof this.getHeaders === 'function' ? this.getHeaders() : {},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
OutgoingMessage.prototype._finish.call(this);
|
|
if (isTraceHTTPEnabled() && typeof this._traceEventId === 'number') {
|
|
const data = {
|
|
url: this.req?.url,
|
|
statusCode: this.statusCode,
|
|
};
|
|
traceEnd(HTTP_SERVER_TRACE_EVENT_NAME, this._traceEventId, data);
|
|
}
|
|
};
|
|
|
|
|
|
ServerResponse.prototype.statusCode = 200;
|
|
ServerResponse.prototype.statusMessage = undefined;
|
|
|
|
function onServerResponseClose() {
|
|
// EventEmitter.emit makes a copy of the 'close' listeners array before
|
|
// calling the listeners. detachSocket() unregisters onServerResponseClose
|
|
// but if detachSocket() is called, directly or indirectly, by a 'close'
|
|
// listener, onServerResponseClose is still in that copy of the listeners
|
|
// array. That is, in the example below, b still gets called even though
|
|
// it's been removed by a:
|
|
//
|
|
// const EventEmitter = require('events');
|
|
// const obj = new EventEmitter();
|
|
// obj.on('event', a);
|
|
// obj.on('event', b);
|
|
// function a() { obj.removeListener('event', b) }
|
|
// function b() { throw "BAM!" }
|
|
// obj.emit('event'); // throws
|
|
//
|
|
// Ergo, we need to deal with stale 'close' events and handle the case
|
|
// where the ServerResponse object has already been deconstructed.
|
|
// Fortunately, that requires only a single if check. :-)
|
|
if (this._httpMessage) {
|
|
emitCloseNT(this._httpMessage);
|
|
}
|
|
}
|
|
|
|
ServerResponse.prototype.assignSocket = function assignSocket(socket) {
|
|
if (socket._httpMessage) {
|
|
throw new ERR_HTTP_SOCKET_ASSIGNED();
|
|
}
|
|
socket._httpMessage = this;
|
|
socket.on('close', onServerResponseClose);
|
|
this.socket = socket;
|
|
this.emit('socket', socket);
|
|
this._flush();
|
|
};
|
|
|
|
ServerResponse.prototype.detachSocket = function detachSocket(socket) {
|
|
assert(socket._httpMessage === this);
|
|
socket.removeListener('close', onServerResponseClose);
|
|
socket._httpMessage = null;
|
|
this.socket = null;
|
|
};
|
|
|
|
ServerResponse.prototype.writeContinue = function writeContinue(cb) {
|
|
this._writeRaw('HTTP/1.1 100 Continue\r\n\r\n', 'ascii', cb);
|
|
this._sent100 = true;
|
|
};
|
|
|
|
ServerResponse.prototype.writeProcessing = function writeProcessing(cb) {
|
|
this._writeRaw('HTTP/1.1 102 Processing\r\n\r\n', 'ascii', cb);
|
|
};
|
|
|
|
ServerResponse.prototype.writeEarlyHints = function writeEarlyHints(hints, cb) {
|
|
let head = 'HTTP/1.1 103 Early Hints\r\n';
|
|
|
|
validateObject(hints, 'hints');
|
|
|
|
if (hints.link === null || hints.link === undefined) {
|
|
return;
|
|
}
|
|
|
|
const link = validateLinkHeaderValue(hints.link);
|
|
|
|
if (link.length === 0) {
|
|
return;
|
|
}
|
|
|
|
head += 'Link: ' + link + '\r\n';
|
|
|
|
for (const key of ObjectKeys(hints)) {
|
|
if (key !== 'link') {
|
|
head += key + ': ' + hints[key] + '\r\n';
|
|
}
|
|
}
|
|
|
|
head += '\r\n';
|
|
|
|
this._writeRaw(head, 'ascii', cb);
|
|
};
|
|
|
|
ServerResponse.prototype._implicitHeader = function _implicitHeader() {
|
|
this.writeHead(this.statusCode);
|
|
};
|
|
|
|
ServerResponse.prototype.writeHead = writeHead;
|
|
function writeHead(statusCode, reason, obj) {
|
|
|
|
if (this._header) {
|
|
throw new ERR_HTTP_HEADERS_SENT('write');
|
|
}
|
|
|
|
const originalStatusCode = statusCode;
|
|
|
|
statusCode |= 0;
|
|
if (statusCode < 100 || statusCode > 999) {
|
|
throw new ERR_HTTP_INVALID_STATUS_CODE(originalStatusCode);
|
|
}
|
|
|
|
|
|
if (typeof reason === 'string') {
|
|
// writeHead(statusCode, reasonPhrase[, headers])
|
|
this.statusMessage = reason;
|
|
} else {
|
|
// writeHead(statusCode[, headers])
|
|
if (!this.statusMessage)
|
|
this.statusMessage = STATUS_CODES[statusCode] || 'unknown';
|
|
obj ??= reason;
|
|
}
|
|
this.statusCode = statusCode;
|
|
|
|
let headers;
|
|
if (this[kOutHeaders]) {
|
|
// Slow-case: when progressive API and header fields are passed.
|
|
let k;
|
|
if (ArrayIsArray(obj)) {
|
|
if (obj.length % 2 !== 0) {
|
|
throw new ERR_INVALID_ARG_VALUE('headers', obj);
|
|
}
|
|
|
|
// Headers in obj should override previous headers but still
|
|
// allow explicit duplicates. To do so, we first remove any
|
|
// existing conflicts, then use appendHeader.
|
|
|
|
for (let n = 0; n < obj.length; n += 2) {
|
|
k = obj[n + 0];
|
|
this.removeHeader(k);
|
|
}
|
|
|
|
for (let n = 0; n < obj.length; n += 2) {
|
|
k = obj[n + 0];
|
|
if (k) this.appendHeader(k, obj[n + 1]);
|
|
}
|
|
} else if (obj) {
|
|
const keys = ObjectKeys(obj);
|
|
// Retain for(;;) loop for performance reasons
|
|
// Refs: https://github.com/nodejs/node/pull/30958
|
|
for (let i = 0; i < keys.length; i++) {
|
|
k = keys[i];
|
|
if (k) this.setHeader(k, obj[k]);
|
|
}
|
|
}
|
|
// Only progressive api is used
|
|
headers = this[kOutHeaders];
|
|
} else {
|
|
// Only writeHead() called
|
|
headers = obj;
|
|
}
|
|
|
|
if (checkInvalidHeaderChar(this.statusMessage))
|
|
throw new ERR_INVALID_CHAR('statusMessage');
|
|
|
|
const statusLine = `HTTP/1.1 ${statusCode} ${this.statusMessage}\r\n`;
|
|
|
|
if (statusCode === 204 || statusCode === 304 ||
|
|
(statusCode >= 100 && statusCode <= 199)) {
|
|
// RFC 2616, 10.2.5:
|
|
// The 204 response MUST NOT include a message-body, and thus is always
|
|
// terminated by the first empty line after the header fields.
|
|
// RFC 2616, 10.3.5:
|
|
// The 304 response MUST NOT contain a message-body, and thus is always
|
|
// terminated by the first empty line after the header fields.
|
|
// RFC 2616, 10.1 Informational 1xx:
|
|
// This class of status code indicates a provisional response,
|
|
// consisting only of the Status-Line and optional headers, and is
|
|
// terminated by an empty line.
|
|
this._hasBody = false;
|
|
}
|
|
|
|
// Don't keep alive connections where the client expects 100 Continue
|
|
// but we sent a final status; they may put extra bytes on the wire.
|
|
if (this._expect_continue && !this._sent100) {
|
|
this.shouldKeepAlive = false;
|
|
}
|
|
|
|
this._storeHeader(statusLine, headers);
|
|
|
|
return this;
|
|
}
|
|
|
|
// Docs-only deprecated: DEP0063
|
|
ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead;
|
|
|
|
function storeHTTPOptions(options) {
|
|
this[kIncomingMessage] = options.IncomingMessage || IncomingMessage;
|
|
this[kServerResponse] = options.ServerResponse || ServerResponse;
|
|
|
|
const maxHeaderSize = options.maxHeaderSize;
|
|
if (maxHeaderSize !== undefined)
|
|
validateInteger(maxHeaderSize, 'maxHeaderSize', 0);
|
|
this.maxHeaderSize = maxHeaderSize;
|
|
|
|
const insecureHTTPParser = options.insecureHTTPParser;
|
|
if (insecureHTTPParser !== undefined)
|
|
validateBoolean(insecureHTTPParser, 'options.insecureHTTPParser');
|
|
this.insecureHTTPParser = insecureHTTPParser;
|
|
|
|
const requestTimeout = options.requestTimeout;
|
|
if (requestTimeout !== undefined) {
|
|
validateInteger(requestTimeout, 'requestTimeout', 0);
|
|
this.requestTimeout = requestTimeout;
|
|
} else {
|
|
this.requestTimeout = 300_000; // 5 minutes
|
|
}
|
|
|
|
const headersTimeout = options.headersTimeout;
|
|
if (headersTimeout !== undefined) {
|
|
validateInteger(headersTimeout, 'headersTimeout', 0);
|
|
this.headersTimeout = headersTimeout;
|
|
} else {
|
|
this.headersTimeout = MathMin(60_000, this.requestTimeout); // Minimum between 60 seconds or requestTimeout
|
|
}
|
|
|
|
if (this.requestTimeout > 0 && this.headersTimeout > 0 && this.headersTimeout > this.requestTimeout) {
|
|
throw new ERR_OUT_OF_RANGE('headersTimeout', '<= requestTimeout', headersTimeout);
|
|
}
|
|
|
|
const keepAliveTimeout = options.keepAliveTimeout;
|
|
if (keepAliveTimeout !== undefined) {
|
|
validateInteger(keepAliveTimeout, 'keepAliveTimeout', 0);
|
|
this.keepAliveTimeout = keepAliveTimeout;
|
|
} else {
|
|
this.keepAliveTimeout = 5_000; // 5 seconds;
|
|
}
|
|
|
|
const connectionsCheckingInterval = options.connectionsCheckingInterval;
|
|
if (connectionsCheckingInterval !== undefined) {
|
|
validateInteger(connectionsCheckingInterval, 'connectionsCheckingInterval', 0);
|
|
this.connectionsCheckingInterval = connectionsCheckingInterval;
|
|
} else {
|
|
this.connectionsCheckingInterval = 30_000; // 30 seconds
|
|
}
|
|
|
|
const requireHostHeader = options.requireHostHeader;
|
|
if (requireHostHeader !== undefined) {
|
|
validateBoolean(requireHostHeader, 'options.requireHostHeader');
|
|
this.requireHostHeader = requireHostHeader;
|
|
} else {
|
|
this.requireHostHeader = true;
|
|
}
|
|
|
|
const joinDuplicateHeaders = options.joinDuplicateHeaders;
|
|
if (joinDuplicateHeaders !== undefined) {
|
|
validateBoolean(joinDuplicateHeaders, 'options.joinDuplicateHeaders');
|
|
}
|
|
this.joinDuplicateHeaders = joinDuplicateHeaders;
|
|
|
|
const rejectNonStandardBodyWrites = options.rejectNonStandardBodyWrites;
|
|
if (rejectNonStandardBodyWrites !== undefined) {
|
|
validateBoolean(rejectNonStandardBodyWrites, 'options.rejectNonStandardBodyWrites');
|
|
this.rejectNonStandardBodyWrites = rejectNonStandardBodyWrites;
|
|
} else {
|
|
this.rejectNonStandardBodyWrites = false;
|
|
}
|
|
}
|
|
|
|
function setupConnectionsTracking() {
|
|
// Start connection handling
|
|
if (!this[kConnections]) {
|
|
this[kConnections] = new ConnectionsList();
|
|
}
|
|
|
|
if (this[kConnectionsCheckingInterval]) {
|
|
clearInterval(this[kConnectionsCheckingInterval]);
|
|
}
|
|
// This checker is started without checking whether any headersTimeout or requestTimeout is non zero
|
|
// otherwise it would not be started if such timeouts are modified after createServer.
|
|
this[kConnectionsCheckingInterval] =
|
|
setInterval(checkConnections.bind(this), this.connectionsCheckingInterval).unref();
|
|
}
|
|
|
|
function httpServerPreClose(server) {
|
|
server.closeIdleConnections();
|
|
clearInterval(server[kConnectionsCheckingInterval]);
|
|
}
|
|
|
|
function Server(options, requestListener) {
|
|
if (!(this instanceof Server)) return new Server(options, requestListener);
|
|
|
|
if (typeof options === 'function') {
|
|
requestListener = options;
|
|
options = kEmptyObject;
|
|
} else if (options == null) {
|
|
options = kEmptyObject;
|
|
} else {
|
|
validateObject(options, 'options');
|
|
}
|
|
|
|
storeHTTPOptions.call(this, options);
|
|
net.Server.call(
|
|
this,
|
|
{ allowHalfOpen: true, noDelay: options.noDelay ?? true,
|
|
keepAlive: options.keepAlive,
|
|
keepAliveInitialDelay: options.keepAliveInitialDelay,
|
|
highWaterMark: options.highWaterMark });
|
|
|
|
if (requestListener) {
|
|
this.on('request', requestListener);
|
|
}
|
|
|
|
// Similar option to this. Too lazy to write my own docs.
|
|
// http://www.squid-cache.org/Doc/config/half_closed_clients/
|
|
// https://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F
|
|
this.httpAllowHalfOpen = false;
|
|
|
|
this.on('connection', connectionListener);
|
|
this.on('listening', setupConnectionsTracking);
|
|
|
|
this.timeout = 0;
|
|
this.maxHeadersCount = null;
|
|
this.maxRequestsPerSocket = 0;
|
|
|
|
this[kUniqueHeaders] = parseUniqueHeadersOption(options.uniqueHeaders);
|
|
}
|
|
ObjectSetPrototypeOf(Server.prototype, net.Server.prototype);
|
|
ObjectSetPrototypeOf(Server, net.Server);
|
|
|
|
Server.prototype.close = function() {
|
|
httpServerPreClose(this);
|
|
ReflectApply(net.Server.prototype.close, this, arguments);
|
|
return this;
|
|
};
|
|
|
|
Server.prototype[SymbolAsyncDispose] = async function() {
|
|
return promisify(this.close).call(this);
|
|
};
|
|
|
|
Server.prototype.closeAllConnections = function() {
|
|
if (!this[kConnections]) {
|
|
return;
|
|
}
|
|
|
|
const connections = this[kConnections].all();
|
|
|
|
for (let i = 0, l = connections.length; i < l; i++) {
|
|
connections[i].socket.destroy();
|
|
}
|
|
};
|
|
|
|
Server.prototype.closeIdleConnections = function() {
|
|
if (!this[kConnections]) {
|
|
return;
|
|
}
|
|
|
|
const connections = this[kConnections].idle();
|
|
|
|
for (let i = 0, l = connections.length; i < l; i++) {
|
|
if (connections[i].socket._httpMessage && !connections[i].socket._httpMessage.finished) {
|
|
continue;
|
|
}
|
|
|
|
connections[i].socket.destroy();
|
|
}
|
|
};
|
|
|
|
Server.prototype.setTimeout = function setTimeout(msecs, callback) {
|
|
this.timeout = msecs;
|
|
if (callback)
|
|
this.on('timeout', callback);
|
|
return this;
|
|
};
|
|
|
|
Server.prototype[EE.captureRejectionSymbol] = function(err, event, ...args) {
|
|
switch (event) {
|
|
case 'request': {
|
|
const { 1: res } = args;
|
|
if (!res.headersSent && !res.writableEnded) {
|
|
// Don't leak headers.
|
|
const names = res.getHeaderNames();
|
|
for (let i = 0; i < names.length; i++) {
|
|
res.removeHeader(names[i]);
|
|
}
|
|
res.statusCode = 500;
|
|
res.end(STATUS_CODES[500]);
|
|
} else {
|
|
res.destroy();
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
net.Server.prototype[SymbolFor('nodejs.rejection')]
|
|
.apply(this, arguments);
|
|
}
|
|
};
|
|
|
|
function checkConnections() {
|
|
if (this.headersTimeout === 0 && this.requestTimeout === 0) {
|
|
return;
|
|
}
|
|
|
|
const expired = this[kConnections].expired(this.headersTimeout, this.requestTimeout);
|
|
|
|
for (let i = 0; i < expired.length; i++) {
|
|
const socket = expired[i].socket;
|
|
|
|
if (socket) {
|
|
onRequestTimeout(socket);
|
|
}
|
|
}
|
|
}
|
|
|
|
function connectionListener(socket) {
|
|
defaultTriggerAsyncIdScope(
|
|
getOrSetAsyncId(socket), connectionListenerInternal, this, socket,
|
|
);
|
|
}
|
|
|
|
function connectionListenerInternal(server, socket) {
|
|
debug('SERVER new http connection');
|
|
|
|
// Ensure that the server property of the socket is correctly set.
|
|
// See https://github.com/nodejs/node/issues/13435
|
|
socket.server = server;
|
|
|
|
// If the user has added a listener to the server,
|
|
// request, or response, then it's their responsibility.
|
|
// otherwise, destroy on timeout by default
|
|
if (server.timeout && typeof socket.setTimeout === 'function')
|
|
socket.setTimeout(server.timeout);
|
|
socket.on('timeout', socketOnTimeout);
|
|
|
|
const parser = parsers.alloc();
|
|
|
|
const lenient = server.insecureHTTPParser === undefined ?
|
|
isLenient() : server.insecureHTTPParser;
|
|
|
|
// TODO(addaleax): This doesn't play well with the
|
|
// `async_hooks.currentResource()` proposal, see
|
|
// https://github.com/nodejs/node/pull/21313
|
|
parser.initialize(
|
|
HTTPParser.REQUEST,
|
|
new HTTPServerAsyncResource('HTTPINCOMINGMESSAGE', socket),
|
|
server.maxHeaderSize || 0,
|
|
lenient ? kLenientAll : kLenientNone,
|
|
server[kConnections],
|
|
);
|
|
parser.socket = socket;
|
|
socket.parser = parser;
|
|
|
|
// Propagate headers limit from server instance to parser
|
|
if (typeof server.maxHeadersCount === 'number') {
|
|
parser.maxHeaderPairs = server.maxHeadersCount << 1;
|
|
}
|
|
|
|
const state = {
|
|
onData: null,
|
|
onEnd: null,
|
|
onClose: null,
|
|
onDrain: null,
|
|
outgoing: [],
|
|
incoming: [],
|
|
// `outgoingData` is an approximate amount of bytes queued through all
|
|
// inactive responses. If more data than the high watermark is queued - we
|
|
// need to pause TCP socket/HTTP parser, and wait until the data will be
|
|
// sent to the client.
|
|
outgoingData: 0,
|
|
requestsCount: 0,
|
|
keepAliveTimeoutSet: false,
|
|
};
|
|
state.onData = socketOnData.bind(undefined,
|
|
server, socket, parser, state);
|
|
state.onEnd = socketOnEnd.bind(undefined,
|
|
server, socket, parser, state);
|
|
state.onClose = socketOnClose.bind(undefined,
|
|
socket, state);
|
|
state.onDrain = socketOnDrain.bind(undefined,
|
|
socket, state);
|
|
socket.on('data', state.onData);
|
|
socket.on('error', socketOnError);
|
|
socket.on('end', state.onEnd);
|
|
socket.on('close', state.onClose);
|
|
socket.on('drain', state.onDrain);
|
|
parser.onIncoming = parserOnIncoming.bind(undefined,
|
|
server, socket, state);
|
|
|
|
// We are consuming socket, so it won't get any actual data
|
|
socket.on('resume', onSocketResume);
|
|
socket.on('pause', onSocketPause);
|
|
|
|
// Overrides to unconsume on `data`, `readable` listeners
|
|
socket.on = generateSocketListenerWrapper('on');
|
|
socket.addListener = generateSocketListenerWrapper('addListener');
|
|
socket.prependListener = generateSocketListenerWrapper('prependListener');
|
|
socket.setEncoding = socketSetEncoding;
|
|
|
|
// We only consume the socket if it has never been consumed before.
|
|
if (socket._handle && socket._handle.isStreamBase &&
|
|
!socket._handle._consumed) {
|
|
parser._consumed = true;
|
|
socket._handle._consumed = true;
|
|
parser.consume(socket._handle);
|
|
}
|
|
parser[kOnExecute] =
|
|
onParserExecute.bind(undefined,
|
|
server, socket, parser, state);
|
|
|
|
parser[kOnTimeout] =
|
|
onParserTimeout.bind(undefined,
|
|
server, socket);
|
|
|
|
socket._paused = false;
|
|
}
|
|
|
|
function socketSetEncoding() {
|
|
throw new ERR_HTTP_SOCKET_ENCODING();
|
|
}
|
|
|
|
function updateOutgoingData(socket, state, delta) {
|
|
state.outgoingData += delta;
|
|
socketOnDrain(socket, state);
|
|
}
|
|
|
|
function socketOnDrain(socket, state) {
|
|
const needPause = state.outgoingData > socket.writableHighWaterMark;
|
|
|
|
// If we previously paused, then start reading again.
|
|
if (socket._paused && !needPause) {
|
|
socket._paused = false;
|
|
if (socket.parser)
|
|
socket.parser.resume();
|
|
socket.resume();
|
|
}
|
|
|
|
const msg = socket._httpMessage;
|
|
if (msg && !msg.finished && msg[kNeedDrain]) {
|
|
msg[kNeedDrain] = false;
|
|
msg.emit('drain');
|
|
}
|
|
}
|
|
|
|
function socketOnTimeout() {
|
|
const req = this.parser && this.parser.incoming;
|
|
const reqTimeout = req && !req.complete && req.emit('timeout', this);
|
|
const res = this._httpMessage;
|
|
const resTimeout = res && res.emit('timeout', this);
|
|
const serverTimeout = this.server.emit('timeout', this);
|
|
|
|
if (!reqTimeout && !resTimeout && !serverTimeout)
|
|
this.destroy();
|
|
}
|
|
|
|
function socketOnClose(socket, state) {
|
|
debug('server socket close');
|
|
freeParser(socket.parser, null, socket);
|
|
abortIncoming(state.incoming);
|
|
}
|
|
|
|
function abortIncoming(incoming) {
|
|
while (incoming.length) {
|
|
const req = incoming.shift();
|
|
req.destroy(new ConnResetException('aborted'));
|
|
}
|
|
// Abort socket._httpMessage ?
|
|
}
|
|
|
|
function socketOnEnd(server, socket, parser, state) {
|
|
const ret = parser.finish();
|
|
|
|
if (ret instanceof Error) {
|
|
debug('parse error');
|
|
// socketOnError has additional logic and will call socket.destroy(err).
|
|
socketOnError.call(socket, ret);
|
|
} else if (!server.httpAllowHalfOpen) {
|
|
socket.end();
|
|
} else if (state.outgoing.length) {
|
|
state.outgoing[state.outgoing.length - 1]._last = true;
|
|
} else if (socket._httpMessage) {
|
|
socket._httpMessage._last = true;
|
|
} else {
|
|
socket.end();
|
|
}
|
|
}
|
|
|
|
function socketOnData(server, socket, parser, state, d) {
|
|
assert(!socket._paused);
|
|
debug('SERVER socketOnData %d', d.length);
|
|
|
|
const ret = parser.execute(d);
|
|
onParserExecuteCommon(server, socket, parser, state, ret, d);
|
|
}
|
|
|
|
function onRequestTimeout(socket) {
|
|
// socketOnError has additional logic and will call socket.destroy(err).
|
|
socketOnError.call(socket, new ERR_HTTP_REQUEST_TIMEOUT());
|
|
}
|
|
|
|
function onParserExecute(server, socket, parser, state, ret) {
|
|
// When underlying `net.Socket` instance is consumed - no
|
|
// `data` events are emitted, and thus `socket.setTimeout` fires the
|
|
// callback even if the data is constantly flowing into the socket.
|
|
// See, https://github.com/nodejs/node/commit/ec2822adaad76b126b5cccdeaa1addf2376c9aa6
|
|
socket._unrefTimer();
|
|
debug('SERVER socketOnParserExecute %d', ret);
|
|
onParserExecuteCommon(server, socket, parser, state, ret, undefined);
|
|
}
|
|
|
|
function onParserTimeout(server, socket) {
|
|
const serverTimeout = server.emit('timeout', socket);
|
|
|
|
if (!serverTimeout)
|
|
socket.destroy();
|
|
}
|
|
|
|
const noop = () => {};
|
|
const badRequestResponse = Buffer.from(
|
|
`HTTP/1.1 400 ${STATUS_CODES[400]}\r\n` +
|
|
'Connection: close\r\n\r\n', 'ascii',
|
|
);
|
|
const requestTimeoutResponse = Buffer.from(
|
|
`HTTP/1.1 408 ${STATUS_CODES[408]}\r\n` +
|
|
'Connection: close\r\n\r\n', 'ascii',
|
|
);
|
|
const requestHeaderFieldsTooLargeResponse = Buffer.from(
|
|
`HTTP/1.1 431 ${STATUS_CODES[431]}\r\n` +
|
|
'Connection: close\r\n\r\n', 'ascii',
|
|
);
|
|
|
|
const requestChunkExtensionsTooLargeResponse = Buffer.from(
|
|
`HTTP/1.1 413 ${STATUS_CODES[413]}\r\n` +
|
|
'Connection: close\r\n\r\n', 'ascii',
|
|
);
|
|
|
|
function socketOnError(e) {
|
|
// Ignore further errors
|
|
this.removeListener('error', socketOnError);
|
|
|
|
if (this.listenerCount('error', noop) === 0) {
|
|
this.on('error', noop);
|
|
}
|
|
|
|
if (!this.server.emit('clientError', e, this)) {
|
|
// Caution must be taken to avoid corrupting the remote peer.
|
|
// Reply an error segment if there is no in-flight `ServerResponse`,
|
|
// or no data of the in-flight one has been written yet to this socket.
|
|
if (this.writable &&
|
|
(!this._httpMessage || !this._httpMessage._headerSent)) {
|
|
let response;
|
|
|
|
switch (e.code) {
|
|
case 'HPE_HEADER_OVERFLOW':
|
|
response = requestHeaderFieldsTooLargeResponse;
|
|
break;
|
|
case 'HPE_CHUNK_EXTENSIONS_OVERFLOW':
|
|
response = requestChunkExtensionsTooLargeResponse;
|
|
break;
|
|
case 'ERR_HTTP_REQUEST_TIMEOUT':
|
|
response = requestTimeoutResponse;
|
|
break;
|
|
default:
|
|
response = badRequestResponse;
|
|
break;
|
|
}
|
|
|
|
this.write(response);
|
|
}
|
|
this.destroy(e);
|
|
}
|
|
}
|
|
|
|
function onParserExecuteCommon(server, socket, parser, state, ret, d) {
|
|
resetSocketTimeout(server, socket, state);
|
|
|
|
if (ret instanceof Error) {
|
|
prepareError(ret, parser, d);
|
|
debug('parse error', ret);
|
|
socketOnError.call(socket, ret);
|
|
} else if (parser.incoming && parser.incoming.upgrade) {
|
|
// Upgrade or CONNECT
|
|
const req = parser.incoming;
|
|
debug('SERVER upgrade or connect', req.method);
|
|
|
|
if (!d)
|
|
d = parser.getCurrentBuffer();
|
|
|
|
socket.removeListener('data', state.onData);
|
|
socket.removeListener('end', state.onEnd);
|
|
socket.removeListener('close', state.onClose);
|
|
socket.removeListener('drain', state.onDrain);
|
|
socket.removeListener('error', socketOnError);
|
|
socket.removeListener('timeout', socketOnTimeout);
|
|
unconsume(parser, socket);
|
|
parser.finish();
|
|
freeParser(parser, req, socket);
|
|
parser = null;
|
|
|
|
const eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade';
|
|
if (eventName === 'upgrade' || server.listenerCount(eventName) > 0) {
|
|
debug('SERVER have listener for %s', eventName);
|
|
const bodyHead = d.slice(ret, d.length);
|
|
|
|
socket.readableFlowing = null;
|
|
|
|
server.emit(eventName, req, socket, bodyHead);
|
|
} else {
|
|
// Got CONNECT method, but have no handler.
|
|
socket.destroy();
|
|
}
|
|
} else if (parser.incoming && parser.incoming.method === 'PRI') {
|
|
debug('SERVER got PRI request');
|
|
socket.destroy();
|
|
}
|
|
|
|
if (socket._paused && socket.parser) {
|
|
// onIncoming paused the socket, we should pause the parser as well
|
|
debug('pause parser');
|
|
socket.parser.pause();
|
|
}
|
|
}
|
|
|
|
function clearIncoming(req) {
|
|
req = req || this;
|
|
const parser = req.socket && req.socket.parser;
|
|
// Reset the .incoming property so that the request object can be gc'ed.
|
|
if (parser && parser.incoming === req) {
|
|
if (req.readableEnded) {
|
|
parser.incoming = null;
|
|
} else {
|
|
req.on('end', clearIncoming);
|
|
}
|
|
}
|
|
}
|
|
|
|
function resOnFinish(req, res, socket, state, server) {
|
|
if (onResponseFinishChannel.hasSubscribers) {
|
|
onResponseFinishChannel.publish({
|
|
request: req,
|
|
response: res,
|
|
socket,
|
|
server,
|
|
});
|
|
}
|
|
|
|
// Usually the first incoming element should be our request. it may
|
|
// be that in the case abortIncoming() was called that the incoming
|
|
// array will be empty.
|
|
assert(state.incoming.length === 0 || state.incoming[0] === req);
|
|
|
|
state.incoming.shift();
|
|
|
|
// If the user never called req.read(), and didn't pipe() or
|
|
// .resume() or .on('data'), then we call req._dump() so that the
|
|
// bytes will be pulled off the wire.
|
|
if (!req._consuming && !req._readableState.resumeScheduled)
|
|
req._dump();
|
|
|
|
res.detachSocket(socket);
|
|
clearIncoming(req);
|
|
process.nextTick(emitCloseNT, res);
|
|
|
|
if (res._last) {
|
|
if (typeof socket.destroySoon === 'function') {
|
|
socket.destroySoon();
|
|
} else {
|
|
socket.end();
|
|
}
|
|
} else if (state.outgoing.length === 0) {
|
|
if (server.keepAliveTimeout && typeof socket.setTimeout === 'function') {
|
|
// Increase the internal timeout wrt the advertised value to reduce
|
|
// the likelihood of ECONNRESET errors.
|
|
socket.setTimeout(server.keepAliveTimeout + HTTP_SERVER_KEEP_ALIVE_TIMEOUT_BUFFER);
|
|
state.keepAliveTimeoutSet = true;
|
|
}
|
|
} else {
|
|
// Start sending the next message
|
|
const m = state.outgoing.shift();
|
|
if (m) {
|
|
m.assignSocket(socket);
|
|
}
|
|
}
|
|
}
|
|
|
|
function emitCloseNT(self) {
|
|
if (!self._closed) {
|
|
self.destroyed = true;
|
|
self._closed = true;
|
|
self.emit('close');
|
|
}
|
|
}
|
|
|
|
// The following callback is issued after the headers have been read on a
|
|
// new message. In this callback we setup the response object and pass it
|
|
// to the user.
|
|
function parserOnIncoming(server, socket, state, req, keepAlive) {
|
|
resetSocketTimeout(server, socket, state);
|
|
|
|
if (req.upgrade) {
|
|
req.upgrade = req.method === 'CONNECT' ||
|
|
server.listenerCount('upgrade') > 0;
|
|
if (req.upgrade)
|
|
return 2;
|
|
}
|
|
|
|
state.incoming.push(req);
|
|
|
|
// If the writable end isn't consuming, then stop reading
|
|
// so that we don't become overwhelmed by a flood of
|
|
// pipelined requests that may never be resolved.
|
|
if (!socket._paused) {
|
|
const ws = socket._writableState;
|
|
if (ws.needDrain || state.outgoingData >= socket.writableHighWaterMark) {
|
|
socket._paused = true;
|
|
// We also need to pause the parser, but don't do that until after
|
|
// the call to execute, because we may still be processing the last
|
|
// chunk.
|
|
socket.pause();
|
|
}
|
|
}
|
|
|
|
const res = new server[kServerResponse](req,
|
|
{
|
|
highWaterMark: socket.writableHighWaterMark,
|
|
rejectNonStandardBodyWrites: server.rejectNonStandardBodyWrites,
|
|
});
|
|
res._keepAliveTimeout = server.keepAliveTimeout;
|
|
res._maxRequestsPerSocket = server.maxRequestsPerSocket;
|
|
res._onPendingData = updateOutgoingData.bind(undefined,
|
|
socket, state);
|
|
|
|
res.shouldKeepAlive = keepAlive;
|
|
res[kUniqueHeaders] = server[kUniqueHeaders];
|
|
|
|
if (onRequestStartChannel.hasSubscribers) {
|
|
onRequestStartChannel.publish({
|
|
request: req,
|
|
response: res,
|
|
socket,
|
|
server,
|
|
});
|
|
}
|
|
|
|
if (socket._httpMessage) {
|
|
// There are already pending outgoing res, append.
|
|
state.outgoing.push(res);
|
|
} else {
|
|
res.assignSocket(socket);
|
|
}
|
|
|
|
// When we're finished writing the response, check if this is the last
|
|
// response, if so destroy the socket.
|
|
res.on('finish',
|
|
resOnFinish.bind(undefined,
|
|
req, res, socket, state, server));
|
|
|
|
let handled = false;
|
|
|
|
|
|
if (req.httpVersionMajor === 1 && req.httpVersionMinor === 1) {
|
|
|
|
// From RFC 7230 5.4 https://datatracker.ietf.org/doc/html/rfc7230#section-5.4
|
|
// A server MUST respond with a 400 (Bad Request) status code to any
|
|
// HTTP/1.1 request message that lacks a Host header field
|
|
if (server.requireHostHeader && req.headers.host === undefined) {
|
|
res.writeHead(400, ['Connection', 'close']);
|
|
res.end();
|
|
return 0;
|
|
}
|
|
|
|
const isRequestsLimitSet = (
|
|
typeof server.maxRequestsPerSocket === 'number' &&
|
|
server.maxRequestsPerSocket > 0
|
|
);
|
|
|
|
if (isRequestsLimitSet) {
|
|
state.requestsCount++;
|
|
res.maxRequestsOnConnectionReached = (
|
|
server.maxRequestsPerSocket <= state.requestsCount);
|
|
}
|
|
|
|
if (isRequestsLimitSet &&
|
|
(server.maxRequestsPerSocket < state.requestsCount)) {
|
|
handled = true;
|
|
server.emit('dropRequest', req, socket);
|
|
res.writeHead(503);
|
|
res.end();
|
|
} else if (req.headers.expect !== undefined) {
|
|
handled = true;
|
|
|
|
if (continueExpression.test(req.headers.expect)) {
|
|
res._expect_continue = true;
|
|
if (server.listenerCount('checkContinue') > 0) {
|
|
server.emit('checkContinue', req, res);
|
|
} else {
|
|
res.writeContinue();
|
|
server.emit('request', req, res);
|
|
}
|
|
} else if (server.listenerCount('checkExpectation') > 0) {
|
|
server.emit('checkExpectation', req, res);
|
|
} else {
|
|
res.writeHead(417);
|
|
res.end();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!handled) {
|
|
server.emit('request', req, res);
|
|
}
|
|
|
|
return 0; // No special treatment.
|
|
}
|
|
|
|
function resetSocketTimeout(server, socket, state) {
|
|
if (!state.keepAliveTimeoutSet)
|
|
return;
|
|
|
|
socket.setTimeout(server.timeout || 0);
|
|
state.keepAliveTimeoutSet = false;
|
|
}
|
|
|
|
function onSocketResume() {
|
|
// It may seem that the socket is resumed, but this is an enemy's trick to
|
|
// deceive us! `resume` is emitted asynchronously, and may be called from
|
|
// `incoming.readStart()`. Stop the socket again here, just to preserve the
|
|
// state.
|
|
//
|
|
// We don't care about stream semantics for the consumed socket anyway.
|
|
if (this._paused) {
|
|
this.pause();
|
|
return;
|
|
}
|
|
|
|
if (this._handle && !this._handle.reading) {
|
|
this._handle.reading = true;
|
|
this._handle.readStart();
|
|
}
|
|
}
|
|
|
|
function onSocketPause() {
|
|
if (this._handle && this._handle.reading) {
|
|
this._handle.reading = false;
|
|
this._handle.readStop();
|
|
}
|
|
}
|
|
|
|
function unconsume(parser, socket) {
|
|
if (socket._handle) {
|
|
if (parser._consumed)
|
|
parser.unconsume();
|
|
parser._consumed = false;
|
|
socket.removeListener('pause', onSocketPause);
|
|
socket.removeListener('resume', onSocketResume);
|
|
}
|
|
}
|
|
|
|
function generateSocketListenerWrapper(originalFnName) {
|
|
return function socketListenerWrap(ev, fn) {
|
|
const res = net.Socket.prototype[originalFnName].call(this,
|
|
ev, fn);
|
|
if (!this.parser) {
|
|
this.on = net.Socket.prototype.on;
|
|
this.addListener = net.Socket.prototype.addListener;
|
|
this.prependListener = net.Socket.prototype.prependListener;
|
|
return res;
|
|
}
|
|
|
|
if (ev === 'data' || ev === 'readable')
|
|
unconsume(this.parser, this);
|
|
|
|
return res;
|
|
};
|
|
}
|
|
|
|
module.exports = {
|
|
STATUS_CODES,
|
|
Server,
|
|
ServerResponse,
|
|
setupConnectionsTracking,
|
|
storeHTTPOptions,
|
|
_connectionListener: connectionListener,
|
|
kServerResponse,
|
|
httpServerPreClose,
|
|
kConnectionsCheckingInterval,
|
|
};
|