Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Optimize cowboy_protocol

* #state{} changes are avoided where possible
* #state{} is now smaller and use less memory
* the Req object is created only after the whole request is parsed
* parsing makes use of a single binary match context
* external calls are avoided in the critical path
* URL fragment is now extracted properly (retrieval API next commit)
* argument orders to local functions modified to avoid extra operations
* dispatching waits as long as possible before tokenizing host/path
* handler opts are no longer shown in the error messages except in init

The code may not look as beautiful as it was before. But it really
is, for parsing code. The parsing section of the file may be skipped
if your eyes start to burn.
  • Loading branch information...
commit b2243aa54438dbea4137960c9dc6f54ac746f429 1 parent bfab8d4
@essen essen authored
View
4 Makefile
@@ -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.
View
275 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.
View
51 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) ->
View
585 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.
View
66 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) ->
View
70 test/dispatcher_prop.erl
@@ -1,70 +0,0 @@
-%% Copyright (c) 2011, Magnus Klaar <magnus.klaar@gmail.com>
-%% Copyright (c) 2011, Loïc Hoguin <essen@ninenines.eu>
-%%
-%% 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.
View
2  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"},
View
37 test/proper_SUITE.erl
@@ -1,37 +0,0 @@
-%% Copyright (c) 2011, Loïc Hoguin <essen@ninenines.eu>
-%%
-%% 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}]).
Please sign in to comment.
Something went wrong with that request. Please try again.