PageRenderTime 65ms CodeModel.GetById 15ms app.highlight 45ms RepoModel.GetById 1ms app.codeStats 1ms

/src/mochiweb_http.erl

http://github.com/basho/mochiweb
Erlang | 321 lines | 237 code | 36 blank | 48 comment | 12 complexity | cbd2f1bc3f8dcc2d7e736887936b5388 MD5 | raw file
  1%% @author Bob Ippolito <bob@mochimedia.com>
  2%% @copyright 2007 Mochi Media, Inc.
  3%%
  4%% Permission is hereby granted, free of charge, to any person obtaining a
  5%% copy of this software and associated documentation files (the "Software"),
  6%% to deal in the Software without restriction, including without limitation
  7%% the rights to use, copy, modify, merge, publish, distribute, sublicense,
  8%% and/or sell copies of the Software, and to permit persons to whom the
  9%% Software is furnished to do so, subject to the following conditions:
 10%%
 11%% The above copyright notice and this permission notice shall be included in
 12%% all copies or substantial portions of the Software.
 13%%
 14%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 15%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 16%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
 17%% THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 18%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 19%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 20%% DEALINGS IN THE SOFTWARE.
 21
 22%% @doc HTTP server.
 23
 24-module(mochiweb_http).
 25-author('bob@mochimedia.com').
 26-export([start/1, start_link/1, stop/0, stop/1]).
 27-export([loop/3]).
 28-export([after_response/2, reentry/1]).
 29-export([parse_range_request/1, range_skip_length/2]).
 30
 31-define(REQUEST_RECV_TIMEOUT, 300000).   %% timeout waiting for request line
 32-define(HEADERS_RECV_TIMEOUT, 30000).    %% timeout waiting for headers
 33
 34-define(MAX_HEADERS, 1000).
 35-define(DEFAULTS, [{name, ?MODULE},
 36                   {port, 8888}]).
 37
 38-ifdef(gen_tcp_r15b_workaround).
 39r15b_workaround() -> true.
 40-else.
 41r15b_workaround() -> false.
 42-endif.
 43
 44parse_options(Options) ->
 45    {loop, HttpLoop} = proplists:lookup(loop, Options),
 46    Loop = {?MODULE, loop, [HttpLoop]},
 47    Options1 = [{loop, Loop} | proplists:delete(loop, Options)],
 48    mochilists:set_defaults(?DEFAULTS, Options1).
 49
 50stop() ->
 51    mochiweb_socket_server:stop(?MODULE).
 52
 53stop(Name) ->
 54    mochiweb_socket_server:stop(Name).
 55
 56%% @spec start(Options) -> ServerRet
 57%%     Options = [option()]
 58%%     Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()}
 59%%              | {nodelay, boolean()} | {acceptor_pool_size, integer()}
 60%%              | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok}
 61%%              | {link, false} | {recbuf, undefined | non_negative_integer()}
 62%% @doc Start a mochiweb server.
 63%%      profile_fun is used to profile accept timing.
 64%%      After each accept, if defined, profile_fun is called with a proplist of a subset of the mochiweb_socket_server state and timing information.
 65%%      The proplist is as follows: [{name, Name}, {port, Port}, {active_sockets, ActiveSockets}, {timing, Timing}].
 66%% @end
 67start(Options) ->
 68    mochiweb_socket_server:start(parse_options(Options)).
 69
 70start_link(Options) ->
 71    mochiweb_socket_server:start_link(parse_options(Options)).
 72
 73loop(Socket, Opts, Body) ->
 74    ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, http}])),
 75    request(Socket, Opts, Body).
 76
 77request(Socket, Opts, Body) ->
 78    ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{active, once}])),
 79    receive
 80        {Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl ->
 81            ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, httph}])),
 82            headers(Socket, Opts, {Method, Path, Version}, [], Body, 0);
 83        {Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl ->
 84            request(Socket, Opts, Body);
 85        {Protocol, _, {http_error, "\n"}} when Protocol == http orelse Protocol == ssl ->
 86            request(Socket, Opts, Body);
 87        {tcp_closed, _} ->
 88            mochiweb_socket:close(Socket),
 89            exit(normal);
 90        {ssl_closed, _} ->
 91            mochiweb_socket:close(Socket),
 92            exit(normal);
 93        Msg = {ProtocolErr, _Socket, emsgsize} when
 94              ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error ->
 95            handle_invalid_msg_request(Msg, Socket);
 96        {ProtocolErr, _Socket, _Reason} when
 97              ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error ->
 98            mochiweb_socket:close(Socket),
 99            exit(normal);
100        Other ->
101            handle_invalid_msg_request(Other, Socket, Opts)
102    after ?REQUEST_RECV_TIMEOUT ->
103        mochiweb_socket:close(Socket),
104        exit(normal)
105    end.
106
107reentry(Body) ->
108    fun (Req) ->
109            ?MODULE:after_response(Body, Req)
110    end.
111
112headers(Socket, Opts, Request, Headers, _Body, ?MAX_HEADERS) ->
113    %% Too many headers sent, bad request.
114    ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])),
115    handle_invalid_request(Socket, Opts, Request, Headers);
116headers(Socket, Opts, Request, Headers, Body, HeaderCount) ->
117    ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{active, once}])),
118    receive
119        {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl ->
120            Req = new_request(Socket, Opts, Request, Headers),
121            call_body(Body, Req),
122            ?MODULE:after_response(Body, Req);
123        {Protocol, _, {http_header, _, Name, _, Value}} when Protocol == http orelse Protocol == ssl ->
124            headers(Socket, Opts, Request, [{Name, Value} | Headers], Body,
125                    1 + HeaderCount);
126        {tcp_closed, _} ->
127            mochiweb_socket:close(Socket),
128            exit(normal);
129        Msg = {ProtocolErr, _Socket, emsgsize} when
130              ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error ->
131            handle_invalid_msg_request(Msg, Socket);
132        Msg = {ProtocolErr, _Socket, _Reason} when
133              ProtocolErr =:= tcp_error ; ProtocolErr =:= ssl_error ->
134            error_logger:warning_msg("Got unexpected TCP error message: ~w (to pid=~w)~n",
135                                     [Msg, self()]),
136            mochiweb_socket:close(Socket),
137            exit(normal);
138        Other ->
139            handle_invalid_msg_request(Other, Socket, Opts, Request, Headers)
140    after ?HEADERS_RECV_TIMEOUT ->
141        mochiweb_socket:close(Socket),
142        exit(normal)
143    end.
144
145call_body({M, F, A}, Req) ->
146    erlang:apply(M, F, [Req | A]);
147call_body({M, F}, Req) ->
148    M:F(Req);
149call_body(Body, Req) ->
150    Body(Req).
151
152-spec handle_invalid_msg_request(term(), term(), term()) -> no_return().
153handle_invalid_msg_request(Msg, Socket, Opts) ->
154    handle_invalid_msg_request(Msg, Socket, Opts, {'GET', {abs_path, "/"}, {0,9}}, []).
155
156-spec handle_invalid_msg_request(term(), term(), term(), term(), term()) -> no_return().
157handle_invalid_msg_request(Msg, Socket, Opts, Request, RevHeaders) ->
158    case {Msg, r15b_workaround()} of
159        {{tcp_error,_,emsgsize}, true} ->
160            %% R15B02 returns this then closes the socket, so close and exit
161            mochiweb_socket:close(Socket),
162            exit(normal);
163        _ ->
164            handle_invalid_request(Socket, Opts, Request, RevHeaders)
165    end.
166
167-spec handle_invalid_request(term(), term(), term(), term()) -> no_return().
168handle_invalid_request(Socket, Opts, Request, RevHeaders) ->
169    Req = new_request(Socket, Opts, Request, RevHeaders),
170    Req:respond({400, [], []}),
171    mochiweb_socket:close(Socket),
172    exit(normal).
173
174new_request(Socket, Opts, Request, RevHeaders) ->
175    ok = mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket, [{packet, raw}])),
176    mochiweb:new_request({Socket, Opts, Request, lists:reverse(RevHeaders)}).
177
178after_response(Body, Req) ->
179    Socket = Req:get(socket),
180    case Req:should_close() of
181        true ->
182            mochiweb_socket:close(Socket),
183            exit(normal);
184        false ->
185            Req:cleanup(),
186            erlang:garbage_collect(),
187            ?MODULE:loop(Socket, mochiweb_request:get(opts, Req), Body)
188    end.
189
190parse_range_request(RawRange) when is_list(RawRange) ->
191    try
192        "bytes=" ++ RangeString = RawRange,
193        RangeTokens = [string:strip(R) || R <- string:tokens(RangeString, ",")],
194        Ranges = [R || R <- RangeTokens, string:len(R) > 0],
195        lists:map(fun ("-" ++ V)  ->
196                          {none, list_to_integer(V)};
197                      (R) ->
198                          case string:tokens(R, "-") of
199                              [S1, S2] ->
200                                  {list_to_integer(S1), list_to_integer(S2)};
201                              [S] ->
202                                  {list_to_integer(S), none}
203                          end
204                  end,
205                  Ranges)
206    catch
207        _:_ ->
208            fail
209    end.
210
211range_skip_length(Spec, Size) ->
212    case Spec of
213        {none, R} when R =< Size, R >= 0 ->
214            {Size - R, R};
215        {none, _OutOfRange} ->
216            {0, Size};
217        {R, none} when R >= 0, R < Size ->
218            {R, Size - R};
219        {_OutOfRange, none} ->
220            invalid_range;
221        {Start, End} when 0 =< Start, Start =< End, End < Size ->
222            {Start, End - Start + 1};
223        {Start, End} when 0 =< Start, Start < Size, Start =< End ->
224            {Start, Size - Start};
225        {_InvalidStart, _InvalidEnd} ->
226            invalid_range
227    end.
228
229%%
230%% Tests
231%%
232-ifdef(TEST).
233-include_lib("eunit/include/eunit.hrl").
234
235range_test() ->
236    %% valid, single ranges
237    ?assertEqual([{20, 30}], parse_range_request("bytes=20-30")),
238    ?assertEqual([{20, none}], parse_range_request("bytes=20-")),
239    ?assertEqual([{none, 20}], parse_range_request("bytes=-20")),
240
241    %% trivial single range
242    ?assertEqual([{0, none}], parse_range_request("bytes=0-")),
243
244    %% invalid, single ranges
245    ?assertEqual(fail, parse_range_request("")),
246    ?assertEqual(fail, parse_range_request("garbage")),
247    ?assertEqual(fail, parse_range_request("bytes=-20-30")),
248
249    %% valid, multiple range
250    ?assertEqual(
251       [{20, 30}, {50, 100}, {110, 200}],
252       parse_range_request("bytes=20-30,50-100,110-200")),
253    ?assertEqual(
254       [{20, none}, {50, 100}, {none, 200}],
255       parse_range_request("bytes=20-,50-100,-200")),
256
257    %% valid, multiple range with whitespace
258    ?assertEqual(
259       [{20, 30}, {50, 100}, {110, 200}],
260       parse_range_request("bytes=20-30, 50-100 , 110-200")),
261
262    %% valid, multiple range with extra commas
263    ?assertEqual(
264       [{20, 30}, {50, 100}, {110, 200}],
265       parse_range_request("bytes=20-30,,50-100,110-200")),
266    ?assertEqual(
267       [{20, 30}, {50, 100}, {110, 200}],
268       parse_range_request("bytes=20-30, ,50-100,,,110-200")),
269
270    %% no ranges
271    ?assertEqual([], parse_range_request("bytes=")),
272    ok.
273
274range_skip_length_test() ->
275    Body = <<"012345678901234567890123456789012345678901234567890123456789">>,
276    BodySize = byte_size(Body), %% 60
277    BodySize = 60,
278
279    %% these values assume BodySize =:= 60
280    ?assertEqual({1,9}, range_skip_length({1,9}, BodySize)), %% 1-9
281    ?assertEqual({10,10}, range_skip_length({10,19}, BodySize)), %% 10-19
282    ?assertEqual({40, 20}, range_skip_length({none, 20}, BodySize)), %% -20
283    ?assertEqual({30, 30}, range_skip_length({30, none}, BodySize)), %% 30-
284
285    %% valid edge cases for range_skip_length
286    ?assertEqual({BodySize, 0}, range_skip_length({none, 0}, BodySize)),
287    ?assertEqual({0, BodySize}, range_skip_length({none, BodySize}, BodySize)),
288    ?assertEqual({0, BodySize}, range_skip_length({0, none}, BodySize)),
289    ?assertEqual({0, BodySize}, range_skip_length({0, BodySize + 1}, BodySize)),
290    BodySizeLess1 = BodySize - 1,
291    ?assertEqual({BodySizeLess1, 1},
292                 range_skip_length({BodySize - 1, none}, BodySize)),
293    ?assertEqual({BodySizeLess1, 1},
294                 range_skip_length({BodySize - 1, BodySize+5}, BodySize)),
295    ?assertEqual({BodySizeLess1, 1},
296                 range_skip_length({BodySize - 1, BodySize}, BodySize)),
297
298    %% out of range, return whole thing
299    ?assertEqual({0, BodySize},
300                 range_skip_length({none, BodySize + 1}, BodySize)),
301    ?assertEqual({0, BodySize},
302                 range_skip_length({none, -1}, BodySize)),
303    ?assertEqual({0, BodySize},
304                 range_skip_length({0, BodySize + 1}, BodySize)),
305
306    %% invalid ranges
307    ?assertEqual(invalid_range,
308                 range_skip_length({-1, 30}, BodySize)),
309    ?assertEqual(invalid_range,
310                 range_skip_length({-1, BodySize + 1}, BodySize)),
311    ?assertEqual(invalid_range,
312                 range_skip_length({BodySize, 40}, BodySize)),
313    ?assertEqual(invalid_range,
314                 range_skip_length({-1, none}, BodySize)),
315    ?assertEqual(invalid_range,
316                 range_skip_length({BodySize, none}, BodySize)),
317    ?assertEqual(invalid_range,
318                 range_skip_length({BodySize + 1, BodySize + 5}, BodySize)),
319    ok.
320
321-endif.