From 3a4fad07cb5097598eefd0cd7f588c5881a61c8f Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Sat, 21 Nov 2009 17:01:50 +0000 Subject: upgrade mochiweb to r113, use hooks instead of forking mochijson2. COUCHDB-474 git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@882941 13f79535-47bb-0310-9956-ffa450edef68 --- src/couchdb/couch_db.hrl | 4 +- src/couchdb/couch_util.erl | 14 +++ src/mochiweb/Makefile.am | 2 +- src/mochiweb/mochijson.erl | 17 ++-- src/mochiweb/mochijson2.erl | 102 ++++++++++++++------ src/mochiweb/mochiweb.app.in | 2 +- src/mochiweb/mochiweb_html.erl | 15 ++- src/mochiweb/mochiweb_http.erl | 26 +++-- src/mochiweb/mochiweb_multipart.erl | 165 +++++++++++++++++++++++++------- src/mochiweb/mochiweb_request.erl | 119 +++++++++++++++-------- src/mochiweb/mochiweb_skel.erl | 6 +- src/mochiweb/mochiweb_socket_server.erl | 2 +- src/mochiweb/mochiweb_util.erl | 6 +- src/mochiweb/reloader.erl | 3 +- 14 files changed, 352 insertions(+), 131 deletions(-) (limited to 'src') diff --git a/src/couchdb/couch_db.hrl b/src/couchdb/couch_db.hrl index bd297b2a..f2ccb453 100644 --- a/src/couchdb/couch_db.hrl +++ b/src/couchdb/couch_db.hrl @@ -17,8 +17,8 @@ -define(MIN_STR, <<"">>). -define(MAX_STR, <<255>>). % illegal utf string --define(JSON_ENCODE(V), mochijson2:encode(V)). --define(JSON_DECODE(V), mochijson2:decode(V)). +-define(JSON_ENCODE(V), couch_util:json_encode(V)). +-define(JSON_DECODE(V), couch_util:json_decode(V)). -define(b2a(V), list_to_atom(binary_to_list(V))). -define(b2l(V), binary_to_list(V)). diff --git a/src/couchdb/couch_util.erl b/src/couchdb/couch_util.erl index d02be8d9..da62375c 100644 --- a/src/couchdb/couch_util.erl +++ b/src/couchdb/couch_util.erl @@ -20,6 +20,7 @@ to_hex/1,parse_term/1, dict_find/3]). -export([file_read_size/1, get_nested_json_value/2, json_user_ctx/1]). -export([to_binary/1, to_integer/1, to_list/1, url_encode/1]). +-export([json_encode/1, json_decode/1]). -include("couch_db.hrl"). -include_lib("kernel/include/file.hrl"). @@ -406,3 +407,16 @@ url_encode([H|T]) -> end; url_encode([]) -> []. + +json_encode(V) -> + Handler = + fun({L}) when is_list(L) -> + {struct,L}; + (Bad) -> + exit({json_encode, {bad_term, Bad}}) + end, + (mochijson2:encoder([{handler, Handler}]))(V). + +json_decode(V) -> + try (mochijson2:decoder([{object_hook, fun({struct,L}) -> {L} end}]))(V) + catch _:_ -> throw({invalid_json,V}) end. diff --git a/src/mochiweb/Makefile.am b/src/mochiweb/Makefile.am index 608d4dcd..c191abfa 100644 --- a/src/mochiweb/Makefile.am +++ b/src/mochiweb/Makefile.am @@ -10,7 +10,7 @@ ## License for the specific language governing permissions and limitations under ## the License. -mochiwebebindir = $(localerlanglibdir)/mochiweb-r97/ebin +mochiwebebindir = $(localerlanglibdir)/mochiweb-r113/ebin mochiweb_file_collection = \ mochifmt.erl \ diff --git a/src/mochiweb/mochijson.erl b/src/mochiweb/mochijson.erl index 029642ac..74695a75 100644 --- a/src/mochiweb/mochijson.erl +++ b/src/mochiweb/mochijson.erl @@ -186,7 +186,8 @@ json_encode_string_utf8_1([C | Cs]) when C >= 0, C =< 16#7f -> end, [NewC | json_encode_string_utf8_1(Cs)]; json_encode_string_utf8_1(All=[C | _]) when C >= 16#80, C =< 16#10FFFF -> - json_encode_string_unicode(xmerl_ucs:from_utf8(All)); + [?Q | Rest] = json_encode_string_unicode(xmerl_ucs:from_utf8(All)), + Rest; json_encode_string_utf8_1([]) -> "\"". @@ -459,16 +460,18 @@ equiv_object(Props1, Props2) -> equiv_list([], []) -> true; equiv_list([V1 | L1], [V2 | L2]) -> - case equiv(V1, V2) of - true -> - equiv_list(L1, L2); - false -> - false - end. + equiv(V1, V2) andalso equiv_list(L1, L2). test_all() -> + test_issue33(), test_one(e2j_test_vec(utf8), 1). +test_issue33() -> + %% http://code.google.com/p/mochiweb/issues/detail?id=33 + Js = {struct, [{"key", [194, 163]}]}, + Encoder = encoder([{input_encoding, utf8}]), + "{\"key\":\"\\u00a3\"}" = lists:flatten(Encoder(Js)). + test_one([], _N) -> %% io:format("~p tests passed~n", [N-1]), ok; diff --git a/src/mochiweb/mochijson2.erl b/src/mochiweb/mochijson2.erl index ee19458c..66f68bf0 100644 --- a/src/mochiweb/mochijson2.erl +++ b/src/mochiweb/mochijson2.erl @@ -42,7 +42,8 @@ %% @type json_term() = json_string() | json_number() | json_array() | %% json_object() --record(encoder, {handler=null}). +-record(encoder, {handler=null, + utf8=false}). -record(decoder, {object_hook=null, offset=0, @@ -52,6 +53,8 @@ %% @spec encoder([encoder_option()]) -> function() %% @doc Create an encoder/1 with the given options. +%% @type encoder_option() = handler_option() | utf8_option() +%% @type utf8_option() = boolean(). Emit unicode as utf8 (default - false) encoder(Options) -> State = parse_encoder_options(Options, #encoder{}), fun (O) -> json_encode(O, State) end. @@ -70,10 +73,7 @@ decoder(Options) -> %% @spec decode(iolist()) -> json_term() %% @doc Decode the given iolist to Erlang terms. decode(S) -> - try json_decode(S, #decoder{}) - catch - _:_ -> throw({invalid_json, S}) - end. + json_decode(S, #decoder{}). test() -> test_all(). @@ -83,7 +83,9 @@ test() -> parse_encoder_options([], State) -> State; parse_encoder_options([{handler, Handler} | Rest], State) -> - parse_encoder_options(Rest, State#encoder{handler=Handler}). + parse_encoder_options(Rest, State#encoder{handler=Handler}); +parse_encoder_options([{utf8, Switch} | Rest], State) -> + parse_encoder_options(Rest, State#encoder{utf8=Switch}). parse_decoder_options([], State) -> State; @@ -96,15 +98,18 @@ json_encode(false, _State) -> <<"false">>; json_encode(null, _State) -> <<"null">>; -json_encode(I, _State) when is_integer(I) -> +json_encode(I, _State) when is_integer(I) andalso I >= -2147483648 andalso I =< 2147483647 -> + %% Anything outside of 32-bit integers should be encoded as a float integer_to_list(I); +json_encode(I, _State) when is_integer(I) -> + mochinum:digits(float(I)); json_encode(F, _State) when is_float(F) -> mochinum:digits(F); json_encode(S, State) when is_binary(S); is_atom(S) -> json_encode_string(S, State); json_encode(Array, State) when is_list(Array) -> json_encode_array(Array, State); -json_encode({Props}, State) when is_list(Props) -> +json_encode({struct, Props}, State) when is_list(Props) -> json_encode_proplist(Props, State); json_encode(Bad, #encoder{handler=null}) -> exit({json_encode, {bad_term, Bad}}); @@ -131,29 +136,29 @@ json_encode_proplist(Props, State) -> [$, | Acc1] = lists:foldl(F, "{", Props), lists:reverse([$\} | Acc1]). -json_encode_string(A, _State) when is_atom(A) -> +json_encode_string(A, State) when is_atom(A) -> L = atom_to_list(A), case json_string_is_safe(L) of true -> [?Q, L, ?Q]; false -> - json_encode_string_unicode(xmerl_ucs:from_utf8(L), [?Q]) + json_encode_string_unicode(xmerl_ucs:from_utf8(L), State, [?Q]) end; -json_encode_string(B, _State) when is_binary(B) -> +json_encode_string(B, State) when is_binary(B) -> case json_bin_is_safe(B) of true -> [?Q, B, ?Q]; false -> - json_encode_string_unicode(xmerl_ucs:from_utf8(B), [?Q]) + json_encode_string_unicode(xmerl_ucs:from_utf8(B), State, [?Q]) end; json_encode_string(I, _State) when is_integer(I) -> [?Q, integer_to_list(I), ?Q]; -json_encode_string(L, _State) when is_list(L) -> +json_encode_string(L, State) when is_list(L) -> case json_string_is_safe(L) of true -> [?Q, L, ?Q]; false -> - json_encode_string_unicode(L, [?Q]) + json_encode_string_unicode(L, State, [?Q]) end. json_string_is_safe([]) -> @@ -208,9 +213,9 @@ json_bin_is_safe(<>) -> false end. -json_encode_string_unicode([], Acc) -> +json_encode_string_unicode([], _State, Acc) -> lists:reverse([$\" | Acc]); -json_encode_string_unicode([C | Cs], Acc) -> +json_encode_string_unicode([C | Cs], State, Acc) -> Acc1 = case C of ?Q -> [?Q, $\\ | Acc]; @@ -236,14 +241,18 @@ json_encode_string_unicode([C | Cs], Acc) -> [$r, $\\ | Acc]; $\t -> [$t, $\\ | Acc]; - C when C >= 0, C < $\s; C >= 16#7f, C =< 16#10FFFF -> + C when C >= 0, C < $\s -> + [unihex(C) | Acc]; + C when C >= 16#7f, C =< 16#10FFFF, State#encoder.utf8 -> + [xmerl_ucs:to_utf8(C) | Acc]; + C when C >= 16#7f, C =< 16#10FFFF, not State#encoder.utf8 -> [unihex(C) | Acc]; C when C < 16#7f -> [C | Acc]; _ -> exit({json_encode, {bad_char, C}}) end, - json_encode_string_unicode(Cs, Acc1). + json_encode_string_unicode(Cs, State, Acc1). hexdigit(C) when C >= 0, C =< 9 -> C + $0; @@ -288,7 +297,7 @@ decode_object(B, S) -> decode_object(B, S=#decoder{state=key}, Acc) -> case tokenize(B, S) of {end_object, S1} -> - V = make_object({lists:reverse(Acc)}, S1), + V = make_object({struct, lists:reverse(Acc)}, S1), {V, S1#decoder{state=null}}; {{const, K}, S1} -> {colon, S2} = tokenize(B, S1), @@ -298,7 +307,7 @@ decode_object(B, S=#decoder{state=key}, Acc) -> decode_object(B, S=#decoder{state=comma}, Acc) -> case tokenize(B, S) of {end_object, S1} -> - V = make_object({lists:reverse(Acc)}, S1), + V = make_object({struct, lists:reverse(Acc)}, S1), {V, S1#decoder{state=null}}; {comma, S1} -> decode_object(B, S1#decoder{state=key}, Acc) @@ -507,9 +516,9 @@ tokenize(B, S=#decoder{offset=O}) -> %% Create an object from a list of Key/Value pairs. obj_new() -> - {[]}. + {struct, []}. -is_obj({Props}) -> +is_obj({struct, Props}) -> F = fun ({K, _}) when is_binary(K) -> true; (_) -> @@ -518,7 +527,7 @@ is_obj({Props}) -> lists:all(F, Props). obj_from_list(Props) -> - Obj = {Props}, + Obj = {struct, Props}, case is_obj(Obj) of true -> Obj; false -> exit({json_bad_object, Obj}) @@ -529,7 +538,7 @@ obj_from_list(Props) -> %% compare unequal as erlang terms, so we need to carefully recurse %% through aggregates (tuples and objects). -equiv({Props1}, {Props2}) -> +equiv({struct, Props1}, {struct, Props2}) -> equiv_object(Props1, Props2); equiv(L1, L2) when is_list(L1), is_list(L2) -> equiv_list(L1, L2); @@ -555,16 +564,13 @@ equiv_object(Props1, Props2) -> equiv_list([], []) -> true; equiv_list([V1 | L1], [V2 | L2]) -> - case equiv(V1, V2) of - true -> - equiv_list(L1, L2); - false -> - false - end. + equiv(V1, V2) andalso equiv_list(L1, L2). test_all() -> [1199344435545.0, 1] = decode(<<"[1199344435545.0,1]">>), <<16#F0,16#9D,16#9C,16#95>> = decode([34,"\\ud835","\\udf15",34]), + test_encoder_utf8(), + test_input_validation(), test_one(e2j_test_vec(utf8), 1). test_one([], _N) -> @@ -619,3 +625,39 @@ e2j_test_vec(utf8) -> {[-123, <<"foo">>, obj_from_list([{<<"bar">>, []}]), null], "[-123,\"foo\",{\"bar\":[]},null]"} ]. + +%% test utf8 encoding +test_encoder_utf8() -> + %% safe conversion case (default) + [34,"\\u0001","\\u0442","\\u0435","\\u0441","\\u0442",34] = + encode(<<1,"\321\202\320\265\321\201\321\202">>), + + %% raw utf8 output (optional) + Enc = mochijson2:encoder([{utf8, true}]), + [34,"\\u0001",[209,130],[208,181],[209,129],[209,130],34] = + Enc(<<1,"\321\202\320\265\321\201\321\202">>). + +test_input_validation() -> + Good = [ + {16#00A3, <>}, % pound + {16#20AC, <>}, % euro + {16#10196, <>} % denarius + ], + lists:foreach(fun({CodePoint, UTF8}) -> + Expect = list_to_binary(xmerl_ucs:to_utf8(CodePoint)), + Expect = decode(UTF8) + end, Good), + + Bad = [ + % 2nd, 3rd, or 4th byte of a multi-byte sequence w/o leading byte + <>, + % missing continuations, last byte in each should be 80-BF + <>, + <>, + <>, + % we don't support code points > 10FFFF per RFC 3629 + <> + ], + lists:foreach(fun(X) -> + ok = try decode(X) catch invalid_utf8 -> ok end + end, Bad). diff --git a/src/mochiweb/mochiweb.app.in b/src/mochiweb/mochiweb.app.in index cd8dbb25..b0f90144 100644 --- a/src/mochiweb/mochiweb.app.in +++ b/src/mochiweb/mochiweb.app.in @@ -1,6 +1,6 @@ {application, mochiweb, [{description, "MochiMedia Web Server"}, - {vsn, "0.01"}, + {vsn, "113"}, {modules, [ mochihex, mochijson, diff --git a/src/mochiweb/mochiweb_html.erl b/src/mochiweb/mochiweb_html.erl index 0e030c13..77100d50 100644 --- a/src/mochiweb/mochiweb_html.erl +++ b/src/mochiweb/mochiweb_html.erl @@ -305,6 +305,18 @@ test_tokens() -> {data, <<" A= B <= C ">>, false}, {end_tag, <<"script">>}] = tokens(<<"">>), + [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, + {data, <<" A= B <= C ">>, false}, + {end_tag, <<"script">>}] = + tokens(<<"">>), + [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, + {data, <<" A= B <= C ">>, false}, + {end_tag, <<"script">>}] = + tokens(<<"">>), + [{start_tag, <<"script">>, [{<<"type">>, <<"text/javascript">>}], false}, + {data, <<" A= B <= C ">>, false}, + {end_tag, <<"script">>}] = + tokens(<<"">>), [{start_tag, <<"textarea">>, [], false}, {data, <<"">>, false}, {end_tag, <<"textarea">>}] = @@ -672,7 +684,8 @@ tokenize_attr_value(Attr, B, S) -> O = S1#decoder.offset, case B of <<_:O/binary, "=", _/binary>> -> - tokenize_word_or_literal(B, ?INC_COL(S1)); + S2 = skip_whitespace(B, ?INC_COL(S1)), + tokenize_word_or_literal(B, S2); _ -> {Attr, S1} end. diff --git a/src/mochiweb/mochiweb_http.erl b/src/mochiweb/mochiweb_http.erl index 14a36578..f1821f40 100644 --- a/src/mochiweb/mochiweb_http.erl +++ b/src/mochiweb/mochiweb_http.erl @@ -7,6 +7,7 @@ -author('bob@mochimedia.com'). -export([start/0, start/1, stop/0, stop/1]). -export([loop/2, default_body/1]). +-export([after_response/2, reentry/1]). -define(IDLE_TIMEOUT, 30000). @@ -110,6 +111,11 @@ request(Socket, Body) -> exit(normal) end. +reentry(Body) -> + fun (Req) -> + ?MODULE:after_response(Body, Req) + end. + headers(Socket, Request, Headers, _Body, ?MAX_HEADERS) -> %% Too many headers sent, bad request. inet:setopts(Socket, [{packet, raw}]), @@ -125,14 +131,7 @@ headers(Socket, Request, Headers, Body, HeaderCount) -> Req = mochiweb:new_request({Socket, Request, lists:reverse(Headers)}), Body(Req), - case Req:should_close() of - true -> - gen_tcp:close(Socket), - exit(normal); - false -> - Req:cleanup(), - ?MODULE:loop(Socket, Body) - end; + ?MODULE:after_response(Body, Req); {ok, {http_header, _, Name, _, Value}} -> headers(Socket, Request, [{Name, Value} | Headers], Body, 1 + HeaderCount); @@ -140,3 +139,14 @@ headers(Socket, Request, Headers, Body, HeaderCount) -> gen_tcp:close(Socket), exit(normal) end. + +after_response(Body, Req) -> + Socket = Req:get(socket), + case Req:should_close() of + true -> + gen_tcp:close(Socket), + exit(normal); + false -> + Req:cleanup(), + ?MODULE:loop(Socket, Body) + end. diff --git a/src/mochiweb/mochiweb_multipart.erl b/src/mochiweb/mochiweb_multipart.erl index b9631613..0368a9a6 100644 --- a/src/mochiweb/mochiweb_multipart.erl +++ b/src/mochiweb/mochiweb_multipart.erl @@ -76,15 +76,15 @@ parse_multipart_request(Req, Callback) -> Boundary = iolist_to_binary( get_boundary(Req:get_header_value("content-type"))), Prefix = <<"\r\n--", Boundary/binary>>, - BS = size(Boundary), + BS = byte_size(Boundary), Chunk = read_chunk(Req, Length), - Length1 = Length - size(Chunk), + Length1 = Length - byte_size(Chunk), <<"--", Boundary:BS/binary, "\r\n", Rest/binary>> = Chunk, - feed_mp(headers, #mp{boundary=Prefix, - length=Length1, - buffer=Rest, - callback=Callback, - req=Req}). + feed_mp(headers, flash_multipart_hack(#mp{boundary=Prefix, + length=Length1, + buffer=Rest, + callback=Callback, + req=Req})). parse_headers(<<>>) -> []; @@ -117,8 +117,27 @@ read_chunk(Req, Length) when Length > 0 -> read_more(State=#mp{length=Length, buffer=Buffer, req=Req}) -> Data = read_chunk(Req, Length), Buffer1 = <>, - State#mp{length=Length - size(Data), - buffer=Buffer1}. + flash_multipart_hack(State#mp{length=Length - byte_size(Data), + buffer=Buffer1}). + +flash_multipart_hack(State=#mp{length=0, buffer=Buffer, boundary=Prefix}) -> + %% http://code.google.com/p/mochiweb/issues/detail?id=22 + %% Flash doesn't terminate multipart with \r\n properly so we fix it up here + PrefixSize = size(Prefix), + case size(Buffer) - (2 + PrefixSize) of + Seek when Seek >= 0 -> + case Buffer of + <<_:Seek/binary, Prefix:PrefixSize/binary, "--">> -> + Buffer1 = <>, + State#mp{buffer=Buffer1}; + _ -> + State + end; + _ -> + State + end; +flash_multipart_hack(State) -> + State. feed_mp(headers, State=#mp{buffer=Buffer, callback=Callback}) -> {State1, P} = case find_in_binary(<<"\r\n\r\n">>, Buffer) of @@ -136,7 +155,8 @@ feed_mp(headers, State=#mp{buffer=Buffer, callback=Callback}) -> feed_mp(body, State1#mp{buffer=Rest, callback=NextCallback}); feed_mp(body, State=#mp{boundary=Prefix, buffer=Buffer, callback=Callback}) -> - case find_boundary(Prefix, Buffer) of + Boundary = find_boundary(Prefix, Buffer), + case Boundary of {end_boundary, Start, Skip} -> <> = Buffer, C1 = Callback({body, Data}), @@ -158,7 +178,7 @@ feed_mp(body, State=#mp{boundary=Prefix, buffer=Buffer, callback=Callback}) -> end. get_boundary(ContentType) -> - {"multipart/" ++ _, Opts} = mochiweb_util:parse_header(ContentType), + {"multipart/form-data", Opts} = mochiweb_util:parse_header(ContentType), case proplists:get_value("boundary", Opts) of S when is_list(S) -> S @@ -242,7 +262,11 @@ test_callback(Expect, [Expect | Rest]) -> ok; _ -> fun (Next) -> test_callback(Next, Rest) end - end. + end; +test_callback({body, Got}, [{body, Expect} | Rest]) -> + GotSize = size(Got), + <> = Expect, + fun (Next) -> test_callback(Next, [{body, Expect1} | Rest]) end. test_parse3() -> ContentType = "multipart/form-data; boundary=---------------------------7386909285754635891697677882", @@ -261,14 +285,12 @@ test_parse3() -> eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> - case gen_tcp:send(Socket, BinContent) of - ok -> - exit(normal) - end + ok = gen_tcp:send(Socket, BinContent), + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, - size(BinContent)), + byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok @@ -294,14 +316,12 @@ test_parse2() -> eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> - case gen_tcp:send(Socket, BinContent) of - ok -> - exit(normal) - end + ok = gen_tcp:send(Socket, BinContent), + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, - size(BinContent)), + byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok @@ -327,14 +347,12 @@ test_parse_form() -> ""], "\r\n"), BinContent = iolist_to_binary(Content), ServerFun = fun (Socket) -> - case gen_tcp:send(Socket, BinContent) of - ok -> - exit(normal) - end + ok = gen_tcp:send(Socket, BinContent), + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, - size(BinContent)), + byte_size(BinContent)), Res = parse_form(Req), [{"submit-name", "Larry"}, {"files", {"file1.txt", {"text/plain",[]}, @@ -376,14 +394,12 @@ test_parse() -> eof], TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> - case gen_tcp:send(Socket, BinContent) of - ok -> - exit(normal) - end + ok = gen_tcp:send(Socket, BinContent), + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, - size(BinContent)), + byte_size(BinContent)), Res = parse_multipart_request(Req, TestCallback), {0, <<>>, ok} = Res, ok @@ -419,6 +435,89 @@ test_find_in_binary() -> {partial, 1, 3} = find_in_binary(<<"foobar">>, <<"afoo">>), ok. +test_flash_parse() -> + ContentType = "multipart/form-data; boundary=----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5", + "----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5" = get_boundary(ContentType), + 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--">>, + Expect = [{headers, + [{"content-disposition", + {"form-data", [{"name", "Filename"}]}}]}, + {body, <<"hello.txt">>}, + body_end, + {headers, + [{"content-disposition", + {"form-data", [{"name", "success_action_status"}]}}]}, + {body, <<"201">>}, + body_end, + {headers, + [{"content-disposition", + {"form-data", [{"name", "file"}, {"filename", "hello.txt"}]}}, + {"content-type", {"application/octet-stream", []}}]}, + {body, <<"hello\n">>}, + body_end, + {headers, + [{"content-disposition", + {"form-data", [{"name", "Upload"}]}}]}, + {body, <<"Submit Query">>}, + body_end, + eof], + TestCallback = fun (Next) -> test_callback(Next, Expect) end, + ServerFun = fun (Socket) -> + ok = gen_tcp:send(Socket, BinContent), + exit(normal) + end, + ClientFun = fun (Socket) -> + Req = fake_request(Socket, ContentType, + byte_size(BinContent)), + Res = parse_multipart_request(Req, TestCallback), + {0, <<>>, ok} = Res, + ok + end, + ok = with_socket_server(ServerFun, ClientFun), + ok. + +test_flash_parse2() -> + ContentType = "multipart/form-data; boundary=----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5", + "----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5" = get_boundary(ContentType), + Chunk = iolist_to_binary(string:copies("%", 4096)), + 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--">>, + Expect = [{headers, + [{"content-disposition", + {"form-data", [{"name", "Filename"}]}}]}, + {body, <<"hello.txt">>}, + body_end, + {headers, + [{"content-disposition", + {"form-data", [{"name", "success_action_status"}]}}]}, + {body, <<"201">>}, + body_end, + {headers, + [{"content-disposition", + {"form-data", [{"name", "file"}, {"filename", "hello.txt"}]}}, + {"content-type", {"application/octet-stream", []}}]}, + {body, Chunk}, + body_end, + {headers, + [{"content-disposition", + {"form-data", [{"name", "Upload"}]}}]}, + {body, <<"Submit Query">>}, + body_end, + eof], + TestCallback = fun (Next) -> test_callback(Next, Expect) end, + ServerFun = fun (Socket) -> + ok = gen_tcp:send(Socket, BinContent), + exit(normal) + end, + ClientFun = fun (Socket) -> + Req = fake_request(Socket, ContentType, + byte_size(BinContent)), + Res = parse_multipart_request(Req, TestCallback), + {0, <<>>, ok} = Res, + ok + end, + ok = with_socket_server(ServerFun, ClientFun), + ok. + test() -> test_find_in_binary(), test_find_boundary(), @@ -426,4 +525,6 @@ test() -> test_parse2(), test_parse3(), test_parse_form(), + test_flash_parse(), + test_flash_parse2(), ok. diff --git a/src/mochiweb/mochiweb_request.erl b/src/mochiweb/mochiweb_request.erl index e8f0a67c..fc296f40 100644 --- a/src/mochiweb/mochiweb_request.erl +++ b/src/mochiweb/mochiweb_request.erl @@ -190,8 +190,13 @@ stream_body(MaxChunkSize, ChunkFun, FunState) -> stream_body(MaxChunkSize, ChunkFun, FunState, undefined). stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) -> - - case get_header_value("expect") of + Expect = case get_header_value("expect") of + undefined -> + undefined; + Value when is_list(Value) -> + string:to_lower(Value) + end, + case Expect of "100-continue" -> start_raw_response({100, gb_trees:empty()}); _Else -> @@ -214,7 +219,7 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) -> MaxBodyLength when is_integer(MaxBodyLength), MaxBodyLength < Length -> exit({body_too_large, content_length}); _ -> - stream_unchunked_body(Length, MaxChunkSize, ChunkFun, FunState) + stream_unchunked_body(Length, ChunkFun, FunState) end; Length -> exit({length_not_integer, Length}) @@ -449,13 +454,20 @@ stream_chunked_body(MaxChunkSize, Fun, FunState) -> stream_chunked_body(MaxChunkSize, Fun, NewState) end. -stream_unchunked_body(0, _MaxChunkSize, Fun, FunState) -> +stream_unchunked_body(0, Fun, FunState) -> Fun({0, <<>>}, FunState); -stream_unchunked_body(Length, _, Fun, FunState) when Length > 0 -> +stream_unchunked_body(Length, Fun, FunState) when Length > 0 -> Bin = recv(0), - BinSize = size(Bin), - NewState = Fun({BinSize, Bin}, FunState), - stream_unchunked_body(Length - BinSize, 0, Fun, NewState). + BinSize = byte_size(Bin), + if BinSize > Length -> + <> = Bin, + gen_tcp:unrecv(Socket, Extra), + NewState = Fun({Length, OurBody}, FunState), + stream_unchunked_body(0, Fun, NewState); + true -> + NewState = Fun({BinSize, Bin}, FunState), + stream_unchunked_body(Length - BinSize, Fun, NewState) + end. %% @spec read_chunk_length() -> integer() @@ -521,40 +533,69 @@ serve_file(Path, DocRoot, ExtraHeaders) -> not_found(ExtraHeaders); RelPath -> FullPath = filename:join([DocRoot, RelPath]), - File = case filelib:is_dir(FullPath) of - true -> - filename:join([FullPath, "index.html"]); - false -> - FullPath - end, - case file:read_file_info(File) of - {ok, FileInfo} -> - LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime), - case get_header_value("if-modified-since") of - LastModified -> - respond({304, ExtraHeaders, ""}); - _ -> - case file:open(File, [raw, binary]) of - {ok, IoDevice} -> - ContentType = mochiweb_util:guess_mime(File), - Res = ok({ContentType, - [{"last-modified", LastModified} - | ExtraHeaders], - {file, IoDevice}}), - file:close(IoDevice), - Res; - _ -> - not_found(ExtraHeaders) - end - end; - {error, _} -> - not_found(ExtraHeaders) + case filelib:is_dir(FullPath) of + true -> + maybe_redirect(RelPath, FullPath, ExtraHeaders); + false -> + maybe_serve_file(FullPath, ExtraHeaders) end end. - %% Internal API +%% This has the same effect as the DirectoryIndex directive in httpd +directory_index(FullPath) -> + filename:join([FullPath, "index.html"]). + +maybe_redirect([], FullPath, ExtraHeaders) -> + maybe_serve_file(directory_index(FullPath), ExtraHeaders); + +maybe_redirect(RelPath, FullPath, ExtraHeaders) -> + case string:right(RelPath, 1) of + "/" -> + maybe_serve_file(directory_index(FullPath), ExtraHeaders); + _ -> + Host = mochiweb_headers:get_value("host", Headers), + Location = "http://" ++ Host ++ "/" ++ RelPath ++ "/", + LocationBin = list_to_binary(Location), + MoreHeaders = [{"Location", Location}, + {"Content-Type", "text/html"} | ExtraHeaders], + Top = <<"" + "" + "301 Moved Permanently" + "" + "

