Browse Source

Add many rfc7540 tests, improve detection of malformed requests

Loïc Hoguin 7 years ago
parent
commit
73126e7693
2 changed files with 734 additions and 66 deletions
  1. 141 65
      src/cowboy_http2.erl
  2. 593 1
      test/rfc7540_SUITE.erl

+ 141 - 65
src/cowboy_http2.erl

@@ -611,7 +611,12 @@ commands(State0=#state{socket=Socket, transport=Transport, server_streamid=Promi
 	{HeaderBlock, EncodeState} = headers_encode(Headers, EncodeState0),
 	{HeaderBlock, EncodeState} = headers_encode(Headers, EncodeState0),
 	Transport:send(Socket, cow_http2:push_promise(StreamID, PromisedStreamID, HeaderBlock)),
 	Transport:send(Socket, cow_http2:push_promise(StreamID, PromisedStreamID, HeaderBlock)),
 	State = stream_req_init(State0#state{server_streamid=PromisedStreamID + 2,
 	State = stream_req_init(State0#state{server_streamid=PromisedStreamID + 2,
-		encode_state=EncodeState}, PromisedStreamID, fin, Headers),
+		encode_state=EncodeState}, PromisedStreamID, fin, Headers1, #{
+			method => Method,
+			scheme => Scheme,
+			authority => Authority,
+			path => Path
+		}),
 	commands(State, Stream, Tail);
 	commands(State, Stream, Tail);
 commands(State=#state{socket=Socket, transport=Transport, remote_window=ConnWindow},
 commands(State=#state{socket=Socket, transport=Transport, remote_window=ConnWindow},
 		Stream=#stream{id=StreamID, remote_window=StreamWindow},
 		Stream=#stream{id=StreamID, remote_window=StreamWindow},
@@ -772,6 +777,15 @@ queue_data(Stream=#stream{local_buffer=Q0, local_buffer_size=Size0}, IsFin, Data
 	Q = queue:In({IsFin, DataSize, Data}, Q0),
 	Q = queue:In({IsFin, DataSize, Data}, Q0),
 	Stream#stream{local_buffer=Q, local_buffer_size=Size0 + DataSize}.
 	Stream#stream{local_buffer=Q, local_buffer_size=Size0 + DataSize}.
 
 
+%% The set-cookie header is special; we can only send one cookie per header.
+headers_encode(Headers0=#{<<"set-cookie">> := SetCookies}, EncodeState) ->
+	Headers1 = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)),
+	Headers = Headers1 ++ [{<<"set-cookie">>, Value} || Value <- SetCookies],
+	cow_hpack:encode(Headers, EncodeState);
+headers_encode(Headers0, EncodeState) ->
+	Headers = maps:to_list(Headers0),
+	cow_hpack:encode(Headers, EncodeState).
+
 -spec terminate(#state{}, _) -> no_return().
 -spec terminate(#state{}, _) -> no_return().
 terminate(undefined, Reason) ->
 terminate(undefined, Reason) ->
 	exit({shutdown, Reason});
 	exit({shutdown, Reason});
@@ -801,26 +815,100 @@ terminate_all_streams([#stream{id=StreamID, state=StreamState}|Tail], Reason) ->
 
 
 %% Stream functions.
 %% Stream functions.
 
 
-stream_decode_init(State=#state{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
-		{Headers=#{<<":method">> := _, <<":scheme">> := _,
-				<<":authority">> := _, <<":path">> := _}, DecodeState} ->
-			stream_req_init(State#state{decode_state=DecodeState}, StreamID, IsFin, Headers);
-		{_, DecodeState} ->
-			Transport:send(Socket, cow_http2:rst_stream(StreamID, protocol_error)),
-			State#state{decode_state=DecodeState}
+stream_decode_init(State=#state{decode_state=DecodeState0}, StreamID, IsFin, HeaderBlock) ->
+	try cow_hpack:decode(HeaderBlock, DecodeState0) of
+		{Headers, DecodeState} ->
+			stream_pseudo_headers_init(State#state{decode_state=DecodeState},
+				StreamID, IsFin, Headers)
 	catch _:_ ->
 	catch _:_ ->
 		terminate(State, {connection_error, compression_error,
 		terminate(State, {connection_error, compression_error,
 			'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_pseudo_headers_init(State, StreamID, IsFin, Headers0) ->
+	case pseudo_headers(Headers0, #{}) of
+		%% @todo Add clause for CONNECT requests (no scheme/path).
+		{ok, PseudoHeaders=#{method := _, scheme := _, authority := _, path := _}, Headers} ->
+			stream_regular_headers_init(State, StreamID, IsFin, Headers, PseudoHeaders);
+		{ok, _, _} ->
+			stream_malformed(State, StreamID,
+				'A required pseudo-header was not found. (RFC7540 8.1.2.3)');
+		{error, HumanReadable} ->
+			stream_malformed(State, StreamID, HumanReadable)
+	end.
+
+pseudo_headers([{<<":method">>, _}|_], #{method := _}) ->
+	{error, 'Multiple :method pseudo-headers were found. (RFC7540 8.1.2.3)'};
+pseudo_headers([{<<":method">>, Method}|Tail], PseudoHeaders) ->
+	pseudo_headers(Tail, PseudoHeaders#{method => Method});
+pseudo_headers([{<<":scheme">>, _}|_], #{scheme := _}) ->
+	{error, 'Multiple :scheme pseudo-headers were found. (RFC7540 8.1.2.3)'};
+pseudo_headers([{<<":scheme">>, Scheme}|Tail], PseudoHeaders) ->
+	pseudo_headers(Tail, PseudoHeaders#{scheme => Scheme});
+pseudo_headers([{<<":authority">>, _}|_], #{authority := _}) ->
+	{error, 'Multiple :authority pseudo-headers were found. (RFC7540 8.1.2.3)'};
+pseudo_headers([{<<":authority">>, Authority}|Tail], PseudoHeaders) ->
+	%% @todo Probably parse the authority here.
+	pseudo_headers(Tail, PseudoHeaders#{authority => Authority});
+pseudo_headers([{<<":path">>, _}|_], #{path := _}) ->
+	{error, 'Multiple :path pseudo-headers were found. (RFC7540 8.1.2.3)'};
+pseudo_headers([{<<":path">>, Path}|Tail], PseudoHeaders) ->
+	%% @todo Probably parse the path here.
+	pseudo_headers(Tail, PseudoHeaders#{path => Path});
+pseudo_headers([{<<":", _/bits>>, _}|_], _) ->
+	{error, 'An unknown or invalid pseudo-header was found. (RFC7540 8.1.2.1)'};
+pseudo_headers(Headers, PseudoHeaders) ->
+	{ok, PseudoHeaders, Headers}.
+
+stream_regular_headers_init(State, StreamID, IsFin, Headers, PseudoHeaders) ->
+	case regular_headers(Headers) of
+		ok ->
+			stream_req_init(State, StreamID, IsFin,
+				headers_to_map(Headers, #{}), PseudoHeaders);
+		{error, HumanReadable} ->
+			stream_malformed(State, StreamID, HumanReadable)
+	end.
+
+regular_headers([{<<":", _/bits>>, _}|_]) ->
+	{error, 'Pseudo-headers were found after regular headers. (RFC7540 8.1.2.1)'};
+regular_headers([{<<"connection">>, _}|_]) ->
+	{error, 'The connection header is not allowed. (RFC7540 8.1.2.2)'};
+regular_headers([{<<"keep-alive">>, _}|_]) ->
+	{error, 'The keep-alive header is not allowed. (RFC7540 8.1.2.2)'};
+regular_headers([{<<"proxy-authenticate">>, _}|_]) ->
+	{error, 'The proxy-authenticate header is not allowed. (RFC7540 8.1.2.2)'};
+regular_headers([{<<"proxy-authorization">>, _}|_]) ->
+	{error, 'The proxy-authorization header is not allowed. (RFC7540 8.1.2.2)'};
+regular_headers([{<<"transfer-encoding">>, _}|_]) ->
+	{error, 'The transfer-encoding header is not allowed. (RFC7540 8.1.2.2)'};
+regular_headers([{<<"upgrade">>, _}|_]) ->
+	{error, 'The upgrade header is not allowed. (RFC7540 8.1.2.2)'};
+regular_headers([{<<"te">>, Value}|_]) when Value =/= <<"trailers">> ->
+	{error, 'The te header with a value other than "trailers" is not allowed. (RFC7540 8.1.2.2)'};
+regular_headers([{Name, _}|Tail]) ->
+	case cowboy_bstr:to_lower(Name) of
+		Name -> regular_headers(Tail);
+		_ -> {error, 'Header names must be lowercase. (RFC7540 8.1.2)'}
+	end;
+regular_headers([]) ->
+	ok.
+
+%% 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).
+
 stream_req_init(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert},
 stream_req_init(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert},
-		StreamID, IsFin, Headers0=#{
-			<<":method">> := Method, <<":scheme">> := Scheme,
-			<<":authority">> := Authority, <<":path">> := PathWithQs}) ->
-	Headers = maps:without([<<":method">>, <<":scheme">>, <<":authority">>, <<":path">>], Headers0),
+		StreamID, IsFin, Headers, #{method := Method, scheme := Scheme,
+			authority := Authority, path := PathWithQs}) ->
 	BodyLength = case Headers of
 	BodyLength = case Headers of
 		_ when IsFin =:= fin ->
 		_ when IsFin =:= fin ->
 			0;
 			0;
@@ -836,28 +924,44 @@ stream_req_init(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert},
 		_ ->
 		_ ->
 			undefined
 			undefined
 	end,
 	end,
-	%% @todo If this fails to parse we want to gracefully handle the crash.
-	{Host, Port} = cow_http_hd:parse_host(Authority),
-	{Path, Qs} = cow_http:parse_fullpath(PathWithQs),
-	Req = #{
-		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,
-		has_body => IsFin =:= nofin,
-		body_length => BodyLength
-	},
-	stream_handler_init(State, StreamID, IsFin, idle, Req).
+	try cow_http_hd:parse_host(Authority) of
+		{Host, Port} ->
+			try cow_http:parse_fullpath(PathWithQs) of
+				{<<>>, _} ->
+					stream_malformed(State, StreamID,
+						'The path component must not be empty. (RFC7540 8.1.2.3)');
+				{Path, Qs} ->
+					Req = #{
+						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,
+						has_body => IsFin =:= nofin,
+						body_length => BodyLength
+					},
+					stream_handler_init(State, StreamID, IsFin, idle, Req)
+			catch _:_ ->
+				stream_malformed(State, StreamID,
+					'The :path pseudo-header is invalid. (RFC7540 8.1.2.3)')
+			end
+	catch _:_ ->
+		stream_malformed(State, StreamID,
+			'The :authority pseudo-header is invalid. (RFC7540 8.1.2.3)')
+	end.
+
+stream_malformed(State=#state{socket=Socket, transport=Transport}, StreamID, _) ->
+	Transport:send(Socket, cow_http2:rst_stream(StreamID, protocol_error)),
+	State.
 
 
 stream_handler_init(State=#state{opts=Opts,
 stream_handler_init(State=#state{opts=Opts,
 		local_settings=#{initial_window_size := RemoteWindow},
 		local_settings=#{initial_window_size := RemoteWindow},
@@ -957,34 +1061,6 @@ stream_call_terminate(StreamID, Reason, StreamState) ->
 			Class, Exception, erlang:get_stacktrace())
 			Class, Exception, erlang:get_stacktrace())
 	end.
 	end.
 
 
-%% Headers encode/decode.
-
-headers_decode(HeaderBlock, DecodeState0) ->
-	{Headers, DecodeState} = cow_hpack:decode(HeaderBlock, DecodeState0),
-	{headers_to_map(Headers, #{}), DecodeState}.
-
-%% 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).
-
-%% The set-cookie header is special; we can only send one cookie per header.
-headers_encode(Headers0=#{<<"set-cookie">> := SetCookies}, EncodeState) ->
-	Headers1 = maps:to_list(maps:remove(<<"set-cookie">>, Headers0)),
-	Headers = Headers1 ++ [{<<"set-cookie">>, Value} || Value <- SetCookies],
-	cow_hpack:encode(Headers, EncodeState);
-headers_encode(Headers0, EncodeState) ->
-	Headers = maps:to_list(Headers0),
-	cow_hpack:encode(Headers, EncodeState).
-
 %% System callbacks.
 %% System callbacks.
 
 
 -spec system_continue(_, _, {#state{}, binary()}) -> ok.
 -spec system_continue(_, _, {#state{}, binary()}) -> ok.

+ 593 - 1
test/rfc7540_SUITE.erl

@@ -17,6 +17,7 @@
 
 
 -import(ct_helper, [config/2]).
 -import(ct_helper, [config/2]).
 -import(ct_helper, [doc/1]).
 -import(ct_helper, [doc/1]).
+-import(cowboy_test, [gun_open/1]).
 -import(cowboy_test, [raw_open/1]).
 -import(cowboy_test, [raw_open/1]).
 -import(cowboy_test, [raw_send/2]).
 -import(cowboy_test, [raw_send/2]).
 -import(cowboy_test, [raw_recv_head/1]).
 -import(cowboy_test, [raw_recv_head/1]).
@@ -45,7 +46,8 @@ end_per_group(Name, _) ->
 init_routes(_) -> [
 init_routes(_) -> [
 	{"localhost", [
 	{"localhost", [
 		{"/", hello_h, []},
 		{"/", hello_h, []},
-		{"/echo/:key", echo_h, []}
+		{"/echo/:key", echo_h, []},
+		{"/resp/:key[/:arg]", resp_h, []}
 	]}
 	]}
 ].
 ].
 
 
@@ -2731,3 +2733,593 @@ settings_initial_window_size_reject_overflow(Config) ->
 	%% Receive a FLOW_CONTROL_ERROR connection error.
 	%% Receive a FLOW_CONTROL_ERROR connection error.
 	{ok, << _:24, 7:8, _:72, 3:32 >>} = gen_tcp:recv(Socket, 17, 6000),
 	{ok, << _:24, 7:8, _:72, 3:32 >>} = gen_tcp:recv(Socket, 17, 6000),
 	ok.
 	ok.
+
+%% (RFC7540 6.9.3)
+%% @todo The right way to do this seems to be to wait for the SETTINGS ack
+%% before we KNOW the flow control window was updated on the other side.
+%   A receiver that wishes to use a smaller flow-control window than the
+%   current size can send a new SETTINGS frame.  However, the receiver
+%   MUST be prepared to receive data that exceeds this window size, since
+%   the sender might send data that exceeds the lower limit prior to
+%   processing the SETTINGS frame.
+
+%% (RFC7540 6.10) CONTINUATION
+%   CONTINUATION frames MUST be associated with a stream.  If a
+%   CONTINUATION frame is received whose stream identifier field is 0x0,
+%   the recipient MUST respond with a connection error (Section 5.4.1) of
+%   type PROTOCOL_ERROR.
+%
+%   A CONTINUATION frame MUST be preceded by a HEADERS, PUSH_PROMISE or
+%   CONTINUATION frame without the END_HEADERS flag set.  A recipient
+%   that observes violation of this rule MUST respond with a connection
+%   error (Section 5.4.1) of type PROTOCOL_ERROR.
+
+%% (RFC7540 7) Error Codes
+%   Unknown or unsupported error codes MUST NOT trigger any special
+%   behavior.  These MAY be treated by an implementation as being
+%   equivalent to INTERNAL_ERROR.
+
+%% (RFC7540 8.1)
+%   A HEADERS frame (and associated CONTINUATION frames) can only appear
+%   at the start or end of a stream.  An endpoint that receives a HEADERS
+%   frame without the END_STREAM flag set after receiving a final (non-
+%   informational) status code MUST treat the corresponding request or
+%   response as malformed (Section 8.1.2.6).
+%
+%% @todo This one is interesting to implement because Cowboy DOES this.
+%   A server can
+%   send a complete response prior to the client sending an entire
+%   request if the response does not depend on any portion of the request
+%   that has not been sent and received.  When this is true, a server MAY
+%   request that the client abort transmission of a request without error
+%   by sending a RST_STREAM with an error code of NO_ERROR after sending
+%   a complete response (i.e., a frame with the END_STREAM flag).
+
+headers_reject_uppercase_header_name(Config) ->
+	doc("Requests containing uppercase header names must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a uppercase header name.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<"HELLO">>, <<"world">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_response_pseudo_headers(Config) ->
+	doc("Requests containing response pseudo-headers must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.1, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a response pseudo-header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<":status">>, <<"200">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_unknown_pseudo_headers(Config) ->
+	doc("Requests containing unknown pseudo-headers must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.1, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with an unknown pseudo-header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<":upgrade">>, <<"websocket">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+%% @todo Implement request trailers. reject_pseudo_headers_in_trailers(Config) ->
+%   Pseudo-header fields MUST NOT appear in trailers.
+%   Endpoints MUST treat a request or response that contains
+%   undefined or invalid pseudo-header fields as malformed
+%   (Section 8.1.2.6).
+
+reject_pseudo_headers_after_regular_headers(Config) ->
+	doc("Requests containing pseudo-headers after regular headers must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.1, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a pseudo-header after regular headers.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<"content-length">>, <<"0">>},
+		{<<":path">>, <<"/">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_connection_header(Config) ->
+	doc("Requests containing a connection header must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a connection header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<"connection">>, <<"close">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_keep_alive_header(Config) ->
+	doc("Requests containing a keep-alive header must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a keep-alive header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<"keep-alive">>, <<"timeout=5, max=1000">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_proxy_authenticate_header(Config) ->
+	doc("Requests containing a connection header must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a proxy-authenticate header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<"proxy-authenticate">>, <<"Basic">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_proxy_authorization_header(Config) ->
+	doc("Requests containing a connection header must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a proxy-authorization header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<"proxy-authorization">>, <<"Basic YWxhZGRpbjpvcGVuc2VzYW1l">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_transfer_encoding_header(Config) ->
+	doc("Requests containing a connection header must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a transfer-encoding header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<"transfer-encoding">>, <<"chunked">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_upgrade_header(Config) ->
+	doc("Requests containing a connection header must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a upgrade header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<"upgrade">>, <<"websocket">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+accept_te_header_value_trailers(Config) ->
+	doc("Requests containing a TE header with a value of \"trailers\" "
+		"must be accepted. (RFC7540 8.1.2.2)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a TE header with value "trailers".
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<"te">>, <<"trailers">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a response.
+	{ok, << _:24, 1:8, _:8, 1:32 >>} = gen_tcp:recv(Socket, 9, 1000),
+	ok.
+
+reject_te_header_other_values(Config) ->
+	doc("Requests containing a TE header with a value other than \"trailers\" must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.2, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a TE header with a different value.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>},
+		{<<"te">>, <<"trailers, deflate;q=0.5">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+%% (RFC7540 8.1.2.2)
+%   This means that an intermediary transforming an HTTP/1.x message to
+%   HTTP/2 will need to remove any header fields nominated by the
+%   Connection header field, along with the Connection header field
+%   itself.  Such intermediaries SHOULD also remove other connection-
+%   specific header fields, such as Keep-Alive, Proxy-Connection,
+%   Transfer-Encoding, and Upgrade, even if they are not nominated by the
+%   Connection header field.
+
+reject_userinfo(Config) ->
+	doc("An authority containing a userinfo component must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with a userinfo authority component.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"user@localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<"/">>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+%% (RFC7540 8.1.2.3)
+%      To ensure that the HTTP/1.1 request line can be reproduced
+%      accurately, this pseudo-header field MUST be omitted when
+%      translating from an HTTP/1.1 request that has a request target in
+%      origin or asterisk form (see [RFC7230], Section 5.3).  Clients
+%      that generate HTTP/2 requests directly SHOULD use the ":authority"
+%      pseudo-header field instead of the Host header field.  An
+%      intermediary that converts an HTTP/2 request to HTTP/1.1 MUST
+%      create a Host header field if one is not present in a request by
+%      copying the value of the ":authority" pseudo-header field.
+
+reject_empty_path(Config) ->
+	doc("A request containing an empty path component must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with an empty path component.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<>>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_missing_pseudo_header_method(Config) ->
+	doc("A request without a method component must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame without a :method pseudo-header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<>>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_many_pseudo_header_method(Config) ->
+	doc("A request containing more than one method component must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with more than one :method pseudo-header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<>>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_missing_pseudo_header_scheme(Config) ->
+	doc("A request without a scheme component must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame without a :scheme pseudo-header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<>>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_many_pseudo_header_scheme(Config) ->
+	doc("A request containing more than one scheme component must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with more than one :scheme pseudo-header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<>>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_missing_pseudo_header_authority(Config) ->
+	doc("A request without an authority component must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame without an :authority pseudo-header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":path">>, <<>>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_many_pseudo_header_authority(Config) ->
+	doc("A request containing more than one authority component must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with more than one :authority pseudo-header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<>>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_missing_pseudo_header_path(Config) ->
+	doc("A request without a path component must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame without a :path pseudo-header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>} %% @todo Correct port number.
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+reject_many_pseudo_header_path(Config) ->
+	doc("A request containing more than one path component must be rejected "
+		"with a PROTOCOL_ERROR stream error. (RFC7540 8.1.2.3, RFC7540 8.1.2.6)"),
+	{ok, Socket} = do_handshake(Config),
+	%% Send a HEADERS frame with more than one :path pseudo-header.
+	{HeadersBlock, _} = cow_hpack:encode([
+		{<<":method">>, <<"GET">>},
+		{<<":scheme">>, <<"http">>},
+		{<<":authority">>, <<"localhost">>}, %% @todo Correct port number.
+		{<<":path">>, <<>>},
+		{<<":path">>, <<>>}
+	]),
+	ok = gen_tcp:send(Socket, cow_http2:headers(1, fin, HeadersBlock)),
+	%% Receive a PROTOCOL_ERROR stream error.
+	{ok, << _:24, 3:8, _:8, 1:32, 1:32 >>} = gen_tcp:recv(Socket, 13, 6000),
+	ok.
+
+%% (RFC7540 8.1.2.4)
+%   For HTTP/2 responses, a single ":status" pseudo-header field is
+%   defined that carries the HTTP status code field (see [RFC7231],
+%   Section 6).  This pseudo-header field MUST be included in all
+%   responses; otherwise, the response is malformed (Section 8.1.2.6).
+
+%% (RFC7540 8.1.2.5)
+%   To allow for better compression efficiency, the Cookie header field
+%   MAY be split into separate header fields, each with one or more
+%   cookie-pairs.  If there are multiple Cookie header fields after
+%   decompression, these MUST be concatenated into a single octet string
+%   using the two-octet delimiter of 0x3B, 0x20 (the ASCII string "; ")
+%   before being passed into a non-HTTP/2 context, such as an HTTP/1.1
+%   connection, or a generic HTTP server application.
+
+%% (RFC7540 8.1.2.6)
+%   A request or response that includes a payload body can include a
+%   content-length header field.  A request or response is also malformed
+%   if the value of a content-length header field does not equal the sum
+%   of the DATA frame payload lengths that form the body.  A response
+%   that is defined to have no payload, as described in [RFC7230],
+%   Section 3.3.2, can have a non-zero content-length header field, even
+%   though no content is included in DATA frames.
+%
+%   Intermediaries that process HTTP requests or responses (i.e., any
+%   intermediary not acting as a tunnel) MUST NOT forward a malformed
+%   request or response.  Malformed requests or responses that are
+%   detected MUST be treated as a stream error (Section 5.4.2) of type
+%   PROTOCOL_ERROR.
+%
+%   For malformed requests, a server MAY send an HTTP response prior to
+%   closing or resetting the stream.  Clients MUST NOT accept a malformed
+%   response.  Note that these requirements are intended to protect
+%   against several types of common attacks against HTTP; they are
+%   deliberately strict because being permissive can expose
+%   implementations to these vulnerabilities.
+
+%% @todo It migh be worth reproducing the good examples. (RFC7540 8.1.3)
+
+%% (RFC7540 8.1.4)
+%   A server MUST NOT indicate that a stream has not been processed
+%   unless it can guarantee that fact.  If frames that are on a stream
+%   are passed to the application layer for any stream, then
+%   REFUSED_STREAM MUST NOT be used for that stream, and a GOAWAY frame
+%   MUST include a stream identifier that is greater than or equal to the
+%   given stream identifier.
+
+%% (RFC7540 8.2)
+%   Promised requests MUST be cacheable (see [RFC7231], Section 4.2.3),
+%   MUST be safe (see [RFC7231], Section 4.2.1), and MUST NOT include a
+%   request body.
+%
+%   The server MUST include a value in the ":authority" pseudo-header
+%   field for which the server is authoritative (see Section 10.1).
+%
+%   A client cannot push.  Thus, servers MUST treat the receipt of a
+%   PUSH_PROMISE frame as a connection error (Section 5.4.1) of type
+%   PROTOCOL_ERROR.
+
+%% (RFC7540 8.2.1)
+%   The header fields in PUSH_PROMISE and any subsequent CONTINUATION
+%   frames MUST be a valid and complete set of request header fields
+%   (Section 8.1.2.3).  The server MUST include a method in the ":method"
+%   pseudo-header field that is safe and cacheable.  If a client receives
+%   a PUSH_PROMISE that does not include a complete and valid set of
+%   header fields or the ":method" pseudo-header field identifies a
+%   method that is not safe, it MUST respond with a stream error
+%   (Section 5.4.2) of type PROTOCOL_ERROR.
+%
+%% @todo This probably should be documented.
+%   The server SHOULD send PUSH_PROMISE (Section 6.6) frames prior to
+%   sending any frames that reference the promised responses.  This
+%   avoids a race where clients issue requests prior to receiving any
+%   PUSH_PROMISE frames.
+%
+%   PUSH_PROMISE frames MUST NOT be sent by the client.
+%
+%   PUSH_PROMISE frames can be sent by the server in response to any
+%   client-initiated stream, but the stream MUST be in either the "open"
+%   or "half-closed (remote)" state with respect to the server.
+%   PUSH_PROMISE frames are interspersed with the frames that comprise a
+%   response, though they cannot be interspersed with HEADERS and
+%   CONTINUATION frames that comprise a single header block.
+
+%% (RFC7540 8.2.2)
+%   If the client determines, for any reason, that it does not wish to
+%   receive the pushed response from the server or if the server takes
+%   too long to begin sending the promised response, the client can send
+%   a RST_STREAM frame, using either the CANCEL or REFUSED_STREAM code
+%   and referencing the pushed stream's identifier.
+%
+%   A client can use the SETTINGS_MAX_CONCURRENT_STREAMS setting to limit
+%   the number of responses that can be concurrently pushed by a server.
+%   Advertising a SETTINGS_MAX_CONCURRENT_STREAMS value of zero disables
+%   server push by preventing the server from creating the necessary
+%   streams.  This does not prohibit a server from sending PUSH_PROMISE
+%   frames; clients need to reset any promised streams that are not
+%   wanted.
+
+%% @todo Implement CONNECT. (RFC7540 8.3)
+
+status_code_421(Config) ->
+	doc("The 421 Misdirected Request status code can be sent. (RFC7540 9.1.2)"),
+	ConnPid = gun_open(Config),
+	Ref = gun:get(ConnPid, "/resp/reply2/421"),
+	{response, fin, 421, _} = gun:await(ConnPid, Ref),
+	ok.
+
+%% @todo Review (RFC7540 9.2, 9.2.1, 9.2.2) TLS 1.2 usage.
+%% We probably want different ways to enforce these to simplify the life
+%% of users. A function cowboy:start_h2_tls could do the same as start_tls
+%% but with the security requirements of HTTP/2 enforced. Another way is to
+%% have an option at the establishment of the connection that checks that
+%% the security of the connection is adequate.
+
+%% (RFC7540 10.3)
+%   The HTTP/2 header field encoding allows the expression of names that
+%   are not valid field names in the Internet Message Syntax used by
+%   HTTP/1.1.  Requests or responses containing invalid header field
+%   names MUST be treated as malformed (Section 8.1.2.6).
+%
+%   Similarly, HTTP/2 allows header field values that are not valid.
+%   While most of the values that can be encoded will not alter header
+%   field parsing, carriage return (CR, ASCII 0xd), line feed (LF, ASCII
+%   0xa), and the zero character (NUL, ASCII 0x0) might be exploited by
+%   an attacker if they are translated verbatim.  Any request or response
+%   that contains a character not permitted in a header field value MUST
+%   be treated as malformed (Section 8.1.2.6).  Valid characters are
+%   defined by the "field-content" ABNF rule in Section 3.2 of [RFC7230].
+
+%% (RFC7540 10.5) Denial-of-Service Considerations
+%   An endpoint that doesn't monitor this behavior exposes itself to a
+%   risk of denial-of-service attack.  Implementations SHOULD track the
+%   use of these features and set limits on their use.  An endpoint MAY
+%   treat activity that is suspicious as a connection error
+%   (Section 5.4.1) of type ENHANCE_YOUR_CALM.
+
+%% (RFC7540 10.5.1)
+%   A server that receives a larger header block than it is willing to
+%   handle can send an HTTP 431 (Request Header Fields Too Large) status
+%   code [RFC6585].  A client can discard responses that it cannot
+%   process.  The header block MUST be processed to ensure a consistent
+%   connection state, unless the connection is closed.
+
+%% @todo Implement CONNECT and limit the number of CONNECT streams (RFC7540 10.5.2).
+
+%% @todo This probably should be documented. (RFC7540 10.6)
+%   Implementations communicating on a secure channel MUST NOT compress
+%   content that includes both confidential and attacker-controlled data
+%   unless separate compression dictionaries are used for each source of
+%   data.  Compression MUST NOT be used if the source of data cannot be
+%   reliably determined.  Generic stream compression, such as that
+%   provided by TLS, MUST NOT be used with HTTP/2 (see Section 9.2).
+
+%% (RFC7540 A)
+%   An HTTP/2 implementation MAY treat the negotiation of any of the
+%   following cipher suites with TLS 1.2 as a connection error
+%   (Section 5.4.1) of type INADEQUATE_SECURITY.