/src/mochiweb_http.erl
Erlang | 321 lines | 237 code | 36 blank | 48 comment | 12 complexity | cbd2f1bc3f8dcc2d7e736887936b5388 MD5 | raw file
Possible License(s): MIT
- %% @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) ->
- mochiweb_socket_server:start(parse_options(Options)).
- start_link(Options) ->
- mochiweb_socket_server:start_link(parse_options(Options)).
- 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);
- Msg = {ProtocolErr, _Socket, emsgsize} when
- ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error ->
- handle_invalid_msg_request(Msg, Socket);
- {ProtocolErr, _Socket, _Reason} when
- ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error ->
- mochiweb_socket:close(Socket),
- exit(normal);
- Other ->
- handle_invalid_msg_request(Other, Socket, Opts)
- 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);
- Msg = {ProtocolErr, _Socket, emsgsize} when
- ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error ->
- handle_invalid_msg_request(Msg, Socket);
- Msg = {ProtocolErr, _Socket, _Reason} when
- ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error ->
- error_logger:warning_msg("Got unexpected TCP error message: ~w (to pid=~w)~n",
- [Msg, self()]),
- mochiweb_socket:close(Socket),
- exit(normal);
- 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()) -> no_return().
- handle_invalid_msg_request(Msg, Socket, Opts) ->
- handle_invalid_msg_request(Msg, Socket, Opts, {'GET', {abs_path, "/"}, {0,9}}, []).
- -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 0 =< Start, Start =< End, End < Size ->
- {Start, End - Start + 1};
- {Start, End} when 0 =< Start, Start < Size, Start =< End ->
- {Start, 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.