aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cowboy.app.src2
-rw-r--r--src/cowboy.erl11
-rw-r--r--src/cowboy_acceptor.erl2
-rw-r--r--src/cowboy_app.erl2
-rw-r--r--src/cowboy_clock.erl26
-rw-r--r--src/cowboy_cookies.erl7
-rw-r--r--src/cowboy_dispatcher.erl20
-rw-r--r--src/cowboy_http.erl240
-rw-r--r--src/cowboy_http_protocol.erl102
-rw-r--r--src/cowboy_http_req.erl252
-rw-r--r--src/cowboy_http_rest.erl872
-rw-r--r--src/cowboy_http_static.erl461
-rw-r--r--src/cowboy_http_websocket.erl149
-rw-r--r--src/cowboy_protocol.erl6
14 files changed, 1890 insertions, 262 deletions
diff --git a/src/cowboy.app.src b/src/cowboy.app.src
index 264607f..9b3ee50 100644
--- a/src/cowboy.app.src
+++ b/src/cowboy.app.src
@@ -14,7 +14,7 @@
{application, cowboy, [
{description, "Small, fast, modular HTTP server."},
- {vsn, "0.3.0"},
+ {vsn, "0.5.0"},
{modules, []},
{registered, [cowboy_clock, cowboy_sup]},
{applications, [
diff --git a/src/cowboy.erl b/src/cowboy.erl
index 9b07921..6defeea 100644
--- a/src/cowboy.erl
+++ b/src/cowboy.erl
@@ -15,7 +15,7 @@
%% @doc Cowboy API to start and stop listeners.
-module(cowboy).
--export([start_listener/6, stop_listener/1, child_spec/6]).
+-export([start_listener/6, stop_listener/1, child_spec/6, accept_ack/1]).
%% @doc Start a listener for the given transport and protocol.
%%
@@ -61,6 +61,7 @@ stop_listener(Ref) ->
end.
%% @doc Return a child spec suitable for embedding.
+%%
%% When you want to embed cowboy in another application, you can use this
%% function to create a <em>ChildSpec</em> suitable for use in a supervisor.
%% The parameters are the same as in <em>start_listener/6</em> but rather
@@ -74,3 +75,11 @@ child_spec(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts)
{{cowboy_listener_sup, Ref}, {cowboy_listener_sup, start_link, [
NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts
]}, permanent, 5000, supervisor, [cowboy_listener_sup]}.
+
+%% @doc Acknowledge the accepted connection.
+%%
+%% Effectively used to make sure the socket control has been given to
+%% the protocol process before starting to use it.
+-spec accept_ack(pid()) -> ok.
+accept_ack(ListenerPid) ->
+ receive {shoot, ListenerPid} -> ok end.
diff --git a/src/cowboy_acceptor.erl b/src/cowboy_acceptor.erl
index f2b603e..4cb9fa7 100644
--- a/src/cowboy_acceptor.erl
+++ b/src/cowboy_acceptor.erl
@@ -40,7 +40,7 @@ acceptor(LSocket, Transport, Protocol, Opts, MaxConns, ListenerPid, ReqsSup) ->
Transport:controlling_process(CSocket, Pid),
{ok, NbConns} = cowboy_listener:add_connection(ListenerPid,
default, Pid),
- Pid ! shoot,
+ Pid ! {shoot, ListenerPid},
limit_reqs(ListenerPid, NbConns, MaxConns);
{error, timeout} ->
ignore;
diff --git a/src/cowboy_app.erl b/src/cowboy_app.erl
index 0ff08f0..c7cefe4 100644
--- a/src/cowboy_app.erl
+++ b/src/cowboy_app.erl
@@ -46,7 +46,7 @@ profile_output() ->
consider_profiling() ->
case application:get_env(profile) of
{ok, true} ->
- eprof:start(),
+ {ok, _Pid} = eprof:start(),
eprof:start_profiling([self()]);
_ ->
not_profiling
diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl
index 3597bdd..c699f4f 100644
--- a/src/cowboy_clock.erl
+++ b/src/cowboy_clock.erl
@@ -25,23 +25,8 @@
-export([init/1, handle_call/3, handle_cast/2,
handle_info/2, terminate/2, code_change/3]). %% gen_server.
-%% @todo Use calendar types whenever they get exported.
--type year() :: non_neg_integer().
--type month() :: 1..12.
--type day() :: 1..31.
--type hour() :: 0..23.
--type minute() :: 0..59.
--type second() :: 0..59.
--type daynum() :: 1..7.
-
--type date() :: {year(), month(), day()}.
--type time() :: {hour(), minute(), second()}.
-
--type datetime() :: {date(), time()}.
--export_type([date/0, time/0, datetime/0]).
-
-record(state, {
- universaltime = undefined :: undefined | datetime(),
+ universaltime = undefined :: undefined | calendar:datetime(),
rfc1123 = <<>> :: binary(),
tref = undefined :: undefined | timer:tref()
}).
@@ -74,7 +59,7 @@ rfc1123() ->
%%
%% This format is used in the <em>'Set-Cookie'</em> header sent with
%% HTTP responses.
--spec rfc2109(datetime()) -> binary().
+-spec rfc2109(calendar:datetime()) -> binary().
rfc2109(LocalTime) ->
{{YYYY,MM,DD},{Hour,Min,Sec}} =
case calendar:local_time_to_universal_time_dst(LocalTime) of
@@ -145,7 +130,8 @@ code_change(_OldVsn, State, _Extra) ->
%% Internal.
--spec update_rfc1123(binary(), undefined | datetime(), datetime()) -> binary().
+-spec update_rfc1123(binary(), undefined | calendar:datetime(),
+ calendar:datetime()) -> binary().
update_rfc1123(Bin, Now, Now) ->
Bin;
update_rfc1123(<< Keep:23/binary, _/bits >>,
@@ -184,7 +170,7 @@ pad_int(X) when X < 10 ->
pad_int(X) ->
list_to_binary(integer_to_list(X)).
--spec weekday(daynum()) -> <<_:24>>.
+-spec weekday(1..7) -> <<_:24>>.
weekday(1) -> <<"Mon">>;
weekday(2) -> <<"Tue">>;
weekday(3) -> <<"Wed">>;
@@ -193,7 +179,7 @@ weekday(5) -> <<"Fri">>;
weekday(6) -> <<"Sat">>;
weekday(7) -> <<"Sun">>.
--spec month(month()) -> <<_:24>>.
+-spec month(1..12) -> <<_:24>>.
month( 1) -> <<"Jan">>;
month( 2) -> <<"Feb">>;
month( 3) -> <<"Mar">>;
diff --git a/src/cowboy_cookies.erl b/src/cowboy_cookies.erl
index 9c6c4c3..6818a86 100644
--- a/src/cowboy_cookies.erl
+++ b/src/cowboy_cookies.erl
@@ -23,7 +23,7 @@
-type kv() :: {Name::binary(), Value::binary()}.
-type kvlist() :: [kv()].
-type cookie_option() :: {max_age, integer()}
- | {local_time, {cowboy_clock:date(), cowboy_clock:time()}}
+ | {local_time, calendar:datetime()}
| {domain, binary()} | {path, binary()}
| {secure, true | false} | {http_only, true | false}.
-export_type([kv/0, kvlist/0, cookie_option/0]).
@@ -171,13 +171,12 @@ quote(V0) ->
V
end.
--spec add_seconds(integer(), cowboy_clock:datetime())
- -> cowboy_clock:datetime().
+-spec add_seconds(integer(), calendar:datetime()) -> calendar:datetime().
add_seconds(Secs, LocalTime) ->
Greg = calendar:datetime_to_gregorian_seconds(LocalTime),
calendar:gregorian_seconds_to_datetime(Greg + Secs).
--spec age_to_cookie_date(integer(), cowboy_clock:datetime()) -> binary().
+-spec age_to_cookie_date(integer(), calendar:datetime()) -> binary().
age_to_cookie_date(Age, LocalTime) ->
cowboy_clock:rfc2109(add_seconds(Age, LocalTime)).
diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl
index 67ea34b..22f6e1e 100644
--- a/src/cowboy_dispatcher.erl
+++ b/src/cowboy_dispatcher.erl
@@ -16,7 +16,7 @@
%% @doc Dispatch requests according to a hostname and path.
-module(cowboy_dispatcher).
--export([split_host/1, split_path/1, match/3]). %% API.
+-export([split_host/1, split_path/2, match/3]). %% API.
-type bindings() :: list({atom(), binary()}).
-type tokens() :: list(binary()).
@@ -50,21 +50,22 @@ split_host(Host) ->
%% Following RFC2396, this function may return path segments containing any
%% character, including <em>/</em> if, and only if, a <em>/</em> was escaped
%% and part of a path segment.
--spec split_path(binary()) -> {tokens(), binary(), binary()}.
-split_path(Path) ->
+-spec split_path(binary(), fun((binary()) -> binary())) ->
+ {tokens(), binary(), binary()}.
+split_path(Path, URLDec) ->
case binary:split(Path, <<"?">>) of
- [Path] -> {do_split_path(Path, <<"/">>), Path, <<>>};
+ [Path] -> {do_split_path(Path, <<"/">>, URLDec), Path, <<>>};
[<<>>, Qs] -> {[], <<>>, Qs};
- [Path2, Qs] -> {do_split_path(Path2, <<"/">>), Path2, Qs}
+ [Path2, Qs] -> {do_split_path(Path2, <<"/">>, URLDec), Path2, Qs}
end.
--spec do_split_path(binary(), <<_:8>>) -> tokens().
-do_split_path(RawPath, Separator) ->
+-spec do_split_path(binary(), <<_:8>>, fun((binary()) -> binary())) -> tokens().
+do_split_path(RawPath, Separator, URLDec) ->
EncodedPath = case binary:split(RawPath, Separator, [global, trim]) of
[<<>>|Path] -> Path;
Path -> Path
end,
- [quoted:from_url(Token) || Token <- EncodedPath].
+ [URLDec(Token) || Token <- EncodedPath].
%% @doc Match hostname tokens and path tokens against dispatch rules.
%%
@@ -224,7 +225,8 @@ split_path_test_() ->
[<<"users">>, <<"a b">>, <<"c!d">>],
<<"/users/a+b/c%21d">>, <<"e+f=g+h">>}
],
- [{P, fun() -> {R, RawP, Qs} = split_path(P) end}
+ URLDecode = fun(Bin) -> cowboy_http:urldecode(Bin, crash) end,
+ [{P, fun() -> {R, RawP, Qs} = split_path(P, URLDecode) end}
|| {P, R, RawP, Qs} <- Tests].
match_test_() ->
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index 6404379..7c1a2d3 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -23,7 +23,8 @@
whitespace/2, digits/1, token/2, token_ci/2, quoted_string/2]).
%% Interpretation.
--export([connection_to_atom/1]).
+-export([connection_to_atom/1, urldecode/1, urldecode/2, urlencode/1,
+ urlencode/2]).
-include("include/http.hrl").
-include_lib("eunit/include/eunit.hrl").
@@ -57,11 +58,11 @@ list(Data, Fun) ->
list(Data, Fun, Acc) ->
whitespace(Data,
fun (<<>>) -> Acc;
- (<< $,, Rest/bits >>) -> list(Rest, Fun, Acc);
+ (<< $,, Rest/binary >>) -> list(Rest, Fun, Acc);
(Rest) -> Fun(Rest,
fun (D, I) -> whitespace(D,
fun (<<>>) -> [I|Acc];
- (<< $,, R/bits >>) -> list(R, Fun, [I|Acc]);
+ (<< $,, R/binary >>) -> list(R, Fun, [I|Acc]);
(_Any) -> {error, badarg}
end)
end)
@@ -80,7 +81,7 @@ content_type(Data) ->
-> any().
content_type_params(Data, Fun, Acc) ->
whitespace(Data,
- fun (<< $;, Rest/bits >>) -> content_type_param(Rest, Fun, Acc);
+ fun (<< $;, Rest/binary >>) -> content_type_param(Rest, Fun, Acc);
(<<>>) -> Fun(lists:reverse(Acc));
(_Rest) -> {error, badarg}
end).
@@ -92,7 +93,7 @@ content_type_param(Data, Fun, Acc) ->
fun (Rest) ->
token_ci(Rest,
fun (_Rest2, <<>>) -> {error, badarg};
- (<< $=, Rest2/bits >>, Attr) ->
+ (<< $=, Rest2/binary >>, Attr) ->
word(Rest2,
fun (Rest3, Value) ->
content_type_params(Rest3, Fun,
@@ -114,7 +115,7 @@ media_range(Data, Fun) ->
[{binary(), binary()}]) -> any().
media_range_params(Data, Fun, Type, SubType, Acc) ->
whitespace(Data,
- fun (<< $;, Rest/bits >>) ->
+ fun (<< $;, Rest/binary >>) ->
whitespace(Rest,
fun (Rest2) ->
media_range_param_attr(Rest2, Fun, Type, SubType, Acc)
@@ -127,7 +128,7 @@ media_range_params(Data, Fun, Type, SubType, Acc) ->
media_range_param_attr(Data, Fun, Type, SubType, Acc) ->
token_ci(Data,
fun (_Rest, <<>>) -> {error, badarg};
- (<< $=, Rest/bits >>, Attr) ->
+ (<< $=, Rest/binary >>, Attr) ->
media_range_param_value(Rest, Fun, Type, SubType, Acc, Attr)
end).
@@ -150,7 +151,7 @@ media_range_param_value(Data, Fun, Type, SubType, Acc, Attr) ->
media_type(Data, Fun) ->
token_ci(Data,
fun (_Rest, <<>>) -> {error, badarg};
- (<< $/, Rest/bits >>, Type) ->
+ (<< $/, Rest/binary >>, Type) ->
token_ci(Rest,
fun (_Rest2, <<>>) -> {error, badarg};
(Rest2, SubType) -> Fun(Rest2, Type, SubType)
@@ -163,7 +164,7 @@ media_type(Data, Fun) ->
[{binary(), binary()} | binary()]) -> any().
accept_ext(Data, Fun, Type, SubType, Params, Quality, Acc) ->
whitespace(Data,
- fun (<< $;, Rest/bits >>) ->
+ fun (<< $;, Rest/binary >>) ->
whitespace(Rest,
fun (Rest2) ->
accept_ext_attr(Rest2, Fun,
@@ -180,7 +181,7 @@ accept_ext(Data, Fun, Type, SubType, Params, Quality, Acc) ->
accept_ext_attr(Data, Fun, Type, SubType, Params, Quality, Acc) ->
token_ci(Data,
fun (_Rest, <<>>) -> {error, badarg};
- (<< $=, Rest/bits >>, Attr) ->
+ (<< $=, Rest/binary >>, Attr) ->
accept_ext_value(Rest, Fun, Type, SubType, Params,
Quality, Acc, Attr);
(Rest, Attr) ->
@@ -213,7 +214,7 @@ conneg(Data, Fun) ->
%% @doc Parse a language range, followed by an optional quality value.
-spec language_range(binary(), fun()) -> any().
-language_range(<< $*, Rest/bits >>, Fun) ->
+language_range(<< $*, Rest/binary >>, Fun) ->
language_range_ret(Rest, Fun, '*');
language_range(Data, Fun) ->
language_tag(Data,
@@ -221,7 +222,7 @@ language_range(Data, Fun) ->
language_range_ret(Rest, Fun, LanguageTag)
end).
--spec language_range_ret(binary(), fun(), '*' | {binary(), binary()}) -> any().
+-spec language_range_ret(binary(), fun(), '*' | {binary(), [binary()]}) -> any().
language_range_ret(Data, Fun, LanguageTag) ->
maybe_qparam(Data,
fun (Rest, Quality) ->
@@ -233,10 +234,10 @@ language_tag(Data, Fun) ->
alpha(Data,
fun (_Rest, Tag) when byte_size(Tag) =:= 0; byte_size(Tag) > 8 ->
{error, badarg};
- (<< $-, Rest/bits >>, Tag) ->
+ (<< $-, Rest/binary >>, Tag) ->
language_subtag(Rest, Fun, Tag, []);
(Rest, Tag) ->
- Fun(Rest, {Tag, []})
+ Fun(Rest, Tag)
end).
-spec language_subtag(binary(), fun(), binary(), [binary()]) -> any().
@@ -244,16 +245,18 @@ language_subtag(Data, Fun, Tag, Acc) ->
alpha(Data,
fun (_Rest, SubTag) when byte_size(SubTag) =:= 0;
byte_size(SubTag) > 8 -> {error, badarg};
- (<< $-, Rest/bits >>, SubTag) ->
+ (<< $-, Rest/binary >>, SubTag) ->
language_subtag(Rest, Fun, Tag, [SubTag|Acc]);
(Rest, SubTag) ->
- Fun(Rest, {Tag, lists:reverse([SubTag|Acc])})
+ %% Rebuild the full tag now that we know it's correct
+ Sub = << << $-, S/binary >> || S <- lists:reverse([SubTag|Acc]) >>,
+ Fun(Rest, << Tag/binary, Sub/binary >>)
end).
-spec maybe_qparam(binary(), fun()) -> any().
maybe_qparam(Data, Fun) ->
whitespace(Data,
- fun (<< $;, Rest/bits >>) ->
+ fun (<< $;, Rest/binary >>) ->
whitespace(Rest,
fun (Rest2) ->
qparam(Rest2, Fun)
@@ -264,12 +267,12 @@ maybe_qparam(Data, Fun) ->
%% @doc Parse a quality parameter string (for example q=0.500).
-spec qparam(binary(), fun()) -> any().
-qparam(<< Q, $=, Data/bits >>, Fun) when Q =:= $q; Q =:= $Q ->
+qparam(<< Q, $=, Data/binary >>, Fun) when Q =:= $q; Q =:= $Q ->
qvalue(Data, Fun).
%% @doc Parse either a list of entity tags or a "*".
-spec entity_tag_match(binary()) -> any().
-entity_tag_match(<< $*, Rest/bits >>) ->
+entity_tag_match(<< $*, Rest/binary >>) ->
whitespace(Rest,
fun (<<>>) -> '*';
(_Any) -> {error, badarg}
@@ -279,7 +282,7 @@ entity_tag_match(Data) ->
%% @doc Parse an entity-tag.
-spec entity_tag(binary(), fun()) -> any().
-entity_tag(<< "W/", Rest/bits >>, Fun) ->
+entity_tag(<< "W/", Rest/binary >>, Fun) ->
opaque_tag(Rest, Fun, weak);
entity_tag(Data, Fun) ->
opaque_tag(Data, Fun, strong).
@@ -320,11 +323,11 @@ http_date(Data) ->
-spec rfc1123_date(binary()) -> any().
rfc1123_date(Data) ->
wkday(Data,
- fun (<< ", ", Rest/bits >>, _WkDay) ->
+ fun (<< ", ", Rest/binary >>, _WkDay) ->
date1(Rest,
- fun (<< " ", Rest2/bits >>, Date) ->
+ fun (<< " ", Rest2/binary >>, Date) ->
time(Rest2,
- fun (<< " GMT", Rest3/bits >>, Time) ->
+ fun (<< " GMT", Rest3/binary >>, Time) ->
http_date_ret(Rest3, {Date, Time});
(_Any, _Time) ->
{error, badarg}
@@ -344,11 +347,11 @@ rfc1123_date(Data) ->
%% in the past (this helps solve the "year 2000" problem).
rfc850_date(Data) ->
weekday(Data,
- fun (<< ", ", Rest/bits >>, _WeekDay) ->
+ fun (<< ", ", Rest/binary >>, _WeekDay) ->
date2(Rest,
- fun (<< " ", Rest2/bits >>, Date) ->
+ fun (<< " ", Rest2/binary >>, Date) ->
time(Rest2,
- fun (<< " GMT", Rest3/bits >>, Time) ->
+ fun (<< " GMT", Rest3/binary >>, Time) ->
http_date_ret(Rest3, {Date, Time});
(_Any, _Time) ->
{error, badarg}
@@ -364,11 +367,11 @@ rfc850_date(Data) ->
-spec asctime_date(binary()) -> any().
asctime_date(Data) ->
wkday(Data,
- fun (<< " ", Rest/bits >>, _WkDay) ->
+ fun (<< " ", Rest/binary >>, _WkDay) ->
date3(Rest,
- fun (<< " ", Rest2/bits >>, PartialDate) ->
+ fun (<< " ", Rest2/binary >>, PartialDate) ->
time(Rest2,
- fun (<< " ", Rest3/bits >>, Time) ->
+ fun (<< " ", Rest3/binary >>, Time) ->
asctime_year(Rest3,
PartialDate, Time);
(_Any, _Time) ->
@@ -382,7 +385,7 @@ asctime_date(Data) ->
end).
-spec asctime_year(binary(), tuple(), tuple()) -> any().
-asctime_year(<< Y1, Y2, Y3, Y4, Rest/bits >>, {Month, Day}, Time)
+asctime_year(<< Y1, Y2, Y3, Y4, Rest/binary >>, {Month, Day}, Time)
when Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9,
Y3 >= $0, Y3 =< $9, Y4 >= $0, Y4 =< $9 ->
Year = (Y1 - $0) * 1000 + (Y2 - $0) * 100 + (Y3 - $0) * 10 + (Y4 - $0),
@@ -402,9 +405,9 @@ http_date_ret(Data, DateTime = {Date, _Time}) ->
%% We never use it, pretty much just checks the wkday is right.
-spec wkday(binary(), fun()) -> any().
-wkday(<< WkDay:3/binary, Rest/bits >>, Fun)
- when WkDay =:= <<"Mon">>; WkDay =:= "Tue"; WkDay =:= "Wed";
- WkDay =:= <<"Thu">>; WkDay =:= "Fri"; WkDay =:= "Sat";
+wkday(<< WkDay:3/binary, Rest/binary >>, Fun)
+ when WkDay =:= <<"Mon">>; WkDay =:= <<"Tue">>; WkDay =:= <<"Wed">>;
+ WkDay =:= <<"Thu">>; WkDay =:= <<"Fri">>; WkDay =:= <<"Sat">>;
WkDay =:= <<"Sun">> ->
Fun(Rest, WkDay);
wkday(_Any, _Fun) ->
@@ -430,7 +433,7 @@ weekday(_Any, _Fun) ->
{error, badarg}.
-spec date1(binary(), fun()) -> any().
-date1(<< D1, D2, " ", M:3/binary, " ", Y1, Y2, Y3, Y4, Rest/bits >>, Fun)
+date1(<< D1, D2, " ", M:3/binary, " ", Y1, Y2, Y3, Y4, Rest/binary >>, Fun)
when D1 >= $0, D1 =< $9, D2 >= $0, D2 =< $9,
Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9,
Y3 >= $0, Y3 =< $9, Y4 >= $0, Y4 =< $9 ->
@@ -448,7 +451,7 @@ date1(_Data, _Fun) ->
{error, badarg}.
-spec date2(binary(), fun()) -> any().
-date2(<< D1, D2, "-", M:3/binary, "-", Y1, Y2, Rest/bits >>, Fun)
+date2(<< D1, D2, "-", M:3/binary, "-", Y1, Y2, Rest/binary >>, Fun)
when D1 >= $0, D1 =< $9, D2 >= $0, D2 =< $9,
Y1 >= $0, Y1 =< $9, Y2 >= $0, Y2 =< $9 ->
case month(M) of
@@ -470,7 +473,7 @@ date2(_Data, _Fun) ->
{error, badarg}.
-spec date3(binary(), fun()) -> any().
-date3(<< M:3/binary, " ", D1, D2, Rest/bits >>, Fun)
+date3(<< M:3/binary, " ", D1, D2, Rest/binary >>, Fun)
when (D1 >= $0 andalso D1 =< $3) orelse D1 =:= $\s,
D2 >= $0, D2 =< $9 ->
case month(M) of
@@ -502,7 +505,7 @@ month(<<"Dec">>) -> 12;
month(_Any) -> {error, badarg}.
-spec time(binary(), fun()) -> any().
-time(<< H1, H2, ":", M1, M2, ":", S1, S2, Rest/bits >>, Fun)
+time(<< H1, H2, ":", M1, M2, ":", S1, S2, Rest/binary >>, Fun)
when H1 >= $0, H1 =< $2, H2 >= $0, H2 =< $9,
M1 >= $0, M1 =< $5, M2 >= $0, M2 =< $9,
S1 >= $0, S1 =< $5, S2 >= $0, S2 =< $9 ->
@@ -521,7 +524,7 @@ time(<< H1, H2, ":", M1, M2, ":", S1, S2, Rest/bits >>, Fun)
%% @doc Skip whitespace.
-spec whitespace(binary(), fun()) -> any().
-whitespace(<< C, Rest/bits >>, Fun)
+whitespace(<< C, Rest/binary >>, Fun)
when C =:= $\s; C =:= $\t ->
whitespace(Rest, Fun);
whitespace(Data, Fun) ->
@@ -541,14 +544,14 @@ digits(Data) ->
end).
-spec digits(binary(), fun()) -> any().
-digits(<< C, Rest/bits >>, Fun)
+digits(<< C, Rest/binary >>, Fun)
when C >= $0, C =< $9 ->
digits(Rest, Fun, C - $0);
digits(_Data, _Fun) ->
{error, badarg}.
-spec digits(binary(), fun(), non_neg_integer()) -> any().
-digits(<< C, Rest/bits >>, Fun, Acc)
+digits(<< C, Rest/binary >>, Fun, Acc)
when C >= $0, C =< $9 ->
digits(Rest, Fun, Acc * 10 + (C - $0));
digits(Data, Fun, Acc) ->
@@ -564,7 +567,7 @@ alpha(Data, Fun) ->
-spec alpha(binary(), fun(), binary()) -> any().
alpha(<<>>, Fun, Acc) ->
Fun(<<>>, Acc);
-alpha(<< C, Rest/bits >>, Fun, Acc)
+alpha(<< C, Rest/binary >>, Fun, Acc)
when C >= $a andalso C =< $z;
C >= $A andalso C =< $Z ->
C2 = cowboy_bstr:char_to_lower(C),
@@ -574,7 +577,7 @@ alpha(Data, Fun, Acc) ->
%% @doc Parse either a token or a quoted string.
-spec word(binary(), fun()) -> any().
-word(Data = << $", _/bits >>, Fun) ->
+word(Data = << $", _/binary >>, Fun) ->
quoted_string(Data, Fun);
word(Data, Fun) ->
token(Data,
@@ -597,47 +600,47 @@ token(Data, Fun) ->
-spec token(binary(), fun(), ci | cs, binary()) -> any().
token(<<>>, Fun, _Case, Acc) ->
Fun(<<>>, Acc);
-token(Data = << C, _Rest/bits >>, Fun, _Case, Acc)
+token(Data = << C, _Rest/binary >>, Fun, _Case, Acc)
when C =:= $(; C =:= $); C =:= $<; C =:= $>; C =:= $@;
C =:= $,; C =:= $;; C =:= $:; C =:= $\\; C =:= $";
C =:= $/; C =:= $[; C =:= $]; C =:= $?; C =:= $=;
C =:= ${; C =:= $}; C =:= $\s; C =:= $\t;
C < 32; C =:= 127 ->
Fun(Data, Acc);
-token(<< C, Rest/bits >>, Fun, Case = ci, Acc) ->
+token(<< C, Rest/binary >>, Fun, Case = ci, Acc) ->
C2 = cowboy_bstr:char_to_lower(C),
token(Rest, Fun, Case, << Acc/binary, C2 >>);
-token(<< C, Rest/bits >>, Fun, Case, Acc) ->
+token(<< C, Rest/binary >>, Fun, Case, Acc) ->
token(Rest, Fun, Case, << Acc/binary, C >>).
%% @doc Parse a quoted string.
-spec quoted_string(binary(), fun()) -> any().
-quoted_string(<< $", Rest/bits >>, Fun) ->
+quoted_string(<< $", Rest/binary >>, Fun) ->
quoted_string(Rest, Fun, <<>>).
-spec quoted_string(binary(), fun(), binary()) -> any().
quoted_string(<<>>, _Fun, _Acc) ->
{error, badarg};
-quoted_string(<< $", Rest/bits >>, Fun, Acc) ->
+quoted_string(<< $", Rest/binary >>, Fun, Acc) ->
Fun(Rest, Acc);
-quoted_string(<< $\\, C, Rest/bits >>, Fun, Acc) ->
+quoted_string(<< $\\, C, Rest/binary >>, Fun, Acc) ->
quoted_string(Rest, Fun, << Acc/binary, C >>);
-quoted_string(<< C, Rest/bits >>, Fun, Acc) ->
+quoted_string(<< C, Rest/binary >>, Fun, Acc) ->
quoted_string(Rest, Fun, << Acc/binary, C >>).
%% @doc Parse a quality value.
-spec qvalue(binary(), fun()) -> any().
-qvalue(<< $0, $., Rest/bits >>, Fun) ->
+qvalue(<< $0, $., Rest/binary >>, Fun) ->
qvalue(Rest, Fun, 0, 100);
-qvalue(<< $0, Rest/bits >>, Fun) ->
+qvalue(<< $0, Rest/binary >>, Fun) ->
Fun(Rest, 0);
-qvalue(<< $1, $., $0, $0, $0, Rest/bits >>, Fun) ->
+qvalue(<< $1, $., $0, $0, $0, Rest/binary >>, Fun) ->
Fun(Rest, 1000);
-qvalue(<< $1, $., $0, $0, Rest/bits >>, Fun) ->
+qvalue(<< $1, $., $0, $0, Rest/binary >>, Fun) ->
Fun(Rest, 1000);
-qvalue(<< $1, $., $0, Rest/bits >>, Fun) ->
+qvalue(<< $1, $., $0, Rest/binary >>, Fun) ->
Fun(Rest, 1000);
-qvalue(<< $1, Rest/bits >>, Fun) ->
+qvalue(<< $1, Rest/binary >>, Fun) ->
Fun(Rest, 1000);
qvalue(_Data, _Fun) ->
{error, badarg}.
@@ -645,7 +648,7 @@ qvalue(_Data, _Fun) ->
-spec qvalue(binary(), fun(), integer(), 1 | 10 | 100) -> any().
qvalue(Data, Fun, Q, 0) ->
Fun(Data, Q);
-qvalue(<< C, Rest/bits >>, Fun, Q, M)
+qvalue(<< C, Rest/binary >>, Fun, Q, M)
when C >= $0, C =< $9 ->
qvalue(Rest, Fun, Q + (C - $0) * M, M div 10);
qvalue(Data, Fun, Q, _M) ->
@@ -668,6 +671,91 @@ connection_to_atom([<<"close">>|_Tail]) ->
connection_to_atom([_Any|Tail]) ->
connection_to_atom(Tail).
+%% @doc Decode a URL encoded binary.
+%% @equiv urldecode(Bin, crash)
+-spec urldecode(binary()) -> binary().
+urldecode(Bin) when is_binary(Bin) ->
+ urldecode(Bin, <<>>, crash).
+
+%% @doc Decode a URL encoded binary.
+%% The second argument specifies how to handle percent characters that are not
+%% followed by two valid hex characters. Use `skip' to ignore such errors,
+%% if `crash' is used the function will fail with the reason `badarg'.
+-spec urldecode(binary(), crash | skip) -> binary().
+urldecode(Bin, OnError) when is_binary(Bin) ->
+ urldecode(Bin, <<>>, OnError).
+
+-spec urldecode(binary(), binary(), crash | skip) -> binary().
+urldecode(<<$%, H, L, Rest/binary>>, Acc, OnError) ->
+ G = unhex(H),
+ M = unhex(L),
+ if G =:= error; M =:= error ->
+ case OnError of skip -> ok; crash -> erlang:error(badarg) end,
+ urldecode(<<H, L, Rest/binary>>, <<Acc/binary, $%>>, OnError);
+ true ->
+ urldecode(Rest, <<Acc/binary, (G bsl 4 bor M)>>, OnError)
+ end;
+urldecode(<<$%, Rest/binary>>, Acc, OnError) ->
+ case OnError of skip -> ok; crash -> erlang:error(badarg) end,
+ urldecode(Rest, <<Acc/binary, $%>>, OnError);
+urldecode(<<$+, Rest/binary>>, Acc, OnError) ->
+ urldecode(Rest, <<Acc/binary, $ >>, OnError);
+urldecode(<<C, Rest/binary>>, Acc, OnError) ->
+ urldecode(Rest, <<Acc/binary, C>>, OnError);
+urldecode(<<>>, Acc, _OnError) ->
+ Acc.
+
+-spec unhex(byte()) -> byte() | error.
+unhex(C) when C >= $0, C =< $9 -> C - $0;
+unhex(C) when C >= $A, C =< $F -> C - $A + 10;
+unhex(C) when C >= $a, C =< $f -> C - $a + 10;
+unhex(_) -> error.
+
+
+%% @doc URL encode a string binary.
+%% @equiv urlencode(Bin, [])
+-spec urlencode(binary()) -> binary().
+urlencode(Bin) ->
+ urlencode(Bin, []).
+
+%% @doc URL encode a string binary.
+%% The `noplus' option disables the default behaviour of quoting space
+%% characters, `\s', as `+'. The `upper' option overrides the default behaviour
+%% of writing hex numbers using lowecase letters to using uppercase letters
+%% instead.
+-spec urlencode(binary(), [noplus|upper]) -> binary().
+urlencode(Bin, Opts) ->
+ Plus = not proplists:get_value(noplus, Opts, false),
+ Upper = proplists:get_value(upper, Opts, false),
+ urlencode(Bin, <<>>, Plus, Upper).
+
+-spec urlencode(binary(), binary(), boolean(), boolean()) -> binary().
+urlencode(<<C, Rest/binary>>, Acc, P=Plus, U=Upper) ->
+ if C >= $0, C =< $9 -> urlencode(Rest, <<Acc/binary, C>>, P, U);
+ C >= $A, C =< $Z -> urlencode(Rest, <<Acc/binary, C>>, P, U);
+ C >= $a, C =< $z -> urlencode(Rest, <<Acc/binary, C>>, P, U);
+ C =:= $.; C =:= $-; C =:= $~; C =:= $_ ->
+ urlencode(Rest, <<Acc/binary, C>>, P, U);
+ C =:= $ , Plus ->
+ urlencode(Rest, <<Acc/binary, $+>>, P, U);
+ true ->
+ H = C band 16#F0 bsr 4, L = C band 16#0F,
+ H1 = if Upper -> tohexu(H); true -> tohexl(H) end,
+ L1 = if Upper -> tohexu(L); true -> tohexl(L) end,
+ urlencode(Rest, <<Acc/binary, $%, H1, L1>>, P, U)
+ end;
+urlencode(<<>>, Acc, _Plus, _Upper) ->
+ Acc.
+
+-spec tohexu(byte()) -> byte().
+tohexu(C) when C < 10 -> $0 + C;
+tohexu(C) when C < 17 -> $A + C - 10.
+
+-spec tohexl(byte()) -> byte().
+tohexl(C) when C < 10 -> $0 + C;
+tohexl(C) when C < 17 -> $a + C - 10.
+
+
%% Tests.
-ifdef(TEST).
@@ -687,16 +775,16 @@ nonempty_language_range_list_test_() ->
%% {Value, Result}
Tests = [
{<<"da, en-gb;q=0.8, en;q=0.7">>, [
- {{<<"da">>, []}, 1000},
- {{<<"en">>, [<<"gb">>]}, 800},
- {{<<"en">>, []}, 700}
+ {<<"da">>, 1000},
+ {<<"en-gb">>, 800},
+ {<<"en">>, 700}
]},
{<<"en, en-US, en-cockney, i-cherokee, x-pig-latin">>, [
- {{<<"en">>, []}, 1000},
- {{<<"en">>, [<<"us">>]}, 1000},
- {{<<"en">>, [<<"cockney">>]}, 1000},
- {{<<"i">>, [<<"cherokee">>]}, 1000},
- {{<<"x">>, [<<"pig">>, <<"latin">>]}, 1000}
+ {<<"en">>, 1000},
+ {<<"en-us">>, 1000},
+ {<<"en-cockney">>, 1000},
+ {<<"i-cherokee">>, 1000},
+ {<<"x-pig-latin">>, 1000}
]}
],
[{V, fun() -> R = nonempty_list(V, fun language_range/2) end}
@@ -834,4 +922,28 @@ digits_test_() ->
],
[{V, fun() -> R = digits(V) end} || {V, R} <- Tests].
+urldecode_test_() ->
+ U = fun urldecode/2,
+ [?_assertEqual(<<" ">>, U(<<"%20">>, crash)),
+ ?_assertEqual(<<" ">>, U(<<"+">>, crash)),
+ ?_assertEqual(<<0>>, U(<<"%00">>, crash)),
+ ?_assertEqual(<<255>>, U(<<"%fF">>, crash)),
+ ?_assertEqual(<<"123">>, U(<<"123">>, crash)),
+ ?_assertEqual(<<"%i5">>, U(<<"%i5">>, skip)),
+ ?_assertEqual(<<"%5">>, U(<<"%5">>, skip)),
+ ?_assertError(badarg, U(<<"%i5">>, crash)),
+ ?_assertError(badarg, U(<<"%5">>, crash))
+ ].
+
+urlencode_test_() ->
+ U = fun urlencode/2,
+ [?_assertEqual(<<"%ff%00">>, U(<<255,0>>, [])),
+ ?_assertEqual(<<"%FF%00">>, U(<<255,0>>, [upper])),
+ ?_assertEqual(<<"+">>, U(<<" ">>, [])),
+ ?_assertEqual(<<"%20">>, U(<<" ">>, [noplus])),
+ ?_assertEqual(<<"aBc">>, U(<<"aBc">>, [])),
+ ?_assertEqual(<<".-~_">>, U(<<".-~_">>, [])),
+ ?_assertEqual(<<"%ff+">>, urlencode(<<255, " ">>))
+ ].
+
-endif.
diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl
index c76c607..cd951d1 100644
--- a/src/cowboy_http_protocol.erl
+++ b/src/cowboy_http_protocol.erl
@@ -22,6 +22,9 @@
%% Defaults to 5.</dd>
%% <dt>timeout</dt><dd>Time in milliseconds before an idle
%% connection is closed. Defaults to 5000 milliseconds.</dd>
+%% <dt>urldecode</dt><dd>Function and options argument to use when decoding
+%% URL encoded strings. Defaults to `{fun cowboy_http:urldecode/2, crash}'.
+%% </dd>
%% </dl>
%%
%% Note that there is no need to monitor these processes when using Cowboy as
@@ -44,8 +47,11 @@
transport :: module(),
dispatch :: cowboy_dispatcher:dispatch_rules(),
handler :: {module(), any()},
+ urldecode :: {fun((binary(), T) -> binary()), T},
req_empty_lines = 0 :: integer(),
max_empty_lines :: integer(),
+ req_keepalive = 1 :: integer(),
+ max_keepalive :: integer(),
max_line_length :: integer(),
timeout :: timeout(),
buffer = <<>> :: binary(),
@@ -69,12 +75,16 @@ start_link(ListenerPid, Socket, Transport, Opts) ->
init(ListenerPid, Socket, Transport, Opts) ->
Dispatch = proplists:get_value(dispatch, Opts, []),
MaxEmptyLines = proplists:get_value(max_empty_lines, Opts, 5),
+ MaxKeepalive = proplists:get_value(max_keepalive, Opts, infinity),
MaxLineLength = proplists:get_value(max_line_length, Opts, 4096),
Timeout = proplists:get_value(timeout, Opts, 5000),
- receive shoot -> ok end,
+ URLDecDefault = {fun cowboy_http:urldecode/2, crash},
+ URLDec = proplists:get_value(urldecode, Opts, URLDecDefault),
+ ok = cowboy:accept_ack(ListenerPid),
wait_request(#state{listener=ListenerPid, socket=Socket, transport=Transport,
dispatch=Dispatch, max_empty_lines=MaxEmptyLines,
- max_line_length=MaxLineLength, timeout=Timeout}).
+ max_keepalive=MaxKeepalive, max_line_length=MaxLineLength,
+ timeout=Timeout, urldecode=URLDec}).
%% @private
-spec parse_request(#state{}) -> ok | none().
@@ -100,24 +110,24 @@ wait_request(State=#state{socket=Socket, transport=Transport,
-spec request({http_request, http_method(), http_uri(),
http_version()}, #state{}) -> ok | none().
-%% @todo We probably want to handle some things differently between versions.
request({http_request, _Method, _URI, Version}, State)
when Version =/= {1, 0}, Version =/= {1, 1} ->
error_terminate(505, State);
-%% @todo We need to cleanup the URI properly.
request({http_request, Method, {abs_path, AbsPath}, Version},
- State=#state{socket=Socket, transport=Transport}) ->
- {Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath),
+ State=#state{socket=Socket, transport=Transport,
+ urldecode={URLDecFun, URLDecArg}=URLDec}) ->
+ URLDecode = fun(Bin) -> URLDecFun(Bin, URLDecArg) end,
+ {Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath, URLDecode),
ConnAtom = version_to_connection(Version),
parse_header(#http_req{socket=Socket, transport=Transport,
- connection=ConnAtom, method=Method, version=Version,
- path=Path, raw_path=RawPath, raw_qs=Qs}, State);
+ connection=ConnAtom, pid=self(), method=Method, version=Version,
+ path=Path, raw_path=RawPath, raw_qs=Qs, urldecode=URLDec}, State);
request({http_request, Method, '*', Version},
- State=#state{socket=Socket, transport=Transport}) ->
+ State=#state{socket=Socket, transport=Transport, urldecode=URLDec}) ->
ConnAtom = version_to_connection(Version),
parse_header(#http_req{socket=Socket, transport=Transport,
- connection=ConnAtom, method=Method, version=Version,
- path='*', raw_path= <<"*">>, raw_qs= <<>>}, State);
+ connection=ConnAtom, pid=self(), method=Method, version=Version,
+ path='*', raw_path= <<"*">>, raw_qs= <<>>, urldecode=URLDec}, State);
request({http_request, _Method, _URI, _Version}, State) ->
error_terminate(501, State);
request({http_error, <<"\r\n">>},
@@ -125,7 +135,7 @@ request({http_error, <<"\r\n">>},
error_terminate(400, State);
request({http_error, <<"\r\n">>}, State=#state{req_empty_lines=N}) ->
parse_request(State#state{req_empty_lines=N + 1});
-request({http_error, _Any}, State) ->
+request(_Any, State) ->
error_terminate(400, State).
-spec parse_header(#http_req{}, #state{}) -> ok | none().
@@ -191,15 +201,17 @@ header(http_eoh, Req=#http_req{version={1, 0}, transport=Transport,
port=Port, buffer=Buffer}, State#state{buffer= <<>>});
header(http_eoh, Req, State=#state{buffer=Buffer}) ->
handler_init(Req#http_req{buffer=Buffer}, State#state{buffer= <<>>});
-header({http_error, _Bin}, _Req, State) ->
- error_terminate(500, State).
+header(_Any, _Req, State) ->
+ error_terminate(400, State).
-spec dispatch(fun((#http_req{}, #state{}) -> ok),
#http_req{}, #state{}) -> ok | none().
dispatch(Next, Req=#http_req{host=Host, path=Path},
State=#state{dispatch=Dispatch}) ->
- %% @todo We probably want to filter the Host and Path here to allow
- %% things like url rewriting.
+ %% @todo We should allow a configurable chain of handlers here to
+ %% allow things like url rewriting, site-wide authentication,
+ %% optional dispatching, and more. It would default to what
+ %% we are doing so far.
case cowboy_dispatcher:match(Host, Path, Dispatch) of
{ok, Handler, Opts, Binds, HostInfo, PathInfo} ->
Next(Req#http_req{host_info=HostInfo, path_info=PathInfo,
@@ -211,8 +223,8 @@ dispatch(Next, Req=#http_req{host=Host, path=Path},
end.
-spec handler_init(#http_req{}, #state{}) -> ok | none().
-handler_init(Req, State=#state{listener=ListenerPid,
- transport=Transport, handler={Handler, Opts}}) ->
+handler_init(Req, State=#state{transport=Transport,
+ handler={Handler, Opts}}) ->
try Handler:init({Transport:name(), http}, Req, Opts) of
{ok, Req2, HandlerState} ->
handler_handle(HandlerState, Req2, State);
@@ -231,7 +243,7 @@ handler_init(Req, State=#state{listener=ListenerPid,
handler_terminate(HandlerState, Req2, State);
%% @todo {upgrade, transport, Module}
{upgrade, protocol, Module} ->
- Module:upgrade(ListenerPid, Handler, Opts, Req)
+ upgrade_protocol(Req, State, Module)
catch Class:Reason ->
error_terminate(500, State),
error_logger:error_msg(
@@ -242,11 +254,19 @@ handler_init(Req, State=#state{listener=ListenerPid,
[Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()])
end.
+-spec upgrade_protocol(#http_req{}, #state{}, atom()) -> ok | none().
+upgrade_protocol(Req, State=#state{listener=ListenerPid,
+ handler={Handler, Opts}}, Module) ->
+ case Module:upgrade(ListenerPid, Handler, Opts, Req) of
+ {UpgradeRes, Req2} -> next_request(Req2, State, UpgradeRes);
+ _Any -> terminate(State)
+ end.
+
-spec handler_handle(any(), #http_req{}, #state{}) -> ok | none().
handler_handle(HandlerState, Req, State=#state{handler={Handler, Opts}}) ->
try Handler:handle(Req, HandlerState) of
{ok, Req2, HandlerState2} ->
- next_request(HandlerState2, Req2, State)
+ terminate_request(HandlerState2, Req2, State)
catch Class:Reason ->
error_logger:error_msg(
"** Handler ~p terminating in handle/2~n"
@@ -256,7 +276,7 @@ handler_handle(HandlerState, Req, State=#state{handler={Handler, Opts}}) ->
[Handler, Class, Reason, Opts,
HandlerState, Req, erlang:get_stacktrace()]),
handler_terminate(HandlerState, Req, State),
- terminate(State)
+ error_terminate(500, State)
end.
%% We don't listen for Transport closes because that would force us
@@ -286,7 +306,7 @@ handler_loop_timeout(State=#state{loop_timeout=Timeout,
handler_loop(HandlerState, Req, State=#state{loop_timeout_ref=TRef}) ->
receive
{?MODULE, timeout, TRef} ->
- next_request(HandlerState, Req, State);
+ terminate_request(HandlerState, Req, State);
{?MODULE, timeout, OlderTRef} when is_reference(OlderTRef) ->
handler_loop(HandlerState, Req, State);
Message ->
@@ -298,7 +318,7 @@ handler_call(HandlerState, Req, State=#state{handler={Handler, Opts}},
Message) ->
try Handler:info(Message, Req, HandlerState) of
{ok, Req2, HandlerState2} ->
- next_request(HandlerState2, Req2, State);
+ terminate_request(HandlerState2, Req2, State);
{loop, Req2, HandlerState2} ->
handler_before_loop(HandlerState2, Req2, State);
{loop, Req2, HandlerState2, hibernate} ->
@@ -311,7 +331,9 @@ handler_call(HandlerState, Req, State=#state{handler={Handler, Opts}},
"** Options were ~p~n** Handler state was ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
[Handler, Class, Reason, Opts,
- HandlerState, Req, erlang:get_stacktrace()])
+ HandlerState, Req, erlang:get_stacktrace()]),
+ handler_terminate(HandlerState, Req, State),
+ error_terminate(500, State)
end.
-spec handler_terminate(any(), #http_req{}, #state{}) -> ok.
@@ -328,16 +350,24 @@ handler_terminate(HandlerState, Req, #state{handler={Handler, Opts}}) ->
HandlerState, Req, erlang:get_stacktrace()])
end.
--spec next_request(any(), #http_req{}, #state{}) -> ok | none().
-next_request(HandlerState, Req=#http_req{connection=Conn, buffer=Buffer},
- State) ->
+-spec terminate_request(any(), #http_req{}, #state{}) -> ok | none().
+terminate_request(HandlerState, Req, State) ->
HandlerRes = handler_terminate(HandlerState, Req, State),
- BodyRes = ensure_body_processed(Req),
+ next_request(Req, State, HandlerRes).
+
+-spec next_request(#http_req{}, #state{}, any()) -> ok | none().
+next_request(Req=#http_req{connection=Conn, buffer=Buffer},
+ State=#state{req_keepalive=Keepalive, max_keepalive=MaxKeepalive},
+ HandlerRes) ->
RespRes = ensure_response(Req),
+ BodyRes = ensure_body_processed(Req),
+ %% Flush the resp_sent message before moving on.
+ receive {cowboy_http_req, resp_sent} -> ok after 0 -> ok end,
case {HandlerRes, BodyRes, RespRes, Conn} of
- {ok, ok, ok, keepalive} ->
+ {ok, ok, ok, keepalive} when Keepalive < MaxKeepalive ->
?MODULE:parse_request(State#state{
- buffer=Buffer, req_empty_lines=0});
+ buffer=Buffer, req_empty_lines=0,
+ req_keepalive=Keepalive + 1});
_Closed ->
terminate(State)
end.
@@ -372,11 +402,17 @@ ensure_response(#http_req{socket=Socket, transport=Transport,
Transport:send(Socket, <<"0\r\n\r\n">>),
close.
+%% Only send an error reply if there is no resp_sent message.
-spec error_terminate(http_status(), #state{}) -> ok.
error_terminate(Code, State=#state{socket=Socket, transport=Transport}) ->
- _ = cowboy_http_req:reply(Code, [], [], #http_req{
- socket=Socket, transport=Transport,
- connection=close, resp_state=waiting}),
+ receive
+ {cowboy_http_req, resp_sent} -> ok
+ after 0 ->
+ _ = cowboy_http_req:reply(Code, #http_req{
+ socket=Socket, transport=Transport,
+ connection=close, pid=self(), resp_state=waiting}),
+ ok
+ end,
terminate(State).
-spec terminate(#state{}) -> ok.
diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl
index f850e52..aa30d2c 100644
--- a/src/cowboy_http_req.erl
+++ b/src/cowboy_http_req.erl
@@ -22,32 +22,32 @@
-module(cowboy_http_req).
-export([
- method/1, version/1, peer/1,
+ method/1, version/1, peer/1, peer_addr/1,
host/1, host_info/1, raw_host/1, port/1,
path/1, path_info/1, raw_path/1,
qs_val/2, qs_val/3, qs_vals/1, raw_qs/1,
binding/2, binding/3, bindings/1,
header/2, header/3, headers/1,
parse_header/2, parse_header/3,
- cookie/2, cookie/3, cookies/1
+ cookie/2, cookie/3, cookies/1,
+ meta/2, meta/3
]). %% Request API.
-export([
- body/1, body/2, body_qs/1
-]). %% Request Body API.
-
--export([
+ body/1, body/2, body_qs/1,
multipart_data/1, multipart_skip/1
-]). %% Request Multipart API.
+]). %% Request Body API.
-export([
+ set_resp_cookie/4, set_resp_header/3, set_resp_body/2,
+ set_resp_body_fun/3, has_resp_header/2, has_resp_body/1,
reply/2, reply/3, reply/4,
chunked_reply/2, chunked_reply/3, chunk/2,
upgrade_reply/3
]). %% Response API.
-export([
- compact/1
+ compact/1, transport/1
]). %% Misc API.
-include("include/http.hrl").
@@ -73,6 +73,29 @@ peer(Req=#http_req{socket=Socket, transport=Transport, peer=undefined}) ->
peer(Req) ->
{Req#http_req.peer, Req}.
+%% @doc Returns the peer address calculated from headers.
+-spec peer_addr(#http_req{}) -> {inet:ip_address(), #http_req{}}.
+peer_addr(Req = #http_req{}) ->
+ {RealIp, Req1} = header(<<"X-Real-Ip">>, Req),
+ {ForwardedForRaw, Req2} = header(<<"X-Forwarded-For">>, Req1),
+ {{PeerIp, _PeerPort}, Req3} = peer(Req2),
+ ForwardedFor = case ForwardedForRaw of
+ undefined ->
+ undefined;
+ ForwardedForRaw ->
+ case re:run(ForwardedForRaw, "^(?<first_ip>[^\\,]+)",
+ [{capture, [first_ip], binary}]) of
+ {match, [FirstIp]} -> FirstIp;
+ _Any -> undefined
+ end
+ end,
+ {ok, PeerAddr} = if
+ is_binary(RealIp) -> inet_parse:address(binary_to_list(RealIp));
+ is_binary(ForwardedFor) -> inet_parse:address(binary_to_list(ForwardedFor));
+ true -> {ok, PeerIp}
+ end,
+ {PeerAddr, Req3}.
+
%% @doc Return the tokens for the hostname requested.
-spec host(#http_req{}) -> {cowboy_dispatcher:tokens(), #http_req{}}.
host(Req) ->
@@ -126,9 +149,9 @@ qs_val(Name, Req) when is_binary(Name) ->
%% missing.
-spec qs_val(binary(), #http_req{}, Default)
-> {binary() | true | Default, #http_req{}} when Default::any().
-qs_val(Name, Req=#http_req{raw_qs=RawQs, qs_vals=undefined}, Default)
- when is_binary(Name) ->
- QsVals = parse_qs(RawQs),
+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),
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
@@ -138,8 +161,9 @@ qs_val(Name, Req, Default) ->
%% @doc Return the full list of query string values.
-spec qs_vals(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
-qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined}) ->
- QsVals = parse_qs(RawQs),
+qs_vals(Req=#http_req{raw_qs=RawQs, qs_vals=undefined,
+ urldecode={URLDecFun, URLDecArg}}) ->
+ QsVals = parse_qs(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}.
@@ -204,13 +228,7 @@ parse_header(Name, Req=#http_req{p_headers=PHeaders}) ->
%% @doc Default values for semantic header parsing.
-spec parse_header_default(http_header()) -> any().
-parse_header_default('Accept') -> [];
-parse_header_default('Accept-Charset') -> [];
-parse_header_default('Accept-Encoding') -> [];
-parse_header_default('Accept-Language') -> [];
parse_header_default('Connection') -> [];
-parse_header_default('If-Match') -> '*';
-parse_header_default('If-None-Match') -> '*';
parse_header_default(_Name) -> undefined.
%% @doc Semantically parse headers.
@@ -265,6 +283,11 @@ parse_header(Name, Req, Default)
fun (Value) ->
cowboy_http:http_date(Value)
end);
+parse_header(Name, Req, Default) when Name =:= 'Upgrade' ->
+ parse_header(Name, Req, Default,
+ fun (Value) ->
+ cowboy_http:nonempty_list(Value, fun cowboy_http:token_ci/2)
+ end);
parse_header(Name, Req, Default) ->
{Value, Req2} = header(Name, Req, Default),
{undefined, Value, Req2}.
@@ -319,11 +342,29 @@ cookies(Req=#http_req{cookies=undefined}) ->
cookies(Req=#http_req{cookies=Cookies}) ->
{Cookies, Req}.
+%% @equiv meta(Name, Req, undefined)
+-spec meta(atom(), #http_req{}) -> {any() | undefined, #http_req{}}.
+meta(Name, Req) ->
+ meta(Name, Req, undefined).
+
+%% @doc Return metadata information about the request.
+%%
+%% Metadata information varies from one protocol to another. Websockets
+%% would define the protocol version here, while REST would use it to
+%% indicate which media type, language and charset were retained.
+-spec meta(atom(), #http_req{}, any()) -> {any(), #http_req{}}.
+meta(Name, Req, Default) ->
+ case lists:keyfind(Name, 1, Req#http_req.meta) of
+ {Name, Value} -> {Value, Req};
+ false -> {Default, Req}
+ end.
+
%% Request Body API.
%% @doc Return the full body sent with the request, or <em>{error, badarg}</em>
%% if no <em>Content-Length</em> is available.
%% @todo We probably want to allow a max length.
+%% @todo Add multipart support to this function.
-spec body(#http_req{}) -> {ok, binary(), #http_req{}} | {error, atom()}.
body(Req) ->
{Length, Req2} = cowboy_http_req:parse_header('Content-Length', Req),
@@ -343,12 +384,11 @@ body(Req) ->
-spec body(non_neg_integer(), #http_req{})
-> {ok, binary(), #http_req{}} | {error, atom()}.
body(Length, Req=#http_req{body_state=waiting, buffer=Buffer})
- when Length =< byte_size(Buffer) ->
+ when is_integer(Length) andalso Length =< byte_size(Buffer) ->
<< Body:Length/binary, Rest/bits >> = Buffer,
{ok, Body, Req#http_req{body_state=done, buffer=Rest}};
body(Length, Req=#http_req{socket=Socket, transport=Transport,
- body_state=waiting, buffer=Buffer})
- when is_integer(Length) andalso Length > byte_size(Buffer) ->
+ body_state=waiting, buffer=Buffer}) ->
case Transport:recv(Socket, Length - byte_size(Buffer), 5000) of
{ok, Body} -> {ok, << Buffer/binary, Body/binary >>,
Req#http_req{body_state=done, buffer= <<>>}};
@@ -358,9 +398,9 @@ body(Length, Req=#http_req{socket=Socket, transport=Transport,
%% @doc Return the full body sent with the reqest, parsed as an
%% application/x-www-form-urlencoded string. Essentially a POST query string.
-spec body_qs(#http_req{}) -> {list({binary(), binary() | true}), #http_req{}}.
-body_qs(Req) ->
+body_qs(Req=#http_req{urldecode={URLDecFun, URLDecArg}}) ->
{ok, Body, Req2} = body(Req),
- {parse_qs(Body), Req2}.
+ {parse_qs(Body, fun(Bin) -> URLDecFun(Bin, URLDecArg) end), Req2}.
%% Multipart Request API.
@@ -373,7 +413,9 @@ body_qs(Req) ->
%%
%% If the request Content-Type is not a multipart one, <em>{error, badarg}</em>
%% is returned.
--spec multipart_data(#http_req{}) -> {multipart_data(), #http_req{}}.
+-spec multipart_data(#http_req{})
+ -> {{headers, http_headers()} | {data, binary()} | end_of_part | eof,
+ #http_req{}}.
multipart_data(Req=#http_req{body_state=waiting}) ->
{{<<"multipart">>, _SubType, Params}, Req2} =
parse_header('Content-Type', Req),
@@ -427,35 +469,95 @@ multipart_skip(Req) ->
%% Response API.
+%% @doc Add a cookie header to the response.
+-spec set_resp_cookie(binary(), binary(), [cowboy_cookies:cookie_option()],
+ #http_req{}) -> {ok, #http_req{}}.
+set_resp_cookie(Name, Value, Options, Req) ->
+ {HeaderName, HeaderValue} = cowboy_cookies:cookie(Name, Value, Options),
+ set_resp_header(HeaderName, HeaderValue, Req).
+
+%% @doc Add a header to the response.
+-spec set_resp_header(http_header(), iodata(), #http_req{})
+ -> {ok, #http_req{}}.
+set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) ->
+ NameBin = header_to_binary(Name),
+ {ok, Req#http_req{resp_headers=[{NameBin, Value}|RespHeaders]}}.
+
+%% @doc Add a body to the response.
+%%
+%% The body set here is ignored if the response is later sent using
+%% anything other than reply/2 or reply/3. The response body is expected
+%% to be a binary or an iolist.
+-spec set_resp_body(iodata(), #http_req{}) -> {ok, #http_req{}}.
+set_resp_body(Body, Req) ->
+ {ok, Req#http_req{resp_body=Body}}.
+
+
+%% @doc Add a body function to the response.
+%%
+%% The response body may also be set to a content-length - stream-function pair.
+%% If the response body is of this type normal response headers will be sent.
+%% After the response headers has been sent the body function is applied.
+%% The body function is expected to write the response body directly to the
+%% socket using the transport module.
+%%
+%% If the body function crashes while writing the response body or writes fewer
+%% bytes than declared the behaviour is undefined. The body set here is ignored
+%% if the response is later sent using anything other than `reply/2' or
+%% `reply/3'.
+%%
+%% @see cowboy_http_req:transport/1.
+-spec set_resp_body_fun(non_neg_integer(), fun(() -> {sent, non_neg_integer()}),
+ #http_req{}) -> {ok, #http_req{}}.
+set_resp_body_fun(StreamLen, StreamFun, Req) ->
+ {ok, Req#http_req{resp_body={StreamLen, StreamFun}}}.
+
+
+%% @doc Return whether the given header has been set for the response.
+-spec has_resp_header(http_header(), #http_req{}) -> boolean().
+has_resp_header(Name, #http_req{resp_headers=RespHeaders}) ->
+ NameBin = header_to_binary(Name),
+ lists:keymember(NameBin, 1, RespHeaders).
+
+%% @doc Return whether a body has been set for the response.
+-spec has_resp_body(#http_req{}) -> boolean().
+has_resp_body(#http_req{resp_body={Length, _}}) ->
+ Length > 0;
+has_resp_body(#http_req{resp_body=RespBody}) ->
+ iolist_size(RespBody) > 0.
+
%% @equiv reply(Status, [], [], Req)
-spec reply(http_status(), #http_req{}) -> {ok, #http_req{}}.
-reply(Status, Req) ->
- reply(Status, [], [], Req).
+reply(Status, Req=#http_req{resp_body=Body}) ->
+ reply(Status, [], Body, Req).
%% @equiv reply(Status, Headers, [], Req)
-spec reply(http_status(), http_headers(), #http_req{}) -> {ok, #http_req{}}.
-reply(Status, Headers, Req) ->
- reply(Status, Headers, [], Req).
+reply(Status, Headers, Req=#http_req{resp_body=Body}) ->
+ reply(Status, Headers, Body, Req).
%% @doc Send a reply to the client.
-spec reply(http_status(), http_headers(), iodata(), #http_req{})
-> {ok, #http_req{}}.
reply(Status, Headers, Body, Req=#http_req{socket=Socket,
- transport=Transport, connection=Connection,
- method=Method, resp_state=waiting}) ->
+ transport=Transport, connection=Connection, pid=ReqPid,
+ method=Method, resp_state=waiting, resp_headers=RespHeaders}) ->
RespConn = response_connection(Headers, Connection),
- Head = response_head(Status, Headers, [
+ ContentLen = case Body of {CL, _} -> CL; _ -> iolist_size(Body) end,
+ Head = response_head(Status, Headers, RespHeaders, [
{<<"Connection">>, atom_to_connection(Connection)},
- {<<"Content-Length">>,
- list_to_binary(integer_to_list(iolist_size(Body)))},
+ {<<"Content-Length">>, integer_to_list(ContentLen)},
{<<"Date">>, cowboy_clock:rfc1123()},
{<<"Server">>, <<"Cowboy">>}
]),
- case Method of
- 'HEAD' -> Transport:send(Socket, Head);
- _ -> Transport:send(Socket, [Head, Body])
+ case {Method, Body} of
+ {'HEAD', _} -> Transport:send(Socket, Head);
+ {_, {_, StreamFun}} -> Transport:send(Socket, Head), StreamFun();
+ {_, _} -> Transport:send(Socket, [Head, Body])
end,
- {ok, Req#http_req{connection=RespConn, resp_state=done}}.
+ ReqPid ! {?MODULE, resp_sent},
+ {ok, Req#http_req{connection=RespConn, resp_state=done,
+ resp_headers=[], resp_body= <<>>}}.
%% @equiv chunked_reply(Status, [], Req)
-spec chunked_reply(http_status(), #http_req{}) -> {ok, #http_req{}}.
@@ -466,17 +568,20 @@ chunked_reply(Status, Req) ->
%% @see cowboy_http_req:chunk/2
-spec chunked_reply(http_status(), http_headers(), #http_req{})
-> {ok, #http_req{}}.
-chunked_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport,
- connection=Connection, resp_state=waiting}) ->
+chunked_reply(Status, Headers, Req=#http_req{socket=Socket,
+ transport=Transport, connection=Connection, pid=ReqPid,
+ resp_state=waiting, resp_headers=RespHeaders}) ->
RespConn = response_connection(Headers, Connection),
- Head = response_head(Status, Headers, [
+ Head = response_head(Status, Headers, RespHeaders, [
{<<"Connection">>, atom_to_connection(Connection)},
{<<"Transfer-Encoding">>, <<"chunked">>},
{<<"Date">>, cowboy_clock:rfc1123()},
{<<"Server">>, <<"Cowboy">>}
]),
Transport:send(Socket, Head),
- {ok, Req#http_req{connection=RespConn, resp_state=chunks}}.
+ ReqPid ! {?MODULE, resp_sent},
+ {ok, Req#http_req{connection=RespConn, resp_state=chunks,
+ resp_headers=[], resp_body= <<>>}}.
%% @doc Send a chunk of data.
%%
@@ -489,15 +594,17 @@ chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) ->
<<"\r\n">>, Data, <<"\r\n">>]).
%% @doc Send an upgrade reply.
+%% @private
-spec upgrade_reply(http_status(), http_headers(), #http_req{})
-> {ok, #http_req{}}.
upgrade_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport,
- resp_state=waiting}) ->
- Head = response_head(Status, Headers, [
+ pid=ReqPid, resp_state=waiting, resp_headers=RespHeaders}) ->
+ Head = response_head(Status, Headers, RespHeaders, [
{<<"Connection">>, <<"Upgrade">>}
]),
Transport:send(Socket, Head),
- {ok, Req#http_req{resp_state=done}}.
+ ReqPid ! {?MODULE, resp_sent},
+ {ok, Req#http_req{resp_state=done, resp_headers=[], resp_body= <<>>}}.
%% Misc API.
@@ -510,18 +617,32 @@ upgrade_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport,
compact(Req) ->
Req#http_req{host=undefined, host_info=undefined, path=undefined,
path_info=undefined, qs_vals=undefined,
- bindings=undefined, headers=[]}.
+ bindings=undefined, headers=[],
+ p_headers=[], cookies=[]}.
+
+%% @doc Return the transport module and socket associated with a request.
+%%
+%% This exposes the same socket interface used internally by the HTTP protocol
+%% implementation to developers that needs low level access to the socket.
+%%
+%% It is preferred to use this in conjuction with the stream function support
+%% in `set_resp_body_fun/3' if this is used to write a response body directly
+%% to the socket. This ensures that the response headers are set correctly.
+-spec transport(#http_req{}) -> {ok, module(), inet:socket()}.
+transport(#http_req{transport=Transport, socket=Socket}) ->
+ {ok, Transport, Socket}.
%% Internal.
--spec parse_qs(binary()) -> list({binary(), binary() | true}).
-parse_qs(<<>>) ->
+-spec parse_qs(binary(), fun((binary()) -> binary())) ->
+ list({binary(), binary() | true}).
+parse_qs(<<>>, _URLDecode) ->
[];
-parse_qs(Qs) ->
+parse_qs(Qs, URLDecode) ->
Tokens = binary:split(Qs, <<"&">>, [global, trim]),
[case binary:split(Token, <<"=">>) of
- [Token] -> {quoted:from_url(Token), true};
- [Name, Value] -> {quoted:from_url(Name), quoted:from_url(Value)}
+ [Token] -> {URLDecode(Token), true};
+ [Name, Value] -> {URLDecode(Name), URLDecode(Value)}
end || Token <- Tokens].
-spec response_connection(http_headers(), keepalive | close)
@@ -545,15 +666,27 @@ response_connection_parse(ReplyConn) ->
Tokens = cowboy_http:nonempty_list(ReplyConn, fun cowboy_http:token/2),
cowboy_http:connection_to_atom(Tokens).
--spec response_head(http_status(), http_headers(), http_headers()) -> iolist().
-response_head(Status, Headers, DefaultHeaders) ->
+-spec response_head(http_status(), http_headers(), http_headers(),
+ http_headers()) -> iolist().
+response_head(Status, Headers, RespHeaders, DefaultHeaders) ->
StatusLine = <<"HTTP/1.1 ", (status(Status))/binary, "\r\n">>,
Headers2 = [{header_to_binary(Key), Value} || {Key, Value} <- Headers],
- Headers3 = lists:keysort(1, Headers2),
- Headers4 = lists:ukeymerge(1, Headers3, DefaultHeaders),
- Headers5 = [[Key, <<": ">>, Value, <<"\r\n">>]
- || {Key, Value} <- Headers4],
- [StatusLine, Headers5, <<"\r\n">>].
+ Headers3 = merge_headers(
+ merge_headers(Headers2, RespHeaders),
+ DefaultHeaders),
+ Headers4 = [[Key, <<": ">>, Value, <<"\r\n">>]
+ || {Key, Value} <- Headers3],
+ [StatusLine, Headers4, <<"\r\n">>].
+
+-spec merge_headers(http_headers(), http_headers()) -> http_headers().
+merge_headers(Headers, []) ->
+ Headers;
+merge_headers(Headers, [{Name, Value}|Tail]) ->
+ Headers2 = case lists:keymember(Name, 1, Headers) of
+ true -> Headers;
+ false -> Headers ++ [{Name, Value}]
+ end,
+ merge_headers(Headers2, Tail).
-spec atom_to_connection(keepalive) -> <<_:80>>;
(close) -> <<_:40>>.
@@ -689,6 +822,7 @@ parse_qs_test_() ->
{<<"a=b=c=d=e&f=g">>, [{<<"a">>, <<"b=c=d=e">>}, {<<"f">>, <<"g">>}]},
{<<"a+b=c+d">>, [{<<"a b">>, <<"c d">>}]}
],
- [{Qs, fun() -> R = parse_qs(Qs) end} || {Qs, R} <- Tests].
+ 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
new file mode 100644
index 0000000..35f82e3
--- /dev/null
+++ b/src/cowboy_http_rest.erl
@@ -0,0 +1,872 @@
+%% Copyright (c) 2011, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% @doc Experimental REST protocol implementation.
+%%
+%% Based on the Webmachine Diagram from Alan Dean and Justin Sheehy, which
+%% can be found in the Webmachine source tree, and on the Webmachine
+%% documentation available at http://wiki.basho.com/Webmachine.html
+%% at the time of writing.
+-module(cowboy_http_rest).
+-export([upgrade/4]).
+
+-record(state, {
+ %% Handler.
+ handler :: atom(),
+ handler_state :: any(),
+
+ %% Media type.
+ content_types_p = [] ::
+ [{{binary(), binary(), [{binary(), binary()}]}, atom()}],
+ content_type_a :: undefined
+ | {{binary(), binary(), [{binary(), binary()}]}, atom()},
+
+ %% Language.
+ languages_p = [] :: [binary()],
+ language_a :: undefined | binary(),
+
+ %% Charset.
+ charsets_p = [] :: [binary()],
+ charset_a :: undefined | binary(),
+
+ %% Cached resource calls.
+ etag :: undefined | no_call | binary(),
+ last_modified :: undefined | no_call | calendar:datetime(),
+ expires :: undefined | no_call | calendar:datetime()
+}).
+
+-include("include/http.hrl").
+
+%% @doc Upgrade a HTTP request to the REST protocol.
+%%
+%% You do not need to call this function manually. To upgrade to the REST
+%% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em>
+%% in your <em>cowboy_http_handler:init/3</em> handler function.
+-spec upgrade(pid(), module(), any(), #http_req{}) -> {ok, #http_req{}}.
+upgrade(_ListenerPid, Handler, Opts, Req) ->
+ try
+ case erlang:function_exported(Handler, rest_init, 2) of
+ true ->
+ case Handler:rest_init(Req, Opts) of
+ {ok, Req2, HandlerState} ->
+ service_available(Req2, #state{handler=Handler,
+ handler_state=HandlerState})
+ end;
+ false ->
+ service_available(Req, #state{handler=Handler})
+ end
+ catch Class:Reason ->
+ error_logger:error_msg(
+ "** Handler ~p terminating in rest_init/3~n"
+ " for the reason ~p:~p~n** Options were ~p~n"
+ "** Request was ~p~n** Stacktrace: ~p~n~n",
+ [Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()]),
+ {ok, _Req2} = cowboy_http_req:reply(500, Req),
+ ok
+ end.
+
+service_available(Req, State) ->
+ expect(Req, State, service_available, true, fun known_methods/2, 503).
+
+%% known_methods/2 should return a list of atoms or binary methods.
+known_methods(Req=#http_req{method=Method}, State) ->
+ case call(Req, State, known_methods) of
+ no_call when Method =:= 'HEAD'; Method =:= 'GET'; Method =:= 'POST';
+ Method =:= 'PUT'; Method =:= 'DELETE'; Method =:= 'TRACE';
+ Method =:= 'CONNECT'; Method =:= 'OPTIONS' ->
+ next(Req, State, fun uri_too_long/2);
+ no_call ->
+ next(Req, State, 501);
+ {List, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
+ case lists:member(Method, List) of
+ true -> next(Req2, State2, fun uri_too_long/2);
+ false -> next(Req2, State2, 501)
+ end
+ end.
+
+uri_too_long(Req, State) ->
+ expect(Req, State, uri_too_long, false, fun allowed_methods/2, 414).
+
+%% allowed_methods/2 should return a list of atoms or binary methods.
+allowed_methods(Req=#http_req{method=Method}, State) ->
+ case call(Req, State, allowed_methods) of
+ no_call when Method =:= 'HEAD'; Method =:= 'GET' ->
+ next(Req, State, fun malformed_request/2);
+ no_call ->
+ method_not_allowed(Req, State, ['GET', 'HEAD']);
+ {List, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
+ case lists:member(Method, List) of
+ true -> next(Req2, State2, fun malformed_request/2);
+ false -> method_not_allowed(Req2, State2, List)
+ end
+ end.
+
+method_not_allowed(Req, State, Methods) ->
+ {ok, Req2} = cowboy_http_req:set_resp_header(
+ <<"Allow">>, method_not_allowed_build(Methods, []), Req),
+ respond(Req2, State, 405).
+
+method_not_allowed_build([], []) ->
+ <<>>;
+method_not_allowed_build([], [_Ignore|Acc]) ->
+ lists:reverse(Acc);
+method_not_allowed_build([Method|Tail], Acc) when is_atom(Method) ->
+ Method2 = list_to_binary(atom_to_list(Method)),
+ method_not_allowed_build(Tail, [<<", ">>, Method2|Acc]);
+method_not_allowed_build([Method|Tail], Acc) ->
+ method_not_allowed_build(Tail, [<<", ">>, Method|Acc]).
+
+malformed_request(Req, State) ->
+ expect(Req, State, malformed_request, false, fun is_authorized/2, 400).
+
+%% is_authorized/2 should return true or {false, WwwAuthenticateHeader}.
+is_authorized(Req, State) ->
+ case call(Req, State, is_authorized) of
+ no_call ->
+ forbidden(Req, State);
+ {true, Req2, HandlerState2} ->
+ forbidden(Req2, State#state{handler_state=HandlerState2});
+ {{false, AuthHead}, Req2, HandlerState2} ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Www-Authenticate">>, AuthHead, Req2),
+ respond(Req3, State#state{handler_state=HandlerState2}, 401)
+ end.
+
+forbidden(Req, State) ->
+ expect(Req, State, forbidden, false, fun valid_content_headers/2, 403).
+
+valid_content_headers(Req, State) ->
+ expect(Req, State, valid_content_headers, true,
+ fun known_content_type/2, 501).
+
+known_content_type(Req, State) ->
+ expect(Req, State, known_content_type, true,
+ fun valid_entity_length/2, 413).
+
+valid_entity_length(Req, State) ->
+ expect(Req, State, valid_entity_length, true, fun options/2, 413).
+
+%% If you need to add additional headers to the response at this point,
+%% you should do it directly in the options/2 call using set_resp_headers.
+options(Req=#http_req{method='OPTIONS'}, State) ->
+ {ok, Req2, HandlerState2} = call(Req, State, options),
+ respond(Req2, State#state{handler_state=HandlerState2}, 200);
+options(Req, State) ->
+ content_types_provided(Req, State).
+
+%% content_types_provided/2 should return a list of content types and their
+%% associated callback function as a tuple: {{Type, SubType, Params}, Fun}.
+%% Type and SubType are the media type as binary. Params is a list of
+%% Key/Value tuple, with Key and Value a binary. Fun is the name of the
+%% callback that will be used to return the content of the response. It is
+%% given as an atom.
+%%
+%% An example of such return value would be:
+%% {{<<"text">>, <<"html">>, []}, to_html}
+%%
+%% Note that it is also possible to return a binary content type that will
+%% then be parsed by Cowboy. However note that while this may make your
+%% resources a little more readable, this is a lot less efficient. An example
+%% of such a return value would be:
+%% {<<"text/html">>, to_html}
+content_types_provided(Req=#http_req{meta=Meta}, State) ->
+ case call(Req, State, content_types_provided) of
+ no_call ->
+ not_acceptable(Req, State);
+ {[], Req2, HandlerState} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState});
+ {CTP, Req2, HandlerState} ->
+ CTP2 = [normalize_content_types_provided(P) || P <- CTP],
+ State2 = State#state{
+ handler_state=HandlerState, content_types_p=CTP2},
+ {Accept, Req3} = cowboy_http_req:parse_header('Accept', Req2),
+ case Accept of
+ undefined ->
+ {PMT, _Fun} = HeadCTP = hd(CTP2),
+ languages_provided(
+ Req3#http_req{meta=[{media_type, PMT}|Meta]},
+ State2#state{content_type_a=HeadCTP});
+ Accept ->
+ Accept2 = prioritize_accept(Accept),
+ choose_media_type(Req3, State2, Accept2)
+ end
+ end.
+
+normalize_content_types_provided({ContentType, Handler})
+ when is_binary(ContentType) ->
+ {cowboy_http:content_type(ContentType), Handler};
+normalize_content_types_provided(Provided) ->
+ Provided.
+
+prioritize_accept(Accept) ->
+ lists:sort(
+ fun ({MediaTypeA, Quality, _AcceptParamsA},
+ {MediaTypeB, Quality, _AcceptParamsB}) ->
+ %% Same quality, check precedence in more details.
+ prioritize_mediatype(MediaTypeA, MediaTypeB);
+ ({_MediaTypeA, QualityA, _AcceptParamsA},
+ {_MediaTypeB, QualityB, _AcceptParamsB}) ->
+ %% Just compare the quality.
+ QualityA > QualityB
+ end, Accept).
+
+%% Media ranges can be overridden by more specific media ranges or
+%% specific media types. If more than one media range applies to a given
+%% type, the most specific reference has precedence.
+%%
+%% We always choose B over A when we can't decide between the two.
+prioritize_mediatype({TypeA, SubTypeA, ParamsA}, {TypeB, SubTypeB, ParamsB}) ->
+ case TypeB of
+ TypeA ->
+ case SubTypeB of
+ SubTypeA -> length(ParamsA) > length(ParamsB);
+ <<"*">> -> true;
+ _Any -> false
+ end;
+ <<"*">> -> true;
+ _Any -> false
+ end.
+
+%% Ignoring the rare AcceptParams. Not sure what should be done about them.
+choose_media_type(Req, State, []) ->
+ not_acceptable(Req, State);
+choose_media_type(Req, State=#state{content_types_p=CTP},
+ [MediaType|Tail]) ->
+ match_media_type(Req, State, Tail, CTP, MediaType).
+
+match_media_type(Req, State, Accept, [], _MediaType) ->
+ choose_media_type(Req, State, Accept);
+match_media_type(Req, State, Accept, CTP,
+ MediaType = {{<<"*">>, <<"*">>, _Params_A}, _QA, _APA}) ->
+ match_media_type_params(Req, State, Accept, CTP, MediaType);
+match_media_type(Req, State, Accept,
+ CTP = [{{Type, SubType_P, _PP}, _Fun}|_Tail],
+ MediaType = {{Type, SubType_A, _PA}, _QA, _APA})
+ when SubType_P =:= SubType_A; SubType_A =:= <<"*">> ->
+ match_media_type_params(Req, State, Accept, CTP, MediaType);
+match_media_type(Req, State, Accept, [_Any|Tail], MediaType) ->
+ match_media_type(Req, State, Accept, Tail, MediaType).
+
+match_media_type_params(Req=#http_req{meta=Meta}, State, Accept,
+ [Provided = {PMT = {_TP, _STP, Params_P}, _Fun}|Tail],
+ MediaType = {{_TA, _STA, Params_A}, _QA, _APA}) ->
+ case lists:sort(Params_P) =:= lists:sort(Params_A) of
+ true ->
+ languages_provided(Req#http_req{meta=[{media_type, PMT}|Meta]},
+ State#state{content_type_a=Provided});
+ false ->
+ match_media_type(Req, State, Accept, Tail, MediaType)
+ end.
+
+%% languages_provided should return a list of binary values indicating
+%% which languages are accepted by the resource.
+%%
+%% @todo I suppose we should also ask the resource if it wants to
+%% set a language itself or if it wants it to be automatically chosen.
+languages_provided(Req, State) ->
+ case call(Req, State, languages_provided) of
+ no_call ->
+ charsets_provided(Req, State);
+ {[], Req2, HandlerState2} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState2});
+ {LP, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2, languages_p=LP},
+ {AcceptLanguage, Req3} =
+ cowboy_http_req:parse_header('Accept-Language', Req2),
+ case AcceptLanguage of
+ undefined ->
+ set_language(Req3, State2#state{language_a=hd(LP)});
+ AcceptLanguage ->
+ AcceptLanguage2 = prioritize_languages(AcceptLanguage),
+ choose_language(Req3, State2, AcceptLanguage2)
+ end
+ end.
+
+%% A language-range matches a language-tag if it exactly equals the tag,
+%% or if it exactly equals a prefix of the tag such that the first tag
+%% character following the prefix is "-". The special range "*", if
+%% present in the Accept-Language field, matches every tag not matched
+%% by any other range present in the Accept-Language field.
+%%
+%% @todo The last sentence probably means we should always put '*'
+%% at the end of the list.
+prioritize_languages(AcceptLanguages) ->
+ lists:sort(
+ fun ({_TagA, QualityA}, {_TagB, QualityB}) ->
+ QualityA > QualityB
+ end, AcceptLanguages).
+
+choose_language(Req, State, []) ->
+ not_acceptable(Req, State);
+choose_language(Req, State=#state{languages_p=LP}, [Language|Tail]) ->
+ match_language(Req, State, Tail, LP, Language).
+
+match_language(Req, State, Accept, [], _Language) ->
+ choose_language(Req, State, Accept);
+match_language(Req, State, _Accept, [Provided|_Tail], {'*', _Quality}) ->
+ set_language(Req, State#state{language_a=Provided});
+match_language(Req, State, _Accept, [Provided|_Tail], {Provided, _Quality}) ->
+ set_language(Req, State#state{language_a=Provided});
+match_language(Req, State, Accept, [Provided|Tail],
+ Language = {Tag, _Quality}) ->
+ Length = byte_size(Tag),
+ case Provided of
+ << Tag:Length/binary, $-, _Any/bits >> ->
+ set_language(Req, State#state{language_a=Provided});
+ _Any ->
+ match_language(Req, State, Accept, Tail, Language)
+ end.
+
+set_language(Req=#http_req{meta=Meta}, State=#state{language_a=Language}) ->
+ {ok, Req2} = cowboy_http_req:set_resp_header(
+ <<"Content-Language">>, Language, Req),
+ charsets_provided(Req2#http_req{meta=[{language, Language}|Meta]}, State).
+
+%% charsets_provided should return a list of binary values indicating
+%% which charsets are accepted by the resource.
+charsets_provided(Req, State) ->
+ case call(Req, State, charsets_provided) of
+ no_call ->
+ set_content_type(Req, State);
+ {[], Req2, HandlerState2} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState2});
+ {CP, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2, charsets_p=CP},
+ {AcceptCharset, Req3} =
+ cowboy_http_req:parse_header('Accept-Charset', Req2),
+ case AcceptCharset of
+ undefined ->
+ set_content_type(Req3, State2#state{charset_a=hd(CP)});
+ AcceptCharset ->
+ AcceptCharset2 = prioritize_charsets(AcceptCharset),
+ choose_charset(Req3, State2, AcceptCharset2)
+ end
+ end.
+
+%% The special value "*", if present in the Accept-Charset field,
+%% matches every character set (including ISO-8859-1) which is not
+%% mentioned elsewhere in the Accept-Charset field. If no "*" is present
+%% in an Accept-Charset field, then all character sets not explicitly
+%% mentioned get a quality value of 0, except for ISO-8859-1, which gets
+%% a quality value of 1 if not explicitly mentioned.
+prioritize_charsets(AcceptCharsets) ->
+ AcceptCharsets2 = lists:sort(
+ fun ({_CharsetA, QualityA}, {_CharsetB, QualityB}) ->
+ QualityA > QualityB
+ end, AcceptCharsets),
+ case lists:keymember(<<"*">>, 1, AcceptCharsets2) of
+ true -> AcceptCharsets2;
+ false -> [{<<"iso-8859-1">>, 1000}|AcceptCharsets2]
+ end.
+
+choose_charset(Req, State, []) ->
+ not_acceptable(Req, State);
+choose_charset(Req, State=#state{charsets_p=CP}, [Charset|Tail]) ->
+ match_charset(Req, State, Tail, CP, Charset).
+
+match_charset(Req, State, Accept, [], _Charset) ->
+ choose_charset(Req, State, Accept);
+match_charset(Req, State, _Accept, [Provided|_Tail],
+ {Provided, _Quality}) ->
+ set_content_type(Req, State#state{charset_a=Provided});
+match_charset(Req, State, Accept, [_Provided|Tail], Charset) ->
+ match_charset(Req, State, Accept, Tail, Charset).
+
+set_content_type(Req=#http_req{meta=Meta}, State=#state{
+ content_type_a={{Type, SubType, Params}, _Fun},
+ charset_a=Charset}) ->
+ ParamsBin = set_content_type_build_params(Params, []),
+ ContentType = [Type, <<"/">>, SubType, ParamsBin],
+ ContentType2 = case Charset of
+ undefined -> ContentType;
+ Charset -> [ContentType, <<"; charset=">>, Charset]
+ end,
+ {ok, Req2} = cowboy_http_req:set_resp_header(
+ <<"Content-Type">>, ContentType2, Req),
+ encodings_provided(Req2#http_req{meta=[{charset, Charset}|Meta]}, State).
+
+set_content_type_build_params([], []) ->
+ <<>>;
+set_content_type_build_params([], Acc) ->
+ lists:reverse(Acc);
+set_content_type_build_params([{Attr, Value}|Tail], Acc) ->
+ set_content_type_build_params(Tail, [[Attr, <<"=">>, Value], <<";">>|Acc]).
+
+%% @todo Match for identity as we provide nothing else for now.
+%% @todo Don't forget to set the Content-Encoding header when we reply a body
+%% and the found encoding is something other than identity.
+encodings_provided(Req, State) ->
+ variances(Req, State).
+
+not_acceptable(Req, State) ->
+ respond(Req, State, 406).
+
+%% variances/2 should return a list of headers that will be added
+%% to the Vary response header. The Accept, Accept-Language,
+%% Accept-Charset and Accept-Encoding headers do not need to be
+%% specified.
+%%
+%% @todo Do Accept-Encoding too when we handle it.
+%% @todo Does the order matter?
+variances(Req, State=#state{content_types_p=CTP,
+ languages_p=LP, charsets_p=CP}) ->
+ Variances = case CTP of
+ [] -> [];
+ [_] -> [];
+ [_|_] -> [<<"Accept">>]
+ end,
+ Variances2 = case LP of
+ [] -> Variances;
+ [_] -> Variances;
+ [_|_] -> [<<"Accept-Language">>|Variances]
+ end,
+ Variances3 = case CP of
+ [] -> Variances2;
+ [_] -> Variances2;
+ [_|_] -> [<<"Accept-Charset">>|Variances2]
+ end,
+ {Variances4, Req3, State2} = case call(Req, State, variances) of
+ no_call ->
+ {Variances3, Req, State};
+ {HandlerVariances, Req2, HandlerState} ->
+ {Variances3 ++ HandlerVariances, Req2,
+ State#state{handler_state=HandlerState}}
+ end,
+ case [[<<", ">>, V] || V <- Variances4] of
+ [] ->
+ resource_exists(Req3, State2);
+ [[<<", ">>, H]|Variances5] ->
+ {ok, Req4} = cowboy_http_req:set_resp_header(
+ <<"Variances">>, [H|Variances5], Req3),
+ resource_exists(Req4, State2)
+ end.
+
+resource_exists(Req, State) ->
+ expect(Req, State, resource_exists, true,
+ fun if_match_exists/2, fun if_match_musnt_exist/2).
+
+if_match_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-Match', Req) of
+ {undefined, Req2} ->
+ if_unmodified_since_exists(Req2, State);
+ {'*', Req2} ->
+ if_unmodified_since_exists(Req2, State);
+ {ETagsList, Req2} ->
+ if_match(Req2, State, ETagsList)
+ end.
+
+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
+ end.
+
+if_match_musnt_exist(Req, State) ->
+ case cowboy_http_req:header('If-Match', Req) of
+ {undefined, Req2} -> is_put_to_missing_resource(Req2, State);
+ {_Any, Req2} -> precondition_failed(Req2, State)
+ end.
+
+if_unmodified_since_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-Unmodified-Since', Req) of
+ {undefined, Req2} ->
+ if_none_match_exists(Req2, State);
+ {{error, badarg}, Req2} ->
+ if_none_match_exists(Req2, State);
+ {IfUnmodifiedSince, Req2} ->
+ if_unmodified_since(Req2, State, IfUnmodifiedSince)
+ end.
+
+%% If LastModified is the atom 'no_call', we continue.
+if_unmodified_since(Req, State, IfUnmodifiedSince) ->
+ {LastModified, Req2, State2} = last_modified(Req, State),
+ case LastModified > IfUnmodifiedSince of
+ true -> precondition_failed(Req2, State2);
+ false -> if_none_match_exists(Req2, State2)
+ end.
+
+if_none_match_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-None-Match', Req) of
+ {undefined, Req2} ->
+ if_modified_since_exists(Req2, State);
+ {'*', Req2} ->
+ precondition_is_head_get(Req2, State);
+ {EtagsList, Req2} ->
+ if_none_match(Req2, State, EtagsList)
+ end.
+
+if_none_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 -> precondition_is_head_get(Req2, State2);
+ false -> if_modified_since_exists(Req2, State2)
+ end
+ end.
+
+precondition_is_head_get(Req=#http_req{method=Method}, State)
+ when Method =:= 'HEAD'; Method =:= 'GET' ->
+ not_modified(Req, State);
+precondition_is_head_get(Req, State) ->
+ precondition_failed(Req, State).
+
+if_modified_since_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-Modified-Since', Req) of
+ {undefined, Req2} ->
+ method(Req2, State);
+ {{error, badarg}, Req2} ->
+ method(Req2, State);
+ {IfModifiedSince, Req2} ->
+ if_modified_since_now(Req2, State, IfModifiedSince)
+ end.
+
+if_modified_since_now(Req, State, IfModifiedSince) ->
+ case IfModifiedSince > erlang:universaltime() of
+ true -> method(Req, State);
+ false -> if_modified_since(Req, State, IfModifiedSince)
+ end.
+
+if_modified_since(Req, State, IfModifiedSince) ->
+ {LastModified, Req2, State2} = last_modified(Req, State),
+ case LastModified of
+ no_call ->
+ method(Req2, State2);
+ LastModified ->
+ case LastModified > IfModifiedSince of
+ true -> method(Req2, State2);
+ false -> not_modified(Req2, State2)
+ end
+ end.
+
+not_modified(Req=#http_req{resp_headers=RespHeaders}, State) ->
+ RespHeaders2 = lists:keydelete(<<"Content-Type">>, 1, RespHeaders),
+ Req2 = Req#http_req{resp_headers=RespHeaders2},
+ {Req3, State2} = set_resp_etag(Req2, State),
+ {Req4, State3} = set_resp_expires(Req3, State2),
+ respond(Req4, State3, 304).
+
+precondition_failed(Req, State) ->
+ respond(Req, State, 412).
+
+is_put_to_missing_resource(Req=#http_req{method='PUT'}, State) ->
+ moved_permanently(Req, State, fun is_conflict/2);
+is_put_to_missing_resource(Req, State) ->
+ previously_existed(Req, State).
+
+%% moved_permanently/2 should return either false or {true, Location}
+%% with Location the full new URI of the resource.
+moved_permanently(Req, State, OnFalse) ->
+ case call(Req, State, moved_permanently) of
+ {{true, Location}, Req2, HandlerState2} ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Location">>, Location, Req2),
+ respond(Req3, State#state{handler_state=HandlerState2}, 301);
+ {false, Req2, HandlerState2} ->
+ OnFalse(Req2, State#state{handler_state=HandlerState2});
+ no_call ->
+ OnFalse(Req, State)
+ end.
+
+previously_existed(Req, State) ->
+ expect(Req, State, previously_existed, false,
+ fun (R, S) -> is_post_to_missing_resource(R, S, 404) end,
+ fun (R, S) -> moved_permanently(R, S, fun moved_temporarily/2) end).
+
+%% moved_temporarily/2 should return either false or {true, Location}
+%% with Location the full new URI of the resource.
+moved_temporarily(Req, State) ->
+ case call(Req, State, moved_temporarily) of
+ {{true, Location}, Req2, HandlerState2} ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Location">>, Location, Req2),
+ respond(Req3, State#state{handler_state=HandlerState2}, 307);
+ {false, Req2, HandlerState2} ->
+ is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState2}, 410);
+ no_call ->
+ is_post_to_missing_resource(Req, State, 410)
+ end.
+
+is_post_to_missing_resource(Req=#http_req{method='POST'}, State, OnFalse) ->
+ allow_missing_post(Req, State, OnFalse);
+is_post_to_missing_resource(Req, State, OnFalse) ->
+ respond(Req, State, OnFalse).
+
+allow_missing_post(Req, State, OnFalse) ->
+ expect(Req, State, allow_missing_post, true, fun post_is_create/2, OnFalse).
+
+method(Req=#http_req{method='DELETE'}, State) ->
+ delete_resource(Req, State);
+method(Req=#http_req{method='POST'}, State) ->
+ post_is_create(Req, State);
+method(Req=#http_req{method='PUT'}, State) ->
+ is_conflict(Req, State);
+method(Req, State) ->
+ set_resp_body(Req, State).
+
+%% delete_resource/2 should start deleting the resource and return.
+delete_resource(Req, State) ->
+ expect(Req, State, delete_resource, true, fun delete_completed/2, 500).
+
+%% delete_completed/2 indicates whether the resource has been deleted yet.
+delete_completed(Req, State) ->
+ expect(Req, State, delete_completed, true, fun has_resp_body/2, 202).
+
+%% post_is_create/2 indicates whether the POST method can create new resources.
+post_is_create(Req, State) ->
+ expect(Req, State, post_is_create, false, fun process_post/2, fun create_path/2).
+
+%% When the POST method can create new resources, create_path/2 will be called
+%% and is expected to return the full path to the new resource
+%% (including the leading /).
+create_path(Req=#http_req{meta=Meta}, State) ->
+ case call(Req, State, create_path) of
+ {Path, Req2, HandlerState} ->
+ Location = create_path_location(Req2, Path),
+ State2 = State#state{handler_state=HandlerState},
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Location">>, Location, Req2),
+ put_resource(Req3#http_req{meta=[{put_path, Path}|Meta]},
+ State2, 303)
+ end.
+
+create_path_location(#http_req{transport=Transport, raw_host=Host,
+ port=Port}, Path) ->
+ TransportName = Transport:name(),
+ << (create_path_location_protocol(TransportName))/binary, "://",
+ Host/binary, (create_path_location_port(TransportName, Port))/binary,
+ Path/binary >>.
+
+create_path_location_protocol(ssl) -> <<"https">>;
+create_path_location_protocol(_) -> <<"http">>.
+
+create_path_location_port(ssl, 443) ->
+ <<>>;
+create_path_location_port(tcp, 80) ->
+ <<>>;
+create_path_location_port(_, Port) ->
+ <<":", (list_to_binary(integer_to_list(Port)))/binary>>.
+
+%% process_post should return true when the POST body could be processed
+%% and false when it hasn't, in which case a 500 error is sent.
+process_post(Req, State) ->
+ case call(Req, State, process_post) of
+ {true, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ next(Req2, State2, 201);
+ {false, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ respond(Req2, State2, 500)
+ end.
+
+is_conflict(Req, State) ->
+ expect(Req, State, is_conflict, false, fun put_resource/2, 409).
+
+put_resource(Req=#http_req{raw_path=RawPath, meta=Meta}, State) ->
+ Req2 = Req#http_req{meta=[{put_path, RawPath}|Meta]},
+ put_resource(Req2, State, fun is_new_resource/2).
+
+%% content_types_accepted should return a list of media types and their
+%% associated callback functions in the same format as content_types_provided.
+%%
+%% The callback will then be called and is expected to process the content
+%% pushed to the resource in the request body. The path to the new resource
+%% may be different from the request path, and is stored as request metadata.
+%% It is always defined past this point. It can be retrieved as demonstrated:
+%% {PutPath, Req2} = cowboy_http_req:meta(put_path, Req)
+put_resource(Req, State, OnTrue) ->
+ case call(Req, State, content_types_accepted) of
+ no_call ->
+ respond(Req, State, 415);
+ {CTA, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
+ {ContentType, Req3}
+ = cowboy_http_req:parse_header('Content-Type', Req2),
+ choose_content_type(Req3, State2, OnTrue, ContentType, CTA)
+ end.
+
+choose_content_type(Req, State, _OnTrue, _ContentType, []) ->
+ respond(Req, State, 415);
+choose_content_type(Req, State, OnTrue, ContentType,
+ [{Accepted, Fun}|_Tail]) when ContentType =:= Accepted ->
+ case call(Req, State, Fun) of
+ {true, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ next(Req2, State2, OnTrue);
+ {false, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ respond(Req2, State2, 500)
+ end;
+choose_content_type(Req, State, OnTrue, ContentType, [_Any|Tail]) ->
+ choose_content_type(Req, State, OnTrue, ContentType, Tail).
+
+%% Whether we created a new resource, either through PUT or POST.
+%% This is easily testable because we would have set the Location
+%% header by this point if we did so.
+is_new_resource(Req, State) ->
+ case cowboy_http_req:has_resp_header(<<"Location">>, Req) of
+ true -> respond(Req, State, 201);
+ false -> has_resp_body(Req, State)
+ end.
+
+has_resp_body(Req, State) ->
+ case cowboy_http_req:has_resp_body(Req) of
+ true -> multiple_choices(Req, State);
+ false -> respond(Req, State, 204)
+ end.
+
+%% Set the response headers and call the callback found using
+%% content_types_provided/2 to obtain the request body and add
+%% it to the response.
+set_resp_body(Req=#http_req{method=Method},
+ State=#state{content_type_a={_Type, Fun}})
+ when Method =:= 'GET'; Method =:= 'HEAD' ->
+ {Req2, State2} = set_resp_etag(Req, State),
+ {LastModified, Req3, State3} = last_modified(Req2, State2),
+ case LastModified of
+ LastModified when is_atom(LastModified) ->
+ Req4 = Req3;
+ LastModified ->
+ LastModifiedStr = httpd_util:rfc1123_date(LastModified),
+ {ok, Req4} = cowboy_http_req:set_resp_header(
+ <<"Last-Modified">>, LastModifiedStr, Req3)
+ end,
+ {Req5, State4} = set_resp_expires(Req4, State3),
+ case call(Req5, State4, Fun) of
+ {Body, Req6, HandlerState} ->
+ State5 = State4#state{handler_state=HandlerState},
+ {ok, Req7} = case Body of
+ {stream, Len, Fun1} ->
+ cowboy_http_req:set_resp_body_fun(Len, Fun1, Req6);
+ _Contents ->
+ cowboy_http_req:set_resp_body(Body, Req6)
+ end,
+ multiple_choices(Req7, State5)
+ end;
+set_resp_body(Req, State) ->
+ multiple_choices(Req, State).
+
+multiple_choices(Req, State) ->
+ expect(Req, State, multiple_choices, false, 200, 300).
+
+%% Response utility functions.
+
+set_resp_etag(Req, State) ->
+ {Etag, Req2, State2} = generate_etag(Req, State),
+ case Etag of
+ undefined ->
+ {Req2, State2};
+ Etag ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Etag">>, Etag, Req2),
+ {Req3, State2}
+ end.
+
+set_resp_expires(Req, State) ->
+ {Expires, Req2, State2} = expires(Req, State),
+ case Expires of
+ Expires when is_atom(Expires) ->
+ {Req2, State2};
+ Expires ->
+ ExpiresStr = httpd_util:rfc1123_date(Expires),
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Expires">>, ExpiresStr, Req2),
+ {Req3, State2}
+ end.
+
+%% Info retrieval. No logic.
+
+generate_etag(Req, State=#state{etag=no_call}) ->
+ {undefined, Req, State};
+generate_etag(Req, State=#state{etag=undefined}) ->
+ case call(Req, State, generate_etag) of
+ no_call ->
+ {undefined, Req, State#state{etag=no_call}};
+ {Etag, Req2, HandlerState2} ->
+ {Etag, Req2, State#state{handler_state=HandlerState2, etag=Etag}}
+ end;
+generate_etag(Req, State=#state{etag=Etag}) ->
+ {Etag, Req, State}.
+
+last_modified(Req, State=#state{last_modified=no_call}) ->
+ {undefined, Req, State};
+last_modified(Req, State=#state{last_modified=undefined}) ->
+ case call(Req, State, last_modified) of
+ no_call ->
+ {undefined, Req, State#state{last_modified=no_call}};
+ {LastModified, Req2, HandlerState2} ->
+ {LastModified, Req2, State#state{handler_state=HandlerState2,
+ last_modified=LastModified}}
+ end;
+last_modified(Req, State=#state{last_modified=LastModified}) ->
+ {LastModified, Req, State}.
+
+expires(Req, State=#state{expires=no_call}) ->
+ {undefined, Req, State};
+expires(Req, State=#state{expires=undefined}) ->
+ case call(Req, State, expires) of
+ no_call ->
+ {undefined, Req, State#state{expires=no_call}};
+ {Expires, Req2, HandlerState2} ->
+ {Expires, Req2, State#state{handler_state=HandlerState2,
+ expires=Expires}}
+ end;
+expires(Req, State=#state{expires=Expires}) ->
+ {Expires, Req, State}.
+
+%% REST primitives.
+
+expect(Req, State, Callback, Expected, OnTrue, OnFalse) ->
+ case call(Req, State, Callback) of
+ no_call ->
+ next(Req, State, OnTrue);
+ {Expected, Req2, HandlerState2} ->
+ next(Req2, State#state{handler_state=HandlerState2}, OnTrue);
+ {_Unexpected, Req2, HandlerState2} ->
+ next(Req2, State#state{handler_state=HandlerState2}, OnFalse)
+ end.
+
+call(Req, #state{handler=Handler, handler_state=HandlerState}, Fun) ->
+ case erlang:function_exported(Handler, Fun, 2) of
+ true -> Handler:Fun(Req, HandlerState);
+ false -> no_call
+ end.
+
+next(Req, State, Next) when is_function(Next) ->
+ Next(Req, State);
+next(Req, State, StatusCode) when is_integer(StatusCode) ->
+ respond(Req, State, StatusCode).
+
+%% @todo Allow some sort of callback for custom error pages.
+respond(Req, State, StatusCode) ->
+ {ok, Req2} = cowboy_http_req:reply(StatusCode, Req),
+ terminate(Req2, State).
+
+terminate(Req, #state{handler=Handler, handler_state=HandlerState}) ->
+ case erlang:function_exported(Handler, rest_terminate, 2) of
+ true -> ok = Handler:rest_terminate(
+ Req#http_req{resp_state=locked}, HandlerState);
+ false -> ok
+ end,
+ {ok, Req}.
diff --git a/src/cowboy_http_static.erl b/src/cowboy_http_static.erl
new file mode 100644
index 0000000..3e3cb9e
--- /dev/null
+++ b/src/cowboy_http_static.erl
@@ -0,0 +1,461 @@
+%% Copyright (c) 2011, Magnus Klaar <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% @doc Static resource handler.
+%%
+%% This built in HTTP handler provides a simple file serving capability for
+%% cowboy applications. It should be considered an experimental feature because
+%% of it's dependency on the experimental REST handler. It's recommended to be
+%% used for small or temporary environments where it is not preferrable to set
+%% up a second server just to serve files.
+%%
+%% If this handler is used the Erlang node running the cowboy application must
+%% be configured to use an async thread pool. This is configured by adding the
+%% `+A $POOL_SIZE' argument to the `erl' command used to start the node. See
+%% <a href="http://erlang.org/pipermail/erlang-bugs/2012-January/002720.html">
+%% this reply</a> from the OTP team to erlang-bugs
+%%
+%% == Base configuration ==
+%%
+%% The handler must be configured with a request path prefix to serve files
+%% under and the path to a directory to read files from. The request path prefix
+%% is defined in the path pattern of the cowboy dispatch rule for the handler.
+%% The request path pattern must end with a ``'...''' token.
+%% The directory path can be set to either an absolute or relative path in the
+%% form of a list or binary string representation of a file system path. A list
+%% of binary path segments, as is used throughout cowboy, is also a valid
+%% directory path.
+%%
+%% The directory path can also be set to a relative path within the `priv/'
+%% directory of an application. This is configured by setting the value of the
+%% directory option to a tuple of the form `{priv_dir, Application, Relpath}'.
+%%
+%% ==== Examples ====
+%% ```
+%% %% Serve files from /var/www/ under http://example.com/static/
+%% {[<<"static">>, '...'], cowboy_http_static,
+%% [{directory, "/var/www"}]}
+%%
+%% %% Serve files from the current working directory under http://example.com/static/
+%% {[<<"static">>, '...'], cowboy_http_static,
+%% [{directory, <<"./">>}]}
+%%
+%% %% Serve files from cowboy/priv/www under http://example.com/
+%% {['...'], cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, [<<"www">>]}}]}
+%% '''
+%%
+%% == Content type configuration ==
+%%
+%% By default the content type of all static resources will be set to
+%% `application/octet-stream'. This can be overriden by supplying a list
+%% of filename extension to mimetypes pairs in the `mimetypes' option.
+%% The filename extension should be a binary string including the leading dot.
+%% The mimetypes must be of a type that the `cowboy_http_rest' protocol can
+%% handle.
+%%
+%% The <a href="https://github.com/spawngrid/mimetypes">spawngrid/mimetypes</a>
+%% application, or an arbitrary function accepting the path to the file being
+%% served, can also be used to generate the list of content types for a static
+%% file resource. The function used must accept an additional argument after
+%% the file path argument.
+%%
+%% ==== Example ====
+%% ```
+%% %% Use a static list of content types.
+%% {[<<"static">>, '...'], cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, []}},
+%% {mimetypes, [
+%% {<<".css">>, [<<"text/css">>]},
+%% {<<".js">>, [<<"application/javascript">>]}]}]}
+%%
+%% %% Use the default database in the mimetypes application.
+%% {[<<"static">>, '...', cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, []}},
+%% {mimetypes, {fun mimetypes:path_to_mimes/2, default}}]]}
+%% '''
+%%
+%% == ETag Header Function ==
+%%
+%% The default behaviour of the static file handler is to not generate ETag
+%% headers. This is because generating ETag headers based on file metadata
+%% causes different servers in a cluster to generate different ETag values for
+%% the same file unless the metadata is also synced. Generating strong ETags
+%% based on the contents of a file is currently out of scope for this module.
+%%
+%% 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.
+%%
+%% 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'.
+%%
+%% ==== Examples ====
+%% ```
+%% %% A value of default is equal to not specifying the option.
+%% {[<<"static">>, '...', cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, []}},
+%% {etag, default}]]}
+%%
+%% %% Use all avaliable ETag function arguments to generate a header value.
+%% {[<<"static">>, '...', cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, []}},
+%% {etag, {attributes, [filepath, filesize, inode, mtime]}}]]}
+%%
+%% %% Use a user defined function to generate a strong ETag header value.
+%% {[<<"static">>, '...', cowboy_http_static,
+%% [{directory, {priv_dir, cowboy, []}},
+%% {etag, {fun generate_strong_etag/2, strong_etag_extra}}]]}
+%%
+%% generate_strong_etag(Arguments, strong_etag_extra) ->
+%% {_, Filepath} = lists:keyfind(filepath, 1, Arguments),
+%% {_, _Filesize} = lists:keyfind(filesize, 1, Arguments),
+%% {_, _INode} = lists:keyfind(inode, 1, Arguments),
+%% {_, _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).
+%% '''
+-module(cowboy_http_static).
+
+%% include files
+-include("http.hrl").
+-include_lib("kernel/include/file.hrl").
+
+%% cowboy_http_protocol callbacks
+-export([init/3]).
+
+%% cowboy_http_rest callbacks
+-export([rest_init/2, allowed_methods/2, malformed_request/2,
+ resource_exists/2, forbidden/2, last_modified/2, generate_etag/2,
+ content_types_provided/2, file_contents/2]).
+
+%% internal
+-export([path_to_mimetypes/2]).
+
+%% types
+-type dirpath() :: string() | binary() | [binary()].
+-type dirspec() :: dirpath() | {priv, atom(), dirpath()}.
+-type mimedef() :: {binary(), binary(), [{binary(), binary()}]}.
+-type etagarg() :: {filepath, binary()} | {mtime, cowboy_clock:datetime()}
+ | {inode, non_neg_integer()} | {filesize, non_neg_integer()}.
+
+%% handler state
+-record(state, {
+ filepath :: binary() | error,
+ fileinfo :: {ok, #file_info{}} | {error, _} | error,
+ mimetypes :: {fun((binary(), T) -> [mimedef()]), T} | undefined,
+ etag_fun :: {fun(([etagarg()], T) -> undefined | binary()), T}}).
+
+
+%% @private Upgrade from HTTP handler to REST handler.
+init({_Transport, http}, _Req, _Opts) ->
+ {upgrade, protocol, cowboy_http_rest}.
+
+
+%% @private Set up initial state of REST handler.
+-spec rest_init(#http_req{}, list()) -> {ok, #http_req{}, #state{}}.
+rest_init(Req, Opts) ->
+ Directory = proplists:get_value(directory, Opts),
+ Directory1 = directory_path(Directory),
+ Mimetypes = proplists:get_value(mimetypes, Opts, []),
+ Mimetypes1 = case Mimetypes of
+ {_, _} -> Mimetypes;
+ [] -> {fun path_to_mimetypes/2, []};
+ [_|_] -> {fun path_to_mimetypes/2, Mimetypes}
+ end,
+ ETagFunction = case proplists:get_value(etag, Opts) of
+ default -> {fun no_etag_function/2, undefined};
+ undefined -> {fun no_etag_function/2, undefined};
+ {attributes, Attrs} -> {fun attr_etag_function/2, Attrs};
+ {_, _}=EtagFunction1 -> EtagFunction1
+ end,
+ {Filepath, Req1} = cowboy_http_req:path_info(Req),
+ State = case check_path(Filepath) of
+ error ->
+ #state{filepath=error, fileinfo=error, mimetypes=undefined,
+ etag_fun=ETagFunction};
+ ok ->
+ Filepath1 = join_paths(Directory1, Filepath),
+ Fileinfo = file:read_file_info(Filepath1),
+ #state{filepath=Filepath1, fileinfo=Fileinfo, mimetypes=Mimetypes1,
+ etag_fun=ETagFunction}
+ end,
+ {ok, Req1, State}.
+
+
+%% @private Only allow GET and HEAD requests on files.
+-spec allowed_methods(#http_req{}, #state{}) ->
+ {[atom()], #http_req{}, #state{}}.
+allowed_methods(Req, State) ->
+ {['GET', 'HEAD'], Req, State}.
+
+%% @private
+-spec malformed_request(#http_req{}, #state{}) ->
+ {boolean(), #http_req{}, #state{}}.
+malformed_request(Req, #state{filepath=error}=State) ->
+ {true, Req, State};
+malformed_request(Req, State) ->
+ {false, Req, State}.
+
+
+%% @private Check if the resource exists under the document root.
+-spec resource_exists(#http_req{}, #state{}) ->
+ {boolean(), #http_req{}, #state{}}.
+resource_exists(Req, #state{fileinfo={error, _}}=State) ->
+ {false, Req, State};
+resource_exists(Req, #state{fileinfo={ok, Fileinfo}}=State) ->
+ {Fileinfo#file_info.type =:= regular, Req, State}.
+
+
+%% @private
+%% Access to a file resource is forbidden if it exists and the local node does
+%% not have permission to read it. Directory listings are always forbidden.
+-spec forbidden(#http_req{}, #state{}) -> {boolean(), #http_req{}, #state{}}.
+forbidden(Req, #state{fileinfo={_, #file_info{type=directory}}}=State) ->
+ {true, Req, State};
+forbidden(Req, #state{fileinfo={error, eacces}}=State) ->
+ {true, Req, State};
+forbidden(Req, #state{fileinfo={error, _}}=State) ->
+ {false, Req, State};
+forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) ->
+ {not (Access =:= read orelse Access =:= read_write), Req, State}.
+
+
+%% @private Read the time a file system system object was last modified.
+-spec last_modified(#http_req{}, #state{}) ->
+ {cowboy_clock:datetime(), #http_req{}, #state{}}.
+last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
+ {Modified, Req, State}.
+
+
+%% @private Generate the ETag header value for this file.
+%% The ETag header value is only generated if the resource is a file that
+%% exists in document root.
+-spec generate_etag(#http_req{}, #state{}) ->
+ {undefined | binary(), #http_req{}, #state{}}.
+generate_etag(Req, #state{fileinfo={_, #file_info{type=regular, inode=INode,
+ mtime=Modified, size=Filesize}}, filepath=Filepath,
+ etag_fun={ETagFun, ETagData}}=State) ->
+ ETagArgs = [
+ {filepath, Filepath}, {filesize, Filesize},
+ {inode, INode}, {mtime, Modified}],
+ {ETagFun(ETagArgs, ETagData), Req, State};
+generate_etag(Req, State) ->
+ {undefined, Req, State}.
+
+
+%% @private Return the content type of a file.
+-spec content_types_provided(#http_req{}, #state{}) -> tuple().
+content_types_provided(Req, #state{filepath=Filepath,
+ mimetypes={MimetypesFun, MimetypesData}}=State) ->
+ Mimetypes = [{T, file_contents}
+ || T <- MimetypesFun(Filepath, MimetypesData)],
+ {Mimetypes, Req, State}.
+
+
+%% @private Return a function that writes a file directly to the socket.
+-spec file_contents(#http_req{}, #state{}) -> tuple().
+file_contents(Req, #state{filepath=Filepath,
+ fileinfo={ok, #file_info{size=Filesize}}}=State) ->
+ {ok, Transport, Socket} = cowboy_http_req:transport(Req),
+ Writefile = content_function(Transport, Socket, Filepath),
+ {{stream, Filesize, Writefile}, Req, State}.
+
+
+%% @private Return a function writing the contents of a file to a socket.
+%% The function returns the number of bytes written to the socket to enable
+%% the calling function to determine if the expected number of bytes were
+%% written to the socket.
+-spec content_function(module(), inet:socket(), binary()) ->
+ fun(() -> {sent, non_neg_integer()}).
+content_function(Transport, Socket, Filepath) ->
+ %% `file:sendfile/2' will only work with the `cowboy_tcp_transport'
+ %% transport module. SSL or future SPDY transports that require the
+ %% content to be encrypted or framed as the content is sent.
+ case erlang:function_exported(file, sendfile, 2) of
+ false ->
+ fun() -> sfallback(Transport, Socket, Filepath) end;
+ _ when Transport =/= cowboy_tcp_transport ->
+ fun() -> sfallback(Transport, Socket, Filepath) end;
+ true ->
+ fun() -> sendfile(Socket, Filepath) end
+ end.
+
+
+%% @private Sendfile fallback function.
+-spec sfallback(module(), inet:socket(), binary()) -> {sent, non_neg_integer()}.
+sfallback(Transport, Socket, Filepath) ->
+ {ok, File} = file:open(Filepath, [read,binary,raw]),
+ sfallback(Transport, Socket, File, 0).
+
+-spec sfallback(module(), inet:socket(), file:io_device(),
+ non_neg_integer()) -> {sent, non_neg_integer()}.
+sfallback(Transport, Socket, File, Sent) ->
+ case file:read(File, 16#1FFF) of
+ eof ->
+ ok = file:close(File),
+ {sent, Sent};
+ {ok, Bin} ->
+ ok = Transport:send(Socket, Bin),
+ sfallback(Transport, Socket, File, Sent + byte_size(Bin))
+ end.
+
+
+%% @private Wrapper for sendfile function.
+-spec sendfile(inet:socket(), binary()) -> {sent, non_neg_integer()}.
+sendfile(Socket, Filepath) ->
+ {ok, Sent} = file:sendfile(Filepath, Socket),
+ {sent, Sent}.
+
+-spec directory_path(dirspec()) -> dirpath().
+directory_path({priv_dir, App, []}) ->
+ priv_dir_path(App);
+directory_path({priv_dir, App, [H|_]=Path}) when is_integer(H) ->
+ filename:join(priv_dir_path(App), Path);
+directory_path({priv_dir, App, [H|_]=Path}) when is_binary(H) ->
+ filename:join(filename:split(priv_dir_path(App)) ++ Path);
+directory_path({priv_dir, App, Path}) when is_binary(Path) ->
+ filename:join(priv_dir_path(App), Path);
+directory_path(Path) ->
+ Path.
+
+
+%% @private Validate a request path for unsafe characters.
+%% There is no way to escape special characters in a filesystem path.
+-spec check_path(Path::[binary()]) -> ok | error.
+check_path([]) -> ok;
+check_path([<<"">>|_T]) -> error;
+check_path([<<".">>|_T]) -> error;
+check_path([<<"..">>|_T]) -> error;
+check_path([H|T]) ->
+ case binary:match(H, <<"/">>) of
+ {_, _} -> error;
+ nomatch -> check_path(T)
+ end.
+
+
+%% @private Join the the directory and request paths.
+-spec join_paths(dirpath(), [binary()]) -> binary().
+join_paths([H|_]=Dirpath, Filepath) when is_integer(H) ->
+ filename:join(filename:split(Dirpath) ++ Filepath);
+join_paths([H|_]=Dirpath, Filepath) when is_binary(H) ->
+ filename:join(Dirpath ++ Filepath);
+join_paths(Dirpath, Filepath) when is_binary(Dirpath) ->
+ filename:join([Dirpath] ++ Filepath);
+join_paths([], Filepath) ->
+ filename:join(Filepath).
+
+
+%% @private Return the path to the priv/ directory of an application.
+-spec priv_dir_path(atom()) -> string().
+priv_dir_path(App) ->
+ case code:priv_dir(App) of
+ {error, bad_name} -> priv_dir_mod(App);
+ Dir -> Dir
+ end.
+
+-spec priv_dir_mod(atom()) -> string().
+priv_dir_mod(Mod) ->
+ case code:which(Mod) of
+ File when not is_list(File) -> "../priv";
+ File -> filename:join([filename:dirname(File),"../priv"])
+ end.
+
+
+%% @private Use application/octet-stream as the default mimetype.
+%% If a list of extension - mimetype pairs are provided as the mimetypes
+%% an attempt to find the mimetype using the file extension. If no match
+%% is found the default mimetype is returned.
+-spec path_to_mimetypes(binary(), [{binary(), [mimedef()]}]) ->
+ [mimedef()].
+path_to_mimetypes(Filepath, Extensions) when is_binary(Filepath) ->
+ Ext = filename:extension(Filepath),
+ case Ext of
+ <<>> -> default_mimetype();
+ _Ext -> path_to_mimetypes_(Ext, Extensions)
+ end.
+
+-spec path_to_mimetypes_(binary(), [{binary(), [mimedef()]}]) -> [mimedef()].
+path_to_mimetypes_(Ext, Extensions) ->
+ case lists:keyfind(Ext, 1, Extensions) of
+ {_, MTs} -> MTs;
+ _Unknown -> default_mimetype()
+ end.
+
+-spec default_mimetype() -> [mimedef()].
+default_mimetype() ->
+ [{<<"application">>, <<"octet-stream">>, []}].
+
+
+%% @private Do not send ETag headers in the default configuration.
+-spec no_etag_function([etagarg()], undefined) -> undefined.
+no_etag_function(_Args, undefined) ->
+ 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().
+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]).
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-define(_eq(E, I), ?_assertEqual(E, I)).
+
+check_path_test_() ->
+ C = fun check_path/1,
+ [?_eq(error, C([<<>>])),
+ ?_eq(ok, C([<<"abc">>])),
+ ?_eq(error, C([<<".">>])),
+ ?_eq(error, C([<<"..">>])),
+ ?_eq(error, C([<<"/">>]))
+ ].
+
+join_paths_test_() ->
+ P = fun join_paths/2,
+ [?_eq(<<"a">>, P([], [<<"a">>])),
+ ?_eq(<<"a/b/c">>, P(<<"a/b">>, [<<"c">>])),
+ ?_eq(<<"a/b/c">>, P("a/b", [<<"c">>])),
+ ?_eq(<<"a/b/c">>, P([<<"a">>, <<"b">>], [<<"c">>]))
+ ].
+
+directory_path_test_() ->
+ P = fun directory_path/1,
+ PL = fun(I) -> length(filename:split(P(I))) end,
+ Base = PL({priv_dir, cowboy, []}),
+ [?_eq(Base + 1, PL({priv_dir, cowboy, "a"})),
+ ?_eq(Base + 1, PL({priv_dir, cowboy, <<"a">>})),
+ ?_eq(Base + 1, PL({priv_dir, cowboy, [<<"a">>]})),
+ ?_eq(Base + 2, PL({priv_dir, cowboy, "a/b"})),
+ ?_eq(Base + 2, PL({priv_dir, cowboy, <<"a/b">>})),
+ ?_eq(Base + 2, PL({priv_dir, cowboy, [<<"a">>, <<"b">>]})),
+ ?_eq("a/b", P("a/b"))
+ ].
+
+
+-endif.
diff --git a/src/cowboy_http_websocket.erl b/src/cowboy_http_websocket.erl
index 08a0c90..0f0204c 100644
--- a/src/cowboy_http_websocket.erl
+++ b/src/cowboy_http_websocket.erl
@@ -30,9 +30,9 @@
%% <li>Firefox 6</li>
%% </ul>
%%
-%% Version 8 is supported by the following browsers:
+%% Version 8+ is supported by the following browsers:
%% <ul>
-%% <li>Firefox 7</li>
+%% <li>Firefox 7+</li>
%% <li>Chrome 14+</li>
%% </ul>
-module(cowboy_http_websocket).
@@ -64,7 +64,7 @@
%% You do not need to call this function manually. To upgrade to the WebSocket
%% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em>
%% in your <em>cowboy_http_handler:init/3</em> handler function.
--spec upgrade(pid(), module(), any(), #http_req{}) -> ok | none().
+-spec upgrade(pid(), module(), any(), #http_req{}) -> closed | none().
upgrade(ListenerPid, Handler, Opts, Req) ->
cowboy_listener:move_connection(ListenerPid, websocket, self()),
case catch websocket_upgrade(#state{handler=Handler, opts=Opts}, Req) of
@@ -72,16 +72,13 @@ upgrade(ListenerPid, Handler, Opts, Req) ->
{'EXIT', _Reason} -> upgrade_error(Req)
end.
-%% @todo We need a function to properly parse headers according to their ABNF,
-%% instead of having ugly code like this case here.
-%% @todo Upgrade is a list of products and should be parsed as such.
-spec websocket_upgrade(#state{}, #http_req{}) -> {ok, #state{}, #http_req{}}.
websocket_upgrade(State, Req) ->
{ConnTokens, Req2}
= cowboy_http_req:parse_header('Connection', Req),
true = lists:member(<<"upgrade">>, ConnTokens),
- {WS, Req3} = cowboy_http_req:header('Upgrade', Req2),
- <<"websocket">> = cowboy_bstr:to_lower(WS),
+ %% @todo Should probably send a 426 if the Upgrade header is missing.
+ {[<<"websocket">>], Req3} = cowboy_http_req:parse_header('Upgrade', Req2),
{Version, Req4} = cowboy_http_req:header(<<"Sec-Websocket-Version">>, Req3),
websocket_upgrade(Version, State, Req4).
@@ -95,25 +92,26 @@ websocket_upgrade(State, Req) ->
%% third part of the challenge key, because proxies will wait for
%% a reply before sending it. Therefore we calculate the challenge
%% key only in websocket_handshake/3.
-websocket_upgrade(undefined, State, Req) ->
+websocket_upgrade(undefined, State, Req=#http_req{meta=Meta}) ->
{Origin, Req2} = cowboy_http_req:header(<<"Origin">>, Req),
{Key1, Req3} = cowboy_http_req:header(<<"Sec-Websocket-Key1">>, Req2),
{Key2, Req4} = cowboy_http_req:header(<<"Sec-Websocket-Key2">>, Req3),
false = lists:member(undefined, [Origin, Key1, Key2]),
EOP = binary:compile_pattern(<< 255 >>),
{ok, State#state{version=0, origin=Origin, challenge={Key1, Key2},
- eop=EOP}, Req4};
+ eop=EOP}, Req4#http_req{meta=[{websocket_version, 0}|Meta]}};
%% Versions 7 and 8. Implementation follows the hybi 7 through 17 drafts.
-websocket_upgrade(Version, State, Req)
+websocket_upgrade(Version, State, Req=#http_req{meta=Meta})
when Version =:= <<"7">>; Version =:= <<"8">>;
Version =:= <<"13">> ->
{Key, Req2} = cowboy_http_req:header(<<"Sec-Websocket-Key">>, Req),
false = Key =:= undefined,
Challenge = hybi_challenge(Key),
IntVersion = list_to_integer(binary_to_list(Version)),
- {ok, State#state{version=IntVersion, challenge=Challenge}, Req2}.
+ {ok, State#state{version=IntVersion, challenge=Challenge},
+ Req2#http_req{meta=[{websocket_version, IntVersion}|Meta]}}.
--spec handler_init(#state{}, #http_req{}) -> ok | none().
+-spec handler_init(#state{}, #http_req{}) -> closed | none().
handler_init(State=#state{handler=Handler, opts=Opts},
Req=#http_req{transport=Transport}) ->
try Handler:websocket_init(Transport:name(), Req, Opts) of
@@ -139,31 +137,27 @@ handler_init(State=#state{handler=Handler, opts=Opts},
[Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()])
end.
--spec upgrade_error(#http_req{}) -> ok.
+-spec upgrade_error(#http_req{}) -> closed.
upgrade_error(Req) ->
- {ok, Req2} = cowboy_http_req:reply(400, [], [],
+ {ok, _Req2} = cowboy_http_req:reply(400, [], [],
Req#http_req{resp_state=waiting}),
- upgrade_terminate(Req2).
+ closed.
%% @see cowboy_http_protocol:ensure_response/1
--spec upgrade_denied(#http_req{}) -> ok.
-upgrade_denied(Req=#http_req{resp_state=done}) ->
- upgrade_terminate(Req);
+-spec upgrade_denied(#http_req{}) -> closed.
+upgrade_denied(#http_req{resp_state=done}) ->
+ closed;
upgrade_denied(Req=#http_req{resp_state=waiting}) ->
- {ok, Req2} = cowboy_http_req:reply(400, [], [], Req),
- upgrade_terminate(Req2);
-upgrade_denied(Req=#http_req{method='HEAD', resp_state=chunks}) ->
- upgrade_terminate(Req);
-upgrade_denied(Req=#http_req{socket=Socket, transport=Transport,
+ {ok, _Req2} = cowboy_http_req:reply(400, [], [], Req),
+ closed;
+upgrade_denied(#http_req{method='HEAD', resp_state=chunks}) ->
+ closed;
+upgrade_denied(#http_req{socket=Socket, transport=Transport,
resp_state=chunks}) ->
Transport:send(Socket, <<"0\r\n\r\n">>),
- upgrade_terminate(Req).
+ closed.
--spec upgrade_terminate(#http_req{}) -> ok.
-upgrade_terminate(#http_req{socket=Socket, transport=Transport}) ->
- Transport:close(Socket).
-
--spec websocket_handshake(#state{}, #http_req{}, any()) -> ok | none().
+-spec websocket_handshake(#state{}, #http_req{}, any()) -> closed | none().
websocket_handshake(State=#state{version=0, origin=Origin,
challenge={Key1, Key2}}, Req=#http_req{socket=Socket,
transport=Transport, raw_host=Host, port=Port,
@@ -175,14 +169,20 @@ websocket_handshake(State=#state{version=0, origin=Origin,
{<<"Sec-Websocket-Location">>, Location},
{<<"Sec-Websocket-Origin">>, Origin}],
Req#http_req{resp_state=waiting}),
+ %% Flush the resp_sent message before moving on.
+ receive {cowboy_http_req, resp_sent} -> ok after 0 -> ok end,
%% We replied with a proper response. Proxies should be happy enough,
%% we can now read the 8 last bytes of the challenge keys and send
%% the challenge response directly to the socket.
- {ok, Key3, Req3} = cowboy_http_req:body(8, Req2),
- Challenge = hixie76_challenge(Key1, Key2, Key3),
- Transport:send(Socket, Challenge),
- handler_before_loop(State#state{messages=Transport:messages()},
- Req3, HandlerState, <<>>);
+ case cowboy_http_req:body(8, Req2) of
+ {ok, Key3, Req3} ->
+ Challenge = hixie76_challenge(Key1, Key2, Key3),
+ Transport:send(Socket, Challenge),
+ handler_before_loop(State#state{messages=Transport:messages()},
+ Req3, HandlerState, <<>>);
+ _Any ->
+ closed %% If an error happened reading the body, stop there.
+ end;
websocket_handshake(State=#state{challenge=Challenge},
Req=#http_req{transport=Transport}, HandlerState) ->
{ok, Req2} = cowboy_http_req:upgrade_reply(
@@ -190,10 +190,12 @@ websocket_handshake(State=#state{challenge=Challenge},
[{<<"Upgrade">>, <<"websocket">>},
{<<"Sec-Websocket-Accept">>, Challenge}],
Req#http_req{resp_state=waiting}),
+ %% Flush the resp_sent message before moving on.
+ receive {cowboy_http_req, resp_sent} -> ok after 0 -> ok end,
handler_before_loop(State#state{messages=Transport:messages()},
Req2, HandlerState, <<>>).
--spec handler_before_loop(#state{}, #http_req{}, any(), binary()) -> ok | none().
+-spec handler_before_loop(#state{}, #http_req{}, any(), binary()) -> closed | none().
handler_before_loop(State=#state{hibernate=true},
Req=#http_req{socket=Socket, transport=Transport},
HandlerState, SoFar) ->
@@ -218,7 +220,7 @@ handler_loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) ->
State#state{timeout_ref=TRef}.
%% @private
--spec handler_loop(#state{}, #http_req{}, any(), binary()) -> ok | none().
+-spec handler_loop(#state{}, #http_req{}, any(), binary()) -> closed | none().
handler_loop(State=#state{messages={OK, Closed, Error}, timeout_ref=TRef},
Req=#http_req{socket=Socket}, HandlerState, SoFar) ->
receive
@@ -238,17 +240,17 @@ handler_loop(State=#state{messages={OK, Closed, Error}, timeout_ref=TRef},
SoFar, websocket_info, Message, fun handler_before_loop/4)
end.
--spec websocket_data(#state{}, #http_req{}, any(), binary()) -> ok | none().
+-spec websocket_data(#state{}, #http_req{}, any(), binary()) -> closed | none().
%% No more data.
websocket_data(State, Req, HandlerState, <<>>) ->
handler_before_loop(State, Req, HandlerState, <<>>);
%% hixie-76 close frame.
websocket_data(State=#state{version=0}, Req, HandlerState,
- << 255, 0, _Rest/bits >>) ->
+ << 255, 0, _Rest/binary >>) ->
websocket_close(State, Req, HandlerState, {normal, closed});
%% hixie-76 data frame. We only support the frame type 0, same as the specs.
websocket_data(State=#state{version=0, eop=EOP}, Req, HandlerState,
- Data = << 0, _/bits >>) ->
+ Data = << 0, _/binary >>) ->
case binary:match(Data, EOP) of
{Pos, 1} ->
Pos2 = Pos - 1,
@@ -268,38 +270,54 @@ websocket_data(State=#state{version=Version}, Req, HandlerState, Data)
websocket_data(State=#state{version=Version}, Req, HandlerState, Data)
when Version =/= 0 ->
<< 1:1, 0:3, Opcode:4, Mask:1, PayloadLen:7, Rest/bits >> = Data,
- {PayloadLen2, Rest2} = case {PayloadLen, Rest} of
- {126, << L:16, R/bits >>} -> {L, R};
- {126, Rest} -> {undefined, Rest};
- {127, << 0:1, L:63, R/bits >>} -> {L, R};
- {127, Rest} -> {undefined, Rest};
- {PayloadLen, Rest} -> {PayloadLen, Rest}
- end,
- case {Mask, PayloadLen2} of
+ case {PayloadLen, Rest} of
+ {126, _} when Opcode >= 8 -> websocket_close(
+ State, Req, HandlerState, {error, protocol});
+ {127, _} when Opcode >= 8 -> websocket_close(
+ State, Req, HandlerState, {error, protocol});
+ {126, << L:16, R/bits >>} -> websocket_before_unmask(
+ State, Req, HandlerState, Data, R, Opcode, Mask, L);
+ {126, Rest} -> websocket_before_unmask(
+ State, Req, HandlerState, Data, Rest, Opcode, Mask, undefined);
+ {127, << 0:1, L:63, R/bits >>} -> websocket_before_unmask(
+ State, Req, HandlerState, Data, R, Opcode, Mask, L);
+ {127, Rest} -> websocket_before_unmask(
+ State, Req, HandlerState, Data, Rest, Opcode, Mask, undefined);
+ {PayloadLen, Rest} -> websocket_before_unmask(
+ State, Req, HandlerState, Data, Rest, Opcode, Mask, PayloadLen)
+ end;
+%% Something was wrong with the frame. Close the connection.
+websocket_data(State, Req, HandlerState, _Bad) ->
+ websocket_close(State, Req, HandlerState, {error, badframe}).
+
+%% hybi routing depending on whether unmasking is needed.
+-spec websocket_before_unmask(#state{}, #http_req{}, any(), binary(),
+ binary(), opcode(), 0 | 1, non_neg_integer() | undefined)
+ -> closed | none().
+websocket_before_unmask(State, Req, HandlerState, Data,
+ Rest, Opcode, Mask, PayloadLen) ->
+ case {Mask, PayloadLen} of
{0, 0} ->
- websocket_dispatch(State, Req, HandlerState, Rest2, Opcode, <<>>);
- {1, N} when N + 4 > byte_size(Rest2); N =:= undefined ->
+ websocket_dispatch(State, Req, HandlerState, Rest, Opcode, <<>>);
+ {1, N} when N + 4 > byte_size(Rest); N =:= undefined ->
%% @todo We probably should allow limiting frame length.
handler_before_loop(State, Req, HandlerState, Data);
{1, _N} ->
- << MaskKey:32, Payload:PayloadLen2/binary, Rest3/bits >> = Rest2,
- websocket_unmask(State, Req, HandlerState, Rest3,
+ << MaskKey:32, Payload:PayloadLen/binary, Rest2/bits >> = Rest,
+ websocket_unmask(State, Req, HandlerState, Rest2,
Opcode, Payload, MaskKey)
- end;
-%% Something was wrong with the frame. Close the connection.
-websocket_data(State, Req, HandlerState, _Bad) ->
- websocket_close(State, Req, HandlerState, {error, badframe}).
+ end.
%% hybi unmasking.
-spec websocket_unmask(#state{}, #http_req{}, any(), binary(),
- opcode(), binary(), mask_key()) -> ok | none().
+ opcode(), binary(), mask_key()) -> closed | none().
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, Payload, MaskKey) ->
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, Payload, MaskKey, <<>>).
-spec websocket_unmask(#state{}, #http_req{}, any(), binary(),
- opcode(), binary(), mask_key(), binary()) -> ok | none().
+ opcode(), binary(), mask_key(), binary()) -> closed | none().
websocket_unmask(State, Req, HandlerState, RemainingData,
Opcode, << O:32, Rest/bits >>, MaskKey, Acc) ->
T = O bxor MaskKey,
@@ -330,7 +348,7 @@ websocket_unmask(State, Req, HandlerState, RemainingData,
%% hybi dispatching.
-spec websocket_dispatch(#state{}, #http_req{}, any(), binary(),
- opcode(), binary()) -> ok | none().
+ opcode(), binary()) -> closed | none().
%% @todo Fragmentation.
%~ websocket_dispatch(State, Req, HandlerState, RemainingData, 0, Payload) ->
%% Text frame.
@@ -358,7 +376,7 @@ websocket_dispatch(State, Req, HandlerState, RemainingData, 10, Payload) ->
websocket_handle, {pong, Payload}, fun websocket_data/4).
-spec handler_call(#state{}, #http_req{}, any(), binary(),
- atom(), any(), fun()) -> ok | none().
+ atom(), any(), fun()) -> closed | none().
handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
RemainingData, Callback, Message, NextState) ->
try Handler:Callback(Message, Req, HandlerState) of
@@ -387,7 +405,7 @@ handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
websocket_close(State, Req, HandlerState, {error, handler})
end.
--spec websocket_send(binary(), #state{}, #http_req{}) -> ok | ignore.
+-spec websocket_send(binary(), #state{}, #http_req{}) -> closed | ignore.
%% hixie-76 text frame.
websocket_send({text, Payload}, #state{version=0},
#http_req{socket=Socket, transport=Transport}) ->
@@ -407,21 +425,19 @@ websocket_send({Type, Payload}, _State,
Transport:send(Socket, [<< 1:1, 0:3, Opcode:4, 0:1, Len/bits >>,
Payload]).
--spec websocket_close(#state{}, #http_req{}, any(), {atom(), atom()}) -> ok.
+-spec websocket_close(#state{}, #http_req{}, any(), {atom(), atom()}) -> closed.
websocket_close(State=#state{version=0}, Req=#http_req{socket=Socket,
transport=Transport}, HandlerState, Reason) ->
Transport:send(Socket, << 255, 0 >>),
- Transport:close(Socket),
handler_terminate(State, Req, HandlerState, Reason);
%% @todo Send a Payload? Using Reason is usually good but we're quite careless.
websocket_close(State, Req=#http_req{socket=Socket,
transport=Transport}, HandlerState, Reason) ->
Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>),
- Transport:close(Socket),
handler_terminate(State, Req, HandlerState, Reason).
-spec handler_terminate(#state{}, #http_req{},
- any(), atom() | {atom(), atom()}) -> ok.
+ any(), atom() | {atom(), atom()}) -> closed.
handler_terminate(#state{handler=Handler, opts=Opts},
Req, HandlerState, TerminateReason) ->
try
@@ -434,7 +450,8 @@ handler_terminate(#state{handler=Handler, opts=Opts},
"** Request was ~p~n** Stacktrace: ~p~n~n",
[Handler, Class, Reason, TerminateReason, Opts,
HandlerState, Req, erlang:get_stacktrace()])
- end.
+ end,
+ closed.
%% hixie-76 specific.
diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl
index 9dc35d9..34bb1a1 100644
--- a/src/cowboy_protocol.erl
+++ b/src/cowboy_protocol.erl
@@ -24,9 +24,9 @@
%% starting the listener. The <em>start_link/4</em> function must follow
%% the supervisor start function specification.
%%
-%% After initializing your protocol, it is recommended to wait to
-%% receive a message containing the atom 'shoot', as it will ensure
-%% Cowboy has been able to fully initialize the socket.
+%% After initializing your protocol, it is recommended to call the
+%% function cowboy:accept_ack/1 with the ListenerPid as argument,
+%% as it will ensure Cowboy has been able to fully initialize the socket.
%% Anything you do past this point is up to you!
%%
%% If you need to change some socket options, like enabling raw mode