1
%% @author Bob Ippolito <bob@mochimedia.com>
2
%% @copyright 2007 Mochi Media, Inc.
4
%% @doc MochiWeb HTTP Request abstraction.
6
-module(mochiweb_request).
7
-author('bob@mochimedia.com').
9
-include_lib("kernel/include/file.hrl").
10
-include("internal.hrl").
12
-define(QUIP, "Any of you quaids got a smint?").
15
-export([get_header_value/2, get_primary_header_value/2, get_combined_header_value/2, get/2, dump/1]).
16
-export([send/2, recv/2, recv/3, recv_body/1, recv_body/2, stream_body/4]).
17
-export([start_response/2, start_response_length/2, start_raw_response/2]).
18
-export([respond/2, ok/2]).
19
-export([not_found/1, not_found/2]).
20
-export([parse_post/1, parse_qs/1]).
21
-export([should_close/1, cleanup/1]).
22
-export([parse_cookie/1, get_cookie_value/2]).
23
-export([serve_file/3, serve_file/4]).
24
-export([accepted_encodings/2]).
25
-export([accepts_content_type/2, accepted_content_types/2]).
27
-define(SAVE_QS, mochiweb_request_qs).
28
-define(SAVE_PATH, mochiweb_request_path).
29
-define(SAVE_RECV, mochiweb_request_recv).
30
-define(SAVE_BODY, mochiweb_request_body).
31
-define(SAVE_BODY_LENGTH, mochiweb_request_body_length).
32
-define(SAVE_POST, mochiweb_request_post).
33
-define(SAVE_COOKIE, mochiweb_request_cookie).
34
-define(SAVE_FORCE_CLOSE, mochiweb_request_force_close).
36
%% @type key() = atom() | string() | binary()
37
%% @type value() = atom() | string() | binary() | integer()
38
%% @type headers(). A mochiweb_headers structure.
39
%% @type request(). A mochiweb_request parameterized module instance.
40
%% @type response(). A mochiweb_response parameterized module instance.
41
%% @type ioheaders() = headers() | [{key(), value()}].
43
% 5 minute default idle timeout
44
-define(IDLE_TIMEOUT, 300000).
46
% Maximum recv_body() length of 1MB
47
-define(MAX_RECV_BODY, (1024*1024)).
49
%% @spec new(Socket, Method, RawPath, Version, headers()) -> request()
50
%% @doc Create a new request instance.
51
new(Socket, Method, RawPath, Version, Headers) ->
52
{?MODULE, [Socket, Method, RawPath, Version, Headers]}.
54
%% @spec get_header_value(K, request()) -> undefined | Value
55
%% @doc Get the value of a given request header.
56
get_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
57
mochiweb_headers:get_value(K, Headers).
59
get_primary_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
60
mochiweb_headers:get_primary_value(K, Headers).
62
get_combined_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
63
mochiweb_headers:get_combined_value(K, Headers).
65
%% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range
67
%% @spec get(field(), request()) -> term()
68
%% @doc Return the internal representation of the given field. If
69
%% <code>socket</code> is requested on a HTTPS connection, then
70
%% an ssl socket will be returned as <code>{ssl, SslSocket}</code>.
71
%% You can use <code>SslSocket</code> with the <code>ssl</code>
72
%% application, eg: <code>ssl:peercert(SslSocket)</code>.
73
get(socket, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
75
get(scheme, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
76
case mochiweb_socket:type(Socket) of
82
get(method, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}) ->
84
get(raw_path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
86
get(version, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}) ->
88
get(headers, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
90
get(peer, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
91
case mochiweb_socket:peername(Socket) of
92
{ok, {Addr={10, _, _, _}, _Port}} ->
93
case get_header_value("x-forwarded-for", THIS) of
95
inet_parse:ntoa(Addr);
97
string:strip(lists:last(string:tokens(Hosts, ",")))
99
{ok, {{127, 0, 0, 1}, _Port}} ->
100
case get_header_value("x-forwarded-for", THIS) of
104
string:strip(lists:last(string:tokens(Hosts, ",")))
106
{ok, {Addr, _Port}} ->
107
inet_parse:ntoa(Addr);
111
get(path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
112
case erlang:get(?SAVE_PATH) of
114
{Path0, _, _} = mochiweb_util:urlsplit_path(RawPath),
115
Path = mochiweb_util:unquote(Path0),
116
put(?SAVE_PATH, Path),
121
get(body_length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
122
case erlang:get(?SAVE_BODY_LENGTH) of
124
BodyLength = body_length(THIS),
125
put(?SAVE_BODY_LENGTH, {cached, BodyLength}),
130
get(range, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
131
case get_header_value(range, THIS) of
135
mochiweb_http:parse_range_request(RawRange)
138
%% @spec dump(request()) -> {mochiweb_request, [{atom(), term()}]}
139
%% @doc Dump the internal representation to a "human readable" set of terms
140
%% for debugging/inspection purposes.
141
dump({?MODULE, [_Socket, Method, RawPath, Version, Headers]}) ->
142
{?MODULE, [{method, Method},
145
{headers, mochiweb_headers:to_list(Headers)}]}.
147
%% @spec send(iodata(), request()) -> ok
148
%% @doc Send data over the socket.
149
send(Data, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
150
case mochiweb_socket:send(Socket, Data) of
157
%% @spec recv(integer(), request()) -> binary()
158
%% @doc Receive Length bytes from the client as a binary, with the default
160
recv(Length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
161
recv(Length, ?IDLE_TIMEOUT, THIS).
163
%% @spec recv(integer(), integer(), request()) -> binary()
164
%% @doc Receive Length bytes from the client as a binary, with the given
166
recv(Length, Timeout, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
167
case mochiweb_socket:recv(Socket, Length, Timeout) of
169
put(?SAVE_RECV, true),
175
%% @spec body_length(request()) -> undefined | chunked | unknown_transfer_encoding | integer()
176
%% @doc Infer body length from transfer-encoding and content-length headers.
177
body_length({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
178
case get_header_value("transfer-encoding", THIS) of
180
case get_combined_header_value("content-length", THIS) of
184
list_to_integer(Length)
189
{unknown_transfer_encoding, Unknown}
193
%% @spec recv_body(request()) -> binary()
194
%% @doc Receive the body of the HTTP request (defined by Content-Length).
195
%% Will only receive up to the default max-body length of 1MB.
196
recv_body({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
197
recv_body(?MAX_RECV_BODY, THIS).
199
%% @spec recv_body(integer(), request()) -> binary()
200
%% @doc Receive the body of the HTTP request (defined by Content-Length).
201
%% Will receive up to MaxBody bytes.
202
recv_body(MaxBody, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
203
case erlang:get(?SAVE_BODY) of
205
% we could use a sane constant for max chunk size
206
Body = stream_body(?MAX_RECV_BODY, fun
207
({0, _ChunkedFooter}, {_LengthAcc, BinAcc}) ->
208
iolist_to_binary(lists:reverse(BinAcc));
209
({Length, Bin}, {LengthAcc, BinAcc}) ->
210
NewLength = Length + LengthAcc,
211
if NewLength > MaxBody ->
212
exit({body_too_large, chunked});
214
{NewLength, [Bin | BinAcc]}
216
end, {0, []}, MaxBody, THIS),
217
put(?SAVE_BODY, Body),
222
stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE,[_Socket,_Method,_RawPath,_Version,_Headers]}=THIS) ->
223
stream_body(MaxChunkSize, ChunkFun, FunState, undefined, THIS).
225
stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength,
226
{?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
227
Expect = case get_header_value("expect", THIS) of
230
Value when is_list(Value) ->
231
string:to_lower(Value)
235
_ = start_raw_response({100, gb_trees:empty()}, THIS),
240
case body_length(THIS) of
243
{unknown_transfer_encoding, Unknown} ->
244
exit({unknown_transfer_encoding, Unknown});
246
% In this case the MaxBody is actually used to
247
% determine the maximum allowed size of a single
249
stream_chunked_body(MaxChunkSize, ChunkFun, FunState, THIS);
252
Length when is_integer(Length) ->
253
case MaxBodyLength of
254
MaxBodyLength when is_integer(MaxBodyLength), MaxBodyLength < Length ->
255
exit({body_too_large, content_length});
257
stream_unchunked_body(Length, ChunkFun, FunState, THIS)
262
%% @spec start_response({integer(), ioheaders()}, request()) -> response()
263
%% @doc Start the HTTP response by sending the Code HTTP response and
264
%% ResponseHeaders. The server will set header defaults such as Server
265
%% and Date if not present in ResponseHeaders.
266
start_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
267
HResponse = mochiweb_headers:make(ResponseHeaders),
268
HResponse1 = mochiweb_headers:default_from_list(server_headers(),
270
start_raw_response({Code, HResponse1}, THIS).
272
%% @spec start_raw_response({integer(), headers()}, request()) -> response()
273
%% @doc Start the HTTP response by sending the Code HTTP response and
275
start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) ->
276
F = fun ({K, V}, Acc) ->
277
[mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
279
End = lists:foldl(F, [<<"\r\n">>],
280
mochiweb_headers:to_list(ResponseHeaders)),
281
send([make_version(Version), make_code(Code), <<"\r\n">> | End], THIS),
282
mochiweb:new_response({THIS, Code, ResponseHeaders}).
285
%% @spec start_response_length({integer(), ioheaders(), integer()}, request()) -> response()
286
%% @doc Start the HTTP response by sending the Code HTTP response and
287
%% ResponseHeaders including a Content-Length of Length. The server
288
%% will set header defaults such as Server
289
%% and Date if not present in ResponseHeaders.
290
start_response_length({Code, ResponseHeaders, Length},
291
{?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
292
HResponse = mochiweb_headers:make(ResponseHeaders),
293
HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse),
294
start_response({Code, HResponse1}, THIS).
296
%% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}, request()) -> response()
297
%% @doc Start the HTTP response with start_response, and send Body to the
298
%% client (if the get(method) /= 'HEAD'). The Content-Length header
299
%% will be set by the Body length, and the server will insert header
301
respond({Code, ResponseHeaders, {file, IoDevice}},
302
{?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) ->
303
Length = mochiweb_io:iodevice_size(IoDevice),
304
Response = start_response_length({Code, ResponseHeaders, Length}, THIS),
309
mochiweb_io:iodevice_stream(
310
fun (Body) -> send(Body, THIS) end,
314
respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, Method, _RawPath, Version, _Headers]}=THIS) ->
315
HResponse = mochiweb_headers:make(ResponseHeaders),
316
HResponse1 = case Method of
318
%% This is what Google does, http://www.google.com/
319
%% is chunked but HEAD gets Content-Length: 0.
320
%% The RFC is ambiguous so emulating Google is smart.
321
mochiweb_headers:enter("Content-Length", "0",
323
_ when Version >= {1, 1} ->
324
%% Only use chunked encoding for HTTP/1.1
325
mochiweb_headers:enter("Transfer-Encoding", "chunked",
328
%% For pre-1.1 clients we send the data as-is
329
%% without a Content-Length header and without
330
%% chunk delimiters. Since the end of the document
331
%% is now ambiguous we must force a close.
332
put(?SAVE_FORCE_CLOSE, true),
335
start_response({Code, HResponse1}, THIS);
336
respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) ->
337
Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}, THIS),
346
%% @spec not_found(request()) -> response()
347
%% @doc Alias for <code>not_found([])</code>.
348
not_found({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
351
%% @spec not_found(ExtraHeaders, request()) -> response()
352
%% @doc Alias for <code>respond({404, [{"Content-Type", "text/plain"}
353
%% | ExtraHeaders], <<"Not found.">>})</code>.
354
not_found(ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
355
respond({404, [{"Content-Type", "text/plain"} | ExtraHeaders],
356
<<"Not found.">>}, THIS).
358
%% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}, request()) ->
360
%% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}).
361
ok({ContentType, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
362
ok({ContentType, [], Body}, THIS);
363
ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
364
HResponse = mochiweb_headers:make(ResponseHeaders),
365
case THIS:get(range) of
366
X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked ->
367
%% http://code.google.com/p/mochiweb/issues/detail?id=54
368
%% Range header not supported when chunked, return 200 and provide
370
HResponse1 = mochiweb_headers:enter("Content-Type", ContentType,
372
respond({200, HResponse1, Body}, THIS);
374
{PartList, Size} = range_parts(Body, Ranges),
376
[] -> %% no valid ranges
377
HResponse1 = mochiweb_headers:enter("Content-Type",
380
%% could be 416, for now we'll just return 200
381
respond({200, HResponse1, Body}, THIS);
383
{RangeHeaders, RangeBody} =
384
mochiweb_multipart:parts_to_body(PartList, ContentType, Size),
385
HResponse1 = mochiweb_headers:enter_from_list(
386
[{"Accept-Ranges", "bytes"} |
389
respond({206, HResponse1, RangeBody}, THIS)
393
%% @spec should_close(request()) -> bool()
394
%% @doc Return true if the connection must be closed. If false, using
395
%% Keep-Alive should be safe.
396
should_close({?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) ->
397
ForceClose = erlang:get(?SAVE_FORCE_CLOSE) =/= undefined,
398
DidNotRecv = erlang:get(?SAVE_RECV) =:= undefined,
399
ForceClose orelse Version < {1, 0}
401
orelse is_close(get_header_value("connection", THIS))
402
%% HTTP 1.0 requires Connection: Keep-Alive
403
orelse (Version =:= {1, 0}
404
andalso get_header_value("connection", THIS) =/= "Keep-Alive")
405
%% unread data left on the socket, can't safely continue
407
andalso get_combined_header_value("content-length", THIS) =/= undefined
408
andalso list_to_integer(get_combined_header_value("content-length", THIS)) > 0)
410
andalso get_header_value("transfer-encoding", THIS) =:= "chunked").
414
is_close(S=[_C, _L, _O, _S, _E]) ->
415
string:to_lower(S) =:= "close";
419
%% @spec cleanup(request()) -> ok
420
%% @doc Clean up any junk in the process dictionary, required before continuing
421
%% a Keep-Alive request.
422
cleanup({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) ->
423
L = [?SAVE_QS, ?SAVE_PATH, ?SAVE_RECV, ?SAVE_BODY, ?SAVE_BODY_LENGTH,
424
?SAVE_POST, ?SAVE_COOKIE, ?SAVE_FORCE_CLOSE],
425
lists:foreach(fun(K) ->
430
%% @spec parse_qs(request()) -> [{Key::string(), Value::string()}]
431
%% @doc Parse the query string of the URL.
432
parse_qs({?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
433
case erlang:get(?SAVE_QS) of
435
{_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath),
436
Parsed = mochiweb_util:parse_qs(QueryString),
437
put(?SAVE_QS, Parsed),
443
%% @spec get_cookie_value(Key::string, request()) -> string() | undefined
444
%% @doc Get the value of the given cookie.
445
get_cookie_value(Key, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
446
proplists:get_value(Key, parse_cookie(THIS)).
448
%% @spec parse_cookie(request()) -> [{Key::string(), Value::string()}]
449
%% @doc Parse the cookie header.
450
parse_cookie({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
451
case erlang:get(?SAVE_COOKIE) of
453
Cookies = case get_header_value("cookie", THIS) of
457
mochiweb_cookies:parse_cookie(Value)
459
put(?SAVE_COOKIE, Cookies),
465
%% @spec parse_post(request()) -> [{Key::string(), Value::string()}]
466
%% @doc Parse an application/x-www-form-urlencoded form POST. This
467
%% has the side-effect of calling recv_body().
468
parse_post({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
469
case erlang:get(?SAVE_POST) of
471
Parsed = case recv_body(THIS) of
475
case get_primary_header_value("content-type",THIS) of
476
"application/x-www-form-urlencoded" ++ _ ->
477
mochiweb_util:parse_qs(Binary);
482
put(?SAVE_POST, Parsed),
488
%% @spec stream_chunked_body(integer(), fun(), term(), request()) -> term()
489
%% @doc The function is called for each chunk.
490
%% Used internally by read_chunked_body.
491
stream_chunked_body(MaxChunkSize, Fun, FunState,
492
{?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
493
case read_chunk_length(THIS) of
495
Fun({0, read_chunk(0, THIS)}, FunState);
496
Length when Length > MaxChunkSize ->
497
NewState = read_sub_chunks(Length, MaxChunkSize, Fun, FunState, THIS),
498
stream_chunked_body(MaxChunkSize, Fun, NewState, THIS);
500
NewState = Fun({Length, read_chunk(Length, THIS)}, FunState),
501
stream_chunked_body(MaxChunkSize, Fun, NewState, THIS)
504
stream_unchunked_body(0, Fun, FunState, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) ->
505
Fun({0, <<>>}, FunState);
506
stream_unchunked_body(Length, Fun, FunState,
507
{?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > 0 ->
508
PktSize = case Length > ?RECBUF_SIZE of
514
Bin = recv(PktSize, THIS),
515
NewState = Fun({PktSize, Bin}, FunState),
516
stream_unchunked_body(Length - PktSize, Fun, NewState, THIS).
518
%% @spec read_chunk_length(request()) -> integer()
519
%% @doc Read the length of the next HTTP chunk.
520
read_chunk_length({?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
521
ok = mochiweb_socket:setopts(Socket, [{packet, line}]),
522
case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
524
ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
525
Splitter = fun (C) ->
526
C =/= $\r andalso C =/= $\n andalso C =/= $
528
{Hex, _Rest} = lists:splitwith(Splitter, binary_to_list(Header)),
529
mochihex:to_int(Hex);
534
%% @spec read_chunk(integer(), request()) -> Chunk::binary() | [Footer::binary()]
535
%% @doc Read in a HTTP chunk of the given length. If Length is 0, then read the
536
%% HTTP footers (as a list of binaries, since they're nominal).
537
read_chunk(0, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
538
ok = mochiweb_socket:setopts(Socket, [{packet, line}]),
540
case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
544
F1(F1, [Footer | Acc]);
550
ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
551
put(?SAVE_RECV, true),
553
read_chunk(Length, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
554
case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of
555
{ok, <<Chunk:Length/binary, "\r\n">>} ->
561
read_sub_chunks(Length, MaxChunkSize, Fun, FunState,
562
{?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > MaxChunkSize ->
563
Bin = recv(MaxChunkSize, THIS),
564
NewState = Fun({size(Bin), Bin}, FunState),
565
read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState, THIS);
567
read_sub_chunks(Length, _MaxChunkSize, Fun, FunState,
568
{?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
569
Fun({Length, read_chunk(Length, THIS)}, FunState).
571
%% @spec serve_file(Path, DocRoot, request()) -> Response
572
%% @doc Serve a file relative to DocRoot.
573
serve_file(Path, DocRoot, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
574
serve_file(Path, DocRoot, [], THIS).
576
%% @spec serve_file(Path, DocRoot, ExtraHeaders, request()) -> Response
577
%% @doc Serve a file relative to DocRoot.
578
serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
579
case mochiweb_util:safe_relative_path(Path) of
581
not_found(ExtraHeaders, THIS);
583
FullPath = filename:join([DocRoot, RelPath]),
584
case filelib:is_dir(FullPath) of
586
maybe_redirect(RelPath, FullPath, ExtraHeaders, THIS);
588
maybe_serve_file(FullPath, ExtraHeaders, THIS)
594
%% This has the same effect as the DirectoryIndex directive in httpd
595
directory_index(FullPath) ->
596
filename:join([FullPath, "index.html"]).
598
maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
599
maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS);
601
maybe_redirect(RelPath, FullPath, ExtraHeaders,
602
{?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}=THIS) ->
603
case string:right(RelPath, 1) of
605
maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS);
607
Host = mochiweb_headers:get_value("host", Headers),
608
Location = "http://" ++ Host ++ "/" ++ RelPath ++ "/",
609
LocationBin = list_to_binary(Location),
610
MoreHeaders = [{"Location", Location},
611
{"Content-Type", "text/html"} | ExtraHeaders],
612
Top = <<"<!DOCTYPE HTML PUBLIC \"-//IETF//DTD HTML 2.0//EN\">"
614
"<title>301 Moved Permanently</title>"
616
"<h1>Moved Permanently</h1>"
617
"<p>The document has moved <a href=\"">>,
618
Bottom = <<">here</a>.</p></body></html>\n">>,
619
Body = <<Top/binary, LocationBin/binary, Bottom/binary>>,
620
respond({301, MoreHeaders, Body}, THIS)
623
maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
624
case file:read_file_info(File) of
626
LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime),
627
case get_header_value("if-modified-since", THIS) of
629
respond({304, ExtraHeaders, ""}, THIS);
631
case file:open(File, [raw, binary]) of
633
ContentType = mochiweb_util:guess_mime(File),
634
Res = ok({ContentType,
635
[{"last-modified", LastModified}
637
{file, IoDevice}}, THIS),
638
ok = file:close(IoDevice),
641
not_found(ExtraHeaders, THIS)
645
not_found(ExtraHeaders, THIS)
649
[{"Server", "MochiWeb/1.0 (" ++ ?QUIP ++ ")"},
650
{"Date", httpd_util:rfc1123_date()}].
652
make_code(X) when is_integer(X) ->
653
[integer_to_list(X), [" " | httpd_util:reason_phrase(X)]];
654
make_code(Io) when is_list(Io); is_binary(Io) ->
657
make_version({1, 0}) ->
662
range_parts({file, IoDevice}, Ranges) ->
663
Size = mochiweb_io:iodevice_size(IoDevice),
664
F = fun (Spec, Acc) ->
665
case mochiweb_http:range_skip_length(Spec, Size) of
672
LocNums = lists:foldr(F, [], Ranges),
673
{ok, Data} = file:pread(IoDevice, LocNums),
674
Bodies = lists:zipwith(fun ({Skip, Length}, PartialBody) ->
675
{Skip, Skip + Length - 1, PartialBody}
679
range_parts(Body0, Ranges) ->
680
Body = iolist_to_binary(Body0),
682
F = fun(Spec, Acc) ->
683
case mochiweb_http:range_skip_length(Spec, Size) of
687
<<_:Skip/binary, PartialBody:Length/binary, _/binary>> = Body,
688
[{Skip, Skip + Length - 1, PartialBody} | Acc]
691
{lists:foldr(F, [], Ranges), Size}.
693
%% @spec accepted_encodings([encoding()], request()) -> [encoding()] | bad_accept_encoding_value
694
%% @type encoding() = string().
696
%% @doc Returns a list of encodings accepted by a request. Encodings that are
697
%% not supported by the server will not be included in the return list.
698
%% This list is computed from the "Accept-Encoding" header and
699
%% its elements are ordered, descendingly, according to their Q values.
701
%% Section 14.3 of the RFC 2616 (HTTP 1.1) describes the "Accept-Encoding"
702
%% header and the process of determining which server supported encodings
703
%% can be used for encoding the body for the request's response.
707
%% 1) For a missing "Accept-Encoding" header:
708
%% accepted_encodings(["gzip", "identity"]) -> ["identity"]
710
%% 2) For an "Accept-Encoding" header with value "gzip, deflate":
711
%% accepted_encodings(["gzip", "identity"]) -> ["gzip", "identity"]
713
%% 3) For an "Accept-Encoding" header with value "gzip;q=0.5, deflate":
714
%% accepted_encodings(["gzip", "deflate", "identity"]) ->
715
%% ["deflate", "gzip", "identity"]
717
accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
718
AcceptEncodingHeader = case get_header_value("Accept-Encoding", THIS) of
724
case mochiweb_util:parse_qvalues(AcceptEncodingHeader) of
725
invalid_qvalue_string ->
726
bad_accept_encoding_value;
728
mochiweb_util:pick_accepted_encodings(
729
QList, SupportedEncodings, "identity"
733
%% @spec accepts_content_type(string() | binary(), request()) -> boolean() | bad_accept_header
735
%% @doc Determines whether a request accepts a given media type by analyzing its
740
%% 1) For a missing "Accept" header:
741
%% accepts_content_type("application/json") -> true
743
%% 2) For an "Accept" header with value "text/plain, application/*":
744
%% accepts_content_type("application/json") -> true
746
%% 3) For an "Accept" header with value "text/plain, */*; q=0.0":
747
%% accepts_content_type("application/json") -> false
749
%% 4) For an "Accept" header with value "text/plain; q=0.5, */*; q=0.1":
750
%% accepts_content_type("application/json") -> true
752
%% 5) For an "Accept" header with value "text/*; q=0.0, */*":
753
%% accepts_content_type("text/plain") -> false
755
accepts_content_type(ContentType1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
756
ContentType = re:replace(ContentType1, "\\s", "", [global, {return, list}]),
757
AcceptHeader = accept_header(THIS),
758
case mochiweb_util:parse_qvalues(AcceptHeader) of
759
invalid_qvalue_string ->
762
[MainType, _SubType] = string:tokens(ContentType, "/"),
763
SuperType = MainType ++ "/*",
765
fun({"*/*", Q}) when Q > 0.0 ->
767
({Type, Q}) when Q > 0.0 ->
768
Type =:= ContentType orelse Type =:= SuperType;
774
(not lists:member({ContentType, 0.0}, QList)) andalso
775
(not lists:member({SuperType, 0.0}, QList))
778
%% @spec accepted_content_types([string() | binary()], request()) -> [string()] | bad_accept_header
780
%% @doc Filters which of the given media types this request accepts. This filtering
781
%% is performed by analyzing the "Accept" header. The returned list is sorted
782
%% according to the preferences specified in the "Accept" header (higher Q values
783
%% first). If two or more types have the same preference (Q value), they're order
784
%% in the returned list is the same as they're order in the input list.
788
%% 1) For a missing "Accept" header:
789
%% accepted_content_types(["text/html", "application/json"]) ->
790
%% ["text/html", "application/json"]
792
%% 2) For an "Accept" header with value "text/html, application/*":
793
%% accepted_content_types(["application/json", "text/html"]) ->
794
%% ["application/json", "text/html"]
796
%% 3) For an "Accept" header with value "text/html, */*; q=0.0":
797
%% accepted_content_types(["text/html", "application/json"]) ->
800
%% 4) For an "Accept" header with value "text/html; q=0.5, */*; q=0.1":
801
%% accepts_content_types(["application/json", "text/html"]) ->
802
%% ["text/html", "application/json"]
804
accepted_content_types(Types1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
806
fun(T) -> re:replace(T, "\\s", "", [global, {return, list}]) end,
808
AcceptHeader = accept_header(THIS),
809
case mochiweb_util:parse_qvalues(AcceptHeader) of
810
invalid_qvalue_string ->
813
TypesQ = lists:foldr(
815
case proplists:get_value(T, QList) of
817
[MainType, _SubType] = string:tokens(T, "/"),
818
case proplists:get_value(MainType ++ "/*", QList) of
820
case proplists:get_value("*/*", QList) of
821
Q when is_float(Q), Q > 0.0 ->
838
% Note: Stable sort. If 2 types have the same Q value we leave them in the
839
% same order as in the input list.
840
SortFun = fun({Q1, _}, {Q2, _}) -> Q1 >= Q2 end,
841
[Type || {_Q, Type} <- lists:sort(SortFun, TypesQ)]
844
accept_header({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
845
case get_header_value("Accept", THIS) of
856
-include_lib("eunit/include/eunit.hrl").