blob: 23a475270069435b94ee5bfc04661dc30b4e7690 [file] [log] [blame]
%% @author Bob Ippolito <bob@mochimedia.com>
%% @copyright 2007 Mochi Media, Inc.
%% @doc HTTP server.
-module(mochiweb_http).
-author('bob@mochimedia.com').
-export([start/0, start/1, stop/0, stop/1]).
-export([loop/2, default_body/1]).
-export([after_response/2, reentry/1]).
-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(MAX_HEADERS, 1000).
-define(DEFAULTS, [{name, ?MODULE},
{port, 8888}]).
parse_options(Options) ->
{loop, HttpLoop} = proplists:lookup(loop, Options),
Loop = fun (S) ->
?MODULE:loop(S, HttpLoop)
end,
Options1 = [{loop, Loop} | proplists:delete(loop, Options)],
mochilists:set_defaults(?DEFAULTS, Options1).
stop() ->
mochiweb_socket_server:stop(?MODULE).
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}
%% @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.
%% The proplist is as follows: [{name, Name}, {port, Port}, {active_sockets, ActiveSockets}, {timing, Timing}].
%% @end
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)).
loop(Socket, Body) ->
mochiweb_socket:setopts(Socket, [{packet, http}]),
request(Socket, Body).
request(Socket, Body) ->
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}]),
headers(Socket, {Method, Path, Version}, [], Body, 0);
{Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl ->
request(Socket, Body);
{Protocol, _, {http_error, "\n"}} when Protocol == http orelse Protocol == ssl ->
request(Socket, Body);
{tcp_closed, _} ->
mochiweb_socket:close(Socket),
exit(normal);
_Other ->
handle_invalid_request(Socket)
after ?REQUEST_RECV_TIMEOUT ->
mochiweb_socket:close(Socket),
exit(normal)
end.
reentry(Body) ->
fun (Req) ->
?MODULE:after_response(Body, Req)
end.
headers(Socket, Request, Headers, _Body, ?MAX_HEADERS) ->
%% Too many headers sent, bad request.
mochiweb_socket:setopts(Socket, [{packet, raw}]),
handle_invalid_request(Socket, Request, Headers);
headers(Socket, Request, Headers, Body, HeaderCount) ->
mochiweb_socket:setopts(Socket, [{active, once}]),
receive
{Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl ->
Req = new_request(Socket, Request, Headers),
call_body(Body, Req),
?MODULE:after_response(Body, Req);
{Protocol, _, {http_header, _, Name, _, Value}} when Protocol == http orelse Protocol == ssl ->
headers(Socket, Request, [{Name, Value} | Headers], Body,
1 + HeaderCount);
{tcp_closed, _} ->
mochiweb_socket:close(Socket),
exit(normal);
_Other ->
handle_invalid_request(Socket, Request, Headers)
after ?HEADERS_RECV_TIMEOUT ->
mochiweb_socket:close(Socket),
exit(normal)
end.
call_body({M, F}, Req) ->
M:F(Req);
call_body(Body, Req) ->
Body(Req).
handle_invalid_request(Socket) ->
handle_invalid_request(Socket, {'GET', {abs_path, "/"}, {0,9}}, []).
handle_invalid_request(Socket, Request, RevHeaders) ->
Req = new_request(Socket, Request, RevHeaders),
Req:respond({400, [], []}),
mochiweb_socket:close(Socket),
exit(normal).
new_request(Socket, Request, RevHeaders) ->
mochiweb_socket:setopts(Socket, [{packet, raw}]),
mochiweb:new_request({Socket, Request, lists:reverse(RevHeaders)}).
after_response(Body, Req) ->
Socket = Req:get(socket),
case Req:should_close() of
true ->
mochiweb_socket:close(Socket),
exit(normal);
false ->
Req:cleanup(),
?MODULE:loop(Socket, Body)
end.
parse_range_request("bytes=0-") ->
undefined;
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.
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.
%%
%% Tests
%%
-include_lib("eunit/include/eunit.hrl").
-ifdef(TEST).
range_test() ->
%% valid, single ranges
?assertEqual([{20, 30}], parse_range_request("bytes=20-30")),
?assertEqual([{20, none}], parse_range_request("bytes=20-")),
?assertEqual([{none, 20}], parse_range_request("bytes=-20")),
%% trivial single range
?assertEqual(undefined, parse_range_request("bytes=0-")),
%% invalid, single ranges
?assertEqual(fail, parse_range_request("")),
?assertEqual(fail, parse_range_request("garbage")),
?assertEqual(fail, parse_range_request("bytes=-20-30")),
%% valid, multiple range
?assertEqual(
[{20, 30}, {50, 100}, {110, 200}],
parse_range_request("bytes=20-30,50-100,110-200")),
?assertEqual(
[{20, none}, {50, 100}, {none, 200}],
parse_range_request("bytes=20-,50-100,-200")),
%% no ranges
?assertEqual([], parse_range_request("bytes=")),
ok.
range_skip_length_test() ->
Body = <<"012345678901234567890123456789012345678901234567890123456789">>,
BodySize = byte_size(Body), %% 60
BodySize = 60,
%% these values assume BodySize =:= 60
?assertEqual({1,9}, range_skip_length({1,9}, BodySize)), %% 1-9
?assertEqual({10,10}, range_skip_length({10,19}, BodySize)), %% 10-19
?assertEqual({40, 20}, range_skip_length({none, 20}, BodySize)), %% -20
?assertEqual({30, 30}, range_skip_length({30, none}, BodySize)), %% 30-
%% valid edge cases for range_skip_length
?assertEqual({BodySize, 0}, range_skip_length({none, 0}, BodySize)),
?assertEqual({0, BodySize}, range_skip_length({none, BodySize}, BodySize)),
?assertEqual({0, BodySize}, range_skip_length({0, none}, BodySize)),
BodySizeLess1 = BodySize - 1,
?assertEqual({BodySizeLess1, 1},
range_skip_length({BodySize - 1, none}, 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)),
%% 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)),
?assertEqual(invalid_range,
range_skip_length({-1, none}, BodySize)),
?assertEqual(invalid_range,
range_skip_length({BodySize, none}, BodySize)),
ok.
-endif.