From aeed073c5bb2a28c8db67358bfdeedea056b8dac Mon Sep 17 00:00:00 2001 From: Guilherme Balena Versiani Date: Sun, 18 Apr 2021 00:06:46 -0300 Subject: [PATCH 1/5] * Removed `elixir_make` from dependencies; * Implemented `Sippet.Parser.parse_first_line/1` and `Sippet.Parser.split_lines/1`. --- lib/sippet/parser.ex | 189 +++++++++++++++++++++++++++++++++++++++---- mix.exs | 9 +-- test/parser_test.exs | 52 ++++++++++++ 3 files changed, 228 insertions(+), 22 deletions(-) create mode 100644 test/parser_test.exs diff --git a/lib/sippet/parser.ex b/lib/sippet/parser.ex index c229f93..9965950 100644 --- a/lib/sippet/parser.ex +++ b/lib/sippet/parser.ex @@ -1,27 +1,188 @@ defmodule Sippet.Parser do @moduledoc """ - Communicates with the C++ NIF parser in order to parse the SIP header. - - The C++ NIF module was created to optimize the parsing. + Parses the SIP header. """ - @on_load {:init, 0} - - app = Mix.Project.config[:app] + alias Sippet.URI + alias Sippet.Message.{RequestLine, StatusLine} @doc """ - Initializes and loads the C++ NIF module. + Parses the first line of a SIP message. + + ## Example: + + iex> Sippet.Parser.parse_first_line("INVITE sip:foo@bar.com SIP/2.0") + %Sippet.Message.RequestLine{ + method: "INVITE", + request_uri: %Sippet.URI{ + authority: "foo@bar.com", + headers: nil, + host: "bar.com", + parameters: nil, + port: 5060, + scheme: "sip", + userinfo: "foo" + }, + version: {2, 0} + } + + iex> Sippet.Parser.parse_first_line("SIP/2.0 200 OK") + %Sippet.Message.StatusLine{ + version: {2, 0}, + status_code: 200, + reason_phrase: "OK" + } """ - def init() do - path = :filename.join(:code.priv_dir(unquote(app)), 'sippet_nif') - :ok = :erlang.load_nif(path, 0) + def parse_first_line("SIP/" <> _ = status_line) do + with {:ok, version, rest} <- parse_sip_version(status_line), + {:ok, code, reason_phrase} <- parse_status_code(rest) do + %StatusLine{ + version: version, + status_code: code, + reason_phrase: reason_phrase + } + end + end + + def parse_first_line(request_line) do + with {:ok, method, rest} <- parse_method(request_line), + {:ok, uri, sip_version} <- parse_uri(rest), + {:ok, version, _} <- parse_sip_version(sip_version) do + %RequestLine{ + method: method, + request_uri: uri, + version: version + } + end + end + + defp parse_method(input), do: parse_method(input, "") + + defp parse_method(<<>>, ""), do: {:error, :enomethod} + + defp parse_method(<<>>, _), do: {:error, :enosp} + + defp parse_method(" " <> rest, method), do: {:ok, method, rest} + + defp parse_method(<>, method), + do: parse_method(rest, <>) + + defp parse_uri(input), do: parse_uri(input, "") + + defp parse_uri(<<>>, ""), do: {:error, :enouri} + + defp parse_uri(<<>>, _), do: {:error, :enosp} + + defp parse_uri(" " <> rest, uri) do + with {:ok, uri} <- URI.parse(uri) do + {:ok, uri, rest} + end + end + + defp parse_uri(<>, uri), + do: parse_uri(rest, <>) + + defp parse_sip_version("SIP/" <> input), do: parse_version(input) + + defp parse_sip_version(_), do: {:error, :ebadver} + + defp parse_version(input), do: parse_version(input, [0]) + + defp parse_version(<<>>, [_, _] = v) do + {:ok, Enum.reverse(v) |> List.to_tuple(), ""} + end + + defp parse_version(<<>>, _), do: {:error, :ebadver} + + defp parse_version("." <> _, [0]), do: {:error, :ebadver} + + defp parse_version("." <> rest, [major]), + do: parse_version(rest, [0, major]) + + defp parse_version("." <> _, _), do: {:error, :ebadver} + + defp parse_version(<>, [last | v]) + when n in ~c[0123456789] do + parse_version(rest, [last * 10 + (n - ?0) | v]) end + defp parse_version(" " <> rest, [_major, _minor] = v), + do: {:ok, Enum.reverse(v) |> List.to_tuple(), rest} + + defp parse_version(_, _), do: {:error, :ebadver} + + defp parse_status_code(input), do: parse_status_code(input, 0) + + defp parse_status_code(<<>>, code) + when code <= 699 and code >= 100 do + {:ok, code, ""} + end + + defp parse_status_code(<<>>, _), do: {:error, :ebadcode} + + defp parse_status_code(" " <> rest, code) + when code <= 699 and code >= 100 do + {:ok, code, rest} + end + + defp parse_status_code(" " <> _, _), do: {:error, :ebadcode} + + defp parse_status_code(<>, code) + when n in ~c[0123456789] do + parse_status_code(rest, code * 10 + (n - ?0)) + end + + defp parse_status_code(_, _), do: {:error, :ebadcode} + @doc """ - Parses the SIP message into an Erlang-like map. + Split header lines. + + ## Example: + + iex> Sippet.Parser.split_lines("Foo: bar") + {["Foo: bar"], ""} - The `Sippet.Message` module translates the result into an Elixir-like struct. + iex> Sippet.Parser.split_lines("Foo: bar\\r\\nFoo: qux") + {["Foo: bar", "Foo: qux"], ""} + + iex> Sippet.Parser.split_lines("Foo: bar\\r\\nFoo: qux\\r\\n") + {["Foo: bar", "Foo: qux"], ""} + + iex> Sippet.Parser.split_lines("Foo: bar\\r\\nFoo: qux\\r\\n\\r\\n") + {["Foo: bar", "Foo: qux"], ""} + + iex> Sippet.Parser.split_lines("Foo: bar\\r\\nFoo: qux\\r\\n\\r\\nrest") + {["Foo: bar", "Foo: qux"], "rest"} """ - def parse(message) when is_binary(message), - do: :erlang.nif_error(:not_loaded) + def split_lines(input), do: split_lines(input, "", []) + + defp split_lines(<<>>, <<>>, lines), do: {lines |> Enum.reverse(), ""} + + defp split_lines(<<>>, line, lines), + do: {[line | lines] |> Enum.reverse(), ""} + + defp split_lines("\r\n\r\n" <> rest, line, lines), + do: {[line | lines] |> Enum.reverse(), rest} + + defp split_lines("\r\n" <> <>, line, lines) + when sp in ~c[ \t] do + split_lines(<>, line, lines) + end + + defp split_lines("\r\n" <> rest, line, lines), + do: split_lines(rest, "", [line | lines]) + + defp split_lines("\n\n" <> rest, line, lines), + do: {[line | lines] |> Enum.reverse(), rest} + + defp split_lines("\n" <> <>, line, lines) + when sp in ~c[ \t] do + split_lines(<>, line, lines) + end + + defp split_lines("\n" <> rest, line, lines), + do: split_lines(rest, "", [line | lines]) + + defp split_lines(<>, line, lines), + do: split_lines(rest, line <> <>, lines) end diff --git a/mix.exs b/mix.exs index 9b7746b..91043e7 100644 --- a/mix.exs +++ b/mix.exs @@ -10,8 +10,6 @@ defmodule Sippet.Mixfile do elixir: "~> 1.10", build_embedded: Mix.env() == :prod, start_permanent: Mix.env() == :prod, - # Add the make compiler - compilers: [:elixir_make] ++ Mix.compilers(), deps: deps(), package: package(), name: "Sippet", @@ -24,9 +22,7 @@ defmodule Sippet.Mixfile do "coveralls.detail": :test, "coveralls.post": :test, "coveralls.html": :test - ], - make_clean: ["clean"], - make_cwd: "c_src" + ] ] end @@ -41,9 +37,6 @@ defmodule Sippet.Mixfile do {:sippet_uri, "~> 0.1"}, {:gen_state_machine, ">= 3.0.0"}, - # Build the NIF - {:elixir_make, "~> 0.4", runtime: false}, - # Docs dependencies {:ex_doc, "~> 0.21", only: :dev, runtime: false}, {:inch_ex, "~> 2.0", only: :docs}, diff --git a/test/parser_test.exs b/test/parser_test.exs new file mode 100644 index 0000000..205d367 --- /dev/null +++ b/test/parser_test.exs @@ -0,0 +1,52 @@ +defmodule ParserTest do + use ExUnit.Case, async: true + doctest Sippet.Parser + + @test_first_line [ + {"SIP/ 200 OK", {:error, :ebadver}}, + {"SIP/2.0.1 200 OK", {:error, :ebadver}}, + {"SIP/2 200 OK", {:error, :ebadver}}, + {"SIP/2 .0 200 OK", {:error, :ebadver}}, + {"SIP/2.0a 200 OK", {:error, :ebadver}}, + {"INVITE sip:foo@bar.com SIP/2", {:error, :ebadver}}, + {"INVITE sip:foo@bar.com SIP/2.0.1", {:error, :ebadver}}, + {"INVITE sip:foo@bar.com SIP/", {:error, :ebadver}}, + {"INVITE sip:foo@bar.com SIP/two", {:error, :ebadver}}, + {"INVITE sip:foo@bar.com SIP/2.zero", {:error, :ebadver}}, + {"INVITE SIP/2.0", {:error, :enosp}}, + {"INVITE", {:error, :enosp}}, + {"SIP/2.0200 OK", {:error, :ebadcode}}, + {"SIP/2.0 99 OK", {:error, :ebadcode}} + ] + + @test_split_lines [ + {"", {[], ""}}, + {"Foo: bar\nFoo: qux", {["Foo: bar", "Foo: qux"], ""}}, + {"Foo: bar\n qux", {["Foo: bar qux"], ""}}, + {"Foo: bar\n\tqux", {["Foo: bar\tqux"], ""}}, + {"Foo: bar\nBar:\n\n", {["Foo: bar", "Bar:"], ""}}, + {"Foo: bar\r\nBar:\n\n", {["Foo: bar", "Bar:"], ""}}, + ] + + for {input, expected} <- @test_first_line do + @tag input: input + @tag expected: expected + test "parse #{inspect(input)} -> #{inspect(expected)}", %{ + input: input, + expected: expected + } do + assert ^expected = Sippet.Parser.parse_first_line(input) + end + end + + for {input, expected} <- @test_split_lines do + @tag input: input + @tag expected: expected + test "split lines in #{inspect(input)} -> #{inspect(expected)}", %{ + input: input, + expected: expected + } do + assert ^expected = Sippet.Parser.split_lines(input) + end + end +end From d26ff0d157f16fb47a597fe92ba6150ed9a224e0 Mon Sep 17 00:00:00 2001 From: Guilherme Balena Versiani Date: Wed, 21 Apr 2021 18:50:08 -0300 Subject: [PATCH 2/5] Added parse/1 and parsing functions for Accept compatible headers. --- lib/sippet/parser.ex | 215 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 182 insertions(+), 33 deletions(-) diff --git a/lib/sippet/parser.ex b/lib/sippet/parser.ex index 9965950..9d764fd 100644 --- a/lib/sippet/parser.ex +++ b/lib/sippet/parser.ex @@ -3,44 +3,107 @@ defmodule Sippet.Parser do Parses the SIP header. """ - alias Sippet.URI + alias Sippet.{URI, Message} alias Sippet.Message.{RequestLine, StatusLine} + @wsp ~c[ \t] + @digits ~c[0123456789] + + def parse(iodata) when is_list(iodata), + do: iodata |> IO.iodata_to_binary() |> parse() + + def parse(binary) when is_binary(binary), + do: binary |> split_lines() |> parse_lines() + + defp parse_lines({[], _}), do: {:error, :ebadmsg} + + defp parse_lines({[first_line | headers], body}) do + with {:ok, start_line} <- parse_first_line(first_line), + {:ok, parsed_headers} <- parse_headers(headers) do + %Message{ + start_line: start_line, + headers: parsed_headers |> Map.new(), + body: body + } + end + end + + defp parse_headers(headers), do: parse_headers(headers, []) + + defp parse_headers([], parsed_headers), do: {:ok, parsed_headers} + + defp parse_headers([header_line | headers], acc) do + with {:ok, header} <- parse_header(header_line) do + parse_headers(headers, [header | acc]) + end + end + + defp parse_header(header_line) do + with {:ok, header_name, header_value} <- split_header(header_line) do + parse_header_value(header_name, header_value) + end + end + + defp split_header(header_line), do: split_header(header_line, "") + + defp split_header(<<>>, _), do: {:error, :ebadheader} + + defp split_header(<>, header_name) when c in @wsp, + do: split_header(header_value, header_name) + + defp split_header(":" <> header_value, header_name), + do: {:ok, header_name |> String.downcase(), header_value} + + defp split_header(<>, header_name), + do: split_header(header_value, header_name <> <>) + + defp parse_header_value("accept", header_value) do + with {:ok, values} <- parse_multiple_type_subtype_params(header_value) do + {:ok, {:accept, values}} + end + end + + defp parse_header_value(header_name, header_value), + do: {:ok, {header_name, header_value}} + @doc """ Parses the first line of a SIP message. ## Example: iex> Sippet.Parser.parse_first_line("INVITE sip:foo@bar.com SIP/2.0") - %Sippet.Message.RequestLine{ - method: "INVITE", - request_uri: %Sippet.URI{ - authority: "foo@bar.com", - headers: nil, - host: "bar.com", - parameters: nil, - port: 5060, - scheme: "sip", - userinfo: "foo" - }, - version: {2, 0} - } + {:ok, + %Sippet.Message.RequestLine{ + method: "INVITE", + request_uri: %Sippet.URI{ + authority: "foo@bar.com", + headers: nil, + host: "bar.com", + parameters: nil, + port: 5060, + scheme: "sip", + userinfo: "foo" + }, + version: {2, 0} + }} iex> Sippet.Parser.parse_first_line("SIP/2.0 200 OK") - %Sippet.Message.StatusLine{ - version: {2, 0}, - status_code: 200, - reason_phrase: "OK" - } + {:ok, + %Sippet.Message.StatusLine{ + version: {2, 0}, + status_code: 200, + reason_phrase: "OK" + }} """ def parse_first_line("SIP/" <> _ = status_line) do with {:ok, version, rest} <- parse_sip_version(status_line), {:ok, code, reason_phrase} <- parse_status_code(rest) do - %StatusLine{ - version: version, - status_code: code, - reason_phrase: reason_phrase - } + {:ok, + %StatusLine{ + version: version, + status_code: code, + reason_phrase: reason_phrase + }} end end @@ -48,11 +111,12 @@ defmodule Sippet.Parser do with {:ok, method, rest} <- parse_method(request_line), {:ok, uri, sip_version} <- parse_uri(rest), {:ok, version, _} <- parse_sip_version(sip_version) do - %RequestLine{ - method: method, - request_uri: uri, - version: version - } + {:ok, + %RequestLine{ + method: method, + request_uri: uri, + version: version + }} end end @@ -102,7 +166,7 @@ defmodule Sippet.Parser do defp parse_version("." <> _, _), do: {:error, :ebadver} defp parse_version(<>, [last | v]) - when n in ~c[0123456789] do + when n in @digits do parse_version(rest, [last * 10 + (n - ?0) | v]) end @@ -128,7 +192,7 @@ defmodule Sippet.Parser do defp parse_status_code(" " <> _, _), do: {:error, :ebadcode} defp parse_status_code(<>, code) - when n in ~c[0123456789] do + when n in @digits do parse_status_code(rest, code * 10 + (n - ?0)) end @@ -165,7 +229,7 @@ defmodule Sippet.Parser do do: {[line | lines] |> Enum.reverse(), rest} defp split_lines("\r\n" <> <>, line, lines) - when sp in ~c[ \t] do + when sp in @wsp do split_lines(<>, line, lines) end @@ -176,7 +240,7 @@ defmodule Sippet.Parser do do: {[line | lines] |> Enum.reverse(), rest} defp split_lines("\n" <> <>, line, lines) - when sp in ~c[ \t] do + when sp in @wsp do split_lines(<>, line, lines) end @@ -185,4 +249,89 @@ defmodule Sippet.Parser do defp split_lines(<>, line, lines), do: split_lines(rest, line <> <>, lines) + + def parse_multiple_type_subtype_params(input), + do: parse_multiple_type_subtype_params(input, "", []) + + defp parse_multiple_type_subtype_params(<<>>, <<>>, list), do: {:ok, list |> Enum.reverse()} + + defp parse_multiple_type_subtype_params(<<>>, part, list) do + with {:ok, type_subtype, params} <- parse_single_type_subtype_params(part) do + {:ok, [{type_subtype, params} | list] |> Enum.reverse()} + end + end + + defp parse_multiple_type_subtype_params("," <> input, part, list) do + with {:ok, type_subtype, params} <- parse_single_type_subtype_params(part) do + parse_multiple_type_subtype_params(input, "", [{type_subtype, params} | list]) + end + end + + defp parse_multiple_type_subtype_params(<>, part, list), + do: parse_multiple_type_subtype_params(input, part <> <>, list) + + def parse_single_type_subtype_params(input), + do: parse_single_type_subtype_params(input, "", "") + + defp parse_single_type_subtype_params(<<>>, _subtype, ""), + do: {:error, :ebadtype} + + defp parse_single_type_subtype_params(<<>>, "", _type), + do: {:error, :ebadtype} + + defp parse_single_type_subtype_params(<<>>, subtype, type), + do: {:ok, {type, subtype}, %{}} + + defp parse_single_type_subtype_params(<>, part, type) when c in @wsp, + do: parse_single_type_subtype_params(input, part, type) + + defp parse_single_type_subtype_params("/" <> input, type, ""), + do: parse_single_type_subtype_params(input, "", type) + + defp parse_single_type_subtype_params("/" <> _input, _subtype, _type), + do: {:error, :ebadtype} + + defp parse_single_type_subtype_params(";" <> _input, "", _type), + do: {:error, :ebadtype} + + defp parse_single_type_subtype_params(";" <> _input, _subtype, ""), + do: {:error, :ebadtype} + + defp parse_single_type_subtype_params(";" <> input, subtype, type) do + with {:ok, params} <- parse_params(input) do + {:ok, {type, subtype}, params} + end + end + + defp parse_single_type_subtype_params(<>, part, type), + do: parse_single_type_subtype_params(input, part <> <>, type) + + def parse_params(input), do: parse_params(input, "", "", []) + + defp parse_params(<<>>, "", "", _list), + do: {:error, :ebadparam} + + defp parse_params(<<>>, key, "", list), + do: {:ok, [{key, ""} | list] |> Map.new()} + + defp parse_params(<<>>, value, key, list), + do: {:ok, [{key, value} | list] |> Map.new()} + + defp parse_params(<>, part, key, list) when c in @wsp, + do: parse_params(input, part, key, list) + + defp parse_params("=" <> input, key, "", list), + do: parse_params(input, "", key, list) + + defp parse_params("=" <> _input, _value, _key, _list), + do: {:error, :ebadparam} + + defp parse_params(";" <> _input, _value, "", _list), + do: {:error, :ebadparam} + + defp parse_params(";" <> input, value, key, list), + do: parse_params(input, "", "", [{key, value} | list]) + + defp parse_params(<>, part, key, list), + do: parse_params(input, part <> <>, key, list) end From 434ca21a5504eb84f7a96586d643ca3adf0a89fb Mon Sep 17 00:00:00 2001 From: Guilherme Balena Versiani Date: Wed, 21 Apr 2021 21:29:39 -0300 Subject: [PATCH 3/5] Added some documentation and normalized the parse_single_type_subtype_params/1 result. --- lib/sippet/parser.ex | 50 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 43 insertions(+), 7 deletions(-) diff --git a/lib/sippet/parser.ex b/lib/sippet/parser.ex index 9d764fd..fadf5cb 100644 --- a/lib/sippet/parser.ex +++ b/lib/sippet/parser.ex @@ -9,6 +9,10 @@ defmodule Sippet.Parser do @wsp ~c[ \t] @digits ~c[0123456789] + @known_headers [ + {"accept", :accept, &Sippet.Parser.parse_multiple_type_subtype_params/1} + ] + def parse(iodata) when is_list(iodata), do: iodata |> IO.iodata_to_binary() |> parse() @@ -57,9 +61,11 @@ defmodule Sippet.Parser do defp split_header(<>, header_name), do: split_header(header_value, header_name <> <>) - defp parse_header_value("accept", header_value) do - with {:ok, values} <- parse_multiple_type_subtype_params(header_value) do - {:ok, {:accept, values}} + for {header_name, header_key, parser} <- @known_headers do + defp parse_header_value(unquote(header_name), header_value) do + with {:ok, result} <- unquote(parser).(header_value) do + {:ok, {unquote(header_key), result}} + end end end @@ -250,19 +256,33 @@ defmodule Sippet.Parser do defp split_lines(<>, line, lines), do: split_lines(rest, line <> <>, lines) + @doc """ + Parse headers such as `Accept`. + + ## Example: + + iex> Sippet.Parser.parse_multiple_type_subtype_params("application/json;q=1") + {:ok, [{{"application", "json"}, %{"q" => "1"}}]} + + iex> Sippet.Parser.parse_multiple_type_subtype_params("image/png;q=1, image/gif;q=0.9") + {:ok, [{{"image", "png"}, %{"q" => "1"}}, {{"image", "gif"}, %{"q" => "0.9"}}]} + + iex> Sippet.Parser.parse_multiple_type_subtype_params("") + {:ok, []} + """ def parse_multiple_type_subtype_params(input), do: parse_multiple_type_subtype_params(input, "", []) defp parse_multiple_type_subtype_params(<<>>, <<>>, list), do: {:ok, list |> Enum.reverse()} defp parse_multiple_type_subtype_params(<<>>, part, list) do - with {:ok, type_subtype, params} <- parse_single_type_subtype_params(part) do + with {:ok, {type_subtype, params}} <- parse_single_type_subtype_params(part) do {:ok, [{type_subtype, params} | list] |> Enum.reverse()} end end defp parse_multiple_type_subtype_params("," <> input, part, list) do - with {:ok, type_subtype, params} <- parse_single_type_subtype_params(part) do + with {:ok, {type_subtype, params}} <- parse_single_type_subtype_params(part) do parse_multiple_type_subtype_params(input, "", [{type_subtype, params} | list]) end end @@ -270,6 +290,14 @@ defmodule Sippet.Parser do defp parse_multiple_type_subtype_params(<>, part, list), do: parse_multiple_type_subtype_params(input, part <> <>, list) + @doc """ + Parse headers such as `Content-Type`. + + ## Example: + + iex> Sippet.Parser.parse_single_type_subtype_params("application/json;q=1") + {:ok, {{"application", "json"}, %{"q" => "1"}}} + """ def parse_single_type_subtype_params(input), do: parse_single_type_subtype_params(input, "", "") @@ -280,7 +308,7 @@ defmodule Sippet.Parser do do: {:error, :ebadtype} defp parse_single_type_subtype_params(<<>>, subtype, type), - do: {:ok, {type, subtype}, %{}} + do: {:ok, {{type, subtype}, %{}}} defp parse_single_type_subtype_params(<>, part, type) when c in @wsp, do: parse_single_type_subtype_params(input, part, type) @@ -299,13 +327,21 @@ defmodule Sippet.Parser do defp parse_single_type_subtype_params(";" <> input, subtype, type) do with {:ok, params} <- parse_params(input) do - {:ok, {type, subtype}, params} + {:ok, {{type, subtype}, params}} end end defp parse_single_type_subtype_params(<>, part, type), do: parse_single_type_subtype_params(input, part <> <>, type) + @doc """ + Parse semicolon separated values. + + ## Example: + + iex> Sippet.Parser.parse_params("tag=abc;q=1") + {:ok, %{"tag" => "abc", "q" => "1"}} + """ def parse_params(input), do: parse_params(input, "", "", []) defp parse_params(<<>>, "", "", _list), From 14e2afc92741c916f69c1957eb7d6c196d0a913f Mon Sep 17 00:00:00 2001 From: Guilherme Balena Versiani Date: Mon, 24 May 2021 16:48:51 -0300 Subject: [PATCH 4/5] * Added `parse_multiple_token_params/1` and `parse_multiple_uri_params/1`. * Converting the method to atoms. * Simplified the SIP version parser. --- lib/sippet/parser.ex | 257 +++++++++++++++++++++++++++++++++++-------- test/parser_test.exs | 2 +- 2 files changed, 212 insertions(+), 47 deletions(-) diff --git a/lib/sippet/parser.ex b/lib/sippet/parser.ex index fadf5cb..616f2c8 100644 --- a/lib/sippet/parser.ex +++ b/lib/sippet/parser.ex @@ -3,14 +3,43 @@ defmodule Sippet.Parser do Parses the SIP header. """ - alias Sippet.{URI, Message} + alias Sippet.Message alias Sippet.Message.{RequestLine, StatusLine} @wsp ~c[ \t] @digits ~c[0123456789] + @alpha Enum.map(?a..?z, & &1) ++ Enum.map(?A..?Z, & &1) + @alphanum @alpha ++ @digits + @token @alphanum ++ ~c[-.!%*_+`'~] + + @methods %{ + "ACK" => :ack, + "BYE" => :bye, + "CANCEL" => :cancel, + "INFO" => :info, + "INVITE" => :invite, + "MESSAGE" => :message, + "NOTIFY" => :notify, + "OPTIONS" => :options, + "PRACK" => :prack, + "PUBLISH" => :publish, + "PULL" => :pull, + "PUSH" => :push, + "REFER" => :refer, + "REGISTER" => :register, + "STORE" => :store, + "SUBSCRIBE" => :subscribe, + "UPDATE" => :update + } @known_headers [ - {"accept", :accept, &Sippet.Parser.parse_multiple_type_subtype_params/1} + {"accept", :accept, &Sippet.Parser.parse_multiple_type_subtype_params/1}, + {"accept-encoding", :accept_encoding, &Sippet.Parser.parse_multiple_token_params/1}, + {"accept-language", :accept_language, &Sippet.Parser.parse_multiple_token_params/1}, + {"alert-info", :alert_info, &Sippet.Parser.parse_multiple_uri_params/1}, + {"content-type", :content_type, &Sippet.Parser.parse_single_type_subtype_params/1}, + {"call-info", :call_info, &Sippet.Parser.parse_multiple_uri_params/1}, + {"error-info", :error_info, &Sippet.Parser.parse_multiple_uri_params/1}, ] def parse(iodata) when is_list(iodata), @@ -57,7 +86,7 @@ defmodule Sippet.Parser do defp split_header(":" <> header_value, header_name), do: {:ok, header_name |> String.downcase(), header_value} - + defp split_header(<>, header_name), do: split_header(header_value, header_name <> <>) @@ -80,7 +109,7 @@ defmodule Sippet.Parser do iex> Sippet.Parser.parse_first_line("INVITE sip:foo@bar.com SIP/2.0") {:ok, %Sippet.Message.RequestLine{ - method: "INVITE", + method: :invite, request_uri: %Sippet.URI{ authority: "foo@bar.com", headers: nil, @@ -132,7 +161,8 @@ defmodule Sippet.Parser do defp parse_method(<<>>, _), do: {:error, :enosp} - defp parse_method(" " <> rest, method), do: {:ok, method, rest} + defp parse_method(<>, method) when c in @wsp, + do: {:ok, Map.get(@methods, method, method), rest} defp parse_method(<>, method), do: parse_method(rest, <>) @@ -144,7 +174,7 @@ defmodule Sippet.Parser do defp parse_uri(<<>>, _), do: {:error, :enosp} defp parse_uri(" " <> rest, uri) do - with {:ok, uri} <- URI.parse(uri) do + with {:ok, uri} <- Sippet.URI.parse(uri) do {:ok, uri, rest} end end @@ -152,55 +182,24 @@ defmodule Sippet.Parser do defp parse_uri(<>, uri), do: parse_uri(rest, <>) - defp parse_sip_version("SIP/" <> input), do: parse_version(input) - - defp parse_sip_version(_), do: {:error, :ebadver} - - defp parse_version(input), do: parse_version(input, [0]) - - defp parse_version(<<>>, [_, _] = v) do - {:ok, Enum.reverse(v) |> List.to_tuple(), ""} - end - - defp parse_version(<<>>, _), do: {:error, :ebadver} - - defp parse_version("." <> _, [0]), do: {:error, :ebadver} - - defp parse_version("." <> rest, [major]), - do: parse_version(rest, [0, major]) + defp parse_sip_version("SIP/2.0"), do: {:ok, {2, 0}, <<>>} - defp parse_version("." <> _, _), do: {:error, :ebadver} + defp parse_sip_version(<<"SIP/2.0", c, rest::binary>>) when c in @wsp, + do: {:ok, {2, 0}, rest} - defp parse_version(<>, [last | v]) - when n in @digits do - parse_version(rest, [last * 10 + (n - ?0) | v]) - end - - defp parse_version(" " <> rest, [_major, _minor] = v), - do: {:ok, Enum.reverse(v) |> List.to_tuple(), rest} - - defp parse_version(_, _), do: {:error, :ebadver} + defp parse_sip_version(_), do: {:error, :ebadver} defp parse_status_code(input), do: parse_status_code(input, 0) - defp parse_status_code(<<>>, code) - when code <= 699 and code >= 100 do - {:ok, code, ""} - end + defp parse_status_code(<<>>, code) when code in 100..699, do: {:ok, code, ""} defp parse_status_code(<<>>, _), do: {:error, :ebadcode} - defp parse_status_code(" " <> rest, code) - when code <= 699 and code >= 100 do - {:ok, code, rest} - end - - defp parse_status_code(" " <> _, _), do: {:error, :ebadcode} + defp parse_status_code(<>, code) when c in @wsp and code in 100..699, + do: {:ok, code, rest} - defp parse_status_code(<>, code) - when n in @digits do - parse_status_code(rest, code * 10 + (n - ?0)) - end + defp parse_status_code(<>, code) when n in @digits, + do: parse_status_code(rest, code * 10 + (n - ?0)) defp parse_status_code(_, _), do: {:error, :ebadcode} @@ -223,6 +222,9 @@ defmodule Sippet.Parser do iex> Sippet.Parser.split_lines("Foo: bar\\r\\nFoo: qux\\r\\n\\r\\nrest") {["Foo: bar", "Foo: qux"], "rest"} + + iex> Sippet.Parser.split_lines("Foo: bar\\nFoo: qux\\n\\nrest") + {["Foo: bar", "Foo: qux"], "rest"} """ def split_lines(input), do: split_lines(input, "", []) @@ -370,4 +372,167 @@ defmodule Sippet.Parser do defp parse_params(<>, part, key, list), do: parse_params(input, part <> <>, key, list) + + @doc """ + Parse multiple token followed or not by semicolon separated values. + + ## Examples: + + iex> Sippet.Parser.parse_multiple_token_params("") + {:ok, []} + + iex> Sippet.Parser.parse_multiple_token_params("a;q=0.9") + {:ok, [{"a", %{"q" => "0.9"}}]} + + iex> Sippet.Parser.parse_multiple_token_params("a;q=0.9, b;q=0.1") + {:ok, [{"a", %{"q" => "0.9"}}, {"b", %{"q" => "0.1"}}]} + """ + def parse_multiple_token_params(input), do: parse_multiple_token_params(input, []) + + defp parse_multiple_token_params(<<>>, list), do: {:ok, Enum.reverse(list)} + + defp parse_multiple_token_params("," <> rest, list), + do: parse_multiple_token_params(rest, [{<<>>, %{}} | list]) + + defp parse_multiple_token_params(";" <> rest, [{token, _} | list]) do + {params, rest} = + case String.split(rest, ",", parts: 2) do + [params] -> {params, <<>>} + [params, rest] -> {params, "," <> rest} + end + + with {:ok, parsed_params} <- parse_params(params) do + parse_multiple_token_params(rest, [{token, parsed_params} | list]) + end + end + + defp parse_multiple_token_params(<>, list) when c in @wsp, + do: parse_multiple_token_params(rest, list) + + defp parse_multiple_token_params(<>, []) when c in @token, + do: parse_multiple_token_params(rest, [{<>, %{}}]) + + defp parse_multiple_token_params(<>, [{token, _} | list]) when c in @token, + do: parse_multiple_token_params(rest, [{token <> <>, %{}} | list]) + + defp parse_multiple_token_params(_, _), do: {:error, :ebadtoken} + + @doc """ + Parse multiple URI followed or not by semicolon separated values. + + ## Examples: + + iex> Sippet.Parser.parse_multiple_uri_params("") + {:ok, []} + + iex> Sippet.Parser.parse_multiple_uri_params("") + {:ok, + [ + {%URI{ + authority: "www.example.com", + fragment: nil, + host: "www.example.com", + path: "/sounds/moo.wav", + port: 80, + query: nil, + scheme: "http", + userinfo: nil + }, %{}} + ]} + + iex> Sippet.Parser.parse_multiple_uri_params(";q=1.0") + {:ok, + [ + {%URI{ + authority: "www.example.com", + fragment: nil, + host: "www.example.com", + path: "/sounds/moo.wav", + port: 80, + query: nil, + scheme: "http", + userinfo: nil + }, %{"q" => "1.0"}} + ]} + + iex> Sippet.Parser.parse_multiple_uri_params(" ;purpose=icon, ;purpose=info") + {:ok, + [ + { + %URI{ + authority: "www.example.com", + fragment: nil, + host: "www.example.com", + path: "/alice/photo.jpg", + port: 80, + query: nil, + scheme: "http", + userinfo: nil + }, + %{"purpose" => "icon"} + }, + { + %URI{ + authority: "www.example.com", + fragment: nil, + host: "www.example.com", + path: "/alice/", + port: 80, + query: nil, + scheme: "http", + userinfo: nil + }, %{"purpose" => "info"} + } + ]} + """ + def parse_multiple_uri_params(input), do: parse_multiple_uri_params(input, []) + + defp parse_multiple_uri_params(<<>>, []), do: {:ok, []} + + defp parse_multiple_uri_params(<<>>, [{%URI{}, _} | _] = list), do: {:ok, Enum.reverse(list)} + + defp parse_multiple_uri_params(<<>>, _), do: {:error, :ebaduri} + + defp parse_multiple_uri_params("," <> rest, [{%URI{}, _} | _] = list), + do: parse_multiple_uri_params(rest, [{<<>>, %{}} | list]) + + defp parse_multiple_uri_params("," <> _rest, _list), do: {:error, :ebaduri} + + defp parse_multiple_uri_params(";" <> rest, [{%URI{} = uri, _} | list]) do + {params, rest} = + case String.split(rest, ",", parts: 2) do + [params] -> {params, <<>>} + [params, rest] -> {params, "," <> rest} + end + + with {:ok, parsed_params} <- parse_params(params) do + parse_multiple_uri_params(rest, [{uri, parsed_params} | list]) + end + end + + defp parse_multiple_uri_params(";" <> _rest, _list), do: {:error, :baduri} + + defp parse_multiple_uri_params(<>, list) when c in @wsp, + do: parse_multiple_uri_params(rest, list) + + defp parse_multiple_uri_params("<" <> rest, []), + do: parse_multiple_uri_params(rest, [{<<"<">>, %{}}]) + + defp parse_multiple_uri_params("<" <> rest, [{<<>>, _} | list]), + do: parse_multiple_uri_params(rest, [{<<"<">>, %{}} | list]) + + defp parse_multiple_uri_params(">" <> rest, [{"<" <> uri, _} | list]) do + case URI.parse(uri) do + %URI{scheme: scheme} = parsed_uri when is_binary(scheme) -> + parse_multiple_uri_params(rest, [{parsed_uri, %{}} | list]) + + _ -> + {:error, :baduri} + end + end + + defp parse_multiple_uri_params(<>, [{"<" <> uri, _} | list]), + do: parse_multiple_uri_params(rest, [{"<" <> uri <> <>, %{}} | list]) + + defp parse_multiple_uri_params(_, _), do: {:error, :ebaduri} end diff --git a/test/parser_test.exs b/test/parser_test.exs index 205d367..29823e7 100644 --- a/test/parser_test.exs +++ b/test/parser_test.exs @@ -15,7 +15,7 @@ defmodule ParserTest do {"INVITE sip:foo@bar.com SIP/2.zero", {:error, :ebadver}}, {"INVITE SIP/2.0", {:error, :enosp}}, {"INVITE", {:error, :enosp}}, - {"SIP/2.0200 OK", {:error, :ebadcode}}, + {"SIP/2.0200 OK", {:error, :ebadver}}, {"SIP/2.0 99 OK", {:error, :ebadcode}} ] From 7db229fdf50479c65b2a6761e5aef59ef3c2e140 Mon Sep 17 00:00:00 2001 From: Guilherme Balena Versiani Date: Mon, 24 May 2021 18:52:29 -0300 Subject: [PATCH 5/5] Added parse_multiple_tokens/1. --- lib/sippet/parser.ex | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/lib/sippet/parser.ex b/lib/sippet/parser.ex index 616f2c8..3d09f85 100644 --- a/lib/sippet/parser.ex +++ b/lib/sippet/parser.ex @@ -37,9 +37,17 @@ defmodule Sippet.Parser do {"accept-encoding", :accept_encoding, &Sippet.Parser.parse_multiple_token_params/1}, {"accept-language", :accept_language, &Sippet.Parser.parse_multiple_token_params/1}, {"alert-info", :alert_info, &Sippet.Parser.parse_multiple_uri_params/1}, + {"allow", :allow, &Sippet.Parser.parse_multiple_tokens/1}, + {"content-encoding", :content_encoding, &Sippet.Parser.parse_multiple_tokens/1}, + {"content-language", :content_language, &Sippet.Parser.parse_multiple_tokens/1}, {"content-type", :content_type, &Sippet.Parser.parse_single_type_subtype_params/1}, {"call-info", :call_info, &Sippet.Parser.parse_multiple_uri_params/1}, {"error-info", :error_info, &Sippet.Parser.parse_multiple_uri_params/1}, + {"in-reply-to", :in_reply_to, &Sippet.Parser.parse_multiple_tokens/1}, + {"proxy-require", :proxy_require, &Sippet.Parser.parse_multiple_tokens/1}, + {"require", :require, &Sippet.Parser.parse_multiple_tokens/1}, + {"supported", :supported, &Sippet.Parser.parse_multiple_tokens/1}, + {"unsupported", :unsupported, &Sippet.Parser.parse_multiple_tokens/1} ] def parse(iodata) when is_list(iodata), @@ -373,6 +381,38 @@ defmodule Sippet.Parser do defp parse_params(<>, part, key, list), do: parse_params(input, part <> <>, key, list) + @doc """ + Parse multiple tokens. + + ## Examples: + + iex> Sippet.Parser.parse_multiple_tokens("") + {:ok, []} + + iex> Sippet.Parser.parse_multiple_tokens("a") + {:ok, ["a"]} + + iex> Sippet.Parser.parse_multiple_tokens("a, b") + {:ok, ["a", "b"]} + """ + def parse_multiple_tokens(input), do: parse_multiple_tokens(input, []) + + defp parse_multiple_tokens(<<>>, list), do: {:ok, Enum.reverse(list)} + + defp parse_multiple_tokens("," <> rest, list), + do: parse_multiple_tokens(rest, [<<>> | list]) + + defp parse_multiple_tokens(<>, list) when c in @wsp, + do: parse_multiple_tokens(rest, list) + + defp parse_multiple_tokens(<>, []) when c in @token, + do: parse_multiple_tokens(rest, [<>]) + + defp parse_multiple_tokens(<>, [token | list]) when c in @token, + do: parse_multiple_tokens(rest, [token <> <> | list]) + + defp parse_multiple_tokens(_, _), do: {:error, :ebadtoken} + @doc """ Parse multiple token followed or not by semicolon separated values.