Browse Source

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.
Loïc Hoguin 8 years ago
parent
commit
6e8b907ae2
3 changed files with 26 additions and 15 deletions
  1. 5 0
      doc/src/manual/cowboy_stream.asciidoc
  2. 1 7
      src/cowboy_http.erl
  3. 20 8
      src/cowboy_http2.erl

+ 5 - 0
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
 related to a stream. Exceptions vary depending on the
 protocol.
 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
 == Callbacks
 
 
 Stream handlers must implement the following interface:
 Stream handlers must implement the following interface:

+ 1 - 7
src/cowboy_http.erl

@@ -635,6 +635,7 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, in_stream
 
 
 %% HTTP/2 upgrade.
 %% HTTP/2 upgrade.
 
 
+%% @todo We must not upgrade to h2c over a TLS connection.
 is_http2_upgrade(#{<<"connection">> := Conn, <<"upgrade">> := Upgrade,
 is_http2_upgrade(#{<<"connection">> := Conn, <<"upgrade">> := Upgrade,
 		<<"http2-settings">> := HTTP2Settings}, 'HTTP/1.1') ->
 		<<"http2-settings">> := HTTP2Settings}, 'HTTP/1.1') ->
 	Conns = cow_http_hd:parse_connection(Conn),
 	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.
 	%% Always half-closed stream coming from this side.
 	try cow_http_hd:parse_http2_settings(HTTP2Settings) of
 	try cow_http_hd:parse_http2_settings(HTTP2Settings) of
 		Settings ->
 		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),
 			_ = cancel_request_timeout(State),
 			cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer, Settings, Req)
 			cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Peer, Buffer, Settings, Req)
 	catch _:_ ->
 	catch _:_ ->

+ 20 - 8
src/cowboy_http2.erl

@@ -27,7 +27,7 @@
 	%% Stream handlers and their state.
 	%% Stream handlers and their state.
 	state = undefined :: {module(), any()},
 	state = undefined :: {module(), any()},
 	%% Whether we finished sending data.
 	%% Whether we finished sending data.
-	local = idle :: idle | cowboy_stream:fin(),
+	local = idle :: idle | upgrade | cowboy_stream:fin(),
 	%% Whether we finished receiving data.
 	%% Whether we finished receiving data.
 	remote = nofin :: cowboy_stream:fin(),
 	remote = nofin :: cowboy_stream:fin(),
 	%% Request body length.
 	%% 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,
 	State0 = #state{parent=Parent, ref=Ref, socket=Socket,
 		transport=Transport, opts=Opts, peer=Peer,
 		transport=Transport, opts=Opts, peer=Peer,
 		parse_state={preface, sequence, preface_timeout(Opts)}},
 		parse_state={preface, sequence, preface_timeout(Opts)}},
-	preface(State0),
 	%% @todo Apply settings.
 	%% @todo Apply settings.
 	%% StreamID from HTTP/1.1 Upgrade requests is always 1.
 	%% StreamID from HTTP/1.1 Upgrade requests is always 1.
 	%% The stream is always in the half-closed (remote) state.
 	%% 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
 	case Buffer of
 		<<>> -> before_loop(State, Buffer);
 		<<>> -> before_loop(State, Buffer);
 		_ -> parse(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 Do we even allow commands after?
 	%% @todo Only reset when the stream still exists.
 	%% @todo Only reset when the stream still exists.
 	stream_reset(after_commands(State, Stream), StreamID, Error);
 	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]) ->
 commands(State, Stream, [{switch_protocol, _Headers, _Mod, _ModState}|Tail]) ->
 	%% @todo This is an error. Not sure what to do here yet.
 	%% @todo This is an error. Not sure what to do here yet.
 	commands(State, Stream, Tail);
 	commands(State, Stream, Tail);
@@ -610,7 +621,7 @@ stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, peer=Peer
 				has_body => IsFin =:= nofin,
 				has_body => IsFin =:= nofin,
 				body_length => BodyLength
 				body_length => BodyLength
 			},
 			},
-			stream_handler_init(State, StreamID, IsFin, Req);
+			stream_handler_init(State, StreamID, IsFin, idle, Req);
 		{_, DecodeState} ->
 		{_, DecodeState} ->
 			Transport:send(Socket, cow_http2:rst_stream(StreamID, protocol_error)),
 			Transport:send(Socket, cow_http2:rst_stream(StreamID, protocol_error)),
 			State0#state{decode_state=DecodeState}
 			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)'})
 			'Error while trying to decode HPACK-encoded header block. (RFC7540 4.3)'})
 	end.
 	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
 	try cowboy_stream:init(StreamID, Req, Opts) of
 		{Commands, StreamState} ->
 		{Commands, StreamState} ->
 			commands(State#state{client_streamid=StreamID},
 			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 ->
 	catch Class:Reason ->
 		error_logger:error_msg("Exception occurred in "
 		error_logger:error_msg("Exception occurred in "
 			"cowboy_stream:init(~p, ~p, ~p) with reason ~p:~p.",
 			"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},
 		stream_reset(State, StreamID, {internal_error, {Class, Reason},
 			'Exception occurred in cowboy_stream:init/3.'})
 			'Exception occurred in cowboy_stream:init/3.'})
 	end.
 	end.