zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

parsers.ex (13394B)


      1 defmodule Plug.Parsers do
      2   defmodule RequestTooLargeError do
      3     @moduledoc """
      4     Error raised when the request is too large.
      5     """
      6 
      7     defexception message:
      8                    "the request is too large. If you are willing to process " <>
      9                      "larger requests, please give a :length to Plug.Parsers",
     10                  plug_status: 413
     11   end
     12 
     13   defmodule UnsupportedMediaTypeError do
     14     @moduledoc """
     15     Error raised when the request body cannot be parsed.
     16     """
     17 
     18     defexception media_type: nil, plug_status: 415
     19 
     20     def message(exception) do
     21       "unsupported media type #{exception.media_type}"
     22     end
     23   end
     24 
     25   defmodule BadEncodingError do
     26     @moduledoc """
     27     Raised when the request body contains bad encoding.
     28     """
     29 
     30     defexception message: nil, plug_status: 400
     31   end
     32 
     33   defmodule ParseError do
     34     @moduledoc """
     35     Error raised when the request body is malformed.
     36     """
     37 
     38     defexception exception: nil, plug_status: 400
     39 
     40     def message(%{exception: exception}) do
     41       "malformed request, a #{inspect(exception.__struct__)} exception was raised " <>
     42         "with message #{inspect(Exception.message(exception))}"
     43     end
     44   end
     45 
     46   @moduledoc """
     47   A plug for parsing the request body.
     48 
     49   It invokes a list of `:parsers`, which are activated based on the
     50   request content-type. Custom parsers are also supported by defining
     51   a module that implements the behaviour defined by this module.
     52 
     53   Once a connection goes through this plug, it will have `:body_params`
     54   set to the map of params parsed by one of the parsers listed in
     55   `:parsers` and `:params` set to the result of merging the `:body_params`
     56   and `:query_params`. In case `:query_params` have not yet been parsed,
     57   `Plug.Conn.fetch_query_params/2` is automatically invoked.
     58 
     59   This plug will raise `Plug.Parsers.UnsupportedMediaTypeError` by default if
     60   the request cannot be parsed by any of the given types and the MIME type has
     61   not been explicitly accepted with the `:pass` option.
     62 
     63   `Plug.Parsers.RequestTooLargeError` will be raised if the request goes over
     64   the given limit. The default length is 8MB and it can be customized by passing
     65   the `:length` option to the Plug. `:read_timeout` and `:read_length`, as
     66   described by `Plug.Conn.read_body/2`, are also supported.
     67 
     68   Parsers may raise a `Plug.Parsers.ParseError` if the request has a malformed
     69   body.
     70 
     71   This plug only parses the body if the request method is one of the following:
     72 
     73     * `POST`
     74     * `PUT`
     75     * `PATCH`
     76     * `DELETE`
     77 
     78   For requests with a different request method, this plug will only fetch the
     79   query params.
     80 
     81   ## Options
     82 
     83     * `:parsers` - a list of modules or atoms of built-in parsers to be
     84       invoked for parsing. These modules need to implement the behaviour
     85       outlined in this module.
     86 
     87     * `:pass` - an optional list of MIME type strings that are allowed
     88       to pass through. Any mime not handled by a parser and not explicitly
     89       listed in `:pass` will `raise UnsupportedMediaTypeError`. For example:
     90 
     91         * `["*/*"]` - never raises
     92         * `["text/html", "application/*"]` - doesn't raise for those values
     93         * `[]` - always raises (default)
     94 
     95     * `:query_string_length` - the maximum allowed size for query strings
     96 
     97     * `:validate_utf8` - boolean that tells whether or not we want to
     98         validate that parsed binaries are utf8 strings.
     99 
    100     * `:body_reader` - an optional replacement (or wrapper) for
    101       `Plug.Conn.read_body/2` to provide a function that gives access to the
    102       raw body before it is parsed and discarded. It is in the standard format
    103       of `{Module, :function, [args]}` (MFA) and defaults to
    104       `{Plug.Conn, :read_body, []}`. Note that this option is not used by
    105       `Plug.Parsers.MULTIPART` which relies instead on other functions defined
    106       in `Plug.Conn`.
    107 
    108   All other options given to this Plug are forwarded to the parsers.
    109 
    110   ## Examples
    111 
    112       plug Plug.Parsers,
    113            parsers: [:urlencoded, :multipart],
    114            pass: ["text/*"]
    115 
    116   Any other option given to Plug.Parsers is forwarded to the underlying
    117   parsers. Therefore, you can use a JSON parser and pass the `:json_decoder`
    118   option at the root:
    119 
    120       plug Plug.Parsers,
    121            parsers: [:urlencoded, :json],
    122            json_decoder: Jason
    123 
    124   Or directly to the parser itself:
    125 
    126       plug Plug.Parsers,
    127            parsers: [:urlencoded, {:json, json_decoder: Jason}]
    128 
    129   It is also possible to pass the `:json_decoder` as a `{module, function, args}` tuple,
    130   useful for passing options to the JSON decoder:
    131 
    132       plug Plug.Parsers,
    133            parsers: [:json],
    134            json_decoder: {Jason, :decode!, [[floats: :decimals]]}
    135 
    136   A common set of shared options given to Plug.Parsers is `:length`,
    137   `:read_length` and `:read_timeout`, which customizes the maximum
    138   request length you want to accept. For example, to support file
    139   uploads, you can do:
    140 
    141       plug Plug.Parsers,
    142            parsers: [:urlencoded, :multipart],
    143            length: 20_000_000
    144 
    145   However, the above will increase the maximum length of all request
    146   types. If you want to increase the limit only for multipart requests
    147   (which is typically the ones used for file uploads), you can do:
    148 
    149       plug Plug.Parsers,
    150            parsers: [
    151              :urlencoded,
    152              {:multipart, length: 20_000_000} # Increase to 20MB max upload
    153            ]
    154 
    155   ## Built-in parsers
    156 
    157   Plug ships with the following parsers:
    158 
    159     * `Plug.Parsers.URLENCODED` - parses `application/x-www-form-urlencoded`
    160       requests (can be used as `:urlencoded` as well in the `:parsers` option)
    161     * `Plug.Parsers.MULTIPART` - parses `multipart/form-data` and
    162       `multipart/mixed` requests (can be used as `:multipart` as well in the
    163       `:parsers` option)
    164     * `Plug.Parsers.JSON` - parses `application/json` requests with the given
    165       `:json_decoder` (can be used as `:json` as well in the `:parsers` option)
    166 
    167   ## File handling
    168 
    169   If a file is uploaded via any of the parsers, Plug will
    170   stream the uploaded contents to a file in a temporary directory in order to
    171   avoid loading the whole file into memory. For such, the `:plug` application
    172   needs to be started in order for file uploads to work. More details on how the
    173   uploaded file is handled can be found in the documentation for `Plug.Upload`.
    174 
    175   When a file is uploaded, the request parameter that identifies that file will
    176   be a `Plug.Upload` struct with information about the uploaded file (e.g.
    177   filename and content type) and about where the file is stored.
    178 
    179   The temporary directory where files are streamed to can be customized by
    180   setting the `PLUG_TMPDIR` environment variable on the host system. If
    181   `PLUG_TMPDIR` isn't set, Plug will look at some environment
    182   variables which usually hold the value of the system's temporary directory
    183   (like `TMPDIR` or `TMP`). If no value is found in any of those variables,
    184   `/tmp` is used as a default.
    185 
    186   ## Custom body reader
    187 
    188   Sometimes you may want to customize how a parser reads the body from the
    189   connection. For example, you may want to cache the body to perform verification
    190   later, such as HTTP Signature Verification. This can be achieved with a custom
    191   body reader that would read the body and store it in the connection, such as:
    192 
    193       defmodule CacheBodyReader do
    194         def read_body(conn, opts) do
    195           {:ok, body, conn} = Plug.Conn.read_body(conn, opts)
    196           conn = update_in(conn.assigns[:raw_body], &[body | (&1 || [])])
    197           {:ok, body, conn}
    198         end
    199       end
    200 
    201   which could then be set as:
    202 
    203       plug Plug.Parsers,
    204         parsers: [:urlencoded, :json],
    205         pass: ["text/*"],
    206         body_reader: {CacheBodyReader, :read_body, []},
    207         json_decoder: Jason
    208 
    209   """
    210 
    211   alias Plug.Conn
    212 
    213   @callback init(opts :: Keyword.t()) :: Plug.opts()
    214 
    215   @doc """
    216   Attempts to parse the connection's request body given the content-type type,
    217   subtype, and its parameters.
    218 
    219   The arguments are:
    220 
    221     * the `Plug.Conn` connection
    222     * `type`, the content-type type (e.g., `"x-sample"` for the
    223       `"x-sample/json"` content-type)
    224     * `subtype`, the content-type subtype (e.g., `"json"` for the
    225       `"x-sample/json"` content-type)
    226     * `params`, the content-type parameters (e.g., `%{"foo" => "bar"}`
    227       for the `"text/plain; foo=bar"` content-type)
    228 
    229   This function should return:
    230 
    231     * `{:ok, body_params, conn}` if the parser is able to handle the given
    232       content-type; `body_params` should be a map
    233     * `{:next, conn}` if the next parser should be invoked
    234     * `{:error, :too_large, conn}` if the request goes over the given limit
    235 
    236   """
    237   @callback parse(
    238               conn :: Conn.t(),
    239               type :: binary,
    240               subtype :: binary,
    241               params :: Conn.Utils.params(),
    242               opts :: Plug.opts()
    243             ) ::
    244               {:ok, Conn.params(), Conn.t()}
    245               | {:error, :too_large, Conn.t()}
    246               | {:next, Conn.t()}
    247 
    248   @behaviour Plug
    249   @methods ~w(POST PUT PATCH DELETE)
    250 
    251   @impl true
    252   def init(opts) do
    253     {parsers, opts} = Keyword.pop(opts, :parsers)
    254     {pass, opts} = Keyword.pop(opts, :pass, [])
    255     {query_string_length, opts} = Keyword.pop(opts, :query_string_length, 1_000_000)
    256     validate_utf8 = Keyword.get(opts, :validate_utf8, true)
    257 
    258     unless parsers do
    259       raise ArgumentError, "Plug.Parsers expects a set of parsers to be given in :parsers"
    260     end
    261 
    262     {convert_parsers(parsers, opts), pass, query_string_length, validate_utf8}
    263   end
    264 
    265   defp convert_parsers(parsers, root_opts) do
    266     for parser <- parsers do
    267       {parser, opts} =
    268         case parser do
    269           {parser, opts} when is_atom(parser) and is_list(opts) ->
    270             {parser, Keyword.merge(root_opts, opts)}
    271 
    272           parser when is_atom(parser) ->
    273             {parser, root_opts}
    274         end
    275 
    276       module =
    277         case Atom.to_string(parser) do
    278           "Elixir." <> _ -> parser
    279           reference -> Module.concat(Plug.Parsers, String.upcase(reference))
    280         end
    281 
    282       # TODO: Remove this check in future releases once all parsers implement init/1 accordingly
    283       if Code.ensure_compiled(module) == {:module, module} and
    284            function_exported?(module, :init, 1) do
    285         {module, module.init(opts)}
    286       else
    287         {module, opts}
    288       end
    289     end
    290   end
    291 
    292   @impl true
    293   def call(%{method: method, body_params: %Plug.Conn.Unfetched{}} = conn, options)
    294       when method in @methods do
    295     {parsers, pass, query_string_length, validate_utf8} = options
    296     %{req_headers: req_headers} = conn
    297 
    298     conn =
    299       Conn.fetch_query_params(conn,
    300         length: query_string_length,
    301         validate_utf8: validate_utf8
    302       )
    303 
    304     case List.keyfind(req_headers, "content-type", 0) do
    305       {"content-type", ct} ->
    306         case Conn.Utils.content_type(ct) do
    307           {:ok, type, subtype, params} ->
    308             reduce(
    309               conn,
    310               parsers,
    311               type,
    312               subtype,
    313               params,
    314               pass,
    315               query_string_length,
    316               validate_utf8
    317             )
    318 
    319           :error ->
    320             reduce(conn, parsers, ct, "", %{}, pass, query_string_length, validate_utf8)
    321         end
    322 
    323       _ ->
    324         {conn, params} = merge_params(conn, %{}, query_string_length, validate_utf8)
    325 
    326         %{conn | params: params, body_params: %{}}
    327     end
    328   end
    329 
    330   def call(%{body_params: body_params} = conn, {_, _, query_string_length, validate_utf8}) do
    331     body_params = make_empty_if_unfetched(body_params)
    332     {conn, params} = merge_params(conn, body_params, query_string_length, validate_utf8)
    333     %{conn | params: params, body_params: body_params}
    334   end
    335 
    336   defp reduce(
    337          conn,
    338          [{parser, options} | rest],
    339          type,
    340          subtype,
    341          params,
    342          pass,
    343          query_string_length,
    344          validate_utf8
    345        ) do
    346     case parser.parse(conn, type, subtype, params, options) do
    347       {:ok, body, conn} ->
    348         {conn, params} = merge_params(conn, body, query_string_length, validate_utf8)
    349         %{conn | params: params, body_params: body}
    350 
    351       {:next, conn} ->
    352         reduce(conn, rest, type, subtype, params, pass, query_string_length, validate_utf8)
    353 
    354       {:error, :too_large, _conn} ->
    355         raise RequestTooLargeError
    356     end
    357   end
    358 
    359   defp reduce(conn, [], type, subtype, _params, pass, query_string_length, validate_utf8) do
    360     if accepted_mime?(type, subtype, pass) do
    361       {conn, params} = merge_params(conn, %{}, query_string_length, validate_utf8)
    362       %{conn | params: params}
    363     else
    364       raise UnsupportedMediaTypeError, media_type: "#{type}/#{subtype}"
    365     end
    366   end
    367 
    368   defp accepted_mime?(_type, _subtype, ["*/*"]),
    369     do: true
    370 
    371   defp accepted_mime?(type, subtype, pass),
    372     do: "#{type}/#{subtype}" in pass || "#{type}/*" in pass
    373 
    374   defp merge_params(conn, body_params, query_string_length, validate_utf8) do
    375     %{params: params, path_params: path_params} = conn
    376     params = make_empty_if_unfetched(params)
    377 
    378     conn =
    379       Plug.Conn.fetch_query_params(conn,
    380         length: query_string_length,
    381         validate_utf8: validate_utf8
    382       )
    383 
    384     {conn,
    385      conn.query_params
    386      |> Map.merge(params)
    387      |> Map.merge(body_params)
    388      |> Map.merge(path_params)}
    389   end
    390 
    391   defp make_empty_if_unfetched(%Plug.Conn.Unfetched{}), do: %{}
    392   defp make_empty_if_unfetched(params), do: params
    393 end