diff --git a/api/envoy/api/v2/core/protocol.proto b/api/envoy/api/v2/core/protocol.proto index 53b6ae8746794..e6a7ddaf34149 100644 --- a/api/envoy/api/v2/core/protocol.proto +++ b/api/envoy/api/v2/core/protocol.proto @@ -112,8 +112,21 @@ message Http1ProtocolOptions { bool enable_trailers = 5; } -// [#next-free-field: 13] +// [#next-free-field: 14] message Http2ProtocolOptions { + // Defines a parameter to be sent in the SETTINGS frame. + // See `RFC7540, sec. 6.5.1 `_ for details. + message SettingsParameter { + // The 16 bit parameter identifier. + google.protobuf.UInt32Value identifier = 1 [ + (validate.rules).uint32 = {lte: 65536 gte: 1}, + (validate.rules).message = {required: true} + ]; + + // The 32 bit parameter value. + google.protobuf.UInt32Value value = 2 [(validate.rules).message = {required: true}]; + } + // `Maximum table size `_ // (in octets) that the encoder is permitted to use for the dynamic HPACK table. Valid values // range from 0 to 4294967295 (2^32 - 1) and defaults to 4096. 0 effectively disables header @@ -216,6 +229,34 @@ message Http2ProtocolOptions { // // See `RFC7540, sec. 8.1 `_ for details. bool stream_error_on_invalid_http_messaging = 12; + + // [#not-implemented-hide:] + // Specifies SETTINGS frame parameters to be sent to the peer, with two exceptions: + // + // 1. SETTINGS_ENABLE_PUSH (0x2) is not configurable as HTTP/2 server push is not supported by + // Envoy. + // + // 2. SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8) is only configurable through the named field + // 'allow_connect'. + // + // Note that custom parameters specified through this field can not also be set in the + // corresponding named parameters: + // + // .. code-block:: text + // + // ID Field Name + // ---------------- + // 0x1 hpack_table_size + // 0x3 max_concurrent_streams + // 0x4 initial_stream_window_size + // + // Collisions will trigger config validation failure on load/update. Likewise, inconsistencies + // between custom parameters with the same identifier will trigger a failure. + // + // See `IANA HTTP/2 Settings + // `_ for + // standardized identifiers. + repeated SettingsParameter custom_settings_parameters = 13; } // [#not-implemented-hide:] diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index b3263a1fdde6e..64c5ec1ea759f 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -128,11 +128,27 @@ message Http1ProtocolOptions { bool enable_trailers = 5; } -// [#next-free-field: 13] +// [#next-free-field: 14] message Http2ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.Http2ProtocolOptions"; + // Defines a parameter to be sent in the SETTINGS frame. + // See `RFC7540, sec. 6.5.1 `_ for details. + message SettingsParameter { + option (udpa.annotations.versioning).previous_message_type = + "envoy.api.v2.core.Http2ProtocolOptions.SettingsParameter"; + + // The 16 bit parameter identifier. + google.protobuf.UInt32Value identifier = 1 [ + (validate.rules).uint32 = {lte: 65536 gte: 1}, + (validate.rules).message = {required: true} + ]; + + // The 32 bit parameter value. + google.protobuf.UInt32Value value = 2 [(validate.rules).message = {required: true}]; + } + // `Maximum table size `_ // (in octets) that the encoder is permitted to use for the dynamic HPACK table. Valid values // range from 0 to 4294967295 (2^32 - 1) and defaults to 4096. 0 effectively disables header @@ -235,6 +251,34 @@ message Http2ProtocolOptions { // // See `RFC7540, sec. 8.1 `_ for details. bool stream_error_on_invalid_http_messaging = 12; + + // [#not-implemented-hide:] + // Specifies SETTINGS frame parameters to be sent to the peer, with two exceptions: + // + // 1. SETTINGS_ENABLE_PUSH (0x2) is not configurable as HTTP/2 server push is not supported by + // Envoy. + // + // 2. SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8) is only configurable through the named field + // 'allow_connect'. + // + // Note that custom parameters specified through this field can not also be set in the + // corresponding named parameters: + // + // .. code-block:: text + // + // ID Field Name + // ---------------- + // 0x1 hpack_table_size + // 0x3 max_concurrent_streams + // 0x4 initial_stream_window_size + // + // Collisions will trigger config validation failure on load/update. Likewise, inconsistencies + // between custom parameters with the same identifier will trigger a failure. + // + // See `IANA HTTP/2 Settings + // `_ for + // standardized identifiers. + repeated SettingsParameter custom_settings_parameters = 13; } // [#not-implemented-hide:] diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index 89737cec0c20e..cc95873611907 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -505,6 +505,11 @@ def _com_google_absl(): actual = "@com_google_absl//absl/time:time", ) + native.bind( + name = "abseil_algorithm", + actual = "@com_google_absl//absl/algorithm:algorithm", + ) + def _com_google_protobuf(): _repository_impl("rules_python") _repository_impl( diff --git a/generated_api_shadow/envoy/api/v2/core/protocol.proto b/generated_api_shadow/envoy/api/v2/core/protocol.proto index 53b6ae8746794..e6a7ddaf34149 100644 --- a/generated_api_shadow/envoy/api/v2/core/protocol.proto +++ b/generated_api_shadow/envoy/api/v2/core/protocol.proto @@ -112,8 +112,21 @@ message Http1ProtocolOptions { bool enable_trailers = 5; } -// [#next-free-field: 13] +// [#next-free-field: 14] message Http2ProtocolOptions { + // Defines a parameter to be sent in the SETTINGS frame. + // See `RFC7540, sec. 6.5.1 `_ for details. + message SettingsParameter { + // The 16 bit parameter identifier. + google.protobuf.UInt32Value identifier = 1 [ + (validate.rules).uint32 = {lte: 65536 gte: 1}, + (validate.rules).message = {required: true} + ]; + + // The 32 bit parameter value. + google.protobuf.UInt32Value value = 2 [(validate.rules).message = {required: true}]; + } + // `Maximum table size `_ // (in octets) that the encoder is permitted to use for the dynamic HPACK table. Valid values // range from 0 to 4294967295 (2^32 - 1) and defaults to 4096. 0 effectively disables header @@ -216,6 +229,34 @@ message Http2ProtocolOptions { // // See `RFC7540, sec. 8.1 `_ for details. bool stream_error_on_invalid_http_messaging = 12; + + // [#not-implemented-hide:] + // Specifies SETTINGS frame parameters to be sent to the peer, with two exceptions: + // + // 1. SETTINGS_ENABLE_PUSH (0x2) is not configurable as HTTP/2 server push is not supported by + // Envoy. + // + // 2. SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8) is only configurable through the named field + // 'allow_connect'. + // + // Note that custom parameters specified through this field can not also be set in the + // corresponding named parameters: + // + // .. code-block:: text + // + // ID Field Name + // ---------------- + // 0x1 hpack_table_size + // 0x3 max_concurrent_streams + // 0x4 initial_stream_window_size + // + // Collisions will trigger config validation failure on load/update. Likewise, inconsistencies + // between custom parameters with the same identifier will trigger a failure. + // + // See `IANA HTTP/2 Settings + // `_ for + // standardized identifiers. + repeated SettingsParameter custom_settings_parameters = 13; } // [#not-implemented-hide:] diff --git a/generated_api_shadow/envoy/config/core/v3/protocol.proto b/generated_api_shadow/envoy/config/core/v3/protocol.proto index b3263a1fdde6e..64c5ec1ea759f 100644 --- a/generated_api_shadow/envoy/config/core/v3/protocol.proto +++ b/generated_api_shadow/envoy/config/core/v3/protocol.proto @@ -128,11 +128,27 @@ message Http1ProtocolOptions { bool enable_trailers = 5; } -// [#next-free-field: 13] +// [#next-free-field: 14] message Http2ProtocolOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.core.Http2ProtocolOptions"; + // Defines a parameter to be sent in the SETTINGS frame. + // See `RFC7540, sec. 6.5.1 `_ for details. + message SettingsParameter { + option (udpa.annotations.versioning).previous_message_type = + "envoy.api.v2.core.Http2ProtocolOptions.SettingsParameter"; + + // The 16 bit parameter identifier. + google.protobuf.UInt32Value identifier = 1 [ + (validate.rules).uint32 = {lte: 65536 gte: 1}, + (validate.rules).message = {required: true} + ]; + + // The 32 bit parameter value. + google.protobuf.UInt32Value value = 2 [(validate.rules).message = {required: true}]; + } + // `Maximum table size `_ // (in octets) that the encoder is permitted to use for the dynamic HPACK table. Valid values // range from 0 to 4294967295 (2^32 - 1) and defaults to 4096. 0 effectively disables header @@ -235,6 +251,34 @@ message Http2ProtocolOptions { // // See `RFC7540, sec. 8.1 `_ for details. bool stream_error_on_invalid_http_messaging = 12; + + // [#not-implemented-hide:] + // Specifies SETTINGS frame parameters to be sent to the peer, with two exceptions: + // + // 1. SETTINGS_ENABLE_PUSH (0x2) is not configurable as HTTP/2 server push is not supported by + // Envoy. + // + // 2. SETTINGS_ENABLE_CONNECT_PROTOCOL (0x8) is only configurable through the named field + // 'allow_connect'. + // + // Note that custom parameters specified through this field can not also be set in the + // corresponding named parameters: + // + // .. code-block:: text + // + // ID Field Name + // ---------------- + // 0x1 hpack_table_size + // 0x3 max_concurrent_streams + // 0x4 initial_stream_window_size + // + // Collisions will trigger config validation failure on load/update. Likewise, inconsistencies + // between custom parameters with the same identifier will trigger a failure. + // + // See `IANA HTTP/2 Settings + // `_ for + // standardized identifiers. + repeated SettingsParameter custom_settings_parameters = 13; } // [#not-implemented-hide:] diff --git a/include/envoy/http/codec.h b/include/envoy/http/codec.h index 9efa84abdbbb7..4e08534741b8c 100644 --- a/include/envoy/http/codec.h +++ b/include/envoy/http/codec.h @@ -326,78 +326,6 @@ struct Http1Settings { HeaderKeyFormat header_key_format_{HeaderKeyFormat::Default}; }; -/** - * HTTP/2 codec settings - */ -struct Http2Settings { - // TODO(jwfang): support other HTTP/2 settings - uint32_t hpack_table_size_{DEFAULT_HPACK_TABLE_SIZE}; - uint32_t max_concurrent_streams_{DEFAULT_MAX_CONCURRENT_STREAMS}; - uint32_t initial_stream_window_size_{DEFAULT_INITIAL_STREAM_WINDOW_SIZE}; - uint32_t initial_connection_window_size_{DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE}; - bool allow_connect_{DEFAULT_ALLOW_CONNECT}; - bool allow_metadata_{DEFAULT_ALLOW_METADATA}; - bool stream_error_on_invalid_http_messaging_{DEFAULT_STREAM_ERROR_ON_INVALID_HTTP_MESSAGING}; - uint32_t max_outbound_frames_{DEFAULT_MAX_OUTBOUND_FRAMES}; - uint32_t max_outbound_control_frames_{DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES}; - uint32_t max_consecutive_inbound_frames_with_empty_payload_{ - DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD}; - uint32_t max_inbound_priority_frames_per_stream_{DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM}; - uint32_t max_inbound_window_update_frames_per_data_frame_sent_{ - DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT}; - - // disable HPACK compression - static const uint32_t MIN_HPACK_TABLE_SIZE = 0; - // initial value from HTTP/2 spec, same as NGHTTP2_DEFAULT_HEADER_TABLE_SIZE from nghttp2 - static const uint32_t DEFAULT_HPACK_TABLE_SIZE = (1 << 12); - // no maximum from HTTP/2 spec, use unsigned 32-bit maximum - static const uint32_t MAX_HPACK_TABLE_SIZE = std::numeric_limits::max(); - - // TODO(jwfang): make this 0, the HTTP/2 spec minimum - static const uint32_t MIN_MAX_CONCURRENT_STREAMS = 1; - // defaults to maximum, same as nghttp2 - static const uint32_t DEFAULT_MAX_CONCURRENT_STREAMS = (1U << 31) - 1; - // no maximum from HTTP/2 spec, total streams is unsigned 32-bit maximum, - // one-side (client/server) is half that, and we need to exclude stream 0. - // same as NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS from nghttp2 - static const uint32_t MAX_MAX_CONCURRENT_STREAMS = (1U << 31) - 1; - - // initial value from HTTP/2 spec, same as NGHTTP2_INITIAL_WINDOW_SIZE from nghttp2 - // NOTE: we only support increasing window size now, so this is also the minimum - // TODO(jwfang): make this 0 to support decrease window size - static const uint32_t MIN_INITIAL_STREAM_WINDOW_SIZE = (1 << 16) - 1; - // initial value from HTTP/2 spec is 65535, but we want more (256MiB) - static const uint32_t DEFAULT_INITIAL_STREAM_WINDOW_SIZE = 256 * 1024 * 1024; - // maximum from HTTP/2 spec, same as NGHTTP2_MAX_WINDOW_SIZE from nghttp2 - static const uint32_t MAX_INITIAL_STREAM_WINDOW_SIZE = (1U << 31) - 1; - - // CONNECTION_WINDOW_SIZE is similar to STREAM_WINDOW_SIZE, but for connection-level window - // TODO(jwfang): make this 0 to support decrease window size - static const uint32_t MIN_INITIAL_CONNECTION_WINDOW_SIZE = (1 << 16) - 1; - // nghttp2's default connection-level window equals to its stream-level, - // our default connection-level window also equals to our stream-level - static const uint32_t DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE = 256 * 1024 * 1024; - static const uint32_t MAX_INITIAL_CONNECTION_WINDOW_SIZE = (1U << 31) - 1; - // By default both nghttp2 and Envoy do not allow CONNECT over H2. - static const bool DEFAULT_ALLOW_CONNECT = false; - // By default Envoy does not allow METADATA support. - static const bool DEFAULT_ALLOW_METADATA = false; - // By default Envoy does not allow invalid headers. - static const bool DEFAULT_STREAM_ERROR_ON_INVALID_HTTP_MESSAGING = false; - - // Default limit on the number of outbound frames of all types. - static const uint32_t DEFAULT_MAX_OUTBOUND_FRAMES = 10000; - // Default limit on the number of outbound frames of types PING, SETTINGS and RST_STREAM. - static const uint32_t DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES = 1000; - // Default limit on the number of consecutive inbound frames with an empty payload - // and no end stream flag. - static const uint32_t DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD = 1; - // Default limit on the number of inbound frames of type PRIORITY (per stream). - static const uint32_t DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM = 100; - // Default limit on the number of inbound frames of type WINDOW_UPDATE (per DATA frame sent). - static const uint32_t DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT = 10; -}; - /** * A connection (client or server) that owns multiple streams. */ diff --git a/include/envoy/upstream/upstream.h b/include/envoy/upstream/upstream.h index 0571b33121e85..d8a41bae7d3d8 100644 --- a/include/envoy/upstream/upstream.h +++ b/include/envoy/upstream/upstream.h @@ -720,10 +720,11 @@ class ClusterInfo { virtual const Http::Http1Settings& http1Settings() const PURE; /** - * @return const Http::Http2Settings& for HTTP/2 connections created on behalf of this cluster. - * @see Http::Http2Settings. + * @return const envoy::config::core::v3::Http2ProtocolOptions& for HTTP/2 connections + * created on behalf of this cluster. + * @see envoy::config::core::v3::Http2ProtocolOptions. */ - virtual const Http::Http2Settings& http2Settings() const PURE; + virtual const envoy::config::core::v3::Http2ProtocolOptions& http2Options() const PURE; /** * @param name std::string containing the well-known name of the extension for which protocol diff --git a/source/common/http/BUILD b/source/common/http/BUILD index e13f8a1fd16b8..9f23789d4a804 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -327,6 +327,7 @@ envoy_cc_library( external_deps = [ "abseil_optional", "http_parser", + "nghttp2", ], deps = [ ":exception_lib", diff --git a/source/common/http/codec_client.cc b/source/common/http/codec_client.cc index 09c294af4a6e9..682d4ea394c31 100644 --- a/source/common/http/codec_client.cc +++ b/source/common/http/codec_client.cc @@ -155,7 +155,7 @@ CodecClientProd::CodecClientProd(Type type, Network::ClientConnectionPtr&& conne } case Type::HTTP2: { codec_ = std::make_unique( - *connection_, *this, host->cluster().statsScope(), host->cluster().http2Settings(), + *connection_, *this, host->cluster().statsScope(), host->cluster().http2Options(), Http::DEFAULT_MAX_REQUEST_HEADERS_KB, host->cluster().maxResponseHeadersCount()); break; } diff --git a/source/common/http/conn_manager_utility.cc b/source/common/http/conn_manager_utility.cc index 01001ca13839d..004ac2453b6e7 100644 --- a/source/common/http/conn_manager_utility.cc +++ b/source/common/http/conn_manager_utility.cc @@ -43,11 +43,11 @@ std::string ConnectionManagerUtility::determineNextProtocol(Network::Connection& ServerConnectionPtr ConnectionManagerUtility::autoCreateCodec( Network::Connection& connection, const Buffer::Instance& data, ServerConnectionCallbacks& callbacks, Stats::Scope& scope, const Http1Settings& http1_settings, - const Http2Settings& http2_settings, uint32_t max_request_headers_kb, - uint32_t max_request_headers_count) { + const envoy::config::core::v3::Http2ProtocolOptions& http2_options, + uint32_t max_request_headers_kb, uint32_t max_request_headers_count) { if (determineNextProtocol(connection, data) == Http2::ALPN_STRING) { return std::make_unique(connection, callbacks, scope, - http2_settings, max_request_headers_kb, + http2_options, max_request_headers_kb, max_request_headers_count); } else { return std::make_unique(connection, scope, callbacks, diff --git a/source/common/http/conn_manager_utility.h b/source/common/http/conn_manager_utility.h index 9737d4ddfcb48..ebc37a4bda186 100644 --- a/source/common/http/conn_manager_utility.h +++ b/source/common/http/conn_manager_utility.h @@ -36,7 +36,8 @@ class ConnectionManagerUtility { static ServerConnectionPtr autoCreateCodec(Network::Connection& connection, const Buffer::Instance& data, ServerConnectionCallbacks& callbacks, Stats::Scope& scope, - const Http1Settings& http1_settings, const Http2Settings& http2_settings, + const Http1Settings& http1_settings, + const envoy::config::core::v3::Http2ProtocolOptions& http2_options, uint32_t max_request_headers_kb, uint32_t max_request_headers_count); /** diff --git a/source/common/http/http2/BUILD b/source/common/http/http2/BUILD index ad8d93119ae51..a6e03c97083b2 100644 --- a/source/common/http/http2/BUILD +++ b/source/common/http/http2/BUILD @@ -15,6 +15,8 @@ envoy_cc_library( external_deps = [ "nghttp2", "abseil_optional", + "abseil_inlined_vector", + "abseil_algorithm", ], deps = [ ":metadata_decoder_lib", diff --git a/source/common/http/http2/codec_impl.cc b/source/common/http/http2/codec_impl.cc index 1b546803c2dcb..cd3235d0c611d 100644 --- a/source/common/http/http2/codec_impl.cc +++ b/source/common/http/http2/codec_impl.cc @@ -18,6 +18,7 @@ #include "common/http/codes.h" #include "common/http/exception.h" #include "common/http/headers.h" +#include "common/http/utility.h" #include "absl/container/fixed_array.h" @@ -423,27 +424,27 @@ void ConnectionImpl::StreamImpl::onMetadataDecoded(MetadataMapPtr&& metadata_map } ConnectionImpl::ConnectionImpl(Network::Connection& connection, Stats::Scope& stats, - const Http2Settings& http2_settings, const uint32_t max_headers_kb, - const uint32_t max_headers_count) + const envoy::config::core::v3::Http2ProtocolOptions& http2_options, + const uint32_t max_headers_kb, const uint32_t max_headers_count) : stats_{ALL_HTTP2_CODEC_STATS(POOL_COUNTER_PREFIX(stats, "http2."))}, connection_(connection), max_headers_kb_(max_headers_kb), max_headers_count_(max_headers_count), - per_stream_buffer_limit_(http2_settings.initial_stream_window_size_), + per_stream_buffer_limit_(http2_options.initial_stream_window_size().value()), stream_error_on_invalid_http_messaging_( - http2_settings.stream_error_on_invalid_http_messaging_), - flood_detected_(false), max_outbound_frames_(http2_settings.max_outbound_frames_), + http2_options.stream_error_on_invalid_http_messaging()), + flood_detected_(false), max_outbound_frames_(http2_options.max_outbound_frames().value()), frame_buffer_releasor_([this](const Buffer::OwnedBufferFragmentImpl* fragment) { releaseOutboundFrame(fragment); }), - max_outbound_control_frames_(http2_settings.max_outbound_control_frames_), + max_outbound_control_frames_(http2_options.max_outbound_control_frames().value()), control_frame_buffer_releasor_([this](const Buffer::OwnedBufferFragmentImpl* fragment) { releaseOutboundControlFrame(fragment); }), max_consecutive_inbound_frames_with_empty_payload_( - http2_settings.max_consecutive_inbound_frames_with_empty_payload_), + http2_options.max_consecutive_inbound_frames_with_empty_payload().value()), max_inbound_priority_frames_per_stream_( - http2_settings.max_inbound_priority_frames_per_stream_), + http2_options.max_inbound_priority_frames_per_stream().value()), max_inbound_window_update_frames_per_data_frame_sent_( - http2_settings.max_inbound_window_update_frames_per_data_frame_sent_), + http2_options.max_inbound_window_update_frames_per_data_frame_sent().value()), dispatching_(false), raised_goaway_(false), pending_deferred_reset_(false) {} ConnectionImpl::~ConnectionImpl() { nghttp2_session_del(session_); } @@ -549,6 +550,10 @@ int ConnectionImpl::onFrameReceived(const nghttp2_frame* frame) { return 0; } + if (frame->hd.type == NGHTTP2_SETTINGS && frame->hd.flags == NGHTTP2_FLAG_NONE) { + onSettingsForTest(frame->settings); + } + StreamImpl* stream = getStream(frame->hd.stream_id); if (!stream) { return 0; @@ -900,52 +905,49 @@ void ConnectionImpl::sendPendingFrames() { } } -void ConnectionImpl::sendSettings(const Http2Settings& http2_settings, bool disable_push) { - ASSERT(http2_settings.hpack_table_size_ <= Http2Settings::MAX_HPACK_TABLE_SIZE); - ASSERT(Http2Settings::MIN_MAX_CONCURRENT_STREAMS <= http2_settings.max_concurrent_streams_ && - http2_settings.max_concurrent_streams_ <= Http2Settings::MAX_MAX_CONCURRENT_STREAMS); - ASSERT( - Http2Settings::MIN_INITIAL_STREAM_WINDOW_SIZE <= http2_settings.initial_stream_window_size_ && - http2_settings.initial_stream_window_size_ <= Http2Settings::MAX_INITIAL_STREAM_WINDOW_SIZE); - ASSERT(Http2Settings::MIN_INITIAL_CONNECTION_WINDOW_SIZE <= - http2_settings.initial_connection_window_size_ && - http2_settings.initial_connection_window_size_ <= - Http2Settings::MAX_INITIAL_CONNECTION_WINDOW_SIZE); - - std::vector iv; - - if (http2_settings.allow_connect_) { - iv.push_back({NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL, 1}); - } - - if (http2_settings.hpack_table_size_ != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { - iv.push_back({NGHTTP2_SETTINGS_HEADER_TABLE_SIZE, http2_settings.hpack_table_size_}); - ENVOY_CONN_LOG(debug, "setting HPACK table size to {}", connection_, - http2_settings.hpack_table_size_); - } - - if (http2_settings.max_concurrent_streams_ != NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS) { - iv.push_back({NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, http2_settings.max_concurrent_streams_}); - ENVOY_CONN_LOG(debug, "setting max concurrent streams to {}", connection_, - http2_settings.max_concurrent_streams_); - } - - if (http2_settings.initial_stream_window_size_ != NGHTTP2_INITIAL_WINDOW_SIZE) { - iv.push_back( - {NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, http2_settings.initial_stream_window_size_}); - ENVOY_CONN_LOG(debug, "setting stream-level initial window size to {}", connection_, - http2_settings.initial_stream_window_size_); - } - +void ConnectionImpl::sendSettings( + const envoy::config::core::v3::Http2ProtocolOptions& http2_options, bool disable_push) { + absl::InlinedVector settings; + auto insertParameter = [&settings](const nghttp2_settings_entry& entry) mutable -> bool { + const auto it = std::find_if(settings.cbegin(), settings.cend(), + [&entry](const nghttp2_settings_entry& existing) { + return entry.settings_id == existing.settings_id; + }); + if (it != settings.end()) { + return false; + } + settings.push_back(entry); + return true; + }; + + // Universally disable receiving push promise frames as we don't currently support + // them. nghttp2 will fail the connection if the other side still sends them. + // TODO(mattklein123): Remove this when we correctly proxy push promise. + // NOTE: This is a special case with respect to custom parameter overrides in that server push is + // not supported and therefore not end user configurable. if (disable_push) { - // Universally disable receiving push promise frames as we don't currently support them. nghttp2 - // will fail the connection if the other side still sends them. - // TODO(mattklein123): Remove this when we correctly proxy push promise. - iv.push_back({NGHTTP2_SETTINGS_ENABLE_PUSH, 0}); - } - - if (!iv.empty()) { - int rc = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, &iv[0], iv.size()); + settings.push_back( + {static_cast(NGHTTP2_SETTINGS_ENABLE_PUSH), disable_push ? 0U : 1U}); + } + + for (const auto it : http2_options.custom_settings_parameters()) { + ASSERT(it.identifier().value() <= std::numeric_limits::max()); + const bool result = + insertParameter({static_cast(it.identifier().value()), it.value().value()}); + ASSERT(result); + ENVOY_CONN_LOG(debug, "adding custom settings parameter with id {:#x} to {}", connection_, + it.identifier().value(), it.value().value()); + } + + // Insert named parameters. + settings.insert( + settings.end(), + {{NGHTTP2_SETTINGS_HEADER_TABLE_SIZE, http2_options.hpack_table_size().value()}, + {NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL, http2_options.allow_connect()}, + {NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS, http2_options.max_concurrent_streams().value()}, + {NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE, http2_options.initial_stream_window_size().value()}}); + if (!settings.empty()) { + int rc = nghttp2_submit_settings(session_, NGHTTP2_FLAG_NONE, &settings[0], settings.size()); ASSERT(rc == 0); } else { // nghttp2_submit_settings need to be called at least once @@ -953,12 +955,14 @@ void ConnectionImpl::sendSettings(const Http2Settings& http2_settings, bool disa ASSERT(rc == 0); } + const uint32_t initial_connection_window_size = + http2_options.initial_connection_window_size().value(); // Increase connection window size up to our default size. - if (http2_settings.initial_connection_window_size_ != NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE) { + if (initial_connection_window_size != NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE) { ENVOY_CONN_LOG(debug, "updating connection-level initial window size to {}", connection_, - http2_settings.initial_connection_window_size_); + initial_connection_window_size); int rc = nghttp2_submit_window_update(session_, NGHTTP2_FLAG_NONE, 0, - http2_settings.initial_connection_window_size_ - + initial_connection_window_size - NGHTTP2_INITIAL_CONNECTION_WINDOW_SIZE); ASSERT(rc == 0); } @@ -1071,60 +1075,64 @@ ConnectionImpl::Http2Callbacks::Http2Callbacks() { ConnectionImpl::Http2Callbacks::~Http2Callbacks() { nghttp2_session_callbacks_del(callbacks_); } -ConnectionImpl::Http2Options::Http2Options(const Http2Settings& http2_settings) { +ConnectionImpl::Http2Options::Http2Options( + const envoy::config::core::v3::Http2ProtocolOptions& http2_options) { nghttp2_option_new(&options_); // Currently we do not do anything with stream priority. Setting the following option prevents // nghttp2 from keeping around closed streams for use during stream priority dependency graph - // calculations. This saves a tremendous amount of memory in cases where there are a large number - // of kept alive HTTP/2 connections. + // calculations. This saves a tremendous amount of memory in cases where there are a large + // number of kept alive HTTP/2 connections. nghttp2_option_set_no_closed_streams(options_, 1); nghttp2_option_set_no_auto_window_update(options_, 1); // The max send header block length is configured to an arbitrarily high number so as to never - // trigger the check within nghttp2, as we check request headers length in codec_impl::saveHeader. + // trigger the check within nghttp2, as we check request headers length in + // codec_impl::saveHeader. nghttp2_option_set_max_send_header_block_length(options_, 0x2000000); - if (http2_settings.hpack_table_size_ != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { - nghttp2_option_set_max_deflate_dynamic_table_size(options_, http2_settings.hpack_table_size_); + if (http2_options.hpack_table_size().value() != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { + nghttp2_option_set_max_deflate_dynamic_table_size(options_, + http2_options.hpack_table_size().value()); } - if (http2_settings.allow_metadata_) { + if (http2_options.allow_metadata()) { nghttp2_option_set_user_recv_extension_type(options_, METADATA_FRAME_TYPE); } - // nghttp2 v1.39.2 lowered the internal flood protection limit from 10K to 1K of ACK frames. This - // new limit may cause the internal nghttp2 mitigation to trigger more often (as it requires just - // 9K of incoming bytes for smallest 9 byte SETTINGS frame), bypassing the same mitigation and its - // associated behavior in the envoy HTTP/2 codec. Since envoy does not rely on this mitigation, - // set back to the old 10K number to avoid any changes in the HTTP/2 codec behavior. + // nghttp2 v1.39.2 lowered the internal flood protection limit from 10K to 1K of ACK frames. + // This new limit may cause the internal nghttp2 mitigation to trigger more often (as it + // requires just 9K of incoming bytes for smallest 9 byte SETTINGS frame), bypassing the same + // mitigation and its associated behavior in the envoy HTTP/2 codec. Since envoy does not rely + // on this mitigation, set back to the old 10K number to avoid any changes in the HTTP/2 codec + // behavior. nghttp2_option_set_max_outbound_ack(options_, 10000); } ConnectionImpl::Http2Options::~Http2Options() { nghttp2_option_del(options_); } -ConnectionImpl::ClientHttp2Options::ClientHttp2Options(const Http2Settings& http2_settings) - : Http2Options(http2_settings) { +ConnectionImpl::ClientHttp2Options::ClientHttp2Options( + const envoy::config::core::v3::Http2ProtocolOptions& http2_options) + : Http2Options(http2_options) { // Temporarily disable initial max streams limit/protection, since we might want to create // more than 100 streams before receiving the HTTP/2 SETTINGS frame from the server. // // TODO(PiotrSikora): remove this once multiple upstream connections or queuing are implemented. - nghttp2_option_set_peer_max_concurrent_streams(options_, - Http2Settings::DEFAULT_MAX_CONCURRENT_STREAMS); + nghttp2_option_set_peer_max_concurrent_streams( + options_, ::Envoy::Http2::Utility::OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS); } -ClientConnectionImpl::ClientConnectionImpl(Network::Connection& connection, - Http::ConnectionCallbacks& callbacks, - Stats::Scope& stats, const Http2Settings& http2_settings, - const uint32_t max_response_headers_kb, - const uint32_t max_response_headers_count) - : ConnectionImpl(connection, stats, http2_settings, max_response_headers_kb, +ClientConnectionImpl::ClientConnectionImpl( + Network::Connection& connection, Http::ConnectionCallbacks& callbacks, Stats::Scope& stats, + const envoy::config::core::v3::Http2ProtocolOptions& http2_options, + const uint32_t max_response_headers_kb, const uint32_t max_response_headers_count) + : ConnectionImpl(connection, stats, http2_options, max_response_headers_kb, max_response_headers_count), callbacks_(callbacks) { - ClientHttp2Options client_http2_options(http2_settings); + ClientHttp2Options client_http2_options(http2_options); nghttp2_session_client_new2(&session_, http2_callbacks_.callbacks(), base(), client_http2_options.options()); - sendSettings(http2_settings, true); - allow_metadata_ = http2_settings.allow_metadata_; + sendSettings(http2_options, true); + allow_metadata_ = http2_options.allow_metadata(); } RequestEncoder& ClientConnectionImpl::newStream(ResponseDecoder& decoder) { @@ -1162,19 +1170,18 @@ int ClientConnectionImpl::onHeader(const nghttp2_frame* frame, HeaderString&& na return saveHeader(frame, std::move(name), std::move(value)); } -ServerConnectionImpl::ServerConnectionImpl(Network::Connection& connection, - Http::ServerConnectionCallbacks& callbacks, - Stats::Scope& scope, const Http2Settings& http2_settings, - const uint32_t max_request_headers_kb, - const uint32_t max_request_headers_count) - : ConnectionImpl(connection, scope, http2_settings, max_request_headers_kb, +ServerConnectionImpl::ServerConnectionImpl( + Network::Connection& connection, Http::ServerConnectionCallbacks& callbacks, + Stats::Scope& scope, const envoy::config::core::v3::Http2ProtocolOptions& http2_options, + const uint32_t max_request_headers_kb, const uint32_t max_request_headers_count) + : ConnectionImpl(connection, scope, http2_options, max_request_headers_kb, max_request_headers_count), callbacks_(callbacks) { - Http2Options http2_options(http2_settings); + Http2Options h2_options(http2_options); nghttp2_session_server_new2(&session_, http2_callbacks_.callbacks(), base(), - http2_options.options()); - sendSettings(http2_settings, false); - allow_metadata_ = http2_settings.allow_metadata_; + h2_options.options()); + sendSettings(http2_options, false); + allow_metadata_ = http2_options.allow_metadata(); } int ServerConnectionImpl::onBeginHeaders(const nghttp2_frame* frame) { diff --git a/source/common/http/http2/codec_impl.h b/source/common/http/http2/codec_impl.h index 686250e5e1d35..e469539f40aa4 100644 --- a/source/common/http/http2/codec_impl.h +++ b/source/common/http/http2/codec_impl.h @@ -79,8 +79,8 @@ class Utility { class ConnectionImpl : public virtual Connection, protected Logger::Loggable { public: ConnectionImpl(Network::Connection& connection, Stats::Scope& stats, - const Http2Settings& http2_settings, const uint32_t max_headers_kb, - const uint32_t max_headers_count); + const envoy::config::core::v3::Http2ProtocolOptions& http2_options, + const uint32_t max_headers_kb, const uint32_t max_headers_count); ~ConnectionImpl() override; @@ -123,7 +123,7 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggablecluster().http2Settings().max_concurrent_streams_) { + parent.host_->cluster().http2Options().max_concurrent_streams().value()) { codec_client_->setCodecClientCallbacks(*this); codec_client_->setCodecConnectionCallbacks(*this); diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index 8821ca2f46c19..ea4aa41cd4e80 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -29,8 +29,158 @@ #include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" +#include "nghttp2/nghttp2.h" namespace Envoy { +namespace Http2 { +namespace Utility { + +namespace { + +void validateCustomSettingsParameters( + const envoy::config::core::v3::Http2ProtocolOptions& options) { + std::vector parameter_collisions, custom_parameter_collisions; + std::unordered_set + custom_parameters; + // User defined and named parameters with the same SETTINGS identifier can not both be set. + for (const auto it : options.custom_settings_parameters()) { + ASSERT(it.identifier().value() <= std::numeric_limits::max()); + // Check for custom parameter inconsistencies. + const auto result = custom_parameters.insert( + {static_cast(it.identifier().value()), it.value().value()}); + if (!result.second) { + if (result.first->value != it.value().value()) { + custom_parameter_collisions.push_back( + absl::StrCat("0x", absl::Hex(it.identifier().value(), absl::kZeroPad2))); + // Fall through to allow unbatched exceptions to throw first. + } + } + switch (it.identifier().value()) { + case NGHTTP2_SETTINGS_ENABLE_PUSH: + if (it.value().value() == 1) { + throw EnvoyException("server push is not supported by Envoy and can not be enabled via a " + "SETTINGS parameter."); + } + break; + case NGHTTP2_SETTINGS_ENABLE_CONNECT_PROTOCOL: + // An exception is made for `allow_connect` which can't be checked for presence due to the + // use of a primitive type (bool). + throw EnvoyException("the \"allow_connect\" SETTINGS parameter must only be configured " + "through the named field"); + case NGHTTP2_SETTINGS_HEADER_TABLE_SIZE: + if (options.has_hpack_table_size()) { + parameter_collisions.push_back("hpack_table_size"); + } + break; + case NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS: + if (options.has_max_concurrent_streams()) { + parameter_collisions.push_back("max_concurrent_streams"); + } + break; + case NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE: + if (options.has_initial_stream_window_size()) { + parameter_collisions.push_back("initial_stream_window_size"); + } + break; + default: + // Ignore unknown parameters. + break; + } + } + + if (!custom_parameter_collisions.empty()) { + throw EnvoyException(fmt::format( + "inconsistent HTTP/2 custom SETTINGS parameter(s) detected; identifiers = {{{}}}", + absl::StrJoin(custom_parameter_collisions, ","))); + } + if (!parameter_collisions.empty()) { + throw EnvoyException(fmt::format( + "the {{{}}} HTTP/2 SETTINGS parameter(s) can not be configured through both named and " + "custom parameters", + absl::StrJoin(parameter_collisions, ","))); + } +} + +} // namespace + +const uint32_t OptionsLimits::MIN_HPACK_TABLE_SIZE; +const uint32_t OptionsLimits::DEFAULT_HPACK_TABLE_SIZE; +const uint32_t OptionsLimits::MAX_HPACK_TABLE_SIZE; +const uint32_t OptionsLimits::MIN_MAX_CONCURRENT_STREAMS; +const uint32_t OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS; +const uint32_t OptionsLimits::MAX_MAX_CONCURRENT_STREAMS; +const uint32_t OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE; +const uint32_t OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE; +const uint32_t OptionsLimits::MAX_INITIAL_STREAM_WINDOW_SIZE; +const uint32_t OptionsLimits::MIN_INITIAL_CONNECTION_WINDOW_SIZE; +const uint32_t OptionsLimits::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE; +const uint32_t OptionsLimits::MAX_INITIAL_CONNECTION_WINDOW_SIZE; +const uint32_t OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES; +const uint32_t OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES; +const uint32_t OptionsLimits::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD; +const uint32_t OptionsLimits::DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM; +const uint32_t OptionsLimits::DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT; + +envoy::config::core::v3::Http2ProtocolOptions +initializeAndValidateOptions(const envoy::config::core::v3::Http2ProtocolOptions& options) { + envoy::config::core::v3::Http2ProtocolOptions options_clone(options); + // This will throw an exception when a custom parameter and a named parameter collide. + validateCustomSettingsParameters(options); + + if (!options_clone.has_hpack_table_size()) { + options_clone.mutable_hpack_table_size()->set_value(OptionsLimits::DEFAULT_HPACK_TABLE_SIZE); + } + ASSERT(options_clone.hpack_table_size().value() <= OptionsLimits::MAX_HPACK_TABLE_SIZE); + if (!options_clone.has_max_concurrent_streams()) { + options_clone.mutable_max_concurrent_streams()->set_value( + OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS); + } + ASSERT( + options_clone.max_concurrent_streams().value() >= OptionsLimits::MIN_MAX_CONCURRENT_STREAMS && + options_clone.max_concurrent_streams().value() <= OptionsLimits::MAX_MAX_CONCURRENT_STREAMS); + if (!options_clone.has_initial_stream_window_size()) { + options_clone.mutable_initial_stream_window_size()->set_value( + OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE); + } + ASSERT(options_clone.initial_stream_window_size().value() >= + OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE && + options_clone.initial_stream_window_size().value() <= + OptionsLimits::MAX_INITIAL_STREAM_WINDOW_SIZE); + if (!options_clone.has_initial_connection_window_size()) { + options_clone.mutable_initial_connection_window_size()->set_value( + OptionsLimits::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE); + } + ASSERT(options_clone.initial_connection_window_size().value() >= + OptionsLimits::MIN_INITIAL_CONNECTION_WINDOW_SIZE && + options_clone.initial_connection_window_size().value() <= + OptionsLimits::MAX_INITIAL_CONNECTION_WINDOW_SIZE); + if (!options_clone.has_max_outbound_frames()) { + options_clone.mutable_max_outbound_frames()->set_value( + OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES); + } + if (!options_clone.has_max_outbound_control_frames()) { + options_clone.mutable_max_outbound_control_frames()->set_value( + OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES); + } + if (!options_clone.has_max_consecutive_inbound_frames_with_empty_payload()) { + options_clone.mutable_max_consecutive_inbound_frames_with_empty_payload()->set_value( + OptionsLimits::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD); + } + if (!options_clone.has_max_inbound_priority_frames_per_stream()) { + options_clone.mutable_max_inbound_priority_frames_per_stream()->set_value( + OptionsLimits::DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM); + } + if (!options_clone.has_max_inbound_window_update_frames_per_data_frame_sent()) { + options_clone.mutable_max_inbound_window_update_frames_per_data_frame_sent()->set_value( + OptionsLimits::DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT); + } + + return options_clone; +} + +} // namespace Utility +} // namespace Http2 + namespace Http { static const char kDefaultPath[] = "/"; @@ -241,38 +391,6 @@ bool Utility::isWebSocketUpgradeRequest(const RequestHeaderMap& headers) { Http::Headers::get().UpgradeValues.WebSocket)); } -Http2Settings -Utility::parseHttp2Settings(const envoy::config::core::v3::Http2ProtocolOptions& config) { - Http2Settings ret; - ret.hpack_table_size_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config, hpack_table_size, Http::Http2Settings::DEFAULT_HPACK_TABLE_SIZE); - ret.max_concurrent_streams_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config, max_concurrent_streams, Http::Http2Settings::DEFAULT_MAX_CONCURRENT_STREAMS); - ret.initial_stream_window_size_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config, initial_stream_window_size, Http::Http2Settings::DEFAULT_INITIAL_STREAM_WINDOW_SIZE); - ret.initial_connection_window_size_ = - PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, initial_connection_window_size, - Http::Http2Settings::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE); - ret.max_outbound_frames_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config, max_outbound_frames, Http::Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES); - ret.max_outbound_control_frames_ = - PROTOBUF_GET_WRAPPED_OR_DEFAULT(config, max_outbound_control_frames, - Http::Http2Settings::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES); - ret.max_consecutive_inbound_frames_with_empty_payload_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config, max_consecutive_inbound_frames_with_empty_payload, - Http::Http2Settings::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD); - ret.max_inbound_priority_frames_per_stream_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config, max_inbound_priority_frames_per_stream, - Http::Http2Settings::DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM); - ret.max_inbound_window_update_frames_per_data_frame_sent_ = PROTOBUF_GET_WRAPPED_OR_DEFAULT( - config, max_inbound_window_update_frames_per_data_frame_sent, - Http::Http2Settings::DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT); - ret.allow_connect_ = config.allow_connect(); - ret.allow_metadata_ = config.allow_metadata(); - ret.stream_error_on_invalid_http_messaging_ = config.stream_error_on_invalid_http_messaging(); - return ret; -} - Http1Settings Utility::parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& config) { Http1Settings ret; diff --git a/source/common/http/utility.h b/source/common/http/utility.h index 34a933606c83f..2daa237894d17 100644 --- a/source/common/http/utility.h +++ b/source/common/http/utility.h @@ -17,8 +17,81 @@ #include "absl/strings/string_view.h" #include "absl/types/optional.h" +#include "nghttp2/nghttp2.h" namespace Envoy { +namespace Http2 { +namespace Utility { + +struct SettingsEntryHash { + size_t operator()(const nghttp2_settings_entry& entry) const { + return absl::Hash()(entry.settings_id); + } +}; + +struct SettingsEntryEquals { + bool operator()(const nghttp2_settings_entry& lhs, const nghttp2_settings_entry& rhs) const { + return lhs.settings_id == rhs.settings_id; + } +}; + +// Limits and defaults for `envoy::config::core::v3::Http2ProtocolOptions` protos. +struct OptionsLimits { + // disable HPACK compression + static const uint32_t MIN_HPACK_TABLE_SIZE = 0; + // initial value from HTTP/2 spec, same as NGHTTP2_DEFAULT_HEADER_TABLE_SIZE from nghttp2 + static const uint32_t DEFAULT_HPACK_TABLE_SIZE = (1 << 12); + // no maximum from HTTP/2 spec, use unsigned 32-bit maximum + static const uint32_t MAX_HPACK_TABLE_SIZE = std::numeric_limits::max(); + // TODO(jwfang): make this 0, the HTTP/2 spec minimum + static const uint32_t MIN_MAX_CONCURRENT_STREAMS = 1; + // defaults to maximum, same as nghttp2 + static const uint32_t DEFAULT_MAX_CONCURRENT_STREAMS = (1U << 31) - 1; + // no maximum from HTTP/2 spec, total streams is unsigned 32-bit maximum, + // one-side (client/server) is half that, and we need to exclude stream 0. + // same as NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS from nghttp2 + static const uint32_t MAX_MAX_CONCURRENT_STREAMS = (1U << 31) - 1; + + // initial value from HTTP/2 spec, same as NGHTTP2_INITIAL_WINDOW_SIZE from nghttp2 + // NOTE: we only support increasing window size now, so this is also the minimum + // TODO(jwfang): make this 0 to support decrease window size + static const uint32_t MIN_INITIAL_STREAM_WINDOW_SIZE = (1 << 16) - 1; + // initial value from HTTP/2 spec is 65535, but we want more (256MiB) + static const uint32_t DEFAULT_INITIAL_STREAM_WINDOW_SIZE = 256 * 1024 * 1024; + // maximum from HTTP/2 spec, same as NGHTTP2_MAX_WINDOW_SIZE from nghttp2 + static const uint32_t MAX_INITIAL_STREAM_WINDOW_SIZE = (1U << 31) - 1; + + // CONNECTION_WINDOW_SIZE is similar to STREAM_WINDOW_SIZE, but for connection-level window + // TODO(jwfang): make this 0 to support decrease window size + static const uint32_t MIN_INITIAL_CONNECTION_WINDOW_SIZE = (1 << 16) - 1; + // nghttp2's default connection-level window equals to its stream-level, + // our default connection-level window also equals to our stream-level + static const uint32_t DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE = 256 * 1024 * 1024; + static const uint32_t MAX_INITIAL_CONNECTION_WINDOW_SIZE = (1U << 31) - 1; + + // Default limit on the number of outbound frames of all types. + static const uint32_t DEFAULT_MAX_OUTBOUND_FRAMES = 10000; + // Default limit on the number of outbound frames of types PING, SETTINGS and RST_STREAM. + static const uint32_t DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES = 1000; + // Default limit on the number of consecutive inbound frames with an empty payload + // and no end stream flag. + static const uint32_t DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD = 1; + // Default limit on the number of inbound frames of type PRIORITY (per stream). + static const uint32_t DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM = 100; + // Default limit on the number of inbound frames of type WINDOW_UPDATE (per DATA frame sent). + static const uint32_t DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT = 10; +}; + +/** + * Validates settings/options already set in |options| and initializes any remaining fields with + * defaults. + */ +envoy::config::core::v3::Http2ProtocolOptions +initializeAndValidateOptions(const envoy::config::core::v3::Http2ProtocolOptions& options); + +} // namespace Utility +} // namespace Http2 + namespace Http { namespace Utility { @@ -163,12 +236,6 @@ bool isH2UpgradeRequest(const RequestHeaderMap& headers); */ bool isWebSocketUpgradeRequest(const RequestHeaderMap& headers); -/** - * @return Http2Settings An Http2Settings populated from the - * envoy::api::v2::core::Http2ProtocolOptions config. - */ -Http2Settings parseHttp2Settings(const envoy::config::core::v3::Http2ProtocolOptions& config); - /** * @return Http1Settings An Http1Settings populated from the * envoy::api::v2::core::Http1ProtocolOptions config. diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index 37e12f1f2c1dd..c927ce9f04f17 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -677,7 +677,7 @@ ClusterInfoImpl::ClusterInfoImpl( : absl::nullopt), features_(parseFeatures(config)), http1_settings_(Http::Utility::parseHttp1Settings(config.http_protocol_options())), - http2_settings_(Http::Utility::parseHttp2Settings(config.http2_protocol_options())), + http2_options_(Http2::Utility::initializeAndValidateOptions(config.http2_protocol_options())), extension_protocol_options_(parseExtensionProtocolOptions(config, validation_visitor)), resource_managers_(config, runtime, name_, *stats_scope_), maintenance_mode_runtime_key_(absl::StrCat("upstream.maintenance_mode.", name_)), diff --git a/source/common/upstream/upstream_impl.h b/source/common/upstream/upstream_impl.h index 78d8fea3a7ef9..588e2cf553449 100644 --- a/source/common/upstream/upstream_impl.h +++ b/source/common/upstream/upstream_impl.h @@ -535,7 +535,9 @@ class ClusterInfoImpl : public ClusterInfo, protected Logger::Loggable timeout_budget_stats_; const uint64_t features_; const Http::Http1Settings http1_settings_; - const Http::Http2Settings http2_settings_; + const envoy::config::core::v3::Http2ProtocolOptions http2_options_; const std::map extension_protocol_options_; mutable ResourceManagers resource_managers_; const std::string maintenance_mode_runtime_key_; diff --git a/source/extensions/filters/network/http_connection_manager/config.cc b/source/extensions/filters/network/http_connection_manager/config.cc index 830a18c655ab7..ef0d3b02ff4b7 100644 --- a/source/extensions/filters/network/http_connection_manager/config.cc +++ b/source/extensions/filters/network/http_connection_manager/config.cc @@ -174,7 +174,7 @@ HttpConnectionManagerConfig::HttpConnectionManagerConfig( skip_xff_append_(config.skip_xff_append()), via_(config.via()), route_config_provider_manager_(route_config_provider_manager), scoped_routes_config_provider_manager_(scoped_routes_config_provider_manager), - http2_settings_(Http::Utility::parseHttp2Settings(config.http2_protocol_options())), + http2_options_(Http2::Utility::initializeAndValidateOptions(config.http2_protocol_options())), http1_settings_(Http::Utility::parseHttp1Settings(config.http_protocol_options())), max_request_headers_kb_(PROTOBUF_GET_WRAPPED_OR_DEFAULT( config, max_request_headers_kb, Http::DEFAULT_MAX_REQUEST_HEADERS_KB)), @@ -466,7 +466,7 @@ HttpConnectionManagerConfig::createCodec(Network::Connection& connection, maxRequestHeadersCount()); case CodecType::HTTP2: return std::make_unique( - connection, callbacks, context_.scope(), http2_settings_, maxRequestHeadersKb(), + connection, callbacks, context_.scope(), http2_options_, maxRequestHeadersKb(), maxRequestHeadersCount()); case CodecType::HTTP3: // Hard code Quiche factory name here to instantiate a QUIC codec implemented. @@ -479,7 +479,7 @@ HttpConnectionManagerConfig::createCodec(Network::Connection& connection, .createQuicServerConnection(connection, callbacks)); case CodecType::AUTO: return Http::ConnectionManagerUtility::autoCreateCodec( - connection, data, callbacks, context_.scope(), http1_settings_, http2_settings_, + connection, data, callbacks, context_.scope(), http1_settings_, http2_options_, maxRequestHeadersKb(), maxRequestHeadersCount()); } diff --git a/source/extensions/filters/network/http_connection_manager/config.h b/source/extensions/filters/network/http_connection_manager/config.h index 0ebd0ee920254..859f8795f48df 100644 --- a/source/extensions/filters/network/http_connection_manager/config.h +++ b/source/extensions/filters/network/http_connection_manager/config.h @@ -186,7 +186,7 @@ class HttpConnectionManagerConfig : Logger::Loggable, Router::RouteConfigProviderManager& route_config_provider_manager_; Config::ConfigProviderManager& scoped_routes_config_provider_manager_; CodecType codec_type_; - const Http::Http2Settings http2_settings_; + envoy::config::core::v3::Http2ProtocolOptions http2_options_; const Http::Http1Settings http1_settings_; HttpConnectionManagerProto::ServerHeaderTransformation server_transformation_{ HttpConnectionManagerProto::OVERWRITE}; diff --git a/source/extensions/quic_listeners/quiche/envoy_quic_dispatcher.cc b/source/extensions/quic_listeners/quiche/envoy_quic_dispatcher.cc index 85da3fcbb7c3c..ab999d5b204d9 100644 --- a/source/extensions/quic_listeners/quiche/envoy_quic_dispatcher.cc +++ b/source/extensions/quic_listeners/quiche/envoy_quic_dispatcher.cc @@ -1,5 +1,7 @@ #include "extensions/quic_listeners/quiche/envoy_quic_dispatcher.h" +#include "common/http/utility.h" + #include "extensions/quic_listeners/quiche/envoy_quic_server_connection.h" #include "extensions/quic_listeners/quiche/envoy_quic_server_session.h" @@ -30,8 +32,9 @@ EnvoyQuicDispatcher::EnvoyQuicDispatcher( // TODO(#8826) Ideally we should use the negotiated value from upstream which is not accessible // for now. 512MB is way to large, but the actual bytes buffered should be bound by the negotiated // upstream flow control window. - SetQuicFlag(FLAGS_quic_buffered_data_threshold, - 2 * Http::Http2Settings::DEFAULT_INITIAL_STREAM_WINDOW_SIZE); // 512MB + SetQuicFlag( + FLAGS_quic_buffered_data_threshold, + 2 * ::Envoy::Http2::Utility::OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE); // 512MB } void EnvoyQuicDispatcher::OnConnectionClosed(quic::QuicConnectionId connection_id, diff --git a/source/server/http/admin.cc b/source/server/http/admin.cc index 1382d48b51b7e..c11db982891ff 100644 --- a/source/server/http/admin.cc +++ b/source/server/http/admin.cc @@ -1457,7 +1457,9 @@ Http::ServerConnectionPtr AdminImpl::createCodec(Network::Connection& connection const Buffer::Instance& data, Http::ServerConnectionCallbacks& callbacks) { return Http::ConnectionManagerUtility::autoCreateCodec( - connection, data, callbacks, server_.stats(), Http::Http1Settings(), Http::Http2Settings(), + connection, data, callbacks, server_.stats(), Http::Http1Settings(), + ::Envoy::Http2::Utility::initializeAndValidateOptions( + envoy::config::core::v3::Http2ProtocolOptions()), maxRequestHeadersKb(), maxRequestHeadersCount()); } diff --git a/test/common/http/codec_impl_fuzz_test.cc b/test/common/http/codec_impl_fuzz_test.cc index 260c886058eca..5557420efeb8b 100644 --- a/test/common/http/codec_impl_fuzz_test.cc +++ b/test/common/http/codec_impl_fuzz_test.cc @@ -32,6 +32,8 @@ using testing::InvokeWithoutArgs; namespace Envoy { namespace Http { +namespace Http2Utility = ::Envoy::Http2::Utility; + // Force drain on each action, useful for figuring out what is going on when // debugging. constexpr bool DebugMode = false; @@ -51,27 +53,31 @@ Http1Settings fromHttp1Settings(const test::common::http::Http1ServerSettings& s return h1_settings; } -// Convert from test proto Http2Settings to Http2Settings. -Http2Settings fromHttp2Settings(const test::common::http::Http2Settings& settings) { - Http2Settings h2_settings; +envoy::config::core::v3::Http2ProtocolOptions +fromHttp2Settings(const test::common::http::Http2Settings& settings) { + envoy::config::core::v3::Http2ProtocolOptions options( + ::Envoy::Http2::Utility::initializeAndValidateOptions( + envoy::config::core::v3::Http2ProtocolOptions())); // We apply an offset and modulo interpretation to settings to ensure that // they are valid. Rejecting invalid settings is orthogonal to the fuzzed // code. - h2_settings.hpack_table_size_ = settings.hpack_table_size(); - h2_settings.max_concurrent_streams_ = - Http2Settings::MIN_MAX_CONCURRENT_STREAMS + - settings.max_concurrent_streams() % (1 + Http2Settings::MAX_MAX_CONCURRENT_STREAMS - - Http2Settings::MIN_MAX_CONCURRENT_STREAMS); - h2_settings.initial_stream_window_size_ = - Http2Settings::MIN_INITIAL_STREAM_WINDOW_SIZE + - settings.initial_stream_window_size() % (1 + Http2Settings::MAX_INITIAL_STREAM_WINDOW_SIZE - - Http2Settings::MIN_INITIAL_STREAM_WINDOW_SIZE); - h2_settings.initial_connection_window_size_ = - Http2Settings::MIN_INITIAL_CONNECTION_WINDOW_SIZE + + options.mutable_hpack_table_size()->set_value(settings.hpack_table_size()); + options.mutable_max_concurrent_streams()->set_value( + Http2Utility::OptionsLimits::MIN_MAX_CONCURRENT_STREAMS + + settings.max_concurrent_streams() % + (1 + Http2Utility::OptionsLimits::MAX_MAX_CONCURRENT_STREAMS - + Http2Utility::OptionsLimits::MIN_MAX_CONCURRENT_STREAMS)); + options.mutable_initial_stream_window_size()->set_value( + Http2Utility::OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE + + settings.initial_stream_window_size() % + (1 + Http2Utility::OptionsLimits::MAX_INITIAL_STREAM_WINDOW_SIZE - + Http2Utility::OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE)); + options.mutable_initial_connection_window_size()->set_value( + Http2Utility::OptionsLimits::MIN_INITIAL_CONNECTION_WINDOW_SIZE + settings.initial_connection_window_size() % - (1 + Http2Settings::MAX_INITIAL_CONNECTION_WINDOW_SIZE - - Http2Settings::MIN_INITIAL_CONNECTION_WINDOW_SIZE); - return h2_settings; + (1 + Http2Utility::OptionsLimits::MAX_INITIAL_CONNECTION_WINDOW_SIZE - + Http2Utility::OptionsLimits::MIN_INITIAL_CONNECTION_WINDOW_SIZE)); + return options; } // Internal representation of stream state. Encapsulates the stream state, mocks @@ -380,7 +386,8 @@ enum class HttpVersion { Http1, Http2 }; void codecFuzz(const test::common::http::CodecImplFuzzTestCase& input, HttpVersion http_version) { Stats::IsolatedStoreImpl stats_store; NiceMock client_connection; - const Http2Settings client_http2settings{fromHttp2Settings(input.h2_settings().client())}; + const envoy::config::core::v3::Http2ProtocolOptions client_http2_options{ + fromHttp2Settings(input.h2_settings().client())}; const Http1Settings client_http1settings; NiceMock client_callbacks; uint32_t max_request_headers_kb = Http::DEFAULT_MAX_REQUEST_HEADERS_KB; @@ -392,7 +399,7 @@ void codecFuzz(const test::common::http::CodecImplFuzzTestCase& input, HttpVersi if (http2) { client = std::make_unique( - client_connection, client_callbacks, stats_store, client_http2settings, + client_connection, client_callbacks, stats_store, client_http2_options, max_request_headers_kb, max_response_headers_count); } else { client = std::make_unique(client_connection, stats_store, @@ -403,9 +410,10 @@ void codecFuzz(const test::common::http::CodecImplFuzzTestCase& input, HttpVersi NiceMock server_connection; NiceMock server_callbacks; if (http2) { - const Http2Settings server_http2settings{fromHttp2Settings(input.h2_settings().server())}; + const envoy::config::core::v3::Http2ProtocolOptions server_http2_options{ + fromHttp2Settings(input.h2_settings().server())}; server = std::make_unique( - server_connection, server_callbacks, stats_store, server_http2settings, + server_connection, server_callbacks, stats_store, server_http2_options, max_request_headers_kb, max_request_headers_count); } else { const Http1Settings server_http1settings{fromHttp1Settings(input.h1_settings().server())}; diff --git a/test/common/http/http2/BUILD b/test/common/http/http2/BUILD index f4e2e4a6a370a..acfb83fe7b5c5 100644 --- a/test/common/http/http2/BUILD +++ b/test/common/http/http2/BUILD @@ -99,6 +99,7 @@ envoy_cc_test_library( deps = [ "//source/common/common:hex_lib", "//source/common/common:macros", + "//source/common/http:utility_lib", "//source/common/http/http2:codec_lib", "//test/common/http:common_lib", "//test/common/http/http2:codec_impl_test_util", diff --git a/test/common/http/http2/codec_impl_test.cc b/test/common/http/http2/codec_impl_test.cc index cc0063b04e3e5..05653042c91be 100644 --- a/test/common/http/http2/codec_impl_test.cc +++ b/test/common/http/http2/codec_impl_test.cc @@ -38,6 +38,7 @@ namespace Http2 { using Http2SettingsTuple = ::testing::tuple; using Http2SettingsTestParam = ::testing::tuple; +namespace CommonUtility = ::Envoy::Http2::Utility; class Http2CodecImplTestFixture { public: @@ -57,18 +58,25 @@ class Http2CodecImplTestFixture { Buffer::OwnedImpl buffer_; }; + enum SettingsTupleIndex { + HpackTableSize = 0, + MaxConcurrentStreams, + InitialStreamWindowSize, + InitialConnectionWindowSize + }; + Http2CodecImplTestFixture(Http2SettingsTuple client_settings, Http2SettingsTuple server_settings) : client_settings_(client_settings), server_settings_(server_settings) {} virtual ~Http2CodecImplTestFixture() = default; virtual void initialize() { - Http2SettingsFromTuple(client_http2settings_, client_settings_); - Http2SettingsFromTuple(server_http2settings_, server_settings_); + Http2OptionsFromTuple(client_http2_options_, client_settings_); + Http2OptionsFromTuple(server_http2_options_, server_settings_); client_ = std::make_unique( - client_connection_, client_callbacks_, stats_store_, client_http2settings_, + client_connection_, client_callbacks_, stats_store_, client_http2_options_, max_request_headers_kb_, max_response_headers_count_); server_ = std::make_unique( - server_connection_, server_callbacks_, stats_store_, server_http2settings_, + server_connection_, server_callbacks_, stats_store_, server_http2_options_, max_request_headers_kb_, max_request_headers_count_); request_encoder_ = &client_->newStream(response_decoder_); @@ -96,20 +104,26 @@ class Http2CodecImplTestFixture { })); } - void Http2SettingsFromTuple(Http2Settings& setting, const Http2SettingsTuple& tp) { - setting.hpack_table_size_ = ::testing::get<0>(tp); - setting.max_concurrent_streams_ = ::testing::get<1>(tp); - setting.initial_stream_window_size_ = ::testing::get<2>(tp); - setting.initial_connection_window_size_ = ::testing::get<3>(tp); - setting.allow_metadata_ = allow_metadata_; - setting.stream_error_on_invalid_http_messaging_ = stream_error_on_invalid_http_messaging_; - setting.max_outbound_frames_ = max_outbound_frames_; - setting.max_outbound_control_frames_ = max_outbound_control_frames_; - setting.max_consecutive_inbound_frames_with_empty_payload_ = - max_consecutive_inbound_frames_with_empty_payload_; - setting.max_inbound_priority_frames_per_stream_ = max_inbound_priority_frames_per_stream_; - setting.max_inbound_window_update_frames_per_data_frame_sent_ = - max_inbound_window_update_frames_per_data_frame_sent_; + void Http2OptionsFromTuple(envoy::config::core::v3::Http2ProtocolOptions& options, + const Http2SettingsTuple& tp) { + options.mutable_hpack_table_size()->set_value( + ::testing::get(tp)); + options.mutable_max_concurrent_streams()->set_value( + ::testing::get(tp)); + options.mutable_initial_stream_window_size()->set_value( + ::testing::get(tp)); + options.mutable_initial_connection_window_size()->set_value( + ::testing::get(tp)); + options.set_allow_metadata(allow_metadata_); + options.set_stream_error_on_invalid_http_messaging(stream_error_on_invalid_http_messaging_); + options.mutable_max_outbound_frames()->set_value(max_outbound_frames_); + options.mutable_max_outbound_control_frames()->set_value(max_outbound_control_frames_); + options.mutable_max_consecutive_inbound_frames_with_empty_payload()->set_value( + max_consecutive_inbound_frames_with_empty_payload_); + options.mutable_max_inbound_priority_frames_per_stream()->set_value( + max_inbound_priority_frames_per_stream_); + options.mutable_max_inbound_window_update_frames_per_data_frame_sent()->set_value( + max_inbound_window_update_frames_per_data_frame_sent_); } // corruptMetadataFramePayload assumes data contains at least 10 bytes of the beginning of a @@ -144,12 +158,12 @@ class Http2CodecImplTestFixture { bool allow_metadata_ = false; bool stream_error_on_invalid_http_messaging_ = false; Stats::IsolatedStoreImpl stats_store_; - Http2Settings client_http2settings_; + envoy::config::core::v3::Http2ProtocolOptions client_http2_options_; NiceMock client_connection_; MockConnectionCallbacks client_callbacks_; std::unique_ptr client_; ConnectionWrapper client_wrapper_; - Http2Settings server_http2settings_; + envoy::config::core::v3::Http2ProtocolOptions server_http2_options_; NiceMock server_connection_; MockServerConnectionCallbacks server_callbacks_; std::unique_ptr server_; @@ -165,14 +179,15 @@ class Http2CodecImplTestFixture { uint32_t max_request_headers_kb_ = Http::DEFAULT_MAX_REQUEST_HEADERS_KB; uint32_t max_request_headers_count_ = Http::DEFAULT_MAX_HEADERS_COUNT; uint32_t max_response_headers_count_ = Http::DEFAULT_MAX_HEADERS_COUNT; - uint32_t max_outbound_frames_ = Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES; - uint32_t max_outbound_control_frames_ = Http2Settings::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES; + uint32_t max_outbound_frames_ = CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES; + uint32_t max_outbound_control_frames_ = + CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES; uint32_t max_consecutive_inbound_frames_with_empty_payload_ = - Http2Settings::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD; + CommonUtility::OptionsLimits::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD; uint32_t max_inbound_priority_frames_per_stream_ = - Http2Settings::DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM; + CommonUtility::OptionsLimits::DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM; uint32_t max_inbound_window_update_frames_per_data_frame_sent_ = - Http2Settings::DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT; + CommonUtility::OptionsLimits::DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT; }; class Http2CodecImplTest : public ::testing::TestWithParam, @@ -193,7 +208,7 @@ class Http2CodecImplTest : public ::testing::TestWithParamsession(), NGHTTP2_FLAG_NONE, 1, &spec)); } @@ -218,7 +233,9 @@ class Http2CodecImplTest : public ::testing::TestWithParamsession(), NGHTTP2_FLAG_NONE, 1, 1)); } @@ -236,7 +253,7 @@ class Http2CodecImplTest : public ::testing::TestWithParamencodeHeaders(response_headers, true); } +class Http2CodecImplSettingsBasicTest : public Http2CodecImplTest {}; +TEST_P(Http2CodecImplSettingsBasicTest, ) {} + class Http2CodecImplStreamLimitTest : public Http2CodecImplTest {}; // Regression test for issue #3076. @@ -966,13 +986,13 @@ class Http2CodecImplStreamLimitTest : public Http2CodecImplTest {}; // TODO(PiotrSikora): add tests that exercise both scenarios: before and after receiving // the HTTP/2 SETTINGS frame. TEST_P(Http2CodecImplStreamLimitTest, MaxClientStreams) { - Http2SettingsFromTuple(client_http2settings_, ::testing::get<0>(GetParam())); - Http2SettingsFromTuple(server_http2settings_, ::testing::get<1>(GetParam())); + Http2OptionsFromTuple(client_http2_options_, ::testing::get<0>(GetParam())); + Http2OptionsFromTuple(server_http2_options_, ::testing::get<1>(GetParam())); client_ = std::make_unique( - client_connection_, client_callbacks_, stats_store_, client_http2settings_, + client_connection_, client_callbacks_, stats_store_, client_http2_options_, max_request_headers_kb_, max_response_headers_count_); server_ = std::make_unique( - server_connection_, server_callbacks_, stats_store_, server_http2settings_, + server_connection_, server_callbacks_, stats_store_, server_http2_options_, max_request_headers_kb_, max_request_headers_count_); for (int i = 0; i < 101; ++i) { @@ -993,10 +1013,11 @@ TEST_P(Http2CodecImplStreamLimitTest, MaxClientStreams) { } #define HTTP2SETTINGS_SMALL_WINDOW_COMBINE \ - ::testing::Combine(::testing::Values(Http2Settings::DEFAULT_HPACK_TABLE_SIZE), \ - ::testing::Values(Http2Settings::DEFAULT_MAX_CONCURRENT_STREAMS), \ - ::testing::Values(Http2Settings::MIN_INITIAL_STREAM_WINDOW_SIZE), \ - ::testing::Values(Http2Settings::MIN_INITIAL_CONNECTION_WINDOW_SIZE)) + ::testing::Combine( \ + ::testing::Values(CommonUtility::OptionsLimits::DEFAULT_HPACK_TABLE_SIZE), \ + ::testing::Values(CommonUtility::OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS), \ + ::testing::Values(CommonUtility::OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE), \ + ::testing::Values(CommonUtility::OptionsLimits::MIN_INITIAL_CONNECTION_WINDOW_SIZE)) // Deferred reset tests use only small windows so that we can test certain conditions. INSTANTIATE_TEST_SUITE_P(Http2CodecImplDeferredResetTest, Http2CodecImplDeferredResetTest, @@ -1010,10 +1031,11 @@ INSTANTIATE_TEST_SUITE_P(Http2CodecImplFlowControlTest, Http2CodecImplFlowContro // we separate default/edge cases here to avoid combinatorial explosion #define HTTP2SETTINGS_DEFAULT_COMBINE \ - ::testing::Combine(::testing::Values(Http2Settings::DEFAULT_HPACK_TABLE_SIZE), \ - ::testing::Values(Http2Settings::DEFAULT_MAX_CONCURRENT_STREAMS), \ - ::testing::Values(Http2Settings::DEFAULT_INITIAL_STREAM_WINDOW_SIZE), \ - ::testing::Values(Http2Settings::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE)) + ::testing::Combine( \ + ::testing::Values(CommonUtility::OptionsLimits::DEFAULT_HPACK_TABLE_SIZE), \ + ::testing::Values(CommonUtility::OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS), \ + ::testing::Values(CommonUtility::OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE), \ + ::testing::Values(CommonUtility::OptionsLimits::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE)) // Stream limit test only uses the default values because not all combinations of // edge settings allow for the number of streams needed by the test. @@ -1027,13 +1049,14 @@ INSTANTIATE_TEST_SUITE_P(Http2CodecImplTestDefaultSettings, Http2CodecImplTest, #define HTTP2SETTINGS_EDGE_COMBINE \ ::testing::Combine( \ - ::testing::Values(Http2Settings::MIN_HPACK_TABLE_SIZE, Http2Settings::MAX_HPACK_TABLE_SIZE), \ - ::testing::Values(Http2Settings::MIN_MAX_CONCURRENT_STREAMS, \ - Http2Settings::MAX_MAX_CONCURRENT_STREAMS), \ - ::testing::Values(Http2Settings::MIN_INITIAL_STREAM_WINDOW_SIZE, \ - Http2Settings::MAX_INITIAL_STREAM_WINDOW_SIZE), \ - ::testing::Values(Http2Settings::MIN_INITIAL_CONNECTION_WINDOW_SIZE, \ - Http2Settings::MAX_INITIAL_CONNECTION_WINDOW_SIZE)) + ::testing::Values(CommonUtility::OptionsLimits::MIN_HPACK_TABLE_SIZE, \ + CommonUtility::OptionsLimits::MAX_HPACK_TABLE_SIZE), \ + ::testing::Values(CommonUtility::OptionsLimits::MIN_MAX_CONCURRENT_STREAMS, \ + CommonUtility::OptionsLimits::MAX_MAX_CONCURRENT_STREAMS), \ + ::testing::Values(CommonUtility::OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE, \ + CommonUtility::OptionsLimits::MAX_INITIAL_STREAM_WINDOW_SIZE), \ + ::testing::Values(CommonUtility::OptionsLimits::MIN_INITIAL_CONNECTION_WINDOW_SIZE, \ + CommonUtility::OptionsLimits::MAX_INITIAL_CONNECTION_WINDOW_SIZE)) // Make sure we have coverage for high and low values for various combinations and permutations // of HTTP settings in at least one test fixture. @@ -1081,6 +1104,117 @@ TEST(Http2CodecUtility, reconstituteCrumbledCookies) { } } +MATCHER_P(HasValue, m, "") { + if (!arg.has_value()) { + *result_listener << "does not contain a value"; + return false; + } + const auto& value = arg.value(); + return ExplainMatchResult(m, value, result_listener); +}; + +class Http2CustomSettingsTestBase : public Http2CodecImplTestFixture { +public: + struct SettingsParameter { + uint16_t identifier; + uint32_t value; + }; + + Http2CustomSettingsTestBase(Http2SettingsTuple client_settings, + Http2SettingsTuple server_settings, bool validate_client) + : Http2CodecImplTestFixture(client_settings, server_settings), + validate_client_(validate_client) {} + + virtual ~Http2CustomSettingsTestBase() = default; + + // Sets the custom settings parameters specified by |parameters| in the |options| proto. + void setHttp2CustomSettingsParameters(envoy::config::core::v3::Http2ProtocolOptions& options, + std::vector parameters) { + for (const auto& parameter : parameters) { + envoy::config::core::v3::Http2ProtocolOptions::SettingsParameter* custom_param = + options.mutable_custom_settings_parameters()->Add(); + custom_param->mutable_identifier()->set_value(parameter.identifier); + custom_param->mutable_value()->set_value(parameter.value); + } + } + + // Returns the Http2ProtocolOptions proto which specifies the settings parameters to be sent to + // the endpoint being validated. + envoy::config::core::v3::Http2ProtocolOptions& getCustomOptions() { + return validate_client_ ? server_http2_options_ : client_http2_options_; + } + + // Returns the endpoint being validated. + const TestCodecSettingsProvider& getSettingsProvider() { + if (validate_client_) { + return *client_; + } + return *server_; + } + + // Returns the settings tuple which specifies a subset of the settings parameters to be sent to + // the endpoint being validated. + const Http2SettingsTuple& getSettingsTuple() { + return validate_client_ ? server_settings_ : client_settings_; + } + +protected: + bool validate_client_{false}; +}; + +class Http2CustomSettingsTest + : public Http2CustomSettingsTestBase, + public ::testing::TestWithParam< + ::testing::tuple> { +public: + Http2CustomSettingsTest() + : Http2CustomSettingsTestBase(::testing::get<0>(GetParam()), ::testing::get<1>(GetParam()), + ::testing::get<2>(GetParam())) {} +}; +INSTANTIATE_TEST_SUITE_P(Http2CodecImplTestEdgeSettings, Http2CustomSettingsTest, + ::testing::Combine(HTTP2SETTINGS_DEFAULT_COMBINE, + HTTP2SETTINGS_DEFAULT_COMBINE, ::testing::Bool())); + +// Validates that custom parameters (those which are not explicitly named in the +// envoy::config::core::v3::Http2ProtocolOptions proto) are properly sent and processed by +// client and server connections. +TEST_P(Http2CustomSettingsTest, UserDefinedSettings) { + std::vector custom_parameters{{0x10, 10}, {0x11, 20}}; + setHttp2CustomSettingsParameters(getCustomOptions(), custom_parameters); + initialize(); + TestRequestHeaderMapImpl request_headers; + HttpTestUtility::addDefaultHeaders(request_headers); + EXPECT_CALL(request_decoder_, decodeHeaders_(_, _)); + request_encoder_->encodeHeaders(request_headers, false); + uint32_t hpack_table_size = + ::testing::get(getSettingsTuple()); + if (hpack_table_size != NGHTTP2_DEFAULT_HEADER_TABLE_SIZE) { + EXPECT_THAT( + getSettingsProvider().getRemoteSettingsParameterValue(NGHTTP2_SETTINGS_HEADER_TABLE_SIZE), + HasValue(hpack_table_size)); + } + uint32_t max_concurrent_streams = + ::testing::get(getSettingsTuple()); + if (max_concurrent_streams != NGHTTP2_INITIAL_MAX_CONCURRENT_STREAMS) { + EXPECT_THAT(getSettingsProvider().getRemoteSettingsParameterValue( + NGHTTP2_SETTINGS_MAX_CONCURRENT_STREAMS), + HasValue(max_concurrent_streams)); + } + uint32_t initial_stream_window_size = + ::testing::get(getSettingsTuple()); + if (max_concurrent_streams != NGHTTP2_INITIAL_WINDOW_SIZE) { + EXPECT_THAT( + getSettingsProvider().getRemoteSettingsParameterValue(NGHTTP2_SETTINGS_INITIAL_WINDOW_SIZE), + HasValue(initial_stream_window_size)); + } + // Validate that custom parameters are received by the endpoint (client or server) under + // test. + for (const auto& parameter : custom_parameters) { + EXPECT_THAT(getSettingsProvider().getRemoteSettingsParameterValue(parameter.identifier), + HasValue(parameter.value)); + } +} + // Tests request headers whose size is larger than the default limit of 60K. TEST_P(Http2CodecImplTest, LargeRequestHeadersInvokeResetStream) { initialize(); @@ -1284,8 +1418,10 @@ TEST_P(Http2CodecImplTestAll, TestCodecHeaderCompression) { EXPECT_EQ(nghttp2_session_get_hd_deflate_dynamic_table_size(client_->session()), nghttp2_session_get_hd_inflate_dynamic_table_size(server_->session())); - // Verify that headers are compressed only when both client and server advertise table size > 0: - if (client_http2settings_.hpack_table_size_ && server_http2settings_.hpack_table_size_) { + // Verify that headers are compressed only when both client and server advertise table size + // > 0: + if (client_http2_options_.hpack_table_size().value() && + server_http2_options_.hpack_table_size().value()) { EXPECT_NE(0, nghttp2_session_get_hd_deflate_dynamic_table_size(client_->session())); EXPECT_NE(0, nghttp2_session_get_hd_deflate_dynamic_table_size(server_->session())); } else { @@ -1304,7 +1440,8 @@ TEST_P(Http2CodecImplTest, PingFlood) { request_encoder_->encodeHeaders(request_headers, false); // Send one frame above the outbound control queue size limit - for (uint32_t i = 0; i < Http2Settings::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES + 1; ++i) { + for (uint32_t i = 0; i < CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES + 1; + ++i) { EXPECT_EQ(0, nghttp2_submit_ping(client_->session(), NGHTTP2_FLAG_NONE, nullptr)); } @@ -1317,7 +1454,7 @@ TEST_P(Http2CodecImplTest, PingFlood) { })); EXPECT_THROW(client_->sendPendingFrames(), FrameFloodException); - EXPECT_EQ(ack_count, Http2Settings::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES); + EXPECT_EQ(ack_count, CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES); EXPECT_EQ(1, stats_store_.counter("http2.outbound_control_flood").value()); } @@ -1332,12 +1469,13 @@ TEST_P(Http2CodecImplTest, PingFloodMitigationDisabled) { request_encoder_->encodeHeaders(request_headers, false); // Send one frame above the outbound control queue size limit - for (uint32_t i = 0; i < Http2Settings::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES + 1; ++i) { + for (uint32_t i = 0; i < CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES + 1; + ++i) { EXPECT_EQ(0, nghttp2_submit_ping(client_->session(), NGHTTP2_FLAG_NONE, nullptr)); } EXPECT_CALL(server_connection_, write(_, _)) - .Times(Http2Settings::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES + 1); + .Times(CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES + 1); EXPECT_NO_THROW(client_->sendPendingFrames()); } @@ -1375,7 +1513,8 @@ TEST_P(Http2CodecImplTest, PingFloodCounterReset) { for (int i = 0; i < kMaxOutboundControlFrames / 2; ++i) { EXPECT_EQ(0, nghttp2_submit_ping(client_->session(), NGHTTP2_FLAG_NONE, nullptr)); } - // The number of outbound frames should be half of max so the connection should not be terminated. + // The number of outbound frames should be half of max so the connection should not be + // terminated. EXPECT_NO_THROW(client_->sendPendingFrames()); // 1 more ping frame should overflow the outbound frame limit. @@ -1401,7 +1540,7 @@ TEST_P(Http2CodecImplTest, ResponseHeadersFlood) { })); TestResponseHeaderMapImpl response_headers{{":status", "200"}}; - for (uint32_t i = 0; i < Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES + 1; ++i) { + for (uint32_t i = 0; i < CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES + 1; ++i) { EXPECT_NO_THROW(response_encoder_->encodeHeaders(response_headers, false)); } // Presently flood mitigation is done only when processing downstream data @@ -1409,7 +1548,7 @@ TEST_P(Http2CodecImplTest, ResponseHeadersFlood) { EXPECT_EQ(0, nghttp2_submit_ping(client_->session(), NGHTTP2_FLAG_NONE, nullptr)); EXPECT_THROW(client_->sendPendingFrames(), FrameFloodException); - EXPECT_EQ(frame_count, Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES + 1); + EXPECT_EQ(frame_count, CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES + 1); EXPECT_EQ(1, stats_store_.counter("http2.outbound_flood").value()); } @@ -1433,7 +1572,7 @@ TEST_P(Http2CodecImplTest, ResponseDataFlood) { TestResponseHeaderMapImpl response_headers{{":status", "200"}}; response_encoder_->encodeHeaders(response_headers, false); // Account for the single HEADERS frame above - for (uint32_t i = 0; i < Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES; ++i) { + for (uint32_t i = 0; i < CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES; ++i) { Buffer::OwnedImpl data("0"); EXPECT_NO_THROW(response_encoder_->encodeData(data, false)); } @@ -1442,7 +1581,7 @@ TEST_P(Http2CodecImplTest, ResponseDataFlood) { EXPECT_EQ(0, nghttp2_submit_ping(client_->session(), NGHTTP2_FLAG_NONE, nullptr)); EXPECT_THROW(client_->sendPendingFrames(), FrameFloodException); - EXPECT_EQ(frame_count, Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES + 1); + EXPECT_EQ(frame_count, CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES + 1); EXPECT_EQ(1, stats_store_.counter("http2.outbound_flood").value()); } @@ -1458,14 +1597,14 @@ TEST_P(Http2CodecImplTest, ResponseDataFloodMitigationDisabled) { // +2 is to account for HEADERS and PING ACK, that is used to trigger mitigation EXPECT_CALL(server_connection_, write(_, _)) - .Times(Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES + 2); + .Times(CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES + 2); EXPECT_CALL(response_decoder_, decodeHeaders_(_, false)).Times(1); EXPECT_CALL(response_decoder_, decodeData(_, false)) - .Times(Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES); + .Times(CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES); TestResponseHeaderMapImpl response_headers{{":status", "200"}}; response_encoder_->encodeHeaders(response_headers, false); // Account for the single HEADERS frame above - for (uint32_t i = 0; i < Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES; ++i) { + for (uint32_t i = 0; i < CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES; ++i) { Buffer::OwnedImpl data("0"); EXPECT_NO_THROW(response_encoder_->encodeData(data, false)); } @@ -1537,7 +1676,7 @@ TEST_P(Http2CodecImplTest, PingStacksWithDataFlood) { TestResponseHeaderMapImpl response_headers{{":status", "200"}}; response_encoder_->encodeHeaders(response_headers, false); // Account for the single HEADERS frame above - for (uint32_t i = 0; i < Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES - 1; ++i) { + for (uint32_t i = 0; i < CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES - 1; ++i) { Buffer::OwnedImpl data("0"); EXPECT_NO_THROW(response_encoder_->encodeData(data, false)); } @@ -1545,7 +1684,7 @@ TEST_P(Http2CodecImplTest, PingStacksWithDataFlood) { EXPECT_EQ(0, nghttp2_submit_ping(client_->session(), NGHTTP2_FLAG_NONE, nullptr)); EXPECT_THROW(client_->sendPendingFrames(), FrameFloodException); - EXPECT_EQ(frame_count, Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES); + EXPECT_EQ(frame_count, CommonUtility::OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES); EXPECT_EQ(1, stats_store_.counter("http2.outbound_flood").value()); } @@ -1584,7 +1723,9 @@ TEST_P(Http2CodecImplTest, EmptyDataFloodOverride) { Buffer::OwnedImpl data; emptyDataFlood(data); EXPECT_CALL(request_decoder_, decodeData(_, false)) - .Times(Http2Settings::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD + 1); + .Times( + CommonUtility::OptionsLimits::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD + + 1); EXPECT_NO_THROW(server_wrapper_.dispatch(data, *server_)); } diff --git a/test/common/http/http2/codec_impl_test_util.h b/test/common/http/http2/codec_impl_test_util.h index 67521db0fec86..80e3c2d096b19 100644 --- a/test/common/http/http2/codec_impl_test_util.h +++ b/test/common/http/http2/codec_impl_test_util.h @@ -1,32 +1,72 @@ +#pragma once + #include "envoy/http/codec.h" #include "common/http/http2/codec_impl.h" +#include "common/http/utility.h" namespace Envoy { namespace Http { namespace Http2 { -class TestServerConnectionImpl : public ServerConnectionImpl { +class TestCodecSettingsProvider { +public: + // Returns the value of the SETTINGS parameter keyed by |identifier| sent by the remote endpoint. + absl::optional getRemoteSettingsParameterValue(int32_t identifier) const { + const auto it = settings_.find({identifier, 0}); + if (it == settings_.end()) { + return absl::nullopt; + } + return it->value; + } + +protected: + // Stores SETTINGS parameters contained in |settings_frame| to make them available via + // getRemoteSettingsParameterValue(). + void onSettingsFrame(const nghttp2_settings& settings_frame) { + for (uint32_t i = 0; i < settings_frame.niv; ++i) { + auto result = settings_.insert(settings_frame.iv[i]); + ASSERT(result.second); + } + } + +private: + std::unordered_set + settings_; +}; + +class TestServerConnectionImpl : public ServerConnectionImpl, public TestCodecSettingsProvider { public: TestServerConnectionImpl(Network::Connection& connection, ServerConnectionCallbacks& callbacks, - Stats::Scope& scope, const Http2Settings& http2_settings, + Stats::Scope& scope, + const envoy::config::core::v3::Http2ProtocolOptions& http2_options, uint32_t max_request_headers_kb, uint32_t max_request_headers_count) - : ServerConnectionImpl(connection, callbacks, scope, http2_settings, max_request_headers_kb, + : ServerConnectionImpl(connection, callbacks, scope, http2_options, max_request_headers_kb, max_request_headers_count) {} nghttp2_session* session() { return session_; } using ServerConnectionImpl::getStream; + +protected: + // Overrides ServerConnectionImpl::onSettingsForTest(). + void onSettingsForTest(const nghttp2_settings& settings) override { onSettingsFrame(settings); } }; -class TestClientConnectionImpl : public ClientConnectionImpl { +class TestClientConnectionImpl : public ClientConnectionImpl, public TestCodecSettingsProvider { public: TestClientConnectionImpl(Network::Connection& connection, Http::ConnectionCallbacks& callbacks, - Stats::Scope& scope, const Http2Settings& http2_settings, + Stats::Scope& scope, + const envoy::config::core::v3::Http2ProtocolOptions& http2_options, uint32_t max_request_headers_kb, uint32_t max_request_headers_count) - : ClientConnectionImpl(connection, callbacks, scope, http2_settings, max_request_headers_kb, + : ClientConnectionImpl(connection, callbacks, scope, http2_options, max_request_headers_kb, max_request_headers_count) {} nghttp2_session* session() { return session_; } using ClientConnectionImpl::getStream; using ConnectionImpl::sendPendingFrames; + +protected: + // Overrides ClientConnectionImpl::onSettingsForTest(). + void onSettingsForTest(const nghttp2_settings& settings) override { onSettingsFrame(settings); } }; } // namespace Http2 diff --git a/test/common/http/http2/conn_pool_test.cc b/test/common/http/http2/conn_pool_test.cc index 03abccfcf2a83..b10619f2795d1 100644 --- a/test/common/http/http2/conn_pool_test.cc +++ b/test/common/http/http2/conn_pool_test.cc @@ -248,7 +248,7 @@ TEST_F(Http2ConnPoolImplTest, DrainConnectionReadyWithRequest) { * complete. */ TEST_F(Http2ConnPoolImplTest, DrainConnectionBusy) { - cluster_->http2_settings_.max_concurrent_streams_ = 1; + cluster_->http2_options_.mutable_max_concurrent_streams()->set_value(1); InSequence s; expectClientCreate(); @@ -331,7 +331,7 @@ TEST_F(Http2ConnPoolImplTest, DrainConnections) { // concurrent requests and causes additional connections to be created. TEST_F(Http2ConnPoolImplTest, MaxConcurrentRequestsPerStream) { cluster_->resetResourceManager(2, 1024, 1024, 1, 1); - cluster_->http2_settings_.max_concurrent_streams_ = 1; + cluster_->http2_options_.mutable_max_concurrent_streams()->set_value(1); InSequence s; @@ -462,7 +462,7 @@ TEST_F(Http2ConnPoolImplTest, PendingRequestsNumberConnectingTotalRequestsPerCon // Verifies that the correct number of CONNECTING connections are created for // the pending requests, when the concurrent requests per connection is limited TEST_F(Http2ConnPoolImplTest, PendingRequestsNumberConnectingConcurrentRequestsPerConnection) { - cluster_->http2_settings_.max_concurrent_streams_ = 2; + cluster_->http2_options_.mutable_max_concurrent_streams()->set_value(2); InSequence s; // Create three requests. The 3rd should create a 2nd connection due to the limit diff --git a/test/common/http/http2/frame_replay.cc b/test/common/http/http2/frame_replay.cc index fb6d5f94688d1..7f63fd514160d 100644 --- a/test/common/http/http2/frame_replay.cc +++ b/test/common/http/http2/frame_replay.cc @@ -2,6 +2,7 @@ #include "common/common/hex.h" #include "common/common/macros.h" +#include "common/http/utility.h" #include "test/common/http/common.h" #include "test/test_common/environment.h" @@ -58,13 +59,9 @@ void FrameUtils::fixupHeaders(Frame& frame) { } CodecFrameInjector::CodecFrameInjector(const std::string& injector_name) - : injector_name_(injector_name) { - settings_.hpack_table_size_ = Http2Settings::DEFAULT_HPACK_TABLE_SIZE; - settings_.max_concurrent_streams_ = Http2Settings::DEFAULT_MAX_CONCURRENT_STREAMS; - settings_.initial_stream_window_size_ = Http2Settings::DEFAULT_INITIAL_STREAM_WINDOW_SIZE; - settings_.initial_connection_window_size_ = Http2Settings::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE; - settings_.allow_metadata_ = false; -} + : options_(::Envoy::Http2::Utility::initializeAndValidateOptions( + envoy::config::core::v3::Http2ProtocolOptions())), + injector_name_(injector_name) {} ClientCodecFrameInjector::ClientCodecFrameInjector() : CodecFrameInjector("server") { ON_CALL(client_connection_, write(_, _)) diff --git a/test/common/http/http2/frame_replay.h b/test/common/http/http2/frame_replay.h index afa9c6014ebfa..7024c292cb4e9 100644 --- a/test/common/http/http2/frame_replay.h +++ b/test/common/http/http2/frame_replay.h @@ -55,7 +55,7 @@ class CodecFrameInjector { // Writes the data using the Http::Connection's nghttp2 session. void write(const Frame& frame, Http::Connection& connection); - Http2Settings settings_; + envoy::config::core::v3::Http2ProtocolOptions options_; Stats::IsolatedStoreImpl stats_store_; const std::string injector_name_; }; diff --git a/test/common/http/http2/frame_replay_test.cc b/test/common/http/http2/frame_replay_test.cc index c040f8adb1fc9..2dcedab7c5819 100644 --- a/test/common/http/http2/frame_replay_test.cc +++ b/test/common/http/http2/frame_replay_test.cc @@ -57,7 +57,7 @@ TEST_F(RequestFrameCommentTest, SimpleExampleHuffman) { // Validate HEADERS decode. ServerCodecFrameInjector codec; TestServerConnectionImpl connection( - codec.server_connection_, codec.server_callbacks_, codec.stats_store_, codec.settings_, + codec.server_connection_, codec.server_callbacks_, codec.stats_store_, codec.options_, Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT); codec.write(WellKnownFrames::clientConnectionPrefaceFrame(), connection); codec.write(WellKnownFrames::defaultSettingsFrame(), connection); @@ -89,7 +89,7 @@ TEST_F(ResponseFrameCommentTest, SimpleExampleHuffman) { // Validate HEADERS decode. ClientCodecFrameInjector codec; TestClientConnectionImpl connection( - codec.client_connection_, codec.client_callbacks_, codec.stats_store_, codec.settings_, + codec.client_connection_, codec.client_callbacks_, codec.stats_store_, codec.options_, Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT); setupStream(codec, connection); @@ -133,7 +133,7 @@ TEST_F(RequestFrameCommentTest, SimpleExamplePlain) { // Validate HEADERS decode. ServerCodecFrameInjector codec; TestServerConnectionImpl connection( - codec.server_connection_, codec.server_callbacks_, codec.stats_store_, codec.settings_, + codec.server_connection_, codec.server_callbacks_, codec.stats_store_, codec.options_, Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT); codec.write(WellKnownFrames::clientConnectionPrefaceFrame(), connection); codec.write(WellKnownFrames::defaultSettingsFrame(), connection); @@ -167,7 +167,7 @@ TEST_F(ResponseFrameCommentTest, SimpleExamplePlain) { // Validate HEADERS decode. ClientCodecFrameInjector codec; TestClientConnectionImpl connection( - codec.client_connection_, codec.client_callbacks_, codec.stats_store_, codec.settings_, + codec.client_connection_, codec.client_callbacks_, codec.stats_store_, codec.options_, Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT); setupStream(codec, connection); @@ -196,7 +196,7 @@ TEST_F(RequestFrameCommentTest, SingleByteNulCrLfInHeaderFrame) { // Play the frames back. ServerCodecFrameInjector codec; TestServerConnectionImpl connection( - codec.server_connection_, codec.server_callbacks_, codec.stats_store_, codec.settings_, + codec.server_connection_, codec.server_callbacks_, codec.stats_store_, codec.options_, Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT); codec.write(WellKnownFrames::clientConnectionPrefaceFrame(), connection); codec.write(WellKnownFrames::defaultSettingsFrame(), connection); @@ -229,7 +229,7 @@ TEST_F(ResponseFrameCommentTest, SingleByteNulCrLfInHeaderFrame) { // Play the frames back. ClientCodecFrameInjector codec; TestClientConnectionImpl connection( - codec.client_connection_, codec.client_callbacks_, codec.stats_store_, codec.settings_, + codec.client_connection_, codec.client_callbacks_, codec.stats_store_, codec.options_, Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT); setupStream(codec, connection); @@ -264,7 +264,7 @@ TEST_F(RequestFrameCommentTest, SingleByteNulCrLfInHeaderField) { // Play the frames back. ServerCodecFrameInjector codec; TestServerConnectionImpl connection( - codec.server_connection_, codec.server_callbacks_, codec.stats_store_, codec.settings_, + codec.server_connection_, codec.server_callbacks_, codec.stats_store_, codec.options_, Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT); codec.write(WellKnownFrames::clientConnectionPrefaceFrame(), connection); codec.write(WellKnownFrames::defaultSettingsFrame(), connection); @@ -302,7 +302,7 @@ TEST_F(ResponseFrameCommentTest, SingleByteNulCrLfInHeaderField) { // Play the frames back. ClientCodecFrameInjector codec; TestClientConnectionImpl connection( - codec.client_connection_, codec.client_callbacks_, codec.stats_store_, codec.settings_, + codec.client_connection_, codec.client_callbacks_, codec.stats_store_, codec.options_, Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT); setupStream(codec, connection); diff --git a/test/common/http/http2/request_header_fuzz_test.cc b/test/common/http/http2/request_header_fuzz_test.cc index 440014afb648b..35bce06813b33 100644 --- a/test/common/http/http2/request_header_fuzz_test.cc +++ b/test/common/http/http2/request_header_fuzz_test.cc @@ -17,7 +17,7 @@ namespace { void Replay(const Frame& frame, ServerCodecFrameInjector& codec) { // Create the server connection containing the nghttp2 session. TestServerConnectionImpl connection( - codec.server_connection_, codec.server_callbacks_, codec.stats_store_, codec.settings_, + codec.server_connection_, codec.server_callbacks_, codec.stats_store_, codec.options_, Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT); codec.write(WellKnownFrames::clientConnectionPrefaceFrame(), connection); codec.write(WellKnownFrames::defaultSettingsFrame(), connection); diff --git a/test/common/http/http2/response_header_fuzz_test.cc b/test/common/http/http2/response_header_fuzz_test.cc index 60586dc81e3b1..09f0336ec8d3f 100644 --- a/test/common/http/http2/response_header_fuzz_test.cc +++ b/test/common/http/http2/response_header_fuzz_test.cc @@ -18,7 +18,7 @@ namespace { void Replay(const Frame& frame, ClientCodecFrameInjector& codec) { // Create the client connection containing the nghttp2 session. TestClientConnectionImpl connection( - codec.client_connection_, codec.client_callbacks_, codec.stats_store_, codec.settings_, + codec.client_connection_, codec.client_callbacks_, codec.stats_store_, codec.options_, Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT); // Create a new stream. codec.request_encoder_ = &connection.newStream(codec.response_decoder_); diff --git a/test/common/http/utility_test.cc b/test/common/http/utility_test.cc index 8e5f41750da9e..8c5c4d55efb62 100644 --- a/test/common/http/utility_test.cc +++ b/test/common/http/utility_test.cc @@ -254,33 +254,35 @@ TEST(HttpUtility, createSslRedirectPath) { namespace { -Http2Settings parseHttp2SettingsFromV2Yaml(const std::string& yaml) { - envoy::config::core::v3::Http2ProtocolOptions http2_protocol_options; - TestUtility::loadFromYamlAndValidate(yaml, http2_protocol_options); - return Utility::parseHttp2Settings(http2_protocol_options); +envoy::config::core::v3::Http2ProtocolOptions parseHttp2OptionsFromV2Yaml(const std::string& yaml) { + envoy::config::core::v3::Http2ProtocolOptions http2_options; + TestUtility::loadFromYamlAndValidate(yaml, http2_options); + return ::Envoy::Http2::Utility::initializeAndValidateOptions(http2_options); } } // namespace TEST(HttpUtility, parseHttp2Settings) { { - auto http2_settings = parseHttp2SettingsFromV2Yaml("{}"); - EXPECT_EQ(Http2Settings::DEFAULT_HPACK_TABLE_SIZE, http2_settings.hpack_table_size_); - EXPECT_EQ(Http2Settings::DEFAULT_MAX_CONCURRENT_STREAMS, - http2_settings.max_concurrent_streams_); - EXPECT_EQ(Http2Settings::DEFAULT_INITIAL_STREAM_WINDOW_SIZE, - http2_settings.initial_stream_window_size_); - EXPECT_EQ(Http2Settings::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE, - http2_settings.initial_connection_window_size_); - EXPECT_EQ(Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES, http2_settings.max_outbound_frames_); - EXPECT_EQ(Http2Settings::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES, - http2_settings.max_outbound_control_frames_); - EXPECT_EQ(Http2Settings::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD, - http2_settings.max_consecutive_inbound_frames_with_empty_payload_); - EXPECT_EQ(Http2Settings::DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM, - http2_settings.max_inbound_priority_frames_per_stream_); - EXPECT_EQ(Http2Settings::DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT, - http2_settings.max_inbound_window_update_frames_per_data_frame_sent_); + using ::Envoy::Http2::Utility::OptionsLimits; + auto http2_options = parseHttp2OptionsFromV2Yaml("{}"); + EXPECT_EQ(OptionsLimits::DEFAULT_HPACK_TABLE_SIZE, http2_options.hpack_table_size().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_CONCURRENT_STREAMS, + http2_options.max_concurrent_streams().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_INITIAL_STREAM_WINDOW_SIZE, + http2_options.initial_stream_window_size().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE, + http2_options.initial_connection_window_size().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_OUTBOUND_FRAMES, + http2_options.max_outbound_frames().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES, + http2_options.max_outbound_control_frames().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD, + http2_options.max_consecutive_inbound_frames_with_empty_payload().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM, + http2_options.max_inbound_priority_frames_per_stream().value()); + EXPECT_EQ(OptionsLimits::DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT, + http2_options.max_inbound_window_update_frames_per_data_frame_sent().value()); } { @@ -290,11 +292,11 @@ max_concurrent_streams: 2 initial_stream_window_size: 65535 initial_connection_window_size: 65535 )EOF"; - auto http2_settings = parseHttp2SettingsFromV2Yaml(yaml); - EXPECT_EQ(1U, http2_settings.hpack_table_size_); - EXPECT_EQ(2U, http2_settings.max_concurrent_streams_); - EXPECT_EQ(65535U, http2_settings.initial_stream_window_size_); - EXPECT_EQ(65535U, http2_settings.initial_connection_window_size_); + auto http2_options = parseHttp2OptionsFromV2Yaml(yaml); + EXPECT_EQ(1U, http2_options.hpack_table_size().value()); + EXPECT_EQ(2U, http2_options.max_concurrent_streams().value()); + EXPECT_EQ(65535U, http2_options.initial_stream_window_size().value()); + EXPECT_EQ(65535U, http2_options.initial_connection_window_size().value()); } } diff --git a/test/common/upstream/upstream_impl_test.cc b/test/common/upstream/upstream_impl_test.cc index c2f4e4041edad..1f03cfc21bc40 100644 --- a/test/common/upstream/upstream_impl_test.cc +++ b/test/common/upstream/upstream_impl_test.cc @@ -295,7 +295,7 @@ TEST_F(StrictDnsClusterImplTest, Basic) { EXPECT_CALL(runtime_.snapshot_, getInteger("circuit_breakers.name.high.max_retries", 4)); EXPECT_EQ(4U, cluster.info()->resourceManager(ResourcePriority::High).retries().max()); EXPECT_EQ(3U, cluster.info()->maxRequestsPerConnection()); - EXPECT_EQ(0U, cluster.info()->http2Settings().hpack_table_size_); + EXPECT_EQ(0U, cluster.info()->http2Options().hpack_table_size().value()); EXPECT_EQ(Http::Http1Settings::HeaderKeyFormat::ProperCase, cluster.info()->http1Settings().header_key_format_); @@ -622,7 +622,7 @@ TEST_F(StrictDnsClusterImplTest, LoadAssignmentBasic) { EXPECT_CALL(runtime_.snapshot_, getInteger("circuit_breakers.name.high.max_retries", 4)); EXPECT_EQ(4U, cluster.info()->resourceManager(ResourcePriority::High).retries().max()); EXPECT_EQ(3U, cluster.info()->maxRequestsPerConnection()); - EXPECT_EQ(0U, cluster.info()->http2Settings().hpack_table_size_); + EXPECT_EQ(0U, cluster.info()->http2Options().hpack_table_size().value()); cluster.info()->stats().upstream_rq_total_.inc(); EXPECT_EQ(1UL, stats_.counter("cluster.name.upstream_rq_total").value()); @@ -1037,6 +1037,57 @@ TEST_F(StrictDnsClusterImplTest, TtlAsDnsRefreshRate) { TestUtility::makeDnsResponse({}, std::chrono::seconds(5))); } +// Ensures that HTTP/2 user defined SETTINGS parameter validation is enforced on clusters. +TEST_F(StrictDnsClusterImplTest, Http2UserDefinedSettingsParametersValidation) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + type: strict_dns + dns_refresh_rate: 4s + dns_failure_refresh_rate: + base_interval: 7s + max_interval: 10s + lb_policy: round_robin + circuit_breakers: + thresholds: + - priority: DEFAULT + max_connections: 43 + max_pending_requests: 57 + max_requests: 50 + max_retries: 10 + - priority: HIGH + max_connections: 1 + max_pending_requests: 2 + max_requests: 3 + max_retries: 4 + max_requests_per_connection: 3 + protocol_selection: USE_DOWNSTREAM_PROTOCOL + http2_protocol_options: + hpack_table_size: 2048 + custom_settings_parameters: { identifier: 1, value: 1024 } + http_protocol_options: + header_key_format: + proper_case_words: {} + hosts: + - { socket_address: { address: localhost1, port_value: 11001 }} + - { socket_address: { address: localhost2, port_value: 11002 }} + )EOF"; + + envoy::config::cluster::v3::Cluster cluster_config = parseClusterFromV2Yaml(yaml); + Envoy::Stats::ScopePtr scope = stats_.createScope(fmt::format( + "cluster.{}.", cluster_config.alt_stat_name().empty() ? cluster_config.name() + : cluster_config.alt_stat_name())); + Envoy::Server::Configuration::TransportSocketFactoryContextImpl factory_context( + admin_, ssl_context_manager_, *scope, cm_, local_info_, dispatcher_, random_, stats_, + singleton_manager_, tls_, validation_visitor_, *api_); + EXPECT_THROW_WITH_REGEX( + StrictDnsClusterImpl(cluster_config, runtime_, dns_resolver_, factory_context, + std::move(scope), false), + EnvoyException, + R"(the \{hpack_table_size\} HTTP/2 SETTINGS parameter\(s\) can not be configured through)" + " both"); +} + TEST(HostImplTest, HostCluster) { MockClusterMockPrioritySet cluster; HostSharedPtr host = makeTestHost(cluster.info_, "tcp://10.0.0.1:1234", 1); @@ -1305,7 +1356,8 @@ TEST_F(StaticClusterImplTest, LoadAssignmentLocality) { EXPECT_FALSE(cluster.info()->addedViaApi()); } -// Validates that setting an EDS health value through LoadAssignment is honored for static clusters. +// Validates that setting an EDS health value through LoadAssignment is honored for static +// clusters. TEST_F(StaticClusterImplTest, LoadAssignmentEdsHealth) { const std::string yaml = R"EOF( name: staticcluster @@ -1601,8 +1653,8 @@ TEST_F(StaticClusterImplTest, UrlConfig) { EXPECT_EQ(1024U, cluster.info()->resourceManager(ResourcePriority::High).requests().max()); EXPECT_EQ(3U, cluster.info()->resourceManager(ResourcePriority::High).retries().max()); EXPECT_EQ(0U, cluster.info()->maxRequestsPerConnection()); - EXPECT_EQ(Http::Http2Settings::DEFAULT_HPACK_TABLE_SIZE, - cluster.info()->http2Settings().hpack_table_size_); + EXPECT_EQ(::Envoy::Http2::Utility::OptionsLimits::DEFAULT_HPACK_TABLE_SIZE, + cluster.info()->http2Options().hpack_table_size().value()); EXPECT_EQ(LoadBalancerType::Random, cluster.info()->lbType()); EXPECT_THAT( std::list({"10.0.0.1:11001", "10.0.0.2:11002"}), @@ -1869,8 +1921,8 @@ TEST(PrioritySet, Extend) { // Test batch host updates. Verify that we can move a host without triggering intermediate host // updates. - // We're going to do a noop host change, so add a callback to assert that we're not announcing any - // host changes. + // We're going to do a noop host change, so add a callback to assert that we're not announcing + // any host changes. priority_set.addMemberUpdateCb([&](const HostVector& added, const HostVector& removed) -> void { EXPECT_TRUE(added.empty() && removed.empty()); }); @@ -2033,9 +2085,9 @@ TEST_F(ClusterInfoImplTest, ExtensionProtocolOptionsForUnknownFilter) { no_such_filter: { option: value } )EOF"; - EXPECT_THROW_WITH_MESSAGE( - makeCluster(yaml), EnvoyException, - "Didn't find a registered network or http filter implementation for name: 'no_such_filter'"); + EXPECT_THROW_WITH_MESSAGE(makeCluster(yaml), EnvoyException, + "Didn't find a registered network or http filter implementation for " + "name: 'no_such_filter'"); } TEST_F(ClusterInfoImplTest, TypedExtensionProtocolOptionsForUnknownFilter) { @@ -2050,9 +2102,9 @@ TEST_F(ClusterInfoImplTest, TypedExtensionProtocolOptionsForUnknownFilter) { "@type": type.googleapis.com/google.protobuf.Struct )EOF"; - EXPECT_THROW_WITH_MESSAGE( - makeCluster(yaml), EnvoyException, - "Didn't find a registered network or http filter implementation for name: 'no_such_filter'"); + EXPECT_THROW_WITH_MESSAGE(makeCluster(yaml), EnvoyException, + "Didn't find a registered network or http filter implementation for " + "name: 'no_such_filter'"); } TEST_F(ClusterInfoImplTest, OneofExtensionProtocolOptionsForUnknownFilter) { @@ -2186,6 +2238,34 @@ TEST_F(ClusterInfoImplTest, TestTrackTimeoutBudgets) { cluster->info()->timeoutBudgetStats()->upstream_rq_timeout_budget_percent_used_.unit()); } +// Validates HTTP2 SETTINGS config. +TEST_F(ClusterInfoImplTest, Http2ProtocolOptions) { + const std::string yaml = R"EOF( + name: name + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + http2_protocol_options: + hpack_table_size: 2048 + initial_stream_window_size: 65536 + custom_settings_parameters: + - identifier: 0x10 + value: 10 + - identifier: 0x12 + value: 12 + )EOF"; + + auto cluster = makeCluster(yaml); + EXPECT_EQ(cluster->info()->http2Options().hpack_table_size().value(), 2048); + EXPECT_EQ(cluster->info()->http2Options().initial_stream_window_size().value(), 65536); + EXPECT_EQ(cluster->info()->http2Options().custom_settings_parameters()[0].identifier().value(), + 0x10); + EXPECT_EQ(cluster->info()->http2Options().custom_settings_parameters()[0].value().value(), 10); + EXPECT_EQ(cluster->info()->http2Options().custom_settings_parameters()[1].identifier().value(), + 0x12); + EXPECT_EQ(cluster->info()->http2Options().custom_settings_parameters()[1].value().value(), 12); +} + class TestFilterConfigFactoryBase { public: TestFilterConfigFactoryBase( diff --git a/test/config/BUILD b/test/config/BUILD index 9112b43249ac5..5b54ddaf13b1e 100644 --- a/test/config/BUILD +++ b/test/config/BUILD @@ -18,6 +18,7 @@ envoy_cc_test_library( ], deps = [ "//source/common/config:resources_lib", + "//source/common/http:utility_lib", "//source/common/network:address_lib", "//source/common/protobuf", "//source/common/protobuf:utility_lib", diff --git a/test/config/utility.cc b/test/config/utility.cc index 10d0249336511..1c96a40e981c9 100644 --- a/test/config/utility.cc +++ b/test/config/utility.cc @@ -16,6 +16,7 @@ #include "common/common/assert.h" #include "common/config/resources.h" +#include "common/http/utility.h" #include "common/protobuf/utility.h" #include "test/config/integration/certs/client_ecdsacert_hash.h" @@ -582,8 +583,8 @@ void ConfigHelper::setBufferLimits(uint32_t upstream_buffer_limit, loadHttpConnectionManager(hcm_config); if (hcm_config.codec_type() == envoy::extensions::filters::network::http_connection_manager:: v3::HttpConnectionManager::HTTP2) { - const uint32_t size = - std::max(downstream_buffer_limit, Http::Http2Settings::MIN_INITIAL_STREAM_WINDOW_SIZE); + const uint32_t size = std::max(downstream_buffer_limit, + Http2::Utility::OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE); auto* options = hcm_config.mutable_http2_protocol_options(); options->mutable_initial_stream_window_size()->set_value(size); storeHttpConnectionManager(hcm_config); diff --git a/test/extensions/filters/network/http_connection_manager/config_test.cc b/test/extensions/filters/network/http_connection_manager/config_test.cc index f5b78c49ce4fa..9dcef518d2cc2 100644 --- a/test/extensions/filters/network/http_connection_manager/config_test.cc +++ b/test/extensions/filters/network/http_connection_manager/config_test.cc @@ -1138,6 +1138,226 @@ stat_prefix: my_stat_prefix "bad_type: Cannot find field"); } +// Validates that HttpConnectionManagerConfig construction succeeds when there are no collisions +// between named and user defined parameters, and server push is not modified. +TEST_F(HttpConnectionManagerConfigTest, UserDefinedSettingsNoCollision) { + const std::string yaml_string = R"EOF( +codec_type: http2 +stat_prefix: my_stat_prefix +route_config: + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: fake_cluster +http_filters: +- name: envoy.filters.http.router + typed_config: {} +http2_protocol_options: + hpack_table_size: 1024 + custom_settings_parameters: { identifier: 3, value: 2048 } + )EOF"; + // This will throw when Http2ProtocolOptions validation fails. + HttpConnectionManagerConfig(parseHttpConnectionManagerFromV2Yaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_); +} + +// Validates that named and user defined parameter collisions will trigger a config validation +// failure. +TEST_F(HttpConnectionManagerConfigTest, UserDefinedSettingsNamedParameterCollision) { + // Override both hpack_table_size (id = 0x1) and max_concurrent_streams (id = 0x3) with custom + // parameters of the same and different values (respectively). + const std::string yaml_string = R"EOF( +codec_type: http2 +stat_prefix: my_stat_prefix +route_config: + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: fake_cluster +http_filters: +- name: envoy.http_dynamo_filter + typed_config: {} +http2_protocol_options: + hpack_table_size: 2048 + max_concurrent_streams: 4096 + custom_settings_parameters: + - { identifier: 1, value: 2048 } + - { identifier: 3, value: 1024 } + )EOF"; + EXPECT_THROW_WITH_REGEX( + HttpConnectionManagerConfig(parseHttpConnectionManagerFromV2Yaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_), + EnvoyException, + R"(the \{hpack_table_size,max_concurrent_streams\} HTTP/2 SETTINGS parameter\(s\) can not be)" + " configured"); +} + +// Validates that `allow_connect` can only be configured through the named field. All other +// SETTINGS parameters can be set via the named _or_ custom parameters fields, but `allow_connect` +// required an exception due to the use of a primitive type which does not support presence +// checks. +TEST_F(HttpConnectionManagerConfigTest, UserDefinedSettingsAllowConnectOnlyViaNamedField) { + const std::string yaml_string = R"EOF( +codec_type: http2 +stat_prefix: my_stat_prefix +route_config: + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: fake_cluster +http_filters: +- name: envoy.filters.http.router + typed_config: {} +http2_protocol_options: + custom_settings_parameters: + - { identifier: 8, value: 0 } + )EOF"; + EXPECT_THROW_WITH_REGEX( + HttpConnectionManagerConfig(parseHttpConnectionManagerFromV2Yaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_), + EnvoyException, + "the \"allow_connect\" SETTINGS parameter must only be configured through the named field"); + + const std::string yaml_string2 = R"EOF( +codec_type: http2 +stat_prefix: my_stat_prefix +route_config: + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: fake_cluster +http_filters: +- name: envoy.filters.http.router + typed_config: {} +http2_protocol_options: + allow_connect: true + )EOF"; + HttpConnectionManagerConfig(parseHttpConnectionManagerFromV2Yaml(yaml_string2), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_); +} + +// Validates that setting the server push parameter via user defined parameters is disallowed. +TEST_F(HttpConnectionManagerConfigTest, UserDefinedSettingsDisallowServerPush) { + const std::string yaml_string = R"EOF( +codec_type: http2 +stat_prefix: my_stat_prefix +route_config: + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: fake_cluster +http_filters: +- name: envoy.http_dynamo_filter + typed_config: {} +http2_protocol_options: + custom_settings_parameters: { identifier: 2, value: 1 } + )EOF"; + + EXPECT_THROW_WITH_REGEX( + HttpConnectionManagerConfig(parseHttpConnectionManagerFromV2Yaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_), + EnvoyException, + "server push is not supported by Envoy and can not be enabled via a SETTINGS parameter."); + + // Specify both the server push parameter and colliding named and user defined parameters. + const std::string yaml_string2 = R"EOF( +codec_type: http2 +stat_prefix: my_stat_prefix +route_config: + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: fake_cluster +http_filters: +- name: envoy.http_dynamo_filter + typed_config: {} +http2_protocol_options: + hpack_table_size: 2048 + max_concurrent_streams: 4096 + custom_settings_parameters: + - { identifier: 1, value: 2048 } + - { identifier: 2, value: 1 } + - { identifier: 3, value: 1024 } + )EOF"; + + // The server push exception is thrown first. + EXPECT_THROW_WITH_REGEX( + HttpConnectionManagerConfig(parseHttpConnectionManagerFromV2Yaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_), + EnvoyException, + "server push is not supported by Envoy and can not be enabled via a SETTINGS parameter."); +} + +// Validates that inconsistent custom parameters are rejected. +TEST_F(HttpConnectionManagerConfigTest, UserDefinedSettingsRejectInconsistentCustomParameters) { + const std::string yaml_string = R"EOF( +codec_type: http2 +stat_prefix: my_stat_prefix +route_config: + virtual_hosts: + - name: default + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: fake_cluster +http_filters: +- name: envoy.filters.http.router + typed_config: {} +http2_protocol_options: + custom_settings_parameters: + - { identifier: 10, value: 0 } + - { identifier: 10, value: 1 } + - { identifier: 12, value: 10 } + - { identifier: 14, value: 1 } + - { identifier: 12, value: 10 } + )EOF"; + EXPECT_THROW_WITH_REGEX( + HttpConnectionManagerConfig(parseHttpConnectionManagerFromV2Yaml(yaml_string), context_, + date_provider_, route_config_provider_manager_, + scoped_routes_config_provider_manager_, http_tracer_manager_), + EnvoyException, + R"(inconsistent HTTP/2 custom SETTINGS parameter\(s\) detected; identifiers = \{0x0a\})"); +} + // Test that the deprecated extension name still functions. TEST_F(HttpConnectionManagerConfigTest, DEPRECATED_FEATURE_TEST(DeprecatedExtensionFilterName)) { const std::string deprecated_name = "envoy.http_connection_manager"; diff --git a/test/integration/fake_upstream.cc b/test/integration/fake_upstream.cc index 0ff45af079c14..276415ceca84d 100644 --- a/test/integration/fake_upstream.cc +++ b/test/integration/fake_upstream.cc @@ -229,11 +229,13 @@ FakeHttpConnection::FakeHttpConnection(SharedConnectionWrapper& shared_connectio shared_connection_.connection(), store, *this, http1_settings, max_request_headers_kb, max_request_headers_count); } else { - auto settings = Http::Http2Settings(); - settings.allow_connect_ = true; - settings.allow_metadata_ = true; + envoy::config::core::v3::Http2ProtocolOptions http2_options = + ::Envoy::Http2::Utility::initializeAndValidateOptions( + envoy::config::core::v3::Http2ProtocolOptions()); + http2_options.set_allow_connect(true); + http2_options.set_allow_metadata(true); codec_ = std::make_unique( - shared_connection_.connection(), *this, store, settings, max_request_headers_kb, + shared_connection_.connection(), *this, store, http2_options, max_request_headers_kb, max_request_headers_count); ASSERT(type == Type::HTTP2); } diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index 9a16749531a8d..ddde721df89fe 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -206,8 +206,8 @@ IntegrationCodecClientPtr HttpIntegrationTest::makeRawHttpConnection(Network::ClientConnectionPtr&& conn) { std::shared_ptr cluster{new NiceMock()}; cluster->max_response_headers_count_ = 200; - cluster->http2_settings_.allow_connect_ = true; - cluster->http2_settings_.allow_metadata_ = true; + cluster->http2_options_.set_allow_connect(true); + cluster->http2_options_.set_allow_metadata(true); cluster->http1_settings_.enable_trailers_ = true; Upstream::HostDescriptionConstSharedPtr host_description{Upstream::makeTestHostDescription( cluster, fmt::format("tcp://{}:80", Network::Test::getLoopbackAddressUrlString(version_)))}; diff --git a/test/integration/protocol_integration_test.cc b/test/integration/protocol_integration_test.cc index 905e84a94a2af..1e44d58782528 100644 --- a/test/integration/protocol_integration_test.cc +++ b/test/integration/protocol_integration_test.cc @@ -18,6 +18,7 @@ #include "common/common/fmt.h" #include "common/common/thread_annotations.h" #include "common/http/headers.h" +#include "common/http/utility.h" #include "common/network/utility.h" #include "common/protobuf/utility.h" #include "common/runtime/runtime_impl.h" @@ -1209,7 +1210,7 @@ name: passthrough-filter [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) -> void { hcm.mutable_http2_protocol_options()->mutable_initial_stream_window_size()->set_value( - Http::Http2Settings::MIN_INITIAL_STREAM_WINDOW_SIZE); + ::Envoy::Http2::Utility::OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE); }); initialize(); @@ -1347,7 +1348,7 @@ name: encode-headers-return-stop-all-filter [&](envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) -> void { hcm.mutable_http2_protocol_options()->mutable_initial_stream_window_size()->set_value( - Http::Http2Settings::MIN_INITIAL_STREAM_WINDOW_SIZE); + ::Envoy::Http2::Utility::OptionsLimits::MIN_INITIAL_STREAM_WINDOW_SIZE); }); initialize(); diff --git a/test/integration/stats_integration_test.cc b/test/integration/stats_integration_test.cc index 947941fe99e75..ce12a2bde1722 100644 --- a/test/integration/stats_integration_test.cc +++ b/test/integration/stats_integration_test.cc @@ -267,6 +267,7 @@ TEST_P(ClusterMemoryTestRunner, MemoryLargeClusterSizeWithFakeSymbolTable) { // upgrading. // 2020/02/13 10042 43797 44136 Metadata: Metadata are shared across different // clusters and hosts. + // 2020/03/16 9964 44085 44600 http2: support custom SETTINGS parameters. // Note: when adjusting this value: EXPECT_MEMORY_EQ is active only in CI // 'release' builds, where we control the platform and tool-chain. So you @@ -280,8 +281,8 @@ TEST_P(ClusterMemoryTestRunner, MemoryLargeClusterSizeWithFakeSymbolTable) { // If you encounter a failure here, please see // https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#stats-memory-tests // for details on how to fix. - EXPECT_MEMORY_EQ(m_per_cluster, 43797); // 104 bytes higher than a debug build. - EXPECT_MEMORY_LE(m_per_cluster, 44136); + EXPECT_MEMORY_EQ(m_per_cluster, 44085); + EXPECT_MEMORY_LE(m_per_cluster, 44600); } TEST_P(ClusterMemoryTestRunner, MemoryLargeClusterSizeWithRealSymbolTable) { @@ -321,6 +322,7 @@ TEST_P(ClusterMemoryTestRunner, MemoryLargeClusterSizeWithRealSymbolTable) { // 2019/01/09 9227 35772 36500 router: per-cluster histograms w/ timeout budget // 2020/01/12 9633 35932 36500 config: support recovery of original message when // upgrading. + // 2020/03/16 9964 36220 36800 http2: support custom SETTINGS parameters. // Note: when adjusting this value: EXPECT_MEMORY_EQ is active only in CI // 'release' builds, where we control the platform and tool-chain. So you @@ -334,8 +336,8 @@ TEST_P(ClusterMemoryTestRunner, MemoryLargeClusterSizeWithRealSymbolTable) { // If you encounter a failure here, please see // https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#stats-memory-tests // for details on how to fix. - EXPECT_MEMORY_EQ(m_per_cluster, 35932); // 104 bytes higher than a debug build. - EXPECT_MEMORY_LE(m_per_cluster, 36500); + EXPECT_MEMORY_EQ(m_per_cluster, 36220); + EXPECT_MEMORY_LE(m_per_cluster, 36800); } TEST_P(ClusterMemoryTestRunner, MemoryLargeHostSizeWithStats) { diff --git a/test/mocks/upstream/BUILD b/test/mocks/upstream/BUILD index 6d3be18e174b4..7319dbfb9ad54 100644 --- a/test/mocks/upstream/BUILD +++ b/test/mocks/upstream/BUILD @@ -17,6 +17,7 @@ envoy_cc_mock( "//include/envoy/upstream:cluster_manager_interface", "//include/envoy/upstream:upstream_interface", "//source/common/config:metadata_lib", + "//source/common/http:utility_lib", "//source/common/network:raw_buffer_socket_lib", "//source/common/upstream:upstream_includes", "//source/common/upstream:upstream_lib", diff --git a/test/mocks/upstream/cluster_info.cc b/test/mocks/upstream/cluster_info.cc index 1a325c4ac9af4..55b0fffbb2b86 100644 --- a/test/mocks/upstream/cluster_info.cc +++ b/test/mocks/upstream/cluster_info.cc @@ -7,6 +7,7 @@ #include "envoy/upstream/upstream.h" #include "common/config/metadata.h" +#include "common/http/utility.h" #include "common/network/raw_buffer_socket.h" #include "common/upstream/upstream_impl.h" @@ -36,7 +37,9 @@ MockIdleTimeEnabledClusterInfo::MockIdleTimeEnabledClusterInfo() { MockIdleTimeEnabledClusterInfo::~MockIdleTimeEnabledClusterInfo() = default; MockClusterInfo::MockClusterInfo() - : stats_(ClusterInfoImpl::generateStats(stats_store_)), + : http2_options_(::Envoy::Http2::Utility::initializeAndValidateOptions( + envoy::config::core::v3::Http2ProtocolOptions())), + stats_(ClusterInfoImpl::generateStats(stats_store_)), transport_socket_matcher_(new NiceMock()), load_report_stats_(ClusterInfoImpl::generateLoadReportStats(load_report_stats_store_)), timeout_budget_stats_(absl::make_optional( @@ -51,7 +54,7 @@ MockClusterInfo::MockClusterInfo() ON_CALL(*this, name()).WillByDefault(ReturnRef(name_)); ON_CALL(*this, eds_service_name()).WillByDefault(ReturnPointee(&eds_service_name_)); ON_CALL(*this, http1Settings()).WillByDefault(ReturnRef(http1_settings_)); - ON_CALL(*this, http2Settings()).WillByDefault(ReturnRef(http2_settings_)); + ON_CALL(*this, http2Options()).WillByDefault(ReturnRef(http2_options_)); ON_CALL(*this, extensionProtocolOptions(_)).WillByDefault(Return(extension_protocol_options_)); ON_CALL(*this, maxResponseHeadersCount()) .WillByDefault(ReturnPointee(&max_response_headers_count_)); diff --git a/test/mocks/upstream/cluster_info.h b/test/mocks/upstream/cluster_info.h index ca238fc6da9bf..6e2e8c3b113f5 100644 --- a/test/mocks/upstream/cluster_info.h +++ b/test/mocks/upstream/cluster_info.h @@ -89,7 +89,7 @@ class MockClusterInfo : public ClusterInfo { MOCK_METHOD(uint32_t, perConnectionBufferLimitBytes, (), (const)); MOCK_METHOD(uint64_t, features, (), (const)); MOCK_METHOD(const Http::Http1Settings&, http1Settings, (), (const)); - MOCK_METHOD(const Http::Http2Settings&, http2Settings, (), (const)); + MOCK_METHOD(const envoy::config::core::v3::Http2ProtocolOptions&, http2Options, (), (const)); MOCK_METHOD(ProtocolOptionsConfigConstSharedPtr, extensionProtocolOptions, (const std::string&), (const)); MOCK_METHOD(const envoy::config::cluster::v3::Cluster::CommonLbConfig&, lbConfig, (), (const)); @@ -130,7 +130,7 @@ class MockClusterInfo : public ClusterInfo { std::string name_{"fake_cluster"}; absl::optional eds_service_name_; Http::Http1Settings http1_settings_; - Http::Http2Settings http2_settings_; + envoy::config::core::v3::Http2ProtocolOptions http2_options_; ProtocolOptionsConfigConstSharedPtr extension_protocol_options_; uint64_t max_requests_per_connection_{}; uint32_t max_response_headers_count_{Http::DEFAULT_MAX_HEADERS_COUNT}; diff --git a/test/test_common/utility.cc b/test/test_common/utility.cc index 3b31c4e6e535f..6209840741b6a 100644 --- a/test/test_common/utility.cc +++ b/test/test_common/utility.cc @@ -331,22 +331,6 @@ void ConditionalInitializer::wait() { constexpr std::chrono::milliseconds TestUtility::DefaultTimeout; -namespace Http { - -// Satisfy linker -const uint32_t Http2Settings::DEFAULT_HPACK_TABLE_SIZE; -const uint32_t Http2Settings::DEFAULT_MAX_CONCURRENT_STREAMS; -const uint32_t Http2Settings::DEFAULT_INITIAL_STREAM_WINDOW_SIZE; -const uint32_t Http2Settings::DEFAULT_INITIAL_CONNECTION_WINDOW_SIZE; -const uint32_t Http2Settings::MIN_INITIAL_STREAM_WINDOW_SIZE; -const uint32_t Http2Settings::DEFAULT_MAX_OUTBOUND_FRAMES; -const uint32_t Http2Settings::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES; -const uint32_t Http2Settings::DEFAULT_MAX_CONSECUTIVE_INBOUND_FRAMES_WITH_EMPTY_PAYLOAD; -const uint32_t Http2Settings::DEFAULT_MAX_INBOUND_PRIORITY_FRAMES_PER_STREAM; -const uint32_t Http2Settings::DEFAULT_MAX_INBOUND_WINDOW_UPDATE_FRAMES_PER_DATA_FRAME_SENT; - -} // namespace Http - namespace Api { class TestImplProvider {