Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .tool-versions

This file was deleted.

8 changes: 5 additions & 3 deletions example/peer.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Mix.install([{:gun, "~> 2.0.1"}, {:ex_ice, path: "../", force: true}, {:jason, "~> 1.4.0"}])

require Logger
Logger.configure(level: :info)
Logger.configure(level: :debug)

defmodule Peer do
use GenServer
Expand Down Expand Up @@ -95,12 +95,14 @@ defmodule Peer do
role = String.to_atom(role)

{:ok, pid} =
ICEAgent.start_link(role,
ICEAgent.start_link(
role: role,
ip_filter: fn
{_, _, _, _} -> true
{_, _, _, _, _, _, _, _} -> false
end,
ice_servers: [%{urls: "stun:stun.l.google.com:19302"}]
ice_servers: [%{urls: "stun:stun.nextcloud.com"}],
transport: :tcp
)

{:ok, ufrag, passwd} = ICEAgent.get_local_credentials(pid)
Expand Down
98 changes: 73 additions & 25 deletions lib/ex_ice/candidate.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@
ICE candidate representation.
"""

@type type() :: :host | :srflx | :prflx | :relay
@type type :: :host | :srflx | :prflx | :relay
@type tcp_type :: :active | :passive | :so

@type t() :: %__MODULE__{
@type t :: %__MODULE__{
id: integer(),
type: type(),
address: :inet.ip_address() | String.t(),
Expand All @@ -14,7 +15,8 @@
foundation: integer(),
port: :inet.port_number(),
priority: integer(),
transport: :udp | :tcp
transport: :udp | :tcp,
tcp_type: tcp_type() | nil
}

@enforce_keys [
Expand All @@ -24,7 +26,8 @@
:port,
:foundation,
:priority,
:transport
:transport,
:tcp_type
]
defstruct @enforce_keys ++ [:base_address, :base_port]

Expand All @@ -38,7 +41,8 @@
priority: priority,
address: address,
port: port,
type: type
type: type,
tcp_type: tcp_type
} = cand

# This is based on RFC 8839 sec. 5.1.
Expand All @@ -54,31 +58,45 @@

transport = transport_to_string(transport)
address = address_to_string(address)

"#{foundation} #{component_id} #{transport} #{priority} #{address} #{port} typ #{type} #{related_addr}"
|> String.trim()
tcp_type = tcp_type_to_string(tcp_type)

[
foundation,
component_id,
transport,
priority,
address,
port,
"typ",
type,
related_addr,
tcp_type
]
|> Enum.reject(&(&1 == ""))
|> Enum.join(" ")
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),
with [f_str, c_str, tr_str, pr_str, a_str, po_str, "typ", ty_str | rest] <-
String.split(string, " "),
{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} <- parse_address(a_str),
{port, ""} <- Integer.parse(po_str),
{:ok, type} <- parse_type(ty_str) do
{:ok,
new(
type,
address: address,
port: port,
priority: priority,
foundation: foundation,
transport: transport
)}
{:ok, type} <- parse_type(ty_str),
{:ok, extra_config} <- parse_optional_attributes(rest) do
config = [
address: address,
port: port,
priority: priority,
foundation: foundation,
transport: transport
]

{:ok, new(type, config ++ extra_config)}
else
err when is_list(err) -> {:error, :invalid_candidate}
err -> err
Expand All @@ -89,12 +107,16 @@
def family(%__MODULE__{address: {_, _, _, _}}), do: :ipv4
def family(%__MODULE__{address: {_, _, _, _, _, _, _, _}}), do: :ipv6

def tcp_type(%__MODULE__{tcp_type: tt}), do: tt

Check warning on line 110 in lib/ex_ice/candidate.ex

View workflow job for this annotation

GitHub Actions / lint OTP 26 / Elixir 1.15

Functions should have a @SPEC type specification.

@doc false
@spec new(type(), Keyword.t()) :: t()
def new(type, config) when type in [:host, :srflx, :prflx, :relay] do
transport = Keyword.get(config, :transport, :udp)
address = Keyword.fetch!(config, :address)

tcp_type = if transport == :tcp, do: Keyword.fetch!(config, :tcp_type)

%__MODULE__{
id: ExICE.Priv.Utils.id(),
address: address,
Expand All @@ -104,16 +126,22 @@
port: Keyword.fetch!(config, :port),
priority: Keyword.fetch!(config, :priority),
transport: transport,
type: type
type: type,
tcp_type: tcp_type
}
end

defp address_to_string(address) when is_binary(address), do: address
defp address_to_string(address), do: :inet.ntoa(address)

defp transport_to_string(:udp), do: "UDP"
defp transport_to_string(:tcp), do: "TCP"

defp tcp_type_to_string(nil), do: ""
defp tcp_type_to_string(type), do: "tcptype #{type}"

defp parse_transport("udp"), do: {:ok, :udp}
defp parse_transport("tcp"), do: {:ok, :tcp}
defp parse_transport(_other), do: {:error, :invalid_transport}

defp parse_address(address) do
Expand All @@ -124,9 +152,29 @@
end
end

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("host"), do: {:ok, :host}
defp parse_type("srflx"), do: {:ok, :srflx}
defp parse_type("prflx"), do: {:ok, :prflx}
defp parse_type("relay"), do: {:ok, :relay}
defp parse_type(_other), do: {:error, :invalid_type}

defp parse_optional_attributes(list, config \\ [])
defp parse_optional_attributes([], config), do: {:ok, config}

defp parse_optional_attributes(["raddr", _2, _3, _4 | rest], config),
do: parse_optional_attributes(rest, config)

defp parse_optional_attributes(["tcptype", tcp_type | rest], config) do
case parse_tcp_type(tcp_type) do
{:ok, tcp_type} -> parse_optional_attributes(rest, config ++ [tcp_type: tcp_type])
err -> err
end
end

defp parse_optional_attributes(_other, config), do: {:ok, config}

defp parse_tcp_type("active"), do: {:ok, :active}
defp parse_tcp_type("passive"), do: {:ok, :passive}
defp parse_tcp_type("so"), do: {:ok, :so}
defp parse_tcp_type(_other), do: {:error, :invalid_tcp_type}
end
20 changes: 19 additions & 1 deletion lib/ex_ice/ice_agent.ex
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,8 @@ defmodule ExICE.ICEAgent do
on_connection_state_change: pid() | nil,
on_data: pid() | nil,
on_new_candidate: pid() | nil,
host_to_srflx_ip_mapper: host_to_srflx_ip_mapper() | nil
host_to_srflx_ip_mapper: host_to_srflx_ip_mapper() | nil,
transport: :udp | :tcp
]

@doc """
Expand Down Expand Up @@ -321,6 +322,15 @@ defmodule ExICE.ICEAgent do

@impl true
def init(opts) do
# TODO: this is ugly, and will not allow us to run more than two TCP ICE agents at the same time
opts =
if opts[:transport] == :tcp do
{:ok, _pid} = ExICE.Priv.Transport.TCP.Client.start_link()
opts ++ [transport_module: ExICE.Priv.Transport.TCP.Client]
else
opts
end

ice_agent = ExICE.Priv.ICEAgent.new(opts)
{:ok, %{ice_agent: ice_agent, pending_eoc: false, pending_remote_cands: MapSet.new()}}
end
Expand Down Expand Up @@ -478,6 +488,14 @@ defmodule ExICE.ICEAgent do
{:noreply, %{state | ice_agent: ice_agent}}
end

@impl true
def handle_info({:tcp, _socket, _packet}, state) do
# TODO: consider receiving TCP data in the ICE Agent process
# ice_agent = ExICE.Priv.ICEAgent.handle_tcp(state.ice_agent, socket, packet)
# {:noreply, %{state | ice_agent: ice_agent}}
{:noreply, state}
end

@impl true
def handle_info({:ex_turn, ref, msg}, state) do
ice_agent = ExICE.Priv.ICEAgent.handle_ex_turn_msg(state.ice_agent, ref, msg)
Expand Down
Loading
Loading