UNPKG

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