@depup/redbird 1.0.2-depup.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 (41) hide show
  1. package/LICENSE +24 -0
  2. package/README.md +35 -0
  3. package/changes.json +26 -0
  4. package/dist/docker.d.ts +30 -0
  5. package/dist/docker.js +136 -0
  6. package/dist/docker.js.map +1 -0
  7. package/dist/etcd-backend.d.ts +12 -0
  8. package/dist/etcd-backend.js +80 -0
  9. package/dist/etcd-backend.js.map +1 -0
  10. package/dist/index.d.ts +4 -0
  11. package/dist/index.js +7 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/interfaces/index.d.ts +2 -0
  14. package/dist/interfaces/index.js +3 -0
  15. package/dist/interfaces/index.js.map +1 -0
  16. package/dist/interfaces/proxy-options.d.ts +36 -0
  17. package/dist/interfaces/proxy-options.js +2 -0
  18. package/dist/interfaces/proxy-options.js.map +1 -0
  19. package/dist/interfaces/proxy-route.d.ts +14 -0
  20. package/dist/interfaces/proxy-route.js +2 -0
  21. package/dist/interfaces/proxy-route.js.map +1 -0
  22. package/dist/interfaces/proxy-target-url.d.ts +8 -0
  23. package/dist/interfaces/proxy-target-url.js +2 -0
  24. package/dist/interfaces/proxy-target-url.js.map +1 -0
  25. package/dist/interfaces/resolver.d.ts +12 -0
  26. package/dist/interfaces/resolver.js +2 -0
  27. package/dist/interfaces/resolver.js.map +1 -0
  28. package/dist/interfaces/route-options.d.ts +15 -0
  29. package/dist/interfaces/route-options.js +2 -0
  30. package/dist/interfaces/route-options.js.map +1 -0
  31. package/dist/letsencrypt.d.ts +19 -0
  32. package/dist/letsencrypt.js +127 -0
  33. package/dist/letsencrypt.js.map +1 -0
  34. package/dist/proxy.d.ts +122 -0
  35. package/dist/proxy.js +812 -0
  36. package/dist/proxy.js.map +1 -0
  37. package/dist/third-party/le-challenge-fs.d.ts +8 -0
  38. package/dist/third-party/le-challenge-fs.js +140 -0
  39. package/dist/third-party/le-challenge-fs.js.map +1 -0
  40. package/dist/tsconfig.tsbuildinfo +1 -0
  41. package/package.json +143 -0
