diff options
-rw-r--r-- | ROADMAP.md | 40 | ||||
-rw-r--r-- | include/http.hrl | 2 | ||||
-rw-r--r-- | src/cowboy.erl | 1 | ||||
-rw-r--r-- | src/cowboy_client.erl | 4 | ||||
-rw-r--r-- | src/cowboy_dispatcher.erl | 2 | ||||
-rw-r--r-- | src/cowboy_http.erl | 16 | ||||
-rw-r--r-- | src/cowboy_http_protocol.erl | 29 | ||||
-rw-r--r-- | src/cowboy_http_req.erl | 145 | ||||
-rw-r--r-- | src/cowboy_http_websocket.erl | 31 | ||||
-rw-r--r-- | test/http_SUITE.erl | 118 | ||||
-rw-r--r-- | test/http_handler_loop_timeout.erl | 15 |
11 files changed, 235 insertions, 168 deletions
@@ -61,56 +61,20 @@ are not ordered. Tools like curl expect a 100 Continue before sending a request body by default. -* Content-Encoding support. - - Cowboy should be able to send encoded content automatically. - The default should be to encode, but the developer must be - able to override this default either for the whole listener - or just for a single reply. - -* Improve body reading API. - - We want to have various different things for reading the - body. First, there should be raw functions for the different - ways to read the body: basic, transfer encoded, multipart. - Each should allow us to limit the size of what is read. - - On top of these functions there should be two more - advanced functions: one would return the result of parsing - a x-www-form-urlencoded body; the other would parse a - multipart request, save files from the multipart data to - a temporary location and return a proplist of values if any - along with the files details. This behavior is similar to - what is done automatically by PHP with its $_FILES array. - - The advanced functions are of course here for convenience - only and it should be trivial to reimplement them directly - in a Cowboy application if needed. +* Convert the multipart code to stream_body. * Complete the work on Websockets. Now that the Autobahn test suite is available (make inttests), we have a definite way to know whether Cowboy's implementation of Websockets is right. The work can thus be completed. The - remaining tasks are proper UTF8 handling and fragmentation. + remaining tasks are proper UTF8 handling. * SPDY support. While SPDY probably won't be added directly to Cowboy, work has been started on making Cowboy use SPDY. -* Hooks. - - Customizable hooks would allow the developer to extend Cowboy - easily. Two kinds of hooks are needed: before dispatching the - request, and before sending a reply. - - The first would allow us to apply site-wide functions like - authentication or request logging and modify the Req if needed. - - The second is more interesting for response logging or to - filter the replies, for example to send custom error pages. - * Transport upgrades. Some protocols allow an upgrade from TCP to SSL without diff --git a/include/http.hrl b/include/http.hrl index 21d837b..dc849c2 100644 --- a/include/http.hrl +++ b/include/http.hrl @@ -52,5 +52,7 @@ fun(() -> {sent, non_neg_integer()})}, %% Functions. + onresponse = undefined :: undefined | fun((cowboy_http:status(), + cowboy_http:headers(), #http_req{}) -> #http_req{}), urldecode :: {fun((binary(), T) -> binary()), T} }). diff --git a/src/cowboy.erl b/src/cowboy.erl index 7963df2..1097197 100644 --- a/src/cowboy.erl +++ b/src/cowboy.erl @@ -51,7 +51,6 @@ start_listener(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts) Transport, TransOpts, Protocol, ProtoOpts)). %% @doc Stop a listener identified by <em>Ref</em>. -%% @todo Currently request processes aren't terminated with the listener. -spec stop_listener(any()) -> ok | {error, not_found}. stop_listener(Ref) -> case supervisor:terminate_child(cowboy_sup, {cowboy_listener_sup, Ref}) of diff --git a/src/cowboy_client.erl b/src/cowboy_client.erl index 21931e1..e46619f 100644 --- a/src/cowboy_client.erl +++ b/src/cowboy_client.erl @@ -158,7 +158,9 @@ response_body_loop(Client, Acc) -> {ok, Data, Client2} -> response_body_loop(Client2, << Acc/binary, Data/binary >>); {done, Client2} -> - {ok, Acc, Client2} + {ok, Acc, Client2}; + {error, Reason} -> + {error, Reason} end. skip_body(Client=#client{state=response_body}) -> diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl index db40e63..6de8b49 100644 --- a/src/cowboy_dispatcher.erl +++ b/src/cowboy_dispatcher.erl @@ -215,6 +215,8 @@ split_path_test_() -> {<<"?">>, [], <<"">>, <<"">>}, {<<"???">>, [], <<"">>, <<"??">>}, {<<"/">>, [], <<"/">>, <<"">>}, + {<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>], + <<"/extend//cowboy">>, <<>>}, {<<"/users">>, [<<"users">>], <<"/users">>, <<"">>}, {<<"/users?">>, [<<"users">>], <<"/users">>, <<"">>}, {<<"/users?a">>, [<<"users">>], <<"/users">>, <<"a">>}, diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index 0289ef3..f8d3314 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -270,7 +270,15 @@ maybe_qparam(Data, Fun) -> fun (<< $;, Rest/binary >>) -> whitespace(Rest, fun (Rest2) -> - qparam(Rest2, Fun) + %% This is a non-strict parsing clause required by some user agents + %% that use the wrong delimiter putting a charset where a qparam is + %% expected. + try qparam(Rest2, Fun) of + Result -> Result + catch + error:function_clause -> + Fun(<<",", Rest2/binary>>, 1000) + end end); (Rest) -> Fun(Rest, 1000) @@ -885,6 +893,12 @@ nonempty_charset_list_test_() -> {<<"iso-8859-5, unicode-1-1;q=0.8">>, [ {<<"iso-8859-5">>, 1000}, {<<"unicode-1-1">>, 800} + ]}, + %% Some user agents send this invalid value for the Accept-Charset header + {<<"ISO-8859-1;utf-8;q=0.7,*;q=0.7">>, [ + {<<"iso-8859-1">>, 1000}, + {<<"utf-8">>, 700}, + {<<"*">>, 700} ]} ], [{V, fun() -> R = nonempty_list(V, fun conneg/2) end} || {V, R} <- Tests]. diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl index 816c825..9e1ad88 100644 --- a/src/cowboy_http_protocol.erl +++ b/src/cowboy_http_protocol.erl @@ -48,6 +48,8 @@ dispatch :: cowboy_dispatcher:dispatch_rules(), handler :: {module(), any()}, onrequest :: undefined | fun((#http_req{}) -> #http_req{}), + onresponse = undefined :: undefined | fun((cowboy_http:status(), + cowboy_http:headers(), #http_req{}) -> #http_req{}), urldecode :: {fun((binary(), T) -> binary()), T}, req_empty_lines = 0 :: integer(), max_empty_lines :: integer(), @@ -79,6 +81,7 @@ init(ListenerPid, Socket, Transport, Opts) -> MaxKeepalive = proplists:get_value(max_keepalive, Opts, infinity), MaxLineLength = proplists:get_value(max_line_length, Opts, 4096), OnRequest = proplists:get_value(onrequest, Opts), + OnResponse = proplists:get_value(onresponse, Opts), Timeout = proplists:get_value(timeout, Opts, 5000), URLDecDefault = {fun cowboy_http:urldecode/2, crash}, URLDec = proplists:get_value(urldecode, Opts, URLDecDefault), @@ -86,7 +89,8 @@ init(ListenerPid, Socket, Transport, Opts) -> wait_request(#state{listener=ListenerPid, socket=Socket, transport=Transport, dispatch=Dispatch, max_empty_lines=MaxEmptyLines, max_keepalive=MaxKeepalive, max_line_length=MaxLineLength, - timeout=Timeout, onrequest=OnRequest, urldecode=URLDec}). + timeout=Timeout, onrequest=OnRequest, onresponse=OnResponse, + urldecode=URLDec}). %% @private -spec parse_request(#state{}) -> ok. @@ -122,7 +126,7 @@ request({http_request, Method, {absoluteURI, _Scheme, _Host, _Port, Path}, request({http_request, Method, {abs_path, AbsPath}, Version}, State=#state{socket=Socket, transport=Transport, req_keepalive=Keepalive, max_keepalive=MaxKeepalive, - urldecode={URLDecFun, URLDecArg}=URLDec}) -> + onresponse=OnResponse, urldecode={URLDecFun, URLDecArg}=URLDec}) -> URLDecode = fun(Bin) -> URLDecFun(Bin, URLDecArg) end, {Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath, URLDecode), ConnAtom = if Keepalive < MaxKeepalive -> version_to_connection(Version); @@ -130,16 +134,19 @@ request({http_request, Method, {abs_path, AbsPath}, Version}, end, parse_header(#http_req{socket=Socket, transport=Transport, connection=ConnAtom, pid=self(), method=Method, version=Version, - path=Path, raw_path=RawPath, raw_qs=Qs, urldecode=URLDec}, State); + path=Path, raw_path=RawPath, raw_qs=Qs, onresponse=OnResponse, + urldecode=URLDec}, State); request({http_request, Method, '*', Version}, State=#state{socket=Socket, transport=Transport, - req_keepalive=Keepalive, max_keepalive=MaxKeepalive, urldecode=URLDec}) -> + req_keepalive=Keepalive, max_keepalive=MaxKeepalive, + onresponse=OnResponse, urldecode=URLDec}) -> ConnAtom = if Keepalive < MaxKeepalive -> version_to_connection(Version); true -> close end, parse_header(#http_req{socket=Socket, transport=Transport, connection=ConnAtom, pid=self(), method=Method, version=Version, - path='*', raw_path= <<"*">>, raw_qs= <<>>, urldecode=URLDec}, State); + path='*', raw_path= <<"*">>, raw_qs= <<>>, onresponse=OnResponse, + urldecode=URLDec}, State); request({http_request, _Method, _URI, _Version}, State) -> error_terminate(501, State); request({http_error, <<"\r\n">>}, @@ -324,16 +331,15 @@ handler_loop_timeout(State=#state{loop_timeout=Timeout, loop_timeout_ref=PrevRef}) -> _ = case PrevRef of undefined -> ignore; PrevRef -> erlang:cancel_timer(PrevRef) end, - TRef = make_ref(), - erlang:send_after(Timeout, self(), {?MODULE, timeout, TRef}), + TRef = erlang:start_timer(Timeout, self(), ?MODULE), State#state{loop_timeout_ref=TRef}. -spec handler_loop(any(), #http_req{}, #state{}) -> ok. handler_loop(HandlerState, Req, State=#state{loop_timeout_ref=TRef}) -> receive - {?MODULE, timeout, TRef} -> + {timeout, TRef, ?MODULE} -> terminate_request(HandlerState, Req, State); - {?MODULE, timeout, OlderTRef} when is_reference(OlderTRef) -> + {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> handler_loop(HandlerState, Req, State); Message -> handler_call(HandlerState, Req, State, Message) @@ -432,12 +438,13 @@ ensure_response(#http_req{socket=Socket, transport=Transport, %% Only send an error reply if there is no resp_sent message. -spec error_terminate(cowboy_http:status(), #state{}) -> ok. -error_terminate(Code, State=#state{socket=Socket, transport=Transport}) -> +error_terminate(Code, State=#state{socket=Socket, transport=Transport, + onresponse=OnResponse}) -> receive {cowboy_http_req, resp_sent} -> ok after 0 -> _ = cowboy_http_req:reply(Code, #http_req{ - socket=Socket, transport=Transport, + socket=Socket, transport=Transport, onresponse=OnResponse, connection=close, pid=self(), resp_state=waiting}), ok end, diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl index dfb73e7..8f1f789 100644 --- a/src/cowboy_http_req.erl +++ b/src/cowboy_http_req.erl @@ -563,34 +563,26 @@ body_qs(Req=#http_req{urldecode={URLDecFun, URLDecArg}}) -> %% %% Use this function for multipart streaming. For each part in the request, %% this function returns <em>{headers, Headers}</em> followed by a sequence of -%% <em>{data, Data}</em> tuples and finally <em>end_of_part</em>. When there +%% <em>{body, Data}</em> tuples and finally <em>end_of_part</em>. When there %% is no part to parse anymore, <em>eof</em> is returned. %% %% If the request Content-Type is not a multipart one, <em>{error, badarg}</em> %% is returned. -spec multipart_data(#http_req{}) -> {{headers, cowboy_http:headers()} - | {data, binary()} | end_of_part | eof, + | {body, binary()} | end_of_part | eof, #http_req{}}. multipart_data(Req=#http_req{body_state=waiting}) -> {{<<"multipart">>, _SubType, Params}, Req2} = parse_header('Content-Type', Req), {_, Boundary} = lists:keyfind(<<"boundary">>, 1, Params), - {Length, Req3=#http_req{buffer=Buffer}} = - parse_header('Content-Length', Req2), - multipart_data(Req3, Length, cowboy_multipart:parser(Boundary), Buffer); + {Length, Req3} = parse_header('Content-Length', Req2), + multipart_data(Req3, Length, {more, cowboy_multipart:parser(Boundary)}); multipart_data(Req=#http_req{body_state={multipart, Length, Cont}}) -> multipart_data(Req, Length, Cont()); multipart_data(Req=#http_req{body_state=done}) -> {eof, Req}. -multipart_data(Req, Length, Parser, Buffer) when byte_size(Buffer) >= Length -> - << Data:Length/binary, Rest/binary >> = Buffer, - multipart_data(Req#http_req{buffer=Rest}, 0, Parser(Data)); -multipart_data(Req, Length, Parser, Buffer) -> - NewLength = Length - byte_size(Buffer), - multipart_data(Req#http_req{buffer= <<>>}, NewLength, Parser(Buffer)). - multipart_data(Req, Length, {headers, Headers, Cont}) -> {{headers, Headers}, Req#http_req{body_state={multipart, Length, Cont}}}; multipart_data(Req, Length, {body, Data, Cont}) -> @@ -601,15 +593,15 @@ multipart_data(Req, 0, eof) -> {eof, Req#http_req{body_state=done}}; multipart_data(Req=#http_req{socket=Socket, transport=Transport}, Length, eof) -> + %% We just want to skip so no need to stream data here. {ok, _Data} = Transport:recv(Socket, Length, 5000), {eof, Req#http_req{body_state=done}}; -multipart_data(Req=#http_req{socket=Socket, transport=Transport}, - Length, {more, Parser}) when Length > 0 -> - case Transport:recv(Socket, 0, 5000) of - {ok, << Data:Length/binary, Buffer/binary >>} -> - multipart_data(Req#http_req{buffer=Buffer}, 0, Parser(Data)); - {ok, Data} -> - multipart_data(Req, Length - byte_size(Data), Parser(Data)) +multipart_data(Req, Length, {more, Parser}) when Length > 0 -> + case stream_body(Req) of + {ok, << Data:Length/binary, Buffer/binary >>, Req2} -> + multipart_data(Req2#http_req{buffer=Buffer}, 0, Parser(Data)); + {ok, Data, Req2} -> + multipart_data(Req2, Length - byte_size(Data), Parser(Data)) end. %% @doc Skip a part returned by the multipart parser. @@ -668,7 +660,6 @@ set_resp_body(Body, Req) -> set_resp_body_fun(StreamLen, StreamFun, Req) -> {ok, Req#http_req{resp_body={StreamLen, StreamFun}}}. - %% @doc Return whether the given header has been set for the response. -spec has_resp_header(cowboy_http:header(), #http_req{}) -> boolean(). has_resp_header(Name, #http_req{resp_headers=RespHeaders}) -> @@ -696,24 +687,29 @@ reply(Status, Headers, Req=#http_req{resp_body=Body}) -> %% @doc Send a reply to the client. -spec reply(cowboy_http:status(), cowboy_http:headers(), iodata(), #http_req{}) -> {ok, #http_req{}}. -reply(Status, Headers, Body, Req=#http_req{socket=Socket, - transport=Transport, connection=Connection, pid=ReqPid, +reply(Status, Headers, Body, Req=#http_req{socket=Socket, transport=Transport, + version=Version, connection=Connection, method=Method, resp_state=waiting, resp_headers=RespHeaders}) -> RespConn = response_connection(Headers, Connection), ContentLen = case Body of {CL, _} -> CL; _ -> iolist_size(Body) end, - Head = response_head(Status, Headers, RespHeaders, [ - {<<"Connection">>, atom_to_connection(Connection)}, + HTTP11Headers = case Version of + {1, 1} -> [{<<"Connection">>, atom_to_connection(Connection)}]; + _ -> [] + end, + {ReplyType, Req2} = response(Status, Headers, RespHeaders, [ {<<"Content-Length">>, integer_to_list(ContentLen)}, {<<"Date">>, cowboy_clock:rfc1123()}, {<<"Server">>, <<"Cowboy">>} - ]), - case {Method, Body} of - {'HEAD', _} -> Transport:send(Socket, Head); - {_, {_, StreamFun}} -> Transport:send(Socket, Head), StreamFun(); - {_, _} -> Transport:send(Socket, [Head, Body]) + |HTTP11Headers], Req), + if Method =:= 'HEAD' -> ok; + ReplyType =:= hook -> ok; %% Hook replied for us, stop there. + true -> + case Body of + {_, StreamFun} -> StreamFun(); + _ -> Transport:send(Socket, Body) + end end, - ReqPid ! {?MODULE, resp_sent}, - {ok, Req#http_req{connection=RespConn, resp_state=done, + {ok, Req2#http_req{connection=RespConn, resp_state=done, resp_headers=[], resp_body= <<>>}}. %% @equiv chunked_reply(Status, [], Req) @@ -725,25 +721,21 @@ chunked_reply(Status, Req) -> %% @see cowboy_http_req:chunk/2 -spec chunked_reply(cowboy_http:status(), cowboy_http:headers(), #http_req{}) -> {ok, #http_req{}}. -chunked_reply(Status, Headers, Req=#http_req{socket=Socket, - transport=Transport, version=Version, connection=Connection, - pid=ReqPid, resp_state=waiting, resp_headers=RespHeaders}) -> +chunked_reply(Status, Headers, Req=#http_req{ + version=Version, connection=Connection, + resp_state=waiting, resp_headers=RespHeaders}) -> RespConn = response_connection(Headers, Connection), - DefaultHeaders = [ - {<<"Date">>, cowboy_clock:rfc1123()}, - {<<"Server">>, <<"Cowboy">>} - ], - DefaultHeaders2 = case Version of + HTTP11Headers = case Version of {1, 1} -> [ - {<<"Connection">>, atom_to_connection(Connection)}, - {<<"Transfer-Encoding">>, <<"chunked">>} - ] ++ DefaultHeaders; - _ -> DefaultHeaders + {<<"Connection">>, atom_to_connection(Connection)}, + {<<"Transfer-Encoding">>, <<"chunked">>}]; + _ -> [] end, - Head = response_head(Status, Headers, RespHeaders, DefaultHeaders2), - Transport:send(Socket, Head), - ReqPid ! {?MODULE, resp_sent}, - {ok, Req#http_req{connection=RespConn, resp_state=chunks, + {_, Req2} = response(Status, Headers, RespHeaders, [ + {<<"Date">>, cowboy_clock:rfc1123()}, + {<<"Server">>, <<"Cowboy">>} + |HTTP11Headers], Req), + {ok, Req2#http_req{connection=RespConn, resp_state=chunks, resp_headers=[], resp_body= <<>>}}. %% @doc Send a chunk of data. @@ -762,14 +754,12 @@ chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) -> %% @private -spec upgrade_reply(cowboy_http:status(), cowboy_http:headers(), #http_req{}) -> {ok, #http_req{}}. -upgrade_reply(Status, Headers, Req=#http_req{socket=Socket, transport=Transport, - pid=ReqPid, resp_state=waiting, resp_headers=RespHeaders}) -> - Head = response_head(Status, Headers, RespHeaders, [ +upgrade_reply(Status, Headers, Req=#http_req{ + resp_state=waiting, resp_headers=RespHeaders}) -> + {_, Req2} = response(Status, Headers, RespHeaders, [ {<<"Connection">>, <<"Upgrade">>} - ]), - Transport:send(Socket, Head), - ReqPid ! {?MODULE, resp_sent}, - {ok, Req#http_req{resp_state=done, resp_headers=[], resp_body= <<>>}}. + ], Req), + {ok, Req2#http_req{resp_state=done, resp_headers=[], resp_body= <<>>}}. %% Misc API. @@ -799,6 +789,35 @@ transport(#http_req{transport=Transport, socket=Socket}) -> %% Internal. +-spec response(cowboy_http:status(), cowboy_http:headers(), + cowboy_http:headers(), cowboy_http:headers(), #http_req{}) + -> {normal | hook, #http_req{}}. +response(Status, Headers, RespHeaders, DefaultHeaders, Req=#http_req{ + socket=Socket, transport=Transport, version=Version, + pid=ReqPid, onresponse=OnResponse}) -> + FullHeaders = response_merge_headers(Headers, RespHeaders, DefaultHeaders), + Req2 = case OnResponse of + undefined -> Req; + OnResponse -> OnResponse(Status, FullHeaders, + %% Don't call 'onresponse' from the hook itself. + Req#http_req{resp_headers=[], resp_body= <<>>, + onresponse=undefined}) + end, + ReplyType = case Req2#http_req.resp_state of + waiting -> + HTTPVer = cowboy_http:version_to_binary(Version), + StatusLine = << HTTPVer/binary, " ", + (status(Status))/binary, "\r\n" >>, + HeaderLines = [[Key, <<": ">>, Value, <<"\r\n">>] + || {Key, Value} <- FullHeaders], + Transport:send(Socket, [StatusLine, HeaderLines, <<"\r\n">>]), + ReqPid ! {?MODULE, resp_sent}, + normal; + _ -> + hook + end, + {ReplyType, Req2}. + -spec response_connection(cowboy_http:headers(), keepalive | close) -> keepalive | close. response_connection([], Connection) -> @@ -820,17 +839,13 @@ response_connection_parse(ReplyConn) -> Tokens = cowboy_http:nonempty_list(ReplyConn, fun cowboy_http:token/2), cowboy_http:connection_to_atom(Tokens). --spec response_head(cowboy_http:status(), cowboy_http:headers(), - cowboy_http:headers(), cowboy_http:headers()) -> iolist(). -response_head(Status, Headers, RespHeaders, DefaultHeaders) -> - StatusLine = <<"HTTP/1.1 ", (status(Status))/binary, "\r\n">>, +-spec response_merge_headers(cowboy_http:headers(), cowboy_http:headers(), + cowboy_http:headers()) -> cowboy_http:headers(). +response_merge_headers(Headers, RespHeaders, DefaultHeaders) -> Headers2 = [{header_to_binary(Key), Value} || {Key, Value} <- Headers], - Headers3 = merge_headers( + merge_headers( merge_headers(Headers2, RespHeaders), - DefaultHeaders), - Headers4 = [[Key, <<": ">>, Value, <<"\r\n">>] - || {Key, Value} <- Headers3], - [StatusLine, Headers4, <<"\r\n">>]. + DefaultHeaders). -spec merge_headers(cowboy_http:headers(), cowboy_http:headers()) -> cowboy_http:headers(). @@ -895,6 +910,9 @@ status(423) -> <<"423 Locked">>; status(424) -> <<"424 Failed Dependency">>; status(425) -> <<"425 Unordered Collection">>; status(426) -> <<"426 Upgrade Required">>; +status(428) -> <<"428 Precondition Required">>; +status(429) -> <<"429 Too Many Requests">>; +status(431) -> <<"431 Request Header Fields Too Large">>; status(500) -> <<"500 Internal Server Error">>; status(501) -> <<"501 Not Implemented">>; status(502) -> <<"502 Bad Gateway">>; @@ -904,6 +922,7 @@ status(505) -> <<"505 HTTP Version Not Supported">>; status(506) -> <<"506 Variant Also Negotiates">>; status(507) -> <<"507 Insufficient Storage">>; status(510) -> <<"510 Not Extended">>; +status(511) -> <<"511 Network Authentication Required">>; status(B) when is_binary(B) -> B. -spec header_to_binary(cowboy_http:header()) -> binary(). diff --git a/src/cowboy_http_websocket.erl b/src/cowboy_http_websocket.erl index ab96e93..f550041 100644 --- a/src/cowboy_http_websocket.erl +++ b/src/cowboy_http_websocket.erl @@ -14,27 +14,9 @@ %% @doc WebSocket protocol implementation. %% -%% Supports the protocol version 0 (hixie-76), version 7 (hybi-7) -%% and version 8 (hybi-8, hybi-9 and hybi-10). -%% -%% Version 0 is supported by the following browsers: -%% <ul> -%% <li>Firefox 4-5 (disabled by default)</li> -%% <li>Chrome 6-13</li> -%% <li>Safari 5.0.1+</li> -%% <li>Opera 11.00+ (disabled by default)</li> -%% </ul> -%% -%% Version 7 is supported by the following browser: -%% <ul> -%% <li>Firefox 6</li> -%% </ul> -%% -%% Version 8+ is supported by the following browsers: -%% <ul> -%% <li>Firefox 7+</li> -%% <li>Chrome 14+</li> -%% </ul> +%% When using websockets, make sure that the crypto application is +%% included in your release. If you are not using releases then there +%% is no need for concern as crypto is already included. -module(cowboy_http_websocket). -export([upgrade/4]). %% API. @@ -232,8 +214,7 @@ handler_loop_timeout(State=#state{timeout=infinity}) -> handler_loop_timeout(State=#state{timeout=Timeout, timeout_ref=PrevRef}) -> _ = case PrevRef of undefined -> ignore; PrevRef -> erlang:cancel_timer(PrevRef) end, - TRef = make_ref(), - erlang:send_after(Timeout, self(), {?MODULE, timeout, TRef}), + TRef = erlang:start_timer(Timeout, self(), ?MODULE), State#state{timeout_ref=TRef}. %% @private @@ -248,9 +229,9 @@ handler_loop(State=#state{messages={OK, Closed, Error}, timeout_ref=TRef}, handler_terminate(State, Req, HandlerState, {error, closed}); {Error, Socket, Reason} -> handler_terminate(State, Req, HandlerState, {error, Reason}); - {?MODULE, timeout, TRef} -> + {timeout, TRef, ?MODULE} -> websocket_close(State, Req, HandlerState, {normal, timeout}); - {?MODULE, timeout, OlderTRef} when is_reference(OlderTRef) -> + {timeout, OlderTRef, ?MODULE} when is_reference(OlderTRef) -> handler_loop(State, Req, HandlerState, SoFar); Message -> handler_call(State, Req, HandlerState, diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 4a26612..095d6b5 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -29,6 +29,7 @@ -export([check_raw_status/1]). -export([check_status/1]). -export([chunked_response/1]). +-export([echo_body/1]). -export([error_chain_handle_after_reply/1]). -export([error_chain_handle_before_reply/1]). -export([error_handle_after_reply/1]). @@ -44,6 +45,8 @@ -export([nc_zero/1]). -export([onrequest/1]). -export([onrequest_reply/1]). +-export([onresponse_crash/1]). +-export([onresponse_reply/1]). -export([pipeline/1]). -export([rest_keepalive/1]). -export([rest_keepalive_post/1]). @@ -56,10 +59,10 @@ -export([static_attribute_etag/1]). -export([static_function_etag/1]). -export([static_mimetypes_function/1]). --export([static_test_file/1]). --export([static_test_file_css/1]). -export([static_specify_file/1]). -export([static_specify_file_catchall/1]). +-export([static_test_file/1]). +-export([static_test_file_css/1]). -export([stream_body_set_resp/1]). -export([te_chunked/1]). -export([te_chunked_delayed/1]). @@ -68,13 +71,14 @@ %% ct. all() -> - [{group, http}, {group, https}, {group, hooks}]. + [{group, http}, {group, https}, {group, onrequest}, {group, onresponse}]. groups() -> Tests = [ check_raw_status, check_status, chunked_response, + echo_body, error_chain_handle_after_reply, error_chain_handle_before_reply, error_handle_after_reply, @@ -100,10 +104,10 @@ groups() -> static_attribute_etag, static_function_etag, static_mimetypes_function, - static_test_file, - static_test_file_css, static_specify_file, static_specify_file_catchall, + static_test_file, + static_test_file_css, stream_body_set_resp, te_chunked, te_chunked_delayed, @@ -112,9 +116,13 @@ groups() -> [ {http, [], Tests}, {https, [], Tests}, - {hooks, [], [ + {onrequest, [], [ onrequest, onrequest_reply + ]}, + {onresponse, [], [ + onresponse_crash, + onresponse_reply ]} ]. @@ -132,7 +140,7 @@ init_per_group(http, Config) -> Port = 33080, Transport = cowboy_tcp_transport, Config1 = init_static_dir(Config), - cowboy:start_listener(http, 100, + {ok, _} = cowboy:start_listener(http, 100, Transport, [{port, Port}], cowboy_http_protocol, [ {dispatch, init_dispatch(Config1)}, @@ -154,7 +162,7 @@ init_per_group(https, Config) -> application:start(crypto), application:start(public_key), application:start(ssl), - {ok,_} = cowboy:start_listener(https, 100, + {ok, _} = cowboy:start_listener(https, 100, Transport, Opts ++ [{port, Port}], cowboy_http_protocol, [ {dispatch, init_dispatch(Config1)}, @@ -164,10 +172,10 @@ init_per_group(https, Config) -> {ok, Client} = cowboy_client:init(Opts), [{scheme, <<"https">>}, {port, Port}, {opts, Opts}, {transport, Transport}, {client, Client}|Config1]; -init_per_group(hooks, Config) -> +init_per_group(onrequest, Config) -> Port = 33082, Transport = cowboy_tcp_transport, - {ok, _} = cowboy:start_listener(hooks, 100, + {ok, _} = cowboy:start_listener(onrequest, 100, Transport, [{port, Port}], cowboy_http_protocol, [ {dispatch, init_dispatch(Config)}, @@ -177,6 +185,20 @@ init_per_group(hooks, Config) -> ]), {ok, Client} = cowboy_client:init([]), [{scheme, <<"http">>}, {port, Port}, {opts, []}, + {transport, Transport}, {client, Client}|Config]; +init_per_group(onresponse, Config) -> + Port = 33083, + Transport = cowboy_tcp_transport, + {ok, _} = cowboy:start_listener(onresponse, 100, + Transport, [{port, Port}], + cowboy_http_protocol, [ + {dispatch, init_dispatch(Config)}, + {max_keepalive, 50}, + {onresponse, fun onresponse_hook/3}, + {timeout, 500} + ]), + {ok, Client} = cowboy_client:init([]), + [{scheme, <<"http">>}, {port, Port}, {opts, []}, {transport, Transport}, {client, Client}|Config]. end_per_group(https, Config) -> @@ -189,8 +211,8 @@ end_per_group(https, Config) -> end_per_group(http, Config) -> cowboy:stop_listener(http), end_static_dir(Config); -end_per_group(hooks, _) -> - cowboy:stop_listener(hooks), +end_per_group(Name, _) -> + cowboy:stop_listener(Name), ok. %% Dispatch configuration. @@ -236,6 +258,7 @@ init_dispatch(Config) -> {[<<"simple_post">>], rest_forbidden_resource, [false]}, {[<<"nodelete">>], rest_nodelete_resource, []}, {[<<"resetags">>], rest_resource_etags, []}, + {[<<"loop_timeout">>], http_handler_loop_timeout, []}, {[], http_handler, []} ]} ]. @@ -360,6 +383,7 @@ check_status(Config) -> {102, "/long_polling"}, {200, "/"}, {200, "/simple"}, + {204, "/loop_timeout"}, {400, "/static/%2f"}, {400, "/static/%2e"}, {400, "/static/%2e%2e"}, @@ -382,6 +406,20 @@ chunked_response(Config) -> {ok, {{"HTTP/1.1", 200, "OK"}, _, "chunked_handler\r\nworks fine!"}} = httpc:request(binary_to_list(build_url("/chunked_response", Config))). +%% Check if sending requests whose size is around the MTU breaks something. +echo_body(Config) -> + Client = ?config(client, Config), + {ok, [{mtu, MTU}]} = inet:ifget("lo", [mtu]), + [begin + Body = list_to_binary(lists:duplicate(Size, $a)), + {ok, Client2} = cowboy_client:request(<<"POST">>, + build_url("/echo/body", Config), + [{<<"connection">>, <<"close">>}], + Body, Client), + {ok, 200, _, Client3} = cowboy_client:response(Client2), + {ok, Body, _} = cowboy_client:response_body(Client3) + end || Size <- lists:seq(MTU - 500, MTU)]. + error_chain_handle_after_reply(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, @@ -576,6 +614,28 @@ onrequest_hook(Req) -> Req3 end. +onresponse_crash(Config) -> + Client = ?config(client, Config), + {ok, Client2} = cowboy_client:request(<<"GET">>, + build_url("/handler_errors?case=init_before_reply", Config), Client), + {ok, 777, Headers, Client3} = cowboy_client:response(Client2), + {<<"x-hook">>, <<"onresponse">>} = lists:keyfind(<<"x-hook">>, 1, Headers). + +onresponse_reply(Config) -> + Client = ?config(client, Config), + {ok, Client2} = cowboy_client:request(<<"GET">>, + build_url("/", Config), Client), + {ok, 777, Headers, Client3} = cowboy_client:response(Client2), + {<<"x-hook">>, <<"onresponse">>} = lists:keyfind(<<"x-hook">>, 1, Headers), + %% Make sure we don't get the body initially sent. + {error, closed} = cowboy_client:response_body(Client3). + +%% Hook for the above onresponse tests. +onresponse_hook(_, Headers, Req) -> + {ok, Req2} = cowboy_http_req:reply( + <<"777 Lucky">>, [{<<"x-hook">>, <<"onresponse">>}|Headers], Req), + Req2. + pipeline(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, @@ -753,6 +813,24 @@ static_mimetypes_function(Config) -> {<<"content-type">>, <<"text/html">>} = lists:keyfind(<<"content-type">>, 1, Headers). +static_specify_file(Config) -> + Client = ?config(client, Config), + {ok, Client2} = cowboy_client:request(<<"GET">>, + build_url("/static_specify_file", Config), Client), + {ok, 200, Headers, Client3} = cowboy_client:response(Client2), + {<<"content-type">>, <<"text/css">>} + = lists:keyfind(<<"content-type">>, 1, Headers), + {ok, <<"test_file.css\n">>, _} = cowboy_client:response_body(Client3). + +static_specify_file_catchall(Config) -> + Client = ?config(client, Config), + {ok, Client2} = cowboy_client:request(<<"GET">>, + build_url("/static_specify_file/none", Config), Client), + {ok, 200, Headers, Client3} = cowboy_client:response(Client2), + {<<"content-type">>, <<"text/css">>} + = lists:keyfind(<<"content-type">>, 1, Headers), + {ok, <<"test_file.css\n">>, _} = cowboy_client:response_body(Client3). + static_test_file(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, @@ -769,22 +847,6 @@ static_test_file_css(Config) -> {<<"content-type">>, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers). -static_specify_file(Config) -> - Client = ?config(client, Config), - {ok, Client2} = cowboy_client:request(<<"GET">>, - build_url("/static_specify_file", Config), Client), - {ok, 200, Headers, Client3} = cowboy_client:response(Client2), - {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), - {ok, <<"test_file.css\n">>, _} = cowboy_client:response_body(Client3). - -static_specify_file_catchall(Config) -> - Client = ?config(client, Config), - {ok, Client2} = cowboy_client:request(<<"GET">>, - build_url("/static_specify_file/none", Config), Client), - {ok, 200, Headers, Client3} = cowboy_client:response(Client2), - {_, <<"text/css">>} = lists:keyfind(<<"content-type">>, 1, Headers), - {ok, <<"test_file.css\n">>, _} = cowboy_client:response_body(Client3). - stream_body_set_resp(Config) -> Client = ?config(client, Config), {ok, Client2} = cowboy_client:request(<<"GET">>, diff --git a/test/http_handler_loop_timeout.erl b/test/http_handler_loop_timeout.erl new file mode 100644 index 0000000..76d6ea8 --- /dev/null +++ b/test/http_handler_loop_timeout.erl @@ -0,0 +1,15 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(http_handler_loop_timeout). +-export([init/3, info/3, terminate/2]). + +init({_, http}, Req, _) -> + erlang:send_after(1000, self(), error_timeout), + {loop, Req, undefined, 500, hibernate}. + +info(error_timeout, Req, State) -> + {ok, Req2} = cowboy_http_req:reply(500, Req), + {ok, Req2, State}. + +terminate(_, _) -> + ok. |