PageRenderTime 72ms CodeModel.GetById 13ms app.highlight 55ms RepoModel.GetById 1ms app.codeStats 0ms

/deps/mochiweb/src/mochiweb_http.erl

https://code.google.com/p/zotonic/
Erlang | 293 lines | 232 code | 35 blank | 26 comment | 10 complexity | 0466baae7175de73ed27b514f07ece73 MD5 | raw file
  1%% @author Bob Ippolito <bob@mochimedia.com>
  2%% @copyright 2007 Mochi Media, Inc.
  3
  4%% @doc HTTP server.
  5
  6-module(mochiweb_http).
  7-author('bob@mochimedia.com').
  8-export([start/0, start/1, stop/0, stop/1]).
  9-export([loop/2, default_body/1]).
 10-export([after_response/2, reentry/1]).
 11-export([parse_range_request/1, range_skip_length/2]).
 12
 13-define(REQUEST_RECV_TIMEOUT, 300000).   % timeout waiting for request line
 14-define(HEADERS_RECV_TIMEOUT, 30000). % timeout waiting for headers
 15
 16-define(MAX_HEADERS, 1000).
 17-define(DEFAULTS, [{name, ?MODULE},
 18                   {port, 8888}]).
 19
 20parse_options(Options) ->
 21    {loop, HttpLoop} = proplists:lookup(loop, Options),
 22    Loop = fun (S) ->
 23                   ?MODULE:loop(S, HttpLoop)
 24           end,
 25    Options1 = [{loop, Loop} | proplists:delete(loop, Options)],
 26    mochilists:set_defaults(?DEFAULTS, Options1).
 27
 28stop() ->
 29    mochiweb_socket_server:stop(?MODULE).
 30
 31stop(Name) ->
 32    mochiweb_socket_server:stop(Name).
 33
 34start() ->
 35    start([{ip, "127.0.0.1"},
 36           {loop, {?MODULE, default_body}}]).
 37
 38%% @spec start(Options) -> ServerRet
 39%%     Options = [option()]
 40%%     Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()}
 41%%              | {nodelay, boolean()} | {acceptor_pool_size, integer()}
 42%%              | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok}
 43%% @doc Start a mochiweb server.
 44%%      profile_fun is used to profile accept timing.
 45%%      After each accept, if defined, profile_fun is called with a proplist of a subset of the mochiweb_socket_server state and timing information.
 46%%      The proplist is as follows: [{name, Name}, {port, Port}, {active_sockets, ActiveSockets}, {timing, Timing}].
 47%% @end
 48start(Options) ->
 49    mochiweb_socket_server:start(parse_options(Options)).
 50
 51frm(Body) ->
 52    ["<html><head></head><body>"
 53     "<form method=\"POST\">"
 54     "<input type=\"hidden\" value=\"message\" name=\"hidden\"/>"
 55     "<input type=\"submit\" value=\"regular POST\">"
 56     "</form>"
 57     "<br />"
 58     "<form method=\"POST\" enctype=\"multipart/form-data\""
 59     " action=\"/multipart\">"
 60     "<input type=\"hidden\" value=\"multipart message\" name=\"hidden\"/>"
 61     "<input type=\"file\" name=\"file\"/>"
 62     "<input type=\"submit\" value=\"multipart POST\" />"
 63     "</form>"
 64     "<pre>", Body, "</pre>"
 65     "</body></html>"].
 66
 67default_body(Req, M, "/chunked") when M =:= 'GET'; M =:= 'HEAD' ->
 68    Res = Req:ok({"text/plain", [], chunked}),
 69    Res:write_chunk("First chunk\r\n"),
 70    timer:sleep(5000),
 71    Res:write_chunk("Last chunk\r\n"),
 72    Res:write_chunk("");
 73default_body(Req, M, _Path) when M =:= 'GET'; M =:= 'HEAD' ->
 74    Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()},
 75                                   {parse_cookie, Req:parse_cookie()},
 76                                   Req:dump()]]),
 77    Req:ok({"text/html",
 78            [mochiweb_cookies:cookie("mochiweb_http", "test_cookie")],
 79            frm(Body)});
 80default_body(Req, 'POST', "/multipart") ->
 81    Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()},
 82                                   {parse_cookie, Req:parse_cookie()},
 83                                   {body, Req:recv_body()},
 84                                   Req:dump()]]),
 85    Req:ok({"text/html", [], frm(Body)});
 86default_body(Req, 'POST', _Path) ->
 87    Body = io_lib:format("~p~n", [[{parse_qs, Req:parse_qs()},
 88                                   {parse_cookie, Req:parse_cookie()},
 89                                   {parse_post, Req:parse_post()},
 90                                   Req:dump()]]),
 91    Req:ok({"text/html", [], frm(Body)});
 92default_body(Req, _Method, _Path) ->
 93    Req:respond({501, [], []}).
 94
 95default_body(Req) ->
 96    default_body(Req, Req:get(method), Req:get(path)).
 97
 98loop(Socket, Body) ->
 99    mochiweb_socket:setopts(Socket, [{packet, http}]),
