diff options
-rw-r--r-- | Makefile | 4 | ||||
-rw-r--r-- | src/cowboy_dispatcher.erl | 275 | ||||
-rw-r--r-- | src/cowboy_http.erl | 51 | ||||
-rw-r--r-- | src/cowboy_protocol.erl | 585 | ||||
-rw-r--r-- | src/cowboy_req.erl | 66 | ||||
-rw-r--r-- | test/dispatcher_prop.erl | 70 | ||||
-rw-r--r-- | test/http_SUITE.erl | 2 | ||||
-rw-r--r-- | test/proper_SUITE.erl | 37 |
8 files changed, 517 insertions, 573 deletions
@@ -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/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl index c52aba9..f5dfcd9 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]). +-export([match/4]). --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,60 +36,6 @@ %% API. -%% @doc Split a hostname into a list of tokens. --spec split_host(binary()) - -> {tokens(), binary(), undefined | inet:port_number()}. -split_host(Host) -> - case binary:match(Host, <<":">>) of - nomatch -> - {do_split_host(Host, []), Host, undefined}; - {Pos, _} -> - << Host2:Pos/binary, _:8, Port/bits >> = Host, - {do_split_host(Host2, []), Host2, - list_to_integer(binary_to_list(Port))} - end. - -do_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, - do_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(), fun((binary()) -> binary())) -> - {tokens(), binary(), binary()}. -split_path(Path, URLDec) -> - case binary:match(Path, <<"?">>) of - nomatch -> - {do_split_path(Path, URLDec), Path, <<>>}; - {Pos, _} -> - << Path2:Pos/binary, _:8, Qs/bits >> = Path, - {do_split_path(Path2, URLDec), Path2, Qs} - end. - -do_split_path(<< "/", Path/bits >>, URLDec) -> - do_split_path(Path, URLDec, []). -do_split_path(Path, URLDec, Acc) -> - case binary:match(Path, <<"/">>) of - nomatch when Path =:= <<>> -> - lists:reverse([URLDec(S) || S <- Acc]); - nomatch -> - lists:reverse([URLDec(S) || S <- [Path|Acc]]); - {Pos, _} -> - << Segment:Pos/binary, _:8, Rest/bits >> = Path, - do_split_path(Rest, URLDec, [Segment|Acc]) - end. - %% @doc Match hostname tokens and path tokens against dispatch rules. %% %% It is typically used for matching tokens for the hostname and path of @@ -119,47 +63,93 @@ do_split_path(Path, URLDec, Acc) -> %% 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(), fun((binary()) -> binary()), + 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 list_match(Host, lists:reverse(HostMatch), []) of +match([{'_', PathMatchs}|_Tail], URLDecode, _, Path) -> + match_path(PathMatchs, URLDecode, undefined, Path, []); +match([{HostMatch, PathMatchs}|Tail], URLDecode, 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, URLDecode, Tokens, Path); + {true, Bindings, undefined} -> + match_path(PathMatchs, URLDecode, undefined, Path, Bindings); + {true, Bindings, HostInfo} -> + match_path(PathMatchs, URLDecode, lists:reverse(HostInfo), + Path, Bindings) + end; +match(Dispatch, URLDecode, Host, Path) -> + match(Dispatch, URLDecode, split_host(Host), Path). --spec match_path(tokens(), dispatch_path(), bindings(), - HostInfo::undefined | tokens()) +-spec match_path(dispatch_path(), fun((binary()) -> binary()), + 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 list_match(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], URLDecode, HostInfo, Tokens, + Bindings) when is_list(Tokens) -> + case list_match(Tokens, PathMatch, []) of false -> - match_path(Path, Tail, HostBinds, HostInfo); + match_path(Tail, URLDecode, 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, URLDecode, HostInfo, Path, Bindings) -> + match_path(Dispatch, URLDecode, HostInfo, + split_path(Path, URLDecode), Bindings). %% Internal. +%% @doc Split a hostname into a list of tokens. +-spec split_host(binary()) -> tokens(). +split_host(Host) -> + split_host(Host, []). + +split_host(Host, Acc) -> + case binary:match(Host, <<".">>) of + nomatch when Host =:= <<>> -> + Acc; + nomatch -> + [Host|Acc]; + {Pos, _} -> + << Segment:Pos/binary, _:8, Rest/bits >> = Host, + false = byte_size(Segment) == 0, + split_host(Rest, [Segment|Acc]) + end. + +%% @doc Split a path into a list of path segments. +%% +%% Following RFC2396, this function may return path segments containing any +%% character, including <em>/</em> if, and only if, a <em>/</em> was escaped +%% and part of a path segment. +-spec split_path(binary(), fun((binary()) -> binary())) -> tokens(). +split_path(<< $/, Path/bits >>, URLDec) -> + split_path(Path, URLDec, []). + +split_path(Path, URLDec, Acc) -> + case binary:match(Path, <<"/">>) of + nomatch when Path =:= <<>> -> + lists:reverse([URLDec(S) || S <- Acc]); + nomatch -> + lists:reverse([URLDec(S) || S <- [Path|Acc]]); + {Pos, _} -> + << Segment:Pos/binary, _:8, Rest/bits >> = Path, + split_path(Rest, URLDec, [Segment|Acc]) + end. + -spec list_match(tokens(), match_rule(), bindings()) -> {true, bindings(), undefined | tokens()} | false. %% Atom '...' matches any trailing path, stop right now. @@ -188,64 +178,32 @@ list_match(_List, _Match, _Binds) -> split_host_test_() -> %% {Host, Result} Tests = [ - {<<"">>, {[], <<"">>, undefined}}, - {<<"*">>, {[<<"*">>], <<"*">>, undefined}}, + {<<"">>, []}, + {<<"*">>, [<<"*">>]}, {<<"cowboy.ninenines.eu">>, - {[<<"eu">>, <<"ninenines">>, <<"cowboy">>], - <<"cowboy.ninenines.eu">>, undefined}}, + [<<"eu">>, <<"ninenines">>, <<"cowboy">>]}, {<<"ninenines.eu">>, - {[<<"eu">>, <<"ninenines">>], <<"ninenines.eu">>, undefined}}, - {<<"ninenines.eu:8080">>, - {[<<"eu">>, <<"ninenines">>], <<"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">>, - {[<<"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">>], - <<"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">>, - <<"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, URLDecode) end} + || {P, R} <- Tests]. match_test_() -> Dispatch = [ @@ -270,28 +228,31 @@ match_test_() -> ], %% {Host, Path, Result} Tests = [ - {[<<"any">>], [], {ok, match_any, [], []}}, - {[<<"eu">>, <<"ninenines">>, <<"any">>, <<"www">>], - [<<"users">>, <<"42">>, <<"mails">>], + {<<"any">>, <<"/">>, {ok, match_any, [], []}}, + {<<"www.any.ninenines.eu">>, <<"/users/42/mails">>, {ok, match_any_subdomain_users, [], []}}, - {[<<"eu">>, <<"ninenines">>, <<"www">>], - [<<"users">>, <<"42">>, <<"mails">>], {ok, match_any, [], []}}, - {[<<"eu">>, <<"ninenines">>, <<"www">>], [], {ok, match_any, [], []}}, - {[<<"eu">>, <<"ninenines">>, <<"any">>, <<"www">>], - [<<"not_users">>, <<"42">>, <<"mails">>], {error, notfound, path}}, - {[<<"eu">>, <<"ninenines">>], [], {ok, match_extend, [], []}}, - {[<<"eu">>, <<"ninenines">>], [<<"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">>}]}}, - {[<<"fr">>, <<"erlang">>], '_', + {<<"erlang.fr">>, '_', {ok, match_erlang_ext, [], [{ext, <<"fr">>}]}}, - {[<<"any">>], [<<"users">>, <<"444">>, <<"friends">>], + {<<"any">>, <<"/users/444/friends">>, {ok, match_users_friends, [], [{id, <<"444">>}]}}, - {[<<"fr">>, <<"ninenines">>], [<<"threads">>, <<"987">>], + {<<"ninenines.fr">>, <<"/threads/987">>, {ok, match_duplicate_vars, [we, {expect, two}, var, here], - [{var, <<"fr">>}, {var, <<"987">>}]}} + [{var, <<"fr">>}, {var, <<"987">>}]}} ], + URLDecode = fun(Bin) -> cowboy_http:urldecode(Bin, crash) end, [{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, URLDecode, H, P) end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests]. match_info_test_() -> @@ -304,24 +265,22 @@ match_info_test_() -> ]} ], Tests = [ - {[<<"eu">>, <<"ninenines">>], [], + {<<"ninenines.eu">>, <<"/">>, {ok, match_any, [], [], [], undefined}}, - {[<<"eu">>, <<"ninenines">>, <<"bugs">>], [], + {<<"bugs.ninenines.eu">>, <<"/">>, {ok, match_any, [], [], [<<"bugs">>], undefined}}, - {[<<"eu">>, <<"ninenines">>, <<"bugs">>, <<"cowboy">>], [], + {<<"cowboy.bugs.ninenines.eu">>, <<"/">>, {ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}}, - {[<<"eu">>, <<"ninenines">>, <<"www">>], - [<<"pathinfo">>, <<"is">>, <<"next">>], + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>, {ok, match_path, [], [], undefined, []}}, - {[<<"eu">>, <<"ninenines">>, <<"www">>], - [<<"pathinfo">>, <<"is">>, <<"next">>, <<"path_info">>], + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>, {ok, match_path, [], [], undefined, [<<"path_info">>]}}, - {[<<"eu">>, <<"ninenines">>, <<"www">>], - [<<"pathinfo">>, <<"is">>, <<"next">>, <<"foo">>, <<"bar">>], + {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>, {ok, match_path, [], [], undefined, [<<"foo">>, <<"bar">>]}} ], + URLDecode = fun(Bin) -> cowboy_http:urldecode(Bin, crash) end, [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> - R = match(H, P, Dispatch) + R = match(Dispatch, URLDecode, H, P) end} || {H, P, R} <- Tests]. -endif. diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index a936e2c..c3bef2f 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -17,7 +17,6 @@ -module(cowboy_http). %% Parsing. --export([request_line/1]). -export([list/2]). -export([nonempty_list/2]). -export([content_type/1]). @@ -51,15 +50,10 @@ -export([urlencode/2]). -export([x_www_form_urlencoded/2]). --type uri() :: '*' | {absoluteURI, http | https, Host::binary(), - Port::integer() | undefined, Path::binary()} - | {scheme, Scheme::binary(), binary()} - | {abs_path, binary()} | binary(). -type version() :: {Major::non_neg_integer(), Minor::non_neg_integer()}. -type headers() :: [{binary(), iodata()}]. -type status() :: non_neg_integer() | binary(). --export_type([uri/0]). -export_type([version/0]). -export_type([headers/0]). -export_type([status/0]). @@ -70,51 +64,6 @@ %% Parsing. -%% @doc Parse a request-line. --spec request_line(binary()) - -> {binary(), binary(), version()} | {error, badarg}. -request_line(Data) -> - token(Data, - fun (Rest, Method) -> - whitespace(Rest, - fun (Rest2) -> - uri_to_abspath(Rest2, - fun (Rest3, AbsPath) -> - whitespace(Rest3, - fun (<< "HTTP/", Maj, ".", Min, _/binary >>) - when Maj >= $0, Maj =< $9, - Min >= $0, Min =< $9 -> - {Method, AbsPath, {Maj - $0, Min - $0}}; - (_) -> - {error, badarg} - end) - end) - end) - end). - -%% We just want to extract the path/qs and skip everything else. -%% We do not really parse the URI, nor do we need to. -uri_to_abspath(Data, Fun) -> - case binary:match(Data, <<" ">>) of - nomatch -> %% We require the HTTP version. - {error, badarg}; - {Pos1, _} -> - << URI:Pos1/binary, _:8, Rest/bits >> = Data, - case binary:match(URI, <<"://">>) of - nomatch -> %% Already is a path or "*". - Fun(Rest, URI); - {Pos2, _} -> - << _:Pos2/binary, _:24, NoScheme/bits >> = Rest, - case binary:match(NoScheme, <<"/">>) of - nomatch -> - Fun(Rest, <<"/">>); - {Pos3, _} -> - << _:Pos3/binary, _:8, NoHost/bits >> = NoScheme, - Fun(Rest, << "/", NoHost/binary >>) - end - end - end. - %% @doc Parse a non-empty list of the given type. -spec nonempty_list(binary(), fun()) -> [any(), ...] | {error, badarg}. nonempty_list(Data, Fun) -> diff --git a/src/cowboy_protocol.erl b/src/cowboy_protocol.erl index 0fe6ed2..b0b5aa6 100644 --- a/src/cowboy_protocol.erl +++ b/src/cowboy_protocol.erl @@ -39,8 +39,8 @@ %% Internal. -export([init/4]). --export([parse_request/2]). --export([handler_loop/3]). +-export([parse_request/3]). +-export([handler_loop/4]). -type onrequest_fun() :: fun((Req) -> Req). -type onresponse_fun() :: @@ -54,11 +54,9 @@ 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(), @@ -66,8 +64,6 @@ max_header_name_length :: integer(), max_header_value_length :: integer(), timeout :: timeout(), - 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() @@ -113,254 +109,425 @@ init(ListenerPid, Socket, Transport, Opts) -> max_header_name_length=MaxHeaderNameLength, max_header_value_length=MaxHeaderValueLength, timeout=Timeout, onrequest=OnRequest, onresponse=OnResponse, - urldecode=URLDec}). - --spec wait_request(binary(), #state{}) -> ok. -wait_request(Buffer, State=#state{ - socket=Socket, transport=Transport, timeout=T}) -> - case Transport:recv(Socket, 0, T) of - {ok, Data} -> parse_request(<< Buffer/binary, Data/binary >>, State); - {error, _Reason} -> terminate(State) + urldecode=URLDec}, 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(binary(), #state{}) -> ok. +-spec parse_request(binary(), #state{}, non_neg_integer()) -> ok. %% Empty lines must be using \r\n. -parse_request(<< "\n", _/binary >>, State) -> +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(Buffer, State=#state{max_request_line_length=MaxLength, - req_empty_lines=ReqEmpty, max_empty_lines=MaxEmpty}) -> - case binary:match(Buffer, <<"\r\n">>) of + max_empty_lines=MaxEmpty}, ReqEmpty) -> + case binary:match(Buffer, <<"\n">>) of nomatch when byte_size(Buffer) > MaxLength -> error_terminate(413, State); nomatch -> - wait_request(Buffer, State); - {0, _} when ReqEmpty =:= MaxEmpty -> + wait_request(Buffer, State, ReqEmpty); + {1, _} when ReqEmpty =:= MaxEmpty -> error_terminate(400, State); - {0, _} -> + {1, _} -> << _:16, Rest/binary >> = Buffer, - parse_request(Rest, State#state{req_empty_lines=ReqEmpty + 1}); - {Pos, _} -> - << RequestLine:Pos/binary, _:16, Rest/binary >> = Buffer, - case cowboy_http:request_line(RequestLine) of - {Method, AbsPath, Version} -> - request(Rest, State, Method, AbsPath, Version); - {error, _} -> - error_terminate(400, State) - end + parse_request(Rest, State, ReqEmpty + 1); + {_, _} -> + parse_method(Buffer, State, <<>>) end. --spec request(binary(), #state{}, binary(), binary(), cowboy_http:version()) - -> ok. -request(_, State, _, _, Version) - when Version =/= {1, 0}, Version =/= {1, 1} -> - error_terminate(505, State); -request(Buffer, State=#state{socket=Socket, transport=Transport, - onresponse=OnResponse, urldecode=URLDec}, - Method, <<"*">>, Version) -> - Connection = version_to_connection(State, Version), - parse_header(Buffer, State#state{path_tokens= '*'}, - cowboy_req:new(Socket, Transport, Connection, Method, Version, - <<"*">>, <<>>, OnResponse, URLDec)); -request(Buffer, State=#state{socket=Socket, transport=Transport, - onresponse=OnResponse, urldecode=URLDec={URLDecFun, URLDecArg}}, - Method, AbsPath, Version) -> - Connection = version_to_connection(State, Version), - {PathTokens, Path, Qs} = cowboy_dispatcher:split_path(AbsPath, - fun(Bin) -> URLDecFun(Bin, URLDecArg) end), - parse_header(Buffer, State#state{path_tokens=PathTokens}, - cowboy_req:new(Socket, Transport, Connection, Method, Version, - Path, Qs, OnResponse, URLDec)). - --spec parse_header(binary(), #state{}, cowboy_req:req()) -> ok. -parse_header(<< "\r\n", Rest/binary >>, State, Req) -> - header_end(Rest, State, Req); -parse_header(Buffer, State=#state{max_header_name_length=MaxLength}, Req) -> +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. + +parse_uri(<< $\r, _/bits >>, State, _) -> + 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. + +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. + +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). + +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(413, State); nomatch -> - wait_header(Buffer, State, Req, fun parse_header/3); - {Pos, _} -> - << Name:Pos/binary, _:8, Rest/binary >> = Buffer, - Name2 = cowboy_bstr:to_lower(Name), - Rest2 = cowboy_http:whitespace(Rest, fun(D) -> D end), - parse_header_value(Rest2, State, Req, Name2, <<>>) + 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_header_value(Buffer, State=#state{max_header_value_length=MaxLength}, - Req, Name, SoFar) -> - case binary:match(Buffer, <<"\r\n">>) of - nomatch when byte_size(Buffer) + byte_size(SoFar) > MaxLength -> +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(413, State); nomatch -> - wait_header(Buffer, State, Req, - fun(B, S, R) -> parse_header_value(B, S, R, Name, SoFar) end); - {Pos, _} when Pos + 2 =:= byte_size(Buffer) -> - wait_header(Buffer, State, Req, - fun(B, S, R) -> parse_header_value(B, S, R, Name, SoFar) end); - {Pos, _} -> - << Value:Pos/binary, _:16, Rest/binary >> = Buffer, - case binary:at(Buffer, Pos + 2) of - C when C =:= $\s; C =:= $\t -> - parse_header_value(Rest, State, Req, Name, - << SoFar/binary, Value/binary >>); - _ -> - header(Rest, State, Req, Name, - << SoFar/binary, Value/binary >>) - end + 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. --spec wait_header(binary(), #state{}, cowboy_req:req(), fun()) -> ok. -wait_header(Buffer, State=#state{socket=Socket, transport=Transport, - timeout=T}, Req, Fun) -> - case Transport:recv(Socket, 0, T) of - {ok, Data} -> Fun(<< Buffer/binary, Data/binary >>, State, Req); - {error, timeout} -> error_terminate(408, State); - {error, closed} -> terminate(State) +%% 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. --spec header(binary(), #state{}, cowboy_req:req(), binary(), binary()) -> ok. -header(Buffer, State=#state{host_tokens=undefined, transport=Transport}, - Req, <<"host">>, RawHost) -> - RawHost2 = cowboy_bstr:to_lower(RawHost), - case catch cowboy_dispatcher:split_host(RawHost2) of - {HostTokens, Host, undefined} -> - Port = default_port(Transport:name()), - parse_header(Buffer, State#state{host_tokens=HostTokens}, - cowboy_req:set_host(Host, Port, RawHost, Req)); - {HostTokens, Host, Port} -> - parse_header(Buffer, State#state{host_tokens=HostTokens}, - cowboy_req:set_host(Host, Port, RawHost, Req)); - {'EXIT', _Reason} -> - error_terminate(400, State) +%% 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; -%% Ignore Host headers if we already have it. -header(Buffer, State, Req, <<"host">>, _) -> - parse_header(Buffer, State, Req); -header(Buffer, State=#state{req_keepalive=Keepalive, - max_keepalive=MaxKeepalive}, Req, <<"connection">>, Connection) - when Keepalive < MaxKeepalive -> - parse_header(Buffer, State, cowboy_req:set_connection(Connection, Req)); -header(Buffer, State, Req, Name, Value) -> - parse_header(Buffer, State, cowboy_req:add_header(Name, Value, Req)). - -%% The Host header is required in HTTP/1.1 and optional in HTTP/1.0. -header_end(Buffer, State=#state{host_tokens=undefined, transport=Transport}, - Req) -> - case cowboy_req:version(Req) of - {{1, 1}, _} -> +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(413, 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); - {{1, 0}, Req2} -> - Port = default_port(Transport:name()), - onrequest( - cowboy_req:set_buffer(Buffer, - cowboy_req:set_host(<<>>, Port, <<>>, Req2)), - State) - end; -header_end(Buffer, State, Req) -> - onrequest(cowboy_req:set_buffer(Buffer, Req), 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, urldecode=URLDecode}, + 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, URLDecode), + 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); + 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, urldecode={URLDecFun, URLDecArg}}, + Host, Path) -> + case cowboy_dispatcher:match(Dispatch, + fun(Bin) -> URLDecFun(Bin, URLDecArg) end, 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{}. @@ -373,59 +540,58 @@ 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}) -> +-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. @@ -439,8 +605,8 @@ next_request(Req, State=#state{req_keepalive=Keepalive}, HandlerRes) -> receive {cowboy_req, resp_sent} -> ok after 0 -> ok end, case {HandlerRes, BodyRes, cowboy_req:get_connection(Req)} of {ok, ok, keepalive} -> - ?MODULE:parse_request(Buffer, State#state{handler=undefined, - req_empty_lines=0, req_keepalive=Keepalive + 1}); + ?MODULE:parse_request(Buffer, State#state{ + req_keepalive=Keepalive + 1}, 0); _Closed -> terminate(State) end. @@ -453,7 +619,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, undefined)), ok end, terminate(State). @@ -462,19 +629,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(#state{}, cowboy_http:version()) - -> keepalive | close. -version_to_connection(#state{req_keepalive=Keepalive, - max_keepalive=MaxKeepalive}, _) when Keepalive >= MaxKeepalive -> - close; -version_to_connection(_, {1, 1}) -> - keepalive; -version_to_connection(_, _) -> - close. - --spec default_port(atom()) -> 80 | 443. -default_port(ssl) -> 443; -default_port(_) -> 80. diff --git a/src/cowboy_req.erl b/src/cowboy_req.erl index 9cc7760..10c5e96 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/14]). -export([method/1]). -export([version/1]). -export([peer/1]). @@ -103,10 +103,6 @@ -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([set_bindings/4]). -export([get_resp_state/1]). -export([get_buffer/1]). @@ -172,15 +168,36 @@ %% %% This function takes care of setting the owner's pid to self(). %% @private --spec new(inet:socket(), module(), keepalive | close, - binary(), cowboy_http:version(), binary(), binary(), - undefined | fun(), undefined | {fun(), atom()}) +%% @todo Fragment. +-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(), + undefined | {fun(), atom()}) -> req(). -new(Socket, Transport, Connection, Method, Version, Path, Qs, +new(Socket, Transport, Method, Path, Query, _Fragment, + Version, Headers, Host, Port, Buffer, CanKeepalive, OnResponse, URLDecode) -> - #http_req{socket=Socket, transport=Transport, connection=Connection, - pid=self(), method=Method, version=Version, path=Path, qs=Qs, - onresponse=OnResponse, urldecode=URLDecode}. + Req = #http_req{socket=Socket, transport=Transport, pid=self(), + method=Method, path=Path, qs=Query, version=Version, + headers=Headers, host=Host, port=Port, buffer=Buffer, + onresponse=OnResponse, urldecode=URLDecode}, + case CanKeepalive of + false -> + Req#http_req{connection=close}; + true -> + case lists:keymember(<<"connection">>, 1, Headers) of + false when Version =:= {1, 1} -> + Req; %% keepalive + false -> + Req#http_req{connection=close}; + true -> + {ok, Tokens, Req2} = parse_header(<<"connection">>, Req), + %% @todo Might want to bring this function into cowboy_req. + Connection = cowboy_http:connection_to_atom(Tokens), + Req2#http_req{connection=Connection} + end + end. %% @doc Return the HTTP method of the request. -spec method(Req) -> {binary(), Req} when Req::req(). @@ -968,31 +985,6 @@ 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]}. - -%% @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(binary(), 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}. - -%% @private -spec set_bindings(cowboy_dispatcher:tokens(), cowboy_dispatcher:tokens(), cowboy_dispatcher:bindings(), Req) -> Req when Req::req(). set_bindings(HostInfo, PathInfo, Bindings, Req) -> diff --git a/test/dispatcher_prop.erl b/test/dispatcher_prop.erl deleted file mode 100644 index 26c4f87..0000000 --- a/test/dispatcher_prop.erl +++ /dev/null @@ -1,70 +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(lists:reverse(Tokens), ".")); - {Tokens, RawHost, Port} -> - PortBin = (list_to_binary(":" ++ integer_to_list(Port))), - (Server == << RawHost/binary, PortBin/binary >>) - and (Server == - << (binary_join(lists:reverse(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 559903a..bb0e345 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -354,7 +354,7 @@ 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"}, 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}]). |