Source: UA.js

module.exports = UA;


var C = {
  // UA status codes
  STATUS_INIT :                0,
  STATUS_READY:                1,
  STATUS_USER_CLOSED:          2,
  STATUS_NOT_READY:            3,

  // UA error codes
  CONFIGURATION_ERROR:  1,
  NETWORK_ERROR:        2
};

/**
 * Expose C object.
 */
UA.C = C;


/**
 * Dependencies.
 */
var debug = require('debug')('JsSIP:UA');
var JsSIP_C = require('./Constants');
var EventEmitter = require('./EventEmitter');
var Registrator = require('./Registrator');
var RTCSession = require('./RTCSession');
var Message = require('./Message');
var Transport = require('./Transport');
var Transactions = require('./Transactions');
var Transactions = require('./Transactions');
var Utils = require('./Utils');
var WebRTC = require('./WebRTC');
var Exceptions = require('./Exceptions');
var URI = require('./URI');
var Grammar = require('./Grammar');



/**
 * The User-Agent class.
 * @class JsSIP.UA
 * @param {Object} configuration Configuration parameters.
 * @throws {JsSIP.Exceptions.ConfigurationError} If a configuration parameter is invalid.
 * @throws {TypeError} If no configuration is given.
 */
function UA(configuration) {
  var events = [
    'connecting',
    'connected',
    'disconnected',
    'newTransaction',
    'transactionDestroyed',
    'registered',
    'unregistered',
    'registrationFailed',
    'newRTCSession',
    'newMessage'
  ];

  this.cache = {
    credentials: {}
  };

  this.configuration = {};
  this.dynConfiguration = {};
  this.dialogs = {};

  //User actions outside any session/dialog (MESSAGE)
  this.applicants = {};

  this.sessions = {};
  this.transport = null;
  this.contact = null;
  this.status = C.STATUS_INIT;
  this.error = null;
  this.transactions = {
    nist: {},
    nict: {},
    ist: {},
    ict: {}
  };

  // Custom UA empty object for high level use
  this.data = {};

  this.transportRecoverAttempts = 0;
  this.transportRecoveryTimer = null;

  Object.defineProperties(this, {
    transactionsCount: {
      get: function() {
        var type,
          transactions = ['nist','nict','ist','ict'],
          count = 0;

        for (type in transactions) {
          count += Object.keys(this.transactions[transactions[type]]).length;
        }

        return count;
      }
    },

    nictTransactionsCount: {
      get: function() {
        return Object.keys(this.transactions.nict).length;
      }
    },

    nistTransactionsCount: {
      get: function() {
        return Object.keys(this.transactions.nist).length;
      }
    },

    ictTransactionsCount: {
      get: function() {
        return Object.keys(this.transactions.ict).length;
      }
    },

    istTransactionsCount: {
      get: function() {
        return Object.keys(this.transactions.ist).length;
      }
    }
  });

  /**
   * Load configuration
   */

  if(configuration === undefined) {
    throw new TypeError('Not enough arguments');
  }

  try {
    this.loadConfig(configuration);
    this.initEvents(events);
  } catch(e) {
    this.status = C.STATUS_NOT_READY;
    this.error = C.CONFIGURATION_ERROR;
    throw e;
  }

  // Initialize registrator
  this._registrator = new Registrator(this);
}


UA.prototype = new EventEmitter();


//=================
//  High Level API
//=================

/**
 * Connect to the WS server if status = STATUS_INIT.
 * Resume UA after being closed.
 */
UA.prototype.start = function() {
  var server;

  debug('user requested startup');

  if (this.status === C.STATUS_INIT) {
    server = this.getNextWsServer();
    this.transport = new Transport(this, server);
    this.transport.connect();
  } else if(this.status === C.STATUS_USER_CLOSED) {
    debug('restarting UA');
    this.status = C.STATUS_READY;
    this.transport.connect();
  } else if (this.status === C.STATUS_READY) {
    debug('UA is in READY status, not restarted');
  } else {
    debug('ERROR: connection is down, Auto-Recovery system is trying to reconnect');
  }

  // Set dynamic configuration.
  this.dynConfiguration.register = this.configuration.register;
};

