pixl-server-web 1.2.4 → 1.3.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
@@ -1,6 +1,6 @@
1
1
  # Overview
2
2
 
3
- This module is a component for use in [pixl-server](https://www.npmjs.com/package/pixl-server). It implements a simple web server with support for both HTTP and HTTPS, serving static files, and hooks for adding custom URI handlers.
3
+ This module is a component for use in [pixl-server](https://www.github.com/jhuckaby/pixl-server). It implements a simple web server with support for both HTTP and HTTPS, serving static files, and hooks for adding custom URI handlers.
4
4
 
5
5
  # Table of Contents
6
6
 
@@ -25,6 +25,7 @@ This module is a component for use in [pixl-server](https://www.npmjs.com/packag
25
25
  + [request](#request)
26
26
  + [close](#close)
27
27
  * [http_keep_alive_timeout](#http_keep_alive_timeout)
28
+ * [http_socket_prelim_timeout](#http_socket_prelim_timeout)
28
29
  * [http_max_requests_per_connection](#http_max_requests_per_connection)
29
30
  * [http_gzip_opts](#http_gzip_opts)
30
31
  * [http_enable_brotli](#http_enable_brotli)
@@ -136,7 +137,7 @@ server.startup( function() {
136
137
  } );
137
138
  ```
138
139
 
139
- Notice how we are loading the [pixl-server](https://www.npmjs.com/package/pixl-server) parent module, and then specifying [pixl-server-web](https://www.npmjs.com/package/pixl-server-web) as a component:
140
+ Notice how we are loading the [pixl-server](https://www.github.com/jhuckaby/pixl-server) parent module, and then specifying [pixl-server-web](https://www.github.com/jhuckaby/pixl-server-web) as a component:
140
141
 
141
142
  ```js
142
143
  components: [
@@ -212,7 +213,7 @@ This param allows you to send back any additional custom HTTP headers with each
212
213
 
213
214
  ```js
214
215
  {
215
- http_response_headers: {
216
+ "http_response_headers": {
216
217
  "X-My-Custom-Header": "12345",
217
218
  "X-Another-Header": "Hello"
218
219
  }
@@ -223,6 +224,20 @@ This param allows you to send back any additional custom HTTP headers with each
223
224
 
224
225
  This sets the idle socket timeout for all incoming HTTP requests. If omitted, the Node.js default is 2 minutes. Please specify your value in seconds.
225
226
 
227
+ This only applies to reading from sockets when data is expected. It is an *idle read timeout* on the socket itself, and doesn't apply to request handlers.
228
+
229
+ ## http_request_timeout
230
+
231
+ This property sets an actual hard request timeout for all incoming requests. If the total combined request processing, handling and response time exceeds this value, specified in seconds, then the request is aborted and a `HTTP 408 Request Timeout` response is sent back to the client. This defaults to `0` (disabled). Example use:
232
+
233
+ ```js
234
+ {
235
+ "http_request_timeout": 300
236
+ }
237
+ ```
238
+
239
+ Note that this includes request processing time (e.g. receiving uploaded data from a HTTP POST).
240
+
226
241
  ## http_keep_alives
227
242
 
228
243
  This controls the [HTTP Keep-Alive](https://en.wikipedia.org/wiki/HTTP_persistent_connection) behavior in the web server. There are three possible settings, which should be specified as a string:
@@ -269,6 +284,20 @@ This sets the HTTP Keep-Alive idle timeout for all sockets. If omitted, the Nod
269
284
 
270
285
  This feature was introduced in Node.js version 8. Prior to that, the [http_timeout](#http_timeout) was used as the Keep-Alive timeout.
271
286
 
287
+ ## http_socket_prelim_timeout
288
+
289
+ This sets a special preliminary timeout for brand new sockets when they are first connected. If an HTTP request doesn't come over the socket within this timeout (specified in seconds), then the socket is hard closed. This timeout should always be set lower than the [http_timeout](#http_timeout) if used. This defaults to `0` (disabled). Example use:
290
+
291
+ ```js
292
+ {
293
+ "http_socket_prelim_timeout": 3
294
+ }
295
+ ```
296
+
297
+ The idea here is to prevent certain DDoS-style attacks, where an attacker opens a large amount of TCP connections without sending any requests over them.
298
+
299
+ **Note:** Do not enable this feature if you attach a WebSocket server such as [ws](https://github.com/websockets/ws).
300
+
272
301
  ## http_max_requests_per_connection
273
302
 
274
303
  This allows you to set a maximum number of requests to allow per Keep-Alive connection. It defaults to `0` which means unlimited. If set, and the maximum is reached, a `Connection: close` header is returned, politely asking the client to close the connection. It does not actually hard-close the socket. Example:
@@ -508,7 +537,7 @@ server.WebServer.addURIHandler( /^\/custom\/match\/$/i, 'Custom2', function(args
508
537
 
509
538
  Your handler function is passed exactly two arguments. First, an `args` object containing all kinds of useful information about the request (see [args](#args) below), and a callback function that you must call when the request is complete and you want to send a response.
510
539
 
511
- If you specified a regular expression with paren groups for the URI, the matches array will be included in the `args` object as `args.matches`. Using this you can extract your matched groups from the URI, for e.g. `/^\/api\/(\w+)/`.
540
+ If you specified a regular expression with parenthesis groups for the URI, the matches array will be included in the `args` object as `args.matches`. Using this you can extract your matched groups from the URI, for e.g. `/^\/api\/(\w+)/`.
512
541
 
513
542
  Note that by default, URIs are only matched on their path portion (i.e. sans query string). To include the query string in URI matches, set the [http_full_uri_match](#http_full_uri_match) configuration property to `true`.
514
543
 
@@ -776,12 +805,16 @@ var session_id = args.cookies['session_id'];
776
805
 
777
806
  ### args.perf
778
807
 
779
- This is a reference to a [pixl-perf](https://www.npmjs.com/package/pixl-perf) object, which is used internally by the web server to track performance metrics for the request. The metrics may be logged at the end of each request (see [Logging](#logging) below) and included in the stats (see [Stats](#stats) below).
808
+ This is a reference to a [pixl-perf](https://www.github.com/jhuckaby/pixl-perf) object, which is used internally by the web server to track performance metrics for the request. The metrics may be logged at the end of each request (see [Logging](#logging) below) and included in the stats (see [Stats](#stats) below).
780
809
 
781
810
  ### args.server
782
811
 
783
812
  This is a reference to the pixl-server object which handled the request.
784
813
 
814
+ ### args.id
815
+
816
+ This is an internal ID string used by the server to track and log individual requests.
817
+
785
818
  ## Request Filters
786
819
 
787
820
  Filters allow you to preprocess a request, before any handlers get their hands on it. They can pass data through, manipulate it, or even interrupt and abort requests. Filters are attached to particular URIs or URI patterns, and multiple may be applied to one request, depending on your rules. They can be asynchronous, and can also pass data between one another if desired.
@@ -885,7 +918,7 @@ Here are descriptions of the data JSON properties:
885
918
  | `host` | The hostname from the request URL. |
886
919
  | `perf` | Performance metrics, see below. |
887
920
 
888
- The `perf` object contains performance metrics for the request, as returned from the [pixl-perf](https://www.npmjs.com/package/pixl-perf) module. It includes a `scale` property denoting that all the metrics are displayed in milliseconds (i.e. `1000`). The metrics themselves are in the `perf` object, and counters such as the number of bytes in/out are in the `counters` object.
921
+ The `perf` object contains performance metrics for the request, as returned from the [pixl-perf](https://www.github.com/jhuckaby/pixl-perf) module. It includes a `scale` property denoting that all the metrics are displayed in milliseconds (i.e. `1000`). The metrics themselves are in the `perf` object, and counters such as the number of bytes in/out are in the `counters` object.
889
922
 
890
923
  If you only want to log *some* requests, but not all of them, you can specify a regular expression in the [http_regex_log](#http_regex_log) configuration property, which is matched against the incoming request URIs. Example:
891
924
 
@@ -1099,7 +1132,7 @@ The `recent` array is a sorted list of the last 10 completed requests (most rece
1099
1132
  | `host` | String | The hostname from the request URL. |
1100
1133
  | `ips` | Array | The array of client IPs, including proxy IPs. |
1101
1134
  | `ua` | String | The client's `User-Agent` string. |
1102
- | `perf` | Object | A [pixl-perf](https://www.npmjs.com/package/pixl-perf) performance metrics object containing stats for the request. |
1135
+ | `perf` | Object | A [pixl-perf](https://www.github.com/jhuckaby/pixl-perf) performance metrics object containing stats for the request. |
1103
1136
 
1104
1137
  If you would like more than 10 requests, set the [http_recent_requests](#http_recent_requests) configuration property to the number you want.
1105
1138
 
@@ -1114,7 +1147,7 @@ The `queue` object contains information about the request queue. This includes
1114
1147
 
1115
1148
  ## Including Custom Stats
1116
1149
 
1117
- To include your own application-level metrics in the `getStats()` output, a [pixl-perf](https://www.npmjs.com/package/pixl-perf) performance tracker is made available to your URI handler code via `args.perf`. you can call `begin()` and `end()` on this object directly, to measure your own operations:
1150
+ To include your own application-level metrics in the `getStats()` output, a [pixl-perf](https://www.github.com/jhuckaby/pixl-perf) performance tracker is made available to your URI handler code via `args.perf`. you can call `begin()` and `end()` on this object directly, to measure your own operations:
1118
1151
 
1119
1152
  ```js
1120
1153
  server.WebServer.addURIHandler( '/my/custom/uri', 'Custom Name', function(args, callback) {
@@ -1135,7 +1168,7 @@ server.WebServer.addURIHandler( '/my/custom/uri', 'Custom Name', function(args,
1135
1168
 
1136
1169
  Please do not call `begin()` or `end()` without arguments, as that will mess up the existing performance tracking. Also, make sure you prefix your perf keys so you don't collide with the built-in ones.
1137
1170
 
1138
- Alternatively, you can use your own private [pixl-perf](https://www.npmjs.com/package/pixl-perf) object, and then "import" it into the `args.perf` object at the very end of your handler code, just before you fire the callback. Example:
1171
+ Alternatively, you can use your own private [pixl-perf](https://www.github.com/jhuckaby/pixl-perf) object, and then "import" it into the `args.perf` object at the very end of your handler code, just before you fire the callback. Example:
1139
1172
 
1140
1173
  ```js
1141
1174
  my_perf.end();
@@ -1144,7 +1177,7 @@ args.perf.import( my_perf, "app_" );
1144
1177
 
1145
1178
  This would import all your metrics and prefix the keys with `app_`.
1146
1179
 
1147
- See the [pixl-perf](https://www.npmjs.com/package/pixl-perf) documentation for more details on how to use the tracker.
1180
+ See the [pixl-perf](https://www.github.com/jhuckaby/pixl-perf) documentation for more details on how to use the tracker.
1148
1181
 
1149
1182
  ## Stats URI Handler
1150
1183
 
@@ -1281,7 +1314,7 @@ Certbot produces its own log file here: `/var/log/letsencrypt/letsencrypt.log`
1281
1314
 
1282
1315
  **The MIT License (MIT)**
1283
1316
 
1284
- *Copyright (c) 2015 - 2019 Joseph Huckaby.*
1317
+ *Copyright (c) 2015 - 2021 Joseph Huckaby.*
1285
1318
 
1286
1319
  Permission is hereby granted, free of charge, to any person obtaining a copy
1287
1320
  of this software and associated documentation files (the "Software"), to deal
package/lib/http.js CHANGED
@@ -14,11 +14,17 @@ module.exports = class HTTP {
14
14
  var port = this.config.get('http_port');
15
15
  var bind_addr = this.config.get('http_bind_address') || '';
16
16
  var max_conns = this.config.get('http_max_connections') || 0;
17
+ var https_force = self.config.get('https_force') || false;
18
+ var socket_prelim_timeout = self.config.get('http_socket_prelim_timeout') || 0;
17
19
 
18
20
  this.logDebug(2, "Starting HTTP server on port: " + port, bind_addr);
19
21
 
20
22
  var handler = function(request, response) {
21
- if (self.config.get('https_force')) {
23
+ if (socket_prelim_timeout && request.socket._pixl_data.prelim_timer) {
24
+ clearTimeout( request.socket._pixl_data.prelim_timer );
25
+ delete request.socket._pixl_data.prelim_timer;
26
+ }
27
+ if (https_force) {
22
28
  self.logDebug(6, "Forcing redirect to HTTPS (SSL)");
23
29
  request.headers.ssl = 1; // force SSL url
24
30
 
@@ -87,6 +93,31 @@ module.exports = class HTTP {
87
93
  bytes_out: 0
88
94
  };
89
95
 
96
+ // optional preliminary socket timeout for first request
97
+ if (socket_prelim_timeout) {
98
+ socket._pixl_data.prelim_timer = setTimeout( function() {
99
+ delete socket._pixl_data.prelim_timer;
100
+ var msg = "Socket preliminary timeout waiting for initial request (" + socket_prelim_timeout + " seconds)";
101
+ var err_args = {
102
+ ip: ip,
103
+ pending: self.queue.length(),
104
+ active: self.queue.running(),
105
+ sockets: self.numConns
106
+ };
107
+ if (self.config.get('http_log_socket_errors')) {
108
+ self.logError('socket', "Socket error: " + socket._pixl_data.id + ": " + msg, err_args);
109
+ }
110
+ else {
111
+ self.logDebug(5, "Socket error: " + socket._pixl_data.id + ": " + msg, err_args);
112
+ }
113
+
114
+ socket._pixl_data.aborted = true;
115
+ socket.end();
116
+ socket.unref();
117
+ socket.destroy(); // hard close
118
+ }, socket_prelim_timeout * 1000 );
119
+ } // socket_prelim_timeout
120
+
90
121
  self.emit('socket', socket);
91
122
 
92
123
  socket.on('error', function(err) {
@@ -116,6 +147,10 @@ module.exports = class HTTP {
116
147
 
117
148
  socket.on('close', function() {
118
149
  // socket has closed
150
+ if (socket._pixl_data.prelim_timer) {
151
+ clearTimeout( socket._pixl_data.prelim_timer );
152
+ delete socket._pixl_data.prelim_timer;
153
+ }
119
154
  var now = (new Date()).getTime();
120
155
  self.logDebug(8, "HTTP connection has closed: " + id, {
121
156
  ip: ip,
@@ -126,6 +161,7 @@ module.exports = class HTTP {
126
161
  });
127
162
  delete self.conns[ id ];
128
163
  self.numConns--;
164
+ socket._pixl_data.aborted = true;
129
165
  } );
130
166
  } );
131
167
 
@@ -140,6 +176,7 @@ module.exports = class HTTP {
140
176
  }
141
177
 
142
178
  var err_args = {
179
+ id: args.id,
143
180
  ip: socket.remoteAddress,
144
181
  ips: args.ips,
145
182
  state: args.state,
package/lib/https.js CHANGED
@@ -24,10 +24,16 @@ module.exports = class HTTP2 {
24
24
  var port = this.config.get('https_port');
25
25
  var bind_addr = this.config.get('https_bind_address') || this.config.get('http_bind_address') || '';
26
26
  var max_conns = this.config.get('https_max_connections') || this.config.get('http_max_connections') || 0;
27
+ var socket_prelim_timeout = self.config.get('https_socket_prelim_timeout') || self.config.get('http_socket_prelim_timeout') || 0;
27
28
 
28
29
  this.logDebug(2, "Starting HTTPS (SSL) server on port: " + port, bind_addr );
29
30
 
30
31
  var handler = function(request, response) {
32
+ if (socket_prelim_timeout && request.socket._pixl_data.prelim_timer) {
33
+ clearTimeout( request.socket._pixl_data.prelim_timer );
34
+ delete request.socket._pixl_data.prelim_timer;
35
+ }
36
+
31
37
  // add a flag in headers for downstream code to detect
32
38
  request.headers['ssl'] = 1;
33
39
  request.headers['https'] = 1;
@@ -85,6 +91,31 @@ module.exports = class HTTP2 {
85
91
  bytes_out: 0
86
92
  };
87
93
 
94
+ // optional preliminary socket timeout for first request
95
+ if (socket_prelim_timeout) {
96
+ socket._pixl_data.prelim_timer = setTimeout( function() {
97
+ delete socket._pixl_data.prelim_timer;
98
+ var msg = "Socket preliminary timeout waiting for initial request (" + socket_prelim_timeout + " seconds)";
99
+ var err_args = {
100
+ ip: ip,
101
+ pending: self.queue.length(),
102
+ active: self.queue.running(),
103
+ sockets: self.numConns
104
+ };
105
+ if (self.config.get('http_log_socket_errors')) {
106
+ self.logError('socket', "Socket error: " + socket._pixl_data.id + ": " + msg, err_args);
107
+ }
108
+ else {
109
+ self.logDebug(5, "Socket error: " + socket._pixl_data.id + ": " + msg, err_args);
110
+ }
111
+
112
+ socket._pixl_data.aborted = true;
113
+ socket.end();
114
+ socket.unref();
115
+ socket.destroy(); // hard close
116
+ }, socket_prelim_timeout * 1000 );
117
+ } // socket_prelim_timeout
118
+
88
119
  self.emit('socket', socket);
89
120
 
90
121
  socket.on('error', function(err) {
@@ -114,6 +145,10 @@ module.exports = class HTTP2 {
114
145
 
115
146
  socket.on('close', function() {
116
147
  // socket has closed
148
+ if (socket._pixl_data.prelim_timer) {
149
+ clearTimeout( socket._pixl_data.prelim_timer );
150
+ delete socket._pixl_data.prelim_timer;
151
+ }
117
152
  var now = (new Date()).getTime();
118
153
  self.logDebug(8, "HTTPS (SSL) connection has closed: " + id, {
119
154
  ip: ip,
@@ -124,6 +159,7 @@ module.exports = class HTTP2 {
124
159
  });
125
160
  delete self.conns[ id ];
126
161
  self.numConns--;
162
+ socket._pixl_data.aborted = true;
127
163
  } );
128
164
  } );
129
165
 
@@ -138,6 +174,7 @@ module.exports = class HTTP2 {
138
174
  }
139
175
 
140
176
  var err_args = {
177
+ id: args.id,
141
178
  ip: socket.remoteAddress,
142
179
  ips: args.ips,
143
180
  state: args.state,
package/lib/request.js CHANGED
@@ -13,6 +13,8 @@ module.exports = class Request {
13
13
  enqueueHTTPRequest(request, response) {
14
14
  // enqueue request for handling as soon as concurrency limits allow
15
15
  var args = {
16
+ id: this.getNextId('r'),
17
+ date: Date.now() / 1000,
16
18
  request: request,
17
19
  response: response
18
20
  };
@@ -23,7 +25,7 @@ module.exports = class Request {
23
25
  var ip = args.ip = this.getPublicIP(ips);
24
26
 
25
27
  this.logError(503, "Server is shutting down, denying request from: " + ip,
26
- { ips: ips, uri: request.url, headers: request.headers }
28
+ { id: args.id, ips: ips, uri: request.url, headers: request.headers }
27
29
  );
28
30
 
29
31
  args.perf = new Perf();
@@ -35,6 +37,7 @@ module.exports = class Request {
35
37
  // allow special URIs to skip the line
36
38
  if (this.queueSkipMatch && request.url.match(this.queueSkipMatch)) {
37
39
  this.logDebug(8, "Bumping request to front of queue: " + request.url);
40
+ this.requests[ args.id ] = args;
38
41
  this.queue.unshift(args);
39
42
  return;
40
43
  }
@@ -45,6 +48,7 @@ module.exports = class Request {
45
48
  var ip = args.ip = this.getPublicIP(ips);
46
49
 
47
50
  this.logError(429, "Queue is maxed out (" + this.queue.running() + " active reqs), denying new request from: " + ip, {
51
+ id: args.id,
48
52
  ips: ips,
49
53
  uri: request.url,
50
54
  headers: request.headers,
@@ -65,6 +69,7 @@ module.exports = class Request {
65
69
  var ip = args.ip = this.getPublicIP(ips);
66
70
 
67
71
  this.logError(429, "Queue is maxed out (" + this.queue.length() + " pending reqs), denying new request from: " + ip, {
72
+ id: args.id,
68
73
  ips: ips,
69
74
  uri: request.url,
70
75
  headers: request.headers,
@@ -79,15 +84,47 @@ module.exports = class Request {
79
84
  return;
80
85
  }
81
86
 
87
+ this.requests[ args.id ] = args;
82
88
  this.queue.push(args);
83
89
  }
84
90
 
85
91
  parseHTTPRequest(args, callback) {
86
92
  // handle raw http request
93
+ // (async dequeue handler function)
87
94
  var self = this;
88
95
  var request = args.request;
89
96
  var response = args.response;
90
- args.callback = callback;
97
+
98
+ // all requests will end up in this callback here
99
+ args.callback = function() {
100
+ if (args.timer) { clearTimeout(args.timer); delete args.timer; }
101
+ delete self.requests[ args.id ];
102
+ callback();
103
+ };
104
+
105
+ // add timer for request timeout
106
+ if (this.config.get('http_request_timeout')) {
107
+ args.timer = setTimeout( function() {
108
+ // request took too long
109
+ delete args.timer;
110
+
111
+ self.logError(408, "Request timed out: " + self.config.get('http_request_timeout') + " seconds", {
112
+ id: args.id,
113
+ socket: request.socket._pixl_data.id,
114
+ ips: args.ips,
115
+ url: self.getSelfURL(args.request, request.url) || request.url,
116
+ state: args.state
117
+ });
118
+
119
+ self.sendHTTPResponse( args,
120
+ "408 Request Timeout",
121
+ { 'Content-Type': "text/html" },
122
+ "408 Request Timeout: " + self.config.get('http_request_timeout') + " seconds.\n"
123
+ );
124
+
125
+ self.deleteUploadTempFiles(args);
126
+ }, this.config.get('http_request_timeout') * 1000 );
127
+ }
91
128
 
92
129
  // check for early abort (client error)
93
130
  if (request.socket._pixl_data.aborted) {
@@ -102,6 +139,7 @@ module.exports = class Request {
102
139
  var ip = this.getPublicIP(ips);
103
140
 
104
141
  this.logDebug(8, "New HTTP request: " + request.method + " " + request.url + " (" + ips.join(', ') + ")", {
142
+ id: args.id,
105
143
  socket: request.socket._pixl_data.id,
106
144
  version: request.httpVersion
107
145
  });
@@ -156,7 +194,7 @@ module.exports = class Request {
156
194
  if (this.server.shut) {
157
195
  // server is shutting down, deny new requests
158
196
  this.logError(503, "Server is shutting down, denying request from: " + ip,
159
- { ips: ips, uri: request.url, headers: request.headers }
197
+ { id: args.id, ips: ips, uri: request.url, headers: request.headers }
160
198
  );
161
199
  this.sendHTTPResponse( args, "503 Service Unavailable", {}, "503 Service Unavailable (server shutting down)" );
162
200
  return;
@@ -200,7 +238,7 @@ module.exports = class Request {
200
238
  args.perf.end('read');
201
239
  if (err) {
202
240
  self.logError(400, "Error processing data from: " + ip + ": " + request.url + ": " + err,
203
- { ips: ips, uri: request.url, headers: request.headers }
241
+ { id: args.id, ips: ips, uri: request.url, headers: request.headers }
204
242
  );
205
243
  self.sendHTTPResponse( args, "400 Bad Request", {}, "400 Bad Request" );
206
244
  return;
@@ -230,7 +268,7 @@ module.exports = class Request {
230
268
  });
231
269
  if (total_bytes > bytesMax) {
232
270
  self.logError(413, "Error processing data from: " + ip + ": " + request.url + ": Max data size exceeded",
233
- { ips: ips, uri: request.url, headers: request.headers }
271
+ { id: args.id, ips: ips, uri: request.url, headers: request.headers }
234
272
  );
235
273
  request.socket.end();
236
274
 
@@ -253,7 +291,7 @@ module.exports = class Request {
253
291
  }
254
292
  catch (e) {
255
293
  self.logError(400, "Error processing data from: " + ip + ": " + request.url + ": Failed to parse JSON: " + e,
256
- { ips: ips, uri: request.url, headers: request.headers, body: body.toString() }
294
+ { id: args.id, ips: ips, uri: request.url, headers: request.headers, body: body.toString() }
257
295
  );
258
296
  self.sendHTTPResponse( args, "400 Bad Request", {}, "400 Bad Request" );
259
297
  return;
@@ -300,7 +338,7 @@ module.exports = class Request {
300
338
  // use async to allow filters to run in sequence
301
339
  async.eachSeries( filters,
302
340
  function(filter, callback) {
303
- self.logDebug(8, "Invoking filter for request: " + args.request.method + ' ' + uri + ": " + filter.name);
341
+ self.logDebug(8, "Invoking filter for request: " + args.request.method + ' ' + uri + ": " + filter.name, { id: args.id });
304
342
 
305
343
  args.perf.begin('filter');
306
344
  filter.callback( args, function() {
@@ -381,7 +419,7 @@ module.exports = class Request {
381
419
  }
382
420
 
383
421
  if (handler) {
384
- this.logDebug(6, "Invoking handler for request: " + args.request.method + ' ' + uri + ": " + handler.name);
422
+ this.logDebug(6, "Invoking handler for request: " + args.request.method + ' ' + uri + ": " + handler.name, { id: args.id });
385
423
 
386
424
  // Check ACL here
387
425
  if (handler.acl) {
@@ -392,6 +430,7 @@ module.exports = class Request {
392
430
  else {
393
431
  // nope
394
432
  this.logError(403, "Forbidden: IP addresses rejected by ACL: " + args.ips.join(', '), {
433
+ id: args.id,
395
434
  acl: handler.acl.toString(),
396
435
  useragent: args.request.headers['user-agent'] || '',
397
436
  referrer: args.request.headers['referer'] || '',
package/lib/response.js CHANGED
@@ -28,6 +28,7 @@ module.exports = class Response {
28
28
  socket_data.total_elapsed = (new Date()).getTime() - socket_data.time_start;
29
29
  socket_data.url = this.getSelfURL(request, request.url) || request.url;
30
30
  socket_data.ips = args.ips;
31
+ socket_data.req_id = args.id;
31
32
  if (this.config.get('http_log_socket_errors')) {
32
33
  this.logError('socket', "Socket closed unexpectedly: " + socket_data.id, socket_data);
33
34
  }
@@ -98,7 +99,7 @@ module.exports = class Response {
98
99
 
99
100
  response.on('finish', function() {
100
101
  // response actually completed writing
101
- self.logDebug(9, "Response finished writing to socket");
102
+ self.logDebug(9, "Response finished writing to socket", { id: args.id });
102
103
 
103
104
  // guess number of bytes in response header, minus data payload
104
105
  args.perf.count('bytes_out', ("HTTP " + args.http_code + " OK\r\n").length);
@@ -120,6 +121,7 @@ module.exports = class Response {
120
121
  // socket closed during active response
121
122
  if (self.config.get('http_log_socket_errors')) {
122
123
  self.logError('socket', "Socket connection terminated unexpectedly during response", {
124
+ id: args.id,
123
125
  ips: args.ips,
124
126
  useragent: request.headers['user-agent'] || '',
125
127
  referrer: request.headers['referer'] || '',
@@ -136,6 +138,7 @@ module.exports = class Response {
136
138
  if (is_stream) {
137
139
  body.on('error', function(err) {
138
140
  self.logError('stream', "Stream error serving response: " + request.url + ": " + err.message, {
141
+ id: args.id,
139
142
  ips: args.ips,
140
143
  useragent: request.headers['user-agent'] || '',
141
144
  referrer: request.headers['referer'] || '',
@@ -260,7 +263,7 @@ module.exports = class Response {
260
263
  response.end();
261
264
  }
262
265
  }
263
- this.logDebug(9, "Request complete");
266
+ this.logDebug(9, "Request complete", { id: args.id });
264
267
  }
265
268
  }
266
269
 
@@ -301,6 +304,7 @@ module.exports = class Response {
301
304
  // write to access log
302
305
  if (this.logRequests && args.request.url.match(this.regexLogRequests)) {
303
306
  this.logTransaction( 'HTTP ' + args.http_code + ' ' + args.http_status, args.request.url, {
307
+ id: args.id,
304
308
  proto: args.request.headers['ssl'] ? 'https' : socket_data.proto,
305
309
  ips: args.ips,
306
310
  host: args.request.headers['host'] || '',
@@ -312,6 +316,7 @@ module.exports = class Response {
312
316
  // keep a list of the most recent N requests
313
317
  if (this.keepRecentRequests) {
314
318
  this.recent.unshift({
319
+ id: args.id,
315
320
  when: (new Date()).getTime() / 1000,
316
321
  proto: args.request.headers['ssl'] ? 'https' : socket_data.proto,
317
322
  port: socket_data.port,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-server-web",
3
- "version": "1.2.4",
3
+ "version": "1.3.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
@@ -2,9 +2,8 @@
2
2
  // Copyright (c) 2017 - 2019 Joseph Huckaby
3
3
  // Released under the MIT License
4
4
 
5
- var os = require('os');
6
5
  var fs = require('fs');
7
- var path = require('path');
6
+ var net = require('net');
8
7
  var crypto = require('crypto');
9
8
  var async = require('async');
10
9
 
@@ -41,6 +40,7 @@ var server = new PixlServer({
41
40
  "http_compress_text": 1,
42
41
  "http_enable_brotli": 1,
43
42
  "http_timeout": 5,
43
+ "http_socket_prelim_timeout": 2,
44
44
  "http_response_headers": {
45
45
  "Via": "WebServerTest 1.0"
46
46
  },
@@ -707,6 +707,22 @@ module.exports = {
707
707
  );
708
708
  },
709
709
 
710
+ // Error (Back-end Timeout)
711
+ function testBackEndTimeout(test) {
712
+ var self = this;
713
+ var web = this.web_server;
714
+ web.config.set('http_request_timeout', 0.5); // 500ms
715
+
716
+ request.get( 'http://127.0.0.1:3020/sleep?ms=750', {},
717
+ function(err, resp, data, perf) {
718
+ web.config.set('http_request_timeout', 0); // reset timeout
719
+ test.ok( !err, "Unexpected error from PixlRequest: " + err );
720
+ test.ok( resp.statusCode == 408, "Unexpected HTTP response code: " + resp.statusCode );
721
+ test.done();
722
+ }
723
+ );
724
+ },
725
+
710
726
  // static file get
711
727
  // check ttl, check gzip
712
728
  function testStaticTextRequest(test) {
@@ -1022,6 +1038,25 @@ module.exports = {
1022
1038
  );
1023
1039
  },
1024
1040
 
1041
+ // socket prelim timeout
1042
+ function testSocketPrelimTimeout(test) {
1043
+ var connected_time = 0;
1044
+ var client = net.connect({ port: 3020 }, function() {
1045
+ test.debug("Connected to port 3020 (raw socket)");
1046
+ connected_time = Date.now() / 1000;
1047
+ });
1048
+ client.on('data', function(data) {
1049
+ test.ok( false, "Should NOT have received any data from socket! " + data );
1050
+ });
1051
+ client.on('end', function() {
1052
+ test.debug("Raw socket disconnected");
1053
+ var now = Date.now() / 1000;
1054
+ var elapsed = now - connected_time;
1055
+ test.ok( Math.abs(elapsed - 2.0) < 1.0, "Incorrect time elapsed for socket prelim timeout: " + elapsed );
1056
+ test.done();
1057
+ });
1058
+ },
1059
+
1025
1060
  function waitForAllSockets(test) {
1026
1061
  // wait for all sockets to close for next test (requires clean slate)
1027
1062
  var self = this;
package/web_server.js CHANGED
@@ -56,7 +56,8 @@ module.exports = Class({
56
56
  http_queue_skip_uri_match: false,
57
57
  http_clean_headers: false,
58
58
  http_log_socket_errors: true,
59
- http_full_uri_match: false
59
+ http_full_uri_match: false,
60
+ http_request_timeout: 0
60
61
  },
61
62
 
62
63
  conns: null,
@@ -82,6 +83,7 @@ class WebServer extends Component {
82
83
 
83
84
  // setup connections and handlers
84
85
  this.conns = {};
86
+ this.requests = {};
85
87
  this.uriFilters = [];
86
88
  this.uriHandlers = [];
87
89
  this.methodHandlers = [];
@@ -354,13 +356,31 @@ class WebServer extends Component {
354
356
  if (this.http) {
355
357
  this.logDebug(2, "Shutting down HTTP server");
356
358
 
359
+ for (var id in this.requests) {
360
+ var args = this.requests[id];
361
+ this.logDebug(4, "Request still active: " + args.id, {
362
+ id: args.id,
363
+ ips: args.ips,
364
+ uri: args.request ? args.request.url : '',
365
+ headers: args.request ? args.request.headers : {},
366
+ socket: (args.request && args.request.socket && args.request.socket._pixl_data) ? args.request.socket._pixl_data.id : '',
367
+ stats: args.state,
368
+ date: args.date,
369
+ age: (Date.now() / 1000) - args.date
370
+ });
371
+ if (args.callback) {
372
+ args.callback();
373
+ delete args.callback;
374
+ }
375
+ } // foreach req
376
+
357
377
  for (var id in this.conns) {
358
- this.logDebug(9, "Closing HTTP connection: " + id);
378
+ this.logDebug(4, "Closing HTTP connection: " + id);
359
379
  // this.conns[id].destroy();
360
380
  this.conns[id].end();
361
381
  this.conns[id].unref();
362
382
  this.numConns--;
363
- }
383
+ } // foreach conn
364
384
 
365
385
  this.http.close( function() { self.logDebug(3, "HTTP server has shut down."); } );
366
386
 
@@ -369,6 +389,7 @@ class WebServer extends Component {
369
389
  }
370
390
  // delete this.http;
371
391
 
392
+ this.requests = {};
372
393
  this.queue.kill();
373
394
  }
374
395