-
Notifications
You must be signed in to change notification settings - Fork 574
/
multipart.ex
310 lines (241 loc) · 9.64 KB
/
multipart.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
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
* `: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
* `:validate_utf8` - specifies whether multipart body parts should be validated
as utf8 binaries. Defaults to true
* `:multipart_to_params` - a MFA that receives the multipart headers and the
connection and it must return a tuple of `{:ok, params, conn}`
## Multipart to params
Once all multiparts are collected, they must be converted to params and this
can be customize with a MFA. The default implementation of this function
is equivalent to:
def multipart_to_params(parts, conn) do
acc =
for {name, _headers, body} <- Enum.reverse(parts),
name != nil,
reduce: Plug.Conn.Query.decode_init() do
acc -> Plug.Conn.Query.decode_each({name, body}, acc)
end
{:ok, Plug.Conn.Query.decode_done(acc), conn}
end
As you can notice, it discards all multiparts without a name. If you want
to keep the unnamed parts, you can store all of them under a known prefix,
such as:
def multipart_to_params(parts, conn) do
acc =
for {name, _headers, body} <- Enum.reverse(parts),
name != nil,
reduce: Plug.Conn.Query.decode_init() do
acc -> Plug.Conn.Query.decode_each({name || "_parts[]", body}, acc)
end
{:ok, Plug.Conn.Query.decode_done(acc), conn}
end
## Dynamic configuration
If you need to dynamically configure how `Plug.Parsers.MULTIPART` behave,
for example, based on the connection or another system parameter, one option
is to create your own parser that wraps it:
defmodule MyMultipart do
@multipart Plug.Parsers.MULTIPART
def init(opts) do
opts
end
def parse(conn, "multipart", subtype, headers, opts) do
length = System.fetch_env!("UPLOAD_LIMIT") |> String.to_integer
opts = @multipart.init([length: length] ++ opts)
@multipart.parse(conn, "multipart", subtype, headers, opts)
end
def parse(conn, _type, _subtype, _headers, _opts) do
{:next, conn}
end
end
"""
@behaviour Plug.Parsers
@impl true
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, [])
unless is_integer(limit) do
raise ":length option for Plug.Parsers.MULTIPART must be an integer"
end
m2p = opts[:multipart_to_params] || {__MODULE__, :multipart_to_params, [opts]}
{m2p, limit, headers_opts, opts}
end
@impl true
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
@doc false
def multipart_to_params(acc, conn, _opts) do
acc =
List.foldr(acc, Plug.Conn.Query.decode_init(), fn
{nil, _headers, _body}, acc -> acc
{name, _headers, body}, acc -> Plug.Conn.Query.decode_each({name, body}, acc)
end)
{:ok, Plug.Conn.Query.decode_done(acc), conn}
end
## Multipart
defp parse_multipart(conn, {m2p, {module, fun, args}, header_opts, opts}) do
# TODO: Remove me once the deprecation is removed
limit = apply(module, fun, args)
parse_multipart(conn, {m2p, limit, header_opts, opts})
end
defp parse_multipart(conn, {m2p, 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
{mod, fun, args} = m2p
apply(mod, fun, [acc, conn | args])
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) 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, headers, 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, headers, 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) do
with {_, disposition} <- List.keyfind(headers, "content-disposition", 0),
[_, params] <- :binary.split(disposition, ";"),
%{"name" => name} = params <- Plug.Conn.Utils.params(params) do
handle_disposition(params, name, headers)
else
_ -> {:binary, nil}
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