Subversion Repositories SE.SVN

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
12 7u83 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
 
26
-author('bob@mochimedia.com').
27
 
28
-export([start/1, start_link/1, stop/0, stop/1]).
29
 
30
-export([loop/3]).
31
 
32
-export([after_response/2, reentry/1]).
33
 
34
-export([parse_range_request/1, range_skip_length/2]).
35
 
36
-define(REQUEST_RECV_TIMEOUT,
37
	300000).   %% timeout waiting for request line
38
 
39
-define(HEADERS_RECV_TIMEOUT,
40
	30000).    %% timeout waiting for headers
41
 
42
-define(MAX_HEADERS, 1000).
43
 
44
-define(DEFAULTS, [{name, ?MODULE}, {port, 8888}]).
45
 
46
-ifdef(gen_tcp_r15b_workaround).
47
 
48
r15b_workaround() -> false.
49
 
50
-else.
51
 
52
r15b_workaround() -> false.
53
 
54
-endif.
55
 
56
parse_options(Options) ->
57
    {loop, HttpLoop} = proplists:lookup(loop, Options),
58
    Loop = {?MODULE, loop, [HttpLoop]},
59
    Options1 = [{loop, Loop} | proplists:delete(loop,
60
						Options)],
61
    mochilists:set_defaults(?DEFAULTS, Options1).
62
 
63
stop() -> mochiweb_socket_server:stop(?MODULE).
64
 
65
stop(Name) -> mochiweb_socket_server:stop(Name).
66
 
67
%% @spec start(Options) -> ServerRet
68
%%     Options = [option()]
69
%%     Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()}
70
%%              | {nodelay, boolean()} | {acceptor_pool_size, integer()}
71
%%              | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok}
72
%%              | {link, false} | {recbuf, undefined | non_negative_integer()}
73
%% @doc Start a mochiweb server.
74
%%      profile_fun is used to profile accept timing.
75
%%      After each accept, if defined, profile_fun is called with a proplist of a subset of the mochiweb_socket_server state and timing information.
76
%%      The proplist is as follows: [{name, Name}, {port, Port}, {active_sockets, ActiveSockets}, {timing, Timing}].
77
%% @end
78
start(Options) ->
79
    ok = ensure_started(mochiweb_clock),
80
    mochiweb_socket_server:start(parse_options(Options)).
81
 
82
start_link(Options) ->
83
    ok = ensure_started(mochiweb_clock),
84
    mochiweb_socket_server:start_link(parse_options(Options)).
85
 
86
ensure_started(M) when is_atom(M) ->
87
    case M:start() of
88
      {ok, _Pid} -> ok;
89
      {error, {already_started, _Pid}} -> ok
90
    end.
91
 
92
loop(Socket, Opts, Body) ->
93
    ok =
94
	mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket,
95
							       [{packet,
96
								 http}])),
97
    request(Socket, Opts, Body).
98
 
99
request(Socket, Opts, Body) ->
100
    ok =
101
	mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket,
102
							       [{active,
103
								 once}])),
104
    receive
105
      {Protocol, _, {http_request, Method, Path, Version}}
106
	  when Protocol == http orelse Protocol == ssl ->
107
	  ok =
108
	      mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket,
109
								     [{packet,
110
								       httph}])),
111
	  headers(Socket, Opts, {Method, Path, Version}, [], Body,
112
		  0);
113
      {Protocol, _, {http_error, "\r\n"}}
114
	  when Protocol == http orelse Protocol == ssl ->
115
	  request(Socket, Opts, Body);
116
      {Protocol, _, {http_error, "\n"}}
117
	  when Protocol == http orelse Protocol == ssl ->
118
	  request(Socket, Opts, Body);
119
      {tcp_closed, _} ->
120
	  mochiweb_socket:close(Socket), exit(normal);
121
      {tcp_error, _, emsgsize} = Other ->
122
	  handle_invalid_msg_request(Other, Socket, Opts);
123
      {ssl_closed, _} ->
124
	  mochiweb_socket:close(Socket), exit(normal)
125
      after ?REQUEST_RECV_TIMEOUT ->
126
		mochiweb_socket:close(Socket), exit(normal)
127
    end.
128
 
129
reentry(Body) ->
130
    fun (Req) -> (?MODULE):after_response(Body, Req) end.
131
 
132
headers(Socket, Opts, Request, Headers, _Body,
133
	?MAX_HEADERS) ->
134
    %% Too many headers sent, bad request.
135
    ok =
136
	mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket,
137
							       [{packet,
138
								 raw}])),
139
    handle_invalid_request(Socket, Opts, Request, Headers);
140
headers(Socket, Opts, Request, Headers, Body,
141
	HeaderCount) ->
142
    ok =
143
	mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket,
144
							       [{active,
145
								 once}])),
146
    receive
147
      {Protocol, _, http_eoh}
