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 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 dsiabled* 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:
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
- Typically this is sent as pure JSON with the Content-Type `application/json`. The raw HTTP response would look something like this:
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>&nbsp;</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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-server-web",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "A web server component for the pixl-server framework.",
5
5
  "author": "Joseph Huckaby <jhuckaby@gmail.com>",
6
6
  "homepage": "https://github.com/jhuckaby/pixl-server-web",
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 testRedirect(test) {
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.defaultACL = new ACL();
103
- this.aclPrivateRanges = new ACL( this.config.get('http_private_ip_ranges') );
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
- // listen for tick events to swap stat buffers
200
- this.server.on( 'tick', this.tick.bind(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
- this.startAll(callback);
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) {