From 4d57f29f79803247ad5f16f97b4d275a88265989 Mon Sep 17 00:00:00 2001 From: Shane Logsdon Date: Wed, 24 Dec 2014 00:24:21 -0500 Subject: [PATCH] round three fix copy_req_content_type router plug. add HttpsOnly and XML parser --- lib/sugar/request/https_only.ex | 10 ++++ lib/sugar/request/parsers/xml.ex | 65 ++++++++++++++++++++++++++ lib/sugar/router.ex | 8 ++-- test/sugar/request/https_only_test.exs | 13 ++++++ test/sugar/request/parsers_test.exs | 56 ++++++++++++++++++++++ 5 files changed, 148 insertions(+), 4 deletions(-) create mode 100644 lib/sugar/request/https_only.ex create mode 100644 lib/sugar/request/parsers/xml.ex create mode 100644 test/sugar/request/https_only_test.exs create mode 100644 test/sugar/request/parsers_test.exs diff --git a/lib/sugar/request/https_only.ex b/lib/sugar/request/https_only.ex new file mode 100644 index 0000000..52d473c --- /dev/null +++ b/lib/sugar/request/https_only.ex @@ -0,0 +1,10 @@ +defmodule Sugar.Request.HttpsOnly do + @moduledoc false + import Plug.Conn + + def init(opts), do: opts + + def call(conn, _opts) do + conn |> send_resp(403, "Forbidden") + end +end \ No newline at end of file diff --git a/lib/sugar/request/parsers/xml.ex b/lib/sugar/request/parsers/xml.ex new file mode 100644 index 0000000..462d796 --- /dev/null +++ b/lib/sugar/request/parsers/xml.ex @@ -0,0 +1,65 @@ +defmodule Sugar.Request.Parsers.XML do + @moduledoc false + alias Plug.Conn + + @types [ "application", "text" ] + + @type conn :: map + @type headers :: map + @type opts :: Keyword.t + + @spec parse(conn, binary, binary, headers, opts) :: {:ok | :error, map | atom, conn} + def parse(%Conn{} = conn, type, "xml", _headers, opts) when type in @types do + case Conn.read_body(conn, opts) do + { :ok, body, conn } -> + { :ok, %{ xml: body |> do_parse }, conn } + { :more, _data, conn } -> + { :error, :too_large, conn } + end + end + def parse(conn, _type, _subtype, _headers, _opts) do + { :next, conn } + end + + defp do_parse(xml) do + :erlang.bitstring_to_list(xml) + |> :xmerl_scan.string + |> elem(0) + |> do_parse_nodes + end + + defp do_parse_nodes([]), do: [] + defp do_parse_nodes([ h | t ]) do + do_parse_nodes(h) ++ do_parse_nodes(t) + end + defp do_parse_nodes({ :xmlAttribute, name, _, _, _, _, _, _, value, _ }) do + [ { name, value |> to_string } ] + end + defp do_parse_nodes({ :xmlElement, name, _, _, _, _, _, attrs, els, _, _, _ }) do + value = els + |> do_parse_nodes + |> flatten + [ %{ name: name, + attr: attrs |> do_parse_nodes, + value: value } ] + end + defp do_parse_nodes({ :xmlText, _, _, _, value, _ }) do + string_value = value + |> to_string + |> String.strip + if string_value |> String.length > 0 do + [ string_value ] + else + [] + end + end + + defp flatten([]), do: [] + defp flatten(values) do + if Enum.all?(values, &(is_binary(&1))) do + [ values |> List.to_string ] + else + values + end + end +end \ No newline at end of file diff --git a/lib/sugar/router.ex b/lib/sugar/router.ex index 6609c4c..645103e 100644 --- a/lib/sugar/router.ex +++ b/lib/sugar/router.ex @@ -79,6 +79,7 @@ defmodule Sugar.Router do :urlencoded, :multipart ], json_decoder: JSEX + plug :copy_req_content_type end end @@ -87,11 +88,10 @@ defmodule Sugar.Router do # Plugs we want predefined but aren't necessary to be before # user-defined plugs defaults = [ { Plug.Head, [], true }, - { Plug.MethodOverride, [], true }, - { :copy_req_content_type, [], true }, + { Plug.MethodOverride, [], true }, { :match, [], true }, { :dispatch, [], true } ] - { conn, body } = Enum.reverse(defaults) ++ + { conn, body } = Enum.reverse(defaults) ++ Module.get_attribute(env.module, :plugs) |> Plug.Builder.compile @@ -107,7 +107,7 @@ defmodule Sugar.Router do defoverridable [init: 1, call: 2] def copy_req_content_type(conn, _opts) do - default = Application.get_env(:sugar, :default_content_type, "application/json; charset=utf-8") + default = Application.get_env(:sugar, :default_content_type, "text/html; charset=utf-8") content_type = case Plug.Conn.get_req_header conn, "content-type" do [content_type] -> content_type _ -> default diff --git a/test/sugar/request/https_only_test.exs b/test/sugar/request/https_only_test.exs new file mode 100644 index 0000000..e4ddfff --- /dev/null +++ b/test/sugar/request/https_only_test.exs @@ -0,0 +1,13 @@ +defmodule Sugar.Request.HttpsOnlyTest do + use ExUnit.Case, async: true + import Plug.Test + + test "translates json extension" do + opts = Sugar.Request.HttpsOnly.init([]) + conn = conn(:get, "/get.json") + |> Sugar.Request.HttpsOnly.call(opts) + + assert conn.status === 403 + assert conn.resp_body === "Forbidden" + end +end \ No newline at end of file diff --git a/test/sugar/request/parsers_test.exs b/test/sugar/request/parsers_test.exs new file mode 100644 index 0000000..6a6ea44 --- /dev/null +++ b/test/sugar/request/parsers_test.exs @@ -0,0 +1,56 @@ +defmodule Sugar.Request.ParsersTest do + use ExUnit.Case, async: true + import Plug.Test + + @parsers [ Sugar.Request.Parsers.XML ] + + def parse(conn, opts \\ []) do + opts = Keyword.put_new(opts, :parsers, @parsers) + Plug.Parsers.call(conn, Plug.Parsers.init(opts)) + end + + test "parses xml encoded bodies" do + headers = [{"content-type", "application/xml"}] + conn = parse(conn(:post, "/post", "baz", headers: headers)) + foo = conn.params.xml + |> Enum.find(fn node -> + node.name === :foo + end) + + assert foo.value |> hd === "baz" + end + + test "parses xml encoded bodies with xml nodes" do + headers = [{"content-type", "application/xml"}] + conn = parse(conn(:post, "/post", "", headers: headers)) + foo = conn.params.xml + |> Enum.find(fn node -> + node.name === :foo + end) + bar = foo.value |> hd + + assert foo.value |> Enum.count === 2 + assert bar.name === :bar + end + + test "parses xml encoded bodies with attributes" do + headers = [{"content-type", "application/xml"}] + conn = parse(conn(:post, "/post", "", headers: headers)) + foo = conn.params.xml + |> Enum.find(fn node -> + node.name === :foo + end) + + assert foo.attr[:bar] === "baz" + assert foo.attr[:id] === "1" + end + + test "xml parser errors when body too large" do + exception = assert_raise Plug.Parsers.RequestTooLargeError, fn -> + headers = [{"content-type", "application/xml"}] + parse(conn(:post, "/post", "baz", headers: headers), length: 5) + end + + assert Plug.Exception.status(exception) === 413 + end +end \ No newline at end of file