Click here to Skip to main content
15,881,172 members
Articles / Programming Languages / C#

STUN Client

Rate me:
Please Sign up or sign in to vote.
4.83/5 (36 votes)
20 Apr 2007CPOL 322.2K   14.9K   85  
STUN client C# implementation with sample application
using System;
using System.Collections.Generic;
using System.Text;
using System.Timers;
using System.Net;
using System.Net.Sockets;

using LumiSoft.Net.SIP.Message;

namespace LumiSoft.Net.SIP.Stack
{
    #region Delegates

    /// <summary>
    /// Represents method what will handle ResponseReceived event.
    /// </summary>
    /// <param name="e">Event data.</param>
    public delegate void SIP_ResponseReceivedEventHandler(SIP_ResponseReceivedEventArgs e);

    #endregion

    /// <summary>
    /// A SIP_Transaction represents SIP client transaction. Defined in RFC 3261 17.1 Client Transaction.
    /// A transaction is a sequence of SIP messages exchanged between SIP network elements. 
    /// Client transaction is created by UAC,UAS or proxy core. Client transaction is responsible for
    /// sending request to remote UA and processing remote UA server transaction responses.
    /// Client transaction causes remote UA to create corresponding server transaction.
    /// </summary>
    /// <remarks>
    /// <img src="../images/SIP_ClientTransaction.gif" />
    /// </remarks>
    public class SIP_ClientTransaction : SIP_Transaction
    {           
        private SIP_Stack                  m_pSipStack                = null;
        private string                     m_ID                       = "";
        private SIP_Request                m_pRequest                 = null;
        private string                     m_Host                     = "";
        private string                     m_Transport                = SIP_Transport.UDP;
        private SIP_ClientTransactionState m_TransactionState         = SIP_ClientTransactionState.Calling;
        private Timer                      m_pTimerA                  = null;
        private Timer                      m_pTimerB                  = null;
        private Timer                      m_pTimerD                  = null;
        private Timer                      m_pTimerE                  = null;
        private Timer                      m_pTimerF                  = null;
        private Timer                      m_pTimerK                  = null;
        private Timer                      m_pTransactionTimeoutTimer = null;
        private Timer                      m_pTimerDisposeLinger      = null;
        private int                        m_Timeout                  = 60;
        private int                        m_T1                       = 500;
        private int                        m_T2                       = 4000;
        private int                        m_T4                       = 5000;
        private bool                       m_Started                  = false;
        private bool                       m_CancelQueued             = false;
        private SIP_ServerTransaction      m_pServerTransaction       = null;
        private List<SIP_Response>         m_pResponses               = null;
        private SIP_Dialog                 m_pDialog                  = null;
        private object                     m_pTag                     = null;
        private bool                       m_Disposed                 = false;

        /// <summary>
        /// Default constructor.
        /// </summary>
        /// <param name="sipStack">Reference to SIP stack.</param>
        /// <param name="request">SIP request to what caused to create client transaction.</param>
        /// <param name="host">Host name or IP with optional port. Examples: sip.lumisoft.ee,sip.lumisoft.ee:5060,100.10.1.22,100.10.1.22:5060.</param>
        /// <param name="transport">SIP transport to use. Supported values are defined in SIP_Transport class.</param>
        /// <param name="addVia">Specified if transaction adds new Via: header. If this value is false,
        /// then its user responsibility to add valid Via: header to <b>request</b> argument.</param>
        internal SIP_ClientTransaction(SIP_Stack sipStack,SIP_Request request,string host,string transport,bool addVia)
        {
            m_pSipStack = sipStack;
            m_pRequest  = request;
            m_Host      = host;
            m_Transport = transport;
                        
            m_pResponses = new List<SIP_Response>();

            // Add Via: header field.
            if(addVia){
                m_ID = SIP_t_ViaParm.CreateBranch();
                SIP_t_ViaParm via = new SIP_t_ViaParm();
                via.ProtocolName = "SIP";
                via.ProtocolVersion = "2.0";
                via.ProtocolTransport = m_Transport;
                via.SentBy = "transport_layer_will_replace_it";
                via.Branch = m_ID;
                request.Via.AddToTop(via.ToStringValue());
            }
            // User provided Via:.
            else{
                // Validate Via:
                SIP_t_ViaParm via = request.Via.GetTopMostValue();
                if(via == null){
                    throw new ArgumentException("Via: header is missing !");
                }
                if(via.Branch == null){
                    throw new ArgumentException("Via: header 'branch' prameter is missing !");
                }

                m_ID = via.Branch;
            }
        }

        #region method Dispose

