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