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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pixl-server-web",
3
- "version": "3.0.3",
3
+ "version": "3.0.4",
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/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? }