couchdb-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From d..@apache.org
Subject [2/8] COUCHDB-1696 import mochiweb from tag v2.4.2
Date Wed, 24 Apr 2013 22:18:33 GMT
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 = <<"<html><img src=/images/icon.png></img></html>">>,
         ?assertEqual(
             {<<"html">>,[],[
                 { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] }
             ]},
             mochiweb_html:parse(D1)),
-    
+
     D2 = <<"<html><img src=/images/icon&gt;.png width=100></img></html>">>,
         ?assertEqual(
             {<<"html">>,[],[
                 { <<"img">>, [ { <<"src">>, <<"/images/icon>.png">> }, { <<"width">>, <<"100">> } ], [] }
             ]},
             mochiweb_html:parse(D2)),
-    ok.        
-    
-parse_quoted_attr_test() ->    
+    ok.
+
+parse_quoted_attr_test() ->
     D0 = <<"<html><img src='/images/icon.png'></html>">>,
     ?assertEqual(
         {<<"html">>,[],[
             { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] }
         ]},
-        mochiweb_html:parse(D0)),     
-        
+        mochiweb_html:parse(D0)),
+
     D1 = <<"<html><img src=\"/images/icon.png'></html>">>,
     ?assertEqual(
         {<<"html">>,[],[
             { <<"img">>, [ { <<"src">>, <<"/images/icon.png'></html>">> } ], [] }
         ]},
-        mochiweb_html:parse(D1)),     
+        mochiweb_html:parse(D1)),
 
     D2 = <<"<html><img src=\"/images/icon&gt;.png\"></html>">>,
     ?assertEqual(
         {<<"html">>,[],[
             { <<"img">>, [ { <<"src">>, <<"/images/icon>.png">> } ], [] }
         ]},
-        mochiweb_html:parse(D2)),     
+        mochiweb_html:parse(D2)),
+
+    %% Quoted attributes can contain whitespace and newlines
+    D3 = <<"<html><a href=\"#\" onclick=\"javascript: test(1,\ntrue);\"></html>">>,
+    ?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 = <<"<html><?xml:namespace prefix = o ns = \"urn:schemas-microsoft-com:office:office\" /></html>">>,
 	?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 = <<"<link />">>,
+    T0 = {<<"link">>,[],[]},
+    ?assertEqual(D0, iolist_to_binary(to_html(T0))),
+
+    D1 = <<"<head><link /></head>">>,
+    T1 = {<<"head">>,[],[{<<"link">>,[],[]}]},
+    ?assertEqual(D1, iolist_to_binary(to_html(T1))),
+
+    D2 = <<"<head><link /><link /></head>">>,
+    T2 = {<<"head">>,[],[{<<"link">>,[],[]}, {<<"link">>,[],[]}]},
+    ?assertEqual(D2, iolist_to_binary(to_html(T2))),
+
+    %% Make sure singletons are converted to singletons.
+    D3 = <<"<head><link /></head>">>,
+    T3 = {<<"head">>,[],[{<<"link">>,[],[<<"funny">>]}]},
+    ?assertEqual(D3, iolist_to_binary(to_html(T3))),
+
+    D4 = <<"<link />">>,
+    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("<html><body onload=\"javascript:A('1&2')\"></body></html>")),
+     ?_assertEqual(
+        {<<"html">>,[],
+         [{<<"body">>,[{<<"onload">>,<<"javascript:A('1& 2')">>}],[]}]},
+        mochiweb_html:parse("<html><body onload=\"javascript:A('1& 2')\"></body></html>")),
+     ?_assertEqual(
+        {<<"html">>,[],
+         [{<<"body">>,[],[<<"& ">>]}]},
+        mochiweb_html:parse("<html><body>& </body></html>")),
+     ?_assertEqual(
+        {<<"html">>,[],
+         [{<<"body">>,[],[<<"&">>]}]},
+        mochiweb_html:parse("<html><body>&</body></html>"))].
+
+parse_unescaped_lt_test() ->
+    D1 = <<"<div> < < <a href=\"/\">Back</a></div>">>,
+    ?assertEqual(
+        {<<"div">>, [], [<<" < < ">>, {<<"a">>, [{<<"href">>, <<"/">>}], 
+                                       [<<"Back">>]}]},
+        mochiweb_html:parse(D1)),
+
+    D2 = <<"<div> << <a href=\"/\">Back</a></div>">>,
+    ?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) ->
-    ["<html><head></head><body>"
-     "<form method=\"POST\">"
-     "<input type=\"hidden\" value=\"message\" name=\"hidden\"/>"
-     "<input type=\"submit\" value=\"regular POST\">"
-     "</form>"
-     "<br />"
-     "<form method=\"POST\" enctype=\"multipart/form-data\""
-     " action=\"/multipart\">"
-     "<input type=\"hidden\" value=\"multipart message\" name=\"hidden\"/>"
-     "<input type=\"file\" name=\"file\"/>"
-     "<input type=\"submit\" value=\"multipart POST\" />"
-     "</form>"
-     "<pre>", Body, "</pre>"
-     "</body></html>"].
-
-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
 %%      <code>socket</code> is requested on a HTTPS connection, then
 %%      an ssl socket will be returned as <code>{ssl, SslSocket}</code>.
 %%      You can use <code>SslSocket</code> with the <code>ssl</code>
 %%      application, eg: <code>ssl:peercert(SslSocket)</code>.
-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 <code>not_found([])</code>.
-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 <code>respond({404, [{"Content-Type", "text/plain"}
 %% | ExtraHeaders], &lt;&lt;"Not found."&gt;&gt;})</code>.
-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:Length/binary, "\r\n">>} ->
             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) ->
             "<p>The document has moved <a href=\"">>,
             Bottom = <<">here</a>.</p></body></html>\n">>,
             Body = <<Top/binary, LocationBin/binary, Bottom/binary>>,
-            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.


Mime
View raw message