Merge tag 'v2.12.0'
diff --git a/.gitignore b/.gitignore
index 8f4edf4..bb59f19 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,6 +6,7 @@
.DS_Store
/TEST-*.xml
/deps
+/.rebar
*.swp
*.beam
*.dump
diff --git a/.travis.yml b/.travis.yml
index 43dad1a..d9c6fd8 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,6 +2,7 @@
notifications:
email: false
otp_release:
- - R15B02
+ - 17.1
+ - 17.0
+ - R16B03-1
- R15B03
- - R16B
diff --git a/CHANGES.md b/CHANGES.md
index 89c8078..24a59d6 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -1,7 +1,66 @@
-Version 2.8.1 released XXXX-XX-XX
+Version 2.12.0 released 2015-01-16
+
+* Send "Connection: close" header when the server is going to close
+ a Keep-Alive connection, usually due to unread data from the
+ client
+ https://github.com/mochi/mochiweb/issues/146
+
+Version 2.11.2 released 2015-01-16
+
+* Fix regression introduced in #147
+ https://github.com/mochi/mochiweb/pull/147
+
+Version 2.11.1 released 2015-01-16
+
+* Accept range end position which exceededs the resource size
+ https://github.com/mochi/mochiweb/pull/147
+
+Version 2.11.0 released 2015-01-12
+
+* Perform SSL handshake after releasing acceptor back into the pool,
+ and slow accept rate when file descriptors are not available,
+ to mitigate a potential DoS attack. Adds new mochiweb_socket
+ functions transport_accept/1 and finish_accept/1 which should be
+ used in preference to the now deprecated accept/1 function.
+ https://github.com/mochi/mochiweb/issues/138
+
+Version 2.10.1 released 2015-01-11
+
+* Fixes issue with SSL and mochiweb_websocket. Note that
+ mochiweb_websocket is still experimental and the API
+ is subject to change in future versions.
+ https://github.com/mochi/mochiweb/pull/144
+
+Version 2.10.0 released 2014-12-17
+
+* Added new `recbuf` option to mochiweb_http to allow the receive
+ buffer to be configured.
+ https://github.com/mochi/mochiweb/pull/134
+
+Version 2.9.2 released 2014-10-16
+
+* Add timeouts to SSL connect to prevent DoS by opening a connection
+ and not doing anything.
+ https://github.com/mochi/mochiweb/pull/140
+* Prevent using ECDH cipher in R16B because it is broken
+ https://github.com/mochi/mochiweb/pull/140
+* For default SSL connections, remove usage of sslv3 and not-so-secure
+ ciphers.
+ https://github.com/mochi/mochiweb/pull/140
+
+Version 2.9.1 released 2014-09-29
+
+* Fix Makefile rule for building docs
+ https://github.com/mochi/mochiweb/issues/135
+* Minimize gen_tcp:send calls to optimize performance.
+ https://github.com/mochi/mochiweb/pull/137
+
+Version 2.9.0 released 2014-06-24
* Increased timeout in test suite for FreeBSD
https://github.com/mochi/mochiweb/pull/121
+* Updated rebar to v2.5.0 and fixed associated build issues
+ https://github.com/mochi/mochiweb/issues/131
Version 2.8.0 released 2014-01-01
diff --git a/Makefile b/Makefile
index b94be4b..33601c7 100644
--- a/Makefile
+++ b/Makefile
@@ -6,25 +6,18 @@
.PHONY: all edoc test clean build_plt dialyzer app
all:
- @$(REBAR) get-deps compile
+ @$(REBAR) prepare-deps
-edoc:
+edoc: all
@$(REBAR) doc
test:
@rm -rf .eunit
@mkdir -p .eunit
- @$(REBAR) skip_deps=true eunit
+ @$(REBAR) eunit
clean:
@$(REBAR) clean
-build_plt:
- @$(REBAR) build-plt
-
-dialyzer:
- @$(REBAR) dialyze
-
app:
- @$(REBAR) create template=mochiwebapp dest=$(DEST) appid=$(PROJECT)
-
+ @$(REBAR) -r create template=mochiwebapp dest=$(DEST) appid=$(PROJECT)
diff --git a/scripts/new_mochiweb.erl b/scripts/new_mochiweb.erl
deleted file mode 100755
index f49ed39..0000000
--- a/scripts/new_mochiweb.erl
+++ /dev/null
@@ -1,23 +0,0 @@
-#!/usr/bin/env escript
-%% -*- mode: erlang -*-
--export([main/1]).
-
-%% External API
-
-main(_) ->
- usage().
-
-%% Internal API
-
-usage() ->
- io:format(
- "new_mochiweb.erl has been replaced by a rebar template!\n"
- "\n"
- "To create a new mochiweb using project:\n"
- " make app PROJECT=project_name\n"
- "\n"
- "To create a new mochiweb using project in a specific directory:\n"
- " make app PROJECT=project_name PREFIX=$HOME/projects/\n"
- "\n"
- ),
- halt(1).
diff --git a/src/mochiweb.app.src b/src/mochiweb.app.src
index b3b3d83..8cb43ac 100644
--- a/src/mochiweb.app.src
+++ b/src/mochiweb.app.src
@@ -1,7 +1,7 @@
%% This is generated from src/mochiweb.app.src
{application, mochiweb,
[{description, "MochiMedia Web Server"},
- {vsn, "2.8.1"},
+ {vsn, "2.12.0"},
{modules, []},
{registered, []},
{env, []},
diff --git a/src/mochiweb.erl b/src/mochiweb.erl
index 927322d..5c4201c 100644
--- a/src/mochiweb.erl
+++ b/src/mochiweb.erl
@@ -51,10 +51,15 @@
uri(HttpString) when is_list(HttpString) ->
HttpString.
-%% @spec new_request({Socket, Request, Headers}) -> MochiWebRequest
+%% @spec new_request( {Socket, Request, Headers}
+%% | {Socket, Opts, Request, Headers} ) -> MochiWebRequest
%% @doc Return a mochiweb_request data structure.
new_request({Socket, {Method, HttpUri, Version}, Headers}) ->
+ new_request({Socket, [], {Method, HttpUri, Version}, Headers});
+
+new_request({Socket, Opts, {Method, HttpUri, Version}, Headers}) ->
mochiweb_request:new(Socket,
+ Opts,
Method,
uri(HttpUri),
Version,
diff --git a/src/mochiweb_acceptor.erl b/src/mochiweb_acceptor.erl
index ebbaf45..8a58fcf 100644
--- a/src/mochiweb_acceptor.erl
+++ b/src/mochiweb_acceptor.erl
@@ -8,24 +8,46 @@
-include("internal.hrl").
--export([start_link/3, init/3]).
+-export([start_link/3, start_link/4, init/4]).
+
+-define(EMFILE_SLEEP_MSEC, 100).
start_link(Server, Listen, Loop) ->
- proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop]).
+ start_link(Server, Listen, Loop, []).
-init(Server, Listen, Loop) ->
+start_link(Server, Listen, Loop, Opts) ->
+ proc_lib:spawn_link(?MODULE, init, [Server, Listen, Loop, Opts]).
+
+do_accept(Server, Listen) ->
T1 = os:timestamp(),
- case catch mochiweb_socket:accept(Listen) of
+ case mochiweb_socket:transport_accept(Listen) of
{ok, Socket} ->
gen_server:cast(Server, {accepted, self(), timer:now_diff(os:timestamp(), T1)}),
- call_loop(Loop, Socket);
- {error, closed} ->
- exit(normal);
- {error, timeout} ->
- init(Server, Listen, Loop);
- {error, esslaccept} ->
+ mochiweb_socket:finish_accept(Socket);
+ Other ->
+ Other
+ end.
+
+init(Server, Listen, Loop, Opts) ->
+ case catch do_accept(Server, Listen) of
+ {ok, Socket} ->
+ call_loop(Loop, Socket, Opts);
+ {error, Err} when Err =:= closed orelse
+ Err =:= esslaccept orelse
+ Err =:= timeout ->
exit(normal);
Other ->
+ %% Mitigate out of file descriptor scenario by sleeping for a
+ %% short time to slow error rate
+ case Other of
+ {error, emfile} ->
+ receive
+ after ?EMFILE_SLEEP_MSEC ->
+ ok
+ end;
+ _ ->
+ ok
+ end,
error_logger:error_report(
[{application, mochiweb},
"Accept failed error",
@@ -33,18 +55,11 @@
exit({error, accept_failed})
end.
-call_loop({M, F}, Socket) ->
- M:F(Socket);
-call_loop({M, F, [A1]}, Socket) ->
- M:F(Socket, A1);
-call_loop({M, F, A}, Socket) ->
- erlang:apply(M, F, [Socket | A]);
-call_loop(Loop, Socket) ->
- Loop(Socket).
-
-%%
-%% Tests
-%%
--ifdef(TEST).
--include_lib("eunit/include/eunit.hrl").
--endif.
+call_loop({M, F}, Socket, Opts) ->
+ M:F(Socket, Opts);
+call_loop({M, F, [A1]}, Socket, Opts) ->
+ M:F(Socket, Opts, A1);
+call_loop({M, F, A}, Socket, Opts) ->
+ erlang:apply(M, F, [Socket, Opts | A]);
+call_loop(Loop, Socket, Opts) ->
+ Loop(Socket, Opts).
diff --git a/src/mochiweb_base64url.erl b/src/mochiweb_base64url.erl
index ab5aaec..5f552e0 100644
--- a/src/mochiweb_base64url.erl
+++ b/src/mochiweb_base64url.erl
@@ -8,13 +8,13 @@
%% '_' is used in place of '/' (63),
%% padding is implicit rather than explicit ('=').
--spec encode(iolist()) -> binary().
+-spec encode(iolist() | binary()) -> binary().
encode(B) when is_binary(B) ->
encode_binary(B);
encode(L) when is_list(L) ->
encode_binary(iolist_to_binary(L)).
--spec decode(iolist()) -> binary().
+-spec decode(iolist() | binary()) -> binary().
decode(B) when is_binary(B) ->
decode_binary(B);
decode(L) when is_list(L) ->
diff --git a/src/mochiweb_http.erl b/src/mochiweb_http.erl
index 38d51d4..1ea1f15 100644
--- a/src/mochiweb_http.erl
+++ b/src/mochiweb_http.erl
@@ -6,7 +6,7 @@
-module(mochiweb_http).
-author('bob@mochimedia.com').
-export([start/1, start_link/1, stop/0, stop/1]).
--export([loop/2]).
+-export([loop/3]).
-export([after_response/2, reentry/1]).
-export([parse_range_request/1, range_skip_length/2]).
@@ -40,7 +40,7 @@
%% Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()}
%% | {nodelay, boolean()} | {acceptor_pool_size, integer()}
%% | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok}
-%% | {link, false}
+%% | {link, false} | {recbuf, non_negative_integer()}
%% @doc Start a mochiweb server.
%% profile_fun is used to profile accept timing.
%% After each accept, if defined, profile_fun is called with a proplist of a subset of the mochiweb_socket_server state and timing information.
@@ -52,20 +52,20 @@
start_link(Options) ->
mochiweb_socket_server:start_link(parse_options(Options)).
-loop(Socket, Body) ->
+loop(Socket, Opts, Body) ->
ok = mochiweb_socket:setopts(Socket, [{packet, http}]),
- request(Socket, Body).
+ request(Socket, Opts, Body).
-request(Socket, Body) ->
+request(Socket, Opts, Body) ->
ok = mochiweb_socket:setopts(Socket, [{active, once}]),
receive
{Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl ->
ok = mochiweb_socket:setopts(Socket, [{packet, httph}]),
- headers(Socket, {Method, Path, Version}, [], Body, 0);
+ headers(Socket, Opts, {Method, Path, Version}, [], Body, 0);
{Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl ->
- request(Socket, Body);
+ request(Socket, Opts, Body);
{Protocol, _, {http_error, "\n"}} when Protocol == http orelse Protocol == ssl ->
- request(Socket, Body);
+ request(Socket, Opts, Body);
{tcp_closed, _} ->
mochiweb_socket:close(Socket),
exit(normal);
@@ -73,7 +73,7 @@
mochiweb_socket:close(Socket),
exit(normal);
Other ->
- handle_invalid_msg_request(Other, Socket)
+ handle_invalid_msg_request(Other, Socket, Opts)
after ?REQUEST_RECV_TIMEOUT ->
mochiweb_socket:close(Socket),
exit(normal)
@@ -84,25 +84,25 @@
?MODULE:after_response(Body, Req)
end.
-headers(Socket, Request, Headers, _Body, ?MAX_HEADERS) ->
+headers(Socket, Opts, Request, Headers, _Body, ?MAX_HEADERS) ->
%% Too many headers sent, bad request.
ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
- handle_invalid_request(Socket, Request, Headers);
-headers(Socket, Request, Headers, Body, HeaderCount) ->
+ handle_invalid_request(Socket, Opts, Request, Headers);
+headers(Socket, Opts, Request, Headers, Body, HeaderCount) ->
ok = mochiweb_socket:setopts(Socket, [{active, once}]),
receive
{Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl ->
- Req = new_request(Socket, Request, Headers),
+ Req = new_request(Socket, Opts, Request, Headers),
call_body(Body, Req),
?MODULE:after_response(Body, Req);
{Protocol, _, {http_header, _, Name, _, Value}} when Protocol == http orelse Protocol == ssl ->
- headers(Socket, Request, [{Name, Value} | Headers], Body,
+ headers(Socket, Opts, Request, [{Name, Value} | Headers], Body,
1 + HeaderCount);
{tcp_closed, _} ->
mochiweb_socket:close(Socket),
exit(normal);
Other ->
- handle_invalid_msg_request(Other, Socket, Request, Headers)
+ handle_invalid_msg_request(Other, Socket, Opts, Request, Headers)
after ?HEADERS_RECV_TIMEOUT ->
mochiweb_socket:close(Socket),
exit(normal)
@@ -115,31 +115,31 @@
call_body(Body, Req) ->
Body(Req).
--spec handle_invalid_msg_request(term(), term()) -> no_return().
-handle_invalid_msg_request(Msg, Socket) ->
- handle_invalid_msg_request(Msg, Socket, {'GET', {abs_path, "/"}, {0,9}}, []).
+-spec handle_invalid_msg_request(term(), term(), term()) -> no_return().
+handle_invalid_msg_request(Msg, Socket, Opts) ->
+ handle_invalid_msg_request(Msg, Socket, Opts, {'GET', {abs_path, "/"}, {0,9}}, []).
--spec handle_invalid_msg_request(term(), term(), term(), term()) -> no_return().
-handle_invalid_msg_request(Msg, Socket, Request, RevHeaders) ->
+-spec handle_invalid_msg_request(term(), term(), term(), term(), term()) -> no_return().
+handle_invalid_msg_request(Msg, Socket, Opts, Request, RevHeaders) ->
case {Msg, r15b_workaround()} of
{{tcp_error,_,emsgsize}, true} ->
%% R15B02 returns this then closes the socket, so close and exit
mochiweb_socket:close(Socket),
exit(normal);
_ ->
- handle_invalid_request(Socket, Request, RevHeaders)
+ handle_invalid_request(Socket, Opts, Request, RevHeaders)
end.
--spec handle_invalid_request(term(), term(), term()) -> no_return().
-handle_invalid_request(Socket, Request, RevHeaders) ->
- Req = new_request(Socket, Request, RevHeaders),
+-spec handle_invalid_request(term(), term(), term(), term()) -> no_return().
+handle_invalid_request(Socket, Opts, Request, RevHeaders) ->
+ Req = new_request(Socket, Opts, Request, RevHeaders),
Req:respond({400, [], []}),
mochiweb_socket:close(Socket),
exit(normal).
-new_request(Socket, Request, RevHeaders) ->
+new_request(Socket, Opts, Request, RevHeaders) ->
ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
- mochiweb:new_request({Socket, Request, lists:reverse(RevHeaders)}).
+ mochiweb:new_request({Socket, Opts, Request, lists:reverse(RevHeaders)}).
after_response(Body, Req) ->
Socket = Req:get(socket),
@@ -150,11 +150,9 @@
false ->
Req:cleanup(),
erlang:garbage_collect(),
- ?MODULE:loop(Socket, Body)
+ ?MODULE:loop(Socket, mochiweb_request:get(opts, Req), Body)
end.
-parse_range_request("bytes=0-") ->
- undefined;
parse_range_request(RawRange) when is_list(RawRange) ->
try
"bytes=" ++ RangeString = RawRange,
@@ -186,11 +184,9 @@
{R, Size - R};
{_OutOfRange, none} ->
invalid_range;
- {Start, End} when 0 =< Start, Start =< End, End < Size ->
- {Start, End - Start + 1};
- {Start, End} when 0 =< Start, Start =< End, End >= Size ->
- {Start, Size - Start};
- {_OutOfRange, _End} ->
+ {Start, End} when Start >= 0, Start < Size, Start =< End ->
+ {Start, erlang:min(End + 1, Size) - Start};
+ {_InvalidStart, _InvalidEnd} ->
invalid_range
end.
@@ -207,7 +203,7 @@
?assertEqual([{none, 20}], parse_range_request("bytes=-20")),
%% trivial single range
- ?assertEqual(undefined, parse_range_request("bytes=0-")),
+ ?assertEqual([{0, none}], parse_range_request("bytes=0-")),
%% invalid, single ranges
?assertEqual(fail, parse_range_request("")),
@@ -254,6 +250,7 @@
?assertEqual({BodySize, 0}, range_skip_length({none, 0}, BodySize)),
?assertEqual({0, BodySize}, range_skip_length({none, BodySize}, BodySize)),
?assertEqual({0, BodySize}, range_skip_length({0, none}, BodySize)),
+ ?assertEqual({0, BodySize}, range_skip_length({0, BodySize + 1}, BodySize)),
BodySizeLess1 = BodySize - 1,
?assertEqual({BodySizeLess1, 1},
range_skip_length({BodySize - 1, none}, BodySize)),
@@ -281,6 +278,8 @@
range_skip_length({-1, none}, BodySize)),
?assertEqual(invalid_range,
range_skip_length({BodySize, none}, BodySize)),
+ ?assertEqual(invalid_range,
+ range_skip_length({BodySize + 1, BodySize + 5}, BodySize)),
ok.
-endif.
diff --git a/src/mochiweb_multipart.erl b/src/mochiweb_multipart.erl
index a83a88c..90bc949 100644
--- a/src/mochiweb_multipart.erl
+++ b/src/mochiweb_multipart.erl
@@ -374,7 +374,7 @@
body_end,
eof],
TestCallback = fun (Next) -> test_callback(Next, Expect) end,
- ServerFun = fun (Socket) ->
+ ServerFun = fun (Socket, _Opts) ->
ok = mochiweb_socket:send(Socket, BinContent),
exit(normal)
end,
@@ -410,7 +410,7 @@
body_end,
eof],
TestCallback = fun (Next) -> test_callback(Next, Expect) end,
- ServerFun = fun (Socket) ->
+ ServerFun = fun (Socket, _Opts) ->
ok = mochiweb_socket:send(Socket, BinContent),
exit(normal)
end,
@@ -447,7 +447,7 @@
"--AaB03x--",
""], "\r\n"),
BinContent = iolist_to_binary(Content),
- ServerFun = fun (Socket) ->
+ ServerFun = fun (Socket, _Opts) ->
ok = mochiweb_socket:send(Socket, BinContent),
exit(normal)
end,
@@ -500,7 +500,7 @@
body_end,
eof],
TestCallback = fun (Next) -> test_callback(Next, Expect) end,
- ServerFun = fun (Socket) ->
+ ServerFun = fun (Socket, _Opts) ->
ok = mochiweb_socket:send(Socket, BinContent),
exit(normal)
end,
@@ -552,7 +552,7 @@
body_end,
eof],
TestCallback = fun (Next) -> test_callback(Next, Expect) end,
- ServerFun = fun (Socket) ->
+ ServerFun = fun (Socket, _Opts) ->
ok = mochiweb_socket:send(Socket, BinContent),
exit(normal)
end,
@@ -605,7 +605,7 @@
body_end,
eof],
TestCallback = fun (Next) -> test_callback(Next, Expect) end,
- ServerFun = fun (Socket) ->
+ ServerFun = fun (Socket, _Opts) ->
ok = mochiweb_socket:send(Socket, BinContent),
exit(normal)
end,
@@ -681,7 +681,7 @@
body_end,
eof],
TestCallback = fun (Next) -> test_callback(Next, Expect) end,
- ServerFun = fun (Socket) ->
+ ServerFun = fun (Socket, _Opts) ->
ok = mochiweb_socket:send(Socket, BinContent),
exit(normal)
end,
@@ -729,7 +729,7 @@
body_end,
eof],
TestCallback = fun (Next) -> test_callback(Next, Expect) end,
- ServerFun = fun (Socket) ->
+ ServerFun = fun (Socket, _Opts) ->
ok = mochiweb_socket:send(Socket, BinContent),
exit(normal)
end,
@@ -856,7 +856,7 @@
body_end,
eof],
TestCallback = fun (Next) -> test_callback(Next, Expect) end,
- ServerFun = fun (Socket) ->
+ ServerFun = fun (Socket, _Opts) ->
ok = mochiweb_socket:send(Socket, BinContent),
exit(normal)
end,
diff --git a/src/mochiweb_request.erl b/src/mochiweb_request.erl
index 859e2d6..c97070f 100644
--- a/src/mochiweb_request.erl
+++ b/src/mochiweb_request.erl
@@ -11,7 +11,7 @@
-define(QUIP, "Any of you quaids got a smint?").
--export([new/5]).
+-export([new/5, new/6]).
-export([get_header_value/2, get_primary_header_value/2, get_combined_header_value/2, get/2, dump/1]).
-export([send/2, recv/2, recv/3, recv_body/1, recv_body/2, stream_body/4]).
-export([start_response/2, start_response_length/2, start_raw_response/2]).
@@ -49,17 +49,22 @@
%% @spec new(Socket, Method, RawPath, Version, headers()) -> request()
%% @doc Create a new request instance.
new(Socket, Method, RawPath, Version, Headers) ->
- {?MODULE, [Socket, Method, RawPath, Version, Headers]}.
+ new(Socket, [], Method, RawPath, Version, Headers).
+
+%% @spec new(Socket, Opts, Method, RawPath, Version, headers()) -> request()
+%% @doc Create a new request instance.
+new(Socket, Opts, Method, RawPath, Version, Headers) ->
+ {?MODULE, [Socket, Opts, Method, RawPath, Version, Headers]}.
%% @spec get_header_value(K, request()) -> undefined | Value
%% @doc Get the value of a given request header.
-get_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
+get_header_value(K, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) ->
mochiweb_headers:get_value(K, Headers).
-get_primary_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
+get_primary_header_value(K, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) ->
mochiweb_headers:get_primary_value(K, Headers).
-get_combined_header_value(K, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
+get_combined_header_value(K, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) ->
mochiweb_headers:get_combined_value(K, Headers).
%% @type field() = socket | scheme | method | raw_path | version | headers | peer | path | body_length | range
@@ -70,24 +75,24 @@
%% 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, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+get(socket, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
Socket;
-get(scheme, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+get(scheme, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
case mochiweb_socket:type(Socket) of
plain ->
http;
ssl ->
https
end;
-get(method, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}) ->
+get(method, {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]}) ->
Method;
-get(raw_path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
+get(raw_path, {?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) ->
RawPath;
-get(version, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}) ->
+get(version, {?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]}) ->
Version;
-get(headers, {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}) ->
+get(headers, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}) ->
Headers;
-get(peer, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+get(peer, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case mochiweb_socket:peername(Socket) of
{ok, {Addr={10, _, _, _}, _Port}} ->
case get_header_value("x-forwarded-for", THIS) of
@@ -108,7 +113,7 @@
{error, enotconn} ->
exit(normal)
end;
-get(path, {?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
+get(path, {?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) ->
case erlang:get(?SAVE_PATH) of
undefined ->
{Path0, _, _} = mochiweb_util:urlsplit_path(RawPath),
@@ -118,7 +123,7 @@
Cached ->
Cached
end;
-get(body_length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+get(body_length, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case erlang:get(?SAVE_BODY_LENGTH) of
undefined ->
BodyLength = body_length(THIS),
@@ -127,26 +132,29 @@
{cached, Cached} ->
Cached
end;
-get(range, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+get(range, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case get_header_value(range, THIS) of
undefined ->
undefined;
RawRange ->
mochiweb_http:parse_range_request(RawRange)
- end.
+ end;
+get(opts, {?MODULE, [_Socket, Opts, _Method, _RawPath, _Version, _Headers]}) ->
+ Opts.
%% @spec dump(request()) -> {mochiweb_request, [{atom(), term()}]}
%% @doc Dump the internal representation to a "human readable" set of terms
%% for debugging/inspection purposes.
-dump({?MODULE, [_Socket, Method, RawPath, Version, Headers]}) ->
+dump({?MODULE, [_Socket, Opts, Method, RawPath, Version, Headers]}) ->
{?MODULE, [{method, Method},
{version, Version},
{raw_path, RawPath},
+ {opts, Opts},
{headers, mochiweb_headers:to_list(Headers)}]}.
%% @spec send(iodata(), request()) -> ok
%% @doc Send data over the socket.
-send(Data, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+send(Data, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
case mochiweb_socket:send(Socket, Data) of
ok ->
ok;
@@ -157,13 +165,13 @@
%% @spec recv(integer(), request()) -> binary()
%% @doc Receive Length bytes from the client as a binary, with the default
%% idle timeout.
-recv(Length, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+recv(Length, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
recv(Length, ?IDLE_TIMEOUT, THIS).
%% @spec recv(integer(), integer(), request()) -> binary()
%% @doc Receive Length bytes from the client as a binary, with the given
%% Timeout in msec.
-recv(Length, Timeout, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+recv(Length, Timeout, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
case mochiweb_socket:recv(Socket, Length, Timeout) of
{ok, Data} ->
put(?SAVE_RECV, true),
@@ -174,7 +182,7 @@
%% @spec body_length(request()) -> undefined | chunked | unknown_transfer_encoding | integer()
%% @doc Infer body length from transfer-encoding and content-length headers.
-body_length({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+body_length({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case get_header_value("transfer-encoding", THIS) of
undefined ->
case get_combined_header_value("content-length", THIS) of
@@ -193,13 +201,13 @@
%% @spec recv_body(request()) -> binary()
%% @doc Receive the body of the HTTP request (defined by Content-Length).
%% Will only receive up to the default max-body length of 1MB.
-recv_body({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+recv_body({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
recv_body(?MAX_RECV_BODY, THIS).
%% @spec recv_body(integer(), request()) -> binary()
%% @doc Receive the body of the HTTP request (defined by Content-Length).
%% Will receive up to MaxBody bytes.
-recv_body(MaxBody, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+recv_body(MaxBody, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case erlang:get(?SAVE_BODY) of
undefined ->
% we could use a sane constant for max chunk size
@@ -219,11 +227,11 @@
Cached -> Cached
end.
-stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE,[_Socket,_Method,_RawPath,_Version,_Headers]}=THIS) ->
+stream_body(MaxChunkSize, ChunkFun, FunState, {?MODULE,[_Socket,_Opts,_Method,_RawPath,_Version,_Headers]}=THIS) ->
stream_body(MaxChunkSize, ChunkFun, FunState, undefined, THIS).
stream_body(MaxChunkSize, ChunkFun, FunState, MaxBodyLength,
- {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
Expect = case get_header_value("expect", THIS) of
undefined ->
undefined;
@@ -263,23 +271,16 @@
%% @doc Start the HTTP response by sending the Code HTTP response and
%% ResponseHeaders. The server will set header defaults such as Server
%% and Date if not present in ResponseHeaders.
-start_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
- HResponse = mochiweb_headers:make(ResponseHeaders),
- HResponse1 = mochiweb_headers:default_from_list(server_headers(),
- HResponse),
- start_raw_response({Code, HResponse1}, THIS).
+start_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ start_raw_response({Code, ResponseHeaders}, THIS).
%% @spec start_raw_response({integer(), headers()}, request()) -> response()
%% @doc Start the HTTP response by sending the Code HTTP response and
%% ResponseHeaders.
-start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) ->
- F = fun ({K, V}, Acc) ->
- [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
- end,
- End = lists:foldl(F, [<<"\r\n">>],
- mochiweb_headers:to_list(ResponseHeaders)),
- send([make_version(Version), make_code(Code), <<"\r\n">> | End], THIS),
- mochiweb:new_response({THIS, Code, ResponseHeaders}).
+start_raw_response({Code, ResponseHeaders}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ {Header, Response} = format_response_header({Code, ResponseHeaders}, THIS),
+ send(Header, THIS),
+ Response.
%% @spec start_response_length({integer(), ioheaders(), integer()}, request()) -> response()
@@ -288,18 +289,44 @@
%% will set header defaults such as Server
%% and Date if not present in ResponseHeaders.
start_response_length({Code, ResponseHeaders, Length},
- {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
HResponse = mochiweb_headers:make(ResponseHeaders),
HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse),
start_response({Code, HResponse1}, THIS).
+%% @spec format_response_header({integer(), ioheaders()} | {integer(), ioheaders(), integer()}, request()) -> iolist()
+%% @doc Format the HTTP response header, including the Code HTTP response and
+%% ResponseHeaders including an optional Content-Length of Length. The server
+%% will set header defaults such as Server
+%% and Date if not present in ResponseHeaders.
+format_response_header({Code, ResponseHeaders}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]}=THIS) ->
+ HResponse = mochiweb_headers:make(ResponseHeaders),
+ HResponse1 = mochiweb_headers:default_from_list(server_headers(), HResponse),
+ HResponse2 = case should_close(THIS) of
+ true ->
+ mochiweb_headers:enter("Connection", "close", HResponse1);
+ false ->
+ HResponse1
+ end,
+ F = fun ({K, V}, Acc) ->
+ [mochiweb_util:make_io(K), <<": ">>, V, <<"\r\n">> | Acc]
+ end,
+ End = lists:foldl(F, [<<"\r\n">>], mochiweb_headers:to_list(HResponse2)),
+ Response = mochiweb:new_response({THIS, Code, HResponse2}),
+ {[make_version(Version), make_code(Code), <<"\r\n">> | End], Response};
+format_response_header({Code, ResponseHeaders, Length},
+ {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ HResponse = mochiweb_headers:make(ResponseHeaders),
+ HResponse1 = mochiweb_headers:enter("Content-Length", Length, HResponse),
+ format_response_header({Code, HResponse1}, THIS).
+
%% @spec respond({integer(), ioheaders(), iodata() | chunked | {file, IoDevice}}, request()) -> response()
%% @doc Start the HTTP response with start_response, and send Body to the
%% client (if the get(method) /= 'HEAD'). The Content-Length header
%% will be set by the Body length, and the server will insert header
%% defaults.
respond({Code, ResponseHeaders, {file, IoDevice}},
- {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) ->
+ {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]}=THIS) ->
Length = mochiweb_io:iodevice_size(IoDevice),
Response = start_response_length({Code, ResponseHeaders, Length}, THIS),
case Method of
@@ -311,7 +338,7 @@
IoDevice)
end,
Response;
-respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, Method, _RawPath, Version, _Headers]}=THIS) ->
+respond({Code, ResponseHeaders, chunked}, {?MODULE, [_Socket, _Opts, Method, _RawPath, Version, _Headers]}=THIS) ->
HResponse = mochiweb_headers:make(ResponseHeaders),
HResponse1 = case Method of
'HEAD' ->
@@ -333,34 +360,32 @@
HResponse
end,
start_response({Code, HResponse1}, THIS);
-respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, Method, _RawPath, _Version, _Headers]}=THIS) ->
- Response = start_response_length({Code, ResponseHeaders, iolist_size(Body)}, THIS),
+respond({Code, ResponseHeaders, Body}, {?MODULE, [_Socket, _Opts, Method, _RawPath, _Version, _Headers]}=THIS) ->
+ {Header, Response} = format_response_header({Code, ResponseHeaders, iolist_size(Body)}, THIS),
case Method of
- 'HEAD' ->
- ok;
- _ ->
- send(Body, THIS)
+ 'HEAD' -> send(Header, THIS);
+ _ -> send([Header, Body], THIS)
end,
Response.
%% @spec not_found(request()) -> response()
%% @doc Alias for <code>not_found([])</code>.
-not_found({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+not_found({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
not_found([], THIS).
%% @spec not_found(ExtraHeaders, request()) -> response()
%% @doc Alias for <code>respond({404, [{"Content-Type", "text/plain"}
%% | ExtraHeaders], <<"Not found.">>})</code>.
-not_found(ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+not_found(ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
respond({404, [{"Content-Type", "text/plain"} | ExtraHeaders],
<<"Not found.">>}, THIS).
%% @spec ok({value(), iodata()} | {value(), ioheaders(), iodata() | {file, IoDevice}}, request()) ->
%% response()
%% @doc respond({200, [{"Content-Type", ContentType} | Headers], Body}).
-ok({ContentType, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ok({ContentType, Body}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
ok({ContentType, [], Body}, THIS);
-ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ok({ContentType, ResponseHeaders, Body}, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
HResponse = mochiweb_headers:make(ResponseHeaders),
case THIS:get(range) of
X when (X =:= undefined orelse X =:= fail) orelse Body =:= chunked ->
@@ -393,7 +418,7 @@
%% @spec should_close(request()) -> bool()
%% @doc Return true if the connection must be closed. If false, using
%% Keep-Alive should be safe.
-should_close({?MODULE, [_Socket, _Method, _RawPath, Version, _Headers]}=THIS) ->
+should_close({?MODULE, [_Socket, _Opts, _Method, _RawPath, Version, _Headers]}=THIS) ->
ForceClose = erlang:get(?SAVE_FORCE_CLOSE) =/= undefined,
DidNotRecv = erlang:get(?SAVE_RECV) =:= undefined,
ForceClose orelse Version < {1, 0}
@@ -419,7 +444,7 @@
%% @spec cleanup(request()) -> ok
%% @doc Clean up any junk in the process dictionary, required before continuing
%% a Keep-Alive request.
-cleanup({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) ->
+cleanup({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
L = [?SAVE_QS, ?SAVE_PATH, ?SAVE_RECV, ?SAVE_BODY, ?SAVE_BODY_LENGTH,
?SAVE_POST, ?SAVE_COOKIE, ?SAVE_FORCE_CLOSE],
lists:foreach(fun(K) ->
@@ -429,7 +454,7 @@
%% @spec parse_qs(request()) -> [{Key::string(), Value::string()}]
%% @doc Parse the query string of the URL.
-parse_qs({?MODULE, [_Socket, _Method, RawPath, _Version, _Headers]}) ->
+parse_qs({?MODULE, [_Socket, _Opts, _Method, RawPath, _Version, _Headers]}) ->
case erlang:get(?SAVE_QS) of
undefined ->
{_, QueryString, _} = mochiweb_util:urlsplit_path(RawPath),
@@ -442,12 +467,12 @@
%% @spec get_cookie_value(Key::string, request()) -> string() | undefined
%% @doc Get the value of the given cookie.
-get_cookie_value(Key, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+get_cookie_value(Key, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
proplists:get_value(Key, parse_cookie(THIS)).
%% @spec parse_cookie(request()) -> [{Key::string(), Value::string()}]
%% @doc Parse the cookie header.
-parse_cookie({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+parse_cookie({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case erlang:get(?SAVE_COOKIE) of
undefined ->
Cookies = case get_header_value("cookie", THIS) of
@@ -465,7 +490,7 @@
%% @spec parse_post(request()) -> [{Key::string(), Value::string()}]
%% @doc Parse an application/x-www-form-urlencoded form POST. This
%% has the side-effect of calling recv_body().
-parse_post({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+parse_post({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case erlang:get(?SAVE_POST) of
undefined ->
Parsed = case recv_body(THIS) of
@@ -489,7 +514,7 @@
%% @doc The function is called for each chunk.
%% Used internally by read_chunked_body.
stream_chunked_body(MaxChunkSize, Fun, FunState,
- {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case read_chunk_length(THIS) of
0 ->
Fun({0, read_chunk(0, THIS)}, FunState);
@@ -501,13 +526,14 @@
stream_chunked_body(MaxChunkSize, Fun, NewState, THIS)
end.
-stream_unchunked_body(0, Fun, FunState, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}) ->
+stream_unchunked_body(0, Fun, FunState, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
Fun({0, <<>>}, FunState);
stream_unchunked_body(Length, Fun, FunState,
- {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > 0 ->
- PktSize = case Length > ?RECBUF_SIZE of
+ {?MODULE, [_Socket, Opts, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > 0 ->
+ RecBuf = mochilists:get_value(recbuf, Opts, ?RECBUF_SIZE),
+ PktSize = case Length > RecBuf of
true ->
- ?RECBUF_SIZE;
+ RecBuf;
false ->
Length
end,
@@ -517,7 +543,7 @@
%% @spec read_chunk_length(request()) -> integer()
%% @doc Read the length of the next HTTP chunk.
-read_chunk_length({?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+read_chunk_length({?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
ok = mochiweb_socket:setopts(Socket, [{packet, line}]),
case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
{ok, Header} ->
@@ -534,7 +560,7 @@
%% @spec read_chunk(integer(), request()) -> Chunk::binary() | [Footer::binary()]
%% @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, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+read_chunk(0, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
ok = mochiweb_socket:setopts(Socket, [{packet, line}]),
F = fun (F1, Acc) ->
case mochiweb_socket:recv(Socket, 0, ?IDLE_TIMEOUT) of
@@ -550,7 +576,7 @@
ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
put(?SAVE_RECV, true),
Footers;
-read_chunk(Length, {?MODULE, [Socket, _Method, _RawPath, _Version, _Headers]}) ->
+read_chunk(Length, {?MODULE, [Socket, _Opts, _Method, _RawPath, _Version, _Headers]}) ->
case mochiweb_socket:recv(Socket, 2 + Length, ?IDLE_TIMEOUT) of
{ok, <<Chunk:Length/binary, "\r\n">>} ->
Chunk;
@@ -559,23 +585,23 @@
end.
read_sub_chunks(Length, MaxChunkSize, Fun, FunState,
- {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > MaxChunkSize ->
+ {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) when Length > MaxChunkSize ->
Bin = recv(MaxChunkSize, THIS),
NewState = Fun({size(Bin), Bin}, FunState),
read_sub_chunks(Length - MaxChunkSize, MaxChunkSize, Fun, NewState, THIS);
read_sub_chunks(Length, _MaxChunkSize, Fun, FunState,
- {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+ {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
Fun({Length, read_chunk(Length, THIS)}, FunState).
%% @spec serve_file(Path, DocRoot, request()) -> Response
%% @doc Serve a file relative to DocRoot.
-serve_file(Path, DocRoot, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+serve_file(Path, DocRoot, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
serve_file(Path, DocRoot, [], THIS).
%% @spec serve_file(Path, DocRoot, ExtraHeaders, request()) -> Response
%% @doc Serve a file relative to DocRoot.
-serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+serve_file(Path, DocRoot, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case mochiweb_util:safe_relative_path(Path) of
undefined ->
not_found(ExtraHeaders, THIS);
@@ -595,11 +621,11 @@
directory_index(FullPath) ->
filename:join([FullPath, "index.html"]).
-maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+maybe_redirect([], FullPath, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS);
maybe_redirect(RelPath, FullPath, ExtraHeaders,
- {?MODULE, [_Socket, _Method, _RawPath, _Version, Headers]}=THIS) ->
+ {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, Headers]}=THIS) ->
case string:right(RelPath, 1) of
"/" ->
maybe_serve_file(directory_index(FullPath), ExtraHeaders, THIS);
@@ -620,7 +646,7 @@
respond({301, MoreHeaders, Body}, THIS)
end.
-maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+maybe_serve_file(File, ExtraHeaders, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case file:read_file_info(File) of
{ok, FileInfo} ->
LastModified = httpd_util:rfc1123_date(FileInfo#file_info.mtime),
@@ -719,7 +745,7 @@
%% accepted_encodings(["gzip", "deflate", "identity"]) ->
%% ["deflate", "gzip", "identity"]
%%
-accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+accepted_encodings(SupportedEncodings, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
AcceptEncodingHeader = case get_header_value("Accept-Encoding", THIS) of
undefined ->
"";
@@ -757,7 +783,7 @@
%% 5) For an "Accept" header with value "text/*; q=0.0, */*":
%% accepts_content_type("text/plain") -> false
%%
-accepts_content_type(ContentType1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+accepts_content_type(ContentType1, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
ContentType = re:replace(ContentType1, "\\s", "", [global, {return, list}]),
AcceptHeader = accept_header(THIS),
case mochiweb_util:parse_qvalues(AcceptHeader) of
@@ -806,7 +832,7 @@
%% accepts_content_types(["application/json", "text/html"]) ->
%% ["text/html", "application/json"]
%%
-accepted_content_types(Types1, {?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+accepted_content_types(Types1, {?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
Types = lists:map(
fun(T) -> re:replace(T, "\\s", "", [global, {return, list}]) end,
Types1),
@@ -846,7 +872,7 @@
[Type || {_Q, Type} <- lists:sort(SortFun, TypesQ)]
end.
-accept_header({?MODULE, [_Socket, _Method, _RawPath, _Version, _Headers]}=THIS) ->
+accept_header({?MODULE, [_Socket, _Opts, _Method, _RawPath, _Version, _Headers]}=THIS) ->
case get_header_value("Accept", THIS) of
undefined ->
"*/*";
diff --git a/src/mochiweb_socket.erl b/src/mochiweb_socket.erl
index 76b018c..1e35e15 100644
--- a/src/mochiweb_socket.erl
+++ b/src/mochiweb_socket.erl
@@ -4,15 +4,22 @@
-module(mochiweb_socket).
--export([listen/4, accept/1, recv/3, send/2, close/1, port/1, peername/1,
- setopts/2, type/1]).
+-export([listen/4,
+ accept/1, transport_accept/1, finish_accept/1,
+ recv/3, send/2, close/1, port/1, peername/1,
+ setopts/2, getopts/2, type/1]).
-define(ACCEPT_TIMEOUT, 2000).
+-define(SSL_TIMEOUT, 10000).
+-define(SSL_HANDSHAKE_TIMEOUT, 20000).
+
listen(Ssl, Port, Opts, SslOpts) ->
case Ssl of
true ->
- case ssl:listen(Port, Opts ++ SslOpts) of
+ Opts1 = add_unbroken_ciphers_default(Opts ++ SslOpts),
+ Opts2 = add_safe_protocol_versions(Opts1),
+ case ssl:listen(Port, Opts2) of
{ok, ListenSocket} ->
{ok, {ssl, ListenSocket}};
{error, _} = Err ->
@@ -22,26 +29,74 @@
gen_tcp:listen(Port, Opts)
end.
-accept({ssl, ListenSocket}) ->
- % There's a bug in ssl:transport_accept/2 at the moment, which is the
- % reason for the try...catch block. Should be fixed in OTP R14.
- try ssl:transport_accept(ListenSocket) of
+add_unbroken_ciphers_default(Opts) ->
+ Default = filter_unsecure_cipher_suites(ssl:cipher_suites()),
+ Ciphers = filter_broken_cipher_suites(proplists:get_value(ciphers, Opts, Default)),
+ [{ciphers, Ciphers} | proplists:delete(ciphers, Opts)].
+
+filter_broken_cipher_suites(Ciphers) ->
+ case proplists:get_value(ssl_app, ssl:versions()) of
+ "5.3" ++ _ ->
+ lists:filter(fun(Suite) ->
+ string:left(atom_to_list(element(1, Suite)), 4) =/= "ecdh"
+ end, Ciphers);
+ _ ->
+ Ciphers
+ end.
+
+filter_unsecure_cipher_suites(Ciphers) ->
+ lists:filter(fun
+ ({_,des_cbc,_}) -> false;
+ ({_,_,md5}) -> false;
+ (_) -> true
+ end,
+ Ciphers).
+
+add_safe_protocol_versions(Opts) ->
+ case proplists:is_defined(versions, Opts) of
+ true ->
+ Opts;
+ false ->
+ Versions = filter_unsafe_protcol_versions(proplists:get_value(available, ssl:versions())),
+ [{versions, Versions} | Opts]
+ end.
+
+filter_unsafe_protcol_versions(Versions) ->
+ lists:filter(fun
+ (sslv3) -> false;
+ (_) -> true
+ end,
+ Versions).
+
+%% Provided for backwards compatibility only
+accept(ListenSocket) ->
+ case transport_accept(ListenSocket) of
{ok, Socket} ->
- case ssl:ssl_accept(Socket) of
- ok ->
- {ok, {ssl, Socket}};
- {error, _} = Err ->
- Err
- end;
+ finish_accept(Socket);
{error, _} = Err ->
Err
- catch
- error:{badmatch, {error, Reason}} ->
- {error, Reason}
+ end.
+
+transport_accept({ssl, ListenSocket}) ->
+ case ssl:transport_accept(ListenSocket, ?SSL_TIMEOUT) of
+ {ok, Socket} ->
+ {ok, {ssl, Socket}};
+ {error, _} = Err ->
+ Err
end;
-accept(ListenSocket) ->
+transport_accept(ListenSocket) ->
gen_tcp:accept(ListenSocket, ?ACCEPT_TIMEOUT).
+finish_accept({ssl, Socket}) ->
+ case ssl:ssl_accept(Socket, ?SSL_HANDSHAKE_TIMEOUT) of
+ ok ->
+ {ok, {ssl, Socket}};
+ {error, _} = Err ->
+ Err
+ end;
+finish_accept(Socket) ->
+ {ok, Socket}.
+
recv({ssl, Socket}, Length, Timeout) ->
ssl:recv(Socket, Length, Timeout);
recv(Socket, Length, Timeout) ->
@@ -77,6 +132,11 @@
setopts(Socket, Opts) ->
inet:setopts(Socket, Opts).
+getopts({ssl, Socket}, Opts) ->
+ ssl:getopts(Socket, Opts);
+getopts(Socket, Opts) ->
+ inet:getopts(Socket, Opts).
+
type({ssl, _}) ->
ssl;
type(_) ->
diff --git a/src/mochiweb_socket_server.erl b/src/mochiweb_socket_server.erl
index a3d4da3..7f8587e 100644
--- a/src/mochiweb_socket_server.erl
+++ b/src/mochiweb_socket_server.erl
@@ -18,11 +18,11 @@
{port,
loop,
name=undefined,
- %% NOTE: This is currently ignored.
max=2048,
ip=any,
listen=null,
nodelay=false,
+ recbuf=?RECBUF_SIZE,
backlog=128,
active_sockets=0,
acceptor_pool_size=16,
@@ -74,7 +74,16 @@
parse_options(Options) ->
parse_options(Options, #mochiweb_socket_server{}).
-parse_options([], State) ->
+parse_options([], State=#mochiweb_socket_server{acceptor_pool_size=PoolSize,
+ max=Max}) ->
+ case Max < PoolSize of
+ true ->
+ error_logger:info_report([{warning, "max is set lower than acceptor_pool_size"},
+ {max, Max},
+ {acceptor_pool_size, PoolSize}]);
+ false ->
+ ok
+ end,
State;
parse_options([{name, L} | Rest], State) when is_list(L) ->
Name = {local, list_to_atom(L)},
@@ -108,13 +117,13 @@
parse_options(Rest, State#mochiweb_socket_server{backlog=Backlog});
parse_options([{nodelay, NoDelay} | Rest], State) ->
parse_options(Rest, State#mochiweb_socket_server{nodelay=NoDelay});
+parse_options([{recbuf, RecBuf} | Rest], State) when is_integer(RecBuf) ->
+ parse_options(Rest, State#mochiweb_socket_server{recbuf=RecBuf});
parse_options([{acceptor_pool_size, Max} | Rest], State) ->
MaxInt = ensure_int(Max),
parse_options(Rest,
State#mochiweb_socket_server{acceptor_pool_size=MaxInt});
parse_options([{max, Max} | Rest], State) ->
- error_logger:info_report([{warning, "TODO: max is currently unsupported"},
- {max, Max}]),
MaxInt = ensure_int(Max),
parse_options(Rest, State#mochiweb_socket_server{max=MaxInt});
parse_options([{ssl, Ssl} | Rest], State) when is_boolean(Ssl) ->
@@ -156,13 +165,15 @@
false
end.
-init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog, nodelay=NoDelay}) ->
+init(State=#mochiweb_socket_server{ip=Ip, port=Port, backlog=Backlog,
+ nodelay=NoDelay, recbuf=RecBuf}) ->
process_flag(trap_exit, true),
+
BaseOpts = [binary,
{reuseaddr, true},
{packet, 0},
{backlog, Backlog},
- {recbuf, ?RECBUF_SIZE},
+ {recbuf, RecBuf},
{exit_on_close, false},
{active, false},
{nodelay, NoDelay}],
@@ -182,28 +193,45 @@
new_acceptor_pool(Listen,
State=#mochiweb_socket_server{acceptor_pool=Pool,
acceptor_pool_size=Size,
+ recbuf=RecBuf,
loop=Loop}) ->
+ LoopOpts = [{recbuf, RecBuf}],
F = fun (_, S) ->
- Pid = mochiweb_acceptor:start_link(self(), Listen, Loop),
+ Pid = mochiweb_acceptor:start_link(
+ self(), Listen, Loop, LoopOpts
+ ),
sets:add_element(Pid, S)
end,
Pool1 = lists:foldl(F, Pool, lists:seq(1, Size)),
State#mochiweb_socket_server{acceptor_pool=Pool1}.
-listen(Port, Opts, State=#mochiweb_socket_server{ssl=Ssl, ssl_opts=SslOpts}) ->
+listen(Port, Opts, State=#mochiweb_socket_server{ssl=Ssl, ssl_opts=SslOpts,
+ recbuf=RecBuf}) ->
case mochiweb_socket:listen(Ssl, Port, Opts, SslOpts) of
{ok, Listen} ->
+ %% XXX: `recbuf' value which is passed to `gen_tcp'
+ %% and value reported by `inet:getopts(P, [recbuf])' may
+ %% differ. They depends on underlying OS. From linux mans:
+ %%
+ %% The kernel doubles this value (to allow space for
+ %% bookkeeping overhead) when it is set using setsockopt(2),
+ %% and this doubled value is returned by getsockopt(2).
+ %%
+ %% See: man 7 socket | grep SO_RCVBUF
{ok, ListenPort} = mochiweb_socket:port(Listen),
{ok, new_acceptor_pool(
Listen,
State#mochiweb_socket_server{listen=Listen,
- port=ListenPort})};
+ port=ListenPort,
+ recbuf=RecBuf})};
{error, Reason} ->
{stop, Reason}
end.
do_get(port, #mochiweb_socket_server{port=Port}) ->
Port;
+do_get(waiting_acceptors, #mochiweb_socket_server{acceptor_pool=Pool}) ->
+ sets:size(Pool);
do_get(active_sockets, #mochiweb_socket_server{active_sockets=ActiveSockets}) ->
ActiveSockets.
@@ -271,16 +299,39 @@
recycle_acceptor(Pid, State=#mochiweb_socket_server{
acceptor_pool=Pool,
+ acceptor_pool_size=PoolSize,
listen=Listen,
loop=Loop,
+ max=Max,
+ recbuf=RecBuf,
active_sockets=ActiveSockets}) ->
+ LoopOpts = [{recbuf, RecBuf}],
case sets:is_element(Pid, Pool) of
true ->
- Acceptor = mochiweb_acceptor:start_link(self(), Listen, Loop),
- Pool1 = sets:add_element(Acceptor, sets:del_element(Pid, Pool)),
- State#mochiweb_socket_server{acceptor_pool=Pool1};
+ Pool1 = sets:del_element(Pid, Pool),
+ case ActiveSockets + sets:size(Pool1) < Max of
+ true ->
+ Acceptor = mochiweb_acceptor:start_link(
+ self(), Listen, Loop, LoopOpts
+ ),
+ Pool2 = sets:add_element(Acceptor, Pool1),
+ State#mochiweb_socket_server{acceptor_pool=Pool2};
+ false ->
+ State#mochiweb_socket_server{acceptor_pool=Pool1}
+ end;
false ->
- State#mochiweb_socket_server{active_sockets=ActiveSockets - 1}
+ case sets:size(Pool) < PoolSize of
+ true ->
+ Acceptor = mochiweb_acceptor:start_link(
+ self(), Listen, Loop, LoopOpts
+ ),
+ Pool1 = sets:add_element(Acceptor, Pool),
+ State#mochiweb_socket_server{active_sockets=ActiveSockets,
+ acceptor_pool=Pool1};
+ false ->
+ State#mochiweb_socket_server{active_sockets=ActiveSockets - 1,
+ acceptor_pool=Pool}
+ end
end.
handle_info(Msg, State) when ?is_old_state(State) ->
diff --git a/src/mochiweb_websocket.erl b/src/mochiweb_websocket.erl
index cc3127e..2768a3e 100644
--- a/src/mochiweb_websocket.erl
+++ b/src/mochiweb_websocket.erl
@@ -27,6 +27,9 @@
-export([loop/5, upgrade_connection/2, request/5]).
-export([send/3]).
+-ifdef(TEST).
+-compile(export_all).
+-endif.
loop(Socket, Body, State, WsVersion, ReplyChannel) ->
ok = mochiweb_socket:setopts(Socket, [{packet, 0}, {active, once}]),
@@ -44,7 +47,7 @@
{tcp_error, _, _} ->
mochiweb_socket:close(Socket),
exit(normal);
- {tcp, _, WsFrames} ->
+ {Proto, _, WsFrames} when Proto =:= tcp orelse Proto =:= ssl ->
case parse_frames(WsVersion, WsFrames, Socket) of
close ->
mochiweb_socket:close(Socket),
@@ -214,7 +217,7 @@
{tcp_error, _, _} ->
mochiweb_socket:close(Socket),
exit(normal);
- {tcp, _, Continuation} ->
+ {Proto, _, Continuation} when Proto =:= tcp orelse Proto =:= ssl ->
parse_hybi_frames(Socket, <<PartFrame/binary, Continuation/binary>>,
Acc);
_ ->
@@ -276,11 +279,3 @@
{Buffer, Rest};
parse_hixie(<<H, T/binary>>, Buffer) ->
parse_hixie(T, <<Buffer/binary, H>>).
-
-%%
-%% Tests
-%%
--ifdef(TEST).
--include_lib("eunit/include/eunit.hrl").
--compile(export_all).
--endif.
diff --git a/support/templates/mochiwebapp_skel/src/mochiapp_web.erl b/support/templates/mochiwebapp_skel/src/mochiapp_web.erl
index 8976265..5fe455a 100644
--- a/support/templates/mochiwebapp_skel/src/mochiapp_web.erl
+++ b/support/templates/mochiwebapp_skel/src/mochiapp_web.erl
@@ -44,9 +44,8 @@
{type, Type}, {what, What},
{trace, erlang:get_stacktrace()}],
error_logger:error_report(Report),
- %% NOTE: mustache templates need \\ because they are not awesome.
Req:respond({500, [{"Content-Type", "text/plain"}],
- "request failed, sorry\\n"})
+ "request failed, sorry\n"})
end.
%% Internal API
diff --git a/support/templates/mochiwebapp_skel/start-dev.sh b/support/templates/mochiwebapp_skel/start-dev.sh
index fb7c45e..65c1692 100755
--- a/support/templates/mochiwebapp_skel/start-dev.sh
+++ b/support/templates/mochiwebapp_skel/start-dev.sh
@@ -1,6 +1,7 @@
#!/bin/sh
-# NOTE: mustache templates need \\ because they are not awesome.
-exec erl -pa ebin edit deps/*/ebin -boot start_sasl \\
- -sname {{appid}}_dev \\
- -s {{appid}} \\
+exec erl \
+ -pa ebin deps/*/ebin \
+ -boot start_sasl \
+ -sname {{appid}}_dev \
+ -s {{appid}} \
-s reloader
diff --git a/test/mochiweb_socket_server_tests.erl b/test/mochiweb_socket_server_tests.erl
new file mode 100644
index 0000000..c64f5b7
--- /dev/null
+++ b/test/mochiweb_socket_server_tests.erl
@@ -0,0 +1,149 @@
+-module(mochiweb_socket_server_tests).
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+socket_server(Opts, ServerFun) ->
+ ServerOpts = [{ip, "127.0.0.1"}, {port, 0}, {backlog, 5}, {loop, ServerFun}],
+ {ok, Server} = mochiweb_socket_server:start(ServerOpts ++ Opts),
+ Port = mochiweb_socket_server:get(Server, port),
+ {Server, Port}.
+
+echo_loop(Socket) ->
+ ok = mochiweb_socket:setopts(Socket, [{active, once}]),
+ receive
+ {_Protocol, _, Data} ->
+ gen_tcp:send(Socket, Data),
+ echo_loop(Socket);
+ {tcp_closed, Socket} ->
+ ok
+ end.
+
+start_client_conns(Port, NumClients, ClientFun, ClientArgs, Tester) ->
+ Opts = [binary, {active, false}, {packet, 1}],
+ lists:foreach(fun (_N) ->
+ case gen_tcp:connect("127.0.0.1", Port, Opts) of
+ {ok, Socket} ->
+ spawn_link(fun() -> ClientFun(Socket, ClientArgs) end);
+ {error, E} ->
+ Tester ! {client_conn_error, E}
+ end
+ end, lists:seq(1, NumClients)).
+
+client_fun(_Socket, []) -> ok;
+client_fun(Socket, [{close_sock} | Cmds]) ->
+ mochiweb_socket:close(Socket),
+ client_fun(Socket, Cmds);
+client_fun(Socket, [{send_pid, To} | Cmds]) ->
+ To ! {client, self()},
+ client_fun(Socket, Cmds);
+client_fun(Socket, [{send, Data, Tester} | Cmds]) ->
+ case gen_tcp:send(Socket, Data) of
+ ok -> ok;
+ {error, E} -> Tester ! {client_send_error, self(), E}
+ end,
+ client_fun(Socket, Cmds);
+client_fun(Socket, [{recv, Length, Timeout, Tester} | Cmds]) ->
+ case gen_tcp:recv(Socket, Length, Timeout) of
+ {ok, _} -> ok;
+ {error, E} -> Tester ! {client_recv_error, self(), E}
+ end,
+ client_fun(Socket, Cmds);
+client_fun(Socket, [{wait_msg, Msg} | Cmds]) ->
+ receive
+ M when M =:= Msg -> ok
+ end,
+ client_fun(Socket, Cmds);
+client_fun(Socket, [{send_msg, Msg, To} | Cmds]) ->
+ To ! {Msg, self()},
+ client_fun(Socket, Cmds).
+
+test_basic_accept(Max, PoolSize, NumClients, ReportTo) ->
+ Tester = self(),
+
+ ServerOpts = [{max, Max}, {acceptor_pool_size, PoolSize}],
+ ServerLoop =
+ fun (Socket, _Opts) ->
+ Tester ! {server_accepted, self()},
+ mochiweb_socket:setopts(Socket, [{packet, 1}]),
+ echo_loop(Socket)
+ end,
+ {Server, Port} = socket_server(ServerOpts, ServerLoop),
+
+ Data = <<"data">>,
+ Timeout = 2000,
+ ClientCmds = [{send_pid, Tester}, {wait_msg, go},
+ {send, Data, Tester}, {recv, size(Data), Timeout, Tester},
+ {close_sock}, {send_msg, done, Tester}],
+ start_client_conns(Port, NumClients, fun client_fun/2, ClientCmds, Tester),
+
+ EventCount = min(NumClients, max(Max, PoolSize)),
+
+ ConnectLoop =
+ fun (Loop, Connected, Accepted, Errors) ->
+ case (length(Accepted) + Errors >= EventCount
+ andalso length(Connected) + Errors >= NumClients) of
+ true -> {Connected, Accepted};
+ false ->
+ receive
+ {server_accepted, ServerPid} ->
+ Loop(Loop, Connected, [ServerPid | Accepted], Errors);
+ {client, ClientPid} ->
+ Loop(Loop, [ClientPid | Connected], Accepted, Errors);
+ {client_conn_error, _E} ->
+ Loop(Loop, Connected, Accepted, Errors + 1)
+ end
+ end
+ end,
+ {Connected, Accepted} = ConnectLoop(ConnectLoop, [], [], 0),
+
+ ActiveAfterConnect = mochiweb_socket_server:get(Server, active_sockets),
+ WaitingAfterConnect = mochiweb_socket_server:get(Server, waiting_acceptors),
+
+ lists:foreach(fun(Pid) -> Pid ! go end, Connected),
+ WaitLoop =
+ fun (Loop, Done) ->
+ case (length(Done) >= length(Connected)) of
+ true ->
+ ok;
+ false ->
+ receive
+ {done, From} ->
+ Loop(Loop, [From | Done])
+ end
+ end
+ end,
+ ok = WaitLoop(WaitLoop, []),
+
+ mochiweb_socket_server:stop(Server),
+
+ ReportTo ! {result, {length(Accepted),
+ ActiveAfterConnect, WaitingAfterConnect}}.
+
+normal_acceptor_test_fun() ->
+ % {Max, PoolSize, NumClients,
+ % {ExpectedAccepts,
+ % ExpectedActiveAfterConnect, ExpectedWaitingAfterConnect}
+ Tests = [{3, 1, 1, {1, 1, 1}},
+ {3, 1, 2, {2, 2, 1}},
+ {3, 1, 3, {3, 3, 0}},
+ {3, 3, 3, {3, 3, 0}},
+ {1, 3, 3, {3, 3, 0}}, % Max is overridden to PoolSize
+ {3, 2, 6, {3, 3, 0}}
+ ],
+ [fun () ->
+ Self = self(),
+ spawn(fun () ->
+ test_basic_accept(Max, PoolSize, NumClients, Self)
+ end),
+ Result = receive {result, R} -> R end,
+ ?assertEqual(Expected, Result)
+ end || {Max, PoolSize, NumClients, Expected} <- Tests].
+
+-define(LARGE_TIMEOUT, 40).
+
+normal_acceptor_test_() ->
+ Tests = normal_acceptor_test_fun(),
+ {timeout, ?LARGE_TIMEOUT, Tests}.
+
+-endif.
diff --git a/test/mochiweb_test_util.erl b/test/mochiweb_test_util.erl
new file mode 100644
index 0000000..2fbf14f
--- /dev/null
+++ b/test/mochiweb_test_util.erl
@@ -0,0 +1,126 @@
+-module(mochiweb_test_util).
+-export([with_server/3, client_request/4, sock_fun/2,
+ read_server_headers/1, drain_reply/3]).
+-include("mochiweb_test_util.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+ssl_cert_opts() ->
+ EbinDir = filename:dirname(code:which(?MODULE)),
+ CertDir = filename:join([EbinDir, "..", "support", "test-materials"]),
+ CertFile = filename:join(CertDir, "test_ssl_cert.pem"),
+ KeyFile = filename:join(CertDir, "test_ssl_key.pem"),
+ [{certfile, CertFile}, {keyfile, KeyFile}].
+
+with_server(Transport, ServerFun, ClientFun) ->
+ ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}],
+ ServerOpts = case Transport of
+ plain ->
+ ServerOpts0;
+ ssl ->
+ ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}]
+ end,
+ {ok, Server} = mochiweb_http:start_link(ServerOpts),
+ Port = mochiweb_socket_server:get(Server, port),
+ Res = (catch ClientFun(Transport, Port)),
+ mochiweb_http:stop(Server),
+ Res.
+
+sock_fun(Transport, Port) ->
+ Opts = [binary, {active, false}, {packet, http}],
+ case Transport of
+ plain ->
+ {ok, Socket} = gen_tcp:connect("127.0.0.1", Port, Opts),
+ fun (recv) ->
+ gen_tcp:recv(Socket, 0);
+ ({recv, Length}) ->
+ gen_tcp:recv(Socket, Length);
+ ({send, Data}) ->
+ gen_tcp:send(Socket, Data);
+ ({setopts, L}) ->
+ inet:setopts(Socket, L);
+ (get) ->
+ Socket
+ end;
+ ssl ->
+ {ok, Socket} = ssl:connect("127.0.0.1", Port, [{ssl_imp, new} | Opts]),
+ fun (recv) ->
+ ssl:recv(Socket, 0);
+ ({recv, Length}) ->
+ ssl:recv(Socket, Length);
+ ({send, Data}) ->
+ ssl:send(Socket, Data);
+ ({setopts, L}) ->
+ ssl:setopts(Socket, L);
+ (get) ->
+ {ssl, Socket}
+ end
+ end.
+
+client_request(Transport, Port, Method, TestReqs) ->
+ client_request(sock_fun(Transport, Port), Method, TestReqs).
+
+client_request(SockFun, _Method, []) ->
+ {the_end, {error, closed}} = {the_end, SockFun(recv)},
+ ok;
+client_request(SockFun, Method,
+ [#treq{path=Path, body=Body, xreply=ExReply} | Rest]) ->
+ Request = [atom_to_list(Method), " ", Path, " HTTP/1.1\r\n",
+ client_headers(Body, Rest =:= []),
+ "\r\n",
+ Body],
+ ok = SockFun({setopts, [{packet, http}]}),
+ ok = SockFun({send, Request}),
+ case Method of
+ 'GET' ->
+ {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv);
+ 'POST' ->
+ {ok, {http_response, {1,1}, 201, "Created"}} = SockFun(recv);
+ 'CONNECT' ->
+ {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv)
+ end,
+ Headers = read_server_headers(SockFun),
+ ?assertMatch("MochiWeb" ++ _, mochiweb_headers:get_value("Server", Headers)),
+ ?assert(mochiweb_headers:get_value("Date", Headers) =/= undefined),
+ ?assert(mochiweb_headers:get_value("Content-Type", Headers) =/= undefined),
+ ContentLength = list_to_integer(mochiweb_headers:get_value("Content-Length", Headers)),
+ {payload, ExReply} = {payload, drain_reply(SockFun, ContentLength, <<>>)},
+ client_request(SockFun, Method, Rest).
+
+read_server_headers(SockFun) ->
+ ok = SockFun({setopts, [{packet, httph}]}),
+ Headers = read_server_headers(SockFun, mochiweb_headers:empty()),
+ ok = SockFun({setopts, [{packet, raw}]}),
+ Headers.
+
+read_server_headers(SockFun, Headers) ->
+ case SockFun(recv) of
+ {ok, http_eoh} ->
+ Headers;
+ {ok, {http_header, _, Header, _, Value}} ->
+ read_server_headers(
+ SockFun,
+ mochiweb_headers:insert(Header, Value, Headers))
+ end.
+
+client_headers(Body, IsLastRequest) ->
+ ["Host: localhost\r\n",
+ case Body of
+ <<>> ->
+ "";
+ _ ->
+ ["Content-Type: application/octet-stream\r\n",
+ "Content-Length: ", integer_to_list(byte_size(Body)), "\r\n"]
+ end,
+ case IsLastRequest of
+ true ->
+ "Connection: close\r\n";
+ false ->
+ ""
+ end].
+
+drain_reply(_SockFun, 0, Acc) ->
+ Acc;
+drain_reply(SockFun, Length, Acc) ->
+ Sz = erlang:min(Length, 1024),
+ {ok, B} = SockFun({recv, Sz}),
+ drain_reply(SockFun, Length - Sz, <<Acc/bytes, B/bytes>>).
diff --git a/test/mochiweb_test_util.hrl b/test/mochiweb_test_util.hrl
new file mode 100644
index 0000000..99fdc4e
--- /dev/null
+++ b/test/mochiweb_test_util.hrl
@@ -0,0 +1 @@
+-record(treq, {path, body= <<>>, xreply= <<>>}).
diff --git a/test/mochiweb_tests.erl b/test/mochiweb_tests.erl
index c8bc8ac..209971b 100644
--- a/test/mochiweb_tests.erl
+++ b/test/mochiweb_tests.erl
@@ -1,28 +1,9 @@
-module(mochiweb_tests).
-include_lib("eunit/include/eunit.hrl").
-
--record(treq, {path, body= <<>>, xreply= <<>>}).
-
-ssl_cert_opts() ->
- EbinDir = filename:dirname(code:which(?MODULE)),
- CertDir = filename:join([EbinDir, "..", "support", "test-materials"]),
- CertFile = filename:join(CertDir, "test_ssl_cert.pem"),
- KeyFile = filename:join(CertDir, "test_ssl_key.pem"),
- [{certfile, CertFile}, {keyfile, KeyFile}].
+-include("mochiweb_test_util.hrl").
with_server(Transport, ServerFun, ClientFun) ->
- ServerOpts0 = [{ip, "127.0.0.1"}, {port, 0}, {loop, ServerFun}],
- ServerOpts = case Transport of
- plain ->
- ServerOpts0;
- ssl ->
- ServerOpts0 ++ [{ssl, true}, {ssl_opts, ssl_cert_opts()}]
- end,
- {ok, Server} = mochiweb_http:start_link(ServerOpts),
- Port = mochiweb_socket_server:get(Server, port),
- Res = (catch ClientFun(Transport, Port)),
- mochiweb_http:stop(Server),
- Res.
+ mochiweb_test_util:with_server(Transport, ServerFun, ClientFun).
request_test() ->
R = mochiweb_request:new(z, z, "/foo/bar/baz%20wibble+quux?qs=2", z, []),
@@ -148,6 +129,7 @@
ClientFun = new_client_fun('GET', TestReqs),
ok = with_server(Transport, ServerFun, ClientFun),
ok.
+
do_POST(Transport, Size, Times) ->
ServerFun = fun (Req) ->
Body = Req:recv_body(),
@@ -165,86 +147,57 @@
new_client_fun(Method, TestReqs) ->
fun (Transport, Port) ->
- client_request(Transport, Port, Method, TestReqs)
+ mochiweb_test_util:client_request(Transport, Port, Method, TestReqs)
end.
-client_request(Transport, Port, Method, TestReqs) ->
- Opts = [binary, {active, false}, {packet, http}],
- SockFun = case Transport of
- plain ->
- {ok, Socket} = gen_tcp:connect("127.0.0.1", Port, Opts),
- fun (recv) ->
- gen_tcp:recv(Socket, 0);
- ({recv, Length}) ->
- gen_tcp:recv(Socket, Length);
- ({send, Data}) ->
- gen_tcp:send(Socket, Data);
- ({setopts, L}) ->
- inet:setopts(Socket, L)
- end;
- ssl ->
- {ok, Socket} = ssl:connect("127.0.0.1", Port, [{ssl_imp, new} | Opts]),
- fun (recv) ->
- ssl:recv(Socket, 0);
- ({recv, Length}) ->
- ssl:recv(Socket, Length);
- ({send, Data}) ->
- ssl:send(Socket, Data);
- ({setopts, L}) ->
- ssl:setopts(Socket, L)
- end
- end,
- client_request(SockFun, Method, TestReqs).
+close_on_unread_data_test() ->
+ ok = with_server(
+ plain,
+ fun mochiweb_request:not_found/1,
+ fun close_on_unread_data_client/2).
-client_request(SockFun, _Method, []) ->
- {the_end, {error, closed}} = {the_end, SockFun(recv)},
- ok;
-client_request(SockFun, Method,
- [#treq{path=Path, body=Body, xreply=ExReply} | Rest]) ->
- Request = [atom_to_list(Method), " ", Path, " HTTP/1.1\r\n",
- client_headers(Body, Rest =:= []),
- "\r\n",
- Body],
- ok = SockFun({send, Request}),
- case Method of
- 'GET' ->
- {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv);
- 'POST' ->
- {ok, {http_response, {1,1}, 201, "Created"}} = SockFun(recv);
- 'CONNECT' ->
- {ok, {http_response, {1,1}, 200, "OK"}} = SockFun(recv)
- end,
- ok = SockFun({setopts, [{packet, httph}]}),
- {ok, {http_header, _, 'Server', _, "MochiWeb" ++ _}} = SockFun(recv),
- {ok, {http_header, _, 'Date', _, _}} = SockFun(recv),
- {ok, {http_header, _, 'Content-Type', _, _}} = SockFun(recv),
- {ok, {http_header, _, 'Content-Length', _, ConLenStr}} = SockFun(recv),
- ContentLength = list_to_integer(ConLenStr),
- {ok, http_eoh} = SockFun(recv),
- ok = SockFun({setopts, [{packet, raw}]}),
- {payload, ExReply} = {payload, drain_reply(SockFun, ContentLength, <<>>)},
+close_on_unread_data_client(Transport, Port) ->
+ SockFun = mochiweb_test_util:sock_fun(Transport, Port),
+ %% A normal GET request should not trigger this behavior
+ Request0 = string:join(
+ ["GET / HTTP/1.1",
+ "Host: localhost",
+ "",
+ ""],
+ "\r\n"),
ok = SockFun({setopts, [{packet, http}]}),
- client_request(SockFun, Method, Rest).
-
-client_headers(Body, IsLastRequest) ->
- ["Host: localhost\r\n",
- case Body of
- <<>> ->
- "";
- _ ->
- ["Content-Type: application/octet-stream\r\n",
- "Content-Length: ", integer_to_list(byte_size(Body)), "\r\n"]
- end,
- case IsLastRequest of
- true ->
- "Connection: close\r\n";
- false ->
- ""
- end].
-
-drain_reply(_SockFun, 0, Acc) ->
- Acc;
-drain_reply(SockFun, Length, Acc) ->
- Sz = erlang:min(Length, 1024),
- {ok, B} = SockFun({recv, Sz}),
- drain_reply(SockFun, Length - Sz, <<Acc/bytes, B/bytes>>).
+ ok = SockFun({send, Request0}),
+ ?assertMatch(
+ {ok, {http_response, {1, 1}, 404, _}},
+ SockFun(recv)),
+ Headers0 = mochiweb_test_util:read_server_headers(SockFun),
+ ?assertEqual(
+ undefined,
+ mochiweb_headers:get_value("Connection", Headers0)),
+ Len0 = list_to_integer(
+ mochiweb_headers:get_value("Content-Length", Headers0)),
+ _Body0 = mochiweb_test_util:drain_reply(SockFun, Len0, <<>>),
+ %% Re-use same socket
+ Request = string:join(
+ ["POST / HTTP/1.1",
+ "Host: localhost",
+ "Content-Type: application/json",
+ "Content-Length: 2",
+ "",
+ "{}"],
+ "\r\n"),
+ ok = SockFun({setopts, [{packet, http}]}),
+ ok = SockFun({send, Request}),
+ ?assertMatch(
+ {ok, {http_response, {1, 1}, 404, _}},
+ SockFun(recv)),
+ Headers = mochiweb_test_util:read_server_headers(SockFun),
+ %% Expect to see a Connection: close header when we know the
+ %% server will close the connection re #146
+ ?assertEqual(
+ "close",
+ mochiweb_headers:get_value("Connection", Headers)),
+ Len = list_to_integer(mochiweb_headers:get_value("Content-Length", Headers)),
+ _Body = mochiweb_test_util:drain_reply(SockFun, Len, <<>>),
+ ?assertEqual({error, closed}, SockFun(recv)),
+ ok.
diff --git a/test/mochiweb_websocket_tests.erl b/test/mochiweb_websocket_tests.erl
index 890aa17..eb8de5b 100644
--- a/test/mochiweb_websocket_tests.erl
+++ b/test/mochiweb_websocket_tests.erl
@@ -82,3 +82,79 @@
mochiweb_websocket:parse_hixie_frames(
<<0,102,111,111,255,0,98,97,114,255>>,
[])).
+
+end_to_end_test_factory(ServerTransport) ->
+ mochiweb_test_util:with_server(
+ ServerTransport,
+ fun end_to_end_server/1,
+ fun (Transport, Port) ->
+ end_to_end_client(mochiweb_test_util:sock_fun(Transport, Port))
+ end).
+
+end_to_end_server(Req) ->
+ ?assertEqual("Upgrade", Req:get_header_value("connection")),
+ ?assertEqual("websocket", Req:get_header_value("upgrade")),
+ {ReentryWs, _ReplyChannel} = mochiweb_websocket:upgrade_connection(
+ Req,
+ fun end_to_end_ws_loop/3),
+ ReentryWs(ok).
+
+end_to_end_ws_loop(Payload, State, ReplyChannel) ->
+ %% Echo server
+ lists:foreach(ReplyChannel, Payload),
+ State.
+
+end_to_end_client(S) ->
+ %% Key and Accept per https://tools.ietf.org/html/rfc6455
+ UpgradeReq = string:join(
+ ["GET / HTTP/1.1",
+ "Host: localhost",
+ "Upgrade: websocket",
+ "Connection: Upgrade",
+ "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==",
+ "",
+ ""], "\r\n"),
+ ok = S({send, UpgradeReq}),
+ {ok, {http_response, {1,1}, 101, _}} = S(recv),
+ read_expected_headers(
+ S,
+ [{'Upgrade', "websocket"},
+ {'Connection', "Upgrade"},
+ {'Content-Length', "0"},
+ {"Sec-Websocket-Accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="}]),
+ %% The first message sent over telegraph :)
+ SmallMessage = <<"What hath God wrought?">>,
+ ok = S({send,
+ << 1:1, %% Fin
+ 0:1, %% Rsv1
+ 0:1, %% Rsv2
+ 0:1, %% Rsv3
+ 2:4, %% Opcode, 1 = text frame
+ 1:1, %% Mask on
+ (byte_size(SmallMessage)):7, %% Length, <125 case
+ 0:32, %% Mask (trivial)
+ SmallMessage/binary >>}),
+ {ok, WsFrames} = S(recv),
+ << 1:1, %% Fin
+ 0:1, %% Rsv1
+ 0:1, %% Rsv2
+ 0:1, %% Rsv3
+ 1:4, %% Opcode, text frame (all mochiweb suports for now)
+ MsgSize:8, %% Expecting small size
+ SmallMessage/binary >> = WsFrames,
+ ?assertEqual(MsgSize, byte_size(SmallMessage)),
+ ok.
+
+read_expected_headers(S, D) ->
+ Headers = mochiweb_test_util:read_server_headers(S),
+ lists:foreach(
+ fun ({K, V}) ->
+ ?assertEqual(V, mochiweb_headers:get_value(K, Headers))
+ end,
+ D).
+
+end_to_end_http_test() ->
+ end_to_end_test_factory(plain).
+
+end_to_end_https_test() ->
+ end_to_end_test_factory(ssl).