Skip to content

Commit 36f982c

Browse files
committed
Merge pull request request#730 from dai-shi/PR_digest_01
better HTTP DIGEST support
2 parents d5b7880 + 782dc11 commit 36f982c

File tree

2 files changed

+70
-24
lines changed

2 files changed

+70
-24
lines changed

request.js

+30-14
Original file line numberDiff line numberDiff line change
@@ -703,38 +703,54 @@ Request.prototype.onResponse = function (response) {
703703
break
704704

705705
case 'Digest':
706-
// TODO: More complete implementation of RFC 2617. For reference:
706+
// TODO: More complete implementation of RFC 2617.
707+
// - check challenge.algorithm
708+
// - support algorithm="MD5-sess"
709+
// - handle challenge.domain
710+
// - support qop="auth-int" only
711+
// - handle Authentication-Info (not necessarily?)
712+
// - check challenge.stale (not necessarily?)
713+
// - increase nc (not necessarily?)
714+
// For reference:
707715
// http://tools.ietf.org/html/rfc2617#section-3
708716
// https://github.com/bagder/curl/blob/master/lib/http_digest.c
709717

710-
var matches = authHeader.match(/([a-z0-9_-]+)="([^"]+)"/gi)
711718
var challenge = {}
712-
713-
for (var i = 0; i < matches.length; i++) {
714-
var eqPos = matches[i].indexOf('=')
715-
var key = matches[i].substring(0, eqPos)
716-
var quotedValue = matches[i].substring(eqPos + 1)
717-
challenge[key] = quotedValue.substring(1, quotedValue.length - 1)
719+
var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi
720+
for (;;) {
721+
var match = re.exec(authHeader)
722+
if (!match) break
723+
challenge[match[1]] = match[2] || match[3];
718724
}
719725

720726
var ha1 = md5(self._user + ':' + challenge.realm + ':' + self._pass)
721727
var ha2 = md5(self.method + ':' + self.uri.path)
722-
var cnonce = uuid().replace(/-/g, '')
723-
var digestResponse = md5(ha1 + ':' + challenge.nonce + ':1:' + cnonce + ':auth:' + ha2)
728+
var qop = /(^|,)auth($|,)/.test(challenge.qop) && 'auth'
729+
var nc = qop && '00000001'
730+
var cnonce = qop && uuid().replace(/-/g, '')
731+
var digestResponse = qop ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) : md5(ha1 + ':' + challenge.nonce + ':' + ha2)
724732
var authValues = {
725733
username: self._user,
726734
realm: challenge.realm,
727735
nonce: challenge.nonce,
728736
uri: self.uri.path,
729-
qop: challenge.qop,
737+
qop: qop,
730738
response: digestResponse,
731-
nc: 1,
732-
cnonce: cnonce
739+
nc: nc,
740+
cnonce: cnonce,
741+
algorithm: challenge.algorithm,
742+
opaque: challenge.opaque
733743
}
734744

735745
authHeader = []
736746
for (var k in authValues) {
737-
authHeader.push(k + '="' + authValues[k] + '"')
747+
if (!authValues[k]) {
748+
//ignore
749+
} else if (k === 'qop' || k === 'nc' || k === 'algorithm') {
750+
authHeader.push(k + '=' + authValues[k])
751+
} else {
752+
authHeader.push(k + '="' + authValues[k] + '"')
753+
}
738754
}
739755
authHeader = 'Digest ' + authHeader.join(', ')
740756
self.setHeader('authorization', authHeader)

tests/test-digest-auth.js

+40-10
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,34 @@ var digestServer = http.createServer(function (req, res) {
1414

1515
var ok;
1616

17-
if (req.headers.authorization) {
18-
if (/^Digest username="test", realm="Private", nonce="WpcHS2\/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93", uri="\/test\/", qop="auth", response="[a-f0-9]{32}", nc="1", cnonce="[a-f0-9]{32}"$/.exec(req.headers.authorization)) {
19-
ok = true;
17+
if (req.url === '/test/') {
18+
if (req.headers.authorization) {
19+
if (/^Digest username="test", realm="Private", nonce="WpcHS2\/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93", uri="\/test\/", qop=auth, response="[a-f0-9]{32}", nc=00000001, cnonce="[a-f0-9]{32}", algorithm=MD5, opaque="5ccc069c403ebaf9f0171e9517f40e41"$/.exec(req.headers.authorization)) {
20+
ok = true;
21+
} else {
22+
// Bad auth header, don't send back WWW-Authenticate header
23+
ok = false;
24+
}
2025
} else {
21-
// Bad auth header, don't send back WWW-Authenticate header
26+
// No auth header, send back WWW-Authenticate header
2227
ok = false;
28+
res.setHeader('www-authenticate', 'Digest realm="Private", nonce="WpcHS2/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93", algorithm=MD5, qop="auth", opaque="5ccc069c403ebaf9f0171e9517f40e41"');
29+
}
30+
} else if (req.url === '/dir/index.html') {
31+
// RFC2069-compatible mode
32+
// check: http://www.rfc-editor.org/errata_search.php?rfc=2069
33+
if (req.headers.authorization) {
34+
if (/^Digest username="Mufasa", realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="\/dir\/index.html", response="[a-f0-9]{32}", opaque="5ccc069c403ebaf9f0171e9517f40e41"$/.exec(req.headers.authorization)) {
35+
ok = true;
36+
} else {
37+
// Bad auth header, don't send back WWW-Authenticate header
38+
ok = false;
39+
}
40+
} else {
41+
// No auth header, send back WWW-Authenticate header
42+
ok = false;
43+
res.setHeader('www-authenticate', 'Digest realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", opaque="5ccc069c403ebaf9f0171e9517f40e41"');
2344
}
24-
} else {
25-
// No auth header, send back WWW-Authenticate header
26-
ok = false;
27-
res.setHeader('www-authenticate', 'Digest realm="Private", nonce="WpcHS2/TBAA=dffcc0dbd5f96d49a5477166649b7c0ae3866a93", algorithm=MD5, qop="auth"');
2845
}
2946

3047
if (ok) {
@@ -63,7 +80,20 @@ request({
6380
assert.equal(response.statusCode, 401);
6481
assert.equal(numDigestRequests, 3);
6582

66-
console.log('All tests passed');
67-
digestServer.close();
83+
request({
84+
'method': 'GET',
85+
'uri': 'http://localhost:6767/dir/index.html',
86+
'auth': {
87+
'user': 'Mufasa',
88+
'pass': 'CircleOfLife',
89+
'sendImmediately': false
90+
}
91+
}, function(error, response, body) {
92+
assert.equal(response.statusCode, 200);
93+
assert.equal(numDigestRequests, 5);
94+
95+
console.log('All tests passed');
96+
digestServer.close();
97+
});
6898
});
6999
});

0 commit comments

Comments
 (0)