Return-Path: X-Original-To: apmail-couchdb-commits-archive@www.apache.org Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: from mail.apache.org (hermes.apache.org [140.211.11.3]) by minotaur.apache.org (Postfix) with SMTP id AC94FF805 for ; Wed, 24 Apr 2013 22:18:33 +0000 (UTC) Received: (qmail 67860 invoked by uid 500); 24 Apr 2013 22:18:33 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 67778 invoked by uid 500); 24 Apr 2013 22:18:33 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 67707 invoked by uid 99); 24 Apr 2013 22:18:33 -0000 Received: from tyr.zones.apache.org (HELO tyr.zones.apache.org) (140.211.11.114) by apache.org (qpsmtpd/0.29) with ESMTP; Wed, 24 Apr 2013 22:18:33 +0000 Received: by tyr.zones.apache.org (Postfix, from userid 65534) id D42A28805D4; Wed, 24 Apr 2013 22:18:32 +0000 (UTC) Content-Type: text/plain; charset="us-ascii" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit From: dch@apache.org To: commits@couchdb.apache.org Date: Wed, 24 Apr 2013 22:18:33 -0000 Message-Id: <3b501c512009443894fffe79d752bdd2@git.apache.org> In-Reply-To: <0508cd9ebbf74002bd47ef495d75027b@git.apache.org> References: <0508cd9ebbf74002bd47ef495d75027b@git.apache.org> X-Mailer: ASF-Git Admin Mailer Subject: [2/8] COUCHDB-1696 import mochiweb from tag v2.4.2 http://git-wip-us.apache.org/repos/asf/couchdb/blob/cbb8a550/src/mochiweb/mochiweb_html.erl ---------------------------------------------------------------------- diff --git a/src/mochiweb/mochiweb_html.erl b/src/mochiweb/mochiweb_html.erl index 0f281db..965c846 100644 --- a/src/mochiweb/mochiweb_html.erl +++ b/src/mochiweb/mochiweb_html.erl @@ -95,7 +95,12 @@ to_tokens({Tag0, Acc}) -> to_tokens({Tag0, [], Acc}); to_tokens({Tag0, Attrs, Acc}) -> Tag = to_tag(Tag0), - to_tokens([{Tag, Acc}], [{start_tag, Tag, Attrs, is_singleton(Tag)}]). + case is_singleton(Tag) of + true -> + to_tokens([], [{start_tag, Tag, Attrs, true}]); + false -> + to_tokens([{Tag, Acc}], [{start_tag, Tag, Attrs, false}]) + end. %% @spec to_html([html_token()] | html_node()) -> iolist() %% @doc Convert a list of html_token() to a HTML document. @@ -312,7 +317,8 @@ tokenize(B, S=#decoder{offset=O}) -> {Tag, S1} = tokenize_literal(B, ?ADV_COL(S, 2)), {S2, _} = find_gt(B, S1), {{end_tag, Tag}, S2}; - <<_:O/binary, "<", C, _/binary>> when ?IS_WHITESPACE(C) -> + <<_:O/binary, "<", C, _/binary>> + when ?IS_WHITESPACE(C); not ?IS_LITERAL_SAFE(C) -> %% This isn't really strict HTML {{data, Data, _Whitespace}, S1} = tokenize_data(B, ?INC_COL(S)), {{data, <<$<, Data/binary>>, false}, S1}; @@ -480,7 +486,7 @@ tokenize_attr_value(Attr, B, S) -> _ -> {Attr, S1} end. - + tokenize_quoted_or_unquoted_attr_value(B, S=#decoder{offset=O}) -> case B of <<_:O/binary>> -> @@ -491,7 +497,7 @@ tokenize_quoted_or_unquoted_attr_value(B, S=#decoder{offset=O}) -> <<_:O/binary, _/binary>> -> tokenize_unquoted_attr_value(B, S, []) end. - + tokenize_quoted_attr_value(B, S=#decoder{offset=O}, Acc, Q) -> case B of <<_:O/binary>> -> @@ -501,12 +507,10 @@ tokenize_quoted_attr_value(B, S=#decoder{offset=O}, Acc, Q) -> tokenize_quoted_attr_value(B, S1, [Data|Acc], Q); <<_:O/binary, Q, _/binary>> -> { iolist_to_binary(lists:reverse(Acc)), ?INC_COL(S) }; - <<_:O/binary, $\n, _/binary>> -> - { iolist_to_binary(lists:reverse(Acc)), ?INC_LINE(S) }; <<_:O/binary, C, _/binary>> -> tokenize_quoted_attr_value(B, ?INC_COL(S), [C|Acc], Q) end. - + tokenize_unquoted_attr_value(B, S=#decoder{offset=O}, Acc) -> case B of <<_:O/binary>> -> @@ -520,7 +524,7 @@ tokenize_unquoted_attr_value(B, S=#decoder{offset=O}, Acc) -> { iolist_to_binary(lists:reverse(Acc)), S }; <<_:O/binary, C, _/binary>> -> tokenize_unquoted_attr_value(B, ?INC_COL(S), [C|Acc]) - end. + end. skip_whitespace(B, S=#decoder{offset=O}) -> case B of @@ -603,32 +607,33 @@ find_gt(Bin, S=#decoder{offset=O}, HasSlash) -> end. tokenize_charref(Bin, S=#decoder{offset=O}) -> - tokenize_charref(Bin, S, O). + try + tokenize_charref(Bin, S, O) + catch + throw:invalid_charref -> + {{data, <<"&">>, false}, S} + end. tokenize_charref(Bin, S=#decoder{offset=O}, Start) -> case Bin of <<_:O/binary>> -> - <<_:Start/binary, Raw/binary>> = Bin, - {{data, Raw, false}, S}; + throw(invalid_charref); <<_:O/binary, C, _/binary>> when ?IS_WHITESPACE(C) orelse C =:= ?SQUOTE orelse C =:= ?QUOTE orelse C =:= $/ orelse C =:= $> -> - Len = O - Start, - <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, - {{data, Raw, false}, S}; + throw(invalid_charref); <<_:O/binary, $;, _/binary>> -> Len = O - Start, <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, Data = case mochiweb_charref:charref(Raw) of undefined -> - Start1 = Start - 1, - Len1 = Len + 2, - <<_:Start1/binary, R:Len1/binary, _/binary>> = Bin, - R; - Unichar -> - mochiutf8:codepoint_to_bytes(Unichar) + throw(invalid_charref); + Unichar when is_integer(Unichar) -> + mochiutf8:codepoint_to_bytes(Unichar); + Unichars when is_list(Unichars) -> + unicode:characters_to_binary(Unichars) end, {{data, Data, false}, ?INC_COL(S)}; _ -> @@ -759,8 +764,8 @@ tokenize_textarea(Bin, S=#decoder{offset=O}, Start) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). to_html_test() -> ?assertEqual( @@ -1195,43 +1200,51 @@ parse_unquoted_attr_test() -> { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] } ]}, mochiweb_html:parse(D0)), - + D1 = <<"">>, ?assertEqual( {<<"html">>,[],[ { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] } ]}, mochiweb_html:parse(D1)), - + D2 = <<"">>, ?assertEqual( {<<"html">>,[],[ { <<"img">>, [ { <<"src">>, <<"/images/icon>.png">> }, { <<"width">>, <<"100">> } ], [] } ]}, mochiweb_html:parse(D2)), - ok. - -parse_quoted_attr_test() -> + ok. + +parse_quoted_attr_test() -> D0 = <<"">>, ?assertEqual( {<<"html">>,[],[ { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] } ]}, - mochiweb_html:parse(D0)), - + mochiweb_html:parse(D0)), + D1 = <<"">>, ?assertEqual( {<<"html">>,[],[ { <<"img">>, [ { <<"src">>, <<"/images/icon.png'>">> } ], [] } ]}, - mochiweb_html:parse(D1)), + mochiweb_html:parse(D1)), D2 = <<"">>, ?assertEqual( {<<"html">>,[],[ { <<"img">>, [ { <<"src">>, <<"/images/icon>.png">> } ], [] } ]}, - mochiweb_html:parse(D2)), + mochiweb_html:parse(D2)), + + %% Quoted attributes can contain whitespace and newlines + D3 = <<"">>, + ?assertEqual( + {<<"html">>,[],[ + { <<"a">>, [ { <<"href">>, <<"#">> }, {<<"onclick">>, <<"javascript: test(1,\ntrue);">>} ], [] } + ]}, + mochiweb_html:parse(D3)), ok. parse_missing_attr_name_test() -> @@ -1245,7 +1258,7 @@ parse_broken_pi_test() -> D0 = <<"">>, ?assertEqual( {<<"html">>, [], [ - { pi, <<"xml:namespace">>, [ { <<"prefix">>, <<"o">> }, + { pi, <<"xml:namespace">>, [ { <<"prefix">>, <<"o">> }, { <<"ns">>, <<"urn:schemas-microsoft-com:office:office">> } ] } ] }, mochiweb_html:parse(D0)), @@ -1260,5 +1273,60 @@ parse_funny_singletons_test() -> ] }, mochiweb_html:parse(D0)), ok. - + +to_html_singleton_test() -> + D0 = <<"">>, + T0 = {<<"link">>,[],[]}, + ?assertEqual(D0, iolist_to_binary(to_html(T0))), + + D1 = <<"">>, + T1 = {<<"head">>,[],[{<<"link">>,[],[]}]}, + ?assertEqual(D1, iolist_to_binary(to_html(T1))), + + D2 = <<"">>, + T2 = {<<"head">>,[],[{<<"link">>,[],[]}, {<<"link">>,[],[]}]}, + ?assertEqual(D2, iolist_to_binary(to_html(T2))), + + %% Make sure singletons are converted to singletons. + D3 = <<"">>, + T3 = {<<"head">>,[],[{<<"link">>,[],[<<"funny">>]}]}, + ?assertEqual(D3, iolist_to_binary(to_html(T3))), + + D4 = <<"">>, + T4 = {<<"link">>,[],[<<"funny">>]}, + ?assertEqual(D4, iolist_to_binary(to_html(T4))), + + ok. + +parse_amp_test_() -> + [?_assertEqual( + {<<"html">>,[], + [{<<"body">>,[{<<"onload">>,<<"javascript:A('1&2')">>}],[]}]}, + mochiweb_html:parse("")), + ?_assertEqual( + {<<"html">>,[], + [{<<"body">>,[{<<"onload">>,<<"javascript:A('1& 2')">>}],[]}]}, + mochiweb_html:parse("")), + ?_assertEqual( + {<<"html">>,[], + [{<<"body">>,[],[<<"& ">>]}]}, + mochiweb_html:parse("& ")), + ?_assertEqual( + {<<"html">>,[], + [{<<"body">>,[],[<<"&">>]}]}, + mochiweb_html:parse("&"))]. + +parse_unescaped_lt_test() -> + D1 = <<"">>, + ?assertEqual( + {<<"div">>, [], [<<" < < ">>, {<<"a">>, [{<<"href">>, <<"/">>}], + [<<"Back">>]}]}, + mochiweb_html:parse(D1)), + + D2 = <<"
<< Back
">>, + ?assertEqual( + {<<"div">>, [], [<<" << ">>, {<<"a">>, [{<<"href">>, <<"/">>}], + [<<"Back">>]}]}, + mochiweb_html:parse(D2)). + -endif. http://git-wip-us.apache.org/repos/asf/couchdb/blob/cbb8a550/src/mochiweb/mochiweb_http.erl ---------------------------------------------------------------------- diff --git a/src/mochiweb/mochiweb_http.erl b/src/mochiweb/mochiweb_http.erl index 23a4752..4f7e947 100644 --- a/src/mochiweb/mochiweb_http.erl +++ b/src/mochiweb/mochiweb_http.erl @@ -5,13 +5,13 @@ -module(mochiweb_http). -author('bob@mochimedia.com'). --export([start/0, start/1, stop/0, stop/1]). --export([loop/2, default_body/1]). +-export([start/1, start_link/1, stop/0, stop/1]). +-export([loop/2]). -export([after_response/2, reentry/1]). -export([parse_range_request/1, range_skip_length/2]). --define(REQUEST_RECV_TIMEOUT, 300000). % timeout waiting for request line --define(HEADERS_RECV_TIMEOUT, 30000). % timeout waiting for headers +-define(REQUEST_RECV_TIMEOUT, 300000). %% timeout waiting for request line +-define(HEADERS_RECV_TIMEOUT, 30000). %% timeout waiting for headers -define(MAX_HEADERS, 1000). -define(DEFAULTS, [{name, ?MODULE}, @@ -19,9 +19,7 @@ parse_options(Options) -> {loop, HttpLoop} = proplists:lookup(loop, Options), - Loop = fun (S) -> - ?MODULE:loop(S, HttpLoop) - end, + Loop = {?MODULE, loop, [HttpLoop]}, Options1 = [{loop, Loop} | proplists:delete(loop, Options)], mochilists:set_defaults(?DEFAULTS, Options1). @@ -31,15 +29,12 @@ stop() -> stop(Name) -> mochiweb_socket_server:stop(Name). -start() -> - start([{ip, "127.0.0.1"}, - {loop, {?MODULE, default_body}}]). - %% @spec start(Options) -> ServerRet %% Options = [option()] %% Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()} %% | {nodelay, boolean()} | {acceptor_pool_size, integer()} %% | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok} +%% | {link, false} %% @doc Start a mochiweb server. %% profile_fun is used to profile accept timing. %% After each accept, if defined, profile_fun is called with a proplist of a subset of the mochiweb_socket_server state and timing information. @@ -48,62 +43,18 @@ start() -> start(Options) -> mochiweb_socket_server:start(parse_options(Options)). -frm(Body) -> - ["" - "
" - "" - "" - "
" - "
" - "
" - "" - "" - "" - "
" - "
", Body, "
" - ""]. - -default_body(Req, M, "/chunked") when M =:= 'GET'; M =:= 'HEAD' -> - Res = Req:ok({"text/plain", [], chunked}), - Res:write_chunk("First chunk\r\n"), - timer:sleep(5000), - Res:write_chunk("Last chunk\r\n"), - Res:write_chunk(""); -default_body(Req, M, _Path) when M =:= 'GET'; M =:= 'HEAD' -> - Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()}, - {parse_cookie, Req:parse_cookie()}, - Req:dump()]]), - Req:ok({"text/html", - [mochiweb_cookies:cookie("mochiweb_http", "test_cookie")], - frm(Body)}); -default_body(Req, 'POST', "/multipart") -> - Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()}, - {parse_cookie, Req:parse_cookie()}, - {body, Req:recv_body()}, - Req:dump()]]), - Req:ok({"text/html", [], frm(Body)}); -default_body(Req, 'POST', _Path) -> - Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()}, - {parse_cookie, Req:parse_cookie()}, - {parse_post, Req:parse_post()}, - Req:dump()]]), - Req:ok({"text/html", [], frm(Body)}); -default_body(Req, _Method, _Path) -> - Req:respond({501, [], []}). - -default_body(Req) -> - default_body(Req, Req:get(method), Req:get(path)). +start_link(Options) -> + mochiweb_socket_server:start_link(parse_options(Options)). loop(Socket, Body) -> - mochiweb_socket:setopts(Socket, [{packet, http}]), + ok = mochiweb_socket:setopts(Socket, [{packet, http}]), request(Socket, Body). request(Socket, Body) -> - mochiweb_socket:setopts(Socket, [{active, once}]), + ok = mochiweb_socket:setopts(Socket, [{active, once}]), receive {Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl -> - mochiweb_socket:setopts(Socket, [{packet, httph}]), + ok = mochiweb_socket:setopts(Socket, [{packet, httph}]), headers(Socket, {Method, Path, Version}, [], Body, 0); {Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl -> request(Socket, Body); @@ -112,6 +63,13 @@ request(Socket, Body) -> {tcp_closed, _} -> mochiweb_socket:close(Socket), exit(normal); + {ssl_closed, _} -> + mochiweb_socket:close(Socket), + exit(normal); + {tcp_error,_,emsgsize} -> + % R15B02 returns this then closes the socket, so close and exit + mochiweb_socket:close(Socket), + exit(normal); _Other -> handle_invalid_request(Socket) after ?REQUEST_RECV_TIMEOUT -> @@ -126,10 +84,10 @@ reentry(Body) -> headers(Socket, Request, Headers, _Body, ?MAX_HEADERS) -> %% Too many headers sent, bad request. - mochiweb_socket:setopts(Socket, [{packet, raw}]), + ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), handle_invalid_request(Socket, Request, Headers); headers(Socket, Request, Headers, Body, HeaderCount) -> - mochiweb_socket:setopts(Socket, [{active, once}]), + ok = mochiweb_socket:setopts(Socket, [{active, once}]), receive {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl -> Req = new_request(Socket, Request, Headers), @@ -141,6 +99,10 @@ headers(Socket, Request, Headers, Body, HeaderCount) -> {tcp_closed, _} -> mochiweb_socket:close(Socket), exit(normal); + {tcp_error,_,emsgsize} -> + % R15B02 returns this then closes the socket, so close and exit + mochiweb_socket:close(Socket), + exit(normal); _Other -> handle_invalid_request(Socket, Request, Headers) after ?HEADERS_RECV_TIMEOUT -> @@ -148,14 +110,19 @@ headers(Socket, Request, Headers, Body, HeaderCount) -> exit(normal) end. +call_body({M, F, A}, Req) -> + erlang:apply(M, F, [Req | A]); call_body({M, F}, Req) -> M:F(Req); call_body(Body, Req) -> Body(Req). +-spec handle_invalid_request(term()) -> no_return(). handle_invalid_request(Socket) -> - handle_invalid_request(Socket, {'GET', {abs_path, "/"}, {0,9}}, []). + handle_invalid_request(Socket, {'GET', {abs_path, "/"}, {0,9}}, []), + exit(normal). +-spec handle_invalid_request(term(), term(), term()) -> no_return(). handle_invalid_request(Socket, Request, RevHeaders) -> Req = new_request(Socket, Request, RevHeaders), Req:respond({400, [], []}), @@ -163,7 +130,7 @@ handle_invalid_request(Socket, Request, RevHeaders) -> exit(normal). new_request(Socket, Request, RevHeaders) -> - mochiweb_socket:setopts(Socket, [{packet, raw}]), + ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), mochiweb:new_request({Socket, Request, lists:reverse(RevHeaders)}). after_response(Body, Req) -> @@ -174,6 +141,7 @@ after_response(Body, Req) -> exit(normal); false -> Req:cleanup(), + erlang:garbage_collect(), ?MODULE:loop(Socket, Body) end. @@ -211,6 +179,8 @@ range_skip_length(Spec, Size) -> invalid_range; {Start, End} when 0 =< Start, Start =< End, End < Size -> {Start, End - Start + 1}; + {Start, End} when 0 =< Start, Start =< End, End >= Size -> + {Start, Size - Start}; {_OutOfRange, _End} -> invalid_range end. @@ -218,8 +188,8 @@ range_skip_length(Spec, Size) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). range_test() -> %% valid, single ranges @@ -265,19 +235,23 @@ range_skip_length_test() -> BodySizeLess1 = BodySize - 1, ?assertEqual({BodySizeLess1, 1}, range_skip_length({BodySize - 1, none}, BodySize)), + ?assertEqual({BodySizeLess1, 1}, + range_skip_length({BodySize - 1, BodySize+5}, BodySize)), + ?assertEqual({BodySizeLess1, 1}, + range_skip_length({BodySize - 1, BodySize}, BodySize)), %% out of range, return whole thing ?assertEqual({0, BodySize}, range_skip_length({none, BodySize + 1}, BodySize)), ?assertEqual({0, BodySize}, range_skip_length({none, -1}, BodySize)), + ?assertEqual({0, BodySize}, + range_skip_length({0, BodySize + 1}, BodySize)), %% invalid ranges ?assertEqual(invalid_range, range_skip_length({-1, 30}, BodySize)), ?assertEqual(invalid_range, - range_skip_length({0, BodySize + 1}, BodySize)), - ?assertEqual(invalid_range, range_skip_length({-1, BodySize + 1}, BodySize)), ?assertEqual(invalid_range, range_skip_length({BodySize, 40}, BodySize)), http://git-wip-us.apache.org/repos/asf/couchdb/blob/cbb8a550/src/mochiweb/mochiweb_io.erl ---------------------------------------------------------------------- diff --git a/src/mochiweb/mochiweb_io.erl b/src/mochiweb/mochiweb_io.erl index 6ce57ec..8454b43 100644 --- a/src/mochiweb/mochiweb_io.erl +++ b/src/mochiweb/mochiweb_io.erl @@ -38,9 +38,6 @@ iodevice_size(IoDevice) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). - - - +-include_lib("eunit/include/eunit.hrl"). -endif. http://git-wip-us.apache.org/repos/asf/couchdb/blob/cbb8a550/src/mochiweb/mochiweb_mime.erl ---------------------------------------------------------------------- diff --git a/src/mochiweb/mochiweb_mime.erl b/src/mochiweb/mochiweb_mime.erl index 5344aee..7d9f249 100644 --- a/src/mochiweb/mochiweb_mime.erl +++ b/src/mochiweb/mochiweb_mime.erl @@ -11,72 +11,393 @@ %% @doc Given a filename extension (e.g. ".html") return a guess for the MIME %% type such as "text/html". Will return the atom undefined if no good %% guess is available. -from_extension(".html") -> - "text/html"; -from_extension(".xhtml") -> - "application/xhtml+xml"; -from_extension(".xml") -> - "application/xml"; -from_extension(".css") -> - "text/css"; + +from_extension(".stl") -> + "application/SLA"; +from_extension(".stp") -> + "application/STEP"; +from_extension(".step") -> + "application/STEP"; +from_extension(".dwg") -> + "application/acad"; +from_extension(".ez") -> + "application/andrew-inset"; +from_extension(".ccad") -> + "application/clariscad"; +from_extension(".drw") -> + "application/drafting"; +from_extension(".tsp") -> + "application/dsptype"; +from_extension(".dxf") -> + "application/dxf"; +from_extension(".xls") -> + "application/excel"; +from_extension(".unv") -> + "application/i-deas"; +from_extension(".jar") -> + "application/java-archive"; +from_extension(".hqx") -> + "application/mac-binhex40"; +from_extension(".cpt") -> + "application/mac-compactpro"; +from_extension(".pot") -> + "application/vnd.ms-powerpoint"; +from_extension(".ppt") -> + "application/vnd.ms-powerpoint"; +from_extension(".dms") -> + "application/octet-stream"; +from_extension(".lha") -> + "application/octet-stream"; +from_extension(".lzh") -> + "application/octet-stream"; +from_extension(".oda") -> + "application/oda"; +from_extension(".ogg") -> + "application/ogg"; +from_extension(".ogm") -> + "application/ogg"; +from_extension(".pdf") -> + "application/pdf"; +from_extension(".pgp") -> + "application/pgp"; +from_extension(".ai") -> + "application/postscript"; +from_extension(".eps") -> + "application/postscript"; +from_extension(".ps") -> + "application/postscript"; +from_extension(".prt") -> + "application/pro_eng"; +from_extension(".rtf") -> + "application/rtf"; +from_extension(".smi") -> + "application/smil"; +from_extension(".smil") -> + "application/smil"; +from_extension(".sol") -> + "application/solids"; +from_extension(".vda") -> + "application/vda"; +from_extension(".xlm") -> + "application/vnd.ms-excel"; +from_extension(".cod") -> + "application/vnd.rim.cod"; +from_extension(".pgn") -> + "application/x-chess-pgn"; +from_extension(".cpio") -> + "application/x-cpio"; +from_extension(".csh") -> + "application/x-csh"; +from_extension(".deb") -> + "application/x-debian-package"; +from_extension(".dcr") -> + "application/x-director"; +from_extension(".dir") -> + "application/x-director"; +from_extension(".dxr") -> + "application/x-director"; +from_extension(".gz") -> + "application/x-gzip"; +from_extension(".hdf") -> + "application/x-hdf"; +from_extension(".ipx") -> + "application/x-ipix"; +from_extension(".ips") -> + "application/x-ipscript"; from_extension(".js") -> "application/x-javascript"; -from_extension(".jpg") -> - "image/jpeg"; -from_extension(".gif") -> - "image/gif"; -from_extension(".png") -> - "image/png"; +from_extension(".skd") -> + "application/x-koan"; +from_extension(".skm") -> + "application/x-koan"; +from_extension(".skp") -> + "application/x-koan"; +from_extension(".skt") -> + "application/x-koan"; +from_extension(".latex") -> + "application/x-latex"; +from_extension(".lsp") -> + "application/x-lisp"; +from_extension(".scm") -> + "application/x-lotusscreencam"; +from_extension(".mif") -> + "application/x-mif"; +from_extension(".com") -> + "application/x-msdos-program"; +from_extension(".exe") -> + "application/octet-stream"; +from_extension(".cdf") -> + "application/x-netcdf"; +from_extension(".nc") -> + "application/x-netcdf"; +from_extension(".pl") -> + "application/x-perl"; +from_extension(".pm") -> + "application/x-perl"; +from_extension(".rar") -> + "application/x-rar-compressed"; +from_extension(".sh") -> + "application/x-sh"; +from_extension(".shar") -> + "application/x-shar"; from_extension(".swf") -> "application/x-shockwave-flash"; -from_extension(".zip") -> - "application/zip"; -from_extension(".bz2") -> - "application/x-bzip2"; -from_extension(".gz") -> - "application/x-gzip"; +from_extension(".sit") -> + "application/x-stuffit"; +from_extension(".sv4cpio") -> + "application/x-sv4cpio"; +from_extension(".sv4crc") -> + "application/x-sv4crc"; +from_extension(".tar.gz") -> + "application/x-tar-gz"; +from_extension(".tgz") -> + "application/x-tar-gz"; from_extension(".tar") -> "application/x-tar"; -from_extension(".tgz") -> - "application/x-gzip"; +from_extension(".tcl") -> + "application/x-tcl"; +from_extension(".texi") -> + "application/x-texinfo"; +from_extension(".texinfo") -> + "application/x-texinfo"; +from_extension(".man") -> + "application/x-troff-man"; +from_extension(".me") -> + "application/x-troff-me"; +from_extension(".ms") -> + "application/x-troff-ms"; +from_extension(".roff") -> + "application/x-troff"; +from_extension(".t") -> + "application/x-troff"; +from_extension(".tr") -> + "application/x-troff"; +from_extension(".ustar") -> + "application/x-ustar"; +from_extension(".src") -> + "application/x-wais-source"; +from_extension(".zip") -> + "application/zip"; +from_extension(".tsi") -> + "audio/TSP-audio"; +from_extension(".au") -> + "audio/basic"; +from_extension(".snd") -> + "audio/basic"; +from_extension(".kar") -> + "audio/midi"; +from_extension(".mid") -> + "audio/midi"; +from_extension(".midi") -> + "audio/midi"; +from_extension(".mp2") -> + "audio/mpeg"; +from_extension(".mp3") -> + "audio/mpeg"; +from_extension(".mpga") -> + "audio/mpeg"; +from_extension(".aif") -> + "audio/x-aiff"; +from_extension(".aifc") -> + "audio/x-aiff"; +from_extension(".aiff") -> + "audio/x-aiff"; +from_extension(".m3u") -> + "audio/x-mpegurl"; +from_extension(".wax") -> + "audio/x-ms-wax"; +from_extension(".wma") -> + "audio/x-ms-wma"; +from_extension(".rpm") -> + "audio/x-pn-realaudio-plugin"; +from_extension(".ram") -> + "audio/x-pn-realaudio"; +from_extension(".rm") -> + "audio/x-pn-realaudio"; +from_extension(".ra") -> + "audio/x-realaudio"; +from_extension(".wav") -> + "audio/x-wav"; +from_extension(".pdb") -> + "chemical/x-pdb"; +from_extension(".ras") -> + "image/cmu-raster"; +from_extension(".gif") -> + "image/gif"; +from_extension(".ief") -> + "image/ief"; +from_extension(".jpe") -> + "image/jpeg"; +from_extension(".jpeg") -> + "image/jpeg"; +from_extension(".jpg") -> + "image/jpeg"; +from_extension(".jp2") -> + "image/jp2"; +from_extension(".png") -> + "image/png"; +from_extension(".tif") -> + "image/tiff"; +from_extension(".tiff") -> + "image/tiff"; +from_extension(".pnm") -> + "image/x-portable-anymap"; +from_extension(".pbm") -> + "image/x-portable-bitmap"; +from_extension(".pgm") -> + "image/x-portable-graymap"; +from_extension(".ppm") -> + "image/x-portable-pixmap"; +from_extension(".rgb") -> + "image/x-rgb"; +from_extension(".xbm") -> + "image/x-xbitmap"; +from_extension(".xwd") -> + "image/x-xwindowdump"; +from_extension(".iges") -> + "model/iges"; +from_extension(".igs") -> + "model/iges"; +from_extension(".mesh") -> + "model/mesh"; +from_extension(".") -> + ""; +from_extension(".msh") -> + "model/mesh"; +from_extension(".silo") -> + "model/mesh"; +from_extension(".vrml") -> + "model/vrml"; +from_extension(".wrl") -> + "model/vrml"; +from_extension(".css") -> + "text/css"; +from_extension(".htm") -> + "text/html"; +from_extension(".html") -> + "text/html"; +from_extension(".asc") -> + "text/plain"; +from_extension(".c") -> + "text/plain"; +from_extension(".cc") -> + "text/plain"; +from_extension(".f90") -> + "text/plain"; +from_extension(".f") -> + "text/plain"; +from_extension(".hh") -> + "text/plain"; +from_extension(".m") -> + "text/plain"; from_extension(".txt") -> "text/plain"; -from_extension(".doc") -> - "application/msword"; -from_extension(".pdf") -> - "application/pdf"; -from_extension(".xls") -> - "application/vnd.ms-excel"; -from_extension(".rtf") -> - "application/rtf"; +from_extension(".rtx") -> + "text/richtext"; +from_extension(".sgm") -> + "text/sgml"; +from_extension(".sgml") -> + "text/sgml"; +from_extension(".tsv") -> + "text/tab-separated-values"; +from_extension(".jad") -> + "text/vnd.sun.j2me.app-descriptor"; +from_extension(".etx") -> + "text/x-setext"; +from_extension(".xml") -> + "application/xml"; +from_extension(".dl") -> + "video/dl"; +from_extension(".fli") -> + "video/fli"; +from_extension(".flv") -> + "video/x-flv"; +from_extension(".gl") -> + "video/gl"; +from_extension(".mp4") -> + "video/mp4"; +from_extension(".mpe") -> + "video/mpeg"; +from_extension(".mpeg") -> + "video/mpeg"; +from_extension(".mpg") -> + "video/mpeg"; from_extension(".mov") -> "video/quicktime"; -from_extension(".mp3") -> - "audio/mpeg"; +from_extension(".qt") -> + "video/quicktime"; +from_extension(".viv") -> + "video/vnd.vivo"; +from_extension(".vivo") -> + "video/vnd.vivo"; +from_extension(".asf") -> + "video/x-ms-asf"; +from_extension(".asx") -> + "video/x-ms-asx"; +from_extension(".wmv") -> + "video/x-ms-wmv"; +from_extension(".wmx") -> + "video/x-ms-wmx"; +from_extension(".wvx") -> + "video/x-ms-wvx"; +from_extension(".avi") -> + "video/x-msvideo"; +from_extension(".movie") -> + "video/x-sgi-movie"; +from_extension(".mime") -> + "www/mime"; +from_extension(".ice") -> + "x-conference/x-cooltalk"; +from_extension(".vrm") -> + "x-world/x-vrml"; +from_extension(".spx") -> + "audio/ogg"; +from_extension(".xhtml") -> + "application/xhtml+xml"; +from_extension(".bz2") -> + "application/x-bzip2"; +from_extension(".doc") -> + "application/msword"; from_extension(".z") -> "application/x-compress"; -from_extension(".wav") -> - "audio/x-wav"; from_extension(".ico") -> "image/x-icon"; from_extension(".bmp") -> "image/bmp"; from_extension(".m4a") -> "audio/mpeg"; -from_extension(".m3u") -> - "audio/x-mpegurl"; -from_extension(".exe") -> - "application/octet-stream"; from_extension(".csv") -> "text/csv"; +from_extension(".eot") -> + "application/vnd.ms-fontobject"; +from_extension(".m4v") -> + "video/mp4"; +from_extension(".svg") -> + "image/svg+xml"; +from_extension(".svgz") -> + "image/svg+xml"; +from_extension(".ttc") -> + "application/x-font-ttf"; +from_extension(".ttf") -> + "application/x-font-ttf"; +from_extension(".vcf") -> + "text/x-vcard"; +from_extension(".webm") -> + "video/web"; +from_extension(".webp") -> + "image/web"; +from_extension(".woff") -> + "application/x-font-woff"; +from_extension(".otf") -> + "font/opentype"; from_extension(_) -> undefined. %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). exhaustive_from_extension_test() -> T = mochiweb_cover:clause_lookup_table(?MODULE, from_extension), http://git-wip-us.apache.org/repos/asf/couchdb/blob/cbb8a550/src/mochiweb/mochiweb_multipart.erl ---------------------------------------------------------------------- diff --git a/src/mochiweb/mochiweb_multipart.erl b/src/mochiweb/mochiweb_multipart.erl index 3069cf4..a83a88c 100644 --- a/src/mochiweb/mochiweb_multipart.erl +++ b/src/mochiweb/mochiweb_multipart.erl @@ -128,7 +128,7 @@ default_file_handler_1(Filename, ContentType, Acc) -> parse_multipart_request(Req, Callback) -> %% TODO: Support chunked? - Length = list_to_integer(Req:get_header_value("content-length")), + Length = list_to_integer(Req:get_combined_header_value("content-length")), Boundary = iolist_to_binary( get_boundary(Req:get_header_value("content-type"))), Prefix = <<"\r\n--", Boundary/binary>>, @@ -240,24 +240,22 @@ get_boundary(ContentType) -> S end. -find_in_binary(B, Data) when size(B) > 0 -> - case size(Data) - size(B) of +%% @spec find_in_binary(Pattern::binary(), Data::binary()) -> +%% {exact, N} | {partial, N, K} | not_found +%% @doc Searches for the given pattern in the given binary. +find_in_binary(P, Data) when size(P) > 0 -> + PS = size(P), + DS = size(Data), + case DS - PS of Last when Last < 0 -> - partial_find(B, Data, 0, size(Data)); + partial_find(P, Data, 0, DS); Last -> - find_in_binary(B, size(B), Data, 0, Last) + case binary:match(Data, P) of + {Pos, _} -> {exact, Pos}; + nomatch -> partial_find(P, Data, Last+1, PS-1) + end end. -find_in_binary(B, BS, D, N, Last) when N =< Last-> - case D of - <<_:N/binary, B:BS/binary, _/binary>> -> - {exact, N}; - _ -> - find_in_binary(B, BS, D, 1 + N, Last) - end; -find_in_binary(B, BS, D, N, Last) when N =:= 1 + Last -> - partial_find(B, D, N, BS - 1). - partial_find(_B, _D, _N, 0) -> not_found; partial_find(B, D, N, K) -> @@ -295,8 +293,8 @@ find_boundary(Prefix, Data) -> %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). ssl_cert_opts() -> EbinDir = filename:dirname(code:which(?MODULE)), @@ -313,7 +311,7 @@ with_socket_server(Transport, ServerFun, ClientFun) -> ssl -> ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}] end, - {ok, Server} = mochiweb_socket_server:start(ServerOpts), + {ok, Server} = mochiweb_socket_server:start_link(ServerOpts), Port = mochiweb_socket_server:get(Server, port), ClientOpts = [binary, {active, false}], {ok, Client} = case Transport of @@ -378,7 +376,7 @@ parse3(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -414,7 +412,7 @@ parse2(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -451,7 +449,7 @@ do_parse_form(Transport) -> BinContent = iolist_to_binary(Content), ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -504,7 +502,7 @@ do_parse(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -556,7 +554,7 @@ parse_partial_body_boundary(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -609,7 +607,7 @@ parse_large_header(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -685,7 +683,7 @@ flash_parse(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -733,7 +731,7 @@ flash_parse2(Transport) -> TestCallback = fun (Next) -> test_callback(Next, Expect) end, ServerFun = fun (Socket) -> ok = mochiweb_socket:send(Socket, BinContent), - exit(normal) + exit(normal) end, ClientFun = fun (Socket) -> Req = fake_request(Socket, ContentType, @@ -821,4 +819,54 @@ multipart_body_test() -> 10))), ok. +%% @todo Move somewhere more appropriate than in the test suite + +multipart_parsing_benchmark_test() -> + run_multipart_parsing_benchmark(1). + +run_multipart_parsing_benchmark(0) -> ok; +run_multipart_parsing_benchmark(N) -> + multipart_parsing_benchmark(), + run_multipart_parsing_benchmark(N-1). + +multipart_parsing_benchmark() -> + ContentType = "multipart/form-data; boundary=----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5", + Chunk = binary:copy(<<"This Is_%Some=Quite0Long4String2Used9For7BenchmarKing.5">>, 102400), + 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 = mochiweb_socket: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(plain, ServerFun, ClientFun), + ok. -endif. http://git-wip-us.apache.org/repos/asf/couchdb/blob/cbb8a550/src/mochiweb/mochiweb_request.erl ---------------------------------------------------------------------- diff --git a/src/mochiweb/mochiweb_request.erl b/src/mochiweb/mochiweb_request.erl index 980f5ad..1b431d3 100644 --- a/src/mochiweb/mochiweb_request.erl +++ b/src/mochiweb/mochiweb_request.erl @@ -3,7 +3,7 @@ %% @doc MochiWeb HTTP Request abstraction. --module(mochiweb_request, [Socket, Method, RawPath, Version, Headers]). +-module(mochiweb_request). -author('bob@mochimedia.com'). -include_lib("kernel/include/file.hrl"). @@ -11,17 +11,18 @@ -define(QUIP, "Any of you quaids got a smint?"). --export([get_header_value/1, get_primary_header_value/1, get/1, dump/0]). --export([send/1, recv/1, recv/2, recv_body/0, recv_body/1, stream_body/3]). --export([start_response/1, start_response_length/1, start_raw_response/1]). --export([respond/1, ok/1]). --export([not_found/0, not_found/1]). --export([parse_post/0, parse_qs/0]). --export([should_close/0, cleanup/0]). --export([parse_cookie/0, get_cookie_value/1]). --export([serve_file/2, serve_file/3]). --export([accepted_encodings/1]). --export([accepts_content_type/1]). +-export([new/5]). +-export([get_header_value/2, get_primary_header_value/2, get_combined_header_value/2, get/2, dump/1]). +-export([send/2, recv/2, recv/3, recv_body/1, recv_body/2, stream_body/4]). +-export([start_response/2, start_response_length/2, start_raw_response/2]). +-export([respond/2, ok/2]). +-export([not_found/1, not_found/2]). +-export([parse_post/1, parse_qs/1]). +-export([should_close/1, cleanup/1]). +-export([parse_cookie/1, get_cookie_value/2]). +-export([serve_file/3, serve_file/4]). +-export([accepted_encodings/2]). +-export([accepts_content_type/2, accepted_content_types/2]). -define(SAVE_QS, mochiweb_request_qs). -define(SAVE_PATH, mochiweb_request_path). @@ -32,11 +33,10 @@ -define(SAVE_COOKIE, mochiweb_request_cookie). -define(SAVE_FORCE_CLOSE, mochiweb_request_force_close). -%% @type iolist() = [iolist() | binary() | char()]. -%% @type iodata() = binary() | iolist(). %% @type key() = atom() | string() | binary() %% @type value() = atom() | string() | binary() | integer() %% @type headers(). A mochiweb_headers structure. +%% @type request() = {mochiweb_request,[_Socket,_Method,_RawPath,_Version,_Headers]} %% @type response(). A mochiweb_response parameterized module instance. %% @type ioheaders() = headers() | [{key(), value()}]. @@ -46,50 +46,58 @@ % Maximum recv_body() length of 1MB -define(MAX_RECV_BODY, (1024*1024)). -%% @spec get_header_value(K) -> undefined | Value +%% @spec new(Socket, Method, RawPath, Version, headers()) -> request() +%% @doc Create a new request instance. +new(Socket, Method, RawPath, Version, Headers) -> + {?MODULE, [Socket, Method, RawPath, Version, Headers]}. + +%% @spec get_header_value(K, request()) -> undefined | Value %% @doc Get the value of a given request header. -get_header_value(K) -> +get_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> mochiweb_headers:get_value(K, Headers). -get_primary_header_value(K) -> +get_primary_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> mochiweb_headers:get_primary_value(K, Headers). +get_combined_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> + mochiweb_headers:get_combined_value(K, Headers). + %% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range -%% @spec get(field()) -> term() +%% @spec get(field(), request()) -> term() %% @doc Return the internal representation of the given field. If %% socket is requested on a HTTPS connection, then %% an ssl socket will be returned as {ssl, SslSocket}. %% You can use SslSocket with the ssl %% application, eg: ssl:peercert(SslSocket). -get(socket) -> +get(socket, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> Socket; -get(scheme) -> +get(scheme, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:type(Socket) of plain -> http; ssl -> https end; -get(method) -> +get(method, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}) -> Method; -get(raw_path) -> +get(raw_path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> RawPath; -get(version) -> +get(version, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}) -> Version; -get(headers) -> +get(headers, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) -> Headers; -get(peer) -> +get(peer, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case mochiweb_socket:peername(Socket) of {ok, {Addr={10, _, _, _}, _Port}} -> - case get_header_value("x-forwarded-for") of + case get_header_value("x-forwarded-for", THIS) of undefined -> inet_parse:ntoa(Addr); Hosts -> string:strip(lists:last(string:tokens(Hosts, ","))) end; {ok, {{127, 0, 0, 1}, _Port}} -> - case get_header_value("x-forwarded-for") of + case get_header_value("x-forwarded-for", THIS) of undefined -> "127.0.0.1"; Hosts -> @@ -100,7 +108,7 @@ get(peer) -> {error, enotconn} -> exit(normal) end; -get(path) -> +get(path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> case erlang:get(?SAVE_PATH) of undefined -> {Path0, _, _} = mochiweb_util:urlsplit_path(RawPath), @@ -110,35 +118,35 @@ get(path) -> Cached -> Cached end; -get(body_length) -> +get(body_length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_BODY_LENGTH) of undefined -> - BodyLength = body_length(), + BodyLength = body_length(THIS), put(?SAVE_BODY_LENGTH, {cached, BodyLength}), BodyLength; {cached, Cached} -> Cached end; -get(range) -> - case get_header_value(range) of +get(range, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + case get_header_value(range, THIS) of undefined -> undefined; RawRange -> mochiweb_http:parse_range_request(RawRange) end. -%% @spec dump() -> {mochiweb_request, [{atom(), term()}]} +%% @spec dump(request()) -> {mochiweb_request, [{atom(), term()}]} %% @doc Dump the internal representation to a "human readable" set of terms %% for debugging/inspection purposes. -dump() -> +dump({?MODULE, [_Socket, Method, RawPath, Version, Headers]}) -> {?MODULE, [{method, Method}, {version, Version}, {raw_path, RawPath}, {headers, mochiweb_headers:to_list(Headers)}]}. -%% @spec send(iodata()) -> ok +%% @spec send(iodata(), request()) -> ok %% @doc Send data over the socket. -send(Data) -> +send(Data, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:send(Socket, Data) of ok -> ok; @@ -146,16 +154,16 @@ send(Data) -> exit(normal) end. -%% @spec recv(integer()) -> binary() +%% @spec recv(integer(), request()) -> binary() %% @doc Receive Length bytes from the client as a binary, with the default %% idle timeout. -recv(Length) -> - recv(Length, ?IDLE_TIMEOUT). +recv(Length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + recv(Length, ?IDLE_TIMEOUT, THIS). -%% @spec recv(integer(), integer()) -> binary() +%% @spec recv(integer(), integer(), request()) -> binary() %% @doc Receive Length bytes from the client as a binary, with the given %% Timeout in msec. -recv(Length, Timeout) -> +recv(Length, Timeout, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:recv(Socket, Length, Timeout) of {ok, Data} -> put(?SAVE_RECV, true), @@ -164,12 +172,12 @@ recv(Length, Timeout) -> exit(normal) end. -%% @spec body_length() -> undefined | chunked | unknown_transfer_encoding | integer() +%% @spec body_length(request()) -> undefined | chunked | unknown_transfer_encoding | integer() %% @doc Infer body length from transfer-encoding and content-length headers. -body_length() -> - case get_header_value("transfer-encoding") of +body_length({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + case get_header_value("transfer-encoding", THIS) of undefined -> - case get_header_value("content-length") of + case get_combined_header_value("content-length", THIS) of undefined -> undefined; Length -> @@ -182,16 +190,16 @@ body_length() -> end. -%% @spec recv_body() -> binary() +%% @spec recv_body(request()) -> binary() %% @doc Receive the body of the HTTP request (defined by Content-Length). %% Will only receive up to the default max-body length of 1MB. -recv_body() -> - recv_body(?MAX_RECV_BODY). +recv_body({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + recv_body(?MAX_RECV_BODY, THIS). -%% @spec recv_body(integer()) -> binary() +%% @spec recv_body(integer(), request()) -> binary() %% @doc Receive the body of the HTTP request (defined by Content-Length). %% Will receive up to MaxBody bytes. -recv_body(MaxBody) -> +recv_body(MaxBody, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_BODY) of undefined -> % we could use a sane constant for max chunk size @@ -205,17 +213,18 @@ recv_body(MaxBody) -> true -> {NewLength, [Bin | BinAcc]} end - end, {0, []}, MaxBody), + end, {0, []}, MaxBody, THIS), put(?SAVE_BODY, Body), Body; Cached -> Cached end. -stream_body(MaxChunkSize, ChunkFun, FunState) -> - stream_body(MaxChunkSize, ChunkFun, FunState, undefined). +stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE,[_Socket,_Method,_RawPath,_Version,_Headers]}=THIS) -> + stream_body(MaxChunkSize, ChunkFun, FunState, undefined, THIS). -stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) -> - Expect = case get_header_value("expect") of +stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + Expect = case get_header_value("expect", THIS) of undefined -> undefined; Value when is_list(Value) -> @@ -223,11 +232,12 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) -> end, case Expect of "100-continue" -> - start_raw_response({100, gb_trees:empty()}); + _ = start_raw_response({100, gb_trees:empty()}, THIS), + ok; _Else -> ok end, - case body_length() of + case body_length(THIS) of undefined -> undefined; {unknown_transfer_encoding, Unknown} -> @@ -236,7 +246,7 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) -> % In this case the MaxBody is actually used to % determine the maximum allowed size of a single % chunk. - stream_chunked_body(MaxChunkSize, ChunkFun, FunState); + stream_chunked_body(MaxChunkSize, ChunkFun, FunState, THIS); 0 -> <<>>; Length when is_integer(Length) -> @@ -244,62 +254,64 @@ stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength) -> MaxBodyLength when is_integer(MaxBodyLength), MaxBodyLength < Length -> exit({body_too_large, content_length}); _ -> - stream_unchunked_body(Length, ChunkFun, FunState) - end; - Length -> - exit({length_not_integer, Length}) + stream_unchunked_body(Length, ChunkFun, FunState, THIS) + end end. -%% @spec start_response({integer(), ioheaders()}) -> response() +%% @spec start_response({integer(), ioheaders()}, request()) -> response() %% @doc Start the HTTP response by sending the Code HTTP response and %% ResponseHeaders. The server will set header defaults such as Server %% and Date if not present in ResponseHeaders. -start_response({Code, ResponseHeaders}) -> +start_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = mochiweb_headers:default_from_list(server_headers(), HResponse), - start_raw_response({Code, HResponse1}). + start_raw_response({Code, HResponse1}, THIS). -%% @spec start_raw_response({integer(), headers()}) -> response() +%% @spec start_raw_response({integer(), headers()}, request()) -> response() %% @doc Start the HTTP response by sending the Code HTTP response and %% ResponseHeaders. -start_raw_response({Code, ResponseHeaders}) -> +start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) -> F = fun ({K, V}, Acc) -> [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc] end, End = lists:foldl(F, [<<"\r\n">>], mochiweb_headers:to_list(ResponseHeaders)), - send([make_version(Version), make_code(Code), <<"\r\n">> | End]), + send([make_version(Version), make_code(Code), <<"\r\n">> | End], THIS), mochiweb:new_response({THIS, Code, ResponseHeaders}). -%% @spec start_response_length({integer(), ioheaders(), integer()}) -> response() +%% @spec start_response_length({integer(), ioheaders(), integer()}, request()) -> response() %% @doc Start the HTTP response by sending the Code HTTP response and %% ResponseHeaders including a Content-Length of Length. The server %% will set header defaults such as Server %% and Date if not present in ResponseHeaders. -start_response_length({Code, ResponseHeaders, Length}) -> +start_response_length({Code, ResponseHeaders, Length}, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse), - start_response({Code, HResponse1}). + start_response({Code, HResponse1}, THIS). -%% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}) -> response() +%% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}, request()) -> response() %% @doc Start the HTTP response with start_response, and send Body to the %% client (if the get(method) /= 'HEAD'). The Content-Length header %% will be set by the Body length, and the server will insert header %% defaults. -respond({Code, ResponseHeaders, {file, IoDevice}}) -> +respond({Code, ResponseHeaders, {file, IoDevice}}, + {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) -> Length = mochiweb_io:iodevice_size(IoDevice), - Response = start_response_length({Code, ResponseHeaders, Length}), + Response = start_response_length({Code, ResponseHeaders, Length}, THIS), case Method of 'HEAD' -> ok; _ -> - mochiweb_io:iodevice_stream(fun send/1, IoDevice) + mochiweb_io:iodevice_stream( + fun (Body) -> send(Body, THIS) end, + IoDevice) end, Response; -respond({Code, ResponseHeaders, chunked}) -> +respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, Method, _RawPath, Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), HResponse1 = case Method of 'HEAD' -> @@ -320,35 +332,35 @@ respond({Code, ResponseHeaders, chunked}) -> put(?SAVE_FORCE_CLOSE, true), HResponse end, - start_response({Code, HResponse1}); -respond({Code, ResponseHeaders, Body}) -> - Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}), + start_response({Code, HResponse1}, THIS); +respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) -> + Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}, THIS), case Method of 'HEAD' -> ok; _ -> - send(Body) + send(Body, THIS) end, Response. -%% @spec not_found() -> response() +%% @spec not_found(request()) -> response() %% @doc Alias for not_found([]). -not_found() -> - not_found([]). +not_found({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + not_found([], THIS). -%% @spec not_found(ExtraHeaders) -> response() +%% @spec not_found(ExtraHeaders, request()) -> response() %% @doc Alias for respond({404, [{"Content-Type", "text/plain"} %% | ExtraHeaders], <<"Not found.">>}). -not_found(ExtraHeaders) -> +not_found(ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> respond({404, [{"Content-Type", "text/plain"} | ExtraHeaders], - <<"Not found.">>}). + <<"Not found.">>}, THIS). -%% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}) -> +%% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}, request()) -> %% response() %% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}). -ok({ContentType, Body}) -> - ok({ContentType, [], Body}); -ok({ContentType, ResponseHeaders, Body}) -> +ok({ContentType, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + ok({ContentType, [], Body}, THIS); +ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> HResponse = mochiweb_headers:make(ResponseHeaders), case THIS:get(range) of X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked -> @@ -357,7 +369,7 @@ ok({ContentType, ResponseHeaders, Body}) -> %% full response. HResponse1 = mochiweb_headers:enter("Content-Type", ContentType, HResponse), - respond({200, HResponse1, Body}); + respond({200, HResponse1, Body}, THIS); Ranges -> {PartList, Size} = range_parts(Body, Ranges), case PartList of @@ -366,7 +378,7 @@ ok({ContentType, ResponseHeaders, Body}) -> ContentType, HResponse), %% could be 416, for now we'll just return 200 - respond({200, HResponse1, Body}); + respond({200, HResponse1, Body}, THIS); PartList -> {RangeHeaders, RangeBody} = mochiweb_multipart:parts_to_body(PartList, ContentType, Size), @@ -374,46 +386,50 @@ ok({ContentType, ResponseHeaders, Body}) -> [{"Accept-Ranges", "bytes"} | RangeHeaders], HResponse), - respond({206, HResponse1, RangeBody}) + respond({206, HResponse1, RangeBody}, THIS) end end. -%% @spec should_close() -> bool() +%% @spec should_close(request()) -> bool() %% @doc Return true if the connection must be closed. If false, using %% Keep-Alive should be safe. -should_close() -> +should_close({?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) -> ForceClose = erlang:get(?SAVE_FORCE_CLOSE) =/= undefined, DidNotRecv = erlang:get(?SAVE_RECV) =:= undefined, ForceClose orelse Version < {1, 0} %% Connection: close - orelse get_header_value("connection") =:= "close" + orelse is_close(get_header_value("connection", THIS)) %% HTTP 1.0 requires Connection: Keep-Alive orelse (Version =:= {1, 0} - andalso get_header_value("connection") =/= "Keep-Alive") + andalso get_header_value("connection", THIS) =/= "Keep-Alive") %% unread data left on the socket, can't safely continue orelse (DidNotRecv - andalso get_header_value("content-length") =/= undefined - andalso list_to_integer(get_header_value("content-length")) > 0) + andalso get_combined_header_value("content-length", THIS) =/= undefined + andalso list_to_integer(get_combined_header_value("content-length", THIS)) > 0) orelse (DidNotRecv - andalso get_header_value("transfer-encoding") =:= "chunked"). + andalso get_header_value("transfer-encoding", THIS) =:= "chunked"). -%% @spec cleanup() -> ok +is_close("close") -> + true; +is_close(S=[_C, _L, _O, _S, _E]) -> + string:to_lower(S) =:= "close"; +is_close(_) -> + false. + +%% @spec cleanup(request()) -> ok %% @doc Clean up any junk in the process dictionary, required before continuing %% a Keep-Alive request. -cleanup() -> - [erase(K) || K <- [?SAVE_QS, - ?SAVE_PATH, - ?SAVE_RECV, - ?SAVE_BODY, - ?SAVE_BODY_LENGTH, - ?SAVE_POST, - ?SAVE_COOKIE, - ?SAVE_FORCE_CLOSE]], +cleanup({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) -> + L = [?SAVE_QS, ?SAVE_PATH, ?SAVE_RECV, ?SAVE_BODY, ?SAVE_BODY_LENGTH, + ?SAVE_POST, ?SAVE_COOKIE, ?SAVE_FORCE_CLOSE], + lists:foreach(fun(K) -> + erase(K) + end, L), ok. -%% @spec parse_qs() -> [{Key::string(), Value::string()}] +%% @spec parse_qs(request()) -> [{Key::string(), Value::string()}] %% @doc Parse the query string of the URL. -parse_qs() -> +parse_qs({?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) -> case erlang:get(?SAVE_QS) of undefined -> {_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath), @@ -424,17 +440,17 @@ parse_qs() -> Cached end. -%% @spec get_cookie_value(Key::string) -> string() | undefined +%% @spec get_cookie_value(Key::string, request()) -> string() | undefined %% @doc Get the value of the given cookie. -get_cookie_value(Key) -> - proplists:get_value(Key, parse_cookie()). +get_cookie_value(Key, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + proplists:get_value(Key, parse_cookie(THIS)). -%% @spec parse_cookie() -> [{Key::string(), Value::string()}] +%% @spec parse_cookie(request()) -> [{Key::string(), Value::string()}] %% @doc Parse the cookie header. -parse_cookie() -> +parse_cookie({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_COOKIE) of undefined -> - Cookies = case get_header_value("cookie") of + Cookies = case get_header_value("cookie", THIS) of undefined -> []; Value -> @@ -446,17 +462,17 @@ parse_cookie() -> Cached end. -%% @spec parse_post() -> [{Key::string(), Value::string()}] +%% @spec parse_post(request()) -> [{Key::string(), Value::string()}] %% @doc Parse an application/x-www-form-urlencoded form POST. This %% has the side-effect of calling recv_body(). -parse_post() -> +parse_post({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case erlang:get(?SAVE_POST) of undefined -> - Parsed = case recv_body() of + Parsed = case recv_body(THIS) of undefined -> []; Binary -> - case get_primary_header_value("content-type") of + case get_primary_header_value("content-type",THIS) of "application/x-www-form-urlencoded" ++ _ -> mochiweb_util:parse_qs(Binary); _ -> @@ -469,41 +485,43 @@ parse_post() -> Cached end. -%% @spec stream_chunked_body(integer(), fun(), term()) -> term() +%% @spec stream_chunked_body(integer(), fun(), term(), request()) -> term() %% @doc The function is called for each chunk. %% Used internally by read_chunked_body. -stream_chunked_body(MaxChunkSize, Fun, FunState) -> - case read_chunk_length() of +stream_chunked_body(MaxChunkSize, Fun, FunState, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + case read_chunk_length(THIS) of 0 -> - Fun({0, read_chunk(0)}, FunState); + Fun({0, read_chunk(0, THIS)}, FunState); Length when Length > MaxChunkSize -> - NewState = read_sub_chunks(Length, MaxChunkSize, Fun, FunState), - stream_chunked_body(MaxChunkSize, Fun, NewState); + NewState = read_sub_chunks(Length, MaxChunkSize, Fun, FunState, THIS), + stream_chunked_body(MaxChunkSize, Fun, NewState, THIS); Length -> - NewState = Fun({Length, read_chunk(Length)}, FunState), - stream_chunked_body(MaxChunkSize, Fun, NewState) + NewState = Fun({Length, read_chunk(Length, THIS)}, FunState), + stream_chunked_body(MaxChunkSize, Fun, NewState, THIS) end. -stream_unchunked_body(0, Fun, FunState) -> +stream_unchunked_body(0, Fun, FunState, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) -> Fun({0, <<>>}, FunState); -stream_unchunked_body(Length, Fun, FunState) when Length > 0 -> +stream_unchunked_body(Length, Fun, FunState, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > 0 -> PktSize = case Length > ?RECBUF_SIZE of true -> ?RECBUF_SIZE; false -> Length end, - Bin = recv(PktSize), + Bin = recv(PktSize, THIS), NewState = Fun({PktSize, Bin}, FunState), - stream_unchunked_body(Length - PktSize, Fun, NewState). + stream_unchunked_body(Length - PktSize, Fun, NewState, THIS). -%% @spec read_chunk_length() -> integer() +%% @spec read_chunk_length(request()) -> integer() %% @doc Read the length of the next HTTP chunk. -read_chunk_length() -> - mochiweb_socket:setopts(Socket, [{packet, line}]), +read_chunk_length({?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> + ok = mochiweb_socket:setopts(Socket, [{packet, line}]), case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of {ok, Header} -> - mochiweb_socket:setopts(Socket, [{packet, raw}]), + ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), Splitter = fun (C) -> C =/= $\r andalso C =/= $\n andalso C =/= $ end, @@ -513,11 +531,11 @@ read_chunk_length() -> exit(normal) end. -%% @spec read_chunk(integer()) -> Chunk::binary() | [Footer::binary()] +%% @spec read_chunk(integer(), request()) -> Chunk::binary() | [Footer::binary()] %% @doc Read in a HTTP chunk of the given length. If Length is 0, then read the %% HTTP footers (as a list of binaries, since they're nominal). -read_chunk(0) -> - mochiweb_socket:setopts(Socket, [{packet, line}]), +read_chunk(0, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> + ok = mochiweb_socket:setopts(Socket, [{packet, line}]), F = fun (F1, Acc) -> case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of {ok, <<"\r\n">>} -> @@ -529,10 +547,10 @@ read_chunk(0) -> end end, Footers = F(F, []), - mochiweb_socket:setopts(Socket, [{packet, raw}]), + ok = mochiweb_socket:setopts(Socket, [{packet, raw}]), put(?SAVE_RECV, true), Footers; -read_chunk(Length) -> +read_chunk(Length, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) -> case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of {ok, <>} -> Chunk; @@ -540,32 +558,34 @@ read_chunk(Length) -> exit(normal) end. -read_sub_chunks(Length, MaxChunkSize, Fun, FunState) when Length > MaxChunkSize -> - Bin = recv(MaxChunkSize), +read_sub_chunks(Length, MaxChunkSize, Fun, FunState, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > MaxChunkSize -> + Bin = recv(MaxChunkSize, THIS), NewState = Fun({size(Bin), Bin}, FunState), - read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState); + read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState, THIS); -read_sub_chunks(Length, _MaxChunkSize, Fun, FunState) -> - Fun({Length, read_chunk(Length)}, FunState). +read_sub_chunks(Length, _MaxChunkSize, Fun, FunState, + {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + Fun({Length, read_chunk(Length, THIS)}, FunState). -%% @spec serve_file(Path, DocRoot) -> Response +%% @spec serve_file(Path, DocRoot, request()) -> Response %% @doc Serve a file relative to DocRoot. -serve_file(Path, DocRoot) -> - serve_file(Path, DocRoot, []). +serve_file(Path, DocRoot, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + serve_file(Path, DocRoot, [], THIS). -%% @spec serve_file(Path, DocRoot, ExtraHeaders) -> Response +%% @spec serve_file(Path, DocRoot, ExtraHeaders, request()) -> Response %% @doc Serve a file relative to DocRoot. -serve_file(Path, DocRoot, ExtraHeaders) -> +serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> case mochiweb_util:safe_relative_path(Path) of undefined -> - not_found(ExtraHeaders); + not_found(ExtraHeaders, THIS); RelPath -> FullPath = filename:join([DocRoot, RelPath]), case filelib:is_dir(FullPath) of true -> - maybe_redirect(RelPath, FullPath, ExtraHeaders); + maybe_redirect(RelPath, FullPath, ExtraHeaders, THIS); false -> - maybe_serve_file(FullPath, ExtraHeaders) + maybe_serve_file(FullPath, ExtraHeaders, THIS) end end. @@ -575,13 +595,14 @@ serve_file(Path, DocRoot, ExtraHeaders) -> directory_index(FullPath) -> filename:join([FullPath, "index.html"]). -maybe_redirect([], FullPath, ExtraHeaders) -> - maybe_serve_file(directory_index(FullPath), ExtraHeaders); +maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS); -maybe_redirect(RelPath, FullPath, ExtraHeaders) -> +maybe_redirect(RelPath, FullPath, ExtraHeaders, + {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}=THIS) -> case string:right(RelPath, 1) of "/" -> - maybe_serve_file(directory_index(FullPath), ExtraHeaders); + maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS); _ -> Host = mochiweb_headers:get_value("host", Headers), Location = "http://" ++ Host ++ "/" ++ RelPath ++ "/", @@ -596,16 +617,16 @@ maybe_redirect(RelPath, FullPath, ExtraHeaders) -> "

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

