diff --git a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js index ad283657d..bcf3b72b9 100644 --- a/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js +++ b/packages/node_modules/@node-red/nodes/core/network/21-httprequest.js @@ -699,25 +699,43 @@ in your Node-RED user directory (${RED.settings.userDir}). }); const md5 = (value) => { return crypto.createHash('md5').update(value).digest('hex') } + const sha256 = (value) => { return crypto.createHash('sha256').update(value).digest('hex') } + const sha512 = (value) => { return crypto.createHash('sha512').update(value).digest('hex') } + + function digestCompute(algorithm, value) { + var lowercaseAlgorithm = "" + if (algorithm) { + lowercaseAlgorithm = algorithm.toLowerCase().replace(/-sess$/, '') + } + + if (lowercaseAlgorithm === "sha-256") { + return sha256(value) + } else if (lowercaseAlgorithm === "sha-512-256") { + var hash = sha512(value) + return hash.slice(0, 64) // Only use the first 256 bits + } else { + return md5(value) + } + } function ha1Compute(algorithm, user, realm, pass, nonce, cnonce) { /** - * RFC 2617: handle both MD5 and MD5-sess algorithms. + * RFC 2617: handle both standard and -sess algorithms. * - * If the algorithm directive's value is "MD5" or unspecified, then HA1 is - * HA1=MD5(username:realm:password) - * If the algorithm directive's value is "MD5-sess", then HA1 is - * HA1=MD5(MD5(username:realm:password):nonce:cnonce) + * If the algorithm directive's value ends with "-sess", then HA1 is + * HA1=digestCompute(digestCompute(username:realm:password):nonce:cnonce) + * + * If the algorithm directive's value does not end with "-sess", then HA1 is + * HA1=digestCompute(username:realm:password) */ - var ha1 = md5(user + ':' + realm + ':' + pass) - if (algorithm && algorithm.toLowerCase() === 'md5-sess') { - return md5(ha1 + ':' + nonce + ':' + cnonce) + var ha1 = digestCompute(algorithm, user + ':' + realm + ':' + pass) + if (algorithm && /-sess$/i.test(algorithm)) { + return digestCompute(algorithm, ha1 + ':' + nonce + ':' + cnonce) } else { return ha1 } } - function buildDigestHeader(user, pass, method, path, authHeader) { var challenge = {} var re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi @@ -732,10 +750,10 @@ in your Node-RED user directory (${RED.settings.userDir}). var nc = qop && '00000001' var cnonce = qop && uuid().replace(/-/g, '') var ha1 = ha1Compute(challenge.algorithm, user, challenge.realm, pass, challenge.nonce, cnonce) - var ha2 = md5(method + ':' + path) + var ha2 = digestCompute(challenge.algorithm, method + ':' + path) var digestResponse = qop - ? md5(ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) - : md5(ha1 + ':' + challenge.nonce + ':' + ha2) + ? digestCompute(challenge.algorithm, ha1 + ':' + challenge.nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) + : digestCompute(challenge.algorithm, ha1 + ':' + challenge.nonce + ':' + ha2) var authValues = { username: user, realm: challenge.realm, diff --git a/test/nodes/core/network/21-httprequest_spec.js b/test/nodes/core/network/21-httprequest_spec.js index 14f3fd628..37e282bcf 100644 --- a/test/nodes/core/network/21-httprequest_spec.js +++ b/test/nodes/core/network/21-httprequest_spec.js @@ -31,6 +31,7 @@ var multer = require("multer"); var RED = require("nr-test-utils").require("node-red/lib/red"); var fs = require('fs-extra'); var auth = require('basic-auth'); +var crypto = require("crypto"); const { version } = require("os"); const net = require('net') @@ -163,6 +164,100 @@ describe('HTTP Request Node', function() { delete process.env.NO_PROXY; } + function getDigestPassword() { + return 'digest-test-password'; + } + + function getDigest(algorithm, value) { + var hash; + if (algorithm === 'SHA-256') { + hash = crypto.createHash('sha256'); + } else if (algorithm === 'SHA-512-256') { + hash = crypto.createHash('sha512'); + } else { + hash = crypto.createHash('md5'); + } + + var hex = hash.update(value).digest('hex'); + if (algorithm === 'SHA-512-256') { + hex = hex.slice(0, 64); + } + return hex; + } + + function getDigestResponse(req, algorithm, sess, realm, username, nonce, nc, cnonce, qop) { + var ha1 = getDigest(algorithm, username + ':' + realm + ':' + getDigestPassword()); + if (sess) { + ha1 = getDigest(algorithm, ha1 + ':' + nonce + ':' + cnonce) + } + let ha2 = getDigest(algorithm, req.method + ':' + req.path); + return qop + ? getDigest(algorithm, ha1 + ':' + nonce + ':' + nc + ':' + cnonce + ':' + qop + ':' + ha2) + : getDigest(algorithm, ha1 + ':' + nonce + ':' + ha2); + } + + function handleDigestResponse(req, res, algorithm, sess, qop) { + let realm = "node-red"; + let nonce = "123456"; + let nc = '00000001'; + let algorithmValue = sess ? `${algorithm}-sess` : algorithm; + + let authHeader = req.headers['authorization']; + if (!authHeader) { + let qopField = qop ? `qop="${qop}", ` : ''; + + res.setHeader( + 'WWW-Authenticate', + `Digest ${qopField}realm="${realm}", nonce="${nonce}", algorithm="${algorithmValue}"` + ); + res.status(401).end(); + return; + } + + var authFields = {}; + let re = /([a-z0-9_-]+)=(?:"([^"]+)"|([a-z0-9_-]+))/gi; + for (;;) { + var match = re.exec(authHeader); + if (!match) { + break; + } + authFields[match[1]] = match[2] || match[3]; + } + console.log(JSON.stringify(authFields)); + + if (qop && authFields['qop'] != qop) { + console.log('test1'); + res.status(401).end(); + return; + } + + if ( + !authFields['username'] || + !authFields['response'] || + authFields['realm'] != realm || + authFields['nonce'] != nonce || + authFields['algorithm'] != algorithmValue + ) { + console.log('test2'); + res.status(401).end(); + return; + } + + let username = authFields['username']; + let response = authFields['response']; + let cnonce = authFields['cnonce'] || ''; + let expectedResponse = getDigestResponse( + req, algorithm, sess, realm, username, nonce, nc, cnonce, qop + ); + if (!response || expectedResponse.toLowerCase() !== response.toLowerCase()) { + console.log('test3'); + res.status(401).end(); + return; + } + + res.status(201).end(); + } + before(function(done) { testApp = express(); @@ -222,6 +317,21 @@ describe('HTTP Request Node', function() { } res.json(result); }); + testApp.get('/authenticate-digest-md5', function(req, res){ + handleDigestResponse(req, res, "MD5", false, false); + }); + testApp.get('/authenticate-digest-md5-sess', function(req, res){ + handleDigestResponse(req, res, "MD5", true, 'auth'); + }); + testApp.get('/authenticate-digest-md5-qop', function(req, res){ + handleDigestResponse(req, res, "MD5", false, 'auth'); + }); + testApp.get('/authenticate-digest-sha-256', function(req, res){ + handleDigestResponse(req, res, "SHA-256", false, 'auth'); + }); + testApp.get('/authenticate-digest-sha-512-256', function(req, res){ + handleDigestResponse(req, res, "SHA-512-256", false, 'auth'); + }); testApp.get('/proxyAuthenticate', function(req, res){ // var user = auth.parse(req.headers['proxy-authorization']); var result = { @@ -2018,6 +2128,100 @@ describe('HTTP Request Node', function() { }); */ + it('should authenticate on server - digest MD5', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",authType:"digest", url:getTestURL('/authenticate-digest-md5')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.credentials = {user:'xxxuser', password:getDigestPassword()}; + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',201); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should authenticate on server - digest MD5 sess', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",authType:"digest", url:getTestURL('/authenticate-digest-md5-sess')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.credentials = {user:'xxxuser', password:getDigestPassword()}; + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',201); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should authenticate on server - digest MD5 qop', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",authType:"digest", url:getTestURL('/authenticate-digest-md5-qop')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.credentials = {user:'xxxuser', password:getDigestPassword()}; + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',201); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should authenticate on server - digest SHA-256', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",authType:"digest", url:getTestURL('/authenticate-digest-sha-256')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.credentials = {user:'xxxuser', password:getDigestPassword()}; + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',201); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); + + it('should authenticate on server - digest SHA-512-256', function(done) { + var flow = [{id:"n1",type:"http request",wires:[["n2"]],method:"GET",ret:"obj",authType:"digest", url:getTestURL('/authenticate-digest-sha-512-256')}, + {id:"n2", type:"helper"}]; + helper.load(httpRequestNode, flow, function() { + var n1 = helper.getNode("n1"); + var n2 = helper.getNode("n2"); + n1.credentials = {user:'xxxuser', password:getDigestPassword()}; + n2.on("input", function(msg) { + try { + msg.should.have.property('statusCode',201); + done(); + } catch(err) { + done(err); + } + }); + n1.receive({payload:"foo"}); + }); + }); }); describe('file-upload', function() {