-
Notifications
You must be signed in to change notification settings - Fork 13
/
Copy pathindex.js
487 lines (431 loc) · 14.7 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
/**
* http-route-proxy
* http://github.com/switer/http-route-proxy
*
* Copyright (c) 2013 "switer" guankaishe
* Licensed under the MIT license.
* https://github.com/switer/http-route-proxy/blob/master/LICENSE
*/
var httpProxy = require('http-proxy'), // base on nodejitsu proxy server
staticServer = require('./lib/static'), // static server
util = require('./lib/util'),
colors = require('colors'), // use to pretty console log
path = require('path'),
Url = require('url'),
_ = require('underscore'); // use each method.....
var server = {
/**
* request proxy route match status code
*/
status: {
STATIC: 0,
FORWARD: 1,
UNMATCHED: 2
},
/**
* id for each proxy server
*/
serverId: 1,
/**
* mapping root
*/
rules: {},
/**
* save proxy host
*/
proxies: {
/**
* from: Object {hostname, name}
* rules: Object {static, forward, rewrite}
*/
},
/**
* defualt port when config port is not pass in
*/
defaultPort: 80,
/**
* defualt port for https
*/
defualtHttpsPort: 443,
/**
* module regexps
*/
regexps: {
// validate host string
HOST: /^[0-9a-zA-Z\.]+\:?[0-9]*$/,
ROUTE_FORWARD: /^([A-Z]+:)?\/.*$/,
// static request must be GET method
ROUTE_STATIC: /^!([A-Z]+:)?\/.*$/
// ROUTE_REWRITE: /^\^.*/
},
/**
* for creating request route matching rule
*/
str: {
ROUTE_METHOD_PREFIX: '([A-Z]+:)?'
},
/**
* Proxy enter api
*/
proxy: function (hosts) {
// handle each proxy config
_.each(hosts, function (hostConfig) {
var hostObj = {
from: this.parseHost(hostConfig.from),
routes: this.parseRouteRule(hostConfig)
}
var serverId = this.create(hostObj.from, hostObj.routes, hostConfig);
this.saveHost(hostObj.from, hostObj.routes, serverId);
}.bind(this));
},
/**
* support as express connect middleware
*/
connect: function (config) {
var proxy = new httpProxy.RoutingProxy(),
connectConfig = {
rules: this.parseRouteRule(config)
},
_this = this;
return function (req, res, next) {
_this.proxyMiddleware(req, res, proxy, connectConfig, function (proxyStatus) {
// processing in express
if (proxyStatus.status !== _this.status.FORWARD) {
next();
}
// forward request
else {
console.log(proxyStatus.message);
}
});
}
},
/**
* create a server for the one proxy config
*/
create: function (from, routes, options) {
var serverId = this.serverId, // for serverid hoisting
// proxy server options
proxyOptions = {
// use client origin
changeOrigin: true,
target: {
// https: true,
// rejectUnauthorized: false
}
},
_this = this; // for hoisting context of `this`
// nessary for fixing node-http-proxy rejectUnauthorized option not work bug
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
// create proxy server
var server = httpProxy.createServer(
/**
* middleware
* Cross-Domain-Access
*/
function (req, res, next) {
// TODO
next();
},
function (req, res, proxy) {
_this.proxyMiddleware(req, res, proxy, _this.proxies[serverId], function (statusObj) {
console.log(statusObj.message);
});
/**
* must listen hostname, otherwise it will be fail when repeat listening
* localhost in the some port
*/
}).listen(from.port, from.hostname)
.on('error', function (e) {
// catch server listen error
console.log('Create proxy error,'.red.grey + "cause by can't listen host from " +
from.hostname.yellow.grey + ' : ' + from.port.toString().blue.grey);
});
console.log('Listen from ' + from.hostname.green.grey + ' : ' + from.port.toString().blue.grey);
// Custom error
server.proxy.on('proxyError', function (err, req, res) {
res.writeHead(500, {
'Content-Type': 'text/plain'
});
res.end('Something went wrong. And we are reporting a custom error message.\n' + err);
});
return this.serverId ++;
},
/**
* Proxy middle for handling request and response
*/
proxyMiddleware: function (req, res, proxy, config, next) {
var from = this.parseHost(req.headers.host),
method = req.method,
// rewrite url to originUrl for proxy agent
requestURL = req.url = req.url || req.originalUrl,
url = method.toUpperCase() + ':' + requestURL,
forwardRouteObj = null;
// get proxy config by proxy server id
var proxyConfig = config;
if (this.staticMatched(url, proxyConfig.rules.static)) {
// match route is static file response
var directory = path.resolve(process.cwd(), '.');
// send static files without server
staticServer.sendfile(req, res, directory);
next({
status: this.status.STATIC,
message: method.blue.grey + ' ' + requestURL + ' from '.green.grey + from.hostname + ':' + from.port.toString().blue.grey
});
} else if (forwardRouteObj = this.forwardMatched(url, proxyConfig.rules.forward)) {
// set headers of config
this.setHeaders(req, res, forwardRouteObj.options.headers);
var forwardObj = forwardRouteObj.forward,
// forward options
proxyForwardOptions = {
changeOrigin: true,
host: forwardObj.hostname,
port: forwardObj.port
};
if (forwardRouteObj.options.https) {
// set https forward options
proxyForwardOptions = _.extend(proxyForwardOptions, {
target: {
https: true,
rejectUnauthorized: false
}
});
}
// forward to remote server
proxy.proxyRequest(req, res, proxyForwardOptions);
next({
status: this.status.FORWARD,
message: 'forward '.yellow.grey + method.blue.grey + ' ' + requestURL + ' from '.green.grey + from.hostname + ':' +
from.port.toString().blue.grey +' to '.green.grey + forwardObj.protocal + '://' + forwardObj.hostname + ':' +
forwardObj.port.toString().blue.grey + requestURL
});
}
else {
next({
status: this.status.UNMATCHED,
message: "Sorry proxy error, http-route-proxy can't match any forward rule, please check your proxy config".red
});
}
},
/**
* Set headers by proxy config
*/
setHeaders: function (req, res, headers) {
var reqHeaders = headers && headers.req ? headers.req: {},
resHeaders = headers && headers.res ? headers.res: {},
origin = req.headers.origin || req.headers.host;
// if exist Origin, use "Cross-Domain-Access"
if (origin) {
// currently, we only support receiving http request
res.setHeader('access-control-allow-origin', 'http://' + origin);
res.setHeader('access-control-allow-credentials', true);
}
//rewrite header on config
if (headers) {
_.each(reqHeaders, function (value, key) {
req.headers[key] = value;
});
_.each(resHeaders, function (value, key) {
res.setHeader(key, value);
});
}
// avoid node-http-proxy to rewrite headers
var setHeader = res.setHeader;
res.setHeader = function (key, value) {
if (!resHeaders[key]) setHeader.call(res, key, value);
}
},
/**
* host is a string, should be parse to hostname + port object format
*/
parseHost: function (host, options) {
options = options || {};
var hostChunks = host.match(/^([a-z]+)\:\/\/(.*)$/),
protocal = hostChunks? hostChunks[1]: '',
hostSection = hostChunks? hostChunks[2]: host,
hostname = hostSection.split(':')[0],
port = hostSection.split(':')[1],
protocalObj = this.protocal(protocal),
protocalOptions = protocalObj.unknow ? options: protocalObj;
return {
protocal: this.options2protocal(protocalOptions),
protocalOption: protocalOptions,
hostname: hostname,
port: port ? parseInt(port) : (protocalOptions.https ? this.defualtHttpsPort : this.defaultPort)
};
},
/**
* save each host config
*/
saveHost: function (fromObj, routeRules, serverId) {
// save proxy config
this.proxies[serverId] = {
from: fromObj,
rules: routeRules
};
},
/**
* Check route if has default action , if has not, put it on first
*/
checkDefaultAction: function (routes) {
var isContainDefaultAction = false;
_.each(routes, function (item) {
if (item == '/' || item.action == '/') {
isContainDefaultAction = true;
return true;
}
});
// put default action on the first tuples
if (!isContainDefaultAction) routes.unshift('/');
return routes;
},
/**
* validate host string
*/
validHost: function (host) {
return this.regexps.HOST.exec(host) ? true : false;
},
/**
*
* @return Object
*/
protocal: function (protocal) {
var protocalObj = {
http: false,
https: false,
websocket: false,
unknow: false
};
if (protocal === 'http') {
protocalObj.http = true;
} else if (protocal === 'https') {
protocalObj.https = true;
} else if (protocal === 'ws') {
protocalObj.websocket = true;
} else {
protocalObj.unknow = true;
}
return protocalObj;
},
/**
* protocal option to protocal name
*/
options2protocal: function (options) {
if (options.https) {
return 'https';
} else if (options.websocket) {
return 'ws';
} else if (options.http) {
return 'http';
} else {
return 'http';
}
},
/**
* parse each route rule to url matched rule
*/
parseRouteRule: function (options) {
options = util.copy(options);
var routes = options.route || [];
// if route config is empty, give it default
routes = this.checkDefaultAction(routes);
var _this = this,
forwards = [], // forward route rules
// rewrites = [], // currently it's useless
statices = [], // static route rules
forwardObj = null;
_.each(routes, function (route) {
// route detail config
if (_.isObject(route)) {
var routeOptions = route,
forwardObj = _this.parseHost(routeOptions.forward);
routeOptions = _.extend(routeOptions, forwardObj.protocalOption);
forwards.push({
rule: _this.forwardRouteRule2Regexp(routeOptions.action),
forward: forwardObj,
options: routeOptions
});
}
// forward route match, route rule in shorthand
else if (_this.regexps.ROUTE_FORWARD.exec(route)) {
// parse forward host
var forwardObj = _this.parseHost(options.to, options);
// set foward options
options = _.extend(options, forwardObj.protocalOption);
// save config
forwards.push({
rule: _this.forwardRouteRule2Regexp(route),
forward: forwardObj,
options: options
});
}
// static route match, route rule in shorthand
else if (_this.regexps.ROUTE_STATIC.exec(route)) {
statices.push(_this.staticRouteRule2Regexp(route));
}
});
return {
forward: forwards,
// rewrite: rewrites,
static: statices
};
},
/**
* url route rule to regexp
*/
string2Regexp: function (rule) {
rule = rule.replace('/','\\/');
rule = '^' + rule + '.*$';
return new RegExp(rule);
},
/**
* find the rule whether is static route rule and return regexpess rule.
*/
staticRouteRule2Regexp: function (rule) {
var matches = this.regexps.ROUTE_STATIC.exec(rule);
rule = rule.replace(/^!/, '');
if (!matches[1]) {
rule = this.str.ROUTE_METHOD_PREFIX + rule;
}
return this.string2Regexp(rule);
},
/**
* find the rule whether is forward route rule and return regexpess rule.
*/
forwardRouteRule2Regexp: function (rule) {
var matches = this.regexps.ROUTE_FORWARD.exec(rule);
if (!matches[1]) {
rule = this.str.ROUTE_METHOD_PREFIX + '(@)'.replace('@', rule);
}
return this.string2Regexp(rule);
},
/**
* Match url by forward route rule
* @return {Object} routeRule
*/
forwardMatched: function (url, forwardRules) {
var matchedRoute = null;
forwardRules.some(function (ruleObj) {
if (ruleObj.rule.exec(url)) {
matchedRoute = ruleObj;
return true;
}
});
return matchedRoute;
},
/**
* Match url by static route rule
* @return <Boolean> isMatched
*/
staticMatched: function (url, staticRules) {
var isMatched = false;
_.each(staticRules, function (rule) {
if (rule.exec(url)) {
isMatched = true;
return true;
}
});
return isMatched;
}
}
module.exports = server;