Code coverage report for lib\mex.js

Statements: 89.51% (128 / 143)      Branches: 58% (29 / 50)      Functions: 100% (13 / 13)      Lines: 89.44% (127 / 142)      Ignored: none     

All files » lib/ » mex.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297                                          1 1 1 1 1 1   1 1   1                 1 22 22 22 22 22 22 22               1   9                               1 14 14 14 14   13 13 13     13 11 4   9 9           1 1                 1 11 11 11 11     11 11 11     11 11             11               1 11 11 11 11       11 11 11 11 11 11     11     1 1 1 1             1 11 11 11 11 11 11 11 11   11 11   11 11 11       11                 1 11 11 11 141 141 141 141 11 11 11 11       11                 1 11 11     1 1             1 11 11     11 143 143     143 143   143 143 11 11 11     11 11 10   1                       1 11 11       11 11               1 11 11 11       11 11       11 11 10 10     1
/*
 * @copyright
 * Copyright © Microsoft Open Technologies, Inc.
 *
 * All Rights Reserved
 *
 * 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
 *
 * THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS
 * OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 * ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A
 * PARTICULAR PURPOSE, MERCHANTABILITY OR NON-INFRINGEMENT.
 *
 * See the Apache License, Version 2.0 for the specific language
 * governing permissions and limitations under the License.
 */
'use strict';
var request = require('request');
var url = require('url');
var DOMParser = require('xmldom').DOMParser;
var _ = require('underscore');
var Logger = require('./log').Logger;
var util = require('./util');
 
var xmlutil = require('./xmlutil');
var select = xmlutil.xpathSelect;
 
var Namespaces = require('./constants').XmlNamespaces;
 
/**
 * Create a new Mex object.
 * @private
 * @constructor
 * @param {object} callContext Contains any context information that applies to the request.
 * @param {string} url  The url of the mex endpoint.
 */
function Mex(callContext, url) {
  this._log = new Logger('MEX', callContext._logContext);
  this._callContext = callContext;
  this._url = url;
  this._dom = null;
  this._mexDoc = null;
  this._usernamePasswordUrl = null;
  this._log.verbose('Mex created with url: ' + url);
}
/**
* Returns the IDP url from which a username passwowrd can be exchanged for a token.
* @instance
* @memberOf Mex
* @name usernamePasswordUrl
*/
Object.defineProperty(Mex.prototype, 'usernamePasswordUrl', {
  get: function() {
    return this._usernamePasswordUrl;
  }
});
 
/**
* @callback DiscoverCallback
* @memberOf Mex
* @param {object} error
*/
 
/**
* Performs Mex discovery.  This method will retrieve the mex document, parse it, and extract
* the username password ws-trust endpoint.
* @private
* @param {Mex.DiscoverCallback}  callback  Called when discover is complete.
*/
Mex.prototype.discover = function (callback) {
  this._log.verbose('Retrieving mex at: ' + this._url);
  var self = this;
  var options = util.createRequestOptions(self, { headers : { 'Content-Type' : 'application/soap+xml'} });
  request.get(this._url, options, util.createRequestHandler('Mex Get', this._log, callback,
    function(response, body) {
      try {
        self._mexDoc = body;
        var options = {
          errorHandler : self._log.error
        };
        self._dom = new DOMParser(options).parseFromString(self._mexDoc);
        self._parse(callback);
        return;
      } catch (err) {
        self._log.error('Failed to parse mex response in to DOM', err);
        callback(err);
      }
    })
  );
};
 
var TRANSPORT_BINDING_XPATH = 'wsp:ExactlyOne/wsp:All/sp:TransportBinding';
var TRANSPORT_BINDING_2005_XPATH = 'wsp:ExactlyOne/wsp:All/sp2005:TransportBinding';
/**
* Checks a DOM policy node that is a potentialy appplicable username password policy
* to ensure that it has the correct transport.
* @private
* @param {object} policyNode  The policy node to check.
* @returns {string} If the policy matches the desired transport then the id of the policy is returned.
*                   If not then null is returned.
*/
Mex.prototype._checkPolicy = function(policyNode) {
  var policyId = null;
  var id = policyNode.getAttributeNS(Namespaces.wsu, 'Id');
  var transportBindingNodes = select(policyNode, TRANSPORT_BINDING_XPATH);
  Iif (0 === transportBindingNodes.length) {
    transportBindingNodes = select(policyNode, TRANSPORT_BINDING_2005_XPATH);
  }
  Eif (0 !== transportBindingNodes.length) {
    Eif (id) {
      policyId = id;
    }
  }
  Eif (policyId) {
    this._log.verbose('found matching policy id: ' + policyId);
  } else {
    if (!id) {
      id = '<no id>';
    }
    this._log.verbose('potential policy did not match required transport binding: ' + id);
  }
  return policyId;
};
 
/**
* Finds all username password policies within the mex document.
* @private
* @returns {object} A map object that contains objects containing the id of username password polices.
*/
Mex.prototype._selectUsernamePasswordPolicies = function() {
  var policies = {};
  var xpath = '//wsdl:definitions/wsp:Policy/wsp:ExactlyOne/wsp:All/sp:SignedEncryptedSupportingTokens/wsp:Policy/sp:UsernameToken/wsp:Policy/sp:WssUsernameToken10';
  var usernameTokenNodes = select(this._dom, xpath);
  Iif (!usernameTokenNodes.length) {
    this._log.warn('no username token policy nodes found');
    return;
  }
  for (var i=0; i < usernameTokenNodes.length; i++) {
    var policyNode = usernameTokenNodes[i].parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
    var id = this._checkPolicy(policyNode);
    Eif (id) {
      var idRef = '#' + id;
      policies[idRef] = { id : idRef };
    }
  }
  return _.isEmpty(policies) ? null : policies;
};
 
