From 4e6a4ee53f8453c900c5439100a249ebb278deda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Sat, 12 Mar 2016 18:25:35 +0100 Subject: 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. --- src/cowboy_http.erl | 71 ++++++++++++++++++----- src/cowboy_http2.erl | 68 ++++++++++++++-------- test/rfc7540_SUITE.erl | 150 ++++++++++++++++++++++++++++++++++--------------- 3 files changed, 208 insertions(+), 81 deletions(-) diff --git a/src/cowboy_http.erl b/src/cowboy_http.erl index d694cff..1582770 100644 --- a/src/cowboy_http.erl +++ b/src/cowboy_http.erl @@ -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= diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl index 67efa61..d73c879 100644 --- a/src/cowboy_http2.erl +++ b/src/cowboy_http2.erl @@ -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, _, _}) -> diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index 620d3b5..9fcc08c 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -70,7 +70,7 @@ http_upgrade_ignore_if_http_10(Config) -> "GET / HTTP/1.0\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000), @@ -84,13 +84,13 @@ http_upgrade_ignore_missing_upgrade_in_connection(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000), ok. -http_upgrade_reject_missing_http2_settings_in_connection(Config) -> +http_upgrade_ignore_missing_http2_settings_in_connection(Config) -> doc("The HTTP2-Settings header must be listed in the " "Connection header field. (RFC7540 3.2.1, RFC7230 6.7)"), {ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]), @@ -98,10 +98,10 @@ http_upgrade_reject_missing_http2_settings_in_connection(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 400">>} = gen_tcp:recv(Socket, 12, 1000), + {ok, <<"HTTP/1.1 200">>} = gen_tcp:recv(Socket, 12, 1000), ok. http_upgrade_reject_zero_http2_settings_header(Config) -> @@ -112,7 +112,7 @@ http_upgrade_reject_zero_http2_settings_header(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "\r\n"]), {ok, <<"HTTP/1.1 400">>} = gen_tcp:recv(Socket, 12, 1000), ok. @@ -125,7 +125,7 @@ http_upgrade_reject_two_http2_settings_header(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), @@ -140,13 +140,23 @@ http_upgrade_reject_bad_http2_settings_header(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" %% We send a full SETTINGS frame on purpose. "HTTP2-Settings: ", base64:encode(cow_http2:settings(#{})), "\r\n", "\r\n"]), {ok, <<"HTTP/1.1 400">>} = gen_tcp:recv(Socket, 12, 1000), ok. +%% Match directly for now. +do_recv_101(Socket) -> + {ok, << + "HTTP/1.1 101 Switching Protocols\r\n" + "connection: Upgrade\r\n" + "upgrade: h2c\r\n" + "\r\n" + >>} = gen_tcp:recv(Socket, 71, 1000), + ok. + http_upgrade_101(Config) -> doc("A 101 response must be sent on successful upgrade " "to HTTP/2 when using the HTTP Upgrade mechanism. (RFC7540 3.2, RFC7230 6.7)"), @@ -155,10 +165,10 @@ http_upgrade_101(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), ok. http_upgrade_server_preface(Config) -> @@ -169,10 +179,10 @@ http_upgrade_server_preface(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), %% Receive the server preface. {ok, << _:24, 4:8, 0:40 >>} = gen_tcp:recv(Socket, 9, 1000), ok. @@ -185,15 +195,15 @@ http_upgrade_client_preface_timeout(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Do not send the preface. Wait for the server to disconnect us. - {error, closed} = gen_tcp:recv(Socket, 3, 6000), + {error, closed} = gen_tcp:recv(Socket, 9, 6000), ok. http_upgrade_reject_missing_client_preface(Config) -> @@ -204,17 +214,17 @@ http_upgrade_reject_missing_client_preface(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), %% Send a SETTINGS frame directly instead of the proper preface. ok = gen_tcp:send(Socket, cow_http2:settings(#{})), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. - {error, closed} = gen_tcp:recv(Socket, 3, 1000), + {error, closed} = gen_tcp:recv(Socket, 9, 1000), ok. http_upgrade_reject_invalid_client_preface(Config) -> @@ -225,18 +235,35 @@ http_upgrade_reject_invalid_client_preface(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), %% Send a slightly incorrect preface. ok = gen_tcp:send(Socket, "PRI * HTTP/2.0\r\n\r\nSM: Value\r\n\r\n"), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. - {error, closed} = gen_tcp:recv(Socket, 3, 1000), - ok. + %% The server may however have already started sending the response to the + %% initial HTTP/1.1 request. + Received = lists:reverse(lists:foldl(fun(_, Acc) -> + case gen_tcp:recv(Socket, 9, 1000) of + {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + [headers|Acc]; + {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + [data|Acc]; + {error, _} -> + [closed|Acc] + end + end, [], [1, 2, 3])), + case Received of + [closed|_] -> ok; + [headers, closed|_] -> ok; + [headers, data, closed] -> ok + end. http_upgrade_reject_missing_client_preface_settings(Config) -> doc("Servers must treat an invalid connection preface as a " @@ -246,17 +273,17 @@ http_upgrade_reject_missing_client_preface_settings(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), %% Send a valid preface sequence except followed by a PING instead of a SETTINGS frame. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:ping(0)]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. - {error, closed} = gen_tcp:recv(Socket, 3, 1000), + {error, closed} = gen_tcp:recv(Socket, 9, 1000), ok. http_upgrade_reject_invalid_client_preface_settings(Config) -> @@ -267,18 +294,35 @@ http_upgrade_reject_invalid_client_preface_settings(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), %% Send a valid preface sequence except followed by a badly formed SETTINGS frame. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", << 0:24, 4:8, 0:9, 1:31 >>]), %% Receive the server preface. {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. - {error, closed} = gen_tcp:recv(Socket, 3, 1000), - ok. + %% The server may however have already started sending the response to the + %% initial HTTP/1.1 request. + Received = lists:reverse(lists:foldl(fun(_, Acc) -> + case gen_tcp:recv(Socket, 9, 1000) of + {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + [headers|Acc]; + {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + [data|Acc]; + {error, _} -> + [closed|Acc] + end + end, [], [1, 2, 3])), + case Received of + [closed|_] -> ok; + [headers, closed|_] -> ok; + [headers, data, closed] -> ok + end. http_upgrade_accept_client_preface_empty_settings(Config) -> doc("The SETTINGS frame in the client preface may be empty. (RFC7540 3.2, RFC7540 3.5)"), @@ -287,10 +331,10 @@ http_upgrade_accept_client_preface_empty_settings(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), %% Send a valid preface sequence except followed by an empty SETTINGS frame. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. @@ -307,10 +351,10 @@ http_upgrade_client_preface_settings_ack_timeout(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Receive the server preface. @@ -324,6 +368,8 @@ http_upgrade_client_preface_settings_ack_timeout(Config) -> %% @todo We need a successful test with actual options in HTTP2-Settings. +%% @todo We need to test an upgrade with a request body (small and too large). + %% @todo Also assigned default priority values but not sure how to test that. http_upgrade_response(Config) -> doc("A response must be sent to the initial HTTP/1.1 request " @@ -334,10 +380,10 @@ http_upgrade_response(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), %% Send a valid preface. %% @todo Use non-empty SETTINGS here. Just because. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), @@ -346,12 +392,24 @@ http_upgrade_response(Config) -> {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% Send the SETTINGS ack. ok = gen_tcp:send(Socket, cow_http2:settings_ack()), - %% Receive the SETTINGS ack. - %% @todo It's possible that we receive the response before the SETTINGS ack. - {ok, << 0:24, 4:8, 1:8, 0:32 >>} = gen_tcp:recv(Socket, 9, 1000), - %% Receive the response to the original request. It uses streamid 1. - {ok, << _:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000), - ok. + %% Receive the SETTINGS ack, and the response HEADERS and DATA (streamid 1). + Received = lists:reverse(lists:foldl(fun(_, Acc) -> + case gen_tcp:recv(Socket, 9, 1000) of + {ok, << 0:24, 4:8, 1:8, 0:32 >>} -> + [settings_ack|Acc]; + {ok, << SkipLen:24, 1:8, _:8, 1:32 >>} -> + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + [headers|Acc]; + {ok, << SkipLen:24, 0:8, _:8, 1:32 >>} -> + {ok, _} = gen_tcp:recv(Socket, SkipLen, 1000), + [data|Acc] + end + end, [], [1, 2, 3])), + case Received of + [settings_ack, headers, data] -> ok; + [headers, settings_ack, data] -> ok; + [headers, data, settings_ack] -> ok + end. http_upgrade_response_half_closed(Config) -> doc("The stream for the initial HTTP/1.1 request is half-closed. (RFC7540 3.2)"), @@ -361,10 +419,10 @@ http_upgrade_response_half_closed(Config) -> "GET / HTTP/1.1\r\n" "Host: localhost\r\n" "Connection: Upgrade, HTTP2-Settings\r\n" - "Upgrade: h2\r\n" + "Upgrade: h2c\r\n" "HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n", "\r\n"]), - {ok, <<"HTTP/1.1 101 Switching Protocols\r\n">>} = gen_tcp:recv(Socket, 36, 1000), + ok = do_recv_101(Socket), %% Send a valid preface. ok = gen_tcp:send(Socket, ["PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n", cow_http2:settings(#{})]), %% Send more data on the stream to trigger an error. @@ -567,7 +625,7 @@ prior_knowledge_reject_invalid_client_preface(Config) -> {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. - {error, closed} = gen_tcp:recv(Socket, 3, 1000), + {error, closed} = gen_tcp:recv(Socket, 9, 1000), ok. prior_knowledge_reject_missing_client_preface_settings(Config) -> @@ -580,7 +638,7 @@ prior_knowledge_reject_missing_client_preface_settings(Config) -> {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. - {error, closed} = gen_tcp:recv(Socket, 3, 1000), + {error, closed} = gen_tcp:recv(Socket, 9, 1000), ok. prior_knowledge_reject_invalid_client_preface_settings(Config) -> @@ -593,7 +651,7 @@ prior_knowledge_reject_invalid_client_preface_settings(Config) -> {ok, << Len:24 >>} = gen_tcp:recv(Socket, 3, 1000), {ok, << 4:8, 0:40, _:Len/binary >>} = gen_tcp:recv(Socket, 6 + Len, 1000), %% We expect the server to close the connection when it receives a bad preface. - {error, closed} = gen_tcp:recv(Socket, 3, 1000), + {error, closed} = gen_tcp:recv(Socket, 9, 1000), ok. prior_knowledge_accept_client_preface_empty_settings(Config) -> -- cgit v1.2.3