File: source/ice-connection.js

/**
 * The list of Peer connection ICE connection triggered states.
 * Refer to [w3c WebRTC Specification Draft](http://www.w3.org/TR/webrtc/#idl-def-RTCIceConnectionState).
 * @attribute ICE_CONNECTION_STATE
 * @type JSON
 * @param {String} STARTING The ICE agent is gathering addresses
 *   and/or waiting for remote candidates to be supplied.
 * @param {String} CHECKING The ICE agent has received remote candidates
 *   on at least one component, and is checking candidate pairs but has
 *   not yet found a connection. In addition to checking, it may also
 *   still be gathering.
 * @param {String} CONNECTED The ICE agent has found a usable connection
 *   for all components but is still checking other candidate pairs to see
 *   if there is a better connection. It may also still be gathering.
 * @param {String} COMPLETED The ICE agent has finished gathering and
 *   checking and found a connection for all components.
 * @param {String} FAILED The ICE agent is finished checking all
 *   candidate pairs and failed to find a connection for at least one
 *   component.
 * @param {String} DISCONNECTED Liveness checks have failed for one or
 *   more components. This is more aggressive than "failed", and may
 *   trigger intermittently (and resolve itself without action) on
 *   a flaky network.
 * @param {String} CLOSED The ICE agent has shut down and is no
 *   longer responding to STUN requests.
 * @readOnly
 * @since 0.1.0
 * @component ICE
 * @for Skylink
 */
Skylink.prototype.ICE_CONNECTION_STATE = {
  STARTING: 'starting',
  CHECKING: 'checking',
  CONNECTED: 'connected',
  COMPLETED: 'completed',
  CLOSED: 'closed',
  FAILED: 'failed',
  TRICKLE_FAILED: 'trickleFailed',
  DISCONNECTED: 'disconnected'
};

/**
 * The list of TURN server transports flags to set for TURN server connections.
 * @attribute TURN_TRANSPORT
 * @type JSON
 * @param {String} TCP Use only TCP transport option.
 *   <i>E.g. <code>turn:turnurl:5523?transport=tcp</code></i>.
 * @param {String} UDP Use only UDP transport option.
 *   <i>E.g. <code>turn:turnurl:5523?transport=udp</code></i>.
 * @param {String} ANY Use any transports option given
 *   by the platform signaling.
 *   <i>E.g. <code>turn:turnurl:5523?transport=udp</code> or
 *   <code>turn:turnurl:5523?transport=tcp</code></i>.
 * @param {String} NONE Set no transport option in TURN servers.
 *   <i>E.g. <code>turn:turnurl:5523</code></i>
 * @param {String} ALL Use both UCP and TCP transports options.
 *   <i>E.g. <code>turn:turnurl:5523?transport=udp</code> and
 *   <code>turn:turnurl:5523?transport=tcp</code></i>.
 * @readOnly
 * @since 0.5.4
 * @component ICE
 * @for Skylink
 */
Skylink.prototype.TURN_TRANSPORT = {
  UDP: 'udp',
  TCP: 'tcp',
  ANY: 'any',
  NONE: 'none',
  ALL: 'all'
};

/**
 * The flag that indicates if PeerConnections should enable
 *    trickling of ICE to connect the ICE connection.
 * @attribute _enableIceTrickle
 * @type Boolean
 * @default true
 * @private
 * @required
 * @since 0.3.0
 * @component ICE
 * @for Skylink
 */
Skylink.prototype._enableIceTrickle = true;

/**
 * The flag that indicates if PeerConnections ICE gathering
 *   should use STUN server connection.
 * @attribute _enableSTUN
 * @type Boolean
 * @default true
 * @private
 * @required
 * @component ICE
 * @since 0.5.4
 */
Skylink.prototype._enableSTUN = true;

/**
 * The flag that indicates if PeerConnections ICE gathering
 *   should use TURN server connection.
 * Tampering this flag may disable any successful Peer connection
 *   that is behind any firewalls.
 * @attribute _enableTURN
 * @type Boolean
 * @default true
 * @private
 * @required
 * @component ICE
 * @since 0.5.4
 */
Skylink.prototype._enableTURN = true;

/**
 * The flag to enable using of public STUN server connections.
 * @attribute _usePublicSTUN
 * @type Boolean
 * @default true
 * @required
 * @private
 * @component ICE
 * @for Skylink
 * @since 0.6.1
 */
Skylink.prototype._usePublicSTUN = true;

