%% @author Bob Ippolito %% @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) -> ["" "
" "" "" "
" "
" "
" "" "" "" "
" "
", Body, "
" ""]. default_body(Req, M, "/chunked") when M =:= 'GET'; M =:= 'HEAD' -> Res = Req:ok({"text/plain", [], chunked}), Res:write_chunk("First chunk\r\n"), timer:sleep(5000), Res:write_chunk("Last chunk\r\n"), Res:write_chunk(""); default_body(Req, M, _Path) when M =:= 'GET'; M =:= 'HEAD' -> Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()}, {parse_cookie, Req:parse_cookie()}, Req:dump()]]), Req:ok({"text/html", [mochiweb_cookies:cookie("mochiweb_http", "test_cookie")], frm(Body)}); default_body(Req, 'POST', "/multipart") -> Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()}, {parse_cookie, Req:parse_cookie()}, {body, Req:recv_body()}, Req:dump()]]), Req:ok({"text/html", [], frm(Body)}); default_body(Req, 'POST', _Path) -> Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()}, {parse_cookie, Req:parse_cookie()}, {parse_post, Req:parse_post()}, Req:dump()]]), Req:ok({"text/html", [], frm(Body)}); default_body(Req, _Method, _Path) -> Req:respond({501, [], []}). default_body(Req) -> default_body(Req, Req:get(method), Req:get(path)). 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.