148
	  when Protocol == http orelse Protocol == ssl ->
149
	  Req = new_request(Socket, Opts, Request, Headers),
150
	  call_body(Body, Req),
151
	  (?MODULE):after_response(Body, Req);
152
      {Protocol, _, {http_header, _, Name, _, Value}}
153
	  when Protocol == http orelse Protocol == ssl ->
154
	  headers(Socket, Opts, Request,
155
		  [{Name, Value} | Headers], Body, 1 + HeaderCount);
156
      {tcp_closed, _} ->
157
	  mochiweb_socket:close(Socket), exit(normal);
158
      {tcp_error, _, emsgsize} = Other ->
159
	  handle_invalid_msg_request(Other, Socket, Opts, Request,
160
				     Headers)
161
      after ?HEADERS_RECV_TIMEOUT ->
162
		mochiweb_socket:close(Socket), exit(normal)
163
    end.
164
 
165
call_body({M, F, A}, Req) when is_atom(M) ->
166
    erlang:apply(M, F, [Req | A]);
167
call_body({M, F}, Req) when is_atom(M) -> M:F(Req);
168
call_body(Body, Req) -> Body(Req).
169
 
170
-spec handle_invalid_msg_request(term(), term(),
171
				 term()) -> no_return().
172
 
173
handle_invalid_msg_request(Msg, Socket, Opts) ->
174
    handle_invalid_msg_request(Msg, Socket, Opts,
175
			       {'GET', {abs_path, "/"}, {0, 9}}, []).
176
 
177
-spec handle_invalid_msg_request(term(), term(), term(),
178
				 term(), term()) -> no_return().
179
 
180
handle_invalid_msg_request(Msg, Socket, Opts, Request,
181
			   RevHeaders) ->
182
    case {Msg, r15b_workaround()} of
183
      {{tcp_error, _, emsgsize}, true} ->
184
	  %% R15B02 returns this then closes the socket, so close and exit
185
	  mochiweb_socket:close(Socket),
186
	  exit(normal);
187
      _ ->
188
	  handle_invalid_request(Socket, Opts, Request,
189
				 RevHeaders)
190
    end.
191
 
192
-spec handle_invalid_request(term(), term(), term(),
193
			     term()) -> no_return().
194
 
195
handle_invalid_request(Socket, Opts, Request,
196
		       RevHeaders) ->
197
    {ReqM, _} = Req = new_request(Socket, Opts, Request,
198
				  RevHeaders),
199
    ReqM:respond({400, [], []}, Req),
200
    mochiweb_socket:close(Socket),
201
    exit(normal).
202
 
203
new_request(Socket, Opts, Request, RevHeaders) ->
204
    ok =
205
	mochiweb_socket:exit_if_closed(mochiweb_socket:setopts(Socket,
206
							       [{packet,
207
								 raw}])),
208
    mochiweb:new_request({Socket, Opts, Request,
209
			  lists:reverse(RevHeaders)}).
210
 
211
after_response(Body, {ReqM, _} = Req) ->
212
    Socket = ReqM:get(socket, Req),
213
    case ReqM:should_close(Req) of
214
      true -> mochiweb_socket:close(Socket), exit(normal);
215
      false ->
216
	  ReqM:cleanup(Req),
217
	  erlang:garbage_collect(),
218
	  (?MODULE):loop(Socket, mochiweb_request:get(opts, Req),
219
			 Body)
220
    end.
221
 
222
parse_range_request(RawRange) when is_list(RawRange) ->
223
    try "bytes=" ++ RangeString = RawRange,
224
	RangeTokens = [string:strip(R)
225
		       || R <- string:tokens(RangeString, ",")],
226
	Ranges = [R || R <- RangeTokens, string:len(R) > 0],
227
	[parse_range_request_1(V1) || V1 <- Ranges]
228
    catch
229
      _:_ -> fail
230
    end.
231
 
232
parse_range_request_1("-" ++ V) ->
233
    {none, list_to_integer(V)};
234
parse_range_request_1(R) ->
235
    case string:tokens(R, "-") of
236
      [S1, S2] -> {list_to_integer(S1), list_to_integer(S2)};
237
      [S] -> {list_to_integer(S), none}
238
    end.
239
 
240
range_skip_length(Spec, Size) ->
241
    case Spec of
242
      {none, R} when R =< Size, R >= 0 -> {Size - R, R};
243
      {none, _OutOfRange} -> {0, Size};
244
      {R, none} when R >= 0, R < Size -> {R, Size - R};
245
      {_OutOfRange, none} -> invalid_range;
246
      {Start, End}
247
	  when Start >= 0, Start < Size, Start =< End ->
248
	  {Start, erlang:min(End + 1, Size) - Start};
249
      {_InvalidStart, _InvalidEnd} -> invalid_range
250
    end.
251
 
252
%%
253
%% Tests
254
%%
255
-ifdef(TEST).
256
 
