From 73126e7693387f1865d04fe3d5384ea5060ac2f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Wed, 29 Nov 2017 14:54:47 +0100 Subject: Add many rfc7540 tests, improve detection of malformed requests --- src/cowboy_http2.erl | 206 +++++++++++------ test/rfc7540_SUITE.erl | 594 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 734 insertions(+), 66 deletions(-) diff --git a/src/cowboy_http2.erl b/src/cowboy_http2.erl index 97aac9a..0301099 100644 --- a/src/cowboy_http2.erl +++ b/src/cowboy_http2.erl @@ -611,7 +611,12 @@ commands(State0=#state{socket=Socket, transport=Transport, server_streamid=Promi {HeaderBlock, EncodeState} = headers_encode(Headers, EncodeState0), Transport:send(Socket, cow_http2:push_promise(StreamID, PromisedStreamID, HeaderBlock)), 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=#state{socket=Socket, transport=Transport, remote_window=ConnWindow}, 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), 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(). terminate(undefined, Reason) -> exit({shutdown, Reason}); @@ -801,26 +815,100 @@ terminate_all_streams([#stream{id=StreamID, state=StreamState}|Tail], Reason) -> %% 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 _:_ -> terminate(State, {connection_error, compression_error, 'Error while trying to decode HPACK-encoded header block. (RFC7540 4.3)'}) 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}, - 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 _ when IsFin =:= fin -> 0; @@ -836,28 +924,44 @@ stream_req_init(State=#state{ref=Ref, peer=Peer, sock=Sock, cert=Cert}, _ -> undefined 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, local_settings=#{initial_window_size := RemoteWindow}, @@ -957,34 +1061,6 @@ stream_call_terminate(StreamID, Reason, StreamState) -> Class, Exception, erlang:get_stacktrace()) 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. -spec system_continue(_, _, {#state{}, binary()}) -> ok. diff --git a/test/rfc7540_SUITE.erl b/test/rfc7540_SUITE.erl index a024d64..65321aa 100644 --- a/test/rfc7540_SUITE.erl +++ b/test/rfc7540_SUITE.erl @@ -17,6 +17,7 @@ -import(ct_helper, [config/2]). -import(ct_helper, [doc/1]). +-import(cowboy_test, [gun_open/1]). -import(cowboy_test, [raw_open/1]). -import(cowboy_test, [raw_send/2]). -import(cowboy_test, [raw_recv_head/1]). @@ -45,7 +46,8 @@ end_per_group(Name, _) -> init_routes(_) -> [ {"localhost", [ {"/", 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. {ok, << _:24, 7:8, _:72, 3:32 >>} = gen_tcp:recv(Socket, 17, 6000), 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. -- cgit v1.2.3