You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by rn...@apache.org on 2010/07/26 19:21:32 UTC

svn commit: r979368 [3/3] - in /couchdb/trunk: etc/couchdb/ share/www/script/ share/www/script/jspec/ share/www/script/test/ src/couchdb/ src/mochiweb/

Modified: couchdb/trunk/src/mochiweb/mochiweb_multipart.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/mochiweb/mochiweb_multipart.erl?rev=979368&r1=979367&r2=979368&view=diff
==============================================================================
--- couchdb/trunk/src/mochiweb/mochiweb_multipart.erl (original)
+++ couchdb/trunk/src/mochiweb/mochiweb_multipart.erl Mon Jul 26 17:21:30 2010
@@ -8,17 +8,73 @@
 
 -export([parse_form/1, parse_form/2]).
 -export([parse_multipart_request/2]).
--export([test/0]).
+-export([parts_to_body/3, parts_to_multipart_body/4]).
+-export([default_file_handler/2]).
 
 -define(CHUNKSIZE, 4096).
 
 -record(mp, {state, boundary, length, buffer, callback, req}).
 
 %% TODO: DOCUMENT THIS MODULE.
-
+%% @type key() = atom() | string() | binary().
+%% @type value() = atom() | iolist() | integer().
+%% @type header() = {key(), value()}.
+%% @type bodypart() = {Start::integer(), End::integer(), Body::iolist()}.
+%% @type formfile() = {Name::string(), ContentType::string(), Content::binary()}.
+%% @type request().
+%% @type file_handler() = (Filename::string(), ContentType::string()) -> file_handler_callback().
+%% @type file_handler_callback() = (binary() | eof) -> file_handler_callback() | term().
+
+%% @spec parts_to_body([bodypart()], ContentType::string(),
+%%                     Size::integer()) -> {[header()], iolist()}
+%% @doc Return {[header()], iolist()} representing the body for the given
+%%      parts, may be a single part or multipart.
+parts_to_body([{Start, End, Body}], ContentType, Size) ->
+    HeaderList = [{"Content-Type", ContentType},
+                  {"Content-Range",
+                   ["bytes ",
+                    mochiweb_util:make_io(Start), "-", mochiweb_util:make_io(End),
+                    "/", mochiweb_util:make_io(Size)]}],
+    {HeaderList, Body};
+parts_to_body(BodyList, ContentType, Size) when is_list(BodyList) ->
+    parts_to_multipart_body(BodyList, ContentType, Size,
+                            mochihex:to_hex(crypto:rand_bytes(8))).
+
+%% @spec parts_to_multipart_body([bodypart()], ContentType::string(),
+%%                               Size::integer(), Boundary::string()) ->
+%%           {[header()], iolist()}
+%% @doc Return {[header()], iolist()} representing the body for the given
+%%      parts, always a multipart response.
+parts_to_multipart_body(BodyList, ContentType, Size, Boundary) ->
+    HeaderList = [{"Content-Type",
+                   ["multipart/byteranges; ",
+                    "boundary=", Boundary]}],
+    MultiPartBody = multipart_body(BodyList, ContentType, Boundary, Size),
+
+    {HeaderList, MultiPartBody}.
+
+%% @spec multipart_body([bodypart()], ContentType::string(),
+%%                      Boundary::string(), Size::integer()) -> iolist()
+%% @doc Return the representation of a multipart body for the given [bodypart()].
+multipart_body([], _ContentType, Boundary, _Size) ->
+    ["--", Boundary, "--\r\n"];
+multipart_body([{Start, End, Body} | BodyList], ContentType, Boundary, Size) ->
+    ["--", Boundary, "\r\n",
+     "Content-Type: ", ContentType, "\r\n",
+     "Content-Range: ",
+         "bytes ", mochiweb_util:make_io(Start), "-", mochiweb_util:make_io(End),
+             "/", mochiweb_util:make_io(Size), "\r\n\r\n",
+     Body, "\r\n"
+     | multipart_body(BodyList, ContentType, Boundary, Size)].
+
+%% @spec parse_form(request()) -> [{string(), string() | formfile()}]
+%% @doc Parse a multipart form from the given request using the in-memory
+%%      default_file_handler/2.
 parse_form(Req) ->
     parse_form(Req, fun default_file_handler/2).
 
+%% @spec parse_form(request(), F::file_handler()) -> [{string(), string() | term()}]
+%% @doc Parse a multipart form from the given request using the given file_handler().
 parse_form(Req, FileHandler) ->
     Callback = fun (Next) -> parse_form_outer(Next, FileHandler, []) end,
     {_, _, Res} = parse_multipart_request(Req, Callback),
@@ -236,13 +292,38 @@ find_boundary(Prefix, Data) ->
             not_found
     end.
 
-with_socket_server(ServerFun, ClientFun) ->
-    {ok, Server} = mochiweb_socket_server:start([{ip, "127.0.0.1"},
-                                                 {port, 0},
-                                                 {loop, ServerFun}]),
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+ssl_cert_opts() ->
+    EbinDir = filename:dirname(code:which(?MODULE)),
+    CertDir = filename:join([EbinDir, "..", "support", "test-materials"]),
+    CertFile = filename:join(CertDir, "test_ssl_cert.pem"),
+    KeyFile = filename:join(CertDir, "test_ssl_key.pem"),
+    [{certfile, CertFile}, {keyfile, KeyFile}].
+
+with_socket_server(Transport, ServerFun, ClientFun) ->
+    ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}],
+    ServerOpts = case Transport of
+        plain ->
+            ServerOpts0;
+        ssl ->
+            ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}]
+    end,
+    {ok, Server} = mochiweb_socket_server:start(ServerOpts),
     Port = mochiweb_socket_server:get(Server, port),
-    {ok, Client} = gen_tcp:connect("127.0.0.1", Port,
-                                   [binary, {active, false}]),
+    ClientOpts = [binary, {active, false}],
+    {ok, Client} = case Transport of
+        plain ->
+            gen_tcp:connect("127.0.0.1", Port, ClientOpts);
+        ssl ->
+            ClientOpts1 = [{ssl_imp, new} | ClientOpts],
+            {ok, SslSocket} = ssl:connect("127.0.0.1", Port, ClientOpts1),
+            {ok, {ssl, SslSocket}}
+    end,
     Res = (catch ClientFun(Client)),
     mochiweb_socket_server:stop(Server),
     Res.
@@ -256,19 +337,30 @@ fake_request(Socket, ContentType, Length
                            [{"content-type", ContentType},
                             {"content-length", Length}])).
 
-test_callback(Expect, [Expect | Rest]) ->
+test_callback({body, <<>>}, Rest=[body_end | _]) ->
+    %% When expecting the body_end we might get an empty binary
+    fun (Next) -> test_callback(Next, Rest) end;
+test_callback({body, Got}, [{body, Expect} | Rest]) when Got =/= Expect ->
+    %% Partial response
+    GotSize = size(Got),
+    <<Got:GotSize/binary, Expect1/binary>> = Expect,
+    fun (Next) -> test_callback(Next, [{body, Expect1} | Rest]) end;
+test_callback(Got, [Expect | Rest]) ->
+    ?assertEqual(Got, Expect),
     case Rest of
         [] ->
             ok;
         _ ->
             fun (Next) -> test_callback(Next, Rest) end
-    end;
-test_callback({body, Got}, [{body, Expect} | Rest]) ->
-    GotSize = size(Got),
-    <<Got:GotSize/binary, Expect1/binary>> = Expect,
-    fun (Next) -> test_callback(Next, [{body, Expect1} | Rest]) end.
+    end.
 
