Source: lib/digest-wrapper.js

/*
Copyright 2012 MarkLogic Corporation

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var http = require('http'),
    crypto = require('crypto'),
    _und = require('underscore'),
    noop = require("./noop"),
    //FormData = require("form-data"),
    mp = require("./multipart");
    winston = require('winston');

var EventEmitter = require('events').EventEmitter;

/**
 * Wraps a HTTP request to the ML server for a particular user. Not to be instantiated directly.
 *
 * @constructor
 */
var DigestWrapper = function(){
  this.configure();
};

/**
 * Configures a HTTP Digest wrapper.
 */
DigestWrapper.prototype.configure = function(username,password,logger) {
  this.logger = logger;
  this.nc = 1;
  this.username = username;
  this.password = password;
  this.cnonce = "0a4f113b";
  this.nonce = undefined;
  this.opaque = undefined;
  this.realm = undefined;
  this.qop = undefined;
  this.ended = false;
};

/**
 * Performs a http request to the server
 */
DigestWrapper.prototype.request = function(options, content, callback_opt) {
  this.logger.debug("DigestWrapper.request()");
  //var cnonce = Math.floor(Math.random()*100000000);
  //this.logger.debug("DigestWrapper: in request() for " + options.host + ":" + options.port);

  var digestWrapper = this;
  var reqWrapper = new RequestWrapper(this.logger);

  var doRequest = function() {
    digestWrapper.logger.debug("in doRequest()");
    var ncUse = padNC(digestWrapper.nc);
    digestWrapper.nc++;
    var realPath = options.path;
    //digestWrapper.logger.debug("DigestWrapper: ----------------------------------------");
    //digestWrapper.logger.debug("DigestWrapper: options.method: '" + options.method + "'");
    //digestWrapper.logger.debug("DigestWrapper: options.hostname: '" + options.host + "'");
    //digestWrapper.logger.debug("DigestWrapper: options.port: '" + options.port + "'");
    //digestWrapper.logger.debug("DigestWrapper: options.path: '" + options.path + "'");
    //digestWrapper.logger.debug("DigestWrapper: options.contentType: '" + options.contentType + "'");
    //digestWrapper.logger.debug("DigestWrapper: path: '" + realPath + "'");
    //digestWrapper.logger.debug("DigestWrapper: cnonce: '" + digestWrapper.cnonce + "'");
    //digestWrapper.logger.debug("DigestWrapper: nonce: '" + digestWrapper.nonce + "'");
    //digestWrapper.logger.debug("DigestWrapper: nc: '" + ncUse + "'");
    //digestWrapper.logger.debug("DigestWrapper: realm: '" + digestWrapper.realm + "'");
    //digestWrapper.logger.debug("DigestWrapper: qop: '" + digestWrapper.qop + "'");
    //digestWrapper.logger.debug("DigestWrapper: opaque: '" + digestWrapper.opaque + "'");
    /*var h = "Headers: ";
    if (undefined == options.headers) {
      h += "indefined";
    } else {
    }*/

    // contentType header usage
    if (undefined != options.contentType) {
      options.headers["Content-type"] = options.contentType;
    }
    if (content instanceof mp) {
      digestWrapper.logger.debug("in doRequest(): Got formdata");
      options.headers = content.getHeaders();
      // workaround for mime type
      //options.headers["content-type"] = options.headers["content-type"].replace(/form-data/g,"mixed");
    }
    digestWrapper.logger.debug("in doRequest(): Content type header now: " + options.headers["Content-type"]);

    // See Client Request at http://en.wikipedia.org/wiki/Digest_access_authentication
    var md5ha1 = crypto.createHash('md5');
    var ha1raw = digestWrapper.username + ":" + digestWrapper.realm + ":" + digestWrapper.password;
    //digestWrapper.logger.debug("DigestWrapper: ha1raw: " + ha1raw);
    md5ha1.update(ha1raw);
    var ha1 = md5ha1.digest('hex');

    var md5ha2 = crypto.createHash('md5');
    var ha2raw = options.method + ":" + realPath;
    //digestWrapper.logger.debug("DigestWrapper: ha2raw: " + ha2raw);
    md5ha2.update(ha2raw);

    var ha2 = md5ha2.digest('hex'); // TODO check ? params are ok for the uri

    var md5r = crypto.createHash('md5');
    var md5rraw = ha1 + ":" + digestWrapper.nonce + ":" + ncUse + ":" + digestWrapper.cnonce + ":" + digestWrapper.qop + ":" + ha2;
    //digestWrapper.logger.debug("DigestWrapper: md5rraw: " + md5rraw);
    md5r.update(md5rraw);

    var response = md5r.digest('hex');
    options.headers['Authorization']= 'Digest username="' + digestWrapper.username + '", realm="' + digestWrapper.realm + '", nonce="' + digestWrapper.nonce + '", uri="' + options.path + '",' + // TODO check if we remove query ? params from uri
      ' cnonce="' + digestWrapper.cnonce + '", nc=' + ncUse + ', qop="' + digestWrapper.qop + '", response="' + response + '", opaque="' + digestWrapper.opaque + '"';
    //digestWrapper.logger.debug("DigestWrapper: Auth header: " + options.headers["Authorization"]);

    //digestWrapper.logger.debug("DigestWrapper: request options: " + JSON.stringify(options));
    //digestWrapper.logger.debug("DigestWrapper: Calling http request...");
    var finalReq = http.request(options,(callback_opt || noop));
    //digestWrapper.logger.debug("DigestWrapper: Returned from http request.");

    // to wrap sending of content by client code after when this request is created
    finalReq.on("end", function(res) {
      reqWrapper.doEnd(res); // NEVER GETS CALLED - EVENT END DOES NOT EXIST ON CLIENTREQUEST
    });
    finalReq.on('error', function(e) {
      //digestWrapper.logger.debug('DigestWrapper: finalReq.error: problem with request: ' + JSON.stringify(e));
      // pass error up
      reqWrapper.error(e);
    });

    digestWrapper.logger.debug("in doRequest(): setting finalReq on wrapper");
    reqWrapper.finalReq = finalReq;
    digestWrapper.logger.debug("in doRequest(): calling finaliseRequest on wrapper");
    reqWrapper.finaliseRequest();
    digestWrapper.logger.debug("in doRequest(): finaliseRequest complete");

    //digestWrapper.logger.debug("DigestWrapper: completed doRequest()");

/*
    if ('GET' == options.method) {

    } else if ('POST' == options.method) {
      //http.post(options,func);
      // TODO
    } else {
      this.logger.debug("DigestWrapper: HTTP METHOD UNSUPPORTED");
    }*/
  };

  // see if we have a realm and nonce
  if (undefined != this.realm) {
    this.logger.debug("DigestWrapper: Got a Realm");
    doRequest();
  } else {
    this.logger.debug("DigestWrapper: Not got a Realm, wrapping request");

    // do authorization request then call doRequest
    var myopts = {
      host: options.host,
      port: options.port
    }

    var self = this;
    this.logger.debug("DigestWrapper: calling http.get for auth");
    var get = http.get(myopts,function(res) {
      self.logger.debug("DigestWrapper: http.get has completed in order to carry out authentication");
      //self.logger.debug("Check: " + res.statusCode);
      res.on('end', function() {
        self.logger.debug("DigestWrapper: auth http.get response end event raised");
        // check if http 401
        //self.logger.debug("DigestWrapper: Got HTTP response: " + res.statusCode);
        // if so, extract WWW-Authenticate header information for later requests
        //self.logger.debug("DigestWrapper: Header: www-authenticate: " + res.headers["www-authenticate"]);
        // E.g. from ML REST API:  Digest realm="public", qop="auth", nonce="5ffb75b7b92c8d30fe2bfce28f024a0f", opaque="b847f531f584350a"

                digestWrapper.nc = 1;

                // response may have failed - check response code prior to calling doRequest
                if (403 == res.statusCode) {
                      self.logger.debug("DigestWrapper: 403 response");
                  // server does not exist - failed
                  var response = new ErrorResponse({statusCode: 403});
                  reqWrapper.__response = response;
                  reqWrapper.__callback = callback_opt;
                  reqWrapper.doEnd(response); // TODO check this works as expected for all requests
                  reqWrapper.finaliseRequest();
                } else {
                  // TODO via a proxy a request could succeed UNTIL AFTER a normal request has happened - need to account for that scenario too

                  self.logger.debug("DigestWrapper: other response: " + res.statusCode);
                  var auth = res.headers["www-authenticate"];
                  self.logger.debug("HEADERS: " + JSON.stringify(res.headers));
                  if (undefined != auth) {
                    var params = parseDigest(auth);
                    digestWrapper.nonce = params.nonce;
                    digestWrapper.realm = params.realm;
                    digestWrapper.qop = params.qop;
                    digestWrapper.opaque = params.opaque;
                  }

                  self.logger.debug("DigestWrapper: calling doRequest()");
                  doRequest();

                }

        self.logger.debug("DigestWrapper: After response handling code");

      });

      self.logger.debug("DigestWrapper: Got response for auth request");

      res.on('readable', function() {
        //self.logger.debug("response read");
        // do nothing with the response
        res.read();
      });
      //res.on('close', function() { this.logger.debug("DigestWrapper: CLOSE");});
      //res.on('data',  function() { this.logger.debug("DigestWrapper: DATA");});
    });

    get.on("error",function(e) {
      self.logger.debug("DigestWrapper: Auth request in error: " + e);
      reqWrapper.error(e);
    });
  }
  return reqWrapper;
};


