srf.js

const drachtio = require('drachtio');
const Dialog = require('./dialog') ;
const assert = require('assert') ;
const Emitter = require('events') ;
const delegate = require('delegates') ;
const parser = require('drachtio-sip').parser ;
const methods = require('sip-methods') ;
const SipError = require('./sip_error') ;
const async = require('async') ;
const deprecate = require('deprecate');
const debug = require('debug')('drachtio-srf') ;
const noop = () => {};

/**
 * Applications create an instance of Srf in order to create and manage SIP [Dialogs]{@link Dialog}
 * and SIP transactions.  An application may have one or more Srf instances, although for most cases a single
 * instance is sufficient.
 */
class Srf extends Emitter {

  /**
   * Creates an Srf instance.  No arguments are supplied.
   * @constructor
   */
  constructor(app) {
    super() ;

    assert(typeof app === 'undefined' || typeof app === 'function' || typeof app === 'object',
      'argument \'app\' if provided must be either a drachtio app or connect opts') ;

    // preferred method of constructing an Srf object is simply:
    // const srf = new Srf() ;
    // then ..
    // srf.connect(), or srf.listen()
    //
    // the old ways:
    // new Srf(connectArgs)
    // or
    // new Srf(app)
    // are deprecated

    if (typeof app !== 'undefined') {
      deprecate('Srf() constructor should be called with no arguments, ' +
        'followed by Srf#connect(opts) or Srf#listen(opts)');
    }
    this.dialogs = new Map() ;

    if (typeof app === 'function') {
      this._app = app ;
    }
    else {
      this._app = drachtio() ;
      ['connect', 'listening', 'reconnecting', 'error', 'close'].forEach((evt) => {
        this._app.on(evt, (...args) => { setImmediate(() => { this.emit(evt, ...args);});});
      }) ;

      if (typeof app === 'object') {
        assert.equal(typeof app.host,  'string', 'invalid drachtio connection opts') ;

        const opts = app ;
        this._app.connect(opts) ;
      }
    }

    this._app.use(this.dialog()) ;
  }

  on(event, fn) {
    //cdr events are handled through a different mechanism - we register with the server
    if (0 === event.indexOf('cdr:')) {
      return this._app.on(event, fn) ;
    }

    //delegate to EventEmitter
    return Emitter.prototype.on.apply(this, arguments)  ;
  }

  get app() {
    return this._app ;
  }

  /*
   * drachtio middleware that enables Dialog handling
   * @param  {Object} opts - configuration arguments, if any (currently unused)
   */
  dialog(opts) {
    opts = opts || {} ;

    return (req, res, next) => {

      debug('examining %s, dialog id: ', req.method, req.stackDialogId);
      if (req.stackDialogId && this.dialogs.has(req.stackDialogId)) {
        debug('calling dialog handler');
        this.dialogs.get(req.stackDialogId).handle(req, res, next) ;
        return ;
      }
      req.srf = res.srf = this;
      next() ;
    } ;
  }

  /**
   * create a SIP dialog, acting as a UAS (user agent server); i.e.
   * respond to an incoming SIP INVITE with a 200 OK
   * (or to a SUBSCRIBE request with a 202 Accepted).
   *
   * Note that the {@link Dialog} is generated (i.e. the callback invoked / the Promise resolved)
   * at the moment that the 200 OK is sent back towards the requestor, not when the ACK is subsequently received.
   * @param  {Object} req the incoming sip request object
   * @param  {Object} res the sip response object
   * @param  {Object} opts configuration options
   * @param {string} opts.localSdp the local session description protocol to include in the SIP response
   * @param {Object} [opts.headers] SIP headers to include on the SIP response to the INVITE
   * @param  {function} [callback] if provided, callback with signature <code>(err, dialog)</code>
   * @return {Srf|Promise} if a callback is supplied, a reference to the Srf instance.
   * <br/>If no callback is supplied, then a Promise that is resolved
   * with the [sip dialog]{@link Dialog} that is created.
   *
   * @example <caption>returning a Promise</caption>
   * const Srf = require('drachtio-srf');
   * const srf = new Srf();
   *
   * srf.invite((req, res) => {
   *   const mySdp; // populated somehow with SDP we want to answer in 200 OK
   *   srf.createUas(req, res, {localSdp: mySdp})
   *     .then((uas) => {
   *       console.log(`dialog established, remote uri is ${uas.remote.uri}`);
   *       uas.on('destroy', () => {
   *         console.log('caller hung up');
   *       });
   *     })
   *     .catch((err) => {
   *       console.log(`Error establishing dialog: ${err}`);
   *     });
   * });
   * @example <caption>using callback</caption>
   * const Srf = require('drachtio-srf');
   * const srf = new Srf();
   *
   * srf.invite((req, res) => {
   *   const mySdp; // populated somehow with SDP we want to offer in 200 OK
   *   srf.createUas(req, res, {localSdp: mySdp},
   *     (err, uas) => {
   *       if (err) {
   *         return console.log(`Error establishing dialog: ${err}`);
   *       }
   *       console.log(`dialog established, local tag is ${uas.sip.localTag}`);
   *       uas.on('destroy', () => {
   *         console.log('caller hung up');
   *       });
   *     });
   * });
   * @example <caption>specifying standard or custom headers</caption>
   * srf.createUas(req, res, {
   *     localSdp: mySdp,
   *     headers: {
   *       'User-Agent': 'drachtio/iechyd-da',
   *       'X-Linked-UUID': '1e2587c'
   *     }
   *   }).then((uas) => { ..});
   */
  createUAS(req, res, opts = {}, callback) {
    opts.headers = opts.headers || {} ;
    const body = opts.body || opts.localSdp;
    const generateSdp = typeof body === 'function' ?
      opts.localSdp : () => { return Promise.resolve(opts.localSdp); };

    const __fail = (callback, err) => {
      callback(err);
    };

    const __send = (callback, content) => {
      let called = false;
      req.on('cancel', () => {
        req.canceled = called = true ;
        callback(new SipError(487, 'Request Terminated')) ;
      }) ;

      return res.send(req.method === 'INVITE'  ? 200 : 202, {
        headers: opts.headers,
        body: content
      }, (err, response) => {
        if (err) {
          if (!called) {
            called = true;
            callback(err);
          }
          return;
        }

        // note: we used to invoke callback after ACK was received
        // now we send it at the time we send the 200 OK
        // this is in keeping with the RFC 3261 spec
        const dialog = new Dialog(this, 'uas', {req: req, res: res, sent: response}) ;
        this.addDialog(dialog);
        callback(null, dialog);

        if ('INVITE' === req.method) {
          dialog.once('ack', () => {
            // should we emit some sort of event?
          }) ;
        }
        else {
          callback(null, dialog) ;
        }
      });
    };

    const __x = (callback) => {
      const send = __send.bind(this, callback);
      const fail = __fail.bind(this, callback);
      generateSdp()
        .then(send)
        .catch(fail);
    };

    if (callback) {
      __x(callback) ;
      return this ;
    }

    return new Promise((resolve, reject) => {
      __x((err, dialog) => {
        if (err) return reject(err);
        resolve(dialog);
      });
    });
  }

