alykoshin/wrtc-ice-cand-parse

View on GitHub
index.js

Summary

Maintainability
A
2 hrs
Test Coverage
/**
 * Created by alykoshin on 7/16/14.
 */

'use strict';

var debug = require('mini-debug');
var Enum = require('mini-enum');

//--------------------------------------------------------------------------------------------------------------------//
// http://tools.ietf.org/html/rfc5245#section-15.1
//
//  candidate-attribute   = "candidate" ":" foundation SP component-id SP
//                          transport SP
//                          priority SP
//                          connection-address SP     ;from RFC 4566
//                          port         ;port from RFC 4566
//                          SP cand-type
//                          [SP rel-addr]
//                          [SP rel-port]
//                          *(SP extension-att-name SP
//                               extension-att-value)
//
//  foundation            = 1*32ice-char
//  component-id          = 1*5DIGIT
//  transport             = "UDP" / transport-extension
//  transport-extension   = token              ; from RFC 3261
//  priority              = 1*10DIGIT
//  cand-type             = "typ" SP candidate-types
//  candidate-types       = "host" / "srflx" / "prflx" / "relay" / token
//  rel-addr              = "raddr" SP connection-address
//  rel-port              = "rport" SP port
//  extension-att-name    = byte-string    ;from RFC 4566
//  extension-att-value   = byte-string
//  ice-char              = ALPHA / DIGIT / "+" / "/"
//
// ------------------------------------------------------------------------------
// From RFC3261:
//    token               = 1*(alphanum / "-" / "." / "!" / "%" / "*"
//                          / "_" / "+" / "`" / "'" / "~" )
//
//    transport-param     = "transport="
//                          ( "udp" / "tcp" / "sctp" / "tls"
//                          / other-transport)
//    other-transport     = token
//------------------------------------------------------------------------------

// Chrome example:
//   a=candidate:1832966643 1 tcp 1509957375 192.168.1.32 0 typ host generation 0
//   a=candidate:3587398871 1 udp 1845501695 123.123.123.123 46670 typ srflx raddr 192.168.1.32 rport 46670 generation 0
//   a=candidate:3454638549 1 udp 33562367 123.12.123.123 64560 typ relay raddr 123.123.123.123 rport 48485 generation 0
//
// Firefox example - no 'a=' part !!! : (and now for Chrome too)
//   candidate:0 1 UDP 2122252543 192.168.1.32 45166 typ host
//   candidate:2 1 UDP 92274687 123.123.12.123 49185 typ relay raddr 12.123.12.123 rport 49185
//
// 2015-08-24 - new version of ICE in Chrome:
// candidate:599991555 2 udp 2122260222 192.168.1.32 49827 typ host generation 0
// {"candidate":"candidate:3689538886 1 udp 2122199807 0124:4567:89ab:cdef:6deb:9894:734:f75f 32950 typ host generation 0","sdpMid":"video","sdpMLineIndex":1}

/**
 * @type ICE_TYPE
 * @const
 */
var ICE_TYPE = Enum( {
  HOST:  'host',
  SRFLX: 'srflx',
  PRFLX: 'prflx',
  RELAY: 'relay'
});

/**
 * @type ICE_TRANSPORT
 * @const
 */
var ICE_TRANSPORT = Enum({
  TCP: 'tcp',
  UDP: 'udp'
});

/**
 * @typedef  {{foundation:string, component_id:string, transport: string, priority: string, localIP: string, localPort: string, type: string, remoteIP: string, remotePort: string, generation: string}} ParsedIce
 */


/**
 * Validate ICE Candidate Object (minimal)
 *
 * @param candObj
 * @returns boolean
 */
function validate(candObj) {
  var res = ICE_TYPE.check(candObj.type.toLowerCase());
  res = res && ICE_TRANSPORT.check(candObj.transport.toLowerCase());
  if (!res) { debug.warn('wrtc-ice-cand-parse: validate(): candObj validation failed'); }
  return res;
}

/**
 *
 * @param {string} candidateString
 * @returns {ParsedIce}
 */
