Skip to content

Commit 456f7ff

Browse files
committed
Merge pull request http-party#245 from neoeno/master
Add HTTP Basic Auth
2 parents 68b48eb + 45add9c commit 456f7ff

File tree

5 files changed

+170
-2
lines changed

5 files changed

+170
-2
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@ Using `npx` you can run the script without installing it first:
6161

6262
`-P` or `--proxy` Proxies all requests which can't be resolved locally to the given url. e.g.: -P http://someurl.com
6363

64+
`--username` Username for basic authentication [none]
65+
66+
`--password` Password for basic authentication [none]
67+
6468
`-S` or `--ssl` Enable https.
6569

6670
`-C` or `--cert` Path to ssl cert file (default: `cert.pem`).

bin/http-server

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ if (argv.h || argv.help) {
3636
'',
3737
' -P --proxy Fallback proxy if the request cannot be resolved. e.g.: http://someurl.com',
3838
'',
39+
' --username Username for basic authentication [none]',
40+
' Can also be specified with the env variable NODE_HTTP_SERVER_USERNAME',
41+
' --password Password for basic authentication [none]',
42+
' Can also be specified with the env variable NODE_HTTP_SERVER_PASSWORD',
43+
'',
3944
' -S --ssl Enable https.',
4045
' -C --cert Path to ssl cert file (default: cert.pem).',
4146
' -K --key Path to ssl key file (default: key.pem).',
@@ -108,7 +113,9 @@ function listen(port) {
108113
ext: argv.e || argv.ext,
109114
logFn: logger.request,
110115
proxy: proxy,
111-
showDotfiles: argv.dotfiles
116+
showDotfiles: argv.dotfiles,
117+
username: argv.username || process.env.NODE_HTTP_SERVER_USERNAME,
118+
password: argv.password || process.env.NODE_HTTP_SERVER_PASSWORD
112119
};
113120

114121
if (argv.cors) {

lib/http-server.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
var fs = require('fs'),
44
union = require('union'),
55
ecstatic = require('ecstatic'),
6+
auth = require('basic-auth'),
67
httpProxy = require('http-proxy'),
7-
corser = require('corser');
8+
corser = require('corser'),
9+
secureCompare = require('secure-compare');
810

911
//
1012
// Remark: backwards compatibility for previous
@@ -72,6 +74,27 @@ function HttpServer(options) {
7274
res.emit('next');
7375
});
7476

77+
if (options.username || options.password) {
78+
before.push(function (req, res) {
79+
var credentials = auth(req);
80+
81+
// We perform these outside the if to avoid short-circuiting and giving
82+
// an attacker knowledge of whether the username is correct via a timing
83+
// attack.
84+
if (credentials) {
85+
var usernameEqual = secureCompare(options.username, credentials.name);
86+
var passwordEqual = secureCompare(options.password, credentials.pass);
87+
if (usernameEqual && passwordEqual) {
88+
return res.emit('next');
89+
}
90+
}
91+
92+
res.statusCode = 401;
93+
res.setHeader('WWW-Authenticate', 'Basic realm=""');
94+
res.end('Access denied');
95+
});
96+
}
97+
7598
if (options.cors) {
7699
this.headers['Access-Control-Allow-Origin'] = '*';
77100
this.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Range';

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,13 +70,15 @@
7070
}
7171
],
7272
"dependencies": {
73+
"basic-auth": "^1.0.3",
7374
"colors": "^1.3.3",
7475
"corser": "^2.0.1",
7576
"ecstatic": "^3.3.0",
7677
"http-proxy": "^1.17.0",
7778
"opener": "^1.5.1",
7879
"optimist": "~0.6.1",
7980
"portfinder": "^1.0.20",
81+
"secure-compare": "3.0.1",
8082
"union": "~0.5.0"
8183
},
8284
"devDependencies": {

test/http-server-test.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,5 +157,137 @@ vows.describe('http-server').addBatch({
157157
assert.ok(res.headers['access-control-allow-headers'].split(/\s*,\s*/g).indexOf('X-Test') >= 0, 204);
158158
}
159159
}
160+
},
161+
'When http-server is listening on 8083 with username "good_username" and password "good_password"': {
162+
topic: function () {
163+
var server = httpServer.createServer({
164+
root: root,
165+
robots: true,
166+
headers: {
167+
'Access-Control-Allow-Origin': '*',
168+
'Access-Control-Allow-Credentials': 'true'
169+
},
170+
username: 'good_username',
171+
password: 'good_password'
172+
});
173+
174+
server.listen(8083);
175+
this.callback(null, server);
176+
},
177+
'and the user requests an existent file with no auth details': {
178+
topic: function () {
179+
request('http://127.0.0.1:8083/file', this.callback);
180+
},
181+
'status code should be 401': function (res) {
182+
assert.equal(res.statusCode, 401);
183+
},
184+
'and file content': {
185+
topic: function (res, body) {
186+
var self = this;
187+
fs.readFile(path.join(root, 'file'), 'utf8', function (err, data) {
188+
self.callback(err, data, body);
189+
});
190+
},
191+
'should be a forbidden message': function (err, file, body) {
192+
assert.equal(body, 'Access denied');
193+
}
194+
}
195+
},
196+
'and the user requests an existent file with incorrect username': {
197+
topic: function () {
198+
request('http://127.0.0.1:8083/file', {
199+
auth: {
200+
user: 'wrong_username',
201+
pass: 'good_password'
202+
}
203+
}, this.callback);
204+
},
205+
'status code should be 401': function (res) {
206+
assert.equal(res.statusCode, 401);
207+
},
208+
'and file content': {
209+
topic: function (res, body) {
210+
var self = this;
211+
fs.readFile(path.join(root, 'file'), 'utf8', function (err, data) {
212+
self.callback(err, data, body);
213+
});
214+
},
215+
'should be a forbidden message': function (err, file, body) {
216+
assert.equal(body, 'Access denied');
217+
}
218+
}
219+
},
220+
'and the user requests an existent file with incorrect password': {
221+
topic: function () {
222+
request('http://127.0.0.1:8083/file', {
223+
auth: {
224+
user: 'good_username',
225+
pass: 'wrong_password'
226+
}
227+
}, this.callback);
228+
},
229+
'status code should be 401': function (res) {
230+
assert.equal(res.statusCode, 401);
231+
},
232+
'and file content': {
233+
topic: function (res, body) {
234+
var self = this;
235+
fs.readFile(path.join(root, 'file'), 'utf8', function (err, data) {
236+
self.callback(err, data, body);
237+
});
238+
},
239+
'should be a forbidden message': function (err, file, body) {
240+
assert.equal(body, 'Access denied');
241+
}
242+
}
243+
},
244+
'and the user requests a non-existent file with incorrect password': {
245+
topic: function () {
246+
request('http://127.0.0.1:8083/404', {
247+
auth: {
248+
user: 'good_username',
249+
pass: 'wrong_password'
250+
}
251+
}, this.callback);
252+
},
253+
'status code should be 401': function (res) {
254+
assert.equal(res.statusCode, 401);
255+
},
256+
'and file content': {
257+
topic: function (res, body) {
258+
var self = this;
259+
fs.readFile(path.join(root, 'file'), 'utf8', function (err, data) {
260+
self.callback(err, data, body);
261+
});
262+
},
263+
'should be a forbidden message': function (err, file, body) {
264+
assert.equal(body, 'Access denied');
265+
}
266+
}
267+
},
268+
'and the user requests an existent file with correct auth details': {
269+
topic: function () {
270+
request('http://127.0.0.1:8083/file', {
271+
auth: {
272+
user: 'good_username',
273+
pass: 'good_password'
274+
}
275+
}, this.callback);
276+
},
277+
'status code should be 200': function (res) {
278+
assert.equal(res.statusCode, 200);
279+
},
280+
'and file content': {
281+
topic: function (res, body) {
282+
var self = this;
283+
fs.readFile(path.join(root, 'file'), 'utf8', function (err, data) {
284+
self.callback(err, data, body);
285+
});
286+
},
287+
'should match content of served file': function (err, file, body) {
288+
assert.equal(body.trim(), file.trim());
289+
}
290+
}
291+
}
160292
}
161293
}).export(module);

0 commit comments

Comments
 (0)