diff options
-rw-r--r-- | README.md | 3 | ||||
-rw-r--r-- | src/cowboy.app.src | 2 | ||||
-rw-r--r-- | src/cowboy_http_protocol.erl | 29 | ||||
-rw-r--r-- | src/cowboy_http_req.erl | 17 | ||||
-rw-r--r-- | src/cowboy_http_websocket.erl | 64 | ||||
-rw-r--r-- | src/cowboy_http_websocket_handler.erl | 8 | ||||
-rw-r--r-- | src/cowboy_ssl_transport.erl | 8 | ||||
-rw-r--r-- | test/http_SUITE.erl | 78 | ||||
-rw-r--r-- | test/ws_timeout_hibernate_handler.erl | 29 |
9 files changed, 193 insertions, 45 deletions
@@ -3,6 +3,9 @@ Cowboy Cowboy is a small, fast and modular HTTP server written in Erlang. +Cowboy is also a socket acceptor pool, able to accept connections +for any kind of TCP protocol. + Goals ----- diff --git a/src/cowboy.app.src b/src/cowboy.app.src index 912673a..264607f 100644 --- a/src/cowboy.app.src +++ b/src/cowboy.app.src @@ -14,7 +14,7 @@ {application, cowboy, [ {description, "Small, fast, modular HTTP server."}, - {vsn, "0.2.0"}, + {vsn, "0.3.0"}, {modules, []}, {registered, [cowboy_clock, cowboy_sup]}, {applications, [ diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl index 9923591..63c971d 100644 --- a/src/cowboy_http_protocol.erl +++ b/src/cowboy_http_protocol.erl @@ -148,10 +148,12 @@ header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{ case catch cowboy_dispatcher:split_host(RawHost2) of {Host, RawHost3, undefined} -> Port = default_port(Transport:name()), - dispatch(Req#http_req{host=Host, raw_host=RawHost3, port=Port, + dispatch(fun parse_header/2, Req#http_req{ + host=Host, raw_host=RawHost3, port=Port, headers=[{'Host', RawHost3}|Req#http_req.headers]}, State); {Host, RawHost3, Port} -> - dispatch(Req#http_req{host=Host, raw_host=RawHost3, port=Port, + dispatch(fun parse_header/2, Req#http_req{ + host=Host, raw_host=RawHost3, port=Port, headers=[{'Host', RawHost3}|Req#http_req.headers]}, State); {'EXIT', _Reason} -> error_terminate(400, State) @@ -168,24 +170,30 @@ header({http_header, _I, Field, _R, Value}, Req, State) -> Field2 = format_header(Field), parse_header(Req#http_req{headers=[{Field2, Value}|Req#http_req.headers]}, State); -%% The Host header is required. -header(http_eoh, #http_req{host=undefined}, State) -> +%% The Host header is required in HTTP/1.1. +header(http_eoh, #http_req{version={1, 1}, host=undefined}, State) -> error_terminate(400, State); +%% It is however optional in HTTP/1.0. +header(http_eoh, Req=#http_req{version={1, 0}, transport=Transport, + host=undefined}, State=#state{buffer=Buffer}) -> + Port = default_port(Transport:name()), + dispatch(fun handler_init/2, Req#http_req{host=[], raw_host= <<>>, + port=Port, buffer=Buffer}, State#state{buffer= <<>>}); header(http_eoh, Req, State=#state{buffer=Buffer}) -> handler_init(Req#http_req{buffer=Buffer}, State#state{buffer= <<>>}); header({http_error, _Bin}, _Req, State) -> error_terminate(500, State). --spec dispatch(#http_req{}, #state{}) -> ok. -dispatch(Req=#http_req{host=Host, path=Path}, +-spec dispatch(fun((#http_req{}, #state{}) -> ok), + #http_req{}, #state{}) -> ok. +dispatch(Next, Req=#http_req{host=Host, path=Path}, State=#state{dispatch=Dispatch}) -> %% @todo We probably want to filter the Host and Path here to allow %% things like url rewriting. case cowboy_dispatcher:match(Host, Path, Dispatch) of {ok, Handler, Opts, Binds, HostInfo, PathInfo} -> - parse_header(Req#http_req{host_info=HostInfo, path_info=PathInfo, - bindings=Binds}, - State#state{handler={Handler, Opts}}); + Next(Req#http_req{host_info=HostInfo, path_info=PathInfo, + bindings=Binds}, State#state{handler={Handler, Opts}}); {error, notfound, host} -> error_terminate(400, State); {error, notfound, path} -> @@ -246,7 +254,8 @@ next_request(HandlerState, Req=#http_req{buffer=Buffer}, State) -> RespRes = ensure_response(Req, State), case {HandlerRes, BodyRes, RespRes, State#state.connection} of {ok, ok, ok, keepalive} -> - ?MODULE:parse_request(State#state{buffer=Buffer}); + ?MODULE:parse_request(State#state{ + buffer=Buffer, req_empty_lines=0}); _Closed -> terminate(State) end. diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl index 69d47b0..5b63599 100644 --- a/src/cowboy_http_req.erl +++ b/src/cowboy_http_req.erl @@ -264,7 +264,7 @@ body_qs(Req) -> -> {ok, #http_req{}}. reply(Code, Headers, Body, Req=#http_req{socket=Socket, transport=Transport, connection=Connection, - resp_state=waiting}) -> + method=Method, resp_state=waiting}) -> Head = response_head(Code, Headers, [ {<<"Connection">>, atom_to_connection(Connection)}, {<<"Content-Length">>, @@ -272,7 +272,10 @@ reply(Code, Headers, Body, Req=#http_req{socket=Socket, {<<"Date">>, cowboy_clock:rfc1123()}, {<<"Server">>, <<"Cowboy">>} ]), - Transport:send(Socket, [Head, Body]), + case Method of + 'HEAD' -> Transport:send(Socket, Head); + _ -> Transport:send(Socket, [Head, Body]) + end, {ok, Req#http_req{resp_state=done}}. %% @doc Initiate the sending of a chunked reply to the client. @@ -280,6 +283,14 @@ reply(Code, Headers, Body, Req=#http_req{socket=Socket, -spec chunked_reply(http_status(), http_headers(), #http_req{}) -> {ok, #http_req{}}. chunked_reply(Code, Headers, Req=#http_req{socket=Socket, transport=Transport, + method='HEAD', resp_state=waiting}) -> + Head = response_head(Code, Headers, [ + {<<"Date">>, cowboy_clock:rfc1123()}, + {<<"Server">>, <<"Cowboy">>} + ]), + Transport:send(Socket, Head), + {ok, Req#http_req{resp_state=done}}; +chunked_reply(Code, Headers, Req=#http_req{socket=Socket, transport=Transport, resp_state=waiting}) -> Head = response_head(Code, Headers, [ {<<"Connection">>, <<"close">>}, @@ -294,6 +305,8 @@ chunked_reply(Code, Headers, Req=#http_req{socket=Socket, transport=Transport, %% %% A chunked reply must have been initiated before calling this function. -spec chunk(iodata(), #http_req{}) -> ok. +chunk(_Data, #http_req{socket=_Socket, transport=_Transport, method='HEAD'}) -> + ok; chunk(Data, #http_req{socket=Socket, transport=Transport, resp_state=chunks}) -> Transport:send(Socket, [integer_to_list(iolist_size(Data), 16), <<"\r\n">>, Data, <<"\r\n">>]). diff --git a/src/cowboy_http_websocket.erl b/src/cowboy_http_websocket.erl index 8e951a5..1164684 100644 --- a/src/cowboy_http_websocket.erl +++ b/src/cowboy_http_websocket.erl @@ -52,6 +52,7 @@ opts :: any(), challenge = undefined :: undefined | binary(), timeout = infinity :: timeout(), + timeout_ref = undefined :: undefined | reference(), messages = undefined :: undefined | {atom(), atom(), atom()}, hibernate = false :: boolean(), eop :: undefined | tuple(), %% hixie-76 specific. @@ -116,9 +117,15 @@ handler_init(State=#state{handler=Handler, opts=Opts}, try Handler:websocket_init(Transport:name(), Req, Opts) of {ok, Req2, HandlerState} -> websocket_handshake(State, Req2, HandlerState); + {ok, Req2, HandlerState, hibernate} -> + websocket_handshake(State#state{hibernate=true}, + Req2, HandlerState); {ok, Req2, HandlerState, Timeout} -> websocket_handshake(State#state{timeout=Timeout}, - Req2, HandlerState) + Req2, HandlerState); + {ok, Req2, HandlerState, Timeout, hibernate} -> + websocket_handshake(State#state{timeout=Timeout, + hibernate=true}, Req2, HandlerState) catch Class:Reason -> upgrade_error(Req), error_logger:error_msg( @@ -137,8 +144,8 @@ upgrade_error(Req=#http_req{socket=Socket, transport=Transport}) -> -spec websocket_handshake(#state{}, #http_req{}, any()) -> ok. websocket_handshake(State=#state{version=0, origin=Origin, challenge=Challenge}, Req=#http_req{transport=Transport, - raw_host=Host, port=Port, raw_path=Path}, HandlerState) -> - Location = hixie76_location(Transport:name(), Host, Port, Path), + raw_host=Host, port=Port, raw_path=Path, raw_qs=QS}, HandlerState) -> + Location = hixie76_location(Transport:name(), Host, Port, Path, QS), {ok, Req2} = cowboy_http_req:reply( <<"101 WebSocket Protocol Handshake">>, [{<<"Connection">>, <<"Upgrade">>}, @@ -164,16 +171,28 @@ handler_before_loop(State=#state{hibernate=true}, Req=#http_req{socket=Socket, transport=Transport}, HandlerState, SoFar) -> Transport:setopts(Socket, [{active, once}]), - erlang:hibernate(?MODULE, handler_loop, [State#state{hibernate=false}, + State2 = handler_loop_timeout(State), + erlang:hibernate(?MODULE, handler_loop, [State2#state{hibernate=false}, Req, HandlerState, SoFar]); handler_before_loop(State, Req=#http_req{socket=Socket, transport=Transport}, HandlerState, SoFar) -> Transport:setopts(Socket, [{active, once}]), - handler_loop(State, Req, HandlerState, SoFar). + State2 = handler_loop_timeout(State), + handler_loop(State2, Req, HandlerState, SoFar). + +-spec handler_loop_timeout(#state{}) -> #state{}. +handler_loop_timeout(State=#state{timeout=infinity}) -> + State#state{timeout_ref=undefined}; +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}), + State#state{timeout_ref=TRef}. %% @private -spec handler_loop(#state{}, #http_req{}, any(), binary()) -> ok. -handler_loop(State=#state{messages={OK, Closed, Error}, timeout=Timeout}, +handler_loop(State=#state{messages={OK, Closed, Error}, timeout_ref=TRef}, Req=#http_req{socket=Socket}, HandlerState, SoFar) -> receive {OK, Socket, Data} -> @@ -183,11 +202,13 @@ handler_loop(State=#state{messages={OK, Closed, Error}, timeout=Timeout}, handler_terminate(State, Req, HandlerState, {error, closed}); {Error, Socket, Reason} -> handler_terminate(State, Req, HandlerState, {error, Reason}); + {?MODULE, timeout, TRef} -> + websocket_close(State, Req, HandlerState, {normal, timeout}); + {?MODULE, timeout, OlderTRef} when is_reference(OlderTRef) -> + handler_loop(State, Req, HandlerState, SoFar); Message -> handler_call(State, Req, HandlerState, SoFar, websocket_info, Message, fun handler_before_loop/4) - after Timeout -> - websocket_close(State, Req, HandlerState, {normal, timeout}) end. -spec websocket_data(#state{}, #http_req{}, any(), binary()) -> ok. @@ -396,11 +417,14 @@ hixie76_key_to_integer(Key) -> Spaces = length([C || << C >> <= Key, C =:= 32]), Number div Spaces. --spec hixie76_location(atom(), binary(), inet:ip_port(), binary()) +-spec hixie76_location(atom(), binary(), inet:ip_port(), binary(), binary()) -> binary(). -hixie76_location(Protocol, Host, Port, Path) -> - << (hixie76_location_protocol(Protocol))/binary, "://", Host/binary, - (hixie76_location_port(ssl, Port))/binary, Path/binary >>. +hixie76_location(Protocol, Host, Port, Path, <<>>) -> + << (hixie76_location_protocol(Protocol))/binary, "://", Host/binary, + (hixie76_location_port(ssl, Port))/binary, Path/binary>>; +hixie76_location(Protocol, Host, Port, Path, QS) -> + << (hixie76_location_protocol(Protocol))/binary, "://", Host/binary, + (hixie76_location_port(ssl, Port))/binary, Path/binary, "?", QS/binary >>. -spec hixie76_location_protocol(atom()) -> binary(). hixie76_location_protocol(ssl) -> <<"wss">>; @@ -408,9 +432,9 @@ hixie76_location_protocol(_) -> <<"ws">>. -spec hixie76_location_port(atom(), inet:ip_port()) -> binary(). hixie76_location_port(ssl, 443) -> - <<"">>; + <<>>; hixie76_location_port(_, 80) -> - <<"">>; + <<>>; hixie76_location_port(_, Port) -> <<":", (list_to_binary(integer_to_list(Port)))/binary>>. @@ -436,13 +460,17 @@ hybi_payload_length(N) -> hixie76_location_test() -> ?assertEqual(<<"ws://localhost/path">>, - hixie76_location(other, <<"localhost">>, 80, <<"/path">>)), + hixie76_location(other, <<"localhost">>, 80, <<"/path">>, <<>>)), ?assertEqual(<<"ws://localhost:8080/path">>, - hixie76_location(other, <<"localhost">>, 8080, <<"/path">>)), + hixie76_location(other, <<"localhost">>, 8080, <<"/path">>, <<>>)), + ?assertEqual(<<"ws://localhost:8080/path?dummy=2785">>, + hixie76_location(other, <<"localhost">>, 8080, <<"/path">>, <<"dummy=2785">>)), ?assertEqual(<<"wss://localhost/path">>, - hixie76_location(ssl, <<"localhost">>, 443, <<"/path">>)), + hixie76_location(ssl, <<"localhost">>, 443, <<"/path">>, <<>>)), ?assertEqual(<<"wss://localhost:8443/path">>, - hixie76_location(ssl, <<"localhost">>, 8443, <<"/path">>)), + hixie76_location(ssl, <<"localhost">>, 8443, <<"/path">>, <<>>)), + ?assertEqual(<<"wss://localhost:8443/path?dummy=2785">>, + hixie76_location(ssl, <<"localhost">>, 8443, <<"/path">>, <<"dummy=2785">>)), ok. -endif. diff --git a/src/cowboy_http_websocket_handler.erl b/src/cowboy_http_websocket_handler.erl index 90cf7ac..2ea0a46 100644 --- a/src/cowboy_http_websocket_handler.erl +++ b/src/cowboy_http_websocket_handler.erl @@ -29,8 +29,7 @@ %% here. %% %% <em>websocket_handle/3</em> receives the data from the socket. It can reply -%% something, do nothing or close the connection. You can choose to hibernate -%% the process by returning <em>hibernate</em> to save memory and CPU. +%% something, do nothing or close the connection. %% %% <em>websocket_info/3</em> receives messages sent to the process. It has %% the same reply format as <em>websocket_handle/3</em> described above. Note @@ -41,6 +40,11 @@ %% <em>websocket_terminate/3</em> is meant for cleaning up. It also receives %% the request and the state previously defined, along with a reason for %% termination. +%% +%% All of <em>websocket_init/3</em>, <em>websocket_handle/3</em> and +%% <em>websocket_info/3</em> can decide to hibernate the process by adding +%% an extra element to the returned tuple, containing the atom +%% <em>hibernate</em>. Doing so helps save memory and improve CPU usage. -module(cowboy_http_websocket_handler). -export([behaviour_info/1]). diff --git a/src/cowboy_ssl_transport.erl b/src/cowboy_ssl_transport.erl index bb53418..bf8b1fb 100644 --- a/src/cowboy_ssl_transport.erl +++ b/src/cowboy_ssl_transport.erl @@ -79,10 +79,10 @@ listen(Opts) -> Ip -> [Ip|ListenOpts0] end, ListenOpts = - case lists:keyfind(cacertfile, 1, Opts) of - false -> ListenOpts1; - CACertFile -> [CACertFile|ListenOpts1] - end, + case lists:keyfind(cacertfile, 1, Opts) of + false -> ListenOpts1; + CACertFile -> [CACertFile|ListenOpts1] + end, ssl:listen(Port, ListenOpts). %% @doc Accept an incoming connection on a listen socket. diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl index 2db3f4e..813aa15 100644 --- a/test/http_SUITE.erl +++ b/test/http_SUITE.erl @@ -19,19 +19,22 @@ -export([all/0, groups/0, init_per_suite/1, end_per_suite/1, init_per_group/2, end_per_group/2]). %% ct. -export([chunked_response/1, headers_dupe/1, headers_huge/1, - nc_rand/1, pipeline/1, raw/1, ws0/1, ws8/1]). %% http. + keepalive_nl/1, nc_rand/1, pipeline/1, raw/1, + ws0/1, ws8/1, ws_timeout_hibernate/1]). %% http. -export([http_200/1, http_404/1]). %% http and https. +-export([http_10_hostless/1]). %% misc. %% ct. all() -> - [{group, http}, {group, https}]. + [{group, http}, {group, https}, {group, misc}]. groups() -> BaseTests = [http_200, http_404], [{http, [], [chunked_response, headers_dupe, headers_huge, - nc_rand, pipeline, raw, ws0, ws8] ++ BaseTests}, - {https, [], BaseTests}]. + keepalive_nl, nc_rand, pipeline, raw, + ws0, ws8, ws_timeout_hibernate] ++ BaseTests}, + {https, [], BaseTests}, {misc, [], [http_10_hostless]}]. init_per_suite(Config) -> application:start(inets), @@ -62,16 +65,24 @@ init_per_group(https, Config) -> {keyfile, DataDir ++ "key.pem"}, {password, "cowboy"}], cowboy_http_protocol, [{dispatch, init_https_dispatch()}] ), - [{scheme, "https"}, {port, Port}|Config]. + [{scheme, "https"}, {port, Port}|Config]; +init_per_group(misc, Config) -> + Port = 33082, + cowboy:start_listener(misc, 100, + cowboy_tcp_transport, [{port, Port}], + cowboy_http_protocol, [{dispatch, [{'_', [ + {[], http_handler, []} + ]}]}]), + [{port, Port}|Config]. -end_per_group(http, _Config) -> - cowboy:stop_listener(http), - ok; end_per_group(https, _Config) -> cowboy:stop_listener(https), application:stop(ssl), application:stop(public_key), application:stop(crypto), + ok; +end_per_group(Listener, _Config) -> + cowboy:stop_listener(Listener), ok. %% Dispatch configuration. @@ -81,6 +92,7 @@ init_http_dispatch() -> {[<<"localhost">>], [ {[<<"chunked_response">>], chunked_handler, []}, {[<<"websocket">>], websocket_handler, []}, + {[<<"ws_timeout_hibernate">>], ws_timeout_hibernate_handler, []}, {[<<"headers">>, <<"dupe">>], http_handler, [{headers, [{<<"Connection">>, <<"close">>}]}]}, {[], http_handler, []} @@ -113,6 +125,24 @@ headers_huge(Config) -> {_Packet, 200} = raw_req(["GET / HTTP/1.0\r\nHost: localhost\r\n" "Set-Cookie: ", Cookie, "\r\n\r\n"], Config). +keepalive_nl(Config) -> + {port, Port} = lists:keyfind(port, 1, Config), + {ok, Socket} = gen_tcp:connect("localhost", Port, + [binary, {active, false}, {packet, raw}]), + ok = keepalive_nl_loop(Socket, 100), + ok = gen_tcp:close(Socket). + +keepalive_nl_loop(_Socket, 0) -> + ok; +keepalive_nl_loop(Socket, N) -> + ok = gen_tcp:send(Socket, "GET / HTTP/1.1\r\n" + "Host: localhost\r\nConnection: keep-alive\r\n\r\n"), + {ok, Data} = gen_tcp:recv(Socket, 0, 6000), + {0, 12} = binary:match(Data, <<"HTTP/1.1 200">>), + nomatch = binary:match(Data, <<"Connection: close">>), + ok = gen_tcp:send(Socket, "\r\n"), %% extra nl + keepalive_nl_loop(Socket, N - 1). + nc_rand(Config) -> Cat = os:find_executable("cat"), Nc = os:find_executable("nc"), @@ -270,6 +300,32 @@ ws8(Config) -> {error, closed} = gen_tcp:recv(Socket, 0, 6000), ok. +ws_timeout_hibernate(Config) -> + {port, Port} = lists:keyfind(port, 1, Config), + {ok, Socket} = gen_tcp:connect("localhost", Port, + [binary, {active, false}, {packet, raw}]), + ok = gen_tcp:send(Socket, [ + "GET /ws_timeout_hibernate HTTP/1.1\r\n" + "Host: localhost\r\n" + "Connection: Upgrade\r\n" + "Upgrade: websocket\r\n" + "Sec-WebSocket-Origin: http://localhost\r\n" + "Sec-WebSocket-Version: 8\r\n" + "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" + "\r\n"]), + {ok, Handshake} = gen_tcp:recv(Socket, 0, 6000), + {ok, {http_response, {1, 1}, 101, "Switching Protocols"}, Rest} + = erlang:decode_packet(http, Handshake, []), + [Headers, <<>>] = websocket_headers( + erlang:decode_packet(httph, Rest, []), []), + {'Connection', "Upgrade"} = lists:keyfind('Connection', 1, Headers), + {'Upgrade', "websocket"} = lists:keyfind('Upgrade', 1, Headers), + {"sec-websocket-accept", "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="} + = lists:keyfind("sec-websocket-accept", 1, Headers), + {ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), + {error, closed} = gen_tcp:recv(Socket, 0, 6000), + ok. + websocket_headers({ok, http_eoh, Rest}, Acc) -> [Acc, Rest]; websocket_headers({ok, {http_header, _I, Key, _R, Value}, Rest}, Acc) -> @@ -291,3 +347,9 @@ http_200(Config) -> http_404(Config) -> {ok, {{"HTTP/1.1", 404, "Not Found"}, _Headers, _Body}} = httpc:request(build_url("/not/found", Config)). + +%% misc. + +http_10_hostless(Config) -> + Packet = "GET / HTTP/1.0\r\n\r\n", + {Packet, 200} = raw_req(Packet, Config). diff --git a/test/ws_timeout_hibernate_handler.erl b/test/ws_timeout_hibernate_handler.erl new file mode 100644 index 0000000..777948a --- /dev/null +++ b/test/ws_timeout_hibernate_handler.erl @@ -0,0 +1,29 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(ws_timeout_hibernate_handler). +-behaviour(cowboy_http_handler). +-behaviour(cowboy_http_websocket_handler). +-export([init/3, handle/2, terminate/2]). +-export([websocket_init/3, websocket_handle/3, + websocket_info/3, websocket_terminate/3]). + +init(_Any, _Req, _Opts) -> + {upgrade, protocol, cowboy_http_websocket}. + +handle(_Req, _State) -> + exit(badarg). + +terminate(_Req, _State) -> + exit(badarg). + +websocket_init(_TransportName, Req, _Opts) -> + {ok, Req, undefined, 1000, hibernate}. + +websocket_handle(_Frame, Req, State) -> + {ok, Req, State, hibernate}. + +websocket_info(_Info, Req, State) -> + {ok, Req, State, hibernate}. + +websocket_terminate(_Reason, _Req, _State) -> + ok. |