package/dist/proxy.js ADDED
@@ -0,0 +1,812 @@
1
+ /*eslint-env node */
2
+ 'use strict';
3
+ // Built-in NodeJS modules.
4
+ import path from 'path';
5
+ import { parse as parseUrl } from 'url';
6
+ import cluster from 'cluster';
7
+ import http, { Agent, ServerResponse } from 'http';
8
+ import https from 'https';
9
+ import http2 from 'http2';
10
+ import fs from 'fs';
11
+ import tls from 'tls';
12
+ // Third party modules.
13
+ import validUrl from 'valid-url';
14
+ import httpProxy from 'http-proxy';
15
+ import lodash from 'lodash';
16
+ import { pino } from 'pino';
17
+ import hash from 'object-hash';
18
+ import safe from 'safe-timers';
19
+ import { LRUCache } from 'lru-cache';
20
+ // Custom modules.
21
+ import * as letsencrypt from './letsencrypt.js';
22
+ const { isFunction, isObject, sortBy, uniq, remove, isString } = lodash;
23
+ const routeCache = new LRUCache({ max: 5000 });
24
+ const defaultLetsencryptPort = 3000;
25
+ const ONE_DAY = 60 * 60 * 24 * 1000;
26
+ const ONE_MONTH = ONE_DAY * 30;
27
+ export class Redbird {
28
+ get defaultResolver() {
29
+ return this._defaultResolver;
30
+ }
31
+ constructor(opts = {}) {
32
+ var _a;
33
+ this.opts = opts;
34
+ this.routing = {};
35
+ this.resolvers = [];
36
+ this.lazyCerts = {};
37
+ if (this.opts.httpProxy == undefined) {
38
+ this.opts.httpProxy = {};
39
+ }
40
+ if (opts.logger) {
41
+ this.logger = pino(opts.logger || {
42
+ name: 'redbird',
43
+ });
44
+ }
45
+ this._defaultResolver = {
46
+ fn: (host, url) => {
47
+ // Given a src resolve it to a target route if any available.
48
+ if (!host) {
49
+ return;
50
+ }
51
+ url = url || '/';
52
+ const routes = this.routing[host];
53
+ let i = 0;
54
+ if (routes) {
55
+ const len = routes.length;
56
+ //
57
+ // Find path that matches the start of req.url
58
+ //
59
+ for (i = 0; i < len; i++) {
60
+ const route = routes[i];
61
+ if (route.path === '/' || startsWith(url, route.path)) {
62
+ return route;
63
+ }
64
+ }
65
+ }
66
+ },
67
+ priority: 0,
68
+ };
69
+ if ((opts.cluster && typeof opts.cluster !== 'number') || opts.cluster > 32) {
70
+ throw Error('cluster setting must be an integer less than 32');
71
+ }
72
+ if (opts.cluster && cluster.isPrimary) {
73
+ for (let i = 0; i < opts.cluster; i++) {
74
+ cluster.fork();
75
+ }
76
+ cluster.on('exit', (worker, code, signal) => {
77
+ var _a;
78
+ // Fork if a worker dies.
79
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error({
80
+ code: code,
81
+ signal: signal,
82
+ }, 'worker died un-expectedly... restarting it.');
83
+ cluster.fork();
84
+ });
85
+ }
86
+ else {
87
+ this.resolvers = [this._defaultResolver];
88
+ opts.port = opts.port || 8080;
89
+ if (opts.letsencrypt) {
90
+ this.setupLetsencrypt(opts);
91
+ }
92
+ if (opts.resolvers) {
93
+ for (let i = 0; i < opts.resolvers.length; i++) {
94
+ this.addResolver(opts.resolvers[i].fn, opts.resolvers[i].priority);
95
+ }
96
+ }
97
+ const websocketsUpgrade = async (req, socket, head) => {
98
+ var _a;
99
+ socket.on('error', (err) => {
100
+ var _a;
101
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(err, 'WebSockets error');
102
+ });
103
+ const src = this.getSource(req);
104
+ const target = await this.getTarget(src, req);
105
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.info({ headers: req.headers, target: target }, 'upgrade to websockets');
106
+ if (target) {
107
+ if (target.useTargetHostHeader === true) {
108
+ req.headers.host = target.host;
109
+ }
110
+ proxy.ws(req, socket, head, { target });
111
+ }
112
+ else {
113
+ respondNotFound(req, socket);
114
+ }
115
+ };
116
+ //
117
+ // Create a proxy server with custom application logic
118
+ //
119
+ let agent;
120
+ if (opts.keepAlive) {
121
+ agent = this.agent = new Agent({
122
+ keepAlive: true,
123
+ });
124
+ }
125
+ const proxy = (this.proxy = httpProxy.createProxyServer({
126
+ xfwd: opts.xfwd != false,
127
+ prependPath: false,
128
+ secure: opts.secure !== false,
129
+ timeout: opts.timeout,
130
+ proxyTimeout: opts.proxyTimeout,
131
+ agent,
132
+ }));
133
+ proxy.on('proxyReq', (proxyReq, req, res, options) => {
134
+ // According to typescript this is the correct way to access the host header
135
+ // const host = req.headers.host;
136
+ const host = req['host'];
137
+ if (host != null) {
138
+ proxyReq.setHeader('host', host);
139
+ }
140
+ });
141
+ //
142
+ // Support NTLM auth
143
+ //
144
+ if (opts.ntlm) {
145
+ proxy.on('proxyRes', (proxyRes, req, res) => {
146
+ const key = 'www-authenticate';
147
+ proxyRes.headers[key] =
148
+ proxyRes.headers[key] && proxyRes.headers[key].split(',');
149
+ });
150
+ }
151
+ //
152
+ // Optionally create an https proxy server.
153
+ //
154
+ if (opts.ssl) {
155
+ if (Array.isArray(opts.ssl)) {
156
+ opts.ssl.forEach((sslOpts) => {
157
+ this.setupHttpsProxy(proxy, websocketsUpgrade, sslOpts);
158
+ });
159
+ }
160
+ else {
161
+ this.setupHttpsProxy(proxy, websocketsUpgrade, opts.ssl);
162
+ }
163
+ }
164
+ //
165
+ // Plain HTTP Proxy
166
+ //
167
+ const server = (this.server = this.setupHttpProxy(proxy, websocketsUpgrade, this.logger, opts));
168
+ server.listen(opts.port, opts.host);
169
+ const handleProxyError = (err, req, resOrSocket, target) => {
170
+ var _a, _b;
171
+ const res = resOrSocket instanceof ServerResponse ? resOrSocket : null;
172
+ //
173
+ // Send a 500 http status if headers have been sent
174
+ //
175
+ if (!res || !res.writeHead) {
176
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(err, 'Proxy Error');
177
+ return;
178
+ }
179
+ else {
180
+ if (err.code === 'ECONNREFUSED') {
181
+ res.writeHead(502);
182
+ }
183
+ else if (!res.headersSent) {
184
+ res.writeHead(500, err.message, { 'content-type': 'text/plain' });
185
+ }
186
+ }
187
+ //
188
+ // Do not log this common error
189
+ //
190
+ if (err.message !== 'socket hang up') {
191
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.error(err, 'Proxy Error');
192
+ }
193
+ //
194
+ // TODO: if err.code=ECONNREFUSED and there are more servers
195
+ // for this route, try another one.
196
+ //
197
+ res.end(err.code);
198
+ };
199
+ if (opts.errorHandler && isFunction(opts.errorHandler)) {
200
+ proxy.on('error', opts.errorHandler);
201
+ }
202
+ else {
203
+ proxy.on('error', handleProxyError);
204
+ }
205
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.info('Started a Redbird reverse proxy server on port %s', opts.port);
206
+ }
207
+ }
208
+ setupHttpProxy(proxy, websocketsUpgrade, log, opts) {
209
+ const httpServerModule = opts.serverModule || http;
210
+ const server = (this.server = httpServerModule.createServer(async (req, res) => {
211
+ const src = this.getSource(req);
212
+ const target = await this.getTarget(src, req, res);
213
+ if (target) {
214
+ if (this.shouldRedirectToHttps(target)) {
215
+ redirectToHttps(req, res, this.opts.ssl, this.logger);
216
+ }
217
+ else {
218
+ proxy.web(req, res, { target, secure: !!opts.secure });
219
+ }
220
+ }
221
+ else {
222
+ respondNotFound(req, res);
223
+ }
224
+ }));
225
+ //
226
+ // Listen to the `upgrade` event and proxy the
227
+ // WebSocket requests as well.
228
+ //
229
+ server.on('upgrade', websocketsUpgrade);
230
+ server.on('error', function (err) {
231
+ log && log.error(err, 'Server Error');
232
+ });
233
+ return server;
234
+ }
235
+ /**
236
+ * Special resolver for handling Let's Encrypt ACME challenges.
237
+ * @param opts
238
+ */
239
+ setupLetsencrypt(opts) {
240
+ if (!opts.letsencrypt.path) {
241
+ throw Error('Missing certificate path for Lets Encrypt');
242
+ }
243
+ const letsencryptPort = opts.letsencrypt.port || defaultLetsencryptPort;
244
+ this.letsencryptServer = letsencrypt.init(opts.letsencrypt.path, letsencryptPort, this.logger);
245
+ this.letsencryptHost = '127.0.0.1:' + letsencryptPort;
246
+ const targetHost = 'http://' + this.letsencryptHost;
247
+ const challengeResolver = (host, url) => {
248
+ if (/^\/.well-known\/acme-challenge/.test(url)) {
249
+ return `${targetHost}/${host}`;
250
+ }
251
+ };
252
+ this.addResolver(challengeResolver, 9999);
253
+ }
254
+ setupHttpsProxy(proxy, websocketsUpgrade, sslOpts) {
255
+ var _a;
256
+ let httpsModule;
257
+ this.certs = this.certs || {};
258
+ const certs = this.certs;
259
+ let ssl = {
260
+ SNICallback: async (hostname, cb) => {
261
+ var _a, _b, _c, _d, _e, _f, _g;
262
+ if (!certs[hostname]) {
263
+ if (!((_b = (_a = this.opts) === null || _a === void 0 ? void 0 : _a.letsencrypt) === null || _b === void 0 ? void 0 : _b.path)) {
264
+ console.error('Missing certificate path for Lets Encrypt');
265
+ return cb(new Error('No certs for hostname ' + hostname));
266
+ }
267
+ if (!this.lazyCerts[hostname]) {
268
+ // Check if we have a resolver that matches the hostname and has letsencrypt enabled
269
+ const results = await this.applyResolvers(this.resolvers, hostname, '', null);
270
+ const route = results.find((route) => { var _a, _b; return (_b = (_a = route === null || route === void 0 ? void 0 : route.opts) === null || _a === void 0 ? void 0 : _a.ssl) === null || _b === void 0 ? void 0 : _b.letsencrypt; });
271
+ const sslOpts = (_c = route === null || route === void 0 ? void 0 : route.opts) === null || _c === void 0 ? void 0 : _c.ssl;
272
+ if (route && sslOpts) {
273
+ this.lazyCerts[hostname] = {
274
+ email: (_d = sslOpts.letsencrypt) === null || _d === void 0 ? void 0 : _d.email,
275
+ production: (_e = sslOpts.letsencrypt) === null || _e === void 0 ? void 0 : _e.production,
276
+ renewWithin: ((_g = (_f = this.opts) === null || _f === void 0 ? void 0 : _f.letsencrypt) === null || _g === void 0 ? void 0 : _g.renewWithin) || ONE_MONTH,
277
+ };
278
+ }
279
+ else {
280
+ return cb(new Error('No certs for hostname ' + hostname));
281
+ }
282
+ }
283
+ try {
284
+ await this.updateCertificates(hostname, this.lazyCerts[hostname].email, this.lazyCerts[hostname].production, this.lazyCerts[hostname].renewWithin);
285
+ }
286
+ catch (err) {
287
+ console.error('Error getting LetsEncrypt certificates', err);
288
+ return cb(err);
289
+ }
290
+ }
291
+ else if (!certs[hostname]) {
292
+ return cb(new Error('No certs for hostname ' + hostname));
293
+ }
294
+ if (cb) {
295
+ cb(null, certs[hostname]);
296
+ }
297
+ },
298
+ //
299
+ // Default certs for clients that do not support SNI.
300
+ //
301
+ key: getCertData(sslOpts.key),
302
+ cert: getCertData(sslOpts.cert),
303
+ };
304
+ // Allows the option to disable older SSL/TLS versions
305
+ if (sslOpts.secureOptions) {
306
+ ssl.secureOptions = sslOpts.secureOptions;
307
+ }
308
+ if (sslOpts.ca) {
309
+ ssl.ca = getCertData(sslOpts.ca, true);
310
+ }
311
+ if (sslOpts.opts) {
312
+ ssl = Object.assign(Object.assign({}, sslOpts.opts), ssl);
313
+ }
314
+ if (sslOpts.http2) {
315
+ httpsModule = sslOpts.serverModule || {
316
+ createServer: (sslOpts, cb) => http2.createSecureServer(sslOpts, cb),
317
+ };
318
+ }
319
+ else {
320
+ httpsModule = sslOpts.serverModule || https;
321
+ }
322
+ const httpsServer = (this.httpsServer = httpsModule.createServer(ssl, async (req, res) => {
323
+ const src = this.getSource(req);
324
+ const httpProxyOpts = Object.assign({}, this.opts.httpProxy);
325
+ const target = await this.getTarget(src, req, res);
326
+ if (target) {
327
+ httpProxyOpts.target = target;
328
+ proxy.web(req, res, httpProxyOpts);
329
+ }
330
+ else {
331
+ respondNotFound(req, res);
332
+ }
333
+ }));
334
+ httpsServer.on('upgrade', websocketsUpgrade);
335
+ httpsServer.on('error', (err) => {
336
+ var _a;
337
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(err, 'HTTPS Server Error');
338
+ });
339
+ httpsServer.on('clientError', (err) => {
340
+ var _a;
341
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(err, 'HTTPS Client Error');
342
+ });
343
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.info('Listening to HTTPS requests on port %s', sslOpts.port);
344
+ httpsServer.listen(sslOpts.port, sslOpts.ip);
345
+ }
346
+ addResolver(resolverFn, priority) {
347
+ if (this.opts.cluster && cluster.isPrimary) {
348
+ return this;
349
+ }
350
+ // Check if the resolver is already added if so just update its priority
351
+ let found = false;
352
+ for (let i = 0; i < this.resolvers.length; i++) {
353
+ if (this.resolvers[i].fn === resolverFn) {
354
+ this.resolvers[i].priority = priority || 0;
355
+ found = true;
356
+ break;
357
+ }
358
+ }
359
+ if (!found) {
360
+ this.resolvers.push({
361
+ fn: resolverFn,
362
+ priority: priority || 0,
363
+ });
364
+ }
365
+ this.resolvers = sortBy(uniq(this.resolvers), 'priority').reverse();
366
+ }
367
+ removeResolver(resolverFn) {
368
+ if (this.opts.cluster && cluster.isPrimary) {
369
+ return this;
370
+ }
371
+ // Since unique resolvers are not checked for performance,
372
+ // just remove every existence.
373
+ this.resolvers = this.resolvers.filter(function (resolver) {
374
+ return resolverFn !== resolver.fn;
375
+ });
376
+ }
377
+ async register(src, target, opts) {
378
+ var _a, _b, _c;
379
+ if (this.opts.cluster && cluster.isPrimary) {
380
+ return;
381
+ }
382
+ // allow registering with src or target as an object to pass in
383
+ // options specific to each one.
384
+ if (src && src.src) {
385
+ target = src.target;
386
+ opts = src;
387
+ src = src.src;
388
+ }
389
+ else if (target && target.target) {
390
+ opts = target;
391
+ target = target.target;
392
+ }
393
+ if (!src || !target) {
394
+ throw Error('Cannot register a new route with unspecified src or target');
395
+ }
396
+ const routing = this.routing;
397
+ src = prepareUrl(src);
398
+ if (opts) {
399
+ const ssl = opts.ssl;
400
+ if (ssl) {
401
+ if (!this.httpsServer) {
402
+ throw Error('Cannot register https routes without defining a ssl port');
403
+ }
404
+ if (!this.certs[src.hostname]) {
405
+ if (ssl.key || ssl.cert || ssl.ca) {
406
+ this.certs[src.hostname] = createCredentialContext(ssl.key, ssl.cert, ssl.ca);
407
+ }
408
+ else if (ssl.letsencrypt) {
409
+ if (!this.opts.letsencrypt || !this.opts.letsencrypt.path) {
410
+ console.error('Missing certificate path for Lets Encrypt');
411
+ return;
412
+ }
413
+ if (!ssl.letsencrypt.lazy) {
414
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.info('Getting Lets Encrypt certificates for %s', src.hostname);
415
+ await this.updateCertificates(src.hostname, ssl.letsencrypt.email, ssl.letsencrypt.production, this.opts.letsencrypt.renewWithin || ONE_MONTH);
416
+ }
417
+ else {
418
+ // We need to store the letsencrypt options for this domain somewhere
419
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.info('Lazy loading Lets Encrypt certificates for %s', src.hostname);
420
+ this.lazyCerts[src.hostname] = Object.assign(Object.assign({}, ssl.letsencrypt), { renewWithin: this.opts.letsencrypt.renewWithin || ONE_MONTH });
421
+ }
422
+ }
423
+ else {
424
+ // Trigger the use of the default certificates.
425
+ this.certs[src.hostname] = void 0;
426
+ }
427
+ }
428
+ }
429
+ }
430
+ target = buildTarget(target, opts);
431
+ const hostname = src.hostname;
432
+ const host = (routing[hostname] = routing[hostname] || []);
433
+ const pathname = src.pathname || '/';
434
+ let route = host.find((route) => route.path === pathname);
435
+ if (!route) {
436
+ route = {
437
+ path: pathname,
438
+ rr: 0,
439
+ urls: [],
440
+ opts: Object.assign({}, opts),
441
+ };
442
+ host.push(route);
443
+ //
444
+ // Sort routes
445
+ //
446
+ routing[src.hostname] = sortBy(host, function (_route) {
447
+ return -_route.path.length;
448
+ });
449
+ }
450
+ route.urls.push(target);
451
+ (_c = this.logger) === null || _c === void 0 ? void 0 : _c.info({ from: src, to: target }, 'Registered a new route');
452
+ }
453
+ async updateCertificates(domain, email, production, renewWithin, renew) {
454
+ var _a, _b, _c;
455
+ try {
456
+ const certs = await letsencrypt.getCertificates(domain, email, (_a = this.opts.letsencrypt) === null || _a === void 0 ? void 0 : _a.port, production, renew, this.logger);
457
+ if (certs) {
458
+ const opts = {
459
+ key: certs.privkey,
460
+ cert: certs.cert + certs.chain,
461
+ };
462
+ this.certs[domain] = tls.createSecureContext(opts).context;
463
+ //
464
+ // TODO: cluster friendly
465
+ //
466
+ let renewTime = certs.expiresAt - Date.now() - renewWithin;
467
+ renewTime =
468
+ renewTime > 0 ? renewTime : this.opts.letsencrypt.minRenewTime || 60 * 60 * 1000;
469
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.info('Renewal of %s in %s days', domain, Math.floor(renewTime / ONE_DAY));
470
+ const renewCertificate = () => {
471
+ var _a;
472
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.info('Renewing letscrypt certificates for %s', domain);
473
+ this.updateCertificates(domain, email, production, renewWithin, true);
474
+ };
475
+ this.certs[domain].renewalTimeout = safe.setTimeout(renewCertificate, renewTime);
476
+ }
477
+ else {
478
+ //
479
+ // TODO: Try again, but we need an exponential backof to avoid getting banned.
480
+ //
481
+ (_c = this.logger) === null || _c === void 0 ? void 0 : _c.info('Could not get any certs for %s', domain);
482
+ }
483
+ }
484
+ catch (err) {
485
+ console.error('Error getting LetsEncrypt certificates', err);
486
+ }
487
+ }
488
+ unregister(src, target) {
489
+ var _a;
490
+ if (this.opts.cluster && cluster.isPrimary) {
491
+ return this;
492
+ }
493
+ if (!src) {
494
+ return this;
495
+ }
496
+ const srcURL = prepareUrl(src);
497
+ const routes = this.routing[srcURL.hostname] || [];
498
+ const pathname = srcURL.pathname || '/';
499
+ let i;
500
+ for (i = 0; i < routes.length; i++) {
501
+ if (routes[i].path === pathname) {
502
+ break;
503
+ }
504
+ }
505
+ if (i < routes.length) {
506
+ const route = routes[i];
507
+ if (target) {
508
+ const targetURL = prepareUrl(target);
509
+ remove(route.urls, (url) => {
510
+ return url.href === targetURL.href;
511
+ });
512
+ }
513
+ else {
514
+ route.urls = [];
515
+ }
516
+ if (route.urls.length === 0) {
517
+ routes.splice(i, 1);
518
+ const certs = this.certs;
519
+ if (certs) {
520
+ if (certs[srcURL.hostname] && certs[srcURL.hostname].renewalTimeout) {
521
+ safe.clearTimeout(certs[srcURL.hostname].renewalTimeout);
522
+ }
523
+ delete certs[srcURL.hostname];
524
+ }
525
+ }
526
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.info({ from: src, to: target }, 'Unregistered a route');
527
+ }
528
+ return this;
529
+ }
530
+ applyResolvers(resolvers, host, url, req) {
531
+ return Promise.all(resolvers.map((resolver) => resolver.fn(host, url, req)));
532
+ }
533
+ /**
534
+ * Resolves to route
535
+ * @param host
536
+ * @param url
537
+ * @returns {*}
538
+ */
539
+ async resolve(host, url, req) {
540
+ try {
541
+ host = host.toLowerCase();
542
+ const resolverResults = await this.applyResolvers(this.resolvers, host, url, req);
543
+ for (let i = 0; i < resolverResults.length; i++) {
544
+ const route = resolverResults[i];
545
+ if (route) {
546
+ const builtRoute = buildRoute(route);
547
+ if (builtRoute) {
548
+ // ensure resolved route has path that prefixes URL
549
+ // no need to check for native routes.
550
+ if (!builtRoute.isResolved ||
551
+ builtRoute.path === '/' ||
552
+ startsWith(url, builtRoute.path)) {
553
+ return builtRoute;
554
+ }
555
+ }
556
+ }
557
+ }
558
+ }
559
+ catch (err) {
560
+ console.error('Resolvers error:', err);
561
+ }
562
+ }
563
+ async getTarget(src, req, res) {
564
+ var _a, _b, _c, _d;
565
+ const url = req.url;
566
+ const route = await this.resolve(src, url, req);
567
+ if (!route) {
568
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn({ src: src, url: url }, 'no valid route found for given source');
569
+ return;
570
+ }
571
+ const pathname = route.path;
572
+ if (pathname.length > 1) {
573
+ //
574
+ // remove prefix from src
575
+ //
576
+ req._url = url; // save original url (hacky but works quite well)
577
+ req.url = url.substr(pathname.length) || '';
578
+ }
579
+ //
580
+ // Perform Round-Robin on the available targets
581
+ // TODO: if target errors with EHOSTUNREACH we should skip this
582
+ // target and try with another.
583
+ //
584
+ const urls = route.urls;
585
+ const j = route.rr;
586
+ route.rr = (j + 1) % urls.length; // get and update Round-robin index.
587
+ const target = route.urls[j];
588
+ //
589
+ // Fix request url if targetname specified.
590
+ //
591
+ if (target.pathname) {
592
+ if (req.url) {
593
+ req.url = path.posix.join(target.pathname, req.url);
594
+ }
595
+ else {
596
+ req.url = target.pathname;
597
+ }
598
+ }
599
+ //
600
+ // Host headers are passed through from the source by default
601
+ // Often we want to use the host header of the target instead
602
+ //
603
+ if (target.useTargetHostHeader === true) {
604
+ req.host = target.host;
605
+ }
606
+ if ((_b = route.opts) === null || _b === void 0 ? void 0 : _b.onRequest) {
607
+ const resultFromRequestHandler = route.opts.onRequest(req, res, target);
608
+ if (resultFromRequestHandler !== undefined) {
609
+ (_c = this.logger) === null || _c === void 0 ? void 0 : _c.info('Proxying %s received result from onRequest handler, returning.', src + url);
610
+ return resultFromRequestHandler;
611
+ }
612
+ }
613
+ (_d = this.logger) === null || _d === void 0 ? void 0 : _d.info('Proxying %s to %s', src + url, path.posix.join(target.host, req.url));
614
+ return target;
615
+ }
616
+ getSource(req) {
617
+ if (this.opts.preferForwardedHost && req.headers['x-forwarded-host']) {
618
+ return req.headers['x-forwarded-host'].split(':')[0];
619
+ }
620
+ if (req.headers.host) {
621
+ return req.headers.host.split(':')[0];
622
+ }
623
+ }
624
+ async close() {
625
+ var _a;
626
+ this.proxy.close();
627
+ this.agent && this.agent.destroy();
628
+ // Clear any renewal timers
629
+ if (this.certs) {
630
+ Object.keys(this.certs).forEach((domain) => {
631
+ const cert = this.certs[domain];
632
+ if (cert && cert.renewalTimeout) {
633
+ safe.clearTimeout(cert.renewalTimeout);
634
+ cert.renewalTimeout = null;
635
+ }
636
+ });
637
+ }
638
+ (_a = this.letsencryptServer) === null || _a === void 0 ? void 0 : _a.close();
639
+ await Promise.all([this.server, this.httpsServer]
640
+ .filter((s) => s)
641
+ .map((server) => new Promise((resolve) => server.close(resolve))));
642
+ }
643
+ //
644
+ // Helpers
645
+ //
646
+ /**
647
+ Routing table structure. An object with hostname as key, and an array as value.
648
+ The array has one element per path associated to the given hostname.
649
+ Every path has a Round-Robin value (rr) and urls array, with all the urls available
650
+ for this target route.
651
+
652
+ {
653
+ hostA :
654
+ [
655
+ {
656
+ path: '/',
657
+ rr: 3,
658
+ urls: []
659
+ }
660
+ ]
661
+ }
662
+ */
663
+ notFound(callback) {
664
+ if (typeof callback == 'function') {
665
+ respondNotFound = callback;
666
+ }
667
+ else {
668
+ throw Error('notFound callback is not a function');
669
+ }
670
+ }
671
+ shouldRedirectToHttps(target) {
672
+ return target.sslRedirect && target.host != this.letsencryptHost;
673
+ }
674
+ }
675
+ //
676
+ // Redirect to the HTTPS proxy
677
+ //
678
+ function redirectToHttps(req, res, ssl, log) {
679
+ req.url = req._url || req.url; // Get the original url since we are going to redirect.
680
+ const targetPort = ssl.redirectPort || ssl.port;
681
+ const hostname = req.headers.host.split(':')[0] + (targetPort ? ':' + targetPort : '');
682
+ const url = 'https://' + path.posix.join(hostname, req.url);
683
+ log && log.info('Redirecting %s to %s', path.posix.join(req.headers.host, req.url), url);
684
+ //
685
+ // We can use 301 for permanent redirect, but its bad for debugging, we may have it as
686
+ // a configurable option.
687
+ //
688
+ res.writeHead(302, { Location: url });
689
+ res.end();
690
+ }
691
+ function prepareUrl(url) {
692
+ if (isString(url)) {
693
+ url = setHttp(url);
694
+ if (!validUrl.isHttpUri(url) && !validUrl.isHttpsUri(url)) {
695
+ throw Error(`uri is not a valid http uri ${url}`);
696
+ }
697
+ return parseUrl(url);
698
+ }
699
+ return url;
700
+ }
701
+ function getCertData(source, unbundle) {
702
+ let data;
703
+ // Handle different source types
704
+ if (source) {
705
+ if (Array.isArray(source)) {
706
+ // Recursively process each item in the array and flatten the result
707
+ const sources = source;
708
+ return sources.map((src) => getCertData(src, unbundle)).flat();
709
+ }
710
+ else if (Buffer.isBuffer(source)) {
711
+ // If source is a buffer, convert to string
712
+ data = source.toString('utf8');
713
+ }
714
+ else if (fs.existsSync(source)) {
715
+ // If source is a file path, read the file content
716
+ data = fs.readFileSync(source, 'utf8');
717
+ }
718
+ }
719
+ // Return unbundled certificate data if required, or raw data
720
+ if (data) {
721
+ return unbundle ? unbundleCert(data) : data;
722
+ }
723
+ return null; // Return null if no valid data is found
724
+ }
725
+ /**
726
+ Unbundles a file composed of several certificates.
727
+ http://www.benjiegillam.com/2012/06/node-dot-js-ssl-certificate-chain/
728
+ */
729
+ function unbundleCert(bundle) {
730
+ const chain = bundle.trim().split('\n');
731
+ const ca = [];
732
+ const cert = [];
733
+ for (let i = 0, len = chain.length; i < len; i++) {
734
+ const line = chain[i].trim();
735
+ if (!(line.length !== 0)) {
736
+ continue;
737
+ }
738
+ cert.push(line);
739
+ if (line.match(/-END CERTIFICATE-/)) {
740
+ const joined = cert.join('\n');
741
+ ca.push(joined);
742
+ //cert = [];
743
+ cert.length = 0;
744
+ }
745
+ }
746
+ return ca;
747
+ }
748
+ function createCredentialContext(key, cert, ca) {
749
+ const opts = {};
750
+ opts.key = getCertData(key);
751
+ opts.cert = getCertData(cert);
752
+ if (ca) {
753
+ opts.ca = getCertData(ca, true);
754
+ }
755
+ const credentials = tls.createSecureContext(opts);
756
+ return credentials.context;
757
+ }
758
+ //
759
+ // https://stackoverflow.com/questions/18052919/javascript-regular-expression-to-add-protocol-to-url-string/18053700#18053700
760
+ // Adds http protocol if non specified.
761
+ function setHttp(link) {
762
+ if (link.search(/^http[s]?\:\/\//) === -1) {
763
+ link = 'http://' + link;
764
+ }
765
+ return link;
766
+ }
767
+ let respondNotFound = function (req, res) {
768
+ if (res instanceof ServerResponse) {
769
+ res.statusCode = 404;
770
+ }
771
+ res.write('Not Found');
772
+ res.end();
773
+ };
774
+ export const buildRoute = function (route) {
775
+ if (!isString(route) && !isObject(route)) {
776
+ return null;
777
+ }
778
+ if (isObject(route) && route.hasOwnProperty('urls') && route.hasOwnProperty('path')) {
779
+ // default route type matched.
780
+ return route;
781
+ }
782
+ const cacheKey = isString(route) ? route : hash(route);
783
+ const entry = routeCache.get(cacheKey);
784
+ if (entry) {
785
+ return entry;
786
+ }
787
+ const routeObject = { rr: 0, isResolved: true };
788
+ if (isString(route)) {
789
+ routeObject.urls = [buildTarget(route)];
790
+ routeObject.path = '/';
791
+ }
792
+ else {
793
+ if (!route.hasOwnProperty('url')) {
794
+ return null;
795
+ }
796
+ routeObject.urls = (Array.isArray(route.url) ? route.url : [route.url]).map(function (url) {
797
+ return buildTarget(url, route.opts || {});
798
+ });
799
+ routeObject.path = route.path || '/';
800
+ }
801
+ routeCache.set(cacheKey, routeObject);
802
+ return routeObject;
803
+ };
804
+ export const buildTarget = function (target, opts) {
805
+ opts = opts || {};
806
+ const targetURL = prepareUrl(target);
807
+ return Object.assign(Object.assign({}, targetURL), { sslRedirect: opts.ssl && opts.ssl.redirect !== false, useTargetHostHeader: opts.useTargetHostHeader === true });
808
+ };
809
+ function startsWith(input, str) {
810
+ return (input.slice(0, str.length) === str && (input.length === str.length || input[str.length] === '/'));
811
+ }
812
+ //# sourceMappingURL=proxy.js.map