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