  /**
  * create a SIP dialog, acting as a UAC (user agent client)
  *
  * @param  {string}   uri -  request uri to send to
  * @param  {Object}  opts   configuration options
  * @param  {Object}  [opts.headers] SIP headers to include on the SIP INVITE request
  * @param  {string}  opts.localSdp the local session description protocol to include in the SIP INVITE request
  * @param  {Object} [progressCallbacks] callbacks providing call progress notification
  * @param {Function} [progressCallbacks.cbRequest] - callback that provides request sent over the wire,
  * with signature (req)
  * @param {Function} [progressCallbacks.cbProvisional] - callback that provides a provisional response
  * with signature (provisionalRes)
  * @param  {function} [callback] if provided, callback with signature <code>(err, dialog)</code>
  * @return {Srf|Promise} if a callback is supplied, a reference to the Srf instance.
  * <br/>If no callback is supplied, then a Promise that is resolved
  * with the [sip dialog]{@link Dialog} that is created.
  * @example <caption>returning a Promise</caption>
  * const Srf = require('drachtio-srf');
  * const srf = new Srf();
  *
  * const mySdp; // populated somehow with SDP we want to offer
  * srf.createUac('sip:1234@10.10.100.1', {localSdp: mySdp})
  *   .then((uac) => {
  *     console.log(`dialog established, call-id is ${uac.sip.callId}`);
  *     uac.on('destroy', () => {
  *       console.log('called party hung up');
  *     });
  *   })
  *   .catch((err) => {
  *     console.log(`INVITE rejected with status: ${err.status}`);
  *   });
  * });
  * @example <caption>Using a callback</caption>
  * const Srf = require('drachtio-srf');
  * const srf = new Srf();
  *
  * const mySdp; // populated somehow with SDP we want to offer
  * srf.createUac('sip:1234@10.10.100.1', {localSdp: mySdp},
  *    (err, uac) => {
  *      if (err) {
  *        return console.log(`INVITE rejected with status: ${err.status}`);
  *      }
  *     uac.on('destroy', () => {
  *       console.log('called party hung up');
  *     });
  *   });
  * @example <caption>Canceling a request by using a progress callback</caption>
  * const Srf = require('drachtio-srf');
  * const srf = new Srf();
  *
  * const mySdp; // populated somehow with SDP we want to offer
  * let inviteSent;
  * srf.createUac('sip:1234@10.10.100.1', {localSdp: mySdp},
  *   {
  *     cbRequest: (reqSent) => { inviteSent = req; }
  *   })
  *   .then((uac) => {
  *     // unexpected, in this case
  *     console.log('dialog established before we could cancel');
  *   })
  *   .catch((err) => {
  *     assert(err.status === 487); // expected sip response to a CANCEL
  *   });
  * });
  *
  * // cancel the request after 0.5s
  * setTimeout(() => {
  *   inviteSent.cancel();
  * }, 500);
  */
  createUAC(uri, opts, cbRequest, cbProvisional, callback) {
    if (typeof uri === 'object') {
      callback = cbProvisional ;
      cbProvisional = cbRequest ;
      cbRequest = opts ;
      opts = uri ;
    }
    else {
      opts.uri = uri ;
    }

    // new signature: uri, opts, {cbRequest, cbProvisional}, callback
    if (cbRequest && typeof cbRequest === 'object') {
      callback = cbProvisional ;
      const obj = cbRequest ;
      cbRequest = obj.cbRequest || noop;
      cbProvisional = obj.cbProvisional || noop;
    }
    else {
      cbProvisional = cbProvisional || noop ;
      cbRequest = cbRequest || noop;
    }

    const __x = (callback) => {
      const method = opts.method || 'INVITE' ;
      opts.headers = opts.headers || {} ;

      assert.ok(method === 'INVITE' || method === 'SUBSCRIBE', 'method must be either INVITE or SUBSCRIBE') ;
      assert.ok(!!opts.uri, 'uri must be specified') ;

      const parsed = parser.parseUri(opts.uri) ;
      if (!parsed) {
        if (-1 === opts.uri.indexOf('@') && 0 !== opts.uri.indexOf('sip')) {
          var address = opts.uri ;
          opts.uri = 'sip:' + (opts.calledNumber ? opts.calledNumber + '@' : '') + address ;
        }
        else if (0 !== opts.uri.indexOf('sip')) {
          opts.uri = 'sip:' + opts.uri ;
        }
      }

      if (opts.callingNumber) {
        opts.headers.from = 'sip:' + opts.callingNumber + '@localhost' ;
        opts.headers.contact = 'sip:' + opts.callingNumber + '@localhost' ;
      }

      const is3pcc = !opts.localSdp && 'INVITE' === method ;

      this._app.request({
        uri: opts.uri,
        method: method,
        proxy: opts.proxy,
        headers: opts.headers,
        body: opts.localSdp,
        auth: opts.auth,
        _socket: opts._socket
      },
      (err, req) => {
        if (err) {
          cbRequest(err);
          return callback(err) ;
        }
        cbRequest(null, req) ;

        req.on('response', (res, ack) => {
          if (res.status < 200) {
            cbProvisional(res) ;
            if (res.has('RSeq')) {
              ack() ; // send PRACK
            }
          }
          else {
            if (is3pcc && 200 === res.status && !!res.body) {

              if (opts.noAck === true) {

                // caller is responsible for invoking ack function with sdp they want to offer
                return callback(null, {
                  sdp: res.body,
                  ack: (localSdp, callback) => {
                    return new Promise((resolve, reject) => {
                      ack({body: localSdp}) ;

                      var dialog = new Dialog(this, 'uac', {req: req, res: res}) ;
                      dialog.local.sdp = localSdp ;
                      this.addDialog(dialog) ;
                      resolve(dialog) ;
                    });
                  }
                });
              }
              var bhSdp = res.body.replace(/c=IN\s+IP4\s+(\d+\.\d+\.\d+\.\d+)/, function(/* match, p1 */) {
                return 'c=IN IP4 0.0.0.0' ;
              }) ;
              bhSdp = bhSdp.replace(/(o=[a-zA-Z0-9]+\s+\d+\s+\d+\s+IN\s+IP4\s+)(\d+\.\d+\.\d+\.\d+)/,
                (match, p1) => { return p1 + '0.0.0.0' ;}
              ) ;
              ack({
                body: bhSdp
              }) ;
            }
            else if (method === 'INVITE') {
              ack() ;
            }

            if ((200 === res.status && method === 'INVITE') ||
                ((202 === res.status || 200 === res.status) && method === 'SUBSCRIBE')) {
              var dialog = new Dialog(this, 'uac', {req: req, res: res}) ;
              this.addDialog(dialog) ;
              return callback(null, dialog) ;
            }
            var error = new SipError(res.status, res.reason) ;
            error.res = res ;
            callback(error) ;
          }
        }) ;
      }) ;
    };

    if (callback) {
      __x(callback) ;
      return this ;
    }

    return new Promise((resolve, reject) => {
      __x((err, dialog) => {
        if (err) return reject(err);
        resolve(dialog);
      });
    });
  }

