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.
55parts_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};
63parts_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.
73parts_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()].
84multipart_body([], _ContentType, Boundary, _Size) ->
85    ["--", Boundary, "--\r\n"];
86multipart_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.
99parse_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().
104parse_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
111parse_form_outer(eof, _, Acc) -> lists:reverse(Acc);
112parse_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
131parse_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;
139parse_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
146parse_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;
153parse_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
160default_file_handler(Filename, ContentType) ->
161    default_file_handler_1(Filename, ContentType, []).
162
163default_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
172parse_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
191parse_headers(<<>>) -> [];
192parse_headers(Binary) -> parse_headers(Binary, []).
193
194parse_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
202split_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
210read_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
217read_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
225flash_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;
240flash_multipart_hack(State) -> State.
241
242feed_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});
261feed_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
290get_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.
300find_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
312partial_find(_B, _D, _N, 0) -> not_found;
313partial_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
320find_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
350ssl_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
358with_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
384fake_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
390test_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;
393test_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;
401test_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
408parse3_http_test() -> parse3(plain).
409
410parse3_https_test() -> parse3(ssl).
411
412parse3(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
455parse2_http_test() -> parse2(plain).
456
457parse2_https_test() -> parse2(ssl).
458
459parse2(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
499parse_form_http_test() -> do_parse_form(plain).
500
501parse_form_https_test() -> do_parse_form(ssl).
502
503do_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
536parse_http_test() -> do_parse(plain).
537
538parse_https_test() -> do_parse(ssl).
539
540do_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
582parse_partial_body_boundary_http_test() ->
583    parse_partial_body_boundary(plain).
584
585parse_partial_body_boundary_https_test() ->
586    parse_partial_body_boundary(ssl).
587
588parse_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
632parse_large_header_http_test() ->
633    parse_large_header(plain).
634
635parse_large_header_https_test() ->
636    parse_large_header(ssl).
637
638parse_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
683find_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
708find_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
723flash_parse_http_test() -> flash_parse(plain).
724
725flash_parse_https_test() -> flash_parse(ssl).
726
727flash_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
782flash_parse2_http_test() -> flash_parse2(plain).
783
784flash_parse2_https_test() -> flash_parse2(ssl).
785
786flash_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
843parse_headers_test() ->
844    ?assertEqual([], (parse_headers(<<>>))).
845
846flash_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
853parts_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
865parts_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
875parts_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
889multipart_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
906multipart_parsing_benchmark_test() ->
907    run_multipart_parsing_benchmark(1).
908
909run_multipart_parsing_benchmark(0) -> ok;
910run_multipart_parsing_benchmark(N) ->
911    multipart_parsing_benchmark(),
912    run_multipart_parsing_benchmark(N - 1).
913
914multipart_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.
972