'use strict'; const util = require('util'); const net = require('net'); const url = require('url'); const HTTPParser = process.binding('http_parser').HTTPParser; const assert = require('assert').ok; const common = require('_http_common'); const httpSocketSetup = common.httpSocketSetup; const parsers = common.parsers; const freeParser = common.freeParser; const debug = common.debug; const OutgoingMessage = require('_http_outgoing').OutgoingMessage; const Agent = require('_http_agent'); const Buffer = require('buffer').Buffer; function ClientRequest(options, cb) { var self = this; OutgoingMessage.call(self); if (typeof options === 'string') { options = url.parse(options); if (!options.hostname) { throw new Error('Unable to determine the domain name'); } } 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) && typeof options.createConnection !== 'function') { agent = defaultAgent; } self.agent = agent; var protocol = options.protocol || defaultAgent.protocol; var expectedProtocol = defaultAgent.protocol; if (self.agent && self.agent.protocol) expectedProtocol = self.agent.protocol; if (options.path && / /.test(options.path)) { // The actual regex 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. That's // why it only scans for spaces because those are guaranteed to create // an invalid request. throw new TypeError('Request path contains unescaped characters'); } else if (protocol !== expectedProtocol) { throw new Error('Protocol "' + protocol + '" not supported. ' + 'Expected "' + expectedProtocol + '"'); } const defaultPort = options.defaultPort || self.agent && self.agent.defaultPort; var port = options.port = options.port || defaultPort || 80; var host = options.host = options.hostname || options.host || 'localhost'; if (options.setHost === undefined) { var setHost = true; } self.socketPath = options.socketPath; var method = self.method = (options.method || 'GET').toUpperCase(); if (!common._checkIsHttpToken(method)) { throw new TypeError('Method must be a valid HTTP token'); } self.path = options.path || '/'; if (cb) { self.once('response', cb); } if (!Array.isArray(options.headers)) { if (options.headers) { var keys = Object.keys(options.headers); for (var i = 0, l = keys.length; i < l; i++) { var key = keys[i]; self.setHeader(key, options.headers[key]); } } if (host && !this.getHeader('host') && setHost) { var hostHeader = host; var posColon = -1; // 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 if (-1 !== (posColon = hostHeader.indexOf(':')) && -1 !== (posColon = hostHeader.indexOf(':', posColon)) && '[' !== hostHeader[0]) { hostHeader = `[${hostHeader}]`; } if (port && +port !== defaultPort) { hostHeader += ':' + port; } this.setHeader('Host', hostHeader); } } if (options.auth && !this.getHeader('Authorization')) { //basic auth this.setHeader('Authorization', 'Basic ' + Buffer.from(options.auth).toString('base64')); } if (method === 'GET' || method === 'HEAD' || method === 'DELETE' || method === 'OPTIONS' || method === 'CONNECT') { self.useChunkedEncodingByDefault = false; } else { self.useChunkedEncodingByDefault = true; } if (Array.isArray(options.headers)) { self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n', options.headers); } else if (self.getHeader('expect')) { self._storeHeader(self.method + ' ' + self.path + ' HTTP/1.1\r\n', self._renderHeaders()); } var called = false; if (self.socketPath) { self._last = true; self.shouldKeepAlive = false; const optionsPath = { path: self.socketPath }; const newSocket = self.agent.createConnection(optionsPath, oncreate); if (newSocket && !called) { called = true; self.onSocket(newSocket); } else { return; } } else if (self.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 (!self.agent.keepAlive && !Number.isFinite(self.agent.maxSockets)) { self._last = true; self.shouldKeepAlive = false; } else { self._last = false; self.shouldKeepAlive = true; } self.agent.addRequest(self, options); } else { // No agent, default to Connection:close. self._last = true; self.shouldKeepAlive = false; if (typeof options.createConnection === 'function') { const newSocket = options.createConnection(options, oncreate); if (newSocket && !called) { called = true; self.onSocket(newSocket); } else { return; } } else { debug('CLIENT use net.createConnection', options); self.onSocket(net.createConnection(options)); } } function oncreate(err, socket) { if (called) return; called = true; if (err) { process.nextTick(function() { self.emit('error', err); }); return; } self.onSocket(socket); self._deferToConnect(null, null, function() { self._flush(); self = null; }); } self._deferToConnect(null, null, function() { self._flush(); self = null; }); } util.inherits(ClientRequest, OutgoingMessage); exports.ClientRequest = ClientRequest; ClientRequest.prototype.aborted = undefined; ClientRequest.prototype._finish = function() { 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() { this._storeHeader(this.method + ' ' + this.path + ' HTTP/1.1\r\n', this._renderHeaders()); }; ClientRequest.prototype.abort = function() { if (this.aborted === undefined) { process.nextTick(emitAbortNT, 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(self) { self.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; req.emit('close'); 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.emit('error', createHangUpError()); req.socket._hadError = true; } // 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) { req.emit('error', err); // 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; } // 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.emit('error', createHangUpError()); req.socket._hadError = true; } 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'); freeParser(parser, req, socket); socket.destroy(); req.emit('error', ret); req.socket._hadError = true; } 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 true; // skip body } // 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. delete req.res; // 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); 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 responseOnEnd() { var res = this; var req = res.req; var socket = req.socket; if (!req.shouldKeepAlive) { if (socket.writable) { debug('AGENT socket.destroySoon()'); socket.destroySoon(); } 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); // Mark this socket as available, AFTER user-added end // handlers have a chance to run. process.nextTick(emitFreeNT, socket); } } 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); req.emit('socket', socket); } ClientRequest.prototype.onSocket = function(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. socket.emit('free'); } else { tickOnSocket(req, socket); } } ClientRequest.prototype._deferToConnect = function(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). var self = this; function callSocketMethod() { if (method) self.socket[method].apply(self.socket, arguments_); if (typeof cb === 'function') cb(); } var onSocket = function() { if (self.socket.writable) { callSocketMethod(); } else { self.socket.once('connect', callSocketMethod); } }; if (!self.socket) { self.once('socket', onSocket); } else { onSocket(); } }; ClientRequest.prototype.setTimeout = function(msecs, callback) { if (callback) this.once('timeout', callback); var self = this; function emitTimeout() { self.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.setTimeout(msecs, emitTimeout); }); return this; }; ClientRequest.prototype.setNoDelay = function() { const argsLen = arguments.length; const args = new Array(argsLen); for (var i = 0; i < argsLen; i++) args[i] = arguments[i]; this._deferToConnect('setNoDelay', args); }; ClientRequest.prototype.setSocketKeepAlive = function() { const argsLen = arguments.length; const args = new Array(argsLen); for (var i = 0; i < argsLen; i++) args[i] = arguments[i]; this._deferToConnect('setKeepAlive', args); }; ClientRequest.prototype.clearTimeout = function(cb) { this.setTimeout(0, cb); };