summaryrefslogtreecommitdiff
path: root/src/mochiweb/mochiweb_request.erl
diff options
context:
space:
mode:
Diffstat (limited to 'src/mochiweb/mochiweb_request.erl')
-rw-r--r--src/mochiweb/mochiweb_request.erl326
1 files changed, 87 insertions, 239 deletions
diff --git a/src/mochiweb/mochiweb_request.erl b/src/mochiweb/mochiweb_request.erl
index 5d7af26b..1cf96160 100644
--- a/src/mochiweb/mochiweb_request.erl
+++ b/src/mochiweb/mochiweb_request.erl
@@ -7,9 +7,9 @@
-author('bob@mochimedia.com').
-include_lib("kernel/include/file.hrl").
+-include("internal.hrl").
-define(QUIP, "Any of you quaids got a smint?").
--define(READ_SIZE, 8192).
-export([get_header_value/1, get_primary_header_value/1, get/1, dump/0]).
-export([send/1, recv/1, recv/2, recv_body/0, recv_body/1, stream_body/3]).
@@ -21,7 +21,6 @@
-export([parse_cookie/0, get_cookie_value/1]).
-export([serve_file/2, serve_file/3]).
-export([accepted_encodings/1]).
--export([test/0]).
-define(SAVE_QS, mochiweb_request_qs).
-define(SAVE_PATH, mochiweb_request_path).
@@ -40,8 +39,8 @@
%% @type response(). A mochiweb_response parameterized module instance.
%% @type ioheaders() = headers() | [{key(), value()}].
-% 5 minute default idle timeout
--define(IDLE_TIMEOUT, 300000).
+% 10 second default idle timeout
+-define(IDLE_TIMEOUT, 10000).
% Maximum recv_body() length of 1MB
-define(MAX_RECV_BODY, (1024*1024)).
@@ -54,12 +53,23 @@ get_header_value(K) ->
get_primary_header_value(K) ->
mochiweb_headers:get_primary_value(K, Headers).
-%% @type field() = socket | method | raw_path | version | headers | peer | path | body_length | range
+%% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range
%% @spec get(field()) -> term()
-%% @doc Return the internal representation of the given field.
+%% @doc Return the internal representation of the given field. If
+%% <code>socket</code> is requested on a HTTPS connection, then
+%% an ssl socket will be returned as <code>{ssl, SslSocket}</code>.
+%% You can use <code>SslSocket</code> with the <code>ssl</code>
+%% application, eg: <code>ssl:peercert(SslSocket)</code>.
get(socket) ->
Socket;
+get(scheme) ->
+ case mochiweb_socket:type(Socket) of
+ plain ->
+ http;
+ ssl ->
+ https
+ end;
get(method) ->
Method;
get(raw_path) ->
@@ -69,7 +79,7 @@ get(version) ->
get(headers) ->
Headers;
get(peer) ->
- case inet:peername(Socket) of
+ case mochiweb_socket:peername(Socket) of
{ok, {Addr={10, _, _, _}, _Port}} ->
case get_header_value("x-forwarded-for") of
undefined ->
@@ -85,7 +95,9 @@ get(peer) ->
string:strip(lists:last(string:tokens(Hosts, ",")))
end;
{ok, {Addr, _Port}} ->
- inet_parse:ntoa(Addr)
+ inet_parse:ntoa(Addr);
+ {error, enotconn} ->
+ exit(normal)
end;
get(path) ->
case erlang:get(?SAVE_PATH) of
@@ -98,13 +110,20 @@ get(path) ->
Cached
end;
get(body_length) ->
- erlang:get(?SAVE_BODY_LENGTH);
+ case erlang:get(?SAVE_BODY_LENGTH) of
+ undefined ->
+ BodyLength = body_length(),
+ put(?SAVE_BODY_LENGTH, {cached, BodyLength}),
+ BodyLength;
+ {cached, Cached} ->
+ Cached
+ end;
get(range) ->
case get_header_value(range) of
undefined ->
undefined;
RawRange ->
- parse_range_request(RawRange)
+ mochiweb_http:parse_range_request(RawRange)
end.
%% @spec dump() -> {mochiweb_request, [{atom(), term()}]}
@@ -119,7 +138,7 @@ dump() ->
%% @spec send(iodata()) -> ok
%% @doc Send data over the socket.
send(Data) ->
- case gen_tcp:send(Socket, Data) of
+ case mochiweb_socket:send(Socket, Data) of
ok ->
ok;
_ ->
@@ -136,7 +155,7 @@ recv(Length) ->
%% @doc Receive Length bytes from the client as a binary, with the given
%% Timeout in msec.
recv(Length, Timeout) ->
- case gen_tcp:recv(Socket, Length, Timeout) of
+ case mochiweb_socket:recv(Socket, Length, Timeout) of
{ok, Data} ->
put(?SAVE_RECV, true),
Data;
@@ -172,20 +191,24 @@ recv_body() ->
%% @doc Receive the body of the HTTP request (defined by Content-Length).
%% Will receive up to MaxBody bytes.
recv_body(MaxBody) ->
- % we could use a sane constant for max chunk size
- Body = stream_body(?MAX_RECV_BODY, fun
- ({0, _ChunkedFooter}, {_LengthAcc, BinAcc}) ->
- iolist_to_binary(lists:reverse(BinAcc));
- ({Length, Bin}, {LengthAcc, BinAcc}) ->
- NewLength = Length + LengthAcc,
- if NewLength > MaxBody ->
- exit({body_too_large, chunked});
- true ->
- {NewLength, [Bin | BinAcc]}
- end
- end, {0, []}, MaxBody),
- put(?SAVE_BODY, Body),
- Body.
+ case erlang:get(?SAVE_BODY) of
+ undefined ->
+ % we could use a sane constant for max chunk size
+ Body = stream_body(?MAX_RECV_BODY, fun
+ ({0, _ChunkedFooter}, {_LengthAcc, BinAcc}) ->
+ iolist_to_binary(lists:reverse(BinAcc));
+ ({Length, Bin}, {LengthAcc, BinAcc}) ->
+ NewLength = Length + LengthAcc,
+ if NewLength > MaxBody ->
+ exit({body_too_large, chunked});
+ true ->
+ {NewLength, [Bin | BinAcc]}
+ end
+ end, {0, []}, MaxBody),
+ put(?SAVE_BODY, Body),
+ Body;
+ Cached -> Cached
+ end.
stream_body(MaxChunkSize, ChunkFun, FunState) ->
stream_body(MaxChunkSize, ChunkFun, FunState, undefined).
@@ -242,7 +265,7 @@ start_response({Code, ResponseHeaders}) ->
%% ResponseHeaders.
start_raw_response({Code, ResponseHeaders}) ->
F = fun ({K, V}, Acc) ->
- [make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
+ [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
end,
End = lists:foldl(F, [<<"\r\n">>],
mochiweb_headers:to_list(ResponseHeaders)),
@@ -266,13 +289,13 @@ start_response_length({Code, ResponseHeaders, Length}) ->
%% will be set by the Body length, and the server will insert header
%% defaults.
respond({Code, ResponseHeaders, {file, IoDevice}}) ->
- Length = iodevice_size(IoDevice),
+ Length = mochiweb_io:iodevice_size(IoDevice),
Response = start_response_length({Code, ResponseHeaders, Length}),
case Method of
'HEAD' ->
ok;
_ ->
- iodevice_stream(IoDevice)
+ mochiweb_io:iodevice_stream(fun send/1, IoDevice)
end,
Response;
respond({Code, ResponseHeaders, chunked}) ->
@@ -327,8 +350,12 @@ ok({ContentType, Body}) ->
ok({ContentType, ResponseHeaders, Body}) ->
HResponse = mochiweb_headers:make(ResponseHeaders),
case THIS:get(range) of
- X when X =:= undefined; X =:= fail ->
- HResponse1 = mochiweb_headers:enter("Content-Type", ContentType, HResponse),
+ X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked ->
+ %% http://code.google.com/p/mochiweb/issues/detail?id=54
+ %% Range header not supported when chunked, return 200 and provide
+ %% full response.
+ HResponse1 = mochiweb_headers:enter("Content-Type", ContentType,
+ HResponse),
respond({200, HResponse1, Body});
Ranges ->
{PartList, Size} = range_parts(Body, Ranges),
@@ -341,7 +368,7 @@ ok({ContentType, ResponseHeaders, Body}) ->
respond({200, HResponse1, Body});
PartList ->
{RangeHeaders, RangeBody} =
- parts_to_body(PartList, ContentType, Size),
+ mochiweb_multipart:parts_to_body(PartList, ContentType, Size),
HResponse1 = mochiweb_headers:enter_from_list(
[{"Accept-Ranges", "bytes"} |
RangeHeaders],
@@ -458,26 +485,23 @@ stream_chunked_body(MaxChunkSize, Fun, FunState) ->
stream_unchunked_body(0, Fun, FunState) ->
Fun({0, <<>>}, FunState);
stream_unchunked_body(Length, Fun, FunState) when Length > 0 ->
- Bin = recv(0),
- BinSize = byte_size(Bin),
- if BinSize > Length ->
- <<OurBody:Length/binary, Extra/binary>> = Bin,
- gen_tcp:unrecv(Socket, Extra),
- NewState = Fun({Length, OurBody}, FunState),
- stream_unchunked_body(0, Fun, NewState);
- true ->
- NewState = Fun({BinSize, Bin}, FunState),
- stream_unchunked_body(Length - BinSize, Fun, NewState)
- end.
-
+ PktSize = case Length > ?RECBUF_SIZE of
+ true ->
+ ?RECBUF_SIZE;
+ false ->
+ Length
+ end,
+ Bin = recv(PktSize),
+ NewState = Fun({PktSize, Bin}, FunState),
+ stream_unchunked_body(Length - PktSize, Fun, NewState).
%% @spec read_chunk_length() -> integer()
%% @doc Read the length of the next HTTP chunk.
read_chunk_length() ->
- inet:setopts(Socket, [{packet, line}]),
- case gen_tcp:recv(Socket, 0, ?IDLE_TIMEOUT) of
+ mochiweb_socket:setopts(Socket, [{packet, line}]),
+ case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
{ok, Header} ->
- inet:setopts(Socket, [{packet, raw}]),
+ mochiweb_socket:setopts(Socket, [{packet, raw}]),
Splitter = fun (C) ->
C =/= $\r andalso C =/= $\n andalso C =/= $
end,
@@ -491,9 +515,9 @@ read_chunk_length() ->
%% @doc Read in a HTTP chunk of the given length. If Length is 0, then read the
%% HTTP footers (as a list of binaries, since they're nominal).
read_chunk(0) ->
- inet:setopts(Socket, [{packet, line}]),
+ mochiweb_socket:setopts(Socket, [{packet, line}]),
F = fun (F1, Acc) ->
- case gen_tcp:recv(Socket, 0, ?IDLE_TIMEOUT) of
+ case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
{ok, <<"\r\n">>} ->
Acc;
{ok, Footer} ->
@@ -503,10 +527,11 @@ read_chunk(0) ->
end
end,
Footers = F(F, []),
- inet:setopts(Socket, [{packet, raw}]),
+ mochiweb_socket:setopts(Socket, [{packet, raw}]),
+ put(?SAVE_RECV, true),
Footers;
read_chunk(Length) ->
- case gen_tcp:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of
+ case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of
{ok, <<Chunk:Length/binary, "\r\n">>} ->
Chunk;
_ ->
@@ -601,13 +626,6 @@ server_headers() ->
[{"Server", "MochiWeb/1.0 (" ++ ?QUIP ++ ")"},
{"Date", httpd_util:rfc1123_date()}].
-make_io(Atom) when is_atom(Atom) ->
- atom_to_list(Atom);
-make_io(Integer) when is_integer(Integer) ->
- integer_to_list(Integer);
-make_io(Io) when is_list(Io); is_binary(Io) ->
- Io.
-
make_code(X) when is_integer(X) ->
[integer_to_list(X), [" " | httpd_util:reason_phrase(X)]];
make_code(Io) when is_list(Io); is_binary(Io) ->
@@ -618,56 +636,10 @@ make_version({1, 0}) ->
make_version(_) ->
<<"HTTP/1.1 ">>.
-iodevice_stream(IoDevice) ->
- case file:read(IoDevice, ?READ_SIZE) of
- eof ->
- ok;
- {ok, Data} ->
- ok = send(Data),
- iodevice_stream(IoDevice)
- end.
-
-
-parts_to_body([{Start, End, Body}], ContentType, Size) ->
- %% return body for a range reponse with a single body
- HeaderList = [{"Content-Type", ContentType},
- {"Content-Range",
- ["bytes ",
- make_io(Start), "-", make_io(End),
- "/", make_io(Size)]}],
- {HeaderList, Body};
-parts_to_body(BodyList, ContentType, Size) when is_list(BodyList) ->
- %% return
- %% header Content-Type: multipart/byteranges; boundary=441934886133bdee4
- %% and multipart body
- Boundary = mochihex:to_hex(crypto:rand_bytes(8)),
- HeaderList = [{"Content-Type",
- ["multipart/byteranges; ",
- "boundary=", Boundary]}],
- MultiPartBody = multipart_body(BodyList, ContentType, Boundary, Size),
-
- {HeaderList, MultiPartBody}.
-
-multipart_body([], _ContentType, Boundary, _Size) ->
- ["--", Boundary, "--\r\n"];
-multipart_body([{Start, End, Body} | BodyList], ContentType, Boundary, Size) ->
- ["--", Boundary, "\r\n",
- "Content-Type: ", ContentType, "\r\n",
- "Content-Range: ",
- "bytes ", make_io(Start), "-", make_io(End),
- "/", make_io(Size), "\r\n\r\n",
- Body, "\r\n"
- | multipart_body(BodyList, ContentType, Boundary, Size)].
-
-iodevice_size(IoDevice) ->
- {ok, Size} = file:position(IoDevice, eof),
- {ok, 0} = file:position(IoDevice, bof),
- Size.
-
range_parts({file, IoDevice}, Ranges) ->
- Size = iodevice_size(IoDevice),
+ Size = mochiweb_io:iodevice_size(IoDevice),
F = fun (Spec, Acc) ->
- case range_skip_length(Spec, Size) of
+ case mochiweb_http:range_skip_length(Spec, Size) of
invalid_range ->
Acc;
V ->
@@ -685,7 +657,7 @@ range_parts(Body0, Ranges) ->
Body = iolist_to_binary(Body0),
Size = size(Body),
F = fun(Spec, Acc) ->
- case range_skip_length(Spec, Size) of
+ case mochiweb_http:range_skip_length(Spec, Size) of
invalid_range ->
Acc;
{Skip, Length} ->
@@ -695,45 +667,8 @@ range_parts(Body0, Ranges) ->
end,
{lists:foldr(F, [], Ranges), Size}.
-range_skip_length(Spec, Size) ->
- case Spec of
- {none, R} when R =< Size, R >= 0 ->
- {Size - R, R};
- {none, _OutOfRange} ->
- {0, Size};
- {R, none} when R >= 0, R < Size ->
- {R, Size - R};
- {_OutOfRange, none} ->
- invalid_range;
- {Start, End} when 0 =< Start, Start =< End, End < Size ->
- {Start, End - Start + 1};
- {_OutOfRange, _End} ->
- invalid_range
- end.
-
-parse_range_request(RawRange) when is_list(RawRange) ->
- try
- "bytes=" ++ RangeString = RawRange,
- Ranges = string:tokens(RangeString, ","),
- lists:map(fun ("-" ++ V) ->
- {none, list_to_integer(V)};
- (R) ->
- case string:tokens(R, "-") of
- [S1, S2] ->
- {list_to_integer(S1), list_to_integer(S2)};
- [S] ->
- {list_to_integer(S), none}
- end
- end,
- Ranges)
- catch
- _:_ ->
- fail
- end.
-
-%% @spec accepted_encodings([encoding()]) -> [encoding()] | error()
-%% @type encoding() -> string()
-%% @type error() -> bad_accept_encoding_value
+%% @spec accepted_encodings([encoding()]) -> [encoding()] | bad_accept_encoding_value
+%% @type encoding() = string().
%%
%% @doc Returns a list of encodings accepted by a request. Encodings that are
%% not supported by the server will not be included in the return list.
@@ -772,96 +707,9 @@ accepted_encodings(SupportedEncodings) ->
)
end.
-test() ->
- ok = test_range(),
- ok.
-
-test_range() ->
- %% valid, single ranges
- io:format("Testing parse_range_request with valid single ranges~n"),
- io:format("1"),
- [{20, 30}] = parse_range_request("bytes=20-30"),
- io:format("2"),
- [{20, none}] = parse_range_request("bytes=20-"),
- io:format("3"),
- [{none, 20}] = parse_range_request("bytes=-20"),
- io:format(".. ok ~n"),
-
- %% invalid, single ranges
- io:format("Testing parse_range_request with invalid ranges~n"),
- io:format("1"),
- fail = parse_range_request(""),
- io:format("2"),
- fail = parse_range_request("garbage"),
- io:format("3"),
- fail = parse_range_request("bytes=-20-30"),
- io:format(".. ok ~n"),
-
- %% valid, multiple range
- io:format("Testing parse_range_request with valid multiple ranges~n"),
- io:format("1"),
- [{20, 30}, {50, 100}, {110, 200}] =
- parse_range_request("bytes=20-30,50-100,110-200"),
- io:format("2"),
- [{20, none}, {50, 100}, {none, 200}] =
- parse_range_request("bytes=20-,50-100,-200"),
- io:format(".. ok~n"),
-
- %% no ranges
- io:format("Testing out parse_range_request with no ranges~n"),
- io:format("1"),
- [] = parse_range_request("bytes="),
- io:format(".. ok~n"),
-
- Body = <<"012345678901234567890123456789012345678901234567890123456789">>,
- BodySize = byte_size(Body), %% 60
- BodySize = 60,
-
- %% these values assume BodySize =:= 60
- io:format("Testing out range_skip_length on valid ranges~n"),
- io:format("1"),
- {1,9} = range_skip_length({1,9}, BodySize), %% 1-9
- io:format("2"),
- {10,10} = range_skip_length({10,19}, BodySize), %% 10-19
- io:format("3"),
- {40, 20} = range_skip_length({none, 20}, BodySize), %% -20
- io:format("4"),
- {30, 30} = range_skip_length({30, none}, BodySize), %% 30-
- io:format(".. ok ~n"),
-
- %% valid edge cases for range_skip_length
- io:format("Testing out range_skip_length on valid edge case ranges~n"),
- io:format("1"),
- {BodySize, 0} = range_skip_length({none, 0}, BodySize),
- io:format("2"),
- {0, BodySize} = range_skip_length({none, BodySize}, BodySize),
- io:format("3"),
- {0, BodySize} = range_skip_length({0, none}, BodySize),
- BodySizeLess1 = BodySize - 1,
- io:format("4"),
- {BodySizeLess1, 1} = range_skip_length({BodySize - 1, none}, BodySize),
-
- %% out of range, return whole thing
- io:format("5"),
- {0, BodySize} = range_skip_length({none, BodySize + 1}, BodySize),
- io:format("6"),
- {0, BodySize} = range_skip_length({none, -1}, BodySize),
- io:format(".. ok ~n"),
-
- %% invalid ranges
- io:format("Testing out range_skip_length on invalid ranges~n"),
- io:format("1"),
- invalid_range = range_skip_length({-1, 30}, BodySize),
- io:format("2"),
- invalid_range = range_skip_length({0, BodySize + 1}, BodySize),
- io:format("3"),
- invalid_range = range_skip_length({-1, BodySize + 1}, BodySize),
- io:format("4"),
- invalid_range = range_skip_length({BodySize, 40}, BodySize),
- io:format("5"),
- invalid_range = range_skip_length({-1, none}, BodySize),
- io:format("6"),
- invalid_range = range_skip_length({BodySize, none}, BodySize),
- io:format(".. ok ~n"),
- ok.
-
+%%
+%% Tests
+%%
+-include_lib("eunit/include/eunit.hrl").
+-ifdef(TEST).
+-endif.