Skip to content

Commit c52b2f3

Browse files
author
José Valim
committed
Add Plug.Conn.read_part_headers/2 and read_part_body/2
1 parent 142ec1a commit c52b2f3

10 files changed

Lines changed: 781 additions & 263 deletions

File tree

lib/plug/adapters/cowboy/conn.ex

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -62,28 +62,6 @@ defmodule Plug.Adapters.Cowboy.Conn do
6262
:cowboy_req.body(req, opts)
6363
end
6464

65-
def parse_req_multipart(req, opts, callback) do
66-
# We need to remove the length from the list
67-
# otherwise cowboy will attempt to load the
68-
# whole length at once.
69-
{limit, opts} = Keyword.pop(opts, :length, 8_000_000)
70-
71-
# We need to construct the header opts using defaults here,
72-
# since once opts are passed cowboy defaults are not applied anymore.
73-
{headers_opts, opts} = Keyword.pop(opts, :headers, [])
74-
headers_opts = headers_opts ++ [length: 64_000, read_length: 64_000, read_timeout: 5000]
75-
76-
{:ok, limit, acc, req} = parse_multipart(:cowboy_req.part(req, headers_opts), limit, opts, headers_opts, [], callback)
77-
78-
params = Enum.reduce(acc, %{}, &Plug.Conn.Query.decode_pair/2)
79-
80-
if limit > 0 do
81-
{:ok, params, req}
82-
else
83-
{:more, params, req}
84-
end
85-
end
86-
8765
## Helpers
8866

8967
defp scheme(:tcp), do: :http
@@ -93,74 +71,4 @@ defmodule Plug.Adapters.Cowboy.Conn do
9371
segments = :binary.split(path, "/", [:global])
9472
for segment <- segments, segment != "", do: segment
9573
end
96-
97-
## Multipart
98-
99-
defp parse_multipart({:ok, headers, req}, limit, opts, headers_opts, acc, callback) when limit >= 0 do
100-
case callback.(headers) do
101-
{:binary, name} ->
102-
{:ok, limit, body, req} =
103-
parse_multipart_body(:cowboy_req.part_body(req, opts), limit, opts, "")
104-
105-
Plug.Conn.Utils.validate_utf8!(body, Plug.Parsers.BadEncodingError, "multipart body")
106-
parse_multipart(:cowboy_req.part(req, headers_opts), limit, opts, headers_opts,
107-
[{name, body}|acc], callback)
108-
109-
{:file, name, path, %Plug.Upload{} = uploaded} ->
110-
{:ok, file} = File.open(path, [:write, :binary, :delayed_write, :raw])
111-
112-
{:ok, limit, req} =
113-
parse_multipart_file(:cowboy_req.part_body(req, opts), limit, opts, file)
114-
115-
:ok = File.close(file)
116-
parse_multipart(:cowboy_req.part(req, headers_opts), limit, opts, headers_opts,
117-
[{name, uploaded}|acc], callback)
118-
119-
:skip ->
120-
parse_multipart(:cowboy_req.part(req, headers_opts), limit, opts, headers_opts,
121-
acc, callback)
122-
end
123-
end
124-
125-
defp parse_multipart({:ok, _headers, req}, limit, _opts, _headers_opts, acc, _callback) do
126-
{:ok, limit, acc, req}
127-
end
128-
129-
defp parse_multipart({:done, req}, limit, _opts, _headers_opts, acc, _callback) do
130-
{:ok, limit, acc, req}
131-
end
132-
133-
defp parse_multipart_body({:more, tail, req}, limit, opts, body) when limit >= byte_size(tail) do
134-
parse_multipart_body(:cowboy_req.part_body(req, opts), limit - byte_size(tail), opts, body <> tail)
135-
end
136-
137-
defp parse_multipart_body({:more, tail, req}, limit, _opts, body) do
138-
{:ok, limit - byte_size(tail), body, req}
139-
end
140-
141-
defp parse_multipart_body({:ok, tail, req}, limit, _opts, body) when limit >= byte_size(tail) do
142-
{:ok, limit - byte_size(tail), body <> tail, req}
143-
end
144-
145-
defp parse_multipart_body({:ok, tail, req}, limit, _opts, body) do
146-
{:ok, limit - byte_size(tail), body, req}
147-
end
148-
149-
defp parse_multipart_file({:more, tail, req}, limit, opts, file) when limit >= byte_size(tail) do
150-
IO.binwrite(file, tail)
151-
parse_multipart_file(:cowboy_req.part_body(req, opts), limit - byte_size(tail), opts, file)
152-
end
153-
154-
defp parse_multipart_file({:more, tail, req}, limit, _opts, _file) do
155-
{:ok, limit - byte_size(tail), req}
156-
end
157-
158-
defp parse_multipart_file({:ok, tail, req}, limit, _opts, file) when limit >= byte_size(tail) do
159-
IO.binwrite(file, tail)
160-
{:ok, limit - byte_size(tail), req}
161-
end
162-
163-
defp parse_multipart_file({:ok, tail, req}, limit, _opts, _file) do
164-
{:ok, limit - byte_size(tail), req}
165-
end
16674
end

