aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cowboy_bstr.erl86
-rw-r--r--src/cowboy_dispatcher.erl38
-rw-r--r--src/cowboy_http.erl124
-rw-r--r--src/cowboy_http_protocol.erl143
-rw-r--r--src/cowboy_http_req.erl105
-rw-r--r--src/cowboy_http_websocket.erl31
-rw-r--r--src/cowboy_protocol.erl61
7 files changed, 427 insertions, 161 deletions
diff --git a/src/cowboy_bstr.erl b/src/cowboy_bstr.erl
new file mode 100644
index 0000000..1c702ef
--- /dev/null
+++ b/src/cowboy_bstr.erl
@@ -0,0 +1,86 @@
+%% Copyright (c) 2011, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% @doc Binary string manipulation.
+-module(cowboy_bstr).
+
+-export([to_lower/1]). %% Binary strings.
+-export([char_to_lower/1, char_to_upper/1]). %% Characters.
+
+%% @doc Convert a binary string to lowercase.
+-spec to_lower(binary()) -> binary().
+to_lower(L) ->
+ << << (char_to_lower(C)) >> || << C >> <= L >>.
+
+%% @doc Convert [A-Z] characters to lowercase.
+%% @end
+%% We gain noticeable speed by matching each value directly.
+-spec char_to_lower(char()) -> char().
+char_to_lower($A) -> $a;
+char_to_lower($B) -> $b;
+char_to_lower($C) -> $c;
+char_to_lower($D) -> $d;
+char_to_lower($E) -> $e;
+char_to_lower($F) -> $f;
+char_to_lower($G) -> $g;
+char_to_lower($H) -> $h;
+char_to_lower($I) -> $i;
+char_to_lower($J) -> $j;
+char_to_lower($K) -> $k;
+char_to_lower($L) -> $l;
+char_to_lower($M) -> $m;
+char_to_lower($N) -> $n;
+char_to_lower($O) -> $o;
+char_to_lower($P) -> $p;
+char_to_lower($Q) -> $q;
+char_to_lower($R) -> $r;
+char_to_lower($S) -> $s;
+char_to_lower($T) -> $t;
+char_to_lower($U) -> $u;
+char_to_lower($V) -> $v;
+char_to_lower($W) -> $w;
+char_to_lower($X) -> $x;
+char_to_lower($Y) -> $y;
+char_to_lower($Z) -> $z;
+char_to_lower(Ch) -> Ch.
+
+%% @doc Convert [a-z] characters to uppercase.
+-spec char_to_upper(char()) -> char().
+char_to_upper($a) -> $A;
+char_to_upper($b) -> $B;
+char_to_upper($c) -> $C;
+char_to_upper($d) -> $D;
+char_to_upper($e) -> $E;
+char_to_upper($f) -> $F;
+char_to_upper($g) -> $G;
+char_to_upper($h) -> $H;
+char_to_upper($i) -> $I;
+char_to_upper($j) -> $J;
+char_to_upper($k) -> $K;
+char_to_upper($l) -> $L;
+char_to_upper($m) -> $M;
+char_to_upper($n) -> $N;
+char_to_upper($o) -> $O;
+char_to_upper($p) -> $P;
+char_to_upper($q) -> $Q;
+char_to_upper($r) -> $R;
+char_to_upper($s) -> $S;
+char_to_upper($t) -> $T;
+char_to_upper($u) -> $U;
+char_to_upper($v) -> $V;
+char_to_upper($w) -> $W;
+char_to_upper($x) -> $X;
+char_to_upper($y) -> $Y;
+char_to_upper($z) -> $Z;
+char_to_upper(Ch) -> Ch.
diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl
index c402e01..67ea34b 100644
--- a/src/cowboy_dispatcher.erl
+++ b/src/cowboy_dispatcher.erl
@@ -19,13 +19,13 @@
-export([split_host/1, split_path/1, match/3]). %% API.
-type bindings() :: list({atom(), binary()}).
--type path_tokens() :: list(binary()).
+-type tokens() :: list(binary()).
-type match_rule() :: '_' | '*' | list(binary() | '_' | '...' | atom()).
-type dispatch_path() :: list({match_rule(), module(), any()}).
-type dispatch_rule() :: {Host::match_rule(), Path::dispatch_path()}.
-type dispatch_rules() :: list(dispatch_rule()).
--export_type([bindings/0, path_tokens/0, dispatch_rules/0]).
+-export_type([bindings/0, tokens/0, dispatch_rules/0]).
-include_lib("eunit/include/eunit.hrl").
@@ -33,7 +33,7 @@
%% @doc Split a hostname into a list of tokens.
-spec split_host(binary())
- -> {path_tokens(), binary(), undefined | inet:ip_port()}.
+ -> {tokens(), binary(), undefined | inet:ip_port()}.
split_host(<<>>) ->
{[], <<>>, undefined};
split_host(Host) ->
@@ -45,8 +45,12 @@ split_host(Host) ->
list_to_integer(binary_to_list(Port))}
end.
-%% @doc Split a path into a list of tokens.
--spec split_path(binary()) -> {path_tokens(), binary(), binary()}.
+%% @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(), binary(), binary()}.
split_path(Path) ->
case binary:split(Path, <<"?">>) of
[Path] -> {do_split_path(Path, <<"/">>), Path, <<>>};
@@ -54,7 +58,7 @@ split_path(Path) ->
[Path2, Qs] -> {do_split_path(Path2, <<"/">>), Path2, Qs}
end.
--spec do_split_path(binary(), <<_:8>>) -> path_tokens().
+-spec do_split_path(binary(), <<_:8>>) -> tokens().
do_split_path(RawPath, Separator) ->
EncodedPath = case binary:split(RawPath, Separator, [global, trim]) of
[<<>>|Path] -> Path;
@@ -89,10 +93,10 @@ do_split_path(RawPath, Separator) ->
%% 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(Host::path_tokens(), Path::path_tokens(), dispatch_rules())
+-spec match(Host::tokens(), Path::tokens(), dispatch_rules())
-> {ok, module(), any(), bindings(),
- HostInfo::undefined | path_tokens(),
- PathInfo::undefined | path_tokens()}
+ HostInfo::undefined | tokens(),
+ PathInfo::undefined | tokens()}
| {error, notfound, host} | {error, notfound, path}.
match(_Host, _Path, []) ->
{error, notfound, host};
@@ -108,11 +112,11 @@ match(Host, Path, [{HostMatch, PathMatchs}|Tail]) ->
match_path(Path, PathMatchs, HostBinds, lists:reverse(HostInfo))
end.
--spec match_path(path_tokens(), dispatch_path(), bindings(),
- HostInfo::undefined | path_tokens())
+-spec match_path(tokens(), dispatch_path(), bindings(),
+ HostInfo::undefined | tokens())
-> {ok, module(), any(), bindings(),
- HostInfo::undefined | path_tokens(),
- PathInfo::undefined | path_tokens()}
+ HostInfo::undefined | tokens(),
+ PathInfo::undefined | tokens()}
| {error, notfound, path}.
match_path(_Path, [], _HostBinds, _HostInfo) ->
{error, notfound, path};
@@ -130,15 +134,15 @@ match_path(Path, [{PathMatch, Handler, Opts}|Tail], HostBinds, HostInfo) ->
%% Internal.
--spec try_match(host | path, path_tokens(), match_rule())
- -> {true, bindings(), undefined | path_tokens()} | false.
+-spec try_match(host | path, tokens(), match_rule())
+ -> {true, bindings(), undefined | tokens()} | false.
try_match(host, List, Match) ->
list_match(lists:reverse(List), lists:reverse(Match), []);
try_match(path, List, Match) ->
list_match(List, Match, []).
--spec list_match(path_tokens(), match_rule(), bindings())
- -> {true, bindings(), undefined | path_tokens()} | false.
+-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};
diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl
new file mode 100644
index 0000000..8d60f82
--- /dev/null
+++ b/src/cowboy_http.erl
@@ -0,0 +1,124 @@
+%% Copyright (c) 2011, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+-module(cowboy_http).
+
+%% Parsing.
+-export([parse_tokens_list/1]).
+
+%% Interpretation.
+-export([connection_to_atom/1]).
+
+-include("include/http.hrl").
+-include_lib("eunit/include/eunit.hrl").
+
+%% Parsing.
+
+%% @doc Parse a list of tokens, as is often found in HTTP headers.
+%%
+%% From the RFC:
+%% <blockquote>Wherever this construct is used, null elements are allowed,
+%% but do not contribute to the count of elements present.
+%% That is, "(element), , (element) " is permitted, but counts
+%% as only two elements. Therefore, where at least one element is required,
+%% at least one non-null element MUST be present.</blockquote>
+-spec parse_tokens_list(binary()) -> [binary()] | {error, badarg}.
+parse_tokens_list(Value) ->
+ case parse_tokens_list(Value, ws_or_sep, <<>>, []) of
+ {error, badarg} ->
+ {error, badarg};
+ L when length(L) =:= 0 ->
+ {error, badarg};
+ L ->
+ lists:reverse(L)
+ end.
+
+-spec parse_tokens_list(binary(), token | ws | ws_or_sep, binary(),
+ [binary()]) -> [binary()] | {error, badarg}.
+parse_tokens_list(<<>>, token, Token, Acc) ->
+ [Token|Acc];
+parse_tokens_list(<< C, Rest/bits >>, token, Token, Acc)
+ when C =:= $\s; C =:= $\t ->
+ parse_tokens_list(Rest, ws, <<>>, [Token|Acc]);
+parse_tokens_list(<< $,, Rest/bits >>, token, Token, Acc) ->
+ parse_tokens_list(Rest, ws_or_sep, <<>>, [Token|Acc]);
+parse_tokens_list(<< C, Rest/bits >>, token, Token, Acc) ->
+ parse_tokens_list(Rest, token, << Token/binary, C >>, Acc);
+parse_tokens_list(<< C, Rest/bits >>, ws, <<>>, Acc)
+ when C =:= $\s; C =:= $\t ->
+ parse_tokens_list(Rest, ws, <<>>, Acc);
+parse_tokens_list(<< $,, Rest/bits >>, ws, <<>>, Acc) ->
+ parse_tokens_list(Rest, ws_or_sep, <<>>, Acc);
+parse_tokens_list(<<>>, ws_or_sep, <<>>, Acc) ->
+ Acc;
+parse_tokens_list(<< C, Rest/bits >>, ws_or_sep, <<>>, Acc)
+ when C =:= $\s; C =:= $\t ->
+ parse_tokens_list(Rest, ws_or_sep, <<>>, Acc);
+parse_tokens_list(<< $,, Rest/bits >>, ws_or_sep, <<>>, Acc) ->
+ parse_tokens_list(Rest, ws_or_sep, <<>>, Acc);
+parse_tokens_list(<< C, Rest/bits >>, ws_or_sep, <<>>, Acc) ->
+ parse_tokens_list(Rest, token, << C >>, Acc);
+parse_tokens_list(_Value, _State, _Token, _Acc) ->
+ {error, badarg}.
+
+%% Interpretation.
+
+%% @doc Walk through a tokens list and return whether
+%% the connection is keepalive or closed.
+-spec connection_to_atom([binary()]) -> keepalive | close.
+connection_to_atom([]) ->
+ keepalive;
+connection_to_atom([<<"keep-alive">>|_Tail]) ->
+ keepalive;
+connection_to_atom([<<"close">>|_Tail]) ->
+ close;
+connection_to_atom([Connection|Tail]) ->
+ case cowboy_bstr:to_lower(Connection) of
+ <<"close">> -> close;
+ <<"keep-alive">> -> keepalive;
+ _Any -> connection_to_atom(Tail)
+ end.
+
+%% Tests.
+
+-ifdef(TEST).
+
+parse_tokens_list_test_() ->
+ %% {Value, Result}
+ Tests = [
+ {<<>>, {error, badarg}},
+ {<<" ">>, {error, badarg}},
+ {<<" , ">>, {error, badarg}},
+ {<<",,,">>, {error, badarg}},
+ {<<"a b">>, {error, badarg}},
+ {<<"a , , , ">>, [<<"a">>]},
+ {<<" , , , a">>, [<<"a">>]},
+ {<<"a, , b">>, [<<"a">>, <<"b">>]},
+ {<<"close">>, [<<"close">>]},
+ {<<"keep-alive, upgrade">>, [<<"keep-alive">>, <<"upgrade">>]}
+ ],
+ [{V, fun() -> R = parse_tokens_list(V) end} || {V, R} <- Tests].
+
+connection_to_atom_test_() ->
+ %% {Tokens, Result}
+ Tests = [
+ {[<<"close">>], close},
+ {[<<"ClOsE">>], close},
+ {[<<"Keep-Alive">>], keepalive},
+ {[<<"Keep-Alive">>, <<"Upgrade">>], keepalive}
+ ],
+ [{lists:flatten(io_lib:format("~p", [T])),
+ fun() -> R = connection_to_atom(T) end} || {T, R} <- Tests].
+
+-endif.
diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl
index fe5f375..0a6bddf 100644
--- a/src/cowboy_http_protocol.erl
+++ b/src/cowboy_http_protocol.erl
@@ -20,7 +20,7 @@
%% <dt>dispatch</dt><dd>The dispatch list for this protocol.</dd>
%% <dt>max_empty_lines</dt><dd>Max number of empty lines before a request.
%% Defaults to 5.</dd>
-%% <dt>timeout</dt><dd>Time in milliseconds before an idle keep-alive
+%% <dt>timeout</dt><dd>Time in milliseconds before an idle
%% connection is closed. Defaults to 5000 milliseconds.</dd>
%% </dl>
%%
@@ -30,6 +30,7 @@
%% @see cowboy_dispatcher
%% @see cowboy_http_handler
-module(cowboy_http_protocol).
+-behaviour(cowboy_protocol).
-export([start_link/4]). %% API.
-export([init/4, parse_request/1]). %% FSM.
@@ -46,7 +47,6 @@
req_empty_lines = 0 :: integer(),
max_empty_lines :: integer(),
timeout :: timeout(),
- connection = keepalive :: keepalive | close,
buffer = <<>> :: binary()
}).
@@ -77,7 +77,7 @@ parse_request(State=#state{buffer=Buffer}) ->
case erlang:decode_packet(http_bin, Buffer, []) of
{ok, Request, Rest} -> request(Request, State#state{buffer=Rest});
{more, _Length} -> wait_request(State);
- {error, _Reason} -> error_response(400, State)
+ {error, _Reason} -> error_terminate(400, State)
end.
-spec wait_request(#state{}) -> ok.
@@ -86,8 +86,7 @@ wait_request(State=#state{socket=Socket, transport=Transport,
case Transport:recv(Socket, 0, T) of
{ok, Data} -> parse_request(State#state{
buffer= << Buffer/binary, Data/binary >>});
- {error, timeout} -> error_terminate(408, State);
- {error, closed} -> terminate(State)
+ {error, _Reason} -> terminate(State)
end.
-spec request({http_request, http_method(), http_uri(),
@@ -103,15 +102,13 @@ request({http_request, Method, {abs_path, AbsPath}, Version},
ConnAtom = version_to_connection(Version),
parse_header(#http_req{socket=Socket, transport=Transport,
connection=ConnAtom, method=Method, version=Version,
- path=Path, raw_path=RawPath, raw_qs=Qs},
- State#state{connection=ConnAtom});
+ path=Path, raw_path=RawPath, raw_qs=Qs}, State);
request({http_request, Method, '*', Version},
State=#state{socket=Socket, transport=Transport}) ->
ConnAtom = version_to_connection(Version),
parse_header(#http_req{socket=Socket, transport=Transport,
connection=ConnAtom, method=Method, version=Version,
- path='*', raw_path= <<"*">>, raw_qs= <<>>},
- State#state{connection=ConnAtom});
+ path='*', raw_path= <<"*">>, raw_qs= <<>>}, State);
request({http_request, _Method, _URI, _Version}, State) ->
error_terminate(501, State);
request({http_error, <<"\r\n">>},
@@ -127,7 +124,7 @@ parse_header(Req, State=#state{buffer=Buffer}) ->
case erlang:decode_packet(httph_bin, Buffer, []) of
{ok, Header, Rest} -> header(Header, Req, State#state{buffer=Rest});
{more, _Length} -> wait_header(Req, State);
- {error, _Reason} -> error_response(400, State)
+ {error, _Reason} -> error_terminate(400, State)
end.
-spec wait_header(#http_req{}, #state{}) -> ok.
@@ -144,7 +141,7 @@ wait_header(Req, State=#state{socket=Socket,
| http_eoh, #http_req{}, #state{}) -> ok.
header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{
transport=Transport, host=undefined}, State) ->
- RawHost2 = binary_to_lower(RawHost),
+ RawHost2 = cowboy_bstr:to_lower(RawHost),
case catch cowboy_dispatcher:split_host(RawHost2) of
{Host, RawHost3, undefined} ->
Port = default_port(Transport:name()),
@@ -161,11 +158,13 @@ header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{
%% Ignore Host headers if we already have it.
header({http_header, _I, 'Host', _R, _V}, Req, State) ->
parse_header(Req, State);
-header({http_header, _I, 'Connection', _R, Connection}, Req, State) ->
- ConnAtom = connection_to_atom(Connection),
- parse_header(Req#http_req{connection=ConnAtom,
- headers=[{'Connection', Connection}|Req#http_req.headers]},
- State#state{connection=ConnAtom});
+header({http_header, _I, 'Connection', _R, Connection},
+ Req=#http_req{headers=Headers}, State) ->
+ Req2 = Req#http_req{headers=[{'Connection', Connection}|Headers]},
+ {tokens, ConnTokens, Req3}
+ = cowboy_http_req:parse_header('Connection', Req2),
+ ConnAtom = cowboy_http:connection_to_atom(ConnTokens),
+ parse_header(Req3#http_req{connection=ConnAtom}, State);
header({http_header, _I, Field, _R, Value}, Req, State) ->
Field2 = format_header(Field),
parse_header(Req#http_req{headers=[{Field2, Value}|Req#http_req.headers]},
@@ -252,11 +251,12 @@ handler_terminate(HandlerState, Req, #state{handler={Handler, Opts}}) ->
end.
-spec next_request(any(), #http_req{}, #state{}) -> ok.
-next_request(HandlerState, Req=#http_req{buffer=Buffer}, State) ->
+next_request(HandlerState, Req=#http_req{connection=Conn, buffer=Buffer},
+ State) ->
HandlerRes = handler_terminate(HandlerState, Req, State),
BodyRes = ensure_body_processed(Req),
- RespRes = ensure_response(Req, State),
- case {HandlerRes, BodyRes, RespRes, State#state.connection} of
+ RespRes = ensure_response(Req),
+ case {HandlerRes, BodyRes, RespRes, Conn} of
{ok, ok, ok, keepalive} ->
?MODULE:parse_request(State#state{
buffer=Buffer, req_empty_lines=0});
@@ -274,31 +274,28 @@ ensure_body_processed(Req=#http_req{body_state=waiting}) ->
_Any -> ok
end.
--spec ensure_response(#http_req{}, #state{}) -> ok.
+-spec ensure_response(#http_req{}) -> ok.
%% The handler has already fully replied to the client.
-ensure_response(#http_req{resp_state=done}, _State) ->
+ensure_response(#http_req{resp_state=done}) ->
ok;
%% No response has been sent but everything apparently went fine.
%% Reply with 204 No Content to indicate this.
-ensure_response(#http_req{resp_state=waiting}, State) ->
- error_response(204, State);
+ensure_response(Req=#http_req{resp_state=waiting}) ->
+ _ = cowboy_http_req:reply(204, [], [], Req),
+ ok;
%% Close the chunked reply.
+ensure_response(#http_req{method='HEAD', resp_state=chunks}) ->
+ close;
ensure_response(#http_req{socket=Socket, transport=Transport,
- resp_state=chunks}, _State) ->
+ resp_state=chunks}) ->
Transport:send(Socket, <<"0\r\n\r\n">>),
close.
--spec error_response(http_status(), #state{}) -> ok.
-error_response(Code, #state{socket=Socket,
- transport=Transport, connection=Connection}) ->
+-spec error_terminate(http_status(), #state{}) -> ok.
+error_terminate(Code, State=#state{socket=Socket, transport=Transport}) ->
_ = cowboy_http_req:reply(Code, [], [], #http_req{
socket=Socket, transport=Transport,
- connection=Connection, resp_state=waiting}),
- ok.
-
--spec error_terminate(http_status(), #state{}) -> ok.
-error_terminate(Code, State) ->
- error_response(Code, State#state{connection=close}),
+ connection=close, resp_state=waiting}),
terminate(State).
-spec terminate(#state{}) -> ok.
@@ -312,18 +309,6 @@ terminate(#state{socket=Socket, transport=Transport}) ->
version_to_connection({1, 1}) -> keepalive;
version_to_connection(_Any) -> close.
-%% @todo Connection can take more than one value.
--spec connection_to_atom(binary()) -> keepalive | close.
-connection_to_atom(<<"keep-alive">>) ->
- keepalive;
-connection_to_atom(<<"close">>) ->
- close;
-connection_to_atom(Connection) ->
- case binary_to_lower(Connection) of
- <<"close">> -> close;
- _Any -> keepalive
- end.
-
-spec default_port(atom()) -> 80 | 443.
default_port(ssl) -> 443;
default_port(_) -> 80.
@@ -346,73 +331,9 @@ format_header(<<>>, _Any, Acc) ->
format_header(<< $-, Rest/bits >>, Bool, Acc) ->
format_header(Rest, not Bool, << Acc/binary, $- >>);
format_header(<< C, Rest/bits >>, true, Acc) ->
- format_header(Rest, false, << Acc/binary, (char_to_upper(C)) >>);
+ format_header(Rest, false, << Acc/binary, (cowboy_bstr:char_to_upper(C)) >>);
format_header(<< C, Rest/bits >>, false, Acc) ->
- format_header(Rest, false, << Acc/binary, (char_to_lower(C)) >>).
-
-%% We are excluding a few characters on purpose.
--spec binary_to_lower(binary()) -> binary().
-binary_to_lower(L) ->
- << << (char_to_lower(C)) >> || << C >> <= L >>.
-
-%% We gain noticeable speed by matching each value directly.
--spec char_to_lower(char()) -> char().
-char_to_lower($A) -> $a;
-char_to_lower($B) -> $b;
-char_to_lower($C) -> $c;
-char_to_lower($D) -> $d;
-char_to_lower($E) -> $e;
-char_to_lower($F) -> $f;
-char_to_lower($G) -> $g;
-char_to_lower($H) -> $h;
-char_to_lower($I) -> $i;
-char_to_lower($J) -> $j;
-char_to_lower($K) -> $k;
-char_to_lower($L) -> $l;
-char_to_lower($M) -> $m;
-char_to_lower($N) -> $n;
-char_to_lower($O) -> $o;
-char_to_lower($P) -> $p;
-char_to_lower($Q) -> $q;
-char_to_lower($R) -> $r;
-char_to_lower($S) -> $s;
-char_to_lower($T) -> $t;
-char_to_lower($U) -> $u;
-char_to_lower($V) -> $v;
-char_to_lower($W) -> $w;
-char_to_lower($X) -> $x;
-char_to_lower($Y) -> $y;
-char_to_lower($Z) -> $z;
-char_to_lower(Ch) -> Ch.
-
--spec char_to_upper(char()) -> char().
-char_to_upper($a) -> $A;
-char_to_upper($b) -> $B;
-char_to_upper($c) -> $C;
-char_to_upper($d) -> $D;
-char_to_upper($e) -> $E;
-char_to_upper($f) -> $F;
-char_to_upper($g) -> $G;
-char_to_upper($h) -> $H;
-char_to_upper($i) -> $I;
-char_to_upper($j) -> $J;
-char_to_upper($k) -> $K;
-char_to_upper($l) -> $L;
-char_to_upper($m) -> $M;
-char_to_upper($n) -> $N;
-char_to_upper($o) -> $O;
-char_to_upper($p) -> $P;
-char_to_upper($q) -> $Q;
-char_to_upper($r) -> $R;
-char_to_upper($s) -> $S;
-char_to_upper($t) -> $T;
-char_to_upper($u) -> $U;
-char_to_upper($v) -> $V;
-char_to_upper($w) -> $W;
-char_to_upper($x) -> $X;
-char_to_upper($y) -> $Y;
-char_to_upper($z) -> $Z;
-char_to_upper(Ch) -> Ch.
+ format_header(Rest, false, << Acc/binary, (cowboy_bstr:char_to_lower(C)) >>).
%% Tests.
diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl
index 5b63599..a1a37bd 100644
--- a/src/cowboy_http_req.erl
+++ b/src/cowboy_http_req.erl
@@ -28,6 +28,7 @@
qs_val/2, qs_val/3, qs_vals/1, raw_qs/1,
binding/2, binding/3, bindings/1,
header/2, header/3, headers/1,
+ parse_header/2, parse_header/3,
cookie/2, cookie/3, cookies/1
]). %% Request API.
@@ -67,14 +68,14 @@ peer(Req) ->
{Req#http_req.peer, Req}.
%% @doc Return the tokens for the hostname requested.
--spec host(#http_req{}) -> {cowboy_dispatcher:path_tokens(), #http_req{}}.
+-spec host(#http_req{}) -> {cowboy_dispatcher:tokens(), #http_req{}}.
host(Req) ->
{Req#http_req.host, Req}.
%% @doc Return the extra host information obtained from partially matching
%% the hostname using <em>'...'</em>.
-spec host_info(#http_req{})
- -> {cowboy_dispatcher:path_tokens() | undefined, #http_req{}}.
+ -> {cowboy_dispatcher:tokens() | undefined, #http_req{}}.
host_info(Req) ->
{Req#http_req.host_info, Req}.
@@ -88,15 +89,19 @@ raw_host(Req) ->
port(Req) ->
{Req#http_req.port, Req}.
-%% @doc Return the tokens for the path requested.
--spec path(#http_req{}) -> {cowboy_dispatcher:path_tokens(), #http_req{}}.
+%% @doc Return the path segments for the path requested.
+%%
+%% 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 in the path requested.
+-spec path(#http_req{}) -> {cowboy_dispatcher:tokens(), #http_req{}}.
path(Req) ->
{Req#http_req.path, Req}.
%% @doc Return the extra path information obtained from partially matching
%% the patch using <em>'...'</em>.
-spec path_info(#http_req{})
- -> {cowboy_dispatcher:path_tokens() | undefined, #http_req{}}.
+ -> {cowboy_dispatcher:tokens() | undefined, #http_req{}}.
path_info(Req) ->
{Req#http_req.path_info, Req}.
@@ -178,6 +183,54 @@ header(Name, Req, Default) when is_atom(Name) orelse is_binary(Name) ->
headers(Req) ->
{Req#http_req.headers, Req}.
+%% @doc Semantically parse headers.
+%%
+%% When the value isn't found, a proper default value for the type
+%% returned is used as a return value.
+%% @see parse_header/3
+-spec parse_header(http_header(), #http_req{})
+ -> {tokens, [binary()], #http_req{}}
+ | {undefined, binary(), #http_req{}}
+ | {error, badarg}.
+parse_header('Connection', Req) ->
+ parse_header('Connection', Req, []);
+parse_header(Name, Req) ->
+ parse_header(Name, Req, undefined).
+
+%% @doc Semantically parse headers.
+%%
+%% When the header is known, a named tuple is returned containing
+%% {Type, P, Req} with Type being the type of value found in P.
+%% For example, the header 'Connection' is a list of tokens, therefore
+%% the value returned will be a list of binary values and Type will be
+%% 'tokens'.
+%%
+%% When the header is known but not found, the tuple {Type, Default, Req}
+%% is returned instead.
+%%
+%% When the header is unknown, the value is returned directly as an
+%% 'undefined' tagged tuple.
+-spec parse_header(http_header(), #http_req{}, any())
+ -> {tokens, [binary()], #http_req{}}
+ | {undefined, binary(), #http_req{}}
+ | {error, badarg}.
+parse_header(Name, Req=#http_req{p_headers=PHeaders}, Default)
+ when Name =:= 'Connection' ->
+ case header(Name, Req) of
+ {undefined, Req2} -> {tokens, Default, Req2};
+ {Value, Req2} ->
+ case cowboy_http:parse_tokens_list(Value) of
+ {error, badarg} ->
+ {error, badarg};
+ P ->
+ {tokens, P, Req2#http_req{
+ p_headers=[{Name, P}|PHeaders]}}
+ end
+ end;
+parse_header(Name, Req, Default) ->
+ {Value, Req2} = header(Name, Req, Default),
+ {undefined, Value, Req2}.
+
%% @equiv cookie(Name, Req, undefined)
-spec cookie(binary(), #http_req{})
-> {binary() | true | undefined, #http_req{}}.
@@ -265,6 +318,7 @@ body_qs(Req) ->
reply(Code, Headers, Body, Req=#http_req{socket=Socket,
transport=Transport, connection=Connection,
method=Method, resp_state=waiting}) ->
+ RespConn = response_connection(Headers, Connection),
Head = response_head(Code, Headers, [
{<<"Connection">>, atom_to_connection(Connection)},
{<<"Content-Length">>,
@@ -276,30 +330,23 @@ reply(Code, Headers, Body, Req=#http_req{socket=Socket,
'HEAD' -> Transport:send(Socket, Head);
_ -> Transport:send(Socket, [Head, Body])
end,
- {ok, Req#http_req{resp_state=done}}.
+ {ok, Req#http_req{connection=RespConn, resp_state=done}}.
%% @doc Initiate the sending of a chunked reply to the client.
%% @see cowboy_http_req:chunk/2
-spec chunked_reply(http_status(), http_headers(), #http_req{})
-> {ok, #http_req{}}.
chunked_reply(Code, Headers, Req=#http_req{socket=Socket, transport=Transport,
- method='HEAD', resp_state=waiting}) ->
+ connection=Connection, resp_state=waiting}) ->
+ RespConn = response_connection(Headers, Connection),
Head = response_head(Code, Headers, [
- {<<"Date">>, cowboy_clock:rfc1123()},
- {<<"Server">>, <<"Cowboy">>}
- ]),
- Transport:send(Socket, Head),
- {ok, Req#http_req{resp_state=done}};
-chunked_reply(Code, Headers, Req=#http_req{socket=Socket, transport=Transport,
- resp_state=waiting}) ->
- Head = response_head(Code, Headers, [
- {<<"Connection">>, <<"close">>},
+ {<<"Connection">>, atom_to_connection(Connection)},
{<<"Transfer-Encoding">>, <<"chunked">>},
{<<"Date">>, cowboy_clock:rfc1123()},
{<<"Server">>, <<"Cowboy">>}
]),
Transport:send(Socket, Head),
- {ok, Req#http_req{resp_state=chunks}}.
+ {ok, Req#http_req{connection=RespConn, resp_state=chunks}}.
%% @doc Send a chunk of data.
%%
@@ -321,7 +368,7 @@ chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) ->
-spec compact(#http_req{}) -> #http_req{}.
compact(Req) ->
Req#http_req{host=undefined, host_info=undefined, path=undefined,
- path_info=undefined, qs_vals=undefined, raw_qs=undefined,
+ path_info=undefined, qs_vals=undefined,
bindings=undefined, headers=[]}.
%% Internal.
@@ -336,13 +383,33 @@ parse_qs(Qs) ->
[Name, Value] -> {quoted:from_url(Name), quoted:from_url(Value)}
end || Token <- Tokens].
+-spec response_connection(http_headers(), keepalive | close)
+ -> keepalive | close.
+response_connection([], Connection) ->
+ Connection;
+response_connection([{Name, Value}|Tail], Connection) ->
+ case Name of
+ 'Connection' -> response_connection_parse(Value);
+ Name ->
+ Name2 = cowboy_bstr:to_lower(Name),
+ case Name2 of
+ <<"connection">> -> response_connection_parse(Value);
+ _Any -> response_connection(Tail, Connection)
+ end
+ end.
+
+-spec response_connection_parse(binary()) -> keepalive | close.
+response_connection_parse(ReplyConn) ->
+ Tokens = cowboy_http:parse_tokens_list(ReplyConn),
+ cowboy_http:connection_to_atom(Tokens).
+
-spec response_head(http_status(), http_headers(), http_headers()) -> iolist().
response_head(Code, Headers, DefaultHeaders) ->
StatusLine = <<"HTTP/1.1 ", (status(Code))/binary, "\r\n">>,
Headers2 = [{header_to_binary(Key), Value} || {Key, Value} <- Headers],
Headers3 = lists:keysort(1, Headers2),
Headers4 = lists:ukeymerge(1, Headers3, DefaultHeaders),
- Headers5 = [<< Key/binary, ": ", Value/binary, "\r\n" >>
+ Headers5 = [[Key, <<": ">>, Value, <<"\r\n">>]
|| {Key, Value} <- Headers4],
[StatusLine, Headers5, <<"\r\n">>].
diff --git a/src/cowboy_http_websocket.erl b/src/cowboy_http_websocket.erl
index e481d4b..61917b4 100644
--- a/src/cowboy_http_websocket.erl
+++ b/src/cowboy_http_websocket.erl
@@ -76,10 +76,9 @@ upgrade(ListenerPid, Handler, Opts, Req) ->
%% instead of having ugly code like this case here.
-spec websocket_upgrade(#state{}, #http_req{}) -> {ok, #state{}, #http_req{}}.
websocket_upgrade(State, Req) ->
- case cowboy_http_req:header('Connection', Req) of
- {<<"Upgrade">>, Req2} -> ok;
- {<<"keep-alive, Upgrade">>, Req2} -> ok %% @todo Temp. For Firefox 6.
- end,
+ {tokens, ConnTokens, Req2}
+ = cowboy_http_req:parse_header('Connection', Req),
+ true = lists:member(<<"Upgrade">>, ConnTokens),
{Version, Req3} = cowboy_http_req:header(<<"Sec-Websocket-Version">>, Req2),
websocket_upgrade(Version, State, Req3).
@@ -364,7 +363,7 @@ handler_call(State=#state{handler=Handler, opts=Opts}, Req, HandlerState,
%% hixie-76 text frame.
websocket_send({text, Payload}, #state{version=0},
#http_req{socket=Socket, transport=Transport}) ->
- Transport:send(Socket, << 0, Payload/binary, 255 >>);
+ Transport:send(Socket, [0, Payload, 255]);
%% Ignore all unknown frame types for compatibility with hixie 76.
websocket_send(_Any, #state{version=0}, _Req) ->
ignore;
@@ -376,9 +375,9 @@ websocket_send({Type, Payload}, _State,
ping -> 9;
pong -> 10
end,
- Len = hybi_payload_length(byte_size(Payload)),
- Transport:send(Socket, << 1:1, 0:3, Opcode:4,
- 0:1, Len/bits, Payload/binary >>).
+ Len = hybi_payload_length(iolist_size(Payload)),
+ Transport:send(Socket, [<< 1:1, 0:3, Opcode:4, 0:1, Len/bits >>,
+ Payload]).
-spec websocket_close(#state{}, #http_req{}, any(), {atom(), atom()}) -> ok.
websocket_close(State=#state{version=0}, Req=#http_req{socket=Socket,
@@ -427,19 +426,21 @@ hixie76_key_to_integer(Key) ->
-> binary().
hixie76_location(Protocol, Host, Port, Path, <<>>) ->
<< (hixie76_location_protocol(Protocol))/binary, "://", Host/binary,
- (hixie76_location_port(ssl, Port))/binary, Path/binary>>;
+ (hixie76_location_port(Protocol, Port))/binary, Path/binary>>;
hixie76_location(Protocol, Host, Port, Path, QS) ->
<< (hixie76_location_protocol(Protocol))/binary, "://", Host/binary,
- (hixie76_location_port(ssl, Port))/binary, Path/binary, "?", QS/binary >>.
+ (hixie76_location_port(Protocol, Port))/binary, Path/binary, "?", QS/binary >>.
-spec hixie76_location_protocol(atom()) -> binary().
hixie76_location_protocol(ssl) -> <<"wss">>;
hixie76_location_protocol(_) -> <<"ws">>.
+%% @todo We should add a secure/0 function to transports
+%% instead of relying on their name.
-spec hixie76_location_port(atom(), inet:ip_port()) -> binary().
hixie76_location_port(ssl, 443) ->
<<>>;
-hixie76_location_port(_, 80) ->
+hixie76_location_port(tcp, 80) ->
<<>>;
hixie76_location_port(_, Port) ->
<<":", (list_to_binary(integer_to_list(Port)))/binary>>.
@@ -466,11 +467,13 @@ hybi_payload_length(N) ->
hixie76_location_test() ->
?assertEqual(<<"ws://localhost/path">>,
- hixie76_location(other, <<"localhost">>, 80, <<"/path">>, <<>>)),
+ hixie76_location(tcp, <<"localhost">>, 80, <<"/path">>, <<>>)),
+ ?assertEqual(<<"ws://localhost:443/path">>,
+ hixie76_location(tcp, <<"localhost">>, 443, <<"/path">>, <<>>)),
?assertEqual(<<"ws://localhost:8080/path">>,
- hixie76_location(other, <<"localhost">>, 8080, <<"/path">>, <<>>)),
+ hixie76_location(tcp, <<"localhost">>, 8080, <<"/path">>, <<>>)),
?assertEqual(<<"ws://localhost:8080/path?dummy=2785">>,
- hixie76_location(other, <<"localhost">>, 8080, <<"/path">>, <<"dummy=2785">>)),
+ hixie76_location(tcp, <<"localhost">>, 8080, <<"/path">>, <<"dummy=2785">>)),
?assertEqual(<<"wss://localhost/path">>,
hixie76_location(ssl, <<"localhost">>, 443, <<"/path">>, <<>>)),
?assertEqual(<<"wss://localhost:8443/path">>,
diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl
new file mode 100644
index 0000000..9dc35d9
--- /dev/null
+++ b/src/cowboy_protocol.erl
@@ -0,0 +1,61 @@
+%% Copyright (c) 2011, Loïc Hoguin <[email protected]>
+%% Copyright (c) 2011, Michiel Hakvoort <[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 Cowboy protocol.
+%%
+%% A Cowboy protocol must implement one callback: <em>start_link/4</em>.
+%%
+%% <em>start_link/4</em> is meant for the initialization of the
+%% protocol process.
+%% It receives the pid to the listener's gen_server, the client socket,
+%% the module name of the chosen transport and the options defined when
+%% starting the listener. The <em>start_link/4</em> function must follow
+%% the supervisor start function specification.
+%%
+%% After initializing your protocol, it is recommended to wait to
+%% receive a message containing the atom 'shoot', as it will ensure
+%% Cowboy has been able to fully initialize the socket.
+%% Anything you do past this point is up to you!
+%%
+%% If you need to change some socket options, like enabling raw mode
+%% for example, you can call the <em>Transport:setopts/2</em> function.
+%% It is the protocol's responsability to manage the socket usage,
+%% there should be no need for an user to specify that kind of options
+%% while starting a listener.
+%%
+%% You should definitely look at the cowboy_http_protocol module for
+%% a great example of fast request handling if you need to.
+%% Otherwise it's probably safe to use <code>{active, once}</code> mode
+%% and handle everything as it comes.
+%%
+%% Note that while you technically can run a protocol handler directly
+%% as a gen_server or a gen_fsm, it's probably not a good idea,
+%% as the only call you'll ever receive from Cowboy is the
+%% <em>start_link/4</em> call. On the other hand, feel free to write
+%% a very basic protocol handler which then forwards requests to a
+%% gen_server or gen_fsm. By doing so however you must take care to
+%% supervise their processes as Cowboy only knows about the protocol
+%% handler itself.
+-module(cowboy_protocol).
+
+-export([behaviour_info/1]).
+
+%% @private
+-spec behaviour_info(_)
+ -> undefined | [{start_link, 4}, ...].
+behaviour_info(callbacks) ->
+ [{start_link, 4}];
+behaviour_info(_Other) ->
+ undefined.