function parse(candidateString) {
  // token                  =  1*(alphanum / "-" / "." / "!" / "%" / "*"
  //                              / "_" / "+" / "`" / "'" / "~" )
  var token_re              = '[0-9a-zA-Z\\-\\.!\\%\\*_\\+\\`\\\'\\~]+';

  // ice-char               = ALPHA / DIGIT / "+" / "/"
  var ice_char_re           = '[a-zA-Z0-9\\+\\/]+';

  // foundation             = 1*32ice-char
  var foundation_re         = ice_char_re;

  // component-id           = 1*5DIGIT
  var component_id_re       = '[0-9]{1,5}';

  // transport             = "UDP" / transport-extension
  // transport-extension   = token      ; from RFC 3261
  var transport_re          = token_re;

  // priority              = 1*10DIGIT
  var priority_re           = '[0-9]{1,10}';

  // connection-address SP      ; from RFC 4566
  var connection_address_v4_re = '[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}';
  var connection_address_v6_re = '\\:?(?:[0-9a-fA-F]{0,4}\\:?)+'; // fde8:cd2d:634c:6b00:6deb:9894:734:f75f

  var connection_address_re = '(?:'+connection_address_v4_re +')|(?:' + connection_address_v6_re+')';

  // port                      ; port from RFC 4566
  var port_re               = '[0-9]{1,5}';

  //  cand-type             = "typ" SP candidate-types
  //  candidate-types       = "host" / "srflx" / "prflx" / "relay" / token
  var cand_type_re  = token_re;
  // ICE_TYPE.HOST  + '|' +
  // ICE_TYPE.SRFLX + '|' +
  // ICE_TYPE.PRFLX + '|' +
  // ICE_TYPE.RELAY ;

  var ICE_RE = '(?:a=)?candidate:(' + foundation_re + ')' + // candidate:599991555 // 'a=' not passed for Firefox (and now for Chrome too)
    '\\s' + '(' + component_id_re       + ')' +                 // 2
    '\\s' + '(' + transport_re          + ')' +                 // udp
    '\\s' + '(' + priority_re           + ')' +                 // 2122260222
    '\\s' + '(' + connection_address_re + ')' +                 // 192.168.1.32 || fde8:cd2d:634c:6b00:6deb:9894:734:f75f
    '\\s' + '(' + port_re               + ')' +                 // 49827
    '\\s' +       'typ'                 +                       // typ
    '\\s' + '(' + cand_type_re          + ')' +                 // host
    '(?:'                               +
    '\\s' +       'raddr'               +
    '\\s' + '(' + connection_address_re + ')' +
    '\\s' +       'rport'               +
    '\\s' + '(' + port_re               + ')' +
    ')?'                                +
    '(?:'                               +
    '\\s' + 'generation'                +                       // generation
    '\\s' + '(' + '\\d+'                + ')' +                 // 0
    ')?'                           ;

  var pattern = new RegExp( ICE_RE);
  var parsed  = candidateString.match(pattern);

//  debug.log('parseIceCandidate(): candidateString:', candidateString);
//  debug.log('parseIceCandidate(): pattern:', pattern);
//  debug.log('parseIceCandidate(): parsed:', parsed);

  // Check if the string was successfully parsed
  if ( !parsed ) {
    debug.warn('parseIceCandidate(): parsed is empty: \'' + parsed + '\'');
    return null;
  }
  //var type = parsed[7] ? parsed[7] : null;
  //ICE_TYPE.check(type.toLowerCase());
  //
  //var transport = parsed[3];
  //ICE_TRANSPORT.check(transport.toLowerCase());

  var propNames = [
    'foundation',
    'component_id',
    'transport',
    'priority',
    'localIP',
    'localPort',
    'type',
    'remoteIP',
    'remotePort',
    'generation'
  ];

  var candObj = {};
  for (var i=0; i<propNames.length; i++) {
    candObj[ propNames[i] ] = parsed[i+1];
  }

  validate(candObj);

  //var candObj = {
  //  foundation: parsed[1],
  //  component_id: parsed[2],
  //  transport:  transport,
  //  priority:   parsed[4],
  //  localIP:    parsed[5],
  //  localPort:  parsed[6],
  //  type:       type,
  //  remoteIP:   parsed[8],
  //  remotePort: parsed[9],
  //  generation: parsed[10]
  //};

  return candObj;
}

/**
 *
 * @param {ParsedIce} iceCandObj
 * @param {{oldChrome:boolean}} [options]
 */
var stringify = function(iceCandObj, options) {
  options = options || {};
  options.oldChrome = options.oldChrome || false;

  var s =
        (options.oldChrome ? 'a=' : '') +
        'candidate:' + iceCandObj.foundation      + ''+
        ' '          + iceCandObj.component_id    + '' +
        ' '          + iceCandObj.transport       + '' +
        ' '          + iceCandObj.priority        + '' +
        ' '          + iceCandObj.localIP         + '' +
        ' '          + iceCandObj.localPort       + '' +
        ' typ '      + iceCandObj.type            + '' +
        (iceCandObj.remoteIP   ? ' raddr '      + iceCandObj.remoteIP   + '' : '') +
        (iceCandObj.remotePort ? ' rport '      + iceCandObj.remotePort + '' : '') +
        (iceCandObj.generation ? ' generation ' + iceCandObj.generation + '' : '');
  return s;
};


/**
 *
 *
 * @param {string} candStr
 * @returns {boolean}
 */
function isRelay( candStr ) {
  debug.warn('isRelay(): deprecated.');
  var candObj = parse( candStr );
  return (candObj.type && candObj.type.toLowerCase() === ICE_TYPE.RELAY);
}

////////////////////////////////////////////////////////////////////////////////

if (typeof module !== 'undefined') {
  module.exports = {
    parse:     parse,
    stringify: stringify,
    validate:  validate,
    isRelay:   isRelay
  };
}

if (typeof window !== 'undefined') {
  window.parseIceCandidate     = parse;
  window.stringifyIceCandidate = stringify;
  window.validateIceCandidate  = validate;
  window.isRelayIceCandidate   = isRelay;
}