tg-redbird 0.12.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +23 -16
  2. package/lib/docker.js +22 -18
  3. package/lib/etcd-backend.js +32 -35
  4. package/lib/letsencrypt.js +41 -38
  5. package/lib/proxy.js +197 -238
  6. package/lib/redis-backend.js +84 -79
  7. package/package.json +17 -4
  8. package/.dockerignore +0 -1
  9. package/.eslintrc +0 -22
  10. package/.travis.yml +0 -21
  11. package/.vscode/launch.json +0 -30
  12. package/Dockerfile +0 -6
  13. package/gulpfile.js +0 -36
  14. package/hl-tests/64/proxy.js +0 -52
  15. package/hl-tests/letsencrypt/a.js +0 -36
  16. package/hl-tests/letsencrypt/certs/accounts/acme-staging.api.letsencrypt.org/directory/367e0270a5d31ab031561f9f284ca350/meta.json +0 -1
  17. package/hl-tests/letsencrypt/certs/accounts/acme-staging.api.letsencrypt.org/directory/367e0270a5d31ab031561f9f284ca350/private_key.json +0 -1
  18. package/hl-tests/letsencrypt/certs/accounts/acme-staging.api.letsencrypt.org/directory/367e0270a5d31ab031561f9f284ca350/regr.json +0 -1
  19. package/hl-tests/letsencrypt/certs/accounts/acme-v01.api.letsencrypt.org/directory/49881ab35d6ac7bb51f05ca3a220fbac/meta.json +0 -1
  20. package/hl-tests/letsencrypt/certs/accounts/acme-v01.api.letsencrypt.org/directory/49881ab35d6ac7bb51f05ca3a220fbac/private_key.json +0 -1
  21. package/hl-tests/letsencrypt/certs/accounts/acme-v01.api.letsencrypt.org/directory/49881ab35d6ac7bb51f05ca3a220fbac/regr.json +0 -1
  22. package/hl-tests/letsencrypt/certs/api/privkey.pem +0 -27
  23. package/hl-tests/letsencrypt/certs/api.com/privkey.pem +0 -27
  24. package/hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/cert0.pem +0 -29
  25. package/hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/chain0.pem +0 -27
  26. package/hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/fullchain0.pem +0 -56
  27. package/hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/privkey0.pem +0 -27
  28. package/hl-tests/letsencrypt/certs/caturra.exactbytes.com/cert.pem +0 -29
  29. package/hl-tests/letsencrypt/certs/caturra.exactbytes.com/chain.pem +0 -27
  30. package/hl-tests/letsencrypt/certs/caturra.exactbytes.com/fullchain.pem +0 -56
  31. package/hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem +0 -27
  32. package/hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem.bak +0 -27
  33. package/hl-tests/letsencrypt/certs/dash/.well-known/acme-challenge/abc +0 -1
  34. package/hl-tests/letsencrypt/certs/dash/privkey.pem +0 -27
  35. package/hl-tests/letsencrypt/certs/dash.com/privkey.pem +0 -27
  36. package/hl-tests/letsencrypt/certs/dash_/privkey.pem +0 -27
  37. package/hl-tests/letsencrypt/certs/dev-cert.pem +0 -17
  38. package/hl-tests/letsencrypt/certs/dev-csr.pem +0 -13
  39. package/hl-tests/letsencrypt/certs/dev-key.pem +0 -15
  40. package/hl-tests/letsencrypt/certs/example.com/privkey.pem +0 -27
  41. package/hl-tests/letsencrypt/certs/localhost/privkey.pem +0 -27
  42. package/hl-tests/letsencrypt/certs/renewal/caturra.exactbytes.com.conf +0 -67
  43. package/hl-tests/letsencrypt/certs/renewal/caturra.exactbytes.com.conf.bak +0 -68
  44. package/hl-tests/letsencrypt/proxy.js +0 -73
  45. package/hl-tests/paths.js +0 -19
  46. package/package-lock.json +0 -3269
  47. package/test/test_custom_resolver.js +0 -276
  48. package/test/test_hostheader.js +0 -141
  49. package/test/test_pathnames.js +0 -75
  50. package/test/test_register.js +0 -493
package/lib/proxy.js CHANGED
@@ -1,20 +1,20 @@
1
1
  /*eslint-env node */
