UNPKG

proxying-agent

Version:
408 lines (348 loc) 12.6 kB
'use strict'; var url = require('url'); var http = require('http'); var httpRequest = http.request; var https = require('https'); var httpsRequest = https.request; var tls = require('tls'); var util = require('util'); var ntlm = require('./ntlm'); function ProxyingAgent(options, agent) { this.openSockets = {}; this.options = util._extend({}, options); this.options.proxy = url.parse(this.options.proxy); this.options.tunnel = this.options.tunnel || false; this.options.ssl = this.options.proxy.protocol ? this.options.proxy.protocol.toLowerCase() == 'https:' : false; this.options.host = this.options.proxy.hostname; this.options.port = this.options.proxy.port || (this.options.ssl ? 443 : 80); this.options.authType = this.options.authType || 'basic'; if (this.options.authType === 'ntlm') { if (!this.options.proxy.auth) { throw new Error('NTLM authentication credentials must be provided'); } if (!this.options.ntlm || !this.options.ntlm.domain) { throw new Error('NTLM domain must be provided'); } } // base64 decode proxy auth if necessary var auth = this.options.proxy.auth; if (auth && auth.indexOf(':') == -1) { auth = new Buffer(auth, 'base64').toString('ascii'); // if after decoding there still isn't a colon, then revert back to the original value if (auth.indexOf(':') == -1) { auth = this.options.proxy.auth; } this.options.proxy.auth = auth; } // select the Agent type to use based on the proxy protocol this.agent = agent; this.agent.call(this, this.options); } /** * Get absolutURI without port when using default */ ProxyingAgent.prototype.getAbsoluteURI = function(ssl, host, port, path) { var absoluteUri = (ssl ? 'https://' : 'http://') + host; if (typeof port === 'string') { // Check if target url have specified port and add it to absoluteUri // When port is defined in target url then we get it as a string otherwise it's a number absoluteUri += ':' + port; } absoluteUri += path; return absoluteUri; }; /** * Overrides the 'addRequest' Agent method for establishing a socket with the proxy * that will e used to issue the actual request */ ProxyingAgent.prototype.addRequest = function(req, host, port) { if (typeof host === 'object') { port = host.port; host = host.hostname || host.host; } if (this.options.authType === 'ntlm') { this.startNtlm(req, host, port); } else { this.startProxying(req, host, port); } }; /** * Start proxying the request through the proxy server. * This automatically opens a tunnel through the proxy if needed, * or just issues a regular request for the proxy to transfer */ ProxyingAgent.prototype.startProxying = function(req, host, port) { // setup the basic authentication header for the proxy. // we do this only if we haven't already authenticated through NTLM if (this.options.authType == 'basic' && this.options.proxy.auth) { this.authHeader = { header: 'Proxy-Authorization', value: 'Basic ' + new Buffer(this.options.proxy.auth).toString('base64') } } // if we need to create a tunnel to the server via the CONNECT method if (this.options.tunnel) { var tunnelOptions = util._extend({}, this.options); tunnelOptions.method = 'CONNECT'; tunnelOptions.path = host+':'+port; tunnelOptions.hostname = this.options.proxy.hostname; tunnelOptions.port = this.options.proxy.port; tunnelOptions.headers = tunnelOptions.headers || {}; // if we already have a socket open then execute the CONNECT method on it var socket = this.getSocket(req); if (socket) { tunnelOptions.agent = new SocketAgent(socket); } // add the authentication header if (this.authHeader) { tunnelOptions.headers[this.authHeader.header] = this.authHeader.value; if (this.authHeader.keepAlive) { tunnelOptions.headers["Proxy-Connection"] = "Keep-Alive"; } } // create a new CONNECT request to the proxy to create the tunnel // to the server var newReq = this.createNewRequest(tunnelOptions); newReq.once('close', function() { this.emitError(req, 'Tunnel creation failed. Socket closed prematurely'); }.bind(this)); newReq.once('error', function(error) { this.emitError(req, 'Tunnel creation failed. Socket error: ' + error); }.bind(this)); // listen for the CONNECT event to complete and execute the original request // on the TLSed socket newReq.once('connect', function(response, socket, head) { newReq.removeAllListeners(); if (response.statusCode != 200) { this.emitError(req, 'Tunnel creation failed. Received status code ' + response.statusCode); return; } var tlsOptions = this.options.tlsOptions || {}; tlsOptions.socket = response.socket; tlsOptions.servername = host; // upgrade the socket to TLS var tlsSocket = tls.connect(tlsOptions, function() { this.setSocket(req, tlsSocket); this.execRequest(req, this.options.host, this.options.port); }.bind(this)); tlsSocket.once('error', function(error) { this.emitError(req, 'Tunnel socket error: ' + error); }.bind(this)); }.bind(this)); // execute the CONNECT method to create the tunnel newReq.end(); } else { // issue a regular proxy request req.path = this.getAbsoluteURI(this.options.ssl, host, port, req.path); if (this.authHeader) { req.setHeader(this.authHeader.header, this.authHeader.value); } this.execRequest(req, this.options.host, this.options.port); } }; /** * Start an NTLM authentication process. The result is an open socket that will be used * to issue the actual request or open a tunnel on */ ProxyingAgent.prototype.startNtlm = function(req, host, port) { var ntlmOptions = util._extend({}, this.options); ntlmOptions.method = ntlmOptions.method || 'GET'; // just for the NTLM handshake ntlmOptions.path = this.getAbsoluteURI(this.options.ssl, host, port, req.path); ntlmOptions.ntlm.workstation = ntlmOptions.ntlm.workstation || require('os').hostname(); var creds = this.options.proxy.auth.match(/([^:]*):?(.*)/); ntlmOptions.ntlm.username = creds[1]; ntlmOptions.ntlm.password = creds[2]; // set the NTLM type 1 message header ntlmOptions.headers = ntlmOptions.headers || {}; ntlmOptions.headers['Proxy-Authorization'] = ntlm.createType1Message(ntlmOptions.ntlm); ntlmOptions.headers['Proxy-Connection'] = "Keep-Alive"; // create the NTLM type 1 request var newReq = this.createNewRequest(ntlmOptions); // capture the response and set the NTLM type 3 authorization header // that will be used when issuing the actual request newReq.once('response', function(response) { if (response.statusCode !== 407 || !response.headers['proxy-authenticate']) { this.emitError(req, 'did not receive NTLM type 2 message'); return; } var type2msg = ntlm.parseType2Message(response.headers['proxy-authenticate'], function(error) { this.emitError(req, error); return null; }.bind(this)); if (!type2msg) { return; } // capture the socket this.setSocket(req, response.socket); this.authHeader = { header: 'Proxy-Authorization', value: ntlm.createType3Message(type2msg, ntlmOptions.ntlm), keepAlive: true } // read all the data from the socket as it may contain a body that should be discarded response.on('data', function() { // just consume the body }.bind(this)); // start proxying this.startProxying(req, host, port); }.bind(this)); // start proxying the actual request only when there is not more body to read. // the socket should have already been captured and associated with the request newReq.once('close', function() { this.emitError(req, 'NTLM failed. Socket closed prematurely'); }.bind(this)); newReq.once('error', function(error) { this.emitError(req, 'NTLM failed. Socket error: ' + error); }.bind(this)); // issue the NTLM type 1 request newReq.end(); }; /** * Create a new request instance according the needed security */ ProxyingAgent.prototype.createNewRequest = function(options) { if (options.ssl) { return new httpsRequest(options); } return new httpRequest(options); }; /** * Execute the provided request by invoking the original Agent 'addRequest' method. * If there is already a socket that was associated with the request, then it * will be used for issuing the request (via the 'createSocket' method) */ ProxyingAgent.prototype.execRequest = function(req, host, port) { this.agent.prototype.addRequest.call(this, req, host, port); // if there is an associated socket to this request then the association is removed // since the socket was already passed to the request if (this.openSockets[req]) { delete this.openSockets[req]; } }; /** * Remember a socket and associate it with a specific request. * When the 'createSocket' method will be called to execute the actual request * then the already existing socket will be used */ ProxyingAgent.prototype.setSocket = function(req, socket) { this.openSockets[req] = socket; var onClose = function() { if (this.openSockets[req]) { delete this.openSockets[req]; } }.bind(this); this.openSockets[req].on('close', onClose); }; ProxyingAgent.prototype.getSocket = function(req) { return this.openSockets[req]; }; /** * This is called during the 'addRequest' call of the original Agent to return a * new socket for executing the request. If a socket already exists then it is used * instead of creating a new one. */ ProxyingAgent.prototype.createSocket = function() { var req; var cb; if (typeof arguments[0] === 'object') { req = arguments[0]; cb = arguments[2]; } else { req = arguments[4]; } if (this.openSockets[req]) { if (cb) { return cb(null, this.openSockets[req]); } else { return this.openSockets[req]; } } return this.agent.prototype.createSocket.apply(this, arguments); }; ProxyingAgent.prototype.emitError = function(req, message) { (req.socket || req).emit('error', new Error(message)); }; //======= SocketAgent /** * A simple agent to execute a request on a given socket */ function SocketAgent(socket) { this.socket = socket; } SocketAgent.prototype.addRequest = function(req, host, port) { req.onSocket(this.socket); }; /** * HttpProxyingAgent * @param options * @constructor */ function HttpProxyingAgent(options) { ProxyingAgent.call(this, options, http.Agent); } util.inherits(HttpProxyingAgent, http.Agent); util._extend(HttpProxyingAgent.prototype, ProxyingAgent.prototype); /** * HttpsProxyingAgent * @param options * @constructor */ function HttpsProxyingAgent(options) { options.tunnel = true; ProxyingAgent.call(this, options, https.Agent); } util.inherits(HttpsProxyingAgent, https.Agent); util._extend(HttpsProxyingAgent.prototype, ProxyingAgent.prototype); /** * Create the proxying agent * @param proxy * @param target * @returns {*} */ exports.create = function(proxy, target) { if (typeof proxy === 'string') { proxy = {proxy: proxy} } if (target.toLowerCase().indexOf('https:') === 0) { return new HttpsProxyingAgent(proxy); } return new HttpProxyingAgent(proxy); }; /** * Set a global agent to forward all http and https requests through the specified proxy * @param proxy */ exports.globalize = function (proxy) { var copyProxy = JSON.parse(JSON.stringify(proxy)); var copySecureProxy = JSON.parse(JSON.stringify(proxy)); var secureAgent = this.create(copySecureProxy, 'https://'); var nonSecureAgent = this.create(copyProxy, 'http://'); http.request = function (options, callback) { if (typeof options === 'string') { options = url.parse(options); } if (!options.agent) { options.agent = nonSecureAgent; } return httpRequest(options, callback); }; http.get = function get(options, cb) { const req = http.request(options, cb); req.end(); return req; }; https.request = function (options, callback) { if (typeof options === 'string') { options = url.parse(options); } if (!options.agent) { options.agent = secureAgent; } return httpsRequest(options, callback); }; https.get = function get(options, cb) { const req = https.request(options, cb); req.end(); return req; }; };