module.exports = function() {
  return new DigestWrapper();
};

var RequestWrapper = function(logger) {
  this.logger = logger;
  this.writeData = "";
  this.emitter = new EventEmitter();
  this.ended = false;
  this.finalReq = undefined;
  this.__response = undefined;
  this.__callback = undefined;
};

RequestWrapper.prototype.write = function(data,encoding) {
  //this.writeData += data;
  this.writeData = data;
};

RequestWrapper.prototype.on = function(evt,func) {
  this.emitter.on(evt,func);
};

RequestWrapper.prototype.end = function() {
  this.logger.debug("DigestWrapper.end called");
  this.ended = true;
  this.finaliseRequest();
};

RequestWrapper.prototype.error = function(e) {
  this.logger.debug("DigestWrapper.error called: " + e);
  this.ended = true;
  this.emitter.emit("error",e);
  /*this.finalReq = undefined;
  this.__response = e;
  this.finaliseRequest();*/
};

RequestWrapper.prototype.finaliseRequest = function() {
  this.logger.debug("DigestWrapper.RequestWrapper.finaliseRequest called");
  var self = this;
  if (this.ended && this.finalReq != undefined) {
    this.logger.debug("DigestWrapper.RequestWrapper.finaliseRequest: Finalising request");

    if (this.writeData != undefined /*&& this.writeData.length > 0*/) {
      this.logger.debug("DigestWrapper.RequestWrapper.finaliseRequest: we have data - sending it now");
      //this.logger.debug("DigestWrapper: Sending POST data: " + this.writeData);
      var data = this.writeData;
      this.writeData = undefined; // clear out for next request
      if (data instanceof mp) {
        self.logger.debug("DigestWrapper.RequestWrapper.finaliseRequest: Got a FormData instance");
        /*data.getLength(function(err, length) {
          if (err) {
            self.logger.debug("DigestWrapper.RequestWrapper.finaliseRequest: error reading formdata.length: " + err);
            throw new Error(err);
          }*/

          //var r = request.post("http://posttestserver.com/post.php", requestCallback);
          // hard code content type too
          //self.finalReq.setHeader("Content-type","multipart/mixed");
          self.logger.debug("DigestWrapper.RequestWrapper.finaliseRequest: Content header sent was: " + self.finalReq.getHeader("Content-type"));
          //self.finalReq._form = data;

          data.pipe(self.finalReq);

          //self.finalReq.setHeader('content-length', length); // doesn't appear to be required on MarkLogic Server (Good too - because otherwise we get a headers already sent http error!)
          self.logger.debug("DigestWrapper.RequestWrapper.finaliseRequest: Finished processing form data");
        //});
      } else {
        this.logger.debug("DigestWrapper.RequestWrapper.finaliseRequest: Writing non-form-data");
        this.finalReq.write(data);
      }
    }
    //this.logger.debug("DigestWrapper.calling finalReq.end ");
    this.finalReq.end();
  } else {
    this.logger.debug("DigestWrapper.RequestWrapper.finaliseRequest: Not ended yet. Skipping for now.");
  }
  if (this.ended && this.__response != undefined) {
    //this.logger.debug("DigestWrapper.calling callback with response: " + JSON.stringify(this.__response));
    // handle bad requests
    var response = this.__response;
    var cb = this.__callback;
    this.__response = undefined; // clear for next request
    this.__callback = undefined;
    (cb || noop)(response);
  }
};