  /**
  * create back-to-back dialogs; i.e. act as a back-to-back user agent (B2BUA), creating a
  * pair of dialogs {uas, uac} -- a UAS dialog facing the caller or A party, and a UAC dialog
  * facing the callee or B party such that media flows between them
  * @param  {Object}  req  - incoming sip request object
  * @param  {Object}  res  - incoming sip response object
  * @param  {string}  uri - sip uri or IP address[:port] to send the UAC INVITE to
  * @param  {Object}  opts -   configuration options
  * @param {Object} [opts.headers] SIP headers to include on the SIP INVITE request
  * @param {string|function} [opts.localSdpA] the local session description protocol
  * to offer in the response to the SIP INVITE request on the A leg; either a string or a function
  * may be provided. If a function is
  * provided, it will be invoked with two parameters (sdp, res) correspnding to the SDP received from the B
  * party, and the sip response object received on the response from B.
  * The function must return either the SDP (as a string)
  * or a Promise that resolves to the SDP. If no value is provided (neither string nor function), then the SDP
  * returned by the B party in the provisional/final response on the UAC leg will be
  * sent back to the A party in the answer.
  * @param {string} opts.localSdpB the local session description protocol to offer in the SIP INVITE
  * request on the B leg
  * @param {Array} [opts.proxyRequestHeaders] an array of header names which, if they appear in the INVITE request
  * on the A leg, should be included unchanged on the generated B leg INVITE
  * @param {Array} [opts.proxyResponseHeaders] an array of header names which, if they appear
  * in the response to the outgoing INVITE, should be included unchanged on the generated response to the A leg
  * @param {Boolean} [opts.passFailure=true] specifies whether to pass a failure returned from B leg back to the A leg
  * @param  {Object} [progressCallbacks] callbacks providing call progress notification
  * @param {Function} [progressCallbacks.cbRequest] - callback that provides request sent over the wire,
  * with signature (req)
  * @param {Function} [progressCallbacks.cbProvisional] - callback that provides a provisional response
  * with signature (provisionalRes)
  * @param  {function} [callback] if provided, callback with signature <code>(err, {uas, uac})</code>
  * @return {Srf|Promise} if a callback is supplied, a reference to the Srf instance.
  * <br/>If no callback is supplied, then a Promise that is resolved
  * with the [sip dialog]{@link Dialog} that is created.
  * @example <caption>simple B2BUA</caption>
  * const Srf = require('drachtio-srf');
  * const srf = new Srf();
  *
  * srf.invite((req, res) => {
  *   srf.createB2BUA('sip:1234@10.10.100.1', req, res, {localSdpB: req.body})
  *     .then({uas, uac} => {
  *       console.log('call connected');
  *
  *       // when one side terminates, hang up the other
  *       uas.on('destroy', () => { uac.destroy(); });
  *       uac.on('destroy', () => { uas.destroy(); });
  *     })
  *     .catch((err) => {
  *       console.log(`call failed to connect: ${err}`);
  *     });
  * });
  * @example <caption>use opts.passFailure to attempt a fallback URI on failure</caption>
  * const Srf = require('drachtio-srf');
  * const srf = new Srf();
  *
  * function endCall(dlg1, dlg2) {
  *   dlg1.on('destroy', () => {dlg2.destroy();})
  *   dlg2.on('destroy', () => {dlg1.destroy();})
  * }
  * srf.invite((req, res) => {
  *   srf.createB2BUA('sip:1234@10.10.100.1', req, res, {localSdpB: req.body, passFailure: false})
  *     .then({uas, uac} => {
  *       console.log('call connected to primary destination');
  *       endcall(uas, uac);
  *     })
  *     .catch((err) => {
  *       // try backup if we got a sip non-success response and the caller did not hang up
  *       if (err instanceof Srf.SipError && err.status !== 487) {
  *           console.log(`failed connecting to primary, will try backup: ${err}`);
  *           srf.createB2BUA('sip:1234@10.10.100.2', req, res, {
  *             localSdpB: req.body}
  *           })
  *             .then({uas, uac} => {
  *               console.log('call connected to backup destination');
  *               endcall(uas.uac);
  *             })
  *             catch((err) => {
  *               console.log(`failed connecting to backup uri: ${err}`);
  *             });
  *       }
  *     });
  * });
  * @example <caption>B2BUA with media proxy using rtpengine</caption>
  * const Srf = require('drachtio-srf');
  * const srf = new Srf();
  * const rtpengine = require('rtpengine-client').Client
  *
  * // helper functions
  *
  * // clean up and free rtpengine resources when either side hangs up
  * function endCall(dlg1, dlg2, details) {
  *   [dlg1, dlg2].each((dlg) => {
  *     dlg.on('destroy', () => {(dlg === dlg1 ? dlg2 : dlg1).destroy();});
  *     rtpengine.delete(details);
  *   });
  * }
  *
  * // function returning a Promise that resolves with the SDP to offer A leg in 18x/200 answer
  * function getSdpA(details, remoteSdp, res) {
  *   return rtpengine.answer(Object.assign(details, {
  *     'sdp': remoteSdp,
  *     'to-tag': res.getParsedHeader('To').params.tag
  *    }))
  *     .then((response) => {
  *       if (response.result !== 'ok') throw new Error(`Error calling answer: ${response['error-reason']}`);
  *       return response.sdp;
  *    })
  * }
  *
  * // handle incoming invite
  * srf.invite((req, res) => {
  *   const from = req.getParsedHeader('From');
  *   const details = {'call-id': req.get('Call-Id'), 'from-tag': from.params.tag};
  *
  *   rtpengine.offer(Object.assign(details, {'sdp': req.body})
  *     .then((rtpResponse) => {
  *       if (rtpResponse && rtpResponse.result === 'ok') return rtpResponse.sdp;
  *       throw new Error('rtpengine failure');
  *     })
  *     .then((sdpB) => {
  *       return srf.createB2BUA('sip:1234@10.10.100.1', req, res, {
  *         localSdpB: sdpB,
  *         localSdpA: getSdpA.bind(null, details)
  *       });
  *     })
  *     .then({uas, uac} => {
  *       console.log('call connected with media proxy');
  *       endcall(uas, uac, details);
  *     })
  *     .catch((err) => {
  *       console.log(`Error proxying call with media: ${err}`);
  *     });
  * });

  */
  createB2BUA(req, res, uri, opts, cbRequest, cbProvisional, callback) {
    let cbFinalizedUac = noop ;

    if (uri && typeof uri === 'object') {
      callback = cbProvisional ;
      cbProvisional = cbRequest ;
      cbRequest = opts ;
      opts = uri ;
    }
    else {

      opts = opts || {} ;
      if (typeof opts !== 'object') {
        callback = cbProvisional ;
        cbProvisional = cbRequest ;
        cbRequest = opts ;
        opts = {} ;
      }
      opts.uri = uri ;
    }

    // new signature: uri, opts, {cbRequest, cbProvisional}, callback
    if (cbRequest && typeof cbRequest === 'object') {
      callback = cbProvisional ;
      const obj = cbRequest ;
      cbRequest = obj.cbRequest || noop;
      cbProvisional = obj.cbProvisional || noop;
      cbFinalizedUac = obj.cbFinalizedUac || noop ;
    }
    else {
      cbProvisional = cbProvisional || noop ;
      cbRequest = cbRequest || noop;
    }

    assert.ok(typeof opts.uri === 'string');   // minimally, we must have a request-uri

    opts.method = req.method ;

    const proxyRequestHeaders = opts.proxyRequestHeaders || [] ;
    const proxyResponseHeaders = opts.proxyResponseHeaders || [] ;
    const propogateFailure = !(opts.passFailure === false);

    // default From, To, and user part of uri if not provided
    opts.headers = opts.headers || {} ;

    // pass specified headers on to the B leg
    proxyRequestHeaders.forEach((hdr) => { if (req.has(hdr)) opts.headers[hdr] = req.get(hdr);}) ;

    if (!opts.headers.from && !opts.callingNumber) { opts.callingNumber = req.callingNumber; }
    if (!opts.headers.to && !opts.calledNumber) { opts.calledNumber = req.calledNumber; }

    opts.localSdp = opts.localSdpB || req.body ;

    let remoteSdpB, translatedRemoteSdpB ;

    /* returns a Promise that resolves with the sdp to use responding to the A leg */
    function generateSdpA(res) {
      debug('createB2BUA: generateSdpA');

      const sdpB = res.body ;
      if (res.getParsedHeader('CSeq').method === 'SUBSCRIBE' || !sdpB) {
        return Promise.resolve(sdpB) ;
      }

      if (remoteSdpB && remoteSdpB === sdpB) {
        // called again with same remote SDP, return previous result
        return Promise.resolve(translatedRemoteSdpB) ;
      }

      remoteSdpB = sdpB ;
      if (!opts.localSdpA) {
        // passthru B leg SDP
        return Promise.resolve(translatedRemoteSdpB = sdpB);
      }
      else if ('function' === typeof opts.localSdpA) {
        // call function that resolves a new SDP based on B leg SDP
        return opts.localSdpA(sdpB, res)
          .then((sdpA) => {
            return translatedRemoteSdpB = sdpA ;
          })
          .catch((err) => {
            return Promise.reject(err);
          });
      }
      else {
        // insert provided SDP
        return Promise.resolve(translatedRemoteSdpB = opts.localSdpA) ;
      }
    }

    /* uac request sent, set handler to propogate CANCEL from A leg if we get it */
    function handleUACSent(err, uacReq) {
      if (err) {
        debug(`createB2BUA: Error sending uac request: ${err}`);
        res.send(500);
      }
      else {
        req.on('cancel', () => {
          debug('createB2BUA: received CANCEL from A party, sending CANCEL to B');
          res.send(487) ;
          uacReq.cancel() ;
        });
      }
      cbRequest(err, uacReq);
    }

    /* get headers from response on uac (B) leg and ready them for inclusion on our response on uas (A) leg */
    function copyUACHeadersToUAS(uacRes) {
      const headers = {} ;
      proxyResponseHeaders.forEach((hdr) => {
        debug(`copyUACHeadersToUAS: hdr ${hdr}`);
        if (uacRes.has(hdr)) {
          debug(`copyUACHeadersToUAS: adding ${hdr}: uacRes.get(hdr)`);
          headers[hdr] = uacRes.get(hdr) ;
        }
      }) ;
      debug(`copyUACHeadersToUAS: ${JSON.stringify(headers)}`);
      return headers ;
    }

    /* propogate any provisional responses from uac (B) leg to uas (A) leg */
    function handleUACProvisionalResponse(provisionalRes, uacReq) {
      if (provisionalRes.status > 101) {
        debug('Srf#createB2BUA: received a provisional response %d', provisionalRes.status) ;

        const opts = { headers: copyUACHeadersToUAS(provisionalRes) } ;

        if (provisionalRes.body) {
          generateSdpA(provisionalRes)
            .then((sdpA) => {
              opts.body = sdpA ;
              return res.send(provisionalRes.status, provisionalRes.reason, opts) ;
            })
            .catch((err) => {
              console.error(`Srf#createB2BUA: failed in call to produceSdpForALeg: ${err.message}`);
              res.send(500) ;
              uacReq.cancel() ;
            });
        }
        else {
          res.send(provisionalRes.status, provisionalRes.reason, opts) ;
        }
      }
      cbProvisional(provisionalRes);
    }

    const __x = (callback) => {
      debug(`createB2BUA: creating UAC, opts: ${JSON.stringify(opts)}`);

      opts._socket = req.socket ;

      this.createUAC(opts, {cbRequest: handleUACSent, cbProvisional: handleUACProvisionalResponse})
        .then((uac) => {

          //success establishing uac (B) leg, now establish uas (A) leg
          debug('createB2BUA: successfully created UAC..');

          cbFinalizedUac(uac);

          return this.createUAS(req, res, {
            headers:  copyUACHeadersToUAS(uac.res),
            localSdp: generateSdpA.bind(null, uac.res)
          })
            .then((uas) => {
              debug('createB2BUA: successfully created UAS..done!');
              callback(null, {uac, uas});  // successfully connected!  resolve promise with both dialogs
            })
            .catch((err) => {
              debug('createB2BUA: failed creating UAS..done!');
              uac.destroy() ;       // failed A leg after success on B: tear down B
              callback(err) ;
            });
        })
        .catch((err) => {
          debug(`createB2BUA: received non-success ${err.status || err} on uac leg`);
          const opts = {headers: copyUACHeadersToUAS(err.res)} ;
          if (propogateFailure) {
            res.send(err.status, opts) ;    // failed B: propogate failure to A
          }
          callback(err);
        });
    };

    if (callback) {
      __x(callback) ;
      return this ;
    }

    return new Promise((resolve, reject) => {
      __x((err, dialogs) => {
        if (err) return reject(err);
        resolve(dialogs);
      });
    });
  }