/**
 * Stores the TURN server transport to enable for TURN server connections.
 * [Rel: Skylink.TURN_TRANSPORT]
 * @attribute _TURNTransport
 * @type String
 * @default Skylink.TURN_TRANSPORT.ANY
 * @private
 * @required
 * @since 0.5.4
 * @component ICE
 * @for Skylink
 */
Skylink.prototype._TURNTransport = 'any';

/**
 * Stores the list of Peer connection ICE connection failures.
 * After an third attempt of ICE connection failure, the
 *   trickling of ICE would be disabled.
 * @attribute _ICEConnectionFailures
 * @param {Number} (#peerId) The Peer ID associated with the
 *   number of Peer connection ICE connection attempt failures.
 * @type JSON
 * @private
 * @required
 * @component Peer
 * @for Skylink
 * @since 0.5.8
 */
Skylink.prototype._ICEConnectionFailures = {};

/**
 * Reconfigures the <code>RTCConfiguration.iceServers</code> that is
 *   to be passed in constructing the new <code>RTCPeerConnection</code>
 *   object for different browsers support.
 * Previously known as <code>_setFirefoxIceServers</code>.
 * This method will reconfigure <code>urls</code> configuration to
 *   an array of <code>url</code> configuration.
 * @method _parseIceServers
 * @param {JSON} config The RTCConfiguration that is to be passed for
 *   constructing the new RTCPeerConnection object.
 * @return {JSON} The updated RTCConfiguration object with Firefox
 *   specific STUN configuration.
 * @private
 * @since 0.6.1
 * @component ICE
 * @for Skylink
 */
Skylink.prototype._parseIceServers = function(config) {
  var self = this;
  var newIceServers = [];

  var parseIceServer = function (iceServer) {
    if (iceServer.url.indexOf('@') > 0) {
      var protocolParts = iceServer.url.split(':');
      var urlParts = protocolParts[1].split('@');
      iceServer.username = urlParts[0];
      iceServer.url = protocolParts[0] + ':' + urlParts[1];

      // add the ICE server port
      if (protocolParts[2]) {
        iceServer.url += ':' + protocolParts[2];
      }
    }

    if (iceServer.url.indexOf('stun:') === 0 &&
      window.webrtcDetectedBrowser === 'firefox' &&
      iceServer.url.indexOf('google') > 0) {
      return null;
    }

    return createIceServer(iceServer.url,
      iceServer.username || null, iceServer.credential || null);
  };

  for (var i = 0; i < config.iceServers.length; i++) {
    var iceServer = config.iceServers[i];
    var newIceServer = null;

    if (Array.isArray(iceServer.urls)) {
      for (var j = 0; j < iceServer.urls.length; j++) {
        var iceServerIndex = {
          username: iceServer.username,
          url: iceServer.urls[j],
          credential: iceServer.credential
        };

        newIceServer = parseIceServer(iceServerIndex);

        if (newIceServer !== null) {
          newIceServers.push(newIceServer);
        }
      }
    } else {
      newIceServer = parseIceServer(iceServer);

      if (newIceServer !== null) {
        newIceServers.push(newIceServer);
      }
    }
  }


  // for firefox STUN
  if (window.webrtcDetectedBrowser === 'firefox' && this._enableSTUN) {
    newIceServers.splice(0, 0, {
      url: 'stun:stun.services.mozilla.com'
    });
  }

  return {
    iceServers: newIceServers
  };
};

/**
 * Reconfigures the <code>RTCConfiguration.iceServers</code> that is
 *   to be passed in constructing the new <code>RTCPeerConnection</code>
 *   object to remove (disable) STUN or remove TURN (disable) server
 *   connections based on the
 *   {{#crossLink "Skylink/init:method"}}init(){{/crossLink}}
 *   configuration passed in.
 * @method _setIceServers
 * @param {JSON} config The RTCConfiguration that is to be passed for
 *   constructing the new RTCPeerConnection object.
 * @return {JSON} The updated RTCConfiguration object based on the
 *   configuration settings in the
 *   {{#crossLink "Skylink/init:method"}}init(){{/crossLink}}
 *   method.
 * @private
 * @since 0.5.4
 * @component ICE
 * @for Skylink
 */
