zf

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

multipart.ex (10233B)


      1 defmodule Plug.Parsers.MULTIPART do
      2   @moduledoc """
      3   Parses multipart request body.
      4 
      5   ## Options
      6 
      7   All options supported by `Plug.Conn.read_body/2` are also supported here.
      8   They are repeated here for convenience:
      9 
     10     * `:length` - sets the maximum number of bytes to read from the request,
     11       defaults to 8_000_000 bytes
     12 
     13     * `:read_length` - sets the amount of bytes to read at one time from the
     14       underlying socket to fill the chunk, defaults to 1_000_000 bytes
     15 
     16     * `:read_timeout` - sets the timeout for each socket read, defaults to
     17       15_000ms
     18 
     19   So by default, `Plug.Parsers` will read 1_000_000 bytes at a time from the
     20   socket with an overall limit of 8_000_000 bytes.
     21 
     22   Besides the options supported by `Plug.Conn.read_body/2`, the multipart parser
     23   also checks for:
     24 
     25     * `:headers` - containing the same `:length`, `:read_length`
     26       and `:read_timeout` options which are used explicitly for parsing multipart
     27       headers
     28 
     29     * `:validate_utf8` - specifies whether multipart body parts should be validated
     30       as utf8 binaries. Defaults to true
     31 
     32     * `:multipart_to_params` - a MFA that receives the multipart headers and the
     33       connection and it must return a tuple of `{:ok, params, conn}`
     34 
     35   ## Multipart to params
     36 
     37   Once all multiparts are collected, they must be converted to params and this
     38   can be customize with a MFA. The default implementation of this function
     39   is equivalent to:
     40 
     41       def multipart_to_params(parts, conn) do
     42         params =
     43           for {name, _headers, body} <- parts,
     44               name != nil,
     45               reduce: %{} do
     46             acc -> Plug.Conn.Query.decode_pair({name, body}, acc)
     47           end
     48 
     49         {:ok, params, conn}
     50       end
     51 
     52   As you can notice, it discards all multiparts without a name. If you want
     53   to keep the unnamed parts, you can store all of them under a known prefix,
     54   such as:
     55 
     56       def multipart_to_params(parts, conn) do
     57         params =
     58           for {name, _headers, body} <- parts, reduce: %{} do
     59             acc -> Plug.Conn.Query.decode_pair({name || "_parts[]", body}, acc)
     60           end
     61 
     62         {:ok, params, conn}
     63       end
     64 
     65   ## Dynamic configuration
     66 
     67   If you need to dynamically configure how `Plug.Parsers.MULTIPART` behave,
     68   for example, based on the connection or another system parameter, one option
     69   is to create your own parser that wraps it:
     70 
     71       defmodule MyMultipart do
     72         @multipart Plug.Parsers.MULTIPART
     73 
     74         def init(opts) do
     75           opts
     76         end
     77 
     78         def parse(conn, "multipart", subtype, headers, opts) do
     79           length = System.fetch_env!("UPLOAD_LIMIT") |> String.to_integer
     80           opts = @multipart.init([length: length] ++ opts)
     81           @multipart.parse(conn, "multipart", subtype, headers, opts)
     82         end
     83 
     84         def parse(conn, _type, _subtype, _headers, _opts) do
     85           {:next, conn}
     86         end
     87       end
     88 
     89   """
     90 
     91   @behaviour Plug.Parsers
     92 
     93   @impl true
     94   def init(opts) do
     95     # Remove the length from options as it would attempt
     96     # to eagerly read the body on the limit value.
     97     {limit, opts} = Keyword.pop(opts, :length, 8_000_000)
     98 
     99     # The read length is now our effective length per call.
    100     {read_length, opts} = Keyword.pop(opts, :read_length, 1_000_000)
    101     opts = [length: read_length, read_length: read_length] ++ opts
    102 
    103     # The header options are handled individually.
    104     {headers_opts, opts} = Keyword.pop(opts, :headers, [])
    105 
    106     with {_, _, _} <- limit do
    107       IO.warn(
    108         "passing a {module, function, args} tuple to Plug.Parsers.MULTIPART is deprecated. " <>
    109           "Please see Plug.Parsers.MULTIPART module docs for better approaches to configuration"
    110       )
    111     end
    112 
    113     if opts[:include_unnamed_parts_at] do
    114       IO.warn(
    115         ":include_unnamed_parts_at for multipart is deprecated. Use :multipart_to_params instead"
    116       )
    117     end
    118 
    119     m2p = opts[:multipart_to_params] || {__MODULE__, :multipart_to_params, [opts]}
    120     {m2p, limit, headers_opts, opts}
    121   end
    122 
    123   @impl true
    124   def parse(conn, "multipart", subtype, _headers, opts_tuple)
    125       when subtype in ["form-data", "mixed"] do
    126     try do
    127       parse_multipart(conn, opts_tuple)
    128     rescue
    129       # Do not ignore upload errors
    130       e in [Plug.UploadError, Plug.Parsers.BadEncodingError] ->
    131         reraise e, __STACKTRACE__
    132 
    133       # All others are wrapped
    134       e ->
    135         reraise Plug.Parsers.ParseError.exception(exception: e), __STACKTRACE__
    136     end
    137   end
    138 
    139   def parse(conn, _type, _subtype, _headers, _opts) do
    140     {:next, conn}
    141   end
    142 
    143   @doc false
    144   def multipart_to_params(acc, conn, opts) do
    145     unnamed_at = opts[:include_unnamed_parts_at]
    146 
    147     params =
    148       Enum.reduce(acc, %{}, fn
    149         {nil, headers, body}, acc when unnamed_at != nil ->
    150           Plug.Conn.Query.decode_pair(
    151             {unnamed_at <> "[]", %{headers: headers, body: body}},
    152             acc
    153           )
    154 
    155         {nil, _headers, _body}, acc ->
    156           acc
    157 
    158         {name, _headers, body}, acc ->
    159           Plug.Conn.Query.decode_pair({name, body}, acc)
    160       end)
    161 
    162     {:ok, params, conn}
    163   end
    164 
    165   ## Multipart
    166 
    167   defp parse_multipart(conn, {m2p, {module, fun, args}, header_opts, opts}) do
    168     # TODO: This is deprecated. Remove me on Plug 2.0.
    169     limit = apply(module, fun, args)
    170     parse_multipart(conn, {m2p, limit, header_opts, opts})
    171   end
    172 
    173   defp parse_multipart(conn, {m2p, limit, headers_opts, opts}) do
    174     read_result = Plug.Conn.read_part_headers(conn, headers_opts)
    175     {:ok, limit, acc, conn} = parse_multipart(read_result, limit, opts, headers_opts, [])
    176 
    177     if limit > 0 do
    178       {mod, fun, args} = m2p
    179       apply(mod, fun, [acc, conn | args])
    180     else
    181       {:error, :too_large, conn}
    182     end
    183   end
    184 
    185   defp parse_multipart({:ok, headers, conn}, limit, opts, headers_opts, acc) when limit >= 0 do
    186     {conn, limit, acc} = parse_multipart_headers(headers, conn, limit, opts, acc)
    187     read_result = Plug.Conn.read_part_headers(conn, headers_opts)
    188     parse_multipart(read_result, limit, opts, headers_opts, acc)
    189   end
    190 
    191   defp parse_multipart({:ok, _headers, conn}, limit, _opts, _headers_opts, acc) do
    192     {:ok, limit, acc, conn}
    193   end
    194 
    195   defp parse_multipart({:done, conn}, limit, _opts, _headers_opts, acc) do
    196     {:ok, limit, acc, conn}
    197   end
    198 
    199   defp parse_multipart_headers(headers, conn, limit, opts, acc) do
    200     case multipart_type(headers) do
    201       {:binary, name} ->
    202         {:ok, limit, body, conn} =
    203           parse_multipart_body(Plug.Conn.read_part_body(conn, opts), limit, opts, "")
    204 
    205         if Keyword.get(opts, :validate_utf8, true) do
    206           Plug.Conn.Utils.validate_utf8!(body, Plug.Parsers.BadEncodingError, "multipart body")
    207         end
    208 
    209         {conn, limit, [{name, headers, body} | acc]}
    210 
    211       {:file, name, path, %Plug.Upload{} = uploaded} ->
    212         {:ok, file} = File.open(path, [:write, :binary, :delayed_write, :raw])
    213 
    214         {:ok, limit, conn} =
    215           parse_multipart_file(Plug.Conn.read_part_body(conn, opts), limit, opts, file)
    216 
    217         :ok = File.close(file)
    218         {conn, limit, [{name, headers, uploaded} | acc]}
    219 
    220       :skip ->
    221         {conn, limit, acc}
    222     end
    223   end
    224 
    225   defp parse_multipart_body({:more, tail, conn}, limit, opts, body)
    226        when limit >= byte_size(tail) do
    227     read_result = Plug.Conn.read_part_body(conn, opts)
    228     parse_multipart_body(read_result, limit - byte_size(tail), opts, body <> tail)
    229   end
    230 
    231   defp parse_multipart_body({:more, tail, conn}, limit, _opts, body) do
    232     {:ok, limit - byte_size(tail), body, conn}
    233   end
    234 
    235   defp parse_multipart_body({:ok, tail, conn}, limit, _opts, body)
    236        when limit >= byte_size(tail) do
    237     {:ok, limit - byte_size(tail), body <> tail, conn}
    238   end
    239 
    240   defp parse_multipart_body({:ok, tail, conn}, limit, _opts, body) do
    241     {:ok, limit - byte_size(tail), body, conn}
    242   end
    243 
    244   defp parse_multipart_file({:more, tail, conn}, limit, opts, file)
    245        when limit >= byte_size(tail) do
    246     binwrite!(file, tail)
    247     read_result = Plug.Conn.read_part_body(conn, opts)
    248     parse_multipart_file(read_result, limit - byte_size(tail), opts, file)
    249   end
    250 
    251   defp parse_multipart_file({:more, tail, conn}, limit, _opts, _file) do
    252     {:ok, limit - byte_size(tail), conn}
    253   end
    254 
    255   defp parse_multipart_file({:ok, tail, conn}, limit, _opts, file)
    256        when limit >= byte_size(tail) do
    257     binwrite!(file, tail)
    258     {:ok, limit - byte_size(tail), conn}
    259   end
    260 
    261   defp parse_multipart_file({:ok, tail, conn}, limit, _opts, _file) do
    262     {:ok, limit - byte_size(tail), conn}
    263   end
    264 
    265   ## Helpers
    266 
    267   defp binwrite!(device, contents) do
    268     case IO.binwrite(device, contents) do
    269       :ok ->
    270         :ok
    271 
    272       {:error, reason} ->
    273         raise Plug.UploadError,
    274               "could not write to file #{inspect(device)} during upload " <>
    275                 "due to reason: #{inspect(reason)}"
    276     end
    277   end
    278 
    279   defp multipart_type(headers) do
    280     with {_, disposition} <- List.keyfind(headers, "content-disposition", 0),
    281          [_, params] <- :binary.split(disposition, ";"),
    282          %{"name" => name} = params <- Plug.Conn.Utils.params(params) do
    283       handle_disposition(params, name, headers)
    284     else
    285       _ -> {:binary, nil}
    286     end
    287   end
    288 
    289   defp handle_disposition(params, name, headers) do
    290     case params do
    291       %{"filename" => ""} ->
    292         :skip
    293 
    294       %{"filename" => filename} ->
    295         path = Plug.Upload.random_file!("multipart")
    296         content_type = get_header(headers, "content-type")
    297         upload = %Plug.Upload{filename: filename, path: path, content_type: content_type}
    298         {:file, name, path, upload}
    299 
    300       %{"filename*" => ""} ->
    301         :skip
    302 
    303       %{"filename*" => "utf-8''" <> filename} ->
    304         filename = URI.decode(filename)
    305 
    306         Plug.Conn.Utils.validate_utf8!(
    307           filename,
    308           Plug.Parsers.BadEncodingError,
    309           "multipart filename"
    310         )
    311 
    312         path = Plug.Upload.random_file!("multipart")
    313         content_type = get_header(headers, "content-type")
    314         upload = %Plug.Upload{filename: filename, path: path, content_type: content_type}
    315         {:file, name, path, upload}
    316 
    317       %{} ->
    318         {:binary, name}
    319     end
    320   end
    321 
    322   defp get_header(headers, key) do
    323     case List.keyfind(headers, key, 0) do
    324       {^key, value} -> value
    325       nil -> nil
    326     end
    327   end
    328 end