  /**
  * proxy an incoming request
  * @param  {Request}   req - drachtio request object representing an incoming SIP request
  * @param {String|Array} [destination] -  an IP address[:port], or list of same, to proxy the request to
  * @param  {Object}   [opts] - configuration options for the proxy operation
  * @param {String} [opts.forking=sequential] - when multiple destinations are provided,
  * this option governs whether they are attempted sequentially or in parallel.
  * Valid values are 'sequential' or 'parallel'
  * @param {Boolean} [opts.remainInDialog=false] - if true, add Record-Route header and
  * remain in the SIP dialog (i.e. receiving futher SIP messaging for the dialog,
  * including the terminating BYE request).
  * Alias: `recordRoute`.
  * @param {String} [opts.provisionalTimeout] - timeout after which to attempt the next destination
  * if no 100 Trying response has been received.  Examples of valid syntax for this property is '1500ms', or '2s'
  * @param {String} [opts.finalTimeout] - timeout, in milliseconds, after which to cancel
  * the current request and attempt the next destination if no final response has been received.
  * Syntax is the same as for the provisionalTimeout property.
  * @param {Boolean} [opts.followRedirects=false] - if true, handle 3XX redirect responses by
  * generating a new request as per the Contact header; otherwise, proxy the 3XX response
  * back upstream without generating a new response
  * @param  {function} [callback] - callback invoked when proxy operation completes, signature (err, results)
  * where `results` is a JSON object describing the individual sip call attempts and results
  * @returns {Srf|Promise} returns a Promise if no callback is supplied, otherwise the Srf object
  * @example <caption>simple proxy</caption>
  * const Srf = require('drachtio-srf');
  * const srf = new Srf();
  *
  * srf.invite((req, res) => {
  *   srf.proxyRequest(req, 'sip.example.com');
  * });
  *
  * @example <caption>proxy with options</caption>
  * const Srf = require('drachtio-srf');
  * const srf = new Srf();
  *
  * srf.invite((req, res) => {
  *   srf.proxyRequest(req, ['sip.example1.com', 'sip.example2.com'], {
  *     recordRoute: true,
  *     followRedirects: true,
  *     provisionalTimeout: '2s'
  *   }).then((results) => {
  *     console.log(JSON.stringify(result)); // {finalStatus: 200, finalResponse:{..}, responses: [..]}
  *   });
  * });
  */
  proxyRequest(req, destination, opts, callback) {
    assert(typeof destination === 'undefined' || typeof destination === 'string' || Array.isArray(destination),
      '\'destination\' is must be a string or an array of strings') ;

    if (typeof destination === 'function') {
      callback = destination;
    }
    else if (typeof opts === 'function') {
      callback = opts;
      opts = {};
    }
    opts = opts || {};
    opts.destination = destination ;

    debug(`Srf#proxyRequest opts ${JSON.stringify(opts)}, callback ${typeof callback}`);
    return req.proxy(opts, callback) ;
  }