/**
 * Register.
 */
UA.prototype.register = function() {
  this.dynConfiguration.register = true;
  this._registrator.register();
};

/**
 * Unregister.
 */
UA.prototype.unregister = function(options) {
  this.dynConfiguration.register = false;
  this._registrator.unregister(options);
};

/**
 * Get the Registrator instance.
 */
UA.prototype.registrator = function() {
  return this._registrator;
};

/**
 * Registration state.
 */
UA.prototype.isRegistered = function() {
  if(this._registrator.registered) {
    return true;
  } else {
    return false;
  }
};

/**
 * Connection state.
 */
UA.prototype.isConnected = function() {
  if(this.transport) {
    return this.transport.connected;
  } else {
    return false;
  }
};

/**
 * Make an outgoing call.
 *
 * -param {String} target
 * -param {Object} views
 * -param {Object} [options]
 *
 * -throws {TypeError}
 *
 */
UA.prototype.call = function(target, options) {
  var session;

  session = new RTCSession(this);
  session.connect(target, options);
};

/**
 * Send a message.
 *
 * -param {String} target
 * -param {String} body
 * -param {Object} [options]
 *
 * -throws {TypeError}
 *
 */
UA.prototype.sendMessage = function(target, body, options) {
  var message;

  message = new Message(this);
  message.send(target, body, options);
};

/**
 * Gracefully close.
 *
 */
UA.prototype.stop = function() {
  var session;
  var applicant;
  var num_sessions;
  var ua = this;

  debug('user requested closure');

  // Remove dynamic settings.
  this.dynConfiguration = {};

  if(this.status === C.STATUS_USER_CLOSED) {
    debug('UA already closed');
    return;
  }

  // Clear transportRecoveryTimer
  clearTimeout(this.transportRecoveryTimer);

  // Close registrator
  this._registrator.close();

  // If there are session wait a bit so CANCEL/BYE can be sent and their responses received.
  num_sessions = Object.keys(this.sessions).length;

  // Run  _terminate_ on every Session
  for(session in this.sessions) {
    debug('closing session ' + session);
    this.sessions[session].terminate();
  }

  // Run  _close_ on every applicant
  for(applicant in this.applicants) {
    this.applicants[applicant].close();
  }

  this.status = C.STATUS_USER_CLOSED;

  // If there are no pending non-INVITE client or server transactions and no
  // sessions, then disconnect now. Otherwise wait for 2 seconds.
  if (this.nistTransactionsCount === 0 && this.nictTransactionsCount === 0 && num_sessions === 0) {
    ua.transport.disconnect();
  }
  else {
    setTimeout(function() {
      ua.transport.disconnect();
    }, 2000);
  }
};

/**
 * Normalice a string into a valid SIP request URI
 * -param {String} target
 * -returns {JsSIP.URI|undefined}
 */
UA.prototype.normalizeTarget = function(target) {
  return Utils.normalizeTarget(target, this.configuration.hostport_params);
};


//===============================
//  Private (For internal use)
//===============================

UA.prototype.saveCredentials = function(credentials) {
  this.cache.credentials[credentials.realm] = this.cache.credentials[credentials.realm] || {};
  this.cache.credentials[credentials.realm][credentials.uri] = credentials;
};

UA.prototype.getCredentials = function(request) {
  var realm, credentials;

  realm = request.ruri.host;

  if (this.cache.credentials[realm] && this.cache.credentials[realm][request.ruri]) {
    credentials = this.cache.credentials[realm][request.ruri];
    credentials.method = request.method;
  }

  return credentials;
};


//==========================
// Event Handlers
//==========================

/**
 * Transport Close event.
 */
