diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | include/http.hrl | 9 | ||||
-rw-r--r-- | src/cowboy_bstr.erl | 86 | ||||
-rw-r--r-- | src/cowboy_dispatcher.erl | 38 | ||||
-rw-r--r-- | src/cowboy_http.erl | 124 | ||||
-rw-r--r-- | src/cowboy_http_protocol.erl | 143 | ||||
-rw-r--r-- | src/cowboy_http_req.erl | 105 | ||||
-rw-r--r-- | src/cowboy_http_websocket.erl | 31 | ||||
-rw-r--r-- | src/cowboy_protocol.erl | 61 | ||||
-rw-r--r-- | test/http_SUITE.erl | 20 | ||||
-rw-r--r-- | test/websocket_handler.erl | 3 |
11 files changed, 447 insertions, 175 deletions
@@ -34,4 +34,4 @@ dialyze: -Wrace_conditions -Wunmatched_returns # -Wunderspecs docs: - @$(REBAR) doc + @$(REBAR) doc skip_deps=true diff --git a/include/http.hrl b/include/http.hrl index 7e0380d..3178381 100644 --- a/include/http.hrl +++ b/include/http.hrl @@ -47,17 +47,18 @@ method = 'GET' :: http_method(), version = {1, 1} :: http_version(), peer = undefined :: undefined | {inet:ip_address(), inet:ip_port()}, - host = undefined :: undefined | cowboy_dispatcher:path_tokens(), - host_info = undefined :: undefined | cowboy_dispatcher:path_tokens(), + host = undefined :: undefined | cowboy_dispatcher:tokens(), + host_info = undefined :: undefined | cowboy_dispatcher:tokens(), raw_host = undefined :: undefined | binary(), port = undefined :: undefined | inet:ip_port(), - path = undefined :: undefined | '*' | cowboy_dispatcher:path_tokens(), - path_info = undefined :: undefined | cowboy_dispatcher:path_tokens(), + path = undefined :: undefined | '*' | cowboy_dispatcher:tokens(), + path_info = undefined :: undefined | cowboy_dispatcher:tokens(), raw_path = undefined :: undefined | binary(), qs_vals = undefined :: undefined | list({binary(), binary() | true}), raw_qs = undefined :: undefined | binary(), bindings = undefined :: undefined | cowboy_dispatcher:bindings(), headers = [] :: http_headers(), + p_headers = [] :: [any()], %% @todo Improve those specs. cookies = undefined :: undefined | http_cookies(), %% Request body. 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. diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 2208256..02d6210 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -119,7 +119,7 @@ headers_dupe(Config) -> {ok, Data} = gen_tcp:recv(Socket, 0, 6000), {_Start, _Length} = binary:match(Data, <<"Connection: close">>), nomatch = binary:match(Data, <<"Connection: keep-alive">>), - ok = gen_tcp:close(Socket). + {error, closed} = gen_tcp:recv(Socket, 0, 1000). headers_huge(Config) -> Cookie = lists:flatten(["whatever_man_biiiiiiiiiiiig_cookie_me_want_77=" @@ -199,10 +199,14 @@ raw_req(Packet, Config) -> {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}, {packet, raw}]), ok = gen_tcp:send(Socket, Packet), - {ok, << "HTTP/1.1 ", Str:24/bits, _Rest/bits >>} - = gen_tcp:recv(Socket, 0, 6000), + Res = case gen_tcp:recv(Socket, 0, 6000) of + {ok, << "HTTP/1.1 ", Str:24/bits, _Rest/bits >>} -> + list_to_integer(binary_to_list(Str)); + {error, Reason} -> + Reason + end, gen_tcp:close(Socket), - {Packet, list_to_integer(binary_to_list(Str))}. + {Packet, Res}. raw(Config) -> Tests = [ @@ -211,10 +215,10 @@ raw(Config) -> {"Garbage\r\n\r\n", 400}, {"\r\n\r\n\r\n\r\n\r\n\r\n", 400}, {"GET / HTTP/1.1\r\nHost: dev-extend.eu\r\n\r\n", 400}, - {"", 408}, - {"\r\n", 408}, - {"\r\n\r\n", 408}, - {"GET / HTTP/1.1", 408}, + {"", closed}, + {"\r\n", closed}, + {"\r\n\r\n", closed}, + {"GET / HTTP/1.1", closed}, {"GET / HTTP/1.1\r\n", 408}, {"GET / HTTP/1.1\r\nHost: localhost", 408}, {"GET / HTTP/1.1\r\nHost: localhost\r\n", 408}, diff --git a/test/websocket_handler.erl b/test/websocket_handler.erl index 4ba2a67..0cfc8f3 100644 --- a/test/websocket_handler.erl +++ b/test/websocket_handler.erl @@ -18,7 +18,8 @@ terminate(_Req, _State) -> websocket_init(_TransportName, Req, _Opts) -> erlang:start_timer(1000, self(), <<"websocket_init">>), - {ok, Req, undefined}. + Req2 = cowboy_http_req:compact(Req), + {ok, Req2, undefined}. websocket_handle({text, Data}, Req, State) -> {reply, {text, Data}, Req, State}; |