digest-client
Version:
Allows to make requests to servers with Digest Authentication
156 lines (118 loc) • 4.04 kB
JavaScript
/*
Digest Client
Use together with HTTP Client to perform requests to servers protected
by digest authentication.
*/
var url = require('url');
var http = require('http');
var https = require('https');
var crypto = require('crypto');
var MAX_NC = 99999999;
function DigestClient(options) {
this.nc = 0;
this.username = options.username;
this.password = options.password;
this._protocol = options.https ? https : http;
}
DigestClient.prototype.request = function (options, callback) {
var self = this;
self._protocol.request(options, function (res) {
// Run callback only when data is received so to be sure, that there are no errors
// Besides, we should listen to it once, because it fires several times (because it is buffer)
res.once('data', self._makeAuthorizedRequest.bind(self, options, res, callback));
// Listening to errors
res.on('error', callback);
}).end();
};
DigestClient.prototype.get = function (uri, callback) {
var urlObject = url.parse(uri);
this.request({
host: urlObject.hostname,
path: urlObject.pathname,
method: 'GET'
}, callback);
};
/*
Handle authentication
Parse authentication headers and set response.
*/
DigestClient.prototype._getAuthHeader = function (options, res) {
var ha1 = crypto.createHash('md5');
var ha2 = crypto.createHash('md5');
var cnonce = crypto.createHash('md5');
var responseHash = crypto.createHash('md5');
var challenge = this._parseChallenge(res.headers['www-authenticate']);
var auth = {
username: this.username,
realm: challenge.realm,
nonce: challenge.nonce,
uri: options.path,
qop: challenge.qop,
opaque: challenge.opaque
};
ha1.update([this.username, challenge.realm, this.password].join(':'));
ha2.update([options.method, options.path].join(':'));
// Generate response hash
var responseParams = [ha1.digest('hex'), auth.nonce];
if (challenge.qop) {
cnonce.update(Math.random().toString(36));
auth.cnonce = cnonce.digest('hex').substr(0, 16);
auth.nc = this._updateNC();
responseParams.push(auth.nc);
responseParams.push(auth.cnonce);
}
responseParams.push(auth.qop);
responseParams.push(ha2.digest('hex'));
responseHash.update(responseParams.join(':'));
auth.response = responseHash.digest('hex');
return this._compileParams(auth);
};
/*
Parse challenge digest
*/
DigestClient.prototype._parseChallenge = function(digest) {
var prefix = "Digest ";
var challenge = digest.substr(digest.indexOf(prefix) + prefix.length);
var parts = challenge.split(',');
var length = parts.length;
var params = {};
for (var i = 0; i < length; i++) {
var part = parts[i].match(/^\s*?([a-zA-Z0-0]+)="(.*)"\s*?$/);
if (part && part.length > 2) {
params[part[1]] = part[2];
}
}
return params;
};
/*
Compose authorization header
*/
DigestClient.prototype._compileParams = function(params) {
var parts = [];
for (var i in params) {
if (params[i] === undefined || params[i] === null) {
continue;
}
parts.push(i + '="' + params[i] + '"');
}
return 'Digest ' + parts.join(', ');
};
/*
Update and zero pad nc
*/
DigestClient.prototype._updateNC = function() {
this.nc = this.nc > MAX_NC ? 1 : this.nc + 1;
return Array(8).join('0').substr(0, 8 - this.nc.toString().length) + this.nc.toString();
};
DigestClient.prototype._makeAuthorizedRequest = function(options, res, callback) {
var self = this;
options.headers = options.headers || {};
options.headers.Authorization = self._getAuthHeader(options, res);
this._protocol.request(options, function(res) {
res.on('error', callback);
res.on('data', function(data) {
callback(null, data.toString());
});
}).end();
}
module.exports = DigestClient;