UA.prototype.onTransportClosed = function(transport) {
  // Run _onTransportError_ callback on every client transaction using _transport_
  var type, idx, length,
  client_transactions = ['nict', 'ict', 'nist', 'ist'];

  transport.server.status = Transport.C.STATUS_DISCONNECTED;
  debug('connection state set to '+ Transport.C.STATUS_DISCONNECTED);

  length = client_transactions.length;
  for (type = 0; type < length; type++) {
    for(idx in this.transactions[client_transactions[type]]) {
      this.transactions[client_transactions[type]][idx].onTransportError();
    }
  }

  // Close sessions if GRUU is not being used
  if (!this.contact.pub_gruu) {
    this.closeSessionsOnTransportError();
  }
};

/**
 * Unrecoverable transport event.
 * Connection reattempt logic has been done and didn't success.
 */
UA.prototype.onTransportError = function(transport) {
  var server;

  debug('transport ' + transport.server.ws_uri + ' failed | connection state set to '+ Transport.C.STATUS_ERROR);

  // Close sessions.
  // Mark this transport as 'down' and try the next one
  transport.server.status = Transport.C.STATUS_ERROR;

  this.emit('disconnected', this, {
    transport: transport,
    code: transport.lastTransportError.code,
    reason: transport.lastTransportError.reason
  });

  // Don't attempt to recover the connection if the user closes the UA.
  if (this.status === C.STATUS_USER_CLOSED) {
    return;
  }

  server = this.getNextWsServer();

  if(server) {
    this.transport = new Transport(this, server);
    this.transport.connect();
  } else {
    this.closeSessionsOnTransportError();
    if (!this.error || this.error !== C.NETWORK_ERROR) {
      this.status = C.STATUS_NOT_READY;
      this.error = C.NETWORK_ERROR;
    }
    // Transport Recovery process
    this.recoverTransport();
  }
};

/**
 * Transport connection event.
 */
UA.prototype.onTransportConnected = function(transport) {
  this.transport = transport;

  // Reset transport recovery counter
  this.transportRecoverAttempts = 0;

  transport.server.status = Transport.C.STATUS_READY;
  debug('connection state set to '+ Transport.C.STATUS_READY);

  if(this.status === C.STATUS_USER_CLOSED) {
    return;
  }

  this.status = C.STATUS_READY;
  this.error = null;

  this.emit('connected', this, {
    transport: transport
  });

  if(this.dynConfiguration.register) {
    this._registrator.register();
  }
};


/**
 * Transport connecting event
 */
UA.prototype.onTransportConnecting = function(transport, attempts) {
  this.emit('connecting', this, {
    transport: transport,
    attempts: attempts
  });
};


/**
 * new Transaction
 */
UA.prototype.newTransaction = function(transaction) {
  this.transactions[transaction.type][transaction.id] = transaction;
    this.emit('newTransaction', this, {
    transaction: transaction
  });
};


/**
 * Transaction destroyed.
 */
UA.prototype.destroyTransaction = function(transaction) {
  delete this.transactions[transaction.type][transaction.id];
    this.emit('transactionDestroyed', this, {
    transaction: transaction
  });
};


//=========================
// receiveRequest
//=========================

/**
 * Request reception
 */
