blob: a157f461dfee2889faaefdf2a5f55997b944ca37 [file] [log] [blame]
%% @author Bob Ippolito <bob@mochimedia.com>
%% @copyright 2007 Mochi Media, Inc.
%% @doc Start and stop the MochiWeb server.
-module(mochiweb).
-author('bob@mochimedia.com').
-export([new_request/1, new_response/1]).
-export([all_loaded/0, all_loaded/1, reload/0]).
-export([ensure_started/1]).
reload() ->
[c:l(Module) || Module <- all_loaded()].
all_loaded() ->
all_loaded(filename:dirname(code:which(?MODULE))).
all_loaded(Base) when is_atom(Base) ->
[];
all_loaded(Base) ->
FullBase = Base ++ "/",
F = fun ({_Module, Loaded}, Acc) when is_atom(Loaded) ->
Acc;
({Module, Loaded}, Acc) ->
case lists:prefix(FullBase, Loaded) of
true ->
[Module | Acc];
false ->
Acc
end
end,
lists:foldl(F, [], code:all_loaded()).
%% @spec new_request({Socket, Request, Headers}) -> MochiWebRequest
%% @doc Return a mochiweb_request data structure.
new_request({Socket, {Method, {abs_path, Uri}, Version}, Headers}) ->
mochiweb_request:new(Socket,
Method,
Uri,
Version,
mochiweb_headers:make(Headers));
% this case probably doesn't "exist".
new_request({Socket, {Method, {absoluteURI, _Protocol, _Host, _Port, Uri},
Version}, Headers}) ->
mochiweb_request:new(Socket,
Method,
Uri,
Version,
mochiweb_headers:make(Headers));
%% Request-URI is "*"
%% From http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
new_request({Socket, {Method, '*'=Uri, Version}, Headers}) ->
mochiweb_request:new(Socket,
Method,
Uri,
Version,
mochiweb_headers:make(Headers)).
%% @spec new_response({Request, integer(), Headers}) -> MochiWebResponse
%% @doc Return a mochiweb_response data structure.
new_response({Request, Code, Headers}) ->
mochiweb_response:new(Request,
Code,
mochiweb_headers:make(Headers)).
%% @spec ensure_started(App::atom()) -> ok
%% @doc Start the given App if it has not been started already.
ensure_started(App) ->
case application:start(App) of
ok ->
ok;
{error, {already_started, App}} ->
ok
end.
%%
%% Tests
%%
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-record(treq, {path, body= <<>>, xreply= <<>>}).
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_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_http:start_link(ServerOpts),
Port = mochiweb_socket_server:get(Server, port),
Res = (catch ClientFun(Transport, Port)),
mochiweb_http:stop(Server),
Res.
request_test() ->
R = mochiweb_request:new(z, z, "/foo/bar/baz%20wibble+quux?qs=2", z, []),
"/foo/bar/baz wibble quux" = R:get(path),
ok.
-define(LARGE_TIMEOUT, 60).
single_http_GET_test() ->
do_GET(plain, 1).
single_https_GET_test() ->
do_GET(ssl, 1).
multiple_http_GET_test() ->
do_GET(plain, 3).
multiple_https_GET_test() ->
do_GET(ssl, 3).
hundred_http_GET_test_() -> % note the underscore
{timeout, ?LARGE_TIMEOUT,
fun() -> ?assertEqual(ok, do_GET(plain,100)) end}.
hundred_https_GET_test_() -> % note the underscore
{timeout, ?LARGE_TIMEOUT,
fun() -> ?assertEqual(ok, do_GET(ssl,100)) end}.
single_128_http_POST_test() ->
do_POST(plain, 128, 1).
single_128_https_POST_test() ->
do_POST(ssl, 128, 1).
single_2k_http_POST_test() ->
do_POST(plain, 2048, 1).
single_2k_https_POST_test() ->
do_POST(ssl, 2048, 1).
single_100k_http_POST_test() ->
do_POST(plain, 102400, 1).
single_100k_https_POST_test() ->
do_POST(ssl, 102400, 1).
multiple_100k_http_POST_test() ->
do_POST(plain, 102400, 3).
multiple_100K_https_POST_test() ->
do_POST(ssl, 102400, 3).
hundred_128_http_POST_test_() -> % note the underscore
{timeout, ?LARGE_TIMEOUT,
fun() -> ?assertEqual(ok, do_POST(plain, 128, 100)) end}.
hundred_128_https_POST_test_() -> % note the underscore
{timeout, ?LARGE_TIMEOUT,
fun() -> ?assertEqual(ok, do_POST(ssl, 128, 100)) end}.
do_GET(Transport, Times) ->
PathPrefix = "/whatever/",
ReplyPrefix = "You requested: ",
ServerFun = fun (Req) ->
Reply = ReplyPrefix ++ Req:get(path),
Req:ok({"text/plain", Reply})
end,
TestReqs = [begin
Path = PathPrefix ++ integer_to_list(N),
ExpectedReply = list_to_binary(ReplyPrefix ++ Path),
#treq{path=Path, xreply=ExpectedReply}
end || N <- lists:seq(1, Times)],
ClientFun = new_client_fun('GET', TestReqs),
ok = with_server(Transport, ServerFun, ClientFun),
ok.
do_POST(Transport, Size, Times) ->
ServerFun = fun (Req) ->
Body = Req:recv_body(),
Headers = [{"Content-Type", "application/octet-stream"}],
Req:respond({201, Headers, Body})
end,
TestReqs = [begin
Path = "/stuff/" ++ integer_to_list(N),
Body = crypto:strong_rand_bytes(Size),
#treq{path=Path, body=Body, xreply=Body}
end || N <- lists:seq(1, Times)],
ClientFun = new_client_fun('POST', TestReqs),
ok = with_server(Transport, ServerFun, ClientFun),
ok.
new_client_fun(Method, TestReqs) ->
fun (Transport, Port) ->
client_request(Transport, Port, Method, TestReqs)
end.
client_request(Transport, Port, Method, TestReqs) ->
Opts = [binary, {active, false}, {packet, http}],
SockFun = case Transport of
plain ->
{ok, Socket} = gen_tcp:connect("127.0.0.1", Port, Opts),
fun (recv) ->
gen_tcp:recv(Socket, 0);
({recv, Length}) ->
gen_tcp:recv(Socket, Length);
({send, Data}) ->
gen_tcp:send(Socket, Data);
({setopts, L}) ->
inet:setopts(Socket, L)
end;
ssl ->
{ok, Socket} = ssl:connect("127.0.0.1", Port, [{ssl_imp, new} | Opts]),
fun (recv) ->
ssl:recv(Socket, 0);
({recv, Length}) ->
ssl:recv(Socket, Length);
({send, Data}) ->
ssl:send(Socket, Data);
({setopts, L}) ->
ssl:setopts(Socket, L)
end
end,
client_request(SockFun, Method, TestReqs).
client_request(SockFun, _Method, []) ->
{the_end, {error, closed}} = {the_end, SockFun(recv)},
ok;
client_request(SockFun, Method,
[#treq{path=Path, body=Body, xreply=ExReply} | Rest]) ->
Request = [atom_to_list(Method), " ", Path, " HTTP/1.1\r\n",
client_headers(Body, Rest =:= []),
"\r\n",
Body],
ok = SockFun({send, Request}),
case Method of
'GET' ->
{ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv);
'POST' ->
{ok, {http_response, {1,1}, 201, "Created"}} = SockFun(recv)
end,
ok = SockFun({setopts, [{packet, httph}]}),
{ok, {http_header, _, 'Server', _, "MochiWeb" ++ _}} = SockFun(recv),
{ok, {http_header, _, 'Date', _, _}} = SockFun(recv),
{ok, {http_header, _, 'Content-Type', _, _}} = SockFun(recv),
{ok, {http_header, _, 'Content-Length', _, ConLenStr}} = SockFun(recv),
ContentLength = list_to_integer(ConLenStr),
{ok, http_eoh} = SockFun(recv),
ok = SockFun({setopts, [{packet, raw}]}),
{payload, ExReply} = {payload, drain_reply(SockFun, ContentLength, <<>>)},
ok = SockFun({setopts, [{packet, http}]}),
client_request(SockFun, Method, Rest).
client_headers(Body, IsLastRequest) ->
["Host: localhost\r\n",
case Body of
<<>> ->
"";
_ ->
["Content-Type: application/octet-stream\r\n",
"Content-Length: ", integer_to_list(byte_size(Body)), "\r\n"]
end,
case IsLastRequest of
true ->
"Connection: close\r\n";
false ->
""
end].
drain_reply(_SockFun, 0, Acc) ->
Acc;
drain_reply(SockFun, Length, Acc) ->
Sz = erlang:min(Length, 1024),
{ok, B} = SockFun({recv, Sz}),
drain_reply(SockFun, Length - Sz, <<Acc/bytes, B/bytes>>).
-endif.