From d3b4e1f119a6b681212b0dbcccda31e6cc20bbc2 Mon Sep 17 00:00:00 2001 From: Gus Caplan Date: Sun, 5 Nov 2017 12:07:27 -0600 Subject: [PATCH] refactor lib/ underscore files into directories --- benchmark/http/check_invalid_header_char.js | 2 +- benchmark/http/check_is_http_token.js | 2 +- doc/api/deprecations.md | 23 + lib/_http_agent.js | 364 +----- lib/_http_client.js | 756 +---------- lib/_http_common.js | 370 +----- lib/_http_incoming.js | 329 +---- lib/_http_outgoing.js | 892 +------------ lib/_http_server.js | 686 +--------- lib/_stream_duplex.js | 110 +- lib/_stream_passthrough.js | 45 +- lib/_stream_readable.js | 1058 +-------------- lib/_stream_transform.js | 220 +--- lib/_stream_wrap.js | 5 +- lib/_stream_writable.js | 667 +--------- lib/_tls_common.js | 226 +--- lib/_tls_legacy.js | 956 +------------- lib/_tls_wrap.js | 1154 +---------------- lib/http.js | 12 +- lib/https.js | 6 +- lib/internal/http/agent.js | 362 ++++++ lib/internal/http/client.js | 754 +++++++++++ lib/internal/http/common.js | 368 ++++++ lib/internal/http/incoming.js | 327 +++++ lib/internal/http/outgoing.js | 890 +++++++++++++ lib/internal/http/server.js | 684 ++++++++++ lib/internal/http2/core.js | 2 +- lib/internal/streams/duplex.js | 108 ++ lib/internal/streams/passthrough.js | 43 + lib/internal/streams/readable.js | 1056 +++++++++++++++ lib/internal/streams/transform.js | 218 ++++ lib/internal/streams/wrap.js | 3 + lib/internal/streams/writable.js | 665 ++++++++++ lib/internal/tls/common.js | 224 ++++ lib/internal/tls/legacy.js | 954 ++++++++++++++ lib/internal/tls/wrap.js | 1152 ++++++++++++++++ lib/stream.js | 10 +- lib/tls.js | 15 +- lib/zlib.js | 2 +- node.gyp | 45 +- test/parallel/test-http-agent-keepalive.js | 2 +- test/parallel/test-http-common.js | 2 +- .../parallel/test-http-invalidheaderfield2.js | 5 +- test/parallel/test-outgoing-message-pipe.js | 2 +- test/parallel/test-stream-pipe-after-end.js | 2 +- test/parallel/test-stream-wrap-encoding.js | 2 +- test/parallel/test-stream-wrap.js | 2 +- ...est-stream2-base64-single-char-read-end.js | 2 +- test/parallel/test-stream2-basic.js | 2 +- test/parallel/test-stream2-compatibility.js | 2 +- test/parallel/test-stream2-decode-partial.js | 2 +- test/parallel/test-stream2-objects.js | 2 +- .../test-stream2-readable-from-list.js | 2 +- .../test-stream2-readable-non-empty-end.js | 2 +- .../test-stream2-readable-wrap-empty.js | 2 +- test/parallel/test-stream2-readable-wrap.js | 2 +- test/parallel/test-stream2-set-encoding.js | 2 +- test/parallel/test-stream2-transform.js | 4 +- test/parallel/test-stream2-writable.js | 2 +- test/parallel/test-tls-legacy-onselect.js | 2 +- test/parallel/test-tls-securepair-leak.js | 2 +- .../test-tls-translate-peer-certificate.js | 2 +- test/parallel/test-wrap-js-stream-duplex.js | 2 +- test/sequential/test-http-regr-gh-2928.js | 2 +- 64 files changed, 7975 insertions(+), 7843 deletions(-) create mode 100644 lib/internal/http/agent.js create mode 100644 lib/internal/http/client.js create mode 100644 lib/internal/http/common.js create mode 100644 lib/internal/http/incoming.js create mode 100644 lib/internal/http/outgoing.js create mode 100644 lib/internal/http/server.js create mode 100644 lib/internal/streams/duplex.js create mode 100644 lib/internal/streams/passthrough.js create mode 100644 lib/internal/streams/readable.js create mode 100644 lib/internal/streams/transform.js create mode 100644 lib/internal/streams/wrap.js create mode 100644 lib/internal/streams/writable.js create mode 100644 lib/internal/tls/common.js create mode 100644 lib/internal/tls/legacy.js create mode 100644 lib/internal/tls/wrap.js diff --git a/benchmark/http/check_invalid_header_char.js b/benchmark/http/check_invalid_header_char.js index d71bc6fc607ef5..23baaf9dac0886 100644 --- a/benchmark/http/check_invalid_header_char.js +++ b/benchmark/http/check_invalid_header_char.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common.js'); -const _checkInvalidHeaderChar = require('_http_common')._checkInvalidHeaderChar; +const { _checkInvalidHeaderChar } = require('internal/http/common'); const bench = common.createBenchmark(main, { key: [ diff --git a/benchmark/http/check_is_http_token.js b/benchmark/http/check_is_http_token.js index 92df3445b45c45..7d349d16483c35 100644 --- a/benchmark/http/check_is_http_token.js +++ b/benchmark/http/check_is_http_token.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common.js'); -const _checkIsHttpToken = require('_http_common')._checkIsHttpToken; +const _checkIsHttpToken = require('internal/http/common')._checkIsHttpToken; const bench = common.createBenchmark(main, { key: [ diff --git a/doc/api/deprecations.md b/doc/api/deprecations.md index 8f94f6b248fe82..090f2604bfe1bb 100644 --- a/doc/api/deprecations.md +++ b/doc/api/deprecations.md @@ -737,6 +737,29 @@ Type: Runtime internal mechanics of the `REPLServer` itself, and is therefore not necessary in user space. + +### DEP00XX: `_http_*`, `_stream_*`, `_tls_*` + +Type: Runtime + +| Old | New | +|-----------------------|------------------------| +| `_http_agent` | `http.Agent` | +| `_http_client` | `http.ClientRequest` | +| `_http_common` | `http` (partial) | +| `_http_incoming` | `http.IncomingMessage` | +| `_http_outgoing` | `http.OutgoingMessage` | +| `_http_server` | `http.Server` | +| `_stream_duplex` | `stream.Duplex` | +| `_stream_passthrough` | `stream.PassThrough` | +| `_stream_readable` | `stream.Readable` | +| `_stream_transform` | `stream.Transform` | +| `_stream_wrap` | *N/A* | +| `_stream_writable` | `stream.Writable` | +| `_tls_common` | `tls` (partial) | +| `_tls_legacy` | `tls` | +| `_tls_wrap` | *N/A* | + [`Buffer.allocUnsafeSlow(size)`]: buffer.html#buffer_class_method_buffer_allocunsafeslow_size [`Buffer.from(array)`]: buffer.html#buffer_class_method_buffer_from_array diff --git a/lib/_http_agent.js b/lib/_http_agent.js index 564eab9254387b..78b9f23c93af77 100644 --- a/lib/_http_agent.js +++ b/lib/_http_agent.js @@ -1,362 +1,6 @@ -// 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'; +process.emitWarning( + 'The _http_agent module is deprecated. Please use http', + 'DeprecationWarning', 'DEP00XX'); -const net = require('net'); -const util = require('util'); -const EventEmitter = require('events'); -const debug = util.debuglog('http'); -const { async_id_symbol } = process.binding('async_wrap'); -const { nextTick } = require('internal/process/next_tick'); - -// New Agent code. - -// The largest departure from the previous implementation is that -// an Agent instance holds connections for a variable number of host:ports. -// Surprisingly, this is still API compatible as far as third parties are -// concerned. The only code that really notices the difference is the -// request object. - -// Another departure is that all code related to HTTP parsing is in -// ClientRequest.onSocket(). The Agent is now *strictly* -// concerned with managing a connection pool. - -function Agent(options) { - if (!(this instanceof Agent)) - return new Agent(options); - - EventEmitter.call(this); - - var self = this; - - self.defaultPort = 80; - self.protocol = 'http:'; - - self.options = util._extend({}, options); - - // don't confuse net and make it think that we're connecting to a pipe - self.options.path = null; - self.requests = {}; - self.sockets = {}; - self.freeSockets = {}; - self.keepAliveMsecs = self.options.keepAliveMsecs || 1000; - self.keepAlive = self.options.keepAlive || false; - self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets; - self.maxFreeSockets = self.options.maxFreeSockets || 256; - - self.on('free', function(socket, options) { - var name = self.getName(options); - debug('agent.on(free)', name); - - if (socket.writable && - self.requests[name] && self.requests[name].length) { - self.requests[name].shift().onSocket(socket); - if (self.requests[name].length === 0) { - // don't leak - delete self.requests[name]; - } - } else { - // If there are no pending requests, then put it in - // the freeSockets pool, but only if we're allowed to do so. - var req = socket._httpMessage; - if (req && - req.shouldKeepAlive && - socket.writable && - self.keepAlive) { - var freeSockets = self.freeSockets[name]; - var freeLen = freeSockets ? freeSockets.length : 0; - var count = freeLen; - if (self.sockets[name]) - count += self.sockets[name].length; - - if (count > self.maxSockets || freeLen >= self.maxFreeSockets) { - socket.destroy(); - } else if (self.keepSocketAlive(socket)) { - freeSockets = freeSockets || []; - self.freeSockets[name] = freeSockets; - socket[async_id_symbol] = -1; - socket._httpMessage = null; - self.removeSocket(socket, options); - freeSockets.push(socket); - } else { - // Implementation doesn't want to keep socket alive - socket.destroy(); - } - } else { - socket.destroy(); - } - } - }); -} - -util.inherits(Agent, EventEmitter); - -Agent.defaultMaxSockets = Infinity; - -Agent.prototype.createConnection = net.createConnection; - -// Get the key for a given set of request options -Agent.prototype.getName = function getName(options) { - var name = options.host || 'localhost'; - - name += ':'; - if (options.port) - name += options.port; - - name += ':'; - if (options.localAddress) - name += options.localAddress; - - // Pacify parallel/test-http-agent-getname by only appending - // the ':' when options.family is set. - if (options.family === 4 || options.family === 6) - name += ':' + options.family; - - if (options.socketPath) - name += ':' + options.socketPath; - - return name; -}; - -Agent.prototype.addRequest = function addRequest(req, options, port/*legacy*/, - localAddress/*legacy*/) { - // Legacy API: addRequest(req, host, port, localAddress) - if (typeof options === 'string') { - options = { - host: options, - port, - localAddress - }; - } - - options = util._extend({}, options); - util._extend(options, this.options); - if (options.socketPath) - options.path = options.socketPath; - - if (!options.servername) - options.servername = calculateServerName(options, req); - - var name = this.getName(options); - if (!this.sockets[name]) { - this.sockets[name] = []; - } - - var freeLen = this.freeSockets[name] ? this.freeSockets[name].length : 0; - var sockLen = freeLen + this.sockets[name].length; - - if (freeLen) { - // we have a free socket, so use that. - var socket = this.freeSockets[name].shift(); - // Guard against an uninitialized or user supplied Socket. - if (socket._handle && typeof socket._handle.asyncReset === 'function') { - // Assign the handle a new asyncId and run any init() hooks. - socket._handle.asyncReset(); - socket[async_id_symbol] = socket._handle.getAsyncId(); - } - - // don't leak - if (!this.freeSockets[name].length) - delete this.freeSockets[name]; - - this.reuseSocket(socket, req); - req.onSocket(socket); - this.sockets[name].push(socket); - } else if (sockLen < this.maxSockets) { - debug('call onSocket', sockLen, freeLen); - // If we are under maxSockets create a new one. - this.createSocket(req, options, handleSocketCreation(req, true)); - } else { - debug('wait for socket'); - // We are over limit so we'll add it to the queue. - if (!this.requests[name]) { - this.requests[name] = []; - } - this.requests[name].push(req); - } -}; - -Agent.prototype.createSocket = function createSocket(req, options, cb) { - var self = this; - options = util._extend({}, options); - util._extend(options, self.options); - if (options.socketPath) - options.path = options.socketPath; - - if (!options.servername) - options.servername = calculateServerName(options, req); - - var name = self.getName(options); - options._agentKey = name; - - debug('createConnection', name, options); - options.encoding = null; - var called = false; - const newSocket = self.createConnection(options, oncreate); - if (newSocket) - oncreate(null, newSocket); - - function oncreate(err, s) { - if (called) - return; - called = true; - if (err) - return cb(err); - if (!self.sockets[name]) { - self.sockets[name] = []; - } - self.sockets[name].push(s); - debug('sockets', name, self.sockets[name].length); - installListeners(self, s, options); - cb(null, s); - } -}; - -function calculateServerName(options, req) { - let servername = options.host; - const hostHeader = req.getHeader('host'); - if (hostHeader) { - // abc => abc - // abc:123 => abc - // [::1] => ::1 - // [::1]:123 => ::1 - if (hostHeader.startsWith('[')) { - const index = hostHeader.indexOf(']'); - if (index === -1) { - // Leading '[', but no ']'. Need to do something... - servername = hostHeader; - } else { - servername = hostHeader.substr(1, index - 1); - } - } else { - servername = hostHeader.split(':', 1)[0]; - } - } - return servername; -} - -function installListeners(agent, s, options) { - function onFree() { - debug('CLIENT socket onFree'); - agent.emit('free', s, options); - } - s.on('free', onFree); - - function onClose(err) { - debug('CLIENT socket onClose'); - // This is the only place where sockets get removed from the Agent. - // If you want to remove a socket from the pool, just close it. - // All socket errors end in a close event anyway. - agent.removeSocket(s, options); - } - s.on('close', onClose); - - function onRemove() { - // We need this function for cases like HTTP 'upgrade' - // (defined by WebSockets) where we need to remove a socket from the - // pool because it'll be locked up indefinitely - debug('CLIENT socket onRemove'); - agent.removeSocket(s, options); - s.removeListener('close', onClose); - s.removeListener('free', onFree); - s.removeListener('agentRemove', onRemove); - } - s.on('agentRemove', onRemove); -} - -Agent.prototype.removeSocket = function removeSocket(s, options) { - var name = this.getName(options); - debug('removeSocket', name, 'writable:', s.writable); - var sets = [this.sockets]; - - // If the socket was destroyed, remove it from the free buffers too. - if (!s.writable) - sets.push(this.freeSockets); - - for (var sk = 0; sk < sets.length; sk++) { - var sockets = sets[sk]; - - if (sockets[name]) { - var index = sockets[name].indexOf(s); - if (index !== -1) { - sockets[name].splice(index, 1); - // Don't leak - if (sockets[name].length === 0) - delete sockets[name]; - } - } - } - - if (this.requests[name] && this.requests[name].length) { - debug('removeSocket, have a request, make a socket'); - var req = this.requests[name][0]; - // If we have pending requests and a socket gets closed make a new one - this.createSocket(req, options, handleSocketCreation(req, false)); - } -}; - -Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { - socket.setKeepAlive(true, this.keepAliveMsecs); - socket.unref(); - - return true; -}; - -Agent.prototype.reuseSocket = function reuseSocket(socket, req) { - debug('have free socket'); - socket.ref(); -}; - -Agent.prototype.destroy = function destroy() { - var sets = [this.freeSockets, this.sockets]; - for (var s = 0; s < sets.length; s++) { - var set = sets[s]; - var keys = Object.keys(set); - for (var v = 0; v < keys.length; v++) { - var setName = set[keys[v]]; - for (var n = 0; n < setName.length; n++) { - setName[n].destroy(); - } - } - } -}; - -function handleSocketCreation(request, informRequest) { - return function handleSocketCreation_Inner(err, socket) { - if (err) { - const asyncId = (socket && socket._handle && socket._handle.getAsyncId) ? - socket._handle.getAsyncId() : - null; - nextTick(asyncId, () => request.emit('error', err)); - return; - } - if (informRequest) - request.onSocket(socket); - else - socket.emit('free'); - }; -} - -module.exports = { - Agent, - globalAgent: new Agent() -}; +module.exports = require('internal/http/agent'); diff --git a/lib/_http_client.js b/lib/_http_client.js index fedadedcc2e657..be8c48cad48531 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -1,754 +1,6 @@ -// 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'; +process.emitWarning( + 'The _http_client module is deprecated. Please use http', + 'DeprecationWarning', 'DEP00XX'); -const util = require('util'); -const net = require('net'); -const url = require('url'); -const { HTTPParser } = process.binding('http_parser'); -const assert = require('assert').ok; -const { - _checkIsHttpToken: checkIsHttpToken, - debug, - freeParser, - httpSocketSetup, - parsers -} = require('_http_common'); -const { OutgoingMessage } = require('_http_outgoing'); -const Agent = require('_http_agent'); -const { Buffer } = require('buffer'); -const { urlToOptions, searchParamsSymbol } = require('internal/url'); -const { outHeadersKey } = require('internal/http'); -const { nextTick } = require('internal/process/next_tick'); -const errors = require('internal/errors'); - -// The actual list of disallowed characters in regexp form is more like: -// /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/ -// with an additional rule for ignoring percentage-escaped characters, but -// that's a) hard to capture in a regular expression that performs well, and -// b) possibly too restrictive for real-world usage. So instead we restrict the -// filter to just control characters and spaces. -// -// This function is used in the case of small paths, where manual character code -// checks can greatly outperform the equivalent regexp (tested in V8 5.4). -function isInvalidPath(s) { - var i = 0; - if (s.charCodeAt(0) <= 32) return true; - if (++i >= s.length) return false; - if (s.charCodeAt(1) <= 32) return true; - if (++i >= s.length) return false; - if (s.charCodeAt(2) <= 32) return true; - if (++i >= s.length) return false; - if (s.charCodeAt(3) <= 32) return true; - if (++i >= s.length) return false; - if (s.charCodeAt(4) <= 32) return true; - if (++i >= s.length) return false; - if (s.charCodeAt(5) <= 32) return true; - ++i; - for (; i < s.length; ++i) - if (s.charCodeAt(i) <= 32) return true; - return false; -} - -function validateHost(host, name) { - if (host != null && typeof host !== 'string') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', `options.${name}`, - ['string', 'undefined', 'null'], host); - } - return host; -} - -function ClientRequest(options, cb) { - OutgoingMessage.call(this); - - if (typeof options === 'string') { - options = url.parse(options); - if (!options.hostname) { - throw new errors.Error('ERR_INVALID_DOMAIN_NAME'); - } - } else if (options && options[searchParamsSymbol] && - options[searchParamsSymbol][searchParamsSymbol]) { - // url.URL instance - options = urlToOptions(options); - } else { - options = util._extend({}, options); - } - - var agent = options.agent; - var defaultAgent = options._defaultAgent || Agent.globalAgent; - if (agent === false) { - agent = new defaultAgent.constructor(); - } else if (agent === null || agent === undefined) { - if (typeof options.createConnection !== 'function') { - agent = defaultAgent; - } - // Explicitly pass through this statement as agent will not be used - // when createConnection is provided. - } else if (typeof agent.addRequest !== 'function') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'Agent option', - ['Agent-like object', 'undefined', 'false']); - } - this.agent = agent; - - var protocol = options.protocol || defaultAgent.protocol; - var expectedProtocol = defaultAgent.protocol; - if (this.agent && this.agent.protocol) - expectedProtocol = this.agent.protocol; - - var path; - if (options.path) { - path = '' + options.path; - var invalidPath; - if (path.length <= 39) { // Determined experimentally in V8 5.4 - invalidPath = isInvalidPath(path); - } else { - invalidPath = /[\u0000-\u0020]/.test(path); - } - if (invalidPath) - throw new errors.TypeError('ERR_UNESCAPED_CHARACTERS', 'Request path'); - } - - if (protocol !== expectedProtocol) { - throw new errors.Error('ERR_INVALID_PROTOCOL', protocol, expectedProtocol); - } - - var defaultPort = options.defaultPort || - this.agent && this.agent.defaultPort; - - var port = options.port = options.port || defaultPort || 80; - var host = options.host = validateHost(options.hostname, 'hostname') || - validateHost(options.host, 'host') || 'localhost'; - - var setHost = (options.setHost === undefined); - - this.socketPath = options.socketPath; - this.timeout = options.timeout; - - var method = options.method; - var methodIsString = (typeof method === 'string'); - if (method != null && !methodIsString) { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'method', - 'string', method); - } - - if (methodIsString && method) { - if (!checkIsHttpToken(method)) { - throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Method', method); - } - method = this.method = method.toUpperCase(); - } else { - method = this.method = 'GET'; - } - - this.path = options.path || '/'; - if (cb) { - this.once('response', cb); - } - - var headersArray = Array.isArray(options.headers); - if (!headersArray) { - if (options.headers) { - var keys = Object.keys(options.headers); - for (var i = 0; i < keys.length; i++) { - var key = keys[i]; - this.setHeader(key, options.headers[key]); - } - } - if (host && !this.getHeader('host') && setHost) { - var hostHeader = host; - - // For the Host header, ensure that IPv6 addresses are enclosed - // in square brackets, as defined by URI formatting - // https://tools.ietf.org/html/rfc3986#section-3.2.2 - var posColon = hostHeader.indexOf(':'); - if (posColon !== -1 && - hostHeader.indexOf(':', posColon + 1) !== -1 && - hostHeader.charCodeAt(0) !== 91/*'['*/) { - hostHeader = `[${hostHeader}]`; - } - - if (port && +port !== defaultPort) { - hostHeader += ':' + port; - } - this.setHeader('Host', hostHeader); - } - } - - if (options.auth && !this.getHeader('Authorization')) { - this.setHeader('Authorization', 'Basic ' + - Buffer.from(options.auth).toString('base64')); - } - - if (method === 'GET' || - method === 'HEAD' || - method === 'DELETE' || - method === 'OPTIONS' || - method === 'CONNECT') { - this.useChunkedEncodingByDefault = false; - } else { - this.useChunkedEncodingByDefault = true; - } - - if (headersArray) { - this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', - options.headers); - } else if (this.getHeader('expect')) { - if (this._header) { - throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'render'); - } - - this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', - this[outHeadersKey]); - } - - this._ended = false; - this.res = null; - this.aborted = undefined; - this.timeoutCb = null; - this.upgradeOrConnect = false; - this.parser = null; - this.maxHeadersCount = null; - - var called = false; - - var oncreate = (err, socket) => { - if (called) - return; - called = true; - if (err) { - process.nextTick(() => this.emit('error', err)); - return; - } - this.onSocket(socket); - this._deferToConnect(null, null, () => this._flush()); - }; - - if (this.agent) { - // If there is an agent we should default to Connection:keep-alive, - // but only if the Agent will actually reuse the connection! - // If it's not a keepAlive agent, and the maxSockets==Infinity, then - // there's never a case where this socket will actually be reused - if (!this.agent.keepAlive && !Number.isFinite(this.agent.maxSockets)) { - this._last = true; - this.shouldKeepAlive = false; - } else { - this._last = false; - this.shouldKeepAlive = true; - } - this.agent.addRequest(this, options); - } else { - // No agent, default to Connection:close. - this._last = true; - this.shouldKeepAlive = false; - if (typeof options.createConnection === 'function') { - const newSocket = options.createConnection(options, oncreate); - if (newSocket && !called) { - called = true; - this.onSocket(newSocket); - } else { - return; - } - } else { - debug('CLIENT use net.createConnection', options); - this.onSocket(net.createConnection(options)); - } - } - - this._deferToConnect(null, null, () => this._flush()); -} - -util.inherits(ClientRequest, OutgoingMessage); - - -ClientRequest.prototype._finish = function _finish() { - DTRACE_HTTP_CLIENT_REQUEST(this, this.connection); - LTTNG_HTTP_CLIENT_REQUEST(this, this.connection); - COUNTER_HTTP_CLIENT_REQUEST(); - OutgoingMessage.prototype._finish.call(this); -}; - -ClientRequest.prototype._implicitHeader = function _implicitHeader() { - if (this._header) { - throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'render'); - } - this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', - this[outHeadersKey]); -}; - -ClientRequest.prototype.abort = function abort() { - if (!this.aborted) { - process.nextTick(emitAbortNT.bind(this)); - } - // Mark as aborting so we can avoid sending queued request data - // This is used as a truthy flag elsewhere. The use of Date.now is for - // debugging purposes only. - this.aborted = Date.now(); - - // If we're aborting, we don't care about any more response data. - if (this.res) { - this.res._dump(); - } else { - this.once('response', function(res) { - res._dump(); - }); - } - - // In the event that we don't have a socket, we will pop out of - // the request queue through handling in onSocket. - if (this.socket) { - // in-progress - this.socket.destroy(); - } -}; - - -function emitAbortNT() { - this.emit('abort'); -} - - -function createHangUpError() { - var error = new Error('socket hang up'); - error.code = 'ECONNRESET'; - return error; -} - - -function socketCloseListener() { - var socket = this; - var req = socket._httpMessage; - debug('HTTP socket close'); - - // Pull through final chunk, if anything is buffered. - // the ondata function will handle it properly, and this - // is a no-op if no final chunk remains. - socket.read(); - - // NOTE: It's important to get parser here, because it could be freed by - // the `socketOnData`. - var parser = socket.parser; - if (req.res && req.res.readable) { - // Socket closed before we emitted 'end' below. - req.res.emit('aborted'); - var res = req.res; - res.on('end', function() { - res.emit('close'); - }); - res.push(null); - } else if (!req.res && !req.socket._hadError) { - // This socket error fired before we started to - // receive a response. The error needs to - // fire on the request. - req.socket._hadError = true; - req.emit('error', createHangUpError()); - } - req.emit('close'); - - // Too bad. That output wasn't getting written. - // This is pretty terrible that it doesn't raise an error. - // Fixed better in v0.10 - if (req.output) - req.output.length = 0; - if (req.outputEncodings) - req.outputEncodings.length = 0; - - if (parser) { - parser.finish(); - freeParser(parser, req, socket); - } -} - -function socketErrorListener(err) { - var socket = this; - var req = socket._httpMessage; - debug('SOCKET ERROR:', err.message, err.stack); - - if (req) { - // For Safety. Some additional errors might fire later on - // and we need to make sure we don't double-fire the error event. - req.socket._hadError = true; - req.emit('error', err); - } - - // Handle any pending data - socket.read(); - - var parser = socket.parser; - if (parser) { - parser.finish(); - freeParser(parser, req, socket); - } - - // Ensure that no further data will come out of the socket - socket.removeListener('data', socketOnData); - socket.removeListener('end', socketOnEnd); - socket.destroy(); -} - -function freeSocketErrorListener(err) { - var socket = this; - debug('SOCKET ERROR on FREE socket:', err.message, err.stack); - socket.destroy(); - socket.emit('agentRemove'); -} - -function socketOnEnd() { - var socket = this; - var req = this._httpMessage; - var parser = this.parser; - - if (!req.res && !req.socket._hadError) { - // If we don't have a response then we know that the socket - // ended prematurely and we need to emit an error on the request. - req.socket._hadError = true; - req.emit('error', createHangUpError()); - } - if (parser) { - parser.finish(); - freeParser(parser, req, socket); - } - socket.destroy(); -} - -function socketOnData(d) { - var socket = this; - var req = this._httpMessage; - var parser = this.parser; - - assert(parser && parser.socket === socket); - - var ret = parser.execute(d); - if (ret instanceof Error) { - debug('parse error', ret); - freeParser(parser, req, socket); - socket.destroy(); - req.socket._hadError = true; - req.emit('error', ret); - } else if (parser.incoming && parser.incoming.upgrade) { - // Upgrade or CONNECT - var bytesParsed = ret; - var res = parser.incoming; - req.res = res; - - socket.removeListener('data', socketOnData); - socket.removeListener('end', socketOnEnd); - parser.finish(); - - var bodyHead = d.slice(bytesParsed, d.length); - - var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; - if (req.listenerCount(eventName) > 0) { - req.upgradeOrConnect = true; - - // detach the socket - socket.emit('agentRemove'); - socket.removeListener('close', socketCloseListener); - socket.removeListener('error', socketErrorListener); - - // TODO(isaacs): Need a way to reset a stream to fresh state - // IE, not flowing, and not explicitly paused. - socket._readableState.flowing = null; - - req.emit(eventName, res, socket, bodyHead); - req.emit('close'); - } else { - // Got Upgrade header or CONNECT method, but have no handler. - socket.destroy(); - } - freeParser(parser, req, socket); - } else if (parser.incoming && parser.incoming.complete && - // When the status code is 100 (Continue), the server will - // send a final response after this client sends a request - // body. So, we must not free the parser. - parser.incoming.statusCode !== 100) { - socket.removeListener('data', socketOnData); - socket.removeListener('end', socketOnEnd); - freeParser(parser, req, socket); - } -} - - -// client -function parserOnIncomingClient(res, shouldKeepAlive) { - var socket = this.socket; - var req = socket._httpMessage; - - - // propagate "domain" setting... - if (req.domain && !res.domain) { - debug('setting "res.domain"'); - res.domain = req.domain; - } - - debug('AGENT incoming response!'); - - if (req.res) { - // We already have a response object, this means the server - // sent a double response. - socket.destroy(); - return; - } - req.res = res; - - // Responses to CONNECT request is handled as Upgrade. - if (req.method === 'CONNECT') { - res.upgrade = true; - return 2; // skip body, and the rest - } - - // Responses to HEAD requests are crazy. - // HEAD responses aren't allowed to have an entity-body - // but *can* have a content-length which actually corresponds - // to the content-length of the entity-body had the request - // been a GET. - var isHeadResponse = req.method === 'HEAD'; - debug('AGENT isHeadResponse', isHeadResponse); - - if (res.statusCode === 100) { - // restart the parser, as this is a continue message. - req.res = null; // Clear res so that we don't hit double-responses. - req.emit('continue'); - return true; - } - - if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) { - // Server MUST respond with Connection:keep-alive for us to enable it. - // If we've been upgraded (via WebSockets) we also shouldn't try to - // keep the connection open. - req.shouldKeepAlive = false; - } - - - DTRACE_HTTP_CLIENT_RESPONSE(socket, req); - LTTNG_HTTP_CLIENT_RESPONSE(socket, req); - COUNTER_HTTP_CLIENT_RESPONSE(); - req.res = res; - res.req = req; - - // add our listener first, so that we guarantee socket cleanup - res.on('end', responseOnEnd); - req.on('prefinish', requestOnPrefinish); - var handled = req.emit('response', res); - - // If the user did not listen for the 'response' event, then they - // can't possibly read the data, so we ._dump() it into the void - // so that the socket doesn't hang there in a paused state. - if (!handled) - res._dump(); - - return isHeadResponse; -} - -// client -function responseKeepAlive(res, req) { - var socket = req.socket; - - if (!req.shouldKeepAlive) { - if (socket.writable) { - debug('AGENT socket.destroySoon()'); - if (typeof socket.destroySoon === 'function') - socket.destroySoon(); - else - socket.end(); - } - assert(!socket.writable); - } else { - debug('AGENT socket keep-alive'); - if (req.timeoutCb) { - socket.setTimeout(0, req.timeoutCb); - req.timeoutCb = null; - } - socket.removeListener('close', socketCloseListener); - socket.removeListener('error', socketErrorListener); - socket.once('error', freeSocketErrorListener); - // There are cases where _handle === null. Avoid those. Passing null to - // nextTick() will call initTriggerId() to retrieve the id. - const asyncId = socket._handle ? socket._handle.getAsyncId() : null; - // Mark this socket as available, AFTER user-added end - // handlers have a chance to run. - nextTick(asyncId, emitFreeNT, socket); - } -} - -function responseOnEnd() { - const res = this; - const req = this.req; - - req._ended = true; - if (!req.shouldKeepAlive || req.finished) - responseKeepAlive(res, req); -} - -function requestOnPrefinish() { - const req = this; - const res = this.res; - - if (!req.shouldKeepAlive) - return; - - if (req._ended) - responseKeepAlive(res, req); -} - -function emitFreeNT(socket) { - socket.emit('free'); -} - -function tickOnSocket(req, socket) { - var parser = parsers.alloc(); - req.socket = socket; - req.connection = socket; - parser.reinitialize(HTTPParser.RESPONSE); - parser.socket = socket; - parser.incoming = null; - parser.outgoing = req; - req.parser = parser; - - socket.parser = parser; - socket._httpMessage = req; - - // Setup "drain" propagation. - httpSocketSetup(socket); - - // Propagate headers limit from request object to parser - if (typeof req.maxHeadersCount === 'number') { - parser.maxHeaderPairs = req.maxHeadersCount << 1; - } else { - // Set default value because parser may be reused from FreeList - parser.maxHeaderPairs = 2000; - } - - parser.onIncoming = parserOnIncomingClient; - socket.removeListener('error', freeSocketErrorListener); - socket.on('error', socketErrorListener); - socket.on('data', socketOnData); - socket.on('end', socketOnEnd); - socket.on('close', socketCloseListener); - - if (req.timeout) { - const emitRequestTimeout = () => req.emit('timeout'); - socket.once('timeout', emitRequestTimeout); - req.once('response', (res) => { - res.once('end', () => { - socket.removeListener('timeout', emitRequestTimeout); - }); - }); - } - req.emit('socket', socket); -} - -ClientRequest.prototype.onSocket = function onSocket(socket) { - process.nextTick(onSocketNT, this, socket); -}; - -function onSocketNT(req, socket) { - if (req.aborted) { - // If we were aborted while waiting for a socket, skip the whole thing. - if (!req.agent) { - socket.destroy(); - } else { - socket.emit('free'); - } - } else { - tickOnSocket(req, socket); - } -} - -ClientRequest.prototype._deferToConnect = _deferToConnect; -function _deferToConnect(method, arguments_, cb) { - // This function is for calls that need to happen once the socket is - // connected and writable. It's an important promisy thing for all the socket - // calls that happen either now (when a socket is assigned) or - // in the future (when a socket gets assigned out of the pool and is - // eventually writable). - - const callSocketMethod = () => { - if (method) - this.socket[method].apply(this.socket, arguments_); - - if (typeof cb === 'function') - cb(); - }; - - const onSocket = () => { - if (this.socket.writable) { - callSocketMethod(); - } else { - this.socket.once('connect', callSocketMethod); - } - }; - - if (!this.socket) { - this.once('socket', onSocket); - } else { - onSocket(); - } -} - -ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) { - if (callback) this.once('timeout', callback); - - const emitTimeout = () => this.emit('timeout'); - - if (this.socket && this.socket.writable) { - if (this.timeoutCb) - this.socket.setTimeout(0, this.timeoutCb); - this.timeoutCb = emitTimeout; - this.socket.setTimeout(msecs, emitTimeout); - return this; - } - - // Set timeoutCb so that it'll get cleaned up on request end - this.timeoutCb = emitTimeout; - if (this.socket) { - var sock = this.socket; - this.socket.once('connect', function() { - sock.setTimeout(msecs, emitTimeout); - }); - return this; - } - - this.once('socket', function(sock) { - sock.once('connect', function() { - sock.setTimeout(msecs, emitTimeout); - }); - }); - - return this; -}; - -ClientRequest.prototype.setNoDelay = function setNoDelay(noDelay) { - this._deferToConnect('setNoDelay', [noDelay]); -}; - -ClientRequest.prototype.setSocketKeepAlive = - function setSocketKeepAlive(enable, initialDelay) { - this._deferToConnect('setKeepAlive', [enable, initialDelay]); - }; - -ClientRequest.prototype.clearTimeout = function clearTimeout(cb) { - this.setTimeout(0, cb); -}; - -module.exports = { - ClientRequest -}; +module.exports = require('internal/http/client'); diff --git a/lib/_http_common.js b/lib/_http_common.js index a1fe29217f59d5..008c30d6bd8b39 100644 --- a/lib/_http_common.js +++ b/lib/_http_common.js @@ -1,368 +1,6 @@ -// 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'; +process.emitWarning( + 'The _http_common module is deprecated. Please use http', + 'DeprecationWarning', 'DEP00XX'); -const binding = process.binding('http_parser'); -const { methods, HTTPParser } = binding; - -const FreeList = require('internal/freelist'); -const { ondrain } = require('internal/http'); -const incoming = require('_http_incoming'); -const { emitDestroy } = require('async_hooks'); -const { - IncomingMessage, - readStart, - readStop -} = incoming; - -const debug = require('util').debuglog('http'); - -const kOnHeaders = HTTPParser.kOnHeaders | 0; -const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; -const kOnBody = HTTPParser.kOnBody | 0; -const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0; -const kOnExecute = HTTPParser.kOnExecute | 0; - -// Only called in the slow case where slow means -// that the request headers were either fragmented -// across multiple TCP packets or too large to be -// processed in a single run. This method is also -// called to process trailing HTTP headers. -function parserOnHeaders(headers, url) { - // Once we exceeded headers limit - stop collecting them - if (this.maxHeaderPairs <= 0 || - this._headers.length < this.maxHeaderPairs) { - this._headers = this._headers.concat(headers); - } - this._url += url; -} - -// `headers` and `url` are set only if .onHeaders() has not been called for -// this request. -// `url` is not set for response parsers but that's not applicable here since -// all our parsers are request parsers. -function parserOnHeadersComplete(versionMajor, versionMinor, headers, method, - url, statusCode, statusMessage, upgrade, - shouldKeepAlive) { - var parser = this; - - if (!headers) { - headers = parser._headers; - parser._headers = []; - } - - if (!url) { - url = parser._url; - parser._url = ''; - } - - parser.incoming = new IncomingMessage(parser.socket); - parser.incoming.httpVersionMajor = versionMajor; - parser.incoming.httpVersionMinor = versionMinor; - parser.incoming.httpVersion = versionMajor + '.' + versionMinor; - parser.incoming.url = url; - - var n = headers.length; - - // If parser.maxHeaderPairs <= 0 assume that there's no limit. - if (parser.maxHeaderPairs > 0) - n = Math.min(n, parser.maxHeaderPairs); - - parser.incoming._addHeaderLines(headers, n); - - if (typeof method === 'number') { - // server only - parser.incoming.method = methods[method]; - } else { - // client only - parser.incoming.statusCode = statusCode; - parser.incoming.statusMessage = statusMessage; - } - - if (upgrade && parser.outgoing !== null && !parser.outgoing.upgrading) { - // The client made non-upgrade request, and server is just advertising - // supported protocols. - // - // See RFC7230 Section 6.7 - upgrade = false; - } - - parser.incoming.upgrade = upgrade; - - var skipBody = 0; // response to HEAD or CONNECT - - if (!upgrade) { - // For upgraded connections and CONNECT method request, we'll emit this - // after parser.execute so that we can capture the first part of the new - // protocol. - skipBody = parser.onIncoming(parser.incoming, shouldKeepAlive); - } - - if (typeof skipBody !== 'number') - return skipBody ? 1 : 0; - else - return skipBody; -} - -// XXX This is a mess. -// TODO: http.Parser should be a Writable emits request/response events. -function parserOnBody(b, start, len) { - var parser = this; - var stream = parser.incoming; - - // if the stream has already been removed, then drop it. - if (!stream) - return; - - var socket = stream.socket; - - // pretend this was the result of a stream._read call. - if (len > 0 && !stream._dumped) { - var slice = b.slice(start, start + len); - var ret = stream.push(slice); - if (!ret) - readStop(socket); - } -} - -function parserOnMessageComplete() { - var parser = this; - var stream = parser.incoming; - - if (stream) { - stream.complete = true; - // Emit any trailing headers. - var headers = parser._headers; - if (headers) { - parser.incoming._addHeaderLines(headers, headers.length); - parser._headers = []; - parser._url = ''; - } - - // For emit end event - stream.push(null); - } - - // force to read the next incoming message - readStart(parser.socket); -} - - -var parsers = new FreeList('parsers', 1000, function() { - var parser = new HTTPParser(HTTPParser.REQUEST); - - parser._headers = []; - parser._url = ''; - parser._consumed = false; - - parser.socket = null; - parser.incoming = null; - parser.outgoing = null; - - // Only called in the slow case where slow means - // that the request headers were either fragmented - // across multiple TCP packets or too large to be - // processed in a single run. This method is also - // called to process trailing HTTP headers. - parser[kOnHeaders] = parserOnHeaders; - parser[kOnHeadersComplete] = parserOnHeadersComplete; - parser[kOnBody] = parserOnBody; - parser[kOnMessageComplete] = parserOnMessageComplete; - parser[kOnExecute] = null; - - return parser; -}); - - -// Free the parser and also break any links that it -// might have to any other things. -// TODO: All parser data should be attached to a -// single object, so that it can be easily cleaned -// up by doing `parser.data = {}`, which should -// be done in FreeList.free. `parsers.free(parser)` -// should be all that is needed. -function freeParser(parser, req, socket) { - if (parser) { - parser._headers = []; - parser.onIncoming = null; - if (parser._consumed) - parser.unconsume(); - parser._consumed = false; - if (parser.socket) - parser.socket.parser = null; - parser.socket = null; - parser.incoming = null; - parser.outgoing = null; - parser[kOnExecute] = null; - if (parsers.free(parser) === false) { - parser.close(); - } else { - // Since the Parser destructor isn't going to run the destroy() callbacks - // it needs to be triggered manually. - emitDestroy(parser.getAsyncId()); - } - } - if (req) { - req.parser = null; - } - if (socket) { - socket.parser = null; - } -} - - -function httpSocketSetup(socket) { - socket.removeListener('drain', ondrain); - socket.on('drain', ondrain); -} - -/** - * Verifies that the given val is a valid HTTP token - * per the rules defined in RFC 7230 - * See https://tools.ietf.org/html/rfc7230#section-3.2.6 - * - * Allowed characters in an HTTP token: - * ^_`a-z 94-122 - * A-Z 65-90 - * - 45 - * 0-9 48-57 - * ! 33 - * #$%&' 35-39 - * *+ 42-43 - * . 46 - * | 124 - * ~ 126 - * - * This implementation of checkIsHttpToken() loops over the string instead of - * using a regular expression since the former is up to 180% faster with v8 4.9 - * depending on the string length (the shorter the string, the larger the - * performance difference) - * - * Additionally, checkIsHttpToken() is currently designed to be inlinable by v8, - * so take care when making changes to the implementation so that the source - * code size does not exceed v8's default max_inlined_source_size setting. - **/ -var validTokens = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 - 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 - 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112 - 127 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 ... - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // ... 255 -]; -function checkIsHttpToken(val) { - if (!validTokens[val.charCodeAt(0)]) - return false; - if (val.length < 2) - return true; - if (!validTokens[val.charCodeAt(1)]) - return false; - if (val.length < 3) - return true; - if (!validTokens[val.charCodeAt(2)]) - return false; - if (val.length < 4) - return true; - if (!validTokens[val.charCodeAt(3)]) - return false; - for (var i = 4; i < val.length; ++i) { - if (!validTokens[val.charCodeAt(i)]) - return false; - } - return true; -} - -/** - * True if val contains an invalid field-vchar - * field-value = *( field-content / obs-fold ) - * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] - * field-vchar = VCHAR / obs-text - * - * checkInvalidHeaderChar() is currently designed to be inlinable by v8, - * so take care when making changes to the implementation so that the source - * code size does not exceed v8's default max_inlined_source_size setting. - **/ -var validHdrChars = [ - 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // 0 - 15 - 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32 - 47 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48 - 63 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 80 - 95 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112 - 127 - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 128 ... - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, - 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // ... 255 -]; -function checkInvalidHeaderChar(val) { - val += ''; - if (val.length < 1) - return false; - if (!validHdrChars[val.charCodeAt(0)]) - return true; - if (val.length < 2) - return false; - if (!validHdrChars[val.charCodeAt(1)]) - return true; - if (val.length < 3) - return false; - if (!validHdrChars[val.charCodeAt(2)]) - return true; - if (val.length < 4) - return false; - if (!validHdrChars[val.charCodeAt(3)]) - return true; - for (var i = 4; i < val.length; ++i) { - if (!validHdrChars[val.charCodeAt(i)]) - return true; - } - return false; -} - -module.exports = { - _checkInvalidHeaderChar: checkInvalidHeaderChar, - _checkIsHttpToken: checkIsHttpToken, - chunkExpression: /(?:^|\W)chunked(?:$|\W)/i, - continueExpression: /(?:^|\W)100-continue(?:$|\W)/i, - CRLF: '\r\n', - debug, - freeParser, - httpSocketSetup, - methods, - parsers -}; +module.exports = require('internal/http/common'); diff --git a/lib/_http_incoming.js b/lib/_http_incoming.js index 696fcc3b4ce53d..ea93bd8e50fce3 100644 --- a/lib/_http_incoming.js +++ b/lib/_http_incoming.js @@ -1,327 +1,6 @@ -// 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'; +process.emitWarning( + 'The _http_incoming module is deprecated. Please use http', + 'DeprecationWarning', 'DEP00XX'); -const util = require('util'); -const Stream = require('stream'); - -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) { - Stream.Readable.call(this); - - // Set this to `true` so that stream.Readable won't attempt to read more - // data on `IncomingMessage#push` (see `maybeReadMore` in - // `_stream_readable.js`). This is important for proper tracking of - // `IncomingMessage#_consuming` which is used to dump requests that users - // haven't attempted to read. - this._readableState.readingMore = true; - - this.socket = socket; - this.connection = socket; - - this.httpVersionMajor = null; - this.httpVersionMinor = null; - this.httpVersion = null; - this.complete = false; - this.headers = {}; - this.rawHeaders = []; - this.trailers = {}; - this.rawTrailers = []; - - this.readable = true; - - this.upgrade = null; - - // request (server) only - this.url = ''; - this.method = null; - - // response (client) only - this.statusCode = null; - this.statusMessage = null; - this.client = socket; - - // flag for backwards compatibility grossness. - 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; -} -util.inherits(IncomingMessage, Stream.Readable); - - -IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { - if (callback) - this.on('timeout', callback); - this.socket.setTimeout(msecs); - return this; -}; - - -IncomingMessage.prototype.read = function read(n) { - if (!this._consuming) - this._readableState.readingMore = false; - this._consuming = true; - this.read = Stream.Readable.prototype.read; - return this.read(n); -}; - - -IncomingMessage.prototype._read = function _read(n) { - // 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(error) { - if (this.socket) - this.socket.destroy(error); -}; - - -IncomingMessage.prototype._addHeaderLines = _addHeaderLines; -function _addHeaderLines(headers, n) { - if (headers && headers.length) { - var dest; - if (this.complete) { - this.rawTrailers = headers; - dest = this.trailers; - } else { - this.rawHeaders = headers; - dest = this.headers; - } - - for (var 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. - -// 'array' header list is taken from: -// https://mxr.mozilla.org/mozilla/source/netwerk/protocol/http/src/nsHttpHeaderArray.cpp -function matchKnownFields(field) { - var low = false; - while (true) { - switch (field) { - case 'Content-Type': - case 'content-type': - return 'content-type'; - case 'Content-Length': - case 'content-length': - return 'content-length'; - case 'User-Agent': - case 'user-agent': - return 'user-agent'; - case 'Referer': - case 'referer': - return 'referer'; - case 'Host': - case 'host': - return 'host'; - case 'Authorization': - case 'authorization': - return 'authorization'; - case 'Proxy-Authorization': - case 'proxy-authorization': - return 'proxy-authorization'; - case 'If-Modified-Since': - case 'if-modified-since': - return 'if-modified-since'; - case 'If-Unmodified-Since': - case 'if-unmodified-since': - return 'if-unmodified-since'; - case 'From': - case 'from': - return 'from'; - case 'Location': - case 'location': - return 'location'; - case 'Max-Forwards': - case 'max-forwards': - return 'max-forwards'; - case 'Retry-After': - case 'retry-after': - return 'retry-after'; - case 'ETag': - case 'etag': - return 'etag'; - case 'Last-Modified': - case 'last-modified': - return 'last-modified'; - case 'Server': - case 'server': - return 'server'; - case 'Age': - case 'age': - return 'age'; - case 'Expires': - case 'expires': - return 'expires'; - case 'Set-Cookie': - case 'set-cookie': - return '\u0001'; - case 'Cookie': - case 'cookie': - return '\u0002cookie'; - // The fields below are not used in _addHeaderLine(), but they are common - // headers where we can avoid toLowerCase() if the mixed or lower case - // versions match the first time through. - case 'Transfer-Encoding': - case 'transfer-encoding': - return '\u0000transfer-encoding'; - case 'Date': - case 'date': - return '\u0000date'; - case 'Connection': - case 'connection': - return '\u0000connection'; - case 'Cache-Control': - case 'cache-control': - return '\u0000cache-control'; - case 'Vary': - case 'vary': - return '\u0000vary'; - case 'Content-Encoding': - case 'content-encoding': - return '\u0000content-encoding'; - case 'Origin': - case 'origin': - return '\u0000origin'; - case 'Upgrade': - case 'upgrade': - return '\u0000upgrade'; - case 'Expect': - case 'expect': - return '\u0000expect'; - case 'If-Match': - case 'if-match': - return '\u0000if-match'; - case 'If-None-Match': - case 'if-none-match': - return '\u0000if-none-match'; - case 'Accept': - case 'accept': - return '\u0000accept'; - case 'Accept-Encoding': - case 'accept-encoding': - return '\u0000accept-encoding'; - case 'Accept-Language': - case 'accept-language': - return '\u0000accept-language'; - case 'X-Forwarded-For': - case 'x-forwarded-for': - return '\u0000x-forwarded-for'; - case 'X-Forwarded-Host': - case 'x-forwarded-host': - return '\u0000x-forwarded-host'; - case 'X-Forwarded-Proto': - case 'x-forwarded-proto': - return '\u0000x-forwarded-proto'; - default: - if (low) - return '\u0000' + field; - field = field.toLowerCase(); - low = 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); - var 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 (dest[field] === undefined) { - // Drop duplicates - dest[field] = 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(); - } -}; - -module.exports = { - IncomingMessage, - readStart, - readStop -}; +module.exports = require('internal/http/incoming'); diff --git a/lib/_http_outgoing.js b/lib/_http_outgoing.js index 76d83b19218ceb..336d63380c4a42 100644 --- a/lib/_http_outgoing.js +++ b/lib/_http_outgoing.js @@ -1,890 +1,6 @@ -// 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'; +process.emitWarning( + 'The _http_outgoing module is deprecated. Please use http', + 'DeprecationWarning', 'DEP00XX'); -const assert = require('assert').ok; -const Stream = require('stream'); -const util = require('util'); -const internalUtil = require('internal/util'); -const internalHttp = require('internal/http'); -const { Buffer } = require('buffer'); -const common = require('_http_common'); -const checkIsHttpToken = common._checkIsHttpToken; -const checkInvalidHeaderChar = common._checkInvalidHeaderChar; -const { outHeadersKey } = require('internal/http'); -const { async_id_symbol } = process.binding('async_wrap'); -const { nextTick } = require('internal/process/next_tick'); -const errors = require('internal/errors'); - -const { CRLF, debug } = common; -const { utcDate } = internalHttp; - -var RE_FIELDS = - /^(?:Connection|Transfer-Encoding|Content-Length|Date|Expect|Trailer|Upgrade)$/i; -var RE_CONN_VALUES = /(?:^|\W)close|upgrade(?:$|\W)/ig; -var RE_TE_CHUNKED = common.chunkExpression; - -// isCookieField performs a case-insensitive comparison of a provided string -// against the word "cookie." This method (at least as of V8 5.4) is faster than -// the equivalent case-insensitive regexp, even if isCookieField does not get -// inlined. -function isCookieField(s) { - if (s.length !== 6) return false; - var ch = s.charCodeAt(0); - if (ch !== 99 && ch !== 67) return false; - ch = s.charCodeAt(1); - if (ch !== 111 && ch !== 79) return false; - ch = s.charCodeAt(2); - if (ch !== 111 && ch !== 79) return false; - ch = s.charCodeAt(3); - if (ch !== 107 && ch !== 75) return false; - ch = s.charCodeAt(4); - if (ch !== 105 && ch !== 73) return false; - ch = s.charCodeAt(5); - if (ch !== 101 && ch !== 69) return false; - return true; -} - -function noopPendingOutput(amount) {} - -function OutgoingMessage() { - Stream.call(this); - - // Queue that holds all currently pending data, until the response will be - // assigned to the socket (until it will its turn in the HTTP pipeline). - this.output = []; - this.outputEncodings = []; - this.outputCallbacks = []; - - // `outputSize` is an approximate measure of how much data is queued on this - // response. `_onPendingData` will be invoked to update similar global - // per-connection counter. That counter will be used to pause/unpause the - // TCP socket and HTTP Parser and thus handle the backpressure. - this.outputSize = 0; - - this.writable = true; - - this._last = false; - this.upgrading = false; - this.chunkedEncoding = false; - this.shouldKeepAlive = true; - this.useChunkedEncodingByDefault = true; - this.sendDate = false; - this._removedConnection = false; - this._removedContLen = false; - this._removedTE = false; - - this._contentLength = null; - this._hasBody = true; - this._trailer = ''; - - this.finished = false; - this._headerSent = false; - - this.socket = null; - this.connection = null; - this._header = null; - this[outHeadersKey] = null; - - this._onPendingData = noopPendingOutput; -} -util.inherits(OutgoingMessage, Stream); - - -Object.defineProperty(OutgoingMessage.prototype, '_headers', { - get: function() { - return this.getHeaders(); - }, - set: function(val) { - if (val == null) { - this[outHeadersKey] = null; - } else if (typeof val === 'object') { - const headers = this[outHeadersKey] = {}; - const keys = Object.keys(val); - for (var i = 0; i < keys.length; ++i) { - const name = keys[i]; - headers[name.toLowerCase()] = [name, val[name]]; - } - } - } -}); - -Object.defineProperty(OutgoingMessage.prototype, '_headerNames', { - get: function() { - const headers = this[outHeadersKey]; - if (headers) { - const out = Object.create(null); - const keys = Object.keys(headers); - for (var i = 0; i < keys.length; ++i) { - const key = keys[i]; - const val = headers[key][0]; - out[key] = val; - } - return out; - } else { - return headers; - } - }, - set: function(val) { - if (typeof val === 'object' && val !== null) { - const headers = this[outHeadersKey]; - if (!headers) - return; - const keys = Object.keys(val); - for (var i = 0; i < keys.length; ++i) { - const header = headers[keys[i]]; - if (header) - header[0] = val[keys[i]]; - } - } - } -}); - - -OutgoingMessage.prototype._renderHeaders = function _renderHeaders() { - if (this._header) { - throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'render'); - } - - var headersMap = this[outHeadersKey]; - if (!headersMap) return {}; - - var headers = {}; - var keys = Object.keys(headersMap); - - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - headers[headersMap[key][0]] = headersMap[key][1]; - } - return headers; -}; - - -exports.OutgoingMessage = OutgoingMessage; - - -OutgoingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { - - if (callback) { - this.on('timeout', callback); - } - - if (!this.socket) { - this.once('socket', function(socket) { - socket.setTimeout(msecs); - }); - } else { - this.socket.setTimeout(msecs); - } - return this; -}; - - -// 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. -OutgoingMessage.prototype.destroy = function destroy(error) { - if (this.socket) { - this.socket.destroy(error); - } else { - this.once('socket', function(socket) { - socket.destroy(error); - }); - } -}; - - -// This abstract either writing directly to the socket or buffering it. -OutgoingMessage.prototype._send = function _send(data, encoding, callback) { - // This is a shameful hack to get the headers and first body chunk onto - // the same packet. Future versions of Node are going to take care of - // this at a lower level and in a more general way. - if (!this._headerSent) { - if (typeof data === 'string' && - (encoding === 'utf8' || encoding === 'latin1' || !encoding)) { - data = this._header + data; - } else { - var header = this._header; - if (this.output.length === 0) { - this.output = [header]; - this.outputEncodings = ['latin1']; - this.outputCallbacks = [null]; - } else { - this.output.unshift(header); - this.outputEncodings.unshift('latin1'); - this.outputCallbacks.unshift(null); - } - this.outputSize += header.length; - this._onPendingData(header.length); - } - this._headerSent = true; - } - return this._writeRaw(data, encoding, callback); -}; - - -OutgoingMessage.prototype._writeRaw = _writeRaw; -function _writeRaw(data, encoding, callback) { - const conn = this.connection; - if (conn && conn.destroyed) { - // The socket was destroyed. If we're still trying to write to it, - // then we haven't gotten the 'close' event yet. - return false; - } - - if (typeof encoding === 'function') { - callback = encoding; - encoding = null; - } - - if (conn && conn._httpMessage === this && conn.writable && !conn.destroyed) { - // There might be pending data in the this.output buffer. - if (this.output.length) { - this._flushOutput(conn); - } else if (!data.length) { - if (typeof callback === 'function') { - let socketAsyncId = this.socket[async_id_symbol]; - // If the socket was set directly it won't be correctly initialized - // with an async_id_symbol. - // TODO(AndreasMadsen): @trevnorris suggested some more correct - // solutions in: - // https://github.com/nodejs/node/pull/14389/files#r128522202 - if (socketAsyncId === undefined) socketAsyncId = null; - - nextTick(socketAsyncId, callback); - } - return true; - } - // Directly write to socket. - return conn.write(data, encoding, callback); - } - // Buffer, as long as we're not destroyed. - this.output.push(data); - this.outputEncodings.push(encoding); - this.outputCallbacks.push(callback); - this.outputSize += data.length; - this._onPendingData(data.length); - return false; -} - - -OutgoingMessage.prototype._storeHeader = _storeHeader; -function _storeHeader(firstLine, headers) { - // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n' - // in the case of response it is: 'HTTP/1.1 200 OK\r\n' - var state = { - connection: false, - connUpgrade: false, - contLen: false, - te: false, - date: false, - expect: false, - trailer: false, - upgrade: false, - header: firstLine - }; - - var field; - var key; - var value; - var i; - var j; - if (headers === this[outHeadersKey]) { - for (key in headers) { - var entry = headers[key]; - field = entry[0]; - value = entry[1]; - - if (value instanceof Array) { - if (value.length < 2 || !isCookieField(field)) { - for (j = 0; j < value.length; j++) - storeHeader(this, state, field, value[j], false); - continue; - } - value = value.join('; '); - } - storeHeader(this, state, field, value, false); - } - } else if (headers instanceof Array) { - for (i = 0; i < headers.length; i++) { - field = headers[i][0]; - value = headers[i][1]; - - if (value instanceof Array) { - for (j = 0; j < value.length; j++) { - storeHeader(this, state, field, value[j], true); - } - } else { - storeHeader(this, state, field, value, true); - } - } - } else if (headers) { - var keys = Object.keys(headers); - for (i = 0; i < keys.length; i++) { - field = keys[i]; - value = headers[field]; - - if (value instanceof Array) { - if (value.length < 2 || !isCookieField(field)) { - for (j = 0; j < value.length; j++) - storeHeader(this, state, field, value[j], true); - continue; - } - value = value.join('; '); - } - storeHeader(this, state, field, value, true); - } - } - - // Are we upgrading the connection? - if (state.connUpgrade && state.upgrade) - this.upgrading = true; - - // Date header - if (this.sendDate && !state.date) { - state.header += 'Date: ' + utcDate() + CRLF; - } - - // Force the connection to close when the response is a 204 No Content or - // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked" - // header. - // - // RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but - // node.js used to send out a zero chunk anyway to accommodate clients - // that don't have special handling for those responses. - // - // It was pointed out that this might confuse reverse proxies to the point - // of creating security liabilities, so suppress the zero chunk and force - // the connection to close. - var statusCode = this.statusCode; - if ((statusCode === 204 || statusCode === 304) && this.chunkedEncoding) { - debug(statusCode + ' response should not use chunked encoding,' + - ' closing connection.'); - this.chunkedEncoding = false; - this.shouldKeepAlive = false; - } - - // keep-alive logic - if (this._removedConnection) { - this._last = true; - this.shouldKeepAlive = false; - } else if (!state.connection) { - var shouldSendKeepAlive = this.shouldKeepAlive && - (state.contLen || this.useChunkedEncodingByDefault || this.agent); - if (shouldSendKeepAlive) { - state.header += 'Connection: keep-alive\r\n'; - } else { - this._last = true; - state.header += 'Connection: close\r\n'; - } - } - - if (!state.contLen && !state.te) { - if (!this._hasBody) { - // Make sure we don't end the 0\r\n\r\n at the end of the message. - this.chunkedEncoding = false; - } else if (!this.useChunkedEncodingByDefault) { - this._last = true; - } else if (!state.trailer && - !this._removedContLen && - typeof this._contentLength === 'number') { - state.header += 'Content-Length: ' + this._contentLength + CRLF; - } else if (!this._removedTE) { - state.header += 'Transfer-Encoding: chunked\r\n'; - this.chunkedEncoding = true; - } else { - // We should only be able to get here if both Content-Length and - // Transfer-Encoding are removed by the user. - // See: test/parallel/test-http-remove-header-stays-removed.js - debug('Both Content-Length and Transfer-Encoding are removed'); - } - } - - // Test non-chunked message does not have trailer header set, - // message will be terminated by the first empty line after the - // header fields, regardless of the header fields present in the - // message, and thus cannot contain a message body or 'trailers'. - if (this.chunkedEncoding !== true && state.trailer) { - throw new errors.Error('ERR_HTTP_TRAILER_INVALID'); - } - - this._header = state.header + CRLF; - this._headerSent = false; - - // wait until the first body chunk, or close(), is sent to flush, - // UNLESS we're sending Expect: 100-continue. - if (state.expect) this._send(''); -} - -function storeHeader(self, state, key, value, validate) { - if (validate) { - if (typeof key !== 'string' || !key || !checkIsHttpToken(key)) { - throw new errors.TypeError( - 'ERR_INVALID_HTTP_TOKEN', 'Header name', key); - } - if (value === undefined) { - throw new errors.TypeError('ERR_MISSING_ARGS', `header "${key}"`); - } else if (checkInvalidHeaderChar(value)) { - debug('Header "%s" contains invalid characters', key); - throw new errors.TypeError('ERR_INVALID_CHAR', 'header content', key); - } - } - state.header += key + ': ' + escapeHeaderValue(value) + CRLF; - matchHeader(self, state, key, value); -} - -function matchConnValue(self, state, value) { - var sawClose = false; - var m = RE_CONN_VALUES.exec(value); - while (m) { - if (m[0].length === 5) - sawClose = true; - else - state.connUpgrade = true; - m = RE_CONN_VALUES.exec(value); - } - if (sawClose) - self._last = true; - else - self.shouldKeepAlive = true; -} - -function matchHeader(self, state, field, value) { - var m = RE_FIELDS.exec(field); - if (!m) - return; - var len = m[0].length; - if (len === 10) { - state.connection = true; - matchConnValue(self, state, value); - } else if (len === 17) { - state.te = true; - if (RE_TE_CHUNKED.test(value)) self.chunkedEncoding = true; - } else if (len === 14) { - state.contLen = true; - } else if (len === 4) { - state.date = true; - } else if (len === 6) { - state.expect = true; - } else if (len === 7) { - var ch = m[0].charCodeAt(0); - if (ch === 85 || ch === 117) - state.upgrade = true; - else - state.trailer = true; - } -} - -function validateHeader(msg, name, value) { - if (typeof name !== 'string' || !name || !checkIsHttpToken(name)) - throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Header name', name); - if (value === undefined) - throw new errors.TypeError('ERR_MISSING_ARGS', 'value'); - if (msg._header) - throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'set'); - if (checkInvalidHeaderChar(value)) { - debug('Header "%s" contains invalid characters', name); - throw new errors.TypeError('ERR_INVALID_CHAR', 'header content', name); - } -} -OutgoingMessage.prototype.setHeader = function setHeader(name, value) { - validateHeader(this, name, value); - - if (!this[outHeadersKey]) - this[outHeadersKey] = {}; - - const key = name.toLowerCase(); - this[outHeadersKey][key] = [name, value]; - - switch (key.length) { - case 10: - if (key === 'connection') - this._removedConnection = false; - break; - case 14: - if (key === 'content-length') - this._removedContLen = false; - break; - case 17: - if (key === 'transfer-encoding') - this._removedTE = false; - break; - } -}; - - -OutgoingMessage.prototype.getHeader = function getHeader(name) { - if (typeof name !== 'string') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string'); - } - - if (!this[outHeadersKey]) return; - - var entry = this[outHeadersKey][name.toLowerCase()]; - if (!entry) - return; - return entry[1]; -}; - - -// Returns an array of the names of the current outgoing headers. -OutgoingMessage.prototype.getHeaderNames = function getHeaderNames() { - return (this[outHeadersKey] ? Object.keys(this[outHeadersKey]) : []); -}; - - -// Returns a shallow copy of the current outgoing headers. -OutgoingMessage.prototype.getHeaders = function getHeaders() { - const headers = this[outHeadersKey]; - const ret = Object.create(null); - if (headers) { - const keys = Object.keys(headers); - for (var i = 0; i < keys.length; ++i) { - const key = keys[i]; - const val = headers[key][1]; - ret[key] = val; - } - } - return ret; -}; - - -OutgoingMessage.prototype.hasHeader = function hasHeader(name) { - if (typeof name !== 'string') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string'); - } - - return !!(this[outHeadersKey] && this[outHeadersKey][name.toLowerCase()]); -}; - - -OutgoingMessage.prototype.removeHeader = function removeHeader(name) { - if (typeof name !== 'string') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string'); - } - - if (this._header) { - throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'remove'); - } - - var key = name.toLowerCase(); - - switch (key.length) { - case 10: - if (key === 'connection') - this._removedConnection = true; - break; - case 14: - if (key === 'content-length') - this._removedContLen = true; - break; - case 17: - if (key === 'transfer-encoding') - this._removedTE = true; - break; - case 4: - if (key === 'date') - this.sendDate = false; - break; - } - - if (this[outHeadersKey]) { - delete this[outHeadersKey][key]; - } -}; - - -OutgoingMessage.prototype._implicitHeader = function _implicitHeader() { - throw new errors.Error('ERR_METHOD_NOT_IMPLEMENTED', '_implicitHeader()'); -}; - -Object.defineProperty(OutgoingMessage.prototype, 'headersSent', { - configurable: true, - enumerable: true, - get: function() { return !!this._header; } -}); - - -const crlf_buf = Buffer.from('\r\n'); -OutgoingMessage.prototype.write = function write(chunk, encoding, callback) { - return write_(this, chunk, encoding, callback, false); -}; - -function write_(msg, chunk, encoding, callback, fromEnd) { - if (msg.finished) { - var err = new Error('write after end'); - nextTick(msg.socket && msg.socket[async_id_symbol], - writeAfterEndNT.bind(msg), - err, - callback); - - return true; - } - - if (!msg._header) { - msg._implicitHeader(); - } - - if (!msg._hasBody) { - debug('This type of response MUST NOT have a body. ' + - 'Ignoring write() calls.'); - return true; - } - - if (!fromEnd && typeof chunk !== 'string' && !(chunk instanceof Buffer)) { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'first argument', - ['string', 'buffer']); - } - - - // If we get an empty string or buffer, then just do nothing, and - // signal the user to keep writing. - if (chunk.length === 0) return true; - - if (!fromEnd && msg.connection && !msg.connection.corked) { - msg.connection.cork(); - process.nextTick(connectionCorkNT, msg.connection); - } - - var len, ret; - if (msg.chunkedEncoding) { - if (typeof chunk === 'string') - len = Buffer.byteLength(chunk, encoding); - else - len = chunk.length; - - msg._send(len.toString(16), 'latin1', null); - msg._send(crlf_buf, null, null); - msg._send(chunk, encoding, null); - ret = msg._send(crlf_buf, null, callback); - } else { - ret = msg._send(chunk, encoding, callback); - } - - debug('write ret = ' + ret); - return ret; -} - - -function writeAfterEndNT(err, callback) { - this.emit('error', err); - if (callback) callback(err); -} - - -function connectionCorkNT(conn) { - conn.uncork(); -} - - -function escapeHeaderValue(value) { - // Protect against response splitting. The regex test is there to - // minimize the performance impact in the common case. - return /[\r\n]/.test(value) ? value.replace(/[\r\n]+[ \t]*/g, '') : value; -} - - -OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { - this._trailer = ''; - var keys = Object.keys(headers); - var isArray = Array.isArray(headers); - var field, value; - for (var i = 0, l = keys.length; i < l; i++) { - var key = keys[i]; - if (isArray) { - field = headers[key][0]; - value = headers[key][1]; - } else { - field = key; - value = headers[key]; - } - if (typeof field !== 'string' || !field || !checkIsHttpToken(field)) { - throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Trailer name', - field); - } - if (checkInvalidHeaderChar(value)) { - debug('Trailer "%s" contains invalid characters', field); - throw new errors.TypeError('ERR_INVALID_CHAR', 'trailer content', field); - } - this._trailer += field + ': ' + escapeHeaderValue(value) + CRLF; - } -}; - -function onFinish(outmsg) { - outmsg.emit('finish'); -} - -OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { - if (typeof chunk === 'function') { - callback = chunk; - chunk = null; - } else if (typeof encoding === 'function') { - callback = encoding; - encoding = null; - } - - if (this.finished) { - return false; - } - - var uncork; - if (chunk) { - if (typeof chunk !== 'string' && !(chunk instanceof Buffer)) { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'first argument', - ['string', 'buffer']); - } - if (!this._header) { - if (typeof chunk === 'string') - this._contentLength = Buffer.byteLength(chunk, encoding); - else - this._contentLength = chunk.length; - } - if (this.connection) { - this.connection.cork(); - uncork = true; - } - write_(this, chunk, encoding, null, true); - } else if (!this._header) { - this._contentLength = 0; - this._implicitHeader(); - } - - if (typeof callback === 'function') - this.once('finish', callback); - - var finish = onFinish.bind(undefined, this); - - var ret; - if (this._hasBody && this.chunkedEncoding) { - ret = this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finish); - } else { - // Force a flush, HACK. - ret = this._send('', 'latin1', finish); - } - - if (uncork) - this.connection.uncork(); - - this.finished = true; - - // There is the first message on the outgoing queue, and we've sent - // everything to the socket. - debug('outgoing message end.'); - if (this.output.length === 0 && - this.connection && - this.connection._httpMessage === this) { - this._finish(); - } - - return ret; -}; - - -OutgoingMessage.prototype._finish = function _finish() { - assert(this.connection); - this.emit('prefinish'); -}; - - -// This logic is probably a bit confusing. Let me explain a bit: -// -// In both HTTP servers and clients it is possible to queue up several -// outgoing messages. This is easiest to imagine in the case of a client. -// Take the following situation: -// -// req1 = client.request('GET', '/'); -// req2 = client.request('POST', '/'); -// -// When the user does -// -// req2.write('hello world\n'); -// -// it's possible that the first request has not been completely flushed to -// the socket yet. Thus the outgoing messages need to be prepared to queue -// up data internally before sending it on further to the socket's queue. -// -// This function, outgoingFlush(), is called by both the Server and Client -// to attempt to flush any pending messages out to the socket. -OutgoingMessage.prototype._flush = function _flush() { - var socket = this.socket; - var ret; - - if (socket && socket.writable) { - // There might be remaining data in this.output; write it out - ret = this._flushOutput(socket); - - if (this.finished) { - // This is a queue to the server or client to bring in the next this. - this._finish(); - } else if (ret) { - // This is necessary to prevent https from breaking - this.emit('drain'); - } - } -}; - -OutgoingMessage.prototype._flushOutput = function _flushOutput(socket) { - var ret; - var outputLength = this.output.length; - if (outputLength <= 0) - return ret; - - var output = this.output; - var outputEncodings = this.outputEncodings; - var outputCallbacks = this.outputCallbacks; - socket.cork(); - for (var i = 0; i < outputLength; i++) { - ret = socket.write(output[i], outputEncodings[i], outputCallbacks[i]); - } - socket.uncork(); - - this.output = []; - this.outputEncodings = []; - this.outputCallbacks = []; - this._onPendingData(-this.outputSize); - this.outputSize = 0; - - return ret; -}; - - -OutgoingMessage.prototype.flushHeaders = function flushHeaders() { - if (!this._header) { - this._implicitHeader(); - } - - // Force-flush the headers. - this._send(''); -}; - -OutgoingMessage.prototype.flush = internalUtil.deprecate(function() { - this.flushHeaders(); -}, 'OutgoingMessage.flush is deprecated. Use flushHeaders instead.', 'DEP0001'); - -OutgoingMessage.prototype.pipe = function pipe() { - // OutgoingMessage should be write-only. Piping from it is disabled. - this.emit('error', new Error('Cannot pipe, not readable')); -}; - -module.exports = { - OutgoingMessage -}; +module.exports = require('internal/http/outgoing'); diff --git a/lib/_http_server.js b/lib/_http_server.js index bfbab222264652..b815864db7abc2 100644 --- a/lib/_http_server.js +++ b/lib/_http_server.js @@ -1,684 +1,6 @@ -// 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'; +process.emitWarning( + 'The _http_server module is deprecated. Please use http', + 'DeprecationWarning', 'DEP00XX'); -const util = require('util'); -const net = require('net'); -const { HTTPParser } = process.binding('http_parser'); -const assert = require('assert').ok; -const { - parsers, - freeParser, - debug, - CRLF, - continueExpression, - chunkExpression, - httpSocketSetup, - _checkInvalidHeaderChar: checkInvalidHeaderChar -} = require('_http_common'); -const { OutgoingMessage } = require('_http_outgoing'); -const { outHeadersKey, ondrain } = require('internal/http'); -const errors = require('internal/errors'); -const Buffer = require('buffer').Buffer; - -const STATUS_CODES = { - 100: 'Continue', - 101: 'Switching Protocols', - 102: 'Processing', // RFC 2518, obsoleted by RFC 4918 - 200: 'OK', - 201: 'Created', - 202: 'Accepted', - 203: 'Non-Authoritative Information', - 204: 'No Content', - 205: 'Reset Content', - 206: 'Partial Content', - 207: 'Multi-Status', // RFC 4918 - 208: 'Already Reported', - 226: 'IM Used', - 300: 'Multiple Choices', - 301: 'Moved Permanently', - 302: 'Found', - 303: 'See Other', - 304: 'Not Modified', - 305: 'Use Proxy', - 307: 'Temporary Redirect', - 308: 'Permanent Redirect', // RFC 7238 - 400: 'Bad Request', - 401: 'Unauthorized', - 402: 'Payment Required', - 403: 'Forbidden', - 404: 'Not Found', - 405: 'Method Not Allowed', - 406: 'Not Acceptable', - 407: 'Proxy Authentication Required', - 408: 'Request Timeout', - 409: 'Conflict', - 410: 'Gone', - 411: 'Length Required', - 412: 'Precondition Failed', - 413: 'Payload Too Large', - 414: 'URI Too Long', - 415: 'Unsupported Media Type', - 416: 'Range Not Satisfiable', - 417: 'Expectation Failed', - 418: 'I\'m a teapot', // RFC 2324 - 421: 'Misdirected Request', - 422: 'Unprocessable Entity', // RFC 4918 - 423: 'Locked', // RFC 4918 - 424: 'Failed Dependency', // RFC 4918 - 425: 'Unordered Collection', // RFC 4918 - 426: 'Upgrade Required', // RFC 2817 - 428: 'Precondition Required', // RFC 6585 - 429: 'Too Many Requests', // RFC 6585 - 431: 'Request Header Fields Too Large', // RFC 6585 - 451: 'Unavailable For Legal Reasons', - 500: 'Internal Server Error', - 501: 'Not Implemented', - 502: 'Bad Gateway', - 503: 'Service Unavailable', - 504: 'Gateway Timeout', - 505: 'HTTP Version Not Supported', - 506: 'Variant Also Negotiates', // RFC 2295 - 507: 'Insufficient Storage', // RFC 4918 - 508: 'Loop Detected', - 509: 'Bandwidth Limit Exceeded', - 510: 'Not Extended', // RFC 2774 - 511: 'Network Authentication Required' // RFC 6585 -}; - -const kOnExecute = HTTPParser.kOnExecute | 0; - - -function ServerResponse(req) { - OutgoingMessage.call(this); - - if (req.method === 'HEAD') this._hasBody = false; - - 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; - } -} -util.inherits(ServerResponse, OutgoingMessage); - -ServerResponse.prototype._finish = function _finish() { - DTRACE_HTTP_SERVER_RESPONSE(this.connection); - LTTNG_HTTP_SERVER_RESPONSE(this.connection); - COUNTER_HTTP_SERVER_RESPONSE(); - OutgoingMessage.prototype._finish.call(this); -}; - - -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: - // - // var EventEmitter = require('events'); - // var 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) this._httpMessage.emit('close'); -} - -ServerResponse.prototype.assignSocket = function assignSocket(socket) { - assert(!socket._httpMessage); - socket._httpMessage = this; - socket.on('close', onServerResponseClose); - this.socket = socket; - this.connection = 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 = this.connection = null; -}; - -ServerResponse.prototype.writeContinue = function writeContinue(cb) { - this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii', cb); - this._sent100 = true; -}; - -ServerResponse.prototype._implicitHeader = function _implicitHeader() { - this.writeHead(this.statusCode); -}; - -ServerResponse.prototype.writeHead = writeHead; -function writeHead(statusCode, reason, obj) { - var originalStatusCode = statusCode; - - statusCode |= 0; - if (statusCode < 100 || statusCode > 999) { - throw new errors.RangeError('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; - - var headers; - if (this[outHeadersKey]) { - // Slow-case: when progressive API and header fields are passed. - var k; - if (obj) { - var keys = Object.keys(obj); - for (var i = 0; i < keys.length; i++) { - k = keys[i]; - if (k) this.setHeader(k, obj[k]); - } - } - if (k === undefined && this._header) { - throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'render'); - } - // only progressive api is used - headers = this[outHeadersKey]; - } else { - // only writeHead() called - headers = obj; - } - - if (checkInvalidHeaderChar(this.statusMessage)) - throw new errors.Error('ERR_INVALID_CHAR', 'statusMessage'); - - var statusLine = 'HTTP/1.1 ' + statusCode + ' ' + this.statusMessage + CRLF; - - 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); -} - -// Docs-only deprecated: DEP0063 -ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead; - - -function Server(requestListener) { - if (!(this instanceof Server)) return new Server(requestListener); - net.Server.call(this, { allowHalfOpen: true }); - - 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/ - // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F - this.httpAllowHalfOpen = false; - - this.on('connection', connectionListener); - - this.timeout = 2 * 60 * 1000; - this.keepAliveTimeout = 5000; - this._pendingResponseData = 0; - this.maxHeadersCount = null; -} -util.inherits(Server, net.Server); - - -Server.prototype.setTimeout = function setTimeout(msecs, callback) { - this.timeout = msecs; - if (callback) - this.on('timeout', callback); - return this; -}; - - -function connectionListener(socket) { - debug('SERVER new http connection'); - - httpSocketSetup(socket); - - // Ensure that the server property of the socket is correctly set. - // See https://github.com/nodejs/node/issues/13435 - if (socket.server === null) - socket.server = this; - - // 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 (this.timeout && typeof socket.setTimeout === 'function') - socket.setTimeout(this.timeout); - socket.on('timeout', socketOnTimeout); - - var parser = parsers.alloc(); - parser.reinitialize(HTTPParser.REQUEST); - parser.socket = socket; - socket.parser = parser; - parser.incoming = null; - - // Propagate headers limit from server instance to parser - if (typeof this.maxHeadersCount === 'number') { - parser.maxHeaderPairs = this.maxHeadersCount << 1; - } else { - // Set default value because parser may be reused from FreeList - parser.maxHeaderPairs = 2000; - } - - var 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, - keepAliveTimeoutSet: false - }; - state.onData = socketOnData.bind(undefined, this, socket, parser, state); - state.onEnd = socketOnEnd.bind(undefined, this, 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, this, socket, state); - - // We are consuming socket, so it won't get any actual data - socket.on('resume', onSocketResume); - socket.on('pause', onSocketPause); - - // Override on to unconsume on `data`, `readable` listeners - socket.on = socketOnWrap; - - // We only consume the socket if it has never been consumed before. - if (socket._handle) { - var external = socket._handle._externalStream; - if (!socket._handle._consumed && external) { - parser._consumed = true; - socket._handle._consumed = true; - parser.consume(external); - } - } - parser[kOnExecute] = - onParserExecute.bind(undefined, this, socket, parser, state); - - socket._paused = false; -} - - -function updateOutgoingData(socket, state, delta) { - state.outgoingData += delta; - if (socket._paused && - state.outgoingData < socket._writableState.highWaterMark) { - return socketOnDrain(socket, state); - } -} - -function socketOnDrain(socket, state) { - var needPause = state.outgoingData > socket._writableState.highWaterMark; - - // If we previously paused, then start reading again. - if (socket._paused && !needPause) { - socket._paused = false; - if (socket.parser) - socket.parser.resume(); - socket.resume(); - } -} - -function socketOnTimeout() { - var req = this.parser && this.parser.incoming; - var reqTimeout = req && !req.complete && req.emit('timeout', this); - var res = this._httpMessage; - var resTimeout = res && res.emit('timeout', this); - var serverTimeout = this.server.emit('timeout', this); - - if (!reqTimeout && !resTimeout && !serverTimeout) - this.destroy(); -} - -function socketOnClose(socket, state) { - debug('server socket close'); - // mark this parser as reusable - if (socket.parser) { - freeParser(socket.parser, null, socket); - } - - abortIncoming(state.incoming); -} - -function abortIncoming(incoming) { - while (incoming.length) { - var req = incoming.shift(); - req.emit('aborted'); - req.emit('close'); - } - // abort socket._httpMessage ? -} - -function socketOnEnd(server, socket, parser, state) { - var ret = parser.finish(); - - if (ret instanceof Error) { - debug('parse error'); - socketOnError.call(socket, ret); - return; - } - - if (!server.httpAllowHalfOpen) { - abortIncoming(state.incoming); - if (socket.writable) socket.end(); - } else if (state.outgoing.length) { - state.outgoing[state.outgoing.length - 1]._last = true; - } else if (socket._httpMessage) { - socket._httpMessage._last = true; - } else if (socket.writable) { - socket.end(); - } -} - -function socketOnData(server, socket, parser, state, d) { - assert(!socket._paused); - debug('SERVER socketOnData %d', d.length); - - var ret = parser.execute(d); - onParserExecuteCommon(server, socket, parser, state, ret, d); -} - -function onParserExecute(server, socket, parser, state, ret, d) { - socket._unrefTimer(); - debug('SERVER socketOnParserExecute %d', ret); - onParserExecuteCommon(server, socket, parser, state, ret, undefined); -} - -const badRequestResponse = Buffer.from( - 'HTTP/1.1 400 ' + STATUS_CODES[400] + CRLF + CRLF, 'ascii' -); -function socketOnError(e) { - // Ignore further errors - this.removeListener('error', socketOnError); - this.on('error', () => {}); - - if (!this.server.emit('clientError', e, this)) { - if (this.writable) { - this.end(badRequestResponse); - return; - } - this.destroy(e); - } -} - -function onParserExecuteCommon(server, socket, parser, state, ret, d) { - resetSocketTimeout(server, socket, state); - - if (ret instanceof Error) { - debug('parse error', ret); - socketOnError.call(socket, ret); - } else if (parser.incoming && parser.incoming.upgrade) { - // Upgrade or CONNECT - var bytesParsed = ret; - var 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('drain', ondrain); - unconsume(parser, socket); - parser.finish(); - freeParser(parser, req, null); - parser = null; - - var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; - if (server.listenerCount(eventName) > 0) { - debug('SERVER have listener for %s', eventName); - var bodyHead = d.slice(bytesParsed, d.length); - - // TODO(isaacs): Need a way to reset a stream to fresh state - // IE, not flowing, and not explicitly paused. - socket._readableState.flowing = null; - server.emit(eventName, req, socket, bodyHead); - } else { - // Got upgrade header or CONNECT method, but have no handler. - 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 resOnFinish(req, res, socket, state, 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); - - 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') { - socket.setTimeout(0); - socket.setTimeout(server.keepAliveTimeout); - state.keepAliveTimeoutSet = true; - } - } else { - // start sending the next message - var m = state.outgoing.shift(); - if (m) { - m.assignSocket(socket); - } - } -} - -// 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); - - 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) { - var ws = socket._writableState; - if (ws.needDrain || state.outgoingData >= ws.highWaterMark) { - 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(); - } - } - - var res = new ServerResponse(req); - res._onPendingData = updateOutgoingData.bind(undefined, socket, state); - - res.shouldKeepAlive = keepAlive; - DTRACE_HTTP_SERVER_REQUEST(req, socket); - LTTNG_HTTP_SERVER_REQUEST(req, socket); - COUNTER_HTTP_SERVER_REQUEST(); - - 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)); - - if (req.headers.expect !== undefined && - (req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) { - 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(); - } - } else { - server.emit('request', req, res); - } - return false; // Not a HEAD response. (Not even a response!) -} - -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(socket._handle._externalStream); - parser._consumed = false; - socket.removeListener('pause', onSocketPause); - socket.removeListener('resume', onSocketResume); - } -} - -function socketOnWrap(ev, fn) { - var res = net.Socket.prototype.on.call(this, ev, fn); - if (!this.parser) { - this.on = net.Socket.prototype.on; - return res; - } - - if (ev === 'data' || ev === 'readable') - unconsume(this.parser, this); - - return res; -} - -module.exports = { - STATUS_CODES, - Server, - ServerResponse, - _connectionListener: connectionListener -}; +module.exports = require('internal/http/server'); diff --git a/lib/_stream_duplex.js b/lib/_stream_duplex.js index 7440cd08729e1c..444fde0d06dcd7 100644 --- a/lib/_stream_duplex.js +++ b/lib/_stream_duplex.js @@ -1,108 +1,6 @@ -// 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. - -// a duplex stream is just a stream that is both readable and writable. -// Since JS doesn't have multiple prototypal inheritance, this class -// prototypally inherits from Readable, and then parasitically from -// Writable. - 'use strict'; +process.emitWarning( + 'The _stream_duplex module is deprecated. Please use stream', + 'DeprecationWarning', 'DEP00XX'); -module.exports = Duplex; - -const util = require('util'); -const Readable = require('_stream_readable'); -const Writable = require('_stream_writable'); - -util.inherits(Duplex, Readable); - -var keys = Object.keys(Writable.prototype); -for (var v = 0; v < keys.length; v++) { - var method = keys[v]; - if (!Duplex.prototype[method]) - Duplex.prototype[method] = Writable.prototype[method]; -} - -function Duplex(options) { - if (!(this instanceof Duplex)) - return new Duplex(options); - - Readable.call(this, options); - Writable.call(this, options); - - if (options && options.readable === false) - this.readable = false; - - if (options && options.writable === false) - this.writable = false; - - this.allowHalfOpen = true; - if (options && options.allowHalfOpen === false) - this.allowHalfOpen = false; - - this.once('end', onend); -} - -// the no-half-open enforcer -function onend() { - // if we allow half-open state, or if the writable side ended, - // then we're ok. - if (this.allowHalfOpen || this._writableState.ended) - return; - - // no more data can be written. - // But allow more writes to happen in this tick. - process.nextTick(onEndNT, this); -} - -function onEndNT(self) { - self.end(); -} - -Object.defineProperty(Duplex.prototype, 'destroyed', { - get() { - if (this._readableState === undefined || - this._writableState === undefined) { - return false; - } - return this._readableState.destroyed && this._writableState.destroyed; - }, - set(value) { - // we ignore the value if the stream - // has not been initialized yet - if (this._readableState === undefined || - this._writableState === undefined) { - return; - } - - // backward compatibility, the user is explicitly - // managing destroyed - this._readableState.destroyed = value; - this._writableState.destroyed = value; - } -}); - -Duplex.prototype._destroy = function(err, cb) { - this.push(null); - this.end(); - - process.nextTick(cb, err); -}; +module.exports = require('internal/streams/duplex'); diff --git a/lib/_stream_passthrough.js b/lib/_stream_passthrough.js index 82adaa8d1c7d86..8908048e9062da 100644 --- a/lib/_stream_passthrough.js +++ b/lib/_stream_passthrough.js @@ -1,43 +1,6 @@ -// 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. - -// a passthrough stream. -// basically just the most minimal sort of Transform stream. -// Every written chunk gets output as-is. - 'use strict'; +process.emitWarning( + 'The _stream_passthrough module is deprecated. Please use stream', + 'DeprecationWarning', 'DEP00XX'); -module.exports = PassThrough; - -const Transform = require('_stream_transform'); -const util = require('util'); -util.inherits(PassThrough, Transform); - -function PassThrough(options) { - if (!(this instanceof PassThrough)) - return new PassThrough(options); - - Transform.call(this, options); -} - -PassThrough.prototype._transform = function(chunk, encoding, cb) { - cb(null, chunk); -}; +module.exports = require('internal/streams/passthrough'); diff --git a/lib/_stream_readable.js b/lib/_stream_readable.js index 6427d2e5f6cb49..8245da831b7b33 100644 --- a/lib/_stream_readable.js +++ b/lib/_stream_readable.js @@ -1,1056 +1,6 @@ -// 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'; +process.emitWarning( + 'The _stream_readable module is deprecated. Please use stream', + 'DeprecationWarning', 'DEP00XX'); -module.exports = Readable; -Readable.ReadableState = ReadableState; - -const EE = require('events'); -const Stream = require('stream'); -const { Buffer } = require('buffer'); -const util = require('util'); -const debug = util.debuglog('stream'); -const BufferList = require('internal/streams/BufferList'); -const destroyImpl = require('internal/streams/destroy'); -const errors = require('internal/errors'); -var StringDecoder; - -util.inherits(Readable, Stream); - -const kProxyEvents = ['error', 'close', 'destroy', 'pause', 'resume']; - -function prependListener(emitter, event, fn) { - // Sadly this is not cacheable as some libraries bundle their own - // event emitter implementation with them. - if (typeof emitter.prependListener === 'function') - return emitter.prependListener(event, fn); - - // This is a hack to make sure that our error handler is attached before any - // userland ones. NEVER DO THIS. This is here only because this code needs - // to continue to work with older versions of Node.js that do not include - // the prependListener() method. The goal is to eventually remove this hack. - if (!emitter._events || !emitter._events[event]) - emitter.on(event, fn); - else if (Array.isArray(emitter._events[event])) - emitter._events[event].unshift(fn); - else - emitter._events[event] = [fn, emitter._events[event]]; -} - -function ReadableState(options, stream) { - options = options || {}; - - // Duplex streams are both readable and writable, but share - // the same options object. - // However, some cases require setting options to different - // values for the readable and the writable sides of the duplex stream. - // These options can be provided separately as readableXXX and writableXXX. - var isDuplex = stream instanceof Stream.Duplex; - - // object stream flag. Used to make read(n) ignore n and to - // make all the buffer merging and length checks go away - this.objectMode = !!options.objectMode; - - if (isDuplex) - this.objectMode = this.objectMode || !!options.readableObjectMode; - - // the point at which it stops calling _read() to fill the buffer - // Note: 0 is a valid value, means "don't call _read preemptively ever" - var hwm = options.highWaterMark; - var readableHwm = options.readableHighWaterMark; - var defaultHwm = this.objectMode ? 16 : 16 * 1024; - - if (hwm || hwm === 0) - this.highWaterMark = hwm; - else if (isDuplex && (readableHwm || readableHwm === 0)) - this.highWaterMark = readableHwm; - else - this.highWaterMark = defaultHwm; - - // cast to ints. - this.highWaterMark = Math.floor(this.highWaterMark); - - // A linked list is used to store data chunks instead of an array because the - // linked list can remove elements from the beginning faster than - // array.shift() - this.buffer = new BufferList(); - this.length = 0; - this.pipes = null; - this.pipesCount = 0; - this.flowing = null; - this.ended = false; - this.endEmitted = false; - this.reading = false; - - // a flag to be able to tell if the event 'readable'/'data' is emitted - // immediately, or on a later tick. We set this to true at first, because - // any actions that shouldn't happen until "later" should generally also - // not happen before the first read call. - this.sync = true; - - // whenever we return null, then we set a flag to say - // that we're awaiting a 'readable' event emission. - this.needReadable = false; - this.emittedReadable = false; - this.readableListening = false; - this.resumeScheduled = false; - - // has it been destroyed - this.destroyed = false; - - // Crypto is kind of old and crusty. Historically, its default string - // encoding is 'binary' so we have to make this configurable. - // Everything else in the universe uses 'utf8', though. - this.defaultEncoding = options.defaultEncoding || 'utf8'; - - // the number of writers that are awaiting a drain event in .pipe()s - this.awaitDrain = 0; - - // if true, a maybeReadMore has been scheduled - this.readingMore = false; - - this.decoder = null; - this.encoding = null; - if (options.encoding) { - if (!StringDecoder) - StringDecoder = require('string_decoder').StringDecoder; - this.decoder = new StringDecoder(options.encoding); - this.encoding = options.encoding; - } -} - -function Readable(options) { - if (!(this instanceof Readable)) - return new Readable(options); - - this._readableState = new ReadableState(options, this); - - // legacy - this.readable = true; - - if (options) { - if (typeof options.read === 'function') - this._read = options.read; - - if (typeof options.destroy === 'function') - this._destroy = options.destroy; - } - - Stream.call(this); -} - -Object.defineProperty(Readable.prototype, 'destroyed', { - get() { - if (this._readableState === undefined) { - return false; - } - return this._readableState.destroyed; - }, - set(value) { - // we ignore the value if the stream - // has not been initialized yet - if (!this._readableState) { - return; - } - - // backward compatibility, the user is explicitly - // managing destroyed - this._readableState.destroyed = value; - } -}); - -Readable.prototype.destroy = destroyImpl.destroy; -Readable.prototype._undestroy = destroyImpl.undestroy; -Readable.prototype._destroy = function(err, cb) { - this.push(null); - cb(err); -}; - -// Manually shove something into the read() buffer. -// This returns true if the highWaterMark has not been hit yet, -// similar to how Writable.write() returns true if you should -// write() some more. -Readable.prototype.push = function(chunk, encoding) { - var state = this._readableState; - var skipChunkCheck; - - if (!state.objectMode) { - if (typeof chunk === 'string') { - encoding = encoding || state.defaultEncoding; - if (encoding !== state.encoding) { - chunk = Buffer.from(chunk, encoding); - encoding = ''; - } - skipChunkCheck = true; - } - } else { - skipChunkCheck = true; - } - - return readableAddChunk(this, chunk, encoding, false, skipChunkCheck); -}; - -// Unshift should *always* be something directly out of read() -Readable.prototype.unshift = function(chunk) { - return readableAddChunk(this, chunk, null, true, false); -}; - -function readableAddChunk(stream, chunk, encoding, addToFront, skipChunkCheck) { - var state = stream._readableState; - if (chunk === null) { - state.reading = false; - onEofChunk(stream, state); - } else { - var er; - if (!skipChunkCheck) - er = chunkInvalid(state, chunk); - if (er) { - stream.emit('error', er); - } else if (state.objectMode || chunk && chunk.length > 0) { - if (typeof chunk !== 'string' && - !state.objectMode && - Object.getPrototypeOf(chunk) !== Buffer.prototype) { - chunk = Stream._uint8ArrayToBuffer(chunk); - } - - if (addToFront) { - if (state.endEmitted) - stream.emit('error', - new errors.Error('ERR_STREAM_UNSHIFT_AFTER_END_EVENT')); - else - addChunk(stream, state, chunk, true); - } else if (state.ended) { - stream.emit('error', new errors.Error('ERR_STREAM_PUSH_AFTER_EOF')); - } else { - state.reading = false; - if (state.decoder && !encoding) { - chunk = state.decoder.write(chunk); - if (state.objectMode || chunk.length !== 0) - addChunk(stream, state, chunk, false); - else - maybeReadMore(stream, state); - } else { - addChunk(stream, state, chunk, false); - } - } - } else if (!addToFront) { - state.reading = false; - } - } - - return needMoreData(state); -} - -function addChunk(stream, state, chunk, addToFront) { - if (state.flowing && state.length === 0 && !state.sync) { - stream.emit('data', chunk); - stream.read(0); - } else { - // update the buffer info. - state.length += state.objectMode ? 1 : chunk.length; - if (addToFront) - state.buffer.unshift(chunk); - else - state.buffer.push(chunk); - - if (state.needReadable) - emitReadable(stream); - } - maybeReadMore(stream, state); -} - -function chunkInvalid(state, chunk) { - var er; - if (!Stream._isUint8Array(chunk) && - typeof chunk !== 'string' && - chunk !== undefined && - !state.objectMode) { - er = new errors.TypeError('ERR_INVALID_ARG_TYPE', - 'chunk', 'string/Buffer/Uint8Array'); - } - return er; -} - - -// if it's past the high water mark, we can push in some more. -// Also, if we have no data yet, we can stand some -// more bytes. This is to work around cases where hwm=0, -// such as the repl. Also, if the push() triggered a -// readable event, and the user called read(largeNumber) such that -// needReadable was set, then we ought to push more, so that another -// 'readable' event will be triggered. -function needMoreData(state) { - return !state.ended && - (state.needReadable || - state.length < state.highWaterMark || - state.length === 0); -} - -Readable.prototype.isPaused = function() { - return this._readableState.flowing === false; -}; - -// backwards compatibility. -Readable.prototype.setEncoding = function(enc) { - if (!StringDecoder) - StringDecoder = require('string_decoder').StringDecoder; - this._readableState.decoder = new StringDecoder(enc); - this._readableState.encoding = enc; - return this; -}; - -// Don't raise the hwm > 8MB -const MAX_HWM = 0x800000; -function computeNewHighWaterMark(n) { - if (n >= MAX_HWM) { - n = MAX_HWM; - } else { - // Get the next highest power of 2 to prevent increasing hwm excessively in - // tiny amounts - n--; - n |= n >>> 1; - n |= n >>> 2; - n |= n >>> 4; - n |= n >>> 8; - n |= n >>> 16; - n++; - } - return n; -} - -// This function is designed to be inlinable, so please take care when making -// changes to the function body. -function howMuchToRead(n, state) { - if (n <= 0 || (state.length === 0 && state.ended)) - return 0; - if (state.objectMode) - return 1; - if (n !== n) { - // Only flow one buffer at a time - if (state.flowing && state.length) - return state.buffer.head.data.length; - else - return state.length; - } - // If we're asking for more than the current hwm, then raise the hwm. - if (n > state.highWaterMark) - state.highWaterMark = computeNewHighWaterMark(n); - if (n <= state.length) - return n; - // Don't have enough - if (!state.ended) { - state.needReadable = true; - return 0; - } - return state.length; -} - -// you can override either this method, or the async _read(n) below. -Readable.prototype.read = function(n) { - debug('read', n); - n = parseInt(n, 10); - var state = this._readableState; - var nOrig = n; - - if (n !== 0) - state.emittedReadable = false; - - // if we're doing read(0) to trigger a readable event, but we - // already have a bunch of data in the buffer, then just trigger - // the 'readable' event and move on. - if (n === 0 && - state.needReadable && - (state.length >= state.highWaterMark || state.ended)) { - debug('read: emitReadable', state.length, state.ended); - if (state.length === 0 && state.ended) - endReadable(this); - else - emitReadable(this); - return null; - } - - n = howMuchToRead(n, state); - - // if we've ended, and we're now clear, then finish it up. - if (n === 0 && state.ended) { - if (state.length === 0) - endReadable(this); - return null; - } - - // All the actual chunk generation logic needs to be - // *below* the call to _read. The reason is that in certain - // synthetic stream cases, such as passthrough streams, _read - // may be a completely synchronous operation which may change - // the state of the read buffer, providing enough data when - // before there was *not* enough. - // - // So, the steps are: - // 1. Figure out what the state of things will be after we do - // a read from the buffer. - // - // 2. If that resulting state will trigger a _read, then call _read. - // Note that this may be asynchronous, or synchronous. Yes, it is - // deeply ugly to write APIs this way, but that still doesn't mean - // that the Readable class should behave improperly, as streams are - // designed to be sync/async agnostic. - // Take note if the _read call is sync or async (ie, if the read call - // has returned yet), so that we know whether or not it's safe to emit - // 'readable' etc. - // - // 3. Actually pull the requested chunks out of the buffer and return. - - // if we need a readable event, then we need to do some reading. - var doRead = state.needReadable; - debug('need readable', doRead); - - // if we currently have less than the highWaterMark, then also read some - if (state.length === 0 || state.length - n < state.highWaterMark) { - doRead = true; - debug('length less than watermark', doRead); - } - - // however, if we've ended, then there's no point, and if we're already - // reading, then it's unnecessary. - if (state.ended || state.reading) { - doRead = false; - debug('reading or ended', doRead); - } else if (doRead) { - debug('do read'); - state.reading = true; - state.sync = true; - // if the length is currently zero, then we *need* a readable event. - if (state.length === 0) - state.needReadable = true; - // call internal read method - this._read(state.highWaterMark); - state.sync = false; - // If _read pushed data synchronously, then `reading` will be false, - // and we need to re-evaluate how much data we can return to the user. - if (!state.reading) - n = howMuchToRead(nOrig, state); - } - - var ret; - if (n > 0) - ret = fromList(n, state); - else - ret = null; - - if (ret === null) { - state.needReadable = true; - n = 0; - } else { - state.length -= n; - } - - if (state.length === 0) { - // If we have nothing in the buffer, then we want to know - // as soon as we *do* get something into the buffer. - if (!state.ended) - state.needReadable = true; - - // If we tried to read() past the EOF, then emit end on the next tick. - if (nOrig !== n && state.ended) - endReadable(this); - } - - if (ret !== null) - this.emit('data', ret); - - return ret; -}; - -function onEofChunk(stream, state) { - if (state.ended) return; - if (state.decoder) { - var chunk = state.decoder.end(); - if (chunk && chunk.length) { - state.buffer.push(chunk); - state.length += state.objectMode ? 1 : chunk.length; - } - } - state.ended = true; - - // emit 'readable' now to make sure it gets picked up. - emitReadable(stream); -} - -// Don't emit readable right away in sync mode, because this can trigger -// another read() call => stack overflow. This way, it might trigger -// a nextTick recursion warning, but that's not so bad. -function emitReadable(stream) { - var state = stream._readableState; - state.needReadable = false; - if (!state.emittedReadable) { - debug('emitReadable', state.flowing); - state.emittedReadable = true; - if (state.sync) - process.nextTick(emitReadable_, stream); - else - emitReadable_(stream); - } -} - -function emitReadable_(stream) { - debug('emit readable'); - stream.emit('readable'); - flow(stream); -} - - -// at this point, the user has presumably seen the 'readable' event, -// and called read() to consume some data. that may have triggered -// in turn another _read(n) call, in which case reading = true if -// it's in progress. -// However, if we're not ended, or reading, and the length < hwm, -// then go ahead and try to read some more preemptively. -function maybeReadMore(stream, state) { - if (!state.readingMore) { - state.readingMore = true; - process.nextTick(maybeReadMore_, stream, state); - } -} - -function maybeReadMore_(stream, state) { - var len = state.length; - while (!state.reading && !state.flowing && !state.ended && - state.length < state.highWaterMark) { - debug('maybeReadMore read 0'); - stream.read(0); - if (len === state.length) - // didn't get any data, stop spinning. - break; - else - len = state.length; - } - state.readingMore = false; -} - -// abstract method. to be overridden in specific implementation classes. -// call cb(er, data) where data is <= n in length. -// for virtual (non-string, non-buffer) streams, "length" is somewhat -// arbitrary, and perhaps not very meaningful. -Readable.prototype._read = function(n) { - this.emit('error', new errors.Error('ERR_STREAM_READ_NOT_IMPLEMENTED')); -}; - -Readable.prototype.pipe = function(dest, pipeOpts) { - var src = this; - var state = this._readableState; - - switch (state.pipesCount) { - case 0: - state.pipes = dest; - break; - case 1: - state.pipes = [state.pipes, dest]; - break; - default: - state.pipes.push(dest); - break; - } - state.pipesCount += 1; - debug('pipe count=%d opts=%j', state.pipesCount, pipeOpts); - - var doEnd = (!pipeOpts || pipeOpts.end !== false) && - dest !== process.stdout && - dest !== process.stderr; - - var endFn = doEnd ? onend : unpipe; - if (state.endEmitted) - process.nextTick(endFn); - else - src.once('end', endFn); - - dest.on('unpipe', onunpipe); - function onunpipe(readable, unpipeInfo) { - debug('onunpipe'); - if (readable === src) { - if (unpipeInfo && unpipeInfo.hasUnpiped === false) { - unpipeInfo.hasUnpiped = true; - cleanup(); - } - } - } - - function onend() { - debug('onend'); - dest.end(); - } - - // when the dest drains, it reduces the awaitDrain counter - // on the source. This would be more elegant with a .once() - // handler in flow(), but adding and removing repeatedly is - // too slow. - var ondrain = pipeOnDrain(src); - dest.on('drain', ondrain); - - var cleanedUp = false; - function cleanup() { - debug('cleanup'); - // cleanup event handlers once the pipe is broken - dest.removeListener('close', onclose); - dest.removeListener('finish', onfinish); - dest.removeListener('drain', ondrain); - dest.removeListener('error', onerror); - dest.removeListener('unpipe', onunpipe); - src.removeListener('end', onend); - src.removeListener('end', unpipe); - src.removeListener('data', ondata); - - cleanedUp = true; - - // if the reader is waiting for a drain event from this - // specific writer, then it would cause it to never start - // flowing again. - // So, if this is awaiting a drain, then we just call it now. - // If we don't know, then assume that we are waiting for one. - if (state.awaitDrain && - (!dest._writableState || dest._writableState.needDrain)) - ondrain(); - } - - // If the user pushes more data while we're writing to dest then we'll end up - // in ondata again. However, we only want to increase awaitDrain once because - // dest will only emit one 'drain' event for the multiple writes. - // => Introduce a guard on increasing awaitDrain. - var increasedAwaitDrain = false; - src.on('data', ondata); - function ondata(chunk) { - debug('ondata'); - increasedAwaitDrain = false; - var ret = dest.write(chunk); - if (false === ret && !increasedAwaitDrain) { - // If the user unpiped during `dest.write()`, it is possible - // to get stuck in a permanently paused state if that write - // also returned false. - // => Check whether `dest` is still a piping destination. - if (((state.pipesCount === 1 && state.pipes === dest) || - (state.pipesCount > 1 && state.pipes.indexOf(dest) !== -1)) && - !cleanedUp) { - debug('false write response, pause', src._readableState.awaitDrain); - src._readableState.awaitDrain++; - increasedAwaitDrain = true; - } - src.pause(); - } - } - - // if the dest has an error, then stop piping into it. - // however, don't suppress the throwing behavior for this. - function onerror(er) { - debug('onerror', er); - unpipe(); - dest.removeListener('error', onerror); - if (EE.listenerCount(dest, 'error') === 0) - dest.emit('error', er); - } - - // Make sure our error handler is attached before userland ones. - prependListener(dest, 'error', onerror); - - // Both close and finish should trigger unpipe, but only once. - function onclose() { - dest.removeListener('finish', onfinish); - unpipe(); - } - dest.once('close', onclose); - function onfinish() { - debug('onfinish'); - dest.removeListener('close', onclose); - unpipe(); - } - dest.once('finish', onfinish); - - function unpipe() { - debug('unpipe'); - src.unpipe(dest); - } - - // tell the dest that it's being piped to - dest.emit('pipe', src); - - // start the flow if it hasn't been started already. - if (!state.flowing) { - debug('pipe resume'); - src.resume(); - } - - return dest; -}; - -function pipeOnDrain(src) { - return function() { - var state = src._readableState; - debug('pipeOnDrain', state.awaitDrain); - if (state.awaitDrain) - state.awaitDrain--; - if (state.awaitDrain === 0 && EE.listenerCount(src, 'data')) { - state.flowing = true; - flow(src); - } - }; -} - - -Readable.prototype.unpipe = function(dest) { - var state = this._readableState; - var unpipeInfo = { hasUnpiped: false }; - - // if we're not piping anywhere, then do nothing. - if (state.pipesCount === 0) - return this; - - // just one destination. most common case. - if (state.pipesCount === 1) { - // passed in one, but it's not the right one. - if (dest && dest !== state.pipes) - return this; - - if (!dest) - dest = state.pipes; - - // got a match. - state.pipes = null; - state.pipesCount = 0; - state.flowing = false; - if (dest) - dest.emit('unpipe', this, unpipeInfo); - return this; - } - - // slow case. multiple pipe destinations. - - if (!dest) { - // remove all. - var dests = state.pipes; - var len = state.pipesCount; - state.pipes = null; - state.pipesCount = 0; - state.flowing = false; - - for (var i = 0; i < len; i++) - dests[i].emit('unpipe', this, unpipeInfo); - return this; - } - - // try to find the right one. - var index = state.pipes.indexOf(dest); - if (index === -1) - return this; - - state.pipes.splice(index, 1); - state.pipesCount -= 1; - if (state.pipesCount === 1) - state.pipes = state.pipes[0]; - - dest.emit('unpipe', this, unpipeInfo); - - return this; -}; - -// set up data events if they are asked for -// Ensure readable listeners eventually get something -Readable.prototype.on = function(ev, fn) { - const res = Stream.prototype.on.call(this, ev, fn); - - if (ev === 'data') { - // Start flowing on next tick if stream isn't explicitly paused - if (this._readableState.flowing !== false) - this.resume(); - } else if (ev === 'readable') { - const state = this._readableState; - if (!state.endEmitted && !state.readableListening) { - state.readableListening = state.needReadable = true; - state.emittedReadable = false; - if (!state.reading) { - process.nextTick(nReadingNextTick, this); - } else if (state.length) { - emitReadable(this); - } - } - } - - return res; -}; -Readable.prototype.addListener = Readable.prototype.on; - -function nReadingNextTick(self) { - debug('readable nexttick read 0'); - self.read(0); -} - -// pause() and resume() are remnants of the legacy readable stream API -// If the user uses them, then switch into old mode. -Readable.prototype.resume = function() { - var state = this._readableState; - if (!state.flowing) { - debug('resume'); - state.flowing = true; - resume(this, state); - } - return this; -}; - -function resume(stream, state) { - if (!state.resumeScheduled) { - state.resumeScheduled = true; - process.nextTick(resume_, stream, state); - } -} - -function resume_(stream, state) { - if (!state.reading) { - debug('resume read 0'); - stream.read(0); - } - - state.resumeScheduled = false; - state.awaitDrain = 0; - stream.emit('resume'); - flow(stream); - if (state.flowing && !state.reading) - stream.read(0); -} - -Readable.prototype.pause = function() { - debug('call pause flowing=%j', this._readableState.flowing); - if (false !== this._readableState.flowing) { - debug('pause'); - this._readableState.flowing = false; - this.emit('pause'); - } - return this; -}; - -function flow(stream) { - const state = stream._readableState; - debug('flow', state.flowing); - while (state.flowing && stream.read() !== null); -} - -// wrap an old-style stream as the async data source. -// This is *not* part of the readable stream interface. -// It is an ugly unfortunate mess of history. -Readable.prototype.wrap = function(stream) { - var state = this._readableState; - var paused = false; - - var self = this; - stream.on('end', function() { - debug('wrapped end'); - if (state.decoder && !state.ended) { - var chunk = state.decoder.end(); - if (chunk && chunk.length) - self.push(chunk); - } - - self.push(null); - }); - - stream.on('data', function(chunk) { - debug('wrapped data'); - if (state.decoder) - chunk = state.decoder.write(chunk); - - // don't skip over falsy values in objectMode - if (state.objectMode && (chunk === null || chunk === undefined)) - return; - else if (!state.objectMode && (!chunk || !chunk.length)) - return; - - var ret = self.push(chunk); - if (!ret) { - paused = true; - stream.pause(); - } - }); - - // proxy all the other methods. - // important when wrapping filters and duplexes. - for (var i in stream) { - if (this[i] === undefined && typeof stream[i] === 'function') { - this[i] = function(method) { - return function() { - return stream[method].apply(stream, arguments); - }; - }(i); - } - } - - // proxy certain important events. - for (var n = 0; n < kProxyEvents.length; n++) { - stream.on(kProxyEvents[n], self.emit.bind(self, kProxyEvents[n])); - } - - // when we try to consume some more bytes, simply unpause the - // underlying stream. - self._read = function(n) { - debug('wrapped _read', n); - if (paused) { - paused = false; - stream.resume(); - } - }; - - return self; -}; - - -// exposed for testing purposes only. -Readable._fromList = fromList; - -// Pluck off n bytes from an array of buffers. -// Length is the combined lengths of all the buffers in the list. -// This function is designed to be inlinable, so please take care when making -// changes to the function body. -function fromList(n, state) { - // nothing buffered - if (state.length === 0) - return null; - - var ret; - if (state.objectMode) - ret = state.buffer.shift(); - else if (!n || n >= state.length) { - // read it all, truncate the list - if (state.decoder) - ret = state.buffer.join(''); - else if (state.buffer.length === 1) - ret = state.buffer.head.data; - else - ret = state.buffer.concat(state.length); - state.buffer.clear(); - } else { - // read part of list - ret = fromListPartial(n, state.buffer, state.decoder); - } - - return ret; -} - -// Extracts only enough buffered data to satisfy the amount requested. -// This function is designed to be inlinable, so please take care when making -// changes to the function body. -function fromListPartial(n, list, hasStrings) { - var ret; - if (n < list.head.data.length) { - // slice is the same for buffers and strings - ret = list.head.data.slice(0, n); - list.head.data = list.head.data.slice(n); - } else if (n === list.head.data.length) { - // first chunk is a perfect match - ret = list.shift(); - } else { - // result spans more than one buffer - ret = hasStrings ? copyFromBufferString(n, list) : copyFromBuffer(n, list); - } - return ret; -} - -// Copies a specified amount of characters from the list of buffered data -// chunks. -// This function is designed to be inlinable, so please take care when making -// changes to the function body. -function copyFromBufferString(n, list) { - var p = list.head; - var c = 1; - var ret = p.data; - n -= ret.length; - while (p = p.next) { - const str = p.data; - const nb = (n > str.length ? str.length : n); - if (nb === str.length) - ret += str; - else - ret += str.slice(0, n); - n -= nb; - if (n === 0) { - if (nb === str.length) { - ++c; - if (p.next) - list.head = p.next; - else - list.head = list.tail = null; - } else { - list.head = p; - p.data = str.slice(nb); - } - break; - } - ++c; - } - list.length -= c; - return ret; -} - -// Copies a specified amount of bytes from the list of buffered data chunks. -// This function is designed to be inlinable, so please take care when making -// changes to the function body. -function copyFromBuffer(n, list) { - const ret = Buffer.allocUnsafe(n); - var p = list.head; - var c = 1; - p.data.copy(ret); - n -= p.data.length; - while (p = p.next) { - const buf = p.data; - const nb = (n > buf.length ? buf.length : n); - buf.copy(ret, ret.length - n, 0, nb); - n -= nb; - if (n === 0) { - if (nb === buf.length) { - ++c; - if (p.next) - list.head = p.next; - else - list.head = list.tail = null; - } else { - list.head = p; - p.data = buf.slice(nb); - } - break; - } - ++c; - } - list.length -= c; - return ret; -} - -function endReadable(stream) { - var state = stream._readableState; - - if (!state.endEmitted) { - state.ended = true; - process.nextTick(endReadableNT, state, stream); - } -} - -function endReadableNT(state, stream) { - // Check that we didn't get one last unshift. - if (!state.endEmitted && state.length === 0) { - state.endEmitted = true; - stream.readable = false; - stream.emit('end'); - } -} +module.exports = require('internal/streams/readable'); diff --git a/lib/_stream_transform.js b/lib/_stream_transform.js index b8d4a7704aebdb..1c147ced0a8adc 100644 --- a/lib/_stream_transform.js +++ b/lib/_stream_transform.js @@ -1,218 +1,6 @@ -// 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. - -// a transform stream is a readable/writable stream where you do -// something with the data. Sometimes it's called a "filter", -// but that's not a great name for it, since that implies a thing where -// some bits pass through, and others are simply ignored. (That would -// be a valid example of a transform, of course.) -// -// While the output is causally related to the input, it's not a -// necessarily symmetric or synchronous transformation. For example, -// a zlib stream might take multiple plain-text writes(), and then -// emit a single compressed chunk some time in the future. -// -// Here's how this works: -// -// The Transform stream has all the aspects of the readable and writable -// stream classes. When you write(chunk), that calls _write(chunk,cb) -// internally, and returns false if there's a lot of pending writes -// buffered up. When you call read(), that calls _read(n) until -// there's enough pending readable data buffered up. -// -// In a transform stream, the written data is placed in a buffer. When -// _read(n) is called, it transforms the queued up data, calling the -// buffered _write cb's as it consumes chunks. If consuming a single -// written chunk would result in multiple output chunks, then the first -// outputted bit calls the readcb, and subsequent chunks just go into -// the read buffer, and will cause it to emit 'readable' if necessary. -// -// This way, back-pressure is actually determined by the reading side, -// since _read has to be called to start processing a new chunk. However, -// a pathological inflate type of transform can cause excessive buffering -// here. For example, imagine a stream where every byte of input is -// interpreted as an integer from 0-255, and then results in that many -// bytes of output. Writing the 4 bytes {ff,ff,ff,ff} would result in -// 1kb of data being output. In this case, you could write a very small -// amount of input, and end up with a very large amount of output. In -// such a pathological inflating mechanism, there'd be no way to tell -// the system to stop doing the transform. A single 4MB write could -// cause the system to run out of memory. -// -// However, even in such a pathological case, only a single written chunk -// would be consumed, and then the rest would wait (un-transformed) until -// the results of the previous transformed chunk were consumed. - 'use strict'; +process.emitWarning( + 'The _stream_transform module is deprecated. Please use stream', + 'DeprecationWarning', 'DEP00XX'); -module.exports = Transform; -const errors = require('internal/errors'); -const Duplex = require('_stream_duplex'); -const util = require('util'); -util.inherits(Transform, Duplex); - - -function afterTransform(er, data) { - var ts = this._transformState; - ts.transforming = false; - - var cb = ts.writecb; - - if (cb === null) { - return this.emit('error', new errors.Error('ERR_MULTIPLE_CALLBACK')); - } - - ts.writechunk = null; - ts.writecb = null; - - if (data != null) // single equals check for both `null` and `undefined` - this.push(data); - - cb(er); - - var rs = this._readableState; - rs.reading = false; - if (rs.needReadable || rs.length < rs.highWaterMark) { - this._read(rs.highWaterMark); - } -} - - -function Transform(options) { - if (!(this instanceof Transform)) - return new Transform(options); - - Duplex.call(this, options); - - this._transformState = { - afterTransform: afterTransform.bind(this), - needTransform: false, - transforming: false, - writecb: null, - writechunk: null, - writeencoding: null - }; - - // start out asking for a readable event once data is transformed. - this._readableState.needReadable = true; - - // we have implemented the _read method, and done the other things - // that Readable wants before the first _read call, so unset the - // sync guard flag. - this._readableState.sync = false; - - if (options) { - if (typeof options.transform === 'function') - this._transform = options.transform; - - if (typeof options.flush === 'function') - this._flush = options.flush; - } - - // When the writable side finishes, then flush out anything remaining. - this.on('prefinish', prefinish); -} - -function prefinish() { - if (typeof this._flush === 'function') { - this._flush((er, data) => { - done(this, er, data); - }); - } else { - done(this, null, null); - } -} - -Transform.prototype.push = function(chunk, encoding) { - this._transformState.needTransform = false; - return Duplex.prototype.push.call(this, chunk, encoding); -}; - -// This is the part where you do stuff! -// override this function in implementation classes. -// 'chunk' is an input chunk. -// -// Call `push(newChunk)` to pass along transformed output -// to the readable side. You may call 'push' zero or more times. -// -// Call `cb(err)` when you are done with this chunk. If you pass -// an error, then that'll put the hurt on the whole operation. If you -// never call cb(), then you'll never get another chunk. -Transform.prototype._transform = function(chunk, encoding, cb) { - throw new errors.Error('ERR_METHOD_NOT_IMPLEMENTED', '_transform'); -}; - -Transform.prototype._write = function(chunk, encoding, cb) { - var ts = this._transformState; - ts.writecb = cb; - ts.writechunk = chunk; - ts.writeencoding = encoding; - if (!ts.transforming) { - var rs = this._readableState; - if (ts.needTransform || - rs.needReadable || - rs.length < rs.highWaterMark) - this._read(rs.highWaterMark); - } -}; - -// Doesn't matter what the args are here. -// _transform does all the work. -// That we got here means that the readable side wants more data. -Transform.prototype._read = function(n) { - var ts = this._transformState; - - if (ts.writechunk !== null && ts.writecb && !ts.transforming) { - ts.transforming = true; - this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform); - } else { - // mark that we need a transform, so that any data that comes in - // will get processed, now that we've asked for it. - ts.needTransform = true; - } -}; - - -Transform.prototype._destroy = function(err, cb) { - Duplex.prototype._destroy.call(this, err, (err2) => { - cb(err2); - this.emit('close'); - }); -}; - - -function done(stream, er, data) { - if (er) - return stream.emit('error', er); - - if (data != null) // single equals check for both `null` and `undefined` - stream.push(data); - - // TODO(BridgeAR): Write a test for these two error cases - // if there's nothing in the write buffer, then that means - // that nothing more will ever be provided - if (stream._writableState.length) - throw new errors.Error('ERR_TRANSFORM_WITH_LENGTH_0'); - - if (stream._transformState.transforming) - throw new errors.Error('ERR_TRANSFORM_ALREADY_TRANSFORMING'); - return stream.push(null); -} +module.exports = require('internal/streams/transform'); diff --git a/lib/_stream_wrap.js b/lib/_stream_wrap.js index 10a0cf57e7789e..86ba027053ae4c 100644 --- a/lib/_stream_wrap.js +++ b/lib/_stream_wrap.js @@ -1,3 +1,6 @@ 'use strict'; +process.emitWarning( + 'The _stream_wrap module is deprecated. Please use stream', + 'DeprecationWarning', 'DEP00XX'); -module.exports = require('internal/wrap_js_stream'); +module.exports = require('internal/streams/wrap'); diff --git a/lib/_stream_writable.js b/lib/_stream_writable.js index 13b79233e91bfb..6da993ff0261a0 100644 --- a/lib/_stream_writable.js +++ b/lib/_stream_writable.js @@ -1,665 +1,6 @@ -// 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. - -// A bit simpler than readable streams. -// Implement an async ._write(chunk, encoding, cb), and it'll handle all -// the drain event emission and buffering. - 'use strict'; +process.emitWarning( + 'The _stream_writable module is deprecated. Please use stream', + 'DeprecationWarning', 'DEP00XX'); -module.exports = Writable; -Writable.WritableState = WritableState; - -const util = require('util'); -const internalUtil = require('internal/util'); -const Stream = require('stream'); -const { Buffer } = require('buffer'); -const destroyImpl = require('internal/streams/destroy'); -const errors = require('internal/errors'); - -util.inherits(Writable, Stream); - -function nop() {} - -function WritableState(options, stream) { - options = options || {}; - - // Duplex streams are both readable and writable, but share - // the same options object. - // However, some cases require setting options to different - // values for the readable and the writable sides of the duplex stream. - // These options can be provided separately as readableXXX and writableXXX. - var isDuplex = stream instanceof Stream.Duplex; - - // object stream flag to indicate whether or not this stream - // contains buffers or objects. - this.objectMode = !!options.objectMode; - - if (isDuplex) - this.objectMode = this.objectMode || !!options.writableObjectMode; - - // the point at which write() starts returning false - // Note: 0 is a valid value, means that we always return false if - // the entire buffer is not flushed immediately on write() - var hwm = options.highWaterMark; - var writableHwm = options.writableHighWaterMark; - var defaultHwm = this.objectMode ? 16 : 16 * 1024; - - if (hwm || hwm === 0) - this.highWaterMark = hwm; - else if (isDuplex && (writableHwm || writableHwm === 0)) - this.highWaterMark = writableHwm; - else - this.highWaterMark = defaultHwm; - - // cast to ints. - this.highWaterMark = Math.floor(this.highWaterMark); - - // if _final has been called - this.finalCalled = false; - - // drain event flag. - this.needDrain = false; - // at the start of calling end() - this.ending = false; - // when end() has been called, and returned - this.ended = false; - // when 'finish' is emitted - this.finished = false; - - // has it been destroyed - this.destroyed = false; - - // should we decode strings into buffers before passing to _write? - // this is here so that some node-core streams can optimize string - // handling at a lower level. - var noDecode = options.decodeStrings === false; - this.decodeStrings = !noDecode; - - // Crypto is kind of old and crusty. Historically, its default string - // encoding is 'binary' so we have to make this configurable. - // Everything else in the universe uses 'utf8', though. - this.defaultEncoding = options.defaultEncoding || 'utf8'; - - // not an actual buffer we keep track of, but a measurement - // of how much we're waiting to get pushed to some underlying - // socket or file. - this.length = 0; - - // a flag to see when we're in the middle of a write. - this.writing = false; - - // when true all writes will be buffered until .uncork() call - this.corked = 0; - - // a flag to be able to tell if the onwrite cb is called immediately, - // or on a later tick. We set this to true at first, because any - // actions that shouldn't happen until "later" should generally also - // not happen before the first write call. - this.sync = true; - - // a flag to know if we're processing previously buffered items, which - // may call the _write() callback in the same tick, so that we don't - // end up in an overlapped onwrite situation. - this.bufferProcessing = false; - - // the callback that's passed to _write(chunk,cb) - this.onwrite = onwrite.bind(undefined, stream); - - // the callback that the user supplies to write(chunk,encoding,cb) - this.writecb = null; - - // the amount that is being written when _write is called. - this.writelen = 0; - - this.bufferedRequest = null; - this.lastBufferedRequest = null; - - // number of pending user-supplied write callbacks - // this must be 0 before 'finish' can be emitted - this.pendingcb = 0; - - // emit prefinish if the only thing we're waiting for is _write cbs - // This is relevant for synchronous Transform streams - this.prefinished = false; - - // True if the error was already emitted and should not be thrown again - this.errorEmitted = false; - - // count buffered requests - this.bufferedRequestCount = 0; - - // allocate the first CorkedRequest, there is always - // one allocated and free to use, and we maintain at most two - var corkReq = { next: null, entry: null, finish: undefined }; - corkReq.finish = onCorkedFinish.bind(undefined, corkReq, this); - this.corkedRequestsFree = corkReq; -} - -WritableState.prototype.getBuffer = function getBuffer() { - var current = this.bufferedRequest; - var out = []; - while (current) { - out.push(current); - current = current.next; - } - return out; -}; - -Object.defineProperty(WritableState.prototype, 'buffer', { - get: internalUtil.deprecate(function() { - return this.getBuffer(); - }, '_writableState.buffer is deprecated. Use _writableState.getBuffer ' + - 'instead.', 'DEP0003') -}); - -// Test _writableState for inheritance to account for Duplex streams, -// whose prototype chain only points to Readable. -var realHasInstance; -if (typeof Symbol === 'function' && Symbol.hasInstance) { - realHasInstance = Function.prototype[Symbol.hasInstance]; - Object.defineProperty(Writable, Symbol.hasInstance, { - value: function(object) { - if (realHasInstance.call(this, object)) - return true; - if (this !== Writable) - return false; - - return object && object._writableState instanceof WritableState; - } - }); -} else { - realHasInstance = function(object) { - return object instanceof this; - }; -} - -function Writable(options) { - // Writable ctor is applied to Duplexes, too. - // `realHasInstance` is necessary because using plain `instanceof` - // would return false, as no `_writableState` property is attached. - - // Trying to use the custom `instanceof` for Writable here will also break the - // Node.js LazyTransform implementation, which has a non-trivial getter for - // `_writableState` that would lead to infinite recursion. - if (!(realHasInstance.call(Writable, this)) && - !(this instanceof Stream.Duplex)) { - return new Writable(options); - } - - this._writableState = new WritableState(options, this); - - // legacy. - this.writable = true; - - if (options) { - if (typeof options.write === 'function') - this._write = options.write; - - if (typeof options.writev === 'function') - this._writev = options.writev; - - if (typeof options.destroy === 'function') - this._destroy = options.destroy; - - if (typeof options.final === 'function') - this._final = options.final; - } - - Stream.call(this); -} - -// Otherwise people can pipe Writable streams, which is just wrong. -Writable.prototype.pipe = function() { - this.emit('error', new errors.Error('ERR_STREAM_CANNOT_PIPE')); -}; - - -function writeAfterEnd(stream, cb) { - var er = new errors.Error('ERR_STREAM_WRITE_AFTER_END'); - // TODO: defer error events consistently everywhere, not just the cb - stream.emit('error', er); - process.nextTick(cb, er); -} - -// Checks that a user-supplied chunk is valid, especially for the particular -// mode the stream is in. Currently this means that `null` is never accepted -// and undefined/non-string values are only allowed in object mode. -function validChunk(stream, state, chunk, cb) { - var valid = true; - var er = false; - - if (chunk === null) { - er = new errors.TypeError('ERR_STREAM_NULL_VALUES'); - } else if (typeof chunk !== 'string' && - chunk !== undefined && - !state.objectMode) { - er = new errors.TypeError('ERR_INVALID_ARG_TYPE', 'chunk', 'string/buffer'); - } - if (er) { - stream.emit('error', er); - process.nextTick(cb, er); - valid = false; - } - return valid; -} - -Writable.prototype.write = function(chunk, encoding, cb) { - var state = this._writableState; - var ret = false; - var isBuf = !state.objectMode && Stream._isUint8Array(chunk); - - if (isBuf && Object.getPrototypeOf(chunk) !== Buffer.prototype) { - chunk = Stream._uint8ArrayToBuffer(chunk); - } - - if (typeof encoding === 'function') { - cb = encoding; - encoding = null; - } - - if (isBuf) - encoding = 'buffer'; - else if (!encoding) - encoding = state.defaultEncoding; - - if (typeof cb !== 'function') - cb = nop; - - if (state.ended) - writeAfterEnd(this, cb); - else if (isBuf || validChunk(this, state, chunk, cb)) { - state.pendingcb++; - ret = writeOrBuffer(this, state, isBuf, chunk, encoding, cb); - } - - return ret; -}; - -Writable.prototype.cork = function() { - var state = this._writableState; - - state.corked++; -}; - -Writable.prototype.uncork = function() { - var state = this._writableState; - - if (state.corked) { - state.corked--; - - if (!state.writing && - !state.corked && - !state.finished && - !state.bufferProcessing && - state.bufferedRequest) - clearBuffer(this, state); - } -}; - -Writable.prototype.setDefaultEncoding = function setDefaultEncoding(encoding) { - // node::ParseEncoding() requires lower case. - if (typeof encoding === 'string') - encoding = encoding.toLowerCase(); - if (!Buffer.isEncoding(encoding)) - throw new errors.TypeError('ERR_UNKNOWN_ENCODING', encoding); - this._writableState.defaultEncoding = encoding; - return this; -}; - -function decodeChunk(state, chunk, encoding) { - if (!state.objectMode && - state.decodeStrings !== false && - typeof chunk === 'string') { - chunk = Buffer.from(chunk, encoding); - } - return chunk; -} - -// if we're already writing something, then just put this -// in the queue, and wait our turn. Otherwise, call _write -// If we return false, then we need a drain event, so set that flag. -function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) { - if (!isBuf) { - var newChunk = decodeChunk(state, chunk, encoding); - if (chunk !== newChunk) { - isBuf = true; - encoding = 'buffer'; - chunk = newChunk; - } - } - var len = state.objectMode ? 1 : chunk.length; - - state.length += len; - - var ret = state.length < state.highWaterMark; - // we must ensure that previous needDrain will not be reset to false. - if (!ret) - state.needDrain = true; - - if (state.writing || state.corked) { - var last = state.lastBufferedRequest; - state.lastBufferedRequest = { - chunk, - encoding, - isBuf, - callback: cb, - next: null - }; - if (last) { - last.next = state.lastBufferedRequest; - } else { - state.bufferedRequest = state.lastBufferedRequest; - } - state.bufferedRequestCount += 1; - } else { - doWrite(stream, state, false, len, chunk, encoding, cb); - } - - return ret; -} - -function doWrite(stream, state, writev, len, chunk, encoding, cb) { - state.writelen = len; - state.writecb = cb; - state.writing = true; - state.sync = true; - if (writev) - stream._writev(chunk, state.onwrite); - else - stream._write(chunk, encoding, state.onwrite); - state.sync = false; -} - -function onwriteError(stream, state, sync, er, cb) { - --state.pendingcb; - - if (sync) { - // defer the callback if we are being called synchronously - // to avoid piling up things on the stack - process.nextTick(cb, er); - // this can emit finish, and it will always happen - // after error - process.nextTick(finishMaybe, stream, state); - stream._writableState.errorEmitted = true; - stream.emit('error', er); - } else { - // the caller expect this to happen before if - // it is async - cb(er); - stream._writableState.errorEmitted = true; - stream.emit('error', er); - // this can emit finish, but finish must - // always follow error - finishMaybe(stream, state); - } -} - -function onwriteStateUpdate(state) { - state.writing = false; - state.writecb = null; - state.length -= state.writelen; - state.writelen = 0; -} - -function onwrite(stream, er) { - var state = stream._writableState; - var sync = state.sync; - var cb = state.writecb; - - onwriteStateUpdate(state); - - if (er) - onwriteError(stream, state, sync, er, cb); - else { - // Check if we're actually ready to finish, but don't emit yet - var finished = needFinish(state); - - if (!finished && - !state.corked && - !state.bufferProcessing && - state.bufferedRequest) { - clearBuffer(stream, state); - } - - if (sync) { - process.nextTick(afterWrite, stream, state, finished, cb); - } else { - afterWrite(stream, state, finished, cb); - } - } -} - -function afterWrite(stream, state, finished, cb) { - if (!finished) - onwriteDrain(stream, state); - state.pendingcb--; - cb(); - finishMaybe(stream, state); -} - -// Must force callback to be called on nextTick, so that we don't -// emit 'drain' before the write() consumer gets the 'false' return -// value, and has a chance to attach a 'drain' listener. -function onwriteDrain(stream, state) { - if (state.length === 0 && state.needDrain) { - state.needDrain = false; - stream.emit('drain'); - } -} - -// if there's something in the buffer waiting, then process it -function clearBuffer(stream, state) { - state.bufferProcessing = true; - var entry = state.bufferedRequest; - - if (stream._writev && entry && entry.next) { - // Fast case, write everything using _writev() - var l = state.bufferedRequestCount; - var buffer = new Array(l); - var holder = state.corkedRequestsFree; - holder.entry = entry; - - var count = 0; - var allBuffers = true; - while (entry) { - buffer[count] = entry; - if (!entry.isBuf) - allBuffers = false; - entry = entry.next; - count += 1; - } - buffer.allBuffers = allBuffers; - - doWrite(stream, state, true, state.length, buffer, '', holder.finish); - - // doWrite is almost always async, defer these to save a bit of time - // as the hot path ends with doWrite - state.pendingcb++; - state.lastBufferedRequest = null; - if (holder.next) { - state.corkedRequestsFree = holder.next; - holder.next = null; - } else { - var corkReq = { next: null, entry: null, finish: undefined }; - corkReq.finish = onCorkedFinish.bind(undefined, corkReq, state); - state.corkedRequestsFree = corkReq; - } - state.bufferedRequestCount = 0; - } else { - // Slow case, write chunks one-by-one - while (entry) { - var chunk = entry.chunk; - var encoding = entry.encoding; - var cb = entry.callback; - var len = state.objectMode ? 1 : chunk.length; - - doWrite(stream, state, false, len, chunk, encoding, cb); - entry = entry.next; - state.bufferedRequestCount--; - // if we didn't call the onwrite immediately, then - // it means that we need to wait until it does. - // also, that means that the chunk and cb are currently - // being processed, so move the buffer counter past them. - if (state.writing) { - break; - } - } - - if (entry === null) - state.lastBufferedRequest = null; - } - - state.bufferedRequest = entry; - state.bufferProcessing = false; -} - -Writable.prototype._write = function(chunk, encoding, cb) { - cb(new errors.Error('ERR_METHOD_NOT_IMPLEMENTED', '_transform')); -}; - -Writable.prototype._writev = null; - -Writable.prototype.end = function(chunk, encoding, cb) { - var state = this._writableState; - - if (typeof chunk === 'function') { - cb = chunk; - chunk = null; - encoding = null; - } else if (typeof encoding === 'function') { - cb = encoding; - encoding = null; - } - - if (chunk !== null && chunk !== undefined) - this.write(chunk, encoding); - - // .end() fully uncorks - if (state.corked) { - state.corked = 1; - this.uncork(); - } - - // ignore unnecessary end() calls. - if (!state.ending && !state.finished) - endWritable(this, state, cb); -}; - - -function needFinish(state) { - return (state.ending && - state.length === 0 && - state.bufferedRequest === null && - !state.finished && - !state.writing); -} -function callFinal(stream, state) { - stream._final((err) => { - state.pendingcb--; - if (err) { - stream.emit('error', err); - } - state.prefinished = true; - stream.emit('prefinish'); - finishMaybe(stream, state); - }); -} -function prefinish(stream, state) { - if (!state.prefinished && !state.finalCalled) { - if (typeof stream._final === 'function') { - state.pendingcb++; - state.finalCalled = true; - process.nextTick(callFinal, stream, state); - } else { - state.prefinished = true; - stream.emit('prefinish'); - } - } -} - -function finishMaybe(stream, state) { - var need = needFinish(state); - if (need) { - prefinish(stream, state); - if (state.pendingcb === 0) { - state.finished = true; - stream.emit('finish'); - } - } - return need; -} - -function endWritable(stream, state, cb) { - state.ending = true; - finishMaybe(stream, state); - if (cb) { - if (state.finished) - process.nextTick(cb); - else - stream.once('finish', cb); - } - state.ended = true; - stream.writable = false; -} - -function onCorkedFinish(corkReq, state, err) { - var entry = corkReq.entry; - corkReq.entry = null; - while (entry) { - var cb = entry.callback; - state.pendingcb--; - cb(err); - entry = entry.next; - } - if (state.corkedRequestsFree) { - state.corkedRequestsFree.next = corkReq; - } else { - state.corkedRequestsFree = corkReq; - } -} - -Object.defineProperty(Writable.prototype, 'destroyed', { - get() { - if (this._writableState === undefined) { - return false; - } - return this._writableState.destroyed; - }, - set(value) { - // we ignore the value if the stream - // has not been initialized yet - if (!this._writableState) { - return; - } - - // backward compatibility, the user is explicitly - // managing destroyed - this._writableState.destroyed = value; - } -}); - -Writable.prototype.destroy = destroyImpl.destroy; -Writable.prototype._undestroy = destroyImpl.undestroy; -Writable.prototype._destroy = function(err, cb) { - this.end(); - cb(err); -}; +module.exports = require('internal/streams/writable'); diff --git a/lib/_tls_common.js b/lib/_tls_common.js index 4196cc084c86c4..fd8964bf632741 100644 --- a/lib/_tls_common.js +++ b/lib/_tls_common.js @@ -1,224 +1,6 @@ -// 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'; +process.emitWarning( + 'The _tls_common module is deprecated. Please use tls', + 'DeprecationWarning', 'DEP00XX'); -const { parseCertString } = require('internal/tls'); -const { isArrayBufferView } = require('internal/util/types'); -const tls = require('tls'); -const errors = require('internal/errors'); - -const { SSL_OP_CIPHER_SERVER_PREFERENCE } = process.binding('constants').crypto; - -// Lazily loaded -var crypto = null; - -const binding = process.binding('crypto'); -const NativeSecureContext = binding.SecureContext; - -function SecureContext(secureProtocol, secureOptions, context) { - if (!(this instanceof SecureContext)) { - return new SecureContext(secureProtocol, secureOptions, context); - } - - if (context) { - this.context = context; - } else { - this.context = new NativeSecureContext(); - - if (secureProtocol) { - this.context.init(secureProtocol); - } else { - this.context.init(); - } - } - - if (secureOptions) this.context.setOptions(secureOptions); -} - -function validateKeyCert(value, type) { - if (typeof value !== 'string' && !isArrayBufferView(value)) - throw new errors.TypeError( - 'ERR_INVALID_ARG_TYPE', type, - ['string', 'Buffer', 'TypedArray', 'DataView'] - ); -} - -exports.SecureContext = SecureContext; - - -exports.createSecureContext = function createSecureContext(options, context) { - if (!options) options = {}; - - var secureOptions = options.secureOptions; - if (options.honorCipherOrder) - secureOptions |= SSL_OP_CIPHER_SERVER_PREFERENCE; - - var c = new SecureContext(options.secureProtocol, secureOptions, context); - var i; - var val; - - if (context) return c; - - // NOTE: It's important to add CA before the cert to be able to load - // cert's issuer in C++ code. - var ca = options.ca; - if (ca) { - if (Array.isArray(ca)) { - for (i = 0; i < ca.length; ++i) { - val = ca[i]; - validateKeyCert(val, 'ca'); - c.context.addCACert(val); - } - } else { - validateKeyCert(ca, 'ca'); - c.context.addCACert(ca); - } - } else { - c.context.addRootCerts(); - } - - var cert = options.cert; - if (cert) { - if (Array.isArray(cert)) { - for (i = 0; i < cert.length; ++i) { - val = cert[i]; - validateKeyCert(val, 'cert'); - c.context.setCert(val); - } - } else { - validateKeyCert(cert, 'cert'); - c.context.setCert(cert); - } - } - - // NOTE: It is important to set the key after the cert. - // `ssl_set_pkey` returns `0` when the key does not match the cert, but - // `ssl_set_cert` returns `1` and nullifies the key in the SSL structure - // which leads to the crash later on. - var key = options.key; - var passphrase = options.passphrase; - if (key) { - if (Array.isArray(key)) { - for (i = 0; i < key.length; ++i) { - val = key[i]; - // eslint-disable-next-line eqeqeq - const pem = (val != undefined && val.pem !== undefined ? val.pem : val); - validateKeyCert(pem, 'key'); - c.context.setKey(pem, val.passphrase || passphrase); - } - } else { - validateKeyCert(key, 'key'); - c.context.setKey(key, passphrase); - } - } - - if (options.ciphers) - c.context.setCiphers(options.ciphers); - else - c.context.setCiphers(tls.DEFAULT_CIPHERS); - - if (options.ecdhCurve === undefined) - c.context.setECDHCurve(tls.DEFAULT_ECDH_CURVE); - else if (options.ecdhCurve) - c.context.setECDHCurve(options.ecdhCurve); - - if (options.dhparam) { - const warning = c.context.setDHParam(options.dhparam); - if (warning) - process.emitWarning(warning, 'SecurityWarning'); - } - - if (options.crl) { - if (Array.isArray(options.crl)) { - for (i = 0; i < options.crl.length; i++) { - c.context.addCRL(options.crl[i]); - } - } else { - c.context.addCRL(options.crl); - } - } - - if (options.sessionIdContext) { - c.context.setSessionIdContext(options.sessionIdContext); - } - - if (options.pfx) { - if (!crypto) - crypto = require('crypto'); - - if (Array.isArray(options.pfx)) { - for (i = 0; i < options.pfx.length; i++) { - const pfx = options.pfx[i]; - const raw = pfx.buf ? pfx.buf : pfx; - const buf = crypto._toBuf(raw); - const passphrase = pfx.passphrase || options.passphrase; - if (passphrase) { - c.context.loadPKCS12(buf, crypto._toBuf(passphrase)); - } else { - c.context.loadPKCS12(buf); - } - } - } else { - const buf = crypto._toBuf(options.pfx); - const passphrase = options.passphrase; - if (passphrase) { - c.context.loadPKCS12(buf, crypto._toBuf(passphrase)); - } else { - c.context.loadPKCS12(buf); - } - } - } - - // Do not keep read/write buffers in free list for OpenSSL < 1.1.0. (For - // OpenSSL 1.1.0, buffers are malloced and freed without the use of a - // freelist.) - if (options.singleUse) { - c.singleUse = true; - c.context.setFreeListLength(0); - } - - return c; -}; - -exports.translatePeerCertificate = function translatePeerCertificate(c) { - if (!c) - return null; - - if (c.issuer != null) c.issuer = parseCertString(c.issuer); - if (c.issuerCertificate != null && c.issuerCertificate !== c) { - c.issuerCertificate = translatePeerCertificate(c.issuerCertificate); - } - if (c.subject != null) c.subject = parseCertString(c.subject); - if (c.infoAccess != null) { - var info = c.infoAccess; - c.infoAccess = Object.create(null); - - // XXX: More key validation? - info.replace(/([^\n:]*):([^\n]*)(?:\n|$)/g, function(all, key, val) { - if (key in c.infoAccess) - c.infoAccess[key].push(val); - else - c.infoAccess[key] = [val]; - }); - } - return c; -}; +module.exports = require('internal/tls/common'); diff --git a/lib/_tls_legacy.js b/lib/_tls_legacy.js index 07b95546b3307a..de5a0268679d14 100644 --- a/lib/_tls_legacy.js +++ b/lib/_tls_legacy.js @@ -1,954 +1,6 @@ -// 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'; +process.emitWarning( + 'The _tls_legacy module is deprecated. Please use tls', + 'DeprecationWarning', 'DEP00XX'); -const internalUtil = require('internal/util'); -internalUtil.assertCrypto(); - -const assert = require('assert'); -const { Buffer } = require('buffer'); -const common = require('_tls_common'); -const { Connection } = process.binding('crypto'); -const EventEmitter = require('events'); -const stream = require('stream'); -const { Timer } = process.binding('timer_wrap'); -const tls = require('tls'); -const util = require('util'); - -const debug = util.debuglog('tls-legacy'); - -function SlabBuffer() { - this.create(); -} - - -SlabBuffer.prototype.create = function create() { - this.isFull = false; - this.pool = Buffer.allocUnsafe(tls.SLAB_BUFFER_SIZE); - this.offset = 0; - this.remaining = this.pool.length; -}; - - -SlabBuffer.prototype.use = function use(context, fn, size) { - if (this.remaining === 0) { - this.isFull = true; - return 0; - } - - var actualSize = this.remaining; - - if (size !== null) actualSize = Math.min(size, actualSize); - - var bytes = fn.call(context, this.pool, this.offset, actualSize); - if (bytes > 0) { - this.offset += bytes; - this.remaining -= bytes; - } - - assert(this.remaining >= 0); - - return bytes; -}; - - -var slabBuffer = null; - - -// Base class of both CleartextStream and EncryptedStream -function CryptoStream(pair, options) { - stream.Duplex.call(this, options); - - this.pair = pair; - this._pending = null; - this._pendingEncoding = ''; - this._pendingCallback = null; - this._doneFlag = false; - this._retryAfterPartial = false; - this._halfRead = false; - this._sslOutCb = null; - this._resumingSession = false; - this._reading = true; - this._destroyed = false; - this._ended = false; - this._finished = false; - this._opposite = null; - - if (slabBuffer === null) slabBuffer = new SlabBuffer(); - this._buffer = slabBuffer; - - this.once('finish', onCryptoStreamFinish); - - // net.Socket calls .onend too - this.once('end', onCryptoStreamEnd); -} -util.inherits(CryptoStream, stream.Duplex); - - -function onCryptoStreamFinish() { - this._finished = true; - - if (this === this.pair.cleartext) { - debug('cleartext.onfinish'); - if (this.pair.ssl) { - // Generate close notify - // NOTE: first call checks if client has sent us shutdown, - // second call enqueues shutdown into the BIO. - if (this.pair.ssl.shutdownSSL() !== 1) { - if (this.pair.ssl && this.pair.ssl.error) - return this.pair.error(); - - this.pair.ssl.shutdownSSL(); - } - - if (this.pair.ssl && this.pair.ssl.error) - return this.pair.error(); - } - } else { - debug('encrypted.onfinish'); - } - - // Try to read just to get sure that we won't miss EOF - if (this._opposite.readable) this._opposite.read(0); - - if (this._opposite._ended) { - this._done(); - - // No half-close, sorry - if (this === this.pair.cleartext) this._opposite._done(); - } -} - - -function onCryptoStreamEnd() { - this._ended = true; - if (this === this.pair.cleartext) { - debug('cleartext.onend'); - } else { - debug('encrypted.onend'); - } -} - - -// NOTE: Called once `this._opposite` is set. -CryptoStream.prototype.init = function init() { - var self = this; - this._opposite.on('sslOutEnd', function() { - if (self._sslOutCb) { - var cb = self._sslOutCb; - self._sslOutCb = null; - cb(null); - } - }); -}; - - -CryptoStream.prototype._write = function _write(data, encoding, cb) { - assert(this._pending === null); - - // Black-hole data - if (!this.pair.ssl) return cb(null); - - // When resuming session don't accept any new data. - // And do not put too much data into openssl, before writing it from encrypted - // side. - // - // TODO(indutny): Remove magic number, use watermark based limits - if (!this._resumingSession && - this._opposite._internallyPendingBytes() < 128 * 1024) { - // Write current buffer now - var written; - if (this === this.pair.cleartext) { - debug('cleartext.write called with %d bytes', data.length); - written = this.pair.ssl.clearIn(data, 0, data.length); - } else { - debug('encrypted.write called with %d bytes', data.length); - written = this.pair.ssl.encIn(data, 0, data.length); - } - - // Handle and report errors - if (this.pair.ssl && this.pair.ssl.error) { - return cb(this.pair.error(true)); - } - - // Force SSL_read call to cycle some states/data inside OpenSSL - this.pair.cleartext.read(0); - - // Cycle encrypted data - if (this.pair.encrypted._internallyPendingBytes()) - this.pair.encrypted.read(0); - - // Get ALPN, NPN and Server name when ready - this.pair.maybeInitFinished(); - - // Whole buffer was written - if (written === data.length) { - if (this === this.pair.cleartext) { - debug('cleartext.write succeed with ' + written + ' bytes'); - } else { - debug('encrypted.write succeed with ' + written + ' bytes'); - } - - // Invoke callback only when all data read from opposite stream - if (this._opposite._halfRead) { - assert(this._sslOutCb === null); - this._sslOutCb = cb; - } else { - cb(null); - } - return; - } else if (written !== 0 && written !== -1) { - assert(!this._retryAfterPartial); - this._retryAfterPartial = true; - this._write(data.slice(written), encoding, cb); - this._retryAfterPartial = false; - return; - } - } else { - debug('cleartext.write queue is full'); - - // Force SSL_read call to cycle some states/data inside OpenSSL - this.pair.cleartext.read(0); - } - - // No write has happened - this._pending = data; - this._pendingEncoding = encoding; - this._pendingCallback = cb; - - if (this === this.pair.cleartext) { - debug('cleartext.write queued with %d bytes', data.length); - } else { - debug('encrypted.write queued with %d bytes', data.length); - } -}; - - -CryptoStream.prototype._writePending = function _writePending() { - const data = this._pending; - const encoding = this._pendingEncoding; - const cb = this._pendingCallback; - - this._pending = null; - this._pendingEncoding = ''; - this._pendingCallback = null; - this._write(data, encoding, cb); -}; - - -CryptoStream.prototype._read = function _read(size) { - // XXX: EOF?! - if (!this.pair.ssl) return this.push(null); - - // Wait for session to be resumed - // Mark that we're done reading, but don't provide data or EOF - if (this._resumingSession || !this._reading) return this.push(''); - - var out; - if (this === this.pair.cleartext) { - debug('cleartext.read called with %d bytes', size); - out = this.pair.ssl.clearOut; - } else { - debug('encrypted.read called with %d bytes', size); - out = this.pair.ssl.encOut; - } - - var bytesRead = 0; - const start = this._buffer.offset; - var last = start; - do { - assert(last === this._buffer.offset); - var read = this._buffer.use(this.pair.ssl, out, size - bytesRead); - if (read > 0) { - bytesRead += read; - } - last = this._buffer.offset; - - // Handle and report errors - if (this.pair.ssl && this.pair.ssl.error) { - this.pair.error(); - break; - } - } while (read > 0 && - !this._buffer.isFull && - bytesRead < size && - this.pair.ssl !== null); - - // Get ALPN, NPN and Server name when ready - this.pair.maybeInitFinished(); - - // Create new buffer if previous was filled up - var pool = this._buffer.pool; - if (this._buffer.isFull) this._buffer.create(); - - assert(bytesRead >= 0); - - if (this === this.pair.cleartext) { - debug('cleartext.read succeed with %d bytes', bytesRead); - } else { - debug('encrypted.read succeed with %d bytes', bytesRead); - } - - // Try writing pending data - if (this._pending !== null) this._writePending(); - if (this._opposite._pending !== null) this._opposite._writePending(); - - if (bytesRead === 0) { - // EOF when cleartext has finished and we have nothing to read - if (this._opposite._finished && this._internallyPendingBytes() === 0 || - this.pair.ssl && this.pair.ssl.receivedShutdown) { - // Perform graceful shutdown - this._done(); - - // No half-open, sorry! - if (this === this.pair.cleartext) { - this._opposite._done(); - - // EOF - this.push(null); - } else if (!this.pair.ssl || !this.pair.ssl.receivedShutdown) { - // EOF - this.push(null); - } - } else { - // Bail out - this.push(''); - } - } else { - // Give them requested data - this.push(pool.slice(start, start + bytesRead)); - } - - // Let users know that we've some internal data to read - var halfRead = this._internallyPendingBytes() !== 0; - - // Smart check to avoid invoking 'sslOutEnd' in the most of the cases - if (this._halfRead !== halfRead) { - this._halfRead = halfRead; - - // Notify listeners about internal data end - if (!halfRead) { - if (this === this.pair.cleartext) { - debug('cleartext.sslOutEnd'); - } else { - debug('encrypted.sslOutEnd'); - } - - this.emit('sslOutEnd'); - } - } -}; - - -CryptoStream.prototype.setTimeout = function(timeout, callback) { - if (this.socket) this.socket.setTimeout(timeout, callback); -}; - - -CryptoStream.prototype.setNoDelay = function(noDelay) { - if (this.socket) this.socket.setNoDelay(noDelay); -}; - - -CryptoStream.prototype.setKeepAlive = function(enable, initialDelay) { - if (this.socket) this.socket.setKeepAlive(enable, initialDelay); -}; - -Object.defineProperty(CryptoStream.prototype, 'bytesWritten', { - configurable: true, - enumerable: true, - get: function() { - return this.socket ? this.socket.bytesWritten : 0; - } -}); - -CryptoStream.prototype.getPeerCertificate = function(detailed) { - if (this.pair.ssl) { - return common.translatePeerCertificate( - this.pair.ssl.getPeerCertificate(detailed)); - } - - return null; -}; - -CryptoStream.prototype.getSession = function() { - if (this.pair.ssl) { - return this.pair.ssl.getSession(); - } - - return null; -}; - -CryptoStream.prototype.isSessionReused = function() { - if (this.pair.ssl) { - return this.pair.ssl.isSessionReused(); - } - - return null; -}; - -CryptoStream.prototype.getCipher = function(err) { - if (this.pair.ssl) { - return this.pair.ssl.getCurrentCipher(); - } else { - return null; - } -}; - - -CryptoStream.prototype.end = function(chunk, encoding) { - if (this === this.pair.cleartext) { - debug('cleartext.end'); - } else { - debug('encrypted.end'); - } - - // Write pending data first - if (this._pending !== null) this._writePending(); - - this.writable = false; - - stream.Duplex.prototype.end.call(this, chunk, encoding); -}; - - -CryptoStream.prototype.destroySoon = function(err) { - if (this === this.pair.cleartext) { - debug('cleartext.destroySoon'); - } else { - debug('encrypted.destroySoon'); - } - - if (this.writable) - this.end(); - - if (this._writableState.finished && this._opposite._ended) { - this.destroy(); - } else { - // Wait for both `finish` and `end` events to ensure that all data that - // was written on this side was read from the other side. - var self = this; - var waiting = 1; - function finish() { - if (--waiting === 0) self.destroy(); - } - this._opposite.once('end', finish); - if (!this._finished) { - this.once('finish', finish); - ++waiting; - } - } -}; - - -CryptoStream.prototype.destroy = function(err) { - if (this._destroyed) return; - this._destroyed = true; - this.readable = this.writable = false; - - // Destroy both ends - if (this === this.pair.cleartext) { - debug('cleartext.destroy'); - } else { - debug('encrypted.destroy'); - } - this._opposite.destroy(); - - process.nextTick(destroyNT, this, err ? true : false); -}; - - -function destroyNT(self, hadErr) { - // Force EOF - self.push(null); - - // Emit 'close' event - self.emit('close', hadErr); -} - - -CryptoStream.prototype._done = function() { - this._doneFlag = true; - - if (this === this.pair.encrypted && !this.pair._secureEstablished) - return this.pair.error(); - - if (this.pair.cleartext._doneFlag && - this.pair.encrypted._doneFlag && - !this.pair._doneFlag) { - // If both streams are done: - this.pair.destroy(); - } -}; - - -// readyState is deprecated. Don't use it. -// Deprecation Code: DEP0004 -Object.defineProperty(CryptoStream.prototype, 'readyState', { - get: function() { - if (this.connecting) { - return 'opening'; - } else if (this.readable && this.writable) { - return 'open'; - } else if (this.readable && !this.writable) { - return 'readOnly'; - } else if (!this.readable && this.writable) { - return 'writeOnly'; - } else { - return 'closed'; - } - } -}); - - -function CleartextStream(pair, options) { - CryptoStream.call(this, pair, options); - - // This is a fake kludge to support how the http impl sits - // on top of net Sockets - var self = this; - this._handle = { - readStop: function() { - self._reading = false; - }, - readStart: function() { - if (self._reading && self._readableState.length > 0) return; - self._reading = true; - self.read(0); - if (self._opposite.readable) self._opposite.read(0); - } - }; -} -util.inherits(CleartextStream, CryptoStream); - - -CleartextStream.prototype._internallyPendingBytes = function() { - if (this.pair.ssl) { - return this.pair.ssl.clearPending(); - } else { - return 0; - } -}; - - -CleartextStream.prototype.address = function() { - return this.socket && this.socket.address(); -}; - -Object.defineProperty(CleartextStream.prototype, 'remoteAddress', { - configurable: true, - enumerable: true, - get: function() { - return this.socket && this.socket.remoteAddress; - } -}); - -Object.defineProperty(CleartextStream.prototype, 'remoteFamily', { - configurable: true, - enumerable: true, - get: function() { - return this.socket && this.socket.remoteFamily; - } -}); - -Object.defineProperty(CleartextStream.prototype, 'remotePort', { - configurable: true, - enumerable: true, - get: function() { - return this.socket && this.socket.remotePort; - } -}); - -Object.defineProperty(CleartextStream.prototype, 'localAddress', { - configurable: true, - enumerable: true, - get: function() { - return this.socket && this.socket.localAddress; - } -}); - -Object.defineProperty(CleartextStream.prototype, 'localPort', { - configurable: true, - enumerable: true, - get: function() { - return this.socket && this.socket.localPort; - } -}); - - -function EncryptedStream(pair, options) { - CryptoStream.call(this, pair, options); -} -util.inherits(EncryptedStream, CryptoStream); - - -EncryptedStream.prototype._internallyPendingBytes = function() { - if (this.pair.ssl) { - return this.pair.ssl.encPending(); - } else { - return 0; - } -}; - - -function onhandshakestart() { - debug('onhandshakestart'); - - var self = this; - var ssl = self.ssl; - var now = Timer.now(); - - assert(now >= ssl.lastHandshakeTime); - - if ((now - ssl.lastHandshakeTime) >= tls.CLIENT_RENEG_WINDOW * 1000) { - ssl.handshakes = 0; - } - - var first = (ssl.lastHandshakeTime === 0); - ssl.lastHandshakeTime = now; - if (first) return; - - if (++ssl.handshakes > tls.CLIENT_RENEG_LIMIT) { - // Defer the error event to the next tick. We're being called from OpenSSL's - // state machine and OpenSSL is not re-entrant. We cannot allow the user's - // callback to destroy the connection right now, it would crash and burn. - setImmediate(function() { - var err = new Error('TLS session renegotiation attack detected'); - if (self.cleartext) self.cleartext.emit('error', err); - }); - } -} - - -function onhandshakedone() { - // for future use - debug('onhandshakedone'); -} - - -function onclienthello(hello) { - const self = this; - var once = false; - - this._resumingSession = true; - function callback(err, session) { - if (once) return; - once = true; - - if (err) return self.socket.destroy(err); - - setImmediate(function() { - self.ssl.loadSession(session); - self.ssl.endParser(); - - // Cycle data - self._resumingSession = false; - self.cleartext.read(0); - self.encrypted.read(0); - }); - } - - if (hello.sessionId.length <= 0 || - !this.server || - !this.server.emit('resumeSession', hello.sessionId, callback)) { - callback(null, null); - } -} - - -function onnewsession(key, session) { - if (!this.server) return; - - var self = this; - var once = false; - - if (!self.server.emit('newSession', key, session, done)) - done(); - - function done() { - if (once) - return; - once = true; - - if (self.ssl) - self.ssl.newSessionDone(); - } -} - - -function onocspresponse(resp) { - this.emit('OCSPResponse', resp); -} - - -/** - * Provides a pair of streams to do encrypted communication. - */ - -function SecurePair(context, isServer, requestCert, rejectUnauthorized, - options) { - if (!(this instanceof SecurePair)) { - return new SecurePair(context, - isServer, - requestCert, - rejectUnauthorized, - options); - } - - options || (options = {}); - - EventEmitter.call(this); - - this.server = options.server; - this._secureEstablished = false; - this._isServer = isServer ? true : false; - this._encWriteState = true; - this._clearWriteState = true; - this._doneFlag = false; - this._destroying = false; - - if (!context) { - this.credentials = tls.createSecureContext(); - } else { - this.credentials = context; - } - - if (!this._isServer) { - // For clients, we will always have either a given ca list or be using - // default one - requestCert = true; - } - - this._rejectUnauthorized = rejectUnauthorized ? true : false; - this._requestCert = requestCert ? true : false; - - this.ssl = new Connection( - this.credentials.context, - this._isServer ? true : false, - this._isServer ? this._requestCert : options.servername, - this._rejectUnauthorized - ); - - if (this._isServer) { - this.ssl.onhandshakestart = () => onhandshakestart.call(this); - this.ssl.onhandshakedone = () => onhandshakedone.call(this); - this.ssl.onclienthello = (hello) => onclienthello.call(this, hello); - this.ssl.onnewsession = - (key, session) => onnewsession.call(this, key, session); - this.ssl.lastHandshakeTime = 0; - this.ssl.handshakes = 0; - } else { - this.ssl.onocspresponse = (resp) => onocspresponse.call(this, resp); - } - - if (process.features.tls_sni) { - if (this._isServer && options.SNICallback) { - this.ssl.setSNICallback(options.SNICallback); - } - this.servername = null; - } - - if (process.features.tls_npn && options.NPNProtocols) { - this.ssl.setNPNProtocols(options.NPNProtocols); - this.npnProtocol = null; - } - - if (process.features.tls_alpn && options.ALPNProtocols) { - // keep reference in secureContext not to be GC-ed - this.ssl._secureContext.alpnBuffer = options.ALPNProtocols; - this.ssl.setALPNrotocols(this.ssl._secureContext.alpnBuffer); - this.alpnProtocol = null; - } - - /* Acts as a r/w stream to the cleartext side of the stream. */ - this.cleartext = new CleartextStream(this, options.cleartext); - - /* Acts as a r/w stream to the encrypted side of the stream. */ - this.encrypted = new EncryptedStream(this, options.encrypted); - - /* Let streams know about each other */ - this.cleartext._opposite = this.encrypted; - this.encrypted._opposite = this.cleartext; - this.cleartext.init(); - this.encrypted.init(); - - process.nextTick(securePairNT, this, options); -} - -util.inherits(SecurePair, EventEmitter); - -function securePairNT(self, options) { - /* The Connection may be destroyed by an abort call */ - if (self.ssl) { - self.ssl.start(); - - if (options.requestOCSP) - self.ssl.requestOCSP(); - - /* In case of cipher suite failures - SSL_accept/SSL_connect may fail */ - if (self.ssl && self.ssl.error) - self.error(); - } -} - - -function createSecurePair(context, isServer, requestCert, - rejectUnauthorized, options) { - return new SecurePair(context, isServer, requestCert, - rejectUnauthorized, options); -} - - -SecurePair.prototype.maybeInitFinished = function() { - if (this.ssl && !this._secureEstablished && this.ssl.isInitFinished()) { - if (process.features.tls_npn) { - this.npnProtocol = this.ssl.getNegotiatedProtocol(); - } - - if (process.features.tls_alpn) { - this.alpnProtocol = this.ssl.getALPNNegotiatedProtocol(); - } - - if (process.features.tls_sni) { - this.servername = this.ssl.getServername(); - } - - this._secureEstablished = true; - debug('secure established'); - this.emit('secure'); - } -}; - - -SecurePair.prototype.destroy = function() { - if (this._destroying) return; - - if (!this._doneFlag) { - debug('SecurePair.destroy'); - this._destroying = true; - - // SecurePair should be destroyed only after it's streams - this.cleartext.destroy(); - this.encrypted.destroy(); - - this._doneFlag = true; - this.ssl.error = null; - this.ssl.close(); - this.ssl = null; - } -}; - - -SecurePair.prototype.error = function(returnOnly) { - var err = this.ssl.error; - this.ssl.error = null; - - if (!this._secureEstablished) { - // Emit ECONNRESET instead of zero return - if (!err || err.message === 'ZERO_RETURN') { - var connReset = new Error('socket hang up'); - connReset.code = 'ECONNRESET'; - connReset.sslError = err && err.message; - - err = connReset; - } - this.destroy(); - if (!returnOnly) this.emit('error', err); - } else if (this._isServer && - this._rejectUnauthorized && - /peer did not return a certificate/.test(err.message)) { - // Not really an error. - this.destroy(); - } else if (!returnOnly) { - this.cleartext.emit('error', err); - } - return err; -}; - - -function pipe(pair, socket) { - pair.encrypted.pipe(socket); - socket.pipe(pair.encrypted); - - pair.encrypted.on('close', function() { - process.nextTick(pipeCloseNT, pair, socket); - }); - - pair.fd = socket.fd; - var cleartext = pair.cleartext; - cleartext.socket = socket; - cleartext.encrypted = pair.encrypted; - cleartext.authorized = false; - - // cycle the data whenever the socket drains, so that - // we can pull some more into it. normally this would - // be handled by the fact that pipe() triggers read() calls - // on writable.drain, but CryptoStreams are a bit more - // complicated. Since the encrypted side actually gets - // its data from the cleartext side, we have to give it a - // light kick to get in motion again. - socket.on('drain', function() { - if (pair.encrypted._pending) - pair.encrypted._writePending(); - if (pair.cleartext._pending) - pair.cleartext._writePending(); - pair.encrypted.read(0); - pair.cleartext.read(0); - }); - - function onerror(e) { - if (cleartext._controlReleased) { - cleartext.emit('error', e); - } - } - - function onclose() { - socket.removeListener('error', onerror); - socket.removeListener('timeout', ontimeout); - } - - function ontimeout() { - cleartext.emit('timeout'); - } - - socket.on('error', onerror); - socket.on('close', onclose); - socket.on('timeout', ontimeout); - - return cleartext; -} - - -function pipeCloseNT(pair, socket) { - // Encrypted should be unpiped from socket to prevent possible - // write after destroy. - pair.encrypted.unpipe(socket); - socket.destroySoon(); -} - -module.exports = { - createSecurePair: - internalUtil.deprecate(createSecurePair, - 'tls.createSecurePair() is deprecated. Please use ' + - 'tls.Socket instead.', 'DEP0064'), - pipe -}; +module.exports = require('internal/tls/legacy'); diff --git a/lib/_tls_wrap.js b/lib/_tls_wrap.js index d7e349b239cb05..29776479a25424 100644 --- a/lib/_tls_wrap.js +++ b/lib/_tls_wrap.js @@ -1,1152 +1,6 @@ -// 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'; +process.emitWarning( + 'The _tls_wrap module is deprecated. Please use tls', + 'DeprecationWarning', 'DEP00XX'); -require('internal/util').assertCrypto(); - -const assert = require('assert'); -const crypto = require('crypto'); -const net = require('net'); -const tls = require('tls'); -const util = require('util'); -const common = require('_tls_common'); -const { StreamWrap } = require('_stream_wrap'); -const { Buffer } = require('buffer'); -const debug = util.debuglog('tls'); -const { Timer } = process.binding('timer_wrap'); -const tls_wrap = process.binding('tls_wrap'); -const { TCP } = process.binding('tcp_wrap'); -const { Pipe } = process.binding('pipe_wrap'); -const errors = require('internal/errors'); -const kConnectOptions = Symbol('connect-options'); -const kDisableRenegotiation = Symbol('disable-renegotiation'); -const kErrorEmitted = Symbol('error-emitted'); -const kHandshakeTimeout = Symbol('handshake-timeout'); -const kRes = Symbol('res'); -const kSNICallback = Symbol('snicallback'); - -const noop = () => {}; - -function onhandshakestart() { - debug('onhandshakestart'); - - const owner = this.owner; - const now = Timer.now(); - - assert(now >= this.lastHandshakeTime); - - if ((now - this.lastHandshakeTime) >= tls.CLIENT_RENEG_WINDOW * 1000) { - this.handshakes = 0; - } - - const first = (this.lastHandshakeTime === 0); - this.lastHandshakeTime = now; - if (first) return; - - if (++this.handshakes > tls.CLIENT_RENEG_LIMIT) { - // Defer the error event to the next tick. We're being called from OpenSSL's - // state machine and OpenSSL is not re-entrant. We cannot allow the user's - // callback to destroy the connection right now, it would crash and burn. - setImmediate(emitSessionAttackError, owner); - } - - if (owner[kDisableRenegotiation] && this.handshakes > 0) { - const err = new Error('TLS session renegotiation disabled for this socket'); - owner._emitTLSError(err); - } -} - -function emitSessionAttackError(socket) { - socket._emitTLSError(new errors.Error('ERR_TLS_SESSION_ATTACK')); -} - -function onhandshakedone() { - debug('onhandshakedone'); - - const owner = this.owner; - - // `newSession` callback wasn't called yet - if (owner._newSessionPending) { - owner._securePending = true; - return; - } - - owner._finishInit(); -} - - -function loadSession(hello) { - const owner = this.owner; - - var once = false; - function onSession(err, session) { - if (once) - return owner.destroy(new errors.Error('ERR_MULTIPLE_CALLBACK')); - once = true; - - if (err) - return owner.destroy(err); - - if (owner._handle === null) - return owner.destroy(new errors.Error('ERR_SOCKET_CLOSED')); - - owner._handle.loadSession(session); - owner._handle.endParser(); - } - - if (hello.sessionId.length <= 0 || - hello.tlsTicket || - owner.server && - !owner.server.emit('resumeSession', hello.sessionId, onSession)) { - owner._handle.endParser(); - } -} - - -function loadSNI(info) { - const owner = this.owner; - const servername = info.servername; - if (!servername || !owner._SNICallback) - return requestOCSP(owner, info); - - let once = false; - owner._SNICallback(servername, (err, context) => { - if (once) - return owner.destroy(new errors.Error('ERR_MULTIPLE_CALLBACK')); - once = true; - - if (err) - return owner.destroy(err); - - if (owner._handle === null) - return owner.destroy(new errors.Error('ERR_SOCKET_CLOSED')); - - // TODO(indutny): eventually disallow raw `SecureContext` - if (context) - owner._handle.sni_context = context.context || context; - - requestOCSP(owner, info); - }); -} - - -function requestOCSP(socket, info) { - if (!info.OCSPRequest || !socket.server) - return requestOCSPDone(socket); - - let ctx = socket._handle.sni_context; - - if (!ctx) { - ctx = socket.server._sharedCreds; - - // TLS socket is using a `net.Server` instead of a tls.TLSServer. - // Some TLS properties like `server._sharedCreds` will not be present - if (!ctx) - return requestOCSPDone(socket); - } - - // TODO(indutny): eventually disallow raw `SecureContext` - if (ctx.context) - ctx = ctx.context; - - if (socket.server.listenerCount('OCSPRequest') === 0) { - return requestOCSPDone(socket); - } - - let once = false; - const onOCSP = (err, response) => { - if (once) - return socket.destroy(new errors.Error('ERR_MULTIPLE_CALLBACK')); - once = true; - - if (err) - return socket.destroy(err); - - if (socket._handle === null) - return socket.destroy(new errors.Error('ERR_SOCKET_CLOSED')); - - if (response) - socket._handle.setOCSPResponse(response); - requestOCSPDone(socket); - }; - - socket.server.emit('OCSPRequest', - ctx.getCertificate(), - ctx.getIssuer(), - onOCSP); -} - -function requestOCSPDone(socket) { - try { - socket._handle.certCbDone(); - } catch (e) { - socket.destroy(e); - } -} - - -function onnewsession(key, session) { - const owner = this.owner; - - if (!owner.server) - return; - - var once = false; - const done = () => { - if (once) - return; - once = true; - - if (owner._handle === null) - return owner.destroy(new errors.Error('ERR_SOCKET_CLOSED')); - - this.newSessionDone(); - - owner._newSessionPending = false; - if (owner._securePending) - owner._finishInit(); - owner._securePending = false; - }; - - owner._newSessionPending = true; - if (!owner.server.emit('newSession', key, session, done)) - done(); -} - - -function onocspresponse(resp) { - this.owner.emit('OCSPResponse', resp); -} - -function onerror(err) { - const owner = this.owner; - - if (owner._writableState.errorEmitted) - return; - - // Destroy socket if error happened before handshake's finish - if (!owner._secureEstablished) { - // When handshake fails control is not yet released, - // so self._tlsError will return null instead of actual error - owner.destroy(err); - } else if (owner._tlsOptions.isServer && - owner._rejectUnauthorized && - /peer did not return a certificate/.test(err.message)) { - // Ignore server's authorization errors - owner.destroy(); - } else { - // Throw error - owner._emitTLSError(err); - } - - owner._writableState.errorEmitted = true; -} - -function initRead(tls, wrapped) { - // If we were destroyed already don't bother reading - if (!tls._handle) - return; - - // Socket already has some buffered data - emulate receiving it - if (wrapped && wrapped._readableState && wrapped._readableState.length) { - var buf; - while ((buf = wrapped.read()) !== null) - tls._handle.receive(buf); - } - - tls.read(0); -} - -/** - * Provides a wrap of socket stream to do encrypted communication. - */ - -function TLSSocket(socket, options) { - if (options === undefined) - this._tlsOptions = {}; - else - this._tlsOptions = options; - this._secureEstablished = false; - this._securePending = false; - this._newSessionPending = false; - this._controlReleased = false; - this._SNICallback = null; - this.servername = null; - this.npnProtocol = null; - this.alpnProtocol = null; - this.authorized = false; - this.authorizationError = null; - this[kRes] = null; - - // Wrap plain JS Stream into StreamWrap - var wrap; - if ((socket instanceof net.Socket && socket._handle) || !socket) - wrap = socket; - else - wrap = new StreamWrap(socket); - - // Just a documented property to make secure sockets - // distinguishable from regular ones. - this.encrypted = true; - - net.Socket.call(this, { - handle: this._wrapHandle(wrap), - allowHalfOpen: socket && socket.allowHalfOpen, - readable: false, - writable: false - }); - - // Proxy for API compatibility - this.ssl = this._handle; - - this.on('error', this._tlsError); - - this._init(socket, wrap); - - // Make sure to setup all required properties like: `connecting` before - // starting the flow of the data - this.readable = true; - this.writable = true; - - // Read on next tick so the caller has a chance to setup listeners - process.nextTick(initRead, this, socket); -} -util.inherits(TLSSocket, net.Socket); -exports.TLSSocket = TLSSocket; - -var proxiedMethods = [ - 'ref', 'unref', 'open', 'bind', 'listen', 'connect', 'bind6', - 'connect6', 'getsockname', 'getpeername', 'setNoDelay', 'setKeepAlive', - 'setSimultaneousAccepts', 'setBlocking', - - // PipeWrap - 'setPendingInstances' -]; - -// Proxy HandleWrap, PipeWrap and TCPWrap methods -function makeMethodProxy(name) { - return function methodProxy(...args) { - if (this._parent[name]) - return this._parent[name].apply(this._parent, args); - }; -} -for (var n = 0; n < proxiedMethods.length; n++) { - tls_wrap.TLSWrap.prototype[proxiedMethods[n]] = - makeMethodProxy(proxiedMethods[n]); -} - -tls_wrap.TLSWrap.prototype.close = function close(cb) { - let ssl; - if (this.owner) { - ssl = this.owner.ssl; - this.owner.ssl = null; - } - - // Invoke `destroySSL` on close to clean up possibly pending write requests - // that may self-reference TLSWrap, leading to leak - const done = () => { - if (ssl) { - ssl.destroySSL(); - if (ssl._secureContext.singleUse) { - ssl._secureContext.context.close(); - ssl._secureContext.context = null; - } - } - if (cb) - cb(); - }; - - if (this._parentWrap && this._parentWrap._handle === this._parent) { - this._parentWrap.once('close', done); - return this._parentWrap.destroy(); - } - return this._parent.close(done); -}; - -TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() { - this[kDisableRenegotiation] = true; -}; - -TLSSocket.prototype._wrapHandle = function(wrap) { - var handle; - - if (wrap) - handle = wrap._handle; - - var options = this._tlsOptions; - if (!handle) { - handle = options.pipe ? new Pipe() : new TCP(); - handle.owner = this; - } - - // Wrap socket's handle - const context = options.secureContext || - options.credentials || - tls.createSecureContext(options); - const res = tls_wrap.wrap(handle._externalStream, - context.context, - !!options.isServer); - res._parent = handle; - res._parentWrap = wrap; - res._secureContext = context; - res.reading = handle.reading; - this[kRes] = res; - defineHandleReading(this, handle); - - this.on('close', onSocketCloseDestroySSL); - - return res; -}; - -// This eliminates a cyclic reference to TLSWrap -// Ref: https://github.com/nodejs/node/commit/f7620fb96d339f704932f9bb9a0dceb9952df2d4 -function defineHandleReading(socket, handle) { - Object.defineProperty(handle, 'reading', { - get: () => { - return socket[kRes].reading; - }, - set: (value) => { - socket[kRes].reading = value; - } - }); -} - -function onSocketCloseDestroySSL() { - // Make sure we are not doing it on OpenSSL's stack - setImmediate(destroySSL, this); - this[kRes] = null; -} - -function destroySSL(self) { - self._destroySSL(); -} - -TLSSocket.prototype._destroySSL = function _destroySSL() { - if (!this.ssl) return; - this.ssl.destroySSL(); - if (this.ssl._secureContext.singleUse) { - this.ssl._secureContext.context.close(); - this.ssl._secureContext.context = null; - } - this.ssl = null; -}; - -TLSSocket.prototype._init = function(socket, wrap) { - var options = this._tlsOptions; - var ssl = this._handle; - - // lib/net.js expect this value to be non-zero if write hasn't been flushed - // immediately. After the handshake is done this will represent the actual - // write queue size - ssl.writeQueueSize = 1; - - this.server = options.server; - - // For clients, we will always have either a given ca list or be using - // default one - const requestCert = !!options.requestCert || !options.isServer; - const rejectUnauthorized = !!options.rejectUnauthorized; - - this._requestCert = requestCert; - this._rejectUnauthorized = rejectUnauthorized; - if (requestCert || rejectUnauthorized) - ssl.setVerifyMode(requestCert, rejectUnauthorized); - - if (options.isServer) { - ssl.onhandshakestart = onhandshakestart; - ssl.onhandshakedone = onhandshakedone; - ssl.onclienthello = loadSession; - ssl.oncertcb = loadSNI; - ssl.onnewsession = onnewsession; - ssl.lastHandshakeTime = 0; - ssl.handshakes = 0; - - if (this.server) { - if (this.server.listenerCount('resumeSession') > 0 || - this.server.listenerCount('newSession') > 0) { - ssl.enableSessionCallbacks(); - } - if (this.server.listenerCount('OCSPRequest') > 0) - ssl.enableCertCb(); - } - } else { - ssl.onhandshakestart = noop; - ssl.onhandshakedone = this._finishInit.bind(this); - ssl.onocspresponse = onocspresponse; - - if (options.session) - ssl.setSession(options.session); - } - - ssl.onerror = onerror; - - // If custom SNICallback was given, or if - // there're SNI contexts to perform match against - - // set `.onsniselect` callback. - if (process.features.tls_sni && - options.isServer && - options.SNICallback && - options.server && - (options.SNICallback !== SNICallback || - options.server._contexts.length)) { - assert(typeof options.SNICallback === 'function'); - this._SNICallback = options.SNICallback; - ssl.enableCertCb(); - } - - if (process.features.tls_npn && options.NPNProtocols) - ssl.setNPNProtocols(options.NPNProtocols); - - if (process.features.tls_alpn && options.ALPNProtocols) { - // keep reference in secureContext not to be GC-ed - ssl._secureContext.alpnBuffer = options.ALPNProtocols; - ssl.setALPNProtocols(ssl._secureContext.alpnBuffer); - } - - if (options.handshakeTimeout > 0) - this.setTimeout(options.handshakeTimeout, this._handleTimeout); - - if (socket instanceof net.Socket) { - this._parent = socket; - - // To prevent assertion in afterConnect() and properly kick off readStart - this.connecting = socket.connecting || !socket._handle; - socket.once('connect', () => { - this.connecting = false; - this.emit('connect'); - }); - } - - // Assume `tls.connect()` - if (wrap) { - wrap.on('error', (err) => this._emitTLSError(err)); - } else { - assert(!socket); - this.connecting = true; - } -}; - -TLSSocket.prototype.renegotiate = function(options, callback) { - if (this.destroyed) - return; - - let requestCert = this._requestCert; - let rejectUnauthorized = this._rejectUnauthorized; - - if (options.requestCert !== undefined) - requestCert = !!options.requestCert; - if (options.rejectUnauthorized !== undefined) - rejectUnauthorized = !!options.rejectUnauthorized; - - if (requestCert !== this._requestCert || - rejectUnauthorized !== this._rejectUnauthorized) { - this._handle.setVerifyMode(requestCert, rejectUnauthorized); - this._requestCert = requestCert; - this._rejectUnauthorized = rejectUnauthorized; - } - if (!this._handle.renegotiate()) { - if (callback) { - process.nextTick(callback, new errors.Error('ERR_TLS_RENEGOTIATE')); - } - return false; - } - - // Ensure that we'll cycle through internal openssl's state - this.write(''); - - if (callback) { - this.once('secure', () => callback(null)); - } - - return true; -}; - -TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment(size) { - return this._handle.setMaxSendFragment(size) === 1; -}; - -TLSSocket.prototype.getTLSTicket = function getTLSTicket() { - return this._handle.getTLSTicket(); -}; - -TLSSocket.prototype._handleTimeout = function() { - this._emitTLSError(new errors.Error('ERR_TLS_HANDSHAKE_TIMEOUT')); -}; - -TLSSocket.prototype._emitTLSError = function(err) { - var e = this._tlsError(err); - if (e) - this.emit('error', e); -}; - -TLSSocket.prototype._tlsError = function(err) { - this.emit('_tlsError', err); - if (this._controlReleased) - return err; - return null; -}; - -TLSSocket.prototype._releaseControl = function() { - if (this._controlReleased) - return false; - this._controlReleased = true; - this.removeListener('error', this._tlsError); - return true; -}; - -TLSSocket.prototype._finishInit = function() { - if (process.features.tls_npn) { - this.npnProtocol = this._handle.getNegotiatedProtocol(); - } - - if (process.features.tls_alpn) { - this.alpnProtocol = this._handle.getALPNNegotiatedProtocol(); - } - - if (process.features.tls_sni && this._tlsOptions.isServer) { - this.servername = this._handle.getServername(); - } - - debug('secure established'); - this._secureEstablished = true; - if (this._tlsOptions.handshakeTimeout > 0) - this.setTimeout(0, this._handleTimeout); - this.emit('secure'); -}; - -TLSSocket.prototype._start = function() { - if (this.connecting) { - this.once('connect', this._start); - return; - } - - // Socket was destroyed before the connection was established - if (!this._handle) - return; - - debug('start'); - if (this._tlsOptions.requestOCSP) - this._handle.requestOCSP(); - this._handle.start(); -}; - -TLSSocket.prototype.setServername = function(name) { - this._handle.setServername(name); -}; - -TLSSocket.prototype.setSession = function(session) { - if (typeof session === 'string') - session = Buffer.from(session, 'latin1'); - this._handle.setSession(session); -}; - -TLSSocket.prototype.getPeerCertificate = function(detailed) { - if (this._handle) { - return common.translatePeerCertificate( - this._handle.getPeerCertificate(detailed)); - } - - return null; -}; - -TLSSocket.prototype.getSession = function() { - if (this._handle) { - return this._handle.getSession(); - } - - return null; -}; - -TLSSocket.prototype.isSessionReused = function() { - if (this._handle) { - return this._handle.isSessionReused(); - } - - return null; -}; - -TLSSocket.prototype.getCipher = function(err) { - if (this._handle) { - return this._handle.getCurrentCipher(); - } else { - return null; - } -}; - -TLSSocket.prototype.getEphemeralKeyInfo = function() { - if (this._handle) - return this._handle.getEphemeralKeyInfo(); - - return null; -}; - -TLSSocket.prototype.getProtocol = function() { - if (this._handle) - return this._handle.getProtocol(); - - return null; -}; - -// TODO: support anonymous (nocert) and PSK - - -function onSocketSecure() { - if (this._requestCert) { - const verifyError = this._handle.verifyError(); - if (verifyError) { - this.authorizationError = verifyError.code; - - if (this._rejectUnauthorized) - this.destroy(); - } else { - this.authorized = true; - } - } - - if (!this.destroyed && this._releaseControl()) - this._tlsOptions.server.emit('secureConnection', this); -} - -function onSocketTLSError(err) { - if (!this._controlReleased && !this[kErrorEmitted]) { - this[kErrorEmitted] = true; - this._tlsOptions.server.emit('tlsClientError', err, this); - } -} - -function onSocketClose(err) { - // Closed because of error - no need to emit it twice - if (err) - return; - - // Emit ECONNRESET - if (!this._controlReleased && !this[kErrorEmitted]) { - this[kErrorEmitted] = true; - const connReset = new Error('socket hang up'); - connReset.code = 'ECONNRESET'; - this._tlsOptions.server.emit('tlsClientError', connReset, this); - } -} - -function tlsConnectionListener(rawSocket) { - const socket = new TLSSocket(rawSocket, { - secureContext: this._sharedCreds, - isServer: true, - server: this, - requestCert: this.requestCert, - rejectUnauthorized: this.rejectUnauthorized, - handshakeTimeout: this[kHandshakeTimeout], - NPNProtocols: this.NPNProtocols, - ALPNProtocols: this.ALPNProtocols, - SNICallback: this[kSNICallback] || SNICallback - }); - - socket.on('secure', onSocketSecure); - - socket[kErrorEmitted] = false; - socket.on('close', onSocketClose); - socket.on('_tlsError', onSocketTLSError); -} - -// AUTHENTICATION MODES -// -// There are several levels of authentication that TLS/SSL supports. -// Read more about this in "man SSL_set_verify". -// -// 1. The server sends a certificate to the client but does not request a -// cert from the client. This is common for most HTTPS servers. The browser -// can verify the identity of the server, but the server does not know who -// the client is. Authenticating the client is usually done over HTTP using -// login boxes and cookies and stuff. -// -// 2. The server sends a cert to the client and requests that the client -// also send it a cert. The client knows who the server is and the server is -// requesting the client also identify themselves. There are several -// outcomes: -// -// A) verifyError returns null meaning the client's certificate is signed -// by one of the server's CAs. The server now knows the client's identity -// and the client is authorized. -// -// B) For some reason the client's certificate is not acceptable - -// verifyError returns a string indicating the problem. The server can -// either (i) reject the client or (ii) allow the client to connect as an -// unauthorized connection. -// -// The mode is controlled by two boolean variables. -// -// requestCert -// If true the server requests a certificate from client connections. For -// the common HTTPS case, users will want this to be false, which is what -// it defaults to. -// -// rejectUnauthorized -// If true clients whose certificates are invalid for any reason will not -// be allowed to make connections. If false, they will simply be marked as -// unauthorized but secure communication will continue. By default this is -// true. -// -// -// -// Options: -// - requestCert. Send verify request. Default to false. -// - rejectUnauthorized. Boolean, default to true. -// - key. string. -// - cert: string. -// - ca: string or array of strings. -// - sessionTimeout: integer. -// -// emit 'secureConnection' -// function (tlsSocket) { } -// -// "UNABLE_TO_GET_ISSUER_CERT", "UNABLE_TO_GET_CRL", -// "UNABLE_TO_DECRYPT_CERT_SIGNATURE", "UNABLE_TO_DECRYPT_CRL_SIGNATURE", -// "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", "CERT_SIGNATURE_FAILURE", -// "CRL_SIGNATURE_FAILURE", "CERT_NOT_YET_VALID" "CERT_HAS_EXPIRED", -// "CRL_NOT_YET_VALID", "CRL_HAS_EXPIRED" "ERROR_IN_CERT_NOT_BEFORE_FIELD", -// "ERROR_IN_CERT_NOT_AFTER_FIELD", "ERROR_IN_CRL_LAST_UPDATE_FIELD", -// "ERROR_IN_CRL_NEXT_UPDATE_FIELD", "OUT_OF_MEM", -// "DEPTH_ZERO_SELF_SIGNED_CERT", "SELF_SIGNED_CERT_IN_CHAIN", -// "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", -// "CERT_CHAIN_TOO_LONG", "CERT_REVOKED" "INVALID_CA", -// "PATH_LENGTH_EXCEEDED", "INVALID_PURPOSE" "CERT_UNTRUSTED", -// "CERT_REJECTED" -// -function Server(options, listener) { - if (!(this instanceof Server)) - return new Server(options, listener); - - if (typeof options === 'function') { - listener = options; - options = {}; - } else if (options == null || typeof options === 'object') { - options = options || {}; - } else { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'object'); - } - - - this._contexts = []; - - // Handle option defaults: - this.setOptions(options); - - var sharedCreds = tls.createSecureContext({ - pfx: this.pfx, - key: this.key, - passphrase: this.passphrase, - cert: this.cert, - ca: this.ca, - ciphers: this.ciphers, - ecdhCurve: this.ecdhCurve, - dhparam: this.dhparam, - secureProtocol: this.secureProtocol, - secureOptions: this.secureOptions, - honorCipherOrder: this.honorCipherOrder, - crl: this.crl, - sessionIdContext: this.sessionIdContext - }); - this._sharedCreds = sharedCreds; - - this[kHandshakeTimeout] = options.handshakeTimeout || (120 * 1000); - this[kSNICallback] = options.SNICallback; - - if (typeof this[kHandshakeTimeout] !== 'number') { - throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'timeout', 'number'); - } - - if (this.sessionTimeout) { - sharedCreds.context.setSessionTimeout(this.sessionTimeout); - } - - if (this.ticketKeys) { - sharedCreds.context.setTicketKeys(this.ticketKeys); - } - - // constructor call - net.Server.call(this, tlsConnectionListener); - - if (listener) { - this.on('secureConnection', listener); - } -} - -util.inherits(Server, net.Server); -exports.Server = Server; -exports.createServer = function(options, listener) { - return new Server(options, listener); -}; - - -Server.prototype._getServerData = function() { - return { - ticketKeys: this.getTicketKeys().toString('hex') - }; -}; - - -Server.prototype._setServerData = function(data) { - this.setTicketKeys(Buffer.from(data.ticketKeys, 'hex')); -}; - - -Server.prototype.getTicketKeys = function getTicketKeys(keys) { - return this._sharedCreds.context.getTicketKeys(keys); -}; - - -Server.prototype.setTicketKeys = function setTicketKeys(keys) { - this._sharedCreds.context.setTicketKeys(keys); -}; - - -Server.prototype.setOptions = function(options) { - this.requestCert = options.requestCert === true; - this.rejectUnauthorized = options.rejectUnauthorized !== false; - - if (options.pfx) this.pfx = options.pfx; - if (options.key) this.key = options.key; - if (options.passphrase) this.passphrase = options.passphrase; - if (options.cert) this.cert = options.cert; - if (options.ca) this.ca = options.ca; - if (options.secureProtocol) this.secureProtocol = options.secureProtocol; - if (options.crl) this.crl = options.crl; - if (options.ciphers) this.ciphers = options.ciphers; - if (options.ecdhCurve !== undefined) - this.ecdhCurve = options.ecdhCurve; - if (options.dhparam) this.dhparam = options.dhparam; - if (options.sessionTimeout) this.sessionTimeout = options.sessionTimeout; - if (options.ticketKeys) this.ticketKeys = options.ticketKeys; - var secureOptions = options.secureOptions || 0; - if (options.honorCipherOrder !== undefined) - this.honorCipherOrder = !!options.honorCipherOrder; - else - this.honorCipherOrder = true; - if (secureOptions) this.secureOptions = secureOptions; - if (options.NPNProtocols) tls.convertNPNProtocols(options.NPNProtocols, this); - if (options.ALPNProtocols) - tls.convertALPNProtocols(options.ALPNProtocols, this); - if (options.sessionIdContext) { - this.sessionIdContext = options.sessionIdContext; - } else { - this.sessionIdContext = crypto.createHash('sha1') - .update(process.argv.join(' ')) - .digest('hex') - .slice(0, 32); - } -}; - -// SNI Contexts High-Level API -Server.prototype.addContext = function(servername, context) { - if (!servername) { - throw new errors.Error('ERR_TLS_REQUIRED_SERVER_NAME'); - } - - var re = new RegExp('^' + - servername.replace(/([.^$+?\-\\[\]{}])/g, '\\$1') - .replace(/\*/g, '[^.]*') + - '$'); - this._contexts.push([re, tls.createSecureContext(context).context]); -}; - -function SNICallback(servername, callback) { - const contexts = this.server._contexts; - - for (var i = 0; i < contexts.length; i++) { - const elem = contexts[i]; - if (elem[0].test(servername)) { - callback(null, elem[1]); - return; - } - } - - callback(null, undefined); -} - - -// Target API: -// -// var s = tls.connect({port: 8000, host: "google.com"}, function() { -// if (!s.authorized) { -// s.destroy(); -// return; -// } -// -// // s.socket; -// -// s.end("hello world\n"); -// }); -// -// -function normalizeConnectArgs(listArgs) { - var args = net._normalizeArgs(listArgs); - var options = args[0]; - var cb = args[1]; - - // If args[0] was options, then normalize dealt with it. - // If args[0] is port, or args[0], args[1] is host, port, we need to - // find the options and merge them in, normalize's options has only - // the host/port/path args that it knows about, not the tls options. - // This means that options.host overrides a host arg. - if (listArgs[1] !== null && typeof listArgs[1] === 'object') { - util._extend(options, listArgs[1]); - } else if (listArgs[2] !== null && typeof listArgs[2] === 'object') { - util._extend(options, listArgs[2]); - } - - return (cb) ? [options, cb] : [options]; -} - -function onConnectSecure() { - const options = this[kConnectOptions]; - - // Check the size of DHE parameter above minimum requirement - // specified in options. - const ekeyinfo = this.getEphemeralKeyInfo(); - if (ekeyinfo.type === 'DH' && ekeyinfo.size < options.minDHSize) { - const err = new errors.Error('ERR_TLS_DH_PARAM_SIZE', ekeyinfo.size); - this.emit('error', err); - this.destroy(); - return; - } - - let verifyError = this._handle.verifyError(); - - // Verify that server's identity matches it's certificate's names - // Unless server has resumed our existing session - if (!verifyError && !this.isSessionReused()) { - const hostname = options.servername || - options.host || - (options.socket && options.socket._host) || - 'localhost'; - const cert = this.getPeerCertificate(); - verifyError = options.checkServerIdentity(hostname, cert); - } - - if (verifyError) { - this.authorized = false; - this.authorizationError = verifyError.code || verifyError.message; - - if (options.rejectUnauthorized) { - this.destroy(verifyError); - return; - } else { - this.emit('secureConnect'); - } - } else { - this.authorized = true; - this.emit('secureConnect'); - } - - // Uncork incoming data - this.removeListener('end', onConnectEnd); -} - -function onConnectEnd() { - // NOTE: This logic is shared with _http_client.js - if (!this._hadError) { - const options = this[kConnectOptions]; - this._hadError = true; - const error = new Error('socket hang up'); - error.code = 'ECONNRESET'; - error.path = options.path; - error.host = options.host; - error.port = options.port; - error.localAddress = options.localAddress; - this.destroy(error); - } -} - -exports.connect = function(...args /* [port,] [host,] [options,] [cb] */) { - args = normalizeConnectArgs(args); - var options = args[0]; - var cb = args[1]; - - var defaults = { - rejectUnauthorized: '0' !== process.env.NODE_TLS_REJECT_UNAUTHORIZED, - ciphers: tls.DEFAULT_CIPHERS, - checkServerIdentity: tls.checkServerIdentity, - minDHSize: 1024 - }; - - options = util._extend(defaults, options || {}); - if (!options.keepAlive) - options.singleUse = true; - - assert(typeof options.checkServerIdentity === 'function'); - assert(typeof options.minDHSize === 'number', - 'options.minDHSize is not a number: ' + options.minDHSize); - assert(options.minDHSize > 0, - 'options.minDHSize is not a positive number: ' + - options.minDHSize); - - const NPN = {}; - const ALPN = {}; - const context = options.secureContext || tls.createSecureContext(options); - tls.convertNPNProtocols(options.NPNProtocols, NPN); - tls.convertALPNProtocols(options.ALPNProtocols, ALPN); - - var socket = new TLSSocket(options.socket, { - pipe: !!options.path, - secureContext: context, - isServer: false, - requestCert: true, - rejectUnauthorized: options.rejectUnauthorized !== false, - session: options.session, - NPNProtocols: NPN.NPNProtocols, - ALPNProtocols: ALPN.ALPNProtocols, - requestOCSP: options.requestOCSP - }); - - socket[kConnectOptions] = options; - - if (cb) - socket.once('secureConnect', cb); - - if (!options.socket) { - const connectOpt = { - path: options.path, - port: options.port, - host: options.host, - family: options.family, - localAddress: options.localAddress, - lookup: options.lookup - }; - socket.connect(connectOpt, socket._start); - } - - socket._releaseControl(); - - if (options.session) - socket.setSession(options.session); - - if (options.servername) - socket.setServername(options.servername); - - if (options.socket) - socket._start(); - - socket.on('secure', onConnectSecure); - socket.once('end', onConnectEnd); - - return socket; -}; +module.exports = require('internal/tls/wrap'); diff --git a/lib/http.js b/lib/http.js index 701a5ccb86cc21..eea6720b668697 100644 --- a/lib/http.js +++ b/lib/http.js @@ -21,12 +21,12 @@ 'use strict'; -const agent = require('_http_agent'); -const { ClientRequest } = require('_http_client'); -const common = require('_http_common'); -const incoming = require('_http_incoming'); -const outgoing = require('_http_outgoing'); -const server = require('_http_server'); +const agent = require('internal/http/agent'); +const { ClientRequest } = require('internal/http/client'); +const common = require('internal/http/common'); +const incoming = require('internal/http/incoming'); +const outgoing = require('internal/http/outgoing'); +const server = require('internal/http/server'); const { Server } = server; diff --git a/lib/https.js b/lib/https.js index f6e5a533b95084..6e6317f6292302 100644 --- a/lib/https.js +++ b/lib/https.js @@ -26,12 +26,12 @@ require('internal/util').assertCrypto(); const tls = require('tls'); const url = require('url'); const util = require('util'); -const { Agent: HttpAgent } = require('_http_agent'); +const { Agent: HttpAgent } = require('internal/http/agent'); const { Server: HttpServer, _connectionListener -} = require('_http_server'); -const { ClientRequest } = require('_http_client'); +} = require('internal/http/server'); +const { ClientRequest } = require('internal/http/client'); const { inherits } = util; const debug = util.debuglog('https'); const { urlToOptions, searchParamsSymbol } = require('internal/url'); diff --git a/lib/internal/http/agent.js b/lib/internal/http/agent.js new file mode 100644 index 00000000000000..564eab9254387b --- /dev/null +++ b/lib/internal/http/agent.js @@ -0,0 +1,362 @@ +// 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 net = require('net'); +const util = require('util'); +const EventEmitter = require('events'); +const debug = util.debuglog('http'); +const { async_id_symbol } = process.binding('async_wrap'); +const { nextTick } = require('internal/process/next_tick'); + +// New Agent code. + +// The largest departure from the previous implementation is that +// an Agent instance holds connections for a variable number of host:ports. +// Surprisingly, this is still API compatible as far as third parties are +// concerned. The only code that really notices the difference is the +// request object. + +// Another departure is that all code related to HTTP parsing is in +// ClientRequest.onSocket(). The Agent is now *strictly* +// concerned with managing a connection pool. + +function Agent(options) { + if (!(this instanceof Agent)) + return new Agent(options); + + EventEmitter.call(this); + + var self = this; + + self.defaultPort = 80; + self.protocol = 'http:'; + + self.options = util._extend({}, options); + + // don't confuse net and make it think that we're connecting to a pipe + self.options.path = null; + self.requests = {}; + self.sockets = {}; + self.freeSockets = {}; + self.keepAliveMsecs = self.options.keepAliveMsecs || 1000; + self.keepAlive = self.options.keepAlive || false; + self.maxSockets = self.options.maxSockets || Agent.defaultMaxSockets; + self.maxFreeSockets = self.options.maxFreeSockets || 256; + + self.on('free', function(socket, options) { + var name = self.getName(options); + debug('agent.on(free)', name); + + if (socket.writable && + self.requests[name] && self.requests[name].length) { + self.requests[name].shift().onSocket(socket); + if (self.requests[name].length === 0) { + // don't leak + delete self.requests[name]; + } + } else { + // If there are no pending requests, then put it in + // the freeSockets pool, but only if we're allowed to do so. + var req = socket._httpMessage; + if (req && + req.shouldKeepAlive && + socket.writable && + self.keepAlive) { + var freeSockets = self.freeSockets[name]; + var freeLen = freeSockets ? freeSockets.length : 0; + var count = freeLen; + if (self.sockets[name]) + count += self.sockets[name].length; + + if (count > self.maxSockets || freeLen >= self.maxFreeSockets) { + socket.destroy(); + } else if (self.keepSocketAlive(socket)) { + freeSockets = freeSockets || []; + self.freeSockets[name] = freeSockets; + socket[async_id_symbol] = -1; + socket._httpMessage = null; + self.removeSocket(socket, options); + freeSockets.push(socket); + } else { + // Implementation doesn't want to keep socket alive + socket.destroy(); + } + } else { + socket.destroy(); + } + } + }); +} + +util.inherits(Agent, EventEmitter); + +Agent.defaultMaxSockets = Infinity; + +Agent.prototype.createConnection = net.createConnection; + +// Get the key for a given set of request options +Agent.prototype.getName = function getName(options) { + var name = options.host || 'localhost'; + + name += ':'; + if (options.port) + name += options.port; + + name += ':'; + if (options.localAddress) + name += options.localAddress; + + // Pacify parallel/test-http-agent-getname by only appending + // the ':' when options.family is set. + if (options.family === 4 || options.family === 6) + name += ':' + options.family; + + if (options.socketPath) + name += ':' + options.socketPath; + + return name; +}; + +Agent.prototype.addRequest = function addRequest(req, options, port/*legacy*/, + localAddress/*legacy*/) { + // Legacy API: addRequest(req, host, port, localAddress) + if (typeof options === 'string') { + options = { + host: options, + port, + localAddress + }; + } + + options = util._extend({}, options); + util._extend(options, this.options); + if (options.socketPath) + options.path = options.socketPath; + + if (!options.servername) + options.servername = calculateServerName(options, req); + + var name = this.getName(options); + if (!this.sockets[name]) { + this.sockets[name] = []; + } + + var freeLen = this.freeSockets[name] ? this.freeSockets[name].length : 0; + var sockLen = freeLen + this.sockets[name].length; + + if (freeLen) { + // we have a free socket, so use that. + var socket = this.freeSockets[name].shift(); + // Guard against an uninitialized or user supplied Socket. + if (socket._handle && typeof socket._handle.asyncReset === 'function') { + // Assign the handle a new asyncId and run any init() hooks. + socket._handle.asyncReset(); + socket[async_id_symbol] = socket._handle.getAsyncId(); + } + + // don't leak + if (!this.freeSockets[name].length) + delete this.freeSockets[name]; + + this.reuseSocket(socket, req); + req.onSocket(socket); + this.sockets[name].push(socket); + } else if (sockLen < this.maxSockets) { + debug('call onSocket', sockLen, freeLen); + // If we are under maxSockets create a new one. + this.createSocket(req, options, handleSocketCreation(req, true)); + } else { + debug('wait for socket'); + // We are over limit so we'll add it to the queue. + if (!this.requests[name]) { + this.requests[name] = []; + } + this.requests[name].push(req); + } +}; + +Agent.prototype.createSocket = function createSocket(req, options, cb) { + var self = this; + options = util._extend({}, options); + util._extend(options, self.options); + if (options.socketPath) + options.path = options.socketPath; + + if (!options.servername) + options.servername = calculateServerName(options, req); + + var name = self.getName(options); + options._agentKey = name; + + debug('createConnection', name, options); + options.encoding = null; + var called = false; + const newSocket = self.createConnection(options, oncreate); + if (newSocket) + oncreate(null, newSocket); + + function oncreate(err, s) { + if (called) + return; + called = true; + if (err) + return cb(err); + if (!self.sockets[name]) { + self.sockets[name] = []; + } + self.sockets[name].push(s); + debug('sockets', name, self.sockets[name].length); + installListeners(self, s, options); + cb(null, s); + } +}; + +function calculateServerName(options, req) { + let servername = options.host; + const hostHeader = req.getHeader('host'); + if (hostHeader) { + // abc => abc + // abc:123 => abc + // [::1] => ::1 + // [::1]:123 => ::1 + if (hostHeader.startsWith('[')) { + const index = hostHeader.indexOf(']'); + if (index === -1) { + // Leading '[', but no ']'. Need to do something... + servername = hostHeader; + } else { + servername = hostHeader.substr(1, index - 1); + } + } else { + servername = hostHeader.split(':', 1)[0]; + } + } + return servername; +} + +function installListeners(agent, s, options) { + function onFree() { + debug('CLIENT socket onFree'); + agent.emit('free', s, options); + } + s.on('free', onFree); + + function onClose(err) { + debug('CLIENT socket onClose'); + // This is the only place where sockets get removed from the Agent. + // If you want to remove a socket from the pool, just close it. + // All socket errors end in a close event anyway. + agent.removeSocket(s, options); + } + s.on('close', onClose); + + function onRemove() { + // We need this function for cases like HTTP 'upgrade' + // (defined by WebSockets) where we need to remove a socket from the + // pool because it'll be locked up indefinitely + debug('CLIENT socket onRemove'); + agent.removeSocket(s, options); + s.removeListener('close', onClose); + s.removeListener('free', onFree); + s.removeListener('agentRemove', onRemove); + } + s.on('agentRemove', onRemove); +} + +Agent.prototype.removeSocket = function removeSocket(s, options) { + var name = this.getName(options); + debug('removeSocket', name, 'writable:', s.writable); + var sets = [this.sockets]; + + // If the socket was destroyed, remove it from the free buffers too. + if (!s.writable) + sets.push(this.freeSockets); + + for (var sk = 0; sk < sets.length; sk++) { + var sockets = sets[sk]; + + if (sockets[name]) { + var index = sockets[name].indexOf(s); + if (index !== -1) { + sockets[name].splice(index, 1); + // Don't leak + if (sockets[name].length === 0) + delete sockets[name]; + } + } + } + + if (this.requests[name] && this.requests[name].length) { + debug('removeSocket, have a request, make a socket'); + var req = this.requests[name][0]; + // If we have pending requests and a socket gets closed make a new one + this.createSocket(req, options, handleSocketCreation(req, false)); + } +}; + +Agent.prototype.keepSocketAlive = function keepSocketAlive(socket) { + socket.setKeepAlive(true, this.keepAliveMsecs); + socket.unref(); + + return true; +}; + +Agent.prototype.reuseSocket = function reuseSocket(socket, req) { + debug('have free socket'); + socket.ref(); +}; + +Agent.prototype.destroy = function destroy() { + var sets = [this.freeSockets, this.sockets]; + for (var s = 0; s < sets.length; s++) { + var set = sets[s]; + var keys = Object.keys(set); + for (var v = 0; v < keys.length; v++) { + var setName = set[keys[v]]; + for (var n = 0; n < setName.length; n++) { + setName[n].destroy(); + } + } + } +}; + +function handleSocketCreation(request, informRequest) { + return function handleSocketCreation_Inner(err, socket) { + if (err) { + const asyncId = (socket && socket._handle && socket._handle.getAsyncId) ? + socket._handle.getAsyncId() : + null; + nextTick(asyncId, () => request.emit('error', err)); + return; + } + if (informRequest) + request.onSocket(socket); + else + socket.emit('free'); + }; +} + +module.exports = { + Agent, + globalAgent: new Agent() +}; diff --git a/lib/internal/http/client.js b/lib/internal/http/client.js new file mode 100644 index 00000000000000..e61c8196eaab5d --- /dev/null +++ b/lib/internal/http/client.js @@ -0,0 +1,754 @@ +// 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 util = require('util'); +const net = require('net'); +const url = require('url'); +const { HTTPParser } = process.binding('http_parser'); +const assert = require('assert').ok; +const { + _checkIsHttpToken: checkIsHttpToken, + debug, + freeParser, + httpSocketSetup, + parsers +} = require('internal/http/common'); +const { OutgoingMessage } = require('internal/http/outgoing'); +const Agent = require('internal/http/agent'); +const { Buffer } = require('buffer'); +const { urlToOptions, searchParamsSymbol } = require('internal/url'); +const { outHeadersKey } = require('internal/http'); +const { nextTick } = require('internal/process/next_tick'); +const errors = require('internal/errors'); + +// The actual list of disallowed characters in regexp form is more like: +// /[^A-Za-z0-9\-._~!$&'()*+,;=/:@]/ +// with an additional rule for ignoring percentage-escaped characters, but +// that's a) hard to capture in a regular expression that performs well, and +// b) possibly too restrictive for real-world usage. So instead we restrict the +// filter to just control characters and spaces. +// +// This function is used in the case of small paths, where manual character code +// checks can greatly outperform the equivalent regexp (tested in V8 5.4). +function isInvalidPath(s) { + var i = 0; + if (s.charCodeAt(0) <= 32) return true; + if (++i >= s.length) return false; + if (s.charCodeAt(1) <= 32) return true; + if (++i >= s.length) return false; + if (s.charCodeAt(2) <= 32) return true; + if (++i >= s.length) return false; + if (s.charCodeAt(3) <= 32) return true; + if (++i >= s.length) return false; + if (s.charCodeAt(4) <= 32) return true; + if (++i >= s.length) return false; + if (s.charCodeAt(5) <= 32) return true; + ++i; + for (; i < s.length; ++i) + if (s.charCodeAt(i) <= 32) return true; + return false; +} + +function validateHost(host, name) { + if (host != null && typeof host !== 'string') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', `options.${name}`, + ['string', 'undefined', 'null'], host); + } + return host; +} + +function ClientRequest(options, cb) { + OutgoingMessage.call(this); + + if (typeof options === 'string') { + options = url.parse(options); + if (!options.hostname) { + throw new errors.Error('ERR_INVALID_DOMAIN_NAME'); + } + } else if (options && options[searchParamsSymbol] && + options[searchParamsSymbol][searchParamsSymbol]) { + // url.URL instance + options = urlToOptions(options); + } else { + options = util._extend({}, options); + } + + var agent = options.agent; + var defaultAgent = options._defaultAgent || Agent.globalAgent; + if (agent === false) { + agent = new defaultAgent.constructor(); + } else if (agent === null || agent === undefined) { + if (typeof options.createConnection !== 'function') { + agent = defaultAgent; + } + // Explicitly pass through this statement as agent will not be used + // when createConnection is provided. + } else if (typeof agent.addRequest !== 'function') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'Agent option', + ['Agent-like object', 'undefined', 'false']); + } + this.agent = agent; + + var protocol = options.protocol || defaultAgent.protocol; + var expectedProtocol = defaultAgent.protocol; + if (this.agent && this.agent.protocol) + expectedProtocol = this.agent.protocol; + + var path; + if (options.path) { + path = '' + options.path; + var invalidPath; + if (path.length <= 39) { // Determined experimentally in V8 5.4 + invalidPath = isInvalidPath(path); + } else { + invalidPath = /[\u0000-\u0020]/.test(path); + } + if (invalidPath) + throw new errors.TypeError('ERR_UNESCAPED_CHARACTERS', 'Request path'); + } + + if (protocol !== expectedProtocol) { + throw new errors.Error('ERR_INVALID_PROTOCOL', protocol, expectedProtocol); + } + + var defaultPort = options.defaultPort || + this.agent && this.agent.defaultPort; + + var port = options.port = options.port || defaultPort || 80; + var host = options.host = validateHost(options.hostname, 'hostname') || + validateHost(options.host, 'host') || 'localhost'; + + var setHost = (options.setHost === undefined); + + this.socketPath = options.socketPath; + this.timeout = options.timeout; + + var method = options.method; + var methodIsString = (typeof method === 'string'); + if (method != null && !methodIsString) { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'method', + 'string', method); + } + + if (methodIsString && method) { + if (!checkIsHttpToken(method)) { + throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Method', method); + } + method = this.method = method.toUpperCase(); + } else { + method = this.method = 'GET'; + } + + this.path = options.path || '/'; + if (cb) { + this.once('response', cb); + } + + var headersArray = Array.isArray(options.headers); + if (!headersArray) { + if (options.headers) { + var keys = Object.keys(options.headers); + for (var i = 0; i < keys.length; i++) { + var key = keys[i]; + this.setHeader(key, options.headers[key]); + } + } + if (host && !this.getHeader('host') && setHost) { + var hostHeader = host; + + // For the Host header, ensure that IPv6 addresses are enclosed + // in square brackets, as defined by URI formatting + // https://tools.ietf.org/html/rfc3986#section-3.2.2 + var posColon = hostHeader.indexOf(':'); + if (posColon !== -1 && + hostHeader.indexOf(':', posColon + 1) !== -1 && + hostHeader.charCodeAt(0) !== 91/*'['*/) { + hostHeader = `[${hostHeader}]`; + } + + if (port && +port !== defaultPort) { + hostHeader += ':' + port; + } + this.setHeader('Host', hostHeader); + } + } + + if (options.auth && !this.getHeader('Authorization')) { + this.setHeader('Authorization', 'Basic ' + + Buffer.from(options.auth).toString('base64')); + } + + if (method === 'GET' || + method === 'HEAD' || + method === 'DELETE' || + method === 'OPTIONS' || + method === 'CONNECT') { + this.useChunkedEncodingByDefault = false; + } else { + this.useChunkedEncodingByDefault = true; + } + + if (headersArray) { + this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', + options.headers); + } else if (this.getHeader('expect')) { + if (this._header) { + throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'render'); + } + + this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', + this[outHeadersKey]); + } + + this._ended = false; + this.res = null; + this.aborted = undefined; + this.timeoutCb = null; + this.upgradeOrConnect = false; + this.parser = null; + this.maxHeadersCount = null; + + var called = false; + + var oncreate = (err, socket) => { + if (called) + return; + called = true; + if (err) { + process.nextTick(() => this.emit('error', err)); + return; + } + this.onSocket(socket); + this._deferToConnect(null, null, () => this._flush()); + }; + + if (this.agent) { + // If there is an agent we should default to Connection:keep-alive, + // but only if the Agent will actually reuse the connection! + // If it's not a keepAlive agent, and the maxSockets==Infinity, then + // there's never a case where this socket will actually be reused + if (!this.agent.keepAlive && !Number.isFinite(this.agent.maxSockets)) { + this._last = true; + this.shouldKeepAlive = false; + } else { + this._last = false; + this.shouldKeepAlive = true; + } + this.agent.addRequest(this, options); + } else { + // No agent, default to Connection:close. + this._last = true; + this.shouldKeepAlive = false; + if (typeof options.createConnection === 'function') { + const newSocket = options.createConnection(options, oncreate); + if (newSocket && !called) { + called = true; + this.onSocket(newSocket); + } else { + return; + } + } else { + debug('CLIENT use net.createConnection', options); + this.onSocket(net.createConnection(options)); + } + } + + this._deferToConnect(null, null, () => this._flush()); +} + +util.inherits(ClientRequest, OutgoingMessage); + + +ClientRequest.prototype._finish = function _finish() { + DTRACE_HTTP_CLIENT_REQUEST(this, this.connection); + LTTNG_HTTP_CLIENT_REQUEST(this, this.connection); + COUNTER_HTTP_CLIENT_REQUEST(); + OutgoingMessage.prototype._finish.call(this); +}; + +ClientRequest.prototype._implicitHeader = function _implicitHeader() { + if (this._header) { + throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'render'); + } + this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', + this[outHeadersKey]); +}; + +ClientRequest.prototype.abort = function abort() { + if (!this.aborted) { + process.nextTick(emitAbortNT.bind(this)); + } + // Mark as aborting so we can avoid sending queued request data + // This is used as a truthy flag elsewhere. The use of Date.now is for + // debugging purposes only. + this.aborted = Date.now(); + + // If we're aborting, we don't care about any more response data. + if (this.res) { + this.res._dump(); + } else { + this.once('response', function(res) { + res._dump(); + }); + } + + // In the event that we don't have a socket, we will pop out of + // the request queue through handling in onSocket. + if (this.socket) { + // in-progress + this.socket.destroy(); + } +}; + + +function emitAbortNT() { + this.emit('abort'); +} + + +function createHangUpError() { + var error = new Error('socket hang up'); + error.code = 'ECONNRESET'; + return error; +} + + +function socketCloseListener() { + var socket = this; + var req = socket._httpMessage; + debug('HTTP socket close'); + + // Pull through final chunk, if anything is buffered. + // the ondata function will handle it properly, and this + // is a no-op if no final chunk remains. + socket.read(); + + // NOTE: It's important to get parser here, because it could be freed by + // the `socketOnData`. + var parser = socket.parser; + if (req.res && req.res.readable) { + // Socket closed before we emitted 'end' below. + req.res.emit('aborted'); + var res = req.res; + res.on('end', function() { + res.emit('close'); + }); + res.push(null); + } else if (!req.res && !req.socket._hadError) { + // This socket error fired before we started to + // receive a response. The error needs to + // fire on the request. + req.socket._hadError = true; + req.emit('error', createHangUpError()); + } + req.emit('close'); + + // Too bad. That output wasn't getting written. + // This is pretty terrible that it doesn't raise an error. + // Fixed better in v0.10 + if (req.output) + req.output.length = 0; + if (req.outputEncodings) + req.outputEncodings.length = 0; + + if (parser) { + parser.finish(); + freeParser(parser, req, socket); + } +} + +function socketErrorListener(err) { + var socket = this; + var req = socket._httpMessage; + debug('SOCKET ERROR:', err.message, err.stack); + + if (req) { + // For Safety. Some additional errors might fire later on + // and we need to make sure we don't double-fire the error event. + req.socket._hadError = true; + req.emit('error', err); + } + + // Handle any pending data + socket.read(); + + var parser = socket.parser; + if (parser) { + parser.finish(); + freeParser(parser, req, socket); + } + + // Ensure that no further data will come out of the socket + socket.removeListener('data', socketOnData); + socket.removeListener('end', socketOnEnd); + socket.destroy(); +} + +function freeSocketErrorListener(err) { + var socket = this; + debug('SOCKET ERROR on FREE socket:', err.message, err.stack); + socket.destroy(); + socket.emit('agentRemove'); +} + +function socketOnEnd() { + var socket = this; + var req = this._httpMessage; + var parser = this.parser; + + if (!req.res && !req.socket._hadError) { + // If we don't have a response then we know that the socket + // ended prematurely and we need to emit an error on the request. + req.socket._hadError = true; + req.emit('error', createHangUpError()); + } + if (parser) { + parser.finish(); + freeParser(parser, req, socket); + } + socket.destroy(); +} + +function socketOnData(d) { + var socket = this; + var req = this._httpMessage; + var parser = this.parser; + + assert(parser && parser.socket === socket); + + var ret = parser.execute(d); + if (ret instanceof Error) { + debug('parse error', ret); + freeParser(parser, req, socket); + socket.destroy(); + req.socket._hadError = true; + req.emit('error', ret); + } else if (parser.incoming && parser.incoming.upgrade) { + // Upgrade or CONNECT + var bytesParsed = ret; + var res = parser.incoming; + req.res = res; + + socket.removeListener('data', socketOnData); + socket.removeListener('end', socketOnEnd); + parser.finish(); + + var bodyHead = d.slice(bytesParsed, d.length); + + var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; + if (req.listenerCount(eventName) > 0) { + req.upgradeOrConnect = true; + + // detach the socket + socket.emit('agentRemove'); + socket.removeListener('close', socketCloseListener); + socket.removeListener('error', socketErrorListener); + + // TODO(isaacs): Need a way to reset a stream to fresh state + // IE, not flowing, and not explicitly paused. + socket._readableState.flowing = null; + + req.emit(eventName, res, socket, bodyHead); + req.emit('close'); + } else { + // Got Upgrade header or CONNECT method, but have no handler. + socket.destroy(); + } + freeParser(parser, req, socket); + } else if (parser.incoming && parser.incoming.complete && + // When the status code is 100 (Continue), the server will + // send a final response after this client sends a request + // body. So, we must not free the parser. + parser.incoming.statusCode !== 100) { + socket.removeListener('data', socketOnData); + socket.removeListener('end', socketOnEnd); + freeParser(parser, req, socket); + } +} + + +// client +function parserOnIncomingClient(res, shouldKeepAlive) { + var socket = this.socket; + var req = socket._httpMessage; + + + // propagate "domain" setting... + if (req.domain && !res.domain) { + debug('setting "res.domain"'); + res.domain = req.domain; + } + + debug('AGENT incoming response!'); + + if (req.res) { + // We already have a response object, this means the server + // sent a double response. + socket.destroy(); + return; + } + req.res = res; + + // Responses to CONNECT request is handled as Upgrade. + if (req.method === 'CONNECT') { + res.upgrade = true; + return 2; // skip body, and the rest + } + + // Responses to HEAD requests are crazy. + // HEAD responses aren't allowed to have an entity-body + // but *can* have a content-length which actually corresponds + // to the content-length of the entity-body had the request + // been a GET. + var isHeadResponse = req.method === 'HEAD'; + debug('AGENT isHeadResponse', isHeadResponse); + + if (res.statusCode === 100) { + // restart the parser, as this is a continue message. + req.res = null; // Clear res so that we don't hit double-responses. + req.emit('continue'); + return true; + } + + if (req.shouldKeepAlive && !shouldKeepAlive && !req.upgradeOrConnect) { + // Server MUST respond with Connection:keep-alive for us to enable it. + // If we've been upgraded (via WebSockets) we also shouldn't try to + // keep the connection open. + req.shouldKeepAlive = false; + } + + + DTRACE_HTTP_CLIENT_RESPONSE(socket, req); + LTTNG_HTTP_CLIENT_RESPONSE(socket, req); + COUNTER_HTTP_CLIENT_RESPONSE(); + req.res = res; + res.req = req; + + // add our listener first, so that we guarantee socket cleanup + res.on('end', responseOnEnd); + req.on('prefinish', requestOnPrefinish); + var handled = req.emit('response', res); + + // If the user did not listen for the 'response' event, then they + // can't possibly read the data, so we ._dump() it into the void + // so that the socket doesn't hang there in a paused state. + if (!handled) + res._dump(); + + return isHeadResponse; +} + +// client +function responseKeepAlive(res, req) { + var socket = req.socket; + + if (!req.shouldKeepAlive) { + if (socket.writable) { + debug('AGENT socket.destroySoon()'); + if (typeof socket.destroySoon === 'function') + socket.destroySoon(); + else + socket.end(); + } + assert(!socket.writable); + } else { + debug('AGENT socket keep-alive'); + if (req.timeoutCb) { + socket.setTimeout(0, req.timeoutCb); + req.timeoutCb = null; + } + socket.removeListener('close', socketCloseListener); + socket.removeListener('error', socketErrorListener); + socket.once('error', freeSocketErrorListener); + // There are cases where _handle === null. Avoid those. Passing null to + // nextTick() will call initTriggerId() to retrieve the id. + const asyncId = socket._handle ? socket._handle.getAsyncId() : null; + // Mark this socket as available, AFTER user-added end + // handlers have a chance to run. + nextTick(asyncId, emitFreeNT, socket); + } +} + +function responseOnEnd() { + const res = this; + const req = this.req; + + req._ended = true; + if (!req.shouldKeepAlive || req.finished) + responseKeepAlive(res, req); +} + +function requestOnPrefinish() { + const req = this; + const res = this.res; + + if (!req.shouldKeepAlive) + return; + + if (req._ended) + responseKeepAlive(res, req); +} + +function emitFreeNT(socket) { + socket.emit('free'); +} + +function tickOnSocket(req, socket) { + var parser = parsers.alloc(); + req.socket = socket; + req.connection = socket; + parser.reinitialize(HTTPParser.RESPONSE); + parser.socket = socket; + parser.incoming = null; + parser.outgoing = req; + req.parser = parser; + + socket.parser = parser; + socket._httpMessage = req; + + // Setup "drain" propagation. + httpSocketSetup(socket); + + // Propagate headers limit from request object to parser + if (typeof req.maxHeadersCount === 'number') { + parser.maxHeaderPairs = req.maxHeadersCount << 1; + } else { + // Set default value because parser may be reused from FreeList + parser.maxHeaderPairs = 2000; + } + + parser.onIncoming = parserOnIncomingClient; + socket.removeListener('error', freeSocketErrorListener); + socket.on('error', socketErrorListener); + socket.on('data', socketOnData); + socket.on('end', socketOnEnd); + socket.on('close', socketCloseListener); + + if (req.timeout) { + const emitRequestTimeout = () => req.emit('timeout'); + socket.once('timeout', emitRequestTimeout); + req.once('response', (res) => { + res.once('end', () => { + socket.removeListener('timeout', emitRequestTimeout); + }); + }); + } + req.emit('socket', socket); +} + +ClientRequest.prototype.onSocket = function onSocket(socket) { + process.nextTick(onSocketNT, this, socket); +}; + +function onSocketNT(req, socket) { + if (req.aborted) { + // If we were aborted while waiting for a socket, skip the whole thing. + if (!req.agent) { + socket.destroy(); + } else { + socket.emit('free'); + } + } else { + tickOnSocket(req, socket); + } +} + +ClientRequest.prototype._deferToConnect = _deferToConnect; +function _deferToConnect(method, arguments_, cb) { + // This function is for calls that need to happen once the socket is + // connected and writable. It's an important promisy thing for all the socket + // calls that happen either now (when a socket is assigned) or + // in the future (when a socket gets assigned out of the pool and is + // eventually writable). + + const callSocketMethod = () => { + if (method) + this.socket[method].apply(this.socket, arguments_); + + if (typeof cb === 'function') + cb(); + }; + + const onSocket = () => { + if (this.socket.writable) { + callSocketMethod(); + } else { + this.socket.once('connect', callSocketMethod); + } + }; + + if (!this.socket) { + this.once('socket', onSocket); + } else { + onSocket(); + } +} + +ClientRequest.prototype.setTimeout = function setTimeout(msecs, callback) { + if (callback) this.once('timeout', callback); + + const emitTimeout = () => this.emit('timeout'); + + if (this.socket && this.socket.writable) { + if (this.timeoutCb) + this.socket.setTimeout(0, this.timeoutCb); + this.timeoutCb = emitTimeout; + this.socket.setTimeout(msecs, emitTimeout); + return this; + } + + // Set timeoutCb so that it'll get cleaned up on request end + this.timeoutCb = emitTimeout; + if (this.socket) { + var sock = this.socket; + this.socket.once('connect', function() { + sock.setTimeout(msecs, emitTimeout); + }); + return this; + } + + this.once('socket', function(sock) { + sock.once('connect', function() { + sock.setTimeout(msecs, emitTimeout); + }); + }); + + return this; +}; + +ClientRequest.prototype.setNoDelay = function setNoDelay(noDelay) { + this._deferToConnect('setNoDelay', [noDelay]); +}; + +ClientRequest.prototype.setSocketKeepAlive = + function setSocketKeepAlive(enable, initialDelay) { + this._deferToConnect('setKeepAlive', [enable, initialDelay]); + }; + +ClientRequest.prototype.clearTimeout = function clearTimeout(cb) { + this.setTimeout(0, cb); +}; + +module.exports = { + ClientRequest +}; diff --git a/lib/internal/http/common.js b/lib/internal/http/common.js new file mode 100644 index 00000000000000..7b6b1666c98c81 --- /dev/null +++ b/lib/internal/http/common.js @@ -0,0 +1,368 @@ +// 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 binding = process.binding('http_parser'); +const { methods, HTTPParser } = binding; + +const FreeList = require('internal/freelist'); +const { ondrain } = require('internal/http'); +const incoming = require('internal/http/incoming'); +const { emitDestroy } = require('async_hooks'); +const { + IncomingMessage, + readStart, + readStop +} = incoming; + +const debug = require('util').debuglog('http'); + +const kOnHeaders = HTTPParser.kOnHeaders | 0; +const kOnHeadersComplete = HTTPParser.kOnHeadersComplete | 0; +const kOnBody = HTTPParser.kOnBody | 0; +const kOnMessageComplete = HTTPParser.kOnMessageComplete | 0; +const kOnExecute = HTTPParser.kOnExecute | 0; + +// Only called in the slow case where slow means +// that the request headers were either fragmented +// across multiple TCP packets or too large to be +// processed in a single run. This method is also +// called to process trailing HTTP headers. +function parserOnHeaders(headers, url) { + // Once we exceeded headers limit - stop collecting them + if (this.maxHeaderPairs <= 0 || + this._headers.length < this.maxHeaderPairs) { + this._headers = this._headers.concat(headers); + } + this._url += url; +} + +// `headers` and `url` are set only if .onHeaders() has not been called for +// this request. +// `url` is not set for response parsers but that's not applicable here since +// all our parsers are request parsers. +function parserOnHeadersComplete(versionMajor, versionMinor, headers, method, + url, statusCode, statusMessage, upgrade, + shouldKeepAlive) { + var parser = this; + + if (!headers) { + headers = parser._headers; + parser._headers = []; + } + + if (!url) { + url = parser._url; + parser._url = ''; + } + + parser.incoming = new IncomingMessage(parser.socket); + parser.incoming.httpVersionMajor = versionMajor; + parser.incoming.httpVersionMinor = versionMinor; + parser.incoming.httpVersion = versionMajor + '.' + versionMinor; + parser.incoming.url = url; + + var n = headers.length; + + // If parser.maxHeaderPairs <= 0 assume that there's no limit. + if (parser.maxHeaderPairs > 0) + n = Math.min(n, parser.maxHeaderPairs); + + parser.incoming._addHeaderLines(headers, n); + + if (typeof method === 'number') { + // server only + parser.incoming.method = methods[method]; + } else { + // client only + parser.incoming.statusCode = statusCode; + parser.incoming.statusMessage = statusMessage; + } + + if (upgrade && parser.outgoing !== null && !parser.outgoing.upgrading) { + // The client made non-upgrade request, and server is just advertising + // supported protocols. + // + // See RFC7230 Section 6.7 + upgrade = false; + } + + parser.incoming.upgrade = upgrade; + + var skipBody = 0; // response to HEAD or CONNECT + + if (!upgrade) { + // For upgraded connections and CONNECT method request, we'll emit this + // after parser.execute so that we can capture the first part of the new + // protocol. + skipBody = parser.onIncoming(parser.incoming, shouldKeepAlive); + } + + if (typeof skipBody !== 'number') + return skipBody ? 1 : 0; + else + return skipBody; +} + +// XXX This is a mess. +// TODO: http.Parser should be a Writable emits request/response events. +function parserOnBody(b, start, len) { + var parser = this; + var stream = parser.incoming; + + // if the stream has already been removed, then drop it. + if (!stream) + return; + + var socket = stream.socket; + + // pretend this was the result of a stream._read call. + if (len > 0 && !stream._dumped) { + var slice = b.slice(start, start + len); + var ret = stream.push(slice); + if (!ret) + readStop(socket); + } +} + +function parserOnMessageComplete() { + var parser = this; + var stream = parser.incoming; + + if (stream) { + stream.complete = true; + // Emit any trailing headers. + var headers = parser._headers; + if (headers) { + parser.incoming._addHeaderLines(headers, headers.length); + parser._headers = []; + parser._url = ''; + } + + // For emit end event + stream.push(null); + } + + // force to read the next incoming message + readStart(parser.socket); +} + + +var parsers = new FreeList('parsers', 1000, function() { + var parser = new HTTPParser(HTTPParser.REQUEST); + + parser._headers = []; + parser._url = ''; + parser._consumed = false; + + parser.socket = null; + parser.incoming = null; + parser.outgoing = null; + + // Only called in the slow case where slow means + // that the request headers were either fragmented + // across multiple TCP packets or too large to be + // processed in a single run. This method is also + // called to process trailing HTTP headers. + parser[kOnHeaders] = parserOnHeaders; + parser[kOnHeadersComplete] = parserOnHeadersComplete; + parser[kOnBody] = parserOnBody; + parser[kOnMessageComplete] = parserOnMessageComplete; + parser[kOnExecute] = null; + + return parser; +}); + + +// Free the parser and also break any links that it +// might have to any other things. +// TODO: All parser data should be attached to a +// single object, so that it can be easily cleaned +// up by doing `parser.data = {}`, which should +// be done in FreeList.free. `parsers.free(parser)` +// should be all that is needed. +function freeParser(parser, req, socket) { + if (parser) { + parser._headers = []; + parser.onIncoming = null; + if (parser._consumed) + parser.unconsume(); + parser._consumed = false; + if (parser.socket) + parser.socket.parser = null; + parser.socket = null; + parser.incoming = null; + parser.outgoing = null; + parser[kOnExecute] = null; + if (parsers.free(parser) === false) { + parser.close(); + } else { + // Since the Parser destructor isn't going to run the destroy() callbacks + // it needs to be triggered manually. + emitDestroy(parser.getAsyncId()); + } + } + if (req) { + req.parser = null; + } + if (socket) { + socket.parser = null; + } +} + + +function httpSocketSetup(socket) { + socket.removeListener('drain', ondrain); + socket.on('drain', ondrain); +} + +/** + * Verifies that the given val is a valid HTTP token + * per the rules defined in RFC 7230 + * See https://tools.ietf.org/html/rfc7230#section-3.2.6 + * + * Allowed characters in an HTTP token: + * ^_`a-z 94-122 + * A-Z 65-90 + * - 45 + * 0-9 48-57 + * ! 33 + * #$%&' 35-39 + * *+ 42-43 + * . 46 + * | 124 + * ~ 126 + * + * This implementation of checkIsHttpToken() loops over the string instead of + * using a regular expression since the former is up to 180% faster with v8 4.9 + * depending on the string length (the shorter the string, the larger the + * performance difference) + * + * Additionally, checkIsHttpToken() is currently designed to be inlinable by v8, + * so take care when making changes to the implementation so that the source + * code size does not exceed v8's default max_inlined_source_size setting. + **/ +var validTokens = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63 + 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0, // 112 - 127 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 128 ... + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 // ... 255 +]; +function checkIsHttpToken(val) { + if (!validTokens[val.charCodeAt(0)]) + return false; + if (val.length < 2) + return true; + if (!validTokens[val.charCodeAt(1)]) + return false; + if (val.length < 3) + return true; + if (!validTokens[val.charCodeAt(2)]) + return false; + if (val.length < 4) + return true; + if (!validTokens[val.charCodeAt(3)]) + return false; + for (var i = 4; i < val.length; ++i) { + if (!validTokens[val.charCodeAt(i)]) + return false; + } + return true; +} + +/** + * True if val contains an invalid field-vchar + * field-value = *( field-content / obs-fold ) + * field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] + * field-vchar = VCHAR / obs-text + * + * checkInvalidHeaderChar() is currently designed to be inlinable by v8, + * so take care when making changes to the implementation so that the source + * code size does not exceed v8's default max_inlined_source_size setting. + **/ +var validHdrChars = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, // 0 - 15 + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 32 - 47 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 48 - 63 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 80 - 95 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, // 112 - 127 + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 128 ... + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 // ... 255 +]; +function checkInvalidHeaderChar(val) { + val += ''; + if (val.length < 1) + return false; + if (!validHdrChars[val.charCodeAt(0)]) + return true; + if (val.length < 2) + return false; + if (!validHdrChars[val.charCodeAt(1)]) + return true; + if (val.length < 3) + return false; + if (!validHdrChars[val.charCodeAt(2)]) + return true; + if (val.length < 4) + return false; + if (!validHdrChars[val.charCodeAt(3)]) + return true; + for (var i = 4; i < val.length; ++i) { + if (!validHdrChars[val.charCodeAt(i)]) + return true; + } + return false; +} + +module.exports = { + _checkInvalidHeaderChar: checkInvalidHeaderChar, + _checkIsHttpToken: checkIsHttpToken, + chunkExpression: /(?:^|\W)chunked(?:$|\W)/i, + continueExpression: /(?:^|\W)100-continue(?:$|\W)/i, + CRLF: '\r\n', + debug, + freeParser, + httpSocketSetup, + methods, + parsers +}; diff --git a/lib/internal/http/incoming.js b/lib/internal/http/incoming.js new file mode 100644 index 00000000000000..696fcc3b4ce53d --- /dev/null +++ b/lib/internal/http/incoming.js @@ -0,0 +1,327 @@ +// 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 util = require('util'); +const Stream = require('stream'); + +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) { + Stream.Readable.call(this); + + // Set this to `true` so that stream.Readable won't attempt to read more + // data on `IncomingMessage#push` (see `maybeReadMore` in + // `_stream_readable.js`). This is important for proper tracking of + // `IncomingMessage#_consuming` which is used to dump requests that users + // haven't attempted to read. + this._readableState.readingMore = true; + + this.socket = socket; + this.connection = socket; + + this.httpVersionMajor = null; + this.httpVersionMinor = null; + this.httpVersion = null; + this.complete = false; + this.headers = {}; + this.rawHeaders = []; + this.trailers = {}; + this.rawTrailers = []; + + this.readable = true; + + this.upgrade = null; + + // request (server) only + this.url = ''; + this.method = null; + + // response (client) only + this.statusCode = null; + this.statusMessage = null; + this.client = socket; + + // flag for backwards compatibility grossness. + 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; +} +util.inherits(IncomingMessage, Stream.Readable); + + +IncomingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { + if (callback) + this.on('timeout', callback); + this.socket.setTimeout(msecs); + return this; +}; + + +IncomingMessage.prototype.read = function read(n) { + if (!this._consuming) + this._readableState.readingMore = false; + this._consuming = true; + this.read = Stream.Readable.prototype.read; + return this.read(n); +}; + + +IncomingMessage.prototype._read = function _read(n) { + // 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(error) { + if (this.socket) + this.socket.destroy(error); +}; + + +IncomingMessage.prototype._addHeaderLines = _addHeaderLines; +function _addHeaderLines(headers, n) { + if (headers && headers.length) { + var dest; + if (this.complete) { + this.rawTrailers = headers; + dest = this.trailers; + } else { + this.rawHeaders = headers; + dest = this.headers; + } + + for (var 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. + +// 'array' header list is taken from: +// https://mxr.mozilla.org/mozilla/source/netwerk/protocol/http/src/nsHttpHeaderArray.cpp +function matchKnownFields(field) { + var low = false; + while (true) { + switch (field) { + case 'Content-Type': + case 'content-type': + return 'content-type'; + case 'Content-Length': + case 'content-length': + return 'content-length'; + case 'User-Agent': + case 'user-agent': + return 'user-agent'; + case 'Referer': + case 'referer': + return 'referer'; + case 'Host': + case 'host': + return 'host'; + case 'Authorization': + case 'authorization': + return 'authorization'; + case 'Proxy-Authorization': + case 'proxy-authorization': + return 'proxy-authorization'; + case 'If-Modified-Since': + case 'if-modified-since': + return 'if-modified-since'; + case 'If-Unmodified-Since': + case 'if-unmodified-since': + return 'if-unmodified-since'; + case 'From': + case 'from': + return 'from'; + case 'Location': + case 'location': + return 'location'; + case 'Max-Forwards': + case 'max-forwards': + return 'max-forwards'; + case 'Retry-After': + case 'retry-after': + return 'retry-after'; + case 'ETag': + case 'etag': + return 'etag'; + case 'Last-Modified': + case 'last-modified': + return 'last-modified'; + case 'Server': + case 'server': + return 'server'; + case 'Age': + case 'age': + return 'age'; + case 'Expires': + case 'expires': + return 'expires'; + case 'Set-Cookie': + case 'set-cookie': + return '\u0001'; + case 'Cookie': + case 'cookie': + return '\u0002cookie'; + // The fields below are not used in _addHeaderLine(), but they are common + // headers where we can avoid toLowerCase() if the mixed or lower case + // versions match the first time through. + case 'Transfer-Encoding': + case 'transfer-encoding': + return '\u0000transfer-encoding'; + case 'Date': + case 'date': + return '\u0000date'; + case 'Connection': + case 'connection': + return '\u0000connection'; + case 'Cache-Control': + case 'cache-control': + return '\u0000cache-control'; + case 'Vary': + case 'vary': + return '\u0000vary'; + case 'Content-Encoding': + case 'content-encoding': + return '\u0000content-encoding'; + case 'Origin': + case 'origin': + return '\u0000origin'; + case 'Upgrade': + case 'upgrade': + return '\u0000upgrade'; + case 'Expect': + case 'expect': + return '\u0000expect'; + case 'If-Match': + case 'if-match': + return '\u0000if-match'; + case 'If-None-Match': + case 'if-none-match': + return '\u0000if-none-match'; + case 'Accept': + case 'accept': + return '\u0000accept'; + case 'Accept-Encoding': + case 'accept-encoding': + return '\u0000accept-encoding'; + case 'Accept-Language': + case 'accept-language': + return '\u0000accept-language'; + case 'X-Forwarded-For': + case 'x-forwarded-for': + return '\u0000x-forwarded-for'; + case 'X-Forwarded-Host': + case 'x-forwarded-host': + return '\u0000x-forwarded-host'; + case 'X-Forwarded-Proto': + case 'x-forwarded-proto': + return '\u0000x-forwarded-proto'; + default: + if (low) + return '\u0000' + field; + field = field.toLowerCase(); + low = 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); + var 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 (dest[field] === undefined) { + // Drop duplicates + dest[field] = 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(); + } +}; + +module.exports = { + IncomingMessage, + readStart, + readStop +}; diff --git a/lib/internal/http/outgoing.js b/lib/internal/http/outgoing.js new file mode 100644 index 00000000000000..e8c4759420ae0e --- /dev/null +++ b/lib/internal/http/outgoing.js @@ -0,0 +1,890 @@ +// 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 assert = require('assert').ok; +const Stream = require('stream'); +const util = require('util'); +const internalUtil = require('internal/util'); +const internalHttp = require('internal/http'); +const { Buffer } = require('buffer'); +const common = require('internal/http/common'); +const checkIsHttpToken = common._checkIsHttpToken; +const checkInvalidHeaderChar = common._checkInvalidHeaderChar; +const { outHeadersKey } = require('internal/http'); +const { async_id_symbol } = process.binding('async_wrap'); +const { nextTick } = require('internal/process/next_tick'); +const errors = require('internal/errors'); + +const { CRLF, debug } = common; +const { utcDate } = internalHttp; + +var RE_FIELDS = + /^(?:Connection|Transfer-Encoding|Content-Length|Date|Expect|Trailer|Upgrade)$/i; +var RE_CONN_VALUES = /(?:^|\W)close|upgrade(?:$|\W)/ig; +var RE_TE_CHUNKED = common.chunkExpression; + +// isCookieField performs a case-insensitive comparison of a provided string +// against the word "cookie." This method (at least as of V8 5.4) is faster than +// the equivalent case-insensitive regexp, even if isCookieField does not get +// inlined. +function isCookieField(s) { + if (s.length !== 6) return false; + var ch = s.charCodeAt(0); + if (ch !== 99 && ch !== 67) return false; + ch = s.charCodeAt(1); + if (ch !== 111 && ch !== 79) return false; + ch = s.charCodeAt(2); + if (ch !== 111 && ch !== 79) return false; + ch = s.charCodeAt(3); + if (ch !== 107 && ch !== 75) return false; + ch = s.charCodeAt(4); + if (ch !== 105 && ch !== 73) return false; + ch = s.charCodeAt(5); + if (ch !== 101 && ch !== 69) return false; + return true; +} + +function noopPendingOutput(amount) {} + +function OutgoingMessage() { + Stream.call(this); + + // Queue that holds all currently pending data, until the response will be + // assigned to the socket (until it will its turn in the HTTP pipeline). + this.output = []; + this.outputEncodings = []; + this.outputCallbacks = []; + + // `outputSize` is an approximate measure of how much data is queued on this + // response. `_onPendingData` will be invoked to update similar global + // per-connection counter. That counter will be used to pause/unpause the + // TCP socket and HTTP Parser and thus handle the backpressure. + this.outputSize = 0; + + this.writable = true; + + this._last = false; + this.upgrading = false; + this.chunkedEncoding = false; + this.shouldKeepAlive = true; + this.useChunkedEncodingByDefault = true; + this.sendDate = false; + this._removedConnection = false; + this._removedContLen = false; + this._removedTE = false; + + this._contentLength = null; + this._hasBody = true; + this._trailer = ''; + + this.finished = false; + this._headerSent = false; + + this.socket = null; + this.connection = null; + this._header = null; + this[outHeadersKey] = null; + + this._onPendingData = noopPendingOutput; +} +util.inherits(OutgoingMessage, Stream); + + +Object.defineProperty(OutgoingMessage.prototype, '_headers', { + get: function() { + return this.getHeaders(); + }, + set: function(val) { + if (val == null) { + this[outHeadersKey] = null; + } else if (typeof val === 'object') { + const headers = this[outHeadersKey] = {}; + const keys = Object.keys(val); + for (var i = 0; i < keys.length; ++i) { + const name = keys[i]; + headers[name.toLowerCase()] = [name, val[name]]; + } + } + } +}); + +Object.defineProperty(OutgoingMessage.prototype, '_headerNames', { + get: function() { + const headers = this[outHeadersKey]; + if (headers) { + const out = Object.create(null); + const keys = Object.keys(headers); + for (var i = 0; i < keys.length; ++i) { + const key = keys[i]; + const val = headers[key][0]; + out[key] = val; + } + return out; + } else { + return headers; + } + }, + set: function(val) { + if (typeof val === 'object' && val !== null) { + const headers = this[outHeadersKey]; + if (!headers) + return; + const keys = Object.keys(val); + for (var i = 0; i < keys.length; ++i) { + const header = headers[keys[i]]; + if (header) + header[0] = val[keys[i]]; + } + } + } +}); + + +OutgoingMessage.prototype._renderHeaders = function _renderHeaders() { + if (this._header) { + throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'render'); + } + + var headersMap = this[outHeadersKey]; + if (!headersMap) return {}; + + var headers = {}; + var keys = Object.keys(headersMap); + + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + headers[headersMap[key][0]] = headersMap[key][1]; + } + return headers; +}; + + +exports.OutgoingMessage = OutgoingMessage; + + +OutgoingMessage.prototype.setTimeout = function setTimeout(msecs, callback) { + + if (callback) { + this.on('timeout', callback); + } + + if (!this.socket) { + this.once('socket', function(socket) { + socket.setTimeout(msecs); + }); + } else { + this.socket.setTimeout(msecs); + } + return this; +}; + + +// 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. +OutgoingMessage.prototype.destroy = function destroy(error) { + if (this.socket) { + this.socket.destroy(error); + } else { + this.once('socket', function(socket) { + socket.destroy(error); + }); + } +}; + + +// This abstract either writing directly to the socket or buffering it. +OutgoingMessage.prototype._send = function _send(data, encoding, callback) { + // This is a shameful hack to get the headers and first body chunk onto + // the same packet. Future versions of Node are going to take care of + // this at a lower level and in a more general way. + if (!this._headerSent) { + if (typeof data === 'string' && + (encoding === 'utf8' || encoding === 'latin1' || !encoding)) { + data = this._header + data; + } else { + var header = this._header; + if (this.output.length === 0) { + this.output = [header]; + this.outputEncodings = ['latin1']; + this.outputCallbacks = [null]; + } else { + this.output.unshift(header); + this.outputEncodings.unshift('latin1'); + this.outputCallbacks.unshift(null); + } + this.outputSize += header.length; + this._onPendingData(header.length); + } + this._headerSent = true; + } + return this._writeRaw(data, encoding, callback); +}; + + +OutgoingMessage.prototype._writeRaw = _writeRaw; +function _writeRaw(data, encoding, callback) { + const conn = this.connection; + if (conn && conn.destroyed) { + // The socket was destroyed. If we're still trying to write to it, + // then we haven't gotten the 'close' event yet. + return false; + } + + if (typeof encoding === 'function') { + callback = encoding; + encoding = null; + } + + if (conn && conn._httpMessage === this && conn.writable && !conn.destroyed) { + // There might be pending data in the this.output buffer. + if (this.output.length) { + this._flushOutput(conn); + } else if (!data.length) { + if (typeof callback === 'function') { + let socketAsyncId = this.socket[async_id_symbol]; + // If the socket was set directly it won't be correctly initialized + // with an async_id_symbol. + // TODO(AndreasMadsen): @trevnorris suggested some more correct + // solutions in: + // https://github.com/nodejs/node/pull/14389/files#r128522202 + if (socketAsyncId === undefined) socketAsyncId = null; + + nextTick(socketAsyncId, callback); + } + return true; + } + // Directly write to socket. + return conn.write(data, encoding, callback); + } + // Buffer, as long as we're not destroyed. + this.output.push(data); + this.outputEncodings.push(encoding); + this.outputCallbacks.push(callback); + this.outputSize += data.length; + this._onPendingData(data.length); + return false; +} + + +OutgoingMessage.prototype._storeHeader = _storeHeader; +function _storeHeader(firstLine, headers) { + // firstLine in the case of request is: 'GET /index.html HTTP/1.1\r\n' + // in the case of response it is: 'HTTP/1.1 200 OK\r\n' + var state = { + connection: false, + connUpgrade: false, + contLen: false, + te: false, + date: false, + expect: false, + trailer: false, + upgrade: false, + header: firstLine + }; + + var field; + var key; + var value; + var i; + var j; + if (headers === this[outHeadersKey]) { + for (key in headers) { + var entry = headers[key]; + field = entry[0]; + value = entry[1]; + + if (value instanceof Array) { + if (value.length < 2 || !isCookieField(field)) { + for (j = 0; j < value.length; j++) + storeHeader(this, state, field, value[j], false); + continue; + } + value = value.join('; '); + } + storeHeader(this, state, field, value, false); + } + } else if (headers instanceof Array) { + for (i = 0; i < headers.length; i++) { + field = headers[i][0]; + value = headers[i][1]; + + if (value instanceof Array) { + for (j = 0; j < value.length; j++) { + storeHeader(this, state, field, value[j], true); + } + } else { + storeHeader(this, state, field, value, true); + } + } + } else if (headers) { + var keys = Object.keys(headers); + for (i = 0; i < keys.length; i++) { + field = keys[i]; + value = headers[field]; + + if (value instanceof Array) { + if (value.length < 2 || !isCookieField(field)) { + for (j = 0; j < value.length; j++) + storeHeader(this, state, field, value[j], true); + continue; + } + value = value.join('; '); + } + storeHeader(this, state, field, value, true); + } + } + + // Are we upgrading the connection? + if (state.connUpgrade && state.upgrade) + this.upgrading = true; + + // Date header + if (this.sendDate && !state.date) { + state.header += 'Date: ' + utcDate() + CRLF; + } + + // Force the connection to close when the response is a 204 No Content or + // a 304 Not Modified and the user has set a "Transfer-Encoding: chunked" + // header. + // + // RFC 2616 mandates that 204 and 304 responses MUST NOT have a body but + // node.js used to send out a zero chunk anyway to accommodate clients + // that don't have special handling for those responses. + // + // It was pointed out that this might confuse reverse proxies to the point + // of creating security liabilities, so suppress the zero chunk and force + // the connection to close. + var statusCode = this.statusCode; + if ((statusCode === 204 || statusCode === 304) && this.chunkedEncoding) { + debug(statusCode + ' response should not use chunked encoding,' + + ' closing connection.'); + this.chunkedEncoding = false; + this.shouldKeepAlive = false; + } + + // keep-alive logic + if (this._removedConnection) { + this._last = true; + this.shouldKeepAlive = false; + } else if (!state.connection) { + var shouldSendKeepAlive = this.shouldKeepAlive && + (state.contLen || this.useChunkedEncodingByDefault || this.agent); + if (shouldSendKeepAlive) { + state.header += 'Connection: keep-alive\r\n'; + } else { + this._last = true; + state.header += 'Connection: close\r\n'; + } + } + + if (!state.contLen && !state.te) { + if (!this._hasBody) { + // Make sure we don't end the 0\r\n\r\n at the end of the message. + this.chunkedEncoding = false; + } else if (!this.useChunkedEncodingByDefault) { + this._last = true; + } else if (!state.trailer && + !this._removedContLen && + typeof this._contentLength === 'number') { + state.header += 'Content-Length: ' + this._contentLength + CRLF; + } else if (!this._removedTE) { + state.header += 'Transfer-Encoding: chunked\r\n'; + this.chunkedEncoding = true; + } else { + // We should only be able to get here if both Content-Length and + // Transfer-Encoding are removed by the user. + // See: test/parallel/test-http-remove-header-stays-removed.js + debug('Both Content-Length and Transfer-Encoding are removed'); + } + } + + // Test non-chunked message does not have trailer header set, + // message will be terminated by the first empty line after the + // header fields, regardless of the header fields present in the + // message, and thus cannot contain a message body or 'trailers'. + if (this.chunkedEncoding !== true && state.trailer) { + throw new errors.Error('ERR_HTTP_TRAILER_INVALID'); + } + + this._header = state.header + CRLF; + this._headerSent = false; + + // wait until the first body chunk, or close(), is sent to flush, + // UNLESS we're sending Expect: 100-continue. + if (state.expect) this._send(''); +} + +function storeHeader(self, state, key, value, validate) { + if (validate) { + if (typeof key !== 'string' || !key || !checkIsHttpToken(key)) { + throw new errors.TypeError( + 'ERR_INVALID_HTTP_TOKEN', 'Header name', key); + } + if (value === undefined) { + throw new errors.TypeError('ERR_MISSING_ARGS', `header "${key}"`); + } else if (checkInvalidHeaderChar(value)) { + debug('Header "%s" contains invalid characters', key); + throw new errors.TypeError('ERR_INVALID_CHAR', 'header content', key); + } + } + state.header += key + ': ' + escapeHeaderValue(value) + CRLF; + matchHeader(self, state, key, value); +} + +function matchConnValue(self, state, value) { + var sawClose = false; + var m = RE_CONN_VALUES.exec(value); + while (m) { + if (m[0].length === 5) + sawClose = true; + else + state.connUpgrade = true; + m = RE_CONN_VALUES.exec(value); + } + if (sawClose) + self._last = true; + else + self.shouldKeepAlive = true; +} + +function matchHeader(self, state, field, value) { + var m = RE_FIELDS.exec(field); + if (!m) + return; + var len = m[0].length; + if (len === 10) { + state.connection = true; + matchConnValue(self, state, value); + } else if (len === 17) { + state.te = true; + if (RE_TE_CHUNKED.test(value)) self.chunkedEncoding = true; + } else if (len === 14) { + state.contLen = true; + } else if (len === 4) { + state.date = true; + } else if (len === 6) { + state.expect = true; + } else if (len === 7) { + var ch = m[0].charCodeAt(0); + if (ch === 85 || ch === 117) + state.upgrade = true; + else + state.trailer = true; + } +} + +function validateHeader(msg, name, value) { + if (typeof name !== 'string' || !name || !checkIsHttpToken(name)) + throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Header name', name); + if (value === undefined) + throw new errors.TypeError('ERR_MISSING_ARGS', 'value'); + if (msg._header) + throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'set'); + if (checkInvalidHeaderChar(value)) { + debug('Header "%s" contains invalid characters', name); + throw new errors.TypeError('ERR_INVALID_CHAR', 'header content', name); + } +} +OutgoingMessage.prototype.setHeader = function setHeader(name, value) { + validateHeader(this, name, value); + + if (!this[outHeadersKey]) + this[outHeadersKey] = {}; + + const key = name.toLowerCase(); + this[outHeadersKey][key] = [name, value]; + + switch (key.length) { + case 10: + if (key === 'connection') + this._removedConnection = false; + break; + case 14: + if (key === 'content-length') + this._removedContLen = false; + break; + case 17: + if (key === 'transfer-encoding') + this._removedTE = false; + break; + } +}; + + +OutgoingMessage.prototype.getHeader = function getHeader(name) { + if (typeof name !== 'string') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string'); + } + + if (!this[outHeadersKey]) return; + + var entry = this[outHeadersKey][name.toLowerCase()]; + if (!entry) + return; + return entry[1]; +}; + + +// Returns an array of the names of the current outgoing headers. +OutgoingMessage.prototype.getHeaderNames = function getHeaderNames() { + return (this[outHeadersKey] ? Object.keys(this[outHeadersKey]) : []); +}; + + +// Returns a shallow copy of the current outgoing headers. +OutgoingMessage.prototype.getHeaders = function getHeaders() { + const headers = this[outHeadersKey]; + const ret = Object.create(null); + if (headers) { + const keys = Object.keys(headers); + for (var i = 0; i < keys.length; ++i) { + const key = keys[i]; + const val = headers[key][1]; + ret[key] = val; + } + } + return ret; +}; + + +OutgoingMessage.prototype.hasHeader = function hasHeader(name) { + if (typeof name !== 'string') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string'); + } + + return !!(this[outHeadersKey] && this[outHeadersKey][name.toLowerCase()]); +}; + + +OutgoingMessage.prototype.removeHeader = function removeHeader(name) { + if (typeof name !== 'string') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'name', 'string'); + } + + if (this._header) { + throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'remove'); + } + + var key = name.toLowerCase(); + + switch (key.length) { + case 10: + if (key === 'connection') + this._removedConnection = true; + break; + case 14: + if (key === 'content-length') + this._removedContLen = true; + break; + case 17: + if (key === 'transfer-encoding') + this._removedTE = true; + break; + case 4: + if (key === 'date') + this.sendDate = false; + break; + } + + if (this[outHeadersKey]) { + delete this[outHeadersKey][key]; + } +}; + + +OutgoingMessage.prototype._implicitHeader = function _implicitHeader() { + throw new errors.Error('ERR_METHOD_NOT_IMPLEMENTED', '_implicitHeader()'); +}; + +Object.defineProperty(OutgoingMessage.prototype, 'headersSent', { + configurable: true, + enumerable: true, + get: function() { return !!this._header; } +}); + + +const crlf_buf = Buffer.from('\r\n'); +OutgoingMessage.prototype.write = function write(chunk, encoding, callback) { + return write_(this, chunk, encoding, callback, false); +}; + +function write_(msg, chunk, encoding, callback, fromEnd) { + if (msg.finished) { + var err = new Error('write after end'); + nextTick(msg.socket && msg.socket[async_id_symbol], + writeAfterEndNT.bind(msg), + err, + callback); + + return true; + } + + if (!msg._header) { + msg._implicitHeader(); + } + + if (!msg._hasBody) { + debug('This type of response MUST NOT have a body. ' + + 'Ignoring write() calls.'); + return true; + } + + if (!fromEnd && typeof chunk !== 'string' && !(chunk instanceof Buffer)) { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'first argument', + ['string', 'buffer']); + } + + + // If we get an empty string or buffer, then just do nothing, and + // signal the user to keep writing. + if (chunk.length === 0) return true; + + if (!fromEnd && msg.connection && !msg.connection.corked) { + msg.connection.cork(); + process.nextTick(connectionCorkNT, msg.connection); + } + + var len, ret; + if (msg.chunkedEncoding) { + if (typeof chunk === 'string') + len = Buffer.byteLength(chunk, encoding); + else + len = chunk.length; + + msg._send(len.toString(16), 'latin1', null); + msg._send(crlf_buf, null, null); + msg._send(chunk, encoding, null); + ret = msg._send(crlf_buf, null, callback); + } else { + ret = msg._send(chunk, encoding, callback); + } + + debug('write ret = ' + ret); + return ret; +} + + +function writeAfterEndNT(err, callback) { + this.emit('error', err); + if (callback) callback(err); +} + + +function connectionCorkNT(conn) { + conn.uncork(); +} + + +function escapeHeaderValue(value) { + // Protect against response splitting. The regex test is there to + // minimize the performance impact in the common case. + return /[\r\n]/.test(value) ? value.replace(/[\r\n]+[ \t]*/g, '') : value; +} + + +OutgoingMessage.prototype.addTrailers = function addTrailers(headers) { + this._trailer = ''; + var keys = Object.keys(headers); + var isArray = Array.isArray(headers); + var field, value; + for (var i = 0, l = keys.length; i < l; i++) { + var key = keys[i]; + if (isArray) { + field = headers[key][0]; + value = headers[key][1]; + } else { + field = key; + value = headers[key]; + } + if (typeof field !== 'string' || !field || !checkIsHttpToken(field)) { + throw new errors.TypeError('ERR_INVALID_HTTP_TOKEN', 'Trailer name', + field); + } + if (checkInvalidHeaderChar(value)) { + debug('Trailer "%s" contains invalid characters', field); + throw new errors.TypeError('ERR_INVALID_CHAR', 'trailer content', field); + } + this._trailer += field + ': ' + escapeHeaderValue(value) + CRLF; + } +}; + +function onFinish(outmsg) { + outmsg.emit('finish'); +} + +OutgoingMessage.prototype.end = function end(chunk, encoding, callback) { + if (typeof chunk === 'function') { + callback = chunk; + chunk = null; + } else if (typeof encoding === 'function') { + callback = encoding; + encoding = null; + } + + if (this.finished) { + return false; + } + + var uncork; + if (chunk) { + if (typeof chunk !== 'string' && !(chunk instanceof Buffer)) { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'first argument', + ['string', 'buffer']); + } + if (!this._header) { + if (typeof chunk === 'string') + this._contentLength = Buffer.byteLength(chunk, encoding); + else + this._contentLength = chunk.length; + } + if (this.connection) { + this.connection.cork(); + uncork = true; + } + write_(this, chunk, encoding, null, true); + } else if (!this._header) { + this._contentLength = 0; + this._implicitHeader(); + } + + if (typeof callback === 'function') + this.once('finish', callback); + + var finish = onFinish.bind(undefined, this); + + var ret; + if (this._hasBody && this.chunkedEncoding) { + ret = this._send('0\r\n' + this._trailer + '\r\n', 'latin1', finish); + } else { + // Force a flush, HACK. + ret = this._send('', 'latin1', finish); + } + + if (uncork) + this.connection.uncork(); + + this.finished = true; + + // There is the first message on the outgoing queue, and we've sent + // everything to the socket. + debug('outgoing message end.'); + if (this.output.length === 0 && + this.connection && + this.connection._httpMessage === this) { + this._finish(); + } + + return ret; +}; + + +OutgoingMessage.prototype._finish = function _finish() { + assert(this.connection); + this.emit('prefinish'); +}; + + +// This logic is probably a bit confusing. Let me explain a bit: +// +// In both HTTP servers and clients it is possible to queue up several +// outgoing messages. This is easiest to imagine in the case of a client. +// Take the following situation: +// +// req1 = client.request('GET', '/'); +// req2 = client.request('POST', '/'); +// +// When the user does +// +// req2.write('hello world\n'); +// +// it's possible that the first request has not been completely flushed to +// the socket yet. Thus the outgoing messages need to be prepared to queue +// up data internally before sending it on further to the socket's queue. +// +// This function, outgoingFlush(), is called by both the Server and Client +// to attempt to flush any pending messages out to the socket. +OutgoingMessage.prototype._flush = function _flush() { + var socket = this.socket; + var ret; + + if (socket && socket.writable) { + // There might be remaining data in this.output; write it out + ret = this._flushOutput(socket); + + if (this.finished) { + // This is a queue to the server or client to bring in the next this. + this._finish(); + } else if (ret) { + // This is necessary to prevent https from breaking + this.emit('drain'); + } + } +}; + +OutgoingMessage.prototype._flushOutput = function _flushOutput(socket) { + var ret; + var outputLength = this.output.length; + if (outputLength <= 0) + return ret; + + var output = this.output; + var outputEncodings = this.outputEncodings; + var outputCallbacks = this.outputCallbacks; + socket.cork(); + for (var i = 0; i < outputLength; i++) { + ret = socket.write(output[i], outputEncodings[i], outputCallbacks[i]); + } + socket.uncork(); + + this.output = []; + this.outputEncodings = []; + this.outputCallbacks = []; + this._onPendingData(-this.outputSize); + this.outputSize = 0; + + return ret; +}; + + +OutgoingMessage.prototype.flushHeaders = function flushHeaders() { + if (!this._header) { + this._implicitHeader(); + } + + // Force-flush the headers. + this._send(''); +}; + +OutgoingMessage.prototype.flush = internalUtil.deprecate(function() { + this.flushHeaders(); +}, 'OutgoingMessage.flush is deprecated. Use flushHeaders instead.', 'DEP0001'); + +OutgoingMessage.prototype.pipe = function pipe() { + // OutgoingMessage should be write-only. Piping from it is disabled. + this.emit('error', new Error('Cannot pipe, not readable')); +}; + +module.exports = { + OutgoingMessage +}; diff --git a/lib/internal/http/server.js b/lib/internal/http/server.js new file mode 100644 index 00000000000000..da7fd0ca9504a9 --- /dev/null +++ b/lib/internal/http/server.js @@ -0,0 +1,684 @@ +// 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 util = require('util'); +const net = require('net'); +const { HTTPParser } = process.binding('http_parser'); +const assert = require('assert').ok; +const { + parsers, + freeParser, + debug, + CRLF, + continueExpression, + chunkExpression, + httpSocketSetup, + _checkInvalidHeaderChar: checkInvalidHeaderChar +} = require('internal/http/common'); +const { OutgoingMessage } = require('internal/http/outgoing'); +const { outHeadersKey, ondrain } = require('internal/http'); +const errors = require('internal/errors'); +const Buffer = require('buffer').Buffer; + +const STATUS_CODES = { + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', // RFC 2518, obsoleted by RFC 4918 + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', // RFC 4918 + 208: 'Already Reported', + 226: 'IM Used', + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 307: 'Temporary Redirect', + 308: 'Permanent Redirect', // RFC 7238 + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Payload Too Large', + 414: 'URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 418: 'I\'m a teapot', // RFC 2324 + 421: 'Misdirected Request', + 422: 'Unprocessable Entity', // RFC 4918 + 423: 'Locked', // RFC 4918 + 424: 'Failed Dependency', // RFC 4918 + 425: 'Unordered Collection', // RFC 4918 + 426: 'Upgrade Required', // RFC 2817 + 428: 'Precondition Required', // RFC 6585 + 429: 'Too Many Requests', // RFC 6585 + 431: 'Request Header Fields Too Large', // RFC 6585 + 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', // RFC 2295 + 507: 'Insufficient Storage', // RFC 4918 + 508: 'Loop Detected', + 509: 'Bandwidth Limit Exceeded', + 510: 'Not Extended', // RFC 2774 + 511: 'Network Authentication Required' // RFC 6585 +}; + +const kOnExecute = HTTPParser.kOnExecute | 0; + + +function ServerResponse(req) { + OutgoingMessage.call(this); + + if (req.method === 'HEAD') this._hasBody = false; + + 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; + } +} +util.inherits(ServerResponse, OutgoingMessage); + +ServerResponse.prototype._finish = function _finish() { + DTRACE_HTTP_SERVER_RESPONSE(this.connection); + LTTNG_HTTP_SERVER_RESPONSE(this.connection); + COUNTER_HTTP_SERVER_RESPONSE(); + OutgoingMessage.prototype._finish.call(this); +}; + + +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: + // + // var EventEmitter = require('events'); + // var 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) this._httpMessage.emit('close'); +} + +ServerResponse.prototype.assignSocket = function assignSocket(socket) { + assert(!socket._httpMessage); + socket._httpMessage = this; + socket.on('close', onServerResponseClose); + this.socket = socket; + this.connection = 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 = this.connection = null; +}; + +ServerResponse.prototype.writeContinue = function writeContinue(cb) { + this._writeRaw('HTTP/1.1 100 Continue' + CRLF + CRLF, 'ascii', cb); + this._sent100 = true; +}; + +ServerResponse.prototype._implicitHeader = function _implicitHeader() { + this.writeHead(this.statusCode); +}; + +ServerResponse.prototype.writeHead = writeHead; +function writeHead(statusCode, reason, obj) { + var originalStatusCode = statusCode; + + statusCode |= 0; + if (statusCode < 100 || statusCode > 999) { + throw new errors.RangeError('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; + + var headers; + if (this[outHeadersKey]) { + // Slow-case: when progressive API and header fields are passed. + var k; + if (obj) { + var keys = Object.keys(obj); + for (var i = 0; i < keys.length; i++) { + k = keys[i]; + if (k) this.setHeader(k, obj[k]); + } + } + if (k === undefined && this._header) { + throw new errors.Error('ERR_HTTP_HEADERS_SENT', 'render'); + } + // only progressive api is used + headers = this[outHeadersKey]; + } else { + // only writeHead() called + headers = obj; + } + + if (checkInvalidHeaderChar(this.statusMessage)) + throw new errors.Error('ERR_INVALID_CHAR', 'statusMessage'); + + var statusLine = 'HTTP/1.1 ' + statusCode + ' ' + this.statusMessage + CRLF; + + 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); +} + +// Docs-only deprecated: DEP0063 +ServerResponse.prototype.writeHeader = ServerResponse.prototype.writeHead; + + +function Server(requestListener) { + if (!(this instanceof Server)) return new Server(requestListener); + net.Server.call(this, { allowHalfOpen: true }); + + 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/ + // http://wiki.squid-cache.org/SquidFaq/InnerWorkings#What_is_a_half-closed_filedescriptor.3F + this.httpAllowHalfOpen = false; + + this.on('connection', connectionListener); + + this.timeout = 2 * 60 * 1000; + this.keepAliveTimeout = 5000; + this._pendingResponseData = 0; + this.maxHeadersCount = null; +} +util.inherits(Server, net.Server); + + +Server.prototype.setTimeout = function setTimeout(msecs, callback) { + this.timeout = msecs; + if (callback) + this.on('timeout', callback); + return this; +}; + + +function connectionListener(socket) { + debug('SERVER new http connection'); + + httpSocketSetup(socket); + + // Ensure that the server property of the socket is correctly set. + // See https://github.com/nodejs/node/issues/13435 + if (socket.server === null) + socket.server = this; + + // 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 (this.timeout && typeof socket.setTimeout === 'function') + socket.setTimeout(this.timeout); + socket.on('timeout', socketOnTimeout); + + var parser = parsers.alloc(); + parser.reinitialize(HTTPParser.REQUEST); + parser.socket = socket; + socket.parser = parser; + parser.incoming = null; + + // Propagate headers limit from server instance to parser + if (typeof this.maxHeadersCount === 'number') { + parser.maxHeaderPairs = this.maxHeadersCount << 1; + } else { + // Set default value because parser may be reused from FreeList + parser.maxHeaderPairs = 2000; + } + + var 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, + keepAliveTimeoutSet: false + }; + state.onData = socketOnData.bind(undefined, this, socket, parser, state); + state.onEnd = socketOnEnd.bind(undefined, this, 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, this, socket, state); + + // We are consuming socket, so it won't get any actual data + socket.on('resume', onSocketResume); + socket.on('pause', onSocketPause); + + // Override on to unconsume on `data`, `readable` listeners + socket.on = socketOnWrap; + + // We only consume the socket if it has never been consumed before. + if (socket._handle) { + var external = socket._handle._externalStream; + if (!socket._handle._consumed && external) { + parser._consumed = true; + socket._handle._consumed = true; + parser.consume(external); + } + } + parser[kOnExecute] = + onParserExecute.bind(undefined, this, socket, parser, state); + + socket._paused = false; +} + + +function updateOutgoingData(socket, state, delta) { + state.outgoingData += delta; + if (socket._paused && + state.outgoingData < socket._writableState.highWaterMark) { + return socketOnDrain(socket, state); + } +} + +function socketOnDrain(socket, state) { + var needPause = state.outgoingData > socket._writableState.highWaterMark; + + // If we previously paused, then start reading again. + if (socket._paused && !needPause) { + socket._paused = false; + if (socket.parser) + socket.parser.resume(); + socket.resume(); + } +} + +function socketOnTimeout() { + var req = this.parser && this.parser.incoming; + var reqTimeout = req && !req.complete && req.emit('timeout', this); + var res = this._httpMessage; + var resTimeout = res && res.emit('timeout', this); + var serverTimeout = this.server.emit('timeout', this); + + if (!reqTimeout && !resTimeout && !serverTimeout) + this.destroy(); +} + +function socketOnClose(socket, state) { + debug('server socket close'); + // mark this parser as reusable + if (socket.parser) { + freeParser(socket.parser, null, socket); + } + + abortIncoming(state.incoming); +} + +function abortIncoming(incoming) { + while (incoming.length) { + var req = incoming.shift(); + req.emit('aborted'); + req.emit('close'); + } + // abort socket._httpMessage ? +} + +function socketOnEnd(server, socket, parser, state) { + var ret = parser.finish(); + + if (ret instanceof Error) { + debug('parse error'); + socketOnError.call(socket, ret); + return; + } + + if (!server.httpAllowHalfOpen) { + abortIncoming(state.incoming); + if (socket.writable) socket.end(); + } else if (state.outgoing.length) { + state.outgoing[state.outgoing.length - 1]._last = true; + } else if (socket._httpMessage) { + socket._httpMessage._last = true; + } else if (socket.writable) { + socket.end(); + } +} + +function socketOnData(server, socket, parser, state, d) { + assert(!socket._paused); + debug('SERVER socketOnData %d', d.length); + + var ret = parser.execute(d); + onParserExecuteCommon(server, socket, parser, state, ret, d); +} + +function onParserExecute(server, socket, parser, state, ret, d) { + socket._unrefTimer(); + debug('SERVER socketOnParserExecute %d', ret); + onParserExecuteCommon(server, socket, parser, state, ret, undefined); +} + +const badRequestResponse = Buffer.from( + 'HTTP/1.1 400 ' + STATUS_CODES[400] + CRLF + CRLF, 'ascii' +); +function socketOnError(e) { + // Ignore further errors + this.removeListener('error', socketOnError); + this.on('error', () => {}); + + if (!this.server.emit('clientError', e, this)) { + if (this.writable) { + this.end(badRequestResponse); + return; + } + this.destroy(e); + } +} + +function onParserExecuteCommon(server, socket, parser, state, ret, d) { + resetSocketTimeout(server, socket, state); + + if (ret instanceof Error) { + debug('parse error', ret); + socketOnError.call(socket, ret); + } else if (parser.incoming && parser.incoming.upgrade) { + // Upgrade or CONNECT + var bytesParsed = ret; + var 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('drain', ondrain); + unconsume(parser, socket); + parser.finish(); + freeParser(parser, req, null); + parser = null; + + var eventName = req.method === 'CONNECT' ? 'connect' : 'upgrade'; + if (server.listenerCount(eventName) > 0) { + debug('SERVER have listener for %s', eventName); + var bodyHead = d.slice(bytesParsed, d.length); + + // TODO(isaacs): Need a way to reset a stream to fresh state + // IE, not flowing, and not explicitly paused. + socket._readableState.flowing = null; + server.emit(eventName, req, socket, bodyHead); + } else { + // Got upgrade header or CONNECT method, but have no handler. + 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 resOnFinish(req, res, socket, state, 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); + + 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') { + socket.setTimeout(0); + socket.setTimeout(server.keepAliveTimeout); + state.keepAliveTimeoutSet = true; + } + } else { + // start sending the next message + var m = state.outgoing.shift(); + if (m) { + m.assignSocket(socket); + } + } +} + +// 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); + + 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) { + var ws = socket._writableState; + if (ws.needDrain || state.outgoingData >= ws.highWaterMark) { + 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(); + } + } + + var res = new ServerResponse(req); + res._onPendingData = updateOutgoingData.bind(undefined, socket, state); + + res.shouldKeepAlive = keepAlive; + DTRACE_HTTP_SERVER_REQUEST(req, socket); + LTTNG_HTTP_SERVER_REQUEST(req, socket); + COUNTER_HTTP_SERVER_REQUEST(); + + 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)); + + if (req.headers.expect !== undefined && + (req.httpVersionMajor === 1 && req.httpVersionMinor === 1)) { + 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(); + } + } else { + server.emit('request', req, res); + } + return false; // Not a HEAD response. (Not even a response!) +} + +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(socket._handle._externalStream); + parser._consumed = false; + socket.removeListener('pause', onSocketPause); + socket.removeListener('resume', onSocketResume); + } +} + +function socketOnWrap(ev, fn) { + var res = net.Socket.prototype.on.call(this, ev, fn); + if (!this.parser) { + this.on = net.Socket.prototype.on; + return res; + } + + if (ev === 'data' || ev === 'readable') + unconsume(this.parser, this); + + return res; +} + +module.exports = { + STATUS_CODES, + Server, + ServerResponse, + _connectionListener: connectionListener +}; diff --git a/lib/internal/http2/core.js b/lib/internal/http2/core.js index a6dbd626ec882f..149ddaff103ae0 100644 --- a/lib/internal/http2/core.js +++ b/lib/internal/http2/core.js @@ -13,7 +13,7 @@ const tls = require('tls'); const util = require('util'); const fs = require('fs'); const errors = require('internal/errors'); -const { StreamWrap } = require('_stream_wrap'); +const { StreamWrap } = require('internal/streams/wrap'); const { Duplex } = require('stream'); const { URL } = require('url'); const { onServerStream, diff --git a/lib/internal/streams/duplex.js b/lib/internal/streams/duplex.js new file mode 100644 index 00000000000000..1308a073630235 --- /dev/null +++ b/lib/internal/streams/duplex.js @@ -0,0 +1,108 @@ +// 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. + +// a duplex stream is just a stream that is both readable and writable. +// Since JS doesn't have multiple prototypal inheritance, this class +// prototypally inherits from Readable, and then parasitically from +// Writable. + +'use strict'; + +module.exports = Duplex; + +const util = require('util'); +const Readable = require('internal/streams/readable'); +const Writable = require('internal/streams/writable'); + +util.inherits(Duplex, Readable); + +var keys = Object.keys(Writable.prototype); +for (var v = 0; v < keys.length; v++) { + var method = keys[v]; + if (!Duplex.prototype[method]) + Duplex.prototype[method] = Writable.prototype[method]; +} + +function Duplex(options) { + if (!(this instanceof Duplex)) + return new Duplex(options); + + Readable.call(this, options); + Writable.call(this, options); + + if (options && options.readable === false) + this.readable = false; + + if (options && options.writable === false) + this.writable = false; + + this.allowHalfOpen = true; + if (options && options.allowHalfOpen === false) + this.allowHalfOpen = false; + + this.once('end', onend); +} + +// the no-half-open enforcer +function onend() { + // if we allow half-open state, or if the writable side ended, + // then we're ok. + if (this.allowHalfOpen || this._writableState.ended) + return; + + // no more data can be written. + // But allow more writes to happen in this tick. + process.nextTick(onEndNT, this); +} + +function onEndNT(self) { + self.end(); +} + +Object.defineProperty(Duplex.prototype, 'destroyed', { + get() { + if (this._readableState === undefined || + this._writableState === undefined) { + return false; + } + return this._readableState.destroyed && this._writableState.destroyed; + }, + set(value) { + // we ignore the value if the stream + // has not been initialized yet + if (this._readableState === undefined || + this._writableState === undefined) { + return; + } + + // backward compatibility, the user is explicitly + // managing destroyed + this._readableState.destroyed = value; + this._writableState.destroyed = value; + } +}); + +Duplex.prototype._destroy = function(err, cb) { + this.push(null); + this.end(); + + process.nextTick(cb, err); +}; diff --git a/lib/internal/streams/passthrough.js b/lib/internal/streams/passthrough.js new file mode 100644 index 00000000000000..9bdf86ef76af3e --- /dev/null +++ b/lib/internal/streams/passthrough.js @@ -0,0 +1,43 @@ +// 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. + +// a passthrough stream. +// basically just the most minimal sort of Transform stream. +// Every written chunk gets output as-is. + +'use strict'; + +module.exports = PassThrough; + +const Transform = require('internal/streams/transform'); +const util = require('util'); +util.inherits(PassThrough, Transform); + +function PassThrough(options) { + if (!(this instanceof PassThrough)) + return new PassThrough(options); + + Transform.call(this, options); +} + +PassThrough.prototype._transform = function(chunk, encoding, cb) { + cb(null, chunk); +}; diff --git a/lib/internal/streams/readable.js b/lib/internal/streams/readable.js new file mode 100644 index 00000000000000..6427d2e5f6cb49 --- /dev/null +++ b/lib/internal/streams/readable.js @@ -0,0 +1,1056 @@ +// 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'; + +module.exports = Readable; +Readable.ReadableState = ReadableState; + +const EE = require('events'); +const Stream = require('stream'); +const { Buffer } = require('buffer'); +const util = require('util'); +const debug = util.debuglog('stream'); +const BufferList = require('internal/streams/BufferList'); +const destroyImpl = require('internal/streams/destroy'); +const errors = require('internal/errors'); +var StringDecoder; + +util.inherits(Readable, Stream); + +const kProxyEvents = ['error', 'close', 'destroy', 'pause', 'resume']; + +function prependListener(emitter, event, fn) { + // Sadly this is not cacheable as some libraries bundle their own + // event emitter implementation with them. + if (typeof emitter.prependListener === 'function') + return emitter.prependListener(event, fn); + + // This is a hack to make sure that our error handler is attached before any + // userland ones. NEVER DO THIS. This is here only because this code needs + // to continue to work with older versions of Node.js that do not include + // the prependListener() method. The goal is to eventually remove this hack. + if (!emitter._events || !emitter._events[event]) + emitter.on(event, fn); + else if (Array.isArray(emitter._events[event])) + emitter._events[event].unshift(fn); + else + emitter._events[event] = [fn, emitter._events[event]]; +} + +function ReadableState(options, stream) { + options = options || {}; + + // Duplex streams are both readable and writable, but share + // the same options object. + // However, some cases require setting options to different + // values for the readable and the writable sides of the duplex stream. + // These options can be provided separately as readableXXX and writableXXX. + var isDuplex = stream instanceof Stream.Duplex; + + // object stream flag. Used to make read(n) ignore n and to + // make all the buffer merging and length checks go away + this.objectMode = !!options.objectMode; + + if (isDuplex) + this.objectMode = this.objectMode || !!options.readableObjectMode; + + // the point at which it stops calling _read() to fill the buffer + // Note: 0 is a valid value, means "don't call _read preemptively ever" + var hwm = options.highWaterMark; + var readableHwm = options.readableHighWaterMark; + var defaultHwm = this.objectMode ? 16 : 16 * 1024; + + if (hwm || hwm === 0) + this.highWaterMark = hwm; + else if (isDuplex && (readableHwm || readableHwm === 0)) + this.highWaterMark = readableHwm; + else + this.highWaterMark = defaultHwm; + + // cast to ints. + this.highWaterMark = Math.floor(this.highWaterMark); + + // A linked list is used to store data chunks instead of an array because the + // linked list can remove elements from the beginning faster than + // array.shift() + this.buffer = new BufferList(); + this.length = 0; + this.pipes = null; + this.pipesCount = 0; + this.flowing = null; + this.ended = false; + this.endEmitted = false; + this.reading = false; + + // a flag to be able to tell if the event 'readable'/'data' is emitted + // immediately, or on a later tick. We set this to true at first, because + // any actions that shouldn't happen until "later" should generally also + // not happen before the first read call. + this.sync = true; + + // whenever we return null, then we set a flag to say + // that we're awaiting a 'readable' event emission. + this.needReadable = false; + this.emittedReadable = false; + this.readableListening = false; + this.resumeScheduled = false; + + // has it been destroyed + this.destroyed = false; + + // Crypto is kind of old and crusty. Historically, its default string + // encoding is 'binary' so we have to make this configurable. + // Everything else in the universe uses 'utf8', though. + this.defaultEncoding = options.defaultEncoding || 'utf8'; + + // the number of writers that are awaiting a drain event in .pipe()s + this.awaitDrain = 0; + + // if true, a maybeReadMore has been scheduled + this.readingMore = false; + + this.decoder = null; + this.encoding = null; + if (options.encoding) { + if (!StringDecoder) + StringDecoder = require('string_decoder').StringDecoder; + this.decoder = new StringDecoder(options.encoding); + this.encoding = options.encoding; + } +} + +function Readable(options) { + if (!(this instanceof Readable)) + return new Readable(options); + + this._readableState = new ReadableState(options, this); + + // legacy + this.readable = true; + + if (options) { + if (typeof options.read === 'function') + this._read = options.read; + + if (typeof options.destroy === 'function') + this._destroy = options.destroy; + } + + Stream.call(this); +} + +Object.defineProperty(Readable.prototype, 'destroyed', { + get() { + if (this._readableState === undefined) { + return false; + } + return this._readableState.destroyed; + }, + set(value) { + // we ignore the value if the stream + // has not been initialized yet + if (!this._readableState) { + return; + } + + // backward compatibility, the user is explicitly + // managing destroyed + this._readableState.destroyed = value; + } +}); + +Readable.prototype.destroy = destroyImpl.destroy; +Readable.prototype._undestroy = destroyImpl.undestroy; +Readable.prototype._destroy = function(err, cb) { + this.push(null); + cb(err); +}; + +// Manually shove something into the read() buffer. +// This returns true if the highWaterMark has not been hit yet, +// similar to how Writable.write() returns true if you should +// write() some more. +Readable.prototype.push = function(chunk, encoding) { + var state = this._readableState; + var skipChunkCheck; + + if (!state.objectMode) { + if (typeof chunk === 'string') { + encoding = encoding || state.defaultEncoding; + if (encoding !== state.encoding) { + chunk = Buffer.from(chunk, encoding); + encoding = ''; + } + skipChunkCheck = true; + } + } else { + skipChunkCheck = true; + } + + return readableAddChunk(this, chunk, encoding, false, skipChunkCheck); +}; + +// Unshift should *always* be something directly out of read() +Readable.prototype.unshift = function(chunk) { + return readableAddChunk(this, chunk, null, true, false); +}; + +function readableAddChunk(stream, chunk, encoding, addToFront, skipChunkCheck) { + var state = stream._readableState; + if (chunk === null) { + state.reading = false; + onEofChunk(stream, state); + } else { + var er; + if (!skipChunkCheck) + er = chunkInvalid(state, chunk); + if (er) { + stream.emit('error', er); + } else if (state.objectMode || chunk && chunk.length > 0) { + if (typeof chunk !== 'string' && + !state.objectMode && + Object.getPrototypeOf(chunk) !== Buffer.prototype) { + chunk = Stream._uint8ArrayToBuffer(chunk); + } + + if (addToFront) { + if (state.endEmitted) + stream.emit('error', + new errors.Error('ERR_STREAM_UNSHIFT_AFTER_END_EVENT')); + else + addChunk(stream, state, chunk, true); + } else if (state.ended) { + stream.emit('error', new errors.Error('ERR_STREAM_PUSH_AFTER_EOF')); + } else { + state.reading = false; + if (state.decoder && !encoding) { + chunk = state.decoder.write(chunk); + if (state.objectMode || chunk.length !== 0) + addChunk(stream, state, chunk, false); + else + maybeReadMore(stream, state); + } else { + addChunk(stream, state, chunk, false); + } + } + } else if (!addToFront) { + state.reading = false; + } + } + + return needMoreData(state); +} + +function addChunk(stream, state, chunk, addToFront) { + if (state.flowing && state.length === 0 && !state.sync) { + stream.emit('data', chunk); + stream.read(0); + } else { + // update the buffer info. + state.length += state.objectMode ? 1 : chunk.length; + if (addToFront) + state.buffer.unshift(chunk); + else + state.buffer.push(chunk); + + if (state.needReadable) + emitReadable(stream); + } + maybeReadMore(stream, state); +} + +function chunkInvalid(state, chunk) { + var er; + if (!Stream._isUint8Array(chunk) && + typeof chunk !== 'string' && + chunk !== undefined && + !state.objectMode) { + er = new errors.TypeError('ERR_INVALID_ARG_TYPE', + 'chunk', 'string/Buffer/Uint8Array'); + } + return er; +} + + +// if it's past the high water mark, we can push in some more. +// Also, if we have no data yet, we can stand some +// more bytes. This is to work around cases where hwm=0, +// such as the repl. Also, if the push() triggered a +// readable event, and the user called read(largeNumber) such that +// needReadable was set, then we ought to push more, so that another +// 'readable' event will be triggered. +function needMoreData(state) { + return !state.ended && + (state.needReadable || + state.length < state.highWaterMark || + state.length === 0); +} + +Readable.prototype.isPaused = function() { + return this._readableState.flowing === false; +}; + +// backwards compatibility. +Readable.prototype.setEncoding = function(enc) { + if (!StringDecoder) + StringDecoder = require('string_decoder').StringDecoder; + this._readableState.decoder = new StringDecoder(enc); + this._readableState.encoding = enc; + return this; +}; + +// Don't raise the hwm > 8MB +const MAX_HWM = 0x800000; +function computeNewHighWaterMark(n) { + if (n >= MAX_HWM) { + n = MAX_HWM; + } else { + // Get the next highest power of 2 to prevent increasing hwm excessively in + // tiny amounts + n--; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + n++; + } + return n; +} + +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function howMuchToRead(n, state) { + if (n <= 0 || (state.length === 0 && state.ended)) + return 0; + if (state.objectMode) + return 1; + if (n !== n) { + // Only flow one buffer at a time + if (state.flowing && state.length) + return state.buffer.head.data.length; + else + return state.length; + } + // If we're asking for more than the current hwm, then raise the hwm. + if (n > state.highWaterMark) + state.highWaterMark = computeNewHighWaterMark(n); + if (n <= state.length) + return n; + // Don't have enough + if (!state.ended) { + state.needReadable = true; + return 0; + } + return state.length; +} + +// you can override either this method, or the async _read(n) below. +Readable.prototype.read = function(n) { + debug('read', n); + n = parseInt(n, 10); + var state = this._readableState; + var nOrig = n; + + if (n !== 0) + state.emittedReadable = false; + + // if we're doing read(0) to trigger a readable event, but we + // already have a bunch of data in the buffer, then just trigger + // the 'readable' event and move on. + if (n === 0 && + state.needReadable && + (state.length >= state.highWaterMark || state.ended)) { + debug('read: emitReadable', state.length, state.ended); + if (state.length === 0 && state.ended) + endReadable(this); + else + emitReadable(this); + return null; + } + + n = howMuchToRead(n, state); + + // if we've ended, and we're now clear, then finish it up. + if (n === 0 && state.ended) { + if (state.length === 0) + endReadable(this); + return null; + } + + // All the actual chunk generation logic needs to be + // *below* the call to _read. The reason is that in certain + // synthetic stream cases, such as passthrough streams, _read + // may be a completely synchronous operation which may change + // the state of the read buffer, providing enough data when + // before there was *not* enough. + // + // So, the steps are: + // 1. Figure out what the state of things will be after we do + // a read from the buffer. + // + // 2. If that resulting state will trigger a _read, then call _read. + // Note that this may be asynchronous, or synchronous. Yes, it is + // deeply ugly to write APIs this way, but that still doesn't mean + // that the Readable class should behave improperly, as streams are + // designed to be sync/async agnostic. + // Take note if the _read call is sync or async (ie, if the read call + // has returned yet), so that we know whether or not it's safe to emit + // 'readable' etc. + // + // 3. Actually pull the requested chunks out of the buffer and return. + + // if we need a readable event, then we need to do some reading. + var doRead = state.needReadable; + debug('need readable', doRead); + + // if we currently have less than the highWaterMark, then also read some + if (state.length === 0 || state.length - n < state.highWaterMark) { + doRead = true; + debug('length less than watermark', doRead); + } + + // however, if we've ended, then there's no point, and if we're already + // reading, then it's unnecessary. + if (state.ended || state.reading) { + doRead = false; + debug('reading or ended', doRead); + } else if (doRead) { + debug('do read'); + state.reading = true; + state.sync = true; + // if the length is currently zero, then we *need* a readable event. + if (state.length === 0) + state.needReadable = true; + // call internal read method + this._read(state.highWaterMark); + state.sync = false; + // If _read pushed data synchronously, then `reading` will be false, + // and we need to re-evaluate how much data we can return to the user. + if (!state.reading) + n = howMuchToRead(nOrig, state); + } + + var ret; + if (n > 0) + ret = fromList(n, state); + else + ret = null; + + if (ret === null) { + state.needReadable = true; + n = 0; + } else { + state.length -= n; + } + + if (state.length === 0) { + // If we have nothing in the buffer, then we want to know + // as soon as we *do* get something into the buffer. + if (!state.ended) + state.needReadable = true; + + // If we tried to read() past the EOF, then emit end on the next tick. + if (nOrig !== n && state.ended) + endReadable(this); + } + + if (ret !== null) + this.emit('data', ret); + + return ret; +}; + +function onEofChunk(stream, state) { + if (state.ended) return; + if (state.decoder) { + var chunk = state.decoder.end(); + if (chunk && chunk.length) { + state.buffer.push(chunk); + state.length += state.objectMode ? 1 : chunk.length; + } + } + state.ended = true; + + // emit 'readable' now to make sure it gets picked up. + emitReadable(stream); +} + +// Don't emit readable right away in sync mode, because this can trigger +// another read() call => stack overflow. This way, it might trigger +// a nextTick recursion warning, but that's not so bad. +function emitReadable(stream) { + var state = stream._readableState; + state.needReadable = false; + if (!state.emittedReadable) { + debug('emitReadable', state.flowing); + state.emittedReadable = true; + if (state.sync) + process.nextTick(emitReadable_, stream); + else + emitReadable_(stream); + } +} + +function emitReadable_(stream) { + debug('emit readable'); + stream.emit('readable'); + flow(stream); +} + + +// at this point, the user has presumably seen the 'readable' event, +// and called read() to consume some data. that may have triggered +// in turn another _read(n) call, in which case reading = true if +// it's in progress. +// However, if we're not ended, or reading, and the length < hwm, +// then go ahead and try to read some more preemptively. +function maybeReadMore(stream, state) { + if (!state.readingMore) { + state.readingMore = true; + process.nextTick(maybeReadMore_, stream, state); + } +} + +function maybeReadMore_(stream, state) { + var len = state.length; + while (!state.reading && !state.flowing && !state.ended && + state.length < state.highWaterMark) { + debug('maybeReadMore read 0'); + stream.read(0); + if (len === state.length) + // didn't get any data, stop spinning. + break; + else + len = state.length; + } + state.readingMore = false; +} + +// abstract method. to be overridden in specific implementation classes. +// call cb(er, data) where data is <= n in length. +// for virtual (non-string, non-buffer) streams, "length" is somewhat +// arbitrary, and perhaps not very meaningful. +Readable.prototype._read = function(n) { + this.emit('error', new errors.Error('ERR_STREAM_READ_NOT_IMPLEMENTED')); +}; + +Readable.prototype.pipe = function(dest, pipeOpts) { + var src = this; + var state = this._readableState; + + switch (state.pipesCount) { + case 0: + state.pipes = dest; + break; + case 1: + state.pipes = [state.pipes, dest]; + break; + default: + state.pipes.push(dest); + break; + } + state.pipesCount += 1; + debug('pipe count=%d opts=%j', state.pipesCount, pipeOpts); + + var doEnd = (!pipeOpts || pipeOpts.end !== false) && + dest !== process.stdout && + dest !== process.stderr; + + var endFn = doEnd ? onend : unpipe; + if (state.endEmitted) + process.nextTick(endFn); + else + src.once('end', endFn); + + dest.on('unpipe', onunpipe); + function onunpipe(readable, unpipeInfo) { + debug('onunpipe'); + if (readable === src) { + if (unpipeInfo && unpipeInfo.hasUnpiped === false) { + unpipeInfo.hasUnpiped = true; + cleanup(); + } + } + } + + function onend() { + debug('onend'); + dest.end(); + } + + // when the dest drains, it reduces the awaitDrain counter + // on the source. This would be more elegant with a .once() + // handler in flow(), but adding and removing repeatedly is + // too slow. + var ondrain = pipeOnDrain(src); + dest.on('drain', ondrain); + + var cleanedUp = false; + function cleanup() { + debug('cleanup'); + // cleanup event handlers once the pipe is broken + dest.removeListener('close', onclose); + dest.removeListener('finish', onfinish); + dest.removeListener('drain', ondrain); + dest.removeListener('error', onerror); + dest.removeListener('unpipe', onunpipe); + src.removeListener('end', onend); + src.removeListener('end', unpipe); + src.removeListener('data', ondata); + + cleanedUp = true; + + // if the reader is waiting for a drain event from this + // specific writer, then it would cause it to never start + // flowing again. + // So, if this is awaiting a drain, then we just call it now. + // If we don't know, then assume that we are waiting for one. + if (state.awaitDrain && + (!dest._writableState || dest._writableState.needDrain)) + ondrain(); + } + + // If the user pushes more data while we're writing to dest then we'll end up + // in ondata again. However, we only want to increase awaitDrain once because + // dest will only emit one 'drain' event for the multiple writes. + // => Introduce a guard on increasing awaitDrain. + var increasedAwaitDrain = false; + src.on('data', ondata); + function ondata(chunk) { + debug('ondata'); + increasedAwaitDrain = false; + var ret = dest.write(chunk); + if (false === ret && !increasedAwaitDrain) { + // If the user unpiped during `dest.write()`, it is possible + // to get stuck in a permanently paused state if that write + // also returned false. + // => Check whether `dest` is still a piping destination. + if (((state.pipesCount === 1 && state.pipes === dest) || + (state.pipesCount > 1 && state.pipes.indexOf(dest) !== -1)) && + !cleanedUp) { + debug('false write response, pause', src._readableState.awaitDrain); + src._readableState.awaitDrain++; + increasedAwaitDrain = true; + } + src.pause(); + } + } + + // if the dest has an error, then stop piping into it. + // however, don't suppress the throwing behavior for this. + function onerror(er) { + debug('onerror', er); + unpipe(); + dest.removeListener('error', onerror); + if (EE.listenerCount(dest, 'error') === 0) + dest.emit('error', er); + } + + // Make sure our error handler is attached before userland ones. + prependListener(dest, 'error', onerror); + + // Both close and finish should trigger unpipe, but only once. + function onclose() { + dest.removeListener('finish', onfinish); + unpipe(); + } + dest.once('close', onclose); + function onfinish() { + debug('onfinish'); + dest.removeListener('close', onclose); + unpipe(); + } + dest.once('finish', onfinish); + + function unpipe() { + debug('unpipe'); + src.unpipe(dest); + } + + // tell the dest that it's being piped to + dest.emit('pipe', src); + + // start the flow if it hasn't been started already. + if (!state.flowing) { + debug('pipe resume'); + src.resume(); + } + + return dest; +}; + +function pipeOnDrain(src) { + return function() { + var state = src._readableState; + debug('pipeOnDrain', state.awaitDrain); + if (state.awaitDrain) + state.awaitDrain--; + if (state.awaitDrain === 0 && EE.listenerCount(src, 'data')) { + state.flowing = true; + flow(src); + } + }; +} + + +Readable.prototype.unpipe = function(dest) { + var state = this._readableState; + var unpipeInfo = { hasUnpiped: false }; + + // if we're not piping anywhere, then do nothing. + if (state.pipesCount === 0) + return this; + + // just one destination. most common case. + if (state.pipesCount === 1) { + // passed in one, but it's not the right one. + if (dest && dest !== state.pipes) + return this; + + if (!dest) + dest = state.pipes; + + // got a match. + state.pipes = null; + state.pipesCount = 0; + state.flowing = false; + if (dest) + dest.emit('unpipe', this, unpipeInfo); + return this; + } + + // slow case. multiple pipe destinations. + + if (!dest) { + // remove all. + var dests = state.pipes; + var len = state.pipesCount; + state.pipes = null; + state.pipesCount = 0; + state.flowing = false; + + for (var i = 0; i < len; i++) + dests[i].emit('unpipe', this, unpipeInfo); + return this; + } + + // try to find the right one. + var index = state.pipes.indexOf(dest); + if (index === -1) + return this; + + state.pipes.splice(index, 1); + state.pipesCount -= 1; + if (state.pipesCount === 1) + state.pipes = state.pipes[0]; + + dest.emit('unpipe', this, unpipeInfo); + + return this; +}; + +// set up data events if they are asked for +// Ensure readable listeners eventually get something +Readable.prototype.on = function(ev, fn) { + const res = Stream.prototype.on.call(this, ev, fn); + + if (ev === 'data') { + // Start flowing on next tick if stream isn't explicitly paused + if (this._readableState.flowing !== false) + this.resume(); + } else if (ev === 'readable') { + const state = this._readableState; + if (!state.endEmitted && !state.readableListening) { + state.readableListening = state.needReadable = true; + state.emittedReadable = false; + if (!state.reading) { + process.nextTick(nReadingNextTick, this); + } else if (state.length) { + emitReadable(this); + } + } + } + + return res; +}; +Readable.prototype.addListener = Readable.prototype.on; + +function nReadingNextTick(self) { + debug('readable nexttick read 0'); + self.read(0); +} + +// pause() and resume() are remnants of the legacy readable stream API +// If the user uses them, then switch into old mode. +Readable.prototype.resume = function() { + var state = this._readableState; + if (!state.flowing) { + debug('resume'); + state.flowing = true; + resume(this, state); + } + return this; +}; + +function resume(stream, state) { + if (!state.resumeScheduled) { + state.resumeScheduled = true; + process.nextTick(resume_, stream, state); + } +} + +function resume_(stream, state) { + if (!state.reading) { + debug('resume read 0'); + stream.read(0); + } + + state.resumeScheduled = false; + state.awaitDrain = 0; + stream.emit('resume'); + flow(stream); + if (state.flowing && !state.reading) + stream.read(0); +} + +Readable.prototype.pause = function() { + debug('call pause flowing=%j', this._readableState.flowing); + if (false !== this._readableState.flowing) { + debug('pause'); + this._readableState.flowing = false; + this.emit('pause'); + } + return this; +}; + +function flow(stream) { + const state = stream._readableState; + debug('flow', state.flowing); + while (state.flowing && stream.read() !== null); +} + +// wrap an old-style stream as the async data source. +// This is *not* part of the readable stream interface. +// It is an ugly unfortunate mess of history. +Readable.prototype.wrap = function(stream) { + var state = this._readableState; + var paused = false; + + var self = this; + stream.on('end', function() { + debug('wrapped end'); + if (state.decoder && !state.ended) { + var chunk = state.decoder.end(); + if (chunk && chunk.length) + self.push(chunk); + } + + self.push(null); + }); + + stream.on('data', function(chunk) { + debug('wrapped data'); + if (state.decoder) + chunk = state.decoder.write(chunk); + + // don't skip over falsy values in objectMode + if (state.objectMode && (chunk === null || chunk === undefined)) + return; + else if (!state.objectMode && (!chunk || !chunk.length)) + return; + + var ret = self.push(chunk); + if (!ret) { + paused = true; + stream.pause(); + } + }); + + // proxy all the other methods. + // important when wrapping filters and duplexes. + for (var i in stream) { + if (this[i] === undefined && typeof stream[i] === 'function') { + this[i] = function(method) { + return function() { + return stream[method].apply(stream, arguments); + }; + }(i); + } + } + + // proxy certain important events. + for (var n = 0; n < kProxyEvents.length; n++) { + stream.on(kProxyEvents[n], self.emit.bind(self, kProxyEvents[n])); + } + + // when we try to consume some more bytes, simply unpause the + // underlying stream. + self._read = function(n) { + debug('wrapped _read', n); + if (paused) { + paused = false; + stream.resume(); + } + }; + + return self; +}; + + +// exposed for testing purposes only. +Readable._fromList = fromList; + +// Pluck off n bytes from an array of buffers. +// Length is the combined lengths of all the buffers in the list. +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function fromList(n, state) { + // nothing buffered + if (state.length === 0) + return null; + + var ret; + if (state.objectMode) + ret = state.buffer.shift(); + else if (!n || n >= state.length) { + // read it all, truncate the list + if (state.decoder) + ret = state.buffer.join(''); + else if (state.buffer.length === 1) + ret = state.buffer.head.data; + else + ret = state.buffer.concat(state.length); + state.buffer.clear(); + } else { + // read part of list + ret = fromListPartial(n, state.buffer, state.decoder); + } + + return ret; +} + +// Extracts only enough buffered data to satisfy the amount requested. +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function fromListPartial(n, list, hasStrings) { + var ret; + if (n < list.head.data.length) { + // slice is the same for buffers and strings + ret = list.head.data.slice(0, n); + list.head.data = list.head.data.slice(n); + } else if (n === list.head.data.length) { + // first chunk is a perfect match + ret = list.shift(); + } else { + // result spans more than one buffer + ret = hasStrings ? copyFromBufferString(n, list) : copyFromBuffer(n, list); + } + return ret; +} + +// Copies a specified amount of characters from the list of buffered data +// chunks. +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function copyFromBufferString(n, list) { + var p = list.head; + var c = 1; + var ret = p.data; + n -= ret.length; + while (p = p.next) { + const str = p.data; + const nb = (n > str.length ? str.length : n); + if (nb === str.length) + ret += str; + else + ret += str.slice(0, n); + n -= nb; + if (n === 0) { + if (nb === str.length) { + ++c; + if (p.next) + list.head = p.next; + else + list.head = list.tail = null; + } else { + list.head = p; + p.data = str.slice(nb); + } + break; + } + ++c; + } + list.length -= c; + return ret; +} + +// Copies a specified amount of bytes from the list of buffered data chunks. +// This function is designed to be inlinable, so please take care when making +// changes to the function body. +function copyFromBuffer(n, list) { + const ret = Buffer.allocUnsafe(n); + var p = list.head; + var c = 1; + p.data.copy(ret); + n -= p.data.length; + while (p = p.next) { + const buf = p.data; + const nb = (n > buf.length ? buf.length : n); + buf.copy(ret, ret.length - n, 0, nb); + n -= nb; + if (n === 0) { + if (nb === buf.length) { + ++c; + if (p.next) + list.head = p.next; + else + list.head = list.tail = null; + } else { + list.head = p; + p.data = buf.slice(nb); + } + break; + } + ++c; + } + list.length -= c; + return ret; +} + +function endReadable(stream) { + var state = stream._readableState; + + if (!state.endEmitted) { + state.ended = true; + process.nextTick(endReadableNT, state, stream); + } +} + +function endReadableNT(state, stream) { + // Check that we didn't get one last unshift. + if (!state.endEmitted && state.length === 0) { + state.endEmitted = true; + stream.readable = false; + stream.emit('end'); + } +} diff --git a/lib/internal/streams/transform.js b/lib/internal/streams/transform.js new file mode 100644 index 00000000000000..781f808dc3eb6c --- /dev/null +++ b/lib/internal/streams/transform.js @@ -0,0 +1,218 @@ +// 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. + +// a transform stream is a readable/writable stream where you do +// something with the data. Sometimes it's called a "filter", +// but that's not a great name for it, since that implies a thing where +// some bits pass through, and others are simply ignored. (That would +// be a valid example of a transform, of course.) +// +// While the output is causally related to the input, it's not a +// necessarily symmetric or synchronous transformation. For example, +// a zlib stream might take multiple plain-text writes(), and then +// emit a single compressed chunk some time in the future. +// +// Here's how this works: +// +// The Transform stream has all the aspects of the readable and writable +// stream classes. When you write(chunk), that calls _write(chunk,cb) +// internally, and returns false if there's a lot of pending writes +// buffered up. When you call read(), that calls _read(n) until +// there's enough pending readable data buffered up. +// +// In a transform stream, the written data is placed in a buffer. When +// _read(n) is called, it transforms the queued up data, calling the +// buffered _write cb's as it consumes chunks. If consuming a single +// written chunk would result in multiple output chunks, then the first +// outputted bit calls the readcb, and subsequent chunks just go into +// the read buffer, and will cause it to emit 'readable' if necessary. +// +// This way, back-pressure is actually determined by the reading side, +// since _read has to be called to start processing a new chunk. However, +// a pathological inflate type of transform can cause excessive buffering +// here. For example, imagine a stream where every byte of input is +// interpreted as an integer from 0-255, and then results in that many +// bytes of output. Writing the 4 bytes {ff,ff,ff,ff} would result in +// 1kb of data being output. In this case, you could write a very small +// amount of input, and end up with a very large amount of output. In +// such a pathological inflating mechanism, there'd be no way to tell +// the system to stop doing the transform. A single 4MB write could +// cause the system to run out of memory. +// +// However, even in such a pathological case, only a single written chunk +// would be consumed, and then the rest would wait (un-transformed) until +// the results of the previous transformed chunk were consumed. + +'use strict'; + +module.exports = Transform; +const errors = require('internal/errors'); +const Duplex = require('internal/streams/duplex'); +const util = require('util'); +util.inherits(Transform, Duplex); + + +function afterTransform(er, data) { + var ts = this._transformState; + ts.transforming = false; + + var cb = ts.writecb; + + if (cb === null) { + return this.emit('error', new errors.Error('ERR_MULTIPLE_CALLBACK')); + } + + ts.writechunk = null; + ts.writecb = null; + + if (data != null) // single equals check for both `null` and `undefined` + this.push(data); + + cb(er); + + var rs = this._readableState; + rs.reading = false; + if (rs.needReadable || rs.length < rs.highWaterMark) { + this._read(rs.highWaterMark); + } +} + + +function Transform(options) { + if (!(this instanceof Transform)) + return new Transform(options); + + Duplex.call(this, options); + + this._transformState = { + afterTransform: afterTransform.bind(this), + needTransform: false, + transforming: false, + writecb: null, + writechunk: null, + writeencoding: null + }; + + // start out asking for a readable event once data is transformed. + this._readableState.needReadable = true; + + // we have implemented the _read method, and done the other things + // that Readable wants before the first _read call, so unset the + // sync guard flag. + this._readableState.sync = false; + + if (options) { + if (typeof options.transform === 'function') + this._transform = options.transform; + + if (typeof options.flush === 'function') + this._flush = options.flush; + } + + // When the writable side finishes, then flush out anything remaining. + this.on('prefinish', prefinish); +} + +function prefinish() { + if (typeof this._flush === 'function') { + this._flush((er, data) => { + done(this, er, data); + }); + } else { + done(this, null, null); + } +} + +Transform.prototype.push = function(chunk, encoding) { + this._transformState.needTransform = false; + return Duplex.prototype.push.call(this, chunk, encoding); +}; + +// This is the part where you do stuff! +// override this function in implementation classes. +// 'chunk' is an input chunk. +// +// Call `push(newChunk)` to pass along transformed output +// to the readable side. You may call 'push' zero or more times. +// +// Call `cb(err)` when you are done with this chunk. If you pass +// an error, then that'll put the hurt on the whole operation. If you +// never call cb(), then you'll never get another chunk. +Transform.prototype._transform = function(chunk, encoding, cb) { + throw new errors.Error('ERR_METHOD_NOT_IMPLEMENTED', '_transform'); +}; + +Transform.prototype._write = function(chunk, encoding, cb) { + var ts = this._transformState; + ts.writecb = cb; + ts.writechunk = chunk; + ts.writeencoding = encoding; + if (!ts.transforming) { + var rs = this._readableState; + if (ts.needTransform || + rs.needReadable || + rs.length < rs.highWaterMark) + this._read(rs.highWaterMark); + } +}; + +// Doesn't matter what the args are here. +// _transform does all the work. +// That we got here means that the readable side wants more data. +Transform.prototype._read = function(n) { + var ts = this._transformState; + + if (ts.writechunk !== null && ts.writecb && !ts.transforming) { + ts.transforming = true; + this._transform(ts.writechunk, ts.writeencoding, ts.afterTransform); + } else { + // mark that we need a transform, so that any data that comes in + // will get processed, now that we've asked for it. + ts.needTransform = true; + } +}; + + +Transform.prototype._destroy = function(err, cb) { + Duplex.prototype._destroy.call(this, err, (err2) => { + cb(err2); + this.emit('close'); + }); +}; + + +function done(stream, er, data) { + if (er) + return stream.emit('error', er); + + if (data != null) // single equals check for both `null` and `undefined` + stream.push(data); + + // TODO(BridgeAR): Write a test for these two error cases + // if there's nothing in the write buffer, then that means + // that nothing more will ever be provided + if (stream._writableState.length) + throw new errors.Error('ERR_TRANSFORM_WITH_LENGTH_0'); + + if (stream._transformState.transforming) + throw new errors.Error('ERR_TRANSFORM_ALREADY_TRANSFORMING'); + return stream.push(null); +} diff --git a/lib/internal/streams/wrap.js b/lib/internal/streams/wrap.js new file mode 100644 index 00000000000000..10a0cf57e7789e --- /dev/null +++ b/lib/internal/streams/wrap.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('internal/wrap_js_stream'); diff --git a/lib/internal/streams/writable.js b/lib/internal/streams/writable.js new file mode 100644 index 00000000000000..13b79233e91bfb --- /dev/null +++ b/lib/internal/streams/writable.js @@ -0,0 +1,665 @@ +// 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. + +// A bit simpler than readable streams. +// Implement an async ._write(chunk, encoding, cb), and it'll handle all +// the drain event emission and buffering. + +'use strict'; + +module.exports = Writable; +Writable.WritableState = WritableState; + +const util = require('util'); +const internalUtil = require('internal/util'); +const Stream = require('stream'); +const { Buffer } = require('buffer'); +const destroyImpl = require('internal/streams/destroy'); +const errors = require('internal/errors'); + +util.inherits(Writable, Stream); + +function nop() {} + +function WritableState(options, stream) { + options = options || {}; + + // Duplex streams are both readable and writable, but share + // the same options object. + // However, some cases require setting options to different + // values for the readable and the writable sides of the duplex stream. + // These options can be provided separately as readableXXX and writableXXX. + var isDuplex = stream instanceof Stream.Duplex; + + // object stream flag to indicate whether or not this stream + // contains buffers or objects. + this.objectMode = !!options.objectMode; + + if (isDuplex) + this.objectMode = this.objectMode || !!options.writableObjectMode; + + // the point at which write() starts returning false + // Note: 0 is a valid value, means that we always return false if + // the entire buffer is not flushed immediately on write() + var hwm = options.highWaterMark; + var writableHwm = options.writableHighWaterMark; + var defaultHwm = this.objectMode ? 16 : 16 * 1024; + + if (hwm || hwm === 0) + this.highWaterMark = hwm; + else if (isDuplex && (writableHwm || writableHwm === 0)) + this.highWaterMark = writableHwm; + else + this.highWaterMark = defaultHwm; + + // cast to ints. + this.highWaterMark = Math.floor(this.highWaterMark); + + // if _final has been called + this.finalCalled = false; + + // drain event flag. + this.needDrain = false; + // at the start of calling end() + this.ending = false; + // when end() has been called, and returned + this.ended = false; + // when 'finish' is emitted + this.finished = false; + + // has it been destroyed + this.destroyed = false; + + // should we decode strings into buffers before passing to _write? + // this is here so that some node-core streams can optimize string + // handling at a lower level. + var noDecode = options.decodeStrings === false; + this.decodeStrings = !noDecode; + + // Crypto is kind of old and crusty. Historically, its default string + // encoding is 'binary' so we have to make this configurable. + // Everything else in the universe uses 'utf8', though. + this.defaultEncoding = options.defaultEncoding || 'utf8'; + + // not an actual buffer we keep track of, but a measurement + // of how much we're waiting to get pushed to some underlying + // socket or file. + this.length = 0; + + // a flag to see when we're in the middle of a write. + this.writing = false; + + // when true all writes will be buffered until .uncork() call + this.corked = 0; + + // a flag to be able to tell if the onwrite cb is called immediately, + // or on a later tick. We set this to true at first, because any + // actions that shouldn't happen until "later" should generally also + // not happen before the first write call. + this.sync = true; + + // a flag to know if we're processing previously buffered items, which + // may call the _write() callback in the same tick, so that we don't + // end up in an overlapped onwrite situation. + this.bufferProcessing = false; + + // the callback that's passed to _write(chunk,cb) + this.onwrite = onwrite.bind(undefined, stream); + + // the callback that the user supplies to write(chunk,encoding,cb) + this.writecb = null; + + // the amount that is being written when _write is called. + this.writelen = 0; + + this.bufferedRequest = null; + this.lastBufferedRequest = null; + + // number of pending user-supplied write callbacks + // this must be 0 before 'finish' can be emitted + this.pendingcb = 0; + + // emit prefinish if the only thing we're waiting for is _write cbs + // This is relevant for synchronous Transform streams + this.prefinished = false; + + // True if the error was already emitted and should not be thrown again + this.errorEmitted = false; + + // count buffered requests + this.bufferedRequestCount = 0; + + // allocate the first CorkedRequest, there is always + // one allocated and free to use, and we maintain at most two + var corkReq = { next: null, entry: null, finish: undefined }; + corkReq.finish = onCorkedFinish.bind(undefined, corkReq, this); + this.corkedRequestsFree = corkReq; +} + +WritableState.prototype.getBuffer = function getBuffer() { + var current = this.bufferedRequest; + var out = []; + while (current) { + out.push(current); + current = current.next; + } + return out; +}; + +Object.defineProperty(WritableState.prototype, 'buffer', { + get: internalUtil.deprecate(function() { + return this.getBuffer(); + }, '_writableState.buffer is deprecated. Use _writableState.getBuffer ' + + 'instead.', 'DEP0003') +}); + +// Test _writableState for inheritance to account for Duplex streams, +// whose prototype chain only points to Readable. +var realHasInstance; +if (typeof Symbol === 'function' && Symbol.hasInstance) { + realHasInstance = Function.prototype[Symbol.hasInstance]; + Object.defineProperty(Writable, Symbol.hasInstance, { + value: function(object) { + if (realHasInstance.call(this, object)) + return true; + if (this !== Writable) + return false; + + return object && object._writableState instanceof WritableState; + } + }); +} else { + realHasInstance = function(object) { + return object instanceof this; + }; +} + +function Writable(options) { + // Writable ctor is applied to Duplexes, too. + // `realHasInstance` is necessary because using plain `instanceof` + // would return false, as no `_writableState` property is attached. + + // Trying to use the custom `instanceof` for Writable here will also break the + // Node.js LazyTransform implementation, which has a non-trivial getter for + // `_writableState` that would lead to infinite recursion. + if (!(realHasInstance.call(Writable, this)) && + !(this instanceof Stream.Duplex)) { + return new Writable(options); + } + + this._writableState = new WritableState(options, this); + + // legacy. + this.writable = true; + + if (options) { + if (typeof options.write === 'function') + this._write = options.write; + + if (typeof options.writev === 'function') + this._writev = options.writev; + + if (typeof options.destroy === 'function') + this._destroy = options.destroy; + + if (typeof options.final === 'function') + this._final = options.final; + } + + Stream.call(this); +} + +// Otherwise people can pipe Writable streams, which is just wrong. +Writable.prototype.pipe = function() { + this.emit('error', new errors.Error('ERR_STREAM_CANNOT_PIPE')); +}; + + +function writeAfterEnd(stream, cb) { + var er = new errors.Error('ERR_STREAM_WRITE_AFTER_END'); + // TODO: defer error events consistently everywhere, not just the cb + stream.emit('error', er); + process.nextTick(cb, er); +} + +// Checks that a user-supplied chunk is valid, especially for the particular +// mode the stream is in. Currently this means that `null` is never accepted +// and undefined/non-string values are only allowed in object mode. +function validChunk(stream, state, chunk, cb) { + var valid = true; + var er = false; + + if (chunk === null) { + er = new errors.TypeError('ERR_STREAM_NULL_VALUES'); + } else if (typeof chunk !== 'string' && + chunk !== undefined && + !state.objectMode) { + er = new errors.TypeError('ERR_INVALID_ARG_TYPE', 'chunk', 'string/buffer'); + } + if (er) { + stream.emit('error', er); + process.nextTick(cb, er); + valid = false; + } + return valid; +} + +Writable.prototype.write = function(chunk, encoding, cb) { + var state = this._writableState; + var ret = false; + var isBuf = !state.objectMode && Stream._isUint8Array(chunk); + + if (isBuf && Object.getPrototypeOf(chunk) !== Buffer.prototype) { + chunk = Stream._uint8ArrayToBuffer(chunk); + } + + if (typeof encoding === 'function') { + cb = encoding; + encoding = null; + } + + if (isBuf) + encoding = 'buffer'; + else if (!encoding) + encoding = state.defaultEncoding; + + if (typeof cb !== 'function') + cb = nop; + + if (state.ended) + writeAfterEnd(this, cb); + else if (isBuf || validChunk(this, state, chunk, cb)) { + state.pendingcb++; + ret = writeOrBuffer(this, state, isBuf, chunk, encoding, cb); + } + + return ret; +}; + +Writable.prototype.cork = function() { + var state = this._writableState; + + state.corked++; +}; + +Writable.prototype.uncork = function() { + var state = this._writableState; + + if (state.corked) { + state.corked--; + + if (!state.writing && + !state.corked && + !state.finished && + !state.bufferProcessing && + state.bufferedRequest) + clearBuffer(this, state); + } +}; + +Writable.prototype.setDefaultEncoding = function setDefaultEncoding(encoding) { + // node::ParseEncoding() requires lower case. + if (typeof encoding === 'string') + encoding = encoding.toLowerCase(); + if (!Buffer.isEncoding(encoding)) + throw new errors.TypeError('ERR_UNKNOWN_ENCODING', encoding); + this._writableState.defaultEncoding = encoding; + return this; +}; + +function decodeChunk(state, chunk, encoding) { + if (!state.objectMode && + state.decodeStrings !== false && + typeof chunk === 'string') { + chunk = Buffer.from(chunk, encoding); + } + return chunk; +} + +// if we're already writing something, then just put this +// in the queue, and wait our turn. Otherwise, call _write +// If we return false, then we need a drain event, so set that flag. +function writeOrBuffer(stream, state, isBuf, chunk, encoding, cb) { + if (!isBuf) { + var newChunk = decodeChunk(state, chunk, encoding); + if (chunk !== newChunk) { + isBuf = true; + encoding = 'buffer'; + chunk = newChunk; + } + } + var len = state.objectMode ? 1 : chunk.length; + + state.length += len; + + var ret = state.length < state.highWaterMark; + // we must ensure that previous needDrain will not be reset to false. + if (!ret) + state.needDrain = true; + + if (state.writing || state.corked) { + var last = state.lastBufferedRequest; + state.lastBufferedRequest = { + chunk, + encoding, + isBuf, + callback: cb, + next: null + }; + if (last) { + last.next = state.lastBufferedRequest; + } else { + state.bufferedRequest = state.lastBufferedRequest; + } + state.bufferedRequestCount += 1; + } else { + doWrite(stream, state, false, len, chunk, encoding, cb); + } + + return ret; +} + +function doWrite(stream, state, writev, len, chunk, encoding, cb) { + state.writelen = len; + state.writecb = cb; + state.writing = true; + state.sync = true; + if (writev) + stream._writev(chunk, state.onwrite); + else + stream._write(chunk, encoding, state.onwrite); + state.sync = false; +} + +function onwriteError(stream, state, sync, er, cb) { + --state.pendingcb; + + if (sync) { + // defer the callback if we are being called synchronously + // to avoid piling up things on the stack + process.nextTick(cb, er); + // this can emit finish, and it will always happen + // after error + process.nextTick(finishMaybe, stream, state); + stream._writableState.errorEmitted = true; + stream.emit('error', er); + } else { + // the caller expect this to happen before if + // it is async + cb(er); + stream._writableState.errorEmitted = true; + stream.emit('error', er); + // this can emit finish, but finish must + // always follow error + finishMaybe(stream, state); + } +} + +function onwriteStateUpdate(state) { + state.writing = false; + state.writecb = null; + state.length -= state.writelen; + state.writelen = 0; +} + +function onwrite(stream, er) { + var state = stream._writableState; + var sync = state.sync; + var cb = state.writecb; + + onwriteStateUpdate(state); + + if (er) + onwriteError(stream, state, sync, er, cb); + else { + // Check if we're actually ready to finish, but don't emit yet + var finished = needFinish(state); + + if (!finished && + !state.corked && + !state.bufferProcessing && + state.bufferedRequest) { + clearBuffer(stream, state); + } + + if (sync) { + process.nextTick(afterWrite, stream, state, finished, cb); + } else { + afterWrite(stream, state, finished, cb); + } + } +} + +function afterWrite(stream, state, finished, cb) { + if (!finished) + onwriteDrain(stream, state); + state.pendingcb--; + cb(); + finishMaybe(stream, state); +} + +// Must force callback to be called on nextTick, so that we don't +// emit 'drain' before the write() consumer gets the 'false' return +// value, and has a chance to attach a 'drain' listener. +function onwriteDrain(stream, state) { + if (state.length === 0 && state.needDrain) { + state.needDrain = false; + stream.emit('drain'); + } +} + +// if there's something in the buffer waiting, then process it +function clearBuffer(stream, state) { + state.bufferProcessing = true; + var entry = state.bufferedRequest; + + if (stream._writev && entry && entry.next) { + // Fast case, write everything using _writev() + var l = state.bufferedRequestCount; + var buffer = new Array(l); + var holder = state.corkedRequestsFree; + holder.entry = entry; + + var count = 0; + var allBuffers = true; + while (entry) { + buffer[count] = entry; + if (!entry.isBuf) + allBuffers = false; + entry = entry.next; + count += 1; + } + buffer.allBuffers = allBuffers; + + doWrite(stream, state, true, state.length, buffer, '', holder.finish); + + // doWrite is almost always async, defer these to save a bit of time + // as the hot path ends with doWrite + state.pendingcb++; + state.lastBufferedRequest = null; + if (holder.next) { + state.corkedRequestsFree = holder.next; + holder.next = null; + } else { + var corkReq = { next: null, entry: null, finish: undefined }; + corkReq.finish = onCorkedFinish.bind(undefined, corkReq, state); + state.corkedRequestsFree = corkReq; + } + state.bufferedRequestCount = 0; + } else { + // Slow case, write chunks one-by-one + while (entry) { + var chunk = entry.chunk; + var encoding = entry.encoding; + var cb = entry.callback; + var len = state.objectMode ? 1 : chunk.length; + + doWrite(stream, state, false, len, chunk, encoding, cb); + entry = entry.next; + state.bufferedRequestCount--; + // if we didn't call the onwrite immediately, then + // it means that we need to wait until it does. + // also, that means that the chunk and cb are currently + // being processed, so move the buffer counter past them. + if (state.writing) { + break; + } + } + + if (entry === null) + state.lastBufferedRequest = null; + } + + state.bufferedRequest = entry; + state.bufferProcessing = false; +} + +Writable.prototype._write = function(chunk, encoding, cb) { + cb(new errors.Error('ERR_METHOD_NOT_IMPLEMENTED', '_transform')); +}; + +Writable.prototype._writev = null; + +Writable.prototype.end = function(chunk, encoding, cb) { + var state = this._writableState; + + if (typeof chunk === 'function') { + cb = chunk; + chunk = null; + encoding = null; + } else if (typeof encoding === 'function') { + cb = encoding; + encoding = null; + } + + if (chunk !== null && chunk !== undefined) + this.write(chunk, encoding); + + // .end() fully uncorks + if (state.corked) { + state.corked = 1; + this.uncork(); + } + + // ignore unnecessary end() calls. + if (!state.ending && !state.finished) + endWritable(this, state, cb); +}; + + +function needFinish(state) { + return (state.ending && + state.length === 0 && + state.bufferedRequest === null && + !state.finished && + !state.writing); +} +function callFinal(stream, state) { + stream._final((err) => { + state.pendingcb--; + if (err) { + stream.emit('error', err); + } + state.prefinished = true; + stream.emit('prefinish'); + finishMaybe(stream, state); + }); +} +function prefinish(stream, state) { + if (!state.prefinished && !state.finalCalled) { + if (typeof stream._final === 'function') { + state.pendingcb++; + state.finalCalled = true; + process.nextTick(callFinal, stream, state); + } else { + state.prefinished = true; + stream.emit('prefinish'); + } + } +} + +function finishMaybe(stream, state) { + var need = needFinish(state); + if (need) { + prefinish(stream, state); + if (state.pendingcb === 0) { + state.finished = true; + stream.emit('finish'); + } + } + return need; +} + +function endWritable(stream, state, cb) { + state.ending = true; + finishMaybe(stream, state); + if (cb) { + if (state.finished) + process.nextTick(cb); + else + stream.once('finish', cb); + } + state.ended = true; + stream.writable = false; +} + +function onCorkedFinish(corkReq, state, err) { + var entry = corkReq.entry; + corkReq.entry = null; + while (entry) { + var cb = entry.callback; + state.pendingcb--; + cb(err); + entry = entry.next; + } + if (state.corkedRequestsFree) { + state.corkedRequestsFree.next = corkReq; + } else { + state.corkedRequestsFree = corkReq; + } +} + +Object.defineProperty(Writable.prototype, 'destroyed', { + get() { + if (this._writableState === undefined) { + return false; + } + return this._writableState.destroyed; + }, + set(value) { + // we ignore the value if the stream + // has not been initialized yet + if (!this._writableState) { + return; + } + + // backward compatibility, the user is explicitly + // managing destroyed + this._writableState.destroyed = value; + } +}); + +Writable.prototype.destroy = destroyImpl.destroy; +Writable.prototype._undestroy = destroyImpl.undestroy; +Writable.prototype._destroy = function(err, cb) { + this.end(); + cb(err); +}; diff --git a/lib/internal/tls/common.js b/lib/internal/tls/common.js new file mode 100644 index 00000000000000..4196cc084c86c4 --- /dev/null +++ b/lib/internal/tls/common.js @@ -0,0 +1,224 @@ +// 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 { parseCertString } = require('internal/tls'); +const { isArrayBufferView } = require('internal/util/types'); +const tls = require('tls'); +const errors = require('internal/errors'); + +const { SSL_OP_CIPHER_SERVER_PREFERENCE } = process.binding('constants').crypto; + +// Lazily loaded +var crypto = null; + +const binding = process.binding('crypto'); +const NativeSecureContext = binding.SecureContext; + +function SecureContext(secureProtocol, secureOptions, context) { + if (!(this instanceof SecureContext)) { + return new SecureContext(secureProtocol, secureOptions, context); + } + + if (context) { + this.context = context; + } else { + this.context = new NativeSecureContext(); + + if (secureProtocol) { + this.context.init(secureProtocol); + } else { + this.context.init(); + } + } + + if (secureOptions) this.context.setOptions(secureOptions); +} + +function validateKeyCert(value, type) { + if (typeof value !== 'string' && !isArrayBufferView(value)) + throw new errors.TypeError( + 'ERR_INVALID_ARG_TYPE', type, + ['string', 'Buffer', 'TypedArray', 'DataView'] + ); +} + +exports.SecureContext = SecureContext; + + +exports.createSecureContext = function createSecureContext(options, context) { + if (!options) options = {}; + + var secureOptions = options.secureOptions; + if (options.honorCipherOrder) + secureOptions |= SSL_OP_CIPHER_SERVER_PREFERENCE; + + var c = new SecureContext(options.secureProtocol, secureOptions, context); + var i; + var val; + + if (context) return c; + + // NOTE: It's important to add CA before the cert to be able to load + // cert's issuer in C++ code. + var ca = options.ca; + if (ca) { + if (Array.isArray(ca)) { + for (i = 0; i < ca.length; ++i) { + val = ca[i]; + validateKeyCert(val, 'ca'); + c.context.addCACert(val); + } + } else { + validateKeyCert(ca, 'ca'); + c.context.addCACert(ca); + } + } else { + c.context.addRootCerts(); + } + + var cert = options.cert; + if (cert) { + if (Array.isArray(cert)) { + for (i = 0; i < cert.length; ++i) { + val = cert[i]; + validateKeyCert(val, 'cert'); + c.context.setCert(val); + } + } else { + validateKeyCert(cert, 'cert'); + c.context.setCert(cert); + } + } + + // NOTE: It is important to set the key after the cert. + // `ssl_set_pkey` returns `0` when the key does not match the cert, but + // `ssl_set_cert` returns `1` and nullifies the key in the SSL structure + // which leads to the crash later on. + var key = options.key; + var passphrase = options.passphrase; + if (key) { + if (Array.isArray(key)) { + for (i = 0; i < key.length; ++i) { + val = key[i]; + // eslint-disable-next-line eqeqeq + const pem = (val != undefined && val.pem !== undefined ? val.pem : val); + validateKeyCert(pem, 'key'); + c.context.setKey(pem, val.passphrase || passphrase); + } + } else { + validateKeyCert(key, 'key'); + c.context.setKey(key, passphrase); + } + } + + if (options.ciphers) + c.context.setCiphers(options.ciphers); + else + c.context.setCiphers(tls.DEFAULT_CIPHERS); + + if (options.ecdhCurve === undefined) + c.context.setECDHCurve(tls.DEFAULT_ECDH_CURVE); + else if (options.ecdhCurve) + c.context.setECDHCurve(options.ecdhCurve); + + if (options.dhparam) { + const warning = c.context.setDHParam(options.dhparam); + if (warning) + process.emitWarning(warning, 'SecurityWarning'); + } + + if (options.crl) { + if (Array.isArray(options.crl)) { + for (i = 0; i < options.crl.length; i++) { + c.context.addCRL(options.crl[i]); + } + } else { + c.context.addCRL(options.crl); + } + } + + if (options.sessionIdContext) { + c.context.setSessionIdContext(options.sessionIdContext); + } + + if (options.pfx) { + if (!crypto) + crypto = require('crypto'); + + if (Array.isArray(options.pfx)) { + for (i = 0; i < options.pfx.length; i++) { + const pfx = options.pfx[i]; + const raw = pfx.buf ? pfx.buf : pfx; + const buf = crypto._toBuf(raw); + const passphrase = pfx.passphrase || options.passphrase; + if (passphrase) { + c.context.loadPKCS12(buf, crypto._toBuf(passphrase)); + } else { + c.context.loadPKCS12(buf); + } + } + } else { + const buf = crypto._toBuf(options.pfx); + const passphrase = options.passphrase; + if (passphrase) { + c.context.loadPKCS12(buf, crypto._toBuf(passphrase)); + } else { + c.context.loadPKCS12(buf); + } + } + } + + // Do not keep read/write buffers in free list for OpenSSL < 1.1.0. (For + // OpenSSL 1.1.0, buffers are malloced and freed without the use of a + // freelist.) + if (options.singleUse) { + c.singleUse = true; + c.context.setFreeListLength(0); + } + + return c; +}; + +exports.translatePeerCertificate = function translatePeerCertificate(c) { + if (!c) + return null; + + if (c.issuer != null) c.issuer = parseCertString(c.issuer); + if (c.issuerCertificate != null && c.issuerCertificate !== c) { + c.issuerCertificate = translatePeerCertificate(c.issuerCertificate); + } + if (c.subject != null) c.subject = parseCertString(c.subject); + if (c.infoAccess != null) { + var info = c.infoAccess; + c.infoAccess = Object.create(null); + + // XXX: More key validation? + info.replace(/([^\n:]*):([^\n]*)(?:\n|$)/g, function(all, key, val) { + if (key in c.infoAccess) + c.infoAccess[key].push(val); + else + c.infoAccess[key] = [val]; + }); + } + return c; +}; diff --git a/lib/internal/tls/legacy.js b/lib/internal/tls/legacy.js new file mode 100644 index 00000000000000..13ff35bf02151b --- /dev/null +++ b/lib/internal/tls/legacy.js @@ -0,0 +1,954 @@ +// 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 internalUtil = require('internal/util'); +internalUtil.assertCrypto(); + +const assert = require('assert'); +const { Buffer } = require('buffer'); +const common = require('internal/tls/common'); +const { Connection } = process.binding('crypto'); +const EventEmitter = require('events'); +const stream = require('stream'); +const { Timer } = process.binding('timer_wrap'); +const tls = require('tls'); +const util = require('util'); + +const debug = util.debuglog('tls-legacy'); + +function SlabBuffer() { + this.create(); +} + + +SlabBuffer.prototype.create = function create() { + this.isFull = false; + this.pool = Buffer.allocUnsafe(tls.SLAB_BUFFER_SIZE); + this.offset = 0; + this.remaining = this.pool.length; +}; + + +SlabBuffer.prototype.use = function use(context, fn, size) { + if (this.remaining === 0) { + this.isFull = true; + return 0; + } + + var actualSize = this.remaining; + + if (size !== null) actualSize = Math.min(size, actualSize); + + var bytes = fn.call(context, this.pool, this.offset, actualSize); + if (bytes > 0) { + this.offset += bytes; + this.remaining -= bytes; + } + + assert(this.remaining >= 0); + + return bytes; +}; + + +var slabBuffer = null; + + +// Base class of both CleartextStream and EncryptedStream +function CryptoStream(pair, options) { + stream.Duplex.call(this, options); + + this.pair = pair; + this._pending = null; + this._pendingEncoding = ''; + this._pendingCallback = null; + this._doneFlag = false; + this._retryAfterPartial = false; + this._halfRead = false; + this._sslOutCb = null; + this._resumingSession = false; + this._reading = true; + this._destroyed = false; + this._ended = false; + this._finished = false; + this._opposite = null; + + if (slabBuffer === null) slabBuffer = new SlabBuffer(); + this._buffer = slabBuffer; + + this.once('finish', onCryptoStreamFinish); + + // net.Socket calls .onend too + this.once('end', onCryptoStreamEnd); +} +util.inherits(CryptoStream, stream.Duplex); + + +function onCryptoStreamFinish() { + this._finished = true; + + if (this === this.pair.cleartext) { + debug('cleartext.onfinish'); + if (this.pair.ssl) { + // Generate close notify + // NOTE: first call checks if client has sent us shutdown, + // second call enqueues shutdown into the BIO. + if (this.pair.ssl.shutdownSSL() !== 1) { + if (this.pair.ssl && this.pair.ssl.error) + return this.pair.error(); + + this.pair.ssl.shutdownSSL(); + } + + if (this.pair.ssl && this.pair.ssl.error) + return this.pair.error(); + } + } else { + debug('encrypted.onfinish'); + } + + // Try to read just to get sure that we won't miss EOF + if (this._opposite.readable) this._opposite.read(0); + + if (this._opposite._ended) { + this._done(); + + // No half-close, sorry + if (this === this.pair.cleartext) this._opposite._done(); + } +} + + +function onCryptoStreamEnd() { + this._ended = true; + if (this === this.pair.cleartext) { + debug('cleartext.onend'); + } else { + debug('encrypted.onend'); + } +} + + +// NOTE: Called once `this._opposite` is set. +CryptoStream.prototype.init = function init() { + var self = this; + this._opposite.on('sslOutEnd', function() { + if (self._sslOutCb) { + var cb = self._sslOutCb; + self._sslOutCb = null; + cb(null); + } + }); +}; + + +CryptoStream.prototype._write = function _write(data, encoding, cb) { + assert(this._pending === null); + + // Black-hole data + if (!this.pair.ssl) return cb(null); + + // When resuming session don't accept any new data. + // And do not put too much data into openssl, before writing it from encrypted + // side. + // + // TODO(indutny): Remove magic number, use watermark based limits + if (!this._resumingSession && + this._opposite._internallyPendingBytes() < 128 * 1024) { + // Write current buffer now + var written; + if (this === this.pair.cleartext) { + debug('cleartext.write called with %d bytes', data.length); + written = this.pair.ssl.clearIn(data, 0, data.length); + } else { + debug('encrypted.write called with %d bytes', data.length); + written = this.pair.ssl.encIn(data, 0, data.length); + } + + // Handle and report errors + if (this.pair.ssl && this.pair.ssl.error) { + return cb(this.pair.error(true)); + } + + // Force SSL_read call to cycle some states/data inside OpenSSL + this.pair.cleartext.read(0); + + // Cycle encrypted data + if (this.pair.encrypted._internallyPendingBytes()) + this.pair.encrypted.read(0); + + // Get ALPN, NPN and Server name when ready + this.pair.maybeInitFinished(); + + // Whole buffer was written + if (written === data.length) { + if (this === this.pair.cleartext) { + debug('cleartext.write succeed with ' + written + ' bytes'); + } else { + debug('encrypted.write succeed with ' + written + ' bytes'); + } + + // Invoke callback only when all data read from opposite stream + if (this._opposite._halfRead) { + assert(this._sslOutCb === null); + this._sslOutCb = cb; + } else { + cb(null); + } + return; + } else if (written !== 0 && written !== -1) { + assert(!this._retryAfterPartial); + this._retryAfterPartial = true; + this._write(data.slice(written), encoding, cb); + this._retryAfterPartial = false; + return; + } + } else { + debug('cleartext.write queue is full'); + + // Force SSL_read call to cycle some states/data inside OpenSSL + this.pair.cleartext.read(0); + } + + // No write has happened + this._pending = data; + this._pendingEncoding = encoding; + this._pendingCallback = cb; + + if (this === this.pair.cleartext) { + debug('cleartext.write queued with %d bytes', data.length); + } else { + debug('encrypted.write queued with %d bytes', data.length); + } +}; + + +CryptoStream.prototype._writePending = function _writePending() { + const data = this._pending; + const encoding = this._pendingEncoding; + const cb = this._pendingCallback; + + this._pending = null; + this._pendingEncoding = ''; + this._pendingCallback = null; + this._write(data, encoding, cb); +}; + + +CryptoStream.prototype._read = function _read(size) { + // XXX: EOF?! + if (!this.pair.ssl) return this.push(null); + + // Wait for session to be resumed + // Mark that we're done reading, but don't provide data or EOF + if (this._resumingSession || !this._reading) return this.push(''); + + var out; + if (this === this.pair.cleartext) { + debug('cleartext.read called with %d bytes', size); + out = this.pair.ssl.clearOut; + } else { + debug('encrypted.read called with %d bytes', size); + out = this.pair.ssl.encOut; + } + + var bytesRead = 0; + const start = this._buffer.offset; + var last = start; + do { + assert(last === this._buffer.offset); + var read = this._buffer.use(this.pair.ssl, out, size - bytesRead); + if (read > 0) { + bytesRead += read; + } + last = this._buffer.offset; + + // Handle and report errors + if (this.pair.ssl && this.pair.ssl.error) { + this.pair.error(); + break; + } + } while (read > 0 && + !this._buffer.isFull && + bytesRead < size && + this.pair.ssl !== null); + + // Get ALPN, NPN and Server name when ready + this.pair.maybeInitFinished(); + + // Create new buffer if previous was filled up + var pool = this._buffer.pool; + if (this._buffer.isFull) this._buffer.create(); + + assert(bytesRead >= 0); + + if (this === this.pair.cleartext) { + debug('cleartext.read succeed with %d bytes', bytesRead); + } else { + debug('encrypted.read succeed with %d bytes', bytesRead); + } + + // Try writing pending data + if (this._pending !== null) this._writePending(); + if (this._opposite._pending !== null) this._opposite._writePending(); + + if (bytesRead === 0) { + // EOF when cleartext has finished and we have nothing to read + if (this._opposite._finished && this._internallyPendingBytes() === 0 || + this.pair.ssl && this.pair.ssl.receivedShutdown) { + // Perform graceful shutdown + this._done(); + + // No half-open, sorry! + if (this === this.pair.cleartext) { + this._opposite._done(); + + // EOF + this.push(null); + } else if (!this.pair.ssl || !this.pair.ssl.receivedShutdown) { + // EOF + this.push(null); + } + } else { + // Bail out + this.push(''); + } + } else { + // Give them requested data + this.push(pool.slice(start, start + bytesRead)); + } + + // Let users know that we've some internal data to read + var halfRead = this._internallyPendingBytes() !== 0; + + // Smart check to avoid invoking 'sslOutEnd' in the most of the cases + if (this._halfRead !== halfRead) { + this._halfRead = halfRead; + + // Notify listeners about internal data end + if (!halfRead) { + if (this === this.pair.cleartext) { + debug('cleartext.sslOutEnd'); + } else { + debug('encrypted.sslOutEnd'); + } + + this.emit('sslOutEnd'); + } + } +}; + + +CryptoStream.prototype.setTimeout = function(timeout, callback) { + if (this.socket) this.socket.setTimeout(timeout, callback); +}; + + +CryptoStream.prototype.setNoDelay = function(noDelay) { + if (this.socket) this.socket.setNoDelay(noDelay); +}; + + +CryptoStream.prototype.setKeepAlive = function(enable, initialDelay) { + if (this.socket) this.socket.setKeepAlive(enable, initialDelay); +}; + +Object.defineProperty(CryptoStream.prototype, 'bytesWritten', { + configurable: true, + enumerable: true, + get: function() { + return this.socket ? this.socket.bytesWritten : 0; + } +}); + +CryptoStream.prototype.getPeerCertificate = function(detailed) { + if (this.pair.ssl) { + return common.translatePeerCertificate( + this.pair.ssl.getPeerCertificate(detailed)); + } + + return null; +}; + +CryptoStream.prototype.getSession = function() { + if (this.pair.ssl) { + return this.pair.ssl.getSession(); + } + + return null; +}; + +CryptoStream.prototype.isSessionReused = function() { + if (this.pair.ssl) { + return this.pair.ssl.isSessionReused(); + } + + return null; +}; + +CryptoStream.prototype.getCipher = function(err) { + if (this.pair.ssl) { + return this.pair.ssl.getCurrentCipher(); + } else { + return null; + } +}; + + +CryptoStream.prototype.end = function(chunk, encoding) { + if (this === this.pair.cleartext) { + debug('cleartext.end'); + } else { + debug('encrypted.end'); + } + + // Write pending data first + if (this._pending !== null) this._writePending(); + + this.writable = false; + + stream.Duplex.prototype.end.call(this, chunk, encoding); +}; + + +CryptoStream.prototype.destroySoon = function(err) { + if (this === this.pair.cleartext) { + debug('cleartext.destroySoon'); + } else { + debug('encrypted.destroySoon'); + } + + if (this.writable) + this.end(); + + if (this._writableState.finished && this._opposite._ended) { + this.destroy(); + } else { + // Wait for both `finish` and `end` events to ensure that all data that + // was written on this side was read from the other side. + var self = this; + var waiting = 1; + function finish() { + if (--waiting === 0) self.destroy(); + } + this._opposite.once('end', finish); + if (!this._finished) { + this.once('finish', finish); + ++waiting; + } + } +}; + + +CryptoStream.prototype.destroy = function(err) { + if (this._destroyed) return; + this._destroyed = true; + this.readable = this.writable = false; + + // Destroy both ends + if (this === this.pair.cleartext) { + debug('cleartext.destroy'); + } else { + debug('encrypted.destroy'); + } + this._opposite.destroy(); + + process.nextTick(destroyNT, this, err ? true : false); +}; + + +function destroyNT(self, hadErr) { + // Force EOF + self.push(null); + + // Emit 'close' event + self.emit('close', hadErr); +} + + +CryptoStream.prototype._done = function() { + this._doneFlag = true; + + if (this === this.pair.encrypted && !this.pair._secureEstablished) + return this.pair.error(); + + if (this.pair.cleartext._doneFlag && + this.pair.encrypted._doneFlag && + !this.pair._doneFlag) { + // If both streams are done: + this.pair.destroy(); + } +}; + + +// readyState is deprecated. Don't use it. +// Deprecation Code: DEP0004 +Object.defineProperty(CryptoStream.prototype, 'readyState', { + get: function() { + if (this.connecting) { + return 'opening'; + } else if (this.readable && this.writable) { + return 'open'; + } else if (this.readable && !this.writable) { + return 'readOnly'; + } else if (!this.readable && this.writable) { + return 'writeOnly'; + } else { + return 'closed'; + } + } +}); + + +function CleartextStream(pair, options) { + CryptoStream.call(this, pair, options); + + // This is a fake kludge to support how the http impl sits + // on top of net Sockets + var self = this; + this._handle = { + readStop: function() { + self._reading = false; + }, + readStart: function() { + if (self._reading && self._readableState.length > 0) return; + self._reading = true; + self.read(0); + if (self._opposite.readable) self._opposite.read(0); + } + }; +} +util.inherits(CleartextStream, CryptoStream); + + +CleartextStream.prototype._internallyPendingBytes = function() { + if (this.pair.ssl) { + return this.pair.ssl.clearPending(); + } else { + return 0; + } +}; + + +CleartextStream.prototype.address = function() { + return this.socket && this.socket.address(); +}; + +Object.defineProperty(CleartextStream.prototype, 'remoteAddress', { + configurable: true, + enumerable: true, + get: function() { + return this.socket && this.socket.remoteAddress; + } +}); + +Object.defineProperty(CleartextStream.prototype, 'remoteFamily', { + configurable: true, + enumerable: true, + get: function() { + return this.socket && this.socket.remoteFamily; + } +}); + +Object.defineProperty(CleartextStream.prototype, 'remotePort', { + configurable: true, + enumerable: true, + get: function() { + return this.socket && this.socket.remotePort; + } +}); + +Object.defineProperty(CleartextStream.prototype, 'localAddress', { + configurable: true, + enumerable: true, + get: function() { + return this.socket && this.socket.localAddress; + } +}); + +Object.defineProperty(CleartextStream.prototype, 'localPort', { + configurable: true, + enumerable: true, + get: function() { + return this.socket && this.socket.localPort; + } +}); + + +function EncryptedStream(pair, options) { + CryptoStream.call(this, pair, options); +} +util.inherits(EncryptedStream, CryptoStream); + + +EncryptedStream.prototype._internallyPendingBytes = function() { + if (this.pair.ssl) { + return this.pair.ssl.encPending(); + } else { + return 0; + } +}; + + +function onhandshakestart() { + debug('onhandshakestart'); + + var self = this; + var ssl = self.ssl; + var now = Timer.now(); + + assert(now >= ssl.lastHandshakeTime); + + if ((now - ssl.lastHandshakeTime) >= tls.CLIENT_RENEG_WINDOW * 1000) { + ssl.handshakes = 0; + } + + var first = (ssl.lastHandshakeTime === 0); + ssl.lastHandshakeTime = now; + if (first) return; + + if (++ssl.handshakes > tls.CLIENT_RENEG_LIMIT) { + // Defer the error event to the next tick. We're being called from OpenSSL's + // state machine and OpenSSL is not re-entrant. We cannot allow the user's + // callback to destroy the connection right now, it would crash and burn. + setImmediate(function() { + var err = new Error('TLS session renegotiation attack detected'); + if (self.cleartext) self.cleartext.emit('error', err); + }); + } +} + + +function onhandshakedone() { + // for future use + debug('onhandshakedone'); +} + + +function onclienthello(hello) { + const self = this; + var once = false; + + this._resumingSession = true; + function callback(err, session) { + if (once) return; + once = true; + + if (err) return self.socket.destroy(err); + + setImmediate(function() { + self.ssl.loadSession(session); + self.ssl.endParser(); + + // Cycle data + self._resumingSession = false; + self.cleartext.read(0); + self.encrypted.read(0); + }); + } + + if (hello.sessionId.length <= 0 || + !this.server || + !this.server.emit('resumeSession', hello.sessionId, callback)) { + callback(null, null); + } +} + + +function onnewsession(key, session) { + if (!this.server) return; + + var self = this; + var once = false; + + if (!self.server.emit('newSession', key, session, done)) + done(); + + function done() { + if (once) + return; + once = true; + + if (self.ssl) + self.ssl.newSessionDone(); + } +} + + +function onocspresponse(resp) { + this.emit('OCSPResponse', resp); +} + + +/** + * Provides a pair of streams to do encrypted communication. + */ + +function SecurePair(context, isServer, requestCert, rejectUnauthorized, + options) { + if (!(this instanceof SecurePair)) { + return new SecurePair(context, + isServer, + requestCert, + rejectUnauthorized, + options); + } + + options || (options = {}); + + EventEmitter.call(this); + + this.server = options.server; + this._secureEstablished = false; + this._isServer = isServer ? true : false; + this._encWriteState = true; + this._clearWriteState = true; + this._doneFlag = false; + this._destroying = false; + + if (!context) { + this.credentials = tls.createSecureContext(); + } else { + this.credentials = context; + } + + if (!this._isServer) { + // For clients, we will always have either a given ca list or be using + // default one + requestCert = true; + } + + this._rejectUnauthorized = rejectUnauthorized ? true : false; + this._requestCert = requestCert ? true : false; + + this.ssl = new Connection( + this.credentials.context, + this._isServer ? true : false, + this._isServer ? this._requestCert : options.servername, + this._rejectUnauthorized + ); + + if (this._isServer) { + this.ssl.onhandshakestart = () => onhandshakestart.call(this); + this.ssl.onhandshakedone = () => onhandshakedone.call(this); + this.ssl.onclienthello = (hello) => onclienthello.call(this, hello); + this.ssl.onnewsession = + (key, session) => onnewsession.call(this, key, session); + this.ssl.lastHandshakeTime = 0; + this.ssl.handshakes = 0; + } else { + this.ssl.onocspresponse = (resp) => onocspresponse.call(this, resp); + } + + if (process.features.tls_sni) { + if (this._isServer && options.SNICallback) { + this.ssl.setSNICallback(options.SNICallback); + } + this.servername = null; + } + + if (process.features.tls_npn && options.NPNProtocols) { + this.ssl.setNPNProtocols(options.NPNProtocols); + this.npnProtocol = null; + } + + if (process.features.tls_alpn && options.ALPNProtocols) { + // keep reference in secureContext not to be GC-ed + this.ssl._secureContext.alpnBuffer = options.ALPNProtocols; + this.ssl.setALPNrotocols(this.ssl._secureContext.alpnBuffer); + this.alpnProtocol = null; + } + + /* Acts as a r/w stream to the cleartext side of the stream. */ + this.cleartext = new CleartextStream(this, options.cleartext); + + /* Acts as a r/w stream to the encrypted side of the stream. */ + this.encrypted = new EncryptedStream(this, options.encrypted); + + /* Let streams know about each other */ + this.cleartext._opposite = this.encrypted; + this.encrypted._opposite = this.cleartext; + this.cleartext.init(); + this.encrypted.init(); + + process.nextTick(securePairNT, this, options); +} + +util.inherits(SecurePair, EventEmitter); + +function securePairNT(self, options) { + /* The Connection may be destroyed by an abort call */ + if (self.ssl) { + self.ssl.start(); + + if (options.requestOCSP) + self.ssl.requestOCSP(); + + /* In case of cipher suite failures - SSL_accept/SSL_connect may fail */ + if (self.ssl && self.ssl.error) + self.error(); + } +} + + +function createSecurePair(context, isServer, requestCert, + rejectUnauthorized, options) { + return new SecurePair(context, isServer, requestCert, + rejectUnauthorized, options); +} + + +SecurePair.prototype.maybeInitFinished = function() { + if (this.ssl && !this._secureEstablished && this.ssl.isInitFinished()) { + if (process.features.tls_npn) { + this.npnProtocol = this.ssl.getNegotiatedProtocol(); + } + + if (process.features.tls_alpn) { + this.alpnProtocol = this.ssl.getALPNNegotiatedProtocol(); + } + + if (process.features.tls_sni) { + this.servername = this.ssl.getServername(); + } + + this._secureEstablished = true; + debug('secure established'); + this.emit('secure'); + } +}; + + +SecurePair.prototype.destroy = function() { + if (this._destroying) return; + + if (!this._doneFlag) { + debug('SecurePair.destroy'); + this._destroying = true; + + // SecurePair should be destroyed only after it's streams + this.cleartext.destroy(); + this.encrypted.destroy(); + + this._doneFlag = true; + this.ssl.error = null; + this.ssl.close(); + this.ssl = null; + } +}; + + +SecurePair.prototype.error = function(returnOnly) { + var err = this.ssl.error; + this.ssl.error = null; + + if (!this._secureEstablished) { + // Emit ECONNRESET instead of zero return + if (!err || err.message === 'ZERO_RETURN') { + var connReset = new Error('socket hang up'); + connReset.code = 'ECONNRESET'; + connReset.sslError = err && err.message; + + err = connReset; + } + this.destroy(); + if (!returnOnly) this.emit('error', err); + } else if (this._isServer && + this._rejectUnauthorized && + /peer did not return a certificate/.test(err.message)) { + // Not really an error. + this.destroy(); + } else if (!returnOnly) { + this.cleartext.emit('error', err); + } + return err; +}; + + +function pipe(pair, socket) { + pair.encrypted.pipe(socket); + socket.pipe(pair.encrypted); + + pair.encrypted.on('close', function() { + process.nextTick(pipeCloseNT, pair, socket); + }); + + pair.fd = socket.fd; + var cleartext = pair.cleartext; + cleartext.socket = socket; + cleartext.encrypted = pair.encrypted; + cleartext.authorized = false; + + // cycle the data whenever the socket drains, so that + // we can pull some more into it. normally this would + // be handled by the fact that pipe() triggers read() calls + // on writable.drain, but CryptoStreams are a bit more + // complicated. Since the encrypted side actually gets + // its data from the cleartext side, we have to give it a + // light kick to get in motion again. + socket.on('drain', function() { + if (pair.encrypted._pending) + pair.encrypted._writePending(); + if (pair.cleartext._pending) + pair.cleartext._writePending(); + pair.encrypted.read(0); + pair.cleartext.read(0); + }); + + function onerror(e) { + if (cleartext._controlReleased) { + cleartext.emit('error', e); + } + } + + function onclose() { + socket.removeListener('error', onerror); + socket.removeListener('timeout', ontimeout); + } + + function ontimeout() { + cleartext.emit('timeout'); + } + + socket.on('error', onerror); + socket.on('close', onclose); + socket.on('timeout', ontimeout); + + return cleartext; +} + + +function pipeCloseNT(pair, socket) { + // Encrypted should be unpiped from socket to prevent possible + // write after destroy. + pair.encrypted.unpipe(socket); + socket.destroySoon(); +} + +module.exports = { + createSecurePair: + internalUtil.deprecate(createSecurePair, + 'tls.createSecurePair() is deprecated. Please use ' + + 'tls.Socket instead.', 'DEP0064'), + pipe +}; diff --git a/lib/internal/tls/wrap.js b/lib/internal/tls/wrap.js new file mode 100644 index 00000000000000..a3c6ccf356e208 --- /dev/null +++ b/lib/internal/tls/wrap.js @@ -0,0 +1,1152 @@ +// 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'; + +require('internal/util').assertCrypto(); + +const assert = require('assert'); +const crypto = require('crypto'); +const net = require('net'); +const tls = require('tls'); +const util = require('util'); +const common = require('internal/tls/common'); +const { StreamWrap } = require('internal/streams/wrap'); +const { Buffer } = require('buffer'); +const debug = util.debuglog('tls'); +const { Timer } = process.binding('timer_wrap'); +const tls_wrap = process.binding('tls_wrap'); +const { TCP } = process.binding('tcp_wrap'); +const { Pipe } = process.binding('pipe_wrap'); +const errors = require('internal/errors'); +const kConnectOptions = Symbol('connect-options'); +const kDisableRenegotiation = Symbol('disable-renegotiation'); +const kErrorEmitted = Symbol('error-emitted'); +const kHandshakeTimeout = Symbol('handshake-timeout'); +const kRes = Symbol('res'); +const kSNICallback = Symbol('snicallback'); + +const noop = () => {}; + +function onhandshakestart() { + debug('onhandshakestart'); + + const owner = this.owner; + const now = Timer.now(); + + assert(now >= this.lastHandshakeTime); + + if ((now - this.lastHandshakeTime) >= tls.CLIENT_RENEG_WINDOW * 1000) { + this.handshakes = 0; + } + + const first = (this.lastHandshakeTime === 0); + this.lastHandshakeTime = now; + if (first) return; + + if (++this.handshakes > tls.CLIENT_RENEG_LIMIT) { + // Defer the error event to the next tick. We're being called from OpenSSL's + // state machine and OpenSSL is not re-entrant. We cannot allow the user's + // callback to destroy the connection right now, it would crash and burn. + setImmediate(emitSessionAttackError, owner); + } + + if (owner[kDisableRenegotiation] && this.handshakes > 0) { + const err = new Error('TLS session renegotiation disabled for this socket'); + owner._emitTLSError(err); + } +} + +function emitSessionAttackError(socket) { + socket._emitTLSError(new errors.Error('ERR_TLS_SESSION_ATTACK')); +} + +function onhandshakedone() { + debug('onhandshakedone'); + + const owner = this.owner; + + // `newSession` callback wasn't called yet + if (owner._newSessionPending) { + owner._securePending = true; + return; + } + + owner._finishInit(); +} + + +function loadSession(hello) { + const owner = this.owner; + + var once = false; + function onSession(err, session) { + if (once) + return owner.destroy(new errors.Error('ERR_MULTIPLE_CALLBACK')); + once = true; + + if (err) + return owner.destroy(err); + + if (owner._handle === null) + return owner.destroy(new errors.Error('ERR_SOCKET_CLOSED')); + + owner._handle.loadSession(session); + owner._handle.endParser(); + } + + if (hello.sessionId.length <= 0 || + hello.tlsTicket || + owner.server && + !owner.server.emit('resumeSession', hello.sessionId, onSession)) { + owner._handle.endParser(); + } +} + + +function loadSNI(info) { + const owner = this.owner; + const servername = info.servername; + if (!servername || !owner._SNICallback) + return requestOCSP(owner, info); + + let once = false; + owner._SNICallback(servername, (err, context) => { + if (once) + return owner.destroy(new errors.Error('ERR_MULTIPLE_CALLBACK')); + once = true; + + if (err) + return owner.destroy(err); + + if (owner._handle === null) + return owner.destroy(new errors.Error('ERR_SOCKET_CLOSED')); + + // TODO(indutny): eventually disallow raw `SecureContext` + if (context) + owner._handle.sni_context = context.context || context; + + requestOCSP(owner, info); + }); +} + + +function requestOCSP(socket, info) { + if (!info.OCSPRequest || !socket.server) + return requestOCSPDone(socket); + + let ctx = socket._handle.sni_context; + + if (!ctx) { + ctx = socket.server._sharedCreds; + + // TLS socket is using a `net.Server` instead of a tls.TLSServer. + // Some TLS properties like `server._sharedCreds` will not be present + if (!ctx) + return requestOCSPDone(socket); + } + + // TODO(indutny): eventually disallow raw `SecureContext` + if (ctx.context) + ctx = ctx.context; + + if (socket.server.listenerCount('OCSPRequest') === 0) { + return requestOCSPDone(socket); + } + + let once = false; + const onOCSP = (err, response) => { + if (once) + return socket.destroy(new errors.Error('ERR_MULTIPLE_CALLBACK')); + once = true; + + if (err) + return socket.destroy(err); + + if (socket._handle === null) + return socket.destroy(new errors.Error('ERR_SOCKET_CLOSED')); + + if (response) + socket._handle.setOCSPResponse(response); + requestOCSPDone(socket); + }; + + socket.server.emit('OCSPRequest', + ctx.getCertificate(), + ctx.getIssuer(), + onOCSP); +} + +function requestOCSPDone(socket) { + try { + socket._handle.certCbDone(); + } catch (e) { + socket.destroy(e); + } +} + + +function onnewsession(key, session) { + const owner = this.owner; + + if (!owner.server) + return; + + var once = false; + const done = () => { + if (once) + return; + once = true; + + if (owner._handle === null) + return owner.destroy(new errors.Error('ERR_SOCKET_CLOSED')); + + this.newSessionDone(); + + owner._newSessionPending = false; + if (owner._securePending) + owner._finishInit(); + owner._securePending = false; + }; + + owner._newSessionPending = true; + if (!owner.server.emit('newSession', key, session, done)) + done(); +} + + +function onocspresponse(resp) { + this.owner.emit('OCSPResponse', resp); +} + +function onerror(err) { + const owner = this.owner; + + if (owner._writableState.errorEmitted) + return; + + // Destroy socket if error happened before handshake's finish + if (!owner._secureEstablished) { + // When handshake fails control is not yet released, + // so self._tlsError will return null instead of actual error + owner.destroy(err); + } else if (owner._tlsOptions.isServer && + owner._rejectUnauthorized && + /peer did not return a certificate/.test(err.message)) { + // Ignore server's authorization errors + owner.destroy(); + } else { + // Throw error + owner._emitTLSError(err); + } + + owner._writableState.errorEmitted = true; +} + +function initRead(tls, wrapped) { + // If we were destroyed already don't bother reading + if (!tls._handle) + return; + + // Socket already has some buffered data - emulate receiving it + if (wrapped && wrapped._readableState && wrapped._readableState.length) { + var buf; + while ((buf = wrapped.read()) !== null) + tls._handle.receive(buf); + } + + tls.read(0); +} + +/** + * Provides a wrap of socket stream to do encrypted communication. + */ + +function TLSSocket(socket, options) { + if (options === undefined) + this._tlsOptions = {}; + else + this._tlsOptions = options; + this._secureEstablished = false; + this._securePending = false; + this._newSessionPending = false; + this._controlReleased = false; + this._SNICallback = null; + this.servername = null; + this.npnProtocol = null; + this.alpnProtocol = null; + this.authorized = false; + this.authorizationError = null; + this[kRes] = null; + + // Wrap plain JS Stream into StreamWrap + var wrap; + if ((socket instanceof net.Socket && socket._handle) || !socket) + wrap = socket; + else + wrap = new StreamWrap(socket); + + // Just a documented property to make secure sockets + // distinguishable from regular ones. + this.encrypted = true; + + net.Socket.call(this, { + handle: this._wrapHandle(wrap), + allowHalfOpen: socket && socket.allowHalfOpen, + readable: false, + writable: false + }); + + // Proxy for API compatibility + this.ssl = this._handle; + + this.on('error', this._tlsError); + + this._init(socket, wrap); + + // Make sure to setup all required properties like: `connecting` before + // starting the flow of the data + this.readable = true; + this.writable = true; + + // Read on next tick so the caller has a chance to setup listeners + process.nextTick(initRead, this, socket); +} +util.inherits(TLSSocket, net.Socket); +exports.TLSSocket = TLSSocket; + +var proxiedMethods = [ + 'ref', 'unref', 'open', 'bind', 'listen', 'connect', 'bind6', + 'connect6', 'getsockname', 'getpeername', 'setNoDelay', 'setKeepAlive', + 'setSimultaneousAccepts', 'setBlocking', + + // PipeWrap + 'setPendingInstances' +]; + +// Proxy HandleWrap, PipeWrap and TCPWrap methods +function makeMethodProxy(name) { + return function methodProxy(...args) { + if (this._parent[name]) + return this._parent[name].apply(this._parent, args); + }; +} +for (var n = 0; n < proxiedMethods.length; n++) { + tls_wrap.TLSWrap.prototype[proxiedMethods[n]] = + makeMethodProxy(proxiedMethods[n]); +} + +tls_wrap.TLSWrap.prototype.close = function close(cb) { + let ssl; + if (this.owner) { + ssl = this.owner.ssl; + this.owner.ssl = null; + } + + // Invoke `destroySSL` on close to clean up possibly pending write requests + // that may self-reference TLSWrap, leading to leak + const done = () => { + if (ssl) { + ssl.destroySSL(); + if (ssl._secureContext.singleUse) { + ssl._secureContext.context.close(); + ssl._secureContext.context = null; + } + } + if (cb) + cb(); + }; + + if (this._parentWrap && this._parentWrap._handle === this._parent) { + this._parentWrap.once('close', done); + return this._parentWrap.destroy(); + } + return this._parent.close(done); +}; + +TLSSocket.prototype.disableRenegotiation = function disableRenegotiation() { + this[kDisableRenegotiation] = true; +}; + +TLSSocket.prototype._wrapHandle = function(wrap) { + var handle; + + if (wrap) + handle = wrap._handle; + + var options = this._tlsOptions; + if (!handle) { + handle = options.pipe ? new Pipe() : new TCP(); + handle.owner = this; + } + + // Wrap socket's handle + const context = options.secureContext || + options.credentials || + tls.createSecureContext(options); + const res = tls_wrap.wrap(handle._externalStream, + context.context, + !!options.isServer); + res._parent = handle; + res._parentWrap = wrap; + res._secureContext = context; + res.reading = handle.reading; + this[kRes] = res; + defineHandleReading(this, handle); + + this.on('close', onSocketCloseDestroySSL); + + return res; +}; + +// This eliminates a cyclic reference to TLSWrap +// Ref: https://github.com/nodejs/node/commit/f7620fb96d339f704932f9bb9a0dceb9952df2d4 +function defineHandleReading(socket, handle) { + Object.defineProperty(handle, 'reading', { + get: () => { + return socket[kRes].reading; + }, + set: (value) => { + socket[kRes].reading = value; + } + }); +} + +function onSocketCloseDestroySSL() { + // Make sure we are not doing it on OpenSSL's stack + setImmediate(destroySSL, this); + this[kRes] = null; +} + +function destroySSL(self) { + self._destroySSL(); +} + +TLSSocket.prototype._destroySSL = function _destroySSL() { + if (!this.ssl) return; + this.ssl.destroySSL(); + if (this.ssl._secureContext.singleUse) { + this.ssl._secureContext.context.close(); + this.ssl._secureContext.context = null; + } + this.ssl = null; +}; + +TLSSocket.prototype._init = function(socket, wrap) { + var options = this._tlsOptions; + var ssl = this._handle; + + // lib/net.js expect this value to be non-zero if write hasn't been flushed + // immediately. After the handshake is done this will represent the actual + // write queue size + ssl.writeQueueSize = 1; + + this.server = options.server; + + // For clients, we will always have either a given ca list or be using + // default one + const requestCert = !!options.requestCert || !options.isServer; + const rejectUnauthorized = !!options.rejectUnauthorized; + + this._requestCert = requestCert; + this._rejectUnauthorized = rejectUnauthorized; + if (requestCert || rejectUnauthorized) + ssl.setVerifyMode(requestCert, rejectUnauthorized); + + if (options.isServer) { + ssl.onhandshakestart = onhandshakestart; + ssl.onhandshakedone = onhandshakedone; + ssl.onclienthello = loadSession; + ssl.oncertcb = loadSNI; + ssl.onnewsession = onnewsession; + ssl.lastHandshakeTime = 0; + ssl.handshakes = 0; + + if (this.server) { + if (this.server.listenerCount('resumeSession') > 0 || + this.server.listenerCount('newSession') > 0) { + ssl.enableSessionCallbacks(); + } + if (this.server.listenerCount('OCSPRequest') > 0) + ssl.enableCertCb(); + } + } else { + ssl.onhandshakestart = noop; + ssl.onhandshakedone = this._finishInit.bind(this); + ssl.onocspresponse = onocspresponse; + + if (options.session) + ssl.setSession(options.session); + } + + ssl.onerror = onerror; + + // If custom SNICallback was given, or if + // there're SNI contexts to perform match against - + // set `.onsniselect` callback. + if (process.features.tls_sni && + options.isServer && + options.SNICallback && + options.server && + (options.SNICallback !== SNICallback || + options.server._contexts.length)) { + assert(typeof options.SNICallback === 'function'); + this._SNICallback = options.SNICallback; + ssl.enableCertCb(); + } + + if (process.features.tls_npn && options.NPNProtocols) + ssl.setNPNProtocols(options.NPNProtocols); + + if (process.features.tls_alpn && options.ALPNProtocols) { + // keep reference in secureContext not to be GC-ed + ssl._secureContext.alpnBuffer = options.ALPNProtocols; + ssl.setALPNProtocols(ssl._secureContext.alpnBuffer); + } + + if (options.handshakeTimeout > 0) + this.setTimeout(options.handshakeTimeout, this._handleTimeout); + + if (socket instanceof net.Socket) { + this._parent = socket; + + // To prevent assertion in afterConnect() and properly kick off readStart + this.connecting = socket.connecting || !socket._handle; + socket.once('connect', () => { + this.connecting = false; + this.emit('connect'); + }); + } + + // Assume `tls.connect()` + if (wrap) { + wrap.on('error', (err) => this._emitTLSError(err)); + } else { + assert(!socket); + this.connecting = true; + } +}; + +TLSSocket.prototype.renegotiate = function(options, callback) { + if (this.destroyed) + return; + + let requestCert = this._requestCert; + let rejectUnauthorized = this._rejectUnauthorized; + + if (options.requestCert !== undefined) + requestCert = !!options.requestCert; + if (options.rejectUnauthorized !== undefined) + rejectUnauthorized = !!options.rejectUnauthorized; + + if (requestCert !== this._requestCert || + rejectUnauthorized !== this._rejectUnauthorized) { + this._handle.setVerifyMode(requestCert, rejectUnauthorized); + this._requestCert = requestCert; + this._rejectUnauthorized = rejectUnauthorized; + } + if (!this._handle.renegotiate()) { + if (callback) { + process.nextTick(callback, new errors.Error('ERR_TLS_RENEGOTIATE')); + } + return false; + } + + // Ensure that we'll cycle through internal openssl's state + this.write(''); + + if (callback) { + this.once('secure', () => callback(null)); + } + + return true; +}; + +TLSSocket.prototype.setMaxSendFragment = function setMaxSendFragment(size) { + return this._handle.setMaxSendFragment(size) === 1; +}; + +TLSSocket.prototype.getTLSTicket = function getTLSTicket() { + return this._handle.getTLSTicket(); +}; + +TLSSocket.prototype._handleTimeout = function() { + this._emitTLSError(new errors.Error('ERR_TLS_HANDSHAKE_TIMEOUT')); +}; + +TLSSocket.prototype._emitTLSError = function(err) { + var e = this._tlsError(err); + if (e) + this.emit('error', e); +}; + +TLSSocket.prototype._tlsError = function(err) { + this.emit('_tlsError', err); + if (this._controlReleased) + return err; + return null; +}; + +TLSSocket.prototype._releaseControl = function() { + if (this._controlReleased) + return false; + this._controlReleased = true; + this.removeListener('error', this._tlsError); + return true; +}; + +TLSSocket.prototype._finishInit = function() { + if (process.features.tls_npn) { + this.npnProtocol = this._handle.getNegotiatedProtocol(); + } + + if (process.features.tls_alpn) { + this.alpnProtocol = this._handle.getALPNNegotiatedProtocol(); + } + + if (process.features.tls_sni && this._tlsOptions.isServer) { + this.servername = this._handle.getServername(); + } + + debug('secure established'); + this._secureEstablished = true; + if (this._tlsOptions.handshakeTimeout > 0) + this.setTimeout(0, this._handleTimeout); + this.emit('secure'); +}; + +TLSSocket.prototype._start = function() { + if (this.connecting) { + this.once('connect', this._start); + return; + } + + // Socket was destroyed before the connection was established + if (!this._handle) + return; + + debug('start'); + if (this._tlsOptions.requestOCSP) + this._handle.requestOCSP(); + this._handle.start(); +}; + +TLSSocket.prototype.setServername = function(name) { + this._handle.setServername(name); +}; + +TLSSocket.prototype.setSession = function(session) { + if (typeof session === 'string') + session = Buffer.from(session, 'latin1'); + this._handle.setSession(session); +}; + +TLSSocket.prototype.getPeerCertificate = function(detailed) { + if (this._handle) { + return common.translatePeerCertificate( + this._handle.getPeerCertificate(detailed)); + } + + return null; +}; + +TLSSocket.prototype.getSession = function() { + if (this._handle) { + return this._handle.getSession(); + } + + return null; +}; + +TLSSocket.prototype.isSessionReused = function() { + if (this._handle) { + return this._handle.isSessionReused(); + } + + return null; +}; + +TLSSocket.prototype.getCipher = function(err) { + if (this._handle) { + return this._handle.getCurrentCipher(); + } else { + return null; + } +}; + +TLSSocket.prototype.getEphemeralKeyInfo = function() { + if (this._handle) + return this._handle.getEphemeralKeyInfo(); + + return null; +}; + +TLSSocket.prototype.getProtocol = function() { + if (this._handle) + return this._handle.getProtocol(); + + return null; +}; + +// TODO: support anonymous (nocert) and PSK + + +function onSocketSecure() { + if (this._requestCert) { + const verifyError = this._handle.verifyError(); + if (verifyError) { + this.authorizationError = verifyError.code; + + if (this._rejectUnauthorized) + this.destroy(); + } else { + this.authorized = true; + } + } + + if (!this.destroyed && this._releaseControl()) + this._tlsOptions.server.emit('secureConnection', this); +} + +function onSocketTLSError(err) { + if (!this._controlReleased && !this[kErrorEmitted]) { + this[kErrorEmitted] = true; + this._tlsOptions.server.emit('tlsClientError', err, this); + } +} + +function onSocketClose(err) { + // Closed because of error - no need to emit it twice + if (err) + return; + + // Emit ECONNRESET + if (!this._controlReleased && !this[kErrorEmitted]) { + this[kErrorEmitted] = true; + const connReset = new Error('socket hang up'); + connReset.code = 'ECONNRESET'; + this._tlsOptions.server.emit('tlsClientError', connReset, this); + } +} + +function tlsConnectionListener(rawSocket) { + const socket = new TLSSocket(rawSocket, { + secureContext: this._sharedCreds, + isServer: true, + server: this, + requestCert: this.requestCert, + rejectUnauthorized: this.rejectUnauthorized, + handshakeTimeout: this[kHandshakeTimeout], + NPNProtocols: this.NPNProtocols, + ALPNProtocols: this.ALPNProtocols, + SNICallback: this[kSNICallback] || SNICallback + }); + + socket.on('secure', onSocketSecure); + + socket[kErrorEmitted] = false; + socket.on('close', onSocketClose); + socket.on('_tlsError', onSocketTLSError); +} + +// AUTHENTICATION MODES +// +// There are several levels of authentication that TLS/SSL supports. +// Read more about this in "man SSL_set_verify". +// +// 1. The server sends a certificate to the client but does not request a +// cert from the client. This is common for most HTTPS servers. The browser +// can verify the identity of the server, but the server does not know who +// the client is. Authenticating the client is usually done over HTTP using +// login boxes and cookies and stuff. +// +// 2. The server sends a cert to the client and requests that the client +// also send it a cert. The client knows who the server is and the server is +// requesting the client also identify themselves. There are several +// outcomes: +// +// A) verifyError returns null meaning the client's certificate is signed +// by one of the server's CAs. The server now knows the client's identity +// and the client is authorized. +// +// B) For some reason the client's certificate is not acceptable - +// verifyError returns a string indicating the problem. The server can +// either (i) reject the client or (ii) allow the client to connect as an +// unauthorized connection. +// +// The mode is controlled by two boolean variables. +// +// requestCert +// If true the server requests a certificate from client connections. For +// the common HTTPS case, users will want this to be false, which is what +// it defaults to. +// +// rejectUnauthorized +// If true clients whose certificates are invalid for any reason will not +// be allowed to make connections. If false, they will simply be marked as +// unauthorized but secure communication will continue. By default this is +// true. +// +// +// +// Options: +// - requestCert. Send verify request. Default to false. +// - rejectUnauthorized. Boolean, default to true. +// - key. string. +// - cert: string. +// - ca: string or array of strings. +// - sessionTimeout: integer. +// +// emit 'secureConnection' +// function (tlsSocket) { } +// +// "UNABLE_TO_GET_ISSUER_CERT", "UNABLE_TO_GET_CRL", +// "UNABLE_TO_DECRYPT_CERT_SIGNATURE", "UNABLE_TO_DECRYPT_CRL_SIGNATURE", +// "UNABLE_TO_DECODE_ISSUER_PUBLIC_KEY", "CERT_SIGNATURE_FAILURE", +// "CRL_SIGNATURE_FAILURE", "CERT_NOT_YET_VALID" "CERT_HAS_EXPIRED", +// "CRL_NOT_YET_VALID", "CRL_HAS_EXPIRED" "ERROR_IN_CERT_NOT_BEFORE_FIELD", +// "ERROR_IN_CERT_NOT_AFTER_FIELD", "ERROR_IN_CRL_LAST_UPDATE_FIELD", +// "ERROR_IN_CRL_NEXT_UPDATE_FIELD", "OUT_OF_MEM", +// "DEPTH_ZERO_SELF_SIGNED_CERT", "SELF_SIGNED_CERT_IN_CHAIN", +// "UNABLE_TO_GET_ISSUER_CERT_LOCALLY", "UNABLE_TO_VERIFY_LEAF_SIGNATURE", +// "CERT_CHAIN_TOO_LONG", "CERT_REVOKED" "INVALID_CA", +// "PATH_LENGTH_EXCEEDED", "INVALID_PURPOSE" "CERT_UNTRUSTED", +// "CERT_REJECTED" +// +function Server(options, listener) { + if (!(this instanceof Server)) + return new Server(options, listener); + + if (typeof options === 'function') { + listener = options; + options = {}; + } else if (options == null || typeof options === 'object') { + options = options || {}; + } else { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'options', 'object'); + } + + + this._contexts = []; + + // Handle option defaults: + this.setOptions(options); + + var sharedCreds = tls.createSecureContext({ + pfx: this.pfx, + key: this.key, + passphrase: this.passphrase, + cert: this.cert, + ca: this.ca, + ciphers: this.ciphers, + ecdhCurve: this.ecdhCurve, + dhparam: this.dhparam, + secureProtocol: this.secureProtocol, + secureOptions: this.secureOptions, + honorCipherOrder: this.honorCipherOrder, + crl: this.crl, + sessionIdContext: this.sessionIdContext + }); + this._sharedCreds = sharedCreds; + + this[kHandshakeTimeout] = options.handshakeTimeout || (120 * 1000); + this[kSNICallback] = options.SNICallback; + + if (typeof this[kHandshakeTimeout] !== 'number') { + throw new errors.TypeError('ERR_INVALID_ARG_TYPE', 'timeout', 'number'); + } + + if (this.sessionTimeout) { + sharedCreds.context.setSessionTimeout(this.sessionTimeout); + } + + if (this.ticketKeys) { + sharedCreds.context.setTicketKeys(this.ticketKeys); + } + + // constructor call + net.Server.call(this, tlsConnectionListener); + + if (listener) { + this.on('secureConnection', listener); + } +} + +util.inherits(Server, net.Server); +exports.Server = Server; +exports.createServer = function(options, listener) { + return new Server(options, listener); +}; + + +Server.prototype._getServerData = function() { + return { + ticketKeys: this.getTicketKeys().toString('hex') + }; +}; + + +Server.prototype._setServerData = function(data) { + this.setTicketKeys(Buffer.from(data.ticketKeys, 'hex')); +}; + + +Server.prototype.getTicketKeys = function getTicketKeys(keys) { + return this._sharedCreds.context.getTicketKeys(keys); +}; + + +Server.prototype.setTicketKeys = function setTicketKeys(keys) { + this._sharedCreds.context.setTicketKeys(keys); +}; + + +Server.prototype.setOptions = function(options) { + this.requestCert = options.requestCert === true; + this.rejectUnauthorized = options.rejectUnauthorized !== false; + + if (options.pfx) this.pfx = options.pfx; + if (options.key) this.key = options.key; + if (options.passphrase) this.passphrase = options.passphrase; + if (options.cert) this.cert = options.cert; + if (options.ca) this.ca = options.ca; + if (options.secureProtocol) this.secureProtocol = options.secureProtocol; + if (options.crl) this.crl = options.crl; + if (options.ciphers) this.ciphers = options.ciphers; + if (options.ecdhCurve !== undefined) + this.ecdhCurve = options.ecdhCurve; + if (options.dhparam) this.dhparam = options.dhparam; + if (options.sessionTimeout) this.sessionTimeout = options.sessionTimeout; + if (options.ticketKeys) this.ticketKeys = options.ticketKeys; + var secureOptions = options.secureOptions || 0; + if (options.honorCipherOrder !== undefined) + this.honorCipherOrder = !!options.honorCipherOrder; + else + this.honorCipherOrder = true; + if (secureOptions) this.secureOptions = secureOptions; + if (options.NPNProtocols) tls.convertNPNProtocols(options.NPNProtocols, this); + if (options.ALPNProtocols) + tls.convertALPNProtocols(options.ALPNProtocols, this); + if (options.sessionIdContext) { + this.sessionIdContext = options.sessionIdContext; + } else { + this.sessionIdContext = crypto.createHash('sha1') + .update(process.argv.join(' ')) + .digest('hex') + .slice(0, 32); + } +}; + +// SNI Contexts High-Level API +Server.prototype.addContext = function(servername, context) { + if (!servername) { + throw new errors.Error('ERR_TLS_REQUIRED_SERVER_NAME'); + } + + var re = new RegExp('^' + + servername.replace(/([.^$+?\-\\[\]{}])/g, '\\$1') + .replace(/\*/g, '[^.]*') + + '$'); + this._contexts.push([re, tls.createSecureContext(context).context]); +}; + +function SNICallback(servername, callback) { + const contexts = this.server._contexts; + + for (var i = 0; i < contexts.length; i++) { + const elem = contexts[i]; + if (elem[0].test(servername)) { + callback(null, elem[1]); + return; + } + } + + callback(null, undefined); +} + + +// Target API: +// +// var s = tls.connect({port: 8000, host: "google.com"}, function() { +// if (!s.authorized) { +// s.destroy(); +// return; +// } +// +// // s.socket; +// +// s.end("hello world\n"); +// }); +// +// +function normalizeConnectArgs(listArgs) { + var args = net._normalizeArgs(listArgs); + var options = args[0]; + var cb = args[1]; + + // If args[0] was options, then normalize dealt with it. + // If args[0] is port, or args[0], args[1] is host, port, we need to + // find the options and merge them in, normalize's options has only + // the host/port/path args that it knows about, not the tls options. + // This means that options.host overrides a host arg. + if (listArgs[1] !== null && typeof listArgs[1] === 'object') { + util._extend(options, listArgs[1]); + } else if (listArgs[2] !== null && typeof listArgs[2] === 'object') { + util._extend(options, listArgs[2]); + } + + return (cb) ? [options, cb] : [options]; +} + +function onConnectSecure() { + const options = this[kConnectOptions]; + + // Check the size of DHE parameter above minimum requirement + // specified in options. + const ekeyinfo = this.getEphemeralKeyInfo(); + if (ekeyinfo.type === 'DH' && ekeyinfo.size < options.minDHSize) { + const err = new errors.Error('ERR_TLS_DH_PARAM_SIZE', ekeyinfo.size); + this.emit('error', err); + this.destroy(); + return; + } + + let verifyError = this._handle.verifyError(); + + // Verify that server's identity matches it's certificate's names + // Unless server has resumed our existing session + if (!verifyError && !this.isSessionReused()) { + const hostname = options.servername || + options.host || + (options.socket && options.socket._host) || + 'localhost'; + const cert = this.getPeerCertificate(); + verifyError = options.checkServerIdentity(hostname, cert); + } + + if (verifyError) { + this.authorized = false; + this.authorizationError = verifyError.code || verifyError.message; + + if (options.rejectUnauthorized) { + this.destroy(verifyError); + return; + } else { + this.emit('secureConnect'); + } + } else { + this.authorized = true; + this.emit('secureConnect'); + } + + // Uncork incoming data + this.removeListener('end', onConnectEnd); +} + +function onConnectEnd() { + // NOTE: This logic is shared with _http_client.js + if (!this._hadError) { + const options = this[kConnectOptions]; + this._hadError = true; + const error = new Error('socket hang up'); + error.code = 'ECONNRESET'; + error.path = options.path; + error.host = options.host; + error.port = options.port; + error.localAddress = options.localAddress; + this.destroy(error); + } +} + +exports.connect = function(...args /* [port,] [host,] [options,] [cb] */) { + args = normalizeConnectArgs(args); + var options = args[0]; + var cb = args[1]; + + var defaults = { + rejectUnauthorized: '0' !== process.env.NODE_TLS_REJECT_UNAUTHORIZED, + ciphers: tls.DEFAULT_CIPHERS, + checkServerIdentity: tls.checkServerIdentity, + minDHSize: 1024 + }; + + options = util._extend(defaults, options || {}); + if (!options.keepAlive) + options.singleUse = true; + + assert(typeof options.checkServerIdentity === 'function'); + assert(typeof options.minDHSize === 'number', + 'options.minDHSize is not a number: ' + options.minDHSize); + assert(options.minDHSize > 0, + 'options.minDHSize is not a positive number: ' + + options.minDHSize); + + const NPN = {}; + const ALPN = {}; + const context = options.secureContext || tls.createSecureContext(options); + tls.convertNPNProtocols(options.NPNProtocols, NPN); + tls.convertALPNProtocols(options.ALPNProtocols, ALPN); + + var socket = new TLSSocket(options.socket, { + pipe: !!options.path, + secureContext: context, + isServer: false, + requestCert: true, + rejectUnauthorized: options.rejectUnauthorized !== false, + session: options.session, + NPNProtocols: NPN.NPNProtocols, + ALPNProtocols: ALPN.ALPNProtocols, + requestOCSP: options.requestOCSP + }); + + socket[kConnectOptions] = options; + + if (cb) + socket.once('secureConnect', cb); + + if (!options.socket) { + const connectOpt = { + path: options.path, + port: options.port, + host: options.host, + family: options.family, + localAddress: options.localAddress, + lookup: options.lookup + }; + socket.connect(connectOpt, socket._start); + } + + socket._releaseControl(); + + if (options.session) + socket.setSession(options.session); + + if (options.servername) + socket.setServername(options.servername); + + if (options.socket) + socket._start(); + + socket.on('secure', onConnectSecure); + socket.once('end', onConnectEnd); + + return socket; +}; diff --git a/lib/stream.js b/lib/stream.js index edc5f231b83411..0bc7e410c7744c 100644 --- a/lib/stream.js +++ b/lib/stream.js @@ -27,11 +27,11 @@ const { Buffer } = require('buffer'); // to avoid a cross-reference(require) issues const Stream = module.exports = require('internal/streams/legacy'); -Stream.Readable = require('_stream_readable'); -Stream.Writable = require('_stream_writable'); -Stream.Duplex = require('_stream_duplex'); -Stream.Transform = require('_stream_transform'); -Stream.PassThrough = require('_stream_passthrough'); +Stream.Readable = require('internal/streams/readable'); +Stream.Writable = require('internal/streams/writable'); +Stream.Duplex = require('internal/streams/duplex'); +Stream.Transform = require('internal/streams/transform'); +Stream.PassThrough = require('internal/streams/passthrough'); // Backwards-compat with node 0.4.x Stream.Stream = Stream; diff --git a/lib/tls.js b/lib/tls.js index a82535df618f99..1f182e43b712a0 100644 --- a/lib/tls.js +++ b/lib/tls.js @@ -237,12 +237,13 @@ exports.parseCertString = internalUtil.deprecate( 'DEP0076'); // Public API -exports.createSecureContext = require('_tls_common').createSecureContext; -exports.SecureContext = require('_tls_common').SecureContext; -exports.TLSSocket = require('_tls_wrap').TLSSocket; -exports.Server = require('_tls_wrap').Server; -exports.createServer = require('_tls_wrap').createServer; -exports.connect = require('_tls_wrap').connect; +exports.createSecureContext = require('internal/tls/common') + .createSecureContext; +exports.SecureContext = require('internal/tls/common').SecureContext; +exports.TLSSocket = require('internal/tls/wrap').TLSSocket; +exports.Server = require('internal/tls/wrap').Server; +exports.createServer = require('internal/tls/wrap').createServer; +exports.connect = require('internal/tls/wrap').connect; // Deprecated: DEP0064 -exports.createSecurePair = require('_tls_legacy').createSecurePair; +exports.createSecurePair = require('internal/tls/legacy').createSecurePair; diff --git a/lib/zlib.js b/lib/zlib.js index 2def716e9b676c..cb54ff03def5a6 100644 --- a/lib/zlib.js +++ b/lib/zlib.js @@ -22,7 +22,7 @@ 'use strict'; const errors = require('internal/errors'); -const Transform = require('_stream_transform'); +const Transform = require('internal/streams/transform'); const { _extend } = require('util'); const { isArrayBufferView } = require('internal/util/types'); const binding = process.binding('zlib'); diff --git a/node.gyp b/node.gyp index 1c3e29147140e7..9f66ef71471c2a 100644 --- a/node.gyp +++ b/node.gyp @@ -38,12 +38,12 @@ 'lib/fs.js', 'lib/http.js', 'lib/http2.js', - 'lib/_http_agent.js', - 'lib/_http_client.js', - 'lib/_http_common.js', - 'lib/_http_incoming.js', - 'lib/_http_outgoing.js', - 'lib/_http_server.js', + 'lib/internal/http/agent.js', + 'lib/internal/http/client.js', + 'lib/internal/http/common.js', + 'lib/internal/http/incoming.js', + 'lib/internal/http/outgoing.js', + 'lib/internal/http/server.js', 'lib/https.js', 'lib/inspector.js', 'lib/module.js', @@ -57,19 +57,19 @@ 'lib/readline.js', 'lib/repl.js', 'lib/stream.js', - 'lib/_stream_readable.js', - 'lib/_stream_writable.js', - 'lib/_stream_duplex.js', - 'lib/_stream_transform.js', - 'lib/_stream_passthrough.js', - 'lib/_stream_wrap.js', + 'lib/internal/streams/readable.js', + 'lib/internal/streams/writable.js', + 'lib/internal/streams/duplex.js', + 'lib/internal/streams/transform.js', + 'lib/internal/streams/passthrough.js', + 'lib/internal/streams/wrap.js', 'lib/string_decoder.js', 'lib/sys.js', 'lib/timers.js', 'lib/tls.js', - 'lib/_tls_common.js', - 'lib/_tls_legacy.js', - 'lib/_tls_wrap.js', + 'lib/internal/tls/common.js', + 'lib/internal/tls/legacy.js', + 'lib/internal/tls/wrap.js', 'lib/tty.js', 'lib/url.js', 'lib/util.js', @@ -148,6 +148,21 @@ 'deps/node-inspect/lib/_inspect.js', 'deps/node-inspect/lib/internal/inspect_client.js', 'deps/node-inspect/lib/internal/inspect_repl.js', + 'lib/_http_agent.js', + 'lib/_http_client.js', + 'lib/_http_common.js', + 'lib/_http_incoming.js', + 'lib/_http_outgoing.js', + 'lib/_http_server.js', + 'lib/_stream_duplex.js', + 'lib/_stream_passthrough.js', + 'lib/_stream_readable.js', + 'lib/_stream_transform.js', + 'lib/_stream_wrap.js', + 'lib/_stream_writable.js', + 'lib/_tls_common.js', + 'lib/_tls_legacy.js', + 'lib/_tls_wrap.js', ], 'conditions': [ [ 'node_shared=="true"', { diff --git a/test/parallel/test-http-agent-keepalive.js b/test/parallel/test-http-agent-keepalive.js index 2c8b6c1cc98d2d..e5bdd8485c8f77 100644 --- a/test/parallel/test-http-agent-keepalive.js +++ b/test/parallel/test-http-agent-keepalive.js @@ -23,7 +23,7 @@ const common = require('../common'); const assert = require('assert'); const http = require('http'); -const Agent = require('_http_agent').Agent; +const Agent = require('internal/http/agent').Agent; let name; diff --git a/test/parallel/test-http-common.js b/test/parallel/test-http-common.js index 1629856ce57d09..3dc296ccaa18d2 100644 --- a/test/parallel/test-http-common.js +++ b/test/parallel/test-http-common.js @@ -1,7 +1,7 @@ 'use strict'; require('../common'); const assert = require('assert'); -const httpCommon = require('_http_common'); +const httpCommon = require('internal/http/common'); const checkIsHttpToken = httpCommon._checkIsHttpToken; const checkInvalidHeaderChar = httpCommon._checkInvalidHeaderChar; diff --git a/test/parallel/test-http-invalidheaderfield2.js b/test/parallel/test-http-invalidheaderfield2.js index 40415d9c368891..a9a8242d288d76 100644 --- a/test/parallel/test-http-invalidheaderfield2.js +++ b/test/parallel/test-http-invalidheaderfield2.js @@ -2,7 +2,10 @@ require('../common'); const assert = require('assert'); const inspect = require('util').inspect; -const { _checkIsHttpToken, _checkInvalidHeaderChar } = require('_http_common'); +const { + _checkIsHttpToken, + _checkInvalidHeaderChar, +} = require('internal/http/common'); // Good header field names [ diff --git a/test/parallel/test-outgoing-message-pipe.js b/test/parallel/test-outgoing-message-pipe.js index 2030d8f43b09be..f73285df267246 100644 --- a/test/parallel/test-outgoing-message-pipe.js +++ b/test/parallel/test-outgoing-message-pipe.js @@ -1,7 +1,7 @@ 'use strict'; const assert = require('assert'); const common = require('../common'); -const OutgoingMessage = require('_http_outgoing').OutgoingMessage; +const OutgoingMessage = require('internal/http/outgoing').OutgoingMessage; // Verify that an error is thrown upon a call to `OutgoingMessage.pipe`. diff --git a/test/parallel/test-stream-pipe-after-end.js b/test/parallel/test-stream-pipe-after-end.js index 02792b44554348..5ed23d862fbdec 100644 --- a/test/parallel/test-stream-pipe-after-end.js +++ b/test/parallel/test-stream-pipe-after-end.js @@ -22,7 +22,7 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const Readable = require('_stream_readable'); +const Readable = require('internal/streams/readable'); const Writable = require('_stream_writable'); const util = require('util'); diff --git a/test/parallel/test-stream-wrap-encoding.js b/test/parallel/test-stream-wrap-encoding.js index ce6f95fa27d68f..f2e77b725114a7 100644 --- a/test/parallel/test-stream-wrap-encoding.js +++ b/test/parallel/test-stream-wrap-encoding.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common'); -const StreamWrap = require('_stream_wrap'); +const StreamWrap = require('internal/streams/wrap'); const Duplex = require('stream').Duplex; { diff --git a/test/parallel/test-stream-wrap.js b/test/parallel/test-stream-wrap.js index 5312596afac40d..7f9b2a762d0bef 100644 --- a/test/parallel/test-stream-wrap.js +++ b/test/parallel/test-stream-wrap.js @@ -2,7 +2,7 @@ const common = require('../common'); const assert = require('assert'); -const StreamWrap = require('_stream_wrap'); +const StreamWrap = require('internal/streams/wrap'); const Duplex = require('stream').Duplex; const ShutdownWrap = process.binding('stream_wrap').ShutdownWrap; diff --git a/test/parallel/test-stream2-base64-single-char-read-end.js b/test/parallel/test-stream2-base64-single-char-read-end.js index 10259e280454c0..3f59b4a10f74de 100644 --- a/test/parallel/test-stream2-base64-single-char-read-end.js +++ b/test/parallel/test-stream2-base64-single-char-read-end.js @@ -21,7 +21,7 @@ 'use strict'; require('../common'); -const R = require('_stream_readable'); +const R = require('internal/streams/readable'); const W = require('_stream_writable'); const assert = require('assert'); diff --git a/test/parallel/test-stream2-basic.js b/test/parallel/test-stream2-basic.js index f544321f9b7778..16ba2db1f6e04f 100644 --- a/test/parallel/test-stream2-basic.js +++ b/test/parallel/test-stream2-basic.js @@ -22,7 +22,7 @@ 'use strict'; const common = require('../common'); -const R = require('_stream_readable'); +const R = require('internal/streams/readable'); const assert = require('assert'); const util = require('util'); diff --git a/test/parallel/test-stream2-compatibility.js b/test/parallel/test-stream2-compatibility.js index 45834ee99e5961..ab73267011fe1b 100644 --- a/test/parallel/test-stream2-compatibility.js +++ b/test/parallel/test-stream2-compatibility.js @@ -21,7 +21,7 @@ 'use strict'; require('../common'); -const R = require('_stream_readable'); +const R = require('internal/streams/readable'); const W = require('_stream_writable'); const assert = require('assert'); diff --git a/test/parallel/test-stream2-decode-partial.js b/test/parallel/test-stream2-decode-partial.js index 9b1baf7fd677f4..cba02111119ba5 100644 --- a/test/parallel/test-stream2-decode-partial.js +++ b/test/parallel/test-stream2-decode-partial.js @@ -1,6 +1,6 @@ 'use strict'; require('../common'); -const Readable = require('_stream_readable'); +const Readable = require('internal/streams/readable'); const assert = require('assert'); let buf = ''; diff --git a/test/parallel/test-stream2-objects.js b/test/parallel/test-stream2-objects.js index f58ea4a32a1e7d..12f08ab4daf11a 100644 --- a/test/parallel/test-stream2-objects.js +++ b/test/parallel/test-stream2-objects.js @@ -22,7 +22,7 @@ 'use strict'; const common = require('../common'); -const Readable = require('_stream_readable'); +const Readable = require('internal/streams/readable'); const Writable = require('_stream_writable'); const assert = require('assert'); diff --git a/test/parallel/test-stream2-readable-from-list.js b/test/parallel/test-stream2-readable-from-list.js index 965f962638d586..3dc1faf25833e0 100644 --- a/test/parallel/test-stream2-readable-from-list.js +++ b/test/parallel/test-stream2-readable-from-list.js @@ -23,7 +23,7 @@ 'use strict'; require('../common'); const assert = require('assert'); -const fromList = require('_stream_readable')._fromList; +const fromList = require('internal/streams/readable')._fromList; const BufferList = require('internal/streams/BufferList'); function bufferListFromArray(arr) { diff --git a/test/parallel/test-stream2-readable-non-empty-end.js b/test/parallel/test-stream2-readable-non-empty-end.js index 4299f31ea12986..3515f0caf2afb4 100644 --- a/test/parallel/test-stream2-readable-non-empty-end.js +++ b/test/parallel/test-stream2-readable-non-empty-end.js @@ -22,7 +22,7 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const Readable = require('_stream_readable'); +const Readable = require('internal/streams/readable'); let len = 0; const chunks = new Array(10); diff --git a/test/parallel/test-stream2-readable-wrap-empty.js b/test/parallel/test-stream2-readable-wrap-empty.js index 1489717f3e49c3..65b771b5b35afc 100644 --- a/test/parallel/test-stream2-readable-wrap-empty.js +++ b/test/parallel/test-stream2-readable-wrap-empty.js @@ -22,7 +22,7 @@ 'use strict'; const common = require('../common'); -const Readable = require('_stream_readable'); +const Readable = require('internal/streams/readable'); const EE = require('events').EventEmitter; const oldStream = new EE(); diff --git a/test/parallel/test-stream2-readable-wrap.js b/test/parallel/test-stream2-readable-wrap.js index fe9e8ce30dcf93..3fc43b2f38cde9 100644 --- a/test/parallel/test-stream2-readable-wrap.js +++ b/test/parallel/test-stream2-readable-wrap.js @@ -22,7 +22,7 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const Readable = require('_stream_readable'); +const Readable = require('internal/streams/readable'); const Writable = require('_stream_writable'); const EE = require('events').EventEmitter; diff --git a/test/parallel/test-stream2-set-encoding.js b/test/parallel/test-stream2-set-encoding.js index 5b2e35fd01f642..691e82b828bffe 100644 --- a/test/parallel/test-stream2-set-encoding.js +++ b/test/parallel/test-stream2-set-encoding.js @@ -22,7 +22,7 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const R = require('_stream_readable'); +const R = require('internal/streams/readable'); const util = require('util'); util.inherits(TestReader, R); diff --git a/test/parallel/test-stream2-transform.js b/test/parallel/test-stream2-transform.js index 0f12476f506a93..0be6a1090c74a9 100644 --- a/test/parallel/test-stream2-transform.js +++ b/test/parallel/test-stream2-transform.js @@ -22,8 +22,8 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const PassThrough = require('_stream_passthrough'); -const Transform = require('_stream_transform'); +const PassThrough = require('internal/streams/passthrough'); +const Transform = require('internal/streams/transform'); { // Verify writable side consumption diff --git a/test/parallel/test-stream2-writable.js b/test/parallel/test-stream2-writable.js index 2af3f683a23478..af0bbf84b4bc28 100644 --- a/test/parallel/test-stream2-writable.js +++ b/test/parallel/test-stream2-writable.js @@ -23,7 +23,7 @@ const common = require('../common'); const W = require('_stream_writable'); -const D = require('_stream_duplex'); +const D = require('internal/streams/duplex'); const assert = require('assert'); const util = require('util'); diff --git a/test/parallel/test-tls-legacy-onselect.js b/test/parallel/test-tls-legacy-onselect.js index efcc5c2c92b889..74b090ba3b3d58 100644 --- a/test/parallel/test-tls-legacy-onselect.js +++ b/test/parallel/test-tls-legacy-onselect.js @@ -14,7 +14,7 @@ const server = net.Server(common.mustCall(function(raw) { raw.destroy(); server.close(); })); - require('_tls_legacy').pipe(pair, raw); + require('internal/tls/legacy').pipe(pair, raw); })).listen(0, function() { tls.connect({ port: this.address().port, diff --git a/test/parallel/test-tls-securepair-leak.js b/test/parallel/test-tls-securepair-leak.js index cbc7c7daddd74b..b61df40d3a81eb 100644 --- a/test/parallel/test-tls-securepair-leak.js +++ b/test/parallel/test-tls-securepair-leak.js @@ -7,7 +7,7 @@ if (!common.hasCrypto) const assert = require('assert'); const { createSecureContext } = require('tls'); -const { createSecurePair } = require('_tls_legacy'); +const { createSecurePair } = require('internal/tls/legacy'); const before = process.memoryUsage().external; { diff --git a/test/parallel/test-tls-translate-peer-certificate.js b/test/parallel/test-tls-translate-peer-certificate.js index f8499e0c7e84ff..c781097725166b 100644 --- a/test/parallel/test-tls-translate-peer-certificate.js +++ b/test/parallel/test-tls-translate-peer-certificate.js @@ -6,7 +6,7 @@ if (!common.hasCrypto) common.skip('missing crypto'); const { strictEqual, deepStrictEqual } = require('assert'); -const { translatePeerCertificate } = require('_tls_common'); +const { translatePeerCertificate } = require('internal/tls/common'); const certString = '__proto__=42\nA=1\nB=2\nC=3'; const certObject = Object.create(null); diff --git a/test/parallel/test-wrap-js-stream-duplex.js b/test/parallel/test-wrap-js-stream-duplex.js index 6bd860e6ba1f56..7a2b06b1e9393d 100644 --- a/test/parallel/test-wrap-js-stream-duplex.js +++ b/test/parallel/test-wrap-js-stream-duplex.js @@ -1,7 +1,7 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const StreamWrap = require('_stream_wrap'); +const StreamWrap = require('internal/streams/wrap'); const { PassThrough } = require('stream'); const { Socket } = require('net'); diff --git a/test/sequential/test-http-regr-gh-2928.js b/test/sequential/test-http-regr-gh-2928.js index 55e3a93bc98eaa..f5849ad1450bce 100644 --- a/test/sequential/test-http-regr-gh-2928.js +++ b/test/sequential/test-http-regr-gh-2928.js @@ -4,7 +4,7 @@ 'use strict'; const common = require('../common'); const assert = require('assert'); -const httpCommon = require('_http_common'); +const httpCommon = require('internal/http/common'); const HTTPParser = process.binding('http_parser').HTTPParser; const net = require('net');