  /* deprecated */
  createUasDialog(req, res, opts, cb) {
    deprecate('please consider migrating to createUAS, the promises-based version');

    assert.ok(!!req.msg, 'argument \'req\' must be a drachtio Request') ;
    assert.equal(typeof res.agent, 'object', 'argument \'res\' must be a drachtio Response') ;
    assert.equal(typeof opts, 'object', 'argument \'opts\' must be provided with connection options') ;
    if (req.method === 'INVITE') {
      assert.equal(typeof opts.localSdp, 'string', 'argument \'opts.localSdp\' was not provided') ;
    }
    assert.equal(typeof cb, 'function', 'a callback function is required');

    opts.headers = opts.headers || {} ;

    res.send(req.method === 'INVITE'  ? 200 : 202, {
      headers: opts.headers,
      body: opts.localSdp
    }, (err, response) => {
      if (err) { return cb(err) ; }

      var dialog = new Dialog(this, 'uas', {req: req, res: res, sent: response}) ;
      this.addDialog(dialog);

      if (req.method === 'INVITE') {
        dialog.once('ack', () => {
          cb(null, dialog) ;
        }) ;
      }
      else {
        cb(null, dialog) ;
      }
    });

    req.on('cancel', () => {
      debug('Srf#createUasDialog: received CANCEL from uac') ;
      cb(new SipError(487, 'Request Terminated')) ;
    }) ;
  }

