Only allowing XML requests

I am trying to create something that can both serve html pages, json api endpoints and some xml endpoints.

At this point I am focussing on the xml endpoint. It’s a /POST request to a single function (so not a controller/resource). I expect a XML body and the header Content-Type set to “application/xml” or “xml”. If that’s not the case I don’t want the route getting hit/matched/triggered.

So far I removed this part from my endpoint.ex:

  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json],
    pass: ["*/*"],
    json_decoder: Poison

I added the above inside a common pipeline in my routes.ex file. I use common pipeline beside my :browser and :api (json) pipelines:

  pipeline :common do
    plug Plug.Parsers,
      parsers: [:urlencoded, :multipart, :json],
      pass: ["*/*"],
      json_decoder: Poison
  end

Partial (example code) of my routes file:

  pipeline :ot do
    plug :accepts, ["xml"]
    plug Plug.Parsers, parsers: [:xml], pass: ["xml"]#, xml_decoder: :Erlsom
  end

  scope "/", Ot.Web do
    pipe_through :xml

    post "/foobar/:id", FooController, :bar
  end

When I change plug Plug.Parsers, parsers: [:xml], pass: ["xml"] to for example plug Plug.Parsers, parsers: [:xml], pass: [] it still lets through every call even though the doc says it would raise for every request?

The above setup does work if I set my Content-Type header to something like “foobar/asd”. The key part seems to be the “/”. Anything else like leaving it empty or strings like “foobar” still let the request hit the method.

Any idea’s? I probably could write a plug that check if the header Content-Type is set and that it is equal to [“xml”, “application/xml”], but I think the above should work?

Did you created a Plug.Parsers.XML, Plug only have default parsers to json, multipart and urlencoded, I think this doc can help you:
https://hexdocs.pm/plug/Plug.Parsers.html

Yes I did. Atleast at the moment it’s a placeholder.

defmodule Plug.Parsers.XML do

  @behaviour Plug.Parsers
  import Plug.Conn

  def parse(conn, "application", subtype, _headers, opts) do
    if subtype == "xml" || String.ends_with?(subtype, "+xml") do
      decoder = Keyword.get(opts, :xml_decoder) ||
                  raise ArgumentError, "XML parser expects a :xml_decoder option"
      conn
      |> read_body(opts)
      |> decode(decoder)
    else
      {:next, conn}
    end
  end

  def parse(conn, _type, _subtype, _headers, _opts) do
    {:next, conn}
  end

  defp decode({:more, _, conn}, _decoder) do
    {:error, :too_large, conn}
  end

  defp decode({:error, :timeout}, _decoder) do
    raise Plug.TimeoutError
  end

  defp decode({:ok, "", conn}, _decoder) do
    {:ok, nil, conn}
  end

  defp decode({:ok, body, conn}, decoder) do
    {:ok, %{"it" => "works"}, conn}
  rescue
    e -> raise Plug.Parsers.ParseError, exception: e
  end
end

I am leaving out the :xml_decoder opts so it should raise an error. That way I know it’s trying to decode, but this doesn’t aways happen because of the above.

the pass option should be ["text/xml"] no?

edit: I think it was not clear but, your parse implementation expect application only. anything else it pass to next.

If I send a request with Content-Type: text/xml and the pass equally I get a “could not find a matching Ot.Web.foobar” because the url parameter was not extracted out of the path. I had this:

  def bar(conn, %{"id" => id}) do

And I add this in front to see what was actually send:

  def bar(conn, x) do
    IEx.pry
    # ...
end

x was in this case empty.

I need to actually look at the list of available mime types and how they are mapped. Will have a look at them in a sec.

I got it following the steps below:

  1. Change Plug.Parses configuration in endpoint.ex.
  plug Plug.Parsers,
    parsers: [:urlencoded, :multipart, :json, :xml],
    pass: ["*/*"],
    json_decoder: Poison,
    xml_decoder: :xmerl_scan
  1. Like you, I implemented a XML parser too, but I just take care about subtype parameter.
defmodule Plug.Parsers.XML do

  @behaviour Plug.Parsers
  import Plug.Conn

  def parse(conn, _, "xml", _headers, opts) do
    decoder = Keyword.get(opts, :xml_decoder) || raise ArgumentError, "XML parser expects a :xml_decoder option"
    conn
    |> read_body(opts)
    |> decode(decoder)
  end

  def parse(conn, _type, _subtype, _headers, _opts) do
    {:next, conn}
  end

  defp decode({:ok, body, conn}, decoder) do
    case decoder.string(String.to_charlist(body)) do
      {parsed, []} ->
        {:ok, %{xml: parsed}, conn}
      error ->
        raise "Malformed XML #{error}"
    end
  rescue
    e -> raise Plug.Parsers.ParseError, exception: e
  end