-test_parse3() ->
+parse3_http_test() ->
+    parse3(plain).
+
+parse3_https_test() ->
+    parse3(ssl).
+
+parse3(Transport) ->
     ContentType = "multipart/form-data; boundary=---------------------------7386909285754635891697677882",
     BinContent = <<"-----------------------------7386909285754635891697677882\r\nContent-Disposition: form-data; name=\"hidden\"\r\n\r\nmultipart message\r\n-----------------------------7386909285754635891697677882\r\nContent-Disposition: form-data; name=\"file\"; filename=\"test_file.txt\"\r\nContent-Type: text/plain\r\n\r\nWoo multiline text file\n\nLa la la\r\n-----------------------------7386909285754635891697677882--\r\n">>,
     Expect = [{headers,
@@ -285,8 +377,8 @@ test_parse3() ->
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
     ServerFun = fun (Socket) ->
-                        ok = gen_tcp:send(Socket, BinContent),
-                        exit(normal)
+                        ok = mochiweb_socket:send(Socket, BinContent),
+			exit(normal)
                 end,
     ClientFun = fun (Socket) ->
                         Req = fake_request(Socket, ContentType,
@@ -295,11 +387,16 @@ test_parse3() ->
                         {0, <<>>, ok} = Res,
                         ok
                 end,
-    ok = with_socket_server(ServerFun, ClientFun),
+    ok = with_socket_server(Transport, ServerFun, ClientFun),
     ok.
 
+parse2_http_test() ->
+    parse2(plain).
+
+parse2_https_test() ->
+    parse2(ssl).
 
-test_parse2() ->
+parse2(Transport) ->
     ContentType = "multipart/form-data; boundary=---------------------------6072231407570234361599764024",
     BinContent = <<"-----------------------------6072231407570234361599764024\r\nContent-Disposition: form-data; name=\"hidden\"\r\n\r\nmultipart message\r\n-----------------------------6072231407570234361599764024\r\nContent-Disposition: form-data; name=\"file\"; filename=\"\"\r\nContent-Type: application/octet-stream\r\n\r\n\r\n-----------------------------6072231407570234361599764024--\r\n">>,
     Expect = [{headers,
@@ -316,8 +413,8 @@ test_parse2() ->
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
     ServerFun = fun (Socket) ->
-                        ok = gen_tcp:send(Socket, BinContent),
-                        exit(normal)
+                        ok = mochiweb_socket:send(Socket, BinContent),
+			exit(normal)
                 end,
     ClientFun = fun (Socket) ->
                         Req = fake_request(Socket, ContentType,
@@ -326,10 +423,16 @@ test_parse2() ->
                         {0, <<>>, ok} = Res,
                         ok
                 end,
-    ok = with_socket_server(ServerFun, ClientFun),
+    ok = with_socket_server(Transport, ServerFun, ClientFun),
     ok.
 
-test_parse_form() ->
+parse_form_http_test() ->
+    do_parse_form(plain).
+
+parse_form_https_test() ->
+    do_parse_form(ssl).
+
+do_parse_form(Transport) ->
     ContentType = "multipart/form-data; boundary=AaB03x",
     "AaB03x" = get_boundary(ContentType),
     Content = mochiweb_util:join(
@@ -347,8 +450,8 @@ test_parse_form() ->
                  ""], "\r\n"),
     BinContent = iolist_to_binary(Content),
     ServerFun = fun (Socket) ->
-                        ok = gen_tcp:send(Socket, BinContent),
-                        exit(normal)
+                        ok = mochiweb_socket:send(Socket, BinContent),
+			exit(normal)
                 end,
     ClientFun = fun (Socket) ->
                         Req = fake_request(Socket, ContentType,
@@ -360,10 +463,16 @@ test_parse_form() ->
                          }] = Res,
                         ok
                 end,
-    ok = with_socket_server(ServerFun, ClientFun),
+    ok = with_socket_server(Transport, ServerFun, ClientFun),
     ok.
 
-test_parse() ->
+parse_http_test() ->
+    do_parse(plain).
+
+parse_https_test() ->
+    do_parse(ssl).
+
+do_parse(Transport) ->
     ContentType = "multipart/form-data; boundary=AaB03x",
     "AaB03x" = get_boundary(ContentType),
     Content = mochiweb_util:join(
@@ -394,8 +503,60 @@ test_parse() ->
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
     ServerFun = fun (Socket) ->
-                        ok = gen_tcp:send(Socket, BinContent),
-                        exit(normal)
+                        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(Transport, ServerFun, ClientFun),
+    ok.
+
+parse_partial_body_boundary_http_test() ->
+   parse_partial_body_boundary(plain).
+
+parse_partial_body_boundary_https_test() ->
+   parse_partial_body_boundary(ssl).
+
+parse_partial_body_boundary(Transport) ->
+    Boundary = string:copies("$", 2048),
+    ContentType = "multipart/form-data; boundary=" ++ Boundary,
+    ?assertEqual(Boundary, get_boundary(ContentType)),
+    Content = mochiweb_util:join(
+                ["--" ++ Boundary,
+                 "Content-Disposition: form-data; name=\"submit-name\"",
+                 "",
+                 "Larry",
+                 "--" ++ Boundary,
+                 "Content-Disposition: form-data; name=\"files\";"
+                 ++ "filename=\"file1.txt\"",
+                 "Content-Type: text/plain",
+                 "",
+                 "... contents of file1.txt ...",
+                 "--" ++ Boundary ++ "--",
+                 ""], "\r\n"),
+    BinContent = iolist_to_binary(Content),
+    Expect = [{headers,
+               [{"content-disposition",
+                 {"form-data", [{"name", "submit-name"}]}}]},
+              {body, <<"Larry">>},
+              body_end,
+              {headers,
+               [{"content-disposition",
+                 {"form-data", [{"name", "files"}, {"filename", "file1.txt"}]}},
+                {"content-type", {"text/plain", []}}
+               ]},
+              {body, <<"... contents of file1.txt ...">>},
+              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,
@@ -404,10 +565,63 @@ test_parse() ->
                         {0, <<>>, ok} = Res,
                         ok
                 end,
-    ok = with_socket_server(ServerFun, ClientFun),
+    ok = with_socket_server(Transport, ServerFun, ClientFun),
     ok.
 
-test_find_boundary() ->
+parse_large_header_http_test() ->
+    parse_large_header(plain).
+
+parse_large_header_https_test() ->
+    parse_large_header(ssl).
+
+parse_large_header(Transport) ->
+    ContentType = "multipart/form-data; boundary=AaB03x",
+    "AaB03x" = get_boundary(ContentType),
+    Content = mochiweb_util:join(
+                ["--AaB03x",
+                 "Content-Disposition: form-data; name=\"submit-name\"",
+                 "",
+                 "Larry",
+                 "--AaB03x",
+                 "Content-Disposition: form-data; name=\"files\";"
+                 ++ "filename=\"file1.txt\"",
+                 "Content-Type: text/plain",
+                 "x-large-header: " ++ string:copies("%", 4096),
+                 "",
+                 "... contents of file1.txt ...",
+                 "--AaB03x--",
+                 ""], "\r\n"),
+    BinContent = iolist_to_binary(Content),
+    Expect = [{headers,
+               [{"content-disposition",
+                 {"form-data", [{"name", "submit-name"}]}}]},
+              {body, <<"Larry">>},
+              body_end,
+              {headers,
+               [{"content-disposition",
+                 {"form-data", [{"name", "files"}, {"filename", "file1.txt"}]}},
+                {"content-type", {"text/plain", []}},
+                {"x-large-header", {string:copies("%", 4096), []}}
+               ]},
+              {body, <<"... contents of file1.txt ...">>},
+              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(Transport, ServerFun, ClientFun),
+    ok.
+
+find_boundary_test() ->
     B = <<"\r\n--X">>,
     {next_boundary, 0, 7} = find_boundary(B, <<"\r\n--X\r\nRest">>),
     {next_boundary, 1, 7} = find_boundary(B, <<"!\r\n--X\r\nRest">>),
@@ -422,9 +636,10 @@ test_find_boundary() ->
           45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,45,
           49,54,48,51,55,52,53,52,51,53,49>>,
     {maybe, 30} = find_boundary(P, B0),
+    not_found = find_boundary(B, <<"\r\n--XJOPKE">>),
     ok.
 
-test_find_in_binary() ->
+find_in_binary_test() ->
     {exact, 0} = find_in_binary(<<"foo">>, <<"foobarbaz">>),
     {exact, 1} = find_in_binary(<<"oo">>, <<"foobarbaz">>),
     {exact, 8} = find_in_binary(<<"z">>, <<"foobarbaz">>),
@@ -435,7 +650,13 @@ test_find_in_binary() ->
     {partial, 1, 3} = find_in_binary(<<"foobar">>, <<"afoo">>),
     ok.
 
-test_flash_parse() ->
+flash_parse_http_test() ->
+    flash_parse(plain).
+
+flash_parse_https_test() ->
+    flash_parse(ssl).
+
+flash_parse(Transport) ->
     ContentType = "multipart/form-data; boundary=----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5",
     "----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5" = get_boundary(ContentType),
     BinContent = <<"------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition: form-data; name=\"Filename\"\r\n\r\nhello.txt\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition: form-data; name=\"success_action_status\"\r\n\r\n201\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition: form-data; name=\"file\"; filename=\"hello.txt\"\r\nContent-Type: application/octet-stream\r\n\r\nhello\n\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5\r\nContent-Disposition: form-data; name=\"Upload\"\r\n\r\nSubmit Query\r\n------------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5--">>,
@@ -463,8 +684,8 @@ test_flash_parse() ->
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
     ServerFun = fun (Socket) ->
-                        ok = gen_tcp:send(Socket, BinContent),
-                        exit(normal)
+                        ok = mochiweb_socket:send(Socket, BinContent),
+			exit(normal)
                 end,
     ClientFun = fun (Socket) ->
                         Req = fake_request(Socket, ContentType,
@@ -473,10 +694,16 @@ test_flash_parse() ->
                         {0, <<>>, ok} = Res,
                         ok
                 end,
-    ok = with_socket_server(ServerFun, ClientFun),
+    ok = with_socket_server(Transport, ServerFun, ClientFun),
     ok.
 
-test_flash_parse2() ->
+flash_parse2_http_test() ->
+    flash_parse2(plain).
+
+flash_parse2_https_test() ->
+    flash_parse2(ssl).
+
+flash_parse2(Transport) ->
     ContentType = "multipart/form-data; boundary=----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5",
     "----------ei4GI3GI3Ij5Ef1ae0KM7Ij5ei4Ij5" = get_boundary(ContentType),
     Chunk = iolist_to_binary(string:copies("%", 4096)),
@@ -505,8 +732,8 @@ test_flash_parse2() ->
               eof],
     TestCallback = fun (Next) -> test_callback(Next, Expect) end,
     ServerFun = fun (Socket) ->
-                        ok = gen_tcp:send(Socket, BinContent),
-                        exit(normal)
+                        ok = mochiweb_socket:send(Socket, BinContent),
+			exit(normal)
                 end,
     ClientFun = fun (Socket) ->
                         Req = fake_request(Socket, ContentType,
@@ -515,16 +742,83 @@ test_flash_parse2() ->
                         {0, <<>>, ok} = Res,
                         ok
                 end,
-    ok = with_socket_server(ServerFun, ClientFun),
+    ok = with_socket_server(Transport, ServerFun, ClientFun),
+    ok.
+
+parse_headers_test() ->
+    ?assertEqual([], parse_headers(<<>>)).
+
+flash_multipart_hack_test() ->
+    Buffer = <<"prefix-">>,
+    Prefix = <<"prefix">>,
+    State = #mp{length=0, buffer=Buffer, boundary=Prefix},
+    ?assertEqual(State,
+                 flash_multipart_hack(State)).
+
+parts_to_body_single_test() ->
+    {HL, B} = parts_to_body([{0, 5, <<"01234">>}],
+                            "text/plain",
+                            10),
+    [{"Content-Range", Range},
+     {"Content-Type", Type}] = lists:sort(HL),
+    ?assertEqual(
+       <<"bytes 0-5/10">>,
+       iolist_to_binary(Range)),
+    ?assertEqual(
+       <<"text/plain">>,
+       iolist_to_binary(Type)),
+    ?assertEqual(
+       <<"01234">>,
+       iolist_to_binary(B)),
+    ok.
+
+parts_to_body_multi_test() ->
+    {[{"Content-Type", Type}],
+     _B} = parts_to_body([{0, 5, <<"01234">>}, {5, 10, <<"56789">>}],
+                        "text/plain",
+                        10),
+    ?assertMatch(
+       <<"multipart/byteranges; boundary=", _/binary>>,
+       iolist_to_binary(Type)),
     ok.
 
-test() ->
-    test_find_in_binary(),
-    test_find_boundary(),
-    test_parse(),
-    test_parse2(),
-    test_parse3(),
-    test_parse_form(),
-    test_flash_parse(),
-    test_flash_parse2(),
+parts_to_multipart_body_test() ->
+    {[{"Content-Type", V}], B} = parts_to_multipart_body(
+                                   [{0, 5, <<"01234">>}, {5, 10, <<"56789">>}],
+                                   "text/plain",
+                                   10,
+                                   "BOUNDARY"),
+    MB = multipart_body(
+           [{0, 5, <<"01234">>}, {5, 10, <<"56789">>}],
+           "text/plain",
+           "BOUNDARY",
+           10),
+    ?assertEqual(
+       <<"multipart/byteranges; boundary=BOUNDARY">>,
+       iolist_to_binary(V)),
+    ?assertEqual(
+       iolist_to_binary(MB),
+       iolist_to_binary(B)),
     ok.
+
+multipart_body_test() ->
+    ?assertEqual(
+       <<"--BOUNDARY--\r\n">>,
+       iolist_to_binary(multipart_body([], "text/plain", "BOUNDARY", 0))),
+    ?assertEqual(
+       <<"--BOUNDARY\r\n"
+         "Content-Type: text/plain\r\n"
+         "Content-Range: bytes 0-5/10\r\n\r\n"
+         "01234\r\n"
+         "--BOUNDARY\r\n"
+         "Content-Type: text/plain\r\n"
+         "Content-Range: bytes 5-10/10\r\n\r\n"
+         "56789\r\n"
+         "--BOUNDARY--\r\n">>,
+       iolist_to_binary(multipart_body([{0, 5, <<"01234">>}, {5, 10, <<"56789">>}],
+                                       "text/plain",
+                                       "BOUNDARY",
+                                       10))),
+    ok.
+
+-endif.

Modified: couchdb/trunk/src/mochiweb/mochiweb_request.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/mochiweb/mochiweb_request.erl?rev=979368&r1=979367&r2=979368&view=diff
==============================================================================
--- couchdb/trunk/src/mochiweb/mochiweb_request.erl (original)
+++ couchdb/trunk/src/mochiweb/mochiweb_request.erl Mon Jul 26 17:21:30 2010
@@ -7,9 +7,9 @@
 -author('bob@mochimedia.com').
 
 -include_lib("kernel/include/file.hrl").
+-include("internal.hrl").
 
 -define(QUIP, "Any of you quaids got a smint?").
--define(READ_SIZE, 8192).
 
 -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]).
@@ -21,7 +21,6 @@
 -export([parse_cookie/0, get_cookie_value/1]).
 -export([serve_file/2, serve_file/3]).
 -export([accepted_encodings/1]).
--export([test/0]).
 
 -define(SAVE_QS, mochiweb_request_qs).
 -define(SAVE_PATH, mochiweb_request_path).
@@ -40,8 +39,8 @@
 %% @type response(). A mochiweb_response parameterized module instance.
 %% @type ioheaders() = headers() | [{key(), value()}].
 
-% 5 minute default idle timeout
--define(IDLE_TIMEOUT, 300000).
+% 10 second default idle timeout
+-define(IDLE_TIMEOUT, 10000).
 
 % Maximum recv_body() length of 1MB
 -define(MAX_RECV_BODY, (1024*1024)).
@@ -54,12 +53,23 @@ get_header_value(K) ->
 get_primary_header_value(K) ->
     mochiweb_headers:get_primary_value(K, Headers).
 
-%% @type field() = socket | method | raw_path | version | headers | peer | path | body_length | range
+%% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range
 
 %% @spec get(field()) -> term()
-%% @doc Return the internal representation of the given field.
+%% @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) ->
     Socket;
+get(scheme) ->
+    case mochiweb_socket:type(Socket) of
+        plain ->
+            http;
+        ssl ->
+            https
+    end;
 get(method) ->
     Method;
 get(raw_path) ->
@@ -69,7 +79,7 @@ get(version) ->
 get(headers) ->
     Headers;
 get(peer) ->
-    case inet:peername(Socket) of
+    case mochiweb_socket:peername(Socket) of
         {ok, {Addr={10, _, _, _}, _Port}} ->
             case get_header_value("x-forwarded-for") of
                 undefined ->
@@ -85,7 +95,9 @@ get(peer) ->
                     string:strip(lists:last(string:tokens(Hosts, ",")))
             end;
         {ok, {Addr, _Port}} ->
-            inet_parse:ntoa(Addr)
+            inet_parse:ntoa(Addr);
+        {error, enotconn} ->
+            exit(normal)
     end;
 get(path) ->
     case erlang:get(?SAVE_PATH) of
@@ -98,13 +110,20 @@ get(path) ->
             Cached
     end;
 get(body_length) ->
-    erlang:get(?SAVE_BODY_LENGTH);
+    case erlang:get(?SAVE_BODY_LENGTH) of
+        undefined ->
+            BodyLength = body_length(),
+            put(?SAVE_BODY_LENGTH, {cached, BodyLength}),
+            BodyLength;
+        {cached, Cached} ->
+            Cached
+    end;
 get(range) ->
     case get_header_value(range) of
         undefined ->
             undefined;
         RawRange ->
-            parse_range_request(RawRange)
+            mochiweb_http:parse_range_request(RawRange)
     end.
 
 %% @spec dump() -> {mochiweb_request, [{atom(), term()}]}
@@ -119,7 +138,7 @@ dump() ->
 %% @spec send(iodata()) -> ok
 %% @doc Send data over the socket.
 send(Data) ->
-    case gen_tcp:send(Socket, Data) of
+    case mochiweb_socket:send(Socket, Data) of
         ok ->
             ok;
         _ ->
@@ -136,7 +155,7 @@ recv(Length) ->
 %% @doc Receive Length bytes from the client as a binary, with the given
 %%      Timeout in msec.
 recv(Length, Timeout) ->
-    case gen_tcp:recv(Socket, Length, Timeout) of
+    case mochiweb_socket:recv(Socket, Length, Timeout) of
         {ok, Data} ->
             put(?SAVE_RECV, true),
             Data;
@@ -172,20 +191,24 @@ recv_body() ->
 %% @doc Receive the body of the HTTP request (defined by Content-Length).
 %%      Will receive up to MaxBody bytes.
 recv_body(MaxBody) ->
-    % we could use a sane constant for max chunk size
-    Body = stream_body(?MAX_RECV_BODY, fun
-        ({0, _ChunkedFooter}, {_LengthAcc, BinAcc}) ->
-            iolist_to_binary(lists:reverse(BinAcc));
-        ({Length, Bin}, {LengthAcc, BinAcc}) ->
-            NewLength = Length + LengthAcc,
-            if NewLength > MaxBody ->
-                exit({body_too_large, chunked});
-            true ->
-                {NewLength, [Bin | BinAcc]}
-            end
-        end, {0, []}, MaxBody),
-    put(?SAVE_BODY, Body),
-    Body.
+    case erlang:get(?SAVE_BODY) of
+        undefined ->
+            % we could use a sane constant for max chunk size
+            Body = stream_body(?MAX_RECV_BODY, fun
+                ({0, _ChunkedFooter}, {_LengthAcc, BinAcc}) ->
+                    iolist_to_binary(lists:reverse(BinAcc));
+                ({Length, Bin}, {LengthAcc, BinAcc}) ->
+                    NewLength = Length + LengthAcc,
+                    if NewLength > MaxBody ->
+                        exit({body_too_large, chunked});
+                    true ->
+                        {NewLength, [Bin | BinAcc]}
+                    end
+                end, {0, []}, MaxBody),
+            put(?SAVE_BODY, Body),
+            Body;
+        Cached -> Cached
+    end.
 
 stream_body(MaxChunkSize, ChunkFun, FunState) ->
     stream_body(MaxChunkSize, ChunkFun, FunState, undefined).
@@ -242,7 +265,7 @@ start_response({Code, ResponseHeaders}) 
 %%      ResponseHeaders.
 start_raw_response({Code, ResponseHeaders}) ->
     F = fun ({K, V}, Acc) ->
-                [make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
+                [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
         end,
     End = lists:foldl(F, [<<"\r\n">>],
                       mochiweb_headers:to_list(ResponseHeaders)),
@@ -266,13 +289,13 @@ start_response_length({Code, ResponseHea
 %%      will be set by the Body length, and the server will insert header
 %%      defaults.
 respond({Code, ResponseHeaders, {file, IoDevice}}) ->
-    Length = iodevice_size(IoDevice),
+    Length = mochiweb_io:iodevice_size(IoDevice),
     Response = start_response_length({Code, ResponseHeaders, Length}),
     case Method of
         'HEAD' ->
             ok;
         _ ->
-            iodevice_stream(IoDevice)
+            mochiweb_io:iodevice_stream(fun send/1, IoDevice)
     end,
     Response;
 respond({Code, ResponseHeaders, chunked}) ->
@@ -327,8 +350,12 @@ ok({ContentType, Body}) ->
 ok({ContentType, ResponseHeaders, Body}) ->
     HResponse = mochiweb_headers:make(ResponseHeaders),
     case THIS:get(range) of
-        X when X =:= undefined; X =:= fail ->
-            HResponse1 = mochiweb_headers:enter("Content-Type", ContentType, HResponse),
+        X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked ->
+            %% http://code.google.com/p/mochiweb/issues/detail?id=54
+            %% Range header not supported when chunked, return 200 and provide
+            %% full response.
+            HResponse1 = mochiweb_headers:enter("Content-Type", ContentType,
+                                                HResponse),
             respond({200, HResponse1, Body});
         Ranges ->
             {PartList, Size} = range_parts(Body, Ranges),
@@ -341,7 +368,7 @@ ok({ContentType, ResponseHeaders, Body})
                     respond({200, HResponse1, Body});
                 PartList ->
                     {RangeHeaders, RangeBody} =
-                        parts_to_body(PartList, ContentType, Size),
+                        mochiweb_multipart:parts_to_body(PartList, ContentType, Size),
                     HResponse1 = mochiweb_headers:enter_from_list(
                                    [{"Accept-Ranges", "bytes"} |
                                     RangeHeaders],
@@ -458,26 +485,23 @@ stream_chunked_body(MaxChunkSize, Fun, F
 stream_unchunked_body(0, Fun, FunState) ->
     Fun({0, <<>>}, FunState);
 stream_unchunked_body(Length, Fun, FunState) when Length > 0 ->
-    Bin = recv(0),
-    BinSize = byte_size(Bin),
-    if BinSize > Length ->
-        <<OurBody:Length/binary, Extra/binary>> = Bin,
-        gen_tcp:unrecv(Socket, Extra),
-        NewState = Fun({Length, OurBody}, FunState),
-        stream_unchunked_body(0, Fun, NewState);
-    true ->
-        NewState = Fun({BinSize, Bin}, FunState),
-        stream_unchunked_body(Length - BinSize, Fun, NewState)
-    end.
-
+    PktSize = case Length > ?RECBUF_SIZE of
+        true ->
+            ?RECBUF_SIZE;
+        false ->
+            Length
+    end,
+    Bin = recv(PktSize),
+    NewState = Fun({PktSize, Bin}, FunState),
+    stream_unchunked_body(Length - PktSize, Fun, NewState).
 
 %% @spec read_chunk_length() -> integer()
 %% @doc Read the length of the next HTTP chunk.
 read_chunk_length() ->
-    inet:setopts(Socket, [{packet, line}]),
-    case gen_tcp:recv(Socket, 0, ?IDLE_TIMEOUT) of
+    mochiweb_socket:setopts(Socket, [{packet, line}]),
+    case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
         {ok, Header} ->
-            inet:setopts(Socket, [{packet, raw}]),
+            mochiweb_socket:setopts(Socket, [{packet, raw}]),
             Splitter = fun (C) ->
                                C =/= $\r andalso C =/= $\n andalso C =/= $
                        end,
@@ -491,9 +515,9 @@ read_chunk_length() ->
 %% @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) ->
-    inet:setopts(Socket, [{packet, line}]),
+    mochiweb_socket:setopts(Socket, [{packet, line}]),
     F = fun (F1, Acc) ->
-                case gen_tcp:recv(Socket, 0, ?IDLE_TIMEOUT) of
+                case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
                     {ok, <<"\r\n">>} ->
                         Acc;
                     {ok, Footer} ->
@@ -503,10 +527,11 @@ read_chunk(0) ->
                 end
         end,
     Footers = F(F, []),
-    inet:setopts(Socket, [{packet, raw}]),
+    mochiweb_socket:setopts(Socket, [{packet, raw}]),
+    put(?SAVE_RECV, true),
     Footers;
 read_chunk(Length) ->
-    case gen_tcp:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of
+    case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of
         {ok, <<Chunk:Length/binary, "\r\n">>} ->
             Chunk;
         _ ->
@@ -601,13 +626,6 @@ server_headers() ->
     [{"Server", "MochiWeb/1.0 (" ++ ?QUIP ++ ")"},
      {"Date", httpd_util:rfc1123_date()}].
 
-make_io(Atom) when is_atom(Atom) ->
-    atom_to_list(Atom);
-make_io(Integer) when is_integer(Integer) ->
-    integer_to_list(Integer);
-make_io(Io) when is_list(Io); is_binary(Io) ->
-    Io.
-
 make_code(X) when is_integer(X) ->
     [integer_to_list(X), [" " | httpd_util:reason_phrase(X)]];
 make_code(Io) when is_list(Io); is_binary(Io) ->
@@ -618,56 +636,10 @@ make_version({1, 0}) ->
 make_version(_) ->
     <<"HTTP/1.1 ">>.
 
-iodevice_stream(IoDevice) ->
-    case file:read(IoDevice, ?READ_SIZE) of
-        eof ->
-            ok;
-        {ok, Data} ->
-            ok = send(Data),
-            iodevice_stream(IoDevice)
-    end.
-
-
-parts_to_body([{Start, End, Body}], ContentType, Size) ->
-    %% return body for a range reponse with a single body
-    HeaderList = [{"Content-Type", ContentType},
-                  {"Content-Range",
-                   ["bytes ",
-                    make_io(Start), "-", make_io(End),
-                    "/", make_io(Size)]}],
-    {HeaderList, Body};
-parts_to_body(BodyList, ContentType, Size) when is_list(BodyList) ->
-    %% return
-    %% header Content-Type: multipart/byteranges; boundary=441934886133bdee4
-    %% and multipart body
-    Boundary = mochihex:to_hex(crypto:rand_bytes(8)),
-    HeaderList = [{"Content-Type",
-                   ["multipart/byteranges; ",
-                    "boundary=", Boundary]}],
-    MultiPartBody = multipart_body(BodyList, ContentType, Boundary, Size),
-
-    {HeaderList, MultiPartBody}.
-
-multipart_body([], _ContentType, Boundary, _Size) ->
-    ["--", Boundary, "--\r\n"];
-multipart_body([{Start, End, Body} | BodyList], ContentType, Boundary, Size) ->
-    ["--", Boundary, "\r\n",
-     "Content-Type: ", ContentType, "\r\n",
-     "Content-Range: ",
-         "bytes ", make_io(Start), "-", make_io(End),
-             "/", make_io(Size), "\r\n\r\n",
-     Body, "\r\n"
-     | multipart_body(BodyList, ContentType, Boundary, Size)].
-
-iodevice_size(IoDevice) ->
-    {ok, Size} = file:position(IoDevice, eof),
-    {ok, 0} = file:position(IoDevice, bof),
-    Size.
-
 range_parts({file, IoDevice}, Ranges) ->
-    Size = iodevice_size(IoDevice),
+    Size = mochiweb_io:iodevice_size(IoDevice),
     F = fun (Spec, Acc) ->
-                case range_skip_length(Spec, Size) of
+                case mochiweb_http:range_skip_length(Spec, Size) of
                     invalid_range ->
                         Acc;
                     V ->
@@ -685,7 +657,7 @@ range_parts(Body0, Ranges) ->
     Body = iolist_to_binary(Body0),
     Size = size(Body),
     F = fun(Spec, Acc) ->
-                case range_skip_length(Spec, Size) of
+                case mochiweb_http:range_skip_length(Spec, Size) of
                     invalid_range ->
                         Acc;
                     {Skip, Length} ->
@@ -695,45 +667,8 @@ range_parts(Body0, Ranges) ->
         end,
     {lists:foldr(F, [], Ranges), Size}.
 
-range_skip_length(Spec, Size) ->
-    case Spec of
-        {none, R} when R =< Size, R >= 0 ->
-            {Size - R, R};
-        {none, _OutOfRange} ->
-            {0, Size};
-        {R, none} when R >= 0, R < Size ->
-            {R, Size - R};
-        {_OutOfRange, none} ->
-            invalid_range;
-        {Start, End} when 0 =< Start, Start =< End, End < Size ->
-            {Start, End - Start + 1};
-        {_OutOfRange, _End} ->
-            invalid_range
-    end.
-
-parse_range_request(RawRange) when is_list(RawRange) ->
-    try
-        "bytes=" ++ RangeString = RawRange,
-        Ranges = string:tokens(RangeString, ","),
-        lists:map(fun ("-" ++ V)  ->
-                          {none, list_to_integer(V)};
-                      (R) ->
-                          case string:tokens(R, "-") of
-                              [S1, S2] ->
-                                  {list_to_integer(S1), list_to_integer(S2)};
-                              [S] ->
-                                  {list_to_integer(S), none}
-                          end
-                  end,
-                  Ranges)
-    catch
-        _:_ ->
-            fail
-    end.
-
-%% @spec accepted_encodings([encoding()]) -> [encoding()] | error()
-%% @type encoding() -> string()
-%% @type error() -> bad_accept_encoding_value
+%% @spec accepted_encodings([encoding()]) -> [encoding()] | bad_accept_encoding_value
+%% @type encoding() = string().
 %%
 %% @doc Returns a list of encodings accepted by a request. Encodings that are
 %%      not supported by the server will not be included in the return list.
@@ -772,96 +707,9 @@ accepted_encodings(SupportedEncodings) -
             )
     end.
 
-test() ->
-    ok = test_range(),
-    ok.
-
-test_range() ->
-    %% valid, single ranges
-    io:format("Testing parse_range_request with valid single ranges~n"),
-    io:format("1"),
-    [{20, 30}] = parse_range_request("bytes=20-30"),
-    io:format("2"),
-    [{20, none}] = parse_range_request("bytes=20-"),
-    io:format("3"),
-    [{none, 20}] = parse_range_request("bytes=-20"),
-    io:format(".. ok ~n"),
-
-    %% invalid, single ranges
-    io:format("Testing parse_range_request with invalid ranges~n"),
-    io:format("1"),
-    fail = parse_range_request(""),
-    io:format("2"),
-    fail = parse_range_request("garbage"),
-    io:format("3"),
-    fail = parse_range_request("bytes=-20-30"),
-    io:format(".. ok ~n"),
-
-    %% valid, multiple range
-    io:format("Testing parse_range_request with valid multiple ranges~n"),
-    io:format("1"),
-    [{20, 30}, {50, 100}, {110, 200}] =
-        parse_range_request("bytes=20-30,50-100,110-200"),
-    io:format("2"),
-    [{20, none}, {50, 100}, {none, 200}] =
-        parse_range_request("bytes=20-,50-100,-200"),
-    io:format(".. ok~n"),
-
-    %% no ranges
-    io:format("Testing out parse_range_request with no ranges~n"),
-    io:format("1"),
-    [] = parse_range_request("bytes="),
-    io:format(".. ok~n"),
-
-    Body = <<"012345678901234567890123456789012345678901234567890123456789">>,
-    BodySize = byte_size(Body), %% 60
-    BodySize = 60,
-
-    %% these values assume BodySize =:= 60
-    io:format("Testing out range_skip_length on valid ranges~n"),
-    io:format("1"),
-    {1,9} = range_skip_length({1,9}, BodySize), %% 1-9
-    io:format("2"),
-    {10,10} = range_skip_length({10,19}, BodySize), %% 10-19
-    io:format("3"),
-    {40, 20} = range_skip_length({none, 20}, BodySize), %% -20
-    io:format("4"),
-    {30, 30} = range_skip_length({30, none}, BodySize), %% 30-
-    io:format(".. ok ~n"),
-
-    %% valid edge cases for range_skip_length
-    io:format("Testing out range_skip_length on valid edge case ranges~n"),
-    io:format("1"),
-    {BodySize, 0} = range_skip_length({none, 0}, BodySize),
-    io:format("2"),
-    {0, BodySize} = range_skip_length({none, BodySize}, BodySize),
-    io:format("3"),
-    {0, BodySize} = range_skip_length({0, none}, BodySize),
-    BodySizeLess1 = BodySize - 1,
-    io:format("4"),
-    {BodySizeLess1, 1} = range_skip_length({BodySize - 1, none}, BodySize),
-
-    %% out of range, return whole thing
-    io:format("5"),
-    {0, BodySize} = range_skip_length({none, BodySize + 1}, BodySize),
-    io:format("6"),
-    {0, BodySize} = range_skip_length({none, -1}, BodySize),
-    io:format(".. ok ~n"),
-
-    %% invalid ranges
-    io:format("Testing out range_skip_length on invalid ranges~n"),
-    io:format("1"),
-    invalid_range = range_skip_length({-1, 30}, BodySize),
-    io:format("2"),
-    invalid_range = range_skip_length({0, BodySize + 1}, BodySize),
-    io:format("3"),
-    invalid_range = range_skip_length({-1, BodySize + 1}, BodySize),
-    io:format("4"),
-    invalid_range = range_skip_length({BodySize, 40}, BodySize),
-    io:format("5"),
-    invalid_range = range_skip_length({-1, none}, BodySize),
-    io:format("6"),
-    invalid_range = range_skip_length({BodySize, none}, BodySize),
-    io:format(".. ok ~n"),
-    ok.
-
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

Modified: couchdb/trunk/src/mochiweb/mochiweb_response.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/mochiweb/mochiweb_response.erl?rev=979368&r1=979367&r2=979368&view=diff
==============================================================================
--- couchdb/trunk/src/mochiweb/mochiweb_response.erl (original)
+++ couchdb/trunk/src/mochiweb/mochiweb_response.erl Mon Jul 26 17:21:30 2010
@@ -54,3 +54,11 @@ write_chunk(Data) ->
         _ ->
             send(Data)
     end.
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

Modified: couchdb/trunk/src/mochiweb/mochiweb_skel.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/mochiweb/mochiweb_skel.erl?rev=979368&r1=979367&r2=979368&view=diff
==============================================================================
--- couchdb/trunk/src/mochiweb/mochiweb_skel.erl (original)
+++ couchdb/trunk/src/mochiweb/mochiweb_skel.erl Mon Jul 26 17:21:30 2010
@@ -14,10 +14,11 @@ skelcopy(DestDir, Name) ->
                    N + 1
            end,
     skelcopy(src(), DestDir, Name, LDst),
+    DestLink = filename:join([DestDir, Name, "deps", "mochiweb-src"]),
+    ok = filelib:ensure_dir(DestLink),
     ok = file:make_symlink(
-        filename:join(filename:dirname(code:which(?MODULE)), ".."),
-        filename:join([DestDir, Name, "deps", "mochiweb-src"])).
-
+           filename:join(filename:dirname(code:which(?MODULE)), ".."),
+           DestLink).
 
 %% Internal API
 
@@ -37,17 +38,22 @@ skelcopy(Src, DestDir, Name, LDst) ->
             EDst = lists:nthtail(LDst, Dir),
             ok = ensuredir(Dir),
             ok = file:write_file_info(Dir, #file_info{mode=Mode}),
-            {ok, Files} = file:list_dir(Src),
-            io:format("~s/~n", [EDst]),
-            lists:foreach(fun ("." ++ _) -> ok;
-                              (F) ->
-                                  skelcopy(filename:join(Src, F),
-                                           Dir,
-                                           Name,
-                                           LDst)
-                          end,
-                          Files),
-            ok;
+            case filename:basename(Src) of
+                "ebin" ->
+                    ok;
+                _ ->
+                    {ok, Files} = file:list_dir(Src),
+                    io:format("~s/~n", [EDst]),
+                    lists:foreach(fun ("." ++ _) -> ok;
+                                      (F) ->
+                                          skelcopy(filename:join(Src, F),
+                                                   Dir,
+                                                   Name,
+                                                   LDst)
+                                  end,
+                                  Files),
+                        ok
+            end;
         {ok, #file_info{type=regular, mode=Mode}} ->
             OutFile = filename:join(DestDir, Dest),
             {ok, B} = file:read_file(Src),
@@ -71,3 +77,10 @@ ensuredir(Dir) ->
         E ->
             E
     end.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

Added: couchdb/trunk/src/mochiweb/mochiweb_socket.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/mochiweb/mochiweb_socket.erl?rev=979368&view=auto
==============================================================================
--- couchdb/trunk/src/mochiweb/mochiweb_socket.erl (added)
+++ couchdb/trunk/src/mochiweb/mochiweb_socket.erl Mon Jul 26 17:21:30 2010
@@ -0,0 +1,84 @@
+%% @copyright 2010 Mochi Media, Inc.
+
+%% @doc MochiWeb socket - wrapper for plain and ssl sockets.
+
+-module(mochiweb_socket).
+
+-export([listen/4, accept/1, recv/3, send/2, close/1, port/1, peername/1,
+         setopts/2, type/1]).
+
+-define(ACCEPT_TIMEOUT, 2000).
+
+listen(Ssl, Port, Opts, SslOpts) ->
+    case Ssl of
+        true ->
+            case ssl:listen(Port, Opts ++ SslOpts) of
+                {ok, ListenSocket} ->
+                    {ok, {ssl, ListenSocket}};
+                {error, _} = Err ->
+                    Err
+            end;
+        false ->
+            gen_tcp:listen(Port, Opts)
+    end.
+
+accept({ssl, ListenSocket}) ->
+    % There's a bug in ssl:transport_accept/2 at the moment, which is the
+    % reason for the try...catch block. Should be fixed in OTP R14.
+    try ssl:transport_accept(ListenSocket) of
+        {ok, Socket} ->
+            case ssl:ssl_accept(Socket) of
+                ok ->
+                    {ok, {ssl, Socket}};
+                {error, _} = Err ->
+                    Err
+            end;
+        {error, _} = Err ->
+            Err
+    catch
+        error:{badmatch, {error, Reason}} ->
+            {error, Reason}
+    end;
+accept(ListenSocket) ->
+    gen_tcp:accept(ListenSocket, ?ACCEPT_TIMEOUT).
+
+recv({ssl, Socket}, Length, Timeout) ->
+    ssl:recv(Socket, Length, Timeout);
+recv(Socket, Length, Timeout) ->
+    gen_tcp:recv(Socket, Length, Timeout).
+
+send({ssl, Socket}, Data) ->
+    ssl:send(Socket, Data);
+send(Socket, Data) ->
+    gen_tcp:send(Socket, Data).
+
+close({ssl, Socket}) ->
+    ssl:close(Socket);
+close(Socket) ->
+    gen_tcp:close(Socket).
+
+port({ssl, Socket}) ->
+    case ssl:sockname(Socket) of
+        {ok, {_, Port}} ->
+            {ok, Port};
+        {error, _} = Err ->
+            Err
+    end;
+port(Socket) ->
+    inet:port(Socket).
+
+peername({ssl, Socket}) ->
+    ssl:peername(Socket);
+peername(Socket) ->
+    inet:peername(Socket).
+
+setopts({ssl, Socket}, Opts) ->
+    ssl:setopts(Socket, Opts);
+setopts(Socket, Opts) ->
+    inet:setopts(Socket, Opts).
+
+type({ssl, _}) ->
+    ssl;
+type(_) ->
+    plain.
+

Modified: couchdb/trunk/src/mochiweb/mochiweb_socket_server.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/mochiweb/mochiweb_socket_server.erl?rev=979368&r1=979367&r2=979368&view=diff
==============================================================================
--- couchdb/trunk/src/mochiweb/mochiweb_socket_server.erl (original)
+++ couchdb/trunk/src/mochiweb/mochiweb_socket_server.erl Mon Jul 26 17:21:30 2010
@@ -7,22 +7,28 @@
 -author('bob@mochimedia.com').
 -behaviour(gen_server).
 
+-include("internal.hrl").
+
 -export([start/1, stop/1]).
 -export([init/1, handle_call/3, handle_cast/2, terminate/2, code_change/3,
          handle_info/2]).
 -export([get/2]).
 
--export([acceptor_loop/1]).
-
 -record(mochiweb_socket_server,
         {port,
          loop,
          name=undefined,
+         %% NOTE: This is currently ignored.
          max=2048,
          ip=any,
          listen=null,
-         acceptor=null,
-         backlog=128}).
+         nodelay=false,
+         backlog=128,
+         active_sockets=0,
+         acceptor_pool_size=16,
+         ssl=false,
+         ssl_opts=[{ssl_imp, new}],
+         acceptor_pool=sets:new()}).
 
 start(State=#mochiweb_socket_server{}) ->
     start_server(State);
@@ -54,6 +60,8 @@ parse_options([], State) ->
 parse_options([{name, L} | Rest], State) when is_list(L) ->
     Name = {local, list_to_atom(L)},
     parse_options(Rest, State#mochiweb_socket_server{name=Name});
+parse_options([{name, A} | Rest], State) when A =:= undefined ->
+    parse_options(Rest, State#mochiweb_socket_server{name=A});
 parse_options([{name, A} | Rest], State) when is_atom(A) ->
     Name = {local, A},
     parse_options(Rest, State#mochiweb_socket_server{name=Name});
@@ -79,16 +87,32 @@ parse_options([{loop, Loop} | Rest], Sta
     parse_options(Rest, State#mochiweb_socket_server{loop=Loop});
 parse_options([{backlog, Backlog} | Rest], State) ->
     parse_options(Rest, State#mochiweb_socket_server{backlog=Backlog});
+parse_options([{nodelay, NoDelay} | Rest], State) ->
+    parse_options(Rest, State#mochiweb_socket_server{nodelay=NoDelay});
+parse_options([{acceptor_pool_size, Max} | Rest], State) ->
+    MaxInt = ensure_int(Max),
+    parse_options(Rest,
+                  State#mochiweb_socket_server{acceptor_pool_size=MaxInt});
 parse_options([{max, Max} | Rest], State) ->
-    MaxInt = case Max of
-                 Max when is_list(Max) ->
-                     list_to_integer(Max);
-                 Max when is_integer(Max) ->
-                     Max
-             end,
-    parse_options(Rest, State#mochiweb_socket_server{max=MaxInt}).
+    error_logger:info_report([{warning, "TODO: max is currently unsupported"},
+                              {max, Max}]),
+    MaxInt = ensure_int(Max),
+    parse_options(Rest, State#mochiweb_socket_server{max=MaxInt});
+parse_options([{ssl, Ssl} | Rest], State) when is_boolean(Ssl) ->
+    parse_options(Rest, State#mochiweb_socket_server{ssl=Ssl});
+parse_options([{ssl_opts, SslOpts} | Rest], State) when is_list(SslOpts) ->
+    SslOpts1 = [{ssl_imp, new} | proplists:delete(ssl_imp, SslOpts)],
+    parse_options(Rest, State#mochiweb_socket_server{ssl_opts=SslOpts1}).
 
-start_server(State=#mochiweb_socket_server{name=Name}) ->
+start_server(State=#mochiweb_socket_server{ssl=Ssl, name=Name}) ->
+    case Ssl of
+        true ->
+            application:start(crypto),
+            application:start(public_key),
+            application:start(ssl);
+        false ->
+            void
+    end,
     case Name of
         undefined ->
             gen_server:start_link(?MODULE, State, []);
@@ -96,6 +120,11 @@ start_server(State=#mochiweb_socket_serv
             gen_server:start_link(Name, ?MODULE, State, [])
     end.
 
+ensure_int(N) when is_integer(N) ->
+    N;
+ensure_int(S) when is_list(S) ->
+    integer_to_list(S).
+
 ipv6_supported() ->
     case (catch inet:getaddr("localhost", inet6)) of
         {ok, _Addr} ->
@@ -104,15 +133,15 @@ ipv6_supported() ->
             false
     end.
 
-init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog}) ->
+init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, nodelay=NoDelay}) ->
     process_flag(trap_exit, true),
     BaseOpts = [binary,
                 {reuseaddr, true},
                 {packet, 0},
                 {backlog, Backlog},
-                {recbuf, 8192},
+                {recbuf, ?RECBUF_SIZE},
                 {active, false},
-                {nodelay, true}],
+                {nodelay, NoDelay}],
     Opts = case Ip of
         any ->
             case ipv6_supported() of % IPv4, and IPv6 if supported
@@ -124,7 +153,7 @@ init(State=#mochiweb_socket_server{ip=Ip
         {_, _, _, _, _, _, _, _} -> % IPv6
             [inet6, {ip, Ip} | BaseOpts]
     end,
-    case gen_tcp_listen(Port, Opts, State) of
+    case listen(Port, Opts, State) of
         {stop, eacces} ->
             case Port < 1024 of
                 true ->
@@ -132,7 +161,7 @@ init(State=#mochiweb_socket_server{ip=Ip
                         {ok, _} ->
                             case fdsrv:bind_socket(tcp, Port) of
                                 {ok, Fd} ->
-                                    gen_tcp_listen(Port, [{fd, Fd} | Opts], State);
+                                    listen(Port, [{fd, Fd} | Opts], State);
                                 _ ->
                                     {stop, fdsrv_bind_failed}
                             end;
@@ -146,47 +175,33 @@ init(State=#mochiweb_socket_server{ip=Ip
             Other
     end.
 
-gen_tcp_listen(Port, Opts, State) ->
-    case gen_tcp:listen(Port, Opts) of
+new_acceptor_pool(Listen,
+                  State=#mochiweb_socket_server{acceptor_pool=Pool,
+                                                acceptor_pool_size=Size,
+                                                loop=Loop}) ->
+    F = fun (_, S) ->
+                Pid = mochiweb_acceptor:start_link(self(), Listen, Loop),
+                sets:add_element(Pid, S)
+        end,
+    Pool1 = lists:foldl(F, Pool, lists:seq(1, Size)),
+    State#mochiweb_socket_server{acceptor_pool=Pool1}.
+
+listen(Port, Opts, State=#mochiweb_socket_server{ssl=Ssl, ssl_opts=SslOpts}) ->
+    case mochiweb_socket:listen(Ssl, Port, Opts, SslOpts) of
         {ok, Listen} ->
-            {ok, ListenPort} = inet:port(Listen),
-            {ok, new_acceptor(State#mochiweb_socket_server{listen=Listen,
-                                                           port=ListenPort})};
+            {ok, ListenPort} = mochiweb_socket:port(Listen),
+            {ok, new_acceptor_pool(
+                   Listen,
+                   State#mochiweb_socket_server{listen=Listen,
+                                                port=ListenPort})};
         {error, Reason} ->
             {stop, Reason}
     end.
 
-new_acceptor(State=#mochiweb_socket_server{max=0}) ->
-    io:format("Not accepting new connections~n"),
-    State#mochiweb_socket_server{acceptor=null};
-new_acceptor(State=#mochiweb_socket_server{listen=Listen,loop=Loop}) ->
-    Pid = proc_lib:spawn_link(?MODULE, acceptor_loop,
-                              [{self(), Listen, Loop}]),
-    State#mochiweb_socket_server{acceptor=Pid}.
-
-call_loop({M, F}, Socket) ->
-    M:F(Socket);
-call_loop(Loop, Socket) ->
-    Loop(Socket).
-
-acceptor_loop({Server, Listen, Loop}) ->
-    case catch gen_tcp:accept(Listen) of
-        {ok, Socket} ->
-            gen_server:cast(Server, {accepted, self()}),
-            call_loop(Loop, Socket);
-        {error, closed} ->
-            exit({error, closed});
-        Other ->
-            error_logger:error_report(
-              [{application, mochiweb},
-               "Accept failed error",
-               lists:flatten(io_lib:format("~p", [Other]))]),
-            exit({error, accept_failed})
-    end.
-
-
 do_get(port, #mochiweb_socket_server{port=Port}) ->
-    Port.
+    Port;
+do_get(active_sockets, #mochiweb_socket_server{active_sockets=ActiveSockets}) ->
+    ActiveSockets.
 
 handle_call({get, Property}, _From, State) ->
     Res = do_get(Property, State),
@@ -195,16 +210,15 @@ handle_call(_Message, _From, State) ->
     Res = error,
     {reply, Res, State}.
 
-handle_cast({accepted, Pid},
-            State=#mochiweb_socket_server{acceptor=Pid, max=Max}) ->
-    % io:format("accepted ~p~n", [Pid]),
-    State1 = State#mochiweb_socket_server{max=Max - 1},
-    {noreply, new_acceptor(State1)};
+handle_cast({accepted, Pid, _Timing},
+            State=#mochiweb_socket_server{active_sockets=ActiveSockets}) ->
+    State1 = State#mochiweb_socket_server{active_sockets=1 + ActiveSockets},
+    {noreply, recycle_acceptor(Pid, State1)};
 handle_cast(stop, State) ->
     {stop, normal, State}.
 
 terminate(_Reason, #mochiweb_socket_server{listen=Listen, port=Port}) ->
-    gen_tcp:close(Listen),
+    mochiweb_socket:close(Listen),
     case Port < 1024 of
         true ->
             catch fdsrv:stop(),
@@ -216,33 +230,43 @@ terminate(_Reason, #mochiweb_socket_serv
 code_change(_OldVsn, State, _Extra) ->
     State.
 
-handle_info({'EXIT', Pid, normal},
-            State=#mochiweb_socket_server{acceptor=Pid}) ->
-    % io:format("normal acceptor down~n"),
-    {noreply, new_acceptor(State)};
+recycle_acceptor(Pid, State=#mochiweb_socket_server{
+                        acceptor_pool=Pool,
+                        listen=Listen,
+                        loop=Loop,
+                        active_sockets=ActiveSockets}) ->
+    case sets:is_element(Pid, Pool) of
+        true ->
+            Acceptor = mochiweb_acceptor:start_link(self(), Listen, Loop),
+            Pool1 = sets:add_element(Acceptor, sets:del_element(Pid, Pool)),
+            State#mochiweb_socket_server{acceptor_pool=Pool1};
+        false ->
+            State#mochiweb_socket_server{active_sockets=ActiveSockets - 1}
+    end.
+
+handle_info({'EXIT', Pid, normal}, State) ->
+    {noreply, recycle_acceptor(Pid, State)};
 handle_info({'EXIT', Pid, Reason},
-            State=#mochiweb_socket_server{acceptor=Pid}) ->
-    error_logger:error_report({?MODULE, ?LINE,
-                               {acceptor_error, Reason}}),
-    timer:sleep(100),
-    {noreply, new_acceptor(State)};
-handle_info({'EXIT', _LoopPid, Reason},
-            State=#mochiweb_socket_server{acceptor=Pid, max=Max}) ->
-    case Reason of
-        normal ->
-            ok;
-        _ ->
+            State=#mochiweb_socket_server{acceptor_pool=Pool}) ->
+    case sets:is_element(Pid, Pool) of
+        true ->
+            %% If there was an unexpected error accepting, log and sleep.
             error_logger:error_report({?MODULE, ?LINE,
-                                       {child_error, Reason}})
+                                       {acceptor_error, Reason}}),
+            timer:sleep(100);
+        false ->
+            ok
     end,
-    State1 = State#mochiweb_socket_server{max=Max + 1},
-    State2 = case Pid of
-                 null ->
-                     new_acceptor(State1);
-                 _ ->
-                     State1
-             end,
-    {noreply, State2};
+    {noreply, recycle_acceptor(Pid, State)};
 handle_info(Info, State) ->
     error_logger:info_report([{'INFO', Info}, {'State', State}]),
     {noreply, State}.
+
+
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

Modified: couchdb/trunk/src/mochiweb/mochiweb_sup.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/mochiweb/mochiweb_sup.erl?rev=979368&r1=979367&r2=979368&view=diff
==============================================================================
--- couchdb/trunk/src/mochiweb/mochiweb_sup.erl (original)
+++ couchdb/trunk/src/mochiweb/mochiweb_sup.erl Mon Jul 26 17:21:30 2010
@@ -32,3 +32,10 @@ upgrade() ->
 init([]) ->
     Processes = [],
     {ok, {{one_for_one, 10, 10}, Processes}}.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.

Modified: couchdb/trunk/src/mochiweb/mochiweb_util.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/mochiweb/mochiweb_util.erl?rev=979368&r1=979367&r2=979368&view=diff
==============================================================================
--- couchdb/trunk/src/mochiweb/mochiweb_util.erl (original)
+++ couchdb/trunk/src/mochiweb/mochiweb_util.erl Mon Jul 26 17:21:30 2010
@@ -9,11 +9,11 @@
 -export([path_split/1]).
 -export([urlsplit/1, urlsplit_path/1, urlunsplit/1, urlunsplit_path/1]).
 -export([guess_mime/1, parse_header/1]).
--export([shell_quote/1, cmd/1, cmd_string/1, cmd_port/2]).
+-export([shell_quote/1, cmd/1, cmd_string/1, cmd_port/2, cmd_status/1]).
 -export([record_to_proplist/2, record_to_proplist/3]).
 -export([safe_relative_path/1, partition/2]).
 -export([parse_qvalues/1, pick_accepted_encodings/3]).
--export([test/0]).
+-export([make_io/1]).
 
 -define(PERCENT, 37).  % $\%
 -define(FULLSTOP, 46). % $\.
@@ -115,11 +115,32 @@ cmd(Argv) ->
 %% @spec cmd_string([string()]) -> string()
 %% @doc Create a shell quoted command string from a list of arguments.
 cmd_string(Argv) ->
-    join([shell_quote(X) || X <- Argv], " ").
+    string:join([shell_quote(X) || X <- Argv], " ").
 
-%% @spec join([string()], Separator) -> string()
-%% @doc Join a list of strings together with the given separator
-%%      string or char.
+%% @spec cmd_status([string()]) -> {ExitStatus::integer(), Stdout::binary()}
+%% @doc Accumulate the output and exit status from the given application, will be
+%%      spawned with cmd_port/2.
+cmd_status(Argv) ->
+    Port = cmd_port(Argv, [exit_status, stderr_to_stdout,
+                           use_stdio, binary]),
+    try cmd_loop(Port, [])
+    after catch port_close(Port)
+    end.
+
+%% @spec cmd_loop(port(), list()) -> {ExitStatus::integer(), Stdout::binary()}
+%% @doc Accumulate the output and exit status from a port.
+cmd_loop(Port, Acc) ->
+    receive
+        {Port, {exit_status, Status}} ->
+            {Status, iolist_to_binary(lists:reverse(Acc))};
+        {Port, {data, Data}} ->
+            cmd_loop(Port, [Data | Acc])
+    end.
+
+%% @spec join([iolist()], iolist()) -> iolist()
+%% @doc Join a list of strings or binaries together with the given separator
+%%      string or char or binary. The output is flattened, but may be an
+%%      iolist() instead of a string() if any of the inputs are binary().
 join([], _Separator) ->
     [];
 join([S], _Separator) ->
@@ -160,10 +181,11 @@ quote_plus([C | Rest], Acc) ->
 %% @spec urlencode([{Key, Value}]) -> string()
 %% @doc URL encode the property list.
 urlencode(Props) ->
-    RevPairs = lists:foldl(fun ({K, V}, Acc) ->
-                                   [[quote_plus(K), $=, quote_plus(V)] | Acc]
-                           end, [], Props),
-    lists:flatten(revjoin(RevPairs, $&, [])).
+    Pairs = lists:foldr(
+              fun ({K, V}, Acc) ->
+                      [quote_plus(K) ++ "=" ++ quote_plus(V) | Acc]
+              end, [], Props),
+    string:join(Pairs, "&").
 
 %% @spec parse_qs(string() | binary()) -> [{Key, Value}]
 %% @doc Parse a query string or application/x-www-form-urlencoded.
@@ -234,20 +256,31 @@ urlsplit(Url) ->
     {Scheme, Netloc, Path, Query, Fragment}.
 
 urlsplit_scheme(Url) ->
-    urlsplit_scheme(Url, []).
+    case urlsplit_scheme(Url, []) of
+        no_scheme ->
+            {"", Url};
+        Res ->
+            Res
+    end.
 
-urlsplit_scheme([], Acc) ->
-    {"", lists:reverse(Acc)};
-urlsplit_scheme(":" ++ Rest, Acc) ->
+urlsplit_scheme([C | Rest], Acc) when ((C >= $a andalso C =< $z) orelse
+                                       (C >= $A andalso C =< $Z) orelse
+                                       (C >= $0 andalso C =< $9) orelse
+                                       C =:= $+ orelse C =:= $- orelse
+                                       C =:= $.) ->
+    urlsplit_scheme(Rest, [C | Acc]);
+urlsplit_scheme([$: | Rest], Acc=[_ | _]) ->
     {string:to_lower(lists:reverse(Acc)), Rest};
-urlsplit_scheme([C | Rest], Acc) ->
-    urlsplit_scheme(Rest, [C | Acc]).
+urlsplit_scheme(_Rest, _Acc) ->
+    no_scheme.
 
 urlsplit_netloc("//" ++ Rest) ->
     urlsplit_netloc(Rest, []);
 urlsplit_netloc(Path) ->
     {"", Path}.
 
+urlsplit_netloc("", Acc) ->
+    {lists:reverse(Acc), ""};
 urlsplit_netloc(Rest=[C | _], Acc) when C =:= $/; C =:= $?; C =:= $# ->
     {lists:reverse(Acc), Rest};
 urlsplit_netloc([C | Rest], Acc) ->
@@ -312,67 +345,11 @@ urlsplit_query([C | Rest], Acc) ->
 %% @spec guess_mime(string()) -> string()
 %% @doc  Guess the mime type of a file by the extension of its filename.
 guess_mime(File) ->
-    case filename:extension(File) of
-        ".html" ->
-            "text/html";
-        ".xhtml" ->
-            "application/xhtml+xml";
-        ".xml" ->
-            "application/xml";
-        ".css" ->
-            "text/css";
-        ".js" ->
-            "application/x-javascript";
-        ".jpg" ->
-            "image/jpeg";
-        ".gif" ->
-            "image/gif";
-        ".png" ->
-            "image/png";
-        ".swf" ->
-            "application/x-shockwave-flash";
-        ".zip" ->
-            "application/zip";
-        ".bz2" ->
-            "application/x-bzip2";
-        ".gz" ->
-            "application/x-gzip";
-        ".tar" ->
-            "application/x-tar";
-        ".tgz" ->
-            "application/x-gzip";
-        ".txt" ->
+    case mochiweb_mime:from_extension(filename:extension(File)) of
+        undefined ->
             "text/plain";
-        ".doc" ->
-            "application/msword";
-        ".pdf" ->
-            "application/pdf";
-        ".xls" ->
-            "application/vnd.ms-excel";
-        ".rtf" ->
-            "application/rtf";
-        ".mov" ->
-            "video/quicktime";
-        ".mp3" ->
-            "audio/mpeg";
-        ".z" ->
-            "application/x-compress";
-        ".wav" ->
-            "audio/x-wav";
-        ".ico" ->
-            "image/x-icon";
-        ".bmp" ->
-            "image/bmp";
-        ".m4a" ->
-            "audio/mpeg";
-        ".m3u" ->
-            "audio/x-mpegurl";
-        ".exe" ->
-            "application/octet-stream";
-        ".csv" ->
-            "text/csv";
-        _ ->
-            "text/plain"
+        Mime ->
+            Mime
     end.
 
 %% @spec parse_header(string()) -> {Type, [{K, V}]}
@@ -436,11 +413,9 @@ shell_quote([C | Rest], Acc) when C =:= 
 shell_quote([C | Rest], Acc) ->
     shell_quote(Rest, [C | Acc]).
 
-%% @spec parse_qvalues(string()) -> [qvalue()] | error()
-%% @type qvalue() -> {element(), q()}
-%% @type element() -> string()
-%% @type q() -> 0.0 .. 1.0
-%% @type error() -> invalid_qvalue_string
+%% @spec parse_qvalues(string()) -> [qvalue()] | invalid_qvalue_string
+%% @type qvalue() = {encoding(), float()}.
+%% @type encoding() = string().
 %%
 %% @doc Parses a list (given as a string) of elements with Q values associated
 %%      to them. Elements are separated by commas and each element is separated
@@ -489,11 +464,8 @@ parse_qvalues(QValuesStr) ->
             invalid_qvalue_string
     end.
 
-%% @spec pick_accepted_encodings(qvalues(), [encoding()], encoding()) ->
+%% @spec pick_accepted_encodings([qvalue()], [encoding()], encoding()) ->
 %%    [encoding()]
-%% @type qvalues() -> [ {encoding(), q()} ]
-%% @type encoding() -> string()
-%% @type q() -> 0.0 .. 1.0
 %%
 %% @doc Determines which encodings specified in the given Q values list are
 %%      valid according to a list of supported encodings and a default encoding.
@@ -566,46 +538,118 @@ pick_accepted_encodings(AcceptedEncs, Su
     [E || E <- Accepted2, lists:member(E, SupportedEncs),
         not lists:member(E, Refused1)].
 
-test() ->
-    test_join(),
-    test_quote_plus(),
-    test_unquote(),
-    test_urlencode(),
-    test_parse_qs(),
-    test_urlsplit_path(),
-    test_urlunsplit_path(),
-    test_urlsplit(),
-    test_urlunsplit(),
-    test_path_split(),
-    test_guess_mime(),
-    test_parse_header(),
-    test_shell_quote(),
-    test_cmd(),
-    test_cmd_string(),
-    test_partition(),
-    test_safe_relative_path(),
-    test_parse_qvalues(),
-    test_pick_accepted_encodings(),
-    ok.
-
-test_shell_quote() ->
-    "\"foo \\$bar\\\"\\`' baz\"" = shell_quote("foo $bar\"`' baz"),
-    ok.
-
-test_cmd() ->
-    "$bling$ `word`!\n" = cmd(["echo", "$bling$ `word`!"]),
-    ok.
-
-test_cmd_string() ->
-    "\"echo\" \"\\$bling\\$ \\`word\\`!\"" = cmd_string(["echo", "$bling$ `word`!"]),
-    ok.
-
-test_parse_header() ->
-    {"multipart/form-data", [{"boundary", "AaB03x"}]} =
-        parse_header("multipart/form-data; boundary=AaB03x"),
+make_io(Atom) when is_atom(Atom) ->
+    atom_to_list(Atom);
+make_io(Integer) when is_integer(Integer) ->
+    integer_to_list(Integer);
+make_io(Io) when is_list(Io); is_binary(Io) ->
+    Io.
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+
+make_io_test() ->
+    ?assertEqual(
+       <<"atom">>,
+       iolist_to_binary(make_io(atom))),
+    ?assertEqual(
+       <<"20">>,
+       iolist_to_binary(make_io(20))),
+    ?assertEqual(
+       <<"list">>,
+       iolist_to_binary(make_io("list"))),
+    ?assertEqual(
+       <<"binary">>,
+       iolist_to_binary(make_io(<<"binary">>))),
+    ok.
+
+-record(test_record, {field1=f1, field2=f2}).
+record_to_proplist_test() ->
+    ?assertEqual(
+       [{'__record', test_record},
+        {field1, f1},
+        {field2, f2}],
+       record_to_proplist(#test_record{}, record_info(fields, test_record))),
+    ?assertEqual(
+       [{'typekey', test_record},
+        {field1, f1},
+        {field2, f2}],
+       record_to_proplist(#test_record{},
+                          record_info(fields, test_record),
+                          typekey)),
+    ok.
+
+shell_quote_test() ->
+    ?assertEqual(
+       "\"foo \\$bar\\\"\\`' baz\"",
+       shell_quote("foo $bar\"`' baz")),
+    ok.
+
+cmd_port_test_spool(Port, Acc) ->
+    receive
+        {Port, eof} ->
+            Acc;
+        {Port, {data, {eol, Data}}} ->
+            cmd_port_test_spool(Port, ["\n", Data | Acc]);
+        {Port, Unknown} ->
+            throw({unknown, Unknown})
+    after 100 ->
+            throw(timeout)
+    end.
+
+cmd_port_test() ->
+    Port = cmd_port(["echo", "$bling$ `word`!"],
+                    [eof, stream, {line, 4096}]),
+    Res = try lists:append(lists:reverse(cmd_port_test_spool(Port, [])))
+          after catch port_close(Port)
+          end,
+    self() ! {Port, wtf},
+    try cmd_port_test_spool(Port, [])
+    catch throw:{unknown, wtf} -> ok
+    end,
+    try cmd_port_test_spool(Port, [])
+    catch throw:timeout -> ok
+    end,
+    ?assertEqual(
+       "$bling$ `word`!\n",
+       Res).
+
+cmd_test() ->
+    ?assertEqual(
+       "$bling$ `word`!\n",
+       cmd(["echo", "$bling$ `word`!"])),
+    ok.
+
+cmd_string_test() ->
+    ?assertEqual(
+       "\"echo\" \"\\$bling\\$ \\`word\\`!\"",
+       cmd_string(["echo", "$bling$ `word`!"])),
+    ok.
+
+cmd_status_test() ->
+    ?assertEqual(
+       {0, <<"$bling$ `word`!\n">>},
+       cmd_status(["echo", "$bling$ `word`!"])),
     ok.
 
-test_guess_mime() ->
+
+parse_header_test() ->
+    ?assertEqual(
+       {"multipart/form-data", [{"boundary", "AaB03x"}]},
+       parse_header("multipart/form-data; boundary=AaB03x")),
+    %% This tests (currently) intentionally broken behavior
+    ?assertEqual(
+       {"multipart/form-data",
+        [{"b", ""},
+         {"cgi", "is"},
+         {"broken", "true\"e"}]},
+       parse_header("multipart/form-data;b=;cgi=\"i\\s;broken=true\"e;=z;z")),
+    ok.
+
+guess_mime_test() ->
     "text/plain" = guess_mime(""),
     "text/plain" = guess_mime(".text"),
     "application/zip" = guess_mime(".zip"),
@@ -614,19 +658,22 @@ test_guess_mime() ->
     "application/xhtml+xml" = guess_mime("x.xhtml"),
     ok.
 
-test_path_split() ->
+path_split_test() ->
     {"", "foo/bar"} = path_split("/foo/bar"),
     {"foo", "bar"} = path_split("foo/bar"),
     {"bar", ""} = path_split("bar"),
     ok.
 
-test_urlsplit() ->
+urlsplit_test() ->
     {"", "", "/foo", "", "bar?baz"} = urlsplit("/foo#bar?baz"),
     {"http", "host:port", "/foo", "", "bar?baz"} =
         urlsplit("http://host:port/foo#bar?baz"),
+    {"http", "host", "", "", ""} = urlsplit("http://host"),
+    {"", "", "/wiki/Category:Fruit", "", ""} =
+        urlsplit("/wiki/Category:Fruit"),
     ok.
 
-test_urlsplit_path() ->
+urlsplit_path_test() ->
     {"/foo/bar", "", ""} = urlsplit_path("/foo/bar"),
     {"/foo", "baz", ""} = urlsplit_path("/foo?baz"),
     {"/foo", "", "bar?baz"} = urlsplit_path("/foo#bar?baz"),
@@ -635,13 +682,13 @@ test_urlsplit_path() ->
     {"/foo", "bar?baz", "baz"} = urlsplit_path("/foo?bar?baz#baz"),
     ok.
 
-test_urlunsplit() ->
+urlunsplit_test() ->
     "/foo#bar?baz" = urlunsplit({"", "", "/foo", "", "bar?baz"}),
     "http://host:port/foo#bar?baz" =
         urlunsplit({"http", "host:port", "/foo", "", "bar?baz"}),
     ok.
 
-test_urlunsplit_path() ->
+urlunsplit_path_test() ->
     "/foo/bar" = urlunsplit_path({"/foo/bar", "", ""}),
     "/foo?baz" = urlunsplit_path({"/foo", "baz", ""}),
     "/foo#bar?baz" = urlunsplit_path({"/foo", "", "bar?baz"}),
@@ -650,16 +697,28 @@ test_urlunsplit_path() ->
     "/foo?bar?baz#baz" = urlunsplit_path({"/foo", "bar?baz", "baz"}),
     ok.
 
-test_join() ->
-    "foo,bar,baz" = join(["foo", "bar", "baz"], $,),
-    "foo,bar,baz" = join(["foo", "bar", "baz"], ","),
-    "foo bar" = join([["foo", " bar"]], ","),
-    "foo bar,baz" = join([["foo", " bar"], "baz"], ","),
-    "foo" = join(["foo"], ","),
-    "foobarbaz" = join(["foo", "bar", "baz"], ""),
+join_test() ->
+    ?assertEqual("foo,bar,baz",
+                  join(["foo", "bar", "baz"], $,)),
+    ?assertEqual("foo,bar,baz",
+                  join(["foo", "bar", "baz"], ",")),
+    ?assertEqual("foo bar",
+                  join([["foo", " bar"]], ",")),
+    ?assertEqual("foo bar,baz",
+                  join([["foo", " bar"], "baz"], ",")),
+    ?assertEqual("foo",
+                  join(["foo"], ",")),
+    ?assertEqual("foobarbaz",
+                  join(["foo", "bar", "baz"], "")),
+    ?assertEqual("foo" ++ [<<>>] ++ "bar" ++ [<<>>] ++ "baz",
+                 join(["foo", "bar", "baz"], <<>>)),
+    ?assertEqual("foobar" ++ [<<"baz">>],
+                 join(["foo", "bar", <<"baz">>], "")),
+    ?assertEqual("",
+                 join([], "any")),
     ok.
 
-test_quote_plus() ->
+quote_plus_test() ->
     "foo" = quote_plus(foo),
     "1" = quote_plus(1),
     "1.1" = quote_plus(1.1),
@@ -668,26 +727,45 @@ test_quote_plus() ->
     "foo%0A" = quote_plus("foo\n"),
     "foo%0A" = quote_plus("foo\n"),
     "foo%3B%26%3D" = quote_plus("foo;&="),
+    "foo%3B%26%3D" = quote_plus(<<"foo;&=">>),
     ok.
 
-test_unquote() ->
-    "foo bar" = unquote("foo+bar"),
-    "foo bar" = unquote("foo%20bar"),
-    "foo\r\n" = unquote("foo%0D%0A"),
+unquote_test() ->
+    ?assertEqual("foo bar",
+                 unquote("foo+bar")),
+    ?assertEqual("foo bar",
+                 unquote("foo%20bar")),
+    ?assertEqual("foo\r\n",
+                 unquote("foo%0D%0A")),
+    ?assertEqual("foo\r\n",
+                 unquote(<<"foo%0D%0A">>)),
     ok.
 
-test_urlencode() ->
+urlencode_test() ->
     "foo=bar&baz=wibble+%0D%0A&z=1" = urlencode([{foo, "bar"},
                                                  {"baz", "wibble \r\n"},
                                                  {z, 1}]),
     ok.
 
-test_parse_qs() ->
-    [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}] =
-        parse_qs("foo=bar&baz=wibble+%0D%0A&z=1"),
+parse_qs_test() ->
+    ?assertEqual(
+       [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}],
+       parse_qs("foo=bar&baz=wibble+%0D%0a&z=1")),
+    ?assertEqual(
+       [{"", "bar"}, {"baz", "wibble \r\n"}, {"z", ""}],
+       parse_qs("=bar&baz=wibble+%0D%0a&z=")),
+    ?assertEqual(
+       [{"foo", "bar"}, {"baz", "wibble \r\n"}, {"z", "1"}],
+       parse_qs(<<"foo=bar&baz=wibble+%0D%0a&z=1">>)),
+    ?assertEqual(
+       [],
+       parse_qs("")),
+    ?assertEqual(
+       [{"foo", ""}, {"bar", ""}, {"baz", ""}],
+       parse_qs("foo;bar&baz")),
     ok.
 
-test_partition() ->
+partition_test() ->
     {"foo", "", ""} = partition("foo", "/"),
     {"foo", "/", "bar"} = partition("foo/bar", "/"),
     {"foo", "/", ""} = partition("foo/", "/"),
@@ -695,7 +773,7 @@ test_partition() ->
     {"f", "oo/ba", "r"} = partition("foo/bar", "oo/ba"),
     ok.
 
-test_safe_relative_path() ->
+safe_relative_path_test() ->
     "foo" = safe_relative_path("foo"),
     "foo/" = safe_relative_path("foo/"),
     "foo" = safe_relative_path("foo/bar/.."),
@@ -709,7 +787,7 @@ test_safe_relative_path() ->
     undefined = safe_relative_path("foo//"),
     ok.
 
-test_parse_qvalues() ->
+parse_qvalues_test() ->
     [] = parse_qvalues(""),
     [{"identity", 0.0}] = parse_qvalues("identity;q=0"),
     [{"identity", 0.0}] = parse_qvalues("identity ;q=0"),
@@ -748,9 +826,10 @@ test_parse_qvalues() ->
     invalid_qvalue_string = parse_qvalues("gzip; q=0.5, deflate;q=2"),
     invalid_qvalue_string = parse_qvalues("gzip, deflate;q=AB"),
     invalid_qvalue_string = parse_qvalues("gzip; q=2.1, deflate"),
+    invalid_qvalue_string = parse_qvalues("gzip; q=0.1234, deflate"),
     ok.
 
-test_pick_accepted_encodings() ->
+pick_accepted_encodings_test() ->
     ["identity"] = pick_accepted_encodings(
         [],
         ["gzip", "identity"],
@@ -857,3 +936,5 @@ test_pick_accepted_encodings() ->
         "identity"
     ),
     ok.
+
+-endif.

Modified: couchdb/trunk/src/mochiweb/reloader.erl
URL: http://svn.apache.org/viewvc/couchdb/trunk/src/mochiweb/reloader.erl?rev=979368&r1=979367&r2=979368&view=diff
==============================================================================
--- couchdb/trunk/src/mochiweb/reloader.erl (original)
+++ couchdb/trunk/src/mochiweb/reloader.erl Mon Jul 26 17:21:30 2010
@@ -13,7 +13,9 @@
 -export([start/0, start_link/0]).
 -export([stop/0]).
 -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-
+-export([all_changed/0]).
+-export([is_changed/1]).
+-export([reload_modules/1]).
 -record(state, {last, tref}).
 
 %% External API
@@ -74,8 +76,37 @@ terminate(_Reason, State) ->
 code_change(_Vsn, State, _Extra) ->
     {ok, State}.
 
+%% @spec reload_modules([atom()]) -> [{module, atom()} | {error, term()}]
+%% @doc code:purge/1 and code:load_file/1 the given list of modules in order,
+%%      return the results of code:load_file/1.
+reload_modules(Modules) ->
+    [begin code:purge(M), code:load_file(M) end || M <- Modules].
+
+%% @spec all_changed() -> [atom()]
+%% @doc Return a list of beam modules that have changed.
+all_changed() ->
+    [M || {M, Fn} <- code:all_loaded(), is_list(Fn), is_changed(M)].
+
+%% @spec is_changed(atom()) -> boolean()
+%% @doc true if the loaded module is a beam with a vsn attribute
+%%      and does not match the on-disk beam file, returns false otherwise.
+is_changed(M) ->
+    try
+        module_vsn(M:module_info()) =/= module_vsn(code:get_object_code(M))
+    catch _:_ ->
+            false
+    end.
+
 %% Internal API
 
+module_vsn({M, Beam, _Fn}) ->
+    {ok, {M, Vsn}} = beam_lib:version(Beam),
+    Vsn;
+module_vsn(L) when is_list(L) ->
+    {_, Attrs} = lists:keyfind(attributes, 1, L),
+    {_, Vsn} = lists:keyfind(vsn, 1, Attrs),
+    Vsn.
+
 doit(From, To) ->
     [case file:read_file_info(Filename) of
          {ok, #file_info{mtime = Mtime}} when Mtime >= From, Mtime < To ->
@@ -121,3 +152,10 @@ reload(Module) ->
 
 stamp() ->
     erlang:localtime().
+
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.