2
- "use strict";
3
-
4
- var http = require("http"),
5
- httpProxy = require("http-proxy"),
6
- validUrl = require("valid-url"),
7
- parseUrl = require("url").parse,
8
- path = require("path"),
9
- _ = require("lodash"),
10
- pino = require("pino"),
11
- cluster = require("cluster"),
12
- hash = require("object-hash"),
13
- LRUCache = require("lru-cache"),
2
+ 'use strict';
3
+
4
+ var http = require('http'),
5
+ httpProxy = require('http-proxy'),
6
+ validUrl = require('valid-url'),
7
+ parseUrl = require('url').parse,
8
+ path = require('path'),
9
+ _ = require('lodash'),
10
+ pino = require('pino'),
11
+ cluster = require('cluster'),
12
+ hash = require('object-hash'),
13
+ LRUCache = require('lru-cache'),
14
14
  routeCache = new LRUCache({ max: 5000 }),
15
- safe = require("safetimeout"),
16
- letsencrypt = require("./letsencrypt.js"),
17
- Promise = require("bluebird");
15
+ safe = require('safetimeout'),
16
+ letsencrypt = require('./letsencrypt.js'),
17
+ Promise = require('bluebird');
18
18
 
19
19
  var ONE_DAY = 60 * 60 * 24 * 1000;
20
20
  var ONE_MONTH = ONE_DAY * 30;
