jsdav-ext
Version:
jsDAV allows you to easily add WebDAV support to a NodeJS application. jsDAV is meant to cover the entire standard, and attempts to allow integration using an easy to understand API.
282 lines (250 loc) • 8.57 kB
JavaScript
/*
* @package jsDAV
* @subpackage DAV
* @copyright Copyright(c) 2011 Ajax.org B.V. <info AT ajax DOT org>
* @author Mike de Boer <info AT mikedeboer DOT nl>
* @license http://github.com/mikedeboer/jsDAV/blob/master/LICENSE MIT License
*/
;
var jsDAV_Auth_iBackend = require("./iBackend");
var Exc = require("./../../../shared/exceptions");
var Util = require("./../../../shared/util");
/**
* These constants are used in setQOP();
*/
var QOP_AUTH = 1;
var QOP_AUTHINT = 2;
/**
* This method returns the full digest string.
*
* If the header could not be found, null will be returned
*
* @return mixed
*/
function getDigest(req) {
// most other servers
var digest = req.headers["authorization"];
if (digest && digest.toLowerCase().indexOf("digest") === 0)
return digest.substr(7);
else
return null;
}
/**
* Parses the different pieces of the digest string into an array.
*
* This method returns false if an incomplete digest was supplied
*
* @param {String} digest
* @return mixed
*/
function parseDigest(digest) {
if (!digest)
return false;
// protect against missing data
var needed_parts = {"nonce": 1, "nc": 1, "cnonce": 1, "qop": 1,
"username": 1, "uri": 1, "response": 1};
var data = {};
digest.replace(/(\w+)=(?:(?:")([^"]+)"|([^\s,]+))/g, function(m, m1, m2, m3) {
data[m1] = m2 ? m2 : m3;
delete needed_parts[m1];
return m;
});
return Object.keys(needed_parts).length ? false : data;
}
/**
* HTTP Digest authentication backend class
*
* This class can be used by authentication objects wishing to use HTTP Digest
* Most of the digest logic is handled, implementors just need to worry about
* the getDigestHash method
*/
var jsDAV_Auth_Backend_AbstractDigest = module.exports = jsDAV_Auth_iBackend.extend({
initialize: function() {
this.nonce = Util.uniqid();
},
/**
* This variable holds the currently logged in username.
*
* @var array|null
*/
currentUser: null,
digestParts: null,
A1: null,
qop: QOP_AUTH,
/**
* Gathers all information from the headers
*
* This method needs to be called prior to anything else.
*
* @return void
*/
init: function(realm, req) {
this.realm = realm;
this.opaque = Util.createHash(this.realm);
this.digest = getDigest(req);
this.digestParts = parseDigest(this.digest);
},
/**
* Sets the quality of protection value.
*
* Possible values are:
* QOP_AUTH
* QOP_AUTHINT
*
* Multiple values can be specified using logical OR.
*
* QOP_AUTHINT ensures integrity of the request body, but this is not
* supported by most HTTP clients. QOP_AUTHINT also requires the entire
* request body to be md5'ed, which can put strains on CPU and memory.
*
* @param {Number} qop
* @return void
*/
setQOP: function(qop) {
this.qop = qop;
},
/**
* Validates the user.
*
* The A1 parameter should be Util.createHash(username + ':' + realm + ':' + password);
*
* @param {String} A1
* @return bool
*/
validateA1: function(handler, newA1, cbvalida1) {
this.A1 = newA1;
this.validate(handler, cbvalida1);
},
/**
* Validates authentication through a password. The actual password must be provided here.
* It is strongly recommended not store the password in plain-text and use validateA1 instead.
*
* @param {String} password
* @return bool
*/
validatePassword: function(handler, password, cbvalidpass) {
this.A1 = Util.createHash(this.digestParts["username"] + ":" + this.realm + ":" + password);
this.validate(handler, cbvalidpass);
},
/**
* Returns the username for the request
*
* @return string
*/
getUsername: function() {
return this.digestParts["username"];
},
/**
* Validates the digest challenge
*
* @return bool
*/
validate: function(handler, cbvalidate) {
var req = handler.httpRequest;
var A2 = req.method + ":" + this.digestParts["uri"];
var self = this;
if (this.digestParts["qop"] == "auth-int") {
// Making sure we support this qop value
if (!(this.qop & QOP_AUTHINT))
return cbvalidate(false);
// We need to add an md5 of the entire request body to the A2 part of the hash
handler.getRequestBody("utf8", null, false, function(noop, body) {
A2 += ":" + Util.createHash(body);
afterBody();
});
}
else {
// We need to make sure we support this qop value
if (!(this.qop & QOP_AUTH))
return cbvalidate(false);
afterBody();
}
function afterBody() {
A2 = Util.createHash(A2);
var validResponse = Util.createHash(self.A1 + ":" + self.digestParts["nonce"] + ":"
+ self.digestParts["nc"] + ":" + self.digestParts["cnonce"] + ":"
+ self.digestParts["qop"] + ":" + A2);
cbvalidate(self.digestParts["response"] == validResponse);
}
},
/**
* Returns a users digest hash based on the username and realm.
*
* If the user was not known, null must be returned.
*
* @param {String} realm
* @param {String} username
* @return string|null
*/
getDigestHash: function(realm, username, cbdighash) {},
/**
* Returns an HTTP 401 header, forcing login
*
* This should be called when username and password are incorrect, or not supplied at all
*
* @return void
*/
requireAuth: function(realm, err, cbreqauth) {
if (!(err instanceof Exc.jsDAV_Exception))
err = new Exc.NotAuthenticated(err);
var currQop = "";
switch (this.qop) {
case QOP_AUTH:
currQop = "auth";
break;
case QOP_AUTHINT:
currQop = "auth-int";
break;
case QOP_AUTH | QOP_AUTHINT:
currQop = "auth,auth-int";
break;
}
err.addHeader("WWW-Authenticate", "Digest realm=\"" + realm +
"\",qop=\"" + currQop + "\",nonce=\"" + this.nonce +
"\",opaque=\"" + this.opaque + "\"");
cbreqauth(err, false);
},
/**
* Authenticates the user based on the current request.
*
* If authentication is succesful, true must be returned.
* If authentication fails, an exception must be thrown.
*
* @throws Sabre_DAV_Exception_NotAuthenticated
* @return bool
*/
authenticate: function(handler, realm, cbauth) {
var req = handler.httpRequest;
this.init(realm, req);
var username = this.digestParts["username"];
// No username was given
if (!username)
return this.requireAuth(realm, "No digest authentication headers were found", cbauth);
var self = this;
this.getDigestHash(realm, username, function(err, hash) {
// If this was false, the user account didn't exist
if (err || !hash)
return self.requireAuth(realm, err || "The supplied username was not on file", cbauth);
if (typeof hash != "string") {
handler.handleError(new Exc.jsDAV_Exception(
"The returned value from getDigestHash must be a string or null"));
return cbauth(null, false);
}
// If this was false, the password or part of the hash was incorrect.
self.validateA1(handler, hash, function(isValid) {
if (!isValid)
return self.requireAuth(realm, "Incorrect username", cbauth);
self.currentUser = username;
cbauth(null, true);
});
});
},
/**
* Returns the currently logged in username.
*
* @return string|null
*/
getCurrentUser: function(callback) {
callback(null, this.currentUser);
}
});