Skip to content

Commit b84a392

Browse files
committed
Move URI module from ex_ice
1 parent 422b46a commit b84a392

File tree

6 files changed

+262
-58
lines changed

6 files changed

+262
-58
lines changed

lib/ex_stun/client.ex

Lines changed: 0 additions & 37 deletions
This file was deleted.

lib/ex_stun/uri.ex

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
defmodule ExSTUN.URI do
2+
@moduledoc """
3+
Module representing STUN/TURN URI.
4+
5+
Implementation of RFC 7064 and RFC 7065.
6+
7+
We could try to use URI module from Elixir
8+
but RFC 7064 and RFC 7065 state:
9+
10+
While these two ABNF productions are defined in [RFC3986]
11+
as components of the generic hierarchical URI, this does
12+
not imply that the "stun" and "stuns" URI schemes are
13+
hierarchical URIs. Developers MUST NOT use a generic
14+
hierarchical URI parser to parse a "stun" or "stuns" URI.
15+
16+
"""
17+
18+
@type scheme :: :stun | :stuns | :turn | :turns
19+
20+
@type transport :: :udp | :tcp
21+
22+
@typedoc """
23+
Type describing URI struct.
24+
25+
`transport` denotes protocol that should be used
26+
by a client to connect to the STUN/TURN server.
27+
`nil` means that the client should try to connect with every protocol it supports.
28+
If scheme indicates secure connection, transport is always set to `:tcp`.
29+
This is based on [RFC 5928, sec. 3](https://datatracker.ietf.org/doc/html/rfc5928#section-3).
30+
"""
31+
@type t() :: %__MODULE__{
32+
scheme: scheme(),
33+
host: String.t(),
34+
port: :inet.port_number(),
35+
transport: transport() | nil
36+
}
37+
38+
@enforce_keys [:scheme, :host, :port]
39+
defstruct @enforce_keys ++ [:transport]
40+
41+
@default_udp_tcp_port 3478
42+
@default_tls_port 5349
43+
44+
@doc """
45+
The same as parse/1 but raises on error.
46+
"""
47+
@spec parse!(String.t()) :: t()
48+
def parse!(uri) do
49+
case parse(uri) do
50+
{:ok, uri} -> uri
51+
:error -> raise "Invalid URI"
52+
end
53+
end
54+
55+
@doc """
56+
Parses URI string into `t:t/0`.
57+
"""
58+
@spec parse(String.t()) :: {:ok, t()} | :error
59+
def parse("stun" <> ":" <> host_port) do
60+
do_parse_stun(:stun, host_port)
61+
end
62+
63+
def parse("stuns" <> ":" <> host_port) do
64+
do_parse_stun(:stuns, host_port)
65+
end
66+
67+
def parse("turn" <> ":" <> host_port_transport) do
68+
do_parse_turn(:turn, host_port_transport)
69+
end
70+
71+
def parse("turns" <> ":" <> host_port_transport) do
72+
do_parse_turn(:turns, host_port_transport)
73+
end
74+
75+
def parse(_other), do: :error
76+
77+
defp do_parse_stun(scheme, host_port) do
78+
default_port = if scheme == :stun, do: @default_udp_tcp_port, else: @default_tls_port
79+
default_transport = if scheme == :stuns, do: :tcp
80+
81+
with {:ok, host, rest} <- parse_host(host_port),
82+
{:ok, port} <- parse_stun_port(rest) do
83+
{:ok,
84+
%__MODULE__{
85+
scheme: scheme,
86+
host: host,
87+
port: port || default_port,
88+
transport: default_transport
89+
}}
90+
else
91+
_ -> :error
92+
end
93+
end
94+
95+
defp do_parse_turn(scheme, host_port_transport) do
96+
default_port = if scheme == :turn, do: @default_udp_tcp_port, else: @default_tls_port
97+
default_transport = if scheme == :turns, do: :tcp
98+
99+
with {:ok, host, rest} <- parse_host(host_port_transport),
100+
{:ok, port, rest} <- parse_turn_port(rest),
101+
{:ok, transport} <- parse_transport(rest) do
102+
{:ok,
103+
%__MODULE__{
104+
scheme: scheme,
105+
host: host,
106+
port: port || default_port,
107+
transport: transport || default_transport
108+
}}
109+
end
110+
end
111+
112+
defp parse_host(data) do
113+
case String.split(data, ":", parts: 2) do
114+
[host, rest] when host != "" ->
115+
{:ok, host, rest}
116+
117+
[host] when host != "" ->
118+
case String.split(host, "?transport=") do
119+
[host, rest] when host != "" -> {:ok, host, "?transport=" <> rest}
120+
[host] -> {:ok, host, ""}
121+
_ -> :error
122+
end
123+
124+
_ ->
125+
:error
126+
end
127+
end
128+
129+
defp parse_stun_port(""), do: {:ok, nil}
130+
defp parse_stun_port(port), do: do_parse_port(port)
131+
132+
defp parse_turn_port(""), do: {:ok, nil, ""}
133+
defp parse_turn_port("?transport=" <> rest), do: {:ok, nil, rest}
134+
135+
defp parse_turn_port(data) do
136+
case String.split(data, "?transport=", parts: 2) do
137+
[port, rest] ->
138+
case do_parse_port(port) do
139+
{:ok, port} -> {:ok, port, rest}
140+
:error -> :error
141+
end
142+
143+
[port] ->
144+
case do_parse_port(port) do
145+
{:ok, port} -> {:ok, port, ""}
146+
:error -> :error
147+
end
148+
149+
_ ->
150+
:error
151+
end
152+
end
153+
154+
defp do_parse_port(port) do
155+
case Integer.parse(port) do
156+
{port, ""} -> {:ok, port}
157+
_ -> :error
158+
end
159+
end
160+
161+
defp parse_transport(""), do: {:ok, nil}
162+
defp parse_transport("udp"), do: {:ok, :udp}
163+
defp parse_transport("tcp"), do: {:ok, :tcp}
164+
defp parse_transport(_), do: :error
165+
end