100    request(Socket, Body).
101
102request(Socket, Body) ->
103    mochiweb_socket:setopts(Socket, [{active, once}]),
104    receive
105        {Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl ->
106            mochiweb_socket:setopts(Socket, [{packet, httph}]),
107            headers(Socket, {Method, Path, Version}, [], Body, 0);
108        {Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl ->
109            request(Socket, Body);
110        {Protocol, _, {http_error, "\n"}} when Protocol == http orelse Protocol == ssl ->
111            request(Socket, Body);
112        {tcp_closed, _} ->
113            mochiweb_socket:close(Socket),
114            exit(normal);
115        _Other ->
116            handle_invalid_request(Socket)
117    after ?REQUEST_RECV_TIMEOUT ->
118        mochiweb_socket:close(Socket),
119        exit(normal)
120    end.
121
122reentry(Body) ->
123    fun (Req) ->
124            ?MODULE:after_response(Body, Req)
125    end.
126
127headers(Socket, Request, Headers, _Body, ?MAX_HEADERS) ->
128    %% Too many headers sent, bad request.
129    mochiweb_socket:setopts(Socket, [{packet, raw}]),
130    handle_invalid_request(Socket, Request, Headers);
131headers(Socket, Request, Headers, Body, HeaderCount) ->
132    mochiweb_socket:setopts(Socket, [{active, once}]),
133    receive
134        {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl ->
135            Req = new_request(Socket, Request, Headers),
136            call_body(Body, Req),
137            ?MODULE:after_response(Body, Req);
138        {Protocol, _, {http_header, _, Name, _, Value}} when Protocol == http orelse Protocol == ssl ->
139            headers(Socket, Request, [{Name, Value} | Headers], Body,
140                    1 + HeaderCount);
141        {tcp_closed, _} ->
142            mochiweb_socket:close(Socket),
143            exit(normal);
144        _Other ->
145            handle_invalid_request(Socket, Request, Headers)
146    after ?HEADERS_RECV_TIMEOUT ->
147        mochiweb_socket:close(Socket),
148        exit(normal)
149    end.
150
151call_body({M, F, A}, Req) ->
152    erlang:apply(M, F, [Req | A]);
153call_body({M, F}, Req) ->
154    M:F(Req);
155call_body(Body, Req) ->
156    Body(Req).
157
158handle_invalid_request(Socket) ->
159    handle_invalid_request(Socket, {'GET', {abs_path, "/"}, {0,9}}, []),
160    exit(normal).
161
162handle_invalid_request(Socket, Request, RevHeaders) ->
163    Req = new_request(Socket, Request, RevHeaders),
164    Req:respond({400, [], []}),
165    mochiweb_socket:close(Socket),
166    exit(normal).
167
168new_request(Socket, Request, RevHeaders) ->
169    mochiweb_socket:setopts(Socket, [{packet, raw}]),
170    mochiweb:new_request({Socket, Request, lists:reverse(RevHeaders)}).
171
172after_response(Body, Req) ->
173    Socket = Req:get(socket),
174    case Req:should_close() of
175        true ->
176            mochiweb_socket:close(Socket),
177            exit(normal);
178        false ->
179            Req:cleanup(),
180            ?MODULE:loop(Socket, Body)
181    end.
182
183parse_range_request("bytes=0-") ->
184    undefined;
185parse_range_request(RawRange) when is_list(RawRange) ->
186    try
187        "bytes=" ++ RangeString = RawRange,
188        Ranges = string:tokens(RangeString, ","),
189        lists:map(fun ("-" ++ V)  ->
190                          {none, list_to_integer(V)};
191                      (R) ->
192                          case string:tokens(R, "-") of
193                              [S1, S2] ->
194                                  {list_to_integer(S1), list_to_integer(S2)};
195                              [S] ->
196                                  {list_to_integer(S), none}
197                          end
198                  end,
199                  Ranges)
200    catch
201        _:_ ->
202            fail
203    end.
204
205range_skip_length(Spec, Size) ->
206    case Spec of
207        {none, R} when R =< Size, R >= 0 ->
208            {Size - R, R};
209        {none, _OutOfRange} ->
210            {0, Size};
211        {R, none} when R >= 0, R < Size ->
212            {R, Size - R};
213        {_OutOfRange, none} ->
214            invalid_range;
215        {Start, End} when 0 =< Start, Start =< End, End < Size ->
216            {Start, End - Start + 1};
217        {_OutOfRange, _End} ->
218            invalid_range
219    end.
220
221%%
222%% Tests
223%%
224-ifdef(TEST).
225-include_lib("eunit/include/eunit.hrl").
226
227range_test() ->
228    %% valid, single ranges
229    ?assertEqual([{20, 30}], parse_range_request("bytes=20-30")),
230    ?assertEqual([{20, none}], parse_range_request("bytes=20-")),
231    ?assertEqual([{none, 20}], parse_range_request("bytes=-20")),
232
233    %% trivial single range
234    ?assertEqual(undefined, parse_range_request("bytes=0-")),
235
236    %% invalid, single ranges
237    ?assertEqual(fail, parse_range_request("")),
238    ?assertEqual(fail, parse_range_request("garbage")),
239    ?assertEqual(fail, parse_range_request("bytes=-20-30")),
240
241    %% valid, multiple range
242    ?assertEqual(
243       [{20, 30}, {50, 100}, {110, 200}],
244       parse_range_request("bytes=20-30,50-100,110-200")),
245    ?assertEqual(
246       [{20, none}, {50, 100}, {none, 200}],
247       parse_range_request("bytes=20-,50-100,-200")),
248
249    %% no ranges
250    ?assertEqual([], parse_range_request("bytes=")),
251    ok.
252
253range_skip_length_test() ->
254    Body = <<"012345678901234567890123456789012345678901234567890123456789">>,
255    BodySize = byte_size(Body), %% 60
256    BodySize = 60,
257
258    %% these values assume BodySize =:= 60
259    ?assertEqual({1,9}, range_skip_length({1,9}, BodySize)), %% 1-9
260    ?assertEqual({10,10}, range_skip_length({10,19}, BodySize)), %% 10-19
261    ?assertEqual({40, 20}, range_skip_length({none, 20}, BodySize)), %% -20
262    ?assertEqual({30, 30}, range_skip_length({30, none}, BodySize)), %% 30-
263
264    %% valid edge cases for range_skip_length
265    ?assertEqual({BodySize, 0}, range_skip_length({none, 0}, BodySize)),
266    ?assertEqual({0, BodySize}, range_skip_length({none, BodySize}, BodySize)),
267    ?assertEqual({0, BodySize}, range_skip_length({0, none}, BodySize)),
268    BodySizeLess1 = BodySize - 1,
269    ?assertEqual({BodySizeLess1, 1},
270                 range_skip_length({BodySize - 1, none}, BodySize)),
271
272    %% out of range, return whole thing
273    ?assertEqual({0, BodySize},
274                 range_skip_length({none, BodySize + 1}, BodySize)),
275    ?assertEqual({0, BodySize},
276                 range_skip_length({none, -1}, BodySize)),
277
278    %% invalid ranges
279    ?assertEqual(invalid_range,
280                 range_skip_length({-1, 30}, BodySize)),
281    ?assertEqual(invalid_range,
282                 range_skip_length({0, BodySize + 1}, BodySize)),
283    ?assertEqual(invalid_range,
284                 range_skip_length({-1, BodySize + 1}, BodySize)),
285    ?assertEqual(invalid_range,
286                 range_skip_length({BodySize, 40}, BodySize)),
287    ?assertEqual(invalid_range,
288                 range_skip_length({-1, none}, BodySize)),
289    ?assertEqual(invalid_range,
290                 range_skip_length({BodySize, none}, BodySize)),
291    ok.
292
293-endif.