|
| 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 | + @type t() :: %__MODULE__{ |
| 23 | + scheme: scheme(), |
| 24 | + host: String.t(), |
| 25 | + port: :inet.port_number(), |
| 26 | + transport: transport() |
| 27 | + } |
| 28 | + |
| 29 | + @enforce_keys [:scheme, :host, :port, :transport] |
| 30 | + defstruct @enforce_keys |
| 31 | + |
| 32 | + @default_udp_tcp_port 3478 |
| 33 | + @default_tls_port 5349 |
| 34 | + |
| 35 | + @doc """ |
| 36 | + The same as parse/1 but raises on error. |
| 37 | + """ |
| 38 | + @spec parse!(String.t()) :: t() |
| 39 | + def parse!(uri) do |
| 40 | + case parse(uri) do |
| 41 | + {:ok, uri} -> uri |
| 42 | + :error -> raise "Invalid URI" |
| 43 | + end |
| 44 | + end |
| 45 | + |
| 46 | + @doc """ |
| 47 | + Parses URI string into `t:t/0`. |
| 48 | + """ |
| 49 | + @spec parse(String.t()) :: {:ok, t()} | :error |
| 50 | + def parse("stun" <> ":" <> host_port) do |
| 51 | + do_parse_stun(:stun, host_port) |
| 52 | + end |
| 53 | + |
| 54 | + def parse("stuns" <> ":" <> host_port) do |
| 55 | + do_parse_stun(:stuns, host_port) |
| 56 | + end |
| 57 | + |
| 58 | + def parse("turn" <> ":" <> host_port_transport) do |
| 59 | + do_parse_turn(:turn, host_port_transport) |
| 60 | + end |
| 61 | + |
| 62 | + def parse("turns" <> ":" <> host_port_transport) do |
| 63 | + do_parse_turn(:turns, host_port_transport) |
| 64 | + end |
| 65 | + |
| 66 | + def parse(_other), do: :error |
| 67 | + |
| 68 | + defp do_parse_stun(scheme, host_port) do |
| 69 | + transport = if scheme == :stun, do: :udp, else: :tcp |
| 70 | + default_port = if scheme == :stun, do: @default_udp_tcp_port, else: @default_tls_port |
| 71 | + |
| 72 | + with {:ok, host, rest} <- parse_host(host_port), |
| 73 | + {:ok, port, ""} <- parse_port(rest) do |
| 74 | + {:ok, |
| 75 | + %__MODULE__{ |
| 76 | + scheme: scheme, |
| 77 | + host: host, |
| 78 | + port: port || default_port, |
| 79 | + transport: transport |
| 80 | + }} |
| 81 | + end |
| 82 | + end |
| 83 | + |
| 84 | + defp do_parse_turn(scheme, host_port_transport) do |
| 85 | + default_port = if scheme == :turn, do: @default_udp_tcp_port, else: @default_tls_port |
| 86 | + default_transport = if scheme == :turn, do: :udp, else: :tcp |
| 87 | + |
| 88 | + with {:ok, host, rest} <- parse_host(host_port_transport), |
| 89 | + {:ok, port, rest} <- parse_port(rest), |
| 90 | + {:ok, transport} <- parse_transport(rest) do |
| 91 | + {:ok, |
| 92 | + %__MODULE__{ |
| 93 | + scheme: scheme, |
| 94 | + host: host, |
| 95 | + port: port || default_port, |
| 96 | + transport: transport || default_transport |
| 97 | + }} |
| 98 | + end |
| 99 | + end |
| 100 | + |
| 101 | + defp parse_host(data) do |
| 102 | + case String.split(data, ":", parts: 2) do |
| 103 | + [host, rest] when host != "" -> |
| 104 | + {:ok, host, rest} |
| 105 | + |
| 106 | + [host] when host != "" -> |
| 107 | + case String.split(host, "?transport=") do |
| 108 | + [host, rest] when host != "" -> {:ok, host, "?transport=" <> rest} |
| 109 | + [host] -> {:ok, host, ""} |
| 110 | + _ -> :error |
| 111 | + end |
| 112 | + |
| 113 | + _ -> |
| 114 | + :error |
| 115 | + end |
| 116 | + end |
| 117 | + |
| 118 | + defp parse_port(""), do: {:ok, nil, ""} |
| 119 | + defp parse_port("?transport=" <> rest), do: {:ok, nil, rest} |
| 120 | + |
| 121 | + defp parse_port(data) do |
| 122 | + case String.split(data, "?transport=", parts: 2) do |
| 123 | + [port, rest] -> |
| 124 | + case do_parse_port(port) do |
| 125 | + {:ok, port} -> {:ok, port, rest} |
| 126 | + :error -> :error |
| 127 | + end |
| 128 | + |
| 129 | + [port] -> |
| 130 | + case do_parse_port(port) do |
| 131 | + {:ok, port} -> {:ok, port, ""} |
| 132 | + :error -> :error |
| 133 | + end |
| 134 | + |
| 135 | + _ -> |
| 136 | + :error |
| 137 | + end |
| 138 | + end |
| 139 | + |
| 140 | + defp do_parse_port(port) do |
| 141 | + case Integer.parse(port) do |
| 142 | + {port, ""} -> {:ok, port} |
| 143 | + :error -> :error |
| 144 | + end |
| 145 | + end |
| 146 | + |
| 147 | + defp parse_transport(""), do: {:ok, nil} |
| 148 | + defp parse_transport("udp"), do: {:ok, :udp} |
| 149 | + defp parse_transport("tcp"), do: {:ok, :tcp} |
| 150 | + defp parse_transport(_), do: :error |
| 151 | +end |
0 commit comments