• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1/**
2 * Module dependencies.
3 */
4
5var net = require('net');
6var tls = require('tls');
7var url = require('url');
8var assert = require('assert');
9var Agent = require('agent-base');
10var inherits = require('util').inherits;
11var debug = require('debug')('https-proxy-agent');
12
13/**
14 * Module exports.
15 */
16
17module.exports = HttpsProxyAgent;
18
19/**
20 * The `HttpsProxyAgent` implements an HTTP Agent subclass that connects to the
21 * specified "HTTP(s) proxy server" in order to proxy HTTPS requests.
22 *
23 * @api public
24 */
25
26function HttpsProxyAgent(opts) {
27	if (!(this instanceof HttpsProxyAgent)) return new HttpsProxyAgent(opts);
28	if ('string' == typeof opts) opts = url.parse(opts);
29	if (!opts)
30		throw new Error(
31			'an HTTP(S) proxy server `host` and `port` must be specified!'
32		);
33	debug('creating new HttpsProxyAgent instance: %o', opts);
34	Agent.call(this, opts);
35
36	var proxy = Object.assign({}, opts);
37
38	// if `true`, then connect to the proxy server over TLS. defaults to `false`.
39	this.secureProxy = proxy.protocol
40		? /^https:?$/i.test(proxy.protocol)
41		: false;
42
43	// prefer `hostname` over `host`, and set the `port` if needed
44	proxy.host = proxy.hostname || proxy.host;
45	proxy.port = +proxy.port || (this.secureProxy ? 443 : 80);
46
47	// ALPN is supported by Node.js >= v5.
48	// attempt to negotiate http/1.1 for proxy servers that support http/2
49	if (this.secureProxy && !('ALPNProtocols' in proxy)) {
50		proxy.ALPNProtocols = ['http 1.1'];
51	}
52
53	if (proxy.host && proxy.path) {
54		// if both a `host` and `path` are specified then it's most likely the
55		// result of a `url.parse()` call... we need to remove the `path` portion so
56		// that `net.connect()` doesn't attempt to open that as a unix socket file.
57		delete proxy.path;
58		delete proxy.pathname;
59	}
60
61	this.proxy = proxy;
62	this.defaultPort = 443;
63}
64inherits(HttpsProxyAgent, Agent);
65
66/**
67 * Called when the node-core HTTP client library is creating a new HTTP request.
68 *
69 * @api public
70 */
71
72HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) {
73	var proxy = this.proxy;
74
75	// create a socket connection to the proxy server
76	var socket;
77	if (this.secureProxy) {
78		socket = tls.connect(proxy);
79	} else {
80		socket = net.connect(proxy);
81	}
82
83	// we need to buffer any HTTP traffic that happens with the proxy before we get
84	// the CONNECT response, so that if the response is anything other than an "200"
85	// response code, then we can re-play the "data" events on the socket once the
86	// HTTP parser is hooked up...
87	var buffers = [];
88	var buffersLength = 0;
89
90	function read() {
91		var b = socket.read();
92		if (b) ondata(b);
93		else socket.once('readable', read);
94	}
95
96	function cleanup() {
97		socket.removeListener('end', onend);
98		socket.removeListener('error', onerror);
99		socket.removeListener('close', onclose);
100		socket.removeListener('readable', read);
101	}
102
103	function onclose(err) {
104		debug('onclose had error %o', err);
105	}
106
107	function onend() {
108		debug('onend');
109	}
110
111	function onerror(err) {
112		cleanup();
113		fn(err);
114	}
115
116	function ondata(b) {
117		buffers.push(b);
118		buffersLength += b.length;
119		var buffered = Buffer.concat(buffers, buffersLength);
120		var str = buffered.toString('ascii');
121
122		if (!~str.indexOf('\r\n\r\n')) {
123			// keep buffering
124			debug('have not received end of HTTP headers yet...');
125			read();
126			return;
127		}
128
129		var firstLine = str.substring(0, str.indexOf('\r\n'));
130		var statusCode = +firstLine.split(' ')[1];
131		debug('got proxy server response: %o', firstLine);
132
133		if (200 == statusCode) {
134			// 200 Connected status code!
135			var sock = socket;
136
137			// nullify the buffered data since we won't be needing it
138			buffers = buffered = null;
139
140			if (opts.secureEndpoint) {
141				// since the proxy is connecting to an SSL server, we have
142				// to upgrade this socket connection to an SSL connection
143				debug(
144					'upgrading proxy-connected socket to TLS connection: %o',
145					opts.host
146				);
147				opts.socket = socket;
148				opts.servername = opts.servername || opts.host;
149				opts.host = null;
150				opts.hostname = null;
151				opts.port = null;
152				sock = tls.connect(opts);
153			}
154
155			cleanup();
156			req.once('socket', resume);
157			fn(null, sock);
158		} else {
159			// some other status code that's not 200... need to re-play the HTTP header
160			// "data" events onto the socket once the HTTP machinery is attached so
161			// that the node core `http` can parse and handle the error status code
162			cleanup();
163
164			// the original socket is closed, and a new closed socket is
165			// returned instead, so that the proxy doesn't get the HTTP request
166			// written to it (which may contain `Authorization` headers or other
167			// sensitive data).
168			//
169			// See: https://hackerone.com/reports/541502
170			socket.destroy();
171			socket = new net.Socket();
172			socket.readable = true;
173
174
175			// save a reference to the concat'd Buffer for the `onsocket` callback
176			buffers = buffered;
177
178			// need to wait for the "socket" event to re-play the "data" events
179			req.once('socket', onsocket);
180
181			fn(null, socket);
182		}
183	}
184
185	function onsocket(socket) {
186		debug('replaying proxy buffer for failed request');
187		assert(socket.listenerCount('data') > 0);
188
189		// replay the "buffers" Buffer onto the `socket`, since at this point
190		// the HTTP module machinery has been hooked up for the user
191		socket.push(buffers);
192
193		// nullify the cached Buffer instance
194		buffers = null;
195	}
196
197	socket.on('error', onerror);
198	socket.on('close', onclose);
199	socket.on('end', onend);
200
201	read();
202
203	var hostname = opts.host + ':' + opts.port;
204	var msg = 'CONNECT ' + hostname + ' HTTP/1.1\r\n';
205
206	var headers = Object.assign({}, proxy.headers);
207	if (proxy.auth) {
208		headers['Proxy-Authorization'] =
209			'Basic ' + Buffer.from(proxy.auth).toString('base64');
210	}
211
212	// the Host header should only include the port
213	// number when it is a non-standard port
214	var host = opts.host;
215	if (!isDefaultPort(opts.port, opts.secureEndpoint)) {
216		host += ':' + opts.port;
217	}
218	headers['Host'] = host;
219
220	headers['Connection'] = 'close';
221	Object.keys(headers).forEach(function(name) {
222		msg += name + ': ' + headers[name] + '\r\n';
223	});
224
225	socket.write(msg + '\r\n');
226};
227
228/**
229 * Resumes a socket.
230 *
231 * @param {(net.Socket|tls.Socket)} socket The socket to resume
232 * @api public
233 */
234
235function resume(socket) {
236	socket.resume();
237}
238
239function isDefaultPort(port, secure) {
240	return Boolean((!secure && port === 80) || (secure && port === 443));
241}
242