Moved Permanently

" + "

The document has moved >, + Bottom = <<">here.

\n">>, + Body = <>, + respond({301, MoreHeaders, Body}) + end. + +maybe_serve_file(File, ExtraHeaders) -> + case file:read_file_info(File) of + {ok, FileInfo} -> + LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime), + case get_header_value("if-modified-since") of + LastModified -> + respond({304, ExtraHeaders, ""}); + _ -> + case file:open(File, [raw, binary]) of + {ok, IoDevice} -> + ContentType = mochiweb_util:guess_mime(File), + Res = ok({ContentType, + [{"last-modified", LastModified} + | ExtraHeaders], + {file, IoDevice}}), + file:close(IoDevice), + Res; + _ -> + not_found(ExtraHeaders) + end + end; + {error, _} -> + not_found(ExtraHeaders) + end. + server_headers() -> [{"Server", "MochiWeb/1.0 (" ++ ?QUIP ++ ")"}, {"Date", httpd_util:rfc1123_date()}]. @@ -639,7 +680,6 @@ range_parts({file, IoDevice}, Ranges) -> end, LocNums, Data), {Bodies, Size}; - range_parts(Body0, Ranges) -> Body = iolist_to_binary(Body0), Size = size(Body), @@ -706,7 +746,6 @@ test_range() -> [{none, 20}] = parse_range_request("bytes=-20"), io:format(".. ok ~n"), - %% invalid, single ranges io:format("Testing parse_range_request with invalid ranges~n"), io:format("1"), @@ -734,7 +773,7 @@ test_range() -> io:format(".. ok~n"), Body = <<"012345678901234567890123456789012345678901234567890123456789">>, - BodySize = size(Body), %% 60 + BodySize = byte_size(Body), %% 60 BodySize = 60, %% these values assume BodySize =:= 60 diff --git a/src/mochiweb/mochiweb_skel.erl b/src/mochiweb/mochiweb_skel.erl index 098951be..36b48be5 100644 --- a/src/mochiweb/mochiweb_skel.erl +++ b/src/mochiweb/mochiweb_skel.erl @@ -29,7 +29,8 @@ skel() -> "skel". skelcopy(Src, DestDir, Name, LDst) -> - {ok, Dest, _} = regexp:gsub(filename:basename(Src), skel(), Name), + Dest = re:replace(filename:basename(Src), skel(), Name, + [global, {return, list}]), case file:read_file_info(Src) of {ok, #file_info{type=directory, mode=Mode}} -> Dir = DestDir ++ "/" ++ Dest, @@ -50,7 +51,8 @@ skelcopy(Src, DestDir, Name, LDst) -> {ok, #file_info{type=regular, mode=Mode}} -> OutFile = filename:join(DestDir, Dest), {ok, B} = file:read_file(Src), - {ok, S, _} = regexp:gsub(binary_to_list(B), skel(), Name), + S = re:replace(binary_to_list(B), skel(), Name, + [{return, list}, global]), ok = file:write_file(OutFile, list_to_binary(S)), ok = file:write_file_info(OutFile, #file_info{mode=Mode}), io:format(" ~s~n", [filename:basename(Src)]), diff --git a/src/mochiweb/mochiweb_socket_server.erl b/src/mochiweb/mochiweb_socket_server.erl index a483c3d0..7aafe290 100644 --- a/src/mochiweb/mochiweb_socket_server.erl +++ b/src/mochiweb/mochiweb_socket_server.erl @@ -22,7 +22,7 @@ ip=any, listen=null, acceptor=null, - backlog=30}). + backlog=128}). start(State=#mochiweb_socket_server{}) -> start_server(State); diff --git a/src/mochiweb/mochiweb_util.erl b/src/mochiweb/mochiweb_util.erl index 7bf18d15..73cacea4 100644 --- a/src/mochiweb/mochiweb_util.erl +++ b/src/mochiweb/mochiweb_util.erl @@ -78,7 +78,7 @@ safe_relative_path("", Acc) -> [] -> ""; _ -> - join(lists:reverse(Acc), "/") + string:join(lists:reverse(Acc), "/") end; safe_relative_path(P, Acc) -> case partition(P, "/") of @@ -423,9 +423,7 @@ record_to_proplist(Record, Fields) -> %% Fields should be obtained by calling record_info(fields, record_type) %% where record_type is the record type of Record record_to_proplist(Record, Fields, TypeKey) - when is_tuple(Record), - is_list(Fields), - size(Record) - 1 =:= length(Fields) -> + when tuple_size(Record) - 1 =:= length(Fields) -> lists:zip([TypeKey | Fields], tuple_to_list(Record)). diff --git a/src/mochiweb/reloader.erl b/src/mochiweb/reloader.erl index 2ff154ba..6835f8f9 100644 --- a/src/mochiweb/reloader.erl +++ b/src/mochiweb/reloader.erl @@ -78,8 +78,7 @@ code_change(_Vsn, State, _Extra) -> doit(From, To) -> [case file:read_file_info(Filename) of - {ok, FileInfo} when FileInfo#file_info.mtime >= From, - FileInfo#file_info.mtime < To -> + {ok, #file_info{mtime = Mtime}} when Mtime >= From, Mtime < To -> reload(Module); {ok, _} -> unmodified; -- cgit v1.2.3