You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
244 lines
6.7 KiB
244 lines
6.7 KiB
'use strict'
|
|
|
|
var net = require('net')
|
|
, tls = require('tls')
|
|
, http = require('http')
|
|
, https = require('https')
|
|
, events = require('events')
|
|
, assert = require('assert')
|
|
, util = require('util')
|
|
, Buffer = require('safe-buffer').Buffer
|
|
;
|
|
|
|
exports.httpOverHttp = httpOverHttp
|
|
exports.httpsOverHttp = httpsOverHttp
|
|
exports.httpOverHttps = httpOverHttps
|
|
exports.httpsOverHttps = httpsOverHttps
|
|
|
|
|
|
function httpOverHttp(options) {
|
|
var agent = new TunnelingAgent(options)
|
|
agent.request = http.request
|
|
return agent
|
|
}
|
|
|
|
function httpsOverHttp(options) {
|
|
var agent = new TunnelingAgent(options)
|
|
agent.request = http.request
|
|
agent.createSocket = createSecureSocket
|
|
agent.defaultPort = 443
|
|
return agent
|
|
}
|
|
|
|
function httpOverHttps(options) {
|
|
var agent = new TunnelingAgent(options)
|
|
agent.request = https.request
|
|
return agent
|
|
}
|
|
|
|
function httpsOverHttps(options) {
|
|
var agent = new TunnelingAgent(options)
|
|
agent.request = https.request
|
|
agent.createSocket = createSecureSocket
|
|
agent.defaultPort = 443
|
|
return agent
|
|
}
|
|
|
|
|
|
function TunnelingAgent(options) {
|
|
var self = this
|
|
self.options = options || {}
|
|
self.proxyOptions = self.options.proxy || {}
|
|
self.maxSockets = self.options.maxSockets || http.Agent.defaultMaxSockets
|
|
self.requests = []
|
|
self.sockets = []
|
|
|
|
self.on('free', function onFree(socket, host, port) {
|
|
for (var i = 0, len = self.requests.length; i < len; ++i) {
|
|
var pending = self.requests[i]
|
|
if (pending.host === host && pending.port === port) {
|
|
// Detect the request to connect same origin server,
|
|
// reuse the connection.
|
|
self.requests.splice(i, 1)
|
|
pending.request.onSocket(socket)
|
|
return
|
|
}
|
|
}
|
|
socket.destroy()
|
|
self.removeSocket(socket)
|
|
})
|
|
}
|
|
util.inherits(TunnelingAgent, events.EventEmitter)
|
|
|
|
TunnelingAgent.prototype.addRequest = function addRequest(req, options) {
|
|
var self = this
|
|
|
|
// Legacy API: addRequest(req, host, port, path)
|
|
if (typeof options === 'string') {
|
|
options = {
|
|
host: options,
|
|
port: arguments[2],
|
|
path: arguments[3]
|
|
};
|
|
}
|
|
|
|
if (self.sockets.length >= this.maxSockets) {
|
|
// We are over limit so we'll add it to the queue.
|
|
self.requests.push({host: options.host, port: options.port, request: req})
|
|
return
|
|
}
|
|
|
|
// If we are under maxSockets create a new one.
|
|
self.createConnection({host: options.host, port: options.port, request: req})
|
|
}
|
|
|
|
TunnelingAgent.prototype.createConnection = function createConnection(pending) {
|
|
var self = this
|
|
|
|
self.createSocket(pending, function(socket) {
|
|
socket.on('free', onFree)
|
|
socket.on('close', onCloseOrRemove)
|
|
socket.on('agentRemove', onCloseOrRemove)
|
|
pending.request.onSocket(socket)
|
|
|
|
function onFree() {
|
|
self.emit('free', socket, pending.host, pending.port)
|
|
}
|
|
|
|
function onCloseOrRemove(err) {
|
|
self.removeSocket(socket)
|
|
socket.removeListener('free', onFree)
|
|
socket.removeListener('close', onCloseOrRemove)
|
|
socket.removeListener('agentRemove', onCloseOrRemove)
|
|
}
|
|
})
|
|
}
|
|
|
|
TunnelingAgent.prototype.createSocket = function createSocket(options, cb) {
|
|
var self = this
|
|
var placeholder = {}
|
|
self.sockets.push(placeholder)
|
|
|
|
var connectOptions = mergeOptions({}, self.proxyOptions,
|
|
{ method: 'CONNECT'
|
|
, path: options.host + ':' + options.port
|
|
, agent: false
|
|
}
|
|
)
|
|
if (connectOptions.proxyAuth) {
|
|
connectOptions.headers = connectOptions.headers || {}
|
|
connectOptions.headers['Proxy-Authorization'] = 'Basic ' +
|
|
Buffer.from(connectOptions.proxyAuth).toString('base64')
|
|
}
|
|
|
|
debug('making CONNECT request')
|
|
var connectReq = self.request(connectOptions)
|
|
connectReq.useChunkedEncodingByDefault = false // for v0.6
|
|
connectReq.once('response', onResponse) // for v0.6
|
|
connectReq.once('upgrade', onUpgrade) // for v0.6
|
|
connectReq.once('connect', onConnect) // for v0.7 or later
|
|
connectReq.once('error', onError)
|
|
connectReq.end()
|
|
|
|
function onResponse(res) {
|
|
// Very hacky. This is necessary to avoid http-parser leaks.
|
|
res.upgrade = true
|
|
}
|
|
|
|
function onUpgrade(res, socket, head) {
|
|
// Hacky.
|
|
process.nextTick(function() {
|
|
onConnect(res, socket, head)
|
|
})
|
|
}
|
|
|
|
function onConnect(res, socket, head) {
|
|
connectReq.removeAllListeners()
|
|
socket.removeAllListeners()
|
|
|
|
if (res.statusCode === 200) {
|
|
assert.equal(head.length, 0)
|
|
debug('tunneling connection has established')
|
|
self.sockets[self.sockets.indexOf(placeholder)] = socket
|
|
cb(socket)
|
|
} else {
|
|
debug('tunneling socket could not be established, statusCode=%d', res.statusCode)
|
|
var error = new Error('tunneling socket could not be established, ' + 'statusCode=' + res.statusCode)
|
|
error.code = 'ECONNRESET'
|
|
options.request.emit('error', error)
|
|
self.removeSocket(placeholder)
|
|
}
|
|
}
|
|
|
|
function onError(cause) {
|
|
connectReq.removeAllListeners()
|
|
|
|
debug('tunneling socket could not be established, cause=%s\n', cause.message, cause.stack)
|
|
var error = new Error('tunneling socket could not be established, ' + 'cause=' + cause.message)
|
|
error.code = 'ECONNRESET'
|
|
options.request.emit('error', error)
|
|
self.removeSocket(placeholder)
|
|
}
|
|
}
|
|
|
|
TunnelingAgent.prototype.removeSocket = function removeSocket(socket) {
|
|
var pos = this.sockets.indexOf(socket)
|
|
if (pos === -1) return
|
|
|
|
this.sockets.splice(pos, 1)
|
|
|
|
var pending = this.requests.shift()
|
|
if (pending) {
|
|
// If we have pending requests and a socket gets closed a new one
|
|
// needs to be created to take over in the pool for the one that closed.
|
|
this.createConnection(pending)
|
|
}
|
|
}
|
|
|
|
function createSecureSocket(options, cb) {
|
|
var self = this
|
|
TunnelingAgent.prototype.createSocket.call(self, options, function(socket) {
|
|
// 0 is dummy port for v0.6
|
|
var secureSocket = tls.connect(0, mergeOptions({}, self.options,
|
|
{ servername: options.host
|
|
, socket: socket
|
|
}
|
|
))
|
|
self.sockets[self.sockets.indexOf(socket)] = secureSocket
|
|
cb(secureSocket)
|
|
})
|
|
}
|
|
|
|
|
|
function mergeOptions(target) {
|
|
for (var i = 1, len = arguments.length; i < len; ++i) {
|
|
var overrides = arguments[i]
|
|
if (typeof overrides === 'object') {
|
|
var keys = Object.keys(overrides)
|
|
for (var j = 0, keyLen = keys.length; j < keyLen; ++j) {
|
|
var k = keys[j]
|
|
if (overrides[k] !== undefined) {
|
|
target[k] = overrides[k]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return target
|
|
}
|
|
|
|
|
|
var debug
|
|
if (process.env.NODE_DEBUG && /\btunnel\b/.test(process.env.NODE_DEBUG)) {
|
|
debug = function() {
|
|
var args = Array.prototype.slice.call(arguments)
|
|
if (typeof args[0] === 'string') {
|
|
args[0] = 'TUNNEL: ' + args[0]
|
|
} else {
|
|
args.unshift('TUNNEL:')
|
|
}
|
|
console.error.apply(console, args)
|
|
}
|
|
} else {
|
|
debug = function() {}
|
|
}
|
|
exports.debug = debug // for test
|
|
|