Skip to content

Commit

Permalink
Move URI module from ex_ice
Browse files Browse the repository at this point in the history
  • Loading branch information
mickel8 committed Mar 21, 2024
1 parent 422b46a commit c136111
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 58 deletions.
37 changes: 0 additions & 37 deletions lib/ex_stun/client.ex

This file was deleted.

162 changes: 162 additions & 0 deletions lib/ex_stun/uri.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
defmodule ExSTUN.URI do
@moduledoc """
Module representing STUN/TURN URI.
Implementation of RFC 7064 and RFC 7065.
We could try to use URI module from Elixir
but RFC 7064 and RFC 7065 state:
While these two ABNF productions are defined in [RFC3986]
as components of the generic hierarchical URI, this does
not imply that the "stun" and "stuns" URI schemes are
hierarchical URIs. Developers MUST NOT use a generic
hierarchical URI parser to parse a "stun" or "stuns" URI.
"""

@type scheme :: :stun | :stuns | :turn | :turns

@type transport :: :udp | :tcp

@typedoc """
Type describing URI struct.
`transport` denotes protocol that should be used
by a client to connect to the STUN/TURN server.
`nil` means that the client should try to connect with every protocol it supports.
If scheme indicates secure connection, transport is always set to `:tcp`.
This is based on [RFC 5928, sec. 3](https://datatracker.ietf.org/doc/html/rfc5928#section-3).
"""
@type t() :: %__MODULE__{
scheme: scheme(),
host: String.t(),
port: :inet.port_number(),
transport: transport() | nil
}

@enforce_keys [:scheme, :host, :port]
defstruct @enforce_keys ++ [:transport]

@default_udp_tcp_port 3478
@default_tls_port 5349

@doc """
The same as parse/1 but raises on error.
"""
@spec parse!(String.t()) :: t()
def parse!(uri) do
case parse(uri) do
{:ok, uri} -> uri
:error -> raise "Invalid URI"
end
end

@doc """
Parses URI string into `t:t/0`.
"""
@spec parse(String.t()) :: {:ok, t()} | :error
def parse("stun" <> ":" <> host_port) do
do_parse_stun(:stun, host_port)
end

def parse("stuns" <> ":" <> host_port) do
do_parse_stun(:stuns, host_port)
end

def parse("turn" <> ":" <> host_port_transport) do
do_parse_turn(:turn, host_port_transport)
end

def parse("turns" <> ":" <> host_port_transport) do
do_parse_turn(:turns, host_port_transport)
end

def parse(_other), do: :error

defp do_parse_stun(scheme, host_port) do
default_port = if scheme == :stun, do: @default_udp_tcp_port, else: @default_tls_port
default_transport = if scheme == :stuns, do: :tcp

with {:ok, host, rest} <- parse_host(host_port),
{:ok, port} <- parse_stun_port(rest) do
{:ok,
%__MODULE__{
scheme: scheme,
host: host,
port: port || default_port,
transport: default_transport
}}
else
_ -> :error
end
end

defp do_parse_turn(scheme, host_port_transport) do
default_port = if scheme == :turn, do: @default_udp_tcp_port, else: @default_tls_port
default_transport = if scheme == :turns, do: :tcp

with {:ok, host, rest} <- parse_host(host_port_transport),
{:ok, port, rest} <- parse_turn_port(rest),
{:ok, transport} <- parse_transport(rest) do
{:ok,
%__MODULE__{
scheme: scheme,
host: host,
port: port || default_port,
transport: transport || default_transport
}}
end
end

defp parse_host(data) do
case String.split(data, ":", parts: 2) do
[host, rest] when host != "" ->
{:ok, host, rest}

[host] when host != "" ->
case String.split(host, "?transport=") do
[host, rest] when host != "" -> {:ok, host, "?transport=" <> rest}
[host] -> {:ok, host, ""}
_ -> :error
end

_ ->
:error
end
end

defp parse_stun_port(""), do: {:ok, nil}
defp parse_stun_port(port), do: do_parse_port(port)

defp parse_turn_port(""), do: {:ok, nil, ""}
defp parse_turn_port("?transport=" <> rest), do: {:ok, nil, rest}

defp parse_turn_port(data) do
case String.split(data, "?transport=", parts: 2) do
[port, rest] ->
case do_parse_port(port) do
{:ok, port} -> {:ok, port, rest}
:error -> :error
end

