aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ROADMAP.md139
-rw-r--r--ebin/.gitignore0
-rw-r--r--examples/README.md6
-rw-r--r--include/http.hrl5
-rw-r--r--src/cowboy_acceptor.erl2
-rw-r--r--src/cowboy_acceptors_sup.erl8
-rw-r--r--src/cowboy_clock.erl15
-rw-r--r--src/cowboy_dispatcher.erl2
-rw-r--r--src/cowboy_http.erl124
-rw-r--r--src/cowboy_http_protocol.erl2
-rw-r--r--src/cowboy_http_req.erl54
-rw-r--r--src/cowboy_http_rest.erl44
-rw-r--r--src/cowboy_http_static.erl34
-rw-r--r--src/cowboy_http_websocket.erl8
-rw-r--r--src/cowboy_listener.erl15
-rw-r--r--src/cowboy_multipart.erl6
-rw-r--r--src/cowboy_requests_sup.erl8
-rw-r--r--src/cowboy_ssl_transport.erl22
-rw-r--r--src/cowboy_sup.erl8
-rw-r--r--src/cowboy_tcp_transport.erl4
-rw-r--r--test/http_SUITE.erl71
-rw-r--r--test/rest_resource_etags.erl30
22 files changed, 471 insertions, 136 deletions
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..a261caf
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,139 @@
+ROADMAP
+=======
+
+This document explains in as much details as possible the
+list of planned changes and work to be done on the Cowboy
+server. It is non-exhaustive and subject to change. Items
+are not ordered.
+
+* Write more, better examples.
+
+ The first step would be to port misultin's examples
+ to Cowboy. Then these examples could be completed with
+ examples for Cowboy specific features.
+
+ The extend/cowboy_examples is to be used for this. As
+ it is a separate repository, we can organize the file
+ structure as appropriate. Ideally we would have one
+ complete example per folder.
+
+ Examples should be commented. They may or may not be
+ used for writing the user guides.
+
+* Write user guides.
+
+ We currently have good API documentation, but no step
+ by step user guides.
+
+* Write more, better tests.
+
+ Amongst the areas less tested there is protocol upgrades
+ and the REST handler.
+
+ Current tests should be completed with unit tests
+ where applicable. We should probably also test the
+ dependencies used, like erlang:decode_packet/3.
+
+ While eunit and ct tests are fine, some parts of the
+ code could benefit from PropEr tests.
+
+* Continuous performance testing.
+
+ Initially dubbed the Horse project, Cowboy could benefit
+ from a continuous performance testing tool that would
+ allow us to easily compare the impact of the changes we
+ are introducing, similar to what the Phoronix test suite
+ allows.
+
+ Depending on the test it may be interesting to compare
+ Cowboy to other servers and eventually take ideas from
+ the servers that outperform Cowboy for the task being tested.
+
+* Improve HTTP/1.0 support.
+
+ Most of the work on Cowboy has been done with HTTP/1.1
+ in mind. But there is still a need for HTTP/1.0 code in
+ Cowboy. The server code should be reviewed and tested
+ to ensure compatibility with remaining HTTP/1.0 products.
+
+* HTTP 100 Continue support.
+
+ Tools like curl expect a 100 Continue before sending a
+ request body by default.
+
+* Content-Encoding support.
+
+ Cowboy should be able to send encoded content automatically.
+ The default should be to encode, but the developer must be
+ able to override this default either for the whole listener
+ or just for a single reply.
+
+* Improve body reading API.
+
+ We want to have various different things for reading the
+ body. First, there should be raw functions for the different
+ ways to read the body: basic, transfer encoded, multipart.
+ Each should allow us to limit the size of what is read.
+
+ On top of these functions there should be two more
+ advanced functions: one would return the result of parsing
+ a x-www-form-urlencoded body; the other would parse a
+ multipart request, save files from the multipart data to
+ a temporary location and return a proplist of values if any
+ along with the files details. This behavior is similar to
+ what is done automatically by PHP with its $_FILES array.
+
+ The advanced functions are of course here for convenience
+ only and it should be trivial to reimplement them directly
+ in a Cowboy application if needed.
+
+* Complete the work on Websockets.
+
+ Now that the Autobahn test suite is available (make inttests),
+ we have a definite way to know whether Cowboy's implementation
+ of Websockets is right. The work can thus be completed. The
+ remaining tasks are proper UTF8 handling and fragmentation.
+
+* SPDY support.
+
+ While SPDY probably won't be added directly to Cowboy, work
+ has been started on making Cowboy use SPDY.
+
+* Hooks.
+
+ Customizable hooks would allow the developer to extend Cowboy
+ easily. Two kinds of hooks are needed: before dispatching the
+ request, and before sending a reply.
+
+ The first would allow us to apply site-wide functions like
+ authentication or request logging and modify the Req if needed.
+
+ The second is more interesting for response logging or to
+ filter the replies, for example to send custom error pages.
+
+* Transport upgrades.
+
+ Some protocols allow an upgrade from TCP to SSL without
+ closing the connection. This is currently not possible
+ through the Cowboy API.
+
+* Resizing the acceptor pool.
+
+ We should be able to add more acceptors to a pool but also
+ to remove some of them as needed.
+
+* Simplified dispatch list.
+
+ For convenience purposes, the dispatch list should allow
+ lists instead of binaries. The lists can be converted to
+ binary by Cowboy at listener initialization.
+
+ There has also been discussion on allowing the dispatch
+ list to be hierarchical.
+
+* Add Transport:secure/0.
+
+ Currently Cowboy checks if a connection is secure by
+ checking if its name is 'ssl'. This isn't a very modular
+ solution, adding an API function that returns whether
+ a connection is secure would fix that issue.
diff --git a/ebin/.gitignore b/ebin/.gitignore
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/ebin/.gitignore
diff --git a/examples/README.md b/examples/README.md
new file mode 100644
index 0000000..dc88057
--- /dev/null
+++ b/examples/README.md
@@ -0,0 +1,6 @@
+Cowboy examples
+===============
+
+The Cowboy examples can be found in a separate repository:
+
+* https://github.com/extend/cowboy_examples
diff --git a/include/http.hrl b/include/http.hrl
index c66f2b0..9ba3787 100644
--- a/include/http.hrl
+++ b/include/http.hrl
@@ -23,11 +23,12 @@
pid = undefined :: pid(),
method = 'GET' :: cowboy_http:method(),
version = {1, 1} :: cowboy_http:version(),
- peer = undefined :: undefined | {inet:ip_address(), inet:ip_port()},
+ peer = undefined :: undefined |
+ {inet:ip_address(), inet:port_number()},
host = undefined :: undefined | cowboy_dispatcher:tokens(),
host_info = undefined :: undefined | cowboy_dispatcher:tokens(),
raw_host = undefined :: undefined | binary(),
- port = undefined :: undefined | inet:ip_port(),
+ port = undefined :: undefined | inet:port_number(),
path = undefined :: undefined | '*' | cowboy_dispatcher:tokens(),
path_info = undefined :: undefined | cowboy_dispatcher:tokens(),
raw_path = undefined :: undefined | binary(),
diff --git a/src/cowboy_acceptor.erl b/src/cowboy_acceptor.erl
index 29f7c09..b2a1ef0 100644
--- a/src/cowboy_acceptor.erl
+++ b/src/cowboy_acceptor.erl
@@ -41,7 +41,7 @@ acceptor(LSocket, Transport, Protocol, Opts, OptsVsn, ListenerPid, ReqsSup) ->
cowboy_listener:add_connection(ListenerPid,
default, Pid, OptsVsn);
{error, timeout} ->
- ok;
+ cowboy_listener:check_upgrades(ListenerPid, OptsVsn);
{error, _Reason} ->
%% @todo Probably do something here. If the socket was closed,
%% we may want to try and listen again on the port?
diff --git a/src/cowboy_acceptors_sup.erl b/src/cowboy_acceptors_sup.erl
index 625028c..7c962d2 100644
--- a/src/cowboy_acceptors_sup.erl
+++ b/src/cowboy_acceptors_sup.erl
@@ -30,7 +30,13 @@ start_link(NbAcceptors, Transport, TransOpts,
%% supervisor.
--spec init(list()) -> {ok, {{one_for_one, 10, 10}, list()}}.
+-spec init([any()]) -> {'ok', {{'one_for_one', 10, 10}, [{
+ any(), {atom() | tuple(), atom(), 'undefined' | [any()]},
+ 'permanent' | 'temporary' | 'transient',
+ 'brutal_kill' | 'infinity' | non_neg_integer(),
+ 'supervisor' | 'worker',
+ 'dynamic' | [atom() | tuple()]}]
+}}.
init([NbAcceptors, Transport, TransOpts,
Protocol, ProtoOpts, ListenerPid, ReqsPid]) ->
{ok, LSocket} = Transport:listen(TransOpts),
diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl
index c699f4f..e22b718 100644
--- a/src/cowboy_clock.erl
+++ b/src/cowboy_clock.erl
@@ -64,7 +64,20 @@ rfc2109(LocalTime) ->
{{YYYY,MM,DD},{Hour,Min,Sec}} =
case calendar:local_time_to_universal_time_dst(LocalTime) of
[Gmt] -> Gmt;
- [_,Gmt] -> Gmt
+ [_,Gmt] -> Gmt;
+ [] ->
+ %% The localtime generated by cowboy_cookies may fall within
+ %% the hour that is skipped by daylight savings time. If this
+ %% is such a localtime, increment the localtime with one hour
+ %% and try again, if this succeeds, subtracting the max_age
+ %% from the resulting universaltime and converting to a local
+ %% time will yield the original localtime.
+ {Date, {Hour1, Min1, Sec1}} = LocalTime,
+ LocalTime2 = {Date, {Hour1 + 1, Min1, Sec1}},
+ case calendar:local_time_to_universal_time_dst(LocalTime2) of
+ [Gmt] -> Gmt;
+ [_,Gmt] -> Gmt
+ end
end,
Wday = calendar:day_of_the_week({YYYY,MM,DD}),
DayBin = pad_int(DD),
diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl
index 22f6e1e..db40e63 100644
--- a/src/cowboy_dispatcher.erl
+++ b/src/cowboy_dispatcher.erl
@@ -33,7 +33,7 @@
%% @doc Split a hostname into a list of tokens.
-spec split_host(binary())
- -> {tokens(), binary(), undefined | inet:ip_port()}.
+ -> {tokens(), binary(), undefined | inet:port_number()}.
split_host(<<>>) ->
{[], <<>>, undefined};
split_host(Host) ->
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index 32b0ca9..9d727f3 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -17,14 +17,14 @@
-module(cowboy_http).
%% Parsing.
--export([list/2, nonempty_list/2, content_type/1, content_type_params/3,
- media_range/2, conneg/2, language_range/2, entity_tag_match/1,
+-export([list/2, nonempty_list/2, content_type/1, media_range/2, conneg/2,
+ language_range/2, entity_tag_match/1, expectation/2, params/2,
http_date/1, rfc1123_date/1, rfc850_date/1, asctime_date/1,
whitespace/2, digits/1, token/2, token_ci/2, quoted_string/2]).
%% Interpretation.
-export([connection_to_atom/1, urldecode/1, urldecode/2, urlencode/1,
- urlencode/2]).
+ urlencode/2, x_www_form_urlencoded/2]).
-type method() :: 'OPTIONS' | 'GET' | 'HEAD'
| 'POST' | 'PUT' | 'DELETE' | 'TRACE' | binary().
@@ -51,7 +51,6 @@
-export_type([method/0, uri/0, version/0, header/0, headers/0, status/0]).
--include("include/http.hrl").
-include_lib("eunit/include/eunit.hrl").
%% Parsing.
@@ -98,33 +97,9 @@ list(Data, Fun, Acc) ->
content_type(Data) ->
media_type(Data,
fun (Rest, Type, SubType) ->
- content_type_params(Rest,
- fun (Params) -> {Type, SubType, Params} end, [])
- end).
-
--spec content_type_params(binary(), fun(), list({binary(), binary()}))
- -> any().
-content_type_params(Data, Fun, Acc) ->
- whitespace(Data,
- fun (<< $;, Rest/binary >>) -> content_type_param(Rest, Fun, Acc);
- (<<>>) -> Fun(lists:reverse(Acc));
- (_Rest) -> {error, badarg}
- end).
-
--spec content_type_param(binary(), fun(), list({binary(), binary()}))
- -> any().
-content_type_param(Data, Fun, Acc) ->
- whitespace(Data,
- fun (Rest) ->
- token_ci(Rest,
- fun (_Rest2, <<>>) -> {error, badarg};
- (<< $=, Rest2/binary >>, Attr) ->
- word(Rest2,
- fun (Rest3, Value) ->
- content_type_params(Rest3, Fun,
- [{Attr, Value}|Acc])
- end);
- (_Rest2, _Attr) -> {error, badarg}
+ params(Rest,
+ fun (<<>>, Params) -> {Type, SubType, Params};
+ (_Rest2, _) -> {error, badarg}
end)
end).
@@ -181,6 +156,13 @@ media_type(Data, Fun) ->
fun (_Rest2, <<>>) -> {error, badarg};
(Rest2, SubType) -> Fun(Rest2, Type, SubType)
end);
+ %% This is a non-strict parsing clause required by some user agents
+ %% that use * instead of */* in the list of media types.
+ (Rest, <<"*">> = Type) ->
+ token_ci(<<"*", Rest/binary>>,
+ fun (_Rest2, <<>>) -> {error, badarg};
+ (Rest2, SubType) -> Fun(Rest2, Type, SubType)
+ end);
(_Rest, _Type) -> {error, badarg}
end).
@@ -319,6 +301,50 @@ opaque_tag(Data, Fun, Strength) ->
(Rest, OpaqueTag) -> Fun(Rest, {Strength, OpaqueTag})
end).
+%% @doc Parse an expectation.
+-spec expectation(binary(), fun()) -> any().
+expectation(Data, Fun) ->
+ token_ci(Data,
+ fun (_Rest, <<>>) -> {error, badarg};
+ (<< $=, Rest/binary >>, Expectation) ->
+ word(Rest,
+ fun (Rest2, ExtValue) ->
+ params(Rest2, fun (Rest3, ExtParams) ->
+ Fun(Rest3, {Expectation, ExtValue, ExtParams})
+ end)
+ end);
+ (Rest, Expectation) ->
+ Fun(Rest, Expectation)
+ end).
+
+%% @doc Parse a list of parameters (a=b;c=d).
+-spec params(binary(), fun()) -> any().
+params(Data, Fun) ->
+ params(Data, Fun, []).
+
+-spec params(binary(), fun(), [{binary(), binary()}]) -> any().
+params(Data, Fun, Acc) ->
+ whitespace(Data,
+ fun (<< $;, Rest/binary >>) -> param(Rest, Fun, Acc);
+ (Rest) -> Fun(Rest, lists:reverse(Acc))
+ end).
+
+-spec param(binary(), fun(), [{binary(), binary()}]) -> any().
+param(Data, Fun, Acc) ->
+ whitespace(Data,
+ fun (Rest) ->
+ token_ci(Rest,
+ fun (_Rest2, <<>>) -> {error, badarg};
+ (<< $=, Rest2/binary >>, Attr) ->
+ word(Rest2,
+ fun (Rest3, Value) ->
+ params(Rest3, Fun,
+ [{Attr, Value}|Acc])
+ end);
+ (_Rest2, _Attr) -> {error, badarg}
+ end)
+ end).
+
%% @doc Parse an HTTP date (RFC1123, RFC850 or asctime date).
%% @end
%%
@@ -657,6 +683,9 @@ quoted_string(<< C, Rest/binary >>, Fun, Acc) ->
-spec qvalue(binary(), fun()) -> any().
qvalue(<< $0, $., Rest/binary >>, Fun) ->
qvalue(Rest, Fun, 0, 100);
+%% Some user agents use q=.x instead of q=0.x
+qvalue(<< $., Rest/binary >>, Fun) ->
+ qvalue(Rest, Fun, 0, 100);
qvalue(<< $0, Rest/binary >>, Fun) ->
Fun(Rest, 0);
qvalue(<< $1, $., $0, $0, $0, Rest/binary >>, Fun) ->
@@ -780,6 +809,16 @@ tohexu(C) when C < 17 -> $A + C - 10.
tohexl(C) when C < 10 -> $0 + C;
tohexl(C) when C < 17 -> $a + C - 10.
+-spec x_www_form_urlencoded(binary(), fun((binary()) -> binary())) ->
+ list({binary(), binary() | true}).
+x_www_form_urlencoded(<<>>, _URLDecode) ->
+ [];
+x_www_form_urlencoded(Qs, URLDecode) ->
+ Tokens = binary:split(Qs, <<"&">>, [global, trim]),
+ [case binary:split(Token, <<"=">>) of
+ [Token] -> {URLDecode(Token), true};
+ [Name, Value] -> {URLDecode(Name), URLDecode(Value)}
+ end || Token <- Tokens].
%% Tests.
@@ -865,6 +904,13 @@ media_range_list_test_() ->
[{<<"level">>, <<"1">>}, {<<"quoted">>, <<"hi hi hi">>}]}, 123,
[<<"standalone">>, {<<"complex">>, <<"gits">>}]},
{{<<"text">>, <<"plain">>, []}, 1000, []}
+ ]},
+ {<<"text/html, image/gif, image/jpeg, *; q=.2, */*; q=.2">>, [
+ {{<<"text">>, <<"html">>, []}, 1000, []},
+ {{<<"image">>, <<"gif">>, []}, 1000, []},
+ {{<<"image">>, <<"jpeg">>, []}, 1000, []},
+ {{<<"*">>, <<"*">>, []}, 200, []},
+ {{<<"*">>, <<"*">>, []}, 200, []}
]}
],
[{V, fun() -> R = list(V, fun media_range/2) end} || {V, R} <- Tests].
@@ -947,6 +993,22 @@ digits_test_() ->
],
[{V, fun() -> R = digits(V) end} || {V, R} <- Tests].
+x_www_form_urlencoded_test_() ->
+ %% {Qs, Result}
+ Tests = [
+ {<<"">>, []},
+ {<<"a=b">>, [{<<"a">>, <<"b">>}]},
+ {<<"aaa=bbb">>, [{<<"aaa">>, <<"bbb">>}]},
+ {<<"a&b">>, [{<<"a">>, true}, {<<"b">>, true}]},
+ {<<"a=b&c&d=e">>, [{<<"a">>, <<"b">>},
+ {<<"c">>, true}, {<<"d">>, <<"e">>}]},
+ {<<"a=b=c=d=e&f=g">>, [{<<"a">>, <<"b=c=d=e">>}, {<<"f">>, <<"g">>}]},
+ {<<"a+b=c+d">>, [{<<"a b">>, <<"c d">>}]}
+ ],
+ URLDecode = fun urldecode/1,
+ [{Qs, fun() -> R = x_www_form_urlencoded(
+ Qs, URLDecode) end} || {Qs, R} <- Tests].
+
urldecode_test_() ->
U = fun urldecode/2,
[?_assertEqual(<<" ">>, U(<<"%20">>, crash)),
diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl
index d7ba508..71518fa 100644
--- a/src/cowboy_http_protocol.erl
+++ b/src/cowboy_http_protocol.erl
@@ -38,7 +38,7 @@
-export([start_link/4]). %% API.
-export([init/4, parse_request/1, handler_loop/3]). %% FSM.
--include("include/http.hrl").
+-include("http.hrl").
-include_lib("eunit/include/eunit.hrl").
-record(state, {
diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl
index 92d96ad..6b947d9 100644
--- a/src/cowboy_http_req.erl
+++ b/src/cowboy_http_req.erl
@@ -50,8 +50,7 @@
compact/1, transport/1
]). %% Misc API.
--include("include/http.hrl").
--include_lib("eunit/include/eunit.hrl").
+-include("http.hrl").
%% Request API.
@@ -66,7 +65,8 @@ version(Req) ->
{Req#http_req.version, Req}.
%% @doc Return the peer address and port number of the remote host.
--spec peer(#http_req{}) -> {{inet:ip_address(), inet:ip_port()}, #http_req{}}.
+-spec peer(#http_req{})
+ -> {{inet:ip_address(), inet:port_number()}, #http_req{}}.
peer(Req=#http_req{socket=Socket, transport=Transport, peer=undefined}) ->
{ok, Peer} = Transport:peername(Socket),
{Peer, Req#http_req{peer=Peer}};
@@ -114,7 +114,7 @@ raw_host(Req) ->
{Req#http_req.raw_host, Req}.
%% @doc Return the port used for this request.
--spec port(#http_req{}) -> {inet:ip_port(), #http_req{}}.
+-spec port(#http_req{}) -> {inet:port_number(), #http_req{}}.
port(Req) ->
{Req#http_req.port, Req}.
@@ -151,7 +151,8 @@ qs_val(Name, Req) when is_binary(Name) ->
-> {binary() | true | Default, #http_req{}} when Default::any().
qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined,
urldecode={URLDecFun, URLDecArg}}, Default) when is_binary(Name) ->
- QsVals = parse_qs(RawQs, fun(Bin) -> URLDecFun(Bin, URLDecArg) end),
+ QsVals = cowboy_http:x_www_form_urlencoded(
+ RawQs, fun(Bin) -> URLDecFun(Bin, URLDecArg) end),
qs_val(Name, Req#http_req{qs_vals=QsVals}, Default);
qs_val(Name, Req, Default) ->
case lists:keyfind(Name, 1, Req#http_req.qs_vals) of
@@ -163,7 +164,8 @@ qs_val(Name, Req, Default) ->
-spec qs_vals(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined,
urldecode={URLDecFun, URLDecArg}}) ->
- QsVals = parse_qs(RawQs, fun(Bin) -> URLDecFun(Bin, URLDecArg) end),
+ QsVals = cowboy_http:x_www_form_urlencoded(
+ RawQs, fun(Bin) -> URLDecFun(Bin, URLDecArg) end),
qs_vals(Req#http_req{qs_vals=QsVals});
qs_vals(Req=#http_req{qs_vals=QsVals}) ->
{QsVals, Req}.
@@ -271,6 +273,11 @@ parse_header(Name, Req, Default) when Name =:= 'Content-Type' ->
fun (Value) ->
cowboy_http:content_type(Value)
end);
+parse_header(Name, Req, Default) when Name =:= 'Expect' ->
+ parse_header(Name, Req, Default,
+ fun (Value) ->
+ cowboy_http:nonempty_list(Value, fun cowboy_http:expectation/2)
+ end);
parse_header(Name, Req, Default)
when Name =:= 'If-Match'; Name =:= 'If-None-Match' ->
parse_header(Name, Req, Default,
@@ -400,7 +407,8 @@ body(Length, Req=#http_req{socket=Socket, transport=Transport,
-spec body_qs(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
body_qs(Req=#http_req{urldecode={URLDecFun, URLDecArg}}) ->
{ok, Body, Req2} = body(Req),
- {parse_qs(Body, fun(Bin) -> URLDecFun(Bin, URLDecArg) end), Req2}.
+ {cowboy_http:x_www_form_urlencoded(
+ Body, fun(Bin) -> URLDecFun(Bin, URLDecArg) end), Req2}.
%% Multipart Request API.
@@ -636,17 +644,6 @@ transport(#http_req{transport=Transport, socket=Socket}) ->
%% Internal.
--spec parse_qs(binary(), fun((binary()) -> binary())) ->
- list({binary(), binary() | true}).
-parse_qs(<<>>, _URLDecode) ->
- [];
-parse_qs(Qs, URLDecode) ->
- Tokens = binary:split(Qs, <<"&">>, [global, trim]),
- [case binary:split(Token, <<"=">>) of
- [Token] -> {URLDecode(Token), true};
- [Name, Value] -> {URLDecode(Name), URLDecode(Value)}
- end || Token <- Tokens].
-
-spec response_connection(cowboy_http:headers(), keepalive | close)
-> keepalive | close.
response_connection([], Connection) ->
@@ -808,24 +805,3 @@ header_to_binary('Cookie') -> <<"Cookie">>;
header_to_binary('Keep-Alive') -> <<"Keep-Alive">>;
header_to_binary('Proxy-Connection') -> <<"Proxy-Connection">>;
header_to_binary(B) when is_binary(B) -> B.
-
-%% Tests.
-
--ifdef(TEST).
-
-parse_qs_test_() ->
- %% {Qs, Result}
- Tests = [
- {<<"">>, []},
- {<<"a=b">>, [{<<"a">>, <<"b">>}]},
- {<<"aaa=bbb">>, [{<<"aaa">>, <<"bbb">>}]},
- {<<"a&b">>, [{<<"a">>, true}, {<<"b">>, true}]},
- {<<"a=b&c&d=e">>, [{<<"a">>, <<"b">>},
- {<<"c">>, true}, {<<"d">>, <<"e">>}]},
- {<<"a=b=c=d=e&f=g">>, [{<<"a">>, <<"b=c=d=e">>}, {<<"f">>, <<"g">>}]},
- {<<"a+b=c+d">>, [{<<"a b">>, <<"c d">>}]}
- ],
- URLDecode = fun cowboy_http:urldecode/1,
- [{Qs, fun() -> R = parse_qs(Qs, URLDecode) end} || {Qs, R} <- Tests].
-
--endif.
diff --git a/src/cowboy_http_rest.erl b/src/cowboy_http_rest.erl
index 589183d..c19d838 100644
--- a/src/cowboy_http_rest.erl
+++ b/src/cowboy_http_rest.erl
@@ -41,12 +41,12 @@
charset_a :: undefined | binary(),
%% Cached resource calls.
- etag :: undefined | no_call | binary(),
+ etag :: undefined | no_call | {strong | weak, binary()},
last_modified :: undefined | no_call | calendar:datetime(),
expires :: undefined | no_call | calendar:datetime()
}).
--include("include/http.hrl").
+-include("http.hrl").
%% @doc Upgrade a HTTP request to the REST protocol.
%%
@@ -487,14 +487,10 @@ if_match_exists(Req, State) ->
if_match(Req, State, EtagsList) ->
{Etag, Req2, State2} = generate_etag(Req, State),
- case Etag of
- no_call ->
- precondition_failed(Req2, State2);
- Etag ->
- case lists:member(Etag, EtagsList) of
- true -> if_unmodified_since_exists(Req2, State2);
- false -> precondition_failed(Req2, State2)
- end
+ case lists:member(Etag, EtagsList) of
+ true -> if_unmodified_since_exists(Req2, State2);
+ %% Etag may be `undefined' which cannot be a member.
+ false -> precondition_failed(Req2, State2)
end.
if_match_musnt_exist(Req, State) ->
@@ -534,7 +530,7 @@ if_none_match_exists(Req, State) ->
if_none_match(Req, State, EtagsList) ->
{Etag, Req2, State2} = generate_etag(Req, State),
case Etag of
- no_call ->
+ undefined ->
precondition_failed(Req2, State2);
Etag ->
case lists:member(Etag, EtagsList) of
@@ -699,7 +695,7 @@ process_post(Req, State) ->
terminate(Req2, State#state{handler_state=HandlerState});
{true, Req2, HandlerState} ->
State2 = State#state{handler_state=HandlerState},
- next(Req2, State2, 201);
+ next(Req2, State2, fun is_new_resource/2);
{false, Req2, HandlerState} ->
State2 = State#state{handler_state=HandlerState},
respond(Req2, State2, 500)
@@ -733,11 +729,14 @@ put_resource(Req, State, OnTrue) ->
choose_content_type(Req3, State2, OnTrue, ContentType, CTA)
end.
+%% The special content type '*' will always match. It can be used as a
+%% catch-all content type for accepting any kind of request content.
+%% Note that because it will always match, it should be the last of the
+%% list of content types, otherwise it'll shadow the ones following.
choose_content_type(Req, State, _OnTrue, _ContentType, []) ->
respond(Req, State, 415);
-choose_content_type(Req, State, OnTrue, ContentType,
- [{Accepted, Fun}|_Tail])
- when Accepted =:= '*' orelse ContentType =:= Accepted ->
+choose_content_type(Req, State, OnTrue, ContentType, [{Accepted, Fun}|_Tail])
+ when Accepted =:= '*' orelse Accepted =:= ContentType ->
case call(Req, State, Fun) of
{halt, Req2, HandlerState} ->
terminate(Req2, State#state{handler_state=HandlerState});
@@ -811,10 +810,14 @@ set_resp_etag(Req, State) ->
{Req2, State2};
Etag ->
{ok, Req3} = cowboy_http_req:set_resp_header(
- <<"Etag">>, Etag, Req2),
+ <<"ETag">>, encode_etag(Etag), Req2),
{Req3, State2}
end.
+-spec encode_etag({strong | weak, binary()}) -> iolist().
+encode_etag({strong, Etag}) -> [$",Etag,$"];
+encode_etag({weak, Etag}) -> ["W/\"",Etag,$"].
+
set_resp_expires(Req, State) ->
{Expires, Req2, State2} = expires(Req, State),
case Expires of
@@ -835,6 +838,15 @@ generate_etag(Req, State=#state{etag=undefined}) ->
case call(Req, State, generate_etag) of
no_call ->
{undefined, Req, State#state{etag=no_call}};
+ %% Previously the return value from the generate_etag/2 callback was set
+ %% as the value of the ETag header in the response. Therefore the only
+ %% valid return type was `binary()'. If a handler returns a `binary()'
+ %% it must be mapped to the expected type or it'll always fail to
+ %% compare equal to any entity tags present in the request headers.
+ %% @todo Remove support for binary return values after 0.6.
+ {Etag, Req2, HandlerState} when is_binary(Etag) ->
+ [Etag2] = cowboy_http:entity_tag_match(Etag),
+ {Etag2, Req2, State#state{handler_state=HandlerState, etag=Etag2}};
{Etag, Req2, HandlerState} ->
{Etag, Req2, State#state{handler_state=HandlerState, etag=Etag}}
end;
diff --git a/src/cowboy_http_static.erl b/src/cowboy_http_static.erl
index 0ee996a..007cd16 100644
--- a/src/cowboy_http_static.erl
+++ b/src/cowboy_http_static.erl
@@ -96,15 +96,16 @@
%%
%% The default behaviour can be overridden to generate an ETag header based on
%% a combination of the file path, file size, inode and mtime values. If the
-%% option value is a list of attribute names tagged with `attributes' a hex
-%% encoded CRC32 checksum of the attribute values are used as the ETag header
-%% value.
+%% option value is a non-empty list of attribute names tagged with `attributes'
+%% a hex encoded checksum of each attribute specified is included in the value
+%% of the the ETag header. If the list of attribute names is empty no ETag
+%% header is generated.
%%
%% If a strong ETag is required a user defined function for generating the
%% header value can be supplied. The function must accept a proplist of the
%% file attributes as the first argument and a second argument containing any
-%% additional data that the function requires. The function must return a
-%% `binary()' or `undefined'.
+%% additional data that the function requires. The function must return a term
+%% of the type `{weak | strong, binary()}' or `undefined'.
%%
%% ==== Examples ====
%% ```
@@ -130,7 +131,7 @@
%% {_, _Modified} = lists:keyfind(mtime, 1, Arguments),
%% ChecksumCommand = lists:flatten(io_lib:format("sha1sum ~s", [Filepath])),
%% [Checksum|_] = string:tokens(os:cmd(ChecksumCommand), " "),
-%% iolist_to_binary(Checksum).
+%% {strong, iolist_to_binary(Checksum)}.
%% '''
-module(cowboy_http_static).
@@ -161,7 +162,8 @@
filepath :: binary() | error,
fileinfo :: {ok, #file_info{}} | {error, _} | error,
mimetypes :: {fun((binary(), T) -> [mimedef()]), T} | undefined,
- etag_fun :: {fun(([etagarg()], T) -> undefined | binary()), T}}).
+ etag_fun :: {fun(([etagarg()], T) ->
+ undefined | {strong | weak, binary()}), T}}).
%% @private Upgrade from HTTP handler to REST handler.
@@ -183,8 +185,9 @@ rest_init(Req, Opts) ->
ETagFunction = case proplists:get_value(etag, Opts) of
default -> {fun no_etag_function/2, undefined};
undefined -> {fun no_etag_function/2, undefined};
+ {attributes, []} -> {fun no_etag_function/2, undefined};
{attributes, Attrs} -> {fun attr_etag_function/2, Attrs};
- {_, _}=EtagFunction1 -> EtagFunction1
+ {_, _}=ETagFunction1 -> ETagFunction1
end,
{Filepath, Req1} = cowboy_http_req:path_info(Req),
State = case check_path(Filepath) of
@@ -411,16 +414,13 @@ no_etag_function(_Args, undefined) ->
%% @private A simple alternative is to send an ETag based on file attributes.
-type fileattr() :: filepath | filesize | mtime | inode.
--spec attr_etag_function([etagarg()], [fileattr()]) -> binary().
+-spec attr_etag_function([etagarg()], [fileattr()]) -> {strong, binary()}.
attr_etag_function(Args, Attrs) ->
- attr_etag_function(Args, Attrs, []).
-
--spec attr_etag_function([etagarg()], [fileattr()], [binary()]) -> binary().
-attr_etag_function(_Args, [], Acc) ->
- list_to_binary(integer_to_list(erlang:crc32(Acc), 16));
-attr_etag_function(Args, [H|T], Acc) ->
- {_, Value} = lists:keyfind(H, 1, Args),
- attr_etag_function(Args, T, [term_to_binary(Value)|Acc]).
+ [[_|H]|T] = [begin
+ {_,Pair} = {_,{_,_}} = {Attr,lists:keyfind(Attr, 1, Args)},
+ [$-|integer_to_list(erlang:phash2(Pair, 1 bsl 32), 16)]
+ end || Attr <- Attrs],
+ {strong, list_to_binary([H|T])}.
-ifdef(TEST).
diff --git a/src/cowboy_http_websocket.erl b/src/cowboy_http_websocket.erl
index ec75571..40fef23 100644
--- a/src/cowboy_http_websocket.erl
+++ b/src/cowboy_http_websocket.erl
@@ -40,7 +40,7 @@
-export([upgrade/4]). %% API.
-export([handler_loop/4]). %% Internal.
--include("include/http.hrl").
+-include("http.hrl").
-include_lib("eunit/include/eunit.hrl").
-type opcode() :: 0 | 1 | 2 | 8 | 9 | 10.
@@ -467,8 +467,8 @@ hixie76_key_to_integer(Key) ->
Spaces = length([C || << C >> <= Key, C =:= 32]),
Number div Spaces.
--spec hixie76_location(atom(), binary(), inet:ip_port(), binary(), binary())
- -> binary().
+-spec hixie76_location(atom(), binary(), inet:port_number(),
+ binary(), binary()) -> binary().
hixie76_location(Protocol, Host, Port, Path, <<>>) ->
<< (hixie76_location_protocol(Protocol))/binary, "://", Host/binary,
(hixie76_location_port(Protocol, Port))/binary, Path/binary>>;
@@ -482,7 +482,7 @@ hixie76_location_protocol(_) -> <<"ws">>.
%% @todo We should add a secure/0 function to transports
%% instead of relying on their name.
--spec hixie76_location_port(atom(), inet:ip_port()) -> binary().
+-spec hixie76_location_port(atom(), inet:port_number()) -> binary().
hixie76_location_port(ssl, 443) ->
<<>>;
hixie76_location_port(tcp, 80) ->
diff --git a/src/cowboy_listener.erl b/src/cowboy_listener.erl
index ad54941..4b2c2fb 100644
--- a/src/cowboy_listener.erl
+++ b/src/cowboy_listener.erl
@@ -17,7 +17,7 @@
-behaviour(gen_server).
-export([start_link/2, stop/1,
- add_connection/4, move_connection/3, remove_connection/2,
+ add_connection/4, move_connection/3, remove_connection/2, check_upgrades/2,
get_protocol_options/1, set_protocol_options/2]). %% API.
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]). %% gen_server.
@@ -84,6 +84,12 @@ move_connection(ServerPid, DestPool, ConnPid) ->
remove_connection(ServerPid, ConnPid) ->
gen_server:cast(ServerPid, {remove_connection, ConnPid}).
+%% @doc Return whether a protocol upgrade is required.
+-spec check_upgrades(pid(), non_neg_integer())
+ -> ok | {upgrade, any(), non_neg_integer()}.
+check_upgrades(ServerPid, OptsVsn) ->
+ gen_server:call(ServerPid, {check_upgrades, OptsVsn}).
+
%% @doc Return the current protocol options.
-spec get_protocol_options(pid()) -> {ok, any()}.
get_protocol_options(ServerPid) ->
@@ -121,6 +127,13 @@ handle_call({add_connection, Pool, ConnPid, AccOptsVsn}, From, State=#state{
true ->
{reply, ok, State2}
end;
+handle_call({check_upgrades, AccOptsVsn}, _From, State=#state{
+ proto_opts=ProtoOpts, proto_opts_vsn=LisOptsVsn}) ->
+ if AccOptsVsn =/= LisOptsVsn ->
+ {reply, {upgrade, ProtoOpts, LisOptsVsn}, State};
+ true ->
+ {reply, ok, State}
+ end;
handle_call(get_protocol_options, _From, State=#state{proto_opts=ProtoOpts}) ->
{reply, {ok, ProtoOpts}, State};
handle_call({set_protocol_options, ProtoOpts}, _From,
diff --git a/src/cowboy_multipart.erl b/src/cowboy_multipart.erl
index b7aeb54..2428b52 100644
--- a/src/cowboy_multipart.erl
+++ b/src/cowboy_multipart.erl
@@ -45,8 +45,10 @@ content_disposition(Data) ->
cowboy_http:token_ci(Data,
fun (_Rest, <<>>) -> {error, badarg};
(Rest, Disposition) ->
- cowboy_http:content_type_params(Rest,
- fun (Params) -> {Disposition, Params} end, [])
+ cowboy_http:params(Rest,
+ fun (<<>>, Params) -> {Disposition, Params};
+ (_Rest2, _) -> {error, badarg}
+ end)
end).
%% Internal.
diff --git a/src/cowboy_requests_sup.erl b/src/cowboy_requests_sup.erl
index 87d5352..ddd8d3b 100644
--- a/src/cowboy_requests_sup.erl
+++ b/src/cowboy_requests_sup.erl
@@ -32,7 +32,13 @@ start_request(ListenerPid, Socket, Transport, Protocol, Opts) ->
%% supervisor.
--spec init([]) -> {ok, {{simple_one_for_one, 0, 1}, [{_, _, _, _, _, _}, ...]}}.
+-spec init([]) -> {'ok', {{'simple_one_for_one', 0, 1}, [{
+ any(), {atom() | tuple(), atom(), 'undefined' | [any()]},
+ 'permanent' | 'temporary' | 'transient',
+ 'brutal_kill' | 'infinity' | non_neg_integer(),
+ 'supervisor' | 'worker',
+ 'dynamic' | [atom() | tuple()]}]
+}}.
init([]) ->
{ok, {{simple_one_for_one, 0, 1}, [{?MODULE, {?MODULE, start_request, []},
temporary, brutal_kill, worker, [?MODULE]}]}}.
diff --git a/src/cowboy_ssl_transport.erl b/src/cowboy_ssl_transport.erl
index bf8b1fb..ccd8e5a 100644
--- a/src/cowboy_ssl_transport.erl
+++ b/src/cowboy_ssl_transport.erl
@@ -58,8 +58,7 @@ messages() -> {ssl, ssl_closed, ssl_error}.
%% </dl>
%%
%% @see ssl:listen/2
-%% @todo The password option shouldn't be mandatory.
--spec listen([{port, inet:ip_port()} | {certfile, string()}
+-spec listen([{port, inet:port_number()} | {certfile, string()}
| {keyfile, string()} | {password, string()}
| {cacertfile, string()} | {ip, inet:ip_address()}])
-> {ok, ssl:sslsocket()} | {error, atom()}.
@@ -68,21 +67,30 @@ listen(Opts) ->
{port, Port} = lists:keyfind(port, 1, Opts),
Backlog = proplists:get_value(backlog, Opts, 1024),
{certfile, CertFile} = lists:keyfind(certfile, 1, Opts),
- {keyfile, KeyFile} = lists:keyfind(keyfile, 1, Opts),
- {password, Password} = lists:keyfind(password, 1, Opts),
+ KeyFileOpts =
+ case lists:keyfind(keyfile, 1, Opts) of
+ false -> [];
+ KeyFile -> [KeyFile]
+ end,
+ PasswordOpts =
+ case lists:keyfind(password, 1, Opts) of
+ false -> [];
+ Password -> [Password]
+ end,
ListenOpts0 = [binary, {active, false},
{backlog, Backlog}, {packet, raw}, {reuseaddr, true},
- {certfile, CertFile}, {keyfile, KeyFile}, {password, Password}],
+ {certfile, CertFile}],
ListenOpts1 =
case lists:keyfind(ip, 1, Opts) of
false -> ListenOpts0;
Ip -> [Ip|ListenOpts0]
end,
- ListenOpts =
+ ListenOpts2 =
case lists:keyfind(cacertfile, 1, Opts) of
false -> ListenOpts1;
CACertFile -> [CACertFile|ListenOpts1]
end,
+ ListenOpts = ListenOpts2 ++ KeyFileOpts ++ PasswordOpts,
ssl:listen(Port, ListenOpts).
%% @doc Accept an incoming connection on a listen socket.
@@ -131,7 +139,7 @@ controlling_process(Socket, Pid) ->
%% @doc Return the address and port for the other end of a connection.
%% @see ssl:peername/1
-spec peername(ssl:sslsocket())
- -> {ok, {inet:ip_address(), inet:ip_port()}} | {error, atom()}.
+ -> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
peername(Socket) ->
ssl:peername(Socket).
diff --git a/src/cowboy_sup.erl b/src/cowboy_sup.erl
index 34591bc..502c592 100644
--- a/src/cowboy_sup.erl
+++ b/src/cowboy_sup.erl
@@ -29,7 +29,13 @@ start_link() ->
%% supervisor.
--spec init([]) -> {ok, {{one_for_one, 10, 10}, [{_, _, _, _, _, _}, ...]}}.
+-spec init([]) -> {'ok', {{'one_for_one', 10, 10}, [{
+ any(), {atom() | tuple(), atom(), 'undefined' | [any()]},
+ 'permanent' | 'temporary' | 'transient',
+ 'brutal_kill' | 'infinity' | non_neg_integer(),
+ 'supervisor' | 'worker',
+ 'dynamic' | [atom() | tuple()]}]
+}}.
init([]) ->
Procs = [{cowboy_clock, {cowboy_clock, start_link, []},
permanent, 5000, worker, [cowboy_clock]}],
diff --git a/src/cowboy_tcp_transport.erl b/src/cowboy_tcp_transport.erl
index c1dad62..82d193b 100644
--- a/src/cowboy_tcp_transport.erl
+++ b/src/cowboy_tcp_transport.erl
@@ -45,7 +45,7 @@ messages() -> {tcp, tcp_closed, tcp_error}.
%% </dl>
%%
%% @see gen_tcp:listen/2
--spec listen([{port, inet:ip_port()} | {ip, inet:ip_address()}])
+-spec listen([{port, inet:port_number()} | {ip, inet:ip_address()}])
-> {ok, inet:socket()} | {error, atom()}.
listen(Opts) ->
{port, Port} = lists:keyfind(port, 1, Opts),
@@ -95,7 +95,7 @@ controlling_process(Socket, Pid) ->
%% @doc Return the address and port for the other end of a connection.
%% @see inet:peername/1
-spec peername(inet:socket())
- -> {ok, {inet:ip_address(), inet:ip_port()}} | {error, atom()}.
+ -> {ok, {inet:ip_address(), inet:port_number()}} | {error, atom()}.
peername(Socket) ->
inet:peername(Socket).
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index 9191f26..72892eb 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -29,7 +29,8 @@
file_200/1, file_403/1, dir_403/1, file_404/1,
file_400/1]). %% http and https.
-export([http_10_hostless/1]). %% misc.
--export([rest_simple/1, rest_keepalive/1, rest_keepalive_post/1, rest_nodelete/1]). %% rest.
+-export([rest_simple/1, rest_keepalive/1, rest_keepalive_post/1,
+ rest_nodelete/1, rest_resource_etags/1]). %% rest.
%% ct.
@@ -47,7 +48,8 @@ groups() ->
static_function_etag, multipart] ++ BaseTests},
{https, [], BaseTests},
{misc, [], [http_10_hostless]},
- {rest, [], [rest_simple, rest_keepalive, rest_keepalive_post, rest_nodelete]}].
+ {rest, [], [rest_simple, rest_keepalive, rest_keepalive_post,
+ rest_nodelete, rest_resource_etags]}].
init_per_suite(Config) ->
application:start(inets),
@@ -75,7 +77,7 @@ init_per_group(https, Config) ->
application:start(public_key),
application:start(ssl),
DataDir = ?config(data_dir, Config),
- cowboy:start_listener(https, 100,
+ {ok,_} = cowboy:start_listener(https, 100,
cowboy_ssl_transport, [
{port, Port}, {certfile, DataDir ++ "cert.pem"},
{keyfile, DataDir ++ "key.pem"}, {password, "cowboy"}],
@@ -84,7 +86,7 @@ init_per_group(https, Config) ->
[{scheme, "https"}, {port, Port}|Config1];
init_per_group(misc, Config) ->
Port = 33082,
- cowboy:start_listener(misc, 100,
+ {ok,_} = cowboy:start_listener(misc, 100,
cowboy_tcp_transport, [{port, Port}],
cowboy_http_protocol, [{dispatch, [{'_', [
{[], http_handler, []}
@@ -92,15 +94,16 @@ init_per_group(misc, Config) ->
[{port, Port}|Config];
init_per_group(rest, Config) ->
Port = 33083,
- cowboy:start_listener(reset, 100,
+ {ok,_} = cowboy:start_listener(rest, 100,
cowboy_tcp_transport, [{port, Port}],
cowboy_http_protocol, [{dispatch, [{'_', [
{[<<"simple">>], rest_simple_resource, []},
{[<<"forbidden_post">>], rest_forbidden_resource, [true]},
{[<<"simple_post">>], rest_forbidden_resource, [false]},
- {[<<"nodelete">>], rest_nodelete_resource, []}
+ {[<<"nodelete">>], rest_nodelete_resource, []},
+ {[<<"resetags">>], rest_resource_etags, []}
]}]}]),
- [{port, Port}|Config].
+ [{scheme, "http"},{port, Port}|Config].
end_per_group(https, Config) ->
cowboy:stop_listener(https),
@@ -512,7 +515,7 @@ static_function_etag(Arguments, etag_data) ->
{_, _Modified} = lists:keyfind(mtime, 1, Arguments),
ChecksumCommand = lists:flatten(io_lib:format("sha1sum ~s", [Filepath])),
[Checksum|_] = string:tokens(os:cmd(ChecksumCommand), " "),
- iolist_to_binary(Checksum).
+ {strong, iolist_to_binary(Checksum)}.
%% http and https.
@@ -623,3 +626,55 @@ rest_nodelete(Config) ->
{ok, Data} = gen_tcp:recv(Socket, 0, 6000),
{0, 12} = binary:match(Data, <<"HTTP/1.1 500">>),
ok = gen_tcp:close(Socket).
+
+rest_resource_etags(Config) ->
+ %% The Etag header should be set to the return value of generate_etag/2.
+ fun() ->
+ %% Correct return values from generate_etag/2.
+ {Packet1, 200} = raw_resp([
+ "GET /resetags?type=tuple-weak HTTP/1.1\r\n",
+ "Host: localhost\r\n", "Connection: close\r\n", "\r\n"], Config),
+ {_,_} = binary:match(Packet1, <<"ETag: W/\"etag-header-value\"\r\n">>),
+ {Packet2, 200} = raw_resp([
+ "GET /resetags?type=tuple-strong HTTP/1.1\r\n",
+ "Host: localhost\r\n", "Connection: close\r\n", "\r\n"], Config),
+ {_,_} = binary:match(Packet2, <<"ETag: \"etag-header-value\"\r\n">>),
+ %% Backwards compatible return values from generate_etag/2.
+ {Packet3, 200} = raw_resp([
+ "GET /resetags?type=binary-weak-quoted HTTP/1.1\r\n",
+ "Host: localhost\r\n", "Connection: close\r\n", "\r\n"], Config),
+ {_,_} = binary:match(Packet3, <<"ETag: W/\"etag-header-value\"\r\n">>),
+ {Packet4, 200} = raw_resp([
+ "GET /resetags?type=binary-strong-quoted HTTP/1.1\r\n",
+ "Host: localhost\r\n", "Connection: close\r\n", "\r\n"], Config),
+ {_,_} = binary:match(Packet4, <<"ETag: \"etag-header-value\"\r\n">>),
+ %% Invalid return values from generate_etag/2.
+ {_Packet5, 500} = raw_resp([
+ "GET /resetags?type=binary-strong-unquoted HTTP/1.1\r\n",
+ "Host: localhost\r\n", "Connection: close\r\n", "\r\n"], Config),
+ {_Packet6, 500} = raw_resp([
+ "GET /resetags?type=binary-weak-unquoted HTTP/1.1\r\n",
+ "Host: localhost\r\n", "Connection: close\r\n", "\r\n"], Config)
+ end(),
+
+ %% The return value of generate_etag/2 should match the request header.
+ fun() ->
+ %% Correct return values from generate_etag/2.
+ {_Packet1, 304} = raw_resp([
+ "GET /resetags?type=tuple-weak HTTP/1.1\r\n",
+ "Host: localhost\r\n", "Connection: close\r\n",
+ "If-None-Match: W/\"etag-header-value\"\r\n", "\r\n"], Config),
+ {_Packet2, 304} = raw_resp([
+ "GET /resetags?type=tuple-strong HTTP/1.1\r\n",
+ "Host: localhost\r\n", "Connection: close\r\n",
+ "If-None-Match: \"etag-header-value\"\r\n", "\r\n"], Config),
+ %% Backwards compatible return values from generate_etag/2.
+ {_Packet3, 304} = raw_resp([
+ "GET /resetags?type=binary-weak-quoted HTTP/1.1\r\n",
+ "Host: localhost\r\n", "Connection: close\r\n",
+ "If-None-Match: W/\"etag-header-value\"\r\n", "\r\n"], Config),
+ {_Packet4, 304} = raw_resp([
+ "GET /resetags?type=binary-strong-quoted HTTP/1.1\r\n",
+ "Host: localhost\r\n", "Connection: close\r\n",
+ "If-None-Match: \"etag-header-value\"\r\n", "\r\n"], Config)
+ end().
diff --git a/test/rest_resource_etags.erl b/test/rest_resource_etags.erl
new file mode 100644
index 0000000..b21aa9f
--- /dev/null
+++ b/test/rest_resource_etags.erl
@@ -0,0 +1,30 @@
+-module(rest_resource_etags).
+-export([init/3, generate_etag/2, content_types_provided/2, get_text_plain/2]).
+
+init(_Transport, _Req, _Opts) ->
+ {upgrade, protocol, cowboy_http_rest}.
+
+generate_etag(Req, State) ->
+ case cowboy_http_req:qs_val(<<"type">>, Req) of
+ %% Correct return values from generate_etag/2.
+ {<<"tuple-weak">>, Req2} ->
+ {{weak, <<"etag-header-value">>}, Req2, State};
+ {<<"tuple-strong">>, Req2} ->
+ {{strong, <<"etag-header-value">>}, Req2, State};
+ %% Backwards compatible return values from generate_etag/2.
+ {<<"binary-weak-quoted">>, Req2} ->
+ {<<"W/\"etag-header-value\"">>, Req2, State};
+ {<<"binary-strong-quoted">>, Req2} ->
+ {<<"\"etag-header-value\"">>, Req2, State};
+ %% Invalid return values from generate_etag/2.
+ {<<"binary-strong-unquoted">>, Req2} ->
+ {<<"etag-header-value">>, Req2, State};
+ {<<"binary-weak-unquoted">>, Req2} ->
+ {<<"W/etag-header-value">>, Req2, State}
+ end.
+
+content_types_provided(Req, State) ->
+ {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}.
+
+get_text_plain(Req, State) ->
+ {<<"This is REST!">>, Req, State}.