@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.
- package/LICENSE +24 -0
- package/README.md +35 -0
- package/changes.json +26 -0
- package/dist/docker.d.ts +30 -0
- package/dist/docker.js +136 -0
- package/dist/docker.js.map +1 -0
- package/dist/etcd-backend.d.ts +12 -0
- package/dist/etcd-backend.js +80 -0
- package/dist/etcd-backend.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces/index.d.ts +2 -0
- package/dist/interfaces/index.js +3 -0
- package/dist/interfaces/index.js.map +1 -0
- package/dist/interfaces/proxy-options.d.ts +36 -0
- package/dist/interfaces/proxy-options.js +2 -0
- package/dist/interfaces/proxy-options.js.map +1 -0
- package/dist/interfaces/proxy-route.d.ts +14 -0
- package/dist/interfaces/proxy-route.js +2 -0
- package/dist/interfaces/proxy-route.js.map +1 -0
- package/dist/interfaces/proxy-target-url.d.ts +8 -0
- package/dist/interfaces/proxy-target-url.js +2 -0
- package/dist/interfaces/proxy-target-url.js.map +1 -0
- package/dist/interfaces/resolver.d.ts +12 -0
- package/dist/interfaces/resolver.js +2 -0
- package/dist/interfaces/resolver.js.map +1 -0
- package/dist/interfaces/route-options.d.ts +15 -0
- package/dist/interfaces/route-options.js +2 -0
- package/dist/interfaces/route-options.js.map +1 -0
- package/dist/letsencrypt.d.ts +19 -0
- package/dist/letsencrypt.js +127 -0
- package/dist/letsencrypt.js.map +1 -0
- package/dist/proxy.d.ts +122 -0
- package/dist/proxy.js +812 -0
- package/dist/proxy.js.map +1 -0
- package/dist/third-party/le-challenge-fs.d.ts +8 -0
- package/dist/third-party/le-challenge-fs.js +140 -0
- package/dist/third-party/le-challenge-fs.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- 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
|