@abtnode/router-provider 1.6.21 → 1.6.24
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 +1 -1
- package/lib/base.js +14 -0
- package/lib/default/daemon.js +316 -0
- package/lib/default/index.js +221 -0
- package/lib/default/proxy.js +567 -0
- package/lib/index.js +19 -19
- package/lib/nginx/includes/params +1 -1
- package/lib/nginx/index.js +115 -179
- package/lib/nginx/util.js +15 -56
- package/lib/util.js +165 -0
- package/package.json +17 -8
- package/lib/none/index.js +0 -50
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/* eslint-disable no-param-reassign */
|
|
2
|
+
/* eslint-disable consistent-return */
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const httpProxy = require('http-proxy');
|
|
6
|
+
const validUrl = require('valid-url');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const _ = require('lodash');
|
|
9
|
+
const hash = require('object-hash');
|
|
10
|
+
const LRUCache = require('lru-cache');
|
|
11
|
+
const tls = require('tls');
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line global-require
|
|
14
|
+
const logger = require('@abtnode/logger')('router:default:proxy', { filename: 'engine' });
|
|
15
|
+
|
|
16
|
+
const ensureHttp = (str) => (!str.startsWith('http://') && !str.startsWith('https://') ? `http://${str}` : str);
|
|
17
|
+
const prepareUrl = (url) => {
|
|
18
|
+
url = _.clone(url);
|
|
19
|
+
if (_.isString(url)) {
|
|
20
|
+
url = ensureHttp(url);
|
|
21
|
+
if (!validUrl.isHttpUri(url) && !validUrl.isHttpsUri(url)) {
|
|
22
|
+
throw new Error(`uri is not a valid http uri ${url}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
url = new URL(url);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return url;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const routeCache = new LRUCache({ max: 5000 });
|
|
32
|
+
|
|
33
|
+
module.exports = class ReverseProxy {
|
|
34
|
+
opts = {};
|
|
35
|
+
|
|
36
|
+
constructor(opts = {}) {
|
|
37
|
+
this._defaultResolver.priority = 0;
|
|
38
|
+
|
|
39
|
+
this.opts = opts;
|
|
40
|
+
|
|
41
|
+
if (!this.opts.httpProxy) {
|
|
42
|
+
this.opts.httpProxy = {};
|
|
43
|
+
}
|
|
44
|
+
if (!this.opts.headers) {
|
|
45
|
+
this.opts.headers = [];
|
|
46
|
+
}
|
|
47
|
+
if (!this.opts.onError) {
|
|
48
|
+
this.opts.onError = (err) => console.error(err);
|
|
49
|
+
}
|
|
50
|
+
if (!this.opts.corsHandler) {
|
|
51
|
+
this.opts.corsHandler = () => true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const websocketUpgrade = (req, socket, head) => {
|
|
55
|
+
socket.on('error', (err) => logger.error('WebSockets error', { error: err }));
|
|
56
|
+
const src = this._getSource(req);
|
|
57
|
+
this._getTarget(src, req).then((target) => {
|
|
58
|
+
logger.info('upgrade to websocket', { host: req.headers.host, url: req._url, target });
|
|
59
|
+
if (target) {
|
|
60
|
+
this.proxy.ws(req, socket, head, { target });
|
|
61
|
+
} else {
|
|
62
|
+
this._handleNotFound(req, socket);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
this.resolvers = [this._defaultResolver];
|
|
68
|
+
opts.port = opts.port || 8080;
|
|
69
|
+
|
|
70
|
+
if (opts.resolvers) {
|
|
71
|
+
this.addResolver(opts.resolvers);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Routing table.
|
|
75
|
+
this.routing = {};
|
|
76
|
+
this.certs = {};
|
|
77
|
+
|
|
78
|
+
// Create a proxy server with custom application logic
|
|
79
|
+
this.proxy = httpProxy.createProxyServer({
|
|
80
|
+
xfwd: !!opts.xfwd,
|
|
81
|
+
prependPath: false,
|
|
82
|
+
secure: opts.secure !== false,
|
|
83
|
+
timeout: opts.timeout,
|
|
84
|
+
proxyTimeout: opts.proxyTimeout,
|
|
85
|
+
changeOrigin: false,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
this.proxy.on('proxyReq', (p, req) => {
|
|
89
|
+
if (req.host != null) {
|
|
90
|
+
p.setHeader('host', req.host);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// @link: https://github.com/http-party/node-http-proxy/issues/1401
|
|
95
|
+
this.proxy.on('proxyRes', (proxyRes) => {
|
|
96
|
+
this.opts.headers.forEach((x) => {
|
|
97
|
+
proxyRes.headers[x.key] = x.value;
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// Optionally create an https proxy server.
|
|
102
|
+
if (opts.ssl) {
|
|
103
|
+
if (Array.isArray(opts.ssl)) {
|
|
104
|
+
opts.ssl.forEach((sslOpts) => {
|
|
105
|
+
this.setupHttpsProxy(this.proxy, websocketUpgrade, sslOpts);
|
|
106
|
+
});
|
|
107
|
+
} else {
|
|
108
|
+
this.setupHttpsProxy(this.proxy, websocketUpgrade, opts.ssl);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Plain HTTP Proxy
|
|
113
|
+
const server = this.setupHttpProxy(this.proxy, websocketUpgrade, opts);
|
|
114
|
+
server.listen(opts.port, opts.host);
|
|
115
|
+
|
|
116
|
+
this.proxy.on('error', opts.onError);
|
|
117
|
+
logger.info('Started reverse proxy server on port %s', opts.port);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
setupHttpProxy(proxy, websocketUpgrade, opts) {
|
|
121
|
+
const httpServerModule = opts.serverModule || http;
|
|
122
|
+
const server = httpServerModule.createServer((req, res) => {
|
|
123
|
+
const src = this._getSource(req);
|
|
124
|
+
if (this.opts.corsHandler(req, res) === false) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
this._getTarget(src, req, res).then((target) => {
|
|
129
|
+
if (target) {
|
|
130
|
+
if (target.abort) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (this._shouldRedirectToHttps(this.certs, src, target, this)) {
|
|
135
|
+
this._redirectToHttps(req, res, target, opts.ssl);
|
|
136
|
+
} else {
|
|
137
|
+
if (opts.internal && target.host) {
|
|
138
|
+
req.host = target.host;
|
|
139
|
+
req.headers.host = target.host;
|
|
140
|
+
}
|
|
141
|
+
proxy.web(req, res, {
|
|
142
|
+
target,
|
|
143
|
+
secure: !proxy.options || proxy.options.secure !== false,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
this._handleNotFound(req, res);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// Listen to the `upgrade` event and proxy the WebSocket requests as well.
|
|
153
|
+
server.on('upgrade', websocketUpgrade);
|
|
154
|
+
|
|
155
|
+
server.on('error', (err) => logger.error('Http server error', { error: err }));
|
|
156
|
+
|
|
157
|
+
return server;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
_shouldRedirectToHttps(certs, src, target) {
|
|
161
|
+
return certs && src in certs && target.sslRedirect;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
setupHttpsProxy(proxy, websocketUpgrade, sslOpts) {
|
|
165
|
+
let https;
|
|
166
|
+
|
|
167
|
+
let ssl = {
|
|
168
|
+
SNICallback: (hostname, cb) => {
|
|
169
|
+
if (cb) {
|
|
170
|
+
cb(null, this.certs[hostname]);
|
|
171
|
+
} else {
|
|
172
|
+
return this.certs[hostname];
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
// Default certs for clients that do not support SNI.
|
|
176
|
+
key: this._getCertData(sslOpts.key),
|
|
177
|
+
cert: this._getCertData(sslOpts.cert),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Allows the option to disable older SSL/TLS versions
|
|
181
|
+
if (sslOpts.secureOptions) {
|
|
182
|
+
ssl.secureOptions = sslOpts.secureOptions;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (sslOpts.opts) {
|
|
186
|
+
ssl = _.defaults(ssl, sslOpts.opts);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (sslOpts.http2) {
|
|
190
|
+
// eslint-disable-next-line
|
|
191
|
+
https = sslOpts.serverModule || require('spdy');
|
|
192
|
+
if (_.isObject(sslOpts.http2)) {
|
|
193
|
+
sslOpts.spdy = sslOpts.http2;
|
|
194
|
+
}
|
|
195
|
+
} else {
|
|
196
|
+
// eslint-disable-next-line global-require
|
|
197
|
+
https = sslOpts.serverModule || require('https');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
this.httpsServer = https.createServer(ssl, (req, res) => {
|
|
201
|
+
const src = this._getSource(req);
|
|
202
|
+
if (this.opts.corsHandler(req, res) === false) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const httpProxyOpts = Object.assign({}, this.opts.httpProxy);
|
|
207
|
+
this._getTarget(src, req, res).then((target) => {
|
|
208
|
+
if (target) {
|
|
209
|
+
if (target.abort) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
httpProxyOpts.target = target;
|
|
214
|
+
proxy.web(req, res, httpProxyOpts);
|
|
215
|
+
} else {
|
|
216
|
+
this._handleNotFound(req, res);
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
this.httpsServer.on('upgrade', websocketUpgrade);
|
|
222
|
+
this.httpsServer.on('error', (err) => logger.error('HTTPS server Error', { error: err }));
|
|
223
|
+
this.httpsServer.on('clientError', (err) => logger.error('HTTPS client Error', { error: err }));
|
|
224
|
+
|
|
225
|
+
logger.info('Listening to HTTPS requests on port %s', sslOpts.port);
|
|
226
|
+
this.httpsServer.listen(sslOpts.port, sslOpts.ip);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
addResolver(_resolver) {
|
|
230
|
+
const resolver = Array.isArray(_resolver) ? _resolver : [_resolver];
|
|
231
|
+
resolver.forEach((resolveObj) => {
|
|
232
|
+
if (!_.isFunction(resolveObj)) {
|
|
233
|
+
throw new Error('Resolver must be an invokable function.');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!resolveObj.priority) {
|
|
237
|
+
resolveObj.priority = 0;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
this.resolvers.push(resolveObj);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
this.resolvers = _.sortBy(_.uniq(this.resolvers), ['priority']).reverse();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
removeResolver(resolver) {
|
|
247
|
+
// since unique resolvers are not checked for performance, just remove every existence.
|
|
248
|
+
this.resolvers = this.resolvers.filter((x) => x !== resolver);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
static buildTarget(target, opts = {}) {
|
|
252
|
+
const newTarget = prepareUrl(target);
|
|
253
|
+
newTarget.sslRedirect = opts.ssl && opts.ssl.redirect !== false;
|
|
254
|
+
return newTarget;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Register a new route.
|
|
259
|
+
*
|
|
260
|
+
* @param {string | URL} src A string or a url parsed by node url module.
|
|
261
|
+
* Note that port is ignored, since the proxy just listens to one port.
|
|
262
|
+
* @param {string} target A string or a url parsed by node url module.
|
|
263
|
+
* @param {*} opts Route options.
|
|
264
|
+
*/
|
|
265
|
+
register(src, target, opts = {}) {
|
|
266
|
+
// allow registering with src or target as an object to pass in
|
|
267
|
+
// options specific to each one.
|
|
268
|
+
if (src && src.src) {
|
|
269
|
+
target = src.target;
|
|
270
|
+
opts = src;
|
|
271
|
+
src = src.src;
|
|
272
|
+
} else if (target && target.target) {
|
|
273
|
+
opts = target;
|
|
274
|
+
target = target.target;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
if (!src || !target) {
|
|
278
|
+
throw Error('Cannot register a new route with unspecified src or target');
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
src = prepareUrl(src);
|
|
282
|
+
|
|
283
|
+
const { ssl } = opts;
|
|
284
|
+
if (ssl) {
|
|
285
|
+
if (!this.httpsServer) {
|
|
286
|
+
throw Error('Cannot register https routes without defining a ssl port');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (!this.certs[src.hostname]) {
|
|
290
|
+
if (ssl.key || ssl.cert) {
|
|
291
|
+
this.certs[src.hostname] = this._createCredentialContext(ssl.key, ssl.cert);
|
|
292
|
+
} else {
|
|
293
|
+
// Trigger the use of the default certificates.
|
|
294
|
+
this.certs[src.hostname] = undefined;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
target = ReverseProxy.buildTarget(target, opts);
|
|
300
|
+
|
|
301
|
+
if (!this.routing[src.hostname]) {
|
|
302
|
+
this.routing[src.hostname] = [];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const host = this.routing[src.hostname];
|
|
306
|
+
const pathname = src.pathname || '/';
|
|
307
|
+
let route = _.find(host, { path: pathname });
|
|
308
|
+
|
|
309
|
+
if (!route) {
|
|
310
|
+
route = { path: pathname, rr: 0, urls: [], opts: Object.assign({}, opts) };
|
|
311
|
+
host.push(route);
|
|
312
|
+
|
|
313
|
+
// Sort routes
|
|
314
|
+
this.routing[src.hostname] = _.sortBy(host, (x) => -x.path.length);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
route.urls.push(target);
|
|
318
|
+
|
|
319
|
+
logger.info('Registered a new route', { from: src, to: target });
|
|
320
|
+
return this;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
unregister(src, target) {
|
|
324
|
+
if (!src) {
|
|
325
|
+
return this;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
src = prepareUrl(src);
|
|
329
|
+
const routes = this.routing[src.hostname] || [];
|
|
330
|
+
const pathname = src.pathname || '/';
|
|
331
|
+
let i = 0;
|
|
332
|
+
|
|
333
|
+
for (i = 0; i < routes.length; i++) {
|
|
334
|
+
if (routes[i].path === pathname) {
|
|
335
|
+
break;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (i < routes.length) {
|
|
340
|
+
const route = routes[i];
|
|
341
|
+
|
|
342
|
+
if (target) {
|
|
343
|
+
target = prepareUrl(target);
|
|
344
|
+
_.remove(route.urls, (x) => x.href === target.href);
|
|
345
|
+
} else {
|
|
346
|
+
route.urls = [];
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (route.urls.length === 0) {
|
|
350
|
+
routes.splice(i, 1);
|
|
351
|
+
delete this.certs[src.hostname];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
logger.info('Unregistered a route', { from: src, to: target });
|
|
355
|
+
}
|
|
356
|
+
return this;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
_defaultResolver(host, url) {
|
|
360
|
+
// Given a src resolve it to a target route if any available.
|
|
361
|
+
if (!host) {
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
url = url || '/';
|
|
366
|
+
|
|
367
|
+
const routes = this.routing[host];
|
|
368
|
+
let i = 0;
|
|
369
|
+
|
|
370
|
+
if (routes) {
|
|
371
|
+
const len = routes.length;
|
|
372
|
+
|
|
373
|
+
// Find path that matches the start of req.url
|
|
374
|
+
for (i = 0; i < len; i++) {
|
|
375
|
+
const route = routes[i];
|
|
376
|
+
|
|
377
|
+
if (route.path === '/' || this._startsWith(url, route.path)) {
|
|
378
|
+
return route;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Resolves to route
|
|
386
|
+
* @param host
|
|
387
|
+
* @param url
|
|
388
|
+
* @returns {*}
|
|
389
|
+
*/
|
|
390
|
+
resolve(host, url, req) {
|
|
391
|
+
const promiseArray = [];
|
|
392
|
+
|
|
393
|
+
host = host && host.toLowerCase();
|
|
394
|
+
for (let i = 0; i < this.resolvers.length; i++) {
|
|
395
|
+
promiseArray.push(this.resolvers[i].call(this, host, url, req));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return Promise.all(promiseArray)
|
|
399
|
+
.then((resolverResults) => {
|
|
400
|
+
for (let i = 0; i < resolverResults.length; i++) {
|
|
401
|
+
let route = resolverResults[i];
|
|
402
|
+
if (route) {
|
|
403
|
+
route = ReverseProxy.buildRoute(route);
|
|
404
|
+
// ensure resolved route has path that prefixes URL
|
|
405
|
+
// no need to check for native routes.
|
|
406
|
+
if (!route.isResolved || route.path === '/' || this._startsWith(url, route.path)) {
|
|
407
|
+
return route;
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
.catch((error) => {
|
|
413
|
+
console.error('Resolvers error:', error);
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
static buildRoute(route) {
|
|
418
|
+
if (!_.isString(route) && !_.isObject(route)) {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// default route type matched.
|
|
423
|
+
if (_.isObject(route) && route.urls && route.path) {
|
|
424
|
+
return route;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// to bust cache for route, you can attach an id to the route object in business layer
|
|
428
|
+
const cacheKey = _.isString(route) ? route : hash(route);
|
|
429
|
+
const entry = routeCache.get(cacheKey);
|
|
430
|
+
if (entry) {
|
|
431
|
+
return entry;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const routeObject = { rr: 0, isResolved: true };
|
|
435
|
+
if (_.isString(route)) {
|
|
436
|
+
routeObject.urls = [ReverseProxy.buildTarget(route)];
|
|
437
|
+
routeObject.path = '/';
|
|
438
|
+
} else {
|
|
439
|
+
if (!route.url) {
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
routeObject.urls = (_.isArray(route.url) ? route.url : [route.url]).map((url) =>
|
|
444
|
+
ReverseProxy.buildTarget(url, route.opts || {})
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
routeObject.path = route.path || '/';
|
|
448
|
+
routeObject.opts = route.opts || {};
|
|
449
|
+
}
|
|
450
|
+
routeCache.set(cacheKey, routeObject);
|
|
451
|
+
return routeObject;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
_getTarget(src, req, res) {
|
|
455
|
+
const { url } = req;
|
|
456
|
+
|
|
457
|
+
return this.resolve(src, url, req).then((route) => {
|
|
458
|
+
if (!route) {
|
|
459
|
+
logger.warn('no valid route found for given source', { src, url });
|
|
460
|
+
return;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
req._url = url; // save original url
|
|
464
|
+
req.url = url.substr(route.path.length) || '';
|
|
465
|
+
|
|
466
|
+
// Perform Round-Robin on the available targets
|
|
467
|
+
// TODO: if target errors with EHOSTUNREACH we should skip this
|
|
468
|
+
// target and try with another.
|
|
469
|
+
const { urls } = route;
|
|
470
|
+
const j = route.rr;
|
|
471
|
+
route.rr = (j + 1) % urls.length; // get and update Round-robin index.
|
|
472
|
+
const target = route.urls[j];
|
|
473
|
+
|
|
474
|
+
// Fix request url if target name specified.
|
|
475
|
+
if (target.pathname) {
|
|
476
|
+
if (req.url) {
|
|
477
|
+
req.url = path.posix.join(target.pathname, req.url);
|
|
478
|
+
} else {
|
|
479
|
+
req.url = target.pathname;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (route.opts && route.opts.onRequest) {
|
|
484
|
+
const resultFromRequestHandler = route.opts.onRequest(req, res, target);
|
|
485
|
+
if (resultFromRequestHandler !== undefined) {
|
|
486
|
+
logger.info('proxy %s received result from onRequest handler, returning.', src + url);
|
|
487
|
+
return resultFromRequestHandler;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
logger.info('proxy: %s => %s', src + url, path.posix.join(target.host, req.url));
|
|
492
|
+
|
|
493
|
+
return target;
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
_getSource(req) {
|
|
498
|
+
if (this.opts.preferForwardedHost === true && req.headers['x-forwarded-host']) {
|
|
499
|
+
return req.headers['x-forwarded-host'].split(':')[0];
|
|
500
|
+
}
|
|
501
|
+
if (req.headers.host) {
|
|
502
|
+
return req.headers.host.split(':')[0];
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
close() {
|
|
507
|
+
try {
|
|
508
|
+
return Promise.all(
|
|
509
|
+
[this.server, this.httpsServer]
|
|
510
|
+
.filter((s) => s)
|
|
511
|
+
.map((server) => new Promise((resolve) => server.close(resolve)))
|
|
512
|
+
);
|
|
513
|
+
} catch (err) {
|
|
514
|
+
// Ignore for now...
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
_handleNotFound = (req, res) => {
|
|
519
|
+
const err = new Error(`Not Found: ${this._getSource(req)}${req.url}`);
|
|
520
|
+
err.code = 'NOTFOUND';
|
|
521
|
+
this.opts.onError(err, req, res);
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// Redirect to the HTTPS proxy
|
|
525
|
+
_redirectToHttps(req, res, target, ssl) {
|
|
526
|
+
req.url = req._url || req.url; // Get the original url since we are going to redirect.
|
|
527
|
+
|
|
528
|
+
const targetPort = Number(ssl.redirectPort || ssl.port);
|
|
529
|
+
const hostname = req.headers.host.split(':')[0] + (targetPort && targetPort !== 443 ? `:${targetPort}` : '');
|
|
530
|
+
const url = `https://${path.posix.join(hostname, req.url)}`;
|
|
531
|
+
logger.info('redirect: %s => %s', path.posix.join(req.headers.host, req.url), url);
|
|
532
|
+
// We can use 301 for permanent redirect, but its bad for debugging, we may have it as
|
|
533
|
+
// a configurable option.
|
|
534
|
+
res.writeHead(302, { Location: url });
|
|
535
|
+
res.end();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
_startsWith(input, str) {
|
|
539
|
+
return input.slice(0, str.length) === str && (input.length === str.length || input[str.length] === '/');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
_getCertData(source, unbundle) {
|
|
543
|
+
let data;
|
|
544
|
+
if (source) {
|
|
545
|
+
if (_.isArray(source)) {
|
|
546
|
+
const sources = source;
|
|
547
|
+
return _.flatten(_.map(sources, (_source) => this._getCertData(_source, unbundle)));
|
|
548
|
+
}
|
|
549
|
+
if (Buffer.isBuffer(source)) {
|
|
550
|
+
data = source.toString('utf8');
|
|
551
|
+
} else if (fs.existsSync(source)) {
|
|
552
|
+
data = fs.readFileSync(source, 'utf8');
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return data;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
_createCredentialContext(key, cert) {
|
|
559
|
+
const opts = {};
|
|
560
|
+
|
|
561
|
+
opts.key = this._getCertData(key);
|
|
562
|
+
opts.cert = this._getCertData(cert);
|
|
563
|
+
|
|
564
|
+
const credentials = tls.createSecureContext(opts);
|
|
565
|
+
return credentials.context;
|
|
566
|
+
}
|
|
567
|
+
};
|
package/lib/index.js
CHANGED
|
@@ -7,13 +7,11 @@ const logger = require('@abtnode/logger')(`${require('../package.json').name}:pr
|
|
|
7
7
|
const debug = require('debug')(`${require('../package.json').name}:provider:index`);
|
|
8
8
|
|
|
9
9
|
const Nginx = require('./nginx');
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const ROUTER_PROVIDER_NONE = 'none';
|
|
10
|
+
const Default = require('./default');
|
|
13
11
|
|
|
14
12
|
const providerMap = new Map([
|
|
15
|
-
[ROUTER_PROVIDER_NONE, None],
|
|
16
13
|
['nginx', Nginx],
|
|
14
|
+
['default', Default],
|
|
17
15
|
]);
|
|
18
16
|
|
|
19
17
|
const getProviderNames = () => [...providerMap.keys()];
|
|
@@ -37,22 +35,20 @@ const getProvider = (name) => {
|
|
|
37
35
|
const listProviders = async (configDir) =>
|
|
38
36
|
Promise.all(getProviderNames().map((x) => providerMap.get(x).describe({ configDir })));
|
|
39
37
|
|
|
40
|
-
const findExistsProvider =
|
|
38
|
+
const findExistsProvider = () => {
|
|
41
39
|
for (const [key, Provider] of providerMap.entries()) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return key;
|
|
47
|
-
}
|
|
48
|
-
} catch (error) {
|
|
49
|
-
debug(error);
|
|
50
|
-
console.error(`provider ${key} exists check failed:`, error.message);
|
|
40
|
+
try {
|
|
41
|
+
const exists = Provider.exists();
|
|
42
|
+
if (exists) {
|
|
43
|
+
return key;
|
|
51
44
|
}
|
|
45
|
+
} catch (error) {
|
|
46
|
+
debug(error);
|
|
47
|
+
console.error(`provider ${key} exists check failed:`, error.message);
|
|
52
48
|
}
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
return
|
|
51
|
+
return 'default';
|
|
56
52
|
};
|
|
57
53
|
|
|
58
54
|
const clearRouterByConfigKeyword = async (keyword) => {
|
|
@@ -64,10 +60,14 @@ const clearRouterByConfigKeyword = async (keyword) => {
|
|
|
64
60
|
debug('clear router by config directory prefix, config :', keyword);
|
|
65
61
|
for (const [key, Provider] of providerMap.entries()) {
|
|
66
62
|
const status = await Provider.getStatus(keyword);
|
|
67
|
-
if (status.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
if (status.managed) {
|
|
64
|
+
try {
|
|
65
|
+
await kill(status.pid);
|
|
66
|
+
debug('killed pid:', status.pid);
|
|
67
|
+
debug('killed router:', key);
|
|
68
|
+
} catch {
|
|
69
|
+
// do nothing
|
|
70
|
+
}
|
|
71
71
|
return key;
|
|
72
72
|
}
|
|
73
73
|
}
|