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.
- package/README.md +23 -16
- package/lib/docker.js +22 -18
- package/lib/etcd-backend.js +32 -35
- package/lib/letsencrypt.js +41 -38
- package/lib/proxy.js +197 -238
- package/lib/redis-backend.js +84 -79
- package/package.json +17 -4
- package/.dockerignore +0 -1
- package/.eslintrc +0 -22
- package/.travis.yml +0 -21
- package/.vscode/launch.json +0 -30
- package/Dockerfile +0 -6
- package/gulpfile.js +0 -36
- package/hl-tests/64/proxy.js +0 -52
- package/hl-tests/letsencrypt/a.js +0 -36
- package/hl-tests/letsencrypt/certs/accounts/acme-staging.api.letsencrypt.org/directory/367e0270a5d31ab031561f9f284ca350/meta.json +0 -1
- package/hl-tests/letsencrypt/certs/accounts/acme-staging.api.letsencrypt.org/directory/367e0270a5d31ab031561f9f284ca350/private_key.json +0 -1
- package/hl-tests/letsencrypt/certs/accounts/acme-staging.api.letsencrypt.org/directory/367e0270a5d31ab031561f9f284ca350/regr.json +0 -1
- package/hl-tests/letsencrypt/certs/accounts/acme-v01.api.letsencrypt.org/directory/49881ab35d6ac7bb51f05ca3a220fbac/meta.json +0 -1
- package/hl-tests/letsencrypt/certs/accounts/acme-v01.api.letsencrypt.org/directory/49881ab35d6ac7bb51f05ca3a220fbac/private_key.json +0 -1
- package/hl-tests/letsencrypt/certs/accounts/acme-v01.api.letsencrypt.org/directory/49881ab35d6ac7bb51f05ca3a220fbac/regr.json +0 -1
- package/hl-tests/letsencrypt/certs/api/privkey.pem +0 -27
- package/hl-tests/letsencrypt/certs/api.com/privkey.pem +0 -27
- package/hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/cert0.pem +0 -29
- package/hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/chain0.pem +0 -27
- package/hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/fullchain0.pem +0 -56
- package/hl-tests/letsencrypt/certs/archive/caturra.exactbytes.com/privkey0.pem +0 -27
- package/hl-tests/letsencrypt/certs/caturra.exactbytes.com/cert.pem +0 -29
- package/hl-tests/letsencrypt/certs/caturra.exactbytes.com/chain.pem +0 -27
- package/hl-tests/letsencrypt/certs/caturra.exactbytes.com/fullchain.pem +0 -56
- package/hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem +0 -27
- package/hl-tests/letsencrypt/certs/caturra.exactbytes.com/privkey.pem.bak +0 -27
- package/hl-tests/letsencrypt/certs/dash/.well-known/acme-challenge/abc +0 -1
- package/hl-tests/letsencrypt/certs/dash/privkey.pem +0 -27
- package/hl-tests/letsencrypt/certs/dash.com/privkey.pem +0 -27
- package/hl-tests/letsencrypt/certs/dash_/privkey.pem +0 -27
- package/hl-tests/letsencrypt/certs/dev-cert.pem +0 -17
- package/hl-tests/letsencrypt/certs/dev-csr.pem +0 -13
- package/hl-tests/letsencrypt/certs/dev-key.pem +0 -15
- package/hl-tests/letsencrypt/certs/example.com/privkey.pem +0 -27
- package/hl-tests/letsencrypt/certs/localhost/privkey.pem +0 -27
- package/hl-tests/letsencrypt/certs/renewal/caturra.exactbytes.com.conf +0 -67
- package/hl-tests/letsencrypt/certs/renewal/caturra.exactbytes.com.conf.bak +0 -68
- package/hl-tests/letsencrypt/proxy.js +0 -73
- package/hl-tests/paths.js +0 -19
- package/package-lock.json +0 -3269
- package/test/test_custom_resolver.js +0 -276
- package/test/test_hostheader.js +0 -141
- package/test/test_pathnames.js +0 -75
- package/test/test_register.js +0 -493
package/lib/proxy.js
CHANGED
@@ -1,20 +1,20 @@
|
|
1
1
|
/*eslint-env node */
|
2
|
-
|
3
|
-
|
4
|
-
var http = require(
|
5
|
-
httpProxy = require(
|
6
|
-
validUrl = require(
|
7
|
-
parseUrl = require(
|
8
|
-
path = require(
|
9
|
-
_ = require(
|
10
|
-
pino = require(
|
11
|
-
cluster = require(
|
12
|
-
hash = require(
|
13
|
-
LRUCache = require(
|
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(
|
16
|
-
letsencrypt = require(
|
17
|
-
Promise = require(
|
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:
|
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 !==
|
45
|
-
throw Error(
|
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(
|
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
|
-
|
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(
|
99
|
+
proxy.on('proxyReq', function (p, req) {
|
100
100
|
if (req.host != null) {
|
101
|
-
p.setHeader(
|
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(
|
110
|
-
var key =
|
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(
|
136
|
+
proxy.on('error', opts.errorHandler);
|
138
137
|
} else {
|
139
|
-
proxy.on(
|
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(
|
148
|
-
log && log.error(err,
|
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 ===
|
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 !==
|
180
|
-
log && log.error(err,
|
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(
|
216
|
+
server.on('upgrade', websocketsUpgrade);
|
228
217
|
|
229
|
-
server.on(
|
230
|
-
log && log.error(err,
|
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(
|
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 =
|
254
|
-
var targetHost =
|
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 +
|
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(
|
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(
|
290
|
+
https = sslOpts.serverModule || require('https');
|
312
291
|
}
|
313
292
|
|
314
|
-
var httpsServer = (this.httpsServer = https.createServer(
|
315
|
-
|
316
|
-
|
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
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
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(
|
307
|
+
httpsServer.on('upgrade', websocketsUpgrade);
|
332
308
|
|
333
|
-
httpsServer.on(
|
334
|
-
log && log.error(err,
|
309
|
+
httpsServer.on('error', function (err) {
|
310
|
+
log && log.error(err, 'HTTPS Server Error');
|
335
311
|
});
|
336
312
|
|
337
|
-
httpsServer.on(
|
338
|
-
log && log.error(err,
|
313
|
+
httpsServer.on('clientError', function (err) {
|
314
|
+
log && log.error(err, 'HTTPS Client Error');
|
339
315
|
});
|
340
316
|
|
341
|
-
log && log.info(
|
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(
|
331
|
+
throw new Error('Resolver must be an invokable function.');
|
356
332
|
}
|
357
333
|
|
358
|
-
if (!resolveObj.hasOwnProperty(
|
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), [
|
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(
|
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(
|
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(
|
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
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
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
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
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
|
-
|
515
|
-
|
516
|
-
|
517
|
-
|
518
|
-
|
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
|
-
|
526
|
-
|
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 ===
|
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(
|
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(
|
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
|
-
|
676
|
-
|
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
|
-
|
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
|
-
|
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(
|
711
|
+
return req.headers.host.split(':')[0];
|
755
712
|
}
|
756
713
|
};
|
757
714
|
|
758
715
|
ReverseProxy.prototype.close = function () {
|
759
716
|
try {
|
760
|
-
|
761
|
-
|
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(
|
750
|
+
res.write('Not Found');
|
791
751
|
res.end();
|
792
752
|
};
|
793
753
|
|
794
754
|
ReverseProxy.prototype.notFound = function (callback) {
|
795
|
-
if (typeof callback ==
|
796
|
-
else throw Error(
|
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
|
-
|
808
|
-
|
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(
|
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(
|
841
|
-
var fs = require(
|
797
|
+
function getCertData(source, unbundle) {
|
798
|
+
var fs = require('fs');
|
799
|
+
var data;
|
800
|
+
// TODO: Support async source.
|
842
801
|
|
843
|
-
|
844
|
-
|
845
|
-
|
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(
|
850
|
-
return getCertData(
|
806
|
+
_.map(sources, function (_source) {
|
807
|
+
return getCertData(_source, unbundle);
|
851
808
|
})
|
852
809
|
);
|
853
|
-
} else if (
|
854
|
-
|
855
|
-
|
856
|
-
|
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(
|
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(
|
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(
|
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 =
|
867
|
+
link = 'http://' + link;
|
909
868
|
}
|
910
869
|
return link;
|
911
870
|
}
|