lib/plug/adapters/test/conn.ex

Lines changed: 3 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -86,18 +86,6 @@ defmodule Plug.Adapters.Test.Conn do
8686
{tag, data, %{state | req_body: rest}}
8787
end
8888

89-
def parse_req_multipart(%{params: params} = state, _opts, _callback) do
90-
{:ok, params, state}
91-
end
92-
93-
def parse_req_multipart(%{req_body: multipart} = state, opts, callback) do
94-
boundary = Keyword.get(opts, :boundary)
95-
params = parse_multipart(:cow_multipart.parse_headers(multipart, boundary), boundary, [], callback)
96-
|> Enum.reduce(%{}, &Plug.Conn.Query.decode_pair/2)
97-
98-
{:ok, params, state}
99-
end
100-
10189
## Private helpers
10290

10391
defp body_or_params(nil, _query, headers),
@@ -112,10 +100,11 @@ defmodule Plug.Adapters.Test.Conn do
112100
end
113101

114102
defp body_or_params(params, query, headers) when is_map(params) do
115-
content_type = List.keyfind(headers, "content-type", 0, {"content-type", "multipart/mixed; charset: utf-8"})
103+
content_type = List.keyfind(headers, "content-type", 0,
104+
{"content-type", "multipart/mixed; boundary=plug_conn_test"})
116105
headers = List.keystore(headers, "content-type", 0, content_type)
117106
params = Map.merge(Plug.Conn.Query.decode(query), stringify_params(params))
118-
{"", params, headers}
107+
{"--plug_conn_test--", params, headers}
119108
end
120109

121110
defp stringify_params([{_, _}|_] = params),
@@ -146,27 +135,4 @@ defmodule Plug.Adapters.Test.Conn do
146135
0 -> :ok
147136
end
148137
end
149-
150-
defp parse_multipart({:ok, headers, body}, boundary, acc, callback) do
151-
{:done, content, rest} = :cow_multipart.parse_body(body, boundary)
152-
153-
case callback.(headers)do
154-
{:file, name, path, %Plug.Upload{} = uploaded} ->
155-
{:ok, file} = File.open(path, [:write, :binary, :delayed_write, :raw])
156-
IO.binwrite(file, content)
157-
File.close(file)
158-
159-
parse_multipart(:cow_multipart.parse_headers(rest, boundary), boundary, [{name, uploaded}|acc], callback)
160-
161-
{:binary, name} ->
162-
parse_multipart(:cow_multipart.parse_headers(rest, boundary), boundary, [{name, content}|acc], callback)
163-
164-
:skip ->
165-
parse_multipart(:cow_multipart.parse_headers(rest, boundary), boundary, acc, callback)
166-
end
167-
end
168-
169-
defp parse_multipart({:done, _rest}, _boundary, acc, _callback) do
170-
acc
171-
end
172138
end

lib/plug/conn.ex

Lines changed: 122 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -774,12 +774,12 @@ defmodule Plug.Conn do
774774
775775
## Options
776776
777-
* `:length` - sets the maximum number of bytes to read from the body for each
778-
chunk, defaults to 8_000_000 bytes
779-
* `:read_length` - sets the amount of bytes to read at one time from the
780-
underlying socket to fill the chunk, defaults to 1_000_000 bytes
781-
* `:read_timeout` - sets the timeout for each socket read, defaults to
782-
15_000 ms
777+
* `:length` - sets the maximum number of bytes to read from the body for
778+
each chunk, defaults to 8_000_000 bytes
779+
* `:read_length` - sets the amount of bytes to read at one time from the
780+
underlying socket to fill the chunk, defaults to 1_000_000 bytes
781+
* `:read_timeout` - sets the timeout for each socket read, defaults to
782+
15_000ms
783783
784784
The values above are not meant to be exact. For example, setting the
785785
length to 8_000_000 may end up reading some hundred bytes more from
@@ -804,6 +804,122 @@ defmodule Plug.Conn do
804804
end
805805
end
806806

