pixl-server-web 2.0.0 → 2.0.2
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 +125 -30
- package/lib/http.js +7 -0
- package/lib/https.js +7 -0
- package/lib/request.js +52 -0
- package/package.json +1 -1
- package/test/test.js +158 -1
- package/web_server.js +70 -27
package/README.md
CHANGED
|
@@ -34,6 +34,9 @@ This module is a component for use in [pixl-server](https://www.github.com/jhuck
|
|
|
34
34
|
* [http_enable_brotli](#http_enable_brotli)
|
|
35
35
|
* [http_brotli_opts](#http_brotli_opts)
|
|
36
36
|
* [http_default_acl](#http_default_acl)
|
|
37
|
+
* [http_blacklist](#http_blacklist)
|
|
38
|
+
* [http_rewrites](#http_rewrites)
|
|
39
|
+
* [http_redirects](#http_redirects)
|
|
37
40
|
* [http_log_requests](#http_log_requests)
|
|
38
41
|
* [http_log_request_details](#http_log_request_details)
|
|
39
42
|
* [http_log_body_max](#http_log_body_max)
|
|
@@ -55,6 +58,7 @@ This module is a component for use in [pixl-server](https://www.github.com/jhuck
|
|
|
55
58
|
* [http_req_max_dump_dir](#http_req_max_dump_dir)
|
|
56
59
|
* [http_req_max_dump_debounce](#http_req_max_dump_debounce)
|
|
57
60
|
* [http_public_ip_offset](#http_public_ip_offset)
|
|
61
|
+
* [http_legacy_callback_support](#http_legacy_callback_support)
|
|
58
62
|
* [https](#https)
|
|
59
63
|
* [https_port](#https_port)
|
|
60
64
|
* [https_alt_ports](#https_alt_ports)
|
|
@@ -421,6 +425,124 @@ This allows you to configure the default [ACL](https://en.wikipedia.org/wiki/Acc
|
|
|
421
425
|
|
|
422
426
|
See [Access Control Lists](#access-control-lists) below for more details.
|
|
423
427
|
|
|
428
|
+
## http_blacklist
|
|
429
|
+
|
|
430
|
+
The `http_blacklist` property allows you to specify a list of IPs or IP ranges which are blacklisted. Meaning, all requests from these IPs are immediately rejected by the web server (see details below). The format of the `http_blacklist` is the same as `http_default_acl` (see [Access Control Lists](#access-control-lists)). It defaults to an empty list (i.e. disabled).
|
|
431
|
+
|
|
432
|
+
To customize it, specify an array of [IPv4](https://en.wikipedia.org/wiki/IPv4) and/or [IPv6](https://en.wikipedia.org/wiki/IPv6) addresses, partials or [CIDR blocks](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing). Example:
|
|
433
|
+
|
|
434
|
+
```json
|
|
435
|
+
{
|
|
436
|
+
"http_blacklist": ["17.0.0.0/8", "12.0.0.0/8"]
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
This example would reject all incoming IP addresses from Apple and AT&T (who own the `17.0.0.0/8` and `12.0.0.0/8` IPv4 blocks, respectively).
|
|
441
|
+
|
|
442
|
+
When a new incoming connection is established, the socket IP is immediately checked against the blacklist, and if matched, the socket is "hard closed". This is an early detection and rejection, before the HTTP request even comes in. In this case a HTTP response isn't sent back (as the socket is simply slammed shut). However, if you are using a load balancer or proxy, the user's true IP address might not be known until later on in the request cycle, once the HTTP headers are read in. At that point all the user's IPs are checked against the blacklist again, and if any of them match, a `HTTP 403 Forbidden` response is sent back.
|
|
443
|
+
|
|
444
|
+
## http_rewrites
|
|
445
|
+
|
|
446
|
+
If you need to rewrite certain incoming URLs on-the-fly, you can define rules in the `http_rewrites` object. The basic format is as follows: keys are regular expressions matched on incoming URI paths, and the values are the substitution strings to use as replacements. Here is a simple example:
|
|
447
|
+
|
|
448
|
+
```json
|
|
449
|
+
{
|
|
450
|
+
"http_rewrites": {
|
|
451
|
+
"^/rewrite/me": "/target/path"
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
This would match any incoming URI paths that start with `/rewrite/me` and replace that section of the path with `/target/path`. So for example a full URI path of `/rewrite/me/please?foo=bar` would rewrite to `/target/path/please?foo=bar`. Note that the suffix after the match was copied over, as well as the query string. Rewriting happens very early in the request cycle before any other processing occurs, including URI filters, method handers and URI handlers, so they all see the final transformed URI, and not the original.
|
|
457
|
+
|
|
458
|
+
Since URIs are matched using regular expressions, you can define capturing groups and refer to them in the target substitution string, using the standard `$1`, `$2`, `$3` syntax. Example:
|
|
459
|
+
|
|
460
|
+
```json
|
|
461
|
+
{
|
|
462
|
+
"http_rewrites": {
|
|
463
|
+
"^/rewrite/me(.*)$": "/target/path?oldpath=$1"
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
This example would grab everything after `/rewrite/me` and store it in a capture group, which is then expanded into the replacement string using the `$1` macro.
|
|
469
|
+
|
|
470
|
+
For even more control over your rewrites, you can specify them using an advanced syntax. Instead of the target path string, set the value to an object containing the following:
|
|
471
|
+
|
|
472
|
+
| Property Name | Type | Description |
|
|
473
|
+
|---------------|------|-------------|
|
|
474
|
+
| `url` | String | The target URI replacement string. |
|
|
475
|
+
| `headers` | Object | Optionally insert custom headers into the incoming request. |
|
|
476
|
+
| `last` | Boolean | Set this to `true` to ensure no futher rewrites happen on the request. |
|
|
477
|
+
|
|
478
|
+
Here is an example showing an advanced configuration:
|
|
479
|
+
|
|
480
|
+
```json
|
|
481
|
+
{
|
|
482
|
+
"http_rewrites": {
|
|
483
|
+
"^/rewrite/me": {
|
|
484
|
+
"url": "/target/path",
|
|
485
|
+
"headers": { "X-Rewritten": "Yes" },
|
|
486
|
+
"last": true
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
A URI may be rewritten multiple times if it matches multiple rules, which are applied in the order which they appear in your configuration. You can specify a `last` property to ensure that rule matching stops when the specified rule matches a request.
|
|
493
|
+
|
|
494
|
+
You can use the `headers` property to insert custom HTTP headers into the request. These will be accessible by downstream URI handlers, and they will also be logged if [http_log_requests](#http_log_requests) is enabled.
|
|
495
|
+
|
|
496
|
+
## http_redirects
|
|
497
|
+
|
|
498
|
+
If you need to redirect certain incoming requests to external URLs, you can define rules in the `http_redirects` object. When matched, these will interrupt the current request and return a redirect response to the client. The basic format is as follows: keys are regular expressions matched on incoming URI paths, and the values are the fully-qualified URLs to redirect to. Here is a simple example:
|
|
499
|
+
|
|
500
|
+
```json
|
|
501
|
+
{
|
|
502
|
+
"http_redirects": {
|
|
503
|
+
"^/redirect/me": "https://disney.com/"
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
This would match any incoming URI paths that start with `/redirect/me` and redirect the user to `https://disney.com/`. Redirects are matched during the URI handling portion of the request cycle, so things like requests and URI filters have already been handled. URI request handlers are not invoked if a redirect occurs.
|
|
509
|
+
|
|
510
|
+
Since URIs are matched using regular expressions, you can define capturing groups and refer to them in the target redirect URL, using the standard `$1`, `$2`, `$3` syntax. Example:
|
|
511
|
+
|
|
512
|
+
```json
|
|
513
|
+
{
|
|
514
|
+
"http_redirects": {
|
|
515
|
+
"^/github/(.*)$": "https://github.com/jhuckaby/$1"
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
This example would grab everything after `/github/` and store it in a capture group, which is then expanded into the replacement string using the `$1` macro. For example, `/github/pixl-server-web` would redirect to `https://github.com/jhuckaby/pixl-server-web`.
|
|
521
|
+
|
|
522
|
+
For even more control over your redirects, you can specify them using an advanced syntax. Instead of the target URL, set the value to an object containing the following:
|
|
523
|
+
|
|
524
|
+
| Property Name | Type | Description |
|
|
525
|
+
|---------------|------|-------------|
|
|
526
|
+
| `url` | String | The fully qualified URL to redirect to. |
|
|
527
|
+
| `headers` | Object | Optionally insert custom headers into the incoming request. |
|
|
528
|
+
| `status` | String | The HTTP response code and status to use, default is `302 Found`. |
|
|
529
|
+
|
|
530
|
+
Here is an example showing an advanced configuration:
|
|
531
|
+
|
|
532
|
+
```json
|
|
533
|
+
{
|
|
534
|
+
"http_redirects": {
|
|
535
|
+
"^/redirect/me": {
|
|
536
|
+
"url": "https://disney.com/",
|
|
537
|
+
"headers": { "X-Redirected": "Yes" },
|
|
538
|
+
"status": "301 Moved Permanently"
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
You can use the `headers` property to insert custom HTTP headers into the redirect response. Use the `status` to customize the HTTP response code and status (it defaults to `302 Found`).
|
|
545
|
+
|
|
424
546
|
## http_log_requests
|
|
425
547
|
|
|
426
548
|
This boolean allows you to enable transaction logging in the web server. It defaults to `false` (disabled). See [Transaction Logging](#transaction-logging) below for details.
|
|
@@ -433,7 +555,7 @@ This boolean adds verbose detail in the transaction log. It defaults to `false`
|
|
|
433
555
|
|
|
434
556
|
## http_log_body_max
|
|
435
557
|
|
|
436
|
-
This property sets the maximum allowed request and response body length that can be logged, when [http_log_request_details](#http_log_request_details) is enabled. If the request or response body length exceeds this amount, they will not be included in the transaction log.
|
|
558
|
+
This property sets the maximum allowed request and response body length that can be logged, when [http_log_request_details](#http_log_request_details) is enabled. It defaults to `32768` (32K). If the request or response body length exceeds this amount, they will not be included in the transaction log.
|
|
437
559
|
|
|
438
560
|
**Note:** This property only has effect if [http_log_request_details](#http_log_request_details) is enabled.
|
|
439
561
|
|
|
@@ -594,7 +716,7 @@ By setting `http_public_ip_offset` to an integer value, you can select *exactly*
|
|
|
594
716
|
|
|
595
717
|
## http_legacy_callback_support
|
|
596
718
|
|
|
597
|
-
This adds support for legacy applications, which require JSONP callback-style API responses, as well as extremely old HTML-wrapped IFRAME API responses. It defaults to disabled. It is **highly recommended** that you *leave this
|
|
719
|
+
This adds support for legacy applications, which require JSONP callback-style API responses, as well as extremely old HTML-wrapped IFRAME API responses. It defaults to disabled. It is **highly recommended** that you *leave this disabled* for all modern applications, as it prevents a classic [XSS reflection attack](https://owasp.org/www-community/attacks/xss/#reflected-xss-attacks) on your APIs:
|
|
598
720
|
|
|
599
721
|
```json
|
|
600
722
|
{
|
|
@@ -812,7 +934,7 @@ server.WebServer.addURIHandler( '/my/custom/uri', 'Custom Name', function(args,
|
|
|
812
934
|
} );
|
|
813
935
|
```
|
|
814
936
|
|
|
815
|
-
|
|
937
|
+
This is sent as pure JSON with the Content-Type `application/json`. The raw HTTP response would look something like this:
|
|
816
938
|
|
|
817
939
|
```
|
|
818
940
|
HTTP/1.1 200 OK
|
|
@@ -825,33 +947,6 @@ Server: Test 1.0
|
|
|
825
947
|
{"Code":0,"Description":"Success","User":{"Name":"Joe","Email":"foo@bar.com"}}
|
|
826
948
|
```
|
|
827
949
|
|
|
828
|
-
Now, depending on the request URL's query string, two variants of the JSON response are possible. First, if there is a `callback` query parameter present, it will be prefixed onto the front of the JSON payload, which will be wrapped in parenthesis, and Content-Type will be switched to `text/javascript`. This is an AJAX / JSONP style of response, and looks like this, assuming a request URL containing `?callback=myfunc`:
|
|
829
|
-
|
|
830
|
-
```
|
|
831
|
-
HTTP/1.1 200 OK
|
|
832
|
-
Connection: keep-alive
|
|
833
|
-
Content-Length: 88
|
|
834
|
-
Content-Type: text/javascript
|
|
835
|
-
Date: Sun, 05 Apr 2015 21:25:49 GMT
|
|
836
|
-
Server: Test 1.0
|
|
837
|
-
|
|
838
|
-
myfunc({"Code":0,"Description":"Success","User":{"Name":"Joe","Email":"foo@bar.com"}});
|
|
839
|
-
```
|
|
840
|
-
|
|
841
|
-
And finally, if the request URL's query string contains both a `callback`, and a `format` parameter set to `html`, the response will be actual HTML (Content-Type `text/html`) with a `<script>` tag embedded containing the JSON and callback wrapper. This is useful for IFRAMEs which may need to talk to their parent window after a form submission. Here is an example assuming a request URL containing `?callback=parent.myfunc&format=html`:
|
|
842
|
-
|
|
843
|
-
```
|
|
844
|
-
HTTP/1.1 200 OK
|
|
845
|
-
Connection: keep-alive
|
|
846
|
-
Content-Length: 151
|
|
847
|
-
Content-Type: text/html
|
|
848
|
-
Date: Sun, 05 Apr 2015 21:28:48 GMT
|
|
849
|
-
Server: Test 1.0
|
|
850
|
-
|
|
851
|
-
<html><head><script>parent.myfunc({"Code":0,"Description":"Success","User":{"Name":"Joe","Email":"foo@bar.com"}});
|
|
852
|
-
</script></head><body> </body></html>
|
|
853
|
-
```
|
|
854
|
-
|
|
855
950
|
### Non-Response
|
|
856
951
|
|
|
857
952
|
The fourth and final type of response is a non-response, and this is achieved by passing `false` to the callback function. This indicates to the web server that your code did *not* handle the request, and it should fall back to looking up a static file on disk. Example:
|
package/lib/http.js
CHANGED
|
@@ -73,6 +73,13 @@ module.exports = class HTTP {
|
|
|
73
73
|
socket.destroy(); // hard close
|
|
74
74
|
return;
|
|
75
75
|
}
|
|
76
|
+
if (ip && self.aclBlacklist.checkAny(ip)) {
|
|
77
|
+
self.logError('blacklist', "IP is blacklisted, denying connection from: " + ip, { ip: ip, port: port });
|
|
78
|
+
socket.end();
|
|
79
|
+
socket.unref();
|
|
80
|
+
socket.destroy(); // hard close
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
76
83
|
|
|
77
84
|
var id = self.getNextId('c');
|
|
78
85
|
self.conns[ id ] = socket;
|
package/lib/https.js
CHANGED
|
@@ -71,6 +71,13 @@ module.exports = class HTTP2 {
|
|
|
71
71
|
socket.destroy(); // hard close
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
|
+
if (ip && self.aclBlacklist.checkAny(ip)) {
|
|
75
|
+
self.logError('blacklist', "IP is blacklisted, denying connection from: " + ip, { ip: ip, port: port });
|
|
76
|
+
socket.end();
|
|
77
|
+
socket.unref();
|
|
78
|
+
socket.destroy(); // hard close
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
74
81
|
|
|
75
82
|
var id = self.getNextId('cs');
|
|
76
83
|
self.conns[ id ] = socket;
|
package/lib/request.js
CHANGED
|
@@ -53,6 +53,24 @@ module.exports = class Request {
|
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
// socket ip was already checked against blacklist at connection time,
|
|
57
|
+
// so here we only need to check the header IPs, if any
|
|
58
|
+
if ((ips.length > 1) && this.aclBlacklist.checkAny( ips.slice(0, -1) )) {
|
|
59
|
+
this.logError(403, "Forbidden: IP addresses blacklisted: " + ips.join(', '), {
|
|
60
|
+
id: args.id,
|
|
61
|
+
useragent: args.request.headers['user-agent'] || '',
|
|
62
|
+
referrer: args.request.headers['referer'] || '',
|
|
63
|
+
cookie: args.request.headers['cookie'] || '',
|
|
64
|
+
url: this.getSelfURL(args.request, args.request.url) || args.request.url
|
|
65
|
+
});
|
|
66
|
+
this.sendHTTPResponse( args,
|
|
67
|
+
"403 Forbidden",
|
|
68
|
+
{ 'Content-Type': "text/html" },
|
|
69
|
+
"403 Forbidden\n"
|
|
70
|
+
);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
56
74
|
// allow special URIs to skip the line
|
|
57
75
|
if (this.queueSkipMatch && request.url.match(this.queueSkipMatch)) {
|
|
58
76
|
this.logDebug(8, "Bumping request to front of queue: " + request.url);
|
|
@@ -157,6 +175,21 @@ module.exports = class Request {
|
|
|
157
175
|
});
|
|
158
176
|
this.logDebug(9, "Incoming HTTP Headers:", request.headers);
|
|
159
177
|
|
|
178
|
+
// url rewrites
|
|
179
|
+
for (var idx = 0, len = this.rewrites.length; idx < len; idx++) {
|
|
180
|
+
var rewrite = this.rewrites[idx];
|
|
181
|
+
if (request.url.match(rewrite.regexp)) {
|
|
182
|
+
request.url = request.url.replace(rewrite.regexp, rewrite.url);
|
|
183
|
+
this.logDebug(8, "URL rewritten to: " + request.url);
|
|
184
|
+
if (rewrite.headers) {
|
|
185
|
+
for (var key in rewrite.headers) {
|
|
186
|
+
request.headers[key] = rewrite.headers[key];
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (rewrite.last) idx = len;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
160
193
|
// detect front-end https
|
|
161
194
|
if (!request.headers.ssl && this.ssl_header_detect) {
|
|
162
195
|
for (var key in this.ssl_header_detect) {
|
|
@@ -409,6 +442,7 @@ module.exports = class Request {
|
|
|
409
442
|
// all filters complete
|
|
410
443
|
// if a filter handled the response, we're done
|
|
411
444
|
if (err === "ABORT") {
|
|
445
|
+
self.deleteUploadTempFiles(args);
|
|
412
446
|
if (args.callback) {
|
|
413
447
|
args.callback();
|
|
414
448
|
delete args.callback;
|
|
@@ -429,6 +463,24 @@ module.exports = class Request {
|
|
|
429
463
|
if (!this.config.get('http_full_uri_match')) uri = uri.replace(/\?.*$/, '');
|
|
430
464
|
var handler = null;
|
|
431
465
|
|
|
466
|
+
// handle redirects first
|
|
467
|
+
for (var idx = 0, len = this.redirects.length; idx < len; idx++) {
|
|
468
|
+
var redirect = this.redirects[idx];
|
|
469
|
+
var matches = args.request.url.match(redirect.regexp);
|
|
470
|
+
if (matches) {
|
|
471
|
+
// redirect now
|
|
472
|
+
var headers = redirect.headers || {};
|
|
473
|
+
|
|
474
|
+
// allow regexp-style placeholder substitution in target url
|
|
475
|
+
headers.Location = redirect.url.replace(/\$(\d+)/g, function(m_all, m_g1) { return matches[ parseInt(m_g1) ]; });
|
|
476
|
+
|
|
477
|
+
this.logDebug(8, "Redirecting to URL: " + headers.Location);
|
|
478
|
+
this.sendHTTPResponse( args, redirect.status || "302 Found", headers, null );
|
|
479
|
+
this.deleteUploadTempFiles(args);
|
|
480
|
+
return;
|
|
481
|
+
} // matched
|
|
482
|
+
} // foreach redirect
|
|
483
|
+
|
|
432
484
|
args.state = 'processing';
|
|
433
485
|
args.perf.begin('process');
|
|
434
486
|
|
package/package.json
CHANGED
package/test/test.js
CHANGED
|
@@ -13,6 +13,8 @@ var PixlServer = require('pixl-server');
|
|
|
13
13
|
|
|
14
14
|
var PixlRequest = require('pixl-request');
|
|
15
15
|
var request = new PixlRequest();
|
|
16
|
+
request.setTimeout( 5 * 1000 ); // 5 seconds
|
|
17
|
+
request.setIdleTimeout( 5 * 1000 ); // 5 seconds
|
|
16
18
|
|
|
17
19
|
var http = require('http');
|
|
18
20
|
var agent = new http.Agent({ keepAlive: true });
|
|
@@ -52,6 +54,20 @@ var server = new PixlServer({
|
|
|
52
54
|
"http_recent_requests": 10,
|
|
53
55
|
"http_max_connections": 10,
|
|
54
56
|
|
|
57
|
+
"http_blacklist": ["5.6.7.0/24"],
|
|
58
|
+
|
|
59
|
+
"http_rewrites": {
|
|
60
|
+
"^/rewrite(.*)$": "/json$1"
|
|
61
|
+
},
|
|
62
|
+
"http_redirects": {
|
|
63
|
+
"^/disney": "https://disney.com/",
|
|
64
|
+
"^/pixar(.*)$": {
|
|
65
|
+
"url": "https://pixar.com$1",
|
|
66
|
+
"headers": { "X-Animal": "Frog" },
|
|
67
|
+
"status": "301 Moved Permanently"
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
55
71
|
"https": 1,
|
|
56
72
|
"https_port": 3021,
|
|
57
73
|
"https_alt_ports": [3121],
|
|
@@ -193,6 +209,100 @@ module.exports = {
|
|
|
193
209
|
);
|
|
194
210
|
},
|
|
195
211
|
|
|
212
|
+
function testSimpleURLRewrite(test) {
|
|
213
|
+
// test simple rewrite
|
|
214
|
+
request.json( 'http://127.0.0.1:3020/rewrite', false,
|
|
215
|
+
{
|
|
216
|
+
headers: {
|
|
217
|
+
'X-Test': "Test"
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
function(err, resp, json, perf) {
|
|
221
|
+
test.ok( !err, "No error from PixlRequest: " + err );
|
|
222
|
+
test.ok( !!resp, "Got resp from PixlRequest" );
|
|
223
|
+
test.ok( resp.statusCode == 200, "Got 200 response: " + resp.statusCode );
|
|
224
|
+
test.ok( resp.headers['via'] == "WebServerTest 1.0", "Correct Via header: " + resp.headers['via'] );
|
|
225
|
+
test.ok( !!json, "Got JSON in response" );
|
|
226
|
+
test.ok( json.code == 0, "Correct code in JSON response: " + json.code );
|
|
227
|
+
test.ok( !!json.user, "Found user object in JSON response" );
|
|
228
|
+
test.ok( json.user.Name == "Joe", "Correct user name in JSON response: " + json.user.Name );
|
|
229
|
+
|
|
230
|
+
// request headers will be echoed back
|
|
231
|
+
test.ok( !!json.headers, "Found headers echoed in JSON response" );
|
|
232
|
+
test.ok( json.headers['x-test'] == "Test", "Found Test header echoed in JSON response" );
|
|
233
|
+
|
|
234
|
+
test.done();
|
|
235
|
+
}
|
|
236
|
+
);
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
function testAdvancedURLRewrite(test) {
|
|
240
|
+
// test advanced rewrite
|
|
241
|
+
request.json( 'http://127.0.0.1:3020/rewrite?foo=bar1234&baz=bop%20pog&animal=frog&animal=dog', false,
|
|
242
|
+
{
|
|
243
|
+
headers: {
|
|
244
|
+
'X-Test': "Test"
|
|
245
|
+
}
|
|
246
|
+
},
|
|
247
|
+
function(err, resp, json, perf) {
|
|
248
|
+
test.ok( !err, "No error from PixlRequest: " + err );
|
|
249
|
+
test.ok( !!resp, "Got resp from PixlRequest" );
|
|
250
|
+
test.ok( resp.statusCode == 200, "Got 200 response: " + resp.statusCode );
|
|
251
|
+
test.ok( resp.headers['via'] == "WebServerTest 1.0", "Correct Via header: " + resp.headers['via'] );
|
|
252
|
+
test.ok( !!json, "Got JSON in response" );
|
|
253
|
+
test.ok( json.code == 0, "Correct code in JSON response: " + json.code );
|
|
254
|
+
test.ok( !!json.user, "Found user object in JSON response" );
|
|
255
|
+
test.ok( json.user.Name == "Joe", "Correct user name in JSON response: " + json.user.Name );
|
|
256
|
+
|
|
257
|
+
test.ok( !!json.query, "Found query object in JSON response" );
|
|
258
|
+
test.ok( json.query.foo == "bar1234", "Query contains correct foo key" );
|
|
259
|
+
test.ok( json.query.baz == "bop pog", "Query contains correct baz key (URL encoding)" );
|
|
260
|
+
|
|
261
|
+
// dupes should become array by default
|
|
262
|
+
test.ok( typeof(json.query.animal) == 'object', "Query param animal is an object" );
|
|
263
|
+
test.ok( json.query.animal.length == 2, "Query param animal has length 2" );
|
|
264
|
+
test.ok( json.query.animal[0] === 'frog', "First animal is frog" );
|
|
265
|
+
test.ok( json.query.animal[1] === 'dog', "Second animal is dog" );
|
|
266
|
+
|
|
267
|
+
// request headers will be echoed back
|
|
268
|
+
test.ok( !!json.headers, "Found headers echoed in JSON response" );
|
|
269
|
+
test.ok( json.headers['x-test'] == "Test", "Found Test header echoed in JSON response" );
|
|
270
|
+
|
|
271
|
+
test.done();
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
function testSimpleURLRedirect(test) {
|
|
277
|
+
// simple 302
|
|
278
|
+
request.get( 'http://127.0.0.1:3020/disney',
|
|
279
|
+
function(err, resp, data, perf) {
|
|
280
|
+
test.ok( !err, "No error from PixlRequest: " + err );
|
|
281
|
+
test.ok( !!resp, "Got resp from PixlRequest" );
|
|
282
|
+
test.ok( resp.statusCode == 302, "Got 302 response: " + resp.statusCode );
|
|
283
|
+
test.ok( !!resp.headers['location'], "Got Location header" );
|
|
284
|
+
test.ok( !!resp.headers['location'].match(/disney\.com/), "Correct Location header");
|
|
285
|
+
test.done();
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
function testAdvancedURLRedirect(test) {
|
|
291
|
+
// more complex redirect config (301, custom header)
|
|
292
|
+
request.get( 'http://127.0.0.1:3020/pixar/toads',
|
|
293
|
+
function(err, resp, data, perf) {
|
|
294
|
+
test.ok( !err, "No error from PixlRequest: " + err );
|
|
295
|
+
test.ok( !!resp, "Got resp from PixlRequest" );
|
|
296
|
+
test.ok( resp.statusCode == 301, "Got 301 response: " + resp.statusCode );
|
|
297
|
+
test.ok( !!resp.headers['location'], "Got Location header" );
|
|
298
|
+
test.ok( !!resp.headers['location'].match(/pixar\.com\/toads/), "Correct Location header");
|
|
299
|
+
test.ok( !!resp.headers['x-animal'], "Got x-animal header" );
|
|
300
|
+
test.ok( !!resp.headers['x-animal'].match(/frog/i), "Correct x-animal header");
|
|
301
|
+
test.done();
|
|
302
|
+
}
|
|
303
|
+
);
|
|
304
|
+
},
|
|
305
|
+
|
|
196
306
|
function testHTTPAltPort(test) {
|
|
197
307
|
// test simple HTTP GET request to webserver backend, alternate port
|
|
198
308
|
request.json( 'http://127.0.0.1:3120/json', false,
|
|
@@ -1360,7 +1470,7 @@ module.exports = {
|
|
|
1360
1470
|
},
|
|
1361
1471
|
|
|
1362
1472
|
// redirect
|
|
1363
|
-
function
|
|
1473
|
+
function testRedirectHandler(test) {
|
|
1364
1474
|
request.get( 'http://127.0.0.1:3020/redirect',
|
|
1365
1475
|
function(err, resp, data, perf) {
|
|
1366
1476
|
test.ok( !err, "No error from PixlRequest: " + err );
|
|
@@ -1522,6 +1632,53 @@ module.exports = {
|
|
|
1522
1632
|
);
|
|
1523
1633
|
},
|
|
1524
1634
|
|
|
1635
|
+
// blacklist
|
|
1636
|
+
function testBlacklistedIP(test) {
|
|
1637
|
+
request.get( 'http://127.0.0.1:3020/json',
|
|
1638
|
+
{
|
|
1639
|
+
headers: {
|
|
1640
|
+
"X-Forwarded-For": "5.6.7.8" // blacklisted
|
|
1641
|
+
}
|
|
1642
|
+
},
|
|
1643
|
+
function(err, resp, data, perf) {
|
|
1644
|
+
test.ok( !err, "No error from PixlRequest: " + err );
|
|
1645
|
+
test.ok( !!resp, "Got resp from PixlRequest" );
|
|
1646
|
+
test.ok( resp.statusCode == 403, "Got 403 response: " + resp.statusCode );
|
|
1647
|
+
test.done();
|
|
1648
|
+
}
|
|
1649
|
+
);
|
|
1650
|
+
},
|
|
1651
|
+
function testAnotherBlacklistedIP(test) {
|
|
1652
|
+
request.get( 'http://127.0.0.1:3020/json',
|
|
1653
|
+
{
|
|
1654
|
+
headers: {
|
|
1655
|
+
"X-Forwarded-For": "1.2.3.4, 5.6.7.255, 2.3.4.5" // blacklisted
|
|
1656
|
+
}
|
|
1657
|
+
},
|
|
1658
|
+
function(err, resp, data, perf) {
|
|
1659
|
+
test.ok( !err, "No error from PixlRequest: " + err );
|
|
1660
|
+
test.ok( !!resp, "Got resp from PixlRequest" );
|
|
1661
|
+
test.ok( resp.statusCode == 403, "Got 403 response: " + resp.statusCode );
|
|
1662
|
+
test.done();
|
|
1663
|
+
}
|
|
1664
|
+
);
|
|
1665
|
+
},
|
|
1666
|
+
function testAllowedIP(test) {
|
|
1667
|
+
request.get( 'http://127.0.0.1:3020/json',
|
|
1668
|
+
{
|
|
1669
|
+
headers: {
|
|
1670
|
+
"X-Forwarded-For": "5.6.8.7" // just outside blacklisted range
|
|
1671
|
+
}
|
|
1672
|
+
},
|
|
1673
|
+
function(err, resp, data, perf) {
|
|
1674
|
+
test.ok( !err, "No error from PixlRequest: " + err );
|
|
1675
|
+
test.ok( !!resp, "Got resp from PixlRequest" );
|
|
1676
|
+
test.ok( resp.statusCode == 200, "Got 200 response: " + resp.statusCode );
|
|
1677
|
+
test.done();
|
|
1678
|
+
}
|
|
1679
|
+
);
|
|
1680
|
+
},
|
|
1681
|
+
|
|
1525
1682
|
function testConditionalResponseHeaders(test) {
|
|
1526
1683
|
// test response headers per http code
|
|
1527
1684
|
var self = this;
|
package/web_server.js
CHANGED
|
@@ -46,6 +46,7 @@ module.exports = Class({
|
|
|
46
46
|
"http_compress_text": false,
|
|
47
47
|
"http_enable_brotli": false,
|
|
48
48
|
"http_default_acl": ['127.0.0.1', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16', '::1/128', 'fd00::/8', '169.254.0.0/16', 'fe80::/10'],
|
|
49
|
+
"http_blacklist": [],
|
|
49
50
|
"http_log_requests": false,
|
|
50
51
|
"http_log_request_details": false,
|
|
51
52
|
"http_log_body_max": 32768,
|
|
@@ -99,8 +100,49 @@ class WebServer extends Component {
|
|
|
99
100
|
this.uriFilters = [];
|
|
100
101
|
this.uriHandlers = [];
|
|
101
102
|
this.methodHandlers = [];
|
|
102
|
-
this.
|
|
103
|
-
this.
|
|
103
|
+
this.stats = { current: {}, last: {} };
|
|
104
|
+
this.recent = [];
|
|
105
|
+
|
|
106
|
+
this.prepConfig();
|
|
107
|
+
this.config.on('reload', this.prepConfig.bind(this) );
|
|
108
|
+
|
|
109
|
+
// setup queue to handle all requests
|
|
110
|
+
// if both max concurrent req AND max connections are not set, just use a very large number
|
|
111
|
+
this.queue = async.queue( this.parseHTTPRequest.bind(this), this.maxConcurrentReqs || 8192 );
|
|
112
|
+
|
|
113
|
+
// listen for tick events to swap stat buffers
|
|
114
|
+
this.server.on( 'tick', this.tick.bind(this) );
|
|
115
|
+
|
|
116
|
+
// start listeners
|
|
117
|
+
this.startAll(callback);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
prepConfig() {
|
|
121
|
+
// prep config at startup, and when config is hot reloaded
|
|
122
|
+
try {
|
|
123
|
+
this.defaultACL = new ACL( this.config.get('http_default_acl') );
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
this.logError('acl', "Failed to initialize default ACL: " + err);
|
|
127
|
+
this.defaultACL = new ACL();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
this.aclBlacklist = new ACL( this.config.get('http_blacklist') );
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
this.logError('acl', "Failed to initialize blacklist: " + err);
|
|
135
|
+
this.aclBlacklist = new ACL();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
this.aclPrivateRanges = new ACL( this.config.get('http_private_ip_ranges') );
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
this.logError('acl', "Failed to initialize private range ACL: " + err);
|
|
143
|
+
this.aclPrivateRanges = new ACL();
|
|
144
|
+
}
|
|
145
|
+
|
|
104
146
|
this.regexTextContent = new RegExp( this.config.get('http_regex_text'), "i" );
|
|
105
147
|
this.regexJSONContent = new RegExp( this.config.get('http_regex_json'), "i" );
|
|
106
148
|
this.logRequests = this.config.get('http_log_requests');
|
|
@@ -111,8 +153,6 @@ class WebServer extends Component {
|
|
|
111
153
|
this.logPerfThreshold = this.config.get('http_perf_threshold_ms');
|
|
112
154
|
this.logPerfReport = this.config.get('http_perf_report');
|
|
113
155
|
this.keepRecentRequests = this.config.get('http_recent_requests');
|
|
114
|
-
this.stats = { current: {}, last: {} };
|
|
115
|
-
this.recent = [];
|
|
116
156
|
|
|
117
157
|
// optionally compress text
|
|
118
158
|
this.compressText = this.config.get('http_compress_text') || this.config.get('http_gzip_text');
|
|
@@ -151,18 +191,15 @@ class WebServer extends Component {
|
|
|
151
191
|
|
|
152
192
|
// optional max requests per KA connection
|
|
153
193
|
this.maxReqsPerConn = this.config.get('http_max_requests_per_connection');
|
|
154
|
-
|
|
155
|
-
// setup queue to handle all requests
|
|
156
|
-
this.maxConcurrentReqs = this.config.get('http_max_concurrent_requests') || this.config.get('http_max_connections');
|
|
157
194
|
this.maxQueueLength = this.config.get('http_max_queue_length');
|
|
158
195
|
this.maxQueueActive = this.config.get('http_max_queue_active');
|
|
159
196
|
|
|
197
|
+
// NOTE: changing maxConcurrentReqs at runtime has no effect
|
|
198
|
+
this.maxConcurrentReqs = this.config.get('http_max_concurrent_requests') || this.config.get('http_max_connections');
|
|
199
|
+
|
|
160
200
|
this.queueSkipMatch = this.config.get('http_queue_skip_uri_match') ?
|
|
161
201
|
new RegExp( this.config.get('http_queue_skip_uri_match') ) : false;
|
|
162
202
|
|
|
163
|
-
// if both max concurrent req AND max connections are not set, just use a very large number
|
|
164
|
-
this.queue = async.queue( this.parseHTTPRequest.bind(this), this.maxConcurrentReqs || 8192 );
|
|
165
|
-
|
|
166
203
|
// front-end https header detection
|
|
167
204
|
var ssl_headers = this.config.get('https_header_detect');
|
|
168
205
|
if (ssl_headers) {
|
|
@@ -171,20 +208,7 @@ class WebServer extends Component {
|
|
|
171
208
|
this.ssl_header_detect[ key.toLowerCase() ] = new RegExp( ssl_headers[key] );
|
|
172
209
|
}
|
|
173
210
|
}
|
|
174
|
-
|
|
175
|
-
// initialize default ACL blocks
|
|
176
|
-
if (this.config.get('http_default_acl')) {
|
|
177
|
-
try {
|
|
178
|
-
this.config.get('http_default_acl').forEach( function(block) {
|
|
179
|
-
self.defaultACL.add( block );
|
|
180
|
-
} );
|
|
181
|
-
}
|
|
182
|
-
catch (err) {
|
|
183
|
-
var err_msg = "Failed to initialize ACL: " + err.message;
|
|
184
|
-
this.logError('acl', err_msg);
|
|
185
|
-
throw new Error(err_msg);
|
|
186
|
-
}
|
|
187
|
-
}
|
|
211
|
+
else delete this.ssl_header_detect;
|
|
188
212
|
|
|
189
213
|
// initialize request max dump system, if enabled
|
|
190
214
|
this.reqMaxDumpEnabled = this.config.get('http_req_max_dump_enabled');
|
|
@@ -196,10 +220,29 @@ class WebServer extends Component {
|
|
|
196
220
|
fs.mkdirSync( this.reqMaxDumpDir, { mode: 0o777, recursive: true } );
|
|
197
221
|
}
|
|
198
222
|
|
|
199
|
-
//
|
|
200
|
-
this.
|
|
223
|
+
// url rewrites
|
|
224
|
+
this.rewrites = [];
|
|
225
|
+
if (this.config.get('http_rewrites')) {
|
|
226
|
+
var rewrite_map = this.config.get('http_rewrites');
|
|
227
|
+
for (var key in rewrite_map) {
|
|
228
|
+
var rewrite = rewrite_map[key];
|
|
229
|
+
if (typeof(rewrite) == 'string') rewrite = { url: rewrite_map[key] };
|
|
230
|
+
rewrite.regexp = new RegExp(key);
|
|
231
|
+
this.rewrites.push(rewrite);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
201
234
|
|
|
202
|
-
|
|
235
|
+
// url redirects
|
|
236
|
+
this.redirects = [];
|
|
237
|
+
if (this.config.get('http_redirects')) {
|
|
238
|
+
var redir_map = this.config.get('http_redirects');
|
|
239
|
+
for (var key in redir_map) {
|
|
240
|
+
var redirect = redir_map[key];
|
|
241
|
+
if (typeof(redirect) == 'string') redirect = { url: redir_map[key] };
|
|
242
|
+
redirect.regexp = new RegExp(key);
|
|
243
|
+
this.redirects.push(redirect);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
203
246
|
}
|
|
204
247
|
|
|
205
248
|
startAll(callback) {
|