UA.prototype.receiveRequest = function(request) {
  var dialog, session, message,
  method = request.method;

  // Check that request URI points to us
  if(request.ruri.user !== this.configuration.uri.user && request.ruri.user !== this.contact.uri.user) {
    debug('Request-URI does not point to us');
    if (request.method !== JsSIP_C.ACK) {
      request.reply_sl(404);
    }
    return;
    }

    // Check request URI scheme
    if(request.ruri.scheme === JsSIP_C.SIPS) {
    request.reply_sl(416);
    return;
  }

  // Check transaction
  if(Transactions.checkTransaction(this, request)) {
    return;
  }

  // Create the server transaction
  if(method === JsSIP_C.INVITE) {
    new Transactions.InviteServerTransaction(request, this);
  } else if(method !== JsSIP_C.ACK && method !== JsSIP_C.CANCEL) {
    new Transactions.NonInviteServerTransaction(request, this);
  }

  /* RFC3261 12.2.2
   * Requests that do not change in any way the state of a dialog may be
   * received within a dialog (for example, an OPTIONS request).
   * They are processed as if they had been received outside the dialog.
   */
  if(method === JsSIP_C.OPTIONS) {
    request.reply(200);
  } else if (method === JsSIP_C.MESSAGE) {
    if (!this.checkEvent('newMessage') || this.listeners('newMessage').length === 0) {
      request.reply(405);
      return;
    }
    message = new Message(this);
    message.init_incoming(request);
  } else if (method === JsSIP_C.INVITE) {
    if (!this.checkEvent('newRTCSession') || this.listeners('newRTCSession').length === 0) {
      request.reply(405);
      return;
    }
  }

  // Initial Request
  if(!request.to_tag) {
    switch(method) {
      case JsSIP_C.INVITE:
        if(WebRTC.isSupported) {
          session = new RTCSession(this);
          session.init_incoming(request);
        } else {
          debug('INVITE received but WebRTC is not supported');
          request.reply(488);
        }
        break;
      case JsSIP_C.BYE:
        // Out of dialog BYE received
        request.reply(481);
        break;
        case JsSIP_C.CANCEL:
        session = this.findSession(request);
        if(session) {
          session.receiveRequest(request);
        } else {
          debug('received CANCEL request for a non existent session');
        }
        break;
      case JsSIP_C.ACK:
        /* Absorb it.
         * ACK request without a corresponding Invite Transaction
         * and without To tag.
         */
        break;
        default:
        request.reply(405);
        break;
    }
  }
  // In-dialog request
  else {
    dialog = this.findDialog(request);

    if(dialog) {
      dialog.receiveRequest(request);
    } else if (method === JsSIP_C.NOTIFY) {
      session = this.findSession(request);
      if(session) {
        session.receiveRequest(request);
      } else {
        debug('received NOTIFY request for a non existent subscription');
        request.reply(481, 'Subscription does not exist');
      }
    }
    /* RFC3261 12.2.2
     * Request with to tag, but no matching dialog found.
     * Exception: ACK for an Invite request for which a dialog has not
     * been created.
     */
    else {
      if(method !== JsSIP_C.ACK) {
        request.reply(481);
      }
    }
  }
};

//=================
// Utils
//=================

/**
 * Get the session to which the request belongs to, if any.
 */
UA.prototype.findSession = function(request) {
  var
  sessionIDa = request.call_id + request.from_tag,
  sessionA = this.sessions[sessionIDa],
  sessionIDb = request.call_id + request.to_tag,
  sessionB = this.sessions[sessionIDb];

  if(sessionA) {
    return sessionA;
  } else if(sessionB) {
    return sessionB;
  } else {
    return null;
  }
};

/**
 * Get the dialog to which the request belongs to, if any.
 */
UA.prototype.findDialog = function(request) {
  var
  id = request.call_id + request.from_tag + request.to_tag,
  dialog = this.dialogs[id];

  if(dialog) {
    return dialog;
  } else {
    id = request.call_id + request.to_tag + request.from_tag;
    dialog = this.dialogs[id];
    if(dialog) {
      return dialog;
    } else {
      return null;
    }
  }
};

/**
 * Retrieve the next server to which connect.
 */
UA.prototype.getNextWsServer = function() {
  // Order servers by weight
  var idx, length, ws_server,
  candidates = [];

  length = this.configuration.ws_servers.length;
  for (idx = 0; idx < length; idx++) {
    ws_server = this.configuration.ws_servers[idx];

    if (ws_server.status === Transport.C.STATUS_ERROR) {
      continue;
    } else if (candidates.length === 0) {
      candidates.push(ws_server);
    } else if (ws_server.weight > candidates[0].weight) {
      candidates = [ws_server];
    } else if (ws_server.weight === candidates[0].weight) {
      candidates.push(ws_server);
    }
  }

  idx = Math.floor((Math.random()* candidates.length));
  return candidates[idx];
};

