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 +44 -11
- package/lib/http.js +38 -1
- package/lib/https.js +37 -0
- package/lib/request.js +47 -8
- package/lib/response.js +7 -2
- package/package.json +1 -1
- package/test/test.js +37 -2
- package/web_server.js +24 -3
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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 -
|
|
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 (
|
|
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
|
-
|
|
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
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
|
|
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(
|
|
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
|
|