\n">>, Body = <>, - respond({301, MoreHeaders, Body}) + respond({301, MoreHeaders, Body}, THIS) end. -maybe_serve_file(File, ExtraHeaders) -> - case read_file_info(File) of +maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + case file:read_file_info(File) of {ok, FileInfo} -> - LastModified = couch_util:rfc1123_date(FileInfo#file_info.mtime), - case get_header_value("if-modified-since") of + LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime), + case get_header_value("if-modified-since", THIS) of LastModified -> - respond({304, ExtraHeaders, ""}); + respond({304, ExtraHeaders, ""}, THIS); _ -> case file:open(File, [raw, binary]) of {ok, IoDevice} -> @@ -613,39 +634,20 @@ maybe_serve_file(File, ExtraHeaders) -> Res = ok({ContentType, [{"last-modified", LastModified} | ExtraHeaders], - {file, IoDevice}}), - file:close(IoDevice), + {file, IoDevice}}, THIS), + ok = file:close(IoDevice), Res; _ -> - not_found(ExtraHeaders) + not_found(ExtraHeaders, THIS) end end; {error, _} -> - not_found(ExtraHeaders) - end. - -read_file_info(File) -> - try - file:read_file_info(File, [{time, universal}]) - catch error:undef -> - case file:read_file_info(File) of - {ok, FileInfo} -> - {ok, FileInfo#file_info{ - atime=to_universal(FileInfo#file_info.atime), - mtime=to_universal(FileInfo#file_info.mtime), - ctime=to_universal(FileInfo#file_info.ctime) - }}; - Else -> - Else - end + not_found(ExtraHeaders, THIS) end. -to_universal(LocalTime) -> - erlang:localtime_to_universaltime(LocalTime). - server_headers() -> [{"Server", "MochiWeb/1.0 (" ++ ?QUIP ++ ")"}, - {"Date", couch_util:rfc1123_date()}]. + {"Date", httpd_util:rfc1123_date()}]. make_code(X) when is_integer(X) -> [integer_to_list(X), [" " | httpd_util:reason_phrase(X)]]; @@ -688,7 +690,7 @@ range_parts(Body0, Ranges) -> end, {lists:foldr(F, [], Ranges), Size}. -%% @spec accepted_encodings([encoding()]) -> [encoding()] | bad_accept_encoding_value +%% @spec accepted_encodings([encoding()], request()) -> [encoding()] | bad_accept_encoding_value %% @type encoding() = string(). %% %% @doc Returns a list of encodings accepted by a request. Encodings that are @@ -712,8 +714,8 @@ range_parts(Body0, Ranges) -> %% accepted_encodings(["gzip", "deflate", "identity"]) -> %% ["deflate", "gzip", "identity"] %% -accepted_encodings(SupportedEncodings) -> - AcceptEncodingHeader = case get_header_value("Accept-Encoding") of +accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + AcceptEncodingHeader = case get_header_value("Accept-Encoding", THIS) of undefined -> ""; Value -> @@ -728,7 +730,7 @@ accepted_encodings(SupportedEncodings) -> ) end. -%% @spec accepts_content_type(string() | binary()) -> boolean() | bad_accept_header +%% @spec accepts_content_type(string() | binary(), request()) -> boolean() | bad_accept_header %% %% @doc Determines whether a request accepts a given media type by analyzing its %% "Accept" header. @@ -750,16 +752,9 @@ accepted_encodings(SupportedEncodings) -> %% 5) For an "Accept" header with value "text/*; q=0.0, */*": %% accepts_content_type("text/plain") -> false %% -accepts_content_type(ContentType) when is_binary(ContentType) -> - accepts_content_type(binary_to_list(ContentType)); -accepts_content_type(ContentType1) -> +accepts_content_type(ContentType1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> ContentType = re:replace(ContentType1, "\\s", "", [global, {return, list}]), - AcceptHeader = case get_header_value("Accept") of - undefined -> - "*/*"; - Value -> - Value - end, + AcceptHeader = accept_header(THIS), case mochiweb_util:parse_qvalues(AcceptHeader) of invalid_qvalue_string -> bad_accept_header; @@ -780,9 +775,83 @@ accepts_content_type(ContentType1) -> (not lists:member({SuperType, 0.0}, QList)) end. +%% @spec accepted_content_types([string() | binary()], request()) -> [string()] | bad_accept_header +%% +%% @doc Filters which of the given media types this request accepts. This filtering +%% is performed by analyzing the "Accept" header. The returned list is sorted +%% according to the preferences specified in the "Accept" header (higher Q values +%% first). If two or more types have the same preference (Q value), they're order +%% in the returned list is the same as they're order in the input list. +%% +%% Examples +%% +%% 1) For a missing "Accept" header: +%% accepted_content_types(["text/html", "application/json"]) -> +%% ["text/html", "application/json"] +%% +%% 2) For an "Accept" header with value "text/html, application/*": +%% accepted_content_types(["application/json", "text/html"]) -> +%% ["application/json", "text/html"] +%% +%% 3) For an "Accept" header with value "text/html, */*; q=0.0": +%% accepted_content_types(["text/html", "application/json"]) -> +%% ["text/html"] +%% +%% 4) For an "Accept" header with value "text/html; q=0.5, */*; q=0.1": +%% accepts_content_types(["application/json", "text/html"]) -> +%% ["text/html", "application/json"] +%% +accepted_content_types(Types1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + Types = lists:map( + fun(T) -> re:replace(T, "\\s", "", [global, {return, list}]) end, + Types1), + AcceptHeader = accept_header(THIS), + case mochiweb_util:parse_qvalues(AcceptHeader) of + invalid_qvalue_string -> + bad_accept_header; + QList -> + TypesQ = lists:foldr( + fun(T, Acc) -> + case proplists:get_value(T, QList) of + undefined -> + [MainType, _SubType] = string:tokens(T, "/"), + case proplists:get_value(MainType ++ "/*", QList) of + undefined -> + case proplists:get_value("*/*", QList) of + Q when is_float(Q), Q > 0.0 -> + [{Q, T} | Acc]; + _ -> + Acc + end; + Q when Q > 0.0 -> + [{Q, T} | Acc]; + _ -> + Acc + end; + Q when Q > 0.0 -> + [{Q, T} | Acc]; + _ -> + Acc + end + end, + [], Types), + % Note: Stable sort. If 2 types have the same Q value we leave them in the + % same order as in the input list. + SortFun = fun({Q1, _}, {Q2, _}) -> Q1 >= Q2 end, + [Type || {_Q, Type} <- lists:sort(SortFun, TypesQ)] + end. + +accept_header({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) -> + case get_header_value("Accept", THIS) of + undefined -> + "*/*"; + Value -> + Value + end. + %% %% Tests %% --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). -endif. http://git-wip-us.apache.org/repos/asf/couchdb/blob/cbb8a550/src/mochiweb/mochiweb_request_tests.erl ---------------------------------------------------------------------- diff --git a/src/mochiweb/mochiweb_request_tests.erl b/src/mochiweb/mochiweb_request_tests.erl index b61a583..b40c867 100644 --- a/src/mochiweb/mochiweb_request_tests.erl +++ b/src/mochiweb/mochiweb_request_tests.erl @@ -1,12 +1,13 @@ -module(mochiweb_request_tests). --include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). accepts_content_type_test() -> Req1 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, mochiweb_headers:make([{"Accept", "multipart/related"}])), ?assertEqual(true, Req1:accepts_content_type("multipart/related")), + ?assertEqual(true, Req1:accepts_content_type(<<"multipart/related">>)), Req2 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, mochiweb_headers:make([{"Accept", "text/html"}])), @@ -60,4 +61,122 @@ accepts_content_type_test() -> mochiweb_headers:make([{"Accept", "text/html;level=1;q=0.1, text/html"}])), ?assertEqual(true, Req14:accepts_content_type("text/html; level=1")). +accepted_encodings_test() -> + Req1 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([])), + ?assertEqual(["identity"], + Req1:accepted_encodings(["gzip", "identity"])), + + Req2 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "gzip, deflate"}])), + ?assertEqual(["gzip", "identity"], + Req2:accepted_encodings(["gzip", "identity"])), + + Req3 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "gzip;q=0.5, deflate"}])), + ?assertEqual(["deflate", "gzip", "identity"], + Req3:accepted_encodings(["gzip", "deflate", "identity"])), + + Req4 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "identity, *;q=0"}])), + ?assertEqual(["identity"], + Req4:accepted_encodings(["gzip", "deflate", "identity"])), + + Req5 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "gzip; q=0.1, *;q=0"}])), + ?assertEqual(["gzip"], + Req5:accepted_encodings(["gzip", "deflate", "identity"])), + + Req6 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "gzip; q=, *;q=0"}])), + ?assertEqual(bad_accept_encoding_value, + Req6:accepted_encodings(["gzip", "deflate", "identity"])), + + Req7 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "gzip;q=2.0, *;q=0"}])), + ?assertEqual(bad_accept_encoding_value, + Req7:accepted_encodings(["gzip", "identity"])), + + Req8 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept-Encoding", "deflate, *;q=0.0"}])), + ?assertEqual([], + Req8:accepted_encodings(["gzip", "identity"])). + +accepted_content_types_test() -> + Req1 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html"}])), + ?assertEqual(["text/html"], + Req1:accepted_content_types(["text/html", "application/json"])), + + Req2 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html, */*;q=0"}])), + ?assertEqual(["text/html"], + Req2:accepted_content_types(["text/html", "application/json"])), + + Req3 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/*, */*;q=0"}])), + ?assertEqual(["text/html"], + Req3:accepted_content_types(["text/html", "application/json"])), + + Req4 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/*;q=0.8, */*;q=0.5"}])), + ?assertEqual(["text/html", "application/json"], + Req4:accepted_content_types(["application/json", "text/html"])), + + Req5 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/*;q=0.8, */*;q=0.5"}])), + ?assertEqual(["text/html", "application/json"], + Req5:accepted_content_types(["text/html", "application/json"])), + + Req6 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/*;q=0.5, */*;q=0.5"}])), + ?assertEqual(["application/json", "text/html"], + Req6:accepted_content_types(["application/json", "text/html"])), + + Req7 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make( + [{"Accept", "text/html;q=0.5, application/json;q=0.5"}])), + ?assertEqual(["application/json", "text/html"], + Req7:accepted_content_types(["application/json", "text/html"])), + + Req8 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html"}])), + ?assertEqual([], + Req8:accepted_content_types(["application/json"])), + + Req9 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/*;q=0.9, text/html;q=0.5, */*;q=0.7"}])), + ?assertEqual(["application/json", "text/html"], + Req9:accepted_content_types(["text/html", "application/json"])). + +should_close_test() -> + F = fun (V, H) -> + (mochiweb_request:new( + nil, 'GET', "/", V, + mochiweb_headers:make(H) + )):should_close() + end, + ?assertEqual( + true, + F({1, 1}, [{"Connection", "close"}])), + ?assertEqual( + true, + F({1, 0}, [{"Connection", "close"}])), + ?assertEqual( + true, + F({1, 1}, [{"Connection", "ClOSe"}])), + ?assertEqual( + false, + F({1, 1}, [{"Connection", "closer"}])), + ?assertEqual( + false, + F({1, 1}, [])), + ?assertEqual( + true, + F({1, 0}, [])), + ?assertEqual( + false, + F({1, 0}, [{"Connection", "Keep-Alive"}])), + ok. + -endif.