balena/elixir-sippet

Client transaction already exists

ldr opened this issue · 7 comments

ldr commented

First of all, this looks like a great application, still investigating, but thanks for making this available!

I have created a simple project using elixir-sippet 1.0.7.

My aim is to create a program that does (many) outbound registrations with authentication for different contacts (so one contact per registration).

I am able to send out a REGISTER request, by creating one as follows:

req = """
REGISTER sip:foo.com SIP/2.0
Via: SIP/2.0/UDP 192.168.64.1:5060;branch=#{Sippet.Message.create_branch()}
From: <sip:phone1@foo.com>;tag=#{Sippet.Message.create_tag()}
To: <sip:phone1@foo.com>
CSeq: 1 REGISTER
Call-ID: #{Sippet.Message.create_call_id()}
Contact: <sip:phone1@192.168.64.1:5060>
Expires: 300
""" |> Sippet.Message.parse!()

And then sending it out as follows:
Sippet.send :mystack, req

I see the request being sent out to the registrar and a 401 challenge being responded by it.

I have a simple Core registered that just prints out the arguments incoming_response and client_key passed to the receive_response function.

Now I'd like to send a second request, but with the proper Authorization header in it.

First, I tried to generate a new request based on the sent request and received response:
{:ok, auth_req} = Sippet.DigestAuth.make_request req, resp, fn(_realm) -> {:ok, "phone1", "topsecret"} end

Then incremented the cseq by one as follows:
auth_req_inc_cseq = put_in auth_req.headers.cseq, {2, :register}

But when then trying to send that request, it says the client transaction already exists.

I was thinking I may need to change the via branch, but still. If the response to a request is received, where does the transaction then remain ? Should I remove it myself ?

What would be the proper way to go for this ?

Thanks!

Hi @ldr.
receive_response calls in the context of a server transaction.
Thus, when your code is called, the transaction has not yet completed execution and will complete after.

def proceeding(:cast, {:outgoing_response, response}, data) do
data = send_response(response, data)
case StatusLine.status_code_class(response.start_line) do
1 -> {:keep_state, data}
_ -> {:next_state, :completed, data}
end
end

ldr commented

Thanks for your response.

But the receive_response is being called after sending the request from sippet and actually receiving the response on that, so I'm creating a uac. Also the transaction remains "present" in some state in sippet because if I retry to send the request, even minutes later, I get the error that the transaction already exists. Even after the final response to the request was already received.

@Nitrino The bit of code you refer to is also about sending a response from sippet, not ? i am not trying to do that, thinking this bit should not be part of the code path taken in my scenario. Or is this some internal function for sending the response to some internal callback function that handles receiving it ?

I will spend more time to comprehend it all :)

Hey @ldr,

Sippet was created as being a very thin layer around the transport and transaction layers, and I didn't attempt to simplify usage with an API that eventually gets outdated as soon different purposes are aggregated to the initial concept. But this "simplicity" comes at a cost: the library user eventually has to deal with some SIP signalling specifics.

This is what you should do to authenticate requests:

  alias Sippet.{DigestAuth, Message}
  alias Sippet.Message.{RequestLine, StatusLine}

  def send_with_auth(message, authenticate_fun) when is_binary(message) do
    message
    |> Message.parse!()
    |> send_with_auth(authenticate_fun)
  end

  def send_with_auth(%Message{start_line: %RequestLine{}} = request, authenticate_fun) do
    Sippet.send(:mystack, request)

    receive do
      {:on_response, _, %Message{start_line: %StatusLine{status_code: status_code}} = response}
      when status_code in [401, 407] ->
        {:ok, new_req} = DigestAuth.make_request(request, response, authenticate_fun)

        new_req =
          new_req
          |> Message.update_header(:cseq, fn {seq, method} ->
            {seq + 1, method}
          end)
          |> Message.update_header_front(:via, fn {ver, proto, hostport, params} ->
            {ver, proto, hostport, %{params | "branch" => Message.create_branch()}}
          end)
          |> Message.update_header(:from, fn {name, uri, params} ->
            {name, uri, %{params | "tag" => Message.create_tag()}}
          end)

        Sippet.send(:mystack, new_req)

      {:on_response, _, _} = event ->
        # handle other responses

      {:on_error, _, _} = event ->
        # handle transport layer errors
    end
  end

In short, after authenticating the request you should increment the CSeq header, create a new branch for the topmost Via header, and create a new tag for the From header. Actually it is not necessary to set a new from-tag, but it is generally a good practice.

When you read Section 22 of the RFC 3261, it may sound like you just have to update the Authorization or Proxy-Authorization headers before resubmitting the request, but actually this is going to be a new request with the same Call-ID, and thus the procedures of section 8.1 will apply.

Explaining the above, the "magic" resides in the branch parameter passed on the topmost Via. It is used to index transactions. The logic is simple: client transactions use the branch to index. Same branch, same transaction. So when you attempted to retransmit the request incrementing the CSeq but maintained the branch, you actually attempted to send it through the already existing transaction, and it is not programmed to send new requests after it has sent one. Check Section 17.1.1 of the RFC 3261, there you will find the FSM flowchart of the client transactions.

ldr commented

Thank you for this elaborate explanation :)

I will dive deeper into it today, surely this will fix my issue, but will definitely give feedback here before closing.

Thanks again !

And now you may ask: "why don't you just add the code above to Sippet?" Well, as a library user, you might want to control the sequence number by yourself. You can also want to generate the branch and from-tag by yourself. Say you have a Registry to help you to route requests/responses along the dialogs; then you will want to create and register them as keys to your process. Your process might be a GenServer and you may want to handle responses using handle_info instead of receiving them synchronously using receive do. You may want to reject the authentication challenge because your password backend isn't working or the passed realm is unknown...

So it's delicate.

If you want to propose an API generic enough to cover most of the cases, let me know, and I'll merge it to the code promptly.

Meanwhile, I agree that maybe a little more documentation might help other library users. I'll check that.

ldr commented

yes the flexibility that's provided this way is really nice.

I didn't get it fully working yet, but that may also be because I have a slightly modified udp transport used :) I will have to continue with it tomorrow, but will come back here.

ldr commented

Works as advertised !! Really nice :) I'm sure I can use this library a lot more for different situations in the future. Thank you again and until next time, hopefully I can contribute something back :)