mirror of
https://github.com/ninenines/cowboy.git
synced 2025-07-14 04:10:24 +00:00
Provide better control over which HTTP protocols are enabled
Over cleartext TCP the `protocols` option lists the enabled protocols. The default is to allow both HTTP/1.1 and HTTP/2. Over TLS the default protocol to use when ALPN is not used can now be configured via the `alpn_default_protocol` option. Performing an HTTP/1.1 upgrade to HTTP/2 over TLS is now rejected with an error as connecting to HTTP/2 over TLS requires the use of ALPN (or that HTTP/2 be the default when connecting over TLS).
This commit is contained in:
parent
971684788d
commit
053e233c56
8 changed files with 160 additions and 28 deletions
|
@ -18,6 +18,7 @@ as a Ranch protocol.
|
|||
----
|
||||
opts() :: #{
|
||||
active_n => pos_integer(),
|
||||
alpn_default_protocol => http | http2,
|
||||
chunked => boolean(),
|
||||
connection_type => worker | supervisor,
|
||||
dynamic_buffer => false | {pos_integer(), pos_integer()},
|
||||
|
@ -36,6 +37,7 @@ opts() :: #{
|
|||
max_method_length => non_neg_integer(),
|
||||
max_request_line_length => non_neg_integer(),
|
||||
max_skip_body_length => non_neg_integer(),
|
||||
protocols => [http | http2],
|
||||
proxy_header => boolean(),
|
||||
request_timeout => timeout(),
|
||||
reset_idle_timeout_on_send => boolean(),
|
||||
|
@ -63,6 +65,12 @@ values reduce the number of times Cowboy need to request more
|
|||
packets from the port driver at the expense of potentially
|
||||
higher memory being used.
|
||||
|
||||
alpn_default_protocol (http)::
|
||||
|
||||
Default protocol to use when the client connects over TLS
|
||||
without ALPN. Can be set to `http2` to disable HTTP/1.1
|
||||
entirely.
|
||||
|
||||
chunked (true)::
|
||||
|
||||
Whether chunked transfer-encoding is enabled for HTTP/1.1 connections.
|
||||
|
@ -156,6 +164,13 @@ max_skip_body_length (1000000)::
|
|||
Maximum length Cowboy is willing to skip when the user code did not read the body fully.
|
||||
When the remaining length is too large or unknown Cowboy will close the connection.
|
||||
|
||||
protocols ([http2, http])::
|
||||
|
||||
Protocols that may be used when the client connects over
|
||||
cleartext TCP. The default is to allow both HTTP/1.1 and
|
||||
HTTP/2. HTTP/1.1 and HTTP/2 can be disabled entirely by
|
||||
omitting them from the list.
|
||||
|
||||
proxy_header (false)::
|
||||
|
||||
Whether incoming connections have a PROXY protocol header. The
|
||||
|
|
|
@ -18,6 +18,7 @@ as a Ranch protocol.
|
|||
----
|
||||
opts() :: #{
|
||||
active_n => pos_integer(),
|
||||
alpn_default_protocol => http | http2,
|
||||
connection_type => worker | supervisor,
|
||||
connection_window_margin_size => 0..16#7fffffff,
|
||||
connection_window_update_threshold => 0..16#7fffffff,
|
||||
|
@ -46,6 +47,7 @@ opts() :: #{
|
|||
max_stream_buffer_size => non_neg_integer(),
|
||||
max_stream_window_size => 0..16#7fffffff,
|
||||
preface_timeout => timeout(),
|
||||
protocols => [http | http2],
|
||||
proxy_header => boolean(),
|
||||
reset_idle_timeout_on_send => boolean(),
|
||||
sendfile => boolean(),
|
||||
|
@ -76,6 +78,12 @@ values reduce the number of times Cowboy need to request more
|
|||
packets from the port driver at the expense of potentially
|
||||
higher memory being used.
|
||||
|
||||
alpn_default_protocol (http)::
|
||||
|
||||
Default protocol to use when the client connects over TLS
|
||||
without ALPN. Can be set to `http2` to disable HTTP/1.1
|
||||
entirely.
|
||||
|
||||
connection_type (supervisor)::
|
||||
|
||||
Whether the connection process also acts as a supervisor.
|
||||
|
@ -259,6 +267,13 @@ preface_timeout (5000)::
|
|||
|
||||
Time in ms Cowboy is willing to wait for the connection preface.
|
||||
|
||||
protocols ([http2, http])::
|
||||
|
||||
Protocols that may be used when the client connects over
|
||||
cleartext TCP. The default is to allow both HTTP/1.1 and
|
||||
HTTP/2. HTTP/1.1 and HTTP/2 can be disabled entirely by
|
||||
omitting them from the list.
|
||||
|
||||
proxy_header (false)::
|
||||
|
||||
Whether incoming connections have a PROXY protocol header. The
|
||||
|
|
|
@ -36,10 +36,6 @@ connection_process(Parent, Ref, Transport, Opts) ->
|
|||
ProxyInfo = get_proxy_info(Ref, Opts),
|
||||
{ok, Socket} = ranch:handshake(Ref),
|
||||
%% Use cowboy_http2 directly only when 'http' is missing.
|
||||
%% Otherwise switch to cowboy_http2 from cowboy_http.
|
||||
%%
|
||||
%% @todo Extend this option to cowboy_tls and allow disabling
|
||||
%% the switch to cowboy_http2 in cowboy_http. Also document it.
|
||||
Protocol = case maps:get(protocols, Opts, [http2, http]) of
|
||||
[http2] -> cowboy_http2;
|
||||
[_|_] -> cowboy_http
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
-type opts() :: #{
|
||||
active_n => pos_integer(),
|
||||
alpn_default_protocol => http | http2,
|
||||
chunked => boolean(),
|
||||
compress_buffering => boolean(),
|
||||
compress_threshold => non_neg_integer(),
|
||||
|
@ -52,6 +53,7 @@
|
|||
metrics_req_filter => fun((cowboy_req:req()) -> map()),
|
||||
metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()),
|
||||
middlewares => [module()],
|
||||
protocols => [http | http2],
|
||||
proxy_header => boolean(),
|
||||
request_timeout => timeout(),
|
||||
reset_idle_timeout_on_send => boolean(),
|
||||
|
@ -511,8 +513,13 @@ parse_request(Buffer, State=#state{opts=Opts, in_streamid=InStreamID}, EmptyLine
|
|||
'The TRACE method is currently not implemented. (RFC7231 4.3.8)'});
|
||||
%% 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.
|
||||
case lists:member(http2, maps:get(protocols, Opts, [http2, http])) of
|
||||
true ->
|
||||
http2_upgrade(State, Buffer);
|
||||
false ->
|
||||
error_terminate(501, State, {connection_error, no_error,
|
||||
'Prior knowledge upgrade to HTTP/2 is disabled by configuration.'})
|
||||
end;
|
||||
_ ->
|
||||
parse_method(Buffer, State, <<>>,
|
||||
maps:get(max_method_length, Opts, 32))
|
||||
|
@ -800,7 +807,7 @@ default_port(_) -> 80.
|
|||
%% End of request parsing.
|
||||
|
||||
request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, sock=Sock, cert=Cert,
|
||||
proxy_header=ProxyHeader, in_streamid=StreamID, in_state=
|
||||
opts=Opts, proxy_header=ProxyHeader, in_streamid=StreamID, in_state=
|
||||
PS=#ps_header{method=Method, path=Path, qs=Qs, version=Version}},
|
||||
Headers, Host, Port) ->
|
||||
Scheme = case Transport:secure() of
|
||||
|
@ -864,7 +871,7 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, sock=Sock
|
|||
undefined -> Req0;
|
||||
_ -> Req0#{proxy_header => ProxyHeader}
|
||||
end,
|
||||
case is_http2_upgrade(Headers, Version) of
|
||||
case is_http2_upgrade(Headers, Version, Opts) of
|
||||
false ->
|
||||
State = case HasBody of
|
||||
true ->
|
||||
|
@ -886,12 +893,13 @@ request(Buffer, State0=#state{ref=Ref, transport=Transport, peer=Peer, sock=Sock
|
|||
|
||||
%% HTTP/2 upgrade.
|
||||
|
||||
%% @todo We must not upgrade to h2c over a TLS connection.
|
||||
is_http2_upgrade(#{<<"connection">> := Conn, <<"upgrade">> := Upgrade,
|
||||
<<"http2-settings">> := HTTP2Settings}, 'HTTP/1.1') ->
|
||||
<<"http2-settings">> := HTTP2Settings}, 'HTTP/1.1', Opts) ->
|
||||
Conns = cow_http_hd:parse_connection(Conn),
|
||||
case {lists:member(<<"upgrade">>, Conns), lists:member(<<"http2-settings">>, Conns)} of
|
||||
{true, true} ->
|
||||
case lists:member(<<"upgrade">>, Conns)
|
||||
andalso lists:member(<<"http2-settings">>, Conns)
|
||||
andalso lists:member(http2, maps:get(protocols, Opts, [http2, http])) of
|
||||
true ->
|
||||
Protocols = cow_http_hd:parse_upgrade(Upgrade),
|
||||
case lists:member(<<"h2c">>, Protocols) of
|
||||
true ->
|
||||
|
@ -902,7 +910,7 @@ is_http2_upgrade(#{<<"connection">> := Conn, <<"upgrade">> := Upgrade,
|
|||
_ ->
|
||||
false
|
||||
end;
|
||||
is_http2_upgrade(_, _) ->
|
||||
is_http2_upgrade(_, _, _) ->
|
||||
false.
|
||||
|
||||
%% Prior knowledge upgrade, without an HTTP/1.1 request.
|
||||
|
@ -922,6 +930,8 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran
|
|||
http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Transport,
|
||||
proxy_header=ProxyHeader, peer=Peer, sock=Sock, cert=Cert},
|
||||
Buffer, HTTP2Settings, Req) ->
|
||||
case Transport:secure() of
|
||||
false ->
|
||||
%% @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.
|
||||
|
@ -934,6 +944,10 @@ http2_upgrade(State=#state{parent=Parent, ref=Ref, socket=Socket, transport=Tran
|
|||
catch _:_ ->
|
||||
error_terminate(400, State, {connection_error, protocol_error,
|
||||
'The HTTP2-Settings header must contain a base64 SETTINGS payload. (RFC7540 3.2, RFC7540 3.2.1)'})
|
||||
end;
|
||||
true ->
|
||||
error_terminate(400, State, {connection_error, protocol_error,
|
||||
'Clients that support HTTP/2 over TLS MUST use ALPN. (RFC7540 3.4)'})
|
||||
end.
|
||||
|
||||
opts_for_upgrade(#state{opts=Opts, dynamic_buffer_size=false}) ->
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
|
||||
-type opts() :: #{
|
||||
active_n => pos_integer(),
|
||||
alpn_default_protocol => http | http2,
|
||||
compress_buffering => boolean(),
|
||||
compress_threshold => non_neg_integer(),
|
||||
connection_type => worker | supervisor,
|
||||
|
@ -62,6 +63,7 @@
|
|||
metrics_resp_headers_filter => fun((cowboy:http_headers()) -> cowboy:http_headers()),
|
||||
middlewares => [module()],
|
||||
preface_timeout => timeout(),
|
||||
protocols => [http | http2],
|
||||
proxy_header => boolean(),
|
||||
reset_idle_timeout_on_send => boolean(),
|
||||
sendfile => boolean(),
|
||||
|
|
|
@ -39,7 +39,11 @@ connection_process(Parent, Ref, Transport, Opts) ->
|
|||
{ok, <<"h2">>} ->
|
||||
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, cowboy_http2);
|
||||
_ -> %% http/1.1 or no protocol negotiated.
|
||||
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, cowboy_http)
|
||||
Protocol = case maps:get(alpn_default_protocol, Opts, http) of
|
||||
http -> cowboy_http;
|
||||
http2 -> cowboy_http2
|
||||
end,
|
||||
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol)
|
||||
end.
|
||||
|
||||
init(Parent, Ref, Socket, Transport, ProxyInfo, Opts, Protocol) ->
|
||||
|
|
|
@ -199,6 +199,73 @@ do_chunked_body(ChunkSize0, Data, Acc) ->
|
|||
do_chunked_body(ChunkSize, Rest,
|
||||
[iolist_to_binary(cow_http_te:chunk(Chunk))|Acc]).
|
||||
|
||||
disable_http1_tls(Config) ->
|
||||
doc("Ensure that we can disable HTTP/1.1 over TLS (force HTTP/2)."),
|
||||
TlsOpts = ct_helper:get_certs_from_ets(),
|
||||
{ok, _} = cowboy:start_tls(?FUNCTION_NAME, TlsOpts ++ [{port, 0}], #{
|
||||
env => #{dispatch => init_dispatch(Config)},
|
||||
alpn_default_protocol => http2
|
||||
}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, Socket} = ssl:connect("localhost", Port,
|
||||
[binary, {active, false}|TlsOpts]),
|
||||
%% ALPN was not negotiated but we're still over HTTP/2.
|
||||
{error, protocol_not_negotiated} = ssl:negotiated_protocol(Socket),
|
||||
%% Send a valid preface.
|
||||
ok = ssl:send(Socket, [
|
||||
"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n",
|
||||
cow_http2:settings(#{})]),
|
||||
%% Receive the server preface.
|
||||
{ok, << Len:24 >>} = ssl:recv(Socket, 3, 1000),
|
||||
{ok, << 4:8, 0:40, _:Len/binary >>} = ssl:recv(Socket, 6 + Len, 1000),
|
||||
ok
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
disable_http2_prior_knowledge(Config) ->
|
||||
doc("Ensure that we can disable prior knowledge HTTP/2 upgrade."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
|
||||
env => #{dispatch => init_dispatch(Config)},
|
||||
protocols => [http]
|
||||
}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}]),
|
||||
%% Send a valid preface.
|
||||
ok = gen_tcp:send(Socket, [
|
||||
"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n",
|
||||
cow_http2:settings(#{})]),
|
||||
{ok, <<"HTTP/1.1 501">>} = gen_tcp:recv(Socket, 12, 1000),
|
||||
ok
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
disable_http2_upgrade(Config) ->
|
||||
doc("Ensure that we can disable HTTP/1.1 upgrade to HTTP/2."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
|
||||
env => #{dispatch => init_dispatch(Config)},
|
||||
protocols => [http]
|
||||
}),
|
||||
Port = ranch:get_port(?FUNCTION_NAME),
|
||||
try
|
||||
{ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {active, false}]),
|
||||
%% Send a valid preface.
|
||||
ok = gen_tcp:send(Socket, [
|
||||
"GET / HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Connection: Upgrade, HTTP2-Settings\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
|
||||
after
|
||||
cowboy:stop_listener(?FUNCTION_NAME)
|
||||
end.
|
||||
|
||||
hibernate(Config) ->
|
||||
doc("Ensure that we can enable hibernation for HTTP/1.1 connections."),
|
||||
{ok, _} = cowboy:start_clear(?FUNCTION_NAME, [{port, 0}], #{
|
||||
|
|
|
@ -35,8 +35,9 @@ all() -> [{group, clear}, {group, tls}].
|
|||
|
||||
groups() ->
|
||||
Tests = ct_helper:all(?MODULE),
|
||||
Clear = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =/= "alpn"] -- [prior_knowledge_reject_tls],
|
||||
TLS = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =:= "alpn"] ++ [prior_knowledge_reject_tls],
|
||||
RejectTLS = [http_upgrade_reject_tls, prior_knowledge_reject_tls],
|
||||
Clear = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =/= "alpn"] -- RejectTLS,
|
||||
TLS = [T || T <- Tests, lists:sublist(atom_to_list(T), 4) =:= "alpn"] ++ RejectTLS,
|
||||
[{clear, [parallel], Clear}, {tls, [parallel], TLS}].
|
||||
|
||||
init_per_group(Name = clear, Config) ->
|
||||
|
@ -68,6 +69,24 @@ init_routes(_) -> [
|
|||
|
||||
%% Starting HTTP/2 for "http" URIs.
|
||||
|
||||
http_upgrade_reject_tls(Config) ->
|
||||
doc("Implementations that support HTTP/2 over TLS must use ALPN. (RFC7540 3.4)"),
|
||||
TlsOpts = ct_helper:get_certs_from_ets(),
|
||||
{ok, Socket} = ssl:connect("localhost", config(port, Config),
|
||||
[binary, {active, false}|TlsOpts]),
|
||||
%% Send a valid preface.
|
||||
ok = ssl:send(Socket, [
|
||||
"GET / HTTP/1.1\r\n"
|
||||
"Host: localhost\r\n"
|
||||
"Connection: Upgrade, HTTP2-Settings\r\n"
|
||||
"Upgrade: h2c\r\n"
|
||||
"HTTP2-Settings: ", base64:encode(cow_http2:settings_payload(#{})), "\r\n",
|
||||
"\r\n"]),
|
||||
%% We expect the server to send an HTTP 400 error
|
||||
%% when trying to use HTTP/2 without going through ALPN negotiation.
|
||||
{ok, <<"HTTP/1.1 400">>} = ssl:recv(Socket, 12, 1000),
|
||||
ok.
|
||||
|
||||
http_upgrade_ignore_h2(Config) ->
|
||||
doc("An h2 token in an Upgrade field must be ignored. (RFC7540 3.2)"),
|
||||
{ok, Socket} = gen_tcp:connect("localhost", config(port, Config), [binary, {active, false}]),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue