| 'use strict'; |
| |
| const util = require('util'); |
| const net = require('net'); |
| const url = require('url'); |
| const EventEmitter = require('events').EventEmitter; |
| 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); |
| } 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) && |
| !options.createConnection) { |
| 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(); |
| 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; |
| if (port && +port !== defaultPort) { |
| hostHeader += ':' + port; |
| } |
| this.setHeader('Host', hostHeader); |
| } |
| } |
| |
| if (options.auth && !this.getHeader('Authorization')) { |
| //basic auth |
| this.setHeader('Authorization', 'Basic ' + |
| new Buffer(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()); |
| } |
| |
| if (self.socketPath) { |
| self._last = true; |
| self.shouldKeepAlive = false; |
| var conn = self.agent.createConnection({ path: self.socketPath }); |
| self.onSocket(conn); |
| } 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 (options.createConnection) { |
| var conn = options.createConnection(options); |
| } else { |
| debug('CLIENT use net.createConnection', options); |
| var conn = net.createConnection(options); |
| } |
| self.onSocket(conn); |
| } |
| |
| 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: Its 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 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 (EventEmitter.listenerCount(req, 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; |
| |
| |
| // propogate "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); |
| // 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; |
| req.parser = parser; |
| |
| socket.parser = parser; |
| socket._httpMessage = req; |
| |
| // Setup "drain" propogation. |
| 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.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; |
| var onSocket = function() { |
| if (self.socket.writable) { |
| if (method) { |
| self.socket[method].apply(self.socket, arguments_); |
| } |
| if (cb) { cb(); } |
| } else { |
| self.socket.once('connect', function() { |
| if (method) { |
| self.socket[method].apply(self.socket, arguments_); |
| } |
| if (cb) { cb(); } |
| }); |
| } |
| }; |
| 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() { |
| this._deferToConnect('setNoDelay', arguments); |
| }; |
| ClientRequest.prototype.setSocketKeepAlive = function() { |
| this._deferToConnect('setKeepAlive', arguments); |
| }; |
| |
| ClientRequest.prototype.clearTimeout = function(cb) { |
| this.setTimeout(0, cb); |
| }; |