/**
 * Close all sessions on transport error.
 */
UA.prototype.closeSessionsOnTransportError = function() {
  var idx;

  // Run _transportError_ for every Session
  for(idx in this.sessions) {
    this.sessions[idx].onTransportError();
  }
  // Call registrator _onTransportClosed_
  this._registrator.onTransportClosed();
};

UA.prototype.recoverTransport = function(ua) {
  var idx, length, k, nextRetry, count, server;

  ua = ua || this;
  count = ua.transportRecoverAttempts;

  length = ua.configuration.ws_servers.length;
  for (idx = 0; idx < length; idx++) {
    ua.configuration.ws_servers[idx].status = 0;
  }

  server = ua.getNextWsServer();

  k = Math.floor((Math.random() * Math.pow(2,count)) +1);
  nextRetry = k * ua.configuration.connection_recovery_min_interval;

  if (nextRetry > ua.configuration.connection_recovery_max_interval) {
    debug('time for next connection attempt exceeds connection_recovery_max_interval, resetting counter');
    nextRetry = ua.configuration.connection_recovery_min_interval;
    count = 0;
  }

  debug('next connection attempt in '+ nextRetry +' seconds');

  this.transportRecoveryTimer = setTimeout(function() {
    ua.transportRecoverAttempts = count + 1;
    ua.transport = new Transport(ua, server);
    ua.transport.connect();
  }, nextRetry * 1000);
};