end

After these steps, the XML becomes parsed in my Plug connections:

%Plug.Conn{adapter: {Plug.Adapters.Cowboy.Conn, :...}, assigns: %{},
 before_send: [#Function<1.20934668/1 in Plug.Logger.call/2>,
  #Function<0.111727833/1 in Phoenix.LiveReloader.before_send_inject_reloader/2>],
 body_params: %{xml: {:xmlElement, :"soapenv:Envelope", :"soapenv:Envelope",
    {'soapenv', 'Envelope'},
    {:xmlNamespace, [],
     [{'soapenv', :"http://schemas.xmlsoap.org/soap/envelope/"},
      {'use', :"http://user.auth.ws.util.componente/"}]}, [], 1,
    [{:xmlAttribute, :"xmlns:soapenv", [], {'xmlns', 'soapenv'}, [],
      ["soapenv:Envelope": 1], 1, [],
      'http://schemas.xmlsoap.org/soap/envelope/', false},
     {:xmlAttribute, :"xmlns:use", [], {'xmlns', 'use'}, [],
      ["soapenv:Envelope": 1], 2, [], 'http://user.auth.ws.util.componente/',
      false}],
    [{:xmlText, ["soapenv:Envelope": 1], 1, [], '\n   ', :text},
     {:xmlElement, :"soapenv:Header", :"soapenv:Header", {'soapenv', 'Header'},
      {:xmlNamespace, [],
       [{'soapenv', :"http://schemas.xmlsoap.org/soap/envelope/"},
        {'use', :"http://user.auth.ws.util.componente/"}]},
      ["soapenv:Envelope": 1], 2, [], [], [], '/Users/castilho/elixir/argonath',
      :undeclared}, {:xmlText, ["soapenv:Envelope": 1], 3, [], '\n   ', :text},
     {:xmlElement, :"soapenv:Body", :"soapenv:Body", {'soapenv', 'Body'},
      {:xmlNamespace, [],
       [{'soapenv', :"http://schemas.xmlsoap.org/soap/envelope/"},
        {'use', :"http://user.auth.ws.util.componente/"}]},
      ["soapenv:Envelope": 1], 4, [],
      [{:xmlText, ["soapenv:Body": 4, "soapenv:Envelope": 1], 1, [], '\n      ',
        :text},
       {:xmlElement, :"use:signin", :"use:signin", {'use', 'signin'},
        {:xmlNamespace, [],
         [{'soapenv', :"http://schemas.xmlsoap.org/soap/envelope/"},
          {'use', :"http://user.auth.ws.util.componente/"}]},
        ["soapenv:Body": 4, "soapenv:Envelope": 1], 2, [],
        [{:xmlText, ["use:signin": 2, "soapenv:Body": 4, "soapenv:Envelope": 1],
          1, [], '\n         ', :text},
         {:xmlElement, :username, :username, [],
          {:xmlNamespace, [],
           [{'soapenv', :"http://schemas.xmlsoap.org/soap/envelope/"},
            {'use', :"http://user.auth.ws.util.componente/"}]},
          ["use:signin": 2, "soapenv:Body": 4, "soapenv:Envelope": 1], 2, [],
          [{:xmlText, [...], ...}], [], '/Users/castilho/elixir/argonath', ...},
         {:xmlText, ["use:signin": 2, "soapenv:Body": 4, "soapenv:Envelope": 1],
                   3, [], '\n         ', :text},
         {:xmlElement, :password, :password, [],
          {:xmlNamespace, [], [{'soapenv', ...}, {'use', ...}]},
          ["use:signin": 2, "soapenv:Body": 4, "soapenv:Envelope": 1], 4, [],
          [...], ...},
         {:xmlText, ["use:signin": 2, "soapenv:Body": 4, "soapenv:Envelope": 1],
          5, [], '\n      ', :text}], [], '/Users/castilho/elixir/argonath',
        :undeclared},
       {:xmlText, ["soapenv:Body": 4, "soapenv:Envelope": 1], 3, [], '\n   ',
        :text}], [], '/Users/castilho/elixir/argonath', :undeclared},
     {:xmlText, ["soapenv:Envelope": 1], 5, [], '\n', :text}], [],
    '/Users/castilho/elixir/argonath', :undeclared}},
 cookies: %Plug.Conn.Unfetched{aspect: :cookies}, halted: false,
 host: "localhost", method: "POST", owner: #PID<0.413.0>,
 params: %{:xml => {:xmlElement, :"soapenv:Envelope", :"soapenv:Envelope",
    {'soapenv', 'Envelope'},
    {:xmlNamespace, [],
     [{'soapenv', :"http://schemas.xmlsoap.org/soap/envelope/"},
      {'use', :"http://user.auth.ws.util.componente/"}]}, [], 1,
    [{:xmlAttribute, :"xmlns:soapenv", [], {'xmlns', 'soapenv'}, [],
      ["soapenv:Envelope": 1], 1, [],
      'http://schemas.xmlsoap.org/soap/envelope/', false},
     {:xmlAttribute, :"xmlns:use", [], {'xmlns', 'use'}, [],
      ["soapenv:Envelope": 1], 2, [], 'http://user.auth.ws.util.componente/',
      false}],
    [{:xmlText, ["soapenv:Envelope": 1], 1, [], '\n   ', :text},
     {:xmlElement, :"soapenv:Header", :"soapenv:Header", {'soapenv', 'Header'},
      {:xmlNamespace, [],
       [{'soapenv', :"http://schemas.xmlsoap.org/soap/envelope/"},
        {'use', :"http://user.auth.ws.util.componente/"}]},
      ["soapenv:Envelope": 1], 2, [], [], [], '/Users/castilho/elixir/argonath',
      :undeclared}, {:xmlText, ["soapenv:Envelope": 1], 3, [], '\n   ', :text},
     {:xmlElement, :"soapenv:Body", :"soapenv:Body", {'soapenv', 'Body'},
      {:xmlNamespace, [],
       [{'soapenv', :"http://schemas.xmlsoap.org/soap/envelope/"},
        {'use', :"http://user.auth.ws.util.componente/"}]},
      ["soapenv:Envelope": 1], 4, [],
      [{:xmlText, ["soapenv:Body": 4, "soapenv:Envelope": 1], 1, [], '\n      ',
        :text},
       {:xmlElement, :"use:signin", :"use:signin", {'use', 'signin'},
        {:xmlNamespace, [],
         [{'soapenv', :"http://schemas.xmlsoap.org/soap/envelope/"},
          {'use', :"http://user.auth.ws.util.componente/"}]},
        ["soapenv:Body": 4, "soapenv:Envelope": 1], 2, [],
        [{:xmlText, ["use:signin": 2, "soapenv:Body": 4, "soapenv:Envelope": 1],
         1, [], '\n         ', ...},
         {:xmlElement, :username, :username, [], ...},
         {:xmlText, ["use:signin": 2, ...], 3, ...},
         {:xmlElement, :password, ...}, {:xmlText, [...], ...}], [],
        '/Users/castilho/elixir/argonath', :undeclared},
       {:xmlText, ["soapenv:Body": 4, "soapenv:Envelope": 1], 3, [], '\n   ',
        :text}], [], '/Users/castilho/elixir/argonath', :undeclared},
     {:xmlText, ["soapenv:Envelope": 1], 5, [], '\n', :text}], [],
    '/Users/castilho/elixir/argonath', :undeclared},
   "path" => ["AutenticadorUsuario", "AutenticadorUsuarioService"]},
 path_info: ["AutenticadorUsuario", "AutenticadorUsuarioService"],
 path_params: %{}, peer: {{127, 0, 0, 1}, 64668}, port: 4000,
 private: %{Argonath.Router => {[], %{}},
   :phoenix_endpoint => Argonath.Endpoint, :phoenix_format => "xml",
   :phoenix_layout => {Argonath.LayoutView, :simple},
   :phoenix_pipelines => [:api],
   :phoenix_route => #Function<15.41572747/1 in Argonath.Router.match_route/4>,
   :phoenix_router => Argonath.Router,
   :plug_session_fetch => #Function<1.131660147/1 in Plug.Session.fetch_session/1>},
 query_params: %{}, query_string: "", remote_ip: {127, 0, 0, 1},
 req_cookies: %Plug.Conn.Unfetched{aspect: :cookies},
 req_headers: [{"host", "localhost:4000"}, {"connection", "keep-alive"},
  {"content-length", "312"},
  {"postman-token", "83a68e53-bc9b-fb4c-3946-22d69cef413c"},
  {"cache-control", "no-cache"},
  {"origin", "chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop"},
  {"soapaction", "\"\""},
  {"user-agent",
   "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36"},
  {"content-type", "text/xml"}, {"accept", "*/*"},
  {"accept-encoding", "gzip, deflate, br"},
  {"accept-language", "en-US,en;q=0.8,pt;q=0.6"}],
 request_path: "/AutenticadorUsuario/AutenticadorUsuarioService",
 resp_body: nil, resp_cookies: %{},
 resp_headers: [{"cache-control", "max-age=0, private, must-revalidate"},
  {"x-request-id", "qnat2ltjm71gfj9hkl8lc0q1p760li0k"}], scheme: :http,
 script_name: [],
 secret_key_base: "WPKMRrd5cIrStaiO1erZu9pW8j1I10gyXzrtU2hy4NR1mgf8U790m9liCIfWqTHP",
 state: :unset, status: nil}
1 Like