0
Fork 0
mirror of https://github.com/ninenines/cowboy.git synced 2025-07-14 12:20:24 +00:00

Completely remove SPDY

This commit is contained in:
Loïc Hoguin 2016-03-06 17:48:35 +01:00
parent b370442a63
commit 7bdd710849
15 changed files with 21 additions and 766 deletions

View file

@ -24,8 +24,9 @@ Sponsors
The project is currently sponsored by The project is currently sponsored by
[Sameroom](https://sameroom.io). [Sameroom](https://sameroom.io).
The SPDY implementation was sponsored by The original SPDY implementation was sponsored by
[LeoFS Cloud Storage](http://leo-project.net/leofs/). [LeoFS Cloud Storage](http://leo-project.net/leofs/).
It has since been superseded by HTTP/2.
Online documentation Online documentation
-------------------- --------------------

View file

@ -33,7 +33,7 @@ capitalize_hook(Status, Headers, Body, Req) ->
cowboy_req:reply(Status, Headers2, Body, Req). cowboy_req:reply(Status, Headers2, Body, Req).
---- ----
Note that SPDY clients do not have that particular issue Note that HTTP/2 clients do not have that particular issue
because the specification explicitly says all headers are because the specification explicitly says all headers are
lowercase, unlike HTTP which allows any case but treats lowercase, unlike HTTP which allows any case but treats
them as case insensitive. them as case insensitive.

View file

@ -106,7 +106,7 @@ new messages, and perform the operations required by only activating
the required parts of the system. the required parts of the system.
The more recent Web technologies, like Websocket of course, but also The more recent Web technologies, like Websocket of course, but also
SPDY and HTTP/2.0, are all fully asynchronous protocols. The concept HTTP/2.0, are all fully asynchronous protocols. The concept
of requests and responses is retained of course, but anything could of requests and responses is retained of course, but anything could
be sent in between, by both the client or the browser, and the be sent in between, by both the client or the browser, and the
responses could also be received in a completely different order. responses could also be received in a completely different order.

View file

@ -4,7 +4,7 @@
Cowboy is a small, fast and modular HTTP server written in Erlang. Cowboy is a small, fast and modular HTTP server written in Erlang.
Cowboy aims to provide a complete HTTP stack, including its derivatives Cowboy aims to provide a complete HTTP stack, including its derivatives
SPDY, Websocket and REST. Cowboy currently supports HTTP/1.0, HTTP/1.1, Websocket and REST. Cowboy currently supports HTTP/1.0, HTTP/1.1, HTTP/2,
Websocket (all implemented drafts + standard) and Webmachine-based REST. Websocket (all implemented drafts + standard) and Webmachine-based REST.
Cowboy is a high quality project. It has a small code base, is very Cowboy is a high quality project. It has a small code base, is very

View file

@ -180,35 +180,21 @@ A Websocket connection can be used to transfer any kind of data,
small or big, text or binary. Because of this Websocket is small or big, text or binary. Because of this Websocket is
sometimes used for communication between systems. sometimes used for communication between systems.
=== SPDY === HTTP/2
SPDY is an attempt to reduce page loading time by opening a HTTP/2 is an attempt to reduce page loading time by opening a
single connection per server, keeping it open for subsequent single connection per server, keeping it open for subsequent
requests, and also by compressing the HTTP headers to reduce requests, and also by compressing the HTTP headers to reduce
the size of requests. the size of requests.
SPDY is compatible with HTTP/1.1 semantics, and is actually HTTP/2 is compatible with HTTP/1.1 semantics, and is actually
just a different way of performing HTTP requests and responses, just a different way of performing HTTP requests and responses,
by using binary frames instead of a text-based protocol. by using binary frames instead of a text-based protocol.
SPDY also allows the server to send extra responses following HTTP/2 also allows the server to send extra responses following
a request. This is meant to allow sending the resources a request. This is meant to allow sending the resources
associated with the request before the client requests them, associated with the request before the client requests them,
saving latency when loading websites. saving latency when loading websites.
SPDY is an experiment that has proven successful and is used Browsers make use of TLS Application-Layer Protocol Negotiation
as the basis for the HTTP/2.0 standard. extension to upgrade to an HTTP/2 connection seamlessly if the
server supports it.
Browsers make use of TLS Next Protocol Negotiation to upgrade
to a SPDY connection seamlessly if the protocol supports it.
The protocol itself has a few shortcomings which are being
fixed in HTTP/2.0.
=== HTTP/2.0
HTTP/2.0 is the long-awaited update to the HTTP/1.1 protocol.
It is based on SPDY although a lot has been improved at the
time of writing.
HTTP/2.0 is an asynchronous two-ways communication channel
between two endpoints.

View file

@ -55,7 +55,7 @@ HTTP/1.1 allows the client to request that the server
keeps the connection alive. This mechanism is described keeps the connection alive. This mechanism is described
in the next section. in the next section.
SPDY is designed to allow sending multiple requests HTTP/2 is designed to allow sending multiple requests
asynchronously on the same connection. Details on what asynchronously on the same connection. Details on what
this means for your application is described in this this means for your application is described in this
chapter. chapter.
@ -126,9 +126,9 @@ static files for example.
This is handled automatically by the server. This is handled automatically by the server.
=== Asynchronous requests (SPDY) === Asynchronous requests (HTTP/2)
In SPDY, the client can send a request at any time. In HTTP/2, the client can send a request at any time.
And the server can send a response at any time too. And the server can send a response at any time too.
This means for example that the client does not need This means for example that the client does not need
@ -142,7 +142,7 @@ Cowboy creates a new process for each request, and these
processes are managed by another process that handles the processes are managed by another process that handles the
connection itself. connection itself.
SPDY servers may also decide to send resources to the HTTP/2 servers may also decide to send resources to the
client before the client requests them. This is especially client before the client requests them. This is especially
useful for sending static files associated with the HTML useful for sending static files associated with the HTML
page requested, as this reduces the latency of the overall page requested, as this reduces the latency of the overall

View file

@ -67,16 +67,6 @@ ProtoOpts = cowboy_protocol:opts():: HTTP protocol options.
Start listening for HTTPS connections. Returns the pid for this Start listening for HTTPS connections. Returns the pid for this
listener's supervisor. listener's supervisor.
=== start_spdy(Ref, NbAcceptors, TransOpts, ProtoOpts) -> {ok, pid()}
Ref = ranch:ref():: Listener name.
NbAcceptors = non_neg_integer():: Number of acceptor processes.
TransOpts = ranch_ssl:opts():: SSL transport options.
ProtoOpts = cowboy_spdy:opts():: SPDY protocol options.
Start listening for SPDY connections. Returns the pid for this
listener's supervisor.
=== stop_listener(Ref) -> ok | {error, not_found} === stop_listener(Ref) -> ok | {error, not_found}
Ref = ranch:ref():: Listener name. Ref = ranch:ref():: Listener name.

View file

@ -1,42 +0,0 @@
= cowboy_spdy(3)
== Name
cowboy_spdy - SPDY protocol
== Description
The `cowboy_spdy` module implements SPDY/3 as a Ranch protocol.
== Types
=== opts() = [Option]
[source,erlang]
----
Option = {env, cowboy_middleware:env()}
| {middlewares, [module()]}
| {onresponse, cowboy:onresponse_fun()}
----
Configuration for the SPDY protocol handler.
This configuration is passed to Cowboy when starting listeners
using the `cowboy:start_spdy/4` function.
It can be updated without restarting listeners using the
Ranch functions `ranch:get_protocol_options/1` and
`ranch:set_protocol_options/2`.
== Option descriptions
The default value is given next to the option name.
env ([{listener, Ref}])::
Initial middleware environment.
middlewares ([cowboy_router, cowboy_handler])::
List of middlewares to execute for every requests.
onresponse (undefined)::
Fun called every time a response is sent.

View file

@ -1,7 +1,7 @@
{application, cowboy, [ {application, cowboy, [
{description, "Small, fast, modular HTTP server."}, {description, "Small, fast, modular HTTP server."},
{vsn, "2.0.0-pre.2"}, {vsn, "2.0.0-pre.2"},
{modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_clear','cowboy_clock','cowboy_constraints','cowboy_handler','cowboy_http','cowboy_http2','cowboy_loop','cowboy_middleware','cowboy_protocol','cowboy_req','cowboy_rest','cowboy_router','cowboy_spdy','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_websocket']}, {modules, ['cowboy','cowboy_app','cowboy_bstr','cowboy_clear','cowboy_clock','cowboy_constraints','cowboy_handler','cowboy_http','cowboy_http2','cowboy_loop','cowboy_middleware','cowboy_req','cowboy_rest','cowboy_router','cowboy_static','cowboy_stream','cowboy_stream_h','cowboy_sub_protocol','cowboy_sup','cowboy_tls','cowboy_websocket']},
{registered, [cowboy_sup,cowboy_clock]}, {registered, [cowboy_sup,cowboy_clock]},
{applications, [kernel,stdlib,crypto,cowlib,ranch]}, {applications, [kernel,stdlib,crypto,cowlib,ranch]},
{mod, {cowboy_app, []}} {mod, {cowboy_app, []}}

View file

@ -52,8 +52,8 @@ start_tls(Ref, NbAcceptors, TransOpts0, ProtoOpts)
when is_integer(NbAcceptors), NbAcceptors > 0 -> when is_integer(NbAcceptors), NbAcceptors > 0 ->
TransOpts = [ TransOpts = [
connection_type(ProtoOpts), connection_type(ProtoOpts),
{next_protocols_advertised, [<<"h2">>, <<"spdy/3">>, <<"http/1.1">>]}, {next_protocols_advertised, [<<"h2">>, <<"http/1.1">>]},
{alpn_preferred_protocols, [<<"h2">>, <<"spdy/3">>, <<"http/1.1">>]} {alpn_preferred_protocols, [<<"h2">>, <<"http/1.1">>]}
|TransOpts0], |TransOpts0],
ranch:start_listener(Ref, NbAcceptors, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts). ranch:start_listener(Ref, NbAcceptors, ranch_ssl, TransOpts, cowboy_tls, ProtoOpts).

View file

@ -769,11 +769,6 @@ response_headers(Headers, Req) ->
% %% We stream the response body until we close the connection. % %% We stream the response body until we close the connection.
% RespConn = close, % RespConn = close,
% {RespType, Req2} = if % {RespType, Req2} = if
% Transport =:= cowboy_spdy ->
% response(Status, Headers, RespHeaders, [
% {<<"date">>, cowboy_clock:rfc1123()},
% {<<"server">>, <<"Cowboy">>}
% ], stream, Req);
% true -> % true ->
% response(Status, Headers, RespHeaders, [ % response(Status, Headers, RespHeaders, [
% {<<"connection">>, <<"close">>}, % {<<"connection">>, <<"close">>},
@ -896,9 +891,6 @@ chunk(Data, #{pid := Pid, streamid := StreamID}) ->
%% If ever made public, need to send nothing if HEAD. %% If ever made public, need to send nothing if HEAD.
-spec last_chunk(Req) -> Req when Req::req(). -spec last_chunk(Req) -> Req when Req::req().
last_chunk(Req=#http_req{socket=Socket, transport=cowboy_spdy}) ->
_ = cowboy_spdy:stream_close(Socket),
Req#http_req{resp_state=done};
last_chunk(Req=#http_req{socket=Socket, transport=Transport}) -> last_chunk(Req=#http_req{socket=Socket, transport=Transport}) ->
_ = Transport:send(Socket, <<"0\r\n\r\n">>), _ = Transport:send(Socket, <<"0\r\n\r\n">>),
Req#http_req{resp_state=done}. Req#http_req{resp_state=done}.
@ -1028,15 +1020,6 @@ to_list(Req) ->
%-spec chunked_response(cowboy:http_status(), cowboy:http_headers(), Req) -> %-spec chunked_response(cowboy:http_status(), cowboy:http_headers(), Req) ->
% {normal | hook, Req} when Req::req(). % {normal | hook, Req} when Req::req().
%chunked_response(Status, Headers, Req=#http_req{ %chunked_response(Status, Headers, Req=#http_req{
% transport=cowboy_spdy, resp_state=waiting,
% resp_headers=RespHeaders}) ->
% {RespType, Req2} = response(Status, Headers, RespHeaders, [
% {<<"date">>, cowboy_clock:rfc1123()},
% {<<"server">>, <<"Cowboy">>}
% ], stream, Req),
% {RespType, Req2#http_req{resp_state=chunks,
% resp_headers=[], resp_body= <<>>}};
%chunked_response(Status, Headers, Req=#http_req{
% version=Version, connection=Connection, % version=Version, connection=Connection,
% resp_state=RespState, resp_headers=RespHeaders}) % resp_state=RespState, resp_headers=RespHeaders})
% when RespState =:= waiting; RespState =:= waiting_stream -> % when RespState =:= waiting; RespState =:= waiting_stream ->
@ -1094,14 +1077,6 @@ response(Status, Headers, RespHeaders, DefaultHeaders, Body, Req=#http_req{
end end
end, end,
ReplyType = case Req2#http_req.resp_state of ReplyType = case Req2#http_req.resp_state of
waiting when Transport =:= cowboy_spdy, Body =:= stream ->
cowboy_spdy:stream_reply(Socket, status(Status2), FullHeaders2),
ReqPid ! {?MODULE, resp_sent},
normal;
waiting when Transport =:= cowboy_spdy ->
cowboy_spdy:reply(Socket, status(Status2), FullHeaders2, Body),
ReqPid ! {?MODULE, resp_sent},
normal;
RespState when RespState =:= waiting; RespState =:= waiting_stream -> RespState when RespState =:= waiting; RespState =:= waiting_stream ->
HTTPVer = atom_to_binary(Version, latin1), HTTPVer = atom_to_binary(Version, latin1),
StatusLine = << HTTPVer/binary, " ", StatusLine = << HTTPVer/binary, " ",

View file

@ -1,488 +0,0 @@
%% Copyright (c) 2013-2014, 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(cowboy_spdy).
%% API.
-export([start_link/4]).
%% Internal.
-export([init/5]).
-export([system_continue/3]).
-export([system_terminate/4]).
-export([system_code_change/4]).
%% Internal request process.
-export([request_init/10]).
-export([resume/5]).
-export([reply/4]).
-export([stream_reply/3]).
-export([stream_data/2]).
-export([stream_close/1]).
%% Internal transport functions.
-export([name/0]).
-export([messages/0]).
-export([recv/3]).
-export([send/2]).
-export([sendfile/2]).
-export([setopts/2]).
-type streamid() :: non_neg_integer().
-type socket() :: {pid(), streamid()}.
-record(child, {
streamid :: streamid(),
pid :: pid(),
input = nofin :: fin | nofin,
in_buffer = <<>> :: binary(),
is_recv = false :: false | {active, socket(), pid()}
| {passive, socket(), pid(), non_neg_integer(), reference()},
output = nofin :: fin | nofin
}).
-record(state, {
parent = undefined :: pid(),
socket :: inet:socket(),
transport :: module(),
buffer = <<>> :: binary(),
middlewares :: [module()],
env :: cowboy_middleware:env(),
onresponse :: cowboy:onresponse_fun(),
peer :: {inet:ip_address(), inet:port_number()},
zdef :: zlib:zstream(),
zinf :: zlib:zstream(),
last_streamid = 0 :: streamid(),
children = [] :: [#child{}]
}).
-type opts() :: [{env, cowboy_middleware:env()}
| {middlewares, [module()]}
| {onresponse, cowboy:onresponse_fun()}].
-export_type([opts/0]).
%% API.
-spec start_link(any(), inet:socket(), module(), any()) -> {ok, pid()}.
start_link(Ref, Socket, Transport, Opts) ->
proc_lib:start_link(?MODULE, init,
[self(), Ref, Socket, Transport, Opts]).
%% Internal.
%% Faster alternative to proplists:get_value/3.
get_value(Key, Opts, Default) ->
case lists:keyfind(Key, 1, Opts) of
{_, Value} -> Value;
_ -> Default
end.
-spec init(pid(), ranch:ref(), inet:socket(), module(), opts()) -> ok.
init(Parent, Ref, Socket, Transport, Opts) ->
process_flag(trap_exit, true),
ok = proc_lib:init_ack(Parent, {ok, self()}),
{ok, Peer} = Transport:peername(Socket),
Middlewares = get_value(middlewares, Opts, [cowboy_router, cowboy_handler]),
Env = [{listener, Ref}|get_value(env, Opts, [])],
OnResponse = get_value(onresponse, Opts, undefined),
Zdef = cow_spdy:deflate_init(),
Zinf = cow_spdy:inflate_init(),
ok = ranch:accept_ack(Ref),
loop(#state{parent=Parent, socket=Socket, transport=Transport,
middlewares=Middlewares, env=Env,
onresponse=OnResponse, peer=Peer, zdef=Zdef, zinf=Zinf}).
loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
buffer=Buffer, children=Children}) ->
{OK, Closed, Error} = Transport:messages(),
Transport:setopts(Socket, [{active, once}]),
receive
{OK, Socket, Data} ->
parse_frame(State, << Buffer/binary, Data/binary >>);
{Closed, Socket} ->
terminate(State);
{Error, Socket, _Reason} ->
terminate(State);
{recv, FromSocket = {Pid, StreamID}, FromPid, Length, Timeout}
when Pid =:= self() ->
Child = #child{in_buffer=InBuffer, is_recv=false}
= get_child(StreamID, State),
if
Length =:= 0, InBuffer =/= <<>> ->
FromPid ! {recv, FromSocket, {ok, InBuffer}},
loop(replace_child(Child#child{in_buffer= <<>>}, State));
byte_size(InBuffer) >= Length ->
<< Data:Length/binary, Rest/bits >> = InBuffer,
FromPid ! {recv, FromSocket, {ok, Data}},
loop(replace_child(Child#child{in_buffer=Rest}, State));
true ->
TRef = erlang:send_after(Timeout, self(),
{recv_timeout, FromSocket}),
loop(replace_child(Child#child{
is_recv={passive, FromSocket, FromPid, Length, TRef}},
State))
end;
{recv_timeout, {Pid, StreamID}}
when Pid =:= self() ->
Child = #child{is_recv={passive, FromSocket, FromPid, _, _}}
= get_child(StreamID, State),
FromPid ! {recv, FromSocket, {error, timeout}},
loop(replace_child(Child, State));
{reply, {Pid, StreamID}, Status, Headers}
when Pid =:= self() ->
Child = #child{output=nofin} = get_child(StreamID, State),
syn_reply(State, StreamID, true, Status, Headers),
loop(replace_child(Child#child{output=fin}, State));
{reply, {Pid, StreamID}, Status, Headers, Body}
when Pid =:= self() ->
Child = #child{output=nofin} = get_child(StreamID, State),
syn_reply(State, StreamID, false, Status, Headers),
data(State, StreamID, true, Body),
loop(replace_child(Child#child{output=fin}, State));
{stream_reply, {Pid, StreamID}, Status, Headers}
when Pid =:= self() ->
#child{output=nofin} = get_child(StreamID, State),
syn_reply(State, StreamID, false, Status, Headers),
loop(State);
{stream_data, {Pid, StreamID}, Data}
when Pid =:= self() ->
#child{output=nofin} = get_child(StreamID, State),
data(State, StreamID, false, Data),
loop(State);
{stream_close, {Pid, StreamID}}
when Pid =:= self() ->
Child = #child{output=nofin} = get_child(StreamID, State),
data(State, StreamID, true, <<>>),
loop(replace_child(Child#child{output=fin}, State));
{sendfile, {Pid, StreamID}, Filepath}
when Pid =:= self() ->
Child = #child{output=nofin} = get_child(StreamID, State),
data_from_file(State, StreamID, Filepath),
loop(replace_child(Child#child{output=fin}, State));
{active, FromSocket = {Pid, StreamID}, FromPid} when Pid =:= self() ->
Child = #child{in_buffer=InBuffer, is_recv=false}
= get_child(StreamID, State),
case InBuffer of
<<>> ->
loop(replace_child(Child#child{
is_recv={active, FromSocket, FromPid}}, State));
_ ->
FromPid ! {spdy, FromSocket, InBuffer},
loop(replace_child(Child#child{in_buffer= <<>>}, State))
end;
{passive, FromSocket = {Pid, StreamID}, FromPid} when Pid =:= self() ->
Child = #child{is_recv=IsRecv} = get_child(StreamID, State),
%% Make sure we aren't in the middle of a recv call.
case IsRecv of false -> ok; {active, FromSocket, FromPid} -> ok end,
loop(replace_child(Child#child{is_recv=false}, State));
{'EXIT', Parent, Reason} ->
exit(Reason);
{'EXIT', Pid, _} ->
%% @todo Report the error if any.
loop(delete_child(Pid, State));
{system, From, Request} ->
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], State);
%% Calls from the supervisor module.
{'$gen_call', {To, Tag}, which_children} ->
Workers = [{?MODULE, Pid, worker, [?MODULE]}
|| #child{pid=Pid} <- Children],
To ! {Tag, Workers},
loop(State);
{'$gen_call', {To, Tag}, count_children} ->
NbChildren = length(Children),
Counts = [{specs, 1}, {active, NbChildren},
{supervisors, 0}, {workers, NbChildren}],
To ! {Tag, Counts},
loop(State);
{'$gen_call', {To, Tag}, _} ->
To ! {Tag, {error, ?MODULE}},
loop(State)
after 60000 ->
goaway(State, ok),
terminate(State)
end.
-spec system_continue(_, _, #state{}) -> ok.
system_continue(_, _, State) ->
loop(State).
-spec system_terminate(any(), _, _, _) -> no_return().
system_terminate(Reason, _, _, _) ->
exit(Reason).
-spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::#state{}.
system_code_change(Misc, _, _, _) ->
{ok, Misc}.
parse_frame(State=#state{zinf=Zinf}, Data) ->
case cow_spdy:split(Data) of
{true, Frame, Rest} ->
P = cow_spdy:parse(Frame, Zinf),
case handle_frame(State#state{buffer = Rest}, P) of
error ->
terminate(State);
State2 ->
parse_frame(State2, Rest)
end;
false ->
loop(State#state{buffer=Data})
end.
%% FLAG_UNIDIRECTIONAL can only be set by the server.
handle_frame(State, {syn_stream, StreamID, _, _, true,
_, _, _, _, _, _, _}) ->
rst_stream(State, StreamID, protocol_error),
State;
%% We do not support Associated-To-Stream-ID.
handle_frame(State, {syn_stream, StreamID, AssocToStreamID,
_, _, _, _, _, _, _, _, _}) when AssocToStreamID =/= 0 ->
rst_stream(State, StreamID, internal_error),
State;
%% SYN_STREAM.
%%
%% Erlang does not allow us to control the priority of processes
%% so we ignore that value entirely.
handle_frame(State=#state{middlewares=Middlewares, env=Env,
onresponse=OnResponse, peer=Peer},
{syn_stream, StreamID, _, IsFin, _, _,
Method, _, Host, Path, Version, Headers}) ->
Pid = spawn_link(?MODULE, request_init, [
{self(), StreamID}, Peer, OnResponse,
Env, Middlewares, Method, Host, Path, Version, Headers
]),
new_child(State, StreamID, Pid, IsFin);
%% RST_STREAM.
handle_frame(State, {rst_stream, StreamID, Status}) ->
error_logger:error_msg("Received RST_STREAM frame ~p ~p",
[StreamID, Status]),
%% @todo Stop StreamID.
State;
%% PING initiated by the server; ignore, we don't send any.
handle_frame(State, {ping, PingID}) when PingID rem 2 =:= 0 ->
error_logger:error_msg("Ignored PING control frame: ~p~n", [PingID]),
State;
%% PING initiated by the client; send it back.
handle_frame(State=#state{socket=Socket, transport=Transport},
{ping, PingID}) ->
Transport:send(Socket, cow_spdy:ping(PingID)),
State;
%% Data received for a stream.
handle_frame(State, {data, StreamID, IsFin, Data}) ->
Child = #child{input=nofin, in_buffer=Buffer, is_recv=IsRecv}
= get_child(StreamID, State),
Data2 = << Buffer/binary, Data/binary >>,
IsFin2 = if IsFin -> fin; true -> nofin end,
Child2 = case IsRecv of
{active, FromSocket, FromPid} ->
FromPid ! {spdy, FromSocket, Data},
Child#child{input=IsFin2, is_recv=false};
{passive, FromSocket, FromPid, 0, TRef} ->
FromPid ! {recv, FromSocket, {ok, Data2}},
cancel_recv_timeout(StreamID, TRef),
Child#child{input=IsFin2, in_buffer= <<>>, is_recv=false};
{passive, FromSocket, FromPid, Length, TRef}
when byte_size(Data2) >= Length ->
<< Data3:Length/binary, Rest/bits >> = Data2,
FromPid ! {recv, FromSocket, {ok, Data3}},
cancel_recv_timeout(StreamID, TRef),
Child#child{input=IsFin2, in_buffer=Rest, is_recv=false};
_ ->
Child#child{input=IsFin2, in_buffer=Data2}
end,
replace_child(Child2, State);
%% General error, can't recover.
handle_frame(State, {error, badprotocol}) ->
goaway(State, protocol_error),
error;
%% Ignore all other frames for now.
handle_frame(State, Frame) ->
error_logger:error_msg("Ignored frame ~p", [Frame]),
State.
cancel_recv_timeout(StreamID, TRef) ->
_ = erlang:cancel_timer(TRef),
receive
{recv_timeout, {Pid, StreamID}}
when Pid =:= self() ->
ok
after 0 ->
ok
end.
%% @todo We must wait for the children to finish here,
%% but only up to N milliseconds. Then we shutdown.
terminate(_State) ->
ok.
syn_reply(#state{socket=Socket, transport=Transport, zdef=Zdef},
StreamID, IsFin, Status, Headers) ->
Transport:send(Socket, cow_spdy:syn_reply(Zdef, StreamID, IsFin,
Status, <<"HTTP/1.1">>, Headers)).
rst_stream(#state{socket=Socket, transport=Transport}, StreamID, Status) ->
Transport:send(Socket, cow_spdy:rst_stream(StreamID, Status)).
goaway(#state{socket=Socket, transport=Transport, last_streamid=LastStreamID},
Status) ->
Transport:send(Socket, cow_spdy:goaway(LastStreamID, Status)).
data(#state{socket=Socket, transport=Transport}, StreamID, IsFin, Data) ->
Transport:send(Socket, cow_spdy:data(StreamID, IsFin, Data)).
data_from_file(#state{socket=Socket, transport=Transport},
StreamID, Filepath) ->
{ok, IoDevice} = file:open(Filepath, [read, binary, raw]),
data_from_file(Socket, Transport, StreamID, IoDevice).
data_from_file(Socket, Transport, StreamID, IoDevice) ->
case file:read(IoDevice, 16#1fff) of
eof ->
_ = Transport:send(Socket, cow_spdy:data(StreamID, true, <<>>)),
ok;
{ok, Data} ->
case Transport:send(Socket, cow_spdy:data(StreamID, false, Data)) of
ok ->
data_from_file(Socket, Transport, StreamID, IoDevice);
{error, _} ->
ok
end
end.
%% Children.
new_child(State=#state{children=Children}, StreamID, Pid, IsFin) ->
IsFin2 = if IsFin -> fin; true -> nofin end,
State#state{last_streamid=StreamID,
children=[#child{streamid=StreamID,
pid=Pid, input=IsFin2}|Children]}.
get_child(StreamID, #state{children=Children}) ->
lists:keyfind(StreamID, #child.streamid, Children).
replace_child(Child=#child{streamid=StreamID},
State=#state{children=Children}) ->
Children2 = lists:keyreplace(StreamID, #child.streamid, Children, Child),
State#state{children=Children2}.
delete_child(Pid, State=#state{children=Children}) ->
Children2 = lists:keydelete(Pid, #child.pid, Children),
State#state{children=Children2}.
%% Request process.
-spec request_init(socket(), {inet:ip_address(), inet:port_number()},
cowboy:onresponse_fun(), cowboy_middleware:env(), [module()],
binary(), binary(), binary(), binary(), [{binary(), binary()}])
-> ok.
request_init(FakeSocket, Peer, OnResponse,
Env, Middlewares, Method, Host, Path, Version, Headers) ->
{Host2, Port} = cow_http_hd:parse_host(Host),
{Path2, Qs} = cow_http:parse_fullpath(Path),
Version2 = cow_http:parse_version(Version),
Req = cowboy_req:new(FakeSocket, ?MODULE, Peer,
Method, Path2, Qs, Version2, Headers,
Host2, Port, <<>>, true, false, OnResponse),
execute(Req, Env, Middlewares).
-spec execute(cowboy_req:req(), cowboy_middleware:env(), [module()])
-> ok.
execute(Req, _, []) ->
cowboy_req:ensure_response(Req, 204);
execute(Req, Env, [Middleware|Tail]) ->
case Middleware:execute(Req, Env) of
{ok, Req2, Env2} ->
execute(Req2, Env2, Tail);
{suspend, Module, Function, Args} ->
erlang:hibernate(?MODULE, resume,
[Env, Tail, Module, Function, Args]);
{stop, Req2} ->
cowboy_req:ensure_response(Req2, 204)
end.
-spec resume(cowboy_middleware:env(), [module()],
module(), module(), [any()]) -> ok.
resume(Env, Tail, Module, Function, Args) ->
case apply(Module, Function, Args) of
{ok, Req2, Env2} ->
execute(Req2, Env2, Tail);
{suspend, Module2, Function2, Args2} ->
erlang:hibernate(?MODULE, resume,
[Env, Tail, Module2, Function2, Args2]);
{stop, Req2} ->
cowboy_req:ensure_response(Req2, 204)
end.
%% Reply functions used by cowboy_req.
-spec reply(socket(), binary(), cowboy:http_headers(), iodata()) -> ok.
reply(Socket = {Pid, _}, Status, Headers, Body) ->
_ = case iolist_size(Body) of
0 -> Pid ! {reply, Socket, Status, Headers};
_ -> Pid ! {reply, Socket, Status, Headers, Body}
end,
ok.
-spec stream_reply(socket(), binary(), cowboy:http_headers()) -> ok.
stream_reply(Socket = {Pid, _}, Status, Headers) ->
_ = Pid ! {stream_reply, Socket, Status, Headers},
ok.
-spec stream_data(socket(), iodata()) -> ok.
stream_data(Socket = {Pid, _}, Data) ->
_ = Pid ! {stream_data, Socket, Data},
ok.
-spec stream_close(socket()) -> ok.
stream_close(Socket = {Pid, _}) ->
_ = Pid ! {stream_close, Socket},
ok.
%% Internal transport functions.
-spec name() -> spdy.
name() ->
spdy.
-spec messages() -> {spdy, spdy_closed, spdy_error}.
messages() ->
{spdy, spdy_closed, spdy_error}.
-spec recv(socket(), non_neg_integer(), timeout())
-> {ok, binary()} | {error, timeout}.
recv(Socket = {Pid, _}, Length, Timeout) ->
_ = Pid ! {recv, Socket, self(), Length, Timeout},
receive
{recv, Socket, Ret} ->
Ret
end.
-spec send(socket(), iodata()) -> ok.
send(Socket, Data) ->
stream_data(Socket, Data).
%% We don't wait for the result of the actual sendfile call,
%% therefore we can't know how much was actually sent.
%% This isn't a problem as we don't use this value in Cowboy.
-spec sendfile(socket(), file:name_all()) -> {ok, undefined}.
sendfile(Socket = {Pid, _}, Filepath) ->
_ = Pid ! {sendfile, Socket, Filepath},
{ok, undefined}.
-spec setopts({pid(), _}, list()) -> ok.
setopts(Socket = {Pid, _}, [{active, once}]) ->
_ = Pid ! {active, Socket, self()},
ok;
setopts(Socket = {Pid, _}, [{active, false}]) ->
_ = Pid ! {passive, Socket, self()},
ok.

View file

@ -29,9 +29,6 @@ init(Parent, Ref, Socket, Transport, Opts) ->
case ssl:negotiated_protocol(Socket) of case ssl:negotiated_protocol(Socket) of
{ok, <<"h2">>} -> {ok, <<"h2">>} ->
init(Parent, Ref, Socket, Transport, Opts, cowboy_http2); init(Parent, Ref, Socket, Transport, Opts, cowboy_http2);
%% @todo Implement cowboy_spdy and cowboy_http.
{ok, <<"spdy/3">>} ->
init(Parent, Ref, Socket, Transport, Opts, cowboy_spdy);
_ -> %% http/1.1 or no protocol negotiated. _ -> %% http/1.1 or no protocol negotiated.
init(Parent, Ref, Socket, Transport, Opts, cowboy_http) init(Parent, Ref, Socket, Transport, Opts, cowboy_http)
end. end.

View file

@ -30,40 +30,28 @@ init_https(Ref, ProtoOpts, Config) ->
Port = ranch:get_port(Ref), Port = ranch:get_port(Ref),
[{type, ssl}, {protocol, http}, {port, Port}, {opts, Opts}|Config]. [{type, ssl}, {protocol, http}, {port, Port}, {opts, Opts}|Config].
init_spdy(Ref, ProtoOpts, Config) ->
Opts = ct_helper:get_certs_from_ets(),
{ok, _} = cowboy:start_tls(Ref, 100, Opts ++ [{port, 0}], ProtoOpts),
Port = ranch:get_port(Ref),
[{type, ssl}, {protocol, spdy}, {port, Port}, {opts, Opts}|Config].
%% Common group of listeners used by most suites. %% Common group of listeners used by most suites.
common_all() -> common_all() ->
[ [
{group, http}, {group, http},
{group, https}, {group, https},
{group, spdy},
{group, http_compress}, {group, http_compress},
{group, https_compress}, {group, https_compress}
{group, spdy_compress}
]. ].
common_groups(Tests) -> common_groups(Tests) ->
[ [
{http, [parallel], Tests}, {http, [parallel], Tests},
{https, [parallel], Tests}, {https, [parallel], Tests},
{spdy, [parallel], Tests},
{http_compress, [parallel], Tests}, {http_compress, [parallel], Tests},
{https_compress, [parallel], Tests}, {https_compress, [parallel], Tests}
{spdy_compress, [parallel], Tests}
]. ].
init_common_groups(Name = http, Config, Mod) -> init_common_groups(Name = http, Config, Mod) ->
init_http(Name, #{env => #{dispatch => Mod:init_dispatch(Config)}}, Config); init_http(Name, #{env => #{dispatch => Mod:init_dispatch(Config)}}, Config);
init_common_groups(Name = https, Config, Mod) -> init_common_groups(Name = https, Config, Mod) ->
init_https(Name, #{env => #{dispatch => Mod:init_dispatch(Config)}}, Config); init_https(Name, #{env => #{dispatch => Mod:init_dispatch(Config)}}, Config);
init_common_groups(Name = spdy, Config, Mod) ->
init_https(Name, #{env => #{dispatch => Mod:init_dispatch(Config)}}, Config);
init_common_groups(Name = http_compress, Config, Mod) -> init_common_groups(Name = http_compress, Config, Mod) ->
init_http(Name, #{ init_http(Name, #{
env => #{dispatch => Mod:init_dispatch(Config)}, env => #{dispatch => Mod:init_dispatch(Config)},
@ -73,11 +61,6 @@ init_common_groups(Name = https_compress, Config, Mod) ->
init_https(Name, #{ init_https(Name, #{
env => #{dispatch => Mod:init_dispatch(Config)}, env => #{dispatch => Mod:init_dispatch(Config)},
compress => true compress => true
}, Config);
init_common_groups(Name = spdy_compress, Config, Mod) ->
init_spdy(Name, #{
env => #{dispatch => Mod:init_dispatch(Config)},
compress => true
}, Config). }, Config).
%% Support functions for testing using Gun. %% Support functions for testing using Gun.

View file

@ -1,147 +0,0 @@
%% Copyright (c) 2013-2014, 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(spdy_SUITE).
-compile(export_all).
-import(ct_helper, [config/2]).
-import(cowboy_test, [gun_open/1]).
-import(cowboy_test, [raw_open/1]).
-import(cowboy_test, [raw_send/2]).
%% ct.
all() ->
[{group, spdy}].
groups() ->
[{spdy, [], ct_helper:all(?MODULE)}].
init_per_suite(Config) ->
case proplists:get_value(ssl_app, ssl:versions()) of
Version when Version < "5.2.1" ->
{skip, "No NPN support in SSL application."};
_ ->
Dir = config(priv_dir, Config) ++ "/static",
ct_helper:create_static_dir(Dir),
[{static_dir, Dir}|Config]
end.
end_per_suite(Config) ->
ct_helper:delete_static_dir(config(static_dir, Config)).
init_per_group(Name, Config) ->
cowboy_test:init_spdy(Name, [
{env, [{dispatch, init_dispatch(Config)}]}
], Config).
end_per_group(Name, _) ->
cowboy:stop_listener(Name).
%% Dispatch configuration.
init_dispatch(Config) ->
cowboy_router:compile([
{"localhost", [
{"/static/[...]", cowboy_static,
{dir, config(static_dir, Config)}},
{"/echo/body", http_echo_body, []},
{"/chunked", http_chunked, []},
{"/", http_handler, []}
]}
]).
%% Convenience functions.
do_get(ConnPid, Host, Path) ->
StreamRef = gun:get(ConnPid, Path, [{<<"host">>, Host}]),
{response, IsFin, Status, _} = gun:await(ConnPid, StreamRef),
{IsFin, Status}.
%% Tests.
check_status(Config) ->
Tests = [
{200, nofin, "localhost", "/"},
{200, nofin, "localhost", "/chunked"},
{200, nofin, "localhost", "/static/style.css"},
{400, fin, "bad-host", "/"},
{400, fin, "localhost", "bad-path"},
{404, fin, "localhost", "/this/path/does/not/exist"}
],
ConnPid = gun_open(Config),
_ = [{Status, Fin, Host, Path} = begin
{IsFin, Ret} = do_get(ConnPid, Host, Path),
{Ret, IsFin, Host, Path}
end || {Status, Fin, Host, Path} <- Tests],
gun:close(ConnPid).
echo_body(Config) ->
ConnPid = gun_open(Config),
Body = << 0:800000 >>,
StreamRef = gun:post(ConnPid, "/echo/body", [
{<<"content-type">>, "application/octet-stream"}
], Body),
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
{ok, Body} = gun:await_body(ConnPid, StreamRef),
gun:close(ConnPid).
echo_body_multi(Config) ->
ConnPid = gun_open(Config),
BodyChunk = << 0:80000 >>,
StreamRef = gun:post(ConnPid, "/echo/body", [
%% @todo I'm still unhappy with this. It shouldn't be required...
{<<"content-length">>, integer_to_list(byte_size(BodyChunk) * 10)},
{<<"content-type">>, "application/octet-stream"}
]),
_ = [gun:data(ConnPid, StreamRef, nofin, BodyChunk) || _ <- lists:seq(1, 9)],
gun:data(ConnPid, StreamRef, fin, BodyChunk),
{response, nofin, 200, _} = gun:await(ConnPid, StreamRef),
{ok, << 0:800000 >>} = gun:await_body(ConnPid, StreamRef),
gun:close(ConnPid).
two_frames_one_packet(Config) ->
{raw_client, Socket, Transport} = Client = raw_open([
{opts, [{client_preferred_next_protocols,
{client, [<<"spdy/3">>], <<"spdy/3">>}}]}
|Config]),
Zdef = cow_spdy:deflate_init(),
Zinf = cow_spdy:inflate_init(),
ok = raw_send(Client, iolist_to_binary([
cow_spdy:syn_stream(Zdef, 1, 0, true, false,
0, <<"GET">>, <<"https">>, <<"localhost">>,
<<"/">>, <<"HTTP/1.1">>, []),
cow_spdy:syn_stream(Zdef, 3, 0, true, false,
0, <<"GET">>, <<"https">>, <<"localhost">>,
<<"/">>, <<"HTTP/1.1">>, [])
])),
{Frame1, Rest1} = spdy_recv(Socket, Transport, <<>>),
{syn_reply, _, false, <<"200 OK">>, _, _} = cow_spdy:parse(Frame1, Zinf),
{Frame2, Rest2} = spdy_recv(Socket, Transport, Rest1),
{data, 1, true, _} = cow_spdy:parse(Frame2, Zinf),
{Frame3, Rest3} = spdy_recv(Socket, Transport, Rest2),
{syn_reply, _, false, <<"200 OK">>, _, _} = cow_spdy:parse(Frame3, Zinf),
{Frame4, <<>>} = spdy_recv(Socket, Transport, Rest3),
{data, 3, true, _} = cow_spdy:parse(Frame4, Zinf),
ok.
spdy_recv(Socket, Transport, Acc) ->
{ok, Data} = Transport:recv(Socket, 0, 5000),
Data2 = << Acc/binary, Data/bits >>,
case cow_spdy:split(Data2) of
false ->
spdy_recv(Socket, Transport, Data2);
{true, Frame, Rest} ->
{Frame, Rest}
end.