test/ex_stun/client_test.exs

Lines changed: 0 additions & 15 deletions
This file was deleted.

test/ex_stun/uri_test.exs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
defmodule ExSTUN.URITest do
2+
use ExUnit.Case, async: true
3+
4+
alias ExSTUN.URI
5+
6+
describe "parse/1" do
7+
test "with valid URI" do
8+
for {uri_string, expected_uri} <- [
9+
{
10+
"stun:stun.l.google.com:19302",
11+
%URI{scheme: :stun, host: "stun.l.google.com", port: 19_302, transport: nil}
12+
},
13+
{
14+
"stuns:stun.l.google.com:19302",
15+
%URI{scheme: :stuns, host: "stun.l.google.com", port: 19_302, transport: :tcp}
16+
},
17+
{
18+
"stun:stun.l.google.com",
19+
%URI{scheme: :stun, host: "stun.l.google.com", port: 3478, transport: nil}
20+
},
21+
{
22+
"stuns:stun.l.google.com",
23+
%URI{scheme: :stuns, host: "stun.l.google.com", port: 5349, transport: :tcp}
24+
},
25+
{
26+
"turn:example.org",
27+
%URI{scheme: :turn, host: "example.org", port: 3478, transport: nil}
28+
},
29+
{
30+
"turns:example.org",
31+
%URI{scheme: :turns, host: "example.org", port: 5349, transport: :tcp}
32+
},
33+
{
34+
"turn:example.org:8000",
35+
%URI{scheme: :turn, host: "example.org", port: 8000, transport: nil}
36+
},
37+
{
38+
"turn:example.org?transport=udp",
39+
%URI{scheme: :turn, host: "example.org", port: 3478, transport: :udp}
40+
},
41+
{
42+
"turn:example.org:1234?transport=udp",
43+
%URI{scheme: :turn, host: "example.org", port: 1234, transport: :udp}
44+
},
45+
{
46+
"turn:example.org?transport=tcp",
47+
%URI{scheme: :turn, host: "example.org", port: 3478, transport: :tcp}
48+
},
49+
{
50+
"turns:example.org?transport=tcp",
51+
%URI{scheme: :turns, host: "example.org", port: 5349, transport: :tcp}
52+
}
53+
] do
54+
assert {:ok, expected_uri} == URI.parse(uri_string)
55+
end
56+
end
57+
58+
test "with invalid URI" do
59+
for invalid_uri_string <- [
60+
"",
61+
"some random string",
62+
"stun:",
63+
"stun::",
64+
"stun::19302",
65+
"abcd:stun.l.google.com:19302",
66+
"stun:stun.l.google.com:ab123",
67+
"stuns:stun.l.google.com:ab123",
68+
"stun:stun.l.google.com:19302?transport=udp",
69+
"stun:stun.l.google.com:19302?transport="
70+
] do
71+
assert :error == URI.parse(invalid_uri_string)
72+
end
73+
end
74+
end
75+
end

test/integration_test.exs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
defmodule ExSTUN.IntegrationTest do
2+
use ExUnit.Case, async: true
3+
4+
alias ExSTUN.Message
5+
alias ExSTUN.Message.Type
6+
alias ExSTUN.Message.Attribute.XORMappedAddress
7+
8+
test "binding request/response" do
9+
{:ok, socket} = :gen_udp.open(0, [{:active, false}, :binary])
10+
11+
req =
12+
%Type{class: :request, method: :binding}
13+
|> Message.new()
14+
|> Message.encode()
15+
16+
:ok = :gen_udp.send(socket, ~c"stun.l.google.com", 19302, req)
17+
{:ok, {_, _, resp}} = :gen_udp.recv(socket, 0)
18+
19+
{:ok, %Message{} = msg} = Message.decode(resp)
20+
assert {:ok, %XORMappedAddress{}} = Message.get_attribute(msg, XORMappedAddress)
21+
end
22+
end

test/realm_test.exs

Lines changed: 0 additions & 6 deletions
This file was deleted.

0 commit comments

Comments
 (0)