[port] ->
case do_parse_port(port) do
{:ok, port} -> {:ok, port, ""}
:error -> :error
end
end
end

defp do_parse_port(port) do
case Integer.parse(port) do
{port, ""} -> {:ok, port}
_ -> :error
end
end

defp parse_transport(""), do: {:ok, nil}
defp parse_transport("udp"), do: {:ok, :udp}
defp parse_transport("tcp"), do: {:ok, :tcp}
defp parse_transport(_), do: :error
end
15 changes: 0 additions & 15 deletions test/ex_stun/client_test.exs

This file was deleted.

84 changes: 84 additions & 0 deletions test/ex_stun/uri_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
defmodule ExSTUN.URITest do
use ExUnit.Case, async: true

alias ExSTUN.URI

describe "parse/1" do
test "with valid URI" do
for {uri_string, expected_uri} <- [
{
"stun:stun.l.google.com:19302",
%URI{scheme: :stun, host: "stun.l.google.com", port: 19_302, transport: nil}
},
{
"stuns:stun.l.google.com:19302",
%URI{scheme: :stuns, host: "stun.l.google.com", port: 19_302, transport: :tcp}
},
{
"stun:stun.l.google.com",
%URI{scheme: :stun, host: "stun.l.google.com", port: 3478, transport: nil}
},
{
"stuns:stun.l.google.com",
%URI{scheme: :stuns, host: "stun.l.google.com", port: 5349, transport: :tcp}
},
{
"turn:example.org",
%URI{scheme: :turn, host: "example.org", port: 3478, transport: nil}
},
{
"turns:example.org",
%URI{scheme: :turns, host: "example.org", port: 5349, transport: :tcp}
},
{
"turn:example.org:8000",
%URI{scheme: :turn, host: "example.org", port: 8000, transport: nil}
},
{
"turn:example.org?transport=udp",
%URI{scheme: :turn, host: "example.org", port: 3478, transport: :udp}
},
{
"turn:example.org:1234?transport=udp",
%URI{scheme: :turn, host: "example.org", port: 1234, transport: :udp}
},
{
"turn:example.org?transport=tcp",
%URI{scheme: :turn, host: "example.org", port: 3478, transport: :tcp}
},
{
"turns:example.org?transport=tcp",
%URI{scheme: :turns, host: "example.org", port: 5349, transport: :tcp}
}
] do
assert {:ok, expected_uri} == URI.parse(uri_string)
end
end

test "with invalid URI" do
for invalid_uri_string <- [
"",
"some random string",
"stun:",
"stun::",
"stun::19302",
"stun:?transport=",
"abcd:stun.l.google.com:19302",
"stun:stun.l.google.com:ab123",
"stuns:stun.l.google.com:ab123",
"stun:stun.l.google.com:19302?transport=udp",
"stun:stun.l.google.com:19302?transport=",
"turn:example.com:abc?transport=tcp",
"turn:example.com:12345?transport=tls",
"turn:example.com:abc"
] do
assert :error == URI.parse(invalid_uri_string)
end
end
end

test "parse!/1" do
assert %URI{} = URI.parse!("stun:stun.l.google.com")
assert_raise RuntimeError, fn -> URI.parse!("invalid uri") end
end
end
22 changes: 22 additions & 0 deletions test/integration_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
defmodule ExSTUN.IntegrationTest do
use ExUnit.Case, async: true

alias ExSTUN.Message
alias ExSTUN.Message.Type
alias ExSTUN.Message.Attribute.XORMappedAddress

test "binding request/response" do
{:ok, socket} = :gen_udp.open(0, [{:active, false}, :binary])

req =
%Type{class: :request, method: :binding}
|> Message.new()
|> Message.encode()

:ok = :gen_udp.send(socket, ~c"stun.l.google.com", 19_302, req)
{:ok, {_, _, resp}} = :gen_udp.recv(socket, 0)

{:ok, %Message{} = msg} = Message.decode(resp)
assert {:ok, %XORMappedAddress{}} = Message.get_attribute(msg, XORMappedAddress)
end
end
6 changes: 0 additions & 6 deletions test/realm_test.exs

This file was deleted.

0 comments on commit c136111

Please sign in to comment.