PageRenderTime 182ms CodeModel.GetById 49ms app.highlight 121ms RepoModel.GetById 1ms app.codeStats 1ms

/src/mochiweb_multipart.erl

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