807+
@doc """
808+
Reads the headers of a multipart request.
809+
810+
It returns `{:ok, headers, conn}` with the headers or
811+
`{:done, conn}` if there are no more parts.
812+
813+
Once `read_part_headers/2` is invoked, a developer may call
814+
`read_part_body/2` to read the body associated to the headers.
815+
If `read_part_headers/2` is called instead, the body is automatically
816+
skipped until the next part headers.
817+
818+
## Options
819+
820+
* `:length` - sets the maximum number of bytes to read from the body for
821+
each chunk, defaults to 64_000 bytes
822+
* `:read_length` - sets the amount of bytes to read at one time from the
823+
underlying socket to fill the chunk, defaults to 64_000 bytes
824+
* `:read_timeout` - sets the timeout for each socket read, defaults to
825+
5_000ms
826+
827+
"""
828+
@spec read_part_headers(t, Keyword.t) :: {:ok, headers, t} | {:done, t}
829+
def read_part_headers(%Conn{adapter: {adapter, state}} = conn, opts \\ []) do
830+
opts = opts ++ [length: 64_000, read_length: 64_000, read_timeout: 5000]
831+
case init_multipart(conn) do
832+
{boundary, buffer} ->
833+
{data, state} = read_multipart_from_buffer_or_adapter(buffer, adapter, state, opts)
834+
read_part_headers(conn, data, boundary, adapter, state, opts)
835+
:done ->
836+
{:done, conn}
837+
end
838+
end
839+
840+
defp read_part_headers(conn, data, boundary, adapter, state, opts) do
841+
case :plug_multipart.parse_headers(data, boundary) do
842+
{:ok, headers, rest} ->
843+
{:ok, headers, store_multipart(conn, {boundary, rest}, adapter, state)}
844+
:more ->
845+
{_, next, state} = next_multipart(adapter, state, opts)
846+
read_part_headers(conn, data <> next, boundary, adapter, state, opts)
847+
{:more, rest} ->
848+
{_, next, state} = next_multipart(adapter, state, opts)
849+
read_part_headers(conn, rest <> next, boundary, adapter, state, opts)
850+
{:done, _} ->
851+
{:done, store_multipart(conn, :done, adapter, state)}
852+
end
853+
end
854+
855+
@doc """
856+
Reads the body of a multipart request.
857+
858+
Returns `{:ok, body, conn}` if all body has been read,
859+
`{:more, binary, conn}` otherwise.
860+
861+
It accepts the same options as `read_body/2`.
862+
"""
863+
@spec read_part_body(t, Keyword.t) :: {:ok, binary, t} | {:more, binary, t}
864+
def read_part_body(%{adapter: {adapter, state}} = conn, opts) do
865+
case init_multipart(conn) do
866+
{boundary, buffer} ->
867+
length = Keyword.get(opts, :length, 8_000_000)
868+
{data, state} = read_multipart_from_buffer_or_adapter(buffer, adapter, state, opts)
869+
read_part_body(conn, data, "", length, boundary, adapter, state, opts)
870+
:done ->
871+
{:done, conn}
872+
end
873+
end
874+
875+
defp read_part_body(conn, data, acc, length, boundary, adapter, state, _opts) when byte_size(acc) > length do
876+
{:more, acc, store_multipart(conn, {boundary, data}, adapter, state)}
877+
end
878+
defp read_part_body(conn, data, acc, length, boundary, adapter, state, opts) do
879+
case :plug_multipart.parse_body(data, boundary) do
880+
{:ok, body} ->
881+
{_, next, state} = next_multipart(adapter, state, opts)
882+
read_part_body(conn, next, acc <> body, length, boundary, adapter, state, opts)
883+
{:ok, body, rest} ->
884+
{_, next, state} = next_multipart(adapter, state, opts)
885+
read_part_body(conn, rest <> next, acc <> body, length, boundary, adapter, state, opts)
886+
:done ->
887+
{:ok, acc, store_multipart(conn, {boundary, ""}, adapter, state)}
888+
{:done, body} ->
889+
{:ok, acc <> body, store_multipart(conn, {boundary, ""}, adapter, state)}
890+
{:done, body, rest} ->
891+
{:ok, acc <> body, store_multipart(conn, {boundary, rest}, adapter, state)}
892+
end
893+
end
894+
895+
defp init_multipart(%{private: %{plug_multipart: plug_multipart}}) do
896+
plug_multipart
897+
end
898+
defp init_multipart(%{req_headers: req_headers}) do
899+
{_, content_type} = List.keyfind(req_headers, "content-type", 0)
900+
{:ok, "multipart", _, %{"boundary" => boundary}} = Plug.Conn.Utils.content_type(content_type)
901+
{boundary, ""}
902+
end
903+
904+
defp next_multipart(adapter, state, opts) do
905+
case adapter.read_req_body(state, opts) do
906+
{:ok, "", _} -> raise "invalid multipart, body terminated too soon"
907+
valid -> valid
908+
end
909+
end
910+
911+
defp store_multipart(conn, multipart, adapter, state) do
912+
%{put_in(conn.private[:plug_multipart], multipart) | adapter: {adapter, state}}
913+
end
914+
915+
defp read_multipart_from_buffer_or_adapter("", adapter, state, opts) do
916+
{_, data, state} = adapter.read_req_body(state, opts)
917+
{data, state}
918+
end
919+
defp read_multipart_from_buffer_or_adapter(buffer, _adapter, state, _opts) do
920+
{buffer, state}
921+
end
922+
807923
@doc """
808924
Fetches cookies from the request headers.
809925
"""

