/// <summary> /// This method will be called periodically on a background thread to handle /// message resending as well as timeout related state transitions. /// </summary> public override void OnBkTask() { bool callOnInviteComplete = false; SipRequest callbackMsg = null; try { using (TimedLock.Lock(agent)) { if (SysTime.Now >= ttd) { SetState(SipTransactionState.Terminated); return; } switch (base.State) { default: case SipTransactionState.Unknown: break; case SipTransactionState.InviteCalling: break; case SipTransactionState.InviteProceeding: break; case SipTransactionState.InviteCompleted: if (base.TimerH.HasFired) { SetState(SipTransactionState.Terminated); // Setup to call OnInviteComplete(ackRequest=null) // indicating that the dialog was not established. callOnInviteComplete = true; return; } if (base.TimerG.HasFired) { transport.Send(remoteEP, finalResponse); base.TimerG.Interval = Helper.Min(base.BaseTimers.T2, Helper.Multiply(base.TimerG.Interval, 2)); } break; case SipTransactionState.InviteConfirmed: if (base.TimerI.HasFired) { SetState(SipTransactionState.Terminated); } break; case SipTransactionState.Trying: break; case SipTransactionState.Proceeding: break; case SipTransactionState.Completed: if (base.TimerJ.HasFired) { SetState(SipTransactionState.Terminated); } break; case SipTransactionState.Terminated: break; } } } finally { // Handle the agent callbacks outside of the lock to avoid // deadlock issues. if (callOnInviteComplete) { agent.OnInviteComplete(this, request, finalResponse, callbackMsg); } } }
/// <summary> /// The managing <see cref="ISipAgent" /> is responsible for calling this /// method whenever it receives requests correlated to this transaction. /// </summary> /// <param name="request">The received <see cref="SipRequest" />.</param> public void OnRequest(SipRequest request) { SipRequest callbackMsg = null; bool callOnRequest = false; bool callOnInviteBegin = false; bool callOnInviteComplete = false; try { request.SourceTransaction = this; using (TimedLock.Lock(agent)) { if (this.request == null) { SipViaValue viaValue; SipContactValue toValue; NetworkBinding sentBy; IPAddress address; // This is the initial transaction request. this.request = request; // Handle the Via "received" and "rport" header parameters (mostly) as described on page // RFC 3261 (page 145) and RFC 3581 (page 4). viaValue = request.GetHeader <SipViaValue>(SipHeader.Via); if (viaValue == null) { // Illegal request SetState(SipTransactionState.Terminated); return; } sentBy = viaValue.SentByBinding; if (sentBy == null || sentBy.IsHost || sentBy.Address != request.RemoteEndpoint.Address) { viaValue.Received = request.RemoteEndpoint.Address.ToString(); } if (viaValue.RPort != null) { viaValue.RPort = request.RemoteEndpoint.Port.ToString(); } // Determine the destination network endpoint based on the // rules described on RFC 3261 (page 146). if (request.SourceTransport.IsStreaming) { // $todo(jeff.lill): // // This implementation is incomplete. To be fully // compliant with the RFC, I'd have to check to // see if the connection is still present in the // transport and if not, use the received and // rport values as described. remoteEP = request.RemoteEndpoint; } else { if (viaValue.MAddr != null) { if (!IPAddress.TryParse(viaValue.MAddr, out address)) { SipException e; // Illegal request SetState(SipTransactionState.Terminated); e = new SipException("Illegal request: Invalid [Via: maddr]."); e.Transport = transport.Name; e.SourceEndpoint = request.RemoteEndpoint; e.BadMessage = request; throw e; } remoteEP = new NetworkBinding(address, viaValue.SentByBinding.Port); } else { remoteEP = request.RemoteEndpoint; } } // INVITE and non-INVITE requests have different state machines. if (request.Method == SipMethod.Invite) { // Start an INVITE transaction SetState(SipTransactionState.InviteProceeding); // If the request has a "To" header without a "tag" parameter then // generate a tag. Note that this code will cause provisional INVITE // responses to include a generated tag which the RFC indicates // SHOULD NOT be done. But, it's much safer to do this once here // for all transaction types, avoiding special cases, and besides, // I've noticed that Asterisk includes a tag in its provisional // INVITE responses. toValue = request.GetHeader <SipContactValue>(SipHeader.To); if (toValue != null) { if (toValue["tag"] == null) { toValue["tag"] = SipHelper.GenerateTagID(); } request.SetHeader(SipHeader.To, toValue); } // Always send an initial provisional trying response. provisionalResponse = request.CreateResponse(SipStatus.Trying, null); SendResponse(provisionalResponse); // Setup to call the agent's OnInviteBegin() method. callOnInviteBegin = true; callbackMsg = request; } else if (request.Method == SipMethod.Ack) { // Allow an ACK request to drop through to the state machine. } else { // Start a non-INVITE transaction SetState(SipTransactionState.Trying); // Setup to call the agent's OnRequest() method. callOnRequest = true; callbackMsg = request; } return; } // Handle state specific processing switch (base.State) { default: case SipTransactionState.Unknown: SysLog.LogError("Unexpected SIP transaction state."); SetState(SipTransactionState.Terminated); return; case SipTransactionState.InviteCalling: break; case SipTransactionState.InviteProceeding: if (provisionalResponse != null) { transport.Send(remoteEP, provisionalResponse); } break; case SipTransactionState.InviteCompleted: if (request.Method == SipMethod.Ack) { SetState(SipTransactionState.InviteConfirmed); // Setup to call OnInviteComplete(ack); callOnInviteComplete = true; callbackMsg = request; return; } Assertion.Test(finalResponse != null); transport.Send(remoteEP, finalResponse); break; case SipTransactionState.InviteConfirmed: break; case SipTransactionState.Trying: break; case SipTransactionState.Proceeding: Assertion.Test(provisionalResponse != null); transport.Send(remoteEP, provisionalResponse); break; case SipTransactionState.Completed: Assertion.Test(finalResponse != null); transport.Send(remoteEP, finalResponse); break; case SipTransactionState.Terminated: break; } } } finally { // Handle the agent callbacks outside of the lock to avoid // deadlock issues. if (callOnRequest) { agent.OnRequest(this, callbackMsg); } if (callOnInviteBegin) { agent.OnInviteBegin(this, request); } if (callOnInviteComplete) { agent.OnInviteComplete(this, this.request, finalResponse, callbackMsg); } } }
/// <summary> /// Constuctor. /// </summary> /// <param name="dialog">The <see cref="SipDialog" />.</param> /// <param name="transaction">The <see cref="SipTransaction" /> associated with the event (or <c>null</c>).</param> /// <param name="core">The <see cref="SipCore" /> that raised the event.</param> /// <param name="clientRequest">Pass the <see cref="SipRequest" /> received for server side dialog creation (or <c>null</c>).</param> /// <param name="serverResult">The <see cref="SipResult" /> returned by the server, completing its side of the dialog creation (or <c>null</c>).</param> internal SipDialogEventArgs(SipDialog dialog, SipTransaction transaction, SipCore core, SipRequest clientRequest, SipResult serverResult) { this.Dialog = dialog; this.Transaction = transaction; this.Core = core; this.ClientRequest = clientRequest; this.ServerResult = serverResult; this.Response = null; }
/// <summary> /// Executes a synchronous SIP request transaction. /// </summary> /// <param name="request">The <see cref="SipRequest" /> to be submitted.</param> /// <param name="dialog">The <see cref="SipDialog" /> for requests that initiate a dialog (or <c>null</c>).</param> /// <returns>The <see cref="SipResult" /> detailing the result of the operation.</returns> /// <remarks> /// <note> /// This method adds reasonable <b>Call-ID</b> and <b>CSeq</b> headers to the request if these /// headers are not already present. /// </note> /// </remarks> public SipResult Request(SipRequest request, SipDialog dialog) { var ar = BeginRequest(request, dialog, null, null); return(EndRequest(ar)); }
/// <summary> /// Initiates an asynchronous SIP request transaction. /// </summary> /// <param name="request">The <see cref="SipRequest" /> to be submitted.</param> /// <param name="dialog">The <see cref="SipDialog" /> for requests that initiate a dialog (or <c>null</c>).</param> /// <param name="callback">The delegate to be called when the operation completes (or <c>null</c>).</param> /// <param name="state">Application defined state (or <c>null</c>).</param> /// <returns>The <see cref="IAsyncResult" /> to be used to track the operation's progress.</returns> /// <remarks> /// <para> /// All requests to <see cref="BeginRequest(SipRequest,SipDialog,AsyncCallback,object)" /> must be matched with a /// call to <see cref="EndRequest" />. /// </para> /// <note> /// This method adds reasonable <b>Call-ID</b> and <b>CSeq</b> headers to the request if these /// headers are not already present. /// </note> /// </remarks> public IAsyncResult BeginRequest(SipRequest request, SipDialog dialog, AsyncCallback callback, object state) { ClientAsyncResult arClient = new ClientAsyncResult(request, dialog, callback, state); SipValue viaValue; SipCSeqValue vCSeq; string transactionID; SipClientTransaction transaction; ISipTransport transport; NetworkBinding remoteEP; if (dialog != null && request.Method != SipMethod.Invite) { throw new InvalidOperationException("Dialogs may be created only for INVITE requests."); } arClient.Dialog = dialog; transport = router.SelectTransport(this, request, out remoteEP); if (transport == null) { throw new SipException("No approriate transport is available."); } // Initialize the request's Via header and transaction ID as necessary. transactionID = SipHelper.GenerateBranchID(); viaValue = new SipValue(string.Format("SIP/2.0/{0} {1}", transport.Name, transport.Settings.ExternalBinding.Address)); viaValue["branch"] = transactionID; viaValue["rport"] = string.Empty; request.PrependHeader(SipHeader.Via, viaValue); // Initialize common headers as necessary if (!request.ContainsHeader(SipHeader.CallID)) { request.AddHeader(SipHeader.CallID, SipHelper.GenerateCallID()); } vCSeq = request.GetHeader <SipCSeqValue>(SipHeader.CSeq); if (vCSeq == null) { vCSeq = new SipCSeqValue(SipHelper.GenCSeq(), request.MethodText); request.AddHeader(SipHeader.CSeq, vCSeq); } // Initialize the transaction transaction = new SipClientTransaction(this, request, transactionID, transport, remoteEP); transaction.AgentState = arClient; // Handle initial dialog INVITE specific initialization if (dialog != null && request.Method == SipMethod.Invite && dialog.State == SipDialogState.Waiting) { // Client-side dialogs need to know the transaction so // they'll be able to send the confirming ACK. dialog.InitiatingTransaction = transaction; // Dialogs need to know about the sequence number used in INVITE requests so // that the ACK can be generated with the same sequence number. dialog.AckCSeq = vCSeq.Number; // The dialog has been intialized enough to be added to the core's // early dialog table. core.AddEarlyDialog(dialog); } // Start the transaction using (TimedLock.Lock(this)) { transactions.Add(transactionID, transaction); } transaction.Start(); arClient.Started(); return(arClient); }
/// <summary> /// Parses a <see cref="SipRequest" /> or <see cref="SipResponse" /> from the /// <paramref name="buffer"/> byte array passed. /// </summary> /// <param name="buffer">The UTF-8 encoded SIP message data.</param> /// <param name="isCompletePacket"><c>true</c> if <paramref name="buffer" /> includes both the message header and payload.</param> /// <returns>A <see cref="SipRequest" /> or <see cref="SipResponse" />.</returns> /// <remarks> /// <para> /// This method is used internally by <see cref="ISipTransport" /> implementations to /// convert raw data read from the transport into SIP messages. This method supports /// two basic situations: /// </para> /// <list type="bullet"> /// <item> /// The transport is packet oriented (aka UDP) and the buffer passed includes /// both the SIP message header and content information. In this case, you /// need to pass <paramref name="isCompletePacket"/> as <c>true</c>, and the /// SIP message returned will be complete, with the <see cref="Contents" /> /// property set to the payload bytes. /// </item> /// <item> /// The transport is stream oriented (aka TCP or TLS) and the buffer passed /// includes only the message header information from the message start line /// up to and including the empty line terminating the header section. In this /// case, the <see cref="Contents" /> property of the SIP message returned will /// be set to <c>null</c> and <see cref="ContentLength" /> will be set to the /// parsed value. The transport is then responsible for extracting the message /// payload from the stream and explictly setting the <see cref="Contents" /> /// property. /// </item> /// </list> /// </remarks> public static SipMessage Parse(byte[] buffer, bool isCompletePacket) { string text; int dataPos; int p, pEnd, pColon; string firstLine; string line; string reasonPhrase; string[] fields; SipMessage message; SipHeader header; int cbContents; dataPos = Helper.IndexOf(buffer, CRLFCRLF); if (dataPos == -1) { throw new SipException("Malformed SIP message: header termination missing."); } dataPos += 4; text = Helper.FromUTF8(buffer, 0, dataPos); // Look at the first line of text to determine whether we have a // request or a response. p = 0; pEnd = text.IndexOf("\r\n"); firstLine = text.Substring(p, pEnd - p); fields = firstLine.Split(' '); if (fields.Length < 3) { throw new SipException("Malformed SIP message: invalid first line."); } p = firstLine.IndexOf(' '); p = firstLine.IndexOf(' ', p + 1); reasonPhrase = firstLine.Substring(p + 1); if (fields[0].ToUpper().StartsWith("SIP/")) { int statusCode; if (!int.TryParse(fields[1], out statusCode)) { throw new SipException("Malformed SIP message: invalid response status code."); } if (statusCode < 100 || statusCode >= 700) { throw new SipException("Invalid status code [{0}].", statusCode); } message = new SipResponse(statusCode, reasonPhrase, fields[0]); } else { message = new SipRequest(fields[0], fields[1], reasonPhrase); } // Parse the headers header = null; p = pEnd + 2; while (true) { pEnd = text.IndexOf("\r\n", p); line = text.Substring(p, pEnd - p); if (line.Length == 0) { break; } if (line[0] == ' ' || line[0] == '\t') { // Header folding if (header == null) { throw new SipException("Malformed SIP message: invalid header folding."); } header.FullText += " " + line.Trim(); } else { string name; string value; // Parse a normal header: <field-name> ":" <field-value> pColon = line.IndexOf(':'); if (pColon == -1) { throw new SipException("Malformed SIP message: header missing a colon."); } name = line.Substring(0, pColon).Trim(); value = line.Substring(pColon + 1).Trim(); header = message.headers.Add(name, value); } p = pEnd + 2; } // Handle the payload if (isCompletePacket) { // Extract the contents from the buffer. If we have a Content-Length header // then use that to determine how much data we have, otherwise extract the // data from the end of the headers to the end of the buffer. header = message[SipHeader.ContentLength]; if (header != null) { if (!int.TryParse(header.Text, out cbContents) || cbContents < 0 || cbContents > buffer.Length - dataPos) { throw new SipException("Malformed SIP message: invalid Content-Length."); } message.contents = Helper.Extract(buffer, dataPos, cbContents); } else { message.contents = Helper.Extract(buffer, dataPos); } } else { // Messages received from streaming transports must have a valid // Content-Length header. header = message[SipHeader.ContentLength]; if (header == null) { throw new SipException("Content-Length required for messages received on stream transports."); } if (!int.TryParse(header.Text, out cbContents) || cbContents < 0) { throw new SipException("Malformed SIP message: invalid Content-Length."); } } return(message); }