Skip to content

Commit

Permalink
Don't use decode_packet/3 for parsing the request-line
Browse files Browse the repository at this point in the history
First step in making all methods and header names binaries to
get rid of many inconsistencies caused by decode_packet/3.

Methods are all binary now. Note that since they are case
sensitive, the usual methods become <<"GET">>, <<"POST">> and so on.
  • Loading branch information
essen committed Sep 21, 2012
1 parent f6791b0 commit 8497c8b
Show file tree
Hide file tree
Showing 10 changed files with 148 additions and 108 deletions.
4 changes: 2 additions & 2 deletions examples/echo_get/src/toppage_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ handle(Req, State) ->
{ok, Req4} = echo(Method, Echo, Req3),
{ok, Req4, State}.

echo('GET', undefined, Req) ->
echo(<<"GET">>, undefined, Req) ->
cowboy_req:reply(400, [], <<"Missing echo parameter.">>, Req);
echo('GET', Echo, Req) ->
echo(<<"GET">>, Echo, Req) ->
cowboy_req:reply(200,
[{<<"Content-Encoding">>, <<"utf-8">>}], Echo, Req);
echo(_, _, Req) ->
Expand Down
4 changes: 2 additions & 2 deletions examples/echo_post/src/toppage_handler.erl
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ handle(Req, State) ->
{ok, Req4} = maybe_echo(Method, HasBody, Req3),
{ok, Req4, State}.

maybe_echo('POST', true, Req) ->
maybe_echo(<<"POST">>, true, Req) ->
{ok, PostVals, Req2} = cowboy_req:body_qs(Req),
Echo = proplists:get_value(<<"echo">>, PostVals),
echo(Echo, Req2);
maybe_echo('POST', false, Req) ->
maybe_echo(<<"POST">>, false, Req) ->
cowboy_req:reply(400, [], <<"Missing body.">>, Req);
maybe_echo(_, _, Req) ->
%% Method not allowed.
Expand Down
44 changes: 41 additions & 3 deletions src/cowboy_http.erl
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
-module(cowboy_http).

%% Parsing.
-export([request_line/1]).
-export([list/2]).
-export([nonempty_list/2]).
-export([content_type/1]).
Expand Down Expand Up @@ -50,8 +51,6 @@
-export([urlencode/2]).
-export([x_www_form_urlencoded/2]).

-type method() :: 'OPTIONS' | 'GET' | 'HEAD'
| 'POST' | 'PUT' | 'DELETE' | 'TRACE' | binary().
-type uri() :: '*' | {absoluteURI, http | https, Host::binary(),
Port::integer() | undefined, Path::binary()}
| {scheme, Scheme::binary(), binary()}
Expand All @@ -73,7 +72,6 @@
-type headers() :: [{header(), iodata()}].
-type status() :: non_neg_integer() | binary().

-export_type([method/0]).
-export_type([uri/0]).
-export_type([version/0]).
-export_type([header/0]).
Expand All @@ -86,6 +84,46 @@

%% 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:split(Data, <<" ">>) of
[_] -> %% We require the HTTP version.
{error, badarg};
[URI, Rest] ->
case binary:split(URI, <<"://">>) of
[_] -> %% Already is a path or "*".
Fun(Rest, URI);
[_, NoScheme] ->
case binary:split(NoScheme, <<"/">>) of
[_] -> <<"/">>;
[_, NoHost] -> 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) ->
Expand Down
133 changes: 67 additions & 66 deletions src/cowboy_protocol.erl
Original file line number Diff line number Diff line change
Expand Up @@ -113,19 +113,6 @@ init(ListenerPid, Socket, Transport, Opts) ->
timeout=Timeout, onrequest=OnRequest, onresponse=OnResponse,
urldecode=URLDec}).

