Skip to content
Open
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules
.project
53 changes: 53 additions & 0 deletions Gruntfile.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

module.exports = function(grunt) {

// Project configuration.
grunt.initConfig({
nodeunit: {
files: ['test/**/*_test.js']
},

jshint: {
options: {
jshintrc: '.jshintrc'
},
gruntfile: {
src: 'Gruntfile.js'
},
lib: {
src: ['lib/**/*.js']
},
test: {
src: ['test/**/*.js']
},
},
watch: {
gruntfile: {
files: '<%= jshint.gruntfile.src %>',
tasks: ['jshint:gruntfile']
},
lib: {
files: '<%= jshint.lib.src %>',
tasks: ['jshint:lib', 'nodeunit']
},
test: {
files: '<%= jshint.test.src %>',
tasks: ['jshint:test', 'nodeunit']
},
work: {
files: ['<%= jshint.lib.src %>', '<%= jshint.test.src %>', 'node_modules/**/*.js'],
tasks: ['nodeunit']
}
},
});

// These plugins provide necessary tasks.
grunt.loadNpmTasks('grunt-contrib-nodeunit');
grunt.loadNpmTasks('grunt-contrib-jshint');
grunt.loadNpmTasks('grunt-contrib-watch');

// Default task.
grunt.registerTask('default', ['jshint', 'nodeunit' ]);

};
146 changes: 0 additions & 146 deletions http-digest-client.js

This file was deleted.

153 changes: 153 additions & 0 deletions lib/http-digest-client.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//
// # Digest Client
//
// Use together with HTTP Client to perform requests to servers protected
// by digest authentication.
//
var url=require('url');

var HTTPDigest = function() {
var crypto = require('crypto');
var request = require('request');

var HTTPDigest = function(username, password) {
this.nc = 0;
this.username = username;
this.password = password;
};

//
// ## Make request
//
// Wraps the http.request function to apply digest authorization.
//
HTTPDigest.prototype.request = function(options, callback) {
var self = this;
options.auth.sendImmediately=false; // ensure no basic auth
request(options, function(err, res, body) {
self._handleResponse(options, err, res, body, callback);
}).end();
};

//
// ## Handle authentication
//
// Parse authentication headers and set response.
//
HTTPDigest.prototype._handleResponse = function handleResponse(options, err, res, body, callback) {
// First check if there is an error condition
if(err != null && typeof res=='undefined')
callback(err, res, body);
if(res.statusCode != "401")
callback(err, res, body);
// If not check if callback is required
else if(typeof res.headers['www-authenticate'] === 'undefined') {
callback(err, res, body);
}
else {
var path=url.parse(options.url).path; // get path from url
var challenge = this._parseChallenge(res.headers['www-authenticate']);
var ha1 = crypto.createHash('md5');
ha1.update([ this.username, challenge.realm, this.password ].join(':'));
var ha2 = crypto.createHash('md5');
ha2.update([ options.method, path ].join(':'));

// Generate cnonce
var cnonce = false;
var nc = false;
if (typeof challenge.qop === 'string') {
var cnonceHash = crypto.createHash('md5');
cnonceHash.update(Math.random().toString(36));
cnonce = cnonceHash.digest('hex').substr(0, 8);
nc = this.updateNC();
}

// Generate response hash
var response = crypto.createHash('md5');
var responseParams = [ ha1.digest('hex'), challenge.nonce ];

if (cnonce) {
responseParams.push(nc);
responseParams.push(cnonce);
}

responseParams.push(challenge.qop);
responseParams.push(ha2.digest('hex'));
response.update(responseParams.join(':'));

// Setup response parameters
var authParams = {
username : this.username,
realm : challenge.realm,
nonce : challenge.nonce,
uri : path,
qop : challenge.qop,
response : response.digest('hex'),
opaque : challenge.opaque
};
if (cnonce) {
authParams.nc = nc;
authParams.cnonce = cnonce;
}

var headers = options.headers || {};
headers.Authorization = this._compileParams(authParams);
options.headers = headers;

request(options, function(err, res, body) {
callback(err, res, body);
});
}
};

//
// ## Parse challenge digest
//
HTTPDigest.prototype._parseChallenge = function parseChallenge(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.length > 2) {
params[part[1]] = part[2];
}
}

return params;
};

//
// ## Compose authorization header
//
HTTPDigest.prototype._compileParams = function compileParams(params) {
var parts = [];
for ( var i in params) {
parts.push(i + '="' + params[i] + '"');
}
return 'Digest ' + parts.join(',');
};

//
// ## Update and zero pad nc
//
HTTPDigest.prototype.updateNC = function updateNC() {
var max = 99999999;
this.nc++;
if (this.nc > max) {
this.nc = 1;
}
var padding = new Array(8).join('0') + "";
var nc = this.nc + "";
return padding.substr(0, 8 - nc.length) + nc;
};

// Return response handler
return HTTPDigest;
}();

module.exports = function createDigestClient(username, password, https) {
return new HTTPDigest(username, password, https);
};
Loading