From 6e8b907ae25a7a988abbcd7206b978028c36f47e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Tue, 18 Apr 2017 14:06:34 +0200 Subject: Pass the HTTP/2 switch_protocol event to stream handlers To accomplish this the code for sending the 101 response was moved to the cowboy_http2 module. --- doc/src/manual/cowboy_stream.asciidoc | 5 +++++ src/cowboy_http.erl | 8 +------- src/cowboy_http2.erl | 28 ++++++++++++++++++++-------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/doc/src/manual/cowboy_stream.asciidoc b/doc/src/manual/cowboy_stream.asciidoc index d332752..de4c8e0 100644 --- a/doc/src/manual/cowboy_stream.asciidoc +++ b/doc/src/manual/cowboy_stream.asciidoc @@ -18,6 +18,11 @@ Cowboy calls the stream handler for nearly all events related to a stream. Exceptions vary depending on the protocol. +Extra care must be taken when implementing stream handlers +to ensure compatibility. While some modification of the +events and commands is allowed, it is generally not a good +idea to completely omit them. + == Callbacks Stream handlers must implement the following interface: diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index e6cceae..983d95f 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -635,6 +635,7 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_stream %% HTTP/2 upgrade. +%% @todo We must not upgrade to h2c over a TLS connection. is_http2_upgrade(#{<<"connection">> := Conn, <<"upgrade">> := Upgrade, <<"http2-settings">> := HTTP2Settings}, 'HTTP/1.1') -> Conns = cow_http_hd:parse_connection(Conn), @@ -675,13 +676,6 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran %% Always half-closed stream coming from this side. try cow_http_hd:parse_http2_settings(HTTP2Settings) of Settings -> - %% @todo We should invoke cowboy_stream:info for this stream, - %% with a switch_protocol tuple. - Transport:send(Socket, cow_http:response(101, 'HTTP/1.1', maps:to_list(#{ - <<"connection">> => <<"Upgrade">>, - <<"upgrade">> => <<"h2c">> - }))), - %% @todo Possibly redirect the request if it was https. _ = cancel_request_timeout(State), cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer, Settings, Req) catch _:_ -> diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl index 780cf05..afac968 100644 --- a/src/cowboy_http2.erl +++ b/src/cowboy_http2.erl @@ -27,7 +27,7 @@ %% Stream handlers and their state. state = undefined :: {module(), any()}, %% Whether we finished sending data. - local = idle :: idle | cowboy_stream:fin(), + local = idle :: idle | upgrade | cowboy_stream:fin(), %% Whether we finished receiving data. remote = nofin :: cowboy_stream:fin(), %% Request body length. @@ -122,11 +122,17 @@ init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer, _Settings, Req) -> State0 = #state{parent=Parent, ref=Ref, socket=Socket, transport=Transport, opts=Opts, peer=Peer, parse_state={preface, sequence, preface_timeout(Opts)}}, - preface(State0), %% @todo Apply settings. %% StreamID from HTTP/1.1 Upgrade requests is always 1. %% The stream is always in the half-closed (remote) state. - State = stream_handler_init(State0, 1, fin, Req), + State1 = stream_handler_init(State0, 1, fin, upgrade, Req), + %% We assume that the upgrade will be applied. A stream handler + %% must not prevent the normal operations of the server. + State = info(State1, 1, {switch_protocol, #{ + <<"connection">> => <<"Upgrade">>, + <<"upgrade">> => <<"h2c">> + }, ?MODULE, undefined}), %% @todo undefined or #{}? + preface(State), case Buffer of <<>> -> before_loop(State, Buffer); _ -> parse(State, Buffer) @@ -496,7 +502,12 @@ commands(State, Stream=#stream{id=StreamID}, [Error = {internal_error, _, _}|_Ta %% @todo Do we even allow commands after? %% @todo Only reset when the stream still exists. stream_reset(after_commands(State, Stream), StreamID, Error); -%% @todo HTTP/2 has no support for the Upgrade mechanism. +%% Upgrade to HTTP/2. This is triggered by cowboy_http2 itself. +commands(State=#state{socket=Socket, transport=Transport}, + Stream=#stream{local=upgrade}, [{switch_protocol, Headers, ?MODULE, _}|Tail]) -> + Transport:send(Socket, cow_http:response(101, 'HTTP/1.1', maps:to_list(Headers))), + commands(State, Stream#stream{local=idle}, Tail); +%% HTTP/2 has no support for the Upgrade mechanism. commands(State, Stream, [{switch_protocol, _Headers, _Mod, _ModState}|Tail]) -> %% @todo This is an error. Not sure what to do here yet. commands(State, Stream, Tail); @@ -610,7 +621,7 @@ stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, peer=Peer has_body => IsFin =:= nofin, body_length => BodyLength }, - stream_handler_init(State, StreamID, IsFin, Req); + stream_handler_init(State, StreamID, IsFin, idle, Req); {_, DecodeState} -> Transport:send(Socket, cow_http2:rst_stream(StreamID, protocol_error)), State0#state{decode_state=DecodeState} @@ -619,15 +630,16 @@ stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, peer=Peer 'Error while trying to decode HPACK-encoded header block. (RFC7540 4.3)'}) end. -stream_handler_init(State=#state{opts=Opts}, StreamID, IsFin, Req) -> +stream_handler_init(State=#state{opts=Opts}, StreamID, RemoteIsFin, LocalIsFin, Req) -> try cowboy_stream:init(StreamID, Req, Opts) of {Commands, StreamState} -> commands(State#state{client_streamid=StreamID}, - #stream{id=StreamID, state=StreamState, remote=IsFin}, Commands) + #stream{id=StreamID, state=StreamState, + remote=RemoteIsFin, local=LocalIsFin}, Commands) catch Class:Reason -> error_logger:error_msg("Exception occurred in " "cowboy_stream:init(~p, ~p, ~p) with reason ~p:~p.", - [StreamID, IsFin, Req, Class, Reason]), + [StreamID, Req, Opts, Class, Reason]), stream_reset(State, StreamID, {internal_error, {Class, Reason}, 'Exception occurred in cowboy_stream:init/3.'}) end. -- cgit v1.2.3