  createUacDialog(uri, opts, cb, cbProvisional) {
    deprecate('please consider migrating to createUAC, the promises-based version');

    return new Promise((resolve, reject) => {
      const method = opts.method || 'INVITE' ;

      if (typeof uri === 'string') { opts.uri = uri ;}
      else if (typeof uri === 'object') {
        cbProvisional = cb ;
        cb = opts ;
        opts = uri ;
      }
      opts.headers = opts.headers || {} ;

      assert.ok(method === 'INVITE' || method === 'SUBSCRIBE', 'method must be either INVITE or SUBSCRIBE') ;
      assert.ok(!!opts.uri, 'uri must be specified') ;
      assert.equal(typeof cb, 'function', 'a callback function is required') ;

      var parsed = parser.parseUri(opts.uri) ;
      if (!parsed) {
        if (-1 === opts.uri.indexOf('@') && 0 !== opts.uri.indexOf('sip')) {
          var address = opts.uri ;
          opts.uri = 'sip:' + (opts.calledNumber ? opts.calledNumber + '@' : '') + address ;
        }
        else if (0 !== opts.uri.indexOf('sip')) {
          opts.uri = 'sip:' + opts.uri ;
        }
      }

      if (opts.callingNumber) {
        opts.headers.from = 'sip:' + opts.callingNumber + '@localhost' ;
        opts.headers.contact = 'sip:' + opts.callingNumber + '@localhost' ;
      }

      var is3pcc = !opts.localSdp && 'INVITE' === method ;


      this._app.request({
        uri: opts.uri,
        method: method,
        headers: opts.headers,
        body: opts.localSdp,
        auth: opts.auth
      },
      (err, req) => {
        if (err) {
          reject(err) ;
          return cb(err) ;
        }
        resolve(req) ;
        req.on('response', (res, ack) => {
          if (res.status < 200) {
            if (res.has('RSeq')) {
              ack() ; // send PRACK
            }
            if (cbProvisional) {
              cbProvisional(res) ;
            }
          }
          else {
            if (is3pcc && 200 === res.status && !!res.body) {

              if (opts.noAck === true) {
                // caller is responsible for sending ACK
                return cb(null, res.body, function(localSdp, callback) {
                  ack({body: localSdp}) ;

                  var dialog = new Dialog(this, 'uac', {req: req, res: res}) ;
                  dialog.local.sdp = localSdp ;
                  this.addDialog(dialog) ;
                  callback(null, dialog) ;
                }.bind(this));
              }
              var bhSdp = res.body.replace(/c=IN\s+IP4\s+(\d+\.\d+\.\d+\.\d+)/, function(/* match, p1 */) {
                return 'c=IN IP4 0.0.0.0' ;
              }) ;
              bhSdp = bhSdp.replace(/(o=[a-zA-Z0-9]+\s+\d+\s+\d+\s+IN\s+IP4\s+)(\d+\.\d+\.\d+\.\d+)/,
                (match, p1) => { return p1 + '0.0.0.0' ;}
              ) ;
              ack({
                body: bhSdp
              }) ;
            }
            else if (method === 'INVITE') {
              ack() ;
            }

            if ((200 === res.status && method === 'INVITE') ||
                ((202 === res.status || 200 === res.status) && method === 'SUBSCRIBE')) {

              var dialog = new Dialog(this, 'uac', {req: req, res: res}) ;
              this.addDialog(dialog) ;
              return cb(null, dialog) ;
            }
            var error = new SipError(res.status, res.reason) ;
            error.res = res ;
            cb(error) ;
          }
        }) ;
      }) ;
    });
  }

