Skip to content

Commit f428324

Browse files
authored
Accept non-numeric ICE candidate foundations per RFC 8445 (#97)
RFC 8839 Section 5.1 defines foundation as 1*32ice-char where ice-char = ALPHA / DIGIT / "+" / "/". Implementations like aiortc generate hex string foundations which caused a crash in unmarshal/1. - Add parse_foundation/1 that accepts both integer and string foundations - Preserve parsed foundation from remote candidates instead of recomputing - Fix else clause in unmarshal/1 to always return {:error, _} tuples Closes #96
1 parent 714bc5b commit f428324

File tree

4 files changed

+94
-8
lines changed

4 files changed

+94
-8
lines changed

lib/ex_ice/candidate.ex

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ defmodule ExICE.Candidate do
1212
address: :inet.ip_address() | String.t(),
1313
base_address: :inet.ip_address() | nil,
1414
base_port: :inet.port_number() | nil,
15-
foundation: integer(),
15+
foundation: String.t(),
1616
port: :inet.port_number(),
1717
priority: integer(),
1818
transport: :udp | :tcp,
@@ -80,7 +80,7 @@ defmodule ExICE.Candidate do
8080
def unmarshal(string) do
8181
with [f_str, c_str, tr_str, pr_str, a_str, po_str, "typ", ty_str | rest] <-
8282
String.split(string, " "),
83-
{foundation, ""} <- Integer.parse(f_str),
83+
{:ok, foundation} <- parse_foundation(f_str),
8484
{_component_id, ""} <- Integer.parse(c_str),
8585
{:ok, transport} <- parse_transport(String.downcase(tr_str)),
8686
{priority, ""} <- Integer.parse(pr_str),
@@ -99,7 +99,8 @@ defmodule ExICE.Candidate do
9999
{:ok, new(type, config ++ extra_config)}
100100
else
101101
err when is_list(err) -> {:error, :invalid_candidate}
102-
err -> err
102+
{:error, _} = err -> err
103+
_other -> {:error, :invalid_candidate}
103104
end
104105
end
105106

@@ -123,7 +124,10 @@ defmodule ExICE.Candidate do
123124
address: address,
124125
base_address: config[:base_address],
125126
base_port: config[:base_port],
126-
foundation: ExICE.Priv.Candidate.foundation(type, address, nil, transport),
127+
foundation:
128+
Keyword.get_lazy(config, :foundation, fn ->
129+
ExICE.Priv.Candidate.foundation(type, address, nil, transport)
130+
end),
127131
port: Keyword.fetch!(config, :port),
128132
priority: Keyword.fetch!(config, :priority),
129133
transport: transport,
@@ -141,6 +145,16 @@ defmodule ExICE.Candidate do
141145
defp tcp_type_to_string(nil), do: ""
142146
defp tcp_type_to_string(type), do: "tcptype #{type}"
143147

148+
# RFC 8839 Section 5.1: foundation = 1*32ice-char, ice-char = ALPHA / DIGIT / "+" / "/"
149+
@foundation_pattern ~r/^[a-zA-Z0-9+\/]+$/
150+
defp parse_foundation(str) when byte_size(str) in 1..32 do
151+
if Regex.match?(@foundation_pattern, str),
152+
do: {:ok, str},
153+
else: {:error, :invalid_foundation}
154+
end
155+
156+
defp parse_foundation(_str), do: {:error, :invalid_foundation}
157+
144158
defp parse_transport("udp"), do: {:ok, :udp}
145159
defp parse_transport("tcp"), do: {:ok, :tcp}
146160
defp parse_transport(_other), do: {:error, :invalid_transport}

lib/ex_ice/priv/candidate.ex

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ defmodule ExICE.Priv.Candidate do
1313
base_port: :inet.port_number(),
1414
socket: :inet.socket(),
1515
priority: integer(),
16-
foundation: integer(),
16+
foundation: String.t(),
1717
transport: :udp | :tcp,
1818
tcp_type: tcp_type()
1919
]
@@ -128,8 +128,8 @@ defmodule ExICE.Priv.Candidate do
128128
end
129129

130130
@spec foundation(type(), :inet.ip_address() | String.t(), :inet.ip_address() | nil, atom()) ::
131-
integer()
131+
String.t()
132132
def foundation(type, ip, stun_turn_ip, transport) do
133-
{type, ip, stun_turn_ip, transport} |> inspect() |> :erlang.crc32()
133+
{type, ip, stun_turn_ip, transport} |> inspect() |> :erlang.crc32() |> Integer.to_string()
134134
end
135135
end