UA.prototype.loadConfig = function(configuration) {
  // Settings and default values
  var parameter, value, checked_value, hostport_params, registrar_server,
  settings = {
    /* Host address
    * Value to be set in Via sent_by and host part of Contact FQDN
    */
    via_host: Utils.createRandomToken(12) + '.invalid',

    // Password
    password: null,

    // Registration parameters
    register_expires: 600,
    register: true,
    registrar_server: null,

    // Transport related parameters
    ws_server_max_reconnection: 3,
    ws_server_reconnection_timeout: 4,

    connection_recovery_min_interval: 2,
    connection_recovery_max_interval: 30,

    use_preloaded_route: false,

    // Session parameters
    no_answer_timeout: 60,
    stun_servers: ['stun:stun.l.google.com:19302'],
    turn_servers: [],

    // Hacks
    hack_via_tcp: false,
    hack_via_ws: false,
    hack_ip_in_contact: false,

    // Options for Node.
    node_ws_options: {}
  };

  // Pre-Configuration

  // Check Mandatory parameters
  for(parameter in UA.configuration_check.mandatory) {
    if(!configuration.hasOwnProperty(parameter)) {
      throw new Exceptions.ConfigurationError(parameter);
    } else {
      value = configuration[parameter];
      checked_value = UA.configuration_check.mandatory[parameter].call(this, value);
      if (checked_value !== undefined) {
        settings[parameter] = checked_value;
      } else {
        throw new Exceptions.ConfigurationError(parameter, value);
      }
    }
  }

  // Check Optional parameters
  for(parameter in UA.configuration_check.optional) {
    if(configuration.hasOwnProperty(parameter)) {
      value = configuration[parameter];

      /* If the parameter value is null, empty string, undefined, empty array
       * or it's a number with NaN value, then apply its default value.
       */
      if (Utils.isEmpty(value)) {
        continue;
      }

      checked_value = UA.configuration_check.optional[parameter].call(this, value);
      if (checked_value !== undefined) {
        settings[parameter] = checked_value;
      } else {
        throw new Exceptions.ConfigurationError(parameter, value);
      }
    }
  }

  // Sanity Checks

  // Connection recovery intervals
  if(settings.connection_recovery_max_interval < settings.connection_recovery_min_interval) {
    throw new Exceptions.ConfigurationError('connection_recovery_max_interval', settings.connection_recovery_max_interval);
  }

  // Post Configuration Process

  // Allow passing 0 number as display_name.
  if (settings.display_name === 0) {
    settings.display_name = '0';
  }

  // Instance-id for GRUU
  if (!settings.instance_id) {
    settings.instance_id = Utils.newUUID();
  }

  // jssip_id instance parameter. Static random tag of length 5
  settings.jssip_id = Utils.createRandomToken(5);

  // String containing settings.uri without scheme and user.
  hostport_params = settings.uri.clone();
  hostport_params.user = null;
  settings.hostport_params = hostport_params.toString().replace(/^sip:/i, '');

  /* Check whether authorization_user is explicitly defined.
   * Take 'settings.uri.user' value if not.
   */
  if (!settings.authorization_user) {
    settings.authorization_user = settings.uri.user;
  }

  /* If no 'registrar_server' is set use the 'uri' value without user portion. */
  if (!settings.registrar_server) {
    registrar_server = settings.uri.clone();
    registrar_server.user = null;
    settings.registrar_server = registrar_server;
  }

  // User no_answer_timeout
  settings.no_answer_timeout = settings.no_answer_timeout * 1000;

  // Via Host
  if (settings.hack_ip_in_contact) {
    settings.via_host = Utils.getRandomTestNetIP();
  }

  // Set empty Stun Server Set if explicitly passed an empty Array
  value = configuration.stun_servers;
  if (value instanceof Array && value.length === 0) {
    settings.stun_servers = [];
  }

  this.contact = {
    pub_gruu: null,
    temp_gruu: null,
    uri: new URI('sip', Utils.createRandomToken(8), settings.via_host, null, {transport: 'ws'}),
    toString: function(options) {
      options = options || {};

      var
      anonymous = options.anonymous || null,
      outbound = options.outbound || null,
      contact = '<';

      if (anonymous) {
        contact += this.temp_gruu || 'sip:anonymous@anonymous.invalid;transport=ws';
      } else {
        contact += this.pub_gruu || this.uri.toString();
      }

      if (outbound && (anonymous ? !this.temp_gruu : !this.pub_gruu)) {
        contact += ';ob';
      }

      contact += '>';

      return contact;
    }
  };

  // Fill the value of the configuration_skeleton
  for(parameter in settings) {
    UA.configuration_skeleton[parameter].value = settings[parameter];
  }

  Object.defineProperties(this.configuration, UA.configuration_skeleton);

  // Clean UA.configuration_skeleton
  for(parameter in settings) {
    UA.configuration_skeleton[parameter].value = '';
  }

  debug('configuration parameters after validation:');
  for(parameter in settings) {
    switch(parameter) {
      case 'uri':
      case 'registrar_server':
        debug('- ' + parameter + ': ' + settings[parameter]);
        break;
      case 'password':
        debug('- ' + parameter + ': ' + 'NOT SHOWN');
        break;
      default:
        debug('- ' + parameter + ': ' + JSON.stringify(settings[parameter]));
    }
  }

  return;
};

/**
 * Configuration Object skeleton.
 */
UA.configuration_skeleton = (function() {
  var idx,  parameter,
  skeleton = {},
  parameters = [
    // Internal parameters
    "jssip_id",
    "ws_server_max_reconnection",
    "ws_server_reconnection_timeout",
    "hostport_params",

    // Mandatory user configurable parameters
    "uri",
    "ws_servers",

    // Optional user configurable parameters
    "authorization_user",
    "connection_recovery_max_interval",
    "connection_recovery_min_interval",
    "display_name",
    "hack_via_tcp", // false
    "hack_via_ws", // false
    "hack_ip_in_contact", //false
    "instance_id",
    "no_answer_timeout", // 30 seconds
    "node_ws_options",
    "password",
    "register_expires", // 600 seconds
    "registrar_server",
    "stun_servers",
    "turn_servers",
    "use_preloaded_route",

    // Post-configuration generated parameters
    "via_core_value",
    "via_host"
  ];

  for(idx in parameters) {
    parameter = parameters[idx];
    skeleton[parameter] = {
      value: '',
      writable: false,
      configurable: false
    };
  }

  skeleton.register = {
    value: '',
    writable: true,
    configurable: false
  };

  return skeleton;
}());

