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.erl2
-rw-r--r--src/cowboy_app.erl2
-rw-r--r--src/cowboy_bstr.erl51
-rw-r--r--src/cowboy_client.erl2
-rw-r--r--src/cowboy_clock.erl4
-rw-r--r--src/cowboy_dispatcher.erl291
-rw-r--r--src/cowboy_handler.erl220
-rw-r--r--src/cowboy_http.erl88
-rw-r--r--src/cowboy_http_handler.erl12
-rw-r--r--src/cowboy_loop_handler.erl12
-rw-r--r--src/cowboy_middleware.erl36
-rw-r--r--src/cowboy_protocol.erl317
-rw-r--r--src/cowboy_req.erl201
-rw-r--r--src/cowboy_rest.erl114
-rw-r--r--src/cowboy_router.erl565
-rw-r--r--src/cowboy_static.erl12
-rw-r--r--src/cowboy_sup.erl2
-rw-r--r--src/cowboy_websocket.erl724
-rw-r--r--src/cowboy_websocket_handler.erl4
20 files changed, 1699 insertions, 962 deletions
diff --git a/src/cowboy.app.src b/src/cowboy.app.src
index d32262e..59fa8fe 100644
--- a/src/cowboy.app.src
+++ b/src/cowboy.app.src
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
diff --git a/src/cowboy.erl b/src/cowboy.erl
index 9e4a66a..79dbb71 100644
--- a/src/cowboy.erl
+++ b/src/cowboy.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
diff --git a/src/cowboy_app.erl b/src/cowboy_app.erl
index 180d400..b46ba1d 100644
--- a/src/cowboy_app.erl
+++ b/src/cowboy_app.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
diff --git a/src/cowboy_bstr.erl b/src/cowboy_bstr.erl
index 6e5b353..bc6818f 100644
--- a/src/cowboy_bstr.erl
+++ b/src/cowboy_bstr.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
@@ -16,16 +16,40 @@
-module(cowboy_bstr).
%% Binary strings.
+-export([capitalize_token/1]).
-export([to_lower/1]).
%% Characters.
-export([char_to_lower/1]).
-export([char_to_upper/1]).
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-endif.
+
+%% @doc Capitalize a token.
+%%
+%% The first letter and all letters after a dash are capitalized.
+%% This is the form seen for header names in the HTTP/1.1 RFC and
+%% others. Note that using this form isn't required, as header name
+%% are case insensitive, and it is only provided for use with eventual
+%% badly implemented clients.
+-spec capitalize_token(B) -> B when B::binary().
+capitalize_token(B) ->
+ capitalize_token(B, true, <<>>).
+capitalize_token(<<>>, _, Acc) ->
+ Acc;
+capitalize_token(<< $-, Rest/bits >>, _, Acc) ->
+ capitalize_token(Rest, true, << Acc/binary, $- >>);
+capitalize_token(<< C, Rest/bits >>, true, Acc) ->
+ capitalize_token(Rest, false, << Acc/binary, (char_to_upper(C)) >>);
+capitalize_token(<< C, Rest/bits >>, false, Acc) ->
+ capitalize_token(Rest, false, << Acc/binary, (char_to_lower(C)) >>).
+
%% @doc Convert a binary string to lowercase.
--spec to_lower(binary()) -> binary().
-to_lower(L) ->
- << << (char_to_lower(C)) >> || << C >> <= L >>.
+-spec to_lower(B) -> B when B::binary().
+to_lower(B) ->
+ << << (char_to_lower(C)) >> || << C >> <= B >>.
%% @doc Convert [A-Z] characters to lowercase.
%% @end
@@ -88,3 +112,22 @@ char_to_upper($x) -> $X;
char_to_upper($y) -> $Y;
char_to_upper($z) -> $Z;
char_to_upper(Ch) -> Ch.
+
+%% Tests.
+
+-ifdef(TEST).
+
+capitalize_token_test_() ->
+ %% {Header, Result}
+ Tests = [
+ {<<"heLLo-woRld">>, <<"Hello-World">>},
+ {<<"Sec-Websocket-Version">>, <<"Sec-Websocket-Version">>},
+ {<<"Sec-WebSocket-Version">>, <<"Sec-Websocket-Version">>},
+ {<<"sec-websocket-version">>, <<"Sec-Websocket-Version">>},
+ {<<"SEC-WEBSOCKET-VERSION">>, <<"Sec-Websocket-Version">>},
+ {<<"Sec-WebSocket--Version">>, <<"Sec-Websocket--Version">>},
+ {<<"Sec-WebSocket---Version">>, <<"Sec-Websocket---Version">>}
+ ],
+ [{H, fun() -> R = capitalize_token(H) end} || {H, R} <- Tests].
+
+-endif.
diff --git a/src/cowboy_client.erl b/src/cowboy_client.erl
index fee6793..4d958b1 100644
--- a/src/cowboy_client.erl
+++ b/src/cowboy_client.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2012-2013, 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
diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl
index b439bb1..71bcb21 100644
--- a/src/cowboy_clock.erl
+++ b/src/cowboy_clock.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
@@ -71,7 +71,7 @@ rfc1123() ->
rfc1123(DateTime) ->
update_rfc1123(<<>>, undefined, DateTime).
-%% @doc Return the current date and time formatted according to RFC-2109.
+%% @doc Return the given date and time formatted according to RFC-2109.
%%
%% This format is used in the <em>set-cookie</em> header sent with
%% HTTP responses.
diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl
deleted file mode 100644
index ef6e8ac..0000000
--- a/src/cowboy_dispatcher.erl
+++ /dev/null
@@ -1,291 +0,0 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
-%% Copyright (c) 2011, Anthony Ramine <[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 Dispatch requests according to a hostname and path.
--module(cowboy_dispatcher).
-
-%% API.
--export([match/3]).
-
--type bindings() :: [{atom(), binary()}].
--type tokens() :: [binary()].
--type match_rule() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()].
--type dispatch_path() :: [{match_rule(), module(), any()}].
--type dispatch_rule() :: {Host::match_rule(), Path::dispatch_path()}.
--type dispatch_rules() :: [dispatch_rule()].
-
--export_type([bindings/0]).
--export_type([tokens/0]).
--export_type([dispatch_rules/0]).
-
--ifdef(TEST).
--include_lib("eunit/include/eunit.hrl").
--endif.
-
-%% API.
-
-%% @doc Match hostname tokens and path tokens against dispatch rules.
-%%
-%% It is typically used for matching tokens for the hostname and path of
-%% the request against a global dispatch rule for your listener.
-%%
-%% Dispatch rules are a list of <em>{Hostname, PathRules}</em> tuples, with
-%% <em>PathRules</em> being a list of <em>{Path, HandlerMod, HandlerOpts}</em>.
-%%
-%% <em>Hostname</em> and <em>Path</em> are match rules and can be either the
-%% atom <em>'_'</em>, which matches everything, `<<"*">>', which match the
-%% wildcard path, or a list of tokens.
-%%
-%% Each token can be either a binary, the atom <em>'_'</em>,
-%% the atom '...' or a named atom. A binary token must match exactly,
-%% <em>'_'</em> matches everything for a single token, <em>'...'</em> matches
-%% everything for the rest of the tokens and a named atom will bind the
-%% corresponding token value and return it.
-%%
-%% The list of hostname tokens is reversed before matching. For example, if
-%% we were to match "www.ninenines.eu", we would first match "eu", then
-%% "ninenines", then "www". This means that in the context of hostnames,
-%% the <em>'...'</em> atom matches properly the lower levels of the domain
-%% as would be expected.
-%%
-%% When a result is found, this function will return the handler module and
-%% options found in the dispatch list, a key-value list of bindings and
-%% the tokens that were matched by the <em>'...'</em> atom for both the
-%% hostname and path.
--spec match(dispatch_rules(), Host::binary() | tokens(), Path::binary())
- -> {ok, module(), any(), bindings(),
- HostInfo::undefined | tokens(),
- PathInfo::undefined | tokens()}
- | {error, notfound, host} | {error, notfound, path}
- | {error, badrequest, path}.
-match([], _, _) ->
- {error, notfound, host};
-match([{'_', PathMatchs}|_Tail], _, Path) ->
- match_path(PathMatchs, undefined, Path, []);
-match([{HostMatch, PathMatchs}|Tail], Tokens, Path)
- when is_list(Tokens) ->
- case list_match(Tokens, lists:reverse(HostMatch), []) of
- false ->
- match(Tail, Tokens, Path);
- {true, Bindings, undefined} ->
- match_path(PathMatchs, undefined, Path, Bindings);
- {true, Bindings, HostInfo} ->
- match_path(PathMatchs, lists:reverse(HostInfo),
- Path, Bindings)
- end;
-match(Dispatch, Host, Path) ->
- match(Dispatch, split_host(Host), Path).
-
--spec match_path(dispatch_path(),
- HostInfo::undefined | tokens(), binary() | tokens(), bindings())
- -> {ok, module(), any(), bindings(),
- HostInfo::undefined | tokens(),
- PathInfo::undefined | tokens()}
- | {error, notfound, path} | {error, badrequest, path}.
-match_path([], _, _, _) ->
- {error, notfound, path};
-match_path([{'_', Handler, Opts}|_Tail], HostInfo, _, Bindings) ->
- {ok, Handler, Opts, Bindings, HostInfo, undefined};
-match_path([{<<"*">>, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) ->
- {ok, Handler, Opts, Bindings, HostInfo, undefined};
-match_path([{PathMatch, Handler, Opts}|Tail], HostInfo, Tokens,
- Bindings) when is_list(Tokens) ->
- case list_match(Tokens, PathMatch, []) of
- false ->
- match_path(Tail, HostInfo, Tokens, Bindings);
- {true, PathBinds, PathInfo} ->
- {ok, Handler, Opts, Bindings ++ PathBinds, HostInfo, PathInfo}
- end;
-match_path(_Dispatch, _HostInfo, badrequest, _Bindings) ->
- {error, badrequest, path};
-match_path(Dispatch, HostInfo, Path, Bindings) ->
- match_path(Dispatch, HostInfo, split_path(Path), Bindings).
-
-%% Internal.
-
-%% @doc Split a hostname into a list of tokens.
--spec split_host(binary()) -> tokens().
-split_host(Host) ->
- split_host(Host, []).
-
-split_host(Host, Acc) ->
- case binary:match(Host, <<".">>) of
- nomatch when Host =:= <<>> ->
- Acc;
- nomatch ->
- [Host|Acc];
- {Pos, _} ->
- << Segment:Pos/binary, _:8, Rest/bits >> = Host,
- false = byte_size(Segment) == 0,
- split_host(Rest, [Segment|Acc])
- end.
-
-%% @doc Split a path into a list of path segments.
-%%
-%% 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().
-split_path(<< $/, Path/bits >>) ->
- split_path(Path, []);
-split_path(_) ->
- badrequest.
-
-split_path(Path, Acc) ->
- try
- case binary:match(Path, <<"/">>) of
- nomatch when Path =:= <<>> ->
- lists:reverse([cowboy_http:urldecode(S) || S <- Acc]);
- nomatch ->
- lists:reverse([cowboy_http:urldecode(S) || S <- [Path|Acc]]);
- {Pos, _} ->
- << Segment:Pos/binary, _:8, Rest/bits >> = Path,
- split_path(Rest, [Segment|Acc])
- end
- catch
- error:badarg ->
- badrequest
- end.
-
--spec list_match(tokens(), match_rule(), bindings())
- -> {true, bindings(), undefined | tokens()} | false.
-%% Atom '...' matches any trailing path, stop right now.
-list_match(List, ['...'], Binds) ->
- {true, Binds, List};
-%% Atom '_' matches anything, continue.
-list_match([_E|Tail], ['_'|TailMatch], Binds) ->
- list_match(Tail, TailMatch, Binds);
-%% Both values match, continue.
-list_match([E|Tail], [E|TailMatch], Binds) ->
- list_match(Tail, TailMatch, Binds);
-%% Bind E to the variable name V and continue.
-list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) ->
- list_match(Tail, TailMatch, [{V, E}|Binds]);
-%% Match complete.
-list_match([], [], Binds) ->
- {true, Binds, undefined};
-%% Values don't match, stop.
-list_match(_List, _Match, _Binds) ->
- false.
-
-%% Tests.
-
--ifdef(TEST).
-
-split_host_test_() ->
- %% {Host, Result}
- Tests = [
- {<<"">>, []},
- {<<"*">>, [<<"*">>]},
- {<<"cowboy.ninenines.eu">>,
- [<<"eu">>, <<"ninenines">>, <<"cowboy">>]},
- {<<"ninenines.eu">>,
- [<<"eu">>, <<"ninenines">>]},
- {<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>,
- [<<"z">>, <<"y">>, <<"x">>, <<"w">>, <<"v">>, <<"u">>, <<"t">>,
- <<"s">>, <<"r">>, <<"q">>, <<"p">>, <<"o">>, <<"n">>, <<"m">>,
- <<"l">>, <<"k">>, <<"j">>, <<"i">>, <<"h">>, <<"g">>, <<"f">>,
- <<"e">>, <<"d">>, <<"c">>, <<"b">>, <<"a">>]}
- ],
- [{H, fun() -> R = split_host(H) end} || {H, R} <- Tests].
-
-split_path_test_() ->
- %% {Path, Result, QueryString}
- Tests = [
- {<<"/">>, []},
- {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]},
- {<<"/users">>, [<<"users">>]},
- {<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]},
- {<<"/users/a+b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]}
- ],
- [{P, fun() -> R = split_path(P) end} || {P, R} <- Tests].
-
-match_test_() ->
- Dispatch = [
- {[<<"www">>, '_', <<"ninenines">>, <<"eu">>], [
- {[<<"users">>, '_', <<"mails">>], match_any_subdomain_users, []}
- ]},
- {[<<"ninenines">>, <<"eu">>], [
- {[<<"users">>, id, <<"friends">>], match_extend_users_friends, []},
- {'_', match_extend, []}
- ]},
- {[<<"ninenines">>, var], [
- {[<<"threads">>, var], match_duplicate_vars,
- [we, {expect, two}, var, here]}
- ]},
- {[<<"erlang">>, ext], [
- {'_', match_erlang_ext, []}
- ]},
- {'_', [
- {[<<"users">>, id, <<"friends">>], match_users_friends, []},
- {'_', match_any, []}
- ]}
- ],
- %% {Host, Path, Result}
- Tests = [
- {<<"any">>, <<"/">>, {ok, match_any, [], []}},
- {<<"www.any.ninenines.eu">>, <<"/users/42/mails">>,
- {ok, match_any_subdomain_users, [], []}},
- {<<"www.ninenines.eu">>, <<"/users/42/mails">>,
- {ok, match_any, [], []}},
- {<<"www.ninenines.eu">>, <<"/">>,
- {ok, match_any, [], []}},
- {<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>,
- {error, notfound, path}},
- {<<"ninenines.eu">>, <<"/">>,
- {ok, match_extend, [], []}},
- {<<"ninenines.eu">>, <<"/users/42/friends">>,
- {ok, match_extend_users_friends, [], [{id, <<"42">>}]}},
- {<<"erlang.fr">>, '_',
- {ok, match_erlang_ext, [], [{ext, <<"fr">>}]}},
- {<<"any">>, <<"/users/444/friends">>,
- {ok, match_users_friends, [], [{id, <<"444">>}]}},
- {<<"ninenines.fr">>, <<"/threads/987">>,
- {ok, match_duplicate_vars, [we, {expect, two}, var, here],
- [{var, <<"fr">>}, {var, <<"987">>}]}}
- ],
- [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
- {ok, Handler, Opts, Binds, undefined, undefined}
- = match(Dispatch, H, P)
- end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests].
-
-match_info_test_() ->
- Dispatch = [
- {[<<"www">>, <<"ninenines">>, <<"eu">>], [
- {[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], match_path, []}
- ]},
- {['...', <<"ninenines">>, <<"eu">>], [
- {'_', match_any, []}
- ]}
- ],
- Tests = [
- {<<"ninenines.eu">>, <<"/">>,
- {ok, match_any, [], [], [], undefined}},
- {<<"bugs.ninenines.eu">>, <<"/">>,
- {ok, match_any, [], [], [<<"bugs">>], undefined}},
- {<<"cowboy.bugs.ninenines.eu">>, <<"/">>,
- {ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}},
- {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>,
- {ok, match_path, [], [], undefined, []}},
- {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>,
- {ok, match_path, [], [], undefined, [<<"path_info">>]}},
- {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>,
- {ok, match_path, [], [], undefined, [<<"foo">>, <<"bar">>]}}
- ],
- [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
- R = match(Dispatch, H, P)
- end} || {H, P, R} <- Tests].
-
--endif.
diff --git a/src/cowboy_handler.erl b/src/cowboy_handler.erl
new file mode 100644
index 0000000..7ed7db3
--- /dev/null
+++ b/src/cowboy_handler.erl
@@ -0,0 +1,220 @@
+%% Copyright (c) 2011-2013, 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 Handler middleware.
+%%
+%% Execute the handler given by the <em>handler</em> and <em>handler_opts</em>
+%% environment values. The result of this execution is added to the
+%% environment under the <em>result</em> value.
+%%
+%% @see cowboy_http_handler
+-module(cowboy_handler).
+-behaviour(cowboy_middleware).
+
+-export([execute/2]).
+-export([handler_loop/4]).
+
+-record(state, {
+ env :: cowboy_middleware:env(),
+ hibernate = false :: boolean(),
+ loop_timeout = infinity :: timeout(),
+ loop_timeout_ref :: undefined | reference(),
+ resp_sent = false :: boolean()
+}).
+
+%% @private
+-spec execute(Req, Env)
+ -> {ok, Req, Env} | {error, 500, Req}
+ | {suspend, ?MODULE, handler_loop, [any()]}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+execute(Req, Env) ->
+ {_, Handler} = lists:keyfind(handler, 1, Env),
+ {_, HandlerOpts} = lists:keyfind(handler_opts, 1, Env),
+ handler_init(Req, #state{env=Env}, Handler, HandlerOpts).
+
+-spec handler_init(Req, #state{}, module(), any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {error, 500, Req} | {suspend, module(), function(), [any()]}
+ when Req::cowboy_req:req().
+handler_init(Req, State, Handler, HandlerOpts) ->
+ Transport = cowboy_req:get(transport, Req),
+ try Handler:init({Transport:name(), http}, Req, HandlerOpts) of
+ {ok, Req2, HandlerState} ->
+ handler_handle(Req2, State, Handler, HandlerState);
+ {loop, Req2, HandlerState} ->
+ handler_before_loop(Req2, State#state{hibernate=false},
+ Handler, HandlerState);
+ {loop, Req2, HandlerState, hibernate} ->
+ handler_before_loop(Req2, State#state{hibernate=true},
+ Handler, HandlerState);
+ {loop, Req2, HandlerState, Timeout} ->
+ handler_before_loop(Req2, State#state{loop_timeout=Timeout},
+ Handler, HandlerState);
+ {loop, Req2, HandlerState, Timeout, hibernate} ->
+ handler_before_loop(Req2, State#state{
+ hibernate=true, loop_timeout=Timeout}, Handler, HandlerState);
+ {shutdown, Req2, HandlerState} ->
+ terminate_request(Req2, State, Handler, HandlerState,
+ {normal, shutdown});
+ %% @todo {upgrade, transport, Module}
+ {upgrade, protocol, Module} ->
+ upgrade_protocol(Req, State, Handler, HandlerOpts, Module);
+ {upgrade, protocol, Module, Req2, HandlerOpts2} ->
+ upgrade_protocol(Req2, State, Handler, HandlerOpts2, Module)
+ catch Class:Reason ->
+ error_logger:error_msg(
+ "** Cowboy handler ~p terminating in ~p/~p~n"
+ " for the reason ~p:~p~n"
+ "** Options were ~p~n"
+ "** Request was ~p~n"
+ "** Stacktrace: ~p~n~n",
+ [Handler, init, 3, Class, Reason, HandlerOpts,
+ cowboy_req:to_list(Req), erlang:get_stacktrace()]),
+ error_terminate(Req, State)
+ end.
+
+-spec upgrade_protocol(Req, #state{}, module(), any(), module())
+ -> {ok, Req, Env}
+ | {suspend, module(), atom(), any()}
+ | {halt, Req}
+ | {error, cowboy_http:status(), Req}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+upgrade_protocol(Req, #state{env=Env},
+ Handler, HandlerOpts, Module) ->
+ Module:upgrade(Req, Env, Handler, HandlerOpts).
+
+-spec handler_handle(Req, #state{}, module(), any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {error, 500, Req}
+ when Req::cowboy_req:req().
+handler_handle(Req, State, Handler, HandlerState) ->
+ try Handler:handle(Req, HandlerState) of
+ {ok, Req2, HandlerState2} ->
+ terminate_request(Req2, State, Handler, HandlerState2,
+ {normal, shutdown})
+ catch Class:Reason ->
+ error_logger:error_msg(
+ "** Cowboy handler ~p terminating in ~p/~p~n"
+ " for the reason ~p:~p~n"
+ "** Handler state was ~p~n"
+ "** Request was ~p~n"
+ "** Stacktrace: ~p~n~n",
+ [Handler, handle, 2, Class, Reason, HandlerState,
+ cowboy_req:to_list(Req), erlang:get_stacktrace()]),
+ handler_terminate(Req, Handler, HandlerState, Reason),
+ error_terminate(Req, State)
+ end.
+
+%% We don't listen for Transport closes because that would force us
+%% to receive data and buffer it indefinitely.
+-spec handler_before_loop(Req, #state{}, module(), any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {error, 500, Req} | {suspend, module(), function(), [any()]}
+ when Req::cowboy_req:req().
+handler_before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) ->
+ State2 = handler_loop_timeout(State),
+ {suspend, ?MODULE, handler_loop,
+ [Req, State2#state{hibernate=false}, Handler, HandlerState]};
+handler_before_loop(Req, State, Handler, HandlerState) ->
+ State2 = handler_loop_timeout(State),
+ handler_loop(Req, State2, Handler, HandlerState).
+
+%% Almost the same code can be found in cowboy_websocket.
+-spec handler_loop_timeout(#state{}) -> #state{}.
+handler_loop_timeout(State=#state{loop_timeout=infinity}) ->
+ State#state{loop_timeout_ref=undefined};
+handler_loop_timeout(State=#state{loop_timeout=Timeout,
+ loop_timeout_ref=PrevRef}) ->
+ _ = case PrevRef of undefined -> ignore; PrevRef ->
+ erlang:cancel_timer(PrevRef) end,
+ TRef = erlang:start_timer(Timeout, self(), ?MODULE),
+ State#state{loop_timeout_ref=TRef}.
+
+%% @private
+-spec handler_loop(Req, #state{}, module(), any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {error, 500, Req} | {suspend, module(), function(), [any()]}
+ when Req::cowboy_req:req().
+handler_loop(Req, State=#state{loop_timeout_ref=TRef}, Handler, HandlerState) ->
+ receive
+ {cowboy_req, resp_sent} ->
+ handler_loop(Req, State#state{resp_sent=true},
+ Handler, HandlerState);
+ {timeout, TRef, ?MODULE} ->
+ terminate_request(Req, State, Handler, HandlerState,
+ {normal, timeout});
+ {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
+ handler_loop(Req, State, Handler, HandlerState);
+ Message ->
+ handler_call(Req, State, Handler, HandlerState, Message)
+ end.
+
+-spec handler_call(Req, #state{}, module(), any(), any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {error, 500, Req} | {suspend, module(), function(), [any()]}
+ when Req::cowboy_req:req().
+handler_call(Req, State, Handler, HandlerState, Message) ->
+ try Handler:info(Message, Req, HandlerState) of
+ {ok, Req2, HandlerState2} ->
+ terminate_request(Req2, State, Handler, HandlerState2,
+ {normal, shutdown});
+ {loop, Req2, HandlerState2} ->
+ handler_before_loop(Req2, State, Handler, HandlerState2);
+ {loop, Req2, HandlerState2, hibernate} ->
+ handler_before_loop(Req2, State#state{hibernate=true},
+ Handler, HandlerState2)
+ catch Class:Reason ->
+ error_logger:error_msg(
+ "** Cowboy handler ~p terminating in ~p/~p~n"
+ " for the reason ~p:~p~n"
+ "** Handler state was ~p~n"
+ "** Request was ~p~n"
+ "** Stacktrace: ~p~n~n",
+ [Handler, info, 3, Class, Reason, HandlerState,
+ cowboy_req:to_list(Req), erlang:get_stacktrace()]),
+ handler_terminate(Req, Handler, HandlerState, Reason),
+ error_terminate(Req, State)
+ end.
+
+-spec terminate_request(Req, #state{}, module(), any(),
+ {normal, timeout | shutdown} | {error, atom()}) ->
+ {ok, Req, cowboy_middleware:env()} when Req::cowboy_req:req().
+terminate_request(Req, #state{env=Env}, Handler, HandlerState, Reason) ->
+ HandlerRes = handler_terminate(Req, Handler, HandlerState, Reason),
+ {ok, Req, [{result, HandlerRes}|Env]}.
+
+-spec handler_terminate(cowboy_req:req(), module(), any(),
+ {normal, timeout | shutdown} | {error, atom()}) -> ok.
+handler_terminate(Req, Handler, HandlerState, Reason) ->
+ try
+ Handler:terminate(Reason, cowboy_req:lock(Req), HandlerState)
+ catch Class:Reason2 ->
+ error_logger:error_msg(
+ "** Cowboy handler ~p terminating in ~p/~p~n"
+ " for the reason ~p:~p~n"
+ "** Handler state was ~p~n"
+ "** Request was ~p~n"
+ "** Stacktrace: ~p~n~n",
+ [Handler, terminate, 3, Class, Reason2, HandlerState,
+ cowboy_req:to_list(Req), erlang:get_stacktrace()])
+ end.
+
+%% Only send an error reply if there is no resp_sent message.
+-spec error_terminate(Req, #state{})
+ -> {error, 500, Req} | {halt, Req} when Req::cowboy_req:req().
+error_terminate(Req, #state{resp_sent=true}) ->
+ %% Close the connection, but do not attempt sending a reply.
+ {halt, cowboy_req:set([{connection, close}, {resp_state, done}], Req)};
+error_terminate(Req, _) ->
+ {error, 500, Req}.
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
index 66383cb..a78e090 100644
--- a/src/cowboy_http.erl
+++ b/src/cowboy_http.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
@@ -36,6 +36,7 @@
-export([token/2]).
-export([token_ci/2]).
-export([quoted_string/2]).
+-export([authorization/2]).
%% Decoding.
-export([te_chunked/2]).
@@ -801,25 +802,72 @@ qvalue(<< C, Rest/binary >>, Fun, Q, M)
qvalue(Data, Fun, Q, _M) ->
Fun(Data, Q).
+%% @doc Parse authorization value according rfc 2617.
+%% Only Basic authorization is supported so far.
+-spec authorization(binary(), binary()) -> {binary(), any()} | {error, badarg}.
+authorization(UserPass, Type = <<"basic">>) ->
+ cowboy_http:whitespace(UserPass,
+ fun(D) ->
+ authorization_basic_userid(base64:mime_decode(D),
+ fun(Rest, Userid) ->
+ authorization_basic_password(Rest,
+ fun(Password) ->
+ {Type, {Userid, Password}}
+ end)
+ end)
+ end);
+authorization(String, Type) ->
+ {Type, String}.
+
+%% @doc Parse user credentials.
+-spec authorization_basic_userid(binary(), fun()) -> any().
+authorization_basic_userid(Data, Fun) ->
+ authorization_basic_userid(Data, Fun, <<>>).
+
+authorization_basic_userid(<<>>, _Fun, _Acc) ->
+ {error, badarg};
+authorization_basic_userid(<<C, _Rest/binary>>, _Fun, Acc)
+ when C < 32; C =:= 127; (C =:=$: andalso Acc =:= <<>>) ->
+ {error, badarg};
+authorization_basic_userid(<<$:, Rest/binary>>, Fun, Acc) ->
+ Fun(Rest, Acc);
+authorization_basic_userid(<<C, Rest/binary>>, Fun, Acc) ->
+ authorization_basic_userid(Rest, Fun, <<Acc/binary, C>>).
+
+-spec authorization_basic_password(binary(), fun()) -> any().
+authorization_basic_password(Data, Fun) ->
+ authorization_basic_password(Data, Fun, <<>>).
+
+authorization_basic_password(<<>>, _Fun, <<>>) ->
+ {error, badarg};
+authorization_basic_password(<<C, _Rest/binary>>, _Fun, _Acc)
+ when C < 32; C=:= 127 ->
+ {error, badarg};
+authorization_basic_password(<<>>, Fun, Acc) ->
+ Fun(Acc);
+authorization_basic_password(<<C, Rest/binary>>, Fun, Acc) ->
+ authorization_basic_password(Rest, Fun, <<Acc/binary, C>>).
+
%% Decoding.
%% @doc Decode a stream of chunks.
--spec te_chunked(binary(), {non_neg_integer(), non_neg_integer()})
- -> more | {ok, binary(), {non_neg_integer(), non_neg_integer()}}
- | {ok, binary(), binary(), {non_neg_integer(), non_neg_integer()}}
- | {done, non_neg_integer(), binary()} | {error, badarg}.
-te_chunked(<<>>, _) ->
- more;
+-spec te_chunked(Bin, TransferState)
+ -> more | {more, non_neg_integer(), Bin, TransferState}
+ | {ok, Bin, TransferState} | {ok, Bin, Bin, TransferState}
+ | {done, non_neg_integer(), Bin} | {error, badarg}
+ when Bin::binary(), TransferState::{non_neg_integer(), non_neg_integer()}.
te_chunked(<< "0\r\n\r\n", Rest/binary >>, {0, Streamed}) ->
{done, Streamed, Rest};
te_chunked(Data, {0, Streamed}) ->
%% @todo We are expecting an hex size, not a general token.
token(Data,
- fun (Rest, _) when byte_size(Rest) < 4 ->
- more;
- (<< "\r\n", Rest/binary >>, BinLen) ->
+ fun (<< "\r\n", Rest/binary >>, BinLen) ->
Len = list_to_integer(binary_to_list(BinLen), 16),
te_chunked(Rest, {Len, Streamed});
+ %% Chunk size shouldn't take too many bytes,
+ %% don't try to stream forever.
+ (Rest, _) when byte_size(Rest) < 16 ->
+ more;
(_, _) ->
{error, badarg}
end);
@@ -827,13 +875,12 @@ te_chunked(Data, {ChunkRem, Streamed}) when byte_size(Data) >= ChunkRem + 2 ->
<< Chunk:ChunkRem/binary, "\r\n", Rest/binary >> = Data,
{ok, Chunk, Rest, {0, Streamed + byte_size(Chunk)}};
te_chunked(Data, {ChunkRem, Streamed}) ->
- Size = byte_size(Data),
- {ok, Data, {ChunkRem - Size, Streamed + Size}}.
+ {more, ChunkRem + 2, Data, {ChunkRem, Streamed}}.
%% @doc Decode an identity stream.
--spec te_identity(binary(), {non_neg_integer(), non_neg_integer()})
- -> {ok, binary(), {non_neg_integer(), non_neg_integer()}}
- | {done, binary(), non_neg_integer(), binary()}.
+-spec te_identity(Bin, TransferState)
+ -> {ok, Bin, TransferState} | {done, Bin, non_neg_integer(), Bin}
+ when Bin::binary(), TransferState::{non_neg_integer(), non_neg_integer()}.
te_identity(Data, {Streamed, Total})
when Streamed + byte_size(Data) < Total ->
{ok, Data, {Streamed + byte_size(Data), Total}};
@@ -1294,4 +1341,15 @@ urlencode_test_() ->
?_assertEqual(<<"%ff+">>, urlencode(<<255, " ">>))
].
+http_authorization_test_() ->
+ [?_assertEqual({<<"basic">>, {<<"Alladin">>, <<"open sesame">>}},
+ authorization(<<"QWxsYWRpbjpvcGVuIHNlc2FtZQ==">>, <<"basic">>)),
+ ?_assertEqual({error, badarg},
+ authorization(<<"dXNlcm5hbWUK">>, <<"basic">>)),
+ ?_assertEqual({error, badarg},
+ authorization(<<"_[]@#$%^&*()-AA==">>, <<"basic">>)),
+ ?_assertEqual({error, badarg},
+ authorization(<<"dXNlcjpwYXNzCA==">>, <<"basic">>)) %% user:pass\010
+ ].
+
-endif.
diff --git a/src/cowboy_http_handler.erl b/src/cowboy_http_handler.erl
index d686f30..0393153 100644
--- a/src/cowboy_http_handler.erl
+++ b/src/cowboy_http_handler.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
@@ -22,8 +22,8 @@
%% <em>handle/2</em> allows you to handle the request. It receives the
%% state previously defined.
%%
-%% <em>terminate/2</em> allows you to clean up. It receives the state
-%% previously defined.
+%% <em>terminate/3</em> allows you to clean up. It receives the
+%% termination reason and the state previously defined.
%%
%% There is no required operation to perform in any of these callbacks
%% other than returning the proper values. Make sure you always return
@@ -33,6 +33,9 @@
-type opts() :: any().
-type state() :: any().
+-type terminate_reason() :: {normal, shutdown}
+ | {normal, timeout} %% Only occurs in loop handlers.
+ | {error, atom()}.
-callback init({atom(), http}, Req, opts())
-> {ok, Req, state()}
@@ -42,7 +45,8 @@
| {loop, Req, state(), timeout(), hibernate}
| {shutdown, Req, state()}
| {upgrade, protocol, module()}
+ | {upgrade, protocol, module(), Req, opts()}
when Req::cowboy_req:req().
-callback handle(Req, State) -> {ok, Req, State}
when Req::cowboy_req:req(), State::state().
--callback terminate(cowboy_req:req(), state()) -> ok.
+-callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok.
diff --git a/src/cowboy_loop_handler.erl b/src/cowboy_loop_handler.erl
index 5ff86cf..f8d008f 100644
--- a/src/cowboy_loop_handler.erl
+++ b/src/cowboy_loop_handler.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
@@ -23,8 +23,8 @@
%% receive. It receives the message and the state previously defined.
%% It can decide to stop the receive loop or continue receiving.
%%
-%% <em>terminate/2</em> allows you to clean up. It receives the state
-%% previously defined.
+%% <em>terminate/3</em> allows you to clean up. It receives the
+%% termination reason and the state previously defined.
%%
%% There is no required operation to perform in any of these callbacks
%% other than returning the proper values. Make sure you always return
@@ -39,6 +39,9 @@
-type opts() :: any().
-type state() :: any().
+-type terminate_reason() :: {normal, shutdown}
+ | {normal, timeout}
+ | {error, atom()}.
-callback init({atom(), http}, Req, opts())
-> {ok, Req, state()}
@@ -48,10 +51,11 @@
| {loop, Req, state(), timeout(), hibernate}
| {shutdown, Req, state()}
| {upgrade, protocol, module()}
+ | {upgrade, protocol, module(), Req, opts()}
when Req::cowboy_req:req().
-callback info(any(), Req, State)
-> {ok, Req, State}
| {loop, Req, State}
| {loop, Req, State, hibernate}
when Req::cowboy_req:req(), State::state().
--callback terminate(cowboy_req:req(), state()) -> ok.
+-callback terminate(terminate_reason(), cowboy_req:req(), state()) -> ok.
diff --git a/src/cowboy_middleware.erl b/src/cowboy_middleware.erl
new file mode 100644
index 0000000..0c1ca77
--- /dev/null
+++ b/src/cowboy_middleware.erl
@@ -0,0 +1,36 @@
+%% Copyright (c) 2013, 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 Behaviour for middlewares.
+%%
+%% Only one function needs to be implemented, <em>execute/2</em>.
+%% It receives the Req and the environment and returns them
+%% optionally modified. It can decide to stop the processing with
+%% or without an error. It is also possible to hibernate the process
+%% if needed.
+%%
+%% A middleware can perform any operation. Make sure you always return
+%% the last modified Req so that Cowboy has the up to date information
+%% about the request.
+-module(cowboy_middleware).
+
+-type env() :: [{atom(), any()}].
+-export_type([env/0]).
+
+-callback execute(Req, Env)
+ -> {ok, Req, Env}
+ | {suspend, module(), atom(), any()}
+ | {halt, Req}
+ | {error, cowboy_http:status(), Req}
+ when Req::cowboy_req:req(), Env::env().
diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl
index 7344d1f..b479fa9 100644
--- a/src/cowboy_protocol.erl
+++ b/src/cowboy_protocol.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
@@ -17,7 +17,10 @@
%%
%% The available options are:
%% <dl>
-%% <dt>dispatch</dt><dd>The dispatch list for this protocol.</dd>
+%% <dt>compress</dt><dd>Whether to automatically compress the response
+%% body when the conditions are met. Disabled by default.</dd>
+%% <dt>env</dt><dd>The environment passed and optionally modified
+%% by middlewares.</dd>
%% <dt>max_empty_lines</dt><dd>Max number of empty lines before a request.
%% Defaults to 5.</dd>
%% <dt>max_header_name_length</dt><dd>Max length allowed for header names.
@@ -27,23 +30,22 @@
%% <dt>max_headers</dt><dd>Max number of headers allowed.
%% Defaults to 100.</dd>
%% <dt>max_keepalive</dt><dd>Max number of requests allowed in a single
-%% keep-alive session. Defaults to infinity.</dd>
+%% keep-alive session. Defaults to 100.</dd>
%% <dt>max_request_line_length</dt><dd>Max length allowed for the request
%% line. Defaults to 4096.</dd>
+%% <dt>middlewares</dt><dd>The list of middlewares to execute when a
+%% request is received.</dd>
%% <dt>onrequest</dt><dd>Optional fun that allows Req interaction before
%% any dispatching is done. Host info, path info and bindings are thus
%% not available at this point.</dd>
%% <dt>onresponse</dt><dd>Optional fun that allows replacing a response
%% sent by the application.</dd>
-%% <dt>timeout</dt><dd>Time in milliseconds before an idle
-%% connection is closed. Defaults to 5000 milliseconds.</dd>
+%% <dt>timeout</dt><dd>Time in milliseconds a client has to send the
+%% full request line and headers. Defaults to 5000 milliseconds.</dd>
%% </dl>
%%
%% Note that there is no need to monitor these processes when using Cowboy as
%% an application as it already supervises them under the listener supervisor.
-%%
-%% @see cowboy_dispatcher
-%% @see cowboy_http_handler
-module(cowboy_protocol).
%% API.
@@ -52,20 +54,20 @@
%% Internal.
-export([init/4]).
-export([parse_request/3]).
--export([handler_loop/4]).
+-export([resume/6]).
-type onrequest_fun() :: fun((Req) -> Req).
-type onresponse_fun() ::
fun((cowboy_http:status(), cowboy_http:headers(), iodata(), Req) -> Req).
-
-export_type([onrequest_fun/0]).
-export_type([onresponse_fun/0]).
-record(state, {
- listener :: pid(),
socket :: inet:socket(),
transport :: module(),
- dispatch :: cowboy_dispatcher:dispatch_rules(),
+ middlewares :: [module()],
+ compress :: boolean(),
+ env :: cowboy_middleware:env(),
onrequest :: undefined | onrequest_fun(),
onresponse = undefined :: undefined | onresponse_fun(),
max_empty_lines :: non_neg_integer(),
@@ -76,9 +78,7 @@
max_header_value_length :: non_neg_integer(),
max_headers :: non_neg_integer(),
timeout :: timeout(),
- hibernate = false :: boolean(),
- loop_timeout = infinity :: timeout(),
- loop_timeout_ref :: undefined | reference()
+ until :: non_neg_integer() | infinity
}).
%% API.
@@ -102,24 +102,34 @@ get_value(Key, Opts, Default) ->
%% @private
-spec init(pid(), inet:socket(), module(), any()) -> ok.
init(ListenerPid, Socket, Transport, Opts) ->
- Dispatch = get_value(dispatch, Opts, []),
+ Compress = get_value(compress, Opts, false),
MaxEmptyLines = get_value(max_empty_lines, Opts, 5),
MaxHeaderNameLength = get_value(max_header_name_length, Opts, 64),
MaxHeaderValueLength = get_value(max_header_value_length, Opts, 4096),
MaxHeaders = get_value(max_headers, Opts, 100),
- MaxKeepalive = get_value(max_keepalive, Opts, infinity),
+ MaxKeepalive = get_value(max_keepalive, Opts, 100),
MaxRequestLineLength = get_value(max_request_line_length, Opts, 4096),
+ Middlewares = get_value(middlewares, Opts, [cowboy_router, cowboy_handler]),
+ Env = [{listener, ListenerPid}|get_value(env, Opts, [])],
OnRequest = get_value(onrequest, Opts, undefined),
OnResponse = get_value(onresponse, Opts, undefined),
Timeout = get_value(timeout, Opts, 5000),
ok = ranch:accept_ack(ListenerPid),
- wait_request(<<>>, #state{listener=ListenerPid, socket=Socket,
- transport=Transport, dispatch=Dispatch,
+ wait_request(<<>>, #state{socket=Socket, transport=Transport,
+ middlewares=Middlewares, compress=Compress, env=Env,
max_empty_lines=MaxEmptyLines, max_keepalive=MaxKeepalive,
max_request_line_length=MaxRequestLineLength,
max_header_name_length=MaxHeaderNameLength,
max_header_value_length=MaxHeaderValueLength, max_headers=MaxHeaders,
- timeout=Timeout, onrequest=OnRequest, onresponse=OnResponse}, 0).
+ onrequest=OnRequest, onresponse=OnResponse,
+ timeout=Timeout, until=until(Timeout)}, 0).
+
+-spec until(timeout()) -> non_neg_integer() | infinity.
+until(infinity) ->
+ infinity;
+until(Timeout) ->
+ {Me, S, Mi} = os:timestamp(),
+ Me * 1000000000 + S * 1000 + Mi div 1000 + Timeout.
%% Request parsing.
%%
@@ -128,10 +138,24 @@ init(ListenerPid, Socket, Transport, Opts) ->
%% right after the header parsing is finished and the code becomes
%% more interesting past that point.
+-spec recv(inet:socket(), module(), non_neg_integer() | infinity)
+ -> {ok, binary()} | {error, closed | timeout | atom()}.
+recv(Socket, Transport, infinity) ->
+ Transport:recv(Socket, 0, infinity);
+recv(Socket, Transport, Until) ->
+ {Me, S, Mi} = os:timestamp(),
+ Now = Me * 1000000000 + S * 1000 + Mi div 1000,
+ Timeout = Until - Now,
+ if Timeout < 0 ->
+ {error, timeout};
+ true ->
+ Transport:recv(Socket, 0, Timeout)
+ end.
+
-spec wait_request(binary(), #state{}, non_neg_integer()) -> ok.
wait_request(Buffer, State=#state{socket=Socket, transport=Transport,
- timeout=Timeout}, ReqEmpty) ->
- case Transport:recv(Socket, 0, Timeout) of
+ until=Until}, ReqEmpty) ->
+ case recv(Socket, Transport, Until) of
{ok, Data} ->
parse_request(<< Buffer/binary, Data/binary >>, State, ReqEmpty);
{error, _} ->
@@ -222,8 +246,8 @@ wait_header(_, State=#state{max_headers=MaxHeaders}, _, _, _, _, _, Headers)
when length(Headers) >= MaxHeaders ->
error_terminate(400, State);
wait_header(Buffer, State=#state{socket=Socket, transport=Transport,
- timeout=Timeout}, M, P, Q, F, V, H) ->
- case Transport:recv(Socket, 0, Timeout) of
+ until=Until}, M, P, Q, F, V, H) ->
+ case recv(Socket, Transport, Until) of
{ok, Data} ->
parse_header(<< Buffer/binary, Data/binary >>,
State, M, P, Q, F, V, H);
@@ -294,9 +318,9 @@ parse_hd_name_ws(<< C, Rest/bits >>, S, M, P, Q, F, V, H, Name) ->
end.
wait_hd_before_value(Buffer, State=#state{
- socket=Socket, transport=Transport, timeout=Timeout},
+ socket=Socket, transport=Transport, until=Until},
M, P, Q, F, V, H, N) ->
- case Transport:recv(Socket, 0, Timeout) of
+ case recv(Socket, Transport, Until) of
{ok, Data} ->
parse_hd_before_value(<< Buffer/binary, Data/binary >>,
State, M, P, Q, F, V, H, N);
@@ -326,9 +350,9 @@ parse_hd_before_value(Buffer, State=#state{
%% to change the other arguments' position and trigger costy
%% operations for no reasons.
wait_hd_value(_, State=#state{
- socket=Socket, transport=Transport, timeout=Timeout},
+ socket=Socket, transport=Transport, until=Until},
M, P, Q, F, V, H, N, SoFar) ->
- case Transport:recv(Socket, 0, Timeout) of
+ case recv(Socket, Transport, Until) of
{ok, Data} ->
parse_hd_value(Data, State, M, P, Q, F, V, H, N, SoFar);
{error, timeout} ->
@@ -341,9 +365,9 @@ wait_hd_value(_, State=#state{
%% to check for multilines allows us to avoid a few tests in
%% the critical path, but forces us to have a special function.
wait_hd_value_nl(_, State=#state{
- socket=Socket, transport=Transport, timeout=Timeout},
+ socket=Socket, transport=Transport, until=Until},
M, P, Q, F, V, Headers, Name, SoFar) ->
- case Transport:recv(Socket, 0, Timeout) of
+ case recv(Socket, Transport, Until) of
{ok, << C, Data/bits >>} when C =:= $\s; C =:= $\t ->
parse_hd_value(Data, State, M, P, Q, F, V, Headers, Name, SoFar);
{ok, Data} ->
@@ -437,197 +461,86 @@ parse_host(<< C, Rest/bits >>, Acc) ->
request(Buffer, State=#state{socket=Socket, transport=Transport,
req_keepalive=ReqKeepalive, max_keepalive=MaxKeepalive,
- onresponse=OnResponse},
+ compress=Compress, onresponse=OnResponse},
Method, Path, Query, Fragment, Version, Headers, Host, Port) ->
Req = cowboy_req:new(Socket, Transport, Method, Path, Query, Fragment,
Version, Headers, Host, Port, Buffer, ReqKeepalive < MaxKeepalive,
- OnResponse),
- onrequest(Req, State, Host).
+ Compress, OnResponse),
+ onrequest(Req, State).
%% Call the global onrequest callback. The callback can send a reply,
%% in which case we consider the request handled and move on to the next
%% one. Note that since we haven't dispatched yet, we don't know the
%% handler, host_info, path_info or bindings yet.
--spec onrequest(cowboy_req:req(), #state{}, binary()) -> ok.
-onrequest(Req, State=#state{onrequest=undefined}, Host) ->
- dispatch(Req, State, Host, cowboy_req:get(path, Req));
-onrequest(Req, State=#state{onrequest=OnRequest}, Host) ->
+-spec onrequest(cowboy_req:req(), #state{}) -> ok.
+onrequest(Req, State=#state{onrequest=undefined}) ->
+ execute(Req, State);
+onrequest(Req, State=#state{onrequest=OnRequest}) ->
Req2 = OnRequest(Req),
case cowboy_req:get(resp_state, Req2) of
- waiting -> dispatch(Req2, State, Host, cowboy_req:get(path, Req2));
+ waiting -> execute(Req2, State);
_ -> next_request(Req2, State, ok)
end.
--spec dispatch(cowboy_req:req(), #state{}, binary(), binary()) -> ok.
-dispatch(Req, State=#state{dispatch=Dispatch}, Host, Path) ->
- case cowboy_dispatcher:match(Dispatch, Host, Path) of
- {ok, Handler, Opts, Bindings, HostInfo, PathInfo} ->
- Req2 = cowboy_req:set_bindings(HostInfo, PathInfo, Bindings, Req),
- handler_init(Req2, State, Handler, Opts);
- {error, notfound, host} ->
- error_terminate(400, Req, State);
- {error, badrequest, path} ->
- error_terminate(400, Req, State);
- {error, notfound, path} ->
- error_terminate(404, Req, State)
- end.
+-spec execute(cowboy_req:req(), #state{}) -> ok.
+execute(Req, State=#state{middlewares=Middlewares, env=Env}) ->
+ execute(Req, State, Env, Middlewares).
--spec handler_init(cowboy_req:req(), #state{}, module(), any()) -> ok.
-handler_init(Req, State=#state{transport=Transport}, Handler, Opts) ->
- try Handler:init({Transport:name(), http}, Req, Opts) of
- {ok, Req2, HandlerState} ->
- handler_handle(Req2, State, Handler, HandlerState);
- {loop, Req2, HandlerState} ->
- handler_before_loop(Req2, State#state{hibernate=false},
- Handler, HandlerState);
- {loop, Req2, HandlerState, hibernate} ->
- handler_before_loop(Req2, State#state{hibernate=true},
- Handler, HandlerState);
- {loop, Req2, HandlerState, Timeout} ->
- handler_before_loop(Req2, State#state{loop_timeout=Timeout},
- Handler, HandlerState);
- {loop, Req2, HandlerState, Timeout, hibernate} ->
- handler_before_loop(Req2, State#state{
- hibernate=true, loop_timeout=Timeout}, Handler, HandlerState);
- {shutdown, Req2, HandlerState} ->
- handler_terminate(Req2, Handler, HandlerState);
- %% @todo {upgrade, transport, Module}
- {upgrade, protocol, Module} ->
- upgrade_protocol(Req, State, Handler, Opts, Module);
- {upgrade, protocol, Module, Req2, Opts2} ->
- upgrade_protocol(Req2, State, Handler, Opts2, Module)
- catch Class:Reason ->
- error_terminate(500, Req, State),
- error_logger:error_msg(
- "** Cowboy handler ~p terminating in ~p/~p~n"
- " for the reason ~p:~p~n"
- "** Options were ~p~n"
- "** Request was ~p~n"
- "** Stacktrace: ~p~n~n",
- [Handler, init, 3, Class, Reason, Opts,
- cowboy_req:to_list(Req), erlang:get_stacktrace()])
- end.
-
--spec upgrade_protocol(cowboy_req:req(), #state{}, module(), any(), module())
+-spec execute(cowboy_req:req(), #state{}, cowboy_middleware:env(), [module()])
-> ok.
-upgrade_protocol(Req, State=#state{listener=ListenerPid},
- Handler, Opts, Module) ->
- case Module:upgrade(ListenerPid, Handler, Opts, Req) of
- {UpgradeRes, Req2} -> next_request(Req2, State, UpgradeRes);
- _Any -> terminate(State)
+execute(Req, State, Env, []) ->
+ next_request(Req, State, get_value(result, Env, ok));
+execute(Req, State, Env, [Middleware|Tail]) ->
+ case Middleware:execute(Req, Env) of
+ {ok, Req2, Env2} ->
+ execute(Req2, State, Env2, Tail);
+ {suspend, Module, Function, Args} ->
+ erlang:hibernate(?MODULE, resume,
+ [State, Env, Tail, Module, Function, Args]);
+ {halt, Req2} ->
+ next_request(Req2, State, ok);
+ {error, Code, Req2} ->
+ error_terminate(Code, Req2, State)
end.
--spec handler_handle(cowboy_req:req(), #state{}, module(), any()) -> ok.
-handler_handle(Req, State, Handler, HandlerState) ->
- try Handler:handle(Req, HandlerState) of
- {ok, Req2, HandlerState2} ->
- terminate_request(Req2, State, Handler, HandlerState2)
- catch Class:Reason ->
- error_logger:error_msg(
- "** Cowboy handler ~p terminating in ~p/~p~n"
- " for the reason ~p:~p~n"
- "** Handler state was ~p~n"
- "** Request was ~p~n"
- "** Stacktrace: ~p~n~n",
- [Handler, handle, 2, Class, Reason, HandlerState,
- cowboy_req:to_list(Req), erlang:get_stacktrace()]),
- handler_terminate(Req, Handler, HandlerState),
- error_terminate(500, Req, State)
+-spec resume(#state{}, cowboy_middleware:env(), [module()],
+ module(), module(), [any()]) -> ok.
+resume(State, Env, Tail, Module, Function, Args) ->
+ case apply(Module, Function, Args) of
+ {ok, Req2, Env2} ->
+ execute(Req2, State, Env2, Tail);
+ {suspend, Module2, Function2, Args2} ->
+ erlang:hibernate(?MODULE, resume,
+ [State, Env, Tail, Module2, Function2, Args2]);
+ {halt, Req2} ->
+ next_request(Req2, State, ok);
+ {error, Code, Req2} ->
+ error_terminate(Code, Req2, State)
end.
-%% We don't listen for Transport closes because that would force us
-%% to receive data and buffer it indefinitely.
--spec handler_before_loop(cowboy_req:req(), #state{}, module(), any()) -> ok.
-handler_before_loop(Req, State=#state{hibernate=true}, Handler, HandlerState) ->
- State2 = handler_loop_timeout(State),
- catch erlang:hibernate(?MODULE, handler_loop,
- [Req, State2#state{hibernate=false}, Handler, HandlerState]),
- ok;
-handler_before_loop(Req, State, Handler, HandlerState) ->
- State2 = handler_loop_timeout(State),
- handler_loop(Req, State2, Handler, HandlerState).
-
-%% Almost the same code can be found in cowboy_websocket.
--spec handler_loop_timeout(#state{}) -> #state{}.
-handler_loop_timeout(State=#state{loop_timeout=infinity}) ->
- State#state{loop_timeout_ref=undefined};
-handler_loop_timeout(State=#state{loop_timeout=Timeout,
- loop_timeout_ref=PrevRef}) ->
- _ = case PrevRef of undefined -> ignore; PrevRef ->
- erlang:cancel_timer(PrevRef) end,
- TRef = erlang:start_timer(Timeout, self(), ?MODULE),
- State#state{loop_timeout_ref=TRef}.
-
-%% @private
--spec handler_loop(cowboy_req:req(), #state{}, module(), any()) -> ok.
-handler_loop(Req, State=#state{loop_timeout_ref=TRef}, Handler, HandlerState) ->
- receive
- {timeout, TRef, ?MODULE} ->
- terminate_request(Req, State, Handler, HandlerState);
- {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
- handler_loop(Req, State, Handler, HandlerState);
- Message ->
- handler_call(Req, State, Handler, HandlerState, Message)
- end.
-
--spec handler_call(cowboy_req:req(), #state{}, module(), any(), any()) -> ok.
-handler_call(Req, State, Handler, HandlerState, Message) ->
- try Handler:info(Message, Req, HandlerState) of
- {ok, Req2, HandlerState2} ->
- terminate_request(Req2, State, Handler, HandlerState2);
- {loop, Req2, HandlerState2} ->
- handler_before_loop(Req2, State, Handler, HandlerState2);
- {loop, Req2, HandlerState2, hibernate} ->
- handler_before_loop(Req2, State#state{hibernate=true},
- Handler, HandlerState2)
- catch Class:Reason ->
- error_logger:error_msg(
- "** Cowboy handler ~p terminating in ~p/~p~n"
- " for the reason ~p:~p~n"
- "** Handler state was ~p~n"
- "** Request was ~p~n"
- "** Stacktrace: ~p~n~n",
- [Handler, info, 3, Class, Reason, HandlerState,
- cowboy_req:to_list(Req), erlang:get_stacktrace()]),
- handler_terminate(Req, Handler, HandlerState),
- error_terminate(500, Req, State)
- end.
-
--spec handler_terminate(cowboy_req:req(), module(), any()) -> ok.
-handler_terminate(Req, Handler, HandlerState) ->
- try
- Handler:terminate(cowboy_req:lock(Req), HandlerState)
- catch Class:Reason ->
- error_logger:error_msg(
- "** Cowboy handler ~p terminating in ~p/~p~n"
- " for the reason ~p:~p~n"
- "** Handler state was ~p~n"
- "** Request was ~p~n"
- "** Stacktrace: ~p~n~n",
- [Handler, terminate, 2, Class, Reason, HandlerState,
- cowboy_req:to_list(Req), erlang:get_stacktrace()])
- end.
-
--spec terminate_request(cowboy_req:req(), #state{}, module(), any()) -> ok.
-terminate_request(Req, State, Handler, HandlerState) ->
- HandlerRes = handler_terminate(Req, Handler, HandlerState),
- next_request(Req, State, HandlerRes).
-
-spec next_request(cowboy_req:req(), #state{}, any()) -> ok.
-next_request(Req, State=#state{req_keepalive=Keepalive}, HandlerRes) ->
+next_request(Req, State=#state{req_keepalive=Keepalive, timeout=Timeout},
+ HandlerRes) ->
cowboy_req:ensure_response(Req, 204),
- {BodyRes, [Buffer, Connection]} = case cowboy_req:skip_body(Req) of
- {ok, Req2} -> {ok, cowboy_req:get([buffer, connection], Req2)};
- {error, _} -> {close, [<<>>, close]}
- end,
- %% Flush the resp_sent message before moving on.
- receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
- case {HandlerRes, BodyRes, Connection} of
- {ok, ok, keepalive} ->
- ?MODULE:parse_request(Buffer, State#state{
- req_keepalive=Keepalive + 1}, 0);
- _Closed ->
- terminate(State)
+ %% If we are going to close the connection,
+ %% we do not want to attempt to skip the body.
+ case cowboy_req:get(connection, Req) of
+ close ->
+ terminate(State);
+ _ ->
+ Buffer = case cowboy_req:skip_body(Req) of
+ {ok, Req2} -> cowboy_req:get(buffer, Req2);
+ _ -> close
+ end,
+ %% Flush the resp_sent message before moving on.
+ receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
+ if HandlerRes =:= ok, Buffer =/= close ->
+ ?MODULE:parse_request(Buffer,
+ State#state{req_keepalive=Keepalive + 1,
+ until=until(Timeout)}, 0);
+ true ->
+ terminate(State)
+ end
end.
%% Only send an error reply if there is no resp_sent message.
@@ -644,13 +557,13 @@ error_terminate(Code, Req, State) ->
%% Only send an error reply if there is no resp_sent message.
-spec error_terminate(cowboy_http:status(), #state{}) -> ok.
error_terminate(Code, State=#state{socket=Socket, transport=Transport,
- onresponse=OnResponse}) ->
+ compress=Compress, onresponse=OnResponse}) ->
receive
{cowboy_req, resp_sent} -> ok
after 0 ->
_ = cowboy_req:reply(Code, cowboy_req:new(Socket, Transport,
<<"GET">>, <<>>, <<>>, <<>>, {1, 1}, [], <<>>, undefined,
- <<>>, false, OnResponse)),
+ <<>>, false, Compress, OnResponse)),
ok
end,
terminate(State).
diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl
index 4a9e1a7..5cb7aa3 100644
--- a/src/cowboy_req.erl
+++ b/src/cowboy_req.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, Loïc Hoguin <[email protected]>
%% Copyright (c) 2011, Anthony Ramine <[email protected]>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
@@ -42,7 +42,7 @@
-module(cowboy_req).
%% Request API.
--export([new/13]).
+-export([new/14]).
-export([method/1]).
-export([version/1]).
-export([peer/1]).
@@ -89,6 +89,7 @@
-export([set_resp_cookie/4]).
-export([set_resp_header/3]).
-export([set_resp_body/2]).
+-export([set_resp_body_fun/2]).
-export([set_resp_body_fun/3]).
-export([has_resp_header/2]).
-export([has_resp_body/1]).
@@ -111,7 +112,6 @@
-export([compact/1]).
-export([lock/1]).
-export([to_list/1]).
--export([transport/1]).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
@@ -123,7 +123,7 @@
-type cookie_opts() :: [cookie_option()].
-export_type([cookie_opts/0]).
--type resp_body_fun() :: fun(() -> {sent, non_neg_integer()}).
+-type resp_body_fun() :: fun((inet:socket(), module()) -> ok).
-record(http_req, {
%% Transport.
@@ -137,14 +137,14 @@
version = {1, 1} :: cowboy_http:version(),
peer = undefined :: undefined | {inet:ip_address(), inet:port_number()},
host = undefined :: undefined | binary(),
- host_info = undefined :: undefined | cowboy_dispatcher:tokens(),
+ host_info = undefined :: undefined | cowboy_router:tokens(),
port = undefined :: undefined | inet:port_number(),
path = undefined :: binary(),
- path_info = undefined :: undefined | cowboy_dispatcher:tokens(),
+ path_info = undefined :: undefined | cowboy_router:tokens(),
qs = undefined :: binary(),
qs_vals = undefined :: undefined | list({binary(), binary() | true}),
fragment = undefined :: binary(),
- bindings = undefined :: undefined | cowboy_dispatcher:bindings(),
+ bindings = undefined :: undefined | cowboy_router:bindings(),
headers = [] :: cowboy_http:headers(),
p_headers = [] :: [any()], %% @todo Improve those specs.
cookies = undefined :: undefined | [{binary(), binary()}],
@@ -156,12 +156,15 @@
buffer = <<>> :: binary(),
%% Response.
+ resp_compress = false :: boolean(),
resp_state = waiting :: locked | waiting | chunks | done,
resp_headers = [] :: cowboy_http:headers(),
- resp_body = <<>> :: iodata() | {non_neg_integer(), resp_body_fun()},
+ resp_body = <<>> :: iodata() | resp_body_fun()
+ | {non_neg_integer(), resp_body_fun()},
%% Functions.
- onresponse = undefined :: undefined | cowboy_protocol:onresponse_fun()
+ onresponse = undefined :: undefined | already_called
+ | cowboy_protocol:onresponse_fun()
}).
-opaque req() :: #http_req{}.
@@ -178,16 +181,16 @@
%% in an optimized way and add the parsed value to p_headers' cache.
-spec new(inet:socket(), module(), binary(), binary(), binary(), binary(),
cowboy_http:version(), cowboy_http:headers(), binary(),
- inet:port_number() | undefined, binary(), boolean(),
+ inet:port_number() | undefined, binary(), boolean(), boolean(),
undefined | cowboy_protocol:onresponse_fun())
-> req().
new(Socket, Transport, Method, Path, Query, Fragment,
Version, Headers, Host, Port, Buffer, CanKeepalive,
- OnResponse) ->
+ Compress, OnResponse) ->
Req = #http_req{socket=Socket, transport=Transport, pid=self(),
method=Method, path=Path, qs=Query, fragment=Fragment, version=Version,
headers=Headers, host=Host, port=Port, buffer=Buffer,
- onresponse=OnResponse},
+ resp_compress=Compress, onresponse=OnResponse},
case CanKeepalive and (Version =:= {1, 1}) of
false ->
Req#http_req{connection=close};
@@ -253,7 +256,7 @@ host(Req) ->
%% @doc Return the extra host information obtained from partially matching
%% the hostname using <em>'...'</em>.
-spec host_info(Req)
- -> {cowboy_dispatcher:tokens() | undefined, Req} when Req::req().
+ -> {cowboy_router:tokens() | undefined, Req} when Req::req().
host_info(Req) ->
{Req#http_req.host_info, Req}.
@@ -270,7 +273,7 @@ path(Req) ->
%% @doc Return the extra path information obtained from partially matching
%% the patch using <em>'...'</em>.
-spec path_info(Req)
- -> {cowboy_dispatcher:tokens() | undefined, Req} when Req::req().
+ -> {cowboy_router:tokens() | undefined, Req} when Req::req().
path_info(Req) ->
{Req#http_req.path_info, Req}.
@@ -438,6 +441,11 @@ parse_header(Name, Req, Default) when Name =:= <<"accept-language">> ->
fun (Value) ->
cowboy_http:nonempty_list(Value, fun cowboy_http:language_range/2)
end);
+parse_header(Name, Req, Default) when Name =:= <<"authorization">> ->
+ parse_header(Name, Req, Default,
+ fun (Value) ->
+ cowboy_http:token_ci(Value, fun cowboy_http:authorization/2)
+ end);
parse_header(Name, Req, Default) when Name =:= <<"content-length">> ->
parse_header(Name, Req, Default, fun cowboy_http:digits/1);
parse_header(Name, Req, Default) when Name =:= <<"content-type">> ->
@@ -456,6 +464,11 @@ parse_header(Name, Req, Default)
when Name =:= <<"if-modified-since">>;
Name =:= <<"if-unmodified-since">> ->
parse_header(Name, Req, Default, fun cowboy_http:http_date/1);
+parse_header(Name, Req, Default) when Name =:= <<"sec-websocket-protocol">> ->
+ parse_header(Name, Req, Default,
+ fun (Value) ->
+ cowboy_http:nonempty_list(Value, fun cowboy_http:token/2)
+ end);
%% @todo Extension parameters.
parse_header(Name, Req, Default) when Name =:= <<"transfer-encoding">> ->
parse_header(Name, Req, Default,
@@ -548,11 +561,10 @@ set_meta(Name, Value, Req=#http_req{meta=Meta}) ->
%% Request Body API.
%% @doc Return whether the request message has a body.
--spec has_body(Req) -> {boolean(), Req} when Req::req().
+-spec has_body(cowboy_req:req()) -> boolean().
has_body(Req) ->
- Has = lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse
- lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers),
- {Has, Req}.
+ lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse
+ lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers).
%% @doc Return the request message body length, if known.
%%
@@ -632,17 +644,18 @@ stream_body(Req=#http_req{buffer=Buffer, body_state={stream, _, _, _}})
when Buffer =/= <<>> ->
transfer_decode(Buffer, Req#http_req{buffer= <<>>});
stream_body(Req=#http_req{body_state={stream, _, _, _}}) ->
- stream_body_recv(Req);
+ stream_body_recv(0, Req);
stream_body(Req=#http_req{body_state=done}) ->
{done, Req}.
--spec stream_body_recv(Req)
+-spec stream_body_recv(non_neg_integer(), Req)
-> {ok, binary(), Req} | {error, atom()} when Req::req().
-stream_body_recv(Req=#http_req{
+stream_body_recv(Length, Req=#http_req{
transport=Transport, socket=Socket, buffer=Buffer}) ->
%% @todo Allow configuring the timeout.
- case Transport:recv(Socket, 0, 5000) of
- {ok, Data} -> transfer_decode(<< Buffer/binary, Data/binary >>, Req);
+ case Transport:recv(Socket, Length, 5000) of
+ {ok, Data} -> transfer_decode(<< Buffer/binary, Data/binary >>,
+ Req#http_req{buffer= <<>>});
{error, Reason} -> {error, Reason}
end.
@@ -660,7 +673,10 @@ transfer_decode(Data, Req=#http_req{
{stream, TransferDecode, TransferState2, ContentDecode}});
%% @todo {header(s) for chunked
more ->
- stream_body_recv(Req#http_req{buffer=Data});
+ stream_body_recv(0, Req#http_req{buffer=Data});
+ {more, Length, Rest, TransferState2} ->
+ stream_body_recv(Length, Req#http_req{buffer=Rest, body_state=
+ {stream, TransferDecode, TransferState2, ContentDecode}});
{done, Length, Rest} ->
Req2 = transfer_decode_done(Length, Rest, Req),
{done, Req2};
@@ -721,7 +737,6 @@ skip_body(Req) ->
%% @doc Return the full body sent with the request, parsed as an
%% application/x-www-form-urlencoded string. Essentially a POST query string.
-%% @todo We need an option to limit the size of the body for QS too.
-spec body_qs(Req)
-> {ok, [{binary(), binary() | true}], Req} | {error, atom()}
when Req::req().
@@ -758,7 +773,6 @@ multipart_data(Req=#http_req{multipart={Length, Cont}}) ->
multipart_data(Req=#http_req{body_state=done}) ->
{eof, Req}.
-%% @todo Typespecs.
multipart_data(Req, Length, {headers, Headers, Cont}) ->
{headers, Headers, Req#http_req{multipart={Length, Cont}}};
multipart_data(Req, Length, {body, Data, Cont}) ->
@@ -822,20 +836,33 @@ set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) ->
set_resp_body(Body, Req) ->
Req#http_req{resp_body=Body}.
+%% @doc Add a body stream function to the response.
+%%
+%% The body set here is ignored if the response is later sent using
+%% anything other than reply/2 or reply/3.
+%%
+%% Setting a response stream function without a length means that the
+%% body will be sent until the connection is closed. Cowboy will make
+%% sure that the connection is closed with no extra step required.
+%%
+%% To inform the client that a body has been sent with this request,
+%% Cowboy will add a "Transfer-Encoding: identity" header to the
+%% response.
+-spec set_resp_body_fun(resp_body_fun(), Req) -> Req when Req::req().
+set_resp_body_fun(StreamFun, Req) ->
+ Req#http_req{resp_body=StreamFun}.
+
%% @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.
+%% The body set here is ignored if the response is later sent using
+%% anything other than reply/2 or reply/3.
%%
-%% 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'.
+%% Cowboy will call the given response stream function after sending the
+%% headers. This function must send the specified number of bytes to the
+%% socket it will receive as argument.
%%
-%% @see cowboy_req:transport/1.
+%% If the body function crashes while writing the response body or writes
+%% fewer bytes than declared the behaviour is undefined.
-spec set_resp_body_fun(non_neg_integer(), resp_body_fun(), Req)
-> Req when Req::req().
set_resp_body_fun(StreamLen, StreamFun, Req) ->
@@ -848,6 +875,8 @@ has_resp_header(Name, #http_req{resp_headers=RespHeaders}) ->
%% @doc Return whether a body has been set for the response.
-spec has_resp_body(req()) -> boolean().
+has_resp_body(#http_req{resp_body=RespBody}) when is_function(RespBody) ->
+ true;
has_resp_body(#http_req{resp_body={Length, _}}) ->
Length > 0;
has_resp_body(#http_req{resp_body=RespBody}) ->
@@ -876,35 +905,93 @@ reply(Status, Headers, Req=#http_req{resp_body=Body}) ->
iodata() | {non_neg_integer() | resp_body_fun()}, Req)
-> {ok, Req} when Req::req().
reply(Status, Headers, Body, Req=#http_req{
+ socket=Socket, transport=Transport,
version=Version, connection=Connection,
- method=Method, resp_state=waiting, resp_headers=RespHeaders}) ->
+ method=Method, resp_compress=Compress,
+ resp_state=waiting, resp_headers=RespHeaders}) ->
RespConn = response_connection(Headers, Connection),
HTTP11Headers = case Version of
{1, 1} -> [{<<"connection">>, atom_to_connection(Connection)}];
_ -> []
end,
case Body of
+ BodyFun when is_function(BodyFun) ->
+ %% We stream the response body until we close the connection.
+ {RespType, Req2} = response(Status, Headers, RespHeaders, [
+ {<<"connection">>, <<"close">>},
+ {<<"date">>, cowboy_clock:rfc1123()},
+ {<<"server">>, <<"Cowboy">>},
+ {<<"transfer-encoding">>, <<"identity">>}
+ ], <<>>, Req#http_req{connection=close}),
+ if RespType =/= hook, Method =/= <<"HEAD">> ->
+ BodyFun(Socket, Transport);
+ true -> ok
+ end;
{ContentLength, BodyFun} ->
+ %% We stream the response body for ContentLength bytes.
{RespType, Req2} = response(Status, Headers, RespHeaders, [
{<<"content-length">>, integer_to_list(ContentLength)},
{<<"date">>, cowboy_clock:rfc1123()},
{<<"server">>, <<"Cowboy">>}
|HTTP11Headers], <<>>, Req),
- if RespType =/= hook, Method =/= <<"HEAD">> -> BodyFun();
+ if RespType =/= hook, Method =/= <<"HEAD">> ->
+ BodyFun(Socket, Transport);
true -> ok
end;
+ _ when Compress ->
+ Req2 = reply_may_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method);
_ ->
- {_, Req2} = response(Status, Headers, RespHeaders, [
- {<<"content-length">>, integer_to_list(iolist_size(Body))},
- {<<"date">>, cowboy_clock:rfc1123()},
- {<<"server">>, <<"Cowboy">>}
- |HTTP11Headers],
- case Method of <<"HEAD">> -> <<>>; _ -> Body end,
- Req)
+ Req2 = reply_no_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method, iolist_size(Body))
end,
{ok, Req2#http_req{connection=RespConn, resp_state=done,
resp_headers=[], resp_body= <<>>}}.
+reply_may_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method) ->
+ BodySize = iolist_size(Body),
+ {ok, Encodings, Req2}
+ = cowboy_req:parse_header(<<"accept-encoding">>, Req),
+ CanGzip = (BodySize > 300)
+ andalso (false =:= lists:keyfind(<<"content-encoding">>,
+ 1, Headers))
+ andalso (false =:= lists:keyfind(<<"content-encoding">>,
+ 1, RespHeaders))
+ andalso (false =:= lists:keyfind(<<"transfer-encoding">>,
+ 1, Headers))
+ andalso (false =:= lists:keyfind(<<"transfer-encoding">>,
+ 1, RespHeaders))
+ andalso (Encodings =/= undefined)
+ andalso (false =/= lists:keyfind(<<"gzip">>, 1, Encodings)),
+ case CanGzip of
+ true ->
+ GzBody = zlib:gzip(Body),
+ {_, Req3} = response(Status, Headers, RespHeaders, [
+ {<<"content-length">>, integer_to_list(byte_size(GzBody))},
+ {<<"content-encoding">>, <<"gzip">>},
+ {<<"date">>, cowboy_clock:rfc1123()},
+ {<<"server">>, <<"Cowboy">>}
+ |HTTP11Headers],
+ case Method of <<"HEAD">> -> <<>>; _ -> GzBody end,
+ Req2),
+ Req3;
+ false ->
+ reply_no_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method, BodySize)
+ end.
+
+reply_no_compress(Status, Headers, Body, Req,
+ RespHeaders, HTTP11Headers, Method, BodySize) ->
+ {_, Req2} = response(Status, Headers, RespHeaders, [
+ {<<"content-length">>, integer_to_list(BodySize)},
+ {<<"date">>, cowboy_clock:rfc1123()},
+ {<<"server">>, <<"Cowboy">>}
+ |HTTP11Headers],
+ case Method of <<"HEAD">> -> <<>>; _ -> Body end,
+ Req),
+ Req2.
+
%% @equiv chunked_reply(Status, [], Req)
-spec chunked_reply(cowboy_http:status(), Req) -> {ok, Req} when Req::req().
chunked_reply(Status, Req) ->
@@ -1044,8 +1131,8 @@ set([{transport, Val}|Tail], Req) -> set(Tail, Req#http_req{transport=Val});
set([{version, Val}|Tail], Req) -> set(Tail, Req#http_req{version=Val}).
%% @private
--spec set_bindings(cowboy_dispatcher:tokens(), cowboy_dispatcher:tokens(),
- cowboy_dispatcher:bindings(), Req) -> Req when Req::req().
+-spec set_bindings(cowboy_router:tokens(), cowboy_router:tokens(),
+ cowboy_router:bindings(), Req) -> Req when Req::req().
set_bindings(HostInfo, PathInfo, Bindings, Req) ->
Req#http_req{host_info=HostInfo, path_info=PathInfo,
bindings=Bindings}.
@@ -1077,18 +1164,6 @@ lock(Req) ->
to_list(Req) ->
lists:zip(record_info(fields, http_req), tl(tuple_to_list(Req))).
-%% @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(req()) -> {ok, module(), inet:socket()}.
-transport(#http_req{transport=Transport, socket=Socket}) ->
- {ok, Transport, Socket}.
-
%% Internal.
-spec response(cowboy_http:status(), cowboy_http:headers(),
@@ -1097,13 +1172,17 @@ transport(#http_req{transport=Transport, socket=Socket}) ->
response(Status, Headers, RespHeaders, DefaultHeaders, Body, Req=#http_req{
socket=Socket, transport=Transport, version=Version,
pid=ReqPid, onresponse=OnResponse}) ->
- FullHeaders = response_merge_headers(Headers, RespHeaders, DefaultHeaders),
+ FullHeaders = case OnResponse of
+ already_called -> Headers;
+ _ -> response_merge_headers(Headers, RespHeaders, DefaultHeaders)
+ end,
Req2 = case OnResponse of
+ already_called -> Req;
undefined -> Req;
OnResponse -> OnResponse(Status, FullHeaders, Body,
%% Don't call 'onresponse' from the hook itself.
Req#http_req{resp_headers=[], resp_body= <<>>,
- onresponse=undefined})
+ onresponse=already_called})
end,
ReplyType = case Req2#http_req.resp_state of
waiting ->
diff --git a/src/cowboy_rest.erl b/src/cowboy_rest.erl
index 06655a4..a49d622 100644
--- a/src/cowboy_rest.erl
+++ b/src/cowboy_rest.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
@@ -23,6 +23,7 @@
-export([upgrade/4]).
-record(state, {
+ env :: cowboy_middleware:env(),
method = undefined :: binary(),
%% Handler.
@@ -54,31 +55,31 @@
%% 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(), Req)
- -> {ok, Req} | close when Req::cowboy_req:req().
-upgrade(_ListenerPid, Handler, Opts, Req) ->
+-spec upgrade(Req, Env, module(), any())
+ -> {ok, Req, Env} | {error, 500, Req}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+upgrade(Req, Env, Handler, HandlerOpts) ->
try
Method = cowboy_req:get(method, Req),
case erlang:function_exported(Handler, rest_init, 2) of
true ->
- case Handler:rest_init(Req, Opts) of
+ case Handler:rest_init(Req, HandlerOpts) of
{ok, Req2, HandlerState} ->
- service_available(Req2, #state{method=Method,
+ service_available(Req2, #state{env=Env, method=Method,
handler=Handler, handler_state=HandlerState})
end;
false ->
- service_available(Req, #state{method=Method,
+ service_available(Req, #state{env=Env, method=Method,
handler=Handler})
end
catch Class:Reason ->
- PLReq = cowboy_req:to_list(Req),
error_logger:error_msg(
"** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n** Options were ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, rest_init, 2, Class, Reason, Opts, PLReq, erlang:get_stacktrace()]),
- {ok, _Req2} = cowboy_req:reply(500, Req),
- close
+ [Handler, rest_init, 2, Class, Reason, HandlerOpts,
+ cowboy_req:to_list(Req), erlang:get_stacktrace()]),
+ {error, 500, Req}
end.
service_available(Req, State) ->
@@ -90,7 +91,8 @@ known_methods(Req, State=#state{method=Method}) ->
no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">>;
Method =:= <<"POST">>; Method =:= <<"PUT">>;
Method =:= <<"DELETE">>; Method =:= <<"TRACE">>;
- Method =:= <<"CONNECT">>; Method =:= <<"OPTIONS">> ->
+ Method =:= <<"CONNECT">>; Method =:= <<"OPTIONS">>;
+ Method =:= <<"PATCH">> ->
next(Req, State, fun uri_too_long/2);
no_call ->
next(Req, State, 501);
@@ -643,6 +645,8 @@ method(Req, State=#state{method= <<"POST">>}) ->
post_is_create(Req, State);
method(Req, State=#state{method= <<"PUT">>}) ->
is_conflict(Req, State);
+method(Req, State=#state{method= <<"PATCH">>}) ->
+ patch_resource(Req, State);
method(Req, State=#state{method=Method})
when Method =:= <<"GET">>; Method =:= <<"HEAD">> ->
set_resp_body(Req, State);
@@ -666,6 +670,8 @@ post_is_create(Req, State) ->
%% (including the leading /).
create_path(Req, State) ->
case call(Req, State, create_path) of
+ no_call ->
+ put_resource(Req, State, fun created_path/2);
{halt, Req2, HandlerState} ->
terminate(Req2, State#state{handler_state=HandlerState});
{Path, Req2, HandlerState} ->
@@ -677,6 +683,23 @@ create_path(Req, State) ->
State2, 303)
end.
+%% Called after content_types_accepted is called for POST methods
+%% when create_path did not exist. Expects the full path to
+%% be returned and MUST exist in the case that create_path
+%% does not.
+created_path(Req, State) ->
+ case call(Req, State, created_path) of
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {Path, Req2, HandlerState} ->
+ {HostURL, Req3} = cowboy_req:host_url(Req2),
+ State2 = State#state{handler_state=HandlerState},
+ Req4 = cowboy_req:set_resp_header(
+ <<"Location">>, << HostURL/binary, Path/binary >>, Req3),
+ respond(cowboy_req:set_meta(put_path, Path, Req4),
+ State2, 303)
+ end.
+
%% 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) ->
@@ -707,6 +730,9 @@ put_resource(Req, State) ->
%% 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_req:meta(put_path, Req)
+%%
+%%content_types_accepted SHOULD return a different list
+%% for each HTTP method.
put_resource(Req, State, OnTrue) ->
case call(Req, State, content_types_accepted) of
no_call ->
@@ -721,6 +747,27 @@ put_resource(Req, State, OnTrue) ->
choose_content_type(Req3, State2, OnTrue, ContentType, CTA2)
end.
+%% 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.
+%%
+%% content_types_accepted SHOULD return a different list
+%% for each HTTP method.
+patch_resource(Req, State) ->
+ case call(Req, State, content_types_accepted) of
+ no_call ->
+ respond(Req, State, 415);
+ {halt, Req2, HandlerState} ->
+ terminate(Req2, State#state{handler_state=HandlerState});
+ {CTM, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ {ok, ContentType, Req3}
+ = cowboy_req:parse_header(<<"content-type">>, Req2),
+ choose_content_type(Req3, State2, 204, ContentType, CTM)
+ end.
+
%% The special content type '*' will always match. It can be used as a
%% catch-all content type for accepting any kind of request content.
%% Note that because it will always match, it should be the last of the
@@ -738,15 +785,14 @@ choose_content_type(Req,
"function ~p/~p was not exported~n"
"** Request was ~p~n** State was ~p~n~n",
[Handler, Fun, 2, cowboy_req:to_list(Req), HandlerState]),
- {ok, _} = cowboy_req:reply(500, Req),
- close;
- {halt, Req2, HandlerState} ->
- terminate(Req2, State#state{handler_state=HandlerState});
- {true, Req2, HandlerState} ->
- State2 = State#state{handler_state=HandlerState},
+ {error, 500, Req};
+ {halt, Req2, HandlerState2} ->
+ terminate(Req2, State#state{handler_state=HandlerState2});
+ {true, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
next(Req2, State2, OnTrue);
- {false, Req2, HandlerState} ->
- State2 = State#state{handler_state=HandlerState},
+ {false, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
respond(Req2, State2, 422)
end;
choose_content_type(Req, State, OnTrue, ContentType, [_Any|Tail]) ->
@@ -790,15 +836,16 @@ set_resp_body(Req, State=#state{handler=Handler, handler_state=HandlerState,
"function ~p/~p was not exported~n"
"** Request was ~p~n** State was ~p~n~n",
[Handler, Fun, 2, cowboy_req:to_list(Req5), HandlerState]),
- {ok, _} = cowboy_req:reply(500, Req5),
- close;
- {halt, Req6, HandlerState} ->
- terminate(Req6, State4#state{handler_state=HandlerState});
- {Body, Req6, HandlerState} ->
- State5 = State4#state{handler_state=HandlerState},
+ {error, 500, Req5};
+ {halt, Req6, HandlerState2} ->
+ terminate(Req6, State4#state{handler_state=HandlerState2});
+ {Body, Req6, HandlerState2} ->
+ State5 = State4#state{handler_state=HandlerState2},
Req7 = case Body of
- {stream, Len, Fun1} ->
- cowboy_req:set_resp_body_fun(Len, Fun1, Req6);
+ {stream, StreamFun} ->
+ cowboy_req:set_resp_body_fun(StreamFun, Req6);
+ {stream, Len, StreamFun} ->
+ cowboy_req:set_resp_body_fun(Len, StreamFun, Req6);
_Contents ->
cowboy_req:set_resp_body(Body, Req6)
end,
@@ -845,12 +892,6 @@ generate_etag(Req, State=#state{etag=undefined}) ->
case call(Req, State, generate_etag) of
no_call ->
{undefined, Req, State#state{etag=no_call}};
- %% Previously the return value from the generate_etag/2 callback was set
- %% as the value of the ETag header in the response. Therefore the only
- %% valid return type was `binary()'. If a handler returns a `binary()'
- %% it must be mapped to the expected type or it'll always fail to
- %% compare equal to any entity tags present in the request headers.
- %% @todo Remove support for binary return values after 0.6.
{Etag, Req2, HandlerState} when is_binary(Etag) ->
[Etag2] = cowboy_http:entity_tag_match(Etag),
{Etag2, Req2, State#state{handler_state=HandlerState, etag=Etag2}};
@@ -915,10 +956,11 @@ respond(Req, State, StatusCode) ->
{ok, Req2} = cowboy_req:reply(StatusCode, Req),
terminate(Req2, State).
-terminate(Req, #state{handler=Handler, handler_state=HandlerState}) ->
+terminate(Req, #state{env=Env, handler=Handler,
+ handler_state=HandlerState}) ->
case erlang:function_exported(Handler, rest_terminate, 2) of
true -> ok = Handler:rest_terminate(
cowboy_req:lock(Req), HandlerState);
false -> ok
end,
- {ok, Req}.
+ {ok, Req, [{result, ok}|Env]}.
diff --git a/src/cowboy_router.erl b/src/cowboy_router.erl
new file mode 100644
index 0000000..a4597ed
--- /dev/null
+++ b/src/cowboy_router.erl
@@ -0,0 +1,565 @@
+%% Copyright (c) 2011-2013, 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 Routing middleware.
+%%
+%% Resolve the handler to be used for the request based on the
+%% routing information found in the <em>dispatch</em> environment value.
+%% When found, the handler module and associated data are added to
+%% the environment as the <em>handler</em> and <em>handler_opts</em> values
+%% respectively.
+%%
+%% If the route cannot be found, processing stops with either
+%% a 400 or a 404 reply.
+-module(cowboy_router).
+-behaviour(cowboy_middleware).
+
+-export([compile/1]).
+-export([execute/2]).
+
+-type bindings() :: [{atom(), binary()}].
+-type tokens() :: [binary()].
+-export_type([bindings/0]).
+-export_type([tokens/0]).
+
+-type constraints() :: [{atom(), int}
+ | {atom(), function, fun ((binary()) -> true | {true, any()} | false)}].
+-export_type([constraints/0]).
+
+-type route_match() :: binary() | string().
+-type route_path() :: {Path::route_match(), Handler::module(), Opts::any()}
+ | {Path::route_match(), constraints(), Handler::module(), Opts::any()}.
+-type route_rule() :: {Host::route_match(), Paths::[route_path()]}
+ | {Host::route_match(), constraints(), Paths::[route_path()]}.
+-opaque routes() :: [route_rule()].
+-export_type([routes/0]).
+
+-type dispatch_match() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()].
+-type dispatch_path() :: {dispatch_match(), module(), any()}.
+-type dispatch_rule() :: {Host::dispatch_match(), Paths::[dispatch_path()]}.
+-opaque dispatch_rules() :: [dispatch_rule()].
+-export_type([dispatch_rules/0]).
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-endif.
+
+%% @doc Compile a list of routes into the dispatch format used
+%% by Cowboy's routing.
+-spec compile(routes()) -> dispatch_rules().
+compile(Routes) ->
+ compile(Routes, []).
+
+compile([], Acc) ->
+ lists:reverse(Acc);
+compile([{Host, Paths}|Tail], Acc) ->
+ compile([{Host, [], Paths}|Tail], Acc);
+compile([{HostMatch, Constraints, Paths}|Tail], Acc) ->
+ HostRules = case HostMatch of
+ '_' -> '_';
+ _ -> compile_host(HostMatch)
+ end,
+ PathRules = compile_paths(Paths, []),
+ Hosts = case HostRules of
+ '_' -> [{'_', Constraints, PathRules}];
+ _ -> [{R, Constraints, PathRules} || R <- HostRules]
+ end,
+ compile(Tail, Hosts ++ Acc).
+
+compile_host(HostMatch) when is_list(HostMatch) ->
+ compile_host(unicode:characters_to_binary(HostMatch));
+compile_host(HostMatch) when is_binary(HostMatch) ->
+ compile_rules(HostMatch, $., [], [], <<>>).
+
+compile_paths([], Acc) ->
+ lists:reverse(Acc);
+compile_paths([{PathMatch, Handler, Opts}|Tail], Acc) ->
+ compile_paths([{PathMatch, [], Handler, Opts}|Tail], Acc);
+compile_paths([{PathMatch, Constraints, Handler, Opts}|Tail], Acc)
+ when is_list(PathMatch) ->
+ compile_paths([{unicode:characters_to_binary(PathMatch),
+ Constraints, Handler, Opts}|Tail], Acc);
+compile_paths([{'_', Constraints, Handler, Opts}|Tail], Acc) ->
+ compile_paths(Tail, [{'_', Constraints, Handler, Opts}] ++ Acc);
+compile_paths([{<< $/, PathMatch/binary >>, Constraints, Handler, Opts}|Tail],
+ Acc) ->
+ PathRules = compile_rules(PathMatch, $/, [], [], <<>>),
+ Paths = [{lists:reverse(R), Constraints, Handler, Opts} || R <- PathRules],
+ compile_paths(Tail, Paths ++ Acc).
+
+compile_rules(<<>>, _, Segments, Rules, <<>>) ->
+ [Segments|Rules];
+compile_rules(<<>>, _, Segments, Rules, Acc) ->
+ [[Acc|Segments]|Rules];
+compile_rules(<< S, Rest/binary >>, S, Segments, Rules, <<>>) ->
+ compile_rules(Rest, S, Segments, Rules, <<>>);
+compile_rules(<< S, Rest/binary >>, S, Segments, Rules, Acc) ->
+ compile_rules(Rest, S, [Acc|Segments], Rules, <<>>);
+compile_rules(<< $:, Rest/binary >>, S, Segments, Rules, <<>>) ->
+ {NameBin, Rest2} = compile_binding(Rest, S, <<>>),
+ Name = binary_to_atom(NameBin, utf8),
+ compile_rules(Rest2, S, Segments, Rules, Name);
+compile_rules(<< $:, _/binary >>, _, _, _, _) ->
+ erlang:error(badarg);
+compile_rules(<< $[, $., $., $., $], Rest/binary >>, S, Segments, Rules, Acc)
+ when Acc =:= <<>> ->
+ compile_rules(Rest, S, ['...'|Segments], Rules, Acc);
+compile_rules(<< $[, $., $., $., $], Rest/binary >>, S, Segments, Rules, Acc) ->
+ compile_rules(Rest, S, ['...', Acc|Segments], Rules, Acc);
+compile_rules(<< $[, S, Rest/binary >>, S, Segments, Rules, Acc) ->
+ compile_brackets(Rest, S, [Acc|Segments], Rules);
+compile_rules(<< $[, Rest/binary >>, S, Segments, Rules, <<>>) ->
+ compile_brackets(Rest, S, Segments, Rules);
+%% Open bracket in the middle of a segment.
+compile_rules(<< $[, _/binary >>, _, _, _, _) ->
+ erlang:error(badarg);
+%% Missing an open bracket.
+compile_rules(<< $], _/binary >>, _, _, _, _) ->
+ erlang:error(badarg);
+compile_rules(<< C, Rest/binary >>, S, Segments, Rules, Acc) ->
+ compile_rules(Rest, S, Segments, Rules, << Acc/binary, C >>).
+
+%% Everything past $: until $. or $[ or $] or end of binary
+%% is the binding name.
+compile_binding(<<>>, _, <<>>) ->
+ erlang:error(badarg);
+compile_binding(Rest = <<>>, _, Acc) ->
+ {Acc, Rest};
+compile_binding(Rest = << C, _/binary >>, S, Acc)
+ when C =:= S; C =:= $[; C =:= $] ->
+ {Acc, Rest};
+compile_binding(<< C, Rest/binary >>, S, Acc) ->
+ compile_binding(Rest, S, << Acc/binary, C >>).
+
+compile_brackets(Rest, S, Segments, Rules) ->
+ {Bracket, Rest2} = compile_brackets_split(Rest, <<>>, 0),
+ Rules1 = compile_rules(Rest2, S, Segments, [], <<>>),
+ Rules2 = compile_rules(<< Bracket/binary, Rest2/binary >>,
+ S, Segments, [], <<>>),
+ Rules ++ Rules2 ++ Rules1.
+
+%% Missing a close bracket.
+compile_brackets_split(<<>>, _, _) ->
+ erlang:error(badarg);
+%% Make sure we don't confuse the closing bracket we're looking for.
+compile_brackets_split(<< C, Rest/binary >>, Acc, N) when C =:= $[ ->
+ compile_brackets_split(Rest, << Acc/binary, C >>, N + 1);
+compile_brackets_split(<< C, Rest/binary >>, Acc, N) when C =:= $], N > 0 ->
+ compile_brackets_split(Rest, << Acc/binary, C >>, N - 1);
+%% That's the right one.
+compile_brackets_split(<< $], Rest/binary >>, Acc, 0) ->
+ {Acc, Rest};
+compile_brackets_split(<< C, Rest/binary >>, Acc, N) ->
+ compile_brackets_split(Rest, << Acc/binary, C >>, N).
+
+%% @private
+-spec execute(Req, Env)
+ -> {ok, Req, Env} | {error, 400 | 404, Req}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+execute(Req, Env) ->
+ {_, Dispatch} = lists:keyfind(dispatch, 1, Env),
+ [Host, Path] = cowboy_req:get([host, path], Req),
+ case match(Dispatch, Host, Path) of
+ {ok, Handler, HandlerOpts, Bindings, HostInfo, PathInfo} ->
+ Req2 = cowboy_req:set_bindings(HostInfo, PathInfo, Bindings, Req),
+ {ok, Req2, [{handler, Handler}, {handler_opts, HandlerOpts}|Env]};
+ {error, notfound, host} ->
+ {error, 400, Req};
+ {error, badrequest, path} ->
+ {error, 400, Req};
+ {error, notfound, path} ->
+ {error, 404, Req}
+ end.
+
+%% Internal.
+
+%% @doc Match hostname tokens and path tokens against dispatch rules.
+%%
+%% It is typically used for matching tokens for the hostname and path of
+%% the request against a global dispatch rule for your listener.
+%%
+%% Dispatch rules are a list of <em>{Hostname, PathRules}</em> tuples, with
+%% <em>PathRules</em> being a list of <em>{Path, HandlerMod, HandlerOpts}</em>.
+%%
+%% <em>Hostname</em> and <em>Path</em> are match rules and can be either the
+%% atom <em>'_'</em>, which matches everything, `<<"*">>', which match the
+%% wildcard path, or a list of tokens.
+%%
+%% Each token can be either a binary, the atom <em>'_'</em>,
+%% the atom '...' or a named atom. A binary token must match exactly,
+%% <em>'_'</em> matches everything for a single token, <em>'...'</em> matches
+%% everything for the rest of the tokens and a named atom will bind the
+%% corresponding token value and return it.
+%%
+%% The list of hostname tokens is reversed before matching. For example, if
+%% we were to match "www.ninenines.eu", we would first match "eu", then
+%% "ninenines", then "www". This means that in the context of hostnames,
+%% the <em>'...'</em> atom matches properly the lower levels of the domain
+%% as would be expected.
+%%
+%% When a result is found, this function will return the handler module and
+%% options found in the dispatch list, a key-value list of bindings and
+%% the tokens that were matched by the <em>'...'</em> atom for both the
+%% hostname and path.
+-spec match(dispatch_rules(), Host::binary() | tokens(), Path::binary())
+ -> {ok, module(), any(), bindings(),
+ HostInfo::undefined | tokens(),
+ PathInfo::undefined | tokens()}
+ | {error, notfound, host} | {error, notfound, path}
+ | {error, badrequest, path}.
+match([], _, _) ->
+ {error, notfound, host};
+%% If the host is '_' then there can be no constraints.
+match([{'_', [], PathMatchs}|_Tail], _, Path) ->
+ match_path(PathMatchs, undefined, Path, []);
+match([{HostMatch, Constraints, PathMatchs}|Tail], Tokens, Path)
+ when is_list(Tokens) ->
+ case list_match(Tokens, HostMatch, []) of
+ false ->
+ match(Tail, Tokens, Path);
+ {true, Bindings, HostInfo} ->
+ HostInfo2 = case HostInfo of
+ undefined -> undefined;
+ _ -> lists:reverse(HostInfo)
+ end,
+ case check_constraints(Constraints, Bindings) of
+ {ok, Bindings2} ->
+ match_path(PathMatchs, HostInfo2, Path, Bindings2);
+ nomatch ->
+ match(Tail, Tokens, Path)
+ end
+ end;
+match(Dispatch, Host, Path) ->
+ match(Dispatch, split_host(Host), Path).
+
+-spec match_path([dispatch_path()],
+ HostInfo::undefined | tokens(), binary() | tokens(), bindings())
+ -> {ok, module(), any(), bindings(),
+ HostInfo::undefined | tokens(),
+ PathInfo::undefined | tokens()}
+ | {error, notfound, path} | {error, badrequest, path}.
+match_path([], _, _, _) ->
+ {error, notfound, path};
+%% If the path is '_' then there can be no constraints.
+match_path([{'_', [], Handler, Opts}|_Tail], HostInfo, _, Bindings) ->
+ {ok, Handler, Opts, Bindings, HostInfo, undefined};
+match_path([{<<"*">>, _Constraints, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) ->
+ {ok, Handler, Opts, Bindings, HostInfo, undefined};
+match_path([{PathMatch, Constraints, Handler, Opts}|Tail], HostInfo, Tokens,
+ Bindings) when is_list(Tokens) ->
+ case list_match(Tokens, PathMatch, Bindings) of
+ false ->
+ match_path(Tail, HostInfo, Tokens, Bindings);
+ {true, PathBinds, PathInfo} ->
+ case check_constraints(Constraints, PathBinds) of
+ {ok, PathBinds2} ->
+ {ok, Handler, Opts, PathBinds2, HostInfo, PathInfo};
+ nomatch ->
+ match_path(Tail, HostInfo, Tokens, Bindings)
+ end
+ end;
+match_path(_Dispatch, _HostInfo, badrequest, _Bindings) ->
+ {error, badrequest, path};
+match_path(Dispatch, HostInfo, Path, Bindings) ->
+ match_path(Dispatch, HostInfo, split_path(Path), Bindings).
+
+check_constraints([], Bindings) ->
+ {ok, Bindings};
+check_constraints([Constraint|Tail], Bindings) ->
+ Name = element(1, Constraint),
+ case lists:keyfind(Name, 1, Bindings) of
+ false ->
+ check_constraints(Tail, Bindings);
+ {_, Value} ->
+ case check_constraint(Constraint, Value) of
+ true ->
+ check_constraints(Tail, Bindings);
+ {true, Value2} ->
+ Bindings2 = lists:keyreplace(Name, 1, Bindings,
+ {Name, Value2}),
+ check_constraints(Tail, Bindings2);
+ false ->
+ nomatch
+ end
+ end.
+
+check_constraint({_, int}, Value) ->
+ try {true, list_to_integer(binary_to_list(Value))}
+ catch _:_ -> false
+ end;
+check_constraint({_, function, Fun}, Value) ->
+ Fun(Value).
+
+%% @doc Split a hostname into a list of tokens.
+-spec split_host(binary()) -> tokens().
+split_host(Host) ->
+ split_host(Host, []).
+
+split_host(Host, Acc) ->
+ case binary:match(Host, <<".">>) of
+ nomatch when Host =:= <<>> ->
+ Acc;
+ nomatch ->
+ [Host|Acc];
+ {Pos, _} ->
+ << Segment:Pos/binary, _:8, Rest/bits >> = Host,
+ false = byte_size(Segment) == 0,
+ split_host(Rest, [Segment|Acc])
+ end.
+
+%% @doc Split a path into a list of path segments.
+%%
+%% 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().
+split_path(<< $/, Path/bits >>) ->
+ split_path(Path, []);
+split_path(_) ->
+ badrequest.
+
+split_path(Path, Acc) ->
+ try
+ case binary:match(Path, <<"/">>) of
+ nomatch when Path =:= <<>> ->
+ lists:reverse([cowboy_http:urldecode(S) || S <- Acc]);
+ nomatch ->
+ lists:reverse([cowboy_http:urldecode(S) || S <- [Path|Acc]]);
+ {Pos, _} ->
+ << Segment:Pos/binary, _:8, Rest/bits >> = Path,
+ split_path(Rest, [Segment|Acc])
+ end
+ catch
+ error:badarg ->
+ badrequest
+ end.
+
+-spec list_match(tokens(), dispatch_match(), bindings())
+ -> {true, bindings(), undefined | tokens()} | false.
+%% Atom '...' matches any trailing path, stop right now.
+list_match(List, ['...'], Binds) ->
+ {true, Binds, List};
+%% Atom '_' matches anything, continue.
+list_match([_E|Tail], ['_'|TailMatch], Binds) ->
+ list_match(Tail, TailMatch, Binds);
+%% Both values match, continue.
+list_match([E|Tail], [E|TailMatch], Binds) ->
+ list_match(Tail, TailMatch, Binds);
+%% Bind E to the variable name V and continue,
+%% unless V was already defined and E isn't identical to the previous value.
+list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) ->
+ case lists:keyfind(V, 1, Binds) of
+ {_, E} ->
+ list_match(Tail, TailMatch, Binds);
+ {_, _} ->
+ false;
+ false ->
+ list_match(Tail, TailMatch, [{V, E}|Binds])
+ end;
+%% Match complete.
+list_match([], [], Binds) ->
+ {true, Binds, undefined};
+%% Values don't match, stop.
+list_match(_List, _Match, _Binds) ->
+ false.
+
+%% Tests.
+
+-ifdef(TEST).
+
+compile_test_() ->
+ %% {Routes, Result}
+ Tests = [
+ %% Match any host and path.
+ {[{'_', [{'_', h, o}]}],
+ [{'_', [], [{'_', [], h, o}]}]},
+ {[{"cowboy.example.org",
+ [{"/", ha, oa}, {"/path/to/resource", hb, ob}]}],
+ [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [
+ {[], [], ha, oa},
+ {[<<"path">>, <<"to">>, <<"resource">>], [], hb, ob}]}]},
+ {[{'_', [{"/path/to/resource/", h, o}]}],
+ [{'_', [], [{[<<"path">>, <<"to">>, <<"resource">>], [], h, o}]}]},
+ {[{"cowboy.example.org.", [{'_', h, o}]}],
+ [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]},
+ {[{".cowboy.example.org", [{'_', h, o}]}],
+ [{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]},
+ {[{":subdomain.example.org", [{"/hats/:name/prices", h, o}]}],
+ [{[<<"org">>, <<"example">>, subdomain], [], [
+ {[<<"hats">>, name, <<"prices">>], [], h, o}]}]},
+ {[{"ninenines.:_", [{"/hats/:_", h, o}]}],
+ [{['_', <<"ninenines">>], [], [{[<<"hats">>, '_'], [], h, o}]}]},
+ {[{"[www.]ninenines.eu",
+ [{"/horses", h, o}, {"/hats/[page/:number]", h, o}]}], [
+ {[<<"eu">>, <<"ninenines">>], [], [
+ {[<<"horses">>], [], h, o},
+ {[<<"hats">>], [], h, o},
+ {[<<"hats">>, <<"page">>, number], [], h, o}]},
+ {[<<"eu">>, <<"ninenines">>, <<"www">>], [], [
+ {[<<"horses">>], [], h, o},
+ {[<<"hats">>], [], h, o},
+ {[<<"hats">>, <<"page">>, number], [], h, o}]}]},
+ {[{'_', [{"/hats/[page/[:number]]", h, o}]}], [{'_', [], [
+ {[<<"hats">>], [], h, o},
+ {[<<"hats">>, <<"page">>], [], h, o},
+ {[<<"hats">>, <<"page">>, number], [], h, o}]}]},
+ {[{"[...]ninenines.eu", [{"/hats/[...]", h, o}]}],
+ [{[<<"eu">>, <<"ninenines">>, '...'], [], [
+ {[<<"hats">>, '...'], [], h, o}]}]}
+ ],
+ [{lists:flatten(io_lib:format("~p", [Rt])),
+ fun() -> Rs = compile(Rt) end} || {Rt, Rs} <- Tests].
+
+split_host_test_() ->
+ %% {Host, Result}
+ Tests = [
+ {<<"">>, []},
+ {<<"*">>, [<<"*">>]},
+ {<<"cowboy.ninenines.eu">>,
+ [<<"eu">>, <<"ninenines">>, <<"cowboy">>]},
+ {<<"ninenines.eu">>,
+ [<<"eu">>, <<"ninenines">>]},
+ {<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>,
+ [<<"z">>, <<"y">>, <<"x">>, <<"w">>, <<"v">>, <<"u">>, <<"t">>,
+ <<"s">>, <<"r">>, <<"q">>, <<"p">>, <<"o">>, <<"n">>, <<"m">>,
+ <<"l">>, <<"k">>, <<"j">>, <<"i">>, <<"h">>, <<"g">>, <<"f">>,
+ <<"e">>, <<"d">>, <<"c">>, <<"b">>, <<"a">>]}
+ ],
+ [{H, fun() -> R = split_host(H) end} || {H, R} <- Tests].
+
+split_path_test_() ->
+ %% {Path, Result, QueryString}
+ Tests = [
+ {<<"/">>, []},
+ {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]},
+ {<<"/users">>, [<<"users">>]},
+ {<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]},
+ {<<"/users/a+b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]}
+ ],
+ [{P, fun() -> R = split_path(P) end} || {P, R} <- Tests].
+
+match_test_() ->
+ Dispatch = [
+ {[<<"eu">>, <<"ninenines">>, '_', <<"www">>], [], [
+ {[<<"users">>, '_', <<"mails">>], [], match_any_subdomain_users, []}
+ ]},
+ {[<<"eu">>, <<"ninenines">>], [], [
+ {[<<"users">>, id, <<"friends">>], [], match_extend_users_friends, []},
+ {'_', [], match_extend, []}
+ ]},
+ {[var, <<"ninenines">>], [], [
+ {[<<"threads">>, var], [], match_duplicate_vars,
+ [we, {expect, two}, var, here]}
+ ]},
+ {[ext, <<"erlang">>], [], [
+ {'_', [], match_erlang_ext, []}
+ ]},
+ {'_', [], [
+ {[<<"users">>, id, <<"friends">>], [], match_users_friends, []},
+ {'_', [], match_any, []}
+ ]}
+ ],
+ %% {Host, Path, Result}
+ Tests = [
+ {<<"any">>, <<"/">>, {ok, match_any, [], []}},
+ {<<"www.any.ninenines.eu">>, <<"/users/42/mails">>,
+ {ok, match_any_subdomain_users, [], []}},
+ {<<"www.ninenines.eu">>, <<"/users/42/mails">>,
+ {ok, match_any, [], []}},
+ {<<"www.ninenines.eu">>, <<"/">>,
+ {ok, match_any, [], []}},
+ {<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>,
+ {error, notfound, path}},
+ {<<"ninenines.eu">>, <<"/">>,
+ {ok, match_extend, [], []}},
+ {<<"ninenines.eu">>, <<"/users/42/friends">>,
+ {ok, match_extend_users_friends, [], [{id, <<"42">>}]}},
+ {<<"erlang.fr">>, '_',
+ {ok, match_erlang_ext, [], [{ext, <<"fr">>}]}},
+ {<<"any">>, <<"/users/444/friends">>,
+ {ok, match_users_friends, [], [{id, <<"444">>}]}}
+ ],
+ [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
+ {ok, Handler, Opts, Binds, undefined, undefined}
+ = match(Dispatch, H, P)
+ end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests].
+
+match_info_test_() ->
+ Dispatch = [
+ {[<<"eu">>, <<"ninenines">>, <<"www">>], [], [
+ {[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], [], match_path, []}
+ ]},
+ {[<<"eu">>, <<"ninenines">>, '...'], [], [
+ {'_', [], match_any, []}
+ ]}
+ ],
+ Tests = [
+ {<<"ninenines.eu">>, <<"/">>,
+ {ok, match_any, [], [], [], undefined}},
+ {<<"bugs.ninenines.eu">>, <<"/">>,
+ {ok, match_any, [], [], [<<"bugs">>], undefined}},
+ {<<"cowboy.bugs.ninenines.eu">>, <<"/">>,
+ {ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}},
+ {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>,
+ {ok, match_path, [], [], undefined, []}},
+ {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>,
+ {ok, match_path, [], [], undefined, [<<"path_info">>]}},
+ {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>,
+ {ok, match_path, [], [], undefined, [<<"foo">>, <<"bar">>]}}
+ ],
+ [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
+ R = match(Dispatch, H, P)
+ end} || {H, P, R} <- Tests].
+
+match_constraints_test() ->
+ Dispatch = [{'_', [],
+ [{[<<"path">>, value], [{value, int}], match, []}]}],
+ {ok, _, [], [{value, 123}], _, _} = match(Dispatch,
+ <<"ninenines.eu">>, <<"/path/123">>),
+ {ok, _, [], [{value, 123}], _, _} = match(Dispatch,
+ <<"ninenines.eu">>, <<"/path/123/">>),
+ {error, notfound, path} = match(Dispatch,
+ <<"ninenines.eu">>, <<"/path/NaN/">>),
+ Dispatch2 = [{'_', [],
+ [{[<<"path">>, username], [{username, function,
+ fun(Value) -> Value =:= cowboy_bstr:to_lower(Value) end}],
+ match, []}]}],
+ {ok, _, [], [{username, <<"essen">>}], _, _} = match(Dispatch2,
+ <<"ninenines.eu">>, <<"/path/essen">>),
+ {error, notfound, path} = match(Dispatch2,
+ <<"ninenines.eu">>, <<"/path/ESSEN">>),
+ ok.
+
+match_same_bindings_test() ->
+ Dispatch = [{[same, same], [], [{'_', [], match, []}]}],
+ {ok, _, [], [{same, <<"eu">>}], _, _} = match(Dispatch,
+ <<"eu.eu">>, <<"/">>),
+ {error, notfound, host} = match(Dispatch,
+ <<"ninenines.eu">>, <<"/">>),
+ Dispatch2 = [{[<<"eu">>, <<"ninenines">>, user], [],
+ [{[<<"path">>, user], [], match, []}]}],
+ {ok, _, [], [{user, <<"essen">>}], _, _} = match(Dispatch2,
+ <<"essen.ninenines.eu">>, <<"/path/essen">>),
+ {ok, _, [], [{user, <<"essen">>}], _, _} = match(Dispatch2,
+ <<"essen.ninenines.eu">>, <<"/path/essen/">>),
+ {error, notfound, path} = match(Dispatch2,
+ <<"essen.ninenines.eu">>, <<"/path/notessen">>),
+ Dispatch3 = [{'_', [], [{[same, same], [], match, []}]}],
+ {ok, _, [], [{same, <<"path">>}], _, _} = match(Dispatch3,
+ <<"ninenines.eu">>, <<"/path/path">>),
+ {error, notfound, path} = match(Dispatch3,
+ <<"ninenines.eu">>, <<"/path/to">>),
+ ok.
+
+-endif.
diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl
index 55d01c7..373ea52 100644
--- a/src/cowboy_static.erl
+++ b/src/cowboy_static.erl
@@ -289,7 +289,7 @@ forbidden(Req, #state{fileinfo={ok, #file_info{access=Access}}}=State) ->
-spec last_modified(Req, #state{})
-> {calendar:datetime(), Req, #state{}} when Req::cowboy_req:req().
last_modified(Req, #state{fileinfo={ok, #file_info{mtime=Modified}}}=State) ->
- {Modified, Req, State}.
+ {erlang:localtime_to_universaltime(Modified), Req, State}.
%% @private Generate the ETag header value for this file.
@@ -321,8 +321,14 @@ content_types_provided(Req, #state{filepath=Filepath,
-spec file_contents(cowboy_req:req(), #state{}) -> tuple().
file_contents(Req, #state{filepath=Filepath,
fileinfo={ok, #file_info{size=Filesize}}}=State) ->
- {ok, Transport, Socket} = cowboy_req:transport(Req),
- Writefile = fun() -> Transport:sendfile(Socket, Filepath) end,
+ Writefile = fun(Socket, Transport) ->
+ %% Transport:sendfile/2 may return {error, closed}
+ %% if the connection is closed while sending the file.
+ case Transport:sendfile(Socket, Filepath) of
+ {ok, _} -> ok;
+ {error, closed} -> ok
+ end
+ end,
{{stream, Filesize, Writefile}, Req, State}.
diff --git a/src/cowboy_sup.erl b/src/cowboy_sup.erl
index 00fcc5e..0e4e59a 100644
--- a/src/cowboy_sup.erl
+++ b/src/cowboy_sup.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl
index 8c02ac7..debb69f 100644
--- a/src/cowboy_websocket.erl
+++ b/src/cowboy_websocket.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
@@ -12,7 +12,10 @@
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-%% @doc WebSocket protocol implementation.
+%% @doc Websocket protocol implementation.
+%%
+%% Cowboy supports versions 7 through 17 of the Websocket drafts.
+%% It also supports RFC6455, the proposed standard for Websocket.
-module(cowboy_websocket).
%% API.
@@ -21,52 +24,52 @@
%% Internal.
-export([handler_loop/4]).
+-type close_code() :: 1000..4999.
+-export_type([close_code/0]).
+
-type frame() :: close | ping | pong
| {text | binary | close | ping | pong, binary()}
- | {close, 1000..4999, binary()}.
+ | {close, close_code(), binary()}.
-export_type([frame/0]).
-type opcode() :: 0 | 1 | 2 | 8 | 9 | 10.
-type mask_key() :: 0..16#ffffffff.
-
-%% The websocket_data/4 function may be called multiple times for a message.
-%% The websocket_dispatch/4 function is only called once for each message.
--type frag_state() ::
- undefined | %% no fragmentation has been seen.
- {nofin, opcode()} | %% first fragment has been seen.
- {nofin, opcode(), binary()} | %% first fragment has been unmasked.
- {fin, opcode(), binary()}. %% last fragment has been seen.
+-type frag_state() :: undefined
+ | {nofin, opcode(), binary()} | {fin, opcode(), binary()}.
-record(state, {
+ env :: cowboy_middleware:env(),
socket = undefined :: inet:socket(),
transport = undefined :: module(),
- version :: 0 | 7 | 8 | 13,
handler :: module(),
- opts :: any(),
- challenge = undefined :: undefined | binary() | {binary(), binary()},
+ handler_opts :: any(),
+ key = undefined :: undefined | binary(),
timeout = infinity :: timeout(),
timeout_ref = undefined :: undefined | reference(),
messages = undefined :: undefined | {atom(), atom(), atom()},
hibernate = false :: boolean(),
- eop :: undefined | tuple(), %% hixie-76 specific.
- origin = undefined :: undefined | binary(), %% hixie-76 specific.
- frag_state = undefined :: frag_state()
+ frag_state = undefined :: frag_state(),
+ utf8_state = <<>> :: binary()
}).
-%% @doc Upgrade a HTTP request to the WebSocket protocol.
+%% @doc Upgrade an HTTP request to the Websocket protocol.
%%
-%% You do not need to call this function manually. To upgrade to the WebSocket
+%% 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(), cowboy_req:req()) -> closed.
-upgrade(ListenerPid, Handler, Opts, Req) ->
+-spec upgrade(Req, Env, module(), any())
+ -> {ok, Req, Env} | {error, 400, Req}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+upgrade(Req, Env, Handler, HandlerOpts) ->
+ {_, ListenerPid} = lists:keyfind(listener, 1, Env),
ranch_listener:remove_connection(ListenerPid),
- {ok, Transport, Socket} = cowboy_req:transport(Req),
- State = #state{socket=Socket, transport=Transport,
- handler=Handler, opts=Opts},
+ [Socket, Transport] = cowboy_req:get([socket, transport], Req),
+ State = #state{env=Env, socket=Socket, transport=Transport,
+ handler=Handler, handler_opts=HandlerOpts},
case catch websocket_upgrade(State, Req) of
{ok, State2, Req2} -> handler_init(State2, Req2);
- {'EXIT', _Reason} -> upgrade_error(Req)
+ {'EXIT', _Reason} -> upgrade_error(Req, Env)
end.
-spec websocket_upgrade(#state{}, Req)
@@ -79,41 +82,21 @@ websocket_upgrade(State, Req) ->
{ok, [<<"websocket">>], Req3}
= cowboy_req:parse_header(<<"upgrade">>, Req2),
{Version, Req4} = cowboy_req:header(<<"sec-websocket-version">>, Req3),
- websocket_upgrade(Version, State, Req4).
-
-%% @todo Handle the Sec-Websocket-Protocol header.
-%% @todo Reply a proper error, don't die, if a required header is undefined.
--spec websocket_upgrade(undefined | <<_:8>>, #state{}, Req)
- -> {ok, #state{}, Req} when Req::cowboy_req:req().
-%% No version given. Assuming hixie-76 draft.
-%%
-%% We need to wait to send a reply back before trying to read the
-%% 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) ->
- {Origin, Req2} = cowboy_req:header(<<"origin">>, Req),
- {Key1, Req3} = cowboy_req:header(<<"sec-websocket-key1">>, Req2),
- {Key2, Req4} = cowboy_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}, cowboy_req:set_meta(websocket_version, 0, Req4)};
-%% Versions 7 and 8. Implementation follows the hybi 7 through 17 drafts.
-websocket_upgrade(Version, State, Req)
- when Version =:= <<"7">>; Version =:= <<"8">>;
- Version =:= <<"13">> ->
- {Key, Req2} = cowboy_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},
- cowboy_req:set_meta(websocket_version, IntVersion, Req2)}.
-
--spec handler_init(#state{}, cowboy_req:req()) -> closed.
-handler_init(State=#state{transport=Transport, handler=Handler, opts=Opts},
- Req) ->
- try Handler:websocket_init(Transport:name(), Req, Opts) of
+ true = (IntVersion =:= 7) orelse (IntVersion =:= 8)
+ orelse (IntVersion =:= 13),
+ {Key, Req5} = cowboy_req:header(<<"sec-websocket-key">>, Req4),
+ false = Key =:= undefined,
+ {ok, State#state{key=Key},
+ cowboy_req:set_meta(websocket_version, IntVersion, Req5)}.
+
+-spec handler_init(#state{}, Req)
+ -> {ok, Req, cowboy_middleware:env()} | {error, 400, Req}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+handler_init(State=#state{env=Env, transport=Transport,
+ handler=Handler, handler_opts=HandlerOpts}, Req) ->
+ try Handler:websocket_init(Transport:name(), Req, HandlerOpts) of
{ok, Req2, HandlerState} ->
websocket_handshake(State, Req2, HandlerState);
{ok, Req2, HandlerState, hibernate} ->
@@ -127,60 +110,36 @@ handler_init(State=#state{transport=Transport, handler=Handler, opts=Opts},
hibernate=true}, Req2, HandlerState);
{shutdown, Req2} ->
cowboy_req:ensure_response(Req2, 400),
- closed
+ {ok, Req2, [{result, closed}|Env]}
catch Class:Reason ->
- upgrade_error(Req),
error_logger:error_msg(
"** Cowboy handler ~p terminating in ~p/~p~n"
" for the reason ~p:~p~n** Options were ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, websocket_init, 3, Class, Reason, Opts,
- cowboy_req:to_list(Req),erlang:get_stacktrace()])
+ [Handler, websocket_init, 3, Class, Reason, HandlerOpts,
+ cowboy_req:to_list(Req),erlang:get_stacktrace()]),
+ upgrade_error(Req, Env)
end.
--spec upgrade_error(cowboy_req:req()) -> closed.
-upgrade_error(Req) ->
+%% Only send an error reply if there is no resp_sent message.
+-spec upgrade_error(Req, Env) -> {ok, Req, Env} | {error, 400, Req}
+ when Req::cowboy_req:req(), Env::cowboy_middleware:env().
+upgrade_error(Req, Env) ->
receive
- {cowboy_req, resp_sent} -> closed
+ {cowboy_req, resp_sent} ->
+ {ok, Req, [{result, closed}|Env]}
after 0 ->
- _ = cowboy_req:reply(400, [], [], Req),
- closed
+ {error, 400, Req}
end.
--spec websocket_handshake(#state{}, cowboy_req:req(), any()) -> closed.
-websocket_handshake(State=#state{socket=Socket, transport=Transport,
- version=0, origin=Origin, challenge={Key1, Key2}},
- Req, HandlerState) ->
- {<< "http", Location/binary >>, Req1} = cowboy_req:url(Req),
- {ok, Req2} = cowboy_req:upgrade_reply(
- <<"101 WebSocket Protocol Handshake">>,
- [{<<"upgrade">>, <<"WebSocket">>},
- {<<"sec-websocket-location">>, << "ws", Location/binary >>},
- {<<"sec-websocket-origin">>, Origin}],
- Req1),
- %% Flush the resp_sent message before moving on.
- receive {cowboy_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.
- %%
- %% We use a trick here to read exactly 8 bytes of the body regardless
- %% of what's in the buffer.
- {ok, Req3} = cowboy_req:init_stream(
- fun cowboy_http:te_identity/2, {0, 8},
- fun cowboy_http:ce_identity/1, Req2),
- case cowboy_req:body(Req3) of
- {ok, Key3, Req4} ->
- Challenge = hixie76_challenge(Key1, Key2, Key3),
- Transport:send(Socket, Challenge),
- handler_before_loop(State#state{messages=Transport:messages()},
- Req4, HandlerState, <<>>);
- _Any ->
- %% If an error happened reading the body, stop there.
- handler_terminate(State, Req3, HandlerState, {error, closed})
- end;
-websocket_handshake(State=#state{transport=Transport, challenge=Challenge},
+-spec websocket_handshake(#state{}, Req, any())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+websocket_handshake(State=#state{transport=Transport, key=Key},
Req, HandlerState) ->
+ Challenge = base64:encode(crypto:sha(
+ << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>)),
{ok, Req2} = cowboy_req:upgrade_reply(
101,
[{<<"upgrade">>, <<"websocket">>},
@@ -189,17 +148,19 @@ websocket_handshake(State=#state{transport=Transport, challenge=Challenge},
%% Flush the resp_sent message before moving on.
receive {cowboy_req, resp_sent} -> ok after 0 -> ok end,
State2 = handler_loop_timeout(State),
- handler_before_loop(State2#state{messages=Transport:messages()},
- Req2, HandlerState, <<>>).
+ handler_before_loop(State2#state{key=undefined,
+ messages=Transport:messages()}, Req2, HandlerState, <<>>).
--spec handler_before_loop(#state{}, cowboy_req:req(), any(), binary()) -> closed.
+-spec handler_before_loop(#state{}, Req, any(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
handler_before_loop(State=#state{
socket=Socket, transport=Transport, hibernate=true},
Req, HandlerState, SoFar) ->
Transport:setopts(Socket, [{active, once}]),
- catch erlang:hibernate(?MODULE, handler_loop,
- [State#state{hibernate=false}, Req, HandlerState, SoFar]),
- closed;
+ {suspend, ?MODULE, handler_loop,
+ [State#state{hibernate=false}, Req, HandlerState, SoFar]};
handler_before_loop(State=#state{socket=Socket, transport=Transport},
Req, HandlerState, SoFar) ->
Transport:setopts(Socket, [{active, once}]),
@@ -215,10 +176,12 @@ handler_loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) ->
State#state{timeout_ref=TRef}.
%% @private
--spec handler_loop(#state{}, cowboy_req:req(), any(), binary()) -> closed.
-handler_loop(State=#state{
- socket=Socket, messages={OK, Closed, Error}, timeout_ref=TRef},
- Req, HandlerState, SoFar) ->
+-spec handler_loop(#state{}, Req, any(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+handler_loop(State=#state{socket=Socket, messages={OK, Closed, Error},
+ timeout_ref=TRef}, Req, HandlerState, SoFar) ->
receive
{OK, Socket, Data} ->
State2 = handler_loop_timeout(State),
@@ -237,191 +200,298 @@ handler_loop(State=#state{
SoFar, websocket_info, Message, fun handler_before_loop/4)
end.
--spec websocket_data(#state{}, cowboy_req:req(), any(), binary()) -> closed.
-%% 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/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, _/binary >>) ->
- case binary:match(Data, EOP) of
- {Pos, 1} ->
- Pos2 = Pos - 1,
- << 0, Payload:Pos2/binary, 255, Rest/bits >> = Data,
- handler_call(State, Req, HandlerState,
- Rest, websocket_handle, {text, Payload}, fun websocket_data/4);
- nomatch ->
- %% @todo We probably should allow limiting frame length.
- handler_before_loop(State, Req, HandlerState, Data)
- end;
-%% incomplete hybi data frame.
-websocket_data(State=#state{version=Version}, Req, HandlerState, Data)
- when Version =/= 0, byte_size(Data) =:= 1 ->
- handler_before_loop(State, Req, HandlerState, Data);
-%% 7 bit payload length prefix exists
+%% All frames passing through this function are considered valid,
+%% with the only exception of text and close frames with a payload
+%% which may still contain errors.
+-spec websocket_data(#state{}, Req, any(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+%% RSV bits MUST be 0 unless an extension is negotiated
+%% that defines meanings for non-zero values.
+websocket_data(State, Req, HandlerState, << _:1, Rsv:3, _/bits >>)
+ when Rsv =/= 0 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Invalid opcode. Note that these opcodes may be used by extensions.
+websocket_data(State, Req, HandlerState, << _:4, Opcode:4, _/bits >>)
+ when Opcode > 2, Opcode =/= 8, Opcode =/= 9, Opcode =/= 10 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Control frames MUST NOT be fragmented.
+websocket_data(State, Req, HandlerState, << 0:1, _:3, Opcode:4, _/bits >>)
+ when Opcode >= 8 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% A frame MUST NOT use the zero opcode unless fragmentation was initiated.
+websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
+ << _:4, 0:4, _/bits >>) ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Non-control opcode when expecting control message or next fragment.
+websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState,
+ << _:4, Opcode:4, _/bits >>)
+ when Opcode =/= 0, Opcode < 8 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Close control frame length MUST be 0 or >= 2.
+websocket_data(State, Req, HandlerState, << _:4, 8:4, _:1, 1:7, _/bits >>) ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Close control frame with incomplete close code. Need more data.
websocket_data(State, Req, HandlerState,
- << Fin:1, Rsv:3, Opcode:4, Mask:1, PayloadLen:7, Rest/bits >>
- = Data) when PayloadLen < 126 ->
+ Data = << _:4, 8:4, 1:1, Len:7, _/bits >>)
+ when Len > 1, byte_size(Data) < 8 ->
+ handler_before_loop(State, Req, HandlerState, Data);
+%% 7 bits payload length.
+websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
+ Len:7, MaskKey:32, Rest/bits >>)
+ when Len < 126 ->
websocket_data(State, Req, HandlerState,
- Fin, Rsv, Opcode, Mask, PayloadLen, Rest, Data);
-%% 7+16 bits payload length prefix exists
-websocket_data(State, Req, HandlerState,
- << Fin:1, Rsv:3, Opcode:4, Mask:1, 126:7, PayloadLen:16, Rest/bits >>
- = Data) when PayloadLen > 125 ->
+ Opcode, Len, MaskKey, Rest, Fin);
+%% 16 bits payload length.
+websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
+ 126:7, Len:16, MaskKey:32, Rest/bits >>)
+ when Len > 125, Opcode < 8 ->
websocket_data(State, Req, HandlerState,
- Fin, Rsv, Opcode, Mask, PayloadLen, Rest, Data);
-%% 7+16 bits payload length prefix missing
-websocket_data(State, Req, HandlerState,
- << _Fin:1, _Rsv:3, _Opcode:4, _Mask:1, 126:7, Rest/bits >>
- = Data) when byte_size(Rest) < 2 ->
- handler_before_loop(State, Req, HandlerState, Data);
-%% 7+64 bits payload length prefix exists
-websocket_data(State, Req, HandlerState,
- << Fin:1, Rsv:3, Opcode:4, Mask:1, 127:7, 0:1, PayloadLen:63,
- Rest/bits >> = Data) when PayloadLen > 16#FFFF ->
+ Opcode, Len, MaskKey, Rest, Fin);
+%% 63 bits payload length.
+websocket_data(State, Req, HandlerState, << Fin:1, _Rsv:3, Opcode:4, 1:1,
+ 127:7, 0:1, Len:63, MaskKey:32, Rest/bits >>)
+ when Len > 16#ffff, Opcode < 8 ->
websocket_data(State, Req, HandlerState,
- Fin, Rsv, Opcode, Mask, PayloadLen, Rest, Data);
-%% 7+64 bits payload length prefix missing
-websocket_data(State, Req, HandlerState,
- << _Fin:1, _Rsv:3, _Opcode:4, _Mask:1, 127:7, Rest/bits >>
- = Data) when byte_size(Rest) < 8 ->
- handler_before_loop(State, Req, HandlerState, Data);
-%% invalid payload length prefix.
-websocket_data(State, Req, HandlerState, _Data) ->
- websocket_close(State, Req, HandlerState, {error, badframe}).
-
--spec websocket_data(#state{}, cowboy_req:req(), any(), non_neg_integer(),
- non_neg_integer(), non_neg_integer(), non_neg_integer(),
- non_neg_integer(), binary(), binary()) -> closed.
-%% A fragmented message MUST start a non-zero opcode.
-websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
- _Fin=0, _Rsv=0, _Opcode=0, _Mask, _PayloadLen, _Rest, _Buffer) ->
+ Opcode, Len, MaskKey, Rest, Fin);
+%% When payload length is over 63 bits, the most significant bit MUST be 0.
+websocket_data(State, Req, HandlerState, << _:8, 1:1, 127:7, 1:1, _/bits >>) ->
websocket_close(State, Req, HandlerState, {error, badframe});
-%% A control message MUST NOT be fragmented.
-websocket_data(State, Req, HandlerState, _Fin=0, _Rsv=0, Opcode, _Mask,
- _PayloadLen, _Rest, _Buffer) when Opcode >= 8 ->
+%% All frames sent from the client to the server are masked.
+websocket_data(State, Req, HandlerState, << _:8, 0:1, _/bits >>) ->
websocket_close(State, Req, HandlerState, {error, badframe});
-%% The opcode is only included in the first message fragment.
-websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
- _Fin=0, _Rsv=0, Opcode, Mask, PayloadLen, Rest, Data) ->
- websocket_before_unmask(
- State#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- Data, Rest, 0, Mask, PayloadLen);
-%% non-control opcode when expecting control message or next fragment.
-websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState, _Fin,
- _Rsv=0, Opcode, _Mask, _Ln, _Rest, _Data) when Opcode > 0, Opcode < 8 ->
+%% For the next two clauses, it can be one of the following:
+%%
+%% * The minimal number of bytes MUST be used to encode the length
+%% * All control frames MUST have a payload length of 125 bytes or less
+websocket_data(State, Req, HandlerState, << _:9, 126:7, _:48, _/bits >>) ->
websocket_close(State, Req, HandlerState, {error, badframe});
-%% If the first message fragment was incomplete, retry unmasking.
-websocket_data(State=#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- _Fin=0, _Rsv=0, Opcode, Mask, PayloadLen, Rest, Data) ->
- websocket_before_unmask(
- State#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- Data, Rest, 0, Mask, PayloadLen);
-%% if the opcode is zero and the fin flag is zero, unmask and await next.
-websocket_data(State=#state{frag_state={nofin, _Opcode, _Payloads}}, Req,
- HandlerState, _Fin=0, _Rsv=0, _Opcode2=0, Mask, PayloadLen, Rest,
- Data) ->
- websocket_before_unmask(
- State, Req, HandlerState, Data, Rest, 0, Mask, PayloadLen);
-%% when the last fragment is seen. Update the fragmentation status.
-websocket_data(State=#state{frag_state={nofin, Opcode, Payloads}}, Req,
- HandlerState, _Fin=1, _Rsv=0, _Opcode=0, Mask, PayloadLen, Rest,
- Data) ->
- websocket_before_unmask(
- State#state{frag_state={fin, Opcode, Payloads}},
- Req, HandlerState, Data, Rest, 0, Mask, PayloadLen);
-%% control messages MUST NOT use 7+16 bits or 7+64 bits payload length prefixes
-websocket_data(State, Req, HandlerState, _Fin, _Rsv, Opcode, _Mask, PayloadLen,
- _Rest, _Data) when Opcode >= 8, PayloadLen > 125 ->
- websocket_close(State, Req, HandlerState, {error, badframe});
-%% unfragmented message. unmask and dispatch the message.
-websocket_data(State=#state{version=Version}, Req, HandlerState, _Fin=1, _Rsv=0,
- Opcode, Mask, PayloadLen, Rest, Data) when Version =/= 0 ->
- websocket_before_unmask(
- State, Req, HandlerState, Data, Rest, Opcode, Mask, PayloadLen);
-%% Something was wrong with the frame. Close the connection.
-websocket_data(State, Req, HandlerState, _Fin, _Rsv, _Opcode, _Mask,
- _PayloadLen, _Rest, _Data) ->
- websocket_close(State, Req, HandlerState, {error, badframe}).
-
-%% hybi routing depending on whether unmasking is needed.
--spec websocket_before_unmask(#state{}, cowboy_req:req(), any(), binary(),
- binary(), opcode(), 0 | 1, non_neg_integer() | undefined) -> closed.
-websocket_before_unmask(State, Req, HandlerState, Data,
- Rest, Opcode, Mask, PayloadLen) ->
- case {Mask, PayloadLen} of
- {0, 0} ->
- 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:PayloadLen/binary, Rest2/bits >> = Rest,
- websocket_unmask(State, Req, HandlerState, Rest2,
- Opcode, Payload, MaskKey)
- end.
-
-%% hybi unmasking.
--spec websocket_unmask(#state{}, cowboy_req:req(), any(), binary(),
- opcode(), binary(), mask_key()) -> closed.
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, Payload, MaskKey) ->
- websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, Payload, MaskKey, <<>>).
-
--spec websocket_unmask(#state{}, cowboy_req:req(), any(), binary(),
- opcode(), binary(), mask_key(), binary()) -> closed.
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:32, Rest/bits >>, MaskKey, Acc) ->
+websocket_data(State, Req, HandlerState, << _:9, 127:7, _:96, _/bits >>) ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+%% Need more data.
+websocket_data(State, Req, HandlerState, Data) ->
+ handler_before_loop(State, Req, HandlerState, Data).
+
+%% Initialize or update fragmentation state.
+-spec websocket_data(#state{}, Req, any(),
+ opcode(), non_neg_integer(), mask_key(), binary(), 0 | 1)
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+%% The opcode is only included in the first frame fragment.
+websocket_data(State=#state{frag_state=undefined}, Req, HandlerState,
+ Opcode, Len, MaskKey, Data, 0) ->
+ websocket_payload(State#state{frag_state={nofin, Opcode, <<>>}},
+ Req, HandlerState, 0, Len, MaskKey, <<>>, Data);
+%% Subsequent frame fragments.
+websocket_data(State=#state{frag_state={nofin, _, _}}, Req, HandlerState,
+ 0, Len, MaskKey, Data, 0) ->
+ websocket_payload(State, Req, HandlerState,
+ 0, Len, MaskKey, <<>>, Data);
+%% Final frame fragment.
+websocket_data(State=#state{frag_state={nofin, Opcode, SoFar}},
+ Req, HandlerState, 0, Len, MaskKey, Data, 1) ->
+ websocket_payload(State#state{frag_state={fin, Opcode, SoFar}},
+ Req, HandlerState, 0, Len, MaskKey, <<>>, Data);
+%% Unfragmented frame.
+websocket_data(State, Req, HandlerState, Opcode, Len, MaskKey, Data, 1) ->
+ websocket_payload(State, Req, HandlerState,
+ Opcode, Len, MaskKey, <<>>, Data).
+
+-spec websocket_payload(#state{}, Req, any(),
+ opcode(), non_neg_integer(), mask_key(), binary(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+%% Close control frames with a payload MUST contain a valid close code.
+websocket_payload(State, Req, HandlerState,
+ Opcode=8, Len, MaskKey, <<>>, << MaskedCode:2/binary, Rest/bits >>) ->
+ Unmasked = << Code:16 >> = websocket_unmask(MaskedCode, MaskKey, <<>>),
+ if Code < 1000; Code =:= 1004; Code =:= 1005; Code =:= 1006;
+ (Code > 1011) and (Code < 3000); Code > 4999 ->
+ websocket_close(State, Req, HandlerState, {error, badframe});
+ true ->
+ websocket_payload(State, Req, HandlerState,
+ Opcode, Len - 2, MaskKey, Unmasked, Rest)
+ end;
+%% Text frames and close control frames MUST have a payload that is valid UTF-8.
+websocket_payload(State=#state{utf8_state=Incomplete},
+ Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Data)
+ when (byte_size(Data) < Len) andalso ((Opcode =:= 1) orelse
+ ((Opcode =:= 8) andalso (Unmasked =/= <<>>))) ->
+ Unmasked2 = websocket_unmask(Data,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
+ case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
+ false ->
+ websocket_close(State, Req, HandlerState, {error, badencoding});
+ Utf8State ->
+ websocket_payload_loop(State#state{utf8_state=Utf8State},
+ Req, HandlerState, Opcode, Len - byte_size(Data), MaskKey,
+ << Unmasked/binary, Unmasked2/binary >>)
+ end;
+websocket_payload(State=#state{utf8_state=Incomplete},
+ Req, HandlerState, Opcode, Len, MaskKey, Unmasked, Data)
+ when Opcode =:= 1; (Opcode =:= 8) and (Unmasked =/= <<>>) ->
+ << End:Len/binary, Rest/bits >> = Data,
+ Unmasked2 = websocket_unmask(End,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
+ case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
+ <<>> ->
+ websocket_dispatch(State#state{utf8_state= <<>>},
+ Req, HandlerState, Rest, Opcode,
+ << Unmasked/binary, Unmasked2/binary >>);
+ _ ->
+ websocket_close(State, Req, HandlerState, {error, badencoding})
+ end;
+%% Fragmented text frames may cut payload in the middle of UTF-8 codepoints.
+websocket_payload(State=#state{frag_state={_, 1, _}, utf8_state=Incomplete},
+ Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, Data)
+ when byte_size(Data) < Len ->
+ Unmasked2 = websocket_unmask(Data,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
+ case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
+ false ->
+ websocket_close(State, Req, HandlerState, {error, badencoding});
+ Utf8State ->
+ websocket_payload_loop(State#state{utf8_state=Utf8State},
+ Req, HandlerState, Opcode, Len - byte_size(Data), MaskKey,
+ << Unmasked/binary, Unmasked2/binary >>)
+ end;
+websocket_payload(State=#state{frag_state={Fin, 1, _}, utf8_state=Incomplete},
+ Req, HandlerState, Opcode=0, Len, MaskKey, Unmasked, Data) ->
+ << End:Len/binary, Rest/bits >> = Data,
+ Unmasked2 = websocket_unmask(End,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), <<>>),
+ case is_utf8(<< Incomplete/binary, Unmasked2/binary >>) of
+ <<>> ->
+ websocket_dispatch(State#state{utf8_state= <<>>},
+ Req, HandlerState, Rest, Opcode,
+ << Unmasked/binary, Unmasked2/binary >>);
+ Utf8State when is_binary(Utf8State), Fin =:= nofin ->
+ websocket_dispatch(State#state{utf8_state=Utf8State},
+ Req, HandlerState, Rest, Opcode,
+ << Unmasked/binary, Unmasked2/binary >>);
+ _ ->
+ websocket_close(State, Req, HandlerState, {error, badencoding})
+ end;
+%% Other frames have a binary payload.
+websocket_payload(State, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked, Data)
+ when byte_size(Data) < Len ->
+ Unmasked2 = websocket_unmask(Data,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), Unmasked),
+ websocket_payload_loop(State, Req, HandlerState,
+ Opcode, Len - byte_size(Data), MaskKey, Unmasked2);
+websocket_payload(State, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked, Data) ->
+ << End:Len/binary, Rest/bits >> = Data,
+ Unmasked2 = websocket_unmask(End,
+ rotate_mask_key(MaskKey, byte_size(Unmasked)), Unmasked),
+ websocket_dispatch(State, Req, HandlerState, Rest, Opcode, Unmasked2).
+
+-spec websocket_unmask(B, mask_key(), B) -> B when B::binary().
+websocket_unmask(<<>>, _, Unmasked) ->
+ Unmasked;
+websocket_unmask(<< O:32, Rest/bits >>, MaskKey, Acc) ->
T = O bxor MaskKey,
- websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, Rest, MaskKey, << Acc/binary, T:32 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:24 >>, MaskKey, Acc) ->
+ websocket_unmask(Rest, MaskKey, << Acc/binary, T:32 >>);
+websocket_unmask(<< O:24 >>, MaskKey, Acc) ->
<< MaskKey2:24, _:8 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, << Acc/binary, T:24 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:16 >>, MaskKey, Acc) ->
+ << Acc/binary, T:24 >>;
+websocket_unmask(<< O:16 >>, MaskKey, Acc) ->
<< MaskKey2:16, _:16 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, << Acc/binary, T:16 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, << O:8 >>, MaskKey, Acc) ->
+ << Acc/binary, T:16 >>;
+websocket_unmask(<< O:8 >>, MaskKey, Acc) ->
<< MaskKey2:8, _:24 >> = << MaskKey:32 >>,
T = O bxor MaskKey2,
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, << Acc/binary, T:8 >>);
-websocket_unmask(State, Req, HandlerState, RemainingData,
- Opcode, <<>>, _MaskKey, Acc) ->
- websocket_dispatch(State, Req, HandlerState, RemainingData,
- Opcode, Acc).
+ << Acc/binary, T:8 >>.
+
+%% Because we unmask on the fly we need to continue from the right mask byte.
+-spec rotate_mask_key(mask_key(), non_neg_integer()) -> mask_key().
+rotate_mask_key(MaskKey, UnmaskedLen) ->
+ Left = UnmaskedLen rem 4,
+ Right = 4 - Left,
+ (MaskKey bsl (Left * 8)) + (MaskKey bsr (Right * 8)).
+
+%% Returns <<>> if the argument is valid UTF-8, false if not,
+%% or the incomplete part of the argument if we need more data.
+-spec is_utf8(binary()) -> false | binary().
+is_utf8(Valid = <<>>) ->
+ Valid;
+is_utf8(<< _/utf8, Rest/binary >>) ->
+ is_utf8(Rest);
+%% 2 bytes. Codepages C0 and C1 are invalid; fail early.
+is_utf8(<< 2#1100000:7, _/bits >>) ->
+ false;
+is_utf8(Incomplete = << 2#110:3, _:5 >>) ->
+ Incomplete;
+%% 3 bytes.
+is_utf8(Incomplete = << 2#1110:4, _:4 >>) ->
+ Incomplete;
+is_utf8(Incomplete = << 2#1110:4, _:4, 2#10:2, _:6 >>) ->
+ Incomplete;
+%% 4 bytes. Codepage F4 may have invalid values greater than 0x10FFFF.
+is_utf8(<< 2#11110100:8, 2#10:2, High:6, _/bits >>) when High >= 2#10000 ->
+ false;
+is_utf8(Incomplete = << 2#11110:5, _:3 >>) ->
+ Incomplete;
+is_utf8(Incomplete = << 2#11110:5, _:3, 2#10:2, _:6 >>) ->
+ Incomplete;
+is_utf8(Incomplete = << 2#11110:5, _:3, 2#10:2, _:6, 2#10:2, _:6 >>) ->
+ Incomplete;
+%% Invalid.
+is_utf8(_) ->
+ false.
+
+-spec websocket_payload_loop(#state{}, Req, any(),
+ opcode(), non_neg_integer(), mask_key(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+websocket_payload_loop(State=#state{socket=Socket, transport=Transport,
+ messages={OK, Closed, Error}, timeout_ref=TRef},
+ Req, HandlerState, Opcode, Len, MaskKey, Unmasked) ->
+ Transport:setopts(Socket, [{active, once}]),
+ receive
+ {OK, Socket, Data} ->
+ State2 = handler_loop_timeout(State),
+ websocket_payload(State2, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked, Data);
+ {Closed, Socket} ->
+ handler_terminate(State, Req, HandlerState, {error, closed});
+ {Error, Socket, Reason} ->
+ handler_terminate(State, Req, HandlerState, {error, Reason});
+ {timeout, TRef, ?MODULE} ->
+ websocket_close(State, Req, HandlerState, {normal, timeout});
+ {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) ->
+ websocket_payload_loop(State, Req, HandlerState,
+ Opcode, Len, MaskKey, Unmasked);
+ Message ->
+ handler_call(State, Req, HandlerState,
+ <<>>, websocket_info, Message,
+ fun (State2, Req2, HandlerState2, _) ->
+ websocket_payload_loop(State2, Req2, HandlerState2,
+ Opcode, Len, MaskKey, Unmasked)
+ end)
+ end.
-%% hybi dispatching.
--spec websocket_dispatch(#state{}, cowboy_req:req(), any(), binary(),
- opcode(), binary()) -> closed.
-%% First frame of a fragmented message unmasked. Expect intermediate or last.
-websocket_dispatch(State=#state{frag_state={nofin, Opcode}}, Req, HandlerState,
- RemainingData, 0, Payload) ->
- websocket_data(State#state{frag_state={nofin, Opcode, Payload}},
- Req, HandlerState, RemainingData);
-%% Intermediate frame of a fragmented message unmasked. Add payload to buffer.
-websocket_dispatch(State=#state{frag_state={nofin, Opcode, Payloads}}, Req,
- HandlerState, RemainingData, 0, Payload) ->
+-spec websocket_dispatch(#state{}, Req, any(), binary(), opcode(), binary())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+%% Continuation frame.
+websocket_dispatch(State=#state{frag_state={nofin, Opcode, SoFar}},
+ Req, HandlerState, RemainingData, 0, Payload) ->
websocket_data(State#state{frag_state={nofin, Opcode,
- <<Payloads/binary, Payload/binary>>}}, Req, HandlerState,
- RemainingData);
-%% Last frame of a fragmented message unmasked. Dispatch to handler.
-websocket_dispatch(State=#state{frag_state={fin, Opcode, Payloads}}, Req,
- HandlerState, RemainingData, 0, Payload) ->
+ << SoFar/binary, Payload/binary >>}}, Req, HandlerState, RemainingData);
+%% Last continuation frame.
+websocket_dispatch(State=#state{frag_state={fin, Opcode, SoFar}},
+ Req, HandlerState, RemainingData, 0, Payload) ->
websocket_dispatch(State#state{frag_state=undefined}, Req, HandlerState,
- RemainingData, Opcode, <<Payloads/binary, Payload/binary>>);
+ RemainingData, Opcode, << SoFar/binary, Payload/binary >>);
%% Text frame.
websocket_dispatch(State, Req, HandlerState, RemainingData, 1, Payload) ->
handler_call(State, Req, HandlerState, RemainingData,
@@ -431,13 +501,15 @@ websocket_dispatch(State, Req, HandlerState, RemainingData, 2, Payload) ->
handler_call(State, Req, HandlerState, RemainingData,
websocket_handle, {binary, Payload}, fun websocket_data/4);
%% Close control frame.
-%% @todo Handle the optional Payload.
-websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, _Payload) ->
- websocket_close(State, Req, HandlerState, {normal, closed});
+websocket_dispatch(State, Req, HandlerState, _RemainingData, 8, <<>>) ->
+ websocket_close(State, Req, HandlerState, {remote, closed});
+websocket_dispatch(State, Req, HandlerState, _RemainingData, 8,
+ << Code:16, Payload/bits >>) ->
+ websocket_close(State, Req, HandlerState, {remote, Code, Payload});
%% Ping control frame. Send a pong back and forward the ping to the handler.
websocket_dispatch(State=#state{socket=Socket, transport=Transport},
Req, HandlerState, RemainingData, 9, Payload) ->
- Len = hybi_payload_length(byte_size(Payload)),
+ Len = payload_length_to_binary(byte_size(Payload)),
Transport:send(Socket, << 1:1, 0:3, 10:4, 0:1, Len/bits, Payload/binary >>),
handler_call(State, Req, HandlerState, RemainingData,
websocket_handle, {ping, Payload}, fun websocket_data/4);
@@ -446,10 +518,12 @@ websocket_dispatch(State, Req, HandlerState, RemainingData, 10, Payload) ->
handler_call(State, Req, HandlerState, RemainingData,
websocket_handle, {pong, Payload}, fun websocket_data/4).
--spec handler_call(#state{}, cowboy_req:req(), any(), binary(),
- atom(), any(), fun()) -> closed.
-handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
- RemainingData, Callback, Message, NextState) ->
+-spec handler_call(#state{}, Req, any(), binary(), atom(), any(), fun())
+ -> {ok, Req, cowboy_middleware:env()}
+ | {suspend, module(), atom(), [any()]}
+ when Req::cowboy_req:req().
+handler_call(State=#state{handler=Handler, handler_opts=HandlerOpts}, Req,
+ HandlerState, RemainingData, Callback, Message, NextState) ->
try Handler:Callback(Message, Req, HandlerState) of
{ok, Req2, HandlerState2} ->
NextState(State, Req2, HandlerState2, RemainingData);
@@ -515,7 +589,7 @@ handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
" for the reason ~p:~p~n** Message was ~p~n"
"** Options were ~p~n** Handler state was ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, Callback, 3, Class, Reason, Message, Opts,
+ [Handler, Callback, 3, Class, Reason, Message, HandlerOpts,
HandlerState, PLReq, erlang:get_stacktrace()]),
websocket_close(State, Req, HandlerState, {error, handler})
end.
@@ -528,13 +602,6 @@ websocket_opcode(pong) -> 10.
-spec websocket_send(frame(), #state{})
-> ok | shutdown | {error, atom()}.
-%% hixie-76 text frame.
-websocket_send({text, Payload}, #state{
- socket=Socket, transport=Transport, version=0}) ->
- Transport:send(Socket, [0, Payload, 255]);
-%% Ignore all unknown frame types for compatibility with hixie 76.
-websocket_send(_Any, #state{version=0}) ->
- ok;
websocket_send(Type, #state{socket=Socket, transport=Transport})
when Type =:= close ->
Opcode = websocket_opcode(Type),
@@ -554,7 +621,7 @@ websocket_send({Type = close, StatusCode, Payload}, #state{
Len = 2 + iolist_size(Payload),
%% Control packets must not be > 125 in length.
true = Len =< 125,
- BinLen = hybi_payload_length(Len),
+ BinLen = payload_length_to_binary(Len),
Transport:send(Socket,
[<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits, StatusCode:16 >>, Payload]),
shutdown;
@@ -567,7 +634,7 @@ websocket_send({Type, Payload}, #state{socket=Socket, transport=Transport}) ->
true ->
true
end,
- BinLen = hybi_payload_length(Len),
+ BinLen = payload_length_to_binary(Len),
Transport:send(Socket,
[<< 1:1, 0:3, Opcode:4, 0:1, BinLen/bits >>, Payload]).
@@ -582,20 +649,32 @@ websocket_send_many([Frame|Tail], State) ->
Error -> Error
end.
--spec websocket_close(#state{}, cowboy_req:req(), any(), {atom(), atom()})
- -> closed.
-websocket_close(State=#state{socket=Socket, transport=Transport, version=0},
- Req, HandlerState, Reason) ->
- Transport:send(Socket, << 255, 0 >>),
- handler_terminate(State, Req, HandlerState, Reason);
+-spec websocket_close(#state{}, Req, any(),
+ {atom(), atom()} | {remote, close_code(), binary()})
+ -> {ok, Req, cowboy_middleware:env()}
+ when Req::cowboy_req:req().
websocket_close(State=#state{socket=Socket, transport=Transport},
Req, HandlerState, Reason) ->
- Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>),
+ case Reason of
+ {normal, _} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1000:16 >>);
+ {error, badframe} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1002:16 >>);
+ {error, badencoding} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1007:16 >>);
+ {error, handler} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, 1011:16 >>);
+ {remote, closed} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>);
+ {remote, Code, _} ->
+ Transport:send(Socket, << 1:1, 0:3, 8:4, 0:1, 2:7, Code:16 >>)
+ end,
handler_terminate(State, Req, HandlerState, Reason).
--spec handler_terminate(#state{}, cowboy_req:req(),
- any(), atom() | {atom(), atom()}) -> closed.
-handler_terminate(#state{handler=Handler, opts=Opts},
+-spec handler_terminate(#state{}, Req, any(), atom() | {atom(), atom()})
+ -> {ok, Req, cowboy_middleware:env()}
+ when Req::cowboy_req:req().
+handler_terminate(#state{env=Env, handler=Handler, handler_opts=HandlerOpts},
Req, HandlerState, TerminateReason) ->
try
Handler:websocket_terminate(TerminateReason, Req, HandlerState)
@@ -606,35 +685,14 @@ handler_terminate(#state{handler=Handler, opts=Opts},
" for the reason ~p:~p~n** Initial reason was ~p~n"
"** Options were ~p~n** Handler state was ~p~n"
"** Request was ~p~n** Stacktrace: ~p~n~n",
- [Handler, websocket_terminate, 3, Class, Reason, TerminateReason, Opts,
- HandlerState, PLReq, erlang:get_stacktrace()])
+ [Handler, websocket_terminate, 3, Class, Reason, TerminateReason,
+ HandlerOpts, HandlerState, PLReq, erlang:get_stacktrace()])
end,
- closed.
-
-%% hixie-76 specific.
-
--spec hixie76_challenge(binary(), binary(), binary()) -> binary().
-hixie76_challenge(Key1, Key2, Key3) ->
- IntKey1 = hixie76_key_to_integer(Key1),
- IntKey2 = hixie76_key_to_integer(Key2),
- erlang:md5(<< IntKey1:32, IntKey2:32, Key3/binary >>).
-
--spec hixie76_key_to_integer(binary()) -> integer().
-hixie76_key_to_integer(Key) ->
- Number = list_to_integer([C || << C >> <= Key, C >= $0, C =< $9]),
- Spaces = length([C || << C >> <= Key, C =:= 32]),
- Number div Spaces.
-
-%% hybi specific.
-
--spec hybi_challenge(binary()) -> binary().
-hybi_challenge(Key) ->
- Bin = << Key/binary, "258EAFA5-E914-47DA-95CA-C5AB0DC85B11" >>,
- base64:encode(crypto:sha(Bin)).
+ {ok, Req, [{result, closed}|Env]}.
--spec hybi_payload_length(0..16#7fffffffffffffff)
+-spec payload_length_to_binary(0..16#7fffffffffffffff)
-> << _:7 >> | << _:23 >> | << _:71 >>.
-hybi_payload_length(N) ->
+payload_length_to_binary(N) ->
case N of
N when N =< 125 -> << N:7 >>;
N when N =< 16#ffff -> << 126:7, N:16 >>;
diff --git a/src/cowboy_websocket_handler.erl b/src/cowboy_websocket_handler.erl
index 6d7f9de..bd2ed5a 100644
--- a/src/cowboy_websocket_handler.erl
+++ b/src/cowboy_websocket_handler.erl
@@ -1,4 +1,4 @@
-%% Copyright (c) 2011-2012, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011-2013, 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
@@ -50,7 +50,7 @@
-type opts() :: any().
-type state() :: any().
--type terminate_reason() :: {normal, closed}
+-type terminate_reason() :: {normal, shutdown}
| {normal, timeout}
| {error, closed}
| {error, badframe}