  createBackToBackDialogs(req, res, uri, opts, cb) {
    deprecate('please consider migrating to createB2BUA, the promises-based version');

    assert.ok(typeof uri === 'string' || Array.isArray(uri), 'argument \'uri\' must be either a string or an array') ;

    if (typeof opts === 'function') {
      cb = opts ;
      opts = {} ;
    }

    assert.ok(!opts.onProvisional ||
      typeof opts.onProvisional === 'function', 'argument \'opts.onProvisional\' must be a function') ;

    let remoteSdpB, translatedRemoteSdpB ;

    function produceSdpForALeg(sdpB, res, callback) {
      const method = res.getParsedHeader('CSeq').method ;

      if (method === 'SUBSCRIBE' || !sdpB) {
        // no-op
        return callback(null, sdpB) ;
      }

      if (remoteSdpB && remoteSdpB === sdpB) {
        // called again with same remote SDP, return previous result
        return callback(null, translatedRemoteSdpB) ;
      }

      remoteSdpB = sdpB ;
      if (!opts.localSdpA) {
        // no-op: caller does not want to do any substitution
        callback(null, translatedRemoteSdpB = sdpB) ;
      }
      else if ('function' === typeof opts.localSdpA) {
        // invoke provided callback to generate SDP
        opts.localSdpA(sdpB, res, (err, sdp) => {
          callback(err, translatedRemoteSdpB = sdp);
        }) ;
      }
      else {
        // insert provided SDP
        callback(null, translatedRemoteSdpB = opts.localSdpA) ;
      }
    }

    opts.method = req.method ;
    var onProvisional = opts.onProvisional ;

    var proxyRequestHeaders = opts.proxyRequestHeaders || [] ;
    var proxyResponseHeaders = opts.proxyResponseHeaders || [] ;

    // default From, To, and user part of uri if not provided
    opts.headers = opts.headers || {} ;

    // pass specified headers on to the B leg
    proxyRequestHeaders.forEach((hdr) => {
      if (req.has(hdr)) {
        opts.headers[hdr] = req.get(hdr) ;
      }
    }) ;
    /*
    opts.headers.forEach(opts.headers, (value, hdr) => {
      opts.headers[hdr] = value ;
    }) ;
    */
    if (!opts.headers.from && !opts.callingNumber) { opts.callingNumber = req.callingNumber; }
    if (!opts.headers.to && !opts.calledNumber) { opts.calledNumber = req.calledNumber; }

    opts.localSdp = opts.localSdpB || req.body ;

    uri = 'string' === typeof uri ? [uri] : uri ;

    var finalUacFail ;
    var finalUacSuccess ;
    var receivedProvisional = false ;
    var canceled = false ;
    var uacBye, reqImmediateNotify, resImmediateNotify ;
    var uacPromise ;

    // DH: NOTE (possible TODO): callback signature changes in async 2.x for detectXXX
    async.detectSeries(

      // list of remote URIs to iterate
      uri,

      // truth test
      (uri, callback) => {

        if (receivedProvisional || canceled) {
          // stop cranking back once we receive a provisional > 100 from somebody or the caller canceled
          return callback(false);
        }

        // launch the next INVITE or SUBSCRIBE
        debug('sending %s to %s', opts.method, uri) ;
        uacPromise = this.createUacDialog(uri, opts,
          (err, uacDialog) => {
            if (err) {
              //non-success: crank back to the next uri if we have one
              finalUacFail = err ;
              debug('got failure %d', err.status) ;
              return callback(false) ;
            }

            // success - we're done
            debug('got success! ') ;
            finalUacSuccess = uacDialog ;

            // for invites, we need to handle a very quick hangup coming before we establish the uas dialog
            uacDialog.on('destroy', (msg) => {
              debug('Srf#createBackToBackDialogs: got a BYE on B leg before A leg has ACK\'ed') ;
              uacBye = msg ;
            }) ;

            //for subscribes, we need to handle the immediate notify that may come back
            //from the B leg before we establish the uas dialog
            if (uacDialog.dialogType === 'SUBSCRIBE') {
              uacDialog.on('notify', function(reqNotify, resNotify) {
                debug('Srf#createBackToBackDialogs: received immediate NOTIFY after SUBSCRIBE, ' +
                  'queueing until we complete A leg dialog') ;
                reqImmediateNotify = reqNotify ;
                resImmediateNotify = resNotify ;
              }) ;
            }
            callback(true) ;
          },
          (provisionalRes) => {
            if (provisionalRes.status > 100) {
              debug('Srf#createBackToBackDialogs: received a provisional response %d', provisionalRes.status) ;

              const opts = { headers: {} } ;
              proxyResponseHeaders.forEach((hdr) => {
                if (provisionalRes.has(hdr)) { opts.headers[hdr] = provisionalRes.get(hdr) ; }
              }) ;

              if (provisionalRes.body) {
                produceSdpForALeg(provisionalRes.body, provisionalRes, (err, sdp) => {
                  if (err) {
                    console.error(`Srf#createBackToBackDialogs: failed in call to produceSdpForALeg; ' + 
                      'terminate dialog: ${err.message}`) ;

                    //TODO: now we have to hang up B and return a 503 or something to A
                  }
                  opts.body = sdp ;
                  res.send(provisionalRes.status, provisionalRes.reason, opts) ;
                }) ;
              }
              else {
                res.send(provisionalRes.status, provisionalRes.reason, opts) ;
              }

              if (onProvisional) {
                onProvisional(provisionalRes) ;
              }
              // we're committed to this uac now
              receivedProvisional = true ;
            }
          }
        ) ;
        uacPromise.then((uacRequest) => {
          req.on('cancel', () => {
            debug('Srf#createBackToBackDialogs: received CANCEL as uas; sending CANCEL as uac') ;
            canceled = true ;
            finalUacFail = new SipError(487, 'Request Terminated') ;
            uacRequest.cancel() ;
          }) ;
        }) ;
      },

      // final handler
      (successUri) => {
        const opts = { headers: {} } ;
        if (typeof successUri === 'undefined') {
          // all failed, send the final failure response back
          // (TODO: should we be tracking the "best" failure to return?)

          // pass specified headers back to the A leg
          if (!finalUacFail.res) {
            res.send(503);
          }
          else {
            proxyResponseHeaders.forEach((hdr) => {
              if (finalUacFail.res.has(hdr)) { opts.headers[hdr] = finalUacFail.res.get(hdr) ; } }) ;
            res.send(finalUacFail.status, finalUacFail.reason, opts) ;
          }
          return cb(finalUacFail) ;
        }

        // success
        proxyResponseHeaders.forEach((hdr) => {
          if (finalUacSuccess.res.has(hdr)) {
            opts.headers[hdr] = finalUacSuccess.res.get(hdr) ;
          }
        }) ;
        produceSdpForALeg(finalUacSuccess.remote.sdp, finalUacSuccess.res, (err, sdp) => {
          opts.localSdp = sdp ;

          // pass specified headers back to the A leg
          this.createUasDialog(req, res, opts, (err, uasDialog) => {
            if (err) {
              return cb(err);
            }

            finalUacSuccess.removeAllListeners('destroy') ;

            // if B leg has already hung up, emit destroy event after caller has a chance to set up handlers
            if (uacBye) {
              setImmediate(() => {
                finalUacSuccess.emit('destroy', uacBye) ;
              }) ;
            }

            // for subscribe dialogs, stitch together the two dialogs
            // so that we automatically forward NOTIFY and SUBSCRIBE requests down the other leg
            // note: we don't currently do this for invite dialogs because it is trickier
            // to know how the app wants to handle re-invites
            if (uasDialog.dialogType === 'SUBSCRIBE') {

              // remove listener for early / immediate notify and handle any such queued requests
              finalUacSuccess.removeAllListeners('notify') ;
              if (reqImmediateNotify) {
                setImmediate(() => {
                  debug('Srf#createBackToBackDialogs: processing immediate notify') ;
                  this._b2bRequestWithinDialog(uasDialog, reqImmediateNotify, resImmediateNotify,
                    ['Event', 'Subscription-State', 'Content-Type'], []) ;
                });
              }

              // notify requests come from the B leg
              finalUacSuccess.on('notify', (req, res) => {
                this._b2bRequestWithinDialog(uasDialog, req, res, ['Event', 'Subscription-State', 'Content-Type'], []) ;
              }) ;

              // subscribes (to refresh or terminate) come from the A leg
              uasDialog.on('subscribe', (req, res) => {
                this._b2bRequestWithinDialog(finalUacSuccess, req, res, ['Event', 'Expires'],
                  ['Expires', 'Subscription-State', 'Allow-Events', 'Allow']) ;
              }) ;
            }

            cb(null, uasDialog, finalUacSuccess) ;
          }) ;
        }) ;
      }
    ) ;
    return uacPromise ;
  }

