Skip to content

Commit 370bdc9

Browse files
Merge pull request #375 from agsh/roger-fixes
Fix bugs in the Digest Header code where it did not meet the RFCs
2 parents 19b18bb + 8653e0b commit 370bdc9

File tree

3 files changed

+3873
-3849
lines changed

3 files changed

+3873
-3849
lines changed

lib/cam.js

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const http = require('http'),
1111
events = require('events'),
1212
url = require('url'),
1313
util = require('util'),
14+
splitargs = require('splitargs'),
1415
linerase = require('./utils').linerase,
1516
parseSOAPString = require('./utils').parseSOAPString,
1617
parseString = require('xml2js').parseString,
@@ -383,8 +384,10 @@ Cam.prototype._requestPart2 = function(options, callback) {
383384
Cam.prototype._parseChallenge = function(digest) {
384385
const prefix = 'Digest ';
385386
const challenge = digest.substring(digest.indexOf(prefix) + prefix.length);
386-
const parts = challenge.split(',')
387-
.map(part => part.match(/^\s*?([a-zA-Z0-9]+)="?([^"]*)"?\s*?$/).slice(1));
387+
// use splitargs to handle things like qop="auth,auth-int"
388+
let parts = splitargs(challenge,',', true); // get a list of Key=Value items. Whitespace will be consumed in the RegEx with \s before the RexEx Groups
389+
// split into Key-Value pairs. We can split on '=' as this cannot appear in the Key name, replacing String with an Array in 'parts'
390+
parts = parts.map(part => part.match(/^\s*?([a-zA-Z0-9]+)="?([^"]*)"?\s*?$/).slice(1));
388391
return Object.fromEntries(parts);
389392
};
390393

@@ -400,48 +403,62 @@ Cam.prototype.digestAuth = function(wwwAuthenticate, reqOptions) {
400403
const challenge = this._parseChallenge(wwwAuthenticate);
401404
const ha1 = crypto.createHash('md5');
402405
ha1.update([this.username, challenge.realm, this.password].join(':'));
406+
407+
// Sony SRG-XP1 sends qop="auth,auth-int" it means the Server will accept either "auth" or "auth-int". We select "auth"
408+
if (typeof challenge.qop === 'string' && challenge.qop === 'auth,auth-int') challenge.qop = 'auth';
409+
403410
const ha2 = crypto.createHash('md5');
404411
ha2.update([reqOptions.method, reqOptions.path].join(':'));
405412

406413
let cnonce = null;
407414
let nc = null;
408-
if (typeof challenge.qop === 'string') {
415+
if (typeof challenge.qop === 'string' && challenge.qop === 'auth') {
409416
const cnonceHash = crypto.createHash('md5');
410417
cnonceHash.update(Math.random().toString(36));
411418
cnonce = cnonceHash.digest('hex').substring(0, 8);
412419
nc = this.updateNC();
413420
}
414421

422+
// No qop -> Response = MD5(HA1:nonce:HA2);
423+
// With qop -> Response = MD5(HA1:nonce:nonceCount:cnonce:qop:HA2)
415424
const response = crypto.createHash('md5');
416425
const responseParams = [
417426
ha1.digest('hex'),
418427
challenge.nonce
419428
];
420-
if (cnonce) {
429+
if (cnonce != null) {
421430
responseParams.push(nc);
422431
responseParams.push(cnonce);
432+
responseParams.push(challenge.qop);
423433
}
424434

425-
responseParams.push(challenge.qop);
426435
responseParams.push(ha2.digest('hex'));
427436
response.update(responseParams.join(':'));
428437

429438
const authParams = {
430-
username: this.username,
431-
realm: challenge.realm,
432-
nonce: challenge.nonce,
433-
uri: reqOptions.path,
434-
qop: challenge.qop,
435-
response: response.digest('hex'),
439+
username: `"${this.username}"`,
440+
realm: `"${challenge.realm}"`,
441+
nonce: `"${challenge.nonce}"`,
442+
uri: `"${reqOptions.path}"`
436443
};
437-
if (challenge.opaque) {
438-
authParams.opaque = challenge.opaque;
444+
445+
// RFC says only send qop, nc and cnonce if there was a QOP in the Header
446+
// 'qop' and 'nc' do not have quotes around the Values
447+
if ('qop' in challenge) {
448+
authParams.qop = challenge.qop; // no quotes
449+
authParams.nc = nc; // no quotes
450+
authParams.cnonce = `"${cnonce}"`;
439451
}
440-
if (cnonce) {
441-
authParams.nc = nc;
442-
authParams.cnonce = cnonce;
452+
453+
authParams.response = `"${response.digest('hex')}"`;
454+
455+
if (challenge.opaque) {
456+
authParams.opaque = `"${challenge.opaque}"`;
443457
}
444-
return 'Digest ' + Object.entries(authParams).map(([key, value]) => `${key}="${value}"`).join(',');
458+
459+
// There are RFC non compliances here. Some values do not need to be in quotes for example 'nc'
460+
const result = 'Digest ' + Object.entries(authParams).map(([key, value]) => `${key}=${value}`).join(',');
461+
return result;
445462
};
446463

447464
/**

0 commit comments

Comments
 (0)