@@ -34,15 +34,15 @@ function ReverseProxy(opts) {
34
34
  if (opts.bunyan !== false) {
35
35
  log = this.log = pino(
36
36
  opts.bunyan || {
37
- name: "redbird",
37
+ name: 'redbird',
38
38
  }
39
39
  );
40
40
  }
41
41
 
42
42
  var _this = this;
43
43
 
44
- if ((opts.cluster && typeof opts.cluster !== "number") || opts.cluster > 32) {
45
- throw Error("cluster setting must be an integer less than 32");
44
+ if ((opts.cluster && typeof opts.cluster !== 'number') || opts.cluster > 32) {
45
+ throw Error('cluster setting must be an integer less than 32');
46
46
  }
47
47
 
48
48
  if (opts.cluster && cluster.isMaster) {
@@ -50,7 +50,7 @@ function ReverseProxy(opts) {
50
50
  cluster.fork();
51
51
  }
52
52
 
53
- cluster.on("exit", function (worker, code, signal) {
53
+ cluster.on('exit', function (worker, code, signal) {
54
54
  // Fork if a worker dies.
55
55
  log &&
56
56
  log.error(
@@ -58,7 +58,7 @@ function ReverseProxy(opts) {
58
58
  code: code,
59
59
  signal: signal,
60
60
  },
61
- "worker died un-expectedly... restarting it."
61
+ 'worker died un-expectedly... restarting it.'
62
62
  );
63
63
  cluster.fork();
64
64
  });
@@ -96,9 +96,9 @@ function ReverseProxy(opts) {
96
96
  */
97
97
  }));
98
98
 
99
- proxy.on("proxyReq", function (p, req) {
99
+ proxy.on('proxyReq', function (p, req) {
100
100
  if (req.host != null) {
101
- p.setHeader("host", req.host);
101
+ p.setHeader('host', req.host);
102
102
  }
103
103
  });
104
104
 
@@ -106,10 +106,9 @@ function ReverseProxy(opts) {
106
106
  // Support NTLM auth
107
107
  //
108
108
  if (opts.ntlm) {
109
- proxy.on("proxyRes", function (proxyRes) {
110
- var key = "www-authenticate";
111
- proxyRes.headers[key] =
112
- proxyRes.headers[key] && proxyRes.headers[key].split(",");
109
+ proxy.on('proxyRes', function (proxyRes) {
110
+ var key = 'www-authenticate';
111
+ proxyRes.headers[key] = proxyRes.headers[key] && proxyRes.headers[key].split(',');
113
112
  });
114
113
  }
115
114
 
@@ -134,27 +133,25 @@ function ReverseProxy(opts) {
134
133
  server.listen(opts.port, opts.host);
135
134
 
136
135
  if (opts.errorHandler && _.isFunction(opts.errorHandler)) {
137
- proxy.on("error", opts.errorHandler);
136
+ proxy.on('error', opts.errorHandler);
138
137
  } else {
139
- proxy.on("error", handleProxyError);
138
+ proxy.on('error', handleProxyError);
140
139
  }
141
140
 
142
- log &&
143
- log.info("Started a Redbird reverse proxy server on port %s", opts.port);
141
+ log && log.info('Started a Redbird reverse proxy server on port %s', opts.port);
144
142
  }
145
143
 
146
144
  function websocketsUpgrade(req, socket, head) {
147
- socket.on("error", function (err) {
148
- log && log.error(err, "WebSockets error");
145
+ socket.on('error', function (err) {
146
+ log && log.error(err, 'WebSockets error');
149
147
  });
150
148
  var src = _this._getSource(req);
151
149
  _this._getTarget(src, req).then(function (target) {
152
- log &&
153
- log.info(
154
- { headers: req.headers, target: target },
155
- "upgrade to websockets"
156
- );
150
+ log && log.info({ headers: req.headers, target: target }, 'upgrade to websockets');
157
151
  if (target) {
152
+ if (target.useTargetHostHeader === true) {
153
+ req.headers.host = target.host;
154
+ }
158
155
  proxy.ws(req, socket, head, { target: target });
159
156
  } else {
160
157
  respondNotFound(req, socket);
@@ -167,7 +164,7 @@ function ReverseProxy(opts) {
167
164
  // Send a 500 http status if headers have been sent
168
165
  //
169
166
 
170
- if (err.code === "ECONNREFUSED") {
167
+ if (err.code === 'ECONNREFUSED') {
171
168
  res.writeHead && res.writeHead(502);
172
169
  } else if (!res.headersSent) {
173
170
  res.writeHead && res.writeHead(500);
@@ -176,8 +173,8 @@ function ReverseProxy(opts) {
176
173
  //
177
174
  // Do not log this common error
178
175
  //
179
- if (err.message !== "socket hang up") {
180
- log && log.error(err, "Proxy Error");
176
+ if (err.message !== 'socket hang up') {
177
+ log && log.error(err, 'Proxy Error');
181
178
  }
182
179
 
183
180
  //
@@ -188,21 +185,13 @@ function ReverseProxy(opts) {
188
185
  }
189
186
  }
190
187
 
191
- ReverseProxy.prototype.setupHttpProxy = function (
192
- proxy,
193
- websocketsUpgrade,
194
- log,
195
- opts
196
- ) {
188
+ ReverseProxy.prototype.setupHttpProxy = function (proxy, websocketsUpgrade, log, opts) {
197
189
  var _this = this;
198
190
  var httpServerModule = opts.serverModule || http;
199
- var server = (this.server = httpServerModule.createServer(function (
200
- req,
201
- res
202
- ) {
191
+ var server = (this.server = httpServerModule.createServer(function (req, res) {
203
192
  var src = _this._getSource(req);
204
193
  _this
205
- ._getTarget(src, req)
194
+ ._getTarget(src, req, res)
206
195
  .bind(_this)
207
196
  .then(function (target) {
208
197
  if (target) {
@@ -224,49 +213,39 @@ ReverseProxy.prototype.setupHttpProxy = function (
224
213
  // Listen to the `upgrade` event and proxy the
225
214
  // WebSocket requests as well.
226
215
  //
227
- server.on("upgrade", websocketsUpgrade);
216
+ server.on('upgrade', websocketsUpgrade);
228
217
 
229
- server.on("error", function (err) {
230
- log && log.error(err, "Server Error");
218
+ server.on('error', function (err) {
219
+ log && log.error(err, 'Server Error');
231
220
  });
232
221
 
233
222
  return server;
234
223
  };
235
224
 
236
225
  function shouldRedirectToHttps(certs, src, target, proxy) {
237
- return (
238
- certs &&
239
- src in certs &&
240
- target.sslRedirect &&
241
- target.host != proxy.letsencryptHost
242
- );
226
+ return certs && src in certs && target.sslRedirect && target.host != proxy.letsencryptHost;
243
227
  }
244
228
 
245
229
  ReverseProxy.prototype.setupLetsencrypt = function (log, opts) {
246
230
  if (!opts.letsencrypt.path) {
247
- throw Error("Missing certificate path for Lets Encrypt");
231
+ throw Error('Missing certificate path for Lets Encrypt');
248
232
  }
249
233
  var letsencryptPort = opts.letsencrypt.port || 3000;
250
234
  letsencrypt.init(opts.letsencrypt.path, letsencryptPort, log);
251
235
 
252
236
  opts.resolvers = opts.resolvers || [];
253
- this.letsencryptHost = "127.0.0.1:" + letsencryptPort;
254
- var targetHost = "http://" + this.letsencryptHost;
237
+ this.letsencryptHost = '127.0.0.1:' + letsencryptPort;
238
+ var targetHost = 'http://' + this.letsencryptHost;
255
239
  var challengeResolver = function (host, url) {
256
240
  if (/^\/.well-known\/acme-challenge/.test(url)) {
257
- return targetHost + "/" + host;
241
+ return targetHost + '/' + host;
258
242
  }
259
243
  };
260
244
  challengeResolver.priority = 9999;
261
245
  this.addResolver(challengeResolver);
262
246
  };
263
247
 
264
- ReverseProxy.prototype.setupHttpsProxy = function (
265
- proxy,
266
- websocketsUpgrade,
267
- log,
268
- sslOpts
269
- ) {
248
+ ReverseProxy.prototype.setupHttpsProxy = function (proxy, websocketsUpgrade, log, sslOpts) {
270
249
  var _this = this;
271
250
  var https;
272
251
 
@@ -303,42 +282,39 @@ ReverseProxy.prototype.setupHttpsProxy = function (
303
282
  }
304
283
 
305
284
  if (sslOpts.http2) {
306
- https = sslOpts.serverModule || require("spdy");
285
+ https = sslOpts.serverModule || require('spdy');
307
286
  if (_.isObject(sslOpts.http2)) {
308
287
  sslOpts.spdy = sslOpts.http2;
309
288
  }
310
289
  } else {
311
- https = sslOpts.serverModule || require("https");
290
+ https = sslOpts.serverModule || require('https');
312
291
  }
313
292
 
314
- var httpsServer = (this.httpsServer = https.createServer(
315
- ssl,
316
- function (req, res) {
317
- var src = _this._getSource(req);
318
- var httpProxyOpts = Object.assign({}, _this.opts.httpProxy);
293
+ var httpsServer = (this.httpsServer = https.createServer(ssl, function (req, res) {
294
+ var src = _this._getSource(req);
295
+ var httpProxyOpts = Object.assign({}, _this.opts.httpProxy);
319
296
 
320
- _this._getTarget(src, req).then(function (target) {
321
- if (target) {
322
- httpProxyOpts.target = target;
323
- proxy.web(req, res, httpProxyOpts);
324
- } else {
325
- respondNotFound(req, res);
326
- }
327
- });
328
- }
329
- ));
297
+ _this._getTarget(src, req, res).then(function (target) {
298
+ if (target) {
299
+ httpProxyOpts.target = target;
300
+ proxy.web(req, res, httpProxyOpts);
301
+ } else {
302
+ respondNotFound(req, res);
303
+ }
304
+ });
305
+ }));
330
306
 
331
- httpsServer.on("upgrade", websocketsUpgrade);
307
+ httpsServer.on('upgrade', websocketsUpgrade);
332
308
 
333
- httpsServer.on("error", function (err) {
334
- log && log.error(err, "HTTPS Server Error");
309
+ httpsServer.on('error', function (err) {
310
+ log && log.error(err, 'HTTPS Server Error');
335
311
  });
336
312
 
337
- httpsServer.on("clientError", function (err) {
338
- log && log.error(err, "HTTPS Client Error");
313
+ httpsServer.on('clientError', function (err) {
314
+ log && log.error(err, 'HTTPS Client Error');
339
315
  });
340
316
 
341
- log && log.info("Listening to HTTPS requests on port %s", sslOpts.port);
317
+ log && log.info('Listening to HTTPS requests on port %s', sslOpts.port);
342
318
  httpsServer.listen(sslOpts.port, sslOpts.ip);
343
319
  };
344
320
 
@@ -352,17 +328,17 @@ ReverseProxy.prototype.addResolver = function (resolver) {
352
328
  var _this = this;
353
329
  resolver.forEach(function (resolveObj) {
354
330
  if (!_.isFunction(resolveObj)) {
355
- throw new Error("Resolver must be an invokable function.");
331
+ throw new Error('Resolver must be an invokable function.');
356
332
  }
357
333
 
358
- if (!resolveObj.hasOwnProperty("priority")) {
334
+ if (!resolveObj.hasOwnProperty('priority')) {
359
335
  resolveObj.priority = 0;
360
336
  }
361
337
 
362
338
  _this.resolvers.push(resolveObj);
363
339
  });
364
340
 
365
- _this.resolvers = _.sortBy(_.uniq(_this.resolvers), ["priority"]).reverse();
341
+ _this.resolvers = _.sortBy(_.uniq(_this.resolvers), ['priority']).reverse();
366
342
  };
367
343
 
368
344
  ReverseProxy.prototype.removeResolver = function (resolver) {
@@ -394,8 +370,19 @@ ReverseProxy.buildTarget = function (target, opts) {
394
370
  ReverseProxy.prototype.register = function (src, target, opts) {
395
371
  if (this.opts.cluster && cluster.isMaster) return this;
396
372
 
373
+ // allow registering with src or target as an object to pass in
374
+ // options specific to each one.
375
+ if (src && src.src) {
376
+ target = src.target;
377
+ opts = src;
378
+ src = src.src;
379
+ } else if (target && target.target) {
380
+ opts = target;
381
+ target = target.target;
382
+ }
383
+
397
384
  if (!src || !target) {
398
- throw Error("Cannot register a new route with unspecified src or target");
385
+ throw Error('Cannot register a new route with unspecified src or target');
399
386
  }
400
387
 
401
388
  var routing = this.routing;
@@ -406,26 +393,18 @@ ReverseProxy.prototype.register = function (src, target, opts) {
406
393
  var ssl = opts.ssl;
407
394
  if (ssl) {
408
395
  if (!this.httpsServer) {
409
- throw Error("Cannot register https routes without defining a ssl port");
396
+ throw Error('Cannot register https routes without defining a ssl port');
410
397
  }
411
398
 
412
399
  if (!this.certs[src.hostname]) {
413
400
  if (ssl.key || ssl.cert || ssl.ca) {
414
- this.certs[src.hostname] = createCredentialContext(
415
- ssl.key,
416
- ssl.cert,
417
- ssl.ca
418
- );
401
+ this.certs[src.hostname] = createCredentialContext(ssl.key, ssl.cert, ssl.ca);
419
402
  } else if (ssl.letsencrypt) {
420
403
  if (!this.opts.letsencrypt || !this.opts.letsencrypt.path) {
421
- console.error("Missing certificate path for Lets Encrypt");
404
+ console.error('Missing certificate path for Lets Encrypt');
422
405
  return;
423
406
  }
424
- this.log &&
425
- this.log.info(
426
- "Getting Lets Encrypt certificates for %s",
427
- src.hostname
428
- );
407
+ this.log && this.log.info('Getting Lets Encrypt certificates for %s', src.hostname);
429
408
  this.updateCertificates(
430
409
  src.hostname,
431
410
  ssl.letsencrypt.email,
@@ -442,11 +421,11 @@ ReverseProxy.prototype.register = function (src, target, opts) {
442
421
  target = ReverseProxy.buildTarget(target, opts);
443
422
 
444
423
  var host = (routing[src.hostname] = routing[src.hostname] || []);
445
- var pathname = src.pathname || "/";
424
+ var pathname = src.pathname || '/';
446
425
  var route = _.find(host, { path: pathname });
447
426
 
448
427
  if (!route) {
449
- route = { path: pathname, rr: 0, urls: [] };
428
+ route = { path: pathname, rr: 0, urls: [], opts: Object.assign({}, opts) };
450
429
  host.push(route);
451
430
 
452
431
  //
@@ -459,8 +438,7 @@ ReverseProxy.prototype.register = function (src, target, opts) {
459
438
 
460
439
  route.urls.push(target);
461
440
 
462
- this.log &&
463
- this.log.info({ from: src, to: target }, "Registered a new route");
441
+ this.log && this.log.info({ from: src, to: target }, 'Registered a new route');
464
442
  return this;
465
443
  };
466
444
 
@@ -472,60 +450,42 @@ ReverseProxy.prototype.updateCertificates = function (
472
450
  renew
473
451
  ) {
474
452
  var _this = this;
475
- return letsencrypt
476
- .getCertificates(domain, email, production, renew, this.log)
477
- .then(
478
- function (certs) {
479
- if (certs) {
480
- var opts = {
481
- key: certs.privkey,
482
- cert: certs.cert + certs.chain,
483
- };
484
- _this.certs[domain] = tls.createSecureContext(opts).context;
485
-
486
- //
487
- // TODO: cluster friendly
488
- //
489
- var renewTime = certs.expiresAt - Date.now() - renewWithin;
490
- renewTime =
491
- renewTime > 0
492
- ? renewTime
493
- : _this.opts.letsencrypt.minRenewTime || 60 * 60 * 1000;
494
-
495
- _this.log &&
496
- _this.log.info(
497
- "Renewal of %s in %s days",
498
- domain,
499
- Math.floor(renewTime / ONE_DAY)
500
- );
453
+ return letsencrypt.getCertificates(domain, email, production, renew, this.log).then(
454
+ function (certs) {
455
+ if (certs) {
456
+ var opts = {
457
+ key: certs.privkey,
458
+ cert: certs.cert + certs.chain,
459
+ };
460
+ _this.certs[domain] = tls.createSecureContext(opts).context;
501
461
 
502
- function renewCertificate() {
503
- _this.log &&
504
- _this.log.info("Renewing letscrypt certificates for %s", domain);
505
- _this.updateCertificates(
506
- domain,
507
- email,
508
- production,
509
- renewWithin,
510
- true
511
- );
512
- }
462
+ //
463
+ // TODO: cluster friendly
464
+ //
465
+ var renewTime = certs.expiresAt - Date.now() - renewWithin;
466
+ renewTime =
467
+ renewTime > 0 ? renewTime : _this.opts.letsencrypt.minRenewTime || 60 * 60 * 1000;
513
468
 
514
- _this.certs[domain].renewalTimeout = safe.setTimeout(
515
- renewCertificate,
516
- renewTime
517
- );
518
- } else {
519
- //
520
- // TODO: Try again, but we need an exponential backof to avoid getting banned.
521
- //
522
- _this.log && _this.log.info("Could not get any certs for %s", domain);
469
+ _this.log &&
470
+ _this.log.info('Renewal of %s in %s days', domain, Math.floor(renewTime / ONE_DAY));
471
+
472
+ function renewCertificate() {
473
+ _this.log && _this.log.info('Renewing letscrypt certificates for %s', domain);
474
+ _this.updateCertificates(domain, email, production, renewWithin, true);
523
475
  }
524
- },
525
- function (err) {
526
- console.error("Error getting LetsEncrypt certificates", err);
476
+
477
+ _this.certs[domain].renewalTimeout = safe.setTimeout(renewCertificate, renewTime);
478
+ } else {
479
+ //
480
+ // TODO: Try again, but we need an exponential backof to avoid getting banned.
481
+ //
482
+ _this.log && _this.log.info('Could not get any certs for %s', domain);
527
483
  }
528
- );
484
+ },
485
+ function (err) {
486
+ console.error('Error getting LetsEncrypt certificates', err);
487
+ }
488
+ );
529
489
  };
530
490
 
531
491
  ReverseProxy.prototype.unregister = function (src, target) {
@@ -537,7 +497,7 @@ ReverseProxy.prototype.unregister = function (src, target) {
537
497
 
538
498
  src = prepareUrl(src);
539
499
  var routes = this.routing[src.hostname] || [];
540
- var pathname = src.pathname || "/";
500
+ var pathname = src.pathname || '/';
541
501
  var i;
542
502
 
543
503
  for (i = 0; i < routes.length; i++) {
@@ -569,8 +529,7 @@ ReverseProxy.prototype.unregister = function (src, target) {
569
529
  }
570
530
  }
571
531
 
572
- this.log &&
573
- this.log.info({ from: src, to: target }, "Unregistered a route");
532
+ this.log && this.log.info({ from: src, to: target }, 'Unregistered a route');
574
533
  }
575
534
  return this;
576
535
  };
@@ -581,7 +540,10 @@ ReverseProxy.prototype._defaultResolver = function (host, url) {
581
540
  return;
582
541
  }
583
542
 
584
- url = url || "/";
543
+ url = url || '/';
544
+
545
+ // When URL comes with query params, the default resolver may fail to get the correct route.
546
+ const parsedUrl = parseUrl(url)
585
547
 
586
548
  var routes = this.routing[host];
587
549
  var i = 0;
@@ -595,7 +557,7 @@ ReverseProxy.prototype._defaultResolver = function (host, url) {
595
557
  for (i = 0; i < len; i++) {
596
558
  var route = routes[i];
597
559
 
598
- if (route.path === "/" || startsWith(url, route.path)) {
560
+ if (route.path === '/' || startsWith(parsedUrl.pathname, route.path)) {
599
561
  return route;
600
562
  }
601
563
  }
@@ -627,18 +589,14 @@ ReverseProxy.prototype.resolve = function (host, url, req) {
627
589
  if (route && (route = ReverseProxy.buildRoute(route))) {
628
590
  // ensure resolved route has path that prefixes URL
629
591
  // no need to check for native routes.
630
- if (
631
- !route.isResolved ||
632
- route.path === "/" ||
633
- startsWith(url, route.path)
634
- ) {
592
+ if (!route.isResolved || route.path === '/' || startsWith(url, route.path)) {
635
593
  return route;
636
594
  }
637
595
  }
638
596
  }
639
597
  })
640
598
  .catch(function (error) {
641
- console.error("Resolvers error:", error);
599
+ console.error('Resolvers error:', error);
642
600
  });
643
601
  };
644
602
 
@@ -647,11 +605,7 @@ ReverseProxy.buildRoute = function (route) {
647
605
  return null;
648
606
  }
649
607
 
650
- if (
651
- _.isObject(route) &&
652
- route.hasOwnProperty("urls") &&
653
- route.hasOwnProperty("path")
654
- ) {
608
+ if (_.isObject(route) && route.hasOwnProperty('urls') && route.hasOwnProperty('path')) {
655
609
  // default route type matched.
656
610
  return route;
657
611
  }
@@ -665,36 +619,30 @@ ReverseProxy.buildRoute = function (route) {
665
619
  var routeObject = { rr: 0, isResolved: true };
666
620
  if (_.isString(route)) {
667
621
  routeObject.urls = [ReverseProxy.buildTarget(route)];
668
- routeObject.path = "/";
622
+ routeObject.path = '/';
669
623
  } else {
670
- if (!route.hasOwnProperty("url")) {
624
+ if (!route.hasOwnProperty('url')) {
671
625
  return null;
672
626
  }
673
627
 
674
- routeObject.urls = (_.isArray(route.url) ? route.url : [route.url]).map(
675
- function (url) {
676
- return ReverseProxy.buildTarget(url, route.opts || {});
677
- }
678
- );
628
+ routeObject.urls = (_.isArray(route.url) ? route.url : [route.url]).map(function (url) {
629
+ return ReverseProxy.buildTarget(url, route.opts || {});
630
+ });
679
631
 
680
- routeObject.path = route.path || "/";
632
+ routeObject.path = route.path || '/';
681
633
  }
682
634
  routeCache.set(cacheKey, routeObject);
683
635
  return routeObject;
684
636
  };
685
637
 
686
- ReverseProxy.prototype._getTarget = function (src, req) {
638
+ ReverseProxy.prototype._getTarget = function (src, req, res) {
687
639
  var url = req.url;
688
640
 
689
641
  return this.resolve(src, url, req)
690
642
  .bind(this)
691
643
  .then(function (route) {
692
644
  if (!route) {
693
- this.log &&
694
- this.log.warn(
695
- { src: src, url: url },
696
- "no valid route found for given source"
697
- );
645
+ this.log && this.log.warn({ src: src, url: url }, 'no valid route found for given source');
698
646
  return;
699
647
  }
700
648
 
@@ -704,7 +652,7 @@ ReverseProxy.prototype._getTarget = function (src, req) {
704
652
  // remove prefix from src
705
653
  //
706
654
  req._url = url; // save original url
707
- req.url = url.substr(pathname.length) || "/";
655
+ req.url = url.substr(pathname.length) || '';
708
656
  }
709
657
 
710
658
  //
@@ -721,7 +669,11 @@ ReverseProxy.prototype._getTarget = function (src, req) {
721
669
  // Fix request url if targetname specified.
722
670
  //
723
671
  if (target.pathname) {
724
- req.url = path.join(target.pathname, req.url);
672
+ if (req.url) {
673
+ req.url = path.posix.join(target.pathname, req.url);
674
+ } else {
675
+ req.url = target.pathname;
676
+ }
725
677
  }
726
678
 
727
679
  //
@@ -732,33 +684,41 @@ ReverseProxy.prototype._getTarget = function (src, req) {
732
684
  req.host = target.host;
733
685
  }
734
686
 
687
+ if (_.get(route, "opts.onRequest")) {
688
+ const resultFromRequestHandler = route.opts.onRequest(req, res, target);
689
+ if (resultFromRequestHandler !== undefined) {
690
+ this.log &&
691
+ this.log.info(
692
+ 'Proxying %s received result from onRequest handler, returning.',
693
+ src + url
694
+ );
695
+ return resultFromRequestHandler;
696
+ }
697
+ }
698
+
735
699
  this.log &&
736
- this.log.info(
737
- "Proxying %s to %s",
738
- src + url,
739
- path.join(target.host, req.url)
740
- );
700
+ this.log.info('Proxying %s to %s', src + url, path.posix.join(target.host, req.url));
741
701
 
742
702
  return target;
743
703
  });
744
704
  };
745
705
 
746
706
  ReverseProxy.prototype._getSource = function (req) {
747
- if (
748
- this.opts.preferForwardedHost === true &&
749
- req.headers["x-forwarded-host"]
750
- ) {
751
- return req.headers["x-forwarded-host"].split(":")[0];
707
+ if (this.opts.preferForwardedHost === true && req.headers['x-forwarded-host']) {
708
+ return req.headers['x-forwarded-host'].split(':')[0];
752
709
  }
753
710
  if (req.headers.host) {
754
- return req.headers.host.split(":")[0];
711
+ return req.headers.host.split(':')[0];
755
712
  }
756
713
  };
757
714
 
758
715
  ReverseProxy.prototype.close = function () {
759
716
  try {
760
- this.server.close();
761
- this.httpsServer && this.httpsServer.close();
717
+ return Promise.all(
718
+ [this.server, this.httpsServer]
719
+ .filter((s) => s)
720
+ .map((server) => new Promise((resolve) => server.close(resolve)))
721
+ );
762
722
  } catch (err) {
763
723
  // Ignore for now...
764
724
  }
@@ -787,13 +747,13 @@ ReverseProxy.prototype.close = function () {
787
747
 
788
748
  var respondNotFound = function (req, res) {
789
749
  res.statusCode = 404;
790
- res.write("Not Found");
750
+ res.write('Not Found');
791
751
  res.end();
792
752
  };
793
753
 
794
754
  ReverseProxy.prototype.notFound = function (callback) {
795
- if (typeof callback == "function") respondNotFound = callback;
796
- else throw Error("notFound callback is not a function");
755
+ if (typeof callback == 'function') respondNotFound = callback;
756
+ else throw Error('notFound callback is not a function');
797
757
  };
798
758
 
799
759
  //
@@ -803,11 +763,9 @@ function redirectToHttps(req, res, target, ssl, log) {
803
763
  req.url = req._url || req.url; // Get the original url since we are going to redirect.
804
764
 
805
765
  var targetPort = ssl.redirectPort || ssl.port;
806
- var hostname =
807
- req.headers.host.split(":")[0] + (targetPort ? ":" + targetPort : "");
808
- var url = "https://" + path.join(hostname, req.url);
809
- log &&
810
- log.info("Redirecting %s to %s", path.join(req.headers.host, req.url), url);
766
+ var hostname = req.headers.host.split(':')[0] + (targetPort ? ':' + targetPort : '');
767
+ var url = 'https://' + path.posix.join(hostname, req.url);
768
+ log && log.info('Redirecting %s to %s', path.posix.join(req.headers.host, req.url), url);
811
769
  //
812
770
  // We can use 301 for permanent redirect, but its bad for debugging, we may have it as
813
771
  // a configurable option.
@@ -818,8 +776,7 @@ function redirectToHttps(req, res, target, ssl, log) {
818
776
 
819
777
  function startsWith(input, str) {
820
778
  return (
821
- input.slice(0, str.length) === str &&
822
- (input.length === str.length || input[str.length] === "/")
779
+ input.slice(0, str.length) === str && (input.length === str.length || input[str.length] === '/')
823
780
  );
824
781
  }
825
782
 
@@ -829,7 +786,7 @@ function prepareUrl(url) {
829
786
  url = setHttp(url);
830
787
 
831
788
  if (!validUrl.isHttpUri(url) && !validUrl.isHttpsUri(url)) {
832
- throw Error("uri is not a valid http uri " + url);
789
+ throw Error('uri is not a valid http uri ' + url);
833
790
  }
834
791
 
835
792
  url = parseUrl(url);
@@ -837,27 +794,29 @@ function prepareUrl(url) {
837
794
  return url;
838
795
  }
839
796
 
840
- function getCertData(pathname, unbundle) {
841
- var fs = require("fs");
797
+ function getCertData(source, unbundle) {
798
+ var fs = require('fs');
799
+ var data;
800
+ // TODO: Support async source.
842
801
 
843
- // TODO: Support input as Buffer, Stream or Pathname.
844
-
845
- if (pathname) {
846
- if (_.isArray(pathname)) {
847
- var pathnames = pathname;
802
+ if (source) {
803
+ if (_.isArray(source)) {
804
+ var sources = source;
848
805
  return _.flatten(
849
- _.map(pathnames, function (_pathname) {
850
- return getCertData(_pathname, unbundle);
806
+ _.map(sources, function (_source) {
807
+ return getCertData(_source, unbundle);
851
808
  })
852
809
  );
853
- } else if (fs.existsSync(pathname)) {
854
- if (unbundle) {
855
- return unbundleCert(fs.readFileSync(pathname, "utf8"));
856
- } else {
857
- return fs.readFileSync(pathname, "utf8");
858
- }
810
+ } else if (Buffer.isBuffer(source)) {
811
+ data = source.toString('utf8');
812
+ } else if (fs.existsSync(source)) {
813
+ data = fs.readFileSync(source, 'utf8');
859
814
  }
860
815
  }
816
+
817
+ if (data) {
818
+ return unbundle ? unbundleCert(data) : data;
819
+ }
861
820
  }
862
821
 
863
822
  /**
@@ -865,7 +824,7 @@ function getCertData(pathname, unbundle) {
865
824
  http://www.benjiegillam.com/2012/06/node-dot-js-ssl-certificate-chain/
866
825
  */
867
826
  function unbundleCert(bundle) {
868
- var chain = bundle.trim().split("\n");
827
+ var chain = bundle.trim().split('\n');
869
828
 
870
829
  var ca = [];
871
830
  var cert = [];
@@ -877,7 +836,7 @@ function unbundleCert(bundle) {
877
836
  }
878
837
  cert.push(line);
879
838
  if (line.match(/-END CERTIFICATE-/)) {
880
- var joined = cert.join("\n");
839
+ var joined = cert.join('\n');
881
840
  ca.push(joined);
882
841
  cert = [];
883
842
  }
@@ -885,7 +844,7 @@ function unbundleCert(bundle) {
885
844
  return ca;
886
845
  }
887
846
 
888
- var tls = require("tls");
847
+ var tls = require('tls');
889
848
  function createCredentialContext(key, cert, ca) {
890
849
  var opts = {};
891
850
 
@@ -905,7 +864,7 @@ function createCredentialContext(key, cert, ca) {
905
864
  // Adds http protocol if non specified.
906
865
  function setHttp(link) {
907
866
  if (link.search(/^http[s]?\:\/\//) === -1) {
908
- link = "http://" + link;
867
+ link = 'http://' + link;
909
868
  }
910
869
  return link;
911
870
  }