Subversion Repositories SE.SVN

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
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.