defmodule ExICE.Candidate do
@moduledoc """
ICE candidate representation.
"""
@type type() :: :host | :srflx | :prflx | :relay
@type t() :: %__MODULE__{
address: :inet.ip_address(),
base_address: :inet.ip_address() | nil,
base_port: :inet.port_number() | nil,
foundation: integer(),
port: :inet.port_number(),
priority: integer(),
transport: :udp,
socket: :inet.socket() | nil,
type: type()
}
@derive {Inspect, except: [:socket]}
defstruct [
:address,
:base_address,
:base_port,
:foundation,
:port,
:priority,
:transport,
:socket,
:type
]
@spec new(
type(),
:inet.ip_address(),
:inet.port_number(),
:inet.ip_address() | nil,
:inet.port_number() | nil,
:inet.socket() | nil,
priority: integer()
) :: t()
def new(type, address, port, base_address, base_port, socket, opts \\ [])
when type in [:host, :srflx, :prflx, :relay] do
transport = :udp
priority = opts[:priority] || priority(type)
%__MODULE__{
address: address,
base_address: base_address,
base_port: base_port,
foundation: foundation(type, address, nil, transport),
port: port,
priority: priority,
transport: transport,
socket: socket,
type: type
}
end
@spec marshal(t()) :: String.t()
def marshal(cand) do
component_id = 1
%__MODULE__{
foundation: foundation,
transport: transport,
priority: priority,
address: address,
port: port,
type: type
} = cand
transport = transport_to_string(transport)
address = address_to_string(address)
"#{foundation} #{component_id} #{transport} #{priority} #{address} #{port} typ #{type}"
end
@spec unmarshal(String.t()) :: {:ok, t()} | {:error, term()}
def unmarshal(string) do
with [f_str, c_str, tr_str, pr_str, a_str, po_str, "typ", ty_str] <-
String.split(string, " ", parts: 8),
{foundation, ""} <- Integer.parse(f_str),
{_component_id, ""} <- Integer.parse(c_str),
{:ok, transport} <- parse_transport(String.downcase(tr_str)),
{priority, ""} <- Integer.parse(pr_str),
{:ok, address} <- :inet.parse_address(String.to_charlist(a_str)),
{port, ""} <- Integer.parse(po_str),
{:ok, type} <- parse_type(ty_str) do
{:ok,
%__MODULE__{
address: address,
foundation: foundation,
port: port,
priority: priority,
transport: transport,
type: type
}}
else
err when is_list(err) -> {:error, :invalid_candidate}
err -> err
end
end
@spec family(t()) :: :ipv4 | :ipv6
def family(%__MODULE__{address: {_, _, _, _}}), do: :ipv4
def family(%__MODULE__{address: {_, _, _, _, _, _, _, _}}), do: :ipv6
@spec priority(type()) :: integer()
def priority(type) do
type_preference =
case type do
:host -> 126
:prflx -> 110
:srflx -> 100
:relay -> 0
end
# That's not fully correct as according to RFC 8445 sec. 5.1.2.1 we should:
# * use value of 65535 when there is only one IP address
# * use different values when there are multiple IP addresses
local_preference = 65_535
2 ** 24 * type_preference + 2 ** 8 * local_preference + 2 ** 0 * (256 - 1)
end
defp parse_transport("udp"), do: {:ok, :udp}
defp parse_transport(_other), do: {:error, :invalid_transport}
defp parse_type("host" <> _rest), do: {:ok, :host}
defp parse_type("srflx" <> _rest), do: {:ok, :srflx}
defp parse_type("prflx" <> _rest), do: {:ok, :prflx}
defp parse_type("relay" <> _rest), do: {:ok, :relay}
defp parse_type(_other), do: {:error, :invalid_type}
defp foundation(type, ip, stun_turn_ip, transport) do
{type, ip, stun_turn_ip, transport}
|> then(&inspect(&1))
|> then(&:erlang.crc32(&1))
end
defp address_to_string(address), do: :inet.ntoa(address)
defp transport_to_string(:udp), do: "UDP"
end