Current section

Files

Jump to
sippet lib sippet.ex
Raw

lib/sippet.ex

defmodule Sippet do
@moduledoc """
Holds the Sippet stack.
Network transport protocols should be registered during initialization:
def init(_) do
Sippet.register_transport(:udp, false)
...
end
Messages are dispatched to transports by sending the following message:
send(pid, {:send_message, message, host, port, transaction})
Whenever a message is received by a transport, the function
`Sippet.handle_transport_message` is called, which will validate and route
messages through the transaction layer or send directly to the core.
"""
use Supervisor
import Kernel, except: [send: 2]
alias Sippet.{Message, Transactions}
alias Sippet.Message.{RequestLine, StatusLine}
require Logger
@typedoc "A SIP message request"
@type request :: Message.request()
@typedoc "A SIP message response"
@type response :: Message.response()
@typedoc "An network error that occurred while sending a message"
@type reason :: term
@typedoc "A client transaction identifier"
@type client_key :: Transactions.Client.Key.t()
@typedoc "A server transaction identifier"
@type server_key :: Transactions.Server.Key.t()
@typedoc "Sippet identifier"
@type sippet :: atom
@doc """
Handles the sigil `~K`.
It returns a client or server transaction key depending on the number of
parameters passed.
## Examples
iex> import Sippet, only: [sigil_K: 2]
iex> Sippet.Transactions.Client.Key.new("z9hG4bK230f2.1", :invite)
~K[z9hG4bK230f2.1|:invite]
iex> ~K[z9hG4bK230f2.1|INVITE]
~K[z9hG4bK230f2.1|:invite]
iex> Sippet.Transactions.Server.Key.new("z9hG4bK74b21", :invite, {"client.biloxi.example.com", 5060})
~K[z9hG4bK74b21|:invite|client.biloxi.example.com:5060]
iex> ~K[z9hG4bK74b21|INVITE|client.biloxi.example.com:5060]
~K[z9hG4bK74b21|:invite|client.biloxi.example.com:5060]
"""
def sigil_K(string, _) do
case String.split(string, "|") do
[branch, method] ->
Transactions.Client.Key.new(branch, sigil_to_method(method))
[branch, method, sentby] ->
[host, port] = String.split(sentby, ":")
Transactions.Server.Key.new(
branch,
sigil_to_method(method),
{host, String.to_integer(port)}
)
end
end
defp sigil_to_method(method) do
case method do
":" <> rest -> Message.to_method(rest)
other -> Message.to_method(other)
end
end
@doc """
Sends a message (request or response) using transactions if possible.
Requests of method `:ack` is sent directly to the transport layer.
A `Sippet.Transactions.Client` is created for requests to handle client
retransmissions, when the transport presumes it, and match response
retransmissions, so the `Sippet.Core` doesn't get retransmissions other than
200 OK for `:invite` requests.
In case of success, returns `:ok`.
"""
@spec send(sippet, request | response) :: :ok | {:error, reason}
def send(sippet, message) when is_atom(sippet) do
unless Message.valid?(message) do
raise ArgumentError, "expected :message argument to be a valid SIP message"
end
do_send(sippet, message)
end
defp do_send(sippet, %Message{start_line: %RequestLine{method: :ack}} = request),
do: Sippet.Router.send_transport_message(sippet, request, nil)
defp do_send(sippet, %Message{start_line: %RequestLine{}} = outgoing_request),
do: Sippet.Router.send_transaction_request(sippet, outgoing_request)
defp do_send(sippet, %Message{start_line: %StatusLine{}} = outgoing_response),
do: Sippet.Router.send_transaction_response(sippet, outgoing_response)
@doc """
Verifies if the transport protocol used to send the given message is
reliable.
"""
@spec reliable?(sippet, Message.t()) :: boolean
def reliable?(sippet, %Message{headers: %{via: [via | _]}})
when is_atom(sippet) do
{_version, protocol, _host_and_port, _params} = via
case Registry.lookup(sippet, {:transport, protocol}) do
[{_, reliable}] ->
reliable
_ ->
raise ArgumentError, message: "protocol not registered"
end
end
@doc """
Registers a transport for a given protocol.
"""
@spec register_transport(sippet, atom, boolean) :: :ok | {:error, :already_registered}
def register_transport(sippet, protocol, reliable)
when is_atom(sippet) and is_atom(protocol) and is_boolean(reliable) do
case Registry.register(sippet, {:transport, protocol}, reliable) do
{:ok, _} ->
:ok
{:error, {:already_registered, _}} ->
{:error, :already_registered}
end
end
@doc """
Registers the stack core.
"""
@spec register_core(sippet, atom) :: :ok
def register_core(sippet, module)
when is_atom(sippet) and is_atom(module) do
Registry.put_meta(sippet, :core, module)
end
@doc """
Terminates a client or server transaction forcefully.
This function is not generally executed by entities; there is a single case
where it is fundamental, which is when a client transaction is in proceeding
state for a long time, and the transaction has to be finished forcibly, or it
will never finish by itself.
If a transaction with such a key does not exist, it will be silently ignored.
"""
@spec terminate(sippet, client_key | server_key) :: :ok
def terminate(sippet, key) do
case Registry.lookup(sippet, {:transaction, key}) do
[] ->
:ok
[{pid, _}] ->
# Send the response through the existing server key.
case key do
%Transactions.Client.Key{} ->
Transactions.Client.terminate(pid)
%Transactions.Server.Key{} ->
Transactions.Server.terminate(pid)
end
end
end
@doc false
def start_link(options) when is_list(options) do
case Keyword.fetch(options, :name) do
{:ok, name} when is_atom(name) ->
:ok
{:ok, other} ->
raise ArgumentError, "expected :name to be an atom, got: #{inspect(other)}"
:error ->
raise ArgumentError, "expected :name option to be present"
end
Supervisor.start_link(__MODULE__, options)
end
@doc false
def child_spec(options) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [options]}
}
end
@impl true
def init(options) do
children = [
{Registry, name: options[:name], keys: :unique, partitions: System.schedulers_online()},
{DynamicSupervisor, strategy: :one_for_one, name: supervisor_name(options[:name])}
]
Supervisor.init(children, strategy: :one_for_one)
end
@doc false
def supervisor_name(name) do
Module.concat(name, "Supervisor")
end
end