UNPKG

6.72 kBJavaScriptView Raw
1/**
2 * Module dependencies.
3 */
4
5var net = require('net');
6var tls = require('tls');
7var url = require('url');
8var events = require('events');
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 ? /^https:?$/i.test(proxy.protocol) : false;
40
41 // prefer `hostname` over `host`, and set the `port` if needed
42 proxy.host = proxy.hostname || proxy.host;
43 proxy.port = +proxy.port || (this.secureProxy ? 443 : 80);
44
45 // ALPN is supported by Node.js >= v5.
46 // attempt to negotiate http/1.1 for proxy servers that support http/2
47 if (this.secureProxy && !('ALPNProtocols' in proxy)) {
48 proxy.ALPNProtocols = ['http 1.1']
49 }
50
51 if (proxy.host && proxy.path) {
52 // if both a `host` and `path` are specified then it's most likely the
53 // result of a `url.parse()` call... we need to remove the `path` portion so
54 // that `net.connect()` doesn't attempt to open that as a unix socket file.
55 delete proxy.path;
56 delete proxy.pathname;
57 }
58
59 this.proxy = proxy;
60 this.defaultPort = 443;
61}
62inherits(HttpsProxyAgent, Agent);
63
64/**
65 * Called when the node-core HTTP client library is creating a new HTTP request.
66 *
67 * @api public
68 */
69
70HttpsProxyAgent.prototype.callback = function connect(req, opts, fn) {
71 var proxy = this.proxy;
72
73 // create a socket connection to the proxy server
74 var socket;
75 if (this.secureProxy) {
76 socket = tls.connect(proxy);
77 } else {
78 socket = net.connect(proxy);
79 }
80
81 // we need to buffer any HTTP traffic that happens with the proxy before we get
82 // the CONNECT response, so that if the response is anything other than an "200"
83 // response code, then we can re-play the "data" events on the socket once the
84 // HTTP parser is hooked up...
85 var buffers = [];
86 var buffersLength = 0;
87
88 function read() {
89 var b = socket.read();
90 if (b) ondata(b);
91 else socket.once('readable', read);
92 }
93
94 function cleanup() {
95 socket.removeListener('end', onend);
96 socket.removeListener('error', onerror);
97 socket.removeListener('close', onclose);
98 socket.removeListener('readable', read);
99 }
100
101 function onclose(err) {
102 debug('onclose had error %o', err);
103 }
104
105 function onend() {
106 debug('onend');
107 }
108
109 function onerror(err) {
110 cleanup();
111 fn(err);
112 }
113
114 function ondata(b) {
115 buffers.push(b);
116 buffersLength += b.length;
117 var buffered = Buffer.concat(buffers, buffersLength);
118 var str = buffered.toString('ascii');
119
120 if (!~str.indexOf('\r\n\r\n')) {
121 // keep buffering
122 debug('have not received end of HTTP headers yet...');
123 read();
124 return;
125 }
126
127 var firstLine = str.substring(0, str.indexOf('\r\n'));
128 var statusCode = +firstLine.split(' ')[1];
129 debug('got proxy server response: %o', firstLine);
130
131 if (200 == statusCode) {
132 // 200 Connected status code!
133 var sock = socket;
134
135 // nullify the buffered data since we won't be needing it
136 buffers = buffered = null;
137
138 if (opts.secureEndpoint) {
139 // since the proxy is connecting to an SSL server, we have
140 // to upgrade this socket connection to an SSL connection
141 debug(
142 'upgrading proxy-connected socket to TLS connection: %o',
143 opts.host
144 );
145 opts.socket = socket;
146 opts.servername = opts.servername || opts.host;
147 opts.host = null;
148 opts.hostname = null;
149 opts.port = null;
150 sock = tls.connect(opts);
151 }
152
153 cleanup();
154 req.once('socket', resume);
155 fn(null, sock);
156 } else {
157 // some other status code that's not 200... need to re-play the HTTP header
158 // "data" events onto the socket once the HTTP machinery is attached so
159 // that the node core `http` can parse and handle the error status code
160 cleanup();
161
162 // the original socket is closed, and a "fake socket" EventEmitter is
163 // returned instead, so that the proxy doesn't get the HTTP request
164 // written to it (which may contain `Authorization` headers or other
165 // sensitive data).
166 //
167 // See: https://hackerone.com/reports/541502
168 socket.destroy();
169 socket = new events.EventEmitter();
170
171 // save a reference to the concat'd Buffer for the `onsocket` callback
172 buffers = buffered;
173
174 // need to wait for the "socket" event to re-play the "data" events
175 req.once('socket', onsocket);
176
177 fn(null, socket);
178 }
179 }
180
181 function onsocket(socket) {
182 debug('replaying proxy buffer for failed request');
183
184 // replay the "buffers" Buffer onto the `socket`, since at this point
185 // the HTTP module machinery has been hooked up for the user
186 if (socket.listenerCount('data') > 0) {
187 socket.emit('data', buffers);
188 } else {
189 // never?
190 throw new Error('should not happen...');
191 }
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}