Skylink.prototype._setIceServers = function(givenConfig) {
  // firstly, set the STUN server specially for firefox
  var config = this._parseIceServers(givenConfig);

  var newConfig = {
    iceServers: []
  };

  // to prevent repeat
  var iceServerUrls = [];

  var useTURNSSLProtocol = false;
  var useTURNSSLPort = false;

  var clone = function (obj) {
    if (obj === null || typeof obj !== 'object') {
      return obj;
    }

    var copy = obj.constructor();
    for (var attr in obj) {
      if (obj.hasOwnProperty(attr)) {
        copy[attr] = obj[attr];
      }
    }
    return copy;
  };

  if (window.location.protocol === 'https:' || this._forceTURNSSL) {
    if (window.webrtcDetectedBrowser === 'chrome' ||
      window.webrtcDetectedBrowser === 'safari' ||
      window.webrtcDetectedBrowser === 'IE') {
      useTURNSSLProtocol = true;
    }
    useTURNSSLPort = true;
  }

  log.log('TURN server connections SSL configuration', {
    useTURNSSLProtocol: useTURNSSLProtocol,
    useTURNSSLPort: useTURNSSLPort
  });

  for (var i = 0; i < config.iceServers.length; i++) {
    var iceServer = config.iceServers[i];
    var iceServerParts = iceServer.url.split(':');
    var copyIceServers = [];

    // check for stun servers
    if (iceServerParts[0] === 'stun' || iceServerParts[0] === 'stuns') {
      if (!this._enableSTUN) {
        log.log('Removing STUN Server support', iceServer);
        continue;
      } else {
        if (!this._usePublicSTUN && iceServer.url.indexOf('temasys') === -1) {
          log.log('Remove public STUN Server support', iceServer);
          continue;
        }
        // STUNS is unsupported
        //iceServerParts[0] = (this._STUNSSL) ? 'stuns' : 'stun';
      }
      iceServer.url = iceServerParts.join(':');
    }
    // check for turn servers
    if (iceServerParts[0] === 'turn' || iceServerParts[0] === 'turns') {
      if (!this._enableTURN) {
        log.log('Removing TURN Server support', iceServer);
        continue;
      } else if (iceServer.url.indexOf(':443') === -1 && useTURNSSLPort) {
        log.log('Ignoring TURN Server with non-SSL port', iceServer);
        continue;
      } else {
        // this is terrible. No turns please
        iceServerParts[0] = (useTURNSSLProtocol) ? 'turns' : 'turn';
        iceServer.url = iceServerParts.join(':');
        // check if requires SSL
        log.log('Transport option:', this._TURNTransport);
        if (this._TURNTransport !== this.TURN_TRANSPORT.ANY) {
          // this has a transport attached to it
          if (iceServer.url.indexOf('?transport=') > -1) {
            // remove transport because user does not want it
            if (this._TURNTransport === this.TURN_TRANSPORT.NONE) {
              log.log('Removing transport option');
              iceServer.url = iceServer.url.split('?')[0];
            } else if (this._TURNTransport === this.TURN_TRANSPORT.ALL) {
              log.log('Setting for all transport option');



              var originalUrl = iceServer.url.split('?')[0];

              // turn:turn.test.com
              var iceServerTransportNone = clone(iceServer);
              iceServerTransportNone.url = originalUrl;
              copyIceServers.push(iceServerTransportNone);

              // turn:turn.test.com?transport=tcp
              var iceServerTransportTcp = clone(iceServer);
              iceServerTransportTcp.url = originalUrl + '?transport=' + this.TURN_TRANSPORT.TCP;
              copyIceServers.push(iceServerTransportTcp);

              // turn:turn.test.com?transport=udp
              var iceServerTransportUdp = clone(iceServer);
              iceServerTransportUdp.url = originalUrl + '?transport=' + this.TURN_TRANSPORT.UDP;
              copyIceServers.push(iceServerTransportUdp);

            } else {
              // UDP or TCP
              log.log('Setting transport option');
              var urlProtocolParts = iceServer.url.split('=');
              urlProtocolParts[1] = this._TURNTransport;
              iceServer.url = urlProtocolParts.join('=');
            }
          } else {
            if (this._TURNTransport !== this.TURN_TRANSPORT.NONE) {
              log.log('Setting transport option');
              // no transport here. manually add
              iceServer.url += '?transport=' + this._TURNTransport;
            }
          }
        }
      }
    }

    if (copyIceServers.length > 0) {
      for (var j = 0; j < copyIceServers.length; j++) {
        if (iceServerUrls.indexOf(copyIceServers[j].url) === -1) {
          newConfig.iceServers.push(copyIceServers[j]);
          iceServerUrls.push(copyIceServers[j].url);
        }
      }
    } else if (iceServerUrls.indexOf(iceServer.url) === -1) {
      newConfig.iceServers.push(iceServer);
      iceServerUrls.push(iceServer.url);
    }
  }

  log.log('Output iceServers configuration:', newConfig.iceServers);
  return newConfig;
};