        /// <summary>
        /// Disposes transaction and cleans up all resources.
        /// </summary>
        public override void Dispose()
        {
            if(m_Disposed){
                return;
            }
                        
            try{
                m_TransactionState = SIP_ClientTransactionState.Terminated;

                base.Dispose();

                this.ResponseReceived = null;

                if(m_pTimerA != null){
                    m_pTimerA.Dispose();
                    m_pTimerA = null;
                }

                if(m_pTimerB != null){
                    m_pTimerB.Dispose();
                    m_pTimerB = null;
                }

                if(m_pTimerD != null){
                    m_pTimerD.Dispose();
                    m_pTimerD = null;
                }

                if(m_pTimerE != null){
                    m_pTimerE.Dispose();
                    m_pTimerE = null;
                }

                if(m_pTimerK != null){
                    m_pTimerK.Dispose();
                    m_pTimerK = null;
                }

                if(m_pTransactionTimeoutTimer != null){
                    m_pTransactionTimeoutTimer.Dispose();
                    m_pTransactionTimeoutTimer = null;
                }

                if(m_pTimerDisposeLinger != null){
                    m_pTimerDisposeLinger.Dispose();
                    m_pTimerDisposeLinger = null;
                }
                               
                // Log
                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) disposed.");
            }
            finally{
                // Remove from transactions collection.
                m_pSipStack.TransactionLayer.RemoveClientTransaction(this);

                OnTerminated();
            }

            m_Disposed = true;
        }

        #endregion

        
        #region method Begin

        /// <summary>
        /// Call this method to start transaction.
        /// </summary>
        /// <exception cref="InvalidOperationException">Is called when Begin is called multiple times.</exception>
        public void Begin()
        {
            if(m_Started){
                throw new InvalidOperationException("Transaction already started !");
            }
            m_Started = true;

            // Log
            m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) created.");

            #region INVITE

            // INVITE transaction
            if(m_pRequest.Method == "INVITE"){
                /* RFC 3261 17.1.1.2.
                    If an unreliable transport is being used, the client transaction MUST start 
                    timer A with a value of T1. (Timer A controls request retransmissions).  
                */
                if(m_pRequest.Via.GetTopMostValue().ProtocolTransport.ToUpper() == "UDP"){
                    m_pTimerA = new Timer(m_T1);
                    m_pTimerA.Elapsed += new ElapsedEventHandler(m_pTimerA_Elapsed);
                    m_pTimerA.Enabled = true;
                    // Log
                    m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer A(requst retransmit timer) started, will triger after " + m_T1 + ".");
                }

                /* RFC 3261 17.1.1.2.
                    For any transport, the client transaction MUST start timer B with a value
                    of 64*T1 seconds (Timer B controls transaction timeouts).
                */
                m_pTimerB = new Timer(64 * m_T1);
                m_pTimerB.AutoReset = false;
                m_pTimerB.Elapsed += new ElapsedEventHandler(m_pTimerB_Elapsed);
                m_pTimerB.Enabled = true;
                // Log
                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer B(calling state timeout timer) started, will triger after " + 64 * m_T1 + ".");
                
                // Set transaction state.
                m_TransactionState = SIP_ClientTransactionState.Calling;
            }

            #endregion

            #region non-INVITE

            // Non-INVITE transaction
            else{
                /* RFC 3261 17.1.2.2.
                    If an unreliable transport is being used, the client transaction MUST start 
                    timer E with a value of T1. (Timer E controls request retransmissions).  
                */
                if(m_pRequest.Via.GetTopMostValue().ProtocolTransport.ToUpper() == "UDP"){
                    m_pTimerE = new Timer(m_T1);
                    m_pTimerE.Elapsed += new ElapsedEventHandler(m_pTimerE_Elapsed);
                    m_pTimerE.Enabled = true;
                    // Log
                    m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer E(Non-INVITE request retransmission interval) started, will triger after " + m_T1 + ".");
                }
                
                /* RFC 3261 17.1.2.2.
                    For any transport, the client transaction MUST start timer F with a value
                    of 64*T1 seconds (Timer F controls transaction timeouts).
                */
                m_pTimerF = new Timer(64 * m_T1);
                m_pTimerF.AutoReset = false;
                m_pTimerF.Elapsed += new ElapsedEventHandler(m_pTimerF_Elapsed);
                m_pTimerF.Enabled = true;
                // Log
                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer F(Non-INVITE trying state timeout timer) started, will triger after " + 64 * m_T1 + ".");

                // Set transaction state.
                m_TransactionState = SIP_ClientTransactionState.Trying;
            }

            #endregion

            //--- Common for all transactions
                        