  addDialog(dialog) {
    this.dialogs.set(dialog.id, dialog) ;
    debug('Srf#addDialog: adding dialog with id %s type %s, dialog count is now %d ',
      dialog.id, dialog.dialogType, this.dialogs.size) ;
  }

  removeDialog(dialog) {
    this.dialogs.delete(dialog.id) ;
    debug('Srf#removeDialog: removing dialog with id %s dialog count is now %d', dialog.id, this.dialogs.size) ;
  }

  _b2bRequestWithinDialog(dlg, req, res, proxyRequestHeaders, proxyResponseHeaders, callback) {
    callback = callback || noop ;
    var headers = {} ;
    proxyRequestHeaders.forEach((h) => {
      if (req.has(h)) { headers[h] = req.get(h); }
    }) ;
    dlg.request({
      method: req.method,
      headers: headers,
      body: req.body
    }, (err, response) => {
      headers = {} ;
      proxyResponseHeaders.forEach((h) => {
        if (!!response && response.has(h)) { headers[h] = response.get(h); }
      }) ;

      if (err) {
        debug('b2bRequestWithinDialog: error forwarding request: %s', err) ;
        res.send(response.status || 503, { headers: headers}) ;
        return callback(err) ;
      }
      var status = response.status ;

      //special case: sending a NOTIFY for subscription terminated can fail if client has already gone away
      if (req.method === 'NOTIFY' && req.has('Subscription-State') &&
        /terminated/.test(req.get('Subscription-State')) && status === 503) {
        debug('b2bRequestWithinDialog: failed forwarding a NOTIFY with ' +
          'subscription-terminated due to client disconnect') ;
        status = 200 ;
      }
      res.send(status, { headers: headers}) ;
      callback(null) ;
    });
  }
}

Srf.Dialog = Dialog ;
Srf.SipError = SipError ;
Srf.parseUri = parser.parseUri;

module.exports = exports = Srf ;

delegate(Srf.prototype, '_app')
  .method('connect')
  .method('listen')
  .method('endSession')
  .method('disconnect')
  .method('set')
  .method('get')
  .method('use')
  .method('request')
  .access('locals')
  .getter('idle') ;

methods.forEach((method) => {
  delegate(Srf.prototype, '_app').method(method.toLowerCase()) ;
}) ;


/** send a SIP request outside of a dialog
* @name Srf#request
* @method
* @param  {string} uri - sip request-uri to send request to
* @param {Object} opts - configuration options
* @param {String} opts.method - SIP method to send (lower-case)
* @param {Object} [headers] - SIP headers to apply to the outbound request
* @param {String} [body] - body to send with the SIP request
* @param {function} [callback] - callback invoked when sip request has been sent, invoked with
* signature (err, request) where `request` is a sip request object representing the sip
* message that was sent.
* @example <caption>sending OPTIONS request</caption>
* srf.request('sip.example.com', {
*   method: 'OPTIONS',
*   headers: {
*     'User-Agent': 'drachtio'
*   }
*  }, (err, req) => {
*   req.on('response', (res) => {
*     console.log(`received ${res.statusCode} response`);
*   });
* });
*
*/

/** make an inbound connection to a drachtio server
* @name Srf#connect
* @method
* @param  {Object} opts - connection options
* @param  {string} [opts.host='localhost'] - address drachtio server is listening on for client connections
* @param  {Number} [opts.port=9022] - address drachtio server is listening on for client connections
* @param  {String} opts.secret - shared secret used to authenticate connections
* @example
* const Srf = require('drachtio-srf');
* const srf = new Srf();
*
* srf.connect({host: '127.0.0.1', port: 9022, secret: 'cymru'});
* srf.on('connect', (hostport) => {
*   console.log(`connected to drachtio server offering sip endpoints: ${hostport}`);
* })
* .on('error', (err) => {
*   console.error(`error connecting: ${err}`);
* });
*
* srf.invite((req, res) => {..});
*/

/** listen for outbound connections from a drachtio server
*   @name Srf#listen
*   @method
*   @param  {Object} opts - listen options
*   @param  {number} opts.port - address drachtio server is listening on for client connections
*   @param  {string} opts.secret - shared secret used to authenticate connections
* @example
* const Srf = require('drachtio-srf');
* const srf = new Srf();
*
* srf.listen({port: 9023, secret: 'cymru'});
*
* srf.invite((req, res) => {..});
*
*/

/**
 * a <code>connect</code> event is emitted by an Srf instance when a connect method completes
 * with either success or failure
 * @event Srf#connect
 * @param {Object} err - error encountered when attempting to connect
 * @param {String} hostport - the SIP address[:port] drachtio server is listening on for incoming SIP messages
 */
/**
 * a <code>cdr:attempt</code> event is emitted by an Srf instance when a call attempt has been
 * received (inbound) or initiated (outbound)
 * @event Srf#cdr:attempt
 * @param {String} source - 'network'|'application', depending on whether the INVITE is
 * \inbound (received), or outbound (sent), respectively
 * @param {String} time - the time (UTC) recorded by the SIP stack corresponding to the attempt
 * @param {Object} msg - the actual message that was sent or received
 */
/**
 * a <code>cdr:start</code> event is emitted by an Srf instance when a call attempt has been connected successfully
 * @event Srf#cdr:start
 * @param {String} source - 'network'|'application', depending on whether the INVITE is
 * inbound (received), or outbound (sent), respectively
 * @param {String} time - the time (UTC) recorded by the SIP stack corresponding to the attempt
 * @param {String} role - 'uac'|'uas'|'uac-proxy'|'uas-proxy' indicating whether the application
 * is acting as a user agent client, user agent server, proxy (sending message), or proxy
 * (receiving message) for this cdr
 * @param {Object} msg - the actual message that was sent or received
 */
/**
 * a <code>cdr:stop</code> event is emitted by an Srf instance when a connected call has ended
 * @event Srf#cdr:stop
 * @param {String} source - 'network'|'application', depending on whether the INVITE is inbound (received),
 * or outbound (sent), respectively
 * @param {String} time - the time (UTC) recorded by the SIP stack corresponding to the attempt
 * @param {String} reason - the reason the call was ended
 * @param {Object} msg - the actual message that was sent or received
 */