257
-include_lib("eunit/include/eunit.hrl").
258
 
259
range_test() ->
260
    %% valid, single ranges
261
    ?assertEqual([{20, 30}],
262
		 (parse_range_request("bytes=20-30"))),
263
    ?assertEqual([{20, none}],
264
		 (parse_range_request("bytes=20-"))),
265
    ?assertEqual([{none, 20}],
266
		 (parse_range_request("bytes=-20"))),
267
    %% trivial single range
268
    ?assertEqual([{0, none}],
269
		 (parse_range_request("bytes=0-"))),
270
    %% invalid, single ranges
271
    ?assertEqual(fail, (parse_range_request(""))),
272
    ?assertEqual(fail, (parse_range_request("garbage"))),
273
    ?assertEqual(fail,
274
		 (parse_range_request("bytes=-20-30"))),
275
    %% valid, multiple range
276
    ?assertEqual([{20, 30}, {50, 100}, {110, 200}],
277
		 (parse_range_request("bytes=20-30,50-100,110-200"))),
278
    ?assertEqual([{20, none}, {50, 100}, {none, 200}],
279
		 (parse_range_request("bytes=20-,50-100,-200"))),
280
    %% valid, multiple range with whitespace
281
    ?assertEqual([{20, 30}, {50, 100}, {110, 200}],
282
		 (parse_range_request("bytes=20-30, 50-100 , 110-200"))),
283
    %% valid, multiple range with extra commas
284
    ?assertEqual([{20, 30}, {50, 100}, {110, 200}],
285
		 (parse_range_request("bytes=20-30,,50-100,110-200"))),
286
    ?assertEqual([{20, 30}, {50, 100}, {110, 200}],
287
		 (parse_range_request("bytes=20-30, ,50-100,,,110-200"))),
288
    %% no ranges
289
    ?assertEqual([], (parse_range_request("bytes="))),
290
    ok.
291
 
292
range_skip_length_test() ->
293
    Body = <<"012345678901234567890123456789012345678901234"
294
	     "567890123456789">>,
295
    BodySize = byte_size(Body), %% 60
296
    BodySize = 60,
297
    %% these values assume BodySize =:= 60
298
    ?assertEqual({1, 9},
299
		 (range_skip_length({1, 9}, BodySize))), %% 1-9
300
    ?assertEqual({10, 10},
301
		 (range_skip_length({10, 19}, BodySize))), %% 10-19
302
    ?assertEqual({40, 20},
303
		 (range_skip_length({none, 20}, BodySize))), %% -20
304
    ?assertEqual({30, 30},
305
		 (range_skip_length({30, none}, BodySize))), %% 30-
306
    %% valid edge cases for range_skip_length
307
    ?assertEqual({BodySize, 0},
308
		 (range_skip_length({none, 0}, BodySize))),
309
    ?assertEqual({0, BodySize},
310
		 (range_skip_length({none, BodySize}, BodySize))),
311
    ?assertEqual({0, BodySize},
312
		 (range_skip_length({0, none}, BodySize))),
313
    ?assertEqual({0, BodySize},
314
		 (range_skip_length({0, BodySize + 1}, BodySize))),
315
    BodySizeLess1 = BodySize - 1,
316
    ?assertEqual({BodySizeLess1, 1},
317
		 (range_skip_length({BodySize - 1, none}, BodySize))),
318
    ?assertEqual({BodySizeLess1, 1},
319
		 (range_skip_length({BodySize - 1, BodySize + 5},
320
				    BodySize))),
321
    ?assertEqual({BodySizeLess1, 1},
322
		 (range_skip_length({BodySize - 1, BodySize},
323
				    BodySize))),
324
    %% out of range, return whole thing
325
    ?assertEqual({0, BodySize},
326
		 (range_skip_length({none, BodySize + 1}, BodySize))),
327
    ?assertEqual({0, BodySize},
328
		 (range_skip_length({none, -1}, BodySize))),
329
    ?assertEqual({0, BodySize},
330
		 (range_skip_length({0, BodySize + 1}, BodySize))),
331
    %% invalid ranges
332
    ?assertEqual(invalid_range,
333
		 (range_skip_length({-1, 30}, BodySize))),
334
    ?assertEqual(invalid_range,
335
		 (range_skip_length({-1, BodySize + 1}, BodySize))),
336
    ?assertEqual(invalid_range,
337
		 (range_skip_length({BodySize, 40}, BodySize))),
338
    ?assertEqual(invalid_range,
339
		 (range_skip_length({-1, none}, BodySize))),
340
    ?assertEqual(invalid_range,
341
		 (range_skip_length({BodySize, none}, BodySize))),
342
    ?assertEqual(invalid_range,
343
		 (range_skip_length({BodySize + 1, BodySize + 5},
344
				    BodySize))),
345
    ok.
346
 
347
-endif.