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.
|