pixl-server-web 2.0.1 → 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)
@@ -438,6 +441,108 @@ This example would reject all incoming IP addresses from Apple and AT&T (who own
438
441
 
439
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.
440
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
+
441
546
  ## http_log_requests
442
547
 
443
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.
@@ -450,7 +555,7 @@ This boolean adds verbose detail in the transaction log. It defaults to `false`
450
555
 
451
556
  ## http_log_body_max
452
557
 
453
- 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.
454
559
 
455
560
  **Note:** This property only has effect if [http_log_request_details](#http_log_request_details) is enabled.
456
561
 
package/lib/request.js CHANGED
@@ -175,6 +175,21 @@ module.exports = class Request {
175
175
  });
176
176
  this.logDebug(9, "Incoming HTTP Headers:", request.headers);
177
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
+
178
193
  // detect front-end https
179
194
  if (!request.headers.ssl && this.ssl_header_detect) {
180
195
  for (var key in this.ssl_header_detect) {
@@ -427,6 +442,7 @@ module.exports = class Request {
427
442
  // all filters complete
428
443
  // if a filter handled the response, we're done
429
444
  if (err === "ABORT") {
445
+ self.deleteUploadTempFiles(args);
430
446
  if (args.callback) {
431
447
  args.callback();
432
448
  delete args.callback;
@@ -447,6 +463,24 @@ module.exports = class Request {
447
463
  if (!this.config.get('http_full_uri_match')) uri = uri.replace(/\?.*$/, '');
448
464
  var handler = null;
449
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
+
450
484
  args.state = 'processing';
451
485
  args.perf.begin('process');
452
486
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-server-web",
3
- "version": "2.0.1",
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
@@ -56,6 +56,18 @@ var server = new PixlServer({
56
56
 
57
57
  "http_blacklist": ["5.6.7.0/24"],
58
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
+
59
71
  "https": 1,
60
72
  "https_port": 3021,
61
73
  "https_alt_ports": [3121],
@@ -197,6 +209,100 @@ module.exports = {
197
209
  );
198
210
  },
199
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
+
200
306
  function testHTTPAltPort(test) {
201
307
  // test simple HTTP GET request to webserver backend, alternate port
202
308
  request.json( 'http://127.0.0.1:3120/json', false,
@@ -1364,7 +1470,7 @@ module.exports = {
1364
1470
  },
1365
1471
 
1366
1472
  // redirect
1367
- function testRedirect(test) {
1473
+ function testRedirectHandler(test) {
1368
1474
  request.get( 'http://127.0.0.1:3020/redirect',
1369
1475
  function(err, resp, data, perf) {
1370
1476
  test.ok( !err, "No error from PixlRequest: " + err );
package/web_server.js CHANGED
@@ -219,6 +219,30 @@ class WebServer extends Component {
219
219
  if (this.reqMaxDumpEnabled && this.reqMaxDumpDir && !fs.existsSync(this.reqMaxDumpDir)) {
220
220
  fs.mkdirSync( this.reqMaxDumpDir, { mode: 0o777, recursive: true } );
221
221
  }
222
+
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
+ }
234
+
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
+ }
222
246
  }
223
247
 
224
248
  startAll(callback) {