diff options
-rw-r--r-- | CHANGELOG.md | 15 | ||||
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | examples/echo_get/src/toppage_handler.erl | 4 | ||||
-rw-r--r-- | examples/echo_post/src/toppage_handler.erl | 4 | ||||
-rw-r--r-- | rebar.config | 3 | ||||
-rw-r--r-- | rebar.tests.config | 3 | ||||
-rw-r--r-- | src/cowboy_clock.erl | 4 | ||||
-rw-r--r-- | src/cowboy_dispatcher.erl | 258 | ||||
-rw-r--r-- | src/cowboy_http.erl | 68 | ||||
-rw-r--r-- | src/cowboy_multipart.erl | 14 | ||||
-rw-r--r-- | src/cowboy_protocol.erl | 696 | ||||
-rw-r--r-- | src/cowboy_req.erl | 527 | ||||
-rw-r--r-- | src/cowboy_rest.erl | 57 | ||||
-rw-r--r-- | src/cowboy_static.erl | 4 | ||||
-rw-r--r-- | src/cowboy_websocket.erl | 15 | ||||
-rw-r--r-- | test/dispatcher_prop.erl | 68 | ||||
-rw-r--r-- | test/http_SUITE.erl | 40 | ||||
-rw-r--r-- | test/http_handler_init_shutdown.erl | 2 | ||||
-rw-r--r-- | test/http_handler_set_resp.erl | 4 | ||||
-rw-r--r-- | test/proper_SUITE.erl | 37 | ||||
-rw-r--r-- | test/rest_forbidden_resource.erl | 2 | ||||
-rw-r--r-- | test/rest_nodelete_resource.erl | 2 |
22 files changed, 943 insertions, 888 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index c6341bb..37d921e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ next * cowboy_req:delete_resp_header/2 deletes a previously set resp header. * cowboy_req:set_meta/3 sets metadata in the Req object. * cowboy_req:to_list/1 converts the Req object to a list of key/values. + * cowboy_req:fragment/1 returns the request URL fragment. * cowboy_req:host_url/1 returns the request URL without the path or qs. * cowboy_req:url/1 returns the full request URL. @@ -51,6 +52,18 @@ next {{body, Body}, Req}. * set_resp_* functions now return Req instead of {ok, Req}. +* Fix consistency issues caused by erlang:decode_packet/3 + + * The method is now always a case sensitive binary string. + * Note that standard method names are uppercase (e.g. <<"GET">>). + * Header names are now always lowercase binary string. + +* The max_line_length cowboy_protocol option was replaced by 3 new options: + + * max_request_line_length, defaults to 4096 bytes + * max_header_name_length, defaults to 64 bytes + * max_header_value_length, defaults to 4096 bytes + * Use -callback in behaviours * Add cowboy_protocol:onrequest_fun/0 and :onresponse_fun/0 types @@ -61,7 +74,7 @@ next * Avoid a duplicate HTTP reply in cowboy_websocket:upgrade_error/1 -* Avoid using proplists:get_value/{2,3} in a few places +* Many, many optimizations for the most critical code path 0.6.1 ----- @@ -43,10 +43,10 @@ eunit: @$(REBAR) -C rebar.tests.config eunit skip_deps=true ct: - @$(REBAR) -C rebar.tests.config ct skip_deps=true suites=http,proper,ws + @$(REBAR) -C rebar.tests.config ct skip_deps=true suites=http,ws intct: - @$(REBAR) -C rebar.tests.config ct skip_deps=true suites=http,proper,ws,autobahn + @$(REBAR) -C rebar.tests.config ct skip_deps=true suites=http,ws,autobahn # Dialyzer. diff --git a/examples/echo_get/src/toppage_handler.erl b/examples/echo_get/src/toppage_handler.erl index a8666dc..6d914ec 100644 --- a/examples/echo_get/src/toppage_handler.erl +++ b/examples/echo_get/src/toppage_handler.erl @@ -16,9 +16,9 @@ handle(Req, State) -> {ok, Req4} = echo(Method, Echo, Req3), {ok, Req4, State}. -echo('GET', undefined, Req) -> +echo(<<"GET">>, undefined, Req) -> cowboy_req:reply(400, [], <<"Missing echo parameter.">>, Req); -echo('GET', Echo, Req) -> +echo(<<"GET">>, Echo, Req) -> cowboy_req:reply(200, [{<<"Content-Encoding">>, <<"utf-8">>}], Echo, Req); echo(_, _, Req) -> diff --git a/examples/echo_post/src/toppage_handler.erl b/examples/echo_post/src/toppage_handler.erl index f8659c9..69aeb9f 100644 --- a/examples/echo_post/src/toppage_handler.erl +++ b/examples/echo_post/src/toppage_handler.erl @@ -16,11 +16,11 @@ handle(Req, State) -> {ok, Req4} = maybe_echo(Method, HasBody, Req3), {ok, Req4, State}. -maybe_echo('POST', true, Req) -> +maybe_echo(<<"POST">>, true, Req) -> {ok, PostVals, Req2} = cowboy_req:body_qs(Req), Echo = proplists:get_value(<<"echo">>, PostVals), echo(Echo, Req2); -maybe_echo('POST', false, Req) -> +maybe_echo(<<"POST">>, false, Req) -> cowboy_req:reply(400, [], <<"Missing body.">>, Req); maybe_echo(_, _, Req) -> %% Method not allowed. diff --git a/rebar.config b/rebar.config index c2317e7..d35044d 100644 --- a/rebar.config +++ b/rebar.config @@ -1,5 +1,6 @@ {deps, [ - {ranch, "0\\.4\\.0.*", {git, "git://github.com/extend/ranch.git", "0.4.0"}} + {ranch, "0\\.4\\.0.*", {git, "git://github.com/extend/ranch.git", + "cd099983b1b807b87fa050d1e4ff0a13aba25b49"}} ]}. {erl_opts, [ %% bin_opt_info, diff --git a/rebar.tests.config b/rebar.tests.config index 9315c7e..12aaba7 100644 --- a/rebar.tests.config +++ b/rebar.tests.config @@ -2,7 +2,8 @@ {deps, [ {proper, ".*", {git, "git://github.com/manopapad/proper.git", "master"}}, - {ranch, "0\\.4\\.0.*", {git, "git://github.com/extend/ranch.git", "0.4.0"}} + {ranch, "0\\.4\\.0.*", {git, "git://github.com/extend/ranch.git", + "cd099983b1b807b87fa050d1e4ff0a13aba25b49"}} ]}. {eunit_opts, [verbose, {report, {eunit_surefire, [{dir, "."}]}}]}. {erl_opts, []}. diff --git a/src/cowboy_clock.erl b/src/cowboy_clock.erl index 747bffe..5e2bf44 100644 --- a/src/cowboy_clock.erl +++ b/src/cowboy_clock.erl @@ -62,14 +62,14 @@ stop() -> %% @doc Return the current date and time formatted according to RFC-1123. %% -%% This format is used in the <em>'Date'</em> header sent with HTTP responses. +%% This format is used in the <em>date</em> header sent with HTTP responses. -spec rfc1123() -> binary(). rfc1123() -> ets:lookup_element(?TABLE, rfc1123, 2). %% @doc Return the current date and time formatted according to RFC-2109. %% -%% This format is used in the <em>'Set-Cookie'</em> header sent with +%% This format is used in the <em>set-cookie</em> header sent with %% HTTP responses. -spec rfc2109(calendar:datetime()) -> binary(). rfc2109(LocalTime) -> diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl index 445e8fa..cfb8fb6 100644 --- a/src/cowboy_dispatcher.erl +++ b/src/cowboy_dispatcher.erl @@ -17,16 +17,14 @@ -module(cowboy_dispatcher). %% API. --export([split_host/1]). --export([split_path/2]). -export([match/3]). --type bindings() :: list({atom(), binary()}). --type tokens() :: list(binary()). --type match_rule() :: '_' | '*' | list(binary() | '_' | '...' | atom()). --type dispatch_path() :: list({match_rule(), module(), any()}). +-type bindings() :: [{atom(), binary()}]. +-type tokens() :: [binary()]. +-type match_rule() :: '_' | '*' | [binary() | '_' | '...' | atom()]. +-type dispatch_path() :: [{match_rule(), module(), any()}]. -type dispatch_rule() :: {Host::match_rule(), Path::dispatch_path()}. --type dispatch_rules() :: list(dispatch_rule()). +-type dispatch_rules() :: [dispatch_rule()]. -export_type([bindings/0]). -export_type([tokens/0]). @@ -38,42 +36,6 @@ %% API. -%% @doc Split a hostname into a list of tokens. --spec split_host(binary()) - -> {tokens(), binary(), undefined | inet:port_number()}. -split_host(<<>>) -> - {[], <<>>, undefined}; -split_host(Host) -> - case binary:split(Host, <<":">>) of - [Host] -> - {binary:split(Host, <<".">>, [global, trim]), Host, undefined}; - [Host2, Port] -> - {binary:split(Host2, <<".">>, [global, trim]), Host2, - list_to_integer(binary_to_list(Port))} - 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(), fun((binary()) -> binary())) -> - {tokens(), binary(), binary()}. -split_path(Path, URLDec) -> - case binary:split(Path, <<"?">>) of - [Path] -> {do_split_path(Path, <<"/">>, URLDec), Path, <<>>}; - [<<>>, Qs] -> {[], <<>>, Qs}; - [Path2, Qs] -> {do_split_path(Path2, <<"/">>, URLDec), Path2, Qs} - end. - --spec do_split_path(binary(), <<_:8>>, fun((binary()) -> binary())) -> tokens(). -do_split_path(RawPath, Separator, URLDec) -> - EncodedPath = case binary:split(RawPath, Separator, [global, trim]) of - [<<>>|Path] -> Path; - Path -> Path - end, - [URLDec(Token) || Token <- EncodedPath]. - %% @doc Match hostname tokens and path tokens against dispatch rules. %% %% It is typically used for matching tokens for the hostname and path of @@ -101,53 +63,90 @@ do_split_path(RawPath, Separator, URLDec) -> %% 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::tokens(), Path::tokens(), dispatch_rules()) +-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}. -match(_Host, _Path, []) -> +match([], _, _) -> {error, notfound, host}; -match(_Host, Path, [{'_', PathMatchs}|_Tail]) -> - match_path(Path, PathMatchs, [], undefined); -match(Host, Path, [{HostMatch, PathMatchs}|Tail]) -> - case try_match(host, Host, HostMatch) of +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(Host, Path, Tail); - {true, HostBinds, undefined} -> - match_path(Path, PathMatchs, HostBinds, undefined); - {true, HostBinds, HostInfo} -> - match_path(Path, PathMatchs, HostBinds, lists:reverse(HostInfo)) - end. + 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(tokens(), dispatch_path(), bindings(), - HostInfo::undefined | tokens()) +-spec match_path(dispatch_path(), + HostInfo::undefined | tokens(), binary() | tokens(), bindings()) -> {ok, module(), any(), bindings(), HostInfo::undefined | tokens(), PathInfo::undefined | tokens()} | {error, notfound, path}. -match_path(_Path, [], _HostBinds, _HostInfo) -> +match_path([], _, _, _) -> {error, notfound, path}; -match_path(_Path, [{'_', Handler, Opts}|_Tail], HostBinds, HostInfo) -> - {ok, Handler, Opts, HostBinds, HostInfo, undefined}; -match_path('*', [{'*', Handler, Opts}|_Tail], HostBinds, HostInfo) -> - {ok, Handler, Opts, HostBinds, HostInfo, undefined}; -match_path(Path, [{PathMatch, Handler, Opts}|Tail], HostBinds, HostInfo) -> - case try_match(path, Path, PathMatch) of +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(Path, Tail, HostBinds, HostInfo); + match_path(Tail, HostInfo, Tokens, Bindings); {true, PathBinds, PathInfo} -> - {ok, Handler, Opts, HostBinds ++ PathBinds, HostInfo, PathInfo} - end. + {ok, Handler, Opts, Bindings ++ PathBinds, HostInfo, PathInfo} + end; +match_path(Dispatch, HostInfo, Path, Bindings) -> + match_path(Dispatch, HostInfo, split_path(Path), Bindings). %% Internal. --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, []). +%% @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(Path, Acc) -> + 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. -spec list_match(tokens(), match_rule(), bindings()) -> {true, bindings(), undefined | tokens()} | false. @@ -177,66 +176,30 @@ list_match(_List, _Match, _Binds) -> split_host_test_() -> %% {Host, Result} Tests = [ - {<<"">>, {[], <<"">>, undefined}}, - {<<".........">>, {[], <<".........">>, undefined}}, - {<<"*">>, {[<<"*">>], <<"*">>, undefined}}, + {<<"">>, []}, + {<<"*">>, [<<"*">>]}, {<<"cowboy.ninenines.eu">>, - {[<<"cowboy">>, <<"ninenines">>, <<"eu">>], - <<"cowboy.ninenines.eu">>, undefined}}, - {<<"ninenines..eu">>, - {[<<"ninenines">>, <<>>, <<"eu">>], - <<"ninenines..eu">>, undefined}}, + [<<"eu">>, <<"ninenines">>, <<"cowboy">>]}, {<<"ninenines.eu">>, - {[<<"ninenines">>, <<"eu">>], <<"ninenines.eu">>, undefined}}, - {<<"ninenines.eu:8080">>, - {[<<"ninenines">>, <<"eu">>], <<"ninenines.eu">>, 8080}}, + [<<"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">>, - {[<<"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">>], - <<"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">>, - undefined}} + [<<"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_host_fail_test_() -> - Tests = [ - <<"ninenines.eu:owns">>, - <<"ninenines.eu: owns">>, - <<"ninenines.eu:42fun">>, - <<"ninenines.eu: 42fun">>, - <<"ninenines.eu:42 fun">>, - <<"ninenines.eu:fun 42">>, - <<"ninenines.eu: 42">>, - <<":owns">>, - <<":42 fun">> - ], - [{H, fun() -> case catch split_host(H) of - {'EXIT', _Reason} -> ok - end end} || H <- Tests]. - split_path_test_() -> %% {Path, Result, QueryString} Tests = [ - {<<"?">>, [], <<"">>, <<"">>}, - {<<"???">>, [], <<"">>, <<"??">>}, - {<<"/">>, [], <<"/">>, <<"">>}, - {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>], - <<"/extend//cowboy">>, <<>>}, - {<<"/users">>, [<<"users">>], <<"/users">>, <<"">>}, - {<<"/users?">>, [<<"users">>], <<"/users">>, <<"">>}, - {<<"/users?a">>, [<<"users">>], <<"/users">>, <<"a">>}, - {<<"/users/42/friends?a=b&c=d&e=notsure?whatever">>, - [<<"users">>, <<"42">>, <<"friends">>], - <<"/users/42/friends">>, <<"a=b&c=d&e=notsure?whatever">>}, - {<<"/users/a+b/c%21d?e+f=g+h">>, - [<<"users">>, <<"a b">>, <<"c!d">>], - <<"/users/a+b/c%21d">>, <<"e+f=g+h">>} + {<<"/">>, []}, + {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]}, + {<<"/users">>, [<<"users">>]}, + {<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]}, + {<<"/users/a+b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]} ], - URLDecode = fun(Bin) -> cowboy_http:urldecode(Bin, crash) end, - [{P, fun() -> {R, RawP, Qs} = split_path(P, URLDecode) end} - || {P, R, RawP, Qs} <- Tests]. + [{P, fun() -> R = split_path(P) end} || {P, R} <- Tests]. match_test_() -> Dispatch = [ @@ -261,28 +224,30 @@ match_test_() -> ], %% {Host, Path, Result} Tests = [ - {[<<"any">>], [], {ok, match_any, [], []}}, - {[<<"www">>, <<"any">>, <<"ninenines">>, <<"eu">>], - [<<"users">>, <<"42">>, <<"mails">>], + {<<"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">>], + {<<"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">>], '_', + {<<"erlang.fr">>, '_', {ok, match_erlang_ext, [], [{ext, <<"fr">>}]}}, - {[<<"any">>], [<<"users">>, <<"444">>, <<"friends">>], + {<<"any">>, <<"/users/444/friends">>, {ok, match_users_friends, [], [{id, <<"444">>}]}}, - {[<<"ninenines">>, <<"fr">>], [<<"threads">>, <<"987">>], + {<<"ninenines.fr">>, <<"/threads/987">>, {ok, match_duplicate_vars, [we, {expect, two}, var, here], - [{var, <<"fr">>}, {var, <<"987">>}]}} + [{var, <<"fr">>}, {var, <<"987">>}]}} ], [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> - {ok, Handler, Opts, Binds, undefined, undefined} = match(H, P, Dispatch) + {ok, Handler, Opts, Binds, undefined, undefined} + = match(Dispatch, H, P) end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests]. match_info_test_() -> @@ -295,24 +260,21 @@ match_info_test_() -> ]} ], Tests = [ - {[<<"ninenines">>, <<"eu">>], [], + {<<"ninenines.eu">>, <<"/">>, {ok, match_any, [], [], [], undefined}}, - {[<<"bugs">>, <<"ninenines">>, <<"eu">>], [], + {<<"bugs.ninenines.eu">>, <<"/">>, {ok, match_any, [], [], [<<"bugs">>], undefined}}, - {[<<"cowboy">>, <<"bugs">>, <<"ninenines">>, <<"eu">>], [], + {<<"cowboy.bugs.ninenines.eu">>, <<"/">>, {ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}}, - {[<<"www">>, <<"ninenines">>, <<"eu">>], - [<<"pathinfo">>, <<"is">>, <<"next">>], + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>, {ok, match_path, [], [], undefined, []}}, - {[<<"www">>, <<"ninenines">>, <<"eu">>], - [<<"pathinfo">>, <<"is">>, <<"next">>, <<"path_info">>], + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>, {ok, match_path, [], [], undefined, [<<"path_info">>]}}, - {[<<"www">>, <<"ninenines">>, <<"eu">>], - [<<"pathinfo">>, <<"is">>, <<"next">>, <<"foo">>, <<"bar">>], + {<<"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(H, P, Dispatch) + R = match(Dispatch, H, P) end} || {H, P, R} <- Tests]. -endif. diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index f3457dc..e0b1632 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -42,41 +42,18 @@ -export([ce_identity/1]). %% Interpretation. --export([connection_to_atom/1]). -export([version_to_binary/1]). -export([urldecode/1]). -export([urldecode/2]). -export([urlencode/1]). -export([urlencode/2]). --export([x_www_form_urlencoded/2]). - --type method() :: 'OPTIONS' | 'GET' | 'HEAD' - | 'POST' | 'PUT' | 'DELETE' | 'TRACE' | binary(). --type uri() :: '*' | {absoluteURI, http | https, Host::binary(), - Port::integer() | undefined, Path::binary()} - | {scheme, Scheme::binary(), binary()} - | {abs_path, binary()} | binary(). +-export([x_www_form_urlencoded/1]). + -type version() :: {Major::non_neg_integer(), Minor::non_neg_integer()}. --type header() :: 'Cache-Control' | 'Connection' | 'Date' | 'Pragma' - | 'Transfer-Encoding' | 'Upgrade' | 'Via' | 'Accept' | 'Accept-Charset' - | 'Accept-Encoding' | 'Accept-Language' | 'Authorization' | 'From' | 'Host' - | 'If-Modified-Since' | 'If-Match' | 'If-None-Match' | 'If-Range' - | 'If-Unmodified-Since' | 'Max-Forwards' | 'Proxy-Authorization' | 'Range' - | 'Referer' | 'User-Agent' | 'Age' | 'Location' | 'Proxy-Authenticate' - | 'Public' | 'Retry-After' | 'Server' | 'Vary' | 'Warning' - | 'Www-Authenticate' | 'Allow' | 'Content-Base' | 'Content-Encoding' - | 'Content-Language' | 'Content-Length' | 'Content-Location' - | 'Content-Md5' | 'Content-Range' | 'Content-Type' | 'Etag' - | 'Expires' | 'Last-Modified' | 'Accept-Ranges' | 'Set-Cookie' - | 'Set-Cookie2' | 'X-Forwarded-For' | 'Cookie' | 'Keep-Alive' - | 'Proxy-Connection' | binary(). --type headers() :: [{header(), iodata()}]. +-type headers() :: [{binary(), iodata()}]. -type status() :: non_neg_integer() | binary(). --export_type([method/0]). --export_type([uri/0]). -export_type([version/0]). --export_type([header/0]). -export_type([headers/0]). -export_type([status/0]). @@ -795,20 +772,6 @@ ce_identity(Data) -> %% Interpretation. -%% @doc Walk through a tokens list and return whether -%% the connection is keepalive or closed. -%% -%% The connection token is expected to be lower-case. --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([_Any|Tail]) -> - connection_to_atom(Tail). - %% @doc Convert an HTTP version tuple to its binary form. -spec version_to_binary(version()) -> binary(). version_to_binary({1, 1}) -> <<"HTTP/1.1">>; @@ -898,15 +861,14 @@ tohexu(C) when C < 17 -> $A + C - 10. tohexl(C) when C < 10 -> $0 + C; tohexl(C) when C < 17 -> $a + C - 10. --spec x_www_form_urlencoded(binary(), fun((binary()) -> binary())) -> - list({binary(), binary() | true}). -x_www_form_urlencoded(<<>>, _URLDecode) -> +-spec x_www_form_urlencoded(binary()) -> list({binary(), binary() | true}). +x_www_form_urlencoded(<<>>) -> []; -x_www_form_urlencoded(Qs, URLDecode) -> +x_www_form_urlencoded(Qs) -> Tokens = binary:split(Qs, <<"&">>, [global, trim]), [case binary:split(Token, <<"=">>) of - [Token] -> {URLDecode(Token), true}; - [Name, Value] -> {URLDecode(Name), URLDecode(Value)} + [Token] -> {urldecode(Token), true}; + [Name, Value] -> {urldecode(Name), urldecode(Value)} end || Token <- Tokens]. %% Tests. @@ -1052,16 +1014,6 @@ asctime_date_test_() -> ], [{V, fun() -> R = asctime_date(V) end} || {V, R} <- Tests]. -connection_to_atom_test_() -> - %% {Tokens, Result} - Tests = [ - {[<<"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]. - content_type_test_() -> %% {ContentType, Result} Tests = [ @@ -1100,9 +1052,7 @@ x_www_form_urlencoded_test_() -> {<<"a=b=c=d=e&f=g">>, [{<<"a">>, <<"b=c=d=e">>}, {<<"f">>, <<"g">>}]}, {<<"a+b=c+d">>, [{<<"a b">>, <<"c d">>}]} ], - URLDecode = fun urldecode/1, - [{Qs, fun() -> R = x_www_form_urlencoded( - Qs, URLDecode) end} || {Qs, R} <- Tests]. + [{Qs, fun() -> R = x_www_form_urlencoded(Qs) end} || {Qs, R} <- Tests]. urldecode_test_() -> U = fun urldecode/2, diff --git a/src/cowboy_multipart.erl b/src/cowboy_multipart.erl index 4040073..fc889ef 100644 --- a/src/cowboy_multipart.erl +++ b/src/cowboy_multipart.erl @@ -23,7 +23,7 @@ -type more(T) :: T | {more, parser(T)}. -type part_result() :: headers() | eof. -type headers() :: {headers, http_headers(), body_cont()}. --type http_headers() :: [{atom() | binary(), binary()}]. +-type http_headers() :: [{binary(), binary()}]. -type body_cont() :: cont(more(body_result())). -type cont(T) :: fun(() -> T). -type body_result() :: {body, binary(), body_cont()} | end_of_part(). @@ -135,7 +135,11 @@ parse_headers(Bin, Pattern) -> parse_headers(Bin, Pattern, Acc) -> case erlang:decode_packet(httph_bin, Bin, []) of {ok, {http_header, _, Name, _, Value}, Rest} -> - parse_headers(Rest, Pattern, [{Name, Value} | Acc]); + Name2 = case is_atom(Name) of + true -> cowboy_bstr:to_lower(atom_to_binary(Name, latin1)); + false -> cowboy_bstr:to_lower(Name) + end, + parse_headers(Rest, Pattern, [{Name2, Value} | Acc]); {ok, http_eoh, Rest} -> Headers = lists:reverse(Acc), {headers, Headers, fun () -> parse_body(Rest, Pattern) end}; @@ -205,7 +209,7 @@ multipart_test_() -> {<<"preamble\r\n--boundary--">>, []}, {<<"--boundary--\r\nepilogue">>, []}, {<<"\r\n--boundary\r\nA:b\r\nC:d\r\n\r\n\r\n--boundary--">>, - [{[{<<"A">>, <<"b">>}, {<<"C">>, <<"d">>}], <<>>}]}, + [{[{<<"a">>, <<"b">>}, {<<"c">>, <<"d">>}], <<>>}]}, { << "--boundary\r\nX-Name:answer\r\n\r\n42" @@ -213,8 +217,8 @@ multipart_test_() -> "\r\n--boundary--" >>, [ - {[{<<"X-Name">>, <<"answer">>}], <<"42">>}, - {[{'Server', <<"Cowboy">>}], <<"It rocks!\r\n">>} + {[{<<"x-name">>, <<"answer">>}], <<"42">>}, + {[{<<"server">>, <<"Cowboy">>}], <<"It rocks!\r\n">>} ] } ], diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl index 2e734c5..bf81e52 100644 --- a/src/cowboy_protocol.erl +++ b/src/cowboy_protocol.erl @@ -20,11 +20,23 @@ %% <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>max_header_name_length</dt><dd>Max length allowed for header names. +%% Defaults to 64.</dd> +%% <dt>max_header_value_length</dt><dd>Max length allowed for header values. +%% Defaults to 4096.</dd> +%% <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> +%% <dt>max_request_line_length</dt><dd>Max length allowed for the request +%% line. Defaults to 4096.</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 based on its status code or headers.</dd> %% <dt>timeout</dt><dd>Time in milliseconds before an idle %% connection is closed. Defaults to 5000 milliseconds.</dd> -%% <dt>urldecode</dt><dd>Function and options argument to use when decoding -%% URL encoded strings. Defaults to `{fun cowboy_http:urldecode/2, crash}'. -%% </dd> %% </dl> %% %% Note that there is no need to monitor these processes when using Cowboy as @@ -39,12 +51,8 @@ %% Internal. -export([init/4]). --export([parse_request/1]). --export([handler_loop/3]). - --ifdef(TEST). --include_lib("eunit/include/eunit.hrl"). --endif. +-export([parse_request/3]). +-export([handler_loop/4]). -type onrequest_fun() :: fun((Req) -> Req). -type onresponse_fun() :: @@ -58,19 +66,16 @@ socket :: inet:socket(), transport :: module(), dispatch :: cowboy_dispatcher:dispatch_rules(), - handler :: {module(), any()}, onrequest :: undefined | onrequest_fun(), onresponse = undefined :: undefined | onresponse_fun(), - urldecode :: {fun((binary(), T) -> binary()), T}, - req_empty_lines = 0 :: integer(), - max_empty_lines :: integer(), - req_keepalive = 1 :: integer(), - max_keepalive :: integer(), - max_line_length :: integer(), + max_empty_lines :: non_neg_integer(), + req_keepalive = 1 :: non_neg_integer(), + max_keepalive :: non_neg_integer(), + max_request_line_length :: non_neg_integer(), + max_header_name_length :: non_neg_integer(), + max_header_value_length :: non_neg_integer(), + max_headers :: non_neg_integer(), timeout :: timeout(), - buffer = <<>> :: binary(), - host_tokens = undefined :: undefined | cowboy_dispatcher:tokens(), - path_tokens = undefined :: undefined | '*' | cowboy_dispatcher:tokens(), hibernate = false :: boolean(), loop_timeout = infinity :: timeout(), loop_timeout_ref :: undefined | reference() @@ -99,244 +104,442 @@ get_value(Key, Opts, Default) -> init(ListenerPid, Socket, Transport, Opts) -> Dispatch = get_value(dispatch, Opts, []), 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), - MaxLineLength = get_value(max_line_length, Opts, 4096), + MaxRequestLineLength = get_value(max_request_line_length, Opts, 4096), OnRequest = get_value(onrequest, Opts, undefined), OnResponse = get_value(onresponse, Opts, undefined), Timeout = get_value(timeout, Opts, 5000), - URLDecDefault = {fun cowboy_http:urldecode/2, crash}, - URLDec = get_value(urldecode, Opts, URLDecDefault), ok = ranch:accept_ack(ListenerPid), - wait_request(#state{listener=ListenerPid, socket=Socket, transport=Transport, - dispatch=Dispatch, max_empty_lines=MaxEmptyLines, - max_keepalive=MaxKeepalive, max_line_length=MaxLineLength, - timeout=Timeout, onrequest=OnRequest, onresponse=OnResponse, - urldecode=URLDec}). + wait_request(<<>>, #state{listener=ListenerPid, socket=Socket, + transport=Transport, dispatch=Dispatch, + 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). + +%% Request parsing. +%% +%% The next set of functions is the request parsing code. All of it +%% runs using a single binary match context. This optimization ends +%% right after the header parsing is finished and the code becomes +%% more interesting past that point. + +-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 + {ok, Data} -> + parse_request(<< Buffer/binary, Data/binary >>, State, ReqEmpty); + {error, _} -> + terminate(State) + end. %% @private --spec parse_request(#state{}) -> ok. +-spec parse_request(binary(), #state{}, non_neg_integer()) -> ok. +%% Empty lines must be using \r\n. +parse_request(<< $\n, _/binary >>, State, _) -> + error_terminate(400, State); %% We limit the length of the Request-line to MaxLength to avoid endlessly %% reading from the socket and eventually crashing. -parse_request(State=#state{buffer=Buffer, max_line_length=MaxLength}) -> - case erlang:decode_packet(http_bin, Buffer, []) of - {ok, Request, Rest} -> request(Request, State#state{buffer=Rest}); - {more, _Length} when byte_size(Buffer) > MaxLength -> - error_terminate(413, State); - {more, _Length} -> wait_request(State); - {error, _Reason} -> error_terminate(400, State) +parse_request(Buffer, State=#state{max_request_line_length=MaxLength, + max_empty_lines=MaxEmpty}, ReqEmpty) -> + case binary:match(Buffer, <<"\n">>) of + nomatch when byte_size(Buffer) > MaxLength -> + error_terminate(414, State); + nomatch -> + wait_request(Buffer, State, ReqEmpty); + {1, _} when ReqEmpty =:= MaxEmpty -> + error_terminate(400, State); + {1, _} -> + << _:16, Rest/binary >> = Buffer, + parse_request(Rest, State, ReqEmpty + 1); + {_, _} -> + parse_method(Buffer, State, <<>>) end. --spec wait_request(#state{}) -> ok. -wait_request(State=#state{socket=Socket, transport=Transport, - timeout=T, buffer=Buffer}) -> - case Transport:recv(Socket, 0, T) of - {ok, Data} -> parse_request(State#state{ - buffer= << Buffer/binary, Data/binary >>}); - {error, _Reason} -> terminate(State) +parse_method(<< C, Rest/bits >>, State, SoFar) -> + case C of + $\r -> error_terminate(400, State); + $\s -> parse_uri(Rest, State, SoFar); + _ -> parse_method(Rest, State, << SoFar/binary, C >>) end. --spec request({http_request, cowboy_http:method(), cowboy_http:uri(), - cowboy_http:version()}, #state{}) -> ok. -request({http_request, _Method, _URI, Version}, State) - when Version =/= {1, 0}, Version =/= {1, 1} -> - error_terminate(505, State); -%% We still receive the original Host header. -request({http_request, Method, {absoluteURI, _Scheme, _Host, _Port, Path}, - Version}, State) -> - request({http_request, Method, {abs_path, Path}, Version}, State); -request({http_request, Method, {abs_path, AbsPath}, Version}, - State=#state{socket=Socket, transport=Transport, - req_keepalive=Keepalive, max_keepalive=MaxKeepalive, - onresponse=OnResponse, urldecode={URLDecFun, URLDecArg}=URLDec}) -> - URLDecode = fun(Bin) -> URLDecFun(Bin, URLDecArg) end, - {PathTokens, RawPath, Qs} - = cowboy_dispatcher:split_path(AbsPath, URLDecode), - ConnAtom = if Keepalive < MaxKeepalive -> version_to_connection(Version); - true -> close - end, - parse_header(cowboy_req:new(Socket, Transport, ConnAtom, Method, Version, - RawPath, Qs, OnResponse, URLDec), State#state{path_tokens=PathTokens}); -request({http_request, Method, '*', Version}, - State=#state{socket=Socket, transport=Transport, - req_keepalive=Keepalive, max_keepalive=MaxKeepalive, - onresponse=OnResponse, urldecode=URLDec}) -> - ConnAtom = if Keepalive < MaxKeepalive -> version_to_connection(Version); - true -> close - end, - parse_header(cowboy_req:new(Socket, Transport, ConnAtom, Method, Version, - <<"*">>, <<>>, OnResponse, URLDec), State#state{path_tokens='*'}); -request({http_request, _Method, _URI, _Version}, State) -> - error_terminate(501, State); -request({http_error, <<"\r\n">>}, - State=#state{req_empty_lines=N, max_empty_lines=N}) -> +parse_uri(<< $\r, _/bits >>, State, _) -> error_terminate(400, State); -request({http_error, <<"\r\n">>}, State=#state{req_empty_lines=N}) -> - parse_request(State#state{req_empty_lines=N + 1}); -request(_Any, State) -> - error_terminate(400, State). - --spec parse_header(cowboy_req:req(), #state{}) -> ok. -parse_header(Req, State=#state{buffer=Buffer, max_line_length=MaxLength}) -> - case erlang:decode_packet(httph_bin, Buffer, []) of - {ok, Header, Rest} -> header(Header, Req, State#state{buffer=Rest}); - {more, _Length} when byte_size(Buffer) > MaxLength -> - error_terminate(413, State); - {more, _Length} -> wait_header(Req, State); - {error, _Reason} -> error_terminate(400, State) +parse_uri(<< "* ", Rest/bits >>, State, Method) -> + parse_version(Rest, State, Method, <<"*">>, <<>>, <<>>); +parse_uri(<< "http://", Rest/bits >>, State, Method) -> + parse_uri_skip_host(Rest, State, Method); +parse_uri(<< "https://", Rest/bits >>, State, Method) -> + parse_uri_skip_host(Rest, State, Method); +parse_uri(Buffer, State, Method) -> + parse_uri_path(Buffer, State, Method, <<>>). + +parse_uri_skip_host(<< C, Rest/bits >>, State, Method) -> + case C of + $\r -> error_terminate(400, State); + $/ -> parse_uri_path(Rest, State, Method, <<"/">>); + _ -> parse_uri_skip_host(Rest, State, Method) end. --spec wait_header(cowboy_req:req(), #state{}) -> ok. -wait_header(Req, State=#state{socket=Socket, - transport=Transport, timeout=T, buffer=Buffer}) -> - case Transport:recv(Socket, 0, T) of - {ok, Data} -> parse_header(Req, State#state{ - buffer= << Buffer/binary, Data/binary >>}); - {error, timeout} -> error_terminate(408, State); - {error, closed} -> terminate(State) +parse_uri_path(<< C, Rest/bits >>, State, Method, SoFar) -> + case C of + $\r -> error_terminate(400, State); + $\s -> parse_version(Rest, State, Method, SoFar, <<>>, <<>>); + $? -> parse_uri_query(Rest, State, Method, SoFar, <<>>); + $# -> parse_uri_fragment(Rest, State, Method, SoFar, <<>>, <<>>); + _ -> parse_uri_path(Rest, State, Method, << SoFar/binary, C >>) end. --spec header({http_header, integer(), cowboy_http:header(), any(), binary()} - | http_eoh, cowboy_req:req(), #state{}) -> ok. -header({http_header, _I, 'Host', _R, RawHost}, Req, - State=#state{host_tokens=undefined, transport=Transport}) -> - RawHost2 = cowboy_bstr:to_lower(RawHost), - case catch cowboy_dispatcher:split_host(RawHost2) of - {HostTokens, Host, undefined} -> - Port = default_port(Transport:name()), - parse_header(cowboy_req:set_host(Host, Port, RawHost, Req), - State#state{host_tokens=HostTokens}); - {HostTokens, Host, Port} -> - parse_header(cowboy_req:set_host(Host, Port, RawHost, Req), - State#state{host_tokens=HostTokens}); - {'EXIT', _Reason} -> - error_terminate(400, State) - end; -%% 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=#state{req_keepalive=Keepalive, max_keepalive=MaxKeepalive}) - when Keepalive < MaxKeepalive -> - parse_header(cowboy_req:set_connection(Connection, Req), State); -header({http_header, _I, Field, _R, Value}, Req, State) -> - Field2 = format_header(Field), - parse_header(cowboy_req:add_header(Field2, Value, Req), State); -%% The Host header is required in HTTP/1.1 and optional in HTTP/1.0. -header(http_eoh, Req, State=#state{host_tokens=undefined, - buffer=Buffer, transport=Transport}) -> - case cowboy_req:version(Req) of - {{1, 1}, _} -> +parse_uri_query(<< C, Rest/bits >>, S, M, P, SoFar) -> + case C of + $\r -> error_terminate(400, S); + $\s -> parse_version(Rest, S, M, P, SoFar, <<>>); + $# -> parse_uri_fragment(Rest, S, M, P, SoFar, <<>>); + _ -> parse_uri_query(Rest, S, M, P, << SoFar/binary, C >>) + end. + +parse_uri_fragment(<< C, Rest/bits >>, S, M, P, Q, SoFar) -> + case C of + $\r -> error_terminate(400, S); + $\s -> parse_version(Rest, S, M, P, Q, SoFar); + _ -> parse_uri_fragment(Rest, S, M, P, Q, << SoFar/binary, C >>) + end. + +parse_version(<< "HTTP/1.1\r\n", Rest/bits >>, S, M, P, Q, F) -> + parse_header(Rest, S, M, P, Q, F, {1, 1}, []); +parse_version(<< "HTTP/1.0\r\n", Rest/bits >>, S, M, P, Q, F) -> + parse_header(Rest, S, M, P, Q, F, {1, 0}, []); +parse_version(_, State, _, _, _, _) -> + error_terminate(505, State). + +%% Stop receiving data if we have more than allowed number of headers. +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 + {ok, Data} -> + parse_header(<< Buffer/binary, Data/binary >>, + State, M, P, Q, F, V, H); + {error, timeout} -> + error_terminate(408, State); + {error, _} -> + terminate(State) + end. + +parse_header(<< $\r, $\n, Rest/bits >>, S, M, P, Q, F, V, Headers) -> + request(Rest, S, M, P, Q, F, V, lists:reverse(Headers)); +parse_header(Buffer, State=#state{max_header_name_length=MaxLength}, + M, P, Q, F, V, H) -> + case binary:match(Buffer, <<":">>) of + nomatch when byte_size(Buffer) > MaxLength -> + error_terminate(400, State); + nomatch -> + wait_header(Buffer, State, M, P, Q, F, V, H); + {_, _} -> + parse_hd_name(Buffer, State, M, P, Q, F, V, H, <<>>) + end. + +%% I know, this isn't exactly pretty. But this is the most critical +%% code path and as such needs to be optimized to death. +%% +%% ... Sorry for your eyes. +%% +%% But let's be honest, that's still pretty readable. +parse_hd_name(<< C, Rest/bits >>, S, M, P, Q, F, V, H, SoFar) -> + case C of + $: -> parse_hd_before_value(Rest, S, M, P, Q, F, V, H, SoFar); + $\s -> parse_hd_name_ws(Rest, S, M, P, Q, F, V, H, SoFar); + $\t -> parse_hd_name_ws(Rest, S, M, P, Q, F, V, H, SoFar); + $A -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $a >>); + $B -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $b >>); + $C -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $c >>); + $D -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $d >>); + $E -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $e >>); + $F -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $f >>); + $G -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $g >>); + $H -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $h >>); + $I -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $i >>); + $J -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $j >>); + $K -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $k >>); + $L -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $l >>); + $M -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $m >>); + $N -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $n >>); + $O -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $o >>); + $P -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $p >>); + $Q -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $q >>); + $R -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $r >>); + $S -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $s >>); + $T -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $t >>); + $U -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $u >>); + $V -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $v >>); + $W -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $w >>); + $X -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $x >>); + $Y -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $y >>); + $Z -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, $z >>); + C -> parse_hd_name(Rest, S, M, P, Q, F, V, H, << SoFar/binary, C >>) + end. + +parse_hd_name_ws(<< C, Rest/bits >>, S, M, P, Q, F, V, H, Name) -> + case C of + $\s -> parse_hd_name_ws(Rest, S, M, P, Q, F, V, H, Name); + $\t -> parse_hd_name_ws(Rest, S, M, P, Q, F, V, H, Name); + $: -> parse_hd_before_value(Rest, S, M, P, Q, F, V, H, Name) + end. + +wait_hd_before_value(Buffer, State=#state{ + socket=Socket, transport=Transport, timeout=Timeout}, + M, P, Q, F, V, H, N) -> + case Transport:recv(Socket, 0, Timeout) of + {ok, Data} -> + parse_hd_before_value(<< Buffer/binary, Data/binary >>, + State, M, P, Q, F, V, H, N); + {error, timeout} -> + error_terminate(408, State); + {error, _} -> + terminate(State) + end. + +parse_hd_before_value(<< $\s, Rest/bits >>, S, M, P, Q, F, V, H, N) -> + parse_hd_before_value(Rest, S, M, P, Q, F, V, H, N); +parse_hd_before_value(<< $\t, Rest/bits >>, S, M, P, Q, F, V, H, N) -> + parse_hd_before_value(Rest, S, M, P, Q, F, V, H, N); +parse_hd_before_value(Buffer, State=#state{ + max_header_value_length=MaxLength}, M, P, Q, F, V, H, N) -> + case binary:match(Buffer, <<"\n">>) of + nomatch when byte_size(Buffer) > MaxLength -> error_terminate(400, State); - {{1, 0}, Req2} -> - Port = default_port(Transport:name()), - onrequest( - cowboy_req:set_buffer(Buffer, - cowboy_req:set_host(<<>>, Port, <<>>, Req2)), - State#state{buffer= <<>>, host_tokens=[]}) + nomatch -> + wait_hd_before_value(Buffer, State, M, P, Q, F, V, H, N); + {_, _} -> + parse_hd_value(Buffer, State, M, P, Q, F, V, H, N, <<>>) + end. + +%% We completely ignore the first argument which is always +%% the empty binary. We keep it there because we don't want +%% to change the other arguments' position and trigger costy +%% operations for no reasons. +wait_hd_value(_, State=#state{ + socket=Socket, transport=Transport, timeout=Timeout}, + M, P, Q, F, V, H, N, SoFar) -> + case Transport:recv(Socket, 0, Timeout) of + {ok, Data} -> + parse_hd_value(Data, State, M, P, Q, F, V, H, N, SoFar); + {error, timeout} -> + error_terminate(408, State); + {error, _} -> + terminate(State) + end. + +%% Pushing back as much as we could the retrieval of new data +%% 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}, + M, P, Q, F, V, Headers, Name, SoFar) -> + case Transport:recv(Socket, 0, Timeout) 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} -> + parse_header(Data, State, M, P, Q, F, V, [{Name, SoFar}|Headers]); + {error, timeout} -> + error_terminate(408, State); + {error, _} -> + terminate(State) + end. + +parse_hd_value(<< $\r, Rest/bits >>, S, M, P, Q, F, V, Headers, Name, SoFar) -> + case Rest of + << $\n >> -> + wait_hd_value_nl(<<>>, S, M, P, Q, F, V, Headers, Name, SoFar); + << $\n, C, Rest2/bits >> when C =:= $\s; C =:= $\t -> + parse_hd_value(Rest2, S, M, P, Q, F, V, Headers, Name, SoFar); + << $\n, Rest2/bits >> -> + parse_header(Rest2, S, M, P, Q, F, V, [{Name, SoFar}|Headers]) end; -header(http_eoh, Req, State=#state{buffer=Buffer}) -> - onrequest(cowboy_req:set_buffer(Buffer, Req), State#state{buffer= <<>>}); -header(_Any, _Req, State) -> - error_terminate(400, State). +parse_hd_value(<< C, Rest/bits >>, S, M, P, Q, F, V, H, N, SoFar) -> + parse_hd_value(Rest, S, M, P, Q, F, V, H, N, << SoFar/binary, C >>); +parse_hd_value(<<>>, State=#state{max_header_value_length=MaxLength}, + _, _, _, _, _, _, _, SoFar) when byte_size(SoFar) > MaxLength -> + error_terminate(400, State); +parse_hd_value(<<>>, S, M, P, Q, F, V, H, N, SoFar) -> + wait_hd_value(<<>>, S, M, P, Q, F, V, H, N, SoFar). + +request(B, State=#state{transport=Transport}, M, P, Q, F, Version, Headers) -> + case lists:keyfind(<<"host">>, 1, Headers) of + false when Version =:= {1, 1} -> + error_terminate(400, State); + false -> + request(B, State, M, P, Q, F, Version, Headers, + <<>>, default_port(Transport:name())); + {_, RawHost} -> + case parse_host(RawHost, <<>>) of + {Host, undefined} -> + request(B, State, M, P, Q, F, Version, Headers, + Host, default_port(Transport:name())); + {Host, Port} -> + request(B, State, M, P, Q, F, Version, Headers, + Host, Port) + end + end. + +-spec default_port(atom()) -> 80 | 443. +default_port(ssl) -> 443; +default_port(_) -> 80. + +%% Another hurtful block of code. :) +parse_host(<<>>, Acc) -> + {Acc, undefined}; +parse_host(<< $:, Rest/bits >>, Acc) -> + {Acc, list_to_integer(binary_to_list(Rest))}; +parse_host(<< C, Rest/bits >>, Acc) -> + case C of + $A -> parse_host(Rest, << Acc/binary, $a >>); + $B -> parse_host(Rest, << Acc/binary, $b >>); + $C -> parse_host(Rest, << Acc/binary, $c >>); + $D -> parse_host(Rest, << Acc/binary, $d >>); + $E -> parse_host(Rest, << Acc/binary, $e >>); + $F -> parse_host(Rest, << Acc/binary, $f >>); + $G -> parse_host(Rest, << Acc/binary, $g >>); + $H -> parse_host(Rest, << Acc/binary, $h >>); + $I -> parse_host(Rest, << Acc/binary, $i >>); + $J -> parse_host(Rest, << Acc/binary, $j >>); + $K -> parse_host(Rest, << Acc/binary, $k >>); + $L -> parse_host(Rest, << Acc/binary, $l >>); + $M -> parse_host(Rest, << Acc/binary, $m >>); + $N -> parse_host(Rest, << Acc/binary, $n >>); + $O -> parse_host(Rest, << Acc/binary, $o >>); + $P -> parse_host(Rest, << Acc/binary, $p >>); + $Q -> parse_host(Rest, << Acc/binary, $q >>); + $R -> parse_host(Rest, << Acc/binary, $r >>); + $S -> parse_host(Rest, << Acc/binary, $s >>); + $T -> parse_host(Rest, << Acc/binary, $t >>); + $U -> parse_host(Rest, << Acc/binary, $u >>); + $V -> parse_host(Rest, << Acc/binary, $v >>); + $W -> parse_host(Rest, << Acc/binary, $w >>); + $X -> parse_host(Rest, << Acc/binary, $x >>); + $Y -> parse_host(Rest, << Acc/binary, $y >>); + $Z -> parse_host(Rest, << Acc/binary, $z >>); + _ -> parse_host(Rest, << Acc/binary, C >>) + end. + +%% End of request parsing. +%% +%% We create the Req object and start handling the request. + +request(Buffer, State=#state{socket=Socket, transport=Transport, + req_keepalive=ReqKeepalive, max_keepalive=MaxKeepalive, + 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, Path). %% 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{}) -> ok. -onrequest(Req, State=#state{onrequest=undefined}) -> - dispatch(Req, State); -onrequest(Req, State=#state{onrequest=OnRequest}) -> +-spec onrequest(cowboy_req:req(), #state{}, binary(), binary()) -> ok. +onrequest(Req, State=#state{onrequest=undefined}, Host, Path) -> + dispatch(Req, State, Host, Path); +onrequest(Req, State=#state{onrequest=OnRequest}, Host, Path) -> Req2 = OnRequest(Req), - case cowboy_req:get_resp_state(Req2) of - waiting -> dispatch(Req2, State); + case cowboy_req:get(resp_state, Req2) of + waiting -> dispatch(Req2, State, Host, Path); _ -> next_request(Req2, State, ok) end. --spec dispatch(cowboy_req:req(), #state{}) -> ok. -dispatch(Req, State=#state{dispatch=Dispatch, - host_tokens=HostTokens, path_tokens=PathTokens}) -> - case cowboy_dispatcher:match(HostTokens, PathTokens, Dispatch) of +-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#state{handler={Handler, Opts}, - host_tokens=undefined, path_tokens=undefined}); + handler_init(Req2, State, Handler, Opts); {error, notfound, host} -> error_terminate(400, State); {error, notfound, path} -> error_terminate(404, State) end. --spec handler_init(cowboy_req:req(), #state{}) -> ok. -handler_init(Req, State=#state{transport=Transport, - handler={Handler, Opts}}) -> +-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(HandlerState, Req2, State); + handler_handle(Req2, State, Handler, HandlerState); {loop, Req2, HandlerState} -> - handler_before_loop(HandlerState, Req2, State); + handler_before_loop(Req2, State#state{hibernate=false}, + Handler, HandlerState); {loop, Req2, HandlerState, hibernate} -> - handler_before_loop(HandlerState, Req2, - State#state{hibernate=true}); + handler_before_loop(Req2, State#state{hibernate=true}, + Handler, HandlerState); {loop, Req2, HandlerState, Timeout} -> - handler_before_loop(HandlerState, Req2, - State#state{loop_timeout=Timeout}); + handler_before_loop(Req2, State#state{loop_timeout=Timeout}, + Handler, HandlerState); {loop, Req2, HandlerState, Timeout, hibernate} -> - handler_before_loop(HandlerState, Req2, - State#state{hibernate=true, loop_timeout=Timeout}); + handler_before_loop(Req2, State#state{ + hibernate=true, loop_timeout=Timeout}, Handler, HandlerState); {shutdown, Req2, HandlerState} -> - handler_terminate(HandlerState, Req2, State); + handler_terminate(Req2, Handler, HandlerState); %% @todo {upgrade, transport, Module} {upgrade, protocol, Module} -> - upgrade_protocol(Req, State, Module) + upgrade_protocol(Req, State, Handler, Opts, Module) catch Class:Reason -> error_terminate(500, State), - PLReq = cowboy_req:to_list(Req), error_logger:error_msg( "** Handler ~p terminating in init/3~n" " for the reason ~p:~p~n" "** Options were ~p~n" - "** Request was ~p~n** Stacktrace: ~p~n~n", - [Handler, Class, Reason, Opts, PLReq, erlang:get_stacktrace()]) + "** Request was ~p~n" + "** Stacktrace: ~p~n~n", + [Handler, Class, Reason, Opts, + cowboy_req:to_list(Req), erlang:get_stacktrace()]) end. --spec upgrade_protocol(cowboy_req:req(), #state{}, atom()) -> ok. -upgrade_protocol(Req, State=#state{listener=ListenerPid, - handler={Handler, Opts}}, Module) -> +-spec upgrade_protocol(cowboy_req:req(), #state{}, module(), any(), 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) end. --spec handler_handle(any(), cowboy_req:req(), #state{}) -> ok. -handler_handle(HandlerState, Req, State=#state{handler={Handler, Opts}}) -> +-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(HandlerState2, Req2, State) + terminate_request(Req2, State, Handler, HandlerState2) catch Class:Reason -> - PLReq = cowboy_req:to_list(Req), error_logger:error_msg( "** Handler ~p terminating in handle/2~n" " for the reason ~p:~p~n" - "** Options were ~p~n** Handler state was ~p~n" - "** Request was ~p~n** Stacktrace: ~p~n~n", - [Handler, Class, Reason, Opts, - HandlerState, PLReq, erlang:get_stacktrace()]), - handler_terminate(HandlerState, Req, State), + "** Handler state was ~p~n" + "** Request was ~p~n" + "** Stacktrace: ~p~n~n", + [Handler, Class, Reason, HandlerState, + cowboy_req:to_list(Req), erlang:get_stacktrace()]), + handler_terminate(Req, Handler, HandlerState), error_terminate(500, 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(any(), cowboy_req:req(), #state{}) -> ok. -handler_before_loop(HandlerState, Req, State=#state{hibernate=true}) -> +-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, - [HandlerState, Req, State2#state{hibernate=false}]), + [Req, State2#state{hibernate=false}, Handler, HandlerState]), ok; -handler_before_loop(HandlerState, Req, State) -> +handler_before_loop(Req, State, Handler, HandlerState) -> State2 = handler_loop_timeout(State), - handler_loop(HandlerState, Req, State2). + handler_loop(Req, State2, Handler, HandlerState). %% Almost the same code can be found in cowboy_websocket. -spec handler_loop_timeout(#state{}) -> #state{}. @@ -349,76 +552,74 @@ handler_loop_timeout(State=#state{loop_timeout=Timeout, TRef = erlang:start_timer(Timeout, self(), ?MODULE), State#state{loop_timeout_ref=TRef}. --spec handler_loop(any(), cowboy_req:req(), #state{}) -> ok. -handler_loop(HandlerState, Req, 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(HandlerState, Req, State); + terminate_request(Req, State, Handler, HandlerState); {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> - handler_loop(HandlerState, Req, State); + handler_loop(Req, State, Handler, HandlerState); Message -> - handler_call(HandlerState, Req, State, Message) + handler_call(Req, State, Handler, HandlerState, Message) end. --spec handler_call(any(), cowboy_req:req(), #state{}, any()) -> ok. -handler_call(HandlerState, Req, State=#state{handler={Handler, Opts}}, - Message) -> +-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(HandlerState2, Req2, State); + terminate_request(Req2, State, Handler, HandlerState2); {loop, Req2, HandlerState2} -> - handler_before_loop(HandlerState2, Req2, State); + handler_before_loop(Req2, State, Handler, HandlerState2); {loop, Req2, HandlerState2, hibernate} -> - handler_before_loop(HandlerState2, Req2, - State#state{hibernate=true}) + handler_before_loop(Req2, State#state{hibernate=true}, + Handler, HandlerState2) catch Class:Reason -> - PLReq = cowboy_req:to_list(Req), error_logger:error_msg( "** Handler ~p terminating in info/3~n" " for the reason ~p:~p~n" - "** Options were ~p~n** Handler state was ~p~n" - "** Request was ~p~n** Stacktrace: ~p~n~n", - [Handler, Class, Reason, Opts, - HandlerState, PLReq, erlang:get_stacktrace()]), - handler_terminate(HandlerState, Req, State), + "** Handler state was ~p~n" + "** Request was ~p~n" + "** Stacktrace: ~p~n~n", + [Handler, Class, Reason, HandlerState, + cowboy_req:to_list(Req), erlang:get_stacktrace()]), + handler_terminate(Req, Handler, HandlerState), error_terminate(500, State) end. --spec handler_terminate(any(), cowboy_req:req(), #state{}) -> ok. -handler_terminate(HandlerState, Req, #state{handler={Handler, Opts}}) -> +-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 -> - PLReq = cowboy_req:to_list(Req), error_logger:error_msg( "** Handler ~p terminating in terminate/2~n" " for the reason ~p:~p~n" - "** Options were ~p~n** Handler state was ~p~n" - "** Request was ~p~n** Stacktrace: ~p~n~n", - [Handler, Class, Reason, Opts, - HandlerState, PLReq, erlang:get_stacktrace()]) + "** Handler state was ~p~n" + "** Request was ~p~n" + "** Stacktrace: ~p~n~n", + [Handler, Class, Reason, HandlerState, + cowboy_req:to_list(Req), erlang:get_stacktrace()]) end. --spec terminate_request(any(), cowboy_req:req(), #state{}) -> ok. -terminate_request(HandlerState, Req, State) -> - HandlerRes = handler_terminate(HandlerState, Req, State), +-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}, HandlerRes) -> cowboy_req:ensure_response(Req, 204), - {BodyRes, Buffer} = case cowboy_req:skip_body(Req) of - {ok, Req2} -> {ok, cowboy_req:get_buffer(Req2)}; - {error, _} -> {close, <<>>} + {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, cowboy_req:get_connection(Req)} of + case {HandlerRes, BodyRes, Connection} of {ok, ok, keepalive} -> - ?MODULE:parse_request(State#state{ - buffer=Buffer, host_tokens=undefined, path_tokens=undefined, - req_empty_lines=0, req_keepalive=Keepalive + 1}); + ?MODULE:parse_request(Buffer, State#state{ + req_keepalive=Keepalive + 1}, 0); _Closed -> terminate(State) end. @@ -431,7 +632,8 @@ error_terminate(Code, State=#state{socket=Socket, transport=Transport, {cowboy_req, resp_sent} -> ok after 0 -> _ = cowboy_req:reply(Code, cowboy_req:new(Socket, Transport, - close, 'GET', {1, 1}, <<>>, <<>>, OnResponse, undefined)), + <<"GET">>, <<>>, <<>>, <<>>, {1, 1}, [], <<>>, undefined, + <<>>, false, OnResponse)), ok end, terminate(State). @@ -440,55 +642,3 @@ error_terminate(Code, State=#state{socket=Socket, transport=Transport, terminate(#state{socket=Socket, transport=Transport}) -> Transport:close(Socket), ok. - -%% Internal. - --spec version_to_connection(cowboy_http:version()) -> keepalive | close. -version_to_connection({1, 1}) -> keepalive; -version_to_connection(_Any) -> close. - --spec default_port(atom()) -> 80 | 443. -default_port(ssl) -> 443; -default_port(_) -> 80. - -%% @todo While 32 should be enough for everybody, we should probably make -%% this configurable or something. --spec format_header(atom()) -> atom(); (binary()) -> binary(). -format_header(Field) when is_atom(Field) -> - Field; -format_header(Field) when byte_size(Field) =< 20; byte_size(Field) > 32 -> - Field; -format_header(Field) -> - format_header(Field, true, <<>>). - --spec format_header(binary(), boolean(), binary()) -> binary(). -format_header(<<>>, _Any, Acc) -> - Acc; -%% Replicate a bug in OTP for compatibility reasons when there's a - right -%% after another. Proper use should always be 'true' instead of 'not Bool'. -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, (cowboy_bstr:char_to_upper(C)) >>); -format_header(<< C, Rest/bits >>, false, Acc) -> - format_header(Rest, false, << Acc/binary, (cowboy_bstr:char_to_lower(C)) >>). - -%% Tests. - --ifdef(TEST). - -format_header_test_() -> - %% {Header, Result} - Tests = [ - {<<"Sec-Websocket-Version">>, <<"Sec-Websocket-Version">>}, - {<<"Sec-WebSocket-Version">>, <<"Sec-Websocket-Version">>}, - {<<"sec-websocket-version">>, <<"Sec-Websocket-Version">>}, - {<<"SEC-WEBSOCKET-VERSION">>, <<"Sec-Websocket-Version">>}, - %% These last tests ensures we're formatting headers exactly like OTP. - %% Even though it's dumb, it's better for compatibility reasons. - {<<"Sec-WebSocket--Version">>, <<"Sec-Websocket--version">>}, - {<<"Sec-WebSocket---Version">>, <<"Sec-Websocket---Version">>} - ], - [{H, fun() -> R = format_header(H) end} || {H, R} <- Tests]. - --endif. diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 151b4dc..cdd1e06 100644 --- a/src/cowboy_req.erl +++ b/src/cowboy_req.erl @@ -42,7 +42,7 @@ -module(cowboy_req). %% Request API. --export([new/9]). +-export([new/13]). -export([method/1]). -export([version/1]). -export([peer/1]). @@ -56,6 +56,7 @@ -export([qs_val/2]). -export([qs_val/3]). -export([qs_vals/1]). +-export([fragment/1]). -export([host_url/1]). -export([url/1]). -export([binding/2]). @@ -103,14 +104,9 @@ -export([ensure_response/2]). %% Private setter/getter API. --export([set_host/4]). --export([set_connection/2]). --export([add_header/3]). --export([set_buffer/2]). +-export([get/2]). +-export([set/2]). -export([set_bindings/4]). --export([get_resp_state/1]). --export([get_buffer/1]). --export([get_connection/1]). %% Misc API. -export([compact/1]). @@ -122,6 +118,8 @@ -include_lib("eunit/include/eunit.hrl"). -endif. +-type resp_body_fun() :: fun(() -> {sent, non_neg_integer()}). + -record(http_req, { %% Transport. socket = undefined :: undefined | inet:socket(), @@ -130,7 +128,7 @@ %% Request. pid = undefined :: pid(), - method = 'GET' :: cowboy_http:method(), + method = <<"GET">> :: binary(), version = {1, 1} :: cowboy_http:version(), peer = undefined :: undefined | {inet:ip_address(), inet:port_number()}, host = undefined :: undefined | binary(), @@ -140,6 +138,7 @@ path_info = undefined :: undefined | cowboy_dispatcher:tokens(), qs = undefined :: binary(), qs_vals = undefined :: undefined | list({binary(), binary() | true}), + fragment = undefined :: binary(), bindings = undefined :: undefined | cowboy_dispatcher:bindings(), headers = [] :: cowboy_http:headers(), p_headers = [] :: [any()], %% @todo Improve those specs. @@ -154,12 +153,10 @@ %% Response. resp_state = waiting :: locked | waiting | chunks | done, resp_headers = [] :: cowboy_http:headers(), - resp_body = <<>> :: iodata() - | {non_neg_integer(), fun(() -> {sent, non_neg_integer()})}, + resp_body = <<>> :: iodata() | {non_neg_integer(), resp_body_fun()}, %% Functions. - onresponse = undefined :: undefined | cowboy_protocol:onresponse_fun(), - urldecode :: {fun((binary(), T) -> binary()), T} + onresponse = undefined :: undefined | cowboy_protocol:onresponse_fun() }). -opaque req() :: #http_req{}. @@ -171,18 +168,40 @@ %% %% This function takes care of setting the owner's pid to self(). %% @private --spec new(inet:socket(), module(), keepalive | close, - cowboy_http:method(), cowboy_http:version(), binary(), binary(), - undefined | fun(), undefined | {fun(), atom()}) +%% +%% Since we always need to parse the Connection header, we do it +%% 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(), + undefined | cowboy_protocol:onresponse_fun()) -> req(). -new(Socket, Transport, Connection, Method, Version, Path, Qs, - OnResponse, URLDecode) -> - #http_req{socket=Socket, transport=Transport, connection=Connection, - pid=self(), method=Method, version=Version, path=Path, qs=Qs, - onresponse=OnResponse, urldecode=URLDecode}. +new(Socket, Transport, Method, Path, Query, Fragment, + Version, Headers, Host, Port, Buffer, CanKeepalive, + 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}, + case CanKeepalive of + false -> + Req#http_req{connection=close}; + true -> + case lists:keyfind(<<"connection">>, 1, Headers) of + false when Version =:= {1, 1} -> + Req; %% keepalive + false -> + Req#http_req{connection=close}; + {_, ConnectionHeader} -> + Tokens = parse_connection_before(ConnectionHeader, []), + Connection = connection_to_atom(Tokens), + Req#http_req{connection=Connection, + p_headers=[{<<"connection">>, Tokens}]} + end + end. %% @doc Return the HTTP method of the request. --spec method(Req) -> {cowboy_http:method(), Req} when Req::req(). +-spec method(Req) -> {binary(), Req} when Req::req(). method(Req) -> {Req#http_req.method, Req}. @@ -203,8 +222,8 @@ peer(Req) -> %% @doc Returns the peer address calculated from headers. -spec peer_addr(Req) -> {inet:ip_address(), Req} when Req::req(). peer_addr(Req = #http_req{}) -> - {RealIp, Req1} = header(<<"X-Real-Ip">>, Req), - {ForwardedForRaw, Req2} = header(<<"X-Forwarded-For">>, Req1), + {RealIp, Req1} = header(<<"x-real-ip">>, Req), + {ForwardedForRaw, Req2} = header(<<"x-forwarded-for">>, Req1), {{PeerIp, _PeerPort}, Req3} = peer(Req2), ForwardedFor = case ForwardedForRaw of undefined -> @@ -267,10 +286,9 @@ qs_val(Name, Req) when is_binary(Name) -> %% missing. -spec qs_val(binary(), Req, Default) -> {binary() | true | Default, Req} when Req::req(), Default::any(). -qs_val(Name, Req=#http_req{qs=RawQs, qs_vals=undefined, - urldecode={URLDecFun, URLDecArg}}, Default) when is_binary(Name) -> - QsVals = cowboy_http:x_www_form_urlencoded( - RawQs, fun(Bin) -> URLDecFun(Bin, URLDecArg) end), +qs_val(Name, Req=#http_req{qs=RawQs, qs_vals=undefined}, Default) + when is_binary(Name) -> + QsVals = cowboy_http:x_www_form_urlencoded(RawQs), qs_val(Name, Req#http_req{qs_vals=QsVals}, Default); qs_val(Name, Req, Default) -> case lists:keyfind(Name, 1, Req#http_req.qs_vals) of @@ -280,14 +298,17 @@ qs_val(Name, Req, Default) -> %% @doc Return the full list of query string values. -spec qs_vals(Req) -> {list({binary(), binary() | true}), Req} when Req::req(). -qs_vals(Req=#http_req{qs=RawQs, qs_vals=undefined, - urldecode={URLDecFun, URLDecArg}}) -> - QsVals = cowboy_http:x_www_form_urlencoded( - RawQs, fun(Bin) -> URLDecFun(Bin, URLDecArg) end), +qs_vals(Req=#http_req{qs=RawQs, qs_vals=undefined}) -> + QsVals = cowboy_http:x_www_form_urlencoded(RawQs), qs_vals(Req#http_req{qs_vals=QsVals}); qs_vals(Req=#http_req{qs_vals=QsVals}) -> {QsVals, Req}. +%% @doc Return the raw fragment directly taken from the request. +-spec fragment(Req) -> {binary(), Req} when Req::req(). +fragment(Req) -> + {Req#http_req.fragment, Req}. + %% @doc Return the request URL as a binary without the path and query string. %% %% The URL includes the scheme, host and port only. @@ -308,15 +329,19 @@ host_url(Req=#http_req{transport=Transport, host=Host, port=Port}) -> %% @doc Return the full request URL as a binary. %% -%% The URL includes the scheme, host, port, path and query string. +%% The URL includes the scheme, host, port, path, query string and fragment. -spec url(Req) -> {binary(), Req} when Req::req(). -url(Req=#http_req{path=Path, qs=QS}) -> +url(Req=#http_req{path=Path, qs=QS, fragment=Fragment}) -> {HostURL, Req2} = host_url(Req), QS2 = case QS of <<>> -> <<>>; _ -> << "?", QS/binary >> end, - {<< HostURL/binary, Path/binary, QS2/binary >>, Req2}. + Fragment2 = case Fragment of + <<>> -> <<>>; + _ -> << "#", Fragment/binary >> + end, + {<< HostURL/binary, Path/binary, QS2/binary, Fragment2/binary >>, Req2}. %% @equiv binding(Name, Req, undefined) -spec binding(atom(), Req) -> {binary() | undefined, Req} when Req::req(). @@ -339,15 +364,15 @@ bindings(Req) -> {Req#http_req.bindings, Req}. %% @equiv header(Name, Req, undefined) --spec header(atom() | binary(), Req) +-spec header(binary(), Req) -> {binary() | undefined, Req} when Req::req(). -header(Name, Req) when is_atom(Name) orelse is_binary(Name) -> +header(Name, Req) -> header(Name, Req, undefined). %% @doc Return the header value for the given key, or a default if missing. --spec header(atom() | binary(), Req, Default) +-spec header(binary(), Req, Default) -> {binary() | Default, Req} when Req::req(), Default::any(). -header(Name, Req, Default) when is_atom(Name) orelse is_binary(Name) -> +header(Name, Req, Default) -> case lists:keyfind(Name, 1, Req#http_req.headers) of {Name, Value} -> {Value, Req}; false -> {Default, Req} @@ -363,7 +388,7 @@ headers(Req) -> %% 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(cowboy_http:header(), Req) +-spec parse_header(binary(), Req) -> {ok, any(), Req} | {undefined, binary(), Req} | {error, badarg} when Req::req(). parse_header(Name, Req=#http_req{p_headers=PHeaders}) -> @@ -373,76 +398,72 @@ parse_header(Name, Req=#http_req{p_headers=PHeaders}) -> end. %% @doc Default values for semantic header parsing. --spec parse_header_default(cowboy_http:header()) -> any(). -parse_header_default('Connection') -> []; -parse_header_default('Transfer-Encoding') -> [<<"identity">>]; +-spec parse_header_default(binary()) -> any(). +parse_header_default(<<"connection">>) -> []; +parse_header_default(<<"transfer-encoding">>) -> [<<"identity">>]; parse_header_default(_Name) -> undefined. %% @doc Semantically parse headers. %% %% When the header is unknown, the value is returned directly without parsing. --spec parse_header(cowboy_http:header(), Req, any()) +-spec parse_header(binary(), Req, any()) -> {ok, any(), Req} | {undefined, binary(), Req} | {error, badarg} when Req::req(). -parse_header(Name, Req, Default) when Name =:= 'Accept' -> +parse_header(Name, Req, Default) when Name =:= <<"accept">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:list(Value, fun cowboy_http:media_range/2) end); -parse_header(Name, Req, Default) when Name =:= 'Accept-Charset' -> +parse_header(Name, Req, Default) when Name =:= <<"accept-charset">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:conneg/2) end); -parse_header(Name, Req, Default) when Name =:= 'Accept-Encoding' -> +parse_header(Name, Req, Default) when Name =:= <<"accept-encoding">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:list(Value, fun cowboy_http:conneg/2) end); -parse_header(Name, Req, Default) when Name =:= 'Accept-Language' -> +parse_header(Name, Req, Default) when Name =:= <<"accept-language">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:language_range/2) end); -parse_header(Name, Req, Default) when Name =:= 'Connection' -> - parse_header(Name, Req, Default, - fun (Value) -> - cowboy_http:nonempty_list(Value, fun cowboy_http:token_ci/2) - end); -parse_header(Name, Req, Default) when Name =:= 'Content-Length' -> +parse_header(Name, Req, Default) when Name =:= <<"content-length">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:digits(Value) end); -parse_header(Name, Req, Default) when Name =:= 'Content-Type' -> +parse_header(Name, Req, Default) when Name =:= <<"content-type">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:content_type(Value) end); -parse_header(Name, Req, Default) when Name =:= <<"Expect">> -> +parse_header(Name, Req, Default) when Name =:= <<"expect">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:expectation/2) end); parse_header(Name, Req, Default) - when Name =:= 'If-Match'; Name =:= 'If-None-Match' -> + when Name =:= <<"if-match">>; Name =:= <<"if-none-match">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:entity_tag_match(Value) end); parse_header(Name, Req, Default) - when Name =:= 'If-Modified-Since'; Name =:= 'If-Unmodified-Since' -> + when Name =:= <<"if-modified-since">>; + Name =:= <<"if-unmodified-since">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:http_date(Value) end); %% @todo Extension parameters. -parse_header(Name, Req, Default) when Name =:= 'Transfer-Encoding' -> +parse_header(Name, Req, Default) when Name =:= <<"transfer-encoding">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:token_ci/2) end); -parse_header(Name, Req, Default) when Name =:= 'Upgrade' -> +parse_header(Name, Req, Default) when Name =:= <<"upgrade">> -> parse_header(Name, Req, Default, fun (Value) -> cowboy_http:nonempty_list(Value, fun cowboy_http:token_ci/2) @@ -475,7 +496,7 @@ cookie(Name, Req) when is_binary(Name) -> -spec cookie(binary(), Req, Default) -> {binary() | true | Default, Req} when Req::req(), Default::any(). cookie(Name, Req=#http_req{cookies=undefined}, Default) when is_binary(Name) -> - case header('Cookie', Req) of + case header(<<"cookie">>, Req) of {undefined, Req2} -> {Default, Req2#http_req{cookies=[]}}; {RawCookie, Req2} -> @@ -491,7 +512,7 @@ cookie(Name, Req, Default) -> %% @doc Return the full list of cookie values. -spec cookies(Req) -> {list({binary(), binary() | true}), Req} when Req::req(). cookies(Req=#http_req{cookies=undefined}) -> - case header('Cookie', Req) of + case header(<<"cookie">>, Req) of {undefined, Req2} -> {[], Req2#http_req{cookies=[]}}; {RawCookie, Req2} -> @@ -532,8 +553,8 @@ set_meta(Name, Value, Req=#http_req{meta=Meta}) -> %% @doc Return whether the request message has a body. -spec has_body(Req) -> {boolean(), Req} when Req::req(). has_body(Req) -> - Has = lists:keymember('Content-Length', 1, Req#http_req.headers) orelse - lists:keymember('Transfer-Encoding', 1, Req#http_req.headers), + Has = lists:keymember(<<"content-length">>, 1, Req#http_req.headers) orelse + lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers), {Has, Req}. %% @doc Return the request message body length, if known. @@ -542,11 +563,11 @@ has_body(Req) -> %% and the body hasn't been read at the time of the call. -spec body_length(Req) -> {undefined | non_neg_integer(), Req} when Req::req(). body_length(Req) -> - case lists:keymember('Transfer-Encoding', 1, Req#http_req.headers) of + case lists:keymember(<<"transfer-encoding">>, 1, Req#http_req.headers) of true -> {undefined, Req}; false -> - {ok, Length, Req2} = parse_header('Content-Length', Req, 0), + {ok, Length, Req2} = parse_header(<<"content-length">>, Req, 0), {Length, Req2} end. @@ -586,7 +607,7 @@ init_stream(TransferDecode, TransferState, ContentDecode, Req) -> | {done, Req} | {error, atom()} when Req::req(). stream_body(Req=#http_req{body_state=waiting, version=Version, transport=Transport, socket=Socket}) -> - case parse_header(<<"Expect">>, Req) of + case parse_header(<<"expect">>, Req) of {ok, [<<"100-continue">>], Req1} -> HTTPVer = cowboy_http:version_to_binary(Version), Transport:send(Socket, @@ -594,7 +615,7 @@ stream_body(Req=#http_req{body_state=waiting, {ok, undefined, Req1} -> ok end, - case parse_header('Transfer-Encoding', Req1) of + case parse_header(<<"transfer-encoding">>, Req1) of {ok, [<<"chunked">>], Req2} -> stream_body(Req2#http_req{body_state= {stream, fun cowboy_http:te_chunked/2, {0, 0}, @@ -657,13 +678,13 @@ transfer_decode(Data, Req=#http_req{ -> Req when Req::req(). transfer_decode_done(Length, Rest, Req=#http_req{ headers=Headers, p_headers=PHeaders}) -> - Headers2 = lists:keystore('Content-Length', 1, Headers, - {'Content-Length', list_to_binary(integer_to_list(Length))}), + Headers2 = lists:keystore(<<"content-length">>, 1, Headers, + {<<"content-length">>, list_to_binary(integer_to_list(Length))}), %% At this point we just assume TEs were all decoded. - Headers3 = lists:keydelete('Transfer-Encoding', 1, Headers2), - PHeaders2 = lists:keystore('Content-Length', 1, PHeaders, - {'Content-Length', Length}), - PHeaders3 = lists:keydelete('Transfer-Encoding', 1, PHeaders2), + Headers3 = lists:keydelete(<<"transfer-encoding">>, 1, Headers2), + PHeaders2 = lists:keystore(<<"content-length">>, 1, PHeaders, + {<<"content-length">>, Length}), + PHeaders3 = lists:keydelete(<<"transfer-encoding">>, 1, PHeaders2), Req#http_req{buffer=Rest, body_state=done, headers=Headers3, p_headers=PHeaders3}. @@ -718,11 +739,10 @@ skip_body(Req) -> -spec body_qs(Req) -> {ok, [{binary(), binary() | true}], Req} | {error, atom()} when Req::req(). -body_qs(Req=#http_req{urldecode={URLDecFun, URLDecArg}}) -> +body_qs(Req) -> case body(Req) of {ok, Body, Req2} -> - {ok, cowboy_http:x_www_form_urlencoded( - Body, fun(Bin) -> URLDecFun(Bin, URLDecArg) end), Req2}; + {ok, cowboy_http:x_www_form_urlencoded(Body), Req2}; {error, Reason} -> {error, Reason} end. @@ -743,9 +763,9 @@ body_qs(Req=#http_req{urldecode={URLDecFun, URLDecArg}}) -> | {end_of_part | eof, Req} when Req::req(). multipart_data(Req=#http_req{body_state=waiting}) -> {ok, {<<"multipart">>, _SubType, Params}, Req2} = - parse_header('Content-Type', Req), + parse_header(<<"content-type">>, Req), {_, Boundary} = lists:keyfind(<<"boundary">>, 1, Params), - {ok, Length, Req3} = parse_header('Content-Length', Req2), + {ok, Length, Req3} = parse_header(<<"content-length">>, Req2), multipart_data(Req3, Length, {more, cowboy_multipart:parser(Boundary)}); multipart_data(Req=#http_req{multipart={Length, Cont}}) -> multipart_data(Req, Length, Cont()); @@ -796,11 +816,10 @@ set_resp_cookie(Name, Value, Options, Req) -> set_resp_header(HeaderName, HeaderValue, Req). %% @doc Add a header to the response. --spec set_resp_header(cowboy_http:header(), iodata(), Req) +-spec set_resp_header(binary(), iodata(), Req) -> Req when Req::req(). set_resp_header(Name, Value, Req=#http_req{resp_headers=RespHeaders}) -> - NameBin = header_to_binary(Name), - Req#http_req{resp_headers=[{NameBin, Value}|RespHeaders]}. + Req#http_req{resp_headers=[{Name, Value}|RespHeaders]}. %% @doc Add a body to the response. %% @@ -825,16 +844,15 @@ set_resp_body(Body, Req) -> %% `reply/3'. %% %% @see cowboy_req:transport/1. --spec set_resp_body_fun(non_neg_integer(), - fun(() -> {sent, non_neg_integer()}), Req) -> Req when Req::req(). +-spec set_resp_body_fun(non_neg_integer(), resp_body_fun(), Req) + -> Req when Req::req(). set_resp_body_fun(StreamLen, StreamFun, Req) -> Req#http_req{resp_body={StreamLen, StreamFun}}. %% @doc Return whether the given header has been set for the response. --spec has_resp_header(cowboy_http:header(), req()) -> boolean(). +-spec has_resp_header(binary(), req()) -> boolean(). has_resp_header(Name, #http_req{resp_headers=RespHeaders}) -> - NameBin = header_to_binary(Name), - lists:keymember(NameBin, 1, RespHeaders). + lists:keymember(Name, 1, RespHeaders). %% @doc Return whether a body has been set for the response. -spec has_resp_body(req()) -> boolean(). @@ -844,7 +862,7 @@ has_resp_body(#http_req{resp_body=RespBody}) -> iolist_size(RespBody) > 0. %% Remove a header previously set for the response. --spec delete_resp_header(cowboy_http:header(), Req) +-spec delete_resp_header(binary(), Req) -> Req when Req::req(). delete_resp_header(Name, Req=#http_req{resp_headers=RespHeaders}) -> RespHeaders2 = lists:keydelete(Name, 1, RespHeaders), @@ -862,29 +880,35 @@ reply(Status, Headers, Req=#http_req{resp_body=Body}) -> reply(Status, Headers, Body, Req). %% @doc Send a reply to the client. --spec reply(cowboy_http:status(), cowboy_http:headers(), iodata(), Req) +-spec reply(cowboy_http:status(), cowboy_http:headers(), + iodata() | {non_neg_integer() | resp_body_fun()}, Req) -> {ok, Req} when Req::req(). -reply(Status, Headers, Body, Req=#http_req{socket=Socket, transport=Transport, +reply(Status, Headers, Body, Req=#http_req{ version=Version, connection=Connection, method=Method, resp_state=waiting, resp_headers=RespHeaders}) -> RespConn = response_connection(Headers, Connection), - ContentLen = case Body of {CL, _} -> CL; _ -> iolist_size(Body) end, HTTP11Headers = case Version of - {1, 1} -> [{<<"Connection">>, atom_to_connection(Connection)}]; + {1, 1} -> [{<<"connection">>, atom_to_connection(Connection)}]; _ -> [] end, - {ReplyType, Req2} = response(Status, Headers, RespHeaders, [ - {<<"Content-Length">>, integer_to_list(ContentLen)}, - {<<"Date">>, cowboy_clock:rfc1123()}, - {<<"Server">>, <<"Cowboy">>} - |HTTP11Headers], Req), - if Method =:= 'HEAD' -> ok; - ReplyType =:= hook -> ok; %% Hook replied for us, stop there. - true -> - case Body of - {_, StreamFun} -> StreamFun(); - _ -> Transport:send(Socket, Body) - end + case Body of + {ContentLength, BodyFun} -> + {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(); + true -> ok + end; + _ -> + {_, 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) end, {ok, Req2#http_req{connection=RespConn, resp_state=done, resp_headers=[], resp_body= <<>>}}. @@ -904,14 +928,14 @@ chunked_reply(Status, Headers, Req=#http_req{ RespConn = response_connection(Headers, Connection), HTTP11Headers = case Version of {1, 1} -> [ - {<<"Connection">>, atom_to_connection(Connection)}, - {<<"Transfer-Encoding">>, <<"chunked">>}]; + {<<"connection">>, atom_to_connection(Connection)}, + {<<"transfer-encoding">>, <<"chunked">>}]; _ -> [] end, {_, Req2} = response(Status, Headers, RespHeaders, [ - {<<"Date">>, cowboy_clock:rfc1123()}, - {<<"Server">>, <<"Cowboy">>} - |HTTP11Headers], Req), + {<<"date">>, cowboy_clock:rfc1123()}, + {<<"server">>, <<"Cowboy">>} + |HTTP11Headers], <<>>, Req), {ok, Req2#http_req{connection=RespConn, resp_state=chunks, resp_headers=[], resp_body= <<>>}}. @@ -919,7 +943,7 @@ chunked_reply(Status, Headers, Req=#http_req{ %% %% A chunked reply must have been initiated before calling this function. -spec chunk(iodata(), req()) -> ok | {error, atom()}. -chunk(_Data, #http_req{socket=_Socket, transport=_Transport, method='HEAD'}) -> +chunk(_Data, #http_req{method= <<"HEAD">>}) -> ok; chunk(Data, #http_req{socket=Socket, transport=Transport, version={1, 0}}) -> Transport:send(Socket, Data); @@ -934,8 +958,8 @@ chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) -> upgrade_reply(Status, Headers, Req=#http_req{ resp_state=waiting, resp_headers=RespHeaders}) -> {_, Req2} = response(Status, Headers, RespHeaders, [ - {<<"Connection">>, <<"Upgrade">>} - ], Req), + {<<"connection">>, <<"Upgrade">>} + ], <<>>, Req), {ok, Req2#http_req{resp_state=done, resp_headers=[], resp_body= <<>>}}. %% @doc Ensure the response has been sent fully. @@ -950,7 +974,7 @@ ensure_response(Req=#http_req{resp_state=waiting}, Status) -> _ = reply(Status, [], [], Req), ok; %% Terminate the chunked body for HTTP/1.1 only. -ensure_response(#http_req{method='HEAD', resp_state=chunks}, _) -> +ensure_response(#http_req{method= <<"HEAD">>, resp_state=chunks}, _) -> ok; ensure_response(#http_req{version={1, 0}, resp_state=chunks}, _) -> ok; @@ -962,29 +986,70 @@ ensure_response(#http_req{socket=Socket, transport=Transport, %% Private setter/getter API. %% @private --spec set_host(binary(), inet:port_number(), binary(), Req) - -> Req when Req::req(). -set_host(Host, Port, RawHost, Req=#http_req{headers=Headers}) -> - Req#http_req{host=Host, port=Port, headers=[{'Host', RawHost}|Headers]}. +-spec get(atom(), req()) -> any(); ([atom()], req()) -> any(). +get(List, Req) when is_list(List) -> + [g(Atom, Req) || Atom <- List]; +get(Atom, Req) when is_atom(Atom) -> + g(Atom, Req). + +g(bindings, #http_req{bindings=Ret}) -> Ret; +g(body_state, #http_req{body_state=Ret}) -> Ret; +g(buffer, #http_req{buffer=Ret}) -> Ret; +g(connection, #http_req{connection=Ret}) -> Ret; +g(cookies, #http_req{cookies=Ret}) -> Ret; +g(fragment, #http_req{fragment=Ret}) -> Ret; +g(headers, #http_req{headers=Ret}) -> Ret; +g(host, #http_req{host=Ret}) -> Ret; +g(host_info, #http_req{host_info=Ret}) -> Ret; +g(meta, #http_req{meta=Ret}) -> Ret; +g(method, #http_req{method=Ret}) -> Ret; +g(multipart, #http_req{multipart=Ret}) -> Ret; +g(onresponse, #http_req{onresponse=Ret}) -> Ret; +g(p_headers, #http_req{p_headers=Ret}) -> Ret; +g(path, #http_req{path=Ret}) -> Ret; +g(path_info, #http_req{path_info=Ret}) -> Ret; +g(peer, #http_req{peer=Ret}) -> Ret; +g(pid, #http_req{pid=Ret}) -> Ret; +g(port, #http_req{port=Ret}) -> Ret; +g(qs, #http_req{qs=Ret}) -> Ret; +g(qs_vals, #http_req{qs_vals=Ret}) -> Ret; +g(resp_body, #http_req{resp_body=Ret}) -> Ret; +g(resp_headers, #http_req{resp_headers=Ret}) -> Ret; +g(resp_state, #http_req{resp_state=Ret}) -> Ret; +g(socket, #http_req{socket=Ret}) -> Ret; +g(transport, #http_req{transport=Ret}) -> Ret; +g(version, #http_req{version=Ret}) -> Ret. %% @private --spec set_connection(binary(), Req) -> Req when Req::req(). -set_connection(RawConnection, Req=#http_req{headers=Headers}) -> - Req2 = Req#http_req{headers=[{'Connection', RawConnection}|Headers]}, - {ok, ConnTokens, Req3} = parse_header('Connection', Req2), - ConnAtom = cowboy_http:connection_to_atom(ConnTokens), - Req3#http_req{connection=ConnAtom}. - -%% @private --spec add_header(cowboy_http:header(), binary(), Req) - -> Req when Req::req(). -add_header(Name, Value, Req=#http_req{headers=Headers}) -> - Req#http_req{headers=[{Name, Value}|Headers]}. - -%% @private --spec set_buffer(binary(), Req) -> Req when Req::req(). -set_buffer(Buffer, Req) -> - Req#http_req{buffer=Buffer}. +-spec set([{atom(), any()}], Req) -> Req when Req::req(). +set([], Req) -> Req; +set([{bindings, Val}|Tail], Req) -> set(Tail, Req#http_req{bindings=Val}); +set([{body_state, Val}|Tail], Req) -> set(Tail, Req#http_req{body_state=Val}); +set([{buffer, Val}|Tail], Req) -> set(Tail, Req#http_req{buffer=Val}); +set([{connection, Val}|Tail], Req) -> set(Tail, Req#http_req{connection=Val}); +set([{cookies, Val}|Tail], Req) -> set(Tail, Req#http_req{cookies=Val}); +set([{fragment, Val}|Tail], Req) -> set(Tail, Req#http_req{fragment=Val}); +set([{headers, Val}|Tail], Req) -> set(Tail, Req#http_req{headers=Val}); +set([{host, Val}|Tail], Req) -> set(Tail, Req#http_req{host=Val}); +set([{host_info, Val}|Tail], Req) -> set(Tail, Req#http_req{host_info=Val}); +set([{meta, Val}|Tail], Req) -> set(Tail, Req#http_req{meta=Val}); +set([{method, Val}|Tail], Req) -> set(Tail, Req#http_req{method=Val}); +set([{multipart, Val}|Tail], Req) -> set(Tail, Req#http_req{multipart=Val}); +set([{onresponse, Val}|Tail], Req) -> set(Tail, Req#http_req{onresponse=Val}); +set([{p_headers, Val}|Tail], Req) -> set(Tail, Req#http_req{p_headers=Val}); +set([{path, Val}|Tail], Req) -> set(Tail, Req#http_req{path=Val}); +set([{path_info, Val}|Tail], Req) -> set(Tail, Req#http_req{path_info=Val}); +set([{peer, Val}|Tail], Req) -> set(Tail, Req#http_req{peer=Val}); +set([{pid, Val}|Tail], Req) -> set(Tail, Req#http_req{pid=Val}); +set([{port, Val}|Tail], Req) -> set(Tail, Req#http_req{port=Val}); +set([{qs, Val}|Tail], Req) -> set(Tail, Req#http_req{qs=Val}); +set([{qs_vals, Val}|Tail], Req) -> set(Tail, Req#http_req{qs_vals=Val}); +set([{resp_body, Val}|Tail], Req) -> set(Tail, Req#http_req{resp_body=Val}); +set([{resp_headers, Val}|Tail], Req) -> set(Tail, Req#http_req{resp_headers=Val}); +set([{resp_state, Val}|Tail], Req) -> set(Tail, Req#http_req{resp_state=Val}); +set([{socket, Val}|Tail], Req) -> set(Tail, Req#http_req{socket=Val}); +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(), @@ -993,21 +1058,6 @@ set_bindings(HostInfo, PathInfo, Bindings, Req) -> Req#http_req{host_info=HostInfo, path_info=PathInfo, bindings=Bindings}. -%% @private --spec get_resp_state(req()) -> locked | waiting | chunks | done. -get_resp_state(#http_req{resp_state=RespState}) -> - RespState. - -%% @private --spec get_buffer(req()) -> binary(). -get_buffer(#http_req{buffer=Buffer}) -> - Buffer. - -%% @private --spec get_connection(req()) -> keepalive | close. -get_connection(#http_req{connection=Connection}) -> - Connection. - %% Misc API. %% @doc Compact the request data by removing all non-system information. @@ -1050,9 +1100,9 @@ transport(#http_req{transport=Transport, socket=Socket}) -> %% Internal. -spec response(cowboy_http:status(), cowboy_http:headers(), - cowboy_http:headers(), cowboy_http:headers(), Req) + cowboy_http:headers(), cowboy_http:headers(), iodata(), Req) -> {normal | hook, Req} when Req::req(). -response(Status, Headers, RespHeaders, DefaultHeaders, Req=#http_req{ +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), @@ -1070,7 +1120,7 @@ response(Status, Headers, RespHeaders, DefaultHeaders, Req=#http_req{ (status(Status))/binary, "\r\n" >>, HeaderLines = [[Key, <<": ">>, Value, <<"\r\n">>] || {Key, Value} <- FullHeaders], - Transport:send(Socket, [StatusLine, HeaderLines, <<"\r\n">>]), + Transport:send(Socket, [StatusLine, HeaderLines, <<"\r\n">>, Body]), ReqPid ! {?MODULE, resp_sent}, normal; _ -> @@ -1084,25 +1134,19 @@ response_connection([], Connection) -> Connection; response_connection([{Name, Value}|Tail], Connection) -> case Name of - 'Connection' -> response_connection_parse(Value); - Name when is_atom(Name) -> response_connection(Tail, Connection); - Name -> - Name2 = cowboy_bstr:to_lower(Name), - case Name2 of - <<"connection">> -> response_connection_parse(Value); - _Any -> response_connection(Tail, Connection) - end + <<"connection">> -> response_connection_parse(Value); + _ -> response_connection(Tail, Connection) end. -spec response_connection_parse(binary()) -> keepalive | close. response_connection_parse(ReplyConn) -> Tokens = cowboy_http:nonempty_list(ReplyConn, fun cowboy_http:token/2), - cowboy_http:connection_to_atom(Tokens). + connection_to_atom(Tokens). -spec response_merge_headers(cowboy_http:headers(), cowboy_http:headers(), cowboy_http:headers()) -> cowboy_http:headers(). response_merge_headers(Headers, RespHeaders, DefaultHeaders) -> - Headers2 = [{header_to_binary(Key), Value} || {Key, Value} <- Headers], + Headers2 = [{Key, Value} || {Key, Value} <- Headers], merge_headers( merge_headers(Headers2, RespHeaders), DefaultHeaders). @@ -1114,7 +1158,7 @@ merge_headers(Headers, []) -> merge_headers(Headers, [{Name, Value}|Tail]) -> Headers2 = case lists:keymember(Name, 1, Headers) of true -> Headers; - false -> Headers ++ [{Name, Value}] + false -> [{Name, Value}|Headers] end, merge_headers(Headers2, Tail). @@ -1125,6 +1169,74 @@ atom_to_connection(keepalive) -> atom_to_connection(close) -> <<"close">>. +%% Optimized parsing functions for the Connection header. +parse_connection_before(<<>>, Acc) -> + lists:reverse(Acc); +parse_connection_before(<< C, Rest/bits >>, Acc) + when C =:= $,; C =:= $\s; C =:= $\t -> + parse_connection_before(Rest, Acc); +parse_connection_before(Buffer, Acc) -> + parse_connection(Buffer, Acc, <<>>). + +%% An evil block of code appeared! +parse_connection(<<>>, Acc, <<>>) -> + lists:reverse(Acc); +parse_connection(<<>>, Acc, Token) -> + lists:reverse([Token|Acc]); +parse_connection(<< C, Rest/bits >>, Acc, Token) + when C =:= $,; C =:= $\s; C =:= $\t -> + parse_connection_after(Rest, [Token|Acc]); +parse_connection(<< C, Rest/bits >>, Acc, Token) -> + case C of + $A -> parse_connection(Rest, Acc, << Token/binary, $a >>); + $B -> parse_connection(Rest, Acc, << Token/binary, $b >>); + $C -> parse_connection(Rest, Acc, << Token/binary, $c >>); + $D -> parse_connection(Rest, Acc, << Token/binary, $d >>); + $E -> parse_connection(Rest, Acc, << Token/binary, $e >>); + $F -> parse_connection(Rest, Acc, << Token/binary, $f >>); + $G -> parse_connection(Rest, Acc, << Token/binary, $g >>); + $H -> parse_connection(Rest, Acc, << Token/binary, $h >>); + $I -> parse_connection(Rest, Acc, << Token/binary, $i >>); + $J -> parse_connection(Rest, Acc, << Token/binary, $j >>); + $K -> parse_connection(Rest, Acc, << Token/binary, $k >>); + $L -> parse_connection(Rest, Acc, << Token/binary, $l >>); + $M -> parse_connection(Rest, Acc, << Token/binary, $m >>); + $N -> parse_connection(Rest, Acc, << Token/binary, $n >>); + $O -> parse_connection(Rest, Acc, << Token/binary, $o >>); + $P -> parse_connection(Rest, Acc, << Token/binary, $p >>); + $Q -> parse_connection(Rest, Acc, << Token/binary, $q >>); + $R -> parse_connection(Rest, Acc, << Token/binary, $r >>); + $S -> parse_connection(Rest, Acc, << Token/binary, $s >>); + $T -> parse_connection(Rest, Acc, << Token/binary, $t >>); + $U -> parse_connection(Rest, Acc, << Token/binary, $u >>); + $V -> parse_connection(Rest, Acc, << Token/binary, $v >>); + $W -> parse_connection(Rest, Acc, << Token/binary, $w >>); + $X -> parse_connection(Rest, Acc, << Token/binary, $x >>); + $Y -> parse_connection(Rest, Acc, << Token/binary, $y >>); + $Z -> parse_connection(Rest, Acc, << Token/binary, $z >>); + C -> parse_connection(Rest, Acc, << Token/binary, C >>) + end. + +parse_connection_after(<<>>, Acc) -> + lists:reverse(Acc); +parse_connection_after(<< $,, Rest/bits >>, Acc) -> + parse_connection_before(Rest, Acc); +parse_connection_after(<< C, Rest/bits >>, Acc) + when C =:= $\s; C =:= $\t -> + parse_connection_after(Rest, Acc). + +%% @doc Walk through a tokens list and return whether +%% the connection is keepalive or closed. +%% +%% We don't match on "keep-alive" since it is the default value. +-spec connection_to_atom([binary()]) -> keepalive | close. +connection_to_atom([]) -> + keepalive; +connection_to_atom([<<"close">>|_]) -> + close; +connection_to_atom([_|Tail]) -> + connection_to_atom(Tail). + -spec status(cowboy_http:status()) -> binary(). status(100) -> <<"100 Continue">>; status(101) -> <<"101 Switching Protocols">>; @@ -1185,61 +1297,6 @@ status(510) -> <<"510 Not Extended">>; status(511) -> <<"511 Network Authentication Required">>; status(B) when is_binary(B) -> B. --spec header_to_binary(cowboy_http:header()) -> binary(). -header_to_binary('Cache-Control') -> <<"Cache-Control">>; -header_to_binary('Connection') -> <<"Connection">>; -header_to_binary('Date') -> <<"Date">>; -header_to_binary('Pragma') -> <<"Pragma">>; -header_to_binary('Transfer-Encoding') -> <<"Transfer-Encoding">>; -header_to_binary('Upgrade') -> <<"Upgrade">>; -header_to_binary('Via') -> <<"Via">>; -header_to_binary('Accept') -> <<"Accept">>; -header_to_binary('Accept-Charset') -> <<"Accept-Charset">>; -header_to_binary('Accept-Encoding') -> <<"Accept-Encoding">>; -header_to_binary('Accept-Language') -> <<"Accept-Language">>; -header_to_binary('Authorization') -> <<"Authorization">>; -header_to_binary('From') -> <<"From">>; -header_to_binary('Host') -> <<"Host">>; -header_to_binary('If-Modified-Since') -> <<"If-Modified-Since">>; -header_to_binary('If-Match') -> <<"If-Match">>; -header_to_binary('If-None-Match') -> <<"If-None-Match">>; -header_to_binary('If-Range') -> <<"If-Range">>; -header_to_binary('If-Unmodified-Since') -> <<"If-Unmodified-Since">>; -header_to_binary('Max-Forwards') -> <<"Max-Forwards">>; -header_to_binary('Proxy-Authorization') -> <<"Proxy-Authorization">>; -header_to_binary('Range') -> <<"Range">>; -header_to_binary('Referer') -> <<"Referer">>; -header_to_binary('User-Agent') -> <<"User-Agent">>; -header_to_binary('Age') -> <<"Age">>; -header_to_binary('Location') -> <<"Location">>; -header_to_binary('Proxy-Authenticate') -> <<"Proxy-Authenticate">>; -header_to_binary('Public') -> <<"Public">>; -header_to_binary('Retry-After') -> <<"Retry-After">>; -header_to_binary('Server') -> <<"Server">>; -header_to_binary('Vary') -> <<"Vary">>; -header_to_binary('Warning') -> <<"Warning">>; -header_to_binary('Www-Authenticate') -> <<"Www-Authenticate">>; -header_to_binary('Allow') -> <<"Allow">>; -header_to_binary('Content-Base') -> <<"Content-Base">>; -header_to_binary('Content-Encoding') -> <<"Content-Encoding">>; -header_to_binary('Content-Language') -> <<"Content-Language">>; -header_to_binary('Content-Length') -> <<"Content-Length">>; -header_to_binary('Content-Location') -> <<"Content-Location">>; -header_to_binary('Content-Md5') -> <<"Content-Md5">>; -header_to_binary('Content-Range') -> <<"Content-Range">>; -header_to_binary('Content-Type') -> <<"Content-Type">>; -header_to_binary('Etag') -> <<"Etag">>; -header_to_binary('Expires') -> <<"Expires">>; -header_to_binary('Last-Modified') -> <<"Last-Modified">>; -header_to_binary('Accept-Ranges') -> <<"Accept-Ranges">>; -header_to_binary('Set-Cookie') -> <<"Set-Cookie">>; -header_to_binary('Set-Cookie2') -> <<"Set-Cookie2">>; -header_to_binary('X-Forwarded-For') -> <<"X-Forwarded-For">>; -header_to_binary('Cookie') -> <<"Cookie">>; -header_to_binary('Keep-Alive') -> <<"Keep-Alive">>; -header_to_binary('Proxy-Connection') -> <<"Proxy-Connection">>; -header_to_binary(B) when is_binary(B) -> B. - %% Tests. -ifdef(TEST). @@ -1247,25 +1304,45 @@ header_to_binary(B) when is_binary(B) -> B. url_test() -> {<<"http://localhost/path">>, _ } = url(#http_req{transport=ranch_tcp, host= <<"localhost">>, port=80, - path= <<"/path">>, qs= <<>>, pid=self()}), + path= <<"/path">>, qs= <<>>, fragment= <<>>, pid=self()}), {<<"http://localhost:443/path">>, _} = url(#http_req{transport=ranch_tcp, host= <<"localhost">>, port=443, - path= <<"/path">>, qs= <<>>, pid=self()}), + path= <<"/path">>, qs= <<>>, fragment= <<>>, pid=self()}), {<<"http://localhost:8080/path">>, _} = url(#http_req{transport=ranch_tcp, host= <<"localhost">>, port=8080, - path= <<"/path">>, qs= <<>>, pid=self()}), + path= <<"/path">>, qs= <<>>, fragment= <<>>, pid=self()}), {<<"http://localhost:8080/path?dummy=2785">>, _} = url(#http_req{transport=ranch_tcp, host= <<"localhost">>, port=8080, - path= <<"/path">>, qs= <<"dummy=2785">>, pid=self()}), + path= <<"/path">>, qs= <<"dummy=2785">>, fragment= <<>>, + pid=self()}), + {<<"http://localhost:8080/path?dummy=2785#fragment">>, _} = + url(#http_req{transport=ranch_tcp, host= <<"localhost">>, port=8080, + path= <<"/path">>, qs= <<"dummy=2785">>, fragment= <<"fragment">>, + pid=self()}), {<<"https://localhost/path">>, _} = url(#http_req{transport=ranch_ssl, host= <<"localhost">>, port=443, - path= <<"/path">>, qs= <<>>, pid=self()}), + path= <<"/path">>, qs= <<>>, fragment= <<>>, pid=self()}), {<<"https://localhost:8443/path">>, _} = url(#http_req{transport=ranch_ssl, host= <<"localhost">>, port=8443, - path= <<"/path">>, qs= <<>>, pid=self()}), + path= <<"/path">>, qs= <<>>, fragment= <<>>, pid=self()}), {<<"https://localhost:8443/path?dummy=2785">>, _} = url(#http_req{transport=ranch_ssl, host= <<"localhost">>, port=8443, - path= <<"/path">>, qs= <<"dummy=2785">>, pid=self()}), + path= <<"/path">>, qs= <<"dummy=2785">>, fragment= <<>>, + pid=self()}), + {<<"https://localhost:8443/path?dummy=2785#fragment">>, _} = + url(#http_req{transport=ranch_ssl, host= <<"localhost">>, port=8443, + path= <<"/path">>, qs= <<"dummy=2785">>, fragment= <<"fragment">>, + pid=self()}), ok. +connection_to_atom_test_() -> + %% {Tokens, Result} + Tests = [ + {[<<"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_rest.erl b/src/cowboy_rest.erl index da52ffe..2f9faa8 100644 --- a/src/cowboy_rest.erl +++ b/src/cowboy_rest.erl @@ -23,7 +23,7 @@ -export([upgrade/4]). -record(state, { - method = undefined :: cowboy_http:method(), + method = undefined :: binary(), %% Handler. handler :: atom(), @@ -58,16 +58,16 @@ -> {ok, Req} | close when Req::cowboy_req:req(). upgrade(_ListenerPid, Handler, Opts, Req) -> try - {Method, Req1} = cowboy_req:method(Req), + Method = cowboy_req:get(method, Req), case erlang:function_exported(Handler, rest_init, 2) of true -> - case Handler:rest_init(Req1, Opts) of + case Handler:rest_init(Req, Opts) of {ok, Req2, HandlerState} -> service_available(Req2, #state{method=Method, handler=Handler, handler_state=HandlerState}) end; false -> - service_available(Req1, #state{method=Method, + service_available(Req, #state{method=Method, handler=Handler}) end catch Class:Reason -> @@ -87,9 +87,10 @@ service_available(Req, State) -> %% known_methods/2 should return a list of atoms or binary methods. known_methods(Req, State=#state{method=Method}) -> case call(Req, State, known_methods) of - no_call when Method =:= 'HEAD'; Method =:= 'GET'; Method =:= 'POST'; - Method =:= 'PUT'; Method =:= 'DELETE'; Method =:= 'TRACE'; - Method =:= 'CONNECT'; Method =:= 'OPTIONS' -> + no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">>; + Method =:= <<"POST">>; Method =:= <<"PUT">>; + Method =:= <<"DELETE">>; Method =:= <<"TRACE">>; + Method =:= <<"CONNECT">>; Method =:= <<"OPTIONS">> -> next(Req, State, fun uri_too_long/2); no_call -> next(Req, State, 501); @@ -109,10 +110,10 @@ uri_too_long(Req, State) -> %% allowed_methods/2 should return a list of atoms or binary methods. allowed_methods(Req, State=#state{method=Method}) -> case call(Req, State, allowed_methods) of - no_call when Method =:= 'HEAD'; Method =:= 'GET' -> + no_call when Method =:= <<"HEAD">>; Method =:= <<"GET">> -> next(Req, State, fun malformed_request/2); no_call -> - method_not_allowed(Req, State, ['GET', 'HEAD']); + method_not_allowed(Req, State, [<<"GET">>, <<"HEAD">>]); {halt, Req2, HandlerState} -> terminate(Req2, State#state{handler_state=HandlerState}); {List, Req2, HandlerState} -> @@ -172,7 +173,7 @@ valid_entity_length(Req, State) -> %% If you need to add additional headers to the response at this point, %% you should do it directly in the options/2 call using set_resp_headers. -options(Req, State=#state{method='OPTIONS'}) -> +options(Req, State=#state{method= <<"OPTIONS">>}) -> case call(Req, State, options) of {halt, Req2, HandlerState} -> terminate(Req2, State#state{handler_state=HandlerState}); @@ -209,7 +210,7 @@ content_types_provided(Req, State) -> CTP2 = [normalize_content_types(P) || P <- CTP], State2 = State#state{ handler_state=HandlerState, content_types_p=CTP2}, - {ok, Accept, Req3} = cowboy_req:parse_header('Accept', Req2), + {ok, Accept, Req3} = cowboy_req:parse_header(<<"accept">>, Req2), case Accept of undefined -> {PMT, _Fun} = HeadCTP = hd(CTP2), @@ -304,7 +305,7 @@ languages_provided(Req, State) -> {LP, Req2, HandlerState} -> State2 = State#state{handler_state=HandlerState, languages_p=LP}, {ok, AcceptLanguage, Req3} = - cowboy_req:parse_header('Accept-Language', Req2), + cowboy_req:parse_header(<<"accept-language">>, Req2), case AcceptLanguage of undefined -> set_language(Req3, State2#state{language_a=hd(LP)}); @@ -366,7 +367,7 @@ charsets_provided(Req, State) -> {CP, Req2, HandlerState} -> State2 = State#state{handler_state=HandlerState, charsets_p=CP}, {ok, AcceptCharset, Req3} = - cowboy_req:parse_header('Accept-Charset', Req2), + cowboy_req:parse_header(<<"accept-charset">>, Req2), case AcceptCharset of undefined -> set_content_type(Req3, State2#state{ @@ -478,7 +479,7 @@ resource_exists(Req, State) -> fun if_match_exists/2, fun if_match_musnt_exist/2). if_match_exists(Req, State) -> - case cowboy_req:parse_header('If-Match', Req) of + case cowboy_req:parse_header(<<"if-match">>, Req) of {ok, undefined, Req2} -> if_unmodified_since_exists(Req2, State); {ok, '*', Req2} -> @@ -496,13 +497,13 @@ if_match(Req, State, EtagsList) -> end. if_match_musnt_exist(Req, State) -> - case cowboy_req:header('If-Match', Req) of + case cowboy_req:header(<<"if-match">>, Req) of {undefined, Req2} -> is_put_to_missing_resource(Req2, State); {_Any, Req2} -> precondition_failed(Req2, State) end. if_unmodified_since_exists(Req, State) -> - case cowboy_req:parse_header('If-Unmodified-Since', Req) of + case cowboy_req:parse_header(<<"if-unmodified-since">>, Req) of {ok, undefined, Req2} -> if_none_match_exists(Req2, State); {ok, IfUnmodifiedSince, Req2} -> @@ -520,7 +521,7 @@ if_unmodified_since(Req, State, IfUnmodifiedSince) -> end. if_none_match_exists(Req, State) -> - case cowboy_req:parse_header('If-None-Match', Req) of + case cowboy_req:parse_header(<<"if-none-match">>, Req) of {ok, undefined, Req2} -> if_modified_since_exists(Req2, State); {ok, '*', Req2} -> @@ -542,13 +543,13 @@ if_none_match(Req, State, EtagsList) -> end. precondition_is_head_get(Req, State=#state{method=Method}) - when Method =:= 'HEAD'; Method =:= 'GET' -> + when Method =:= <<"HEAD">>; Method =:= <<"GET">> -> not_modified(Req, State); precondition_is_head_get(Req, State) -> precondition_failed(Req, State). if_modified_since_exists(Req, State) -> - case cowboy_req:parse_header('If-Modified-Since', Req) of + case cowboy_req:parse_header(<<"if-modified-since">>, Req) of {ok, undefined, Req2} -> method(Req2, State); {ok, IfModifiedSince, Req2} -> @@ -584,7 +585,7 @@ not_modified(Req, State) -> precondition_failed(Req, State) -> respond(Req, State, 412). -is_put_to_missing_resource(Req, State=#state{method='PUT'}) -> +is_put_to_missing_resource(Req, State=#state{method= <<"PUT">>}) -> moved_permanently(Req, State, fun is_conflict/2); is_put_to_missing_resource(Req, State) -> previously_existed(Req, State). @@ -626,7 +627,7 @@ moved_temporarily(Req, State) -> is_post_to_missing_resource(Req, State, 410) end. -is_post_to_missing_resource(Req, State=#state{method='POST'}, OnFalse) -> +is_post_to_missing_resource(Req, State=#state{method= <<"POST">>}, OnFalse) -> allow_missing_post(Req, State, OnFalse); is_post_to_missing_resource(Req, State, OnFalse) -> respond(Req, State, OnFalse). @@ -634,14 +635,14 @@ is_post_to_missing_resource(Req, State, OnFalse) -> allow_missing_post(Req, State, OnFalse) -> expect(Req, State, allow_missing_post, true, fun post_is_create/2, OnFalse). -method(Req, State=#state{method='DELETE'}) -> +method(Req, State=#state{method= <<"DELETE">>}) -> delete_resource(Req, State); -method(Req, State=#state{method='POST'}) -> +method(Req, State=#state{method= <<"POST">>}) -> post_is_create(Req, State); -method(Req, State=#state{method='PUT'}) -> +method(Req, State=#state{method= <<"PUT">>}) -> is_conflict(Req, State); method(Req, State=#state{method=Method}) - when Method =:= 'GET'; Method =:= 'HEAD' -> + when Method =:= <<"GET">>; Method =:= <<"HEAD">> -> set_resp_body(Req, State); method(Req, State) -> multiple_choices(Req, State). @@ -692,8 +693,8 @@ is_conflict(Req, State) -> expect(Req, State, is_conflict, false, fun put_resource/2, 409). put_resource(Req, State) -> - {Path, Req2} = cowboy_req:path(Req), - put_resource(cowboy_req:set_meta(put_path, Path, Req2), + Path = cowboy_req:get(path, Req), + put_resource(cowboy_req:set_meta(put_path, Path, Req), State, fun is_new_resource/2). %% content_types_accepted should return a list of media types and their @@ -714,7 +715,7 @@ put_resource(Req, State, OnTrue) -> CTA2 = [normalize_content_types(P) || P <- CTA], State2 = State#state{handler_state=HandlerState}, {ok, ContentType, Req3} - = cowboy_req:parse_header('Content-Type', Req2), + = cowboy_req:parse_header(<<"content-type">>, Req2), choose_content_type(Req3, State2, OnTrue, ContentType, CTA2) end. diff --git a/src/cowboy_static.erl b/src/cowboy_static.erl index aaf798c..5450115 100644 --- a/src/cowboy_static.erl +++ b/src/cowboy_static.erl @@ -247,9 +247,9 @@ rest_init(Req, Opts) -> %% @private Only allow GET and HEAD requests on files. -spec allowed_methods(Req, #state{}) - -> {[atom()], Req, #state{}} when Req::cowboy_req:req(). + -> {[binary()], Req, #state{}} when Req::cowboy_req:req(). allowed_methods(Req, State) -> - {['GET', 'HEAD'], Req, State}. + {[<<"GET">>, <<"HEAD">>], Req, State}. %% @private -spec malformed_request(Req, #state{}) diff --git a/src/cowboy_websocket.erl b/src/cowboy_websocket.erl index a6f2be3..dcd3008 100644 --- a/src/cowboy_websocket.erl +++ b/src/cowboy_websocket.erl @@ -72,11 +72,12 @@ upgrade(ListenerPid, Handler, Opts, Req) -> -> {ok, #state{}, Req} when Req::cowboy_req:req(). websocket_upgrade(State, Req) -> {ok, ConnTokens, Req2} - = cowboy_req:parse_header('Connection', Req), + = cowboy_req:parse_header(<<"connection">>, Req), true = lists:member(<<"upgrade">>, ConnTokens), %% @todo Should probably send a 426 if the Upgrade header is missing. - {ok, [<<"websocket">>], Req3} = cowboy_req:parse_header('Upgrade', Req2), - {Version, Req4} = cowboy_req:header(<<"Sec-Websocket-Version">>, Req3), + {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. @@ -90,9 +91,9 @@ websocket_upgrade(State, Req) -> %% 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), + {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}, @@ -101,7 +102,7 @@ websocket_upgrade(undefined, State, Req) -> websocket_upgrade(Version, State, Req) when Version =:= <<"7">>; Version =:= <<"8">>; Version =:= <<"13">> -> - {Key, Req2} = cowboy_req:header(<<"Sec-Websocket-Key">>, Req), + {Key, Req2} = cowboy_req:header(<<"sec-websocket-key">>, Req), false = Key =:= undefined, Challenge = hybi_challenge(Key), IntVersion = list_to_integer(binary_to_list(Version)), diff --git a/test/dispatcher_prop.erl b/test/dispatcher_prop.erl deleted file mode 100644 index 163fcd8..0000000 --- a/test/dispatcher_prop.erl +++ /dev/null @@ -1,68 +0,0 @@ -%% Copyright (c) 2011, Magnus Klaar <[email protected]> -%% 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(dispatcher_prop). --include_lib("proper/include/proper.hrl"). - -%% Generators. - -hostname_head_char() -> - oneof([choose($a, $z), choose($A, $Z), choose($0, $9)]). - -hostname_char() -> - oneof([choose($a, $z), choose($A, $Z), choose($0, $9), $-]). - -hostname_label() -> - ?SUCHTHAT(Label, [hostname_head_char()|list(hostname_char())], - length(Label) < 64). - -hostname() -> - ?SUCHTHAT(Hostname, - ?LET(Labels, list(hostname_label()), string:join(Labels, ".")), - length(Hostname) > 0 andalso length(Hostname) =< 255). - -port_number() -> - choose(1, 16#ffff). - -port_str() -> - oneof(["", ?LET(Port, port_number(), ":" ++ integer_to_list(Port))]). - -server() -> - ?LET({Hostname, PortStr}, {hostname(), port_str()}, - list_to_binary(Hostname ++ PortStr)). - -%% Properties. - -prop_split_host_symmetric() -> - ?FORALL(Server, server(), - begin case cowboy_dispatcher:split_host(Server) of - {Tokens, RawHost, undefined} -> - (Server == RawHost) and (Server == binary_join(Tokens, ".")); - {Tokens, RawHost, Port} -> - PortBin = (list_to_binary(":" ++ integer_to_list(Port))), - (Server == << RawHost/binary, PortBin/binary >>) - and (Server == << (binary_join(Tokens, "."))/binary, - PortBin/binary >>) - end end). - -%% Internal. - -%% Contributed by MononcQc on #erlounge. -binary_join(Flowers, Leaf) -> - case Flowers of - [] -> <<>>; - [Petal|Pot] -> iolist_to_binary( - [Petal | [[Leaf | Pollen] || Pollen <- Pot]]) - end. diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 1140cf9..cb59c07 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -218,11 +218,11 @@ init_dispatch(Config) -> {[<<"init_shutdown">>], http_handler_init_shutdown, []}, {[<<"long_polling">>], http_handler_long_polling, []}, {[<<"headers">>, <<"dupe">>], http_handler, - [{headers, [{<<"Connection">>, <<"close">>}]}]}, + [{headers, [{<<"connection">>, <<"close">>}]}]}, {[<<"set_resp">>, <<"header">>], http_handler_set_resp, - [{headers, [{<<"Vary">>, <<"Accept">>}]}]}, + [{headers, [{<<"vary">>, <<"Accept">>}]}]}, {[<<"set_resp">>, <<"overwrite">>], http_handler_set_resp, - [{headers, [{<<"Server">>, <<"DesireDrive/1.0">>}]}]}, + [{headers, [{<<"server">>, <<"DesireDrive/1.0">>}]}]}, {[<<"set_resp">>, <<"body">>], http_handler_set_resp, [{body, <<"A flameless dance does not equal a cycle">>}]}, {[<<"stream_body">>, <<"set_resp">>], http_handler_stream_body, @@ -327,17 +327,17 @@ check_raw_status(Config) -> HugeCookie = lists:flatten(["whatever_man_biiiiiiiiiiiig_cookie_me_want_77=" "Wed Apr 06 2011 10:38:52 GMT-0500 (CDT)" || _ <- lists:seq(1, 40)]), ResponsePacket = -"HTTP/1.0 302 Found -Location: http://www.google.co.il/ -Cache-Control: private -Content-Type: text/html; charset=UTF-8 -Set-Cookie: PREF=ID=568f67013d4a7afa:FF=0:TM=1323014101:LM=1323014101:S=XqctDWC65MzKT0zC; expires=Tue, 03-Dec-2013 15:55:01 GMT; path=/; domain=.google.com -Date: Sun, 04 Dec 2011 15:55:01 GMT -Server: gws -Content-Length: 221 -X-XSS-Protection: 1; mode=block -X-Frame-Options: SAMEORIGIN - +"HTTP/1.0 302 Found\r +Location: http://www.google.co.il/\r +Cache-Control: private\r +Content-Type: text/html; charset=UTF-8\r +Set-Cookie: PREF=ID=568f67013d4a7afa:FF=0:TM=1323014101:LM=1323014101:S=XqctDWC65MzKT0zC; expires=Tue, 03-Dec-2013 15:55:01 GMT; path=/; domain=.google.com\r +Date: Sun, 04 Dec 2011 15:55:01 GMT\r +Server: gws\r +Content-Length: 221\r +X-XSS-Protection: 1; mode=block\r +X-Frame-Options: SAMEORIGIN\r +\r <HTML><HEAD><meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"> <TITLE>302 Moved</TITLE></HEAD><BODY> <H1>302 Moved</H1> @@ -354,13 +354,13 @@ The document has moved {400, "\r\n\r\n\r\n\r\n\r\n\r\n"}, {400, "GET / HTTP/1.1\r\nHost: ninenines.eu\r\n\r\n"}, {400, "GET http://proxy/ HTTP/1.1\r\n\r\n"}, - {400, ResponsePacket}, + {505, ResponsePacket}, {408, "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, "GET / HTTP/1.1\r\nHost: localhost\r\n\r"}, - {413, Huge}, - {413, "GET / HTTP/1.1\r\n" ++ Huge}, + {414, Huge}, + {400, "GET / HTTP/1.1\r\n" ++ Huge}, {505, "GET / HTTP/1.2\r\nHost: localhost\r\n\r\n"}, {closed, ""}, {closed, "\r\n"}, @@ -552,8 +552,8 @@ multipart(Config) -> {ok, RespBody, _} = cowboy_client:response_body(Client3), Parts = binary_to_term(RespBody), Parts = [ - {[{<<"X-Name">>, <<"answer">>}], <<"42">>}, - {[{'Server', <<"Cowboy">>}], <<"It rocks!\r\n">>} + {[{<<"x-name">>, <<"answer">>}], <<"42">>}, + {[{<<"server">>, <<"Cowboy">>}], <<"It rocks!\r\n">>} ]. nc_reqs(Config, Input) -> @@ -599,7 +599,7 @@ onrequest_reply(Config) -> onrequest_hook(Req) -> case cowboy_req:qs_val(<<"reply">>, Req) of {undefined, Req2} -> - cowboy_req:set_resp_header('Server', <<"Serenity">>, Req2); + cowboy_req:set_resp_header(<<"server">>, <<"Serenity">>, Req2); {_, Req2} -> {ok, Req3} = cowboy_req:reply( 200, [], <<"replied!">>, Req2), diff --git a/test/http_handler_init_shutdown.erl b/test/http_handler_init_shutdown.erl index c26fa97..edea1a0 100644 --- a/test/http_handler_init_shutdown.erl +++ b/test/http_handler_init_shutdown.erl @@ -6,7 +6,7 @@ init({_Transport, http}, Req, _Opts) -> {ok, Req2} = cowboy_req:reply(<<"666 Init Shutdown Testing">>, - [{'Connection', <<"close">>}], Req), + [{<<"connection">>, <<"close">>}], Req), {shutdown, Req2, undefined}. handle(Req, State) -> diff --git a/test/http_handler_set_resp.erl b/test/http_handler_set_resp.erl index 806bca8..70ddf79 100644 --- a/test/http_handler_set_resp.erl +++ b/test/http_handler_set_resp.erl @@ -11,12 +11,12 @@ init({_Transport, http}, Req, Opts) -> cowboy_req:set_resp_header(Name, Value, R) end, Req, Headers), Req3 = cowboy_req:set_resp_body(Body, Req2), - Req4 = cowboy_req:set_resp_header(<<"X-Cowboy-Test">>, <<"ok">>, Req3), + Req4 = cowboy_req:set_resp_header(<<"x-cowboy-test">>, <<"ok">>, Req3), Req5 = cowboy_req:set_resp_cookie(<<"cake">>, <<"lie">>, [], Req4), {ok, Req5, undefined}. handle(Req, State) -> - case cowboy_req:has_resp_header(<<"X-Cowboy-Test">>, Req) of + case cowboy_req:has_resp_header(<<"x-cowboy-test">>, Req) of false -> {ok, Req, State}; true -> case cowboy_req:has_resp_body(Req) of diff --git a/test/proper_SUITE.erl b/test/proper_SUITE.erl deleted file mode 100644 index 113e135..0000000 --- a/test/proper_SUITE.erl +++ /dev/null @@ -1,37 +0,0 @@ -%% 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(proper_SUITE). - --include_lib("common_test/include/ct.hrl"). - --export([all/0, groups/0]). %% ct. --export([dispatcher_split_host/1]). %% cowboy_dispatcher. - -%% ct. - -all() -> - [{group, dispatcher}]. - -groups() -> - [{dispatcher, [], [dispatcher_split_host]}]. - -%% cowboy_dispatcher. - -dispatcher_split_host(_Config) -> - true = proper:quickcheck(dispatcher_prop:prop_split_host_symmetric(), - [{on_output, fun(Format, Data) -> - io:format(user, Format, Data), %% Console. - io:format(Format, Data) %% Logs. - end}]). diff --git a/test/rest_forbidden_resource.erl b/test/rest_forbidden_resource.erl index d34e7b2..63aac7e 100644 --- a/test/rest_forbidden_resource.erl +++ b/test/rest_forbidden_resource.erl @@ -10,7 +10,7 @@ rest_init(Req, [Forbidden]) -> {ok, Req, Forbidden}. allowed_methods(Req, State) -> - {['GET', 'HEAD', 'POST'], Req, State}. + {[<<"GET">>, <<"HEAD">>, <<"POST">>], Req, State}. forbidden(Req, State=true) -> {true, Req, State}; diff --git a/test/rest_nodelete_resource.erl b/test/rest_nodelete_resource.erl index 8c83422..9f9670c 100644 --- a/test/rest_nodelete_resource.erl +++ b/test/rest_nodelete_resource.erl @@ -6,7 +6,7 @@ init(_Transport, _Req, _Opts) -> {upgrade, protocol, cowboy_rest}. allowed_methods(Req, State) -> - {['GET', 'HEAD', 'DELETE'], Req, State}. + {[<<"GET">>, <<"HEAD">>, <<"DELETE">>], Req, State}. content_types_provided(Req, State) -> |