PageRenderTime 73ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/src/mochiweb_multipart.erl

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