lib/plug/conn/adapter.ex

Lines changed: 0 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,26 +72,4 @@ defmodule Plug.Conn.Adapter do
7272
{:ok, data :: binary, payload} |
7373
{:more, data :: binary, payload} |
7474
{:error, term}
75-
76-
@doc """
77-
Parses a multipart request.
78-
79-
This function receives the payload, the body limit and a callback.
80-
When parsing each multipart segment, the parser should invoke the
81-
given fallback passing the headers for that segment, before consuming
82-
the body. The callback will return one of the following values:
83-
84-
* `{:binary, name}` - the current segment must be treated as a regular
85-
binary value with the given `name`
86-
* `{:file, name, file, upload}` - the current segment is a file upload with `name`
87-
and contents should be written to the given `file`
88-
* `:skip` - this multipart segment should be skipped
89-
90-
This function may return a `:ok` or `:more` tuple. The first one is
91-
returned when there is no more multipart data to be processed.
92-
93-
For the supported options, please read `Plug.Conn.read_body/2` docs.
94-
"""
95-
@callback parse_req_multipart(payload, options :: Keyword.t, fun) ::
96-
{:ok, Conn.params, payload} | {:more, Conn.params, payload}
9775
end

lib/plug/parsers.ex

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,13 @@ defmodule Plug.Parsers do
104104
105105
Plug ships with the following parsers:
106106
107-
* `Plug.Parsers.URLENCODED` - parses `application/x-www-form-urlencoded`
108-
requests (can be used as `:urlencoded` as well in the `:parsers` option)
109-
* `Plug.Parsers.MULTIPART` - parses `multipart/form-data` and
110-
`multipart/mixed` requests (can be used as `:multipart` as well in the
111-
`:parsers` option)
112-
* `Plug.Parsers.JSON` - parses `application/json` requests with the given
113-
`:json_decoder` (can be used as `:json` as well in the `:parsers` option)
107+
* `Plug.Parsers.URLENCODED` - parses `application/x-www-form-urlencoded`
108+
requests (can be used as `:urlencoded` as well in the `:parsers` option)
109+
* `Plug.Parsers.MULTIPART` - parses `multipart/form-data` and
110+
`multipart/mixed` requests (can be used as `:multipart` as well in the
111+
`:parsers` option)
112+
* `Plug.Parsers.JSON` - parses `application/json` requests with the given
113+
`:json_decoder` (can be used as `:json` as well in the `:parsers` option)
114114
115115
## File handling
116116

0 commit comments

Comments
 (0)