| %% @author Bob Ippolito <bob@mochimedia.com> |
| %% @copyright 2007 Mochi Media, Inc. |
| %% |
| %% Permission is hereby granted, free of charge, to any person obtaining a |
| %% copy of this software and associated documentation files (the "Software"), |
| %% to deal in the Software without restriction, including without limitation |
| %% the rights to use, copy, modify, merge, publish, distribute, sublicense, |
| %% and/or sell copies of the Software, and to permit persons to whom the |
| %% Software is furnished to do so, subject to the following conditions: |
| %% |
| %% The above copyright notice and this permission notice shall be included in |
| %% all copies or substantial portions of the Software. |
| %% |
| %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR |
| %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, |
| %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL |
| %% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER |
| %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING |
| %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER |
| %% DEALINGS IN THE SOFTWARE. |
| |
| %% @doc HTTP server. |
| |
| -module(mochiweb_http). |
| -author('bob@mochimedia.com'). |
| -export([start/1, start_link/1, stop/0, stop/1]). |
| -export([loop/3]). |
| -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}]). |
| |
| -ifdef(gen_tcp_r15b_workaround). |
| r15b_workaround() -> true. |
| -else. |
| r15b_workaround() -> false. |
| -endif. |
| |
| parse_options(Options) -> |
| {loop, HttpLoop} = proplists:lookup(loop, Options), |
| Loop = {?MODULE, loop, [HttpLoop]}, |
| 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). |
| |
| %% @spec start(Options) -> ServerRet |
| %% Options = [option()] |
| %% Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()} |
| %% | {nodelay, boolean()} | {acceptor_pool_size, integer()} |
| %% | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok} |
| %% | {link, false} | {recbuf, undefined | non_negative_integer()} |
| %% @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) -> |
| ok = ensure_started(mochiweb_clock), |
| mochiweb_socket_server:start(parse_options(Options)). |
| |
| start_link(Options) -> |
| ok = ensure_started(mochiweb_clock), |
| mochiweb_socket_server:start_link(parse_options(Options)). |
| |
| ensure_started(M) -> |
| case M:start() of |
| {ok, _Pid} -> |
| ok; |
| {error, {already_started, _Pid}} -> |
| ok |
| end. |
| |
| loop(Socket, Opts, Body) -> |
| ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, http}])), |
| request(Socket, Opts, Body). |
| |
| request(Socket, Opts, Body) -> |
| ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{active, once}])), |
| receive |
| {Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl -> |
| ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, httph}])), |
| headers(Socket, Opts, {Method, Path, Version}, [], Body, 0); |
| {Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl -> |
| request(Socket, Opts, Body); |
| {Protocol, _, {http_error, "\n"}} when Protocol == http orelse Protocol == ssl -> |
| request(Socket, Opts, Body); |
| {tcp_closed, _} -> |
| mochiweb_socket:close(Socket), |
| exit(normal); |
| {ssl_closed, _} -> |
| mochiweb_socket:close(Socket), |
| exit(normal) |
| after ?REQUEST_RECV_TIMEOUT -> |
| mochiweb_socket:close(Socket), |
| exit(normal) |
| end. |
| |
| reentry(Body) -> |
| fun (Req) -> |
| ?MODULE:after_response(Body, Req) |
| end. |
| |
| headers(Socket, Opts, Request, Headers, _Body, ?MAX_HEADERS) -> |
| %% Too many headers sent, bad request. |
| ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), |
| handle_invalid_request(Socket, Opts, Request, Headers); |
| headers(Socket, Opts, Request, Headers, Body, HeaderCount) -> |
| ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{active, once}])), |
| receive |
| {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl -> |
| Req = new_request(Socket, Opts, Request, Headers), |
| call_body(Body, Req), |
| ?MODULE:after_response(Body, Req); |
| {Protocol, _, {http_header, _, Name, _, Value}} when Protocol == http orelse Protocol == ssl -> |
| headers(Socket, Opts, Request, [{Name, Value} | Headers], Body, |
| 1 + HeaderCount); |
| {tcp_closed, _} -> |
| mochiweb_socket:close(Socket), |
| exit(normal); |
| {tcp_error, _, emsgsize} = Other -> |
| handle_invalid_msg_request(Other, Socket, Opts, Request, Headers) |
| after ?HEADERS_RECV_TIMEOUT -> |
| mochiweb_socket:close(Socket), |
| exit(normal) |
| end. |
| |
| call_body({M, F, A}, Req) -> |
| erlang:apply(M, F, [Req | A]); |
| call_body({M, F}, Req) -> |
| M:F(Req); |
| call_body(Body, Req) -> |
| Body(Req). |
| |
| -spec handle_invalid_msg_request(term(), term(), term(), term(), term()) -> no_return(). |
| handle_invalid_msg_request(Msg, Socket, Opts, Request, RevHeaders) -> |
| case {Msg, r15b_workaround()} of |
| {{tcp_error,_,emsgsize}, true} -> |
| %% R15B02 returns this then closes the socket, so close and exit |
| mochiweb_socket:close(Socket), |
| exit(normal); |
| _ -> |
| handle_invalid_request(Socket, Opts, Request, RevHeaders) |
| end. |
| |
| -spec handle_invalid_request(term(), term(), term(), term()) -> no_return(). |
| handle_invalid_request(Socket, Opts, Request, RevHeaders) -> |
| Req = new_request(Socket, Opts, Request, RevHeaders), |
| Req:respond({400, [], []}), |
| mochiweb_socket:close(Socket), |
| exit(normal). |
| |
| new_request(Socket, Opts, Request, RevHeaders) -> |
| ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])), |
| mochiweb:new_request({Socket, Opts, 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(), |
| erlang:garbage_collect(), |
| ?MODULE:loop(Socket, mochiweb_request:get(opts, Req), Body) |
| end. |
| |
| parse_range_request(RawRange) when is_list(RawRange) -> |
| try |
| "bytes=" ++ RangeString = RawRange, |
| RangeTokens = [string:strip(R) || R <- string:tokens(RangeString, ",")], |
| Ranges = [R || R <- RangeTokens, string:len(R) > 0], |
| 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 Start >= 0, Start < Size, Start =< End -> |
| {Start, erlang:min(End + 1, Size) - Start}; |
| {_InvalidStart, _InvalidEnd} -> |
| invalid_range |
| end. |
| |
| %% |
| %% Tests |
| %% |
| -ifdef(TEST). |
| -include_lib("eunit/include/eunit.hrl"). |
| |
| 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([{0, none}], 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")), |
| |
| %% valid, multiple range with whitespace |
| ?assertEqual( |
| [{20, 30}, {50, 100}, {110, 200}], |
| parse_range_request("bytes=20-30, 50-100 , 110-200")), |
| |
| %% valid, multiple range with extra commas |
| ?assertEqual( |
| [{20, 30}, {50, 100}, {110, 200}], |
| parse_range_request("bytes=20-30,,50-100,110-200")), |
| ?assertEqual( |
| [{20, 30}, {50, 100}, {110, 200}], |
| parse_range_request("bytes=20-30, ,50-100,,,110-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)), |
| ?assertEqual({0, BodySize}, range_skip_length({0, BodySize + 1}, BodySize)), |
| BodySizeLess1 = BodySize - 1, |
| ?assertEqual({BodySizeLess1, 1}, |
| range_skip_length({BodySize - 1, none}, BodySize)), |
| ?assertEqual({BodySizeLess1, 1}, |
| range_skip_length({BodySize - 1, BodySize+5}, BodySize)), |
| ?assertEqual({BodySizeLess1, 1}, |
| range_skip_length({BodySize - 1, BodySize}, BodySize)), |
| |
| %% out of range, return whole thing |
| ?assertEqual({0, BodySize}, |
| range_skip_length({none, BodySize + 1}, BodySize)), |
| ?assertEqual({0, BodySize}, |
| range_skip_length({none, -1}, BodySize)), |
| ?assertEqual({0, BodySize}, |
| range_skip_length({0, BodySize + 1}, BodySize)), |
| |
| %% invalid ranges |
| ?assertEqual(invalid_range, |
| range_skip_length({-1, 30}, BodySize)), |
| ?assertEqual(invalid_range, |
| range_skip_length({-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)), |
| ?assertEqual(invalid_range, |
| range_skip_length({BodySize + 1, BodySize + 5}, BodySize)), |
| ok. |
| |
| -endif. |