/**
 * Configuration checker.
 */
UA.configuration_check = {
  mandatory: {

    uri: function(uri) {
      var parsed;

      if (!/^sip:/i.test(uri)) {
        uri = JsSIP_C.SIP + ':' + uri;
      }
      parsed = URI.parse(uri);

      if(!parsed) {
        return;
      } else if(!parsed.user) {
        return;
      } else {
        return parsed;
      }
    },

    ws_servers: function(ws_servers) {
      var idx, length, url;

      /* Allow defining ws_servers parameter as:
       *  String: "host"
       *  Array of Strings: ["host1", "host2"]
       *  Array of Objects: [{ws_uri:"host1", weight:1}, {ws_uri:"host2", weight:0}]
       *  Array of Objects and Strings: [{ws_uri:"host1"}, "host2"]
       */
      if (typeof ws_servers === 'string') {
        ws_servers = [{ws_uri: ws_servers}];
      } else if (ws_servers instanceof Array) {
        length = ws_servers.length;
        for (idx = 0; idx < length; idx++) {
          if (typeof ws_servers[idx] === 'string') {
            ws_servers[idx] = {ws_uri: ws_servers[idx]};
          }
        }
      } else {
        return;
      }

      if (ws_servers.length === 0) {
        return false;
      }

      length = ws_servers.length;
      for (idx = 0; idx < length; idx++) {
        if (!ws_servers[idx].ws_uri) {
          debug('ERROR: missing "ws_uri" attribute in ws_servers parameter');
          return;
        }
        if (ws_servers[idx].weight && !Number(ws_servers[idx].weight)) {
          debug('ERROR: "weight" attribute in ws_servers parameter must be a Number');
          return;
        }

        url = Grammar.parse(ws_servers[idx].ws_uri, 'absoluteURI');

        if(url === -1) {
          debug('ERROR: invalid "ws_uri" attribute in ws_servers parameter: ' + ws_servers[idx].ws_uri);
          return;
        } else if(url.scheme !== 'wss' && url.scheme !== 'ws') {
          debug('ERROR: invalid URI scheme in ws_servers parameter: ' + url.scheme);
          return;
        } else {
          ws_servers[idx].sip_uri = '<sip:' + url.host + (url.port ? ':' + url.port : '') + ';transport=ws;lr>';

          if (!ws_servers[idx].weight) {
            ws_servers[idx].weight = 0;
          }

          ws_servers[idx].status = 0;
          ws_servers[idx].scheme = url.scheme.toUpperCase();
        }
      }
      return ws_servers;
    }
  },

  optional: {

    authorization_user: function(authorization_user) {
      if(Grammar.parse('"'+ authorization_user +'"', 'quoted_string') === -1) {
        return;
      } else {
        return authorization_user;
      }
    },

    connection_recovery_max_interval: function(connection_recovery_max_interval) {
      var value;
      if(Utils.isDecimal(connection_recovery_max_interval)) {
        value = Number(connection_recovery_max_interval);
        if(value > 0) {
          return value;
        }
      }
    },

    connection_recovery_min_interval: function(connection_recovery_min_interval) {
      var value;
      if(Utils.isDecimal(connection_recovery_min_interval)) {
        value = Number(connection_recovery_min_interval);
        if(value > 0) {
          return value;
        }
      }
    },

    display_name: function(display_name) {
      if(Grammar.parse('"' + display_name + '"', 'display_name') === -1) {
        return;
      } else {
        return display_name;
      }
    },

    hack_via_tcp: function(hack_via_tcp) {
      if (typeof hack_via_tcp === 'boolean') {
        return hack_via_tcp;
      }
    },

    hack_via_ws: function(hack_via_ws) {
      if (typeof hack_via_ws === 'boolean') {
        return hack_via_ws;
      }
    },

    hack_ip_in_contact: function(hack_ip_in_contact) {
      if (typeof hack_ip_in_contact === 'boolean') {
        return hack_ip_in_contact;
      }
    },

    instance_id: function(instance_id) {
      if ((/^uuid:/i.test(instance_id))) {
        instance_id = instance_id.substr(5);
      }

      if(Grammar.parse(instance_id, 'uuid') === -1) {
        return;
      } else {
        return instance_id;
      }
    },

    no_answer_timeout: function(no_answer_timeout) {
      var value;
      if (Utils.isDecimal(no_answer_timeout)) {
        value = Number(no_answer_timeout);
        if (value > 0) {
          return value;
        }
      }
    },

    node_ws_options: function(node_ws_options) {
      return (typeof node_ws_options === 'object') ? node_ws_options : {};
    },

    password: function(password) {
      return String(password);
    },

    register: function(register) {
      if (typeof register === 'boolean') {
        return register;
      }
    },

    register_expires: function(register_expires) {
      var value;
      if (Utils.isDecimal(register_expires)) {
        value = Number(register_expires);
        if (value > 0) {
          return value;
        }
      }
    },

    registrar_server: function(registrar_server) {
      var parsed;

      if (!/^sip:/i.test(registrar_server)) {
        registrar_server = JsSIP_C.SIP + ':' + registrar_server;
      }
      parsed = URI.parse(registrar_server);

      if(!parsed) {
        return;
      } else if(parsed.user) {
        return;
      } else {
        return parsed;
      }
    },

    stun_servers: function(stun_servers) {
      var idx, length, stun_server;

      if (typeof stun_servers === 'string') {
        stun_servers = [stun_servers];
      } else if (!(stun_servers instanceof Array)) {
        return;
      }

      length = stun_servers.length;
      for (idx = 0; idx < length; idx++) {
        stun_server = stun_servers[idx];
        if (!(/^stuns?:/.test(stun_server))) {
          stun_server = 'stun:' + stun_server;
        }

        if(Grammar.parse(stun_server, 'stun_URI') === -1) {
          return;
        } else {
          stun_servers[idx] = stun_server;
        }
      }
      return stun_servers;
    },

    turn_servers: function(turn_servers) {
      var idx, idx2, length, length2, turn_server, url;

      if (! turn_servers instanceof Array) {
        turn_servers = [turn_servers];
      }

      length = turn_servers.length;
      for (idx = 0; idx < length; idx++) {
        turn_server = turn_servers[idx];

        // Backward compatibility:
        //Allow defining the turn_server 'urls' with the 'server' property.
        if (turn_server.server) {
          turn_server.urls = [turn_server.server];
        }

        // Backward compatibility:
        //Allow defining the turn_server 'credential' with the 'password' property.
        if (turn_server.password) {
          turn_server.credential = [turn_server.password];
        }

        if (!turn_server.urls || !turn_server.username || !turn_server.credential) {
          return;
        }

        if (!(turn_server.urls instanceof Array)) {
          turn_server.urls = [turn_server.urls];
        }

        length2 = turn_server.urls.length;
        for (idx2 = 0; idx2 < length2; idx2++) {
          url = turn_server.urls[idx2];

          if (!(/^turns?:/.test(url))) {
            url = 'turn:' + url;
          }

          if(Grammar.parse(url, 'turn_URI') === -1) {
            return;
          }
        }
      }
      return turn_servers;
    },

    use_preloaded_route: function(use_preloaded_route) {
      if (typeof use_preloaded_route === 'boolean') {
        return use_preloaded_route;
      }
    }
  }
};