pixl-server-web 3.0.3 → 3.0.4
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 +21 -0
- package/package.json +1 -1
- package/prompt.md +40 -0
- package/test/test.js +70 -0
- package/web_server.js +57 -0
package/README.md
CHANGED
|
@@ -66,6 +66,7 @@ This module is a component for use in [pixl-server](https://www.github.com/jhuck
|
|
|
66
66
|
* [debug_ttl](#debug_ttl)
|
|
67
67
|
* [debug_bind_local](#debug_bind_local)
|
|
68
68
|
* [chaos](#chaos)
|
|
69
|
+
* [auth](#auth)
|
|
69
70
|
* [https](#https)
|
|
70
71
|
* [https_port](#https_port)
|
|
71
72
|
* [https_alt_ports](#https_alt_ports)
|
|
@@ -863,6 +864,26 @@ Use the `chaos` feature to introduce optional and random fault injection into yo
|
|
|
863
864
|
|
|
864
865
|
Set the `chaos.enabled` flag to `true` to enable fault injection. By default, all URIs will be affected, unless you specify a `chaos.uri` (regular expression) to limit the requests. Set `chaos.delay.min` and `chaos.delay.max` to the range you want to delay requests (in milliseconds). Fill the `chaos.errors` object the HTTP repsonse codes (and status messages) you want to see, and how often. The values are interpreted as probabilities from `0.0` (never) to `1.0` (always). In the above example, the `HTTP 503` error code will be injected approximately 10% of the time. When errors are injected, you can include additional response headers in the `chaos.headers` object.
|
|
865
866
|
|
|
867
|
+
## auth
|
|
868
|
+
|
|
869
|
+
Use the `auth` feature to protect URI patterns behind HTTP authentication challenges. Each key in the `auth` object should be a URI regular expression, and each value should be an auth definition. Here is an example:
|
|
870
|
+
|
|
871
|
+
```json
|
|
872
|
+
"auth": {
|
|
873
|
+
"^/protected": {
|
|
874
|
+
"enabled": true,
|
|
875
|
+
"type": "basic",
|
|
876
|
+
"realm": "Secret Area",
|
|
877
|
+
"username": "foo",
|
|
878
|
+
"password": "bar"
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
Set `auth.URI.enabled` to `true` to enable auth for that URI pattern. Set `auth.URI.type` to `basic` to enable HTTP Basic Auth. Currently, `basic` is the only supported authentication scheme. The server will challenge unauthorized clients with the configured `auth.URI.realm`, and then compare `auth.URI.username` and `auth.URI.password` against the credentials provided by the client.
|
|
884
|
+
|
|
885
|
+
The URI match key (e.g. `^/protected`) is treated as a regular expression and is evaluated as a request URI filter. If credentials are missing or invalid, the request is rejected with `HTTP 401 Unauthorized`.
|
|
886
|
+
|
|
866
887
|
## https
|
|
867
888
|
|
|
868
889
|
This boolean allows you to enable HTTPS (SSL) support in the web server. It defaults to `false`. Note that you must also set `https_port`, and possibly `https_cert_file` and `https_key_file` for this to work.
|
package/package.json
CHANGED
package/prompt.md
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
Hello there, friend! Welcome to my Node.js web server, called pixl-server-web, which is a full-featured web server for Node.js.
|
|
2
|
+
|
|
3
|
+
This is the full source code, and the library is code complete. I would like some help with all kinds of things, but my focus is the documentation at this stage. Please feel free to get a feel for the place first. Look into all the directories, read all the files, and learn all that you can. In particular:
|
|
4
|
+
|
|
5
|
+
- The app is Node.js with vanilla JavaScript.
|
|
6
|
+
- There is NO TypeScript, and NO React.
|
|
7
|
+
- All of the documentation in `README.md` is complete. Feel free to read it.
|
|
8
|
+
- This local sandbox has a full `node_modules/` folder with all dependencies installed, for local development. Be careful about eating up your context window by digging into this directory. The `package.json` file should tell you all you need to know about deps, but in some cases I may ask you to dive into a specific dependency.
|
|
9
|
+
- Do not make a git branch or PR -- just edit the documentation files directly.
|
|
10
|
+
- Please write all docs using GitHub Flavored Markdown.
|
|
11
|
+
- Please do not use Emoji in the documentation -- especially not in headers / links.
|
|
12
|
+
- No em dashes please.
|
|
13
|
+
|
|
14
|
+
Let's work on the `README.md` file today. This is the only file you should edit.
|
|
15
|
+
|
|
16
|
+
I've just added a new feature to the web server where users can protect certain URIs behind HTTP Basic Auth. Please perform a local dit diff to see my changes, as they are uncommitted. If you have trouble doing a git diff, the changes are isolated to the `web_server.js` file. See the `setupAuth()` function.
|
|
17
|
+
|
|
18
|
+
Can you please add documentation for this new feature? I think it should go just below the "chaos" section, and above the https section.
|
|
19
|
+
|
|
20
|
+
Please read the existing docs so you can mimic the same format and style.
|
|
21
|
+
|
|
22
|
+
Example use in the config:
|
|
23
|
+
|
|
24
|
+
```json
|
|
25
|
+
"auth": {
|
|
26
|
+
"^/protected": {
|
|
27
|
+
"enabled": true,
|
|
28
|
+
"type": "basic",
|
|
29
|
+
"realm": "Secret Area",
|
|
30
|
+
"username": "foo",
|
|
31
|
+
"password": "bar"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Make sense?
|
|
37
|
+
|
|
38
|
+
If you are confused about anything, please feel free to pause the conversation and ask me. I'm here to help, and we can colaborate on these changes.
|
|
39
|
+
|
|
40
|
+
Any questions for me before we begin?
|
package/test/test.js
CHANGED
|
@@ -68,6 +68,15 @@ var server = new PixlServer({
|
|
|
68
68
|
"status": "301 Moved Permanently"
|
|
69
69
|
}
|
|
70
70
|
},
|
|
71
|
+
"auth": {
|
|
72
|
+
"^/protected": {
|
|
73
|
+
"enabled": true,
|
|
74
|
+
"type": "basic",
|
|
75
|
+
"realm": "Secret Area",
|
|
76
|
+
"username": "foo",
|
|
77
|
+
"password": "bar"
|
|
78
|
+
}
|
|
79
|
+
},
|
|
71
80
|
|
|
72
81
|
"https": 1,
|
|
73
82
|
"https_port": 3021,
|
|
@@ -163,6 +172,14 @@ module.exports = {
|
|
|
163
172
|
callback( server.WebServer.getStats() );
|
|
164
173
|
} );
|
|
165
174
|
|
|
175
|
+
web_server.addURIHandler( '/protected', 'Protected Test', function(args, callback) {
|
|
176
|
+
// auth protected endpoint
|
|
177
|
+
callback( {
|
|
178
|
+
code: 0,
|
|
179
|
+
description: "Protected endpoint success"
|
|
180
|
+
} );
|
|
181
|
+
} );
|
|
182
|
+
|
|
166
183
|
web_server.addURIHandler( '/binary-force-compress', 'Force Compress Test', function(args, callback) {
|
|
167
184
|
// send custom compressed response
|
|
168
185
|
callback(
|
|
@@ -1666,6 +1683,59 @@ module.exports = {
|
|
|
1666
1683
|
);
|
|
1667
1684
|
},
|
|
1668
1685
|
|
|
1686
|
+
function testAuthRequired(test) {
|
|
1687
|
+
// no auth header should be challenged
|
|
1688
|
+
request.get( 'http://127.0.0.1:3020/protected',
|
|
1689
|
+
function(err, resp, data, perf) {
|
|
1690
|
+
test.ok( !err, "No error from PixlRequest: " + err );
|
|
1691
|
+
test.ok( !!resp, "Got resp from PixlRequest" );
|
|
1692
|
+
test.ok( resp.statusCode == 401, "Got 401 response: " + resp.statusCode );
|
|
1693
|
+
test.ok( !!resp.headers['www-authenticate'], "Got WWW-Authenticate header" );
|
|
1694
|
+
test.ok( !!resp.headers['www-authenticate'].match(/Basic realm="Secret Area"/), "Correct WWW-Authenticate header: " + resp.headers['www-authenticate'] );
|
|
1695
|
+
test.ok( data.toString() == "Unauthorized", "Correct response body: " + data.toString() );
|
|
1696
|
+
test.done();
|
|
1697
|
+
}
|
|
1698
|
+
);
|
|
1699
|
+
},
|
|
1700
|
+
|
|
1701
|
+
function testAuthBadCredentials(test) {
|
|
1702
|
+
// invalid basic auth should fail
|
|
1703
|
+
request.get( 'http://127.0.0.1:3020/protected',
|
|
1704
|
+
{
|
|
1705
|
+
headers: {
|
|
1706
|
+
"Authorization": "Basic " + Buffer.from("foo:nope").toString('base64')
|
|
1707
|
+
}
|
|
1708
|
+
},
|
|
1709
|
+
function(err, resp, data, perf) {
|
|
1710
|
+
test.ok( !err, "No error from PixlRequest: " + err );
|
|
1711
|
+
test.ok( !!resp, "Got resp from PixlRequest" );
|
|
1712
|
+
test.ok( resp.statusCode == 401, "Got 401 response: " + resp.statusCode );
|
|
1713
|
+
test.ok( data.toString() == "Unauthorized", "Correct response body: " + data.toString() );
|
|
1714
|
+
test.done();
|
|
1715
|
+
}
|
|
1716
|
+
);
|
|
1717
|
+
},
|
|
1718
|
+
|
|
1719
|
+
function testAuthGoodCredentials(test) {
|
|
1720
|
+
// valid basic auth should pass through
|
|
1721
|
+
request.json( 'http://127.0.0.1:3020/protected', false,
|
|
1722
|
+
{
|
|
1723
|
+
headers: {
|
|
1724
|
+
"Authorization": "Basic " + Buffer.from("foo:bar").toString('base64')
|
|
1725
|
+
}
|
|
1726
|
+
},
|
|
1727
|
+
function(err, resp, json, perf) {
|
|
1728
|
+
test.ok( !err, "No error from PixlRequest: " + err );
|
|
1729
|
+
test.ok( !!resp, "Got resp from PixlRequest" );
|
|
1730
|
+
test.ok( resp.statusCode == 200, "Got 200 response: " + resp.statusCode );
|
|
1731
|
+
test.ok( !!json, "Got JSON in response" );
|
|
1732
|
+
test.ok( json.code == 0, "Correct code in JSON response: " + json.code );
|
|
1733
|
+
test.ok( json.description == "Protected endpoint success", "Correct description in JSON response: " + json.description );
|
|
1734
|
+
test.done();
|
|
1735
|
+
}
|
|
1736
|
+
);
|
|
1737
|
+
},
|
|
1738
|
+
|
|
1669
1739
|
// blacklist
|
|
1670
1740
|
function testBlacklistedIP(test) {
|
|
1671
1741
|
request.get( 'http://127.0.0.1:3020/json',
|
package/web_server.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const fs = require('fs');
|
|
7
7
|
const os = require('os');
|
|
8
|
+
const crypto = require('crypto');
|
|
8
9
|
const zlib = require('zlib');
|
|
9
10
|
const async = require('async');
|
|
10
11
|
const Class = require("class-plus");
|
|
@@ -186,10 +187,66 @@ class WebServer extends Component {
|
|
|
186
187
|
// optional chaos (fault injection)
|
|
187
188
|
if (this.config.getPath('chaos.enabled')) this.setupChaos();
|
|
188
189
|
|
|
190
|
+
// optional auth endpoints
|
|
191
|
+
if (this.config.get('auth')) this.setupAuth();
|
|
192
|
+
|
|
189
193
|
// start listeners
|
|
190
194
|
this.startAll(callback);
|
|
191
195
|
}
|
|
192
196
|
|
|
197
|
+
setupAuth() {
|
|
198
|
+
// basic auth behind custom URI patterns
|
|
199
|
+
// auth: { "URI": { enabled, type, realm, username, password } }
|
|
200
|
+
var self = this;
|
|
201
|
+
var auth_config = this.config.get('auth');
|
|
202
|
+
|
|
203
|
+
// constant-time comparison to avoid timing attacks
|
|
204
|
+
var safeCompare = function(a, b) {
|
|
205
|
+
const bufA = Buffer.from(a);
|
|
206
|
+
const bufB = Buffer.from(b);
|
|
207
|
+
if (bufA.length !== bufB.length) return false;
|
|
208
|
+
return crypto.timingSafeEqual(bufA, bufB);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
var checkBasicAuth = function(req, auth) {
|
|
212
|
+
const header = req.headers['authorization'];
|
|
213
|
+
if (!header) return false;
|
|
214
|
+
|
|
215
|
+
const [scheme, encoded] = header.split(' ');
|
|
216
|
+
if (scheme !== 'Basic' || !encoded) return false;
|
|
217
|
+
|
|
218
|
+
const decoded = Buffer.from(encoded, 'base64').toString('utf8');
|
|
219
|
+
const index = decoded.indexOf(':');
|
|
220
|
+
if (index === -1) return false;
|
|
221
|
+
|
|
222
|
+
const username = decoded.slice(0, index);
|
|
223
|
+
const password = decoded.slice(index + 1);
|
|
224
|
+
|
|
225
|
+
return safeCompare(username, auth.username) && safeCompare(password, auth.password);
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
Object.keys(auth_config).forEach( function(uri_match) {
|
|
229
|
+
var auth = auth_config[uri_match];
|
|
230
|
+
if (!auth.enabled) return;
|
|
231
|
+
if (auth.type != 'basic') {
|
|
232
|
+
self.logError('auth', "Unsupported auth type: " + auth.type);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
self.addURIFilter( new RegExp(uri_match), "Auth", function(args, callback) {
|
|
237
|
+
self.logDebug(9, "Checking auth for: " + uri_match, { realm: auth.realm });
|
|
238
|
+
if (!checkBasicAuth(args.request, auth)) {
|
|
239
|
+
return callback( "401 Unauthorized", {
|
|
240
|
+
'WWW-Authenticate': `Basic realm="${auth.realm}"`,
|
|
241
|
+
'Content-Type': 'text/plain'
|
|
242
|
+
}, "Unauthorized" );
|
|
243
|
+
}
|
|
244
|
+
self.logDebug(9, "Authentication successful for: " + uri_match, { realm: auth.realm, username: auth.username });
|
|
245
|
+
callback(false); // passthru
|
|
246
|
+
} ); // addURIFilter
|
|
247
|
+
} ); // foreach auth
|
|
248
|
+
}
|
|
249
|
+
|
|
193
250
|
setupChaos() {
|
|
194
251
|
// setup chaos system (random delays, errors, etc.)
|
|
195
252
|
// chaos: { enabled, uri?, delay?: { min:0, max:250 }, errors?: { "503 Service Unavailable": 0.1 }, headers? }
|