lib/ex_ice/priv/candidate_base.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule ExICE.Priv.CandidateBase do
77
address: :inet.ip_address() | String.t(),
88
base_address: :inet.ip_address() | nil,
99
base_port: :inet.port_number() | nil,
10-
foundation: integer(),
10+
foundation: String.t(),
1111
port: :inet.port_number(),
1212
priority: integer(),
1313
transport: :udp | :tcp,

test/candidate_test.exs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,76 @@ defmodule ExICE.CandidateTest do
8383
c = %Candidate{c | id: expected_c.id}
8484
assert c == expected_c
8585
end
86+
87+
test "unmarshal/1 with string foundation" do
88+
m_c = "834be7808a5c955b681935b0c6ad99df 1 UDP 2130706431 192.168.1.74 55474 typ host"
89+
90+
assert {:ok, c} = Candidate.unmarshal(m_c)
91+
assert c.foundation == "834be7808a5c955b681935b0c6ad99df"
92+
assert c.address == {192, 168, 1, 74}
93+
assert c.port == 55_474
94+
assert c.priority == 2_130_706_431
95+
assert c.type == :host
96+
assert c.transport == :udp
97+
end
98+
99+
test "unmarshal/1 preserves numeric foundation as string" do
100+
m_c = "936255739 1 UDP 1234 192.168.1.1 12345 typ host"
101+
102+
assert {:ok, c} = Candidate.unmarshal(m_c)
103+
assert c.foundation == "936255739"
104+
assert is_binary(c.foundation)
105+
end
106+
107+
test "unmarshal/1 rejects candidate with leading space (empty foundation)" do
108+
m_c = " 1 UDP 1234 192.168.1.1 12345 typ host"
109+
assert {:error, :invalid_foundation} = Candidate.unmarshal(m_c)
110+
end
111+
112+
test "unmarshal/1 accepts foundation of exactly 32 characters" do
113+
foundation = String.duplicate("a", 32)
114+
m_c = "#{foundation} 1 UDP 1234 192.168.1.1 12345 typ host"
115+
assert {:ok, c} = Candidate.unmarshal(m_c)
116+
assert c.foundation == foundation
117+
end
118+
119+
test "unmarshal/1 accepts single character foundation" do
120+
m_c = "a 1 UDP 1234 192.168.1.1 12345 typ host"
121+
assert {:ok, c} = Candidate.unmarshal(m_c)
122+
assert c.foundation == "a"
123+
end
124+
125+
test "unmarshal/1 rejects foundation longer than 32 characters" do
126+
long_foundation = String.duplicate("a", 33)
127+
m_c = "#{long_foundation} 1 UDP 1234 192.168.1.1 12345 typ host"
128+
assert {:error, :invalid_foundation} = Candidate.unmarshal(m_c)
129+
end
130+
131+
test "unmarshal/1 rejects foundation with invalid characters" do
132+
m_c = "abc!@#def 1 UDP 1234 192.168.1.1 12345 typ host"
133+
assert {:error, :invalid_foundation} = Candidate.unmarshal(m_c)
134+
end
135+
136+
test "unmarshal/1 preserves foundation with leading zeros" do
137+
m_c = "001 1 UDP 1234 192.168.1.1 12345 typ host"
138+
139+
assert {:ok, c} = Candidate.unmarshal(m_c)
140+
assert c.foundation == "001"
141+
assert Candidate.marshal(c) == m_c
142+
end
143+
144+
test "unmarshal/1 preserves foundation with plus and slash" do
145+
m_c = "abc+/def 1 UDP 1234 192.168.1.1 12345 typ host"
146+
147+
assert {:ok, c} = Candidate.unmarshal(m_c)
148+
assert c.foundation == "abc+/def"
149+
assert Candidate.marshal(c) == m_c
150+
end
151+
152+
test "marshal/1 roundtrips with string foundation" do
153+
m_c = "834be7808a5c955b681935b0c6ad99df 1 UDP 2130706431 192.168.1.74 55474 typ host"
154+
155+
assert {:ok, c} = Candidate.unmarshal(m_c)
156+
assert Candidate.marshal(c) == m_c
157+
end
86158
end

0 commit comments

Comments
 (0)