From 1ff345e85dbe5363fd8b3100998e7eb207795542 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=C5=9Aled=C5=BA?= Date: Sat, 16 Mar 2024 19:09:33 +0100 Subject: [PATCH] Move URI module from ex_ice --- lib/ex_stun/message.ex | 24 ++++++ lib/ex_stun/uri.ex | 151 ++++++++++++++++++++++++++++++++++++++ test/ex_stun/uri_test.exs | 73 ++++++++++++++++++ test/realm_test.exs | 6 -- 4 files changed, 248 insertions(+), 6 deletions(-) create mode 100644 lib/ex_stun/uri.ex create mode 100644 test/ex_stun/uri_test.exs delete mode 100644 test/realm_test.exs diff --git a/lib/ex_stun/message.ex b/lib/ex_stun/message.ex index ab57cf6..d32e575 100644 --- a/lib/ex_stun/message.ex +++ b/lib/ex_stun/message.ex @@ -228,6 +228,30 @@ defmodule ExSTUN.Message do :crypto.hash(:md5, username <> ":" <> realm <> ":" <> password) end + def authenticate_lt(msg, username, realm, password) do + with {:ok, %MessageIntegrity{} = msg_int} <- get_message_integrity(msg) do + key = username <> ":" <> realm <> ":" <> password + key = :crypto.hash(:md5, key) + + # + 20 for STUN message header + # - 24 for message integrity + len = msg.len_to_int + 20 - 24 + <> = msg.raw + <> = msg_without_integrity + msg_without_integrity = <> + + mac = :crypto.mac(:hmac, :sha, key, msg_without_integrity) + + if mac == msg_int.value do + {:ok, key} + else + {:error, :no_matching_message_integrity} + end + else + {:error, _reason} = err -> err + end + end + @doc """ Authenticates a message. diff --git a/lib/ex_stun/uri.ex b/lib/ex_stun/uri.ex new file mode 100644 index 0000000..45da791 --- /dev/null +++ b/lib/ex_stun/uri.ex @@ -0,0 +1,151 @@ +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 + + @type t() :: %__MODULE__{ + scheme: scheme(), + host: String.t(), + port: :inet.port_number(), + transport: transport() + } + + @enforce_keys [:scheme, :host, :port, :transport] + defstruct @enforce_keys + + @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 + transport = if scheme == :stun, do: :udp, else: :tcp + default_port = if scheme == :stun, do: @default_udp_tcp_port, else: @default_tls_port + + with {:ok, host, rest} <- parse_host(host_port), + {:ok, port, ""} <- parse_port(rest) do + {:ok, + %__MODULE__{ + scheme: scheme, + host: host, + port: port || default_port, + transport: transport + }} + 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 == :turn, do: :udp, else: :tcp + + with {:ok, host, rest} <- parse_host(host_port_transport), + {:ok, port, rest} <- parse_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_port(""), do: {:ok, nil, ""} + defp parse_port("?transport=" <> rest), do: {:ok, nil, rest} + + defp parse_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 + + _ -> + :error + end + end + + defp do_parse_port(port) do + case Integer.parse(port) do + {port, ""} -> {:ok, port} + :error -> :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 diff --git a/test/ex_stun/uri_test.exs b/test/ex_stun/uri_test.exs new file mode 100644 index 0000000..4cfe946 --- /dev/null +++ b/test/ex_stun/uri_test.exs @@ -0,0 +1,73 @@ +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: :udp} + }, + { + "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: :udp} + }, + { + "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: :udp} + }, + { + "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: :udp} + }, + { + "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", + "abcd:stun.l.google.com:19302", + "stun:stun.l.google.com:ab123", + "stuns:stun.l.google.com:ab123" + ] do + assert :error == URI.parse(invalid_uri_string) + end + end + end +end diff --git a/test/realm_test.exs b/test/realm_test.exs deleted file mode 100644 index 1960ca3..0000000 --- a/test/realm_test.exs +++ /dev/null @@ -1,6 +0,0 @@ -defmodule ExSTUN.Message.Attribute.RealmTest do - use ExUnit.Case - - test "" do - end -end