%% @private
-spec parse_request(#state{}) -> ok.
%% We limit the length of the Request-line to MaxLength to avoid endlessly
%% reading from the socket and eventually crashing.
parse_request(State=#state{buffer=Buffer, max_line_length=MaxLength}) ->
case erlang:decode_packet(http_bin, Buffer, []) of
{ok, Request, Rest} -> request(Request, State#state{buffer=Rest});
{more, _Length} when byte_size(Buffer) > MaxLength ->
error_terminate(413, State);
{more, _Length} -> wait_request(State);
{error, _Reason} -> error_terminate(400, State)
end.

-spec wait_request(#state{}) -> ok.
wait_request(State=#state{socket=Socket, transport=Transport,
timeout=T, buffer=Buffer}) ->
Expand All @@ -135,48 +122,56 @@ wait_request(State=#state{socket=Socket, transport=Transport,
{error, _Reason} -> terminate(State)
end.

-spec request({http_request, cowboy_http:method(), cowboy_http:uri(),
cowboy_http:version()}, #state{}) -> ok.
request({http_request, _Method, _URI, Version}, State)
%% @private
-spec parse_request(#state{}) -> ok.
%% We limit the length of the Request-line to MaxLength to avoid endlessly
%% reading from the socket and eventually crashing.
parse_request(State=#state{buffer=Buffer, max_line_length=MaxLength,
req_empty_lines=ReqEmpty, max_empty_lines=MaxEmpty}) ->
case binary:split(Buffer, <<"\r\n">>) of
[_] when byte_size(Buffer) > MaxLength ->
error_terminate(413, State);
[<< "\n", _/binary >>] ->
error_terminate(400, State);
[_] ->
wait_request(State);
[<<>>, _] when ReqEmpty =:= MaxEmpty ->
error_terminate(400, State);
[<<>>, Rest] ->
parse_request(State#state{
buffer=Rest, req_empty_lines=ReqEmpty + 1});
[RequestLine, Rest] ->
case cowboy_http:request_line(RequestLine) of
{Method, AbsPath, Version} ->
request(State#state{buffer=Rest}, Method, AbsPath, Version);
{error, _} ->
error_terminate(400, State)
end
end.

-spec request(#state{}, binary(), binary(), cowboy_http:version()) -> ok.
request(State, _, _, Version)
when Version =/= {1, 0}, Version =/= {1, 1} ->
error_terminate(505, State);
%% We still receive the original Host header.
request({http_request, Method, {absoluteURI, _Scheme, _Host, _Port, Path},
Version}, State) ->
request({http_request, Method, {abs_path, Path}, Version}, State);
request({http_request, Method, {abs_path, AbsPath}, Version},
State=#state{socket=Socket, transport=Transport,
req_keepalive=Keepalive, max_keepalive=MaxKeepalive,
onresponse=OnResponse, urldecode={URLDecFun, URLDecArg}=URLDec}) ->
URLDecode = fun(Bin) -> URLDecFun(Bin, URLDecArg) end,
{PathTokens, RawPath, Qs}
= cowboy_dispatcher:split_path(AbsPath, URLDecode),
ConnAtom = if Keepalive < MaxKeepalive -> version_to_connection(Version);
true -> close
end,
parse_header(cowboy_req:new(Socket, Transport, ConnAtom, Method, Version,
RawPath, Qs, OnResponse, URLDec), State#state{path_tokens=PathTokens});
request({http_request, Method, '*', Version},
State=#state{socket=Socket, transport=Transport,
req_keepalive=Keepalive, max_keepalive=MaxKeepalive,
onresponse=OnResponse, urldecode=URLDec}) ->
ConnAtom = if Keepalive < MaxKeepalive -> version_to_connection(Version);
true -> close
end,
parse_header(cowboy_req:new(Socket, Transport, ConnAtom, Method, Version,
<<"*">>, <<>>, OnResponse, URLDec), State#state{path_tokens='*'});
request({http_request, _Method, _URI, _Version}, State) ->
error_terminate(501, State);
request({http_error, <<"\r\n">>},
State=#state{req_empty_lines=N, max_empty_lines=N}) ->
error_terminate(400, State);
request({http_error, <<"\r\n">>}, State=#state{req_empty_lines=N}) ->
parse_request(State#state{req_empty_lines=N + 1});
request(_Any, State) ->
error_terminate(400, State).

-spec parse_header(cowboy_req:req(), #state{}) -> ok.
parse_header(Req, State=#state{buffer=Buffer, max_line_length=MaxLength}) ->
request(State=#state{socket=Socket, transport=Transport,
onresponse=OnResponse, urldecode=URLDec},
Method, <<"*">>, Version) ->
Connection = version_to_connection(State, Version),
parse_header(State#state{path_tokens= '*'},
cowboy_req:new(Socket, Transport, Connection, Method, Version,
<<"*">>, <<>>, OnResponse, URLDec));
request(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(State#state{path_tokens=PathTokens},
cowboy_req:new(Socket, Transport, Connection, Method, Version,
Path, Qs, OnResponse, URLDec)).

-spec parse_header(#state{}, cowboy_req:req()) -> ok.
parse_header(State=#state{buffer=Buffer, max_line_length=MaxLength}, Req) ->
case erlang:decode_packet(httph_bin, Buffer, []) of
{ok, Header, Rest} -> header(Header, Req, State#state{buffer=Rest});
{more, _Length} when byte_size(Buffer) > MaxLength ->
Expand All @@ -189,8 +184,8 @@ parse_header(Req, State=#state{buffer=Buffer, max_line_length=MaxLength}) ->
wait_header(Req, State=#state{socket=Socket,
transport=Transport, timeout=T, buffer=Buffer}) ->
case Transport:recv(Socket, 0, T) of
{ok, Data} -> parse_header(Req, State#state{
buffer= << Buffer/binary, Data/binary >>});
{ok, Data} -> parse_header(State#state{
buffer= << Buffer/binary, Data/binary >>}, Req);
{error, timeout} -> error_terminate(408, State);
{error, closed} -> terminate(State)
end.
Expand All @@ -203,24 +198,24 @@ header({http_header, _I, 'Host', _R, RawHost}, Req,
case catch cowboy_dispatcher:split_host(RawHost2) of
{HostTokens, Host, undefined} ->
Port = default_port(Transport:name()),
parse_header(cowboy_req:set_host(Host, Port, RawHost, Req),
State#state{host_tokens=HostTokens});
parse_header(State#state{host_tokens=HostTokens},
cowboy_req:set_host(Host, Port, RawHost, Req));
{HostTokens, Host, Port} ->
parse_header(cowboy_req:set_host(Host, Port, RawHost, Req),
State#state{host_tokens=HostTokens});
parse_header(State#state{host_tokens=HostTokens},
cowboy_req:set_host(Host, Port, RawHost, Req));
{'EXIT', _Reason} ->
error_terminate(400, State)
end;
%% Ignore Host headers if we already have it.
header({http_header, _I, 'Host', _R, _V}, Req, State) ->
parse_header(Req, State);
parse_header(State, Req);
header({http_header, _I, 'Connection', _R, Connection}, Req,
State=#state{req_keepalive=Keepalive, max_keepalive=MaxKeepalive})
when Keepalive < MaxKeepalive ->
parse_header(cowboy_req:set_connection(Connection, Req), State);
parse_header(State, cowboy_req:set_connection(Connection, Req));
header({http_header, _I, Field, _R, Value}, Req, State) ->
Field2 = format_header(Field),
parse_header(cowboy_req:add_header(Field2, Value, Req), State);
parse_header(State, cowboy_req:add_header(Field2, Value, Req));
%% The Host header is required in HTTP/1.1 and optional in HTTP/1.0.
header(http_eoh, Req, State=#state{host_tokens=undefined,
buffer=Buffer, transport=Transport}) ->
Expand Down Expand Up @@ -431,7 +426,7 @@ 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)),
close, <<"GET">>, {1, 1}, <<>>, <<>>, OnResponse, undefined)),
ok
end,
terminate(State).
Expand All @@ -443,9 +438,15 @@ terminate(#state{socket=Socket, transport=Transport}) ->

%% Internal.

-spec version_to_connection(cowboy_http:version()) -> keepalive | close.
version_to_connection({1, 1}) -> keepalive;
version_to_connection(_Any) -> close.
-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;
Expand Down
12 changes: 6 additions & 6 deletions src/cowboy_req.erl
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@

%% Request.
pid = undefined :: pid(),
method = 'GET' :: cowboy_http:method(),
method = <<"GET">> :: binary(),
version = {1, 1} :: cowboy_http:version(),
peer = undefined :: undefined | {inet:ip_address(), inet:port_number()},
host = undefined :: undefined | binary(),
Expand Down Expand Up @@ -172,7 +172,7 @@
%% This function takes care of setting the owner's pid to self().
%% @private
-spec new(inet:socket(), module(), keepalive | close,
cowboy_http:method(), cowboy_http:version(), binary(), binary(),
binary(), cowboy_http:version(), binary(), binary(),
undefined | fun(), undefined | {fun(), atom()})
-> req().
new(Socket, Transport, Connection, Method, Version, Path, Qs,
Expand All @@ -182,7 +182,7 @@ new(Socket, Transport, Connection, Method, Version, Path, Qs,
onresponse=OnResponse, urldecode=URLDecode}.

%% @doc Return the HTTP method of the request.
-spec method(Req) -> {cowboy_http:method(), Req} when Req::req().
-spec method(Req) -> {binary(), Req} when Req::req().
method(Req) ->
{Req#http_req.method, Req}.

Expand Down Expand Up @@ -878,7 +878,7 @@ reply(Status, Headers, Body, Req=#http_req{socket=Socket, transport=Transport,
{<<"Date">>, cowboy_clock:rfc1123()},
{<<"Server">>, <<"Cowboy">>}
|HTTP11Headers], Req),
if Method =:= 'HEAD' -> ok;
if Method =:= <<"HEAD">> -> ok;
ReplyType =:= hook -> ok; %% Hook replied for us, stop there.
true ->
case Body of
Expand Down Expand Up @@ -919,7 +919,7 @@ chunked_reply(Status, Headers, Req=#http_req{
%%
%% A chunked reply must have been initiated before calling this function.
-spec chunk(iodata(), req()) -> ok | {error, atom()}.
chunk(_Data, #http_req{socket=_Socket, transport=_Transport, method='HEAD'}) ->
chunk(_Data, #http_req{method= <<"HEAD">>}) ->
ok;
chunk(Data, #http_req{socket=Socket, transport=Transport, version={1, 0}}) ->
Transport:send(Socket, Data);
Expand Down Expand Up @@ -950,7 +950,7 @@ ensure_response(Req=#http_req{resp_state=waiting}, Status) ->
_ = reply(Status, [], [], Req),
ok;
%% Terminate the chunked body for HTTP/1.1 only.
ensure_response(#http_req{method='HEAD', resp_state=chunks}, _) ->
ensure_response(#http_req{method= <<"HEAD">>, resp_state=chunks}, _) ->
ok;
ensure_response(#http_req{version={1, 0}, resp_state=chunks}, _) ->
ok;
Expand Down
Loading

0 comments on commit 8497c8b

Please sign in to comment.