            // This is just timout timer and ensures that transaction ends or will terminated.
            m_pTransactionTimeoutTimer = new Timer(m_Timeout * 1000);
            m_pTransactionTimeoutTimer.Elapsed += new ElapsedEventHandler(m_pTransactionTimeoutTimer_Elapsed);
            m_pTransactionTimeoutTimer.Enabled = true;
            // Log
            m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Transcation timeout timer started,timeout after " + m_pTransactionTimeoutTimer.Interval + " ms");

            try{
                // Transmits intial request to destination recipient.
                m_pSipStack.TransportLayer.SendRequest(m_pRequest,m_Host,m_Transport);
            }
            catch{
                // Log
                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) transport error.");

                OnTransportError();
                Dispose();
            }
        }
                                                
        #endregion

        #region method Cancel

        /// <summary>
        /// Cancels transaction.
        /// </summary>
        /// <remarks>
        /// Cancel behaviour:
        ///     *) If non-INVITE method, skip cancel.
        ///     *) If has got final response, skip cancel because there is nothing to cancel.
        ///     *) If transaction has got provisional response, send cancel.
        ///     *) If transaction hasn't got provisional response, queue cancel request.
        ///          If provisional response received, send cancel.
        ///          If final response received, skip cancel because there is nothing to cancel.
        /// 
        /// NOTE: Canceled event is raised only if '478 Request Terminated' is received from calee !
        /// </remarks>
        public override void Cancel()
        {  
            /* RFC 3261 9.2.
                A CANCEL request SHOULD NOT be sent to cancel a request other than INVITE.
                If no provisional response has been received, the CANCEL request MUST NOT be sent; rather, 
                the client MUST wait for the arrival of a provisional response before sending the request. 
                If the original request has generated a final response, the CANCEL SHOULD NOT be sent, 
                as it is an effective no-op, since CANCEL has no effect on requests that have already 
                generated a final response.
            */

            // We have already pending cancel.
            if(m_CancelQueued){
                return;
            }

            if(m_pRequest.Method == SIP_Methods.INVITE){
                DoCancel();
            }
        }

        #endregion


        #region method m_pTimerA_Elapsed

        /// <summary>
        /// Is called when INVITE request retransission must be done.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void m_pTimerA_Elapsed(object sender,ElapsedEventArgs e)
        {
            /* RFC 3261 17.1.1.2.
                When timer A fires, the client transaction MUST retransmit the request by passing 
                it to the transport layer, and MUST reset the timer with a value of 2*T1.
            */
            
            try{
                // Retransmit intial request
                m_pSipStack.TransportLayer.SendRequest(m_pRequest,m_Host,m_Transport);

                // Update interval.
                m_pTimerA.Interval = m_pTimerA.Interval * 2;
            
                // Log
                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer A(INVITE request retransmission) triggered,next transmit after " + m_pTimerA.Interval + " ms.");
            }
            catch{
                // Log
                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) transport error.");

                OnTransportError();
                Dispose();
            }
        }

        #endregion

        #region method m_pTimerB_Elapsed

        /// <summary>
        /// This method is raised when INVITE transaction calling state didn't get any response.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void m_pTimerB_Elapsed(object sender,ElapsedEventArgs e)
        {
            /* RFC 3261 17.1.1.2.
                If the client transaction is still in the "Calling" state when timer
                B fires, the client transaction SHOULD inform the TU that a timeout
                has occurred.  The client transaction MUST NOT generate an ACK.
            */

            m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer B (INVITE transaction calling state timeout timer) triggered.");

            OnTimedOut();
            Dispose();
        }

        #endregion

        #region method m_pTimerD_Elapsed

        /// <summary>
        /// Is called when INVITE completed state linger time ended.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void m_pTimerD_Elapsed(object sender,ElapsedEventArgs e)
        {
            /* RFC 3261 17.1.1.2.
                If timer D fires while the client transaction is in the "Completed"
                state, the client transaction MUST move to the terminated state.
            */

            m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer D (INVITE completed state linger timer) triggered.");

            Dispose();
        }

        #endregion

        #region method m_pTimerE_Elapsed

        /// <summary>
        /// Is called when non-INVITE request retransission must be done.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void m_pTimerE_Elapsed(object sender,ElapsedEventArgs e)
        {                      
            /* RFC 3261 17.1.2.2.
                When the timer fires again, it is reset to a MIN(4*T1,T2).
                For the default values of T1 and T2, this results in
                intervals of 500 ms, 1 s, 2 s, 4 s, 4 s, 4 s, etc.
            */

            // Retransmit intial request
            m_pSipStack.TransportLayer.SendRequest(m_pRequest,m_Host,m_Transport);

            // Update interval
            m_pTimerE.Interval = Math.Min(m_pTimerE.Interval * 2,m_T2);

            // Log
            m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer E (retransission timer) triggered,next transmit after " + m_pTimerE.Interval + " ms.");            
        }

        #endregion

        #region method m_pTimerF_Elapsed

        /// <summary>
        /// This method is raised when non-INVITE transaction trying or proceeding state didn't get any response.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void m_pTimerF_Elapsed(object sender,ElapsedEventArgs e)
        {
            /* RFC 3261 17.1.2.2.
                If Timer F fires while the client transaction is still in the
                "Trying" state, the client transaction SHOULD inform the TU about the
                timeout, and then it SHOULD enter the "Terminated" state.
            */

            m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer F (non-INVITE transaction trying or proceeding state timeout timer) triggered.");

            OnTimedOut();
            Dispose();
        }

        #endregion

        #region method m_pTimerK_Elapsed

        /// <summary>
        /// This method is called when non-INVITE completed state linger time ended.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void m_pTimerK_Elapsed(object sender,ElapsedEventArgs e)
        {
            /* RFC 3261 17.1.2.2.
                If Timer K fires while in completed state, the client transaction
                MUST transition to the "Terminated" state.
            */

            m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer K (non-INVITE completed state linger timer) triggered.");

            Dispose();
        }

        #endregion

        #region method m_pTransactionTimeoutTimer_Elapsed

        /// <summary>
        /// Is called when transaction has timed out.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void m_pTransactionTimeoutTimer_Elapsed(object sender,ElapsedEventArgs e)
        {
            // Log
            m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Transaction timeout timer triggered.");
                        
            OnTimedOut();
            
            /* We may not dispose at once, with normal INVITE cancel we must get '487 Request Terminated'.
               We linger 5 sec before dispose, in that time we must get '487 Request Terminated'.
               Don't know if thats best/clearest way but, that will do the job. */
            if(m_pRequest.Method == SIP_Methods.INVITE){
                Cancel();

                m_pTimerDisposeLinger = new Timer(5000);
                m_pTimerDisposeLinger.AutoReset = false;
                m_pTimerDisposeLinger.Elapsed += new ElapsedEventHandler(m_pTimerDisposeLinger_Elapsed);
                m_pTimerDisposeLinger.Enabled = true;
            }
            else{
                Dispose();
            }
        }
                
        #endregion

        #region method m_pTimerDisposeLinger_Elapsed

        /// <summary>
        /// This timer is called when Dispose linger timer ended, we now just need to dispose transaction.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void m_pTimerDisposeLinger_Elapsed(object sender,ElapsedEventArgs e)
        {
            Dispose();
        }

        #endregion


        #region method ProcessResponse

        /// <summary>
        /// Processes recipient returned response.
        /// </summary>
        /// <param name="response">SIP reponse what is returned from destination recipient.</param>
        internal void ProcessResponse(SIP_Response response)
        {   
            /* NOTES:
                We may not change transaction state before raising "ResponseRecievived" event,
                otherwise "proxy context" choose best final response will work wrong.
            */

            lock(this){
                /* RFC 3261 9.1.
                    If cancel queued, then we have Calling state, don't process non-final response.
                    Send cancel, we must get '478 Request terminated' or then timer B will timeout.
                    If final reponse process, skip cancel, because nothing to cancel.
                */
                if(m_CancelQueued){
                    m_CancelQueued = false;
                    if(response.StausCodeType == SIP_StatusCodeType.Provisional){
                        DoCancel();
                        return;
                    }
                }

                // If '478 Request Terminated' Raise Canceled event, dispose.
                if(response.StatusCode == 478){
                    OnCanceled();
                    Dispose();
                    return;
                }

                #region INVITE

                // INVITE transaction
                if(m_pRequest.Method == "INVITE"){
                                        
                    #region Calling

                    // Calling state
                    if(m_TransactionState == SIP_ClientTransactionState.Calling){
                        // If there is timer A (request retransmit timer), stop it,because we got response.
                        if(m_pTimerA != null){
                            m_pTimerA.Dispose();
                            m_pTimerA = null;
                            // Log
                            m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer A (retransmit timer) stopped.");
                        }
                        // If there is timer B (calling state timeout timer), stop it because we got response.
                        if(m_pTimerB != null){
                            m_pTimerB.Dispose();
                            m_pTimerB = null;
                            // Log
                            m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer B (calling state timeout timer) stopped.");
                        }

                        // 1xx response
                        if(response.StausCodeType == SIP_StatusCodeType.Provisional){                            
                            m_pResponses.Add(response);
                            m_TransactionState = SIP_ClientTransactionState.Proceeding;

                            // 101 - 199 create early dialog
                            if(response.StatusCode > 100){
                                EnsureDialog(response);
                                PassResponseToDialog(response);
                            }

                            // Raise ResponseReceived event.
                            OnResponseReceived(response);                            
                        }
                        // 2xx response
                        else if(response.StausCodeType == SIP_StatusCodeType.Success){                            
                            m_pResponses.Add(response);

                            EnsureDialog(response);
                            PassResponseToDialog(response);

                            // Raise ResponseReceived event.
                            OnResponseReceived(response);
                            
                            m_TransactionState = SIP_ClientTransactionState.Terminated;
                            Dispose();
                        }
                        // 300 - 699 response
                        else{                            
                            m_pResponses.Add(response);

                            // Raise ResponseReceived event.
                            OnResponseReceived(response);

                            // Send ACK
                            SendAck(response);

                            /* RFC 3261 17.1.1.2.
                                The client transaction SHOULD start timer D when it enters the "Completed" state, 
                                with a value of at least 32 seconds for unreliable transports.
                            */
                            if(response.Via.GetTopMostValue().ProtocolTransport.ToUpper() == "UDP"){
                                m_pTimerD = new Timer(64 * m_T1);
                                m_pTimerD.AutoReset = false;
                                m_pTimerD.Elapsed += new ElapsedEventHandler(m_pTimerD_Elapsed);
                                m_pTimerD.Enabled = true;
                                // Log
                                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer D(linger timer for retransimted negative final responses) started, will triger after " + m_pTimerD.Interval + ".");
                            }

                            m_TransactionState = SIP_ClientTransactionState.Completed;
                        }
                    }

                    #endregion

                    #region Proceeding

                    // Proceeding state
                    else if(m_TransactionState == SIP_ClientTransactionState.Proceeding){
                        // 1xx response
                        if(response.StausCodeType == SIP_StatusCodeType.Provisional){
                            m_pResponses.Add(response);

                            // 101 - 199 create early dialog
                            if(response.StatusCode > 100){
                                EnsureDialog(response);
                                PassResponseToDialog(response);
                            }

                            // Raise ResponseReceived event.
                            OnResponseReceived(response);
                        }
                        // 2xx reponse
                        else if(response.StausCodeType == SIP_StatusCodeType.Success){                            
                            m_pResponses.Add(response);

                            EnsureDialog(response);
                            PassResponseToDialog(response);

                            // Raise ResponseReceived event.
                            OnResponseReceived(response);
                            
                            m_TransactionState = SIP_ClientTransactionState.Terminated;
                            Dispose();
                        }
                        // 300 - 699 response
                        else{                            
                            m_pResponses.Add(response);

                            PassResponseToDialog(response);

                            // Raise ResponseReceived event.
                            OnResponseReceived(response);

                            // Send ACK
                            SendAck(response);

                            /* RFC 3261 17.1.1.2.
                                The client transaction SHOULD start timer D when it enters the "Completed" state, 
                                with a value of at least 32 seconds for unreliable transports.
                            */
                            if(response.Via.GetTopMostValue().ProtocolTransport.ToUpper() == "UDP"){
                                m_pTimerD = new Timer(64 * m_T1);
                                m_pTimerD.AutoReset = false;
                                m_pTimerD.Elapsed += new ElapsedEventHandler(m_pTimerD_Elapsed);
                                m_pTimerD.Enabled = true;
                                // Log
                                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer D(linger timer for retransimted negative final responses) started, will triger after " + m_pTimerD.Interval + ".");
                            }
                
                            m_TransactionState = SIP_ClientTransactionState.Completed;
                        }
                    }

                    #endregion

                    #region Completed

                    // Completed state
                    else if(m_TransactionState == SIP_ClientTransactionState.Completed){                    
                        // 1xx - 299 response
                        if(response.StatusCode >= 100 || response.StatusCode <= 299){
                            // Do nothing
                        }
                        // 300 - 699 response
                        else{
                            // Send ACK
                            SendAck(response);
                        }
                    }
 
                    #endregion

                    #region Terminated

                    // Terminated state
                    else if(m_TransactionState == SIP_ClientTransactionState.Terminated){
                        // We should never reach here, but if so, do nothing.
                    }

                    #endregion
                }

                #endregion

                #region Non-INVITE

                // Non-INVITE transaction
                else{

                    #region Trying

                    // Trying state
                    if(m_TransactionState == SIP_ClientTransactionState.Trying){
                        // 1xx response
                        if(response.StausCodeType == SIP_StatusCodeType.Provisional){                            
                            m_pResponses.Add(response);

                            // Raise ResponseReceived event.
                            OnResponseReceived(response);

                            m_TransactionState = SIP_ClientTransactionState.Proceeding;
                        }
                        // 200-699  response
                        else{                            
                            m_pResponses.Add(response);

                            // If there is timer E (retransmit timer), stop it,because we got response.
                            if(m_pTimerE != null){
                                m_pTimerE.Dispose();
                                m_pTimerE = null;

                                // Log
                                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer E (retransmit timer) stopped.");
                            }
                            // If there is timer F (trying,proceeding state timeout timer), stop it because we got response.
                            if(m_pTimerF != null){
                                m_pTimerF.Dispose();
                                m_pTimerF = null;
                                // Log
                                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer F (trying,proceeding state timeout timer) stopped.");
                            }

                            // Raise ResponseReceived event.
                            OnResponseReceived(response);

                            /* RFC 3261 17.1.1.2.
                                Once the client transaction enters the "Completed" state, it MUST set
                                Timer K to fire in T4 seconds for unreliable transports.
                            */
                            if(response.Via.GetTopMostValue().ProtocolTransport.ToUpper() == "UDP"){
                                m_pTimerK = new Timer(m_T4);
                                m_pTimerK.AutoReset = false;
                                m_pTimerK.Elapsed += new ElapsedEventHandler(m_pTimerK_Elapsed);
                                m_pTimerK.Enabled = true;
                                // Log
                                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer K(linger timer for retransimted final responses) started, will triger after " + m_pTimerK.Interval + ".");
                            }

                            m_TransactionState = SIP_ClientTransactionState.Completed;
                        }
                    }

                    #endregion

                    #region Proceeding

                    // Proceeding state
                    else if(m_TransactionState == SIP_ClientTransactionState.Proceeding){
                        // 1xx response
                        if(response.StausCodeType == SIP_StatusCodeType.Provisional){
                            m_pResponses.Add(response);

                            // Raise ResponseReceived event.
                            OnResponseReceived(response);
                        }
                        // 200-699 response
                        else{                            
                            m_pResponses.Add(response);

                            // If there is timer E (retransmit timer), stop it,because we got response.
                            if(m_pTimerE != null){
                                m_pTimerE.Dispose();
                                m_pTimerE = null;

                                // Log
                                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer E (retransmit timer) stopped.");
                            }
                            // If there is timer F (trying,proceeding state timeout timer), stop it because we got response.
                            if(m_pTimerF != null){
                                m_pTimerF.Dispose();
                                m_pTimerF = null;
                                // Log
                                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer F (trying,proceeding state timeout timer) stopped.");
                            }

                            // Raise ResponseReceived event.
                            OnResponseReceived(response);

                            /* RFC 3261 17.1.1.2.
                                Once the client transaction enters the "Completed" state, it MUST set
                                Timer K to fire in T4 seconds for unreliable transports.
                            */
                            if(response.Via.GetTopMostValue().ProtocolTransport.ToUpper() == "UDP"){
                                m_pTimerK = new Timer(m_T4);
                                m_pTimerK.AutoReset = false;
                                m_pTimerK.Elapsed += new ElapsedEventHandler(m_pTimerK_Elapsed);
                                m_pTimerK.Enabled = true;
                                // Log
                                m_pSipStack.Logger.AddDebug("Transaction(id='" + m_ID + "' method=" + m_pRequest.Method + " server=false) Timer K(linger timer for retransimted final responses) started, will triger after " + m_pTimerK.Interval + ".");
                            }

                            m_TransactionState = SIP_ClientTransactionState.Completed;
                        }
                    }

                    #endregion

                    #region Completed

                    // Completed state
                    else if(m_TransactionState == SIP_ClientTransactionState.Completed){
                        // Do nothing
                    }

                    #endregion

                    #region Terminated

                    // Terminated state
                    else if(m_TransactionState == SIP_ClientTransactionState.Terminated){
                        // We should never reach here, but if so, do nothing.
                    }

                    #endregion
                }

                #endregion

            }
        }
                                                
        #endregion


        #region method SendAck

        /// <summary>
        /// Sends ACK to recipient.
        /// </summary>
        /// <param name="response">SIP response which to generate ACK.</param>
        private void SendAck(SIP_Response response)
        {
            /* RFC 3261 17.1.1.3 Construction of the ACK Request.
                The ACK request constructed by the client transaction MUST contain
                values for the Call-ID, From, and Request-URI that are equal to the
                values of those header fields in the request passed to the transport
                by the client transaction (call this the "original request").  The To
                header field in the ACK MUST equal the To header field in the
                response being acknowledged, and therefore will usually differ from
                the To header field in the original request by the addition of the
                tag parameter.  The ACK MUST contain a single Via header field, and
                this MUST be equal to the top Via header field of the original
                request.  The CSeq header field in the ACK MUST contain the same
                value for the sequence number as was present in the original request,
                but the method parameter MUST be equal to "ACK".
              
                If the INVITE request whose response is being acknowledged had Route
                header fields, those header fields MUST appear in the ACK.  This is
                to ensure that the ACK can be routed properly through any downstream
                stateless proxies.
            */

            SIP_Request ack = new SIP_Request();
            ack.Method = "ACK";
            ack.Uri = m_pRequest.Uri;
            ack.Via.AddToTop(m_pRequest.Via.GetTopMostValue().ToStringValue());
            ack.CallID = m_pRequest.CallID;
            ack.From = m_pRequest.From;
            ack.To = response.To;
            ack.CSeq = new SIP_t_CSeq(m_pRequest.CSeq.SequenceNumber,"ACK");
            foreach(SIP_HeaderField h in response.Header.Get("Route:")){
                ack.Header.Add("Route:",h.Value);
            }
            ack.MaxForwards = 70;

            // Send request to recipient.
            m_pSipStack.TransportLayer.SendRequest(ack,m_Host,m_Transport);
        }

        #endregion

        #region method DoCancel

        /// <summary>
        /// Cancels transaction as RFC 3261 specifies.
        /// </summary>
        private void DoCancel()
        {
            /*
                The following procedures are used to construct a CANCEL request.  The
                Request-URI, Call-ID, To, the numeric part of CSeq, and From header
                fields in the CANCEL request MUST be identical to those in the
                request being cancelled, including tags.  A CANCEL constructed by a
                client MUST have only a single Via header field value matching the
                top Via value in the request being cancelled.  Using the same values
                for these header fields allows the CANCEL to be matched with the
                request it cancels (Section 9.2 indicates how such matching occurs).
                However, the method part of the CSeq header field MUST have a value
                of CANCEL.  This allows it to be identified and processed as a
                transaction in its own right (See Section 17).

                If the request being cancelled contains a Route header field, the
                CANCEL request MUST include that Route header field's values.

                  This is needed so that stateless proxies are able to route CANCEL
                  requests properly.

                The CANCEL request MUST NOT contain any Require or Proxy-Require
                header fields.

                Once the CANCEL is constructed, the client SHOULD check whether it
                has received any response (provisional or final) for the request
                being cancelled (herein referred to as the "original request").
                If no provisional response has been received, the CANCEL request MUST
                NOT be sent; rather, the client MUST wait for the arrival of a
                provisional response before sending the request.  If the original
                request has generated a final response, the CANCEL SHOULD NOT be
                sent, as it is an effective no-op, since CANCEL has no effect on
                requests that have already generated a final response.  When the
                client decides to send the CANCEL, it creates a client transaction
                for the CANCEL and passes it the CANCEL request along with the
                destination address, port, and transport.  The destination address,
                port, and transport for the CANCEL MUST be identical to those used to
                send the original request.

                  If it was allowed to send the CANCEL before receiving a response
                  for the previous request, the server could receive the CANCEL
                  before the original request.

                Note that both the transaction corresponding to the original request
                and the CANCEL transaction will complete independently.  However, a
                UAC canceling a request cannot rely on receiving a 487 (Request
                Terminated) response for the original request, as an RFC 2543-
                compliant UAS will not generate such a response.  If there is no
                final response for the original request in 64*T1 seconds (T1 is
                defined in Section 17.1.1.1), the client SHOULD then consider the
                original transaction cancelled and SHOULD destroy the client
                transaction handling the original request.
            */
                                   
            // If has got final response, skip cancel because there is nothing to cancel.
            if(GetFinalResponse() != null){
                return;
            }
            else if(GetLastProvisionalResponse() != null){
                SIP_Request cancelRequest = new SIP_Request();
                cancelRequest.Method = SIP_Methods.CANCEL;
                cancelRequest.Uri = m_pRequest.Uri;
                cancelRequest.Via.Add(m_pRequest.Via.GetTopMostValue().ToStringValue());
                cancelRequest.CallID = m_pRequest.CallID;
                cancelRequest.From = m_pRequest.From;
                cancelRequest.To = m_pRequest.To;
                cancelRequest.CSeq = new SIP_t_CSeq(m_pRequest.CSeq.SequenceNumber,SIP_Methods.CANCEL);
                foreach(SIP_t_AddressParam route in m_pRequest.Route.GetAllValues()){
                    cancelRequest.Route.Add(route.ToStringValue());
                }
                cancelRequest.MaxForwards = 70;

                SIP_ClientTransaction transaction = m_pSipStack.TransactionLayer.CreateClientTransaction(cancelRequest,m_Host,m_Transport,false);
                transaction.Begin();
            }
            // If transaction hasn't got provisional response, queue cancel request.
            else{                
                m_CancelQueued = true;
            }           
        }

        #endregion

        #region method PassResponseToDialog

        /// <summary>
        /// Passes specified response to SIP dialog. If dialog doesn't exist, nothing is done.
        /// </summary>
        /// <param name="response">SIP response.</param>
        private void PassResponseToDialog(SIP_Response response)
        {
            lock(this){
                if(this.Dialog != null){
                    this.Dialog.ProcessResponse(response);
                }
            }
        }

        #endregion

        #region method EnsureDialog

        /// <summary>
        /// Ensures that SIP dialog exists. If not, creates SIP dialog, if exists, updates dialog state.
        /// </summary>
        /// <param name="response">SIP response what causes dialog creation.</param>
        private void EnsureDialog(SIP_Response response)
        {
            lock(this){
                // Create dialog
                if(this.CanCreateDialog && this.Dialog == null){
                    m_pDialog = m_pSipStack.TransactionLayer.CreateDialog(this,response);
                }
            }
        }

        #endregion


        #region Properties Implementation

        /// <summary>
        /// Gets transaction ID (Via: branch parameter value).
        /// </summary>
        public override string ID
        {
            get{ return m_ID; }
        }

        /// <summary>
        /// Gets current transaction state.
        /// </summary>
        public SIP_ClientTransactionState TransactionSate
        {
            get{ return m_TransactionState; }
        }

        /// <summary>
        /// SIP request what transaction handles.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this class is Disposed and this property is accessed.</exception>
        public override SIP_Request Request
        {
            get{
                if(m_Disposed){
                    throw new ObjectDisposedException("SIP_ClientTransaction");
                }

                return m_pRequest; 
            }
        }
                
        /// <summary>
        /// Gets server transaction what child transaction this transaction is. Returns null if no owner server transaction.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this class is Disposed and this property is accessed.</exception>
        public SIP_ServerTransaction ServerTransaction
        {
            get{ 
                if(m_Disposed){
                    throw new ObjectDisposedException("SIP_ClientTransaction");
                }

                return m_pServerTransaction; 
            }
        }

        /// <summary>
        /// Gets transaction related responses.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this class is Disposed and this property is accessed.</exception>
        public override SIP_Response[] Responses
        {
            get{ 
                if(m_Disposed){
                    throw new ObjectDisposedException("SIP_ClientTransaction");
                }

                return m_pResponses.ToArray(); 
            }
        }

        /// <summary>
        /// Gets transaction dialog. Returns null if no dialog available.
        /// </summary>
        /// <exception cref="ObjectDisposedException">Is raised when this class is Disposed and this property is accessed.</exception>
        public override SIP_Dialog Dialog
        {
            get{
                if(m_Disposed){
                    throw new ObjectDisposedException("SIP_ClientTransaction");
                }

                return m_pDialog; 
            }
        }

        /// <summary>
        /// Gets or sets after how many seconds this client transaction times out.
        /// NOTE: This property takes effect only if transaction hasn't started yet.
        /// </summary>
        /// <exception cref="ArgumentException">Is raised when ivalid value is passed.</exception>
        public int Timeout
        {
            get{ return m_Timeout; }

            set{
                if(value < 1){
                    throw new ArgumentException("Property Timeout value must be >= 1 !");
                }

                m_Timeout = value;
            }
        }

        /// <summary>
        /// Gets or sets user data.
        /// </summary>
        public object Tag
        {
            get{ return m_pTag; }

            set{ m_pTag = value; }
        }

        #endregion

        #region Events Implementation
                
        /// <summary>
        /// Is called when this transaction has got response destination end point.
        /// </summary>
        public event SIP_ResponseReceivedEventHandler ResponseReceived = null;

        /// <summary>
        /// Raises ResponseReceived event.
        /// </summary>
        /// <param name="response">SIP response received.</param>
        protected void OnResponseReceived(SIP_Response response)
        {
            if(this.ResponseReceived != null){
                this.ResponseReceived(new SIP_ResponseReceivedEventArgs(m_pSipStack,this,response));
            }
        }

        #endregion

    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Estonia Estonia
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions