@abtnode/router-provider 1.15.17 → 1.16.0-beta-8ee536d7

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