@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.
@@ -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 None = require('./none');
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 = async () => {
38
+ const findExistsProvider = () => {
41
39
  for (const [key, Provider] of providerMap.entries()) {
42
- if (key !== ROUTER_PROVIDER_NONE) {
43
- try {
44
- const exists = Provider.exists();
45
- if (exists) {
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 ROUTER_PROVIDER_NONE;
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.ownByABTNode) {
68
- await kill(status.pid);
69
- debug('killed pid:', status.pid);
70
- debug('killed router:', key);
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
  }
@@ -7,7 +7,7 @@ keepalive_timeout 30;
7
7
  client_body_buffer_size 32k;
8
8
  client_header_buffer_size 16k;
9
9
  large_client_header_buffers 4 256k;
10
- server_names_hash_bucket_size 80;
10
+ server_names_hash_bucket_size 512;
11
11
 
12
12
  ## proxy
13
13
  proxy_headers_hash_bucket_size 512;