2017-01-02 19:36:36 +01:00
|
|
|
%% Copyright (c) 2015-2017, Loïc Hoguin <essen@ninenines.eu>
|
2015-06-11 17:04:21 +02:00
|
|
|
%%
|
|
|
|
%% 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_http2).
|
|
|
|
|
2018-06-27 17:52:25 +02:00
|
|
|
-ifdef(OTP_RELEASE).
|
|
|
|
-compile({nowarn_deprecated_function, [{erlang, get_stacktrace, 0}]}).
|
|
|
|
-endif.
|
|
|
|
|
2017-01-16 14:22:43 +01:00
|
|
|
-export([init/5]).
|
|
|
|
-export([init/9]).
|
2017-10-25 20:17:21 +01:00
|
|
|
-export([init/11]).
|
2015-06-11 17:04:21 +02:00
|
|
|
|
|
|
|
-export([system_continue/3]).
|
|
|
|
-export([system_terminate/4]).
|
|
|
|
-export([system_code_change/4]).
|
|
|
|
|
2017-05-05 13:48:25 +02:00
|
|
|
-type opts() :: #{
|
|
|
|
connection_type => worker | supervisor,
|
2018-04-23 14:34:53 +02:00
|
|
|
enable_connect_protocol => boolean(),
|
2017-05-05 13:48:25 +02:00
|
|
|
env => cowboy_middleware:env(),
|
|
|
|
inactivity_timeout => timeout(),
|
2018-04-26 22:08:05 +02:00
|
|
|
initial_connection_window_size => 65535..16#7fffffff,
|
|
|
|
initial_stream_window_size => 0..16#7fffffff,
|
2018-07-09 10:08:48 +02:00
|
|
|
logger => module(),
|
2018-04-25 21:32:58 +02:00
|
|
|
max_concurrent_streams => non_neg_integer() | infinity,
|
2018-04-25 16:55:52 +02:00
|
|
|
max_decode_table_size => non_neg_integer(),
|
|
|
|
max_encode_table_size => non_neg_integer(),
|
2018-04-27 17:58:11 +02:00
|
|
|
max_frame_size_received => 16384..16777215,
|
|
|
|
max_frame_size_sent => 16384..16777215 | infinity,
|
2017-05-05 13:48:25 +02:00
|
|
|
middlewares => [module()],
|
|
|
|
preface_timeout => timeout(),
|
2018-04-28 10:59:56 +02:00
|
|
|
settings_timeout => timeout(),
|
2017-05-05 13:48:25 +02:00
|
|
|
shutdown_timeout => timeout(),
|
|
|
|
stream_handlers => [module()]
|
|
|
|
}.
|
|
|
|
-export_type([opts/0]).
|
|
|
|
|
2015-06-11 17:04:21 +02:00
|
|
|
-record(state, {
|
|
|
|
parent = undefined :: pid(),
|
|
|
|
ref :: ranch:ref(),
|
|
|
|
socket = undefined :: inet:socket(),
|
|
|
|
transport :: module(),
|
2017-05-05 13:48:25 +02:00
|
|
|
opts = #{} :: opts(),
|
2015-06-11 17:04:21 +02:00
|
|
|
|
2016-06-20 17:28:59 +02:00
|
|
|
%% Remote address and port for the connection.
|
|
|
|
peer = undefined :: {inet:ip_address(), inet:port_number()},
|
|
|
|
|
2017-10-25 20:17:21 +01:00
|
|
|
%% Local address and port for the connection.
|
|
|
|
sock = undefined :: {inet:ip_address(), inet:port_number()},
|
|
|
|
|
|
|
|
%% Client certificate (TLS only).
|
|
|
|
cert :: undefined | binary(),
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
%% HTTP/2 state machine.
|
2018-10-27 10:07:34 +02:00
|
|
|
http2_init :: sequence | settings | upgrade | complete,
|
2018-10-26 10:18:57 +02:00
|
|
|
http2_machine :: cow_http2_machine:http2_machine(),
|
2015-06-11 17:04:21 +02:00
|
|
|
|
|
|
|
%% Currently active HTTP/2 streams. Streams may be initiated either
|
|
|
|
%% by the client or by the server through PUSH_PROMISE frames.
|
2018-10-26 10:18:57 +02:00
|
|
|
streams = #{} :: #{cow_http2:streamid() => {running | stopping, {module, any()}}},
|
2018-04-27 20:45:34 +02:00
|
|
|
|
2015-06-11 17:04:21 +02:00
|
|
|
%% Streams can spawn zero or more children which are then managed
|
|
|
|
%% by this module if operating as a supervisor.
|
2018-10-26 10:18:57 +02:00
|
|
|
children = cowboy_children:init() :: cowboy_children:children()
|
2015-06-11 17:04:21 +02:00
|
|
|
}).
|
|
|
|
|
2017-01-16 14:22:43 +01:00
|
|
|
-spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts()) -> ok.
|
|
|
|
init(Parent, Ref, Socket, Transport, Opts) ->
|
2017-10-25 20:17:21 +01:00
|
|
|
Peer0 = Transport:peername(Socket),
|
|
|
|
Sock0 = Transport:sockname(Socket),
|
|
|
|
Cert1 = case Transport:name() of
|
|
|
|
ssl ->
|
|
|
|
case ssl:peercert(Socket) of
|
|
|
|
{error, no_peercert} ->
|
|
|
|
{ok, undefined};
|
|
|
|
Cert0 ->
|
|
|
|
Cert0
|
|
|
|
end;
|
|
|
|
_ ->
|
|
|
|
{ok, undefined}
|
|
|
|
end,
|
|
|
|
case {Peer0, Sock0, Cert1} of
|
|
|
|
{{ok, Peer}, {ok, Sock}, {ok, Cert}} ->
|
|
|
|
init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, <<>>);
|
|
|
|
{{error, Reason}, _, _} ->
|
|
|
|
terminate(undefined, {socket_error, Reason,
|
|
|
|
'A socket error occurred when retrieving the peer name.'});
|
|
|
|
{_, {error, Reason}, _} ->
|
|
|
|
terminate(undefined, {socket_error, Reason,
|
|
|
|
'A socket error occurred when retrieving the sock name.'});
|
|
|
|
{_, _, {error, Reason}} ->
|
|
|
|
terminate(undefined, {socket_error, Reason,
|
|
|
|
'A socket error occurred when retrieving the client TLS certificate.'})
|
2016-06-20 17:28:59 +02:00
|
|
|
end.
|
2016-03-10 23:30:49 +01:00
|
|
|
|
2017-01-16 14:22:43 +01:00
|
|
|
-spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts(),
|
2017-10-25 20:17:21 +01:00
|
|
|
{inet:ip_address(), inet:port_number()}, {inet:ip_address(), inet:port_number()},
|
|
|
|
binary() | undefined, binary()) -> ok.
|
|
|
|
init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer) ->
|
2018-10-26 10:18:57 +02:00
|
|
|
{ok, Preface, HTTP2Machine} = cow_http2_machine:init(server, Opts),
|
|
|
|
State = #state{parent=Parent, ref=Ref, socket=Socket,
|
2017-10-25 20:17:21 +01:00
|
|
|
transport=Transport, opts=Opts, peer=Peer, sock=Sock, cert=Cert,
|
2018-10-27 00:16:13 +02:00
|
|
|
http2_init=sequence, http2_machine=HTTP2Machine},
|
2018-10-26 10:18:57 +02:00
|
|
|
Transport:send(Socket, Preface),
|
2016-03-10 23:30:49 +01:00
|
|
|
case Buffer of
|
|
|
|
<<>> -> before_loop(State, Buffer);
|
|
|
|
_ -> parse(State, Buffer)
|
|
|
|
end.
|
|
|
|
|
2016-03-12 18:25:35 +01:00
|
|
|
%% @todo Add an argument for the request body.
|
2017-01-16 14:22:43 +01:00
|
|
|
-spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts(),
|
2017-10-25 20:17:21 +01:00
|
|
|
{inet:ip_address(), inet:port_number()}, {inet:ip_address(), inet:port_number()},
|
|
|
|
binary() | undefined, binary(), map() | undefined, cowboy_req:req()) -> ok.
|
2018-10-26 10:18:57 +02:00
|
|
|
init(Parent, Ref, Socket, Transport, Opts, Peer, Sock, Cert, Buffer,
|
|
|
|
_Settings, Req=#{method := Method}) ->
|
|
|
|
{ok, Preface, HTTP2Machine0} = cow_http2_machine:init(server, Opts),
|
|
|
|
{ok, StreamID, HTTP2Machine}
|
|
|
|
= cow_http2_machine:init_upgrade_stream(Method, HTTP2Machine0),
|
2016-03-12 18:25:35 +01:00
|
|
|
State0 = #state{parent=Parent, ref=Ref, socket=Socket,
|
2017-10-25 20:17:21 +01:00
|
|
|
transport=Transport, opts=Opts, peer=Peer, sock=Sock, cert=Cert,
|
2018-10-27 00:16:13 +02:00
|
|
|
http2_init=upgrade, http2_machine=HTTP2Machine},
|
2018-10-26 10:18:57 +02:00
|
|
|
State1 = headers_frame(State0#state{
|
|
|
|
http2_machine=HTTP2Machine}, StreamID, Req),
|
2017-04-18 14:06:34 +02:00
|
|
|
%% We assume that the upgrade will be applied. A stream handler
|
|
|
|
%% must not prevent the normal operations of the server.
|
2018-10-27 00:16:13 +02:00
|
|
|
State2 = info(State1, 1, {switch_protocol, #{
|
2017-04-18 14:06:34 +02:00
|
|
|
<<"connection">> => <<"Upgrade">>,
|
|
|
|
<<"upgrade">> => <<"h2c">>
|
|
|
|
}, ?MODULE, undefined}), %% @todo undefined or #{}?
|
2018-10-27 00:16:13 +02:00
|
|
|
State = State2#state{http2_init=sequence},
|
2018-10-26 10:18:57 +02:00
|
|
|
Transport:send(Socket, Preface),
|
2016-03-12 18:25:35 +01:00
|
|
|
case Buffer of
|
|
|
|
<<>> -> before_loop(State, Buffer);
|
|
|
|
_ -> parse(State, Buffer)
|
|
|
|
end.
|
|
|
|
|
2015-06-11 17:04:21 +02:00
|
|
|
%% @todo Add the timeout for last time since we heard of connection.
|
|
|
|
before_loop(State, Buffer) ->
|
|
|
|
loop(State, Buffer).
|
|
|
|
|
2016-03-13 23:14:57 +01:00
|
|
|
loop(State=#state{parent=Parent, socket=Socket, transport=Transport,
|
2018-10-27 00:16:13 +02:00
|
|
|
opts=Opts, children=Children}, Buffer) ->
|
2015-06-11 17:04:21 +02:00
|
|
|
Transport:setopts(Socket, [{active, once}]),
|
|
|
|
{OK, Closed, Error} = Transport:messages(),
|
2017-05-05 13:48:25 +02:00
|
|
|
InactivityTimeout = maps:get(inactivity_timeout, Opts, 300000),
|
2015-06-11 17:04:21 +02:00
|
|
|
receive
|
|
|
|
%% Socket messages.
|
|
|
|
{OK, Socket, Data} ->
|
|
|
|
parse(State, << Buffer/binary, Data/binary >>);
|
|
|
|
{Closed, Socket} ->
|
|
|
|
terminate(State, {socket_error, closed, 'The socket has been closed.'});
|
|
|
|
{Error, Socket, Reason} ->
|
|
|
|
terminate(State, {socket_error, Reason, 'An error has occurred on the socket.'});
|
|
|
|
%% System messages.
|
|
|
|
{'EXIT', Parent, Reason} ->
|
2018-10-28 10:20:43 +01:00
|
|
|
terminate(State, {stop, {exit, Reason}, 'Parent process terminated.'});
|
2015-06-11 17:04:21 +02:00
|
|
|
{system, From, Request} ->
|
|
|
|
sys:handle_system_msg(Request, From, Parent, ?MODULE, [], {State, Buffer});
|
2017-08-08 16:59:33 +02:00
|
|
|
%% Timeouts.
|
|
|
|
{timeout, Ref, {shutdown, Pid}} ->
|
|
|
|
cowboy_children:shutdown_timeout(Children, Ref, Pid),
|
|
|
|
loop(State, Buffer);
|
2018-10-27 00:16:13 +02:00
|
|
|
{timeout, TRef, {cow_http2_machine, Name}} ->
|
|
|
|
loop(timeout(State, Name, TRef), Buffer);
|
2015-06-11 17:04:21 +02:00
|
|
|
%% Messages pertaining to a stream.
|
Initial commit with connection/streams
Breaking changes with previous commit. This is a very large change,
and I am giving up on making a single commit that fixes everything.
More commits will follow slowly adding back features, introducing
new tests and fixing the documentation.
This change contains most of the work toward unifying the interface
for handling both HTTP/1.1 and HTTP/2. HTTP/1.1 connections are now
no longer 1 process per connection; instead by default 1 process per
request is also created. This has a number of pros and cons.
Because it has cons, we also allow users to use a lower-level API
that acts on "streams" (requests/responses) directly at the connection
process-level. If performance is a concern, one can always write a
stream handler. The performance in this case will be even greater
than with Cowboy 1, although all the special handlers are unavailable.
When switching to Websocket, after the handler returns from init/2,
Cowboy stops the stream and the Websocket protocol takes over the
connection process. Websocket then calls websocket_init/2 for any
additional initialization such as timers, because the process is
different in init/2 and websocket_*/* functions. This however would
allow us to use websocket_init/2 for sending messages on connect,
instead of sending ourselves a message and be subject to races.
Note that websocket_init/2 is optional.
This is all a big change and while most of the tests pass, some
functionality currently doesn't. SPDY is broken and will be removed
soon in favor of HTTP/2. Automatic compression is currently disabled.
The cowboy_req interface probably still have a few functions that
need to be updated. The docs and examples do not refer the current
functionality anymore.
Everything will be fixed over time. Feedback is more than welcome.
Open a ticket!
2016-02-10 17:28:32 +01:00
|
|
|
{{Pid, StreamID}, Msg} when Pid =:= self() ->
|
2015-06-11 17:04:21 +02:00
|
|
|
loop(info(State, StreamID, Msg), Buffer);
|
|
|
|
%% Exit signal from children.
|
|
|
|
Msg = {'EXIT', Pid, _} ->
|
|
|
|
loop(down(State, Pid, Msg), Buffer);
|
|
|
|
%% Calls from supervisor module.
|
2018-03-13 10:40:14 +01:00
|
|
|
{'$gen_call', From, Call} ->
|
|
|
|
cowboy_children:handle_supervisor_call(Call, From, Children, ?MODULE),
|
2015-06-11 17:04:21 +02:00
|
|
|
loop(State, Buffer);
|
|
|
|
Msg ->
|
2018-06-28 17:10:18 +02:00
|
|
|
cowboy:log(warning, "Received stray message ~p.", [Msg], Opts),
|
2015-06-11 17:04:21 +02:00
|
|
|
loop(State, Buffer)
|
2017-05-05 13:48:25 +02:00
|
|
|
after InactivityTimeout ->
|
2015-06-11 17:04:21 +02:00
|
|
|
terminate(State, {internal_error, timeout, 'No message or data received before timeout.'})
|
|
|
|
end.
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
%% HTTP/2 protocol parsing.
|
|
|
|
|
2018-10-27 00:16:13 +02:00
|
|
|
parse(State=#state{http2_init=sequence}, Data) ->
|
2018-10-26 18:51:32 +02:00
|
|
|
case cow_http2:parse_sequence(Data) of
|
|
|
|
{ok, Rest} ->
|
2018-10-27 10:07:34 +02:00
|
|
|
parse(State#state{http2_init=settings}, Rest);
|
2018-10-26 18:51:32 +02:00
|
|
|
more ->
|
|
|
|
before_loop(State, Data);
|
|
|
|
Error = {connection_error, _, _} ->
|
|
|
|
terminate(State, Error)
|
2015-06-11 17:04:21 +02:00
|
|
|
end;
|
2018-10-26 10:18:57 +02:00
|
|
|
parse(State=#state{http2_machine=HTTP2Machine}, Data) ->
|
|
|
|
MaxFrameSize = cow_http2_machine:get_local_setting(max_frame_size, HTTP2Machine),
|
2017-02-25 20:05:31 +01:00
|
|
|
case cow_http2:parse(Data, MaxFrameSize) of
|
2015-06-11 17:04:21 +02:00
|
|
|
{ok, Frame, Rest} ->
|
2018-10-26 10:18:57 +02:00
|
|
|
parse(frame(State, Frame), Rest);
|
2017-02-26 13:24:15 +01:00
|
|
|
{ignore, Rest} ->
|
2018-10-26 10:18:57 +02:00
|
|
|
parse(ignored_frame(State), Rest);
|
2015-06-11 17:04:21 +02:00
|
|
|
{stream_error, StreamID, Reason, Human, Rest} ->
|
2018-10-28 11:47:49 +01:00
|
|
|
parse(reset_stream(State, StreamID, {stream_error, Reason, Human}), Rest);
|
2015-06-11 17:04:21 +02:00
|
|
|
Error = {connection_error, _, _} ->
|
|
|
|
terminate(State, Error);
|
|
|
|
more ->
|
|
|
|
before_loop(State, Data)
|
|
|
|
end.
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
%% Frames received.
|
|
|
|
|
|
|
|
frame(State=#state{http2_machine=HTTP2Machine0}, Frame) ->
|
|
|
|
case cow_http2_machine:frame(Frame, HTTP2Machine0) of
|
|
|
|
{ok, HTTP2Machine} ->
|
2018-10-27 00:16:13 +02:00
|
|
|
maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame);
|
2018-10-26 10:18:57 +02:00
|
|
|
{ok, {data, StreamID, IsFin, Data}, HTTP2Machine} ->
|
|
|
|
data_frame(State#state{http2_machine=HTTP2Machine}, StreamID, IsFin, Data);
|
|
|
|
{ok, {headers, StreamID, IsFin, Headers, PseudoHeaders, BodyLen}, HTTP2Machine} ->
|
|
|
|
headers_frame(State#state{http2_machine=HTTP2Machine},
|
|
|
|
StreamID, IsFin, Headers, PseudoHeaders, BodyLen);
|
|
|
|
{ok, {trailers, _StreamID, _Trailers}, HTTP2Machine} ->
|
|
|
|
%% @todo Propagate trailers.
|
|
|
|
State#state{http2_machine=HTTP2Machine};
|
|
|
|
{ok, {rst_stream, StreamID, Reason}, HTTP2Machine} ->
|
|
|
|
rst_stream_frame(State#state{http2_machine=HTTP2Machine}, StreamID, Reason);
|
|
|
|
{ok, Frame={goaway, _StreamID, _Reason, _Data}, HTTP2Machine} ->
|
|
|
|
terminate(State#state{http2_machine=HTTP2Machine},
|
|
|
|
{stop, Frame, 'Client is going away.'});
|
|
|
|
{send, SendData, HTTP2Machine} ->
|
2018-10-27 00:16:13 +02:00
|
|
|
send_data(maybe_ack(State#state{http2_machine=HTTP2Machine}, Frame), SendData);
|
2018-10-26 10:18:57 +02:00
|
|
|
{error, {stream_error, StreamID, Reason, Human}, HTTP2Machine} ->
|
2018-10-28 11:47:49 +01:00
|
|
|
reset_stream(State#state{http2_machine=HTTP2Machine},
|
2018-10-26 10:18:57 +02:00
|
|
|
StreamID, {stream_error, Reason, Human});
|
|
|
|
{error, Error={connection_error, _, _}, HTTP2Machine} ->
|
|
|
|
terminate(State#state{http2_machine=HTTP2Machine}, Error)
|
|
|
|
end.
|
|
|
|
|
2018-10-27 10:07:34 +02:00
|
|
|
%% We use this opportunity to mark the HTTP/2 initialization
|
|
|
|
%% as complete if we were still waiting for a SETTINGS frame.
|
|
|
|
maybe_ack(State=#state{http2_init=settings}, Frame) ->
|
|
|
|
maybe_ack(State#state{http2_init=complete}, Frame);
|
2018-10-26 10:18:57 +02:00
|
|
|
maybe_ack(State=#state{socket=Socket, transport=Transport}, Frame) ->
|
|
|
|
case Frame of
|
|
|
|
{settings, _} -> Transport:send(Socket, cow_http2:settings_ack());
|
|
|
|
{ping, Opaque} -> Transport:send(Socket, cow_http2:ping_ack(Opaque));
|
|
|
|
_ -> ok
|
|
|
|
end,
|
|
|
|
State.
|
|
|
|
|
|
|
|
data_frame(State=#state{opts=Opts, streams=Streams}, StreamID, IsFin, Data) ->
|
|
|
|
case Streams of
|
|
|
|
#{StreamID := {running, StreamState0}} ->
|
|
|
|
try cowboy_stream:data(StreamID, IsFin, Data, StreamState0) of
|
|
|
|
{Commands, StreamState} ->
|
|
|
|
commands(State#state{streams=Streams#{StreamID => {running, StreamState}}},
|
|
|
|
StreamID, Commands)
|
|
|
|
catch Class:Exception ->
|
|
|
|
cowboy:log(cowboy_stream:make_error_log(data,
|
|
|
|
[StreamID, IsFin, Data, StreamState0],
|
|
|
|
Class, Exception, erlang:get_stacktrace()), Opts),
|
2018-10-28 11:47:49 +01:00
|
|
|
reset_stream(State, StreamID, {internal_error, {Class, Exception},
|
2018-10-26 10:18:57 +02:00
|
|
|
'Unhandled exception in cowboy_stream:data/4.'})
|
|
|
|
end;
|
|
|
|
%% We ignore DATA frames for streams that are stopping.
|
|
|
|
#{} ->
|
2018-04-25 16:55:52 +02:00
|
|
|
State
|
2018-10-26 10:18:57 +02:00
|
|
|
end.
|
2015-06-11 17:04:21 +02:00
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
headers_frame(State, StreamID, IsFin, Headers,
|
|
|
|
PseudoHeaders=#{method := <<"CONNECT">>}, _)
|
|
|
|
when map_size(PseudoHeaders) =:= 2 ->
|
|
|
|
early_error(State, StreamID, IsFin, Headers, PseudoHeaders, 501,
|
|
|
|
'The CONNECT method is currently not implemented. (RFC7231 4.3.6)');
|
|
|
|
headers_frame(State, StreamID, IsFin, Headers,
|
|
|
|
PseudoHeaders=#{method := <<"TRACE">>}, _) ->
|
|
|
|
early_error(State, StreamID, IsFin, Headers, PseudoHeaders, 501,
|
|
|
|
'The TRACE method is currently not implemented. (RFC7231 4.3.8)');
|
|
|
|
headers_frame(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert},
|
|
|
|
StreamID, IsFin, Headers, PseudoHeaders=#{method := Method, scheme := Scheme,
|
|
|
|
authority := Authority, path := PathWithQs}, BodyLen) ->
|
|
|
|
try cow_http_hd:parse_host(Authority) of
|
|
|
|
{Host, Port0} ->
|
|
|
|
Port = ensure_port(Scheme, Port0),
|
|
|
|
try cow_http:parse_fullpath(PathWithQs) of
|
|
|
|
{<<>>, _} ->
|
2018-10-28 11:47:49 +01:00
|
|
|
reset_stream(State, StreamID, {stream_error, protocol_error,
|
2018-10-28 11:42:18 +01:00
|
|
|
'The path component must not be empty. (RFC7540 8.1.2.3)'});
|
2018-10-26 10:18:57 +02:00
|
|
|
{Path, Qs} ->
|
|
|
|
Req0 = #{
|
|
|
|
ref => Ref,
|
|
|
|
pid => self(),
|
|
|
|
streamid => StreamID,
|
|
|
|
peer => Peer,
|
|
|
|
sock => Sock,
|
|
|
|
cert => Cert,
|
|
|
|
method => Method,
|
|
|
|
scheme => Scheme,
|
|
|
|
host => Host,
|
|
|
|
port => Port,
|
|
|
|
path => Path,
|
|
|
|
qs => Qs,
|
|
|
|
version => 'HTTP/2',
|
|
|
|
headers => headers_to_map(Headers, #{}),
|
|
|
|
has_body => IsFin =:= nofin,
|
|
|
|
body_length => BodyLen
|
|
|
|
},
|
|
|
|
%% We add the protocol information for extended CONNECTs.
|
|
|
|
Req = case PseudoHeaders of
|
|
|
|
#{protocol := Protocol} ->
|
|
|
|
Req0#{protocol => Protocol};
|
|
|
|
_ ->
|
|
|
|
Req0
|
|
|
|
end,
|
|
|
|
headers_frame(State, StreamID, Req)
|
|
|
|
catch _:_ ->
|
2018-10-28 11:47:49 +01:00
|
|
|
reset_stream(State, StreamID, {stream_error, protocol_error,
|
2018-10-28 11:42:18 +01:00
|
|
|
'The :path pseudo-header is invalid. (RFC7540 8.1.2.3)'})
|
2018-10-26 10:18:57 +02:00
|
|
|
end
|
|
|
|
catch _:_ ->
|
2018-10-28 11:47:49 +01:00
|
|
|
reset_stream(State, StreamID, {stream_error, protocol_error,
|
2018-10-28 11:42:18 +01:00
|
|
|
'The :authority pseudo-header is invalid. (RFC7540 8.1.2.3)'})
|
2018-04-30 13:47:33 +02:00
|
|
|
end.
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
ensure_port(<<"http">>, undefined) -> 80;
|
|
|
|
ensure_port(<<"https">>, undefined) -> 443;
|
|
|
|
ensure_port(_, Port) -> Port.
|
|
|
|
|
|
|
|
%% This function is necessary to properly handle duplicate headers
|
|
|
|
%% and the special-case cookie header.
|
|
|
|
headers_to_map([], Acc) ->
|
|
|
|
Acc;
|
|
|
|
headers_to_map([{Name, Value}|Tail], Acc0) ->
|
|
|
|
Acc = case Acc0 of
|
|
|
|
%% The cookie header does not use proper HTTP header lists.
|
|
|
|
#{Name := Value0} when Name =:= <<"cookie">> ->
|
|
|
|
Acc0#{Name => << Value0/binary, "; ", Value/binary >>};
|
|
|
|
#{Name := Value0} ->
|
|
|
|
Acc0#{Name => << Value0/binary, ", ", Value/binary >>};
|
|
|
|
_ ->
|
|
|
|
Acc0#{Name => Value}
|
|
|
|
end,
|
|
|
|
headers_to_map(Tail, Acc).
|
|
|
|
|
|
|
|
headers_frame(State=#state{opts=Opts, streams=Streams}, StreamID, Req) ->
|
|
|
|
try cowboy_stream:init(StreamID, Req, Opts) of
|
2018-04-30 13:47:33 +02:00
|
|
|
{Commands, StreamState} ->
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State#state{
|
|
|
|
streams=Streams#{StreamID => {running, StreamState}}},
|
|
|
|
StreamID, Commands)
|
2018-04-30 13:47:33 +02:00
|
|
|
catch Class:Exception ->
|
2018-10-26 10:18:57 +02:00
|
|
|
cowboy:log(cowboy_stream:make_error_log(init,
|
|
|
|
[StreamID, Req, Opts],
|
2018-06-28 17:10:18 +02:00
|
|
|
Class, Exception, erlang:get_stacktrace()), Opts),
|
2018-10-28 11:47:49 +01:00
|
|
|
reset_stream(State, StreamID, {internal_error, {Class, Exception},
|
2018-10-26 10:18:57 +02:00
|
|
|
'Unhandled exception in cowboy_stream:init/3.'})
|
|
|
|
end.
|
|
|
|
|
|
|
|
early_error(State0=#state{ref=Ref, opts=Opts, peer=Peer},
|
|
|
|
StreamID, _IsFin, _Headers, #{method := Method},
|
|
|
|
StatusCode0, HumanReadable) ->
|
|
|
|
%% We automatically terminate the stream but it is not an error
|
|
|
|
%% per se (at least not in the first implementation).
|
|
|
|
Reason = {stream_error, no_error, HumanReadable},
|
|
|
|
%% The partial Req is minimal for now. We only have one case
|
|
|
|
%% where it can be called (when a method is completely disabled).
|
|
|
|
%% @todo Fill in the other elements.
|
|
|
|
PartialReq = #{
|
|
|
|
ref => Ref,
|
|
|
|
peer => Peer,
|
|
|
|
method => Method
|
|
|
|
},
|
|
|
|
Resp = {response, StatusCode0, RespHeaders0=#{<<"content-length">> => <<"0">>}, <<>>},
|
|
|
|
try cowboy_stream:early_error(StreamID, Reason, PartialReq, Resp, Opts) of
|
|
|
|
{response, StatusCode, RespHeaders, RespBody} ->
|
|
|
|
send_response(State0, StreamID, StatusCode, RespHeaders, RespBody)
|
|
|
|
catch Class:Exception ->
|
|
|
|
cowboy:log(cowboy_stream:make_error_log(early_error,
|
|
|
|
[StreamID, Reason, PartialReq, Resp, Opts],
|
|
|
|
Class, Exception, erlang:get_stacktrace()), Opts),
|
|
|
|
%% We still need to send an error response, so send what we initially
|
|
|
|
%% wanted to send. It's better than nothing.
|
|
|
|
send_headers(State0, StreamID, fin, StatusCode0, RespHeaders0)
|
2018-04-30 13:47:33 +02:00
|
|
|
end.
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
rst_stream_frame(State=#state{streams=Streams0, children=Children0}, StreamID, Reason) ->
|
|
|
|
case maps:take(StreamID, Streams0) of
|
|
|
|
{{_, StreamState}, Streams} ->
|
2018-10-28 11:47:49 +01:00
|
|
|
terminate_stream_handler(State, StreamID, Reason, StreamState),
|
2018-10-26 10:18:57 +02:00
|
|
|
Children = cowboy_children:shutdown(Children0, StreamID),
|
|
|
|
State#state{streams=Streams, children=Children};
|
|
|
|
error ->
|
|
|
|
State
|
|
|
|
end.
|
2018-04-30 13:47:33 +02:00
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
ignored_frame(State=#state{http2_machine=HTTP2Machine0}) ->
|
|
|
|
case cow_http2_machine:ignored_frame(HTTP2Machine0) of
|
|
|
|
{ok, HTTP2Machine} ->
|
|
|
|
State#state{http2_machine=HTTP2Machine};
|
|
|
|
{error, Error={connection_error, _, _}, HTTP2Machine} ->
|
|
|
|
terminate(State#state{http2_machine=HTTP2Machine}, Error)
|
|
|
|
end.
|
|
|
|
|
2018-10-27 00:16:13 +02:00
|
|
|
%% HTTP/2 timeouts.
|
|
|
|
|
2018-10-27 10:07:34 +02:00
|
|
|
timeout(State=#state{http2_machine=HTTP2Machine0}, Name, TRef) ->
|
2018-10-27 00:16:13 +02:00
|
|
|
case cow_http2_machine:timeout(Name, TRef, HTTP2Machine0) of
|
|
|
|
{ok, HTTP2Machine} ->
|
|
|
|
State#state{http2_machine=HTTP2Machine};
|
|
|
|
{error, Error={connection_error, _, _}, HTTP2Machine} ->
|
|
|
|
terminate(State#state{http2_machine=HTTP2Machine}, Error)
|
|
|
|
end.
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
%% Erlang messages.
|
2015-06-11 17:04:21 +02:00
|
|
|
|
2018-06-28 17:10:18 +02:00
|
|
|
down(State=#state{opts=Opts, children=Children0}, Pid, Msg) ->
|
2017-08-08 16:59:33 +02:00
|
|
|
case cowboy_children:down(Children0, Pid) of
|
|
|
|
%% The stream was terminated already.
|
|
|
|
{ok, undefined, Children} ->
|
|
|
|
State#state{children=Children};
|
|
|
|
%% The stream is still running.
|
|
|
|
{ok, StreamID, Children} ->
|
2015-06-11 17:04:21 +02:00
|
|
|
info(State#state{children=Children}, StreamID, Msg);
|
2017-08-08 16:59:33 +02:00
|
|
|
%% The process was unknown.
|
|
|
|
error ->
|
2018-06-28 17:10:18 +02:00
|
|
|
cowboy:log(warning, "Received EXIT signal ~p for unknown process ~p.~n",
|
|
|
|
[Msg, Pid], Opts),
|
2015-06-11 17:04:21 +02:00
|
|
|
State
|
|
|
|
end.
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
info(State=#state{opts=Opts, streams=Streams}, StreamID, Msg) ->
|
|
|
|
case Streams of
|
|
|
|
#{StreamID := {IsRunning, StreamState0}} ->
|
2017-01-16 14:22:43 +01:00
|
|
|
try cowboy_stream:info(StreamID, Msg, StreamState0) of
|
2015-06-11 17:04:21 +02:00
|
|
|
{Commands, StreamState} ->
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State#state{streams=Streams#{StreamID => {IsRunning, StreamState}}},
|
|
|
|
StreamID, Commands)
|
2017-09-21 12:53:21 +02:00
|
|
|
catch Class:Exception ->
|
2018-06-28 17:10:18 +02:00
|
|
|
cowboy:log(cowboy_stream:make_error_log(info,
|
2017-09-21 12:53:21 +02:00
|
|
|
[StreamID, Msg, StreamState0],
|
2018-06-28 17:10:18 +02:00
|
|
|
Class, Exception, erlang:get_stacktrace()), Opts),
|
2018-10-28 11:47:49 +01:00
|
|
|
reset_stream(State, StreamID, {internal_error, {Class, Exception},
|
2017-09-27 14:17:27 +02:00
|
|
|
'Unhandled exception in cowboy_stream:info/3.'})
|
2015-06-11 17:04:21 +02:00
|
|
|
end;
|
2018-10-26 10:18:57 +02:00
|
|
|
_ ->
|
|
|
|
cowboy:log(warning, "Received message ~p for unknown or terminated stream ~p.",
|
2018-06-28 17:10:18 +02:00
|
|
|
[Msg, StreamID], Opts),
|
2015-06-11 17:04:21 +02:00
|
|
|
State
|
|
|
|
end.
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
%% Stream handler commands.
|
|
|
|
%%
|
2018-04-30 18:39:27 +02:00
|
|
|
%% @todo Kill the stream if it tries to send a response, headers,
|
|
|
|
%% data or push promise when the stream is closed or half-closed.
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State, _, []) ->
|
|
|
|
State;
|
2016-08-10 17:15:02 +02:00
|
|
|
%% Error responses are sent only if a response wasn't sent already.
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State=#state{http2_machine=HTTP2Machine}, StreamID,
|
|
|
|
[{error_response, StatusCode, Headers, Body}|Tail]) ->
|
|
|
|
case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of
|
|
|
|
{ok, idle, _} ->
|
|
|
|
commands(State, StreamID, [{response, StatusCode, Headers, Body}|Tail]);
|
|
|
|
_ ->
|
|
|
|
commands(State, StreamID, Tail)
|
|
|
|
end;
|
2017-10-29 19:52:27 +00:00
|
|
|
%% Send an informational response.
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State0, StreamID, [{inform, StatusCode, Headers}|Tail]) ->
|
|
|
|
State = send_headers(State0, StreamID, idle, StatusCode, Headers),
|
|
|
|
commands(State, StreamID, Tail);
|
2015-06-11 17:04:21 +02:00
|
|
|
%% Send response headers.
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State0, StreamID, [{response, StatusCode, Headers, Body}|Tail]) ->
|
|
|
|
State = send_response(State0, StreamID, StatusCode, Headers, Body),
|
|
|
|
commands(State, StreamID, Tail);
|
2017-09-25 13:52:58 +02:00
|
|
|
%% Send response headers.
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State0, StreamID, [{headers, StatusCode, Headers}|Tail]) ->
|
|
|
|
State = send_headers(State0, StreamID, nofin, StatusCode, Headers),
|
|
|
|
commands(State, StreamID, Tail);
|
2015-06-11 17:04:21 +02:00
|
|
|
%% Send a response body chunk.
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State0, StreamID, [{data, IsFin, Data}|Tail]) ->
|
|
|
|
State = maybe_send_data(State0, StreamID, IsFin, Data),
|
|
|
|
commands(State, StreamID, Tail);
|
2017-11-15 14:58:49 +01:00
|
|
|
%% Send trailers.
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State0, StreamID, [{trailers, Trailers}|Tail]) ->
|
|
|
|
State = maybe_send_data(State0, StreamID, fin, {trailers, maps:to_list(Trailers)}),
|
|
|
|
commands(State, StreamID, Tail);
|
2016-06-06 17:27:48 +02:00
|
|
|
%% Send a file.
|
2017-12-13 12:40:00 +01:00
|
|
|
%% @todo Add the sendfile command.
|
|
|
|
%commands(State0, Stream0=#stream{local=nofin},
|
|
|
|
% [{sendfile, IsFin, Offset, Bytes, Path}|Tail]) ->
|
2018-10-26 10:18:57 +02:00
|
|
|
% {State, Stream} = maybe_send_data(State0, Stream0, IsFin, {sendfile, Offset, Bytes, Path}),
|
2017-12-13 12:40:00 +01:00
|
|
|
% commands(State, Stream, Tail);
|
2015-06-11 17:04:21 +02:00
|
|
|
%% Send a push promise.
|
|
|
|
%%
|
2018-10-26 10:18:57 +02:00
|
|
|
%% @todo Responses sent as a result of a push_promise request
|
|
|
|
%% must not send push_promise frames themselves.
|
|
|
|
commands(State0=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0},
|
|
|
|
StreamID, [{push, Method, Scheme, Host, Port, Path, Qs, Headers0}|Tail]) ->
|
2016-08-10 11:49:31 +02:00
|
|
|
Authority = case {Scheme, Port} of
|
|
|
|
{<<"http">>, 80} -> Host;
|
|
|
|
{<<"https">>, 443} -> Host;
|
2017-07-26 17:31:12 +02:00
|
|
|
_ -> iolist_to_binary([Host, $:, integer_to_binary(Port)])
|
2016-08-10 11:49:31 +02:00
|
|
|
end,
|
2017-11-30 15:01:01 +01:00
|
|
|
PathWithQs = iolist_to_binary(case Qs of
|
2016-08-10 11:49:31 +02:00
|
|
|
<<>> -> Path;
|
|
|
|
_ -> [Path, $?, Qs]
|
2017-11-30 15:01:01 +01:00
|
|
|
end),
|
2018-10-26 10:18:57 +02:00
|
|
|
PseudoHeaders = #{
|
|
|
|
method => Method,
|
|
|
|
scheme => Scheme,
|
|
|
|
authority => Authority,
|
|
|
|
path => PathWithQs
|
|
|
|
},
|
2017-07-26 17:31:12 +02:00
|
|
|
%% We need to make sure the header value is binary before we can
|
2018-10-26 10:18:57 +02:00
|
|
|
%% create the Req object, as it expects them to be flat.
|
|
|
|
Headers = maps:to_list(maps:map(fun(_, V) -> iolist_to_binary(V) end, Headers0)),
|
|
|
|
State = case cow_http2_machine:prepare_push_promise(StreamID, HTTP2Machine0,
|
|
|
|
PseudoHeaders, Headers) of
|
|
|
|
{ok, PromisedStreamID, HeaderBlock, HTTP2Machine} ->
|
|
|
|
Transport:send(Socket, cow_http2:push_promise(
|
|
|
|
StreamID, PromisedStreamID, HeaderBlock)),
|
|
|
|
headers_frame(State0#state{http2_machine=HTTP2Machine},
|
|
|
|
PromisedStreamID, fin, Headers, PseudoHeaders, 0);
|
|
|
|
{error, no_push} ->
|
|
|
|
State0
|
|
|
|
end,
|
|
|
|
commands(State, StreamID, Tail);
|
|
|
|
commands(State=#state{socket=Socket, transport=Transport, http2_machine=HTTP2Machine0},
|
|
|
|
StreamID, [{flow, Size}|Tail]) ->
|
2017-05-19 20:18:00 +02:00
|
|
|
Transport:send(Socket, [
|
|
|
|
cow_http2:window_update(Size),
|
|
|
|
cow_http2:window_update(StreamID, Size)
|
|
|
|
]),
|
2018-10-26 10:18:57 +02:00
|
|
|
HTTP2Machine1 = cow_http2_machine:update_window(Size, HTTP2Machine0),
|
|
|
|
HTTP2Machine = cow_http2_machine:update_window(StreamID, Size, HTTP2Machine1),
|
|
|
|
commands(State#state{http2_machine=HTTP2Machine}, StreamID, Tail);
|
2015-06-11 17:04:21 +02:00
|
|
|
%% Supervise a child process.
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State=#state{children=Children}, StreamID, [{spawn, Pid, Shutdown}|Tail]) ->
|
2017-08-08 16:59:33 +02:00
|
|
|
commands(State#state{children=cowboy_children:up(Children, Pid, StreamID, Shutdown)},
|
2018-10-26 10:18:57 +02:00
|
|
|
StreamID, Tail);
|
2016-06-06 17:28:35 +02:00
|
|
|
%% Error handling.
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State, StreamID, [Error = {internal_error, _, _}|_Tail]) ->
|
2016-06-16 19:46:57 +02:00
|
|
|
%% @todo Do we want to run the commands after an internal_error?
|
|
|
|
%% @todo Do we even allow commands after?
|
2016-06-06 17:28:35 +02:00
|
|
|
%% @todo Only reset when the stream still exists.
|
2018-10-28 11:47:49 +01:00
|
|
|
reset_stream(State, StreamID, Error);
|
2017-04-18 14:06:34 +02:00
|
|
|
%% Upgrade to HTTP/2. This is triggered by cowboy_http2 itself.
|
2018-10-27 00:16:13 +02:00
|
|
|
commands(State=#state{socket=Socket, transport=Transport, http2_init=upgrade},
|
2018-10-26 10:18:57 +02:00
|
|
|
StreamID, [{switch_protocol, Headers, ?MODULE, _}|Tail]) ->
|
|
|
|
%% @todo This 101 response needs to be passed through stream handlers.
|
2017-04-18 14:06:34 +02:00
|
|
|
Transport:send(Socket, cow_http:response(101, 'HTTP/1.1', maps:to_list(Headers))),
|
2018-10-27 00:16:13 +02:00
|
|
|
commands(State, StreamID, Tail);
|
2018-04-04 17:23:37 +02:00
|
|
|
%% Use a different protocol within the stream (CONNECT :protocol).
|
|
|
|
%% @todo Make sure we error out when the feature is disabled.
|
2018-10-26 10:18:57 +02:00
|
|
|
commands(State0, StreamID, [{switch_protocol, Headers, _Mod, _ModState}|Tail]) ->
|
|
|
|
State = info(State0, StreamID, {headers, 200, Headers}),
|
|
|
|
commands(State, StreamID, Tail);
|
|
|
|
commands(State, StreamID, [stop|_Tail]) ->
|
2016-03-12 18:25:35 +01:00
|
|
|
%% @todo Do we want to run the commands after a stop?
|
2016-06-16 19:46:57 +02:00
|
|
|
%% @todo Do we even allow commands after?
|
2018-10-26 10:18:57 +02:00
|
|
|
stop_stream(State, StreamID);
|
2018-06-28 17:10:18 +02:00
|
|
|
%% Log event.
|
|
|
|
commands(State=#state{opts=Opts}, StreamID, [Log={log, _, _, _}|Tail]) ->
|
|
|
|
cowboy:log(Log, Opts),
|
|
|
|
commands(State, StreamID, Tail).
|
2016-06-16 19:46:57 +02:00
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
%% Send the response, trailers or data.
|
2017-05-19 20:18:00 +02:00
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
send_response(State0, StreamID, StatusCode, Headers, Body) ->
|
|
|
|
Size = case Body of
|
|
|
|
{sendfile, _, Bytes, _} -> Bytes;
|
|
|
|
_ -> iolist_size(Body)
|
|
|
|
end,
|
|
|
|
case Size of
|
|
|
|
0 ->
|
|
|
|
State = send_headers(State0, StreamID, fin, StatusCode, Headers),
|
|
|
|
maybe_terminate_stream(State, StreamID, fin);
|
2017-10-23 14:49:33 +01:00
|
|
|
_ ->
|
2018-10-26 10:18:57 +02:00
|
|
|
State = send_headers(State0, StreamID, nofin, StatusCode, Headers),
|
|
|
|
maybe_send_data(State, StreamID, fin, Body)
|
2017-10-23 14:49:33 +01:00
|
|
|
end.
|
2017-05-19 20:18:00 +02:00
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
send_headers(State=#state{socket=Socket, transport=Transport,
|
|
|
|
http2_machine=HTTP2Machine0}, StreamID, IsFin0, StatusCode, Headers) ->
|
|
|
|
{ok, IsFin, HeaderBlock, HTTP2Machine}
|
|
|
|
= cow_http2_machine:prepare_headers(StreamID, HTTP2Machine0, IsFin0,
|
|
|
|
#{status => cow_http:status_to_integer(StatusCode)},
|
|
|
|
headers_to_list(Headers)),
|
|
|
|
Transport:send(Socket, cow_http2:headers(StreamID, IsFin, HeaderBlock)),
|
|
|
|
State#state{http2_machine=HTTP2Machine}.
|
2017-08-21 16:55:30 +02:00
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
%% The set-cookie header is special; we can only send one cookie per header.
|
|
|
|
headers_to_list(Headers0=#{<<"set-cookie">> := SetCookies}) ->
|
|
|
|
Headers = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)),
|
|
|
|
Headers ++ [{<<"set-cookie">>, Value} || Value <- SetCookies];
|
|
|
|
headers_to_list(Headers) ->
|
|
|
|
maps:to_list(Headers).
|
|
|
|
|
|
|
|
maybe_send_data(State=#state{http2_machine=HTTP2Machine0}, StreamID, IsFin, Data0) ->
|
|
|
|
Data = case is_tuple(Data0) of
|
|
|
|
false -> {data, Data0};
|
|
|
|
true -> Data0
|
|
|
|
end,
|
|
|
|
case cow_http2_machine:send_or_queue_data(StreamID, HTTP2Machine0, IsFin, Data) of
|
|
|
|
{ok, HTTP2Machine} ->
|
|
|
|
State#state{http2_machine=HTTP2Machine};
|
|
|
|
{send, SendData, HTTP2Machine} ->
|
|
|
|
send_data(State#state{http2_machine=HTTP2Machine}, SendData)
|
2017-02-05 13:45:35 +01:00
|
|
|
end.
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
send_data(State, []) ->
|
|
|
|
State;
|
|
|
|
send_data(State0, [{StreamID, IsFin, SendData}|Tail]) ->
|
|
|
|
State = send_data(State0, StreamID, IsFin, SendData),
|
|
|
|
send_data(State, Tail).
|
|
|
|
|
|
|
|
send_data(State0, StreamID, IsFin, [Data]) ->
|
|
|
|
State = send_data_frame(State0, StreamID, IsFin, Data),
|
|
|
|
maybe_terminate_stream(State, StreamID, IsFin);
|
|
|
|
send_data(State0, StreamID, IsFin, [Data|Tail]) ->
|
|
|
|
State = send_data_frame(State0, StreamID, nofin, Data),
|
|
|
|
send_data(State, StreamID, IsFin, Tail).
|
|
|
|
|
|
|
|
send_data_frame(State=#state{socket=Socket, transport=Transport},
|
|
|
|
StreamID, IsFin, {data, Data}) ->
|
|
|
|
Transport:send(Socket, cow_http2:data(StreamID, IsFin, Data)),
|
|
|
|
State;
|
|
|
|
send_data_frame(State=#state{socket=Socket, transport=Transport},
|
|
|
|
StreamID, IsFin, {sendfile, Offset, Bytes, Path}) ->
|
|
|
|
Transport:send(Socket, cow_http2:data_header(StreamID, IsFin, Bytes)),
|
|
|
|
Transport:sendfile(Socket, Path, Offset, Bytes),
|
|
|
|
State;
|
|
|
|
%% The stream is terminated in cow_http2_machine:prepare_trailers.
|
|
|
|
send_data_frame(State=#state{socket=Socket, transport=Transport,
|
|
|
|
http2_machine=HTTP2Machine0}, StreamID, nofin, {trailers, Trailers}) ->
|
|
|
|
{ok, HeaderBlock, HTTP2Machine}
|
|
|
|
= cow_http2_machine:prepare_trailers(StreamID, HTTP2Machine0, Trailers),
|
2017-11-20 15:46:23 +01:00
|
|
|
Transport:send(Socket, cow_http2:headers(StreamID, fin, HeaderBlock)),
|
2018-10-26 10:18:57 +02:00
|
|
|
State#state{http2_machine=HTTP2Machine}.
|
2017-05-19 20:18:00 +02:00
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
%% Terminate a stream or the connection.
|
2017-11-29 14:54:47 +01:00
|
|
|
|
2017-01-02 18:27:03 +01:00
|
|
|
-spec terminate(#state{}, _) -> no_return().
|
2017-02-25 20:05:31 +01:00
|
|
|
terminate(undefined, Reason) ->
|
|
|
|
exit({shutdown, Reason});
|
2018-10-27 00:16:13 +02:00
|
|
|
terminate(State=#state{socket=Socket, transport=Transport, http2_init=complete,
|
2018-10-26 10:18:57 +02:00
|
|
|
http2_machine=HTTP2Machine, streams=Streams, children=Children}, Reason) ->
|
2017-02-25 20:05:31 +01:00
|
|
|
%% @todo We might want to optionally send the Reason value
|
|
|
|
%% as debug data in the GOAWAY frame here. Perhaps more.
|
2018-10-26 10:18:57 +02:00
|
|
|
Transport:send(Socket, cow_http2:goaway(
|
|
|
|
cow_http2_machine:get_last_streamid(HTTP2Machine),
|
|
|
|
terminate_reason(Reason), <<>>)),
|
|
|
|
terminate_all_streams(State, maps:to_list(Streams), Reason),
|
2017-08-08 16:59:33 +02:00
|
|
|
cowboy_children:terminate(Children),
|
2018-10-26 10:18:57 +02:00
|
|
|
Transport:close(Socket),
|
|
|
|
exit({shutdown, Reason});
|
|
|
|
terminate(#state{socket=Socket, transport=Transport}, Reason) ->
|
2015-06-11 17:04:21 +02:00
|
|
|
Transport:close(Socket),
|
|
|
|
exit({shutdown, Reason}).
|
|
|
|
|
2017-02-25 20:05:31 +01:00
|
|
|
terminate_reason({connection_error, Reason, _}) -> Reason;
|
|
|
|
terminate_reason({stop, _, _}) -> no_error;
|
|
|
|
terminate_reason({socket_error, _, _}) -> internal_error;
|
|
|
|
terminate_reason({internal_error, _, _}) -> internal_error.
|
|
|
|
|
2018-06-28 17:10:18 +02:00
|
|
|
terminate_all_streams(_, [], _) ->
|
2015-06-11 17:04:21 +02:00
|
|
|
ok;
|
2018-10-26 10:18:57 +02:00
|
|
|
terminate_all_streams(State, [{StreamID, {_, StreamState}}|Tail], Reason) ->
|
2018-10-28 11:47:49 +01:00
|
|
|
terminate_stream_handler(State, StreamID, Reason, StreamState),
|
2018-06-28 17:10:18 +02:00
|
|
|
terminate_all_streams(State, Tail, Reason).
|
2015-06-11 17:04:21 +02:00
|
|
|
|
2017-10-23 14:49:33 +01:00
|
|
|
%% @todo Don't send an RST_STREAM if one was already sent.
|
2018-10-28 11:47:49 +01:00
|
|
|
reset_stream(State=#state{socket=Socket, transport=Transport,
|
2018-10-26 10:18:57 +02:00
|
|
|
http2_machine=HTTP2Machine0}, StreamID, Error) ->
|
|
|
|
Reason = case Error of
|
2017-10-23 14:49:33 +01:00
|
|
|
{internal_error, _, _} -> internal_error;
|
|
|
|
{stream_error, Reason0, _} -> Reason0
|
|
|
|
end,
|
2015-06-11 17:04:21 +02:00
|
|
|
Transport:send(Socket, cow_http2:rst_stream(StreamID, Reason)),
|
2018-10-26 10:18:57 +02:00
|
|
|
case cow_http2_machine:reset_stream(StreamID, HTTP2Machine0) of
|
|
|
|
{ok, HTTP2Machine} ->
|
|
|
|
terminate_stream(State#state{http2_machine=HTTP2Machine}, StreamID, Error);
|
|
|
|
{error, not_found} ->
|
|
|
|
terminate_stream(State, StreamID, Error)
|
|
|
|
end.
|
2018-04-27 20:45:34 +02:00
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
stop_stream(State=#state{http2_machine=HTTP2Machine}, StreamID) ->
|
|
|
|
case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine) of
|
2017-04-18 16:07:01 +02:00
|
|
|
%% When the stream terminates normally (without sending RST_STREAM)
|
|
|
|
%% and no response was sent, we need to send a proper response back to the client.
|
2018-10-26 10:18:57 +02:00
|
|
|
%% We delay the termination of the stream until the response is fully sent.
|
|
|
|
{ok, idle, _} ->
|
|
|
|
info(stopping(State, StreamID), StreamID, {response, 204, #{}, <<>>});
|
2017-04-18 16:07:01 +02:00
|
|
|
%% When a response was sent but not terminated, we need to close the stream.
|
2018-10-26 10:18:57 +02:00
|
|
|
%% We delay the termination of the stream until the response is fully sent.
|
|
|
|
{ok, nofin, fin} ->
|
|
|
|
stopping(State, StreamID);
|
|
|
|
%% We only send a final DATA frame if there isn't one queued yet.
|
|
|
|
{ok, nofin, _} ->
|
|
|
|
info(stopping(State, StreamID), StreamID, {data, fin, <<>>});
|
|
|
|
%% When a response was sent fully we can terminate the stream,
|
|
|
|
%% regardless of the stream being in half-closed or closed state.
|
|
|
|
_ ->
|
|
|
|
terminate_stream(State, StreamID)
|
2015-06-11 17:04:21 +02:00
|
|
|
end.
|
|
|
|
|
2018-10-26 10:18:57 +02:00
|
|
|
stopping(State=#state{streams=Streams}, StreamID) ->
|
|
|
|
#{StreamID := {_, StreamState}} = Streams,
|
|
|
|
State#state{streams=Streams#{StreamID => {stopping, StreamState}}}.
|
|
|
|
|
|
|
|
%% If we finished sending data and the stream is stopping, terminate it.
|
|
|
|
maybe_terminate_stream(State=#state{streams=Streams}, StreamID, fin) ->
|
|
|
|
case Streams of
|
|
|
|
#{StreamID := {stopping, _}} ->
|
|
|
|
terminate_stream(State, StreamID);
|
|
|
|
_ ->
|
|
|
|
State
|
|
|
|
end;
|
|
|
|
maybe_terminate_stream(State, _, _) ->
|
|
|
|
State.
|
|
|
|
|
2017-10-23 14:49:33 +01:00
|
|
|
%% When the stream stops normally without reading the request
|
|
|
|
%% body fully we need to tell the client to stop sending it.
|
|
|
|
%% We do this by sending an RST_STREAM with reason NO_ERROR. (RFC7540 8.1.0)
|
2018-10-26 10:18:57 +02:00
|
|
|
terminate_stream(State0=#state{socket=Socket, transport=Transport,
|
|
|
|
http2_machine=HTTP2Machine0}, StreamID) ->
|
|
|
|
State = case cow_http2_machine:get_stream_local_state(StreamID, HTTP2Machine0) of
|
|
|
|
{ok, fin, _} ->
|
|
|
|
Transport:send(Socket, cow_http2:rst_stream(StreamID, no_error)),
|
|
|
|
{ok, HTTP2Machine} = cow_http2_machine:reset_stream(StreamID, HTTP2Machine0),
|
|
|
|
State0#state{http2_machine=HTTP2Machine};
|
|
|
|
{error, closed} ->
|
|
|
|
State0
|
|
|
|
end,
|
|
|
|
terminate_stream(State, StreamID, normal).
|
|
|
|
|
|
|
|
terminate_stream(State=#state{streams=Streams0, children=Children0}, StreamID, Reason) ->
|
|
|
|
case maps:take(StreamID, Streams0) of
|
|
|
|
{{_, StreamState}, Streams} ->
|
2018-10-28 11:47:49 +01:00
|
|
|
terminate_stream_handler(State, StreamID, Reason, StreamState),
|
2018-10-26 10:18:57 +02:00
|
|
|
Children = cowboy_children:shutdown(Children0, StreamID),
|
|
|
|
State#state{streams=Streams, children=Children};
|
|
|
|
error ->
|
|
|
|
State
|
|
|
|
end.
|
2017-10-23 14:49:33 +01:00
|
|
|
|
2018-10-28 11:47:49 +01:00
|
|
|
terminate_stream_handler(#state{opts=Opts}, StreamID, Reason, StreamState) ->
|
2015-06-11 17:04:21 +02:00
|
|
|
try
|
2017-01-16 14:22:43 +01:00
|
|
|
cowboy_stream:terminate(StreamID, Reason, StreamState)
|
2017-09-21 12:53:21 +02:00
|
|
|
catch Class:Exception ->
|
2018-06-28 17:10:18 +02:00
|
|
|
cowboy:log(cowboy_stream:make_error_log(terminate,
|
2017-09-21 12:53:21 +02:00
|
|
|
[StreamID, Reason, StreamState],
|
2018-06-28 17:10:18 +02:00
|
|
|
Class, Exception, erlang:get_stacktrace()), Opts)
|
2015-06-11 17:04:21 +02:00
|
|
|
end.
|
|
|
|
|
|
|
|
%% System callbacks.
|
|
|
|
|
2017-01-02 18:27:03 +01:00
|
|
|
-spec system_continue(_, _, {#state{}, binary()}) -> ok.
|
2015-06-11 17:04:21 +02:00
|
|
|
system_continue(_, _, {State, Buffer}) ->
|
|
|
|
loop(State, Buffer).
|
|
|
|
|
2017-08-08 16:59:33 +02:00
|
|
|
-spec system_terminate(any(), _, _, {#state{}, binary()}) -> no_return().
|
|
|
|
system_terminate(Reason, _, _, {State, _}) ->
|
2018-10-28 10:20:43 +01:00
|
|
|
terminate(State, {stop, {exit, Reason}, 'sys:terminate/2,3 was called.'}).
|
2015-06-11 17:04:21 +02:00
|
|
|
|
|
|
|
-spec system_code_change(Misc, _, _, _) -> {ok, Misc} when Misc::{#state{}, binary()}.
|
|
|
|
system_code_change(Misc, _, _, _) ->
|
|
|
|
{ok, Misc}.
|