Packages
parrot_platform
0.0.1-alpha.2
Elixir libraries and OTP behaviours for building telecom applications with SIP protocol and media handling
Current section
Files
Jump to
Current section
Files
lib/parrot/sip/dialog_statem.ex
defmodule Parrot.Sip.DialogStatem do
@moduledoc """
SIP Dialog State Machine
States:
- :early - Initial state for early dialogs
- :confirmed - State for established dialogs
- :terminated - Terminal state
Events:
- {:uas_request, sip_msg} - Process UAS request
- {:uas_response, resp, req} - Process UAS response
- {:uac_request, sip_msg} - Process UAC request
- {:uac_trans_result, result} - Process UAC transaction result
- {:set_owner, pid} - Set dialog owner
"""
@behaviour :gen_statem
require Logger
alias Parrot.Sip.{Dialog, DialogId, Message, Branch}
alias Parrot.Sip.Headers.{Contact, Via}
@type trans :: {:trans, pid()}
@type trans_result :: {:message, any()} | {:stop, any()}
@type dialog_handle :: pid()
@type dialog_type :: :invite | :notify
@type start_link_ret :: {:ok, pid()} | {:error, {:already_started, pid()}} | {:error, term()}
defmodule Data do
@moduledoc false
defstruct [
# Dialog ID
:id,
# Dialog state
:dialog,
# Local contact
:local_contact,
# Early branch for forked responses
:early_branch,
# Log ID for logging
:log_id,
# Dialog type (:invite or :notify)
:dialog_type,
need_cleanup: true,
# Owner process monitor
owner_mon: nil
]
@type t :: %__MODULE__{
id: String.t() | nil,
dialog: Dialog.t() | nil,
local_contact: Contact.t() | nil,
early_branch: String.t() | nil,
log_id: String.t() | nil,
dialog_type: :invite | :notify | nil,
need_cleanup: boolean(),
owner_mon: reference() | nil
}
end
# API
def child_spec(args) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [args]},
type: :worker,
restart: :temporary,
shutdown: 5000
}
end
@spec start_link(term()) :: start_link_ret()
def start_link(args) do
:gen_statem.start_link(
{:via, Registry, {Parrot.Registry, via_tuple(args)}},
__MODULE__,
args,
[]
)
end
@impl :gen_statem
def callback_mode, do: :state_functions
@impl :gen_statem
def init({:uas, resp_sip_msg, req_sip_msg}) do
Logger.debug(
"dialog: init called with UAS args: #{inspect(resp_sip_msg)}, #{inspect(req_sip_msg)}"
)
# Create dialog from UAS perspective
{:ok, dialog} = Dialog.uas_create(req_sip_msg, resp_sip_msg)
dialog_id = dialog.id
Logger.debug("dialog: init #{inspect(dialog_id)}")
Registry.register(Parrot.Registry, dialog_id, nil)
data = %Data{
id: dialog_id,
dialog: dialog,
local_contact: Message.get_header(req_sip_msg, "contact"),
log_id: uas_log_id(resp_sip_msg),
dialog_type: dialog_type(req_sip_msg)
}
# Set a timer for NOTIFY dialogs
actions =
if data.dialog_type == :notify do
# Get expiration from Expires header or default to 3600 seconds
expires = get_expires(req_sip_msg, 3600)
Logger.info(
"dialog #{inspect(data.id)}: setting subscription timeout for #{expires} seconds"
)
[{:state_timeout, expires * 1000, :subscription_expired}]
else
[]
end
initial_state = if Dialog.is_early?(dialog), do: :early, else: :confirmed
Logger.info("dialog #{inspect(data.id)}: starting in #{inspect(initial_state)} state")
req_method = req_sip_msg.method
res_method = resp_sip_msg.method
call_id = Message.get_header(req_sip_msg, "call-id")
Logger.metadata(
dialog_id: data.id,
dialog_type: data.dialog_type,
req_method: req_method,
res_method: res_method,
call_id: call_id
)
{:ok, initial_state, data, actions}
end
def init({:uac, out_req, resp_sip_msg}) do
# Create dialog from UAC perspective
# Dialog.uac_create always returns {:ok, dialog}
{:ok, dialog} = Dialog.uac_create(out_req, resp_sip_msg)
dialog_id = dialog.id
# Handle early branch for provisional responses
early_branch =
if resp_sip_msg.status_code >= 100 and resp_sip_msg.status_code < 200 do
branch = get_branch_from_request(out_req)
branch_key = "branch:" <> branch
Logger.debug("dialog: early branch #{inspect(branch_key)}")
Registry.register(Parrot.Registry, branch_key, nil)
branch
else
nil
end
Logger.debug("dialog: init #{inspect(dialog_id)}")
Logger.debug("dialog: early branch #{inspect(early_branch)}")
Registry.register(Parrot.Registry, dialog_id, nil)
data = %Data{
id: dialog_id,
dialog: dialog,
local_contact: Message.get_header(out_req, "contact"),
early_branch: early_branch,
log_id: uac_log_id(resp_sip_msg),
dialog_type: dialog_type(out_req)
}
Logger.debug("dialog: data #{inspect(data)}")
initial_state = if Dialog.is_early?(dialog), do: :early, else: :confirmed
Logger.info("dialog #{inspect(dialog_id)}: starting in #{inspect(initial_state)} state")
{:ok, initial_state, data}
end
@spec uas_find(Message.t()) :: {:ok, dialog_handle()} | :not_found
def uas_find(%Message{} = req_sip_msg) do
# Try to extract dialog ID from the message
dialog_id = DialogId.from_message(req_sip_msg)
if DialogId.is_complete?(dialog_id) do
find_dialog(DialogId.to_string(dialog_id))
else
:not_found
end
end
@spec uas_request(Message.t()) :: :process | {:reply, Message.t()}
def uas_request(%Message{} = sip_msg) do
Logger.debug("dialog: uas_request #{inspect(sip_msg)}")
# Check if this message has a complete dialog ID
dialog_id = DialogId.from_message(sip_msg)
if DialogId.is_complete?(dialog_id) do
dialog_id_str = DialogId.to_string(dialog_id)
Logger.debug("dialog: found dialog id #{inspect(dialog_id_str)} in request")
case find_dialog(dialog_id_str) do
{:error, :no_dialog} ->
Logger.warning("dialog #{uas_log_id(sip_msg)}: cannot find dialog")
resp = Message.reply(sip_msg, 481, "Call/Transaction Does Not Exist")
{:reply, resp}
{:ok, dialog_pid} ->
Logger.debug("dialog #{inspect(dialog_pid)}: found dialog")
try do
:gen_statem.call(dialog_pid, {:uas_request, sip_msg})
catch
:exit, {reason, _} when reason in [:normal, :noproc] ->
resp = Message.reply(sip_msg, 481, "Call/Transaction Does Not Exist")
{:reply, resp}
end
end
else
Logger.debug("dialog: no complete dialog id in request")
uas_validate_request(sip_msg)
end
end
@spec uas_response(Message.t(), Message.t()) :: Message.t()
def uas_response(%Message{} = resp_sip_msg, %Message{} = req_sip_msg) do
Logger.debug("dialog: uas_response #{inspect(resp_sip_msg)}")
# Check if response creates or continues a dialog
dialog_id = DialogId.from_message(resp_sip_msg)
if DialogId.is_complete?(dialog_id) do
dialog_id_str = DialogId.to_string(dialog_id)
Logger.debug("dialog: dialog id #{inspect(dialog_id_str)} in response")
case find_dialog(dialog_id_str) do
{:error, :no_dialog} ->
Logger.warning("dialog #{uas_log_id(resp_sip_msg)}: cannot find dialog")
uas_maybe_create_dialog(resp_sip_msg, req_sip_msg)
{:ok, dialog_pid} ->
Logger.debug("dialog #{inspect(dialog_pid)}: found dialog")
uas_pass_response(dialog_pid, resp_sip_msg, req_sip_msg)
end
else
Logger.debug("dialog: no complete dialog id in response")
resp_sip_msg
end
end
@spec uac_request(String.t(), Message.t()) ::
{:ok, Message.t()} | {:error, :no_dialog}
def uac_request(dialog_id, sip_msg) do
case find_dialog(dialog_id) do
{:ok, dialog_pid} ->
:gen_statem.call(dialog_pid, {:uac_request, sip_msg})
{:error, reason} ->
{:error, reason}
end
end
@spec uac_result(Message.t(), trans_result()) :: :ok
def uac_result(%Message{} = out_req, trans_result) do
# Extract dialog ID from the request
dialog_id = DialogId.from_message(out_req)
if DialogId.is_complete?(dialog_id) do
dialog_id_str = DialogId.to_string(dialog_id)
case find_dialog(dialog_id_str) do
{:error, :no_dialog} ->
Logger.warning(
"dialog: #{uac_log_id(out_req)} is not found for request #{out_req.method}"
)
:ok
{:ok, dialog_pid} ->
uac_trans_result(dialog_pid, trans_result)
end
else
uac_no_dialog_result(out_req, trans_result)
end
end
@spec set_owner(pid(), String.t()) :: :ok
def set_owner(pid, dialog_id) when is_pid(pid) do
case find_dialog(dialog_id) do
{:ok, dialog_pid} ->
:gen_statem.cast(dialog_pid, {:set_owner, pid})
{:error, :no_dialog} ->
:ok
end
end
@spec count() :: non_neg_integer()
def count do
Parrot.Sip.Dialog.Supervisor.num_active()
end
@spec find_dialog(String.t()) :: {:ok, pid()} | {:error, :no_dialog}
def find_dialog(dialog_id) do
case Registry.lookup(Parrot.Registry, dialog_id) do
[{pid, _}] -> {:ok, pid}
[] -> {:error, :no_dialog}
end
end
# State Functions
# Early state
def early({:call, from}, {:uas_request, req_sip_msg}, data) do
process_uas_request(:early, req_sip_msg, data, from)
end
def early(:cast, {:uas_response, resp_sip_msg, req_sip_msg}, data) do
process_uas_response(:early, resp_sip_msg, req_sip_msg, data)
end
def early(:cast, {:uac_trans_result, trans_result}, data) do
process_uac_trans_result(:early, trans_result, data)
end
def early({:call, from}, {:uac_request, req_sip_msg}, data) do
process_uac_request(:early, req_sip_msg, data, from)
end
def early(:cast, {:set_owner, pid}, data) do
process_set_owner(:early, pid, data)
end
def early(:info, {:DOWN, _ref, :process, _pid, _reason}, data) do
Logger.info("dialog #{inspect(data.id)}: owner process terminated")
{:stop, :normal}
end
def early(event_type, event_content, data) do
handle_event(event_type, event_content, data)
end
# Confirmed state
def confirmed({:call, from}, {:uas_request, req_sip_msg}, data) do
process_uas_request(:confirmed, req_sip_msg, data, from)
end
def confirmed(:cast, {:uas_response, resp_sip_msg, req_sip_msg}, data) do
process_uas_response(:confirmed, resp_sip_msg, req_sip_msg, data)
end
def confirmed(:cast, {:uac_trans_result, trans_result}, data) do
process_uac_trans_result(:confirmed, trans_result, data)
end
def confirmed({:call, from}, {:uac_request, req_sip_msg}, data) do
process_uac_request(:confirmed, req_sip_msg, data, from)
end
def confirmed(:cast, {:set_owner, pid}, data) do
process_set_owner(:confirmed, pid, data)
end
def confirmed(:state_timeout, :subscription_expired, data) do
Logger.info("dialog #{inspect(data.id)}: subscription expired")
{:stop, :normal}
end
def confirmed(:info, {:DOWN, _ref, :process, _pid, _reason}, data) do
Logger.info("dialog #{inspect(data.id)}: owner process terminated")
{:stop, :normal}
end
def confirmed(event_type, event_content, data) do
handle_event(event_type, event_content, data)
end
# Terminated state
def terminated(_, _, _data) do
{:stop, :normal}
end
# Private Functions
defp process_uas_request(state, req_sip_msg, data, from) do
# Process the request in the dialog
# Dialog.uas_process always returns {:ok, updated_dialog}
{:ok, updated_dialog} = Dialog.uas_process(req_sip_msg, data.dialog)
updated_data = %{data | dialog: updated_dialog}
# Check if dialog should transition states
new_state = if updated_dialog.state == :terminated, do: :terminated, else: state
{:next_state, new_state, updated_data, [{:reply, from, :process}]}
end
defp process_uas_response(state, resp_sip_msg, _req_sip_msg, data) do
# For early dialogs, check if this response confirms the dialog
new_state =
if state == :early and resp_sip_msg.status_code >= 200 and resp_sip_msg.status_code < 300 do
:confirmed
else
state
end
{:next_state, new_state, data}
end
defp process_uac_trans_result(state, {:message, resp_sip_msg}, data) do
# Process response in dialog context
# Dialog.uac_response always returns {:ok, updated_dialog}
{:ok, updated_dialog} = Dialog.uac_response(resp_sip_msg, data.dialog)
updated_data = %{data | dialog: updated_dialog}
# Check state transitions
new_state =
cond do
updated_dialog.state == :terminated -> :terminated
updated_dialog.state == :confirmed and state == :early -> :confirmed
true -> state
end
{:next_state, new_state, updated_data}
end
defp process_uac_trans_result(_state, {:stop, _reason}, _data) do
{:stop, :normal}
end
defp process_uac_request(_state, req_sip_msg, data, from) do
# Create in-dialog request
# Dialog.uac_request always returns {:ok, request, updated_dialog}
{:ok, request, updated_dialog} = Dialog.uac_request(req_sip_msg.method, data.dialog)
updated_data = %{data | dialog: updated_dialog}
{:keep_state, updated_data, [{:reply, from, {:ok, request}}]}
end
defp process_set_owner(_state, pid, data) do
# Monitor the owner process
if data.owner_mon do
Process.demonitor(data.owner_mon)
end
mon = Process.monitor(pid)
updated_data = %{data | owner_mon: mon}
{:keep_state, updated_data}
end
defp handle_event(:enter, _old_state, _data) do
:keep_state_and_data
end
defp handle_event({:call, from}, event_content, data) do
Logger.warning("dialog #{inspect(data.id)}: unexpected call: #{inspect(event_content)}")
# Reply with an error for unexpected calls
{:keep_state_and_data, [{:reply, from, {:error, :unexpected_call}}]}
end
defp handle_event(event_type, event_content, data) do
Logger.warning(
"dialog #{inspect(data.id)}: unexpected event #{inspect(event_type)}: #{inspect(event_content)}"
)
:keep_state_and_data
end
# Helper functions
defp via_tuple({:uas, resp_sip_msg, _req_sip_msg}) do
dialog_id = DialogId.from_message(resp_sip_msg)
{:dialog, DialogId.to_string(dialog_id)}
end
defp via_tuple({:uac, _out_req, resp_sip_msg}) do
dialog_id = DialogId.from_message(resp_sip_msg)
{:dialog, DialogId.to_string(dialog_id)}
end
defp dialog_type(%Message{method: method}) do
case String.upcase(to_string(method)) do
"NOTIFY" -> :notify
"SUBSCRIBE" -> :notify
_ -> :invite
end
end
defp uas_log_id(%Message{} = msg) do
call_id = Message.get_header(msg, "call-id")
method = msg.method
"#{method} #{call_id}"
end
defp uac_log_id(%Message{} = msg) do
call_id = Message.get_header(msg, "call-id")
method = if msg.type == :request, do: msg.method, else: "response"
"#{method} #{call_id}"
end
defp get_expires(%Message{} = msg, default) do
case Message.get_header(msg, "expires") do
nil ->
default
expires when is_integer(expires) ->
expires
expires when is_binary(expires) ->
case Integer.parse(expires) do
{val, _} -> val
:error -> default
end
_ ->
default
end
end
defp get_branch_from_request(%Message{} = request) do
case Message.get_header(request, "via") do
%Via{parameters: %{"branch" => branch}} -> branch
[%Via{parameters: %{"branch" => branch}} | _] -> branch
_ -> Branch.generate()
end
end
defp uas_validate_request(%Message{} = _sip_msg) do
# For now, just allow processing
# In a real implementation, this would validate the request
:process
end
defp uas_maybe_create_dialog(%Message{} = resp_sip_msg, %Message{} = req_sip_msg) do
# Check if this response creates a dialog
if should_create_dialog?(resp_sip_msg, req_sip_msg) do
# Start a new dialog
case Parrot.Sip.Dialog.Supervisor.start_child({:uas, resp_sip_msg, req_sip_msg}) do
{:ok, _pid} ->
resp_sip_msg
{:error, _reason} ->
resp_sip_msg
end
else
resp_sip_msg
end
end
defp should_create_dialog?(%Message{status_code: status_code}, %Message{method: method}) do
# Dialogs are created by 2xx responses to INVITE or SUBSCRIBE
dialog_creating_method = String.upcase(to_string(method)) in ["INVITE", "SUBSCRIBE"]
successful_response = status_code >= 200 and status_code < 300
dialog_creating_method and successful_response
end
defp uas_pass_response(dialog_pid, resp_sip_msg, req_sip_msg) do
:gen_statem.cast(dialog_pid, {:uas_response, resp_sip_msg, req_sip_msg})
resp_sip_msg
end
defp uac_no_dialog_result(%Message{} = out_req, {:message, resp_sip_msg}) do
# Check if this response creates a dialog
if should_create_dialog?(resp_sip_msg, out_req) do
# Start a new dialog
case Parrot.Sip.Dialog.Supervisor.start_child({:uac, out_req, resp_sip_msg}) do
{:ok, _pid} ->
:ok
{:error, reason} ->
Logger.warning("Failed to create dialog: #{inspect(reason)}")
:ok
end
else
:ok
end
end
defp uac_no_dialog_result(_out_req, {:stop, _reason}) do
:ok
end
defp uac_trans_result(dialog_pid, trans_result) do
:gen_statem.cast(dialog_pid, {:uac_trans_result, trans_result})
:ok
end
end