12 |
7u83 |
1 |
%% @author Bob Ippolito <bob@mochimedia.com>
|
|
|
2 |
%% @copyright 2007 Mochi Media, Inc.
|
|
|
3 |
%%
|
|
|
4 |
%% Permission is hereby granted, free of charge, to any person obtaining a
|
|
|
5 |
%% copy of this software and associated documentation files (the "Software"),
|
|
|
6 |
%% to deal in the Software without restriction, including without limitation
|
|
|
7 |
%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
|
|
8 |
%% and/or sell copies of the Software, and to permit persons to whom the
|
|
|
9 |
%% Software is furnished to do so, subject to the following conditions:
|
|
|
10 |
%%
|
|
|
11 |
%% The above copyright notice and this permission notice shall be included in
|
|
|
12 |
%% all copies or substantial portions of the Software.
|
|
|
13 |
%%
|
|
|
14 |
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
|
15 |
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
|
16 |
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
|
17 |
%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
18 |
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
19 |
%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
|
20 |
%% DEALINGS IN THE SOFTWARE.
|
|
|
21 |
|
|
|
22 |
%% @doc Utilities for parsing multipart/form-data.
|
|
|
23 |
|
|
|
24 |
-module(mochiweb_multipart).
|
|
|
25 |
|
|
|
26 |
-author('bob@mochimedia.com').
|
|
|
27 |
|
|
|
28 |
-export([parse_form/1, parse_form/2]).
|
|
|
29 |
|
|
|
30 |
-export([parse_multipart_request/2]).
|
|
|
31 |
|
|
|
32 |
-export([parts_to_body/3, parts_to_multipart_body/4]).
|
|
|
33 |
|
|
|
34 |
-export([default_file_handler/2]).
|
|
|
35 |
|
|
|
36 |
-define(CHUNKSIZE, 4096).
|
|
|
37 |
|
|
|
38 |
-record(mp,
|
|
|
39 |
{state, boundary, length, buffer, callback, req}).
|
|
|
40 |
|
|
|
41 |
%% TODO: DOCUMENT THIS MODULE.
|
|
|
42 |
%% @type key() = atom() | string() | binary().
|
|
|
43 |
%% @type value() = atom() | iolist() | integer().
|
|
|
44 |
%% @type header() = {key(), value()}.
|
|
|
45 |
%% @type bodypart() = {Start::integer(), End::integer(), Body::iolist()}.
|
|
|
46 |
%% @type formfile() = {Name::string(), ContentType::string(), Content::binary()}.
|
|
|
47 |
%% @type request().
|
|
|
48 |
%% @type file_handler() = (Filename::string(), ContentType::string()) -> file_handler_callback().
|
|
|
49 |
%% @type file_handler_callback() = (binary() | eof) -> file_handler_callback() | term().
|
|
|
50 |
|
|
|
51 |
%% @spec parts_to_body([bodypart()], ContentType::string(),
|
|
|
52 |
%% Size::integer()) -> {[header()], iolist()}
|
|
|
53 |
%% @doc Return {[header()], iolist()} representing the body for the given
|
|
|
54 |
%% parts, may be a single part or multipart.
|
|
|
55 |
parts_to_body([{Start, End, Body}], ContentType,
|
|
|
56 |
Size) ->
|
|
|
57 |
HeaderList = [{"Content-Type", ContentType},
|
|
|
58 |
{"Content-Range",
|
|
|
59 |
["bytes ", mochiweb_util:make_io(Start), "-",
|
|
|
60 |
mochiweb_util:make_io(End), "/",
|
|
|
61 |
mochiweb_util:make_io(Size)]}],
|
|
|
62 |
{HeaderList, Body};
|
|
|
63 |
parts_to_body(BodyList, ContentType, Size)
|
|
|
64 |
when is_list(BodyList) ->
|
|
|
65 |
parts_to_multipart_body(BodyList, ContentType, Size,
|
|
|
66 |
mochihex:to_hex(crypto:strong_rand_bytes(8))).
|
|
|
67 |
|
|
|
68 |
%% @spec parts_to_multipart_body([bodypart()], ContentType::string(),
|
|
|
69 |
%% Size::integer(), Boundary::string()) ->
|
|
|
70 |
%% {[header()], iolist()}
|
|
|
71 |
%% @doc Return {[header()], iolist()} representing the body for the given
|
|
|
72 |
%% parts, always a multipart response.
|
|
|
73 |
parts_to_multipart_body(BodyList, ContentType, Size,
|
|
|
74 |
Boundary) ->
|
|
|
75 |
HeaderList = [{"Content-Type",
|
|
|
76 |
["multipart/byteranges; ", "boundary=", Boundary]}],
|
|
|
77 |
MultiPartBody = multipart_body(BodyList, ContentType,
|
|
|
78 |
Boundary, Size),
|
|
|
79 |
{HeaderList, MultiPartBody}.
|
|
|
80 |
|
|
|
81 |
%% @spec multipart_body([bodypart()], ContentType::string(),
|
|
|
82 |
%% Boundary::string(), Size::integer()) -> iolist()
|
|
|
83 |
%% @doc Return the representation of a multipart body for the given [bodypart()].
|
|
|
84 |
multipart_body([], _ContentType, Boundary, _Size) ->
|
|
|
85 |
["--", Boundary, "--\r\n"];
|
|
|
86 |
multipart_body([{Start, End, Body} | BodyList],
|
|
|
87 |
ContentType, Boundary, Size) ->
|
|
|
88 |
["--", Boundary, "\r\n", "Content-Type: ", ContentType,
|
|
|
89 |
"\r\n", "Content-Range: ", "bytes ",
|
|
|
90 |
mochiweb_util:make_io(Start), "-",
|
|
|
91 |
mochiweb_util:make_io(End), "/",
|
|
|
92 |
mochiweb_util:make_io(Size), "\r\n\r\n", Body, "\r\n"
|
|
|
93 |
| multipart_body(BodyList, ContentType, Boundary,
|
|
|
94 |
Size)].
|
|
|
95 |
|
|
|
96 |
%% @spec parse_form(request()) -> [{string(), string() | formfile()}]
|
|
|
97 |
%% @doc Parse a multipart form from the given request using the in-memory
|
|
|
98 |
%% default_file_handler/2.
|
|
|
99 |
parse_form(Req) ->
|
|
|
100 |
parse_form(Req, fun default_file_handler/2).
|
|
|
101 |
|
|
|
102 |
%% @spec parse_form(request(), F::file_handler()) -> [{string(), string() | term()}]
|
|
|
103 |
%% @doc Parse a multipart form from the given request using the given file_handler().
|
|
|
104 |
parse_form(Req, FileHandler) ->
|
|
|
105 |
Callback = fun (Next) ->
|
|
|
106 |
parse_form_outer(Next, FileHandler, [])
|
|
|
107 |
end,
|
|
|
108 |
{_, _, Res} = parse_multipart_request(Req, Callback),
|
|
|
109 |
Res.
|
|
|
110 |
|
|
|
111 |
parse_form_outer(eof, _, Acc) -> lists:reverse(Acc);
|
|
|
112 |
parse_form_outer({headers, H}, FileHandler, State) ->
|
|
|
113 |
{"form-data", H1} =
|
|
|
114 |
proplists:get_value("content-disposition", H),
|
|
|
115 |
Name = proplists:get_value("name", H1),
|
|
|
116 |
Filename = proplists:get_value("filename", H1),
|
|
|
117 |
case Filename of
|
|
|
118 |
undefined ->
|
|
|
119 |
fun (Next) ->
|
|
|
120 |
parse_form_value(Next, {Name, []}, FileHandler, State)
|
|
|
121 |
end;
|
|
|
122 |
_ ->
|
|
|
123 |
ContentType = proplists:get_value("content-type", H),
|
|
|
124 |
Handler = FileHandler(Filename, ContentType),
|
|
|
125 |
fun (Next) ->
|
|
|
126 |
parse_form_file(Next, {Name, Handler}, FileHandler,
|
|
|
127 |
State)
|
|
|
128 |
end
|
|
|
129 |
end.
|
|
|
130 |
|
|
|
131 |
parse_form_value(body_end, {Name, Acc}, FileHandler,
|
|
|
132 |
State) ->
|
|
|
133 |
Value =
|
|
|
134 |
binary_to_list(iolist_to_binary(lists:reverse(Acc))),
|
|
|
135 |
State1 = [{Name, Value} | State],
|
|
|
136 |
fun (Next) ->
|
|
|
137 |
parse_form_outer(Next, FileHandler, State1)
|
|
|
138 |
end;
|
|
|
139 |
parse_form_value({body, Data}, {Name, Acc}, FileHandler,
|
|
|
140 |
State) ->
|
|
|
141 |
Acc1 = [Data | Acc],
|
|
|
142 |
fun (Next) ->
|
|
|
143 |
parse_form_value(Next, {Name, Acc1}, FileHandler, State)
|
|
|
144 |
end.
|
|
|
145 |
|
|
|
146 |
parse_form_file(body_end, {Name, Handler}, FileHandler,
|
|
|
147 |
State) ->
|
|
|
148 |
Value = Handler(eof),
|
|
|
149 |
State1 = [{Name, Value} | State],
|
|
|
150 |
fun (Next) ->
|
|
|
151 |
parse_form_outer(Next, FileHandler, State1)
|
|
|
152 |
end;
|
|
|
153 |
parse_form_file({body, Data}, {Name, Handler},
|
|
|
154 |
FileHandler, State) ->
|
|
|
155 |
H1 = Handler(Data),
|
|
|
156 |
fun (Next) ->
|
|
|
157 |
parse_form_file(Next, {Name, H1}, FileHandler, State)
|
|
|
158 |
end.
|
|
|
159 |
|
|
|
160 |
default_file_handler(Filename, ContentType) ->
|
|
|
161 |
default_file_handler_1(Filename, ContentType, []).
|
|
|
162 |
|
|
|
163 |
default_file_handler_1(Filename, ContentType, Acc) ->
|
|
|
164 |
fun (eof) ->
|
|
|
165 |
Value = iolist_to_binary(lists:reverse(Acc)),
|
|
|
166 |
{Filename, ContentType, Value};
|
|
|
167 |
(Next) ->
|
|
|
168 |
default_file_handler_1(Filename, ContentType,
|
|
|
169 |
[Next | Acc])
|
|
|
170 |
end.
|
|
|
171 |
|
|
|
172 |
parse_multipart_request({ReqM, _} = Req, Callback) ->
|
|
|
173 |
%% TODO: Support chunked?
|
|
|
174 |
Length =
|
|
|
175 |
list_to_integer(ReqM:get_combined_header_value("content-length",
|
|
|
176 |
Req)),
|
|
|
177 |
Boundary =
|
|
|
178 |
iolist_to_binary(get_boundary(ReqM:get_header_value("content-type",
|
|
|
179 |
Req))),
|
|
|
180 |
Prefix = <<"\r\n--", Boundary/binary>>,
|
|
|
181 |
BS = byte_size(Boundary),
|
|
|
182 |
Chunk = read_chunk(Req, Length),
|
|
|
183 |
Length1 = Length - byte_size(Chunk),
|
|
|
184 |
<<"--", Boundary:BS/binary, "\r\n", Rest/binary>> =
|
|
|
185 |
Chunk,
|
|
|
186 |
feed_mp(headers,
|
|
|
187 |
flash_multipart_hack(#mp{boundary = Prefix,
|
|
|
188 |
length = Length1, buffer = Rest,
|
|
|
189 |
callback = Callback, req = Req})).
|
|
|
190 |
|
|
|
191 |
parse_headers(<<>>) -> [];
|
|
|
192 |
parse_headers(Binary) -> parse_headers(Binary, []).
|
|
|
193 |
|
|
|
194 |
parse_headers(Binary, Acc) ->
|
|
|
195 |
case find_in_binary(<<"\r\n">>, Binary) of
|
|
|
196 |
{exact, N} ->
|
|
|
197 |
<<Line:N/binary, "\r\n", Rest/binary>> = Binary,
|
|
|
198 |
parse_headers(Rest, [split_header(Line) | Acc]);
|
|
|
199 |
not_found -> lists:reverse([split_header(Binary) | Acc])
|
|
|
200 |
end.
|
|
|
201 |
|
|
|
202 |
split_header(Line) ->
|
|
|
203 |
{Name, [$: | Value]} = lists:splitwith(fun (C) ->
|
|
|
204 |
C =/= $:
|
|
|
205 |
end,
|
|
|
206 |
binary_to_list(Line)),
|
|
|
207 |
{string:to_lower(string:strip(Name)),
|
|
|
208 |
mochiweb_util:parse_header(Value)}.
|
|
|
209 |
|
|
|
210 |
read_chunk({ReqM, _} = Req, Length) when Length > 0 ->
|
|
|
211 |
case Length of
|
|
|
212 |
Length when Length < (?CHUNKSIZE) ->
|
|
|
213 |
ReqM:recv(Length, Req);
|
|
|
214 |
_ -> ReqM:recv(?CHUNKSIZE, Req)
|
|
|
215 |
end.
|
|
|
216 |
|
|
|
217 |
read_more(State = #mp{length = Length, buffer = Buffer,
|
|
|
218 |
req = Req}) ->
|
|
|
219 |
Data = read_chunk(Req, Length),
|
|
|
220 |
Buffer1 = <<Buffer/binary, Data/binary>>,
|
|
|
221 |
flash_multipart_hack(State#mp{length =
|
|
|
222 |
Length - byte_size(Data),
|
|
|
223 |
buffer = Buffer1}).
|
|
|
224 |
|
|
|
225 |
flash_multipart_hack(State = #mp{length = 0,
|
|
|
226 |
buffer = Buffer, boundary = Prefix}) ->
|
|
|
227 |
%% http://code.google.com/p/mochiweb/issues/detail?id=22
|
|
|
228 |
%% Flash doesn't terminate multipart with \r\n properly so we fix it up here
|
|
|
229 |
PrefixSize = size(Prefix),
|
|
|
230 |
case size(Buffer) - (2 + PrefixSize) of
|
|
|
231 |
Seek when Seek >= 0 ->
|
|
|
232 |
case Buffer of
|
|
|
233 |
<<_:Seek/binary, Prefix:PrefixSize/binary, "--">> ->
|
|
|
234 |
Buffer1 = <<Buffer/binary, "\r\n">>,
|
|
|
235 |
State#mp{buffer = Buffer1};
|
|
|
236 |
_ -> State
|
|
|
237 |
end;
|
|
|
238 |
_ -> State
|
|
|
239 |
end;
|
|
|
240 |
flash_multipart_hack(State) -> State.
|
|
|
241 |
|
|
|
242 |
feed_mp(headers,
|
|
|
243 |
State = #mp{buffer = Buffer, callback = Callback}) ->
|
|
|
244 |
{State1, P} = case find_in_binary(<<"\r\n\r\n">>,
|
|
|
245 |
Buffer)
|
|
|
246 |
of
|
|
|
247 |
{exact, N} -> {State, N};
|
|
|
248 |
_ ->
|
|
|
249 |
S1 = read_more(State),
|
|
|
250 |
%% Assume headers must be less than ?CHUNKSIZE
|
|
|
251 |
{exact, N} = find_in_binary(<<"\r\n\r\n">>,
|
|
|
252 |
S1#mp.buffer),
|
|
|
253 |
{S1, N}
|
|
|
254 |
end,
|
|
|
255 |
<<Headers:P/binary, "\r\n\r\n", Rest/binary>> =
|
|
|
256 |
State1#mp.buffer,
|
|
|
257 |
NextCallback = Callback({headers,
|
|
|
258 |
parse_headers(Headers)}),
|
|
|
259 |
feed_mp(body,
|
|
|
260 |
State1#mp{buffer = Rest, callback = NextCallback});
|
|
|
261 |
feed_mp(body,
|
|
|
262 |
State = #mp{boundary = Prefix, buffer = Buffer,
|
|
|
263 |
callback = Callback}) ->
|
|
|
264 |
Boundary = find_boundary(Prefix, Buffer),
|
|
|
265 |
case Boundary of
|
|
|
266 |
{end_boundary, Start, Skip} ->
|
|
|
267 |
<<Data:Start/binary, _:Skip/binary, Rest/binary>> =
|
|
|
268 |
Buffer,
|
|
|
269 |
C1 = Callback({body, Data}),
|
|
|
270 |
C2 = C1(body_end),
|
|
|
271 |
{State#mp.length, Rest, C2(eof)};
|
|
|
272 |
{next_boundary, Start, Skip} ->
|
|
|
273 |
<<Data:Start/binary, _:Skip/binary, Rest/binary>> =
|
|
|
274 |
Buffer,
|
|
|
275 |
C1 = Callback({body, Data}),
|
|
|
276 |
feed_mp(headers,
|
|
|
277 |
State#mp{callback = C1(body_end), buffer = Rest});
|
|
|
278 |
{maybe, Start} ->
|
|
|
279 |
<<Data:Start/binary, Rest/binary>> = Buffer,
|
|
|
280 |
feed_mp(body,
|
|
|
281 |
read_more(State#mp{callback = Callback({body, Data}),
|
|
|
282 |
buffer = Rest}));
|
|
|
283 |
not_found ->
|
|
|
284 |
{Data, Rest} = {Buffer, <<>>},
|
|
|
285 |
feed_mp(body,
|
|
|
286 |
read_more(State#mp{callback = Callback({body, Data}),
|
|
|
287 |
buffer = Rest}))
|
|
|
288 |
end.
|
|
|
289 |
|
|
|
290 |
get_boundary(ContentType) ->
|
|
|
291 |
{"multipart/form-data", Opts} =
|
|
|
292 |
mochiweb_util:parse_header(ContentType),
|
|
|
293 |
case proplists:get_value("boundary", Opts) of
|
|
|
294 |
S when is_list(S) -> S
|
|
|
295 |
end.
|
|
|
296 |
|
|
|
297 |
%% @spec find_in_binary(Pattern::binary(), Data::binary()) ->
|
|
|
298 |
%% {exact, N} | {partial, N, K} | not_found
|
|
|
299 |
%% @doc Searches for the given pattern in the given binary.
|
|
|
300 |
find_in_binary(P, Data) when size(P) > 0 ->
|
|
|
301 |
PS = size(P),
|
|
|
302 |
DS = size(Data),
|
|
|
303 |
case DS - PS of
|
|
|
304 |
Last when Last < 0 -> partial_find(P, Data, 0, DS);
|
|
|
305 |
Last ->
|
|
|
306 |
case binary:match(Data, P) of
|
|
|
307 |
{Pos, _} -> {exact, Pos};
|
|
|
308 |
nomatch -> partial_find(P, Data, Last + 1, PS - 1)
|
|
|
309 |
end
|
|
|
310 |
end.
|
|
|
311 |
|
|
|
312 |
partial_find(_B, _D, _N, 0) -> not_found;
|
|
|
313 |
partial_find(B, D, N, K) ->
|
|
|
314 |
<<B1:K/binary, _/binary>> = B,
|
|
|
315 |
case D of
|
|
|
316 |
<<_Skip:N/binary, B1:K/binary>> -> {partial, N, K};
|
|
|
317 |
_ -> partial_find(B, D, 1 + N, K - 1)
|
|
|
318 |
end.
|
|
|
319 |
|
|
|
320 |
find_boundary(Prefix, Data) ->
|
|
|
321 |
case find_in_binary(Prefix, Data) of
|
|
|
322 |
{exact, Skip} ->
|
|
|
323 |
PrefixSkip = Skip + size(Prefix),
|
|
|
324 |
case Data of
|
|
|
325 |
<<_:PrefixSkip/binary, "\r\n", _/binary>> ->
|
|
|
326 |
{next_boundary, Skip, size(Prefix) + 2};
|
|
|
327 |
<<_:PrefixSkip/binary, "--\r\n", _/binary>> ->
|
|
|
328 |
{end_boundary, Skip, size(Prefix) + 4};
|
|
|
329 |
_ when size(Data) < PrefixSkip + 4 ->
|
|
|
330 |
%% Underflow
|
|
|
331 |
{maybe, Skip};
|
|
|
332 |
_ ->
|
|
|
333 |
%% False positive
|
|
|
334 |
not_found
|
|
|
335 |
end;
|
|
|
336 |
{partial, Skip, Length}
|
|
|
337 |
when Skip + Length =:= size(Data) ->
|
|
|
338 |
%% Underflow
|
|
|
339 |
{maybe, Skip};
|
|
|
340 |
_ -> not_found
|
|
|
341 |
end.
|
|
|
342 |
|
|
|
343 |
%%
|
|
|
344 |
%% Tests
|
|
|
345 |
%%
|
|
|
346 |
-ifdef(TEST).
|
|
|
347 |
|
|
|
348 |
-include_lib("eunit/include/eunit.hrl").
|
|
|
349 |
|
|
|
350 |
ssl_cert_opts() ->
|
|
|
351 |
EbinDir = filename:dirname(code:which(?MODULE)),
|
|
|
352 |
CertDir = filename:join([EbinDir, "..", "support",
|
|
|
353 |
"test-materials"]),
|
|
|
354 |
CertFile = filename:join(CertDir, "test_ssl_cert.pem"),
|
|
|
355 |
KeyFile = filename:join(CertDir, "test_ssl_key.pem"),
|
|
|
356 |
[{certfile, CertFile}, {keyfile, KeyFile}].
|
|
|
357 |
|
|
|
358 |
with_socket_server(Transport, ServerFun, ClientFun) ->
|
|
|
359 |
ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0},
|
|
|
360 |
{loop, ServerFun}],
|
|
|
361 |
ServerOpts = case Transport of
|
|
|
362 |
plain -> ServerOpts0;
|
|
|
363 |
ssl ->
|
|
|
364 |
ServerOpts0 ++
|
|
|
365 |
[{ssl, true}, {ssl_opts, ssl_cert_opts()}]
|
|
|
366 |
end,
|
|
|
367 |
{ok, Server} =
|
|
|
368 |
mochiweb_socket_server:start_link(ServerOpts),
|
|
|
369 |
Port = mochiweb_socket_server:get(Server, port),
|
|
|
370 |
ClientOpts = [binary, {active, false}],
|
|
|
371 |
{ok, Client} = case Transport of
|
|
|
372 |
plain -> gen_tcp:connect("127.0.0.1", Port, ClientOpts);
|
|
|
373 |
ssl ->
|
|
|
374 |
ClientOpts1 =
|
|
|
375 |
mochiweb_test_util:ssl_client_opts(ClientOpts),
|
|
|
376 |
{ok, SslSocket} = ssl:connect("127.0.0.1", Port,
|
|
|
377 |
ClientOpts1),
|
|
|
378 |
{ok, {ssl, SslSocket}}
|
|
|
379 |
end,
|
|
|
380 |
Res = (catch ClientFun(Client)),
|
|
|
381 |
mochiweb_socket_server:stop(Server),
|
|
|
382 |
Res.
|
|
|
383 |
|
|
|
384 |
fake_request(Socket, ContentType, Length) ->
|
|
|
385 |
mochiweb_request:new(Socket, 'POST', "/multipart",
|
|
|
386 |
{1, 1},
|
|
|
387 |
mochiweb_headers:make([{"content-type", ContentType},
|
|
|
388 |
{"content-length", Length}])).
|
|
|
389 |
|
|
|
390 |
test_callback({body, <<>>}, Rest = [body_end | _]) ->
|
|
|
391 |
%% When expecting the body_end we might get an empty binary
|
|
|
392 |
fun (Next) -> test_callback(Next, Rest) end;
|
|
|
393 |
test_callback({body, Got}, [{body, Expect} | Rest])
|
|
|
394 |
when Got =/= Expect ->
|
|
|
395 |
%% Partial response
|
|
|
396 |
GotSize = size(Got),
|
|
|
397 |
<<Got:GotSize/binary, Expect1/binary>> = Expect,
|
|
|
398 |
fun (Next) ->
|
|
|
399 |
test_callback(Next, [{body, Expect1} | Rest])
|
|
|
400 |
end;
|
|
|
401 |
test_callback(Got, [Expect | Rest]) ->
|
|
|
402 |
?assertEqual(Got, Expect),
|
|
|
403 |
case Rest of
|
|
|
404 |
[] -> ok;
|
|
|
405 |
_ -> fun (Next) -> test_callback(Next, Rest) end
|
|
|
406 |
end.
|
|
|
407 |
|
|
|
408 |
parse3_http_test() -> parse3(plain).
|
|
|
409 |
|
|
|
410 |
parse3_https_test() -> parse3(ssl).
|
|
|
411 |
|
|
|
412 |
parse3(Transport) ->
|
|
|
413 |
ContentType =
|
|
|
414 |
"multipart/form-data; boundary=---------------"
|
|
|
415 |
"------------7386909285754635891697677882",
|
|
|
416 |
BinContent =
|
|
|
417 |
<<"-----------------------------7386909285754635"
|
|
|
418 |
"891697677882\r\nContent-Disposition: "
|
|
|
419 |
"form-data; name=\"hidden\"\r\n\r\nmultipart "
|
|
|
420 |
"message\r\n-----------------------------73869"
|
|
|
421 |
"09285754635891697677882\r\nContent-Dispositio"
|
|
|
422 |
"n: form-data; name=\"file\"; filename=\"test_"
|
|
|
423 |
"file.txt\"\r\nContent-Type: text/plain\r\n\r\n"
|
|
|
424 |
"Woo multiline text file\n\nLa la la\r\n------"
|
|
|
425 |
"-----------------------7386909285754635891697"
|
|
|
426 |
"677882--\r\n">>,
|
|
|
427 |
Expect = [{headers,
|
|
|
428 |
[{"content-disposition",
|
|
|
429 |
{"form-data", [{"name", "hidden"}]}}]},
|
|
|
430 |
{body, <<"multipart message">>}, body_end,
|
|
|
431 |
{headers,
|
|
|
432 |
[{"content-disposition",
|
|
|
433 |
{"form-data",
|
|
|
434 |
[{"name", "file"}, {"filename", "test_file.txt"}]}},
|
|
|
435 |
{"content-type", {"text/plain", []}}]},
|
|
|
436 |
{body, <<"Woo multiline text file\n\nLa la la">>},
|
|
|
437 |
body_end, eof],
|
|
|
438 |
TestCallback = fun (Next) -> test_callback(Next, Expect)
|
|
|
439 |
end,
|
|
|
440 |
ServerFun = fun (Socket, _Opts) ->
|
|
|
441 |
ok = mochiweb_socket:send(Socket, BinContent),
|
|
|
442 |
exit(normal)
|
|
|
443 |
end,
|
|
|
444 |
ClientFun = fun (Socket) ->
|
|
|
445 |
Req = fake_request(Socket, ContentType,
|
|
|
446 |
byte_size(BinContent)),
|
|
|
447 |
Res = parse_multipart_request(Req, TestCallback),
|
|
|
448 |
{0, <<>>, ok} = Res,
|
|
|
449 |
ok
|
|
|
450 |
end,
|
|
|
451 |
ok = with_socket_server(Transport, ServerFun,
|
|
|
452 |
ClientFun),
|
|
|
453 |
ok.
|
|
|
454 |
|
|
|
455 |
parse2_http_test() -> parse2(plain).
|
|
|
456 |
|
|
|
457 |
parse2_https_test() -> parse2(ssl).
|
|
|
458 |
|
|
|
459 |
parse2(Transport) ->
|
|
|
460 |
ContentType =
|
|
|
461 |
"multipart/form-data; boundary=---------------"
|
|
|
462 |
"------------6072231407570234361599764024",
|
|
|
463 |
BinContent =
|
|
|
464 |
<<"-----------------------------6072231407570234"
|
|
|
465 |
"361599764024\r\nContent-Disposition: "
|
|
|
466 |
"form-data; name=\"hidden\"\r\n\r\nmultipart "
|
|
|
467 |
"message\r\n-----------------------------60722"
|
|
|
468 |
"31407570234361599764024\r\nContent-Dispositio"
|
|
|
469 |
"n: form-data; name=\"file\"; filename=\"\"\r\n"
|
|
|
470 |
"Content-Type: application/octet-stream\r\n\r\n\r\n"
|
|
|
471 |
"-----------------------------6072231407570234"
|
|
|
472 |
"361599764024--\r\n">>,
|
|
|
473 |
Expect = [{headers,
|
|
|
474 |
[{"content-disposition",
|
|
|
475 |
{"form-data", [{"name", "hidden"}]}}]},
|
|
|
476 |
{body, <<"multipart message">>}, body_end,
|
|
|
477 |
{headers,
|
|
|
478 |
[{"content-disposition",
|
|
|
479 |
{"form-data", [{"name", "file"}, {"filename", ""}]}},
|
|
|
480 |
{"content-type", {"application/octet-stream", []}}]},
|
|
|
481 |
{body, <<>>}, body_end, eof],
|
|
|
482 |
TestCallback = fun (Next) -> test_callback(Next, Expect)
|
|
|
483 |
end,
|
|
|
484 |
ServerFun = fun (Socket, _Opts) ->
|
|
|
485 |
ok = mochiweb_socket:send(Socket, BinContent),
|
|
|
486 |
exit(normal)
|
|
|
487 |
end,
|
|
|
488 |
ClientFun = fun (Socket) ->
|
|
|
489 |
Req = fake_request(Socket, ContentType,
|
|
|
490 |
byte_size(BinContent)),
|
|
|
491 |
Res = parse_multipart_request(Req, TestCallback),
|
|
|
492 |
{0, <<>>, ok} = Res,
|
|
|
493 |
ok
|
|
|
494 |
end,
|
|
|
495 |
ok = with_socket_server(Transport, ServerFun,
|
|
|
496 |
ClientFun),
|
|
|
497 |
ok.
|
|
|
498 |
|
|
|
499 |
parse_form_http_test() -> do_parse_form(plain).
|
|
|
500 |
|
|
|
501 |
parse_form_https_test() -> do_parse_form(ssl).
|
|
|
502 |
|
|
|
503 |
do_parse_form(Transport) ->
|
|
|
504 |
ContentType = "multipart/form-data; boundary=AaB03x",
|
|
|
505 |
"AaB03x" = get_boundary(ContentType),
|
|
|
506 |
Content = mochiweb_util:join(["--AaB03x",
|
|
|
507 |
"Content-Disposition: form-data; name=\"submit"
|
|
|
508 |
"-name\"",
|
|
|
509 |
"", "Larry", "--AaB03x",
|
|
|
510 |
"Content-Disposition: form-data; name=\"files\";"
|
|
|
511 |
++ "filename=\"file1.txt\"",
|
|
|
512 |
"Content-Type: text/plain", "",
|
|
|
513 |
"... contents of file1.txt ...", "--AaB03x--",
|
|
|
514 |
""],
|
|
|
515 |
"\r\n"),
|
|
|
516 |
BinContent = iolist_to_binary(Content),
|
|
|
517 |
ServerFun = fun (Socket, _Opts) ->
|
|
|
518 |
ok = mochiweb_socket:send(Socket, BinContent),
|
|
|
519 |
exit(normal)
|
|
|
520 |
end,
|
|
|
521 |
ClientFun = fun (Socket) ->
|
|
|
522 |
Req = fake_request(Socket, ContentType,
|
|
|
523 |
byte_size(BinContent)),
|
|
|
524 |
Res = parse_form(Req),
|
|
|
525 |
[{"submit-name", "Larry"},
|
|
|
526 |
{"files",
|
|
|
527 |
{"file1.txt", {"text/plain", []},
|
|
|
528 |
<<"... contents of file1.txt ...">>}}] =
|
|
|
529 |
Res,
|
|
|
530 |
ok
|
|
|
531 |
end,
|
|
|
532 |
ok = with_socket_server(Transport, ServerFun,
|
|
|
533 |
ClientFun),
|
|
|
534 |
ok.
|
|
|
535 |
|
|
|
536 |
parse_http_test() -> do_parse(plain).
|
|
|
537 |
|
|
|
538 |
parse_https_test() -> do_parse(ssl).
|
|
|
539 |
|
|
|
540 |
do_parse(Transport) ->
|
|
|
541 |
ContentType = "multipart/form-data; boundary=AaB03x",
|
|
|
542 |
"AaB03x" = get_boundary(ContentType),
|
|
|
543 |
Content = mochiweb_util:join(["--AaB03x",
|
|
|
544 |
"Content-Disposition: form-data; name=\"submit"
|
|
|
545 |
"-name\"",
|
|
|
546 |
"", "Larry", "--AaB03x",
|
|
|
547 |
"Content-Disposition: form-data; name=\"files\";"
|
|
|
548 |
++ "filename=\"file1.txt\"",
|
|
|
549 |
"Content-Type: text/plain", "",
|
|
|
550 |
"... contents of file1.txt ...", "--AaB03x--",
|
|
|
551 |
""],
|
|
|
552 |
"\r\n"),
|
|
|
553 |
BinContent = iolist_to_binary(Content),
|
|
|
554 |
Expect = [{headers,
|
|
|
555 |
[{"content-disposition",
|
|
|
556 |
{"form-data", [{"name", "submit-name"}]}}]},
|
|
|
557 |
{body, <<"Larry">>}, body_end,
|
|
|
558 |
{headers,
|
|
|
559 |
[{"content-disposition",
|
|
|
560 |
{"form-data",
|
|
|
561 |
[{"name", "files"}, {"filename", "file1.txt"}]}},
|
|
|
562 |
{"content-type", {"text/plain", []}}]},
|
|
|
563 |
{body, <<"... contents of file1.txt ...">>}, body_end,
|
|
|
564 |
eof],
|
|
|
565 |
TestCallback = fun (Next) -> test_callback(Next, Expect)
|
|
|
566 |
end,
|
|
|
567 |
ServerFun = fun (Socket, _Opts) ->
|
|
|
568 |
ok = mochiweb_socket:send(Socket, BinContent),
|
|
|
569 |
exit(normal)
|
|
|
570 |
end,
|
|
|
571 |
ClientFun = fun (Socket) ->
|
|
|
572 |
Req = fake_request(Socket, ContentType,
|
|
|
573 |
byte_size(BinContent)),
|
|
|
574 |
Res = parse_multipart_request(Req, TestCallback),
|
|
|
575 |
{0, <<>>, ok} = Res,
|
|
|
576 |
ok
|
|
|
577 |
end,
|
|
|
578 |
ok = with_socket_server(Transport, ServerFun,
|
|
|
579 |
ClientFun),
|
|
|
580 |
ok.
|
|
|
581 |
|
|
|
582 |
parse_partial_body_boundary_http_test() ->
|
|
|
583 |
parse_partial_body_boundary(plain).
|
|
|
584 |
|
|
|
585 |
parse_partial_body_boundary_https_test() ->
|
|
|
586 |
parse_partial_body_boundary(ssl).
|
|
|
587 |
|
|
|
588 |
parse_partial_body_boundary(Transport) ->
|
|
|
589 |
Boundary = string:copies("$", 2048),
|
|
|
590 |
ContentType = "multipart/form-data; boundary=" ++
|
|
|
591 |
Boundary,
|
|
|
592 |
?assertEqual(Boundary, (get_boundary(ContentType))),
|
|
|
593 |
Content = mochiweb_util:join(["--" ++ Boundary,
|
|
|
594 |
"Content-Disposition: form-data; name=\"submit"
|
|
|
595 |
"-name\"",
|
|
|
596 |
"", "Larry", "--" ++ Boundary,
|
|
|
597 |
"Content-Disposition: form-data; name=\"files\";"
|
|
|
598 |
++ "filename=\"file1.txt\"",
|
|
|
599 |
"Content-Type: text/plain", "",
|
|
|
600 |
"... contents of file1.txt ...",
|
|
|
601 |
"--" ++ Boundary ++ "--", ""],
|
|
|
602 |
"\r\n"),
|
|
|
603 |
BinContent = iolist_to_binary(Content),
|
|
|
604 |
Expect = [{headers,
|
|
|
605 |
[{"content-disposition",
|
|
|
606 |
{"form-data", [{"name", "submit-name"}]}}]},
|
|
|
607 |
{body, <<"Larry">>}, body_end,
|
|
|
608 |
{headers,
|
|
|
609 |
[{"content-disposition",
|
|
|
610 |
{"form-data",
|
|
|
611 |
[{"name", "files"}, {"filename", "file1.txt"}]}},
|
|
|
612 |
{"content-type", {"text/plain", []}}]},
|
|
|
613 |
{body, <<"... contents of file1.txt ...">>}, body_end,
|
|
|
614 |
eof],
|
|
|
615 |
TestCallback = fun (Next) -> test_callback(Next, Expect)
|
|
|
616 |
end,
|
|
|
617 |
ServerFun = fun (Socket, _Opts) ->
|
|
|
618 |
ok = mochiweb_socket:send(Socket, BinContent),
|
|
|
619 |
exit(normal)
|
|
|
620 |
end,
|
|
|
621 |
ClientFun = fun (Socket) ->
|
|
|
622 |
Req = fake_request(Socket, ContentType,
|
|
|
623 |
byte_size(BinContent)),
|
|
|
624 |
Res = parse_multipart_request(Req, TestCallback),
|
|
|
625 |
{0, <<>>, ok} = Res,
|
|
|
626 |
ok
|
|
|
627 |
end,
|
|
|
628 |
ok = with_socket_server(Transport, ServerFun,
|
|
|
629 |
ClientFun),
|
|
|
630 |
ok.
|
|
|
631 |
|
|
|
632 |
parse_large_header_http_test() ->
|
|
|
633 |
parse_large_header(plain).
|
|
|
634 |
|
|
|
635 |
parse_large_header_https_test() ->
|
|
|
636 |
parse_large_header(ssl).
|
|
|
637 |
|
|
|
638 |
parse_large_header(Transport) ->
|
|
|
639 |
ContentType = "multipart/form-data; boundary=AaB03x",
|
|
|
640 |
"AaB03x" = get_boundary(ContentType),
|
|
|
641 |
Content = mochiweb_util:join(["--AaB03x",
|
|
|
642 |
"Content-Disposition: form-data; name=\"submit"
|
|
|
643 |
"-name\"",
|
|
|
644 |
"", "Larry", "--AaB03x",
|
|
|
645 |
"Content-Disposition: form-data; name=\"files\";"
|
|
|
646 |
++ "filename=\"file1.txt\"",
|
|
|
647 |
"Content-Type: text/plain",
|
|
|
648 |
"x-large-header: " ++
|
|
|
649 |
string:copies("%", 4096),
|
|
|
650 |
"", "... contents of file1.txt ...",
|
|
|
651 |
"--AaB03x--", ""],
|
|
|
652 |
"\r\n"),
|
|
|
653 |
BinContent = iolist_to_binary(Content),
|
|
|
654 |
Expect = [{headers,
|
|
|
655 |
[{"content-disposition",
|
|
|
656 |
{"form-data", [{"name", "submit-name"}]}}]},
|
|
|
657 |
{body, <<"Larry">>}, body_end,
|
|
|
658 |
{headers,
|
|
|
659 |
[{"content-disposition",
|
|
|
660 |
{"form-data",
|
|
|
661 |
[{"name", "files"}, {"filename", "file1.txt"}]}},
|
|
|
662 |
{"content-type", {"text/plain", []}},
|
|
|
663 |
{"x-large-header", {string:copies("%", 4096), []}}]},
|
|
|
664 |
{body, <<"... contents of file1.txt ...">>}, body_end,
|
|
|
665 |
eof],
|
|
|
666 |
TestCallback = fun (Next) -> test_callback(Next, Expect)
|
|
|
667 |
end,
|
|
|
668 |
ServerFun = fun (Socket, _Opts) ->
|
|
|
669 |
ok = mochiweb_socket:send(Socket, BinContent),
|
|
|
670 |
exit(normal)
|
|
|
671 |
end,
|
|
|
672 |
ClientFun = fun (Socket) ->
|
|
|
673 |
Req = fake_request(Socket, ContentType,
|
|
|
674 |
byte_size(BinContent)),
|
|
|
675 |
Res = parse_multipart_request(Req, TestCallback),
|
|
|
676 |
{0, <<>>, ok} = Res,
|
|
|
677 |
ok
|
|
|
678 |
end,
|
|
|
679 |
ok = with_socket_server(Transport, ServerFun,
|
|
|
680 |
ClientFun),
|
|
|
681 |
ok.
|
|
|
682 |
|
|
|
683 |
find_boundary_test() ->
|
|
|
684 |
B = <<"\r\n--X">>,
|
|
|
685 |
{next_boundary, 0, 7} = find_boundary(B,
|
|
|
686 |
<<"\r\n--X\r\nRest">>),
|
|
|
687 |
{next_boundary, 1, 7} = find_boundary(B,
|
|
|
688 |
<<"!\r\n--X\r\nRest">>),
|
|
|
689 |
{end_boundary, 0, 9} = find_boundary(B,
|
|
|
690 |
<<"\r\n--X--\r\nRest">>),
|
|
|
691 |
{end_boundary, 1, 9} = find_boundary(B,
|
|
|
692 |
<<"!\r\n--X--\r\nRest">>),
|
|
|
693 |
not_found = find_boundary(B, <<"--X\r\nRest">>),
|
|
|
694 |
{maybe, 0} = find_boundary(B, <<"\r\n--X\r">>),
|
|
|
695 |
{maybe, 1} = find_boundary(B, <<"!\r\n--X\r">>),
|
|
|
696 |
P = <<"\r\n-----------------------------160374543510"
|
|
|
697 |
"82272548568224146">>,
|
|
|
698 |
B0 = <<55, 212, 131, 77, 206, 23, 216, 198, 35, 87, 252,
|
|
|
699 |
118, 252, 8, 25, 211, 132, 229, 182, 42, 29, 188, 62,
|
|
|
700 |
175, 247, 243, 4, 4, 0, 59, 13, 10, 45, 45, 45, 45, 45,
|
|
|
701 |
45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 45,
|
|
|
702 |
45, 45, 45, 45, 45, 45, 45, 45, 45, 45, 49, 54, 48, 51,
|
|
|
703 |
55, 52, 53, 52, 51, 53, 49>>,
|
|
|
704 |
{maybe, 30} = find_boundary(P, B0),
|
|
|
705 |
not_found = find_boundary(B, <<"\r\n--XJOPKE">>),
|
|
|
706 |
ok.
|
|
|
707 |
|
|
|
708 |
find_in_binary_test() ->
|
|
|
709 |
{exact, 0} = find_in_binary(<<"foo">>, <<"foobarbaz">>),
|
|
|
710 |
{exact, 1} = find_in_binary(<<"oo">>, <<"foobarbaz">>),
|
|
|
711 |
{exact, 8} = find_in_binary(<<"z">>, <<"foobarbaz">>),
|
|
|
712 |
not_found = find_in_binary(<<"q">>, <<"foobarbaz">>),
|
|
|
713 |
{partial, 7, 2} = find_in_binary(<<"azul">>,
|
|
|
714 |
<<"foobarbaz">>),
|
|
|
715 |
{exact, 0} = find_in_binary(<<"foobarbaz">>,
|
|
|
716 |
<<"foobarbaz">>),
|
|
|
717 |
{partial, 0, 3} = find_in_binary(<<"foobar">>,
|
|
|
718 |
<<"foo">>),
|
|
|
719 |
{partial, 1, 3} = find_in_binary(<<"foobar">>,
|
|
|
720 |
<<"afoo">>),
|
|
|
721 |
ok.
|
|
|
722 |
|
|
|
723 |
flash_parse_http_test() -> flash_parse(plain).
|
|
|
724 |
|
|
|
725 |
flash_parse_https_test() -> flash_parse(ssl).
|
|
|
726 |
|
|
|
727 |
flash_parse(Transport) ->
|
|
|
728 |
ContentType =
|
|
|
729 |
"multipart/form-data; boundary=----------ei4GI"
|
|
|
730 |
"3GI3Ij5Ef1ae0KM7Ij5ei4Ij5",
|
|
|
731 |
"----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5" =
|
|
|
732 |
get_boundary(ContentType),
|
|
|
733 |
BinContent =
|
|
|
734 |
<<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\n"
|
|
|
735 |
"Content-Disposition: form-data; name=\"Filena"
|
|
|
736 |
"me\"\r\n\r\nhello.txt\r\n------------ei4GI3GI"
|
|
|
737 |
"3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition"
|
|
|
738 |
": form-data; name=\"success_action_status\"\r\n\r\n"
|
|
|
739 |
"201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei"
|
|
|
740 |
"4Ij5\r\nContent-Disposition: form-data; "
|
|
|
741 |
"name=\"file\"; filename=\"hello.txt\"\r\nCont"
|
|
|
742 |
"ent-Type: application/octet-stream\r\n\r\nhel"
|
|
|
743 |
"lo\n\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5e"
|
|
|
744 |
"i4Ij5\r\nContent-Disposition: form-data; "
|
|
|
745 |
"name=\"Upload\"\r\n\r\nSubmit Query\r\n------"
|
|
|
746 |
"------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5--">>,
|
|
|
747 |
Expect = [{headers,
|
|
|
748 |
[{"content-disposition",
|
|
|
749 |
{"form-data", [{"name", "Filename"}]}}]},
|
|
|
750 |
{body, <<"hello.txt">>}, body_end,
|
|
|
751 |
{headers,
|
|
|
752 |
[{"content-disposition",
|
|
|
753 |
{"form-data", [{"name", "success_action_status"}]}}]},
|
|
|
754 |
{body, <<"201">>}, body_end,
|
|
|
755 |
{headers,
|
|
|
756 |
[{"content-disposition",
|
|
|
757 |
{"form-data",
|
|
|
758 |
[{"name", "file"}, {"filename", "hello.txt"}]}},
|
|
|
759 |
{"content-type", {"application/octet-stream", []}}]},
|
|
|
760 |
{body, <<"hello\n">>}, body_end,
|
|
|
761 |
{headers,
|
|
|
762 |
[{"content-disposition",
|
|
|
763 |
{"form-data", [{"name", "Upload"}]}}]},
|
|
|
764 |
{body, <<"Submit Query">>}, body_end, eof],
|
|
|
765 |
TestCallback = fun (Next) -> test_callback(Next, Expect)
|
|
|
766 |
end,
|
|
|
767 |
ServerFun = fun (Socket, _Opts) ->
|
|
|
768 |
ok = mochiweb_socket:send(Socket, BinContent),
|
|
|
769 |
exit(normal)
|
|
|
770 |
end,
|
|
|
771 |
ClientFun = fun (Socket) ->
|
|
|
772 |
Req = fake_request(Socket, ContentType,
|
|
|
773 |
byte_size(BinContent)),
|
|
|
774 |
Res = parse_multipart_request(Req, TestCallback),
|
|
|
775 |
{0, <<>>, ok} = Res,
|
|
|
776 |
ok
|
|
|
777 |
end,
|
|
|
778 |
ok = with_socket_server(Transport, ServerFun,
|
|
|
779 |
ClientFun),
|
|
|
780 |
ok.
|
|
|
781 |
|
|
|
782 |
flash_parse2_http_test() -> flash_parse2(plain).
|
|
|
783 |
|
|
|
784 |
flash_parse2_https_test() -> flash_parse2(ssl).
|
|
|
785 |
|
|
|
786 |
flash_parse2(Transport) ->
|
|
|
787 |
ContentType =
|
|
|
788 |
"multipart/form-data; boundary=----------ei4GI"
|
|
|
789 |
"3GI3Ij5Ef1ae0KM7Ij5ei4Ij5",
|
|
|
790 |
"----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5" =
|
|
|
791 |
get_boundary(ContentType),
|
|
|
792 |
Chunk = iolist_to_binary(string:copies("%", 4096)),
|
|
|
793 |
BinContent =
|
|
|
794 |
<<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\n"
|
|
|
795 |
"Content-Disposition: form-data; name=\"Filena"
|
|
|
796 |
"me\"\r\n\r\nhello.txt\r\n------------ei4GI3GI"
|
|
|
797 |
"3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition"
|
|
|
798 |
": form-data; name=\"success_action_status\"\r\n\r\n"
|
|
|
799 |
"201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei"
|
|
|
800 |
"4Ij5\r\nContent-Disposition: form-data; "
|
|
|
801 |
"name=\"file\"; filename=\"hello.txt\"\r\nCont"
|
|
|
802 |
"ent-Type: application/octet-stream\r\n\r\n",
|
|
|
803 |
Chunk/binary,
|
|
|
804 |
"\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij"
|
|
|
805 |
"5\r\nContent-Disposition: form-data; "
|
|
|
806 |
"name=\"Upload\"\r\n\r\nSubmit Query\r\n------"
|
|
|
807 |
"------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5--">>,
|
|
|
808 |
Expect = [{headers,
|
|
|
809 |
[{"content-disposition",
|
|
|
810 |
{"form-data", [{"name", "Filename"}]}}]},
|
|
|
811 |
{body, <<"hello.txt">>}, body_end,
|
|
|
812 |
{headers,
|
|
|
813 |
[{"content-disposition",
|
|
|
814 |
{"form-data", [{"name", "success_action_status"}]}}]},
|
|
|
815 |
{body, <<"201">>}, body_end,
|
|
|
816 |
{headers,
|
|
|
817 |
[{"content-disposition",
|
|
|
818 |
{"form-data",
|
|
|
819 |
[{"name", "file"}, {"filename", "hello.txt"}]}},
|
|
|
820 |
{"content-type", {"application/octet-stream", []}}]},
|
|
|
821 |
{body, Chunk}, body_end,
|
|
|
822 |
{headers,
|
|
|
823 |
[{"content-disposition",
|
|
|
824 |
{"form-data", [{"name", "Upload"}]}}]},
|
|
|
825 |
{body, <<"Submit Query">>}, body_end, eof],
|
|
|
826 |
TestCallback = fun (Next) -> test_callback(Next, Expect)
|
|
|
827 |
end,
|
|
|
828 |
ServerFun = fun (Socket, _Opts) ->
|
|
|
829 |
ok = mochiweb_socket:send(Socket, BinContent),
|
|
|
830 |
exit(normal)
|
|
|
831 |
end,
|
|
|
832 |
ClientFun = fun (Socket) ->
|
|
|
833 |
Req = fake_request(Socket, ContentType,
|
|
|
834 |
byte_size(BinContent)),
|
|
|
835 |
Res = parse_multipart_request(Req, TestCallback),
|
|
|
836 |
{0, <<>>, ok} = Res,
|
|
|
837 |
ok
|
|
|
838 |
end,
|
|
|
839 |
ok = with_socket_server(Transport, ServerFun,
|
|
|
840 |
ClientFun),
|
|
|
841 |
ok.
|
|
|
842 |
|
|
|
843 |
parse_headers_test() ->
|
|
|
844 |
?assertEqual([], (parse_headers(<<>>))).
|
|
|
845 |
|
|
|
846 |
flash_multipart_hack_test() ->
|
|
|
847 |
Buffer = <<"prefix-">>,
|
|
|
848 |
Prefix = <<"prefix">>,
|
|
|
849 |
State = #mp{length = 0, buffer = Buffer,
|
|
|
850 |
boundary = Prefix},
|
|
|
851 |
?assertEqual(State, (flash_multipart_hack(State))).
|
|
|
852 |
|
|
|
853 |
parts_to_body_single_test() ->
|
|
|
854 |
{HL, B} = parts_to_body([{0, 5, <<"01234">>}],
|
|
|
855 |
"text/plain", 10),
|
|
|
856 |
[{"Content-Range", Range}, {"Content-Type", Type}] =
|
|
|
857 |
lists:sort(HL),
|
|
|
858 |
?assertEqual(<<"bytes 0-5/10">>,
|
|
|
859 |
(iolist_to_binary(Range))),
|
|
|
860 |
?assertEqual(<<"text/plain">>,
|
|
|
861 |
(iolist_to_binary(Type))),
|
|
|
862 |
?assertEqual(<<"01234">>, (iolist_to_binary(B))),
|
|
|
863 |
ok.
|
|
|
864 |
|
|
|
865 |
parts_to_body_multi_test() ->
|
|
|
866 |
{[{"Content-Type", Type}], _B} = parts_to_body([{0, 5,
|
|
|
867 |
<<"01234">>},
|
|
|
868 |
{5, 10, <<"56789">>}],
|
|
|
869 |
"text/plain", 10),
|
|
|
870 |
?assertMatch(<<"multipart/byteranges; boundary=",
|
|
|
871 |
_/binary>>,
|
|
|
872 |
(iolist_to_binary(Type))),
|
|
|
873 |
ok.
|
|
|
874 |
|
|
|
875 |
parts_to_multipart_body_test() ->
|
|
|
876 |
{[{"Content-Type", V}], B} =
|
|
|
877 |
parts_to_multipart_body([{0, 5, <<"01234">>},
|
|
|
878 |
{5, 10, <<"56789">>}],
|
|
|
879 |
"text/plain", 10, "BOUNDARY"),
|
|
|
880 |
MB = multipart_body([{0, 5, <<"01234">>},
|
|
|
881 |
{5, 10, <<"56789">>}],
|
|
|
882 |
"text/plain", "BOUNDARY", 10),
|
|
|
883 |
?assertEqual(<<"multipart/byteranges; boundary=BOUNDARY">>,
|
|
|
884 |
(iolist_to_binary(V))),
|
|
|
885 |
?assertEqual((iolist_to_binary(MB)),
|
|
|
886 |
(iolist_to_binary(B))),
|
|
|
887 |
ok.
|
|
|
888 |
|
|
|
889 |
multipart_body_test() ->
|
|
|
890 |
?assertEqual(<<"--BOUNDARY--\r\n">>,
|
|
|
891 |
(iolist_to_binary(multipart_body([], "text/plain",
|
|
|
892 |
"BOUNDARY", 0)))),
|
|
|
893 |
?assertEqual(<<"--BOUNDARY\r\nContent-Type: text/plain\r\nCon"
|
|
|
894 |
"tent-Range: bytes 0-5/10\r\n\r\n01234\r\n--BO"
|
|
|
895 |
"UNDARY\r\nContent-Type: text/plain\r\nContent"
|
|
|
896 |
"-Range: bytes 5-10/10\r\n\r\n56789\r\n--BOUND"
|
|
|
897 |
"ARY--\r\n">>,
|
|
|
898 |
(iolist_to_binary(multipart_body([{0, 5, <<"01234">>},
|
|
|
899 |
{5, 10, <<"56789">>}],
|
|
|
900 |
"text/plain", "BOUNDARY",
|
|
|
901 |
10)))),
|
|
|
902 |
ok.
|
|
|
903 |
|
|
|
904 |
%% @todo Move somewhere more appropriate than in the test suite
|
|
|
905 |
|
|
|
906 |
multipart_parsing_benchmark_test() ->
|
|
|
907 |
run_multipart_parsing_benchmark(1).
|
|
|
908 |
|
|
|
909 |
run_multipart_parsing_benchmark(0) -> ok;
|
|
|
910 |
run_multipart_parsing_benchmark(N) ->
|
|
|
911 |
multipart_parsing_benchmark(),
|
|
|
912 |
run_multipart_parsing_benchmark(N - 1).
|
|
|
913 |
|
|
|
914 |
multipart_parsing_benchmark() ->
|
|
|
915 |
ContentType =
|
|
|
916 |
"multipart/form-data; boundary=----------ei4GI"
|
|
|
917 |
"3GI3Ij5Ef1ae0KM7Ij5ei4Ij5",
|
|
|
918 |
Chunk =
|
|
|
919 |
binary:copy(<<"This Is_%Some=Quite0Long4String2Used9For7Benc"
|
|
|
920 |
"hmarKing.5">>,
|
|
|
921 |
102400),
|
|
|
922 |
BinContent =
|
|
|
923 |
<<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\n"
|
|
|
924 |
"Content-Disposition: form-data; name=\"Filena"
|
|
|
925 |
"me\"\r\n\r\nhello.txt\r\n------------ei4GI3GI"
|
|
|
926 |
"3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition"
|
|
|
927 |
": form-data; name=\"success_action_status\"\r\n\r\n"
|
|
|
928 |
"201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei"
|
|
|
929 |
"4Ij5\r\nContent-Disposition: form-data; "
|
|
|
930 |
"name=\"file\"; filename=\"hello.txt\"\r\nCont"
|
|
|
931 |
"ent-Type: application/octet-stream\r\n\r\n",
|
|
|
932 |
Chunk/binary,
|
|
|
933 |
"\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij"
|
|
|
934 |
"5\r\nContent-Disposition: form-data; "
|
|
|
935 |
"name=\"Upload\"\r\n\r\nSubmit Query\r\n------"
|
|
|
936 |
"------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5--">>,
|
|
|
937 |
Expect = [{headers,
|
|
|
938 |
[{"content-disposition",
|
|
|
939 |
{"form-data", [{"name", "Filename"}]}}]},
|
|
|
940 |
{body, <<"hello.txt">>}, body_end,
|
|
|
941 |
{headers,
|
|
|
942 |
[{"content-disposition",
|
|
|
943 |
{"form-data", [{"name", "success_action_status"}]}}]},
|
|
|
944 |
{body, <<"201">>}, body_end,
|
|
|
945 |
{headers,
|
|
|
946 |
[{"content-disposition",
|
|
|
947 |
{"form-data",
|
|
|
948 |
[{"name", "file"}, {"filename", "hello.txt"}]}},
|
|
|
949 |
{"content-type", {"application/octet-stream", []}}]},
|
|
|
950 |
{body, Chunk}, body_end,
|
|
|
951 |
{headers,
|
|
|
952 |
[{"content-disposition",
|
|
|
953 |
{"form-data", [{"name", "Upload"}]}}]},
|
|
|
954 |
{body, <<"Submit Query">>}, body_end, eof],
|
|
|
955 |
TestCallback = fun (Next) -> test_callback(Next, Expect)
|
|
|
956 |
end,
|
|
|
957 |
ServerFun = fun (Socket, _Opts) ->
|
|
|
958 |
ok = mochiweb_socket:send(Socket, BinContent),
|
|
|
959 |
exit(normal)
|
|
|
960 |
end,
|
|
|
961 |
ClientFun = fun (Socket) ->
|
|
|
962 |
Req = fake_request(Socket, ContentType,
|
|
|
963 |
byte_size(BinContent)),
|
|
|
964 |
Res = parse_multipart_request(Req, TestCallback),
|
|
|
965 |
{0, <<>>, ok} = Res,
|
|
|
966 |
ok
|
|
|
967 |
end,
|
|
|
968 |
ok = with_socket_server(plain, ServerFun, ClientFun),
|
|
|
969 |
ok.
|
|
|
970 |
|
|
|
971 |
-endif.
|