UNPKG

digest-client

Version:

Allows to make requests to servers with Digest Authentication

156 lines (118 loc) 4.04 kB
/* 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;