RequestWrapper.prototype.doEnd = function(res) {
  this.logger.debug("DigestWrapper.doEnd: end Called.");
  this.emitter.emit("end",res);
};


var ErrorResponse = function(response) {
  this.response = response;
  this.statusCode = response.statusCode;
  this.emitter = new EventEmitter();
};

ErrorResponse.prototype.on = function(evt,callback) {
  this.emitter.on(evt,callback);
  if (evt == "error") {
    // call it now
    this.emitter.emit(evt,this.response);
  }
};



/**
 * A function to split a Digest auth request header in to its constituent parts.
 *
 * @param {string} header - the raw http auth header to parse
 */
function parseDigest(header) {
  console.log("HEADER: " + header);
  return _und(header.substring(7).split(/,\s+/)).reduce(function(obj, s) {
    var parts = s.split('=')
    obj[parts[0]] = parts[1].replace(/"/g, '')
    return obj
    }, {})
  };

/**
 * Functions to pad the nc value. E.g. turns '1' in to '00000001'.
 */
  function padNC(num) {
    var pad = "";
    for (var i = 0;i < (8 - ("" + num).length);i++) {
      pad += "0";
    }
    var ret = pad + num;
    //this.logger.debug("pad: " + ret);
    return ret;
  };
MLJS - A JavaScript wrapper for the MarkLogic REST API
MarkLogic 2012-2014
Documentation generated by JSDoc 3.2.3-dev on Mon Jul 18 2016 09:14:11 GMT+0100 (BST) using the DocStrap template.