var SOAP_ACTION_XPATH = 'wsdl:operation/soap12:operation/@soapAction';
var RST_SOAP_ACTION = 'http://docs.oasis-open.org/ws-sx/ws-trust/200512/RST/Issue';
var SOAP_TRANSPORT_XPATH = 'soap12:binding/@transport';
var SOAP_HTTP_TRANSPORT_VALUE = 'http://schemas.xmlsoap.org/soap/http';
/**
* Given a DOM binding node determines whether it matches the correct soap action and transport.
* @private
* @param {object} bindingNode   The DOM node to check.
* @returns {bool}
*/
Mex.prototype._checkSoapActionAndTransport = function(bindingNode) {
  var soapTransportAttributes;
  var soapAction;
  var soapTransport;
  var bindingName = bindingNode.getAttribute('name');
  var soapActionAttributes = select(bindingNode, SOAP_ACTION_XPATH);
  Eif (soapActionAttributes.length) {
    soapAction = soapActionAttributes[0].value;
    soapTransportAttributes = select(bindingNode, SOAP_TRANSPORT_XPATH);
  }
  Eif (soapTransportAttributes.length) {
    soapTransport = soapTransportAttributes[0].value;
  }
  var found = soapAction === RST_SOAP_ACTION && soapTransport === SOAP_HTTP_TRANSPORT_VALUE;
  Eif (found) {
    this._log.verbose('found binding matching Action and Transport: ' + bindingName);
  } else {
    this._log.verbose('binding node did not match soap Action or Transport: ' + bindingName);
  }
  return found;
};
 
/**
* Given a map with policy id keys, finds the bindings in the mex document that are linked to thos policies.
* @private
* @param {object}   policies  A map with policy id keys.
* @returns {object} a map of bindings id's to policy id's.
*/
Mex.prototype._getMatchingBindings = function(policies) {
  var bindings = {};
  var bindingPolicyRefNodes = select(this._dom, '//wsdl:definitions/wsdl:binding/wsp:PolicyReference');
  for (var i=0; i < bindingPolicyRefNodes.length; i++) {
    var node = bindingPolicyRefNodes[i];
    var uri = node.getAttribute('URI');
    var policy = policies[uri];
    if (policy) {
      var bindingNode = node.parentNode;
      var bindingName = bindingNode.getAttribute('name');
      Eif (this._checkSoapActionAndTransport(bindingNode)) {
        bindings[bindingName] = uri;
      }
    }
  }
  return _.isEmpty(bindings) ? null : bindings;
};
 
/**
* Ensures that a url points to an SSL endpoint.
* @private
* @param {string} endpointUrl   The url to check.
* @returns {bool}
*/
Mex.prototype._urlIsSecure = function(endpointUrl) {
  var parsedUrl = url.parse(endpointUrl);
  return parsedUrl.protocol === 'https:';
};
 
var PORT_XPATH = '//wsdl:definitions/wsdl:service/wsdl:port';
var ADDRESS_XPATH = 'wsa10:EndpointReference/wsa10:Address';
/**
* Finds all of the wsdl ports in the mex document that are associated with username password policies.  Augments
* the passed in bindings with the endpoint url of the correct port.
* @private
* @param {object} bindings  A map of binding id's to policy id's.
*/
Mex.prototype._getPortsForPolicyBindings = function(bindings, policies) {
  var portNodes = select(this._dom, PORT_XPATH);
  Iif (0 === portNodes.length) {
    this._log.warning('no ports found');
  }
  for (var i=0; i < portNodes.length; i++) {
    var portNode = portNodes[i];
    var bindingId = portNode.getAttribute('binding');
 
    // Clear any prefix
    var bindingIdParts = bindingId.split(':');
    bindingId = bindingIdParts[bindingIdParts.length - 1];
 
    var bindingPolicy = policies[bindings[bindingId]];
    if (bindingPolicy) {
      Eif (!bindingPolicy.url) {
        var addressNode = select(portNode, ADDRESS_XPATH);
        Iif (0 === addressNode) {
          throw this._log.createError('no address nodes on port.');
        }
        var address = xmlutil.findElementText(addressNode[0]);
        if (this._urlIsSecure(address)) {
          bindingPolicy.url = address;
        } else {
          this._log.warn('skipping insecure endpoint: ' + address);
        }
      }
    }
  }
};
 
/**
* Given a list of username password policies chooses one of them at random as the policy chosen by this Mex instance.
* @private
* @param {object} policies  A map of policy id's to an object containing username password ws-trust endpoint addresses.
*/
Mex.prototype._selectSingleMatchingPolicy = function(policies) {
  var matchingPolicies = _.filter(policies, function(policy) { return policy.url ? true : false; });
  Iif (!matchingPolicies) {
    this._log.warning('no policies found with an url');
    return;
  }
  matchingPolicies = _.shuffle(matchingPolicies);
  this._usernamePasswordUrl = matchingPolicies[0].url;
};
 
/**
* Parses the mex document previously retrieved.
* @private
* @param {Mex.DiscoverCallback} callback
*/
Mex.prototype._parse = function(callback) {
  var self = this;
  var policies = self._selectUsernamePasswordPolicies();
  Iif (!policies) {
    callback(self._log.createError('No matching policies'));
    return;
  }
  var bindings = self._getMatchingBindings(policies);
  Iif (!bindings) {
    callback(self._log.createError('No matching bindings'));
    return;
  }
  self._getPortsForPolicyBindings(bindings, policies);
  self._selectSingleMatchingPolicy(policies);
  var err = this._url ? undefined : this._log.createError('No ws-trust endpoints match requirements.');
  callback(err);
};
 
module.exports = Mex;