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

Add initial HTTP/1.1 Upgrade to HTTP/2

The same edge cases that fail with other handshake methods
also fail here (mostly bad preface/timeouts stuff). In
addition, the HTTP2-Settings header contents are currently
not checked and so the related edge case tests also fail.
This commit is contained in:
Loïc Hoguin 2016-03-12 18:25:35 +01:00
parent 92edad53d2
commit 4e6a4ee53f
3 changed files with 208 additions and 81 deletions

View file

@ -327,7 +327,7 @@ parse_request(Buffer, State=#state{opts=Opts, in_streamid=InStreamID}, EmptyLine
%% Accept direct HTTP/2 only at the beginning of the connection.
<< "PRI * HTTP/2.0\r\n", _/bits >> when InStreamID =:= 1 ->
%% @todo Might be worth throwing to get a clean stacktrace.
http2_upgrade(State, Buffer, undefined);
http2_upgrade(State, Buffer);
_ ->
parse_method(Buffer, State, <<>>,
maps:get(max_method_length, Opts, 32))
@ -628,31 +628,76 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, in_streamid=StreamID
%% meta values (cowboy_websocket, cowboy_rest)
},
State = case HasBody of
true ->
cancel_request_timeout(State0#state{in_state=#ps_body{
%% @todo Don't need length anymore?
transfer_decode_fun = TDecodeFun,
transfer_decode_state = TDecodeState
}});
case is_http2_upgrade(Headers, Version) of
false ->
set_request_timeout(State0#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}})
end,
{request, Req, State, Buffer}.
State = case HasBody of
true ->
cancel_request_timeout(State0#state{in_state=#ps_body{
%% @todo Don't need length anymore?
transfer_decode_fun = TDecodeFun,
transfer_decode_state = TDecodeState
}});
false ->
set_request_timeout(State0#state{in_streamid=StreamID + 1, in_state=#ps_request_line{}})
end,
{request, Req, State, Buffer};
{true, SettingsPayload} ->
http2_upgrade(State0, Buffer, SettingsPayload, Req)
end.
%% HTTP/2 upgrade.
is_http2_upgrade(#{<<"connection">> := Conn, <<"upgrade">> := Upgrade,
<<"http2-settings">> := HTTP2Settings}, 'HTTP/1.1') ->
Conns = cow_http_hd:parse_connection(Conn),
io:format(user, "CONNS ~p~n", [Conns]),
case {lists:member(<<"upgrade">>, Conns), lists:member(<<"http2-settings">>, Conns)} of
{true, true} ->
Protocols = cow_http_hd:parse_upgrade(Upgrade),
io:format(user, "PROTOCOLS ~p~n", [Protocols]),
case lists:member(<<"h2c">>, Protocols) of
true ->
SettingsPayload = cow_http_hd:parse_http2_settings(HTTP2Settings),
{true, SettingsPayload};
false ->
false
end;
_ ->
false
end;
is_http2_upgrade(_, _) ->
false.
%% Upgrade through an HTTP/1.1 request.
%% Prior knowledge upgrade, without an HTTP/1.1 request.
http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport,
opts=Opts, handler=Handler}, Buffer, Settings) ->
opts=Opts, handler=Handler}, Buffer) ->
case Transport:secure() of
false ->
_ = cancel_request_timeout(State),
cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Handler, Buffer, Settings);
cowboy_http2:init(Parent, Ref, Socket, Transport, Opts, Handler, Buffer);
true ->
error_terminate(400, State, {connection_error, protocol_error,
'Clients that support HTTP/2 over TLS MUST use ALPN. (RFC7540 3.4)'})
end.
http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport,
opts=Opts, handler=Handler}, Buffer, SettingsPayload, Req) ->
%% @todo
%% However if the client sent a body, we need to read the body in full
%% and if we can't do that, return a 413 response. Some options are in order.
%% Always half-closed stream coming from this side.
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, Handler, Buffer, SettingsPayload, Req).
%% Request body parsing.
parse_body(Buffer, State=#state{in_streamid=StreamID, in_state=

View file

@ -15,7 +15,8 @@
-module(cowboy_http2).
-export([init/6]).
-export([init/8]).
-export([init/7]).
-export([init/9]).
-export([system_continue/3]).
-export([system_terminate/4]).
@ -80,11 +81,10 @@
-spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts(), module()) -> ok.
init(Parent, Ref, Socket, Transport, Opts, Handler) ->
init(Parent, Ref, Socket, Transport, Opts, Handler, <<>>, undefined).
init(Parent, Ref, Socket, Transport, Opts, Handler, <<>>).
-spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts(), module(),
binary(), binary() | undefined) -> ok.
init(Parent, Ref, Socket, Transport, Opts, Handler, Buffer, SettingsPayload) ->
-spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts(), module(), binary()) -> ok.
init(Parent, Ref, Socket, Transport, Opts, Handler, Buffer) ->
State = #state{parent=Parent, ref=Ref, socket=Socket,
transport=Transport, opts=Opts, handler=Handler},
preface(State),
@ -93,6 +93,22 @@ init(Parent, Ref, Socket, Transport, Opts, Handler, Buffer, SettingsPayload) ->
_ -> parse(State, Buffer)
end.
%% @todo Add an argument for the request body.
-spec init(pid(), ranch:ref(), inet:socket(), module(), cowboy:opts(), module(),
binary(), binary() | undefined, cowboy_req:req()) -> ok.
init(Parent, Ref, Socket, Transport, Opts, Handler, Buffer, SettingsPayload, Req) ->
State0 = #state{parent=Parent, ref=Ref, socket=Socket,
transport=Transport, opts=Opts, handler=Handler},
preface(State0),
%% @todo SettingsPayload.
%% 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),
case Buffer of
<<>> -> before_loop(State, Buffer);
_ -> parse(State, Buffer)
end.
preface(#state{socket=Socket, transport=Transport, next_settings=Settings}) ->
%% We send next_settings and use defaults until we get a ack.
ok = Transport:send(Socket, cow_http2:settings(Settings)).
@ -317,10 +333,13 @@ commands(State, _, []) ->
%% @todo Keep IsFin in the state.
%% @todo Same two things above apply to DATA, possibly promise too.
commands(State=#state{socket=Socket, transport=Transport, encode_state=EncodeState0}, StreamID,
[{response, IsFin, StatusCode, Headers0}|Tail]) ->
[{response, StatusCode, Headers0, Body}|Tail]) ->
Headers = Headers0#{<<":status">> => integer_to_binary(StatusCode)},
{HeaderBlock, EncodeState} = headers_encode(Headers, EncodeState0),
Transport:send(Socket, cow_http2:headers(StreamID, IsFin, HeaderBlock)),
Transport:send(Socket, [
cow_http2:headers(StreamID, nofin, HeaderBlock),
cow_http2:data(StreamID, fin, Body)
]),
commands(State#state{encode_state=EncodeState}, StreamID, Tail);
%% Send a response body chunk.
%%
@ -361,7 +380,10 @@ commands(State, StreamID, [{upgrade, _Mod, _ModState}]) ->
commands(State, StreamID, []);
commands(State, StreamID, [{upgrade, _Mod, _ModState}|Tail]) ->
%% @todo This is an error. Not sure what to do here yet.
commands(State, StreamID, Tail).
commands(State, StreamID, Tail);
commands(State, StreamID, [stop|Tail]) ->
%% @todo Do we want to run the commands after a stop?
stream_terminate(State, StreamID, stop).
terminate(#state{socket=Socket, transport=Transport, handler=Handler,
streams=Streams, children=Children}, Reason) ->
@ -379,8 +401,8 @@ terminate_all_streams([#stream{id=StreamID, state=StreamState}|Tail], Reason, Ha
%% Stream functions.
stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, handler=Handler, opts=Opts,
streams=Streams0, decode_state=DecodeState0}, StreamID, IsFin, HeaderBlock) ->
stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, decode_state=DecodeState0},
StreamID, IsFin, HeaderBlock) ->
%% @todo Add clause for CONNECT requests (no scheme/path).
try headers_decode(HeaderBlock, DecodeState0) of
{Headers0=#{
@ -425,18 +447,7 @@ stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, handler=H
%% meta values (cowboy_websocket, cowboy_rest)
},
try Handler:init(StreamID, Req, Opts) of
{Commands, StreamState} ->
Streams = [#stream{id=StreamID, state=StreamState}|Streams0],
commands(State#state{streams=Streams}, StreamID, Commands)
catch Class:Reason ->
error_logger:error_msg("Exception occurred in ~s:init(~p, ~p, ~p, ~p, ~p, ~p, ~p) "
"with reason ~p:~p.",
[Handler, StreamID, IsFin, Method, Scheme, Authority, Path, Headers, Class, Reason]),
stream_reset(State, StreamID, {internal_error, {Class, Reason},
'Exception occurred in StreamHandler:init/7 call.'}) %% @todo Check final arity.
end;
stream_handler_init(State, StreamID, IsFin, Req);
{_, DecodeState} ->
Transport:send(Socket, cow_http2:rst_stream(StreamID, protocol_error)),
State0#state{decode_state=DecodeState}
@ -445,6 +456,19 @@ stream_init(State0=#state{ref=Ref, socket=Socket, transport=Transport, handler=H
'Error while trying to decode HPACK-encoded header block. (RFC7540 4.3)'})
end.
stream_handler_init(State=#state{handler=Handler, opts=Opts, streams=Streams0}, StreamID, IsFin, Req) ->
try Handler:init(StreamID, Req, Opts) of
{Commands, StreamState} ->
Streams = [#stream{id=StreamID, state=StreamState, remote=IsFin}|Streams0],
commands(State#state{streams=Streams}, StreamID, Commands)
catch Class:Reason ->
error_logger:error_msg("Exception occurred in ~s:init(~p, ~p, ~p) "
"with reason ~p:~p.",
[Handler, StreamID, IsFin, Req, Class, Reason]),
stream_reset(State, StreamID, {internal_error, {Class, Reason},
'Exception occurred in StreamHandler:init/7 call.'}) %% @todo Check final arity.
end.
%% @todo We might need to keep track of which stream has been reset so we don't send lots of them.
stream_reset(State=#state{socket=Socket, transport=Transport}, StreamID,
StreamError={internal_error, _, _}) ->