summaryrefslogtreecommitdiff
path: root/src/mochiweb/mochiweb_request.erl
diff options
context:
space:
mode:
authorRobert Newson <rnewson@apache.org>2010-07-26 17:21:30 +0000
committerRobert Newson <rnewson@apache.org>2010-07-26 17:21:30 +0000
commit4b0948ddb3a428f8a5330e05745b2fbd4ccf9375 (patch)
tree5ab1dde286028653d5569ceae6dfc883fa365b7a /src/mochiweb/mochiweb_request.erl
parentcd214b23e8129868d4a7020ddafd55a16e496652 (diff)
Add SSL support to CouchDB.
To enable SSL you need to do three things; 1) enable the httpsd daemon in local.ini (you can just uncomment the line). 2) supply your PEM-encoded cert and key files in the [ssl] section. 3) start CouchDB. CouchDB will now, in addition to handling HTTP on port 5984, accept SSL connections on port 6984. The patch itself adds SSL support by updating the local version of Mochiweb to the latest. The upstream release includes our local tweak to support large numbers and to handle Accept-Encoding headers. Our local Mochiweb fork changed the default idle timeout from 10 seconds to 5 minutes, and it was agreed on #irc to revert this change. The only tweaks to Mochiweb were in mochiweb.app.src (to record the git commit I built from) and the removal of Makefile (replaced by Makefile.am). Futon received many tweaks as we have 'http://' hardcoded all over. All such instances now use window.location.protocol + '//'. CouchDB received a tweak to use the right scheme in couch_httpd:absolute_uri (it now gets it from the Mochireq and not mochiweb_socket_server). git-svn-id: https://svn.apache.org/repos/asf/couchdb/trunk@979368 13f79535-47bb-0310-9956-ffa450edef68
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.