Packages
plug
1.10.1
1.20.3
1.20.2
1.20.1
retired
1.20.0
retired
1.19.5
1.19.4
1.19.3
1.19.2
1.19.1
1.19.0
1.18.5
1.18.4
1.18.3
1.18.2
1.18.1
1.18.0
1.17.4
1.17.3
1.17.2
1.17.1
1.17.0
1.16.6
1.16.5
1.16.4
1.16.3
1.16.2
1.16.1
1.16.0
1.15.6
1.15.5
1.15.4
1.15.3
1.15.2
1.15.1
1.15.0
1.14.2
1.14.1
1.14.0
1.13.6
1.13.5
1.13.4
1.13.3
1.13.2
1.13.1
retired
1.13.0
retired
1.12.1
1.12.0
1.11.1
1.11.0
1.10.4
1.10.3
1.10.2
1.10.1
1.10.0
1.9.0
1.8.3
1.8.2
1.8.1
1.8.0
1.7.2
1.7.1
1.7.0
1.6.4
1.6.3
1.6.2
1.6.1
1.6.0
1.5.1
1.5.0
1.5.0-rc.2
1.5.0-rc.1
1.5.0-rc.0
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.4.0-rc.0
1.3.6
1.3.5
1.3.4
1.3.3
1.3.2
1.3.1
1.3.0
1.2.6
1.2.5
1.2.4
1.2.3
1.2.2
1.2.1
1.2.0
1.2.0-rc.0
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.6
1.0.5
1.0.4
1.0.3
1.0.2
1.0.1
1.0.0
0.14.0
0.13.1
0.13.0
0.12.2
0.12.1
0.12.0
0.11.3
0.11.2
0.11.1
0.11.0
0.10.0
0.9.0
0.8.4
0.8.3
0.8.2
0.8.1
0.8.0
0.7.0
0.6.0
0.5.3
0.5.2
0.5.1
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
Compose web applications with functions
Security advisory:
This version has known vulnerabilities.
View advisories
Current section
Files
Jump to
Current section
Files
lib/plug/parsers/multipart.ex
defmodule Plug.Parsers.MULTIPART do
@moduledoc """
Parses multipart request body.
## Options
All options supported by `Plug.Conn.read_body/2` are also supported here.
They are repeated here for convenience:
* `:length` - sets the maximum number of bytes to read from the request,
defaults to 8_000_000 bytes. Unlike `Plug.Conn.read_body/2` supports
passing an MFA (`{module, function, args}`) which will be evaluated
on every request to determine the value.
* `:read_length` - sets the amount of bytes to read at one time from the
underlying socket to fill the chunk, defaults to 1_000_000 bytes
* `:read_timeout` - sets the timeout for each socket read, defaults to
15_000ms
So by default, `Plug.Parsers` will read 1_000_000 bytes at a time from the
socket with an overall limit of 8_000_000 bytes.
Besides the options supported by `Plug.Conn.read_body/2`, the multipart parser
also checks for:
* `:headers` - containing the same `:length`, `:read_length`
and `:read_timeout` options which are used explicitly for parsing multipart
headers.
* `:include_unnamed_parts_at` - string specifying a body parameter that can
hold a lists of body parts that didn't have a 'Content-Disposition' header.
For instance, `include_unnamed_parts_at: "_parts"` would result in
a body parameter `"_parts"`, containing a list of parts, each with `:body`
and `:headers` fields, like `[%{body: "{}", headers: [{"content-type", "application/json"}]}]`.
* `:validate_utf8` - specifies whether multipart body parts should be validated
as utf8 binaries. Defaults to true.
"""
@behaviour Plug.Parsers
def init(opts) do
# Remove the length from options as it would attempt
# to eagerly read the body on the limit value.
{limit, opts} = Keyword.pop(opts, :length, 8_000_000)
# The read length is now our effective length per call.
{read_length, opts} = Keyword.pop(opts, :read_length, 1_000_000)
opts = [length: read_length, read_length: read_length] ++ opts
# The header options are handled individually.
{headers_opts, opts} = Keyword.pop(opts, :headers, [])
{limit, headers_opts, opts}
end
def parse(conn, "multipart", subtype, _headers, opts_tuple)
when subtype in ["form-data", "mixed"] do
try do
parse_multipart(conn, opts_tuple)
rescue
# Do not ignore upload errors
e in [Plug.UploadError, Plug.Parsers.BadEncodingError] ->
reraise e, __STACKTRACE__
# All others are wrapped
e ->
reraise Plug.Parsers.ParseError.exception(exception: e), __STACKTRACE__
end
end
def parse(conn, _type, _subtype, _headers, _opts) do
{:next, conn}
end
## Multipart
defp parse_multipart(conn, {{module, fun, args}, header_opts, opts}) do
limit = apply(module, fun, args)
parse_multipart(conn, {limit, header_opts, opts})
end
defp parse_multipart(conn, {limit, headers_opts, opts}) do
read_result = Plug.Conn.read_part_headers(conn, headers_opts)
{:ok, limit, acc, conn} = parse_multipart(read_result, limit, opts, headers_opts, [])
if limit > 0 do
{:ok, Enum.reduce(acc, %{}, &Plug.Conn.Query.decode_pair/2), conn}
else
{:error, :too_large, conn}
end
end
defp parse_multipart({:ok, headers, conn}, limit, opts, headers_opts, acc) when limit >= 0 do
{conn, limit, acc} = parse_multipart_headers(headers, conn, limit, opts, acc)
read_result = Plug.Conn.read_part_headers(conn, headers_opts)
parse_multipart(read_result, limit, opts, headers_opts, acc)
end
defp parse_multipart({:ok, _headers, conn}, limit, _opts, _headers_opts, acc) do
{:ok, limit, acc, conn}
end
defp parse_multipart({:done, conn}, limit, _opts, _headers_opts, acc) do
{:ok, limit, acc, conn}
end
defp parse_multipart_headers(headers, conn, limit, opts, acc) do
case multipart_type(headers, opts) do
{:binary, name} ->
{:ok, limit, body, conn} =
parse_multipart_body(Plug.Conn.read_part_body(conn, opts), limit, opts, "")
if Keyword.get(opts, :validate_utf8, true) do
Plug.Conn.Utils.validate_utf8!(body, Plug.Parsers.BadEncodingError, "multipart body")
end
{conn, limit, [{name, body} | acc]}
{:part, name} ->
{:ok, limit, body, conn} =
parse_multipart_body(Plug.Conn.read_part_body(conn, opts), limit, opts, "")
{conn, limit, [{name, %{headers: headers, body: body}} | acc]}
{:file, name, path, %Plug.Upload{} = uploaded} ->
{:ok, file} = File.open(path, [:write, :binary, :delayed_write, :raw])
{:ok, limit, conn} =
parse_multipart_file(Plug.Conn.read_part_body(conn, opts), limit, opts, file)
:ok = File.close(file)
{conn, limit, [{name, uploaded} | acc]}
:skip ->
{conn, limit, acc}
end
end
defp parse_multipart_body({:more, tail, conn}, limit, opts, body)
when limit >= byte_size(tail) do
read_result = Plug.Conn.read_part_body(conn, opts)
parse_multipart_body(read_result, limit - byte_size(tail), opts, body <> tail)
end
defp parse_multipart_body({:more, tail, conn}, limit, _opts, body) do
{:ok, limit - byte_size(tail), body, conn}
end
defp parse_multipart_body({:ok, tail, conn}, limit, _opts, body)
when limit >= byte_size(tail) do
{:ok, limit - byte_size(tail), body <> tail, conn}
end
defp parse_multipart_body({:ok, tail, conn}, limit, _opts, body) do
{:ok, limit - byte_size(tail), body, conn}
end
defp parse_multipart_file({:more, tail, conn}, limit, opts, file)
when limit >= byte_size(tail) do
binwrite!(file, tail)
read_result = Plug.Conn.read_part_body(conn, opts)
parse_multipart_file(read_result, limit - byte_size(tail), opts, file)
end
defp parse_multipart_file({:more, tail, conn}, limit, _opts, _file) do
{:ok, limit - byte_size(tail), conn}
end
defp parse_multipart_file({:ok, tail, conn}, limit, _opts, file)
when limit >= byte_size(tail) do
binwrite!(file, tail)
{:ok, limit - byte_size(tail), conn}
end
defp parse_multipart_file({:ok, tail, conn}, limit, _opts, _file) do
{:ok, limit - byte_size(tail), conn}
end
## Helpers
defp binwrite!(device, contents) do
case IO.binwrite(device, contents) do
:ok ->
:ok
{:error, reason} ->
raise Plug.UploadError,
"could not write to file #{inspect(device)} during upload " <>
"due to reason: #{inspect(reason)}"
end
end
defp multipart_type(headers, opts) do
if disposition = get_header(headers, "content-disposition") do
multipart_type_from_disposition(headers, disposition)
else
multipart_type_from_unnamed(opts)
end
end
defp multipart_type_from_unnamed(opts) do
case Keyword.fetch(opts, :include_unnamed_parts_at) do
{:ok, name} when is_binary(name) -> {:part, name <> "[]"}
:error -> :skip
end
end
defp multipart_type_from_disposition(headers, disposition) do
with [_, params] <- :binary.split(disposition, ";"),
%{"name" => name} = params <- Plug.Conn.Utils.params(params) do
handle_disposition(params, name, headers)
else
_ -> :skip
end
end
defp handle_disposition(params, name, headers) do
case params do
%{"filename" => ""} ->
:skip
%{"filename" => filename} ->
path = Plug.Upload.random_file!("multipart")
content_type = get_header(headers, "content-type")
upload = %Plug.Upload{filename: filename, path: path, content_type: content_type}
{:file, name, path, upload}
%{"filename*" => ""} ->
:skip
%{"filename*" => "utf-8''" <> filename} ->
filename = URI.decode(filename)
Plug.Conn.Utils.validate_utf8!(
filename,
Plug.Parsers.BadEncodingError,
"multipart filename"
)
path = Plug.Upload.random_file!("multipart")
content_type = get_header(headers, "content-type")
upload = %Plug.Upload{filename: filename, path: path, content_type: content_type}
{:file, name, path, upload}
%{} ->
{:binary, name}
end
end
defp get_header(headers, key) do
case List.keyfind(headers, key, 0) do
{^key, value} -> value
nil -> nil
end
end
end