diff --git a/CODEOWNERS b/CODEOWNERS index 7f684161d7041..8131a28c8c81c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -111,6 +111,7 @@ extensions/filters/common/original_src @snowp @klarose /*/extensions/filters/http/grpc_json_transcoder @qiwzhang @lizan /*/extensions/filters/http/router @alyssawilk @mattklein123 @snowp /*/extensions/filters/http/ext_authz @gsagula @dio +/*/extensions/filters/http/response_map @esmet @alyssawilk /*/extensions/filters/http/grpc_web @fengli79 @lizan /*/extensions/filters/http/grpc_stats @kyessenov @lizan /*/extensions/filters/http/squash @yuval-k @alyssawilk diff --git a/api/BUILD b/api/BUILD index 6b8fc64edab11..6c85d3b904506 100644 --- a/api/BUILD +++ b/api/BUILD @@ -201,6 +201,7 @@ proto_library( "//envoy/extensions/filters/http/original_src/v3:pkg", "//envoy/extensions/filters/http/ratelimit/v3:pkg", "//envoy/extensions/filters/http/rbac/v3:pkg", + "//envoy/extensions/filters/http/response_map/v3:pkg", "//envoy/extensions/filters/http/router/v3:pkg", "//envoy/extensions/filters/http/squash/v3:pkg", "//envoy/extensions/filters/http/tap/v3:pkg", diff --git a/api/envoy/api/v2/core/protocol.proto b/api/envoy/api/v2/core/protocol.proto index ae1a86424cf07..c45e6bcdc8e65 100644 --- a/api/envoy/api/v2/core/protocol.proto +++ b/api/envoy/api/v2/core/protocol.proto @@ -99,6 +99,14 @@ message Http1ProtocolOptions { message ProperCaseWords { } + message Custom { + // Custom header rewrite rules. + // In each rule of the map, the key is a case-insensitive header name. The value + // is the new header value, case-sensitive. This allows for custom header + // capitalization, eg: `x-my-header-key` -> `X-MY-HEADER-Key` + map rules = 1; + } + oneof header_format { option (validate.required) = true; @@ -108,6 +116,9 @@ message Http1ProtocolOptions { // Note that while this results in most headers following conventional casing, certain headers // are not covered. For example, the "TE" header will be formatted as "Te". ProperCaseWords proper_case_words = 1; + + // Formats the header according to custom rules. + Custom custom = 2; } } diff --git a/api/envoy/api/v2/route/route_components.proto b/api/envoy/api/v2/route/route_components.proto index d73fbb8674c90..b1fca201079a5 100644 --- a/api/envoy/api/v2/route/route_components.proto +++ b/api/envoy/api/v2/route/route_components.proto @@ -1149,7 +1149,7 @@ message HedgePolicy { bool hedge_on_per_try_timeout = 3; } -// [#next-free-field: 9] +// [#next-free-field: 10] message RedirectAction { enum RedirectResponseCode { // Moved Permanently HTTP Status Code - 301. @@ -1218,6 +1218,36 @@ message RedirectAction { // :ref:`RouteAction's prefix_rewrite `. string prefix_rewrite = 5 [(validate.rules).string = {well_known_regex: HTTP_HEADER_VALUE strict: false}]; + + // Indicates that during forwarding, portions of the path that match the + // pattern should be rewritten, even allowing the substitution of capture + // groups from the pattern into the new path as specified by the rewrite + // substitution string. This is useful to allow application paths to be + // rewritten in a way that is aware of segments with variable content like + // identifiers. The router filter will place the original path as it was + // before the rewrite into the :ref:`x-envoy-original-path + // ` header. + // + // Only one of :ref:`prefix_rewrite ` + // or *regex_rewrite* may be specified. + // + // Examples using Google's `RE2 `_ engine: + // + // * The path pattern ``^/service/([^/]+)(/.*)$`` paired with a substitution + // string of ``\2/instance/\1`` would transform ``/service/foo/v1/api`` + // into ``/v1/api/instance/foo``. + // + // * The pattern ``one`` paired with a substitution string of ``two`` would + // transform ``/xxx/one/yyy/one/zzz`` into ``/xxx/two/yyy/two/zzz``. + // + // * The pattern ``^(.*?)one(.*)$`` paired with a substitution string of + // ``\1two\2`` would replace only the first occurrence of ``one``, + // transforming path ``/xxx/one/yyy/one/zzz`` into ``/xxx/two/yyy/one/zzz``. + // + // * The pattern ``(?i)/xxx/`` paired with a substitution string of ``/yyy/`` + // would do a case-insensitive match and transform path ``/aaa/XxX/bbb`` to + // ``/aaa/yyy/bbb``. + type.matcher.RegexMatchAndSubstitute regex_rewrite = 9; } // The HTTP status code to use in the redirect response. The default response diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index 80d971c1466ba..1fb19088ef602 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -120,6 +120,17 @@ message Http1ProtocolOptions { "envoy.api.v2.core.Http1ProtocolOptions.HeaderKeyFormat.ProperCaseWords"; } + message Custom { + option (udpa.annotations.versioning).previous_message_type = + "envoy.api.v2.core.Http1ProtocolOptions.HeaderKeyFormat.Custom"; + + // Custom header rewrite rules. + // In each rule of the map, the key is a case-insensitive header name. The value + // is the new header value, case-sensitive. This allows for custom header + // capitalization, eg: `x-my-header-key` -> `X-MY-HEADER-Key` + map rules = 1; + } + oneof header_format { option (validate.required) = true; @@ -129,6 +140,9 @@ message Http1ProtocolOptions { // Note that while this results in most headers following conventional casing, certain headers // are not covered. For example, the "TE" header will be formatted as "Te". ProperCaseWords proper_case_words = 1; + + // Formats the header according to custom rules. + Custom custom = 2; } } diff --git a/api/envoy/config/core/v4alpha/protocol.proto b/api/envoy/config/core/v4alpha/protocol.proto index 60f0b3210805b..24ec9ba5d84d3 100644 --- a/api/envoy/config/core/v4alpha/protocol.proto +++ b/api/envoy/config/core/v4alpha/protocol.proto @@ -120,6 +120,17 @@ message Http1ProtocolOptions { "envoy.config.core.v3.Http1ProtocolOptions.HeaderKeyFormat.ProperCaseWords"; } + message Custom { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.core.v3.Http1ProtocolOptions.HeaderKeyFormat.Custom"; + + // Custom header rewrite rules. + // In each rule of the map, the key is a case-insensitive header name. The value + // is the new header value, case-sensitive. This allows for custom header + // capitalization, eg: `x-my-header-key` -> `X-MY-HEADER-Key` + map rules = 1; + } + oneof header_format { option (validate.required) = true; @@ -129,6 +140,9 @@ message Http1ProtocolOptions { // Note that while this results in most headers following conventional casing, certain headers // are not covered. For example, the "TE" header will be formatted as "Te". ProperCaseWords proper_case_words = 1; + + // Formats the header according to custom rules. + Custom custom = 2; } } diff --git a/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto b/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto index c05032df21a4d..f5ae852595692 100644 --- a/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto +++ b/api/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto @@ -32,7 +32,7 @@ option (udpa.annotations.file_status).package_version_status = FROZEN; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 37] +// [#next-free-field: 40] message HttpConnectionManager { enum CodecType { // For every new connection, the connection manager will determine which @@ -453,6 +453,12 @@ message HttpConnectionManager { // is the current Envoy behaviour. This defaults to false. bool preserve_external_request_id = 32; + // If set, Envoy will always set :ref:`x-request-id ` header in response. + // If this is false or not set, the request ID is returned in responses only if tracing is forced using + // :ref:`x-envoy-force-trace ` header. + // XXX: Only exposed in the v3 API + //bool always_set_request_id_in_response = 37; + // How to handle the :ref:`config_http_conn_man_headers_x-forwarded-client-cert` (XFCC) HTTP // header. ForwardClientCertDetails forward_client_cert_details = 16 @@ -521,6 +527,21 @@ message HttpConnectionManager { // // 3. Tracing decision (sampled, forced, etc) is set in 14th byte of the UUID. RequestIDExtension request_id_extension = 36; + + // The configuration to customize local reply returned by Envoy. It can customize status code, + // body text and response content type. If not specified, status code and text body are hard + // coded in Envoy, the response content type is plain text. + // XXX: Only exposed in the v3 API + //LocalReplyConfig local_reply_config = 38; + + // Determines if the port part should be removed from host/authority header before any processing + // of request by HTTP filters or routing. The port would be removed only if it is equal to the :ref:`listener's` + // local port and request method is not CONNECT. This affects the upstream host header as well. + // Without setting this option, incoming requests with host `example:443` will not match against + // route with :ref:`domains` match set to `example`. Defaults to `false`. Note that port removal is not part + // of `HTTP spec `_ and is provided for convenience. + // XXX: Backported from the v3 API + bool strip_matching_host_port = 39; } message Rds { diff --git a/api/envoy/config/trace/v2/zipkin.proto b/api/envoy/config/trace/v2/zipkin.proto index a825d85bb7f94..2289e55bfe68c 100644 --- a/api/envoy/config/trace/v2/zipkin.proto +++ b/api/envoy/config/trace/v2/zipkin.proto @@ -17,7 +17,7 @@ option (udpa.annotations.file_status).package_version_status = FROZEN; // Configuration for the Zipkin tracer. // [#extension: envoy.tracers.zipkin] -// [#next-free-field: 6] +// [#next-free-field: 7] message ZipkinConfig { // Available Zipkin collector endpoint versions. enum CollectorEndpointVersion { @@ -61,4 +61,8 @@ message ZipkinConfig { // Determines the selected collector endpoint version. By default, the ``HTTP_JSON_V1`` will be // used. CollectorEndpointVersion collector_endpoint_version = 5; + + // Optional hostname to use when sending spans to the collector_cluster. Useful for collectors + // that require a specific hostname. Defaults to `collector_cluster` above. + string collector_hostname = 6; } diff --git a/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto b/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto index 395258802f561..98c026aa9a607 100644 --- a/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto +++ b/api/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto @@ -183,6 +183,9 @@ message BufferSettings { // additional headers metadata may be added to the original client request. See // :ref:`allowed_upstream_headers // ` +// for details. Additionally, the filter may add additional headers to the client's response. See +// :ref:`allowed_client_headers_on_success +// ` // for details. // // On other authorization response statuses, the filter will not allow traffic. Additional headers @@ -253,6 +256,12 @@ message AuthorizationResponse { // (Host)* will be in the response to the client. When a header is included in this list, *Path*, // *Status*, *Content-Length*, *WWWAuthenticate* and *Location* are automatically added. type.matcher.v3.ListStringMatcher allowed_client_headers = 2; + + // When this :ref:`list `. is set, authorization + // response headers that have a correspondent match will be added to the client's response when + // the authorization response itself is successful, i.e. not failed or denied. When this list is + // *not* set, no additional headers will be added to the client's response on success. + type.matcher.v3.ListStringMatcher allowed_client_headers_on_success = 4; } // Extra settings on a per virtualhost/route/weighted-cluster level. diff --git a/api/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto b/api/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto index ec8854f5d1be3..371aee0709443 100644 --- a/api/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto +++ b/api/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto @@ -183,6 +183,9 @@ message BufferSettings { // additional headers metadata may be added to the original client request. See // :ref:`allowed_upstream_headers // ` +// for details. Additionally, the filter may add additional headers to the client's response. See +// :ref:`allowed_client_headers_on_success +// ` // for details. // // On other authorization response statuses, the filter will not allow traffic. Additional headers @@ -253,6 +256,12 @@ message AuthorizationResponse { // (Host)* will be in the response to the client. When a header is included in this list, *Path*, // *Status*, *Content-Length*, *WWWAuthenticate* and *Location* are automatically added. type.matcher.v4alpha.ListStringMatcher allowed_client_headers = 2; + + // When this :ref:`list `. is set, authorization + // response headers that have a correspondent match will be added to the client's response when + // the authorization response itself is successful, i.e. not failed or denied. When this list is + // *not* set, no additional headers will be added to the client's response on success. + type.matcher.v4alpha.ListStringMatcher allowed_client_headers_on_success = 4; } // Extra settings on a per virtualhost/route/weighted-cluster level. diff --git a/api/envoy/extensions/filters/http/response_map/v3/BUILD b/api/envoy/extensions/filters/http/response_map/v3/BUILD new file mode 100644 index 0000000000000..25c7a9c3aed13 --- /dev/null +++ b/api/envoy/extensions/filters/http/response_map/v3/BUILD @@ -0,0 +1,13 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/accesslog/v3:pkg", + "//envoy/config/core/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/http/response_map/v3/response_map.proto b/api/envoy/extensions/filters/http/response_map/v3/response_map.proto new file mode 100644 index 0000000000000..72a92ee84e276 --- /dev/null +++ b/api/envoy/extensions/filters/http/response_map/v3/response_map.proto @@ -0,0 +1,103 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.response_map.v3; + +import "envoy/config/accesslog/v3/accesslog.proto"; +import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/substitution_format_string.proto"; + +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.response_map.v3"; +option java_outer_classname = "ResponseMapProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: ResponseMap] +// Response map filter :ref:`configuration overview `. +// [#extension: envoy.filters.http.response_map] + +// The configuration to filter and change local response. +// [#next-free-field: 6] +message ResponseMapper { + // Filter to determine if this mapper should apply. + config.accesslog.v3.AccessLogFilter filter = 1 [(validate.rules).message = {required: true}]; + + // The new response status code if specified. + google.protobuf.UInt32Value status_code = 2 [(validate.rules).uint32 = {lt: 600 gte: 200}]; + + // The new body text if specified. It will be used in the `%LOCAL_REPLY_BODY%` + // command operator in the `body_format`. + config.core.v3.DataSource body = 3; + + config.core.v3.SubstitutionFormatString body_format_override = 4; + + // HTTP headers to add to a local reply. This allows the response mapper to append, to add + // or to override headers of any local reply before it is sent to a downstream client. + repeated config.core.v3.HeaderValueOption headers_to_add = 5 + [(validate.rules).repeated = {max_items: 1000}]; +} + +// The configuration to customize HTTP responses read by Envoy. +message ResponseMap { + // Configuration of list of mappers which allows to filter and change HTTP response. + // The mappers will be checked by the specified order until one is matched. + repeated ResponseMapper mappers = 1; + + // The configuration to form response body from the :ref:`command operators ` + // and to specify response content type as one of: plain/text or application/json. + // + // Example one: plain/text body_format. + // + // .. code-block:: + // + // text_format: %LOCAL_REPLY_BODY%:%RESPONSE_CODE%:path=$REQ(:path)% + // + // The following response body in `plain/text` format will be generated for a request with + // local reply body of "upstream connection error", response_code=503 and path=/foo. + // + // .. code-block:: + // + // upstream connection error:503:path=/foo + // + // Example two: application/json body_format. + // + // .. code-block:: + // + // json_format: + // status: %RESPONSE_CODE% + // message: %LOCAL_REPLY_BODY% + // path: $REQ(:path)% + // + // The following response body in "application/json" format would be generated for a request with + // local reply body of "upstream connection error", response_code=503 and path=/foo. + // + // .. code-block:: json + // + // { + // "status": 503, + // "message": "upstream connection error", + // "path": "/foo" + // } + // + config.core.v3.SubstitutionFormatString body_format = 2; +} + +// Extra settings on a per virtualhost/route/weighted-cluster level. +message ResponseMapPerRoute { + oneof override { + option (validate.required) = true; + + // Disable the response map filter for this particular vhost or route. + // If disabled is specified in multiple per-filter-configs, the most specific one will be used. + bool disabled = 1 [(validate.rules).bool = {const: true}]; + + // Override the global configuration of the response map filter with this new config. + ResponseMap response_map = 2 [(validate.rules).message = {required: true}]; + } +} diff --git a/api/envoy/extensions/filters/http/response_map/v4alpha/BUILD b/api/envoy/extensions/filters/http/response_map/v4alpha/BUILD new file mode 100644 index 0000000000000..78acd50088226 --- /dev/null +++ b/api/envoy/extensions/filters/http/response_map/v4alpha/BUILD @@ -0,0 +1,14 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/accesslog/v4alpha:pkg", + "//envoy/config/core/v4alpha:pkg", + "//envoy/extensions/filters/http/response_map/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/http/response_map/v4alpha/response_map.proto b/api/envoy/extensions/filters/http/response_map/v4alpha/response_map.proto new file mode 100644 index 0000000000000..df03c367eccd9 --- /dev/null +++ b/api/envoy/extensions/filters/http/response_map/v4alpha/response_map.proto @@ -0,0 +1,112 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.response_map.v4alpha; + +import "envoy/config/accesslog/v4alpha/accesslog.proto"; +import "envoy/config/core/v4alpha/base.proto"; +import "envoy/config/core/v4alpha/substitution_format_string.proto"; + +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.response_map.v4alpha"; +option java_outer_classname = "ResponseMapProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: ResponseMap] +// Response map filter :ref:`configuration overview `. +// [#extension: envoy.filters.http.response_map] + +// The configuration to filter and change local response. +// [#next-free-field: 6] +message ResponseMapper { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.response_map.v3.ResponseMapper"; + + // Filter to determine if this mapper should apply. + config.accesslog.v4alpha.AccessLogFilter filter = 1 [(validate.rules).message = {required: true}]; + + // The new response status code if specified. + google.protobuf.UInt32Value status_code = 2 [(validate.rules).uint32 = {lt: 600 gte: 200}]; + + // The new body text if specified. It will be used in the `%LOCAL_REPLY_BODY%` + // command operator in the `body_format`. + config.core.v4alpha.DataSource body = 3; + + config.core.v4alpha.SubstitutionFormatString body_format_override = 4; + + // HTTP headers to add to a local reply. This allows the response mapper to append, to add + // or to override headers of any local reply before it is sent to a downstream client. + repeated config.core.v4alpha.HeaderValueOption headers_to_add = 5 + [(validate.rules).repeated = {max_items: 1000}]; +} + +// The configuration to customize HTTP responses read by Envoy. +message ResponseMap { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.response_map.v3.ResponseMap"; + + // Configuration of list of mappers which allows to filter and change HTTP response. + // The mappers will be checked by the specified order until one is matched. + repeated ResponseMapper mappers = 1; + + // The configuration to form response body from the :ref:`command operators ` + // and to specify response content type as one of: plain/text or application/json. + // + // Example one: plain/text body_format. + // + // .. code-block:: + // + // text_format: %LOCAL_REPLY_BODY%:%RESPONSE_CODE%:path=$REQ(:path)% + // + // The following response body in `plain/text` format will be generated for a request with + // local reply body of "upstream connection error", response_code=503 and path=/foo. + // + // .. code-block:: + // + // upstream connection error:503:path=/foo + // + // Example two: application/json body_format. + // + // .. code-block:: + // + // json_format: + // status: %RESPONSE_CODE% + // message: %LOCAL_REPLY_BODY% + // path: $REQ(:path)% + // + // The following response body in "application/json" format would be generated for a request with + // local reply body of "upstream connection error", response_code=503 and path=/foo. + // + // .. code-block:: json + // + // { + // "status": 503, + // "message": "upstream connection error", + // "path": "/foo" + // } + // + config.core.v4alpha.SubstitutionFormatString body_format = 2; +} + +// Extra settings on a per virtualhost/route/weighted-cluster level. +message ResponseMapPerRoute { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.response_map.v3.ResponseMapPerRoute"; + + oneof override { + option (validate.required) = true; + + // Disable the response map filter for this particular vhost or route. + // If disabled is specified in multiple per-filter-configs, the most specific one will be used. + bool disabled = 1 [(validate.rules).bool = {const: true}]; + + // Override the global configuration of the response map filter with this new config. + ResponseMap response_map = 2 [(validate.rules).message = {required: true}]; + } +} diff --git a/api/envoy/service/auth/v3/external_auth.proto b/api/envoy/service/auth/v3/external_auth.proto index 9e2bf8fccd5bd..4860be38c4b71 100644 --- a/api/envoy/service/auth/v3/external_auth.proto +++ b/api/envoy/service/auth/v3/external_auth.proto @@ -50,7 +50,7 @@ message DeniedHttpResponse { type.v3.HttpStatus status = 1 [(validate.rules).message = {required: true}]; // This field allows the authorization service to send HTTP response headers - // to the downstream client. Note that the `append` field in `HeaderValueOption` defaults to + // to the downstream client. Note that the :ref:`append field in HeaderValueOption ` defaults to // false when used in this message. repeated config.core.v3.HeaderValueOption headers = 2; @@ -60,14 +60,14 @@ message DeniedHttpResponse { } // HTTP attributes for an OK response. -// [#next-free-field: 6] +// [#next-free-field: 7] message OkHttpResponse { option (udpa.annotations.versioning).previous_message_type = "envoy.service.auth.v2.OkHttpResponse"; // HTTP entity headers in addition to the original request headers. This allows the authorization // service to append, to add or to override headers from the original request before - // dispatching it to the upstream. Note that the `append` field in `HeaderValueOption` defaults to + // dispatching it to the upstream. Note that the :ref:`append field in HeaderValueOption ` defaults to // false when used in this message. By setting the `append` field to `true`, // the filter will append the correspondent header value to the matched request header. // By leaving `append` as false, the filter will either add a new header, or override an existing @@ -96,6 +96,11 @@ message OkHttpResponse { // setting this field overrides :ref:`CheckResponse.dynamic_metadata // `. google.protobuf.Struct dynamic_metadata = 3 [deprecated = true]; + + // This field allows the authorization service to send HTTP response headers + // to the downstream client on success. Note that the :ref:`append field in HeaderValueOption ` + // defaults to false when used in this message. + repeated config.core.v3.HeaderValueOption response_headers_to_add = 6; } // Intended for gRPC and Network Authorization servers `only`. diff --git a/api/envoy/service/auth/v4alpha/external_auth.proto b/api/envoy/service/auth/v4alpha/external_auth.proto index 06ccecec15da0..f368516c302e6 100644 --- a/api/envoy/service/auth/v4alpha/external_auth.proto +++ b/api/envoy/service/auth/v4alpha/external_auth.proto @@ -50,7 +50,7 @@ message DeniedHttpResponse { type.v3.HttpStatus status = 1 [(validate.rules).message = {required: true}]; // This field allows the authorization service to send HTTP response headers - // to the downstream client. Note that the `append` field in `HeaderValueOption` defaults to + // to the downstream client. Note that the :ref:`append field in HeaderValueOption ` defaults to // false when used in this message. repeated config.core.v4alpha.HeaderValueOption headers = 2; @@ -60,7 +60,7 @@ message DeniedHttpResponse { } // HTTP attributes for an OK response. -// [#next-free-field: 6] +// [#next-free-field: 7] message OkHttpResponse { option (udpa.annotations.versioning).previous_message_type = "envoy.service.auth.v3.OkHttpResponse"; @@ -71,7 +71,7 @@ message OkHttpResponse { // HTTP entity headers in addition to the original request headers. This allows the authorization // service to append, to add or to override headers from the original request before - // dispatching it to the upstream. Note that the `append` field in `HeaderValueOption` defaults to + // dispatching it to the upstream. Note that the :ref:`append field in HeaderValueOption ` defaults to // false when used in this message. By setting the `append` field to `true`, // the filter will append the correspondent header value to the matched request header. // By leaving `append` as false, the filter will either add a new header, or override an existing @@ -94,6 +94,11 @@ message OkHttpResponse { // authorization service as a comma separated list like so: // ``x-envoy-auth-headers-to-remove: one-auth-header, another-auth-header``. repeated string headers_to_remove = 5; + + // This field allows the authorization service to send HTTP response headers + // to the downstream client on success. Note that the :ref:`append field in HeaderValueOption ` + // defaults to false when used in this message. + repeated config.core.v4alpha.HeaderValueOption response_headers_to_add = 6; } // Intended for gRPC and Network Authorization servers `only`. diff --git a/api/envoy/service/ratelimit/v2/rls.proto b/api/envoy/service/ratelimit/v2/rls.proto index 6d97718b4b329..8096dab2a21b3 100644 --- a/api/envoy/service/ratelimit/v2/rls.proto +++ b/api/envoy/service/ratelimit/v2/rls.proto @@ -5,6 +5,8 @@ package envoy.service.ratelimit.v2; import "envoy/api/v2/core/base.proto"; import "envoy/api/v2/ratelimit/ratelimit.proto"; +import "google/protobuf/struct.proto"; + import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; import "validate/validate.proto"; @@ -46,6 +48,7 @@ message RateLimitRequest { } // A response from a ShouldRateLimit call. +// [#next-free-field: 7] message RateLimitResponse { enum Code { // The response code is not known. @@ -113,4 +116,15 @@ message RateLimitResponse { // A list of headers to add to the request when forwarded repeated api.v2.core.HeaderValue request_headers_to_add = 4; + + // A response body to send to the downstream client when the response code is not OK. + bytes raw_body = 5; + + // Optional response metadata that will be emitted as dynamic metadata to be consumed by the next + // filter. This metadata lives in a namespace specified by the canonical name of extension filter + // that requires it: + // + // - :ref:`envoy.filters.http.ratelimit ` for HTTP filter. + // - :ref:`envoy.filters.network.ratelimit ` for network filter. + google.protobuf.Struct dynamic_metadata = 6; } diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 6f67627221480..b462c03de3eb6 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -84,6 +84,7 @@ proto_library( "//envoy/extensions/filters/http/original_src/v3:pkg", "//envoy/extensions/filters/http/ratelimit/v3:pkg", "//envoy/extensions/filters/http/rbac/v3:pkg", + "//envoy/extensions/filters/http/response_map/v3:pkg", "//envoy/extensions/filters/http/router/v3:pkg", "//envoy/extensions/filters/http/squash/v3:pkg", "//envoy/extensions/filters/http/tap/v3:pkg", diff --git a/docs/root/configuration/http/http_conn_man/headers.rst b/docs/root/configuration/http/http_conn_man/headers.rst index 143def096e48f..1c9fe4156093a 100644 --- a/docs/root/configuration/http/http_conn_man/headers.rst +++ b/docs/root/configuration/http/http_conn_man/headers.rst @@ -605,12 +605,18 @@ Supported variable names are: TCP The validity start date of the client certificate used to establish the downstream TLS connection. + DOWNSTREAM_PEER_CERT_V_START can be customized with specifiers as specified in + :ref:`access log format rules`. + %DOWNSTREAM_PEER_CERT_V_END% HTTP The validity end date of the client certificate used to establish the downstream TLS connection. TCP The validity end date of the client certificate used to establish the downstream TLS connection. + DOWNSTREAM_PEER_CERT_V_END can be customized with specifiers as specified in + :ref:`access log format rules`. + %HOSTNAME% The system hostname. diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 8cdc862a51f76..467c342ce898d 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -9,6 +9,8 @@ Minor Behavior Changes ---------------------- *Changes that may cause incompatibilities for some users, but should not for most* +* router: extended custom date formatting to DOWNSTREAM_PEER_CERT_V_START and DOWNSTREAM_PEER_CERT_V_END when using :ref:`custom request/response header formats `. + Bug Fixes --------- *Changes expected to improve the state of the world and are unlikely to have negative effects* @@ -20,6 +22,9 @@ Removed Config or Runtime New Features ------------ * http: added the ability to :ref:`unescape slash sequences` in the path. Requests with unescaped slashes can be proxied, rejected or redirected to the new unescaped path. By default this feature is disabled. The default behavior can be overridden through :ref:`http_connection_manager.path_with_escaped_slashes_action` runtime variable. This action can be selectively enabled for a portion of requests by setting the :ref:`http_connection_manager.path_with_escaped_slashes_action_sampling` runtime variable. +* dispatcher: supports a stack of `Envoy::ScopeTrackedObject` instead of a single tracked object. This will allow Envoy to dump more debug information on crash. +* ext_authz: added :ref:`response_headers_to_add ` to support sending response headers to downstream clients on OK authorization checks via gRPC. +* ext_authz: added :ref:`allowed_client_headers_on_success ` to support sending response headers to downstream clients on OK external authorization checks via HTTP. Deprecated ---------- diff --git a/generated_api_shadow/envoy/api/v2/core/protocol.proto b/generated_api_shadow/envoy/api/v2/core/protocol.proto index ae1a86424cf07..c45e6bcdc8e65 100644 --- a/generated_api_shadow/envoy/api/v2/core/protocol.proto +++ b/generated_api_shadow/envoy/api/v2/core/protocol.proto @@ -99,6 +99,14 @@ message Http1ProtocolOptions { message ProperCaseWords { } + message Custom { + // Custom header rewrite rules. + // In each rule of the map, the key is a case-insensitive header name. The value + // is the new header value, case-sensitive. This allows for custom header + // capitalization, eg: `x-my-header-key` -> `X-MY-HEADER-Key` + map rules = 1; + } + oneof header_format { option (validate.required) = true; @@ -108,6 +116,9 @@ message Http1ProtocolOptions { // Note that while this results in most headers following conventional casing, certain headers // are not covered. For example, the "TE" header will be formatted as "Te". ProperCaseWords proper_case_words = 1; + + // Formats the header according to custom rules. + Custom custom = 2; } } diff --git a/generated_api_shadow/envoy/api/v2/route/route_components.proto b/generated_api_shadow/envoy/api/v2/route/route_components.proto index d73fbb8674c90..b1fca201079a5 100644 --- a/generated_api_shadow/envoy/api/v2/route/route_components.proto +++ b/generated_api_shadow/envoy/api/v2/route/route_components.proto @@ -1149,7 +1149,7 @@ message HedgePolicy { bool hedge_on_per_try_timeout = 3; } -// [#next-free-field: 9] +// [#next-free-field: 10] message RedirectAction { enum RedirectResponseCode { // Moved Permanently HTTP Status Code - 301. @@ -1218,6 +1218,36 @@ message RedirectAction { // :ref:`RouteAction's prefix_rewrite `. string prefix_rewrite = 5 [(validate.rules).string = {well_known_regex: HTTP_HEADER_VALUE strict: false}]; + + // Indicates that during forwarding, portions of the path that match the + // pattern should be rewritten, even allowing the substitution of capture + // groups from the pattern into the new path as specified by the rewrite + // substitution string. This is useful to allow application paths to be + // rewritten in a way that is aware of segments with variable content like + // identifiers. The router filter will place the original path as it was + // before the rewrite into the :ref:`x-envoy-original-path + // ` header. + // + // Only one of :ref:`prefix_rewrite ` + // or *regex_rewrite* may be specified. + // + // Examples using Google's `RE2 `_ engine: + // + // * The path pattern ``^/service/([^/]+)(/.*)$`` paired with a substitution + // string of ``\2/instance/\1`` would transform ``/service/foo/v1/api`` + // into ``/v1/api/instance/foo``. + // + // * The pattern ``one`` paired with a substitution string of ``two`` would + // transform ``/xxx/one/yyy/one/zzz`` into ``/xxx/two/yyy/two/zzz``. + // + // * The pattern ``^(.*?)one(.*)$`` paired with a substitution string of + // ``\1two\2`` would replace only the first occurrence of ``one``, + // transforming path ``/xxx/one/yyy/one/zzz`` into ``/xxx/two/yyy/one/zzz``. + // + // * The pattern ``(?i)/xxx/`` paired with a substitution string of ``/yyy/`` + // would do a case-insensitive match and transform path ``/aaa/XxX/bbb`` to + // ``/aaa/yyy/bbb``. + type.matcher.RegexMatchAndSubstitute regex_rewrite = 9; } // The HTTP status code to use in the redirect response. The default response diff --git a/generated_api_shadow/envoy/config/core/v3/protocol.proto b/generated_api_shadow/envoy/config/core/v3/protocol.proto index 80d971c1466ba..1fb19088ef602 100644 --- a/generated_api_shadow/envoy/config/core/v3/protocol.proto +++ b/generated_api_shadow/envoy/config/core/v3/protocol.proto @@ -120,6 +120,17 @@ message Http1ProtocolOptions { "envoy.api.v2.core.Http1ProtocolOptions.HeaderKeyFormat.ProperCaseWords"; } + message Custom { + option (udpa.annotations.versioning).previous_message_type = + "envoy.api.v2.core.Http1ProtocolOptions.HeaderKeyFormat.Custom"; + + // Custom header rewrite rules. + // In each rule of the map, the key is a case-insensitive header name. The value + // is the new header value, case-sensitive. This allows for custom header + // capitalization, eg: `x-my-header-key` -> `X-MY-HEADER-Key` + map rules = 1; + } + oneof header_format { option (validate.required) = true; @@ -129,6 +140,9 @@ message Http1ProtocolOptions { // Note that while this results in most headers following conventional casing, certain headers // are not covered. For example, the "TE" header will be formatted as "Te". ProperCaseWords proper_case_words = 1; + + // Formats the header according to custom rules. + Custom custom = 2; } } diff --git a/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto b/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto index c9fc21d4cfa4a..fa8606a262845 100644 --- a/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto +++ b/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto @@ -120,6 +120,17 @@ message Http1ProtocolOptions { "envoy.config.core.v3.Http1ProtocolOptions.HeaderKeyFormat.ProperCaseWords"; } + message Custom { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.core.v3.Http1ProtocolOptions.HeaderKeyFormat.Custom"; + + // Custom header rewrite rules. + // In each rule of the map, the key is a case-insensitive header name. The value + // is the new header value, case-sensitive. This allows for custom header + // capitalization, eg: `x-my-header-key` -> `X-MY-HEADER-Key` + map rules = 1; + } + oneof header_format { option (validate.required) = true; @@ -129,6 +140,9 @@ message Http1ProtocolOptions { // Note that while this results in most headers following conventional casing, certain headers // are not covered. For example, the "TE" header will be formatted as "Te". ProperCaseWords proper_case_words = 1; + + // Formats the header according to custom rules. + Custom custom = 2; } } diff --git a/generated_api_shadow/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto b/generated_api_shadow/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto index c05032df21a4d..f5ae852595692 100644 --- a/generated_api_shadow/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto +++ b/generated_api_shadow/envoy/config/filter/network/http_connection_manager/v2/http_connection_manager.proto @@ -32,7 +32,7 @@ option (udpa.annotations.file_status).package_version_status = FROZEN; // HTTP connection manager :ref:`configuration overview `. // [#extension: envoy.filters.network.http_connection_manager] -// [#next-free-field: 37] +// [#next-free-field: 40] message HttpConnectionManager { enum CodecType { // For every new connection, the connection manager will determine which @@ -453,6 +453,12 @@ message HttpConnectionManager { // is the current Envoy behaviour. This defaults to false. bool preserve_external_request_id = 32; + // If set, Envoy will always set :ref:`x-request-id ` header in response. + // If this is false or not set, the request ID is returned in responses only if tracing is forced using + // :ref:`x-envoy-force-trace ` header. + // XXX: Only exposed in the v3 API + //bool always_set_request_id_in_response = 37; + // How to handle the :ref:`config_http_conn_man_headers_x-forwarded-client-cert` (XFCC) HTTP // header. ForwardClientCertDetails forward_client_cert_details = 16 @@ -521,6 +527,21 @@ message HttpConnectionManager { // // 3. Tracing decision (sampled, forced, etc) is set in 14th byte of the UUID. RequestIDExtension request_id_extension = 36; + + // The configuration to customize local reply returned by Envoy. It can customize status code, + // body text and response content type. If not specified, status code and text body are hard + // coded in Envoy, the response content type is plain text. + // XXX: Only exposed in the v3 API + //LocalReplyConfig local_reply_config = 38; + + // Determines if the port part should be removed from host/authority header before any processing + // of request by HTTP filters or routing. The port would be removed only if it is equal to the :ref:`listener's` + // local port and request method is not CONNECT. This affects the upstream host header as well. + // Without setting this option, incoming requests with host `example:443` will not match against + // route with :ref:`domains` match set to `example`. Defaults to `false`. Note that port removal is not part + // of `HTTP spec `_ and is provided for convenience. + // XXX: Backported from the v3 API + bool strip_matching_host_port = 39; } message Rds { diff --git a/generated_api_shadow/envoy/config/trace/v2/zipkin.proto b/generated_api_shadow/envoy/config/trace/v2/zipkin.proto index a825d85bb7f94..2289e55bfe68c 100644 --- a/generated_api_shadow/envoy/config/trace/v2/zipkin.proto +++ b/generated_api_shadow/envoy/config/trace/v2/zipkin.proto @@ -17,7 +17,7 @@ option (udpa.annotations.file_status).package_version_status = FROZEN; // Configuration for the Zipkin tracer. // [#extension: envoy.tracers.zipkin] -// [#next-free-field: 6] +// [#next-free-field: 7] message ZipkinConfig { // Available Zipkin collector endpoint versions. enum CollectorEndpointVersion { @@ -61,4 +61,8 @@ message ZipkinConfig { // Determines the selected collector endpoint version. By default, the ``HTTP_JSON_V1`` will be // used. CollectorEndpointVersion collector_endpoint_version = 5; + + // Optional hostname to use when sending spans to the collector_cluster. Useful for collectors + // that require a specific hostname. Defaults to `collector_cluster` above. + string collector_hostname = 6; } diff --git a/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto b/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto index b1ab3989f20ee..81be512556f19 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v3/ext_authz.proto @@ -182,6 +182,9 @@ message BufferSettings { // additional headers metadata may be added to the original client request. See // :ref:`allowed_upstream_headers // ` +// for details. Additionally, the filter may add additional headers to the client's response. See +// :ref:`allowed_client_headers_on_success +// ` // for details. // // On other authorization response statuses, the filter will not allow traffic. Additional headers @@ -252,6 +255,12 @@ message AuthorizationResponse { // (Host)* will be in the response to the client. When a header is included in this list, *Path*, // *Status*, *Content-Length*, *WWWAuthenticate* and *Location* are automatically added. type.matcher.v3.ListStringMatcher allowed_client_headers = 2; + + // When this :ref:`list `. is set, authorization + // response headers that have a correspondent match will be added to the client's response when + // the authorization response itself is successful, i.e. not failed or denied. When this list is + // *not* set, no additional headers will be added to the client's response on success. + type.matcher.v3.ListStringMatcher allowed_client_headers_on_success = 4; } // Extra settings on a per virtualhost/route/weighted-cluster level. diff --git a/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto b/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto index ec8854f5d1be3..371aee0709443 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/ext_authz/v4alpha/ext_authz.proto @@ -183,6 +183,9 @@ message BufferSettings { // additional headers metadata may be added to the original client request. See // :ref:`allowed_upstream_headers // ` +// for details. Additionally, the filter may add additional headers to the client's response. See +// :ref:`allowed_client_headers_on_success +// ` // for details. // // On other authorization response statuses, the filter will not allow traffic. Additional headers @@ -253,6 +256,12 @@ message AuthorizationResponse { // (Host)* will be in the response to the client. When a header is included in this list, *Path*, // *Status*, *Content-Length*, *WWWAuthenticate* and *Location* are automatically added. type.matcher.v4alpha.ListStringMatcher allowed_client_headers = 2; + + // When this :ref:`list `. is set, authorization + // response headers that have a correspondent match will be added to the client's response when + // the authorization response itself is successful, i.e. not failed or denied. When this list is + // *not* set, no additional headers will be added to the client's response on success. + type.matcher.v4alpha.ListStringMatcher allowed_client_headers_on_success = 4; } // Extra settings on a per virtualhost/route/weighted-cluster level. diff --git a/generated_api_shadow/envoy/extensions/filters/http/response_map/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/http/response_map/v3/BUILD new file mode 100644 index 0000000000000..25c7a9c3aed13 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/response_map/v3/BUILD @@ -0,0 +1,13 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/accesslog/v3:pkg", + "//envoy/config/core/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/http/response_map/v3/response_map.proto b/generated_api_shadow/envoy/extensions/filters/http/response_map/v3/response_map.proto new file mode 100644 index 0000000000000..72a92ee84e276 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/response_map/v3/response_map.proto @@ -0,0 +1,103 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.response_map.v3; + +import "envoy/config/accesslog/v3/accesslog.proto"; +import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/substitution_format_string.proto"; + +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.response_map.v3"; +option java_outer_classname = "ResponseMapProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: ResponseMap] +// Response map filter :ref:`configuration overview `. +// [#extension: envoy.filters.http.response_map] + +// The configuration to filter and change local response. +// [#next-free-field: 6] +message ResponseMapper { + // Filter to determine if this mapper should apply. + config.accesslog.v3.AccessLogFilter filter = 1 [(validate.rules).message = {required: true}]; + + // The new response status code if specified. + google.protobuf.UInt32Value status_code = 2 [(validate.rules).uint32 = {lt: 600 gte: 200}]; + + // The new body text if specified. It will be used in the `%LOCAL_REPLY_BODY%` + // command operator in the `body_format`. + config.core.v3.DataSource body = 3; + + config.core.v3.SubstitutionFormatString body_format_override = 4; + + // HTTP headers to add to a local reply. This allows the response mapper to append, to add + // or to override headers of any local reply before it is sent to a downstream client. + repeated config.core.v3.HeaderValueOption headers_to_add = 5 + [(validate.rules).repeated = {max_items: 1000}]; +} + +// The configuration to customize HTTP responses read by Envoy. +message ResponseMap { + // Configuration of list of mappers which allows to filter and change HTTP response. + // The mappers will be checked by the specified order until one is matched. + repeated ResponseMapper mappers = 1; + + // The configuration to form response body from the :ref:`command operators ` + // and to specify response content type as one of: plain/text or application/json. + // + // Example one: plain/text body_format. + // + // .. code-block:: + // + // text_format: %LOCAL_REPLY_BODY%:%RESPONSE_CODE%:path=$REQ(:path)% + // + // The following response body in `plain/text` format will be generated for a request with + // local reply body of "upstream connection error", response_code=503 and path=/foo. + // + // .. code-block:: + // + // upstream connection error:503:path=/foo + // + // Example two: application/json body_format. + // + // .. code-block:: + // + // json_format: + // status: %RESPONSE_CODE% + // message: %LOCAL_REPLY_BODY% + // path: $REQ(:path)% + // + // The following response body in "application/json" format would be generated for a request with + // local reply body of "upstream connection error", response_code=503 and path=/foo. + // + // .. code-block:: json + // + // { + // "status": 503, + // "message": "upstream connection error", + // "path": "/foo" + // } + // + config.core.v3.SubstitutionFormatString body_format = 2; +} + +// Extra settings on a per virtualhost/route/weighted-cluster level. +message ResponseMapPerRoute { + oneof override { + option (validate.required) = true; + + // Disable the response map filter for this particular vhost or route. + // If disabled is specified in multiple per-filter-configs, the most specific one will be used. + bool disabled = 1 [(validate.rules).bool = {const: true}]; + + // Override the global configuration of the response map filter with this new config. + ResponseMap response_map = 2 [(validate.rules).message = {required: true}]; + } +} diff --git a/generated_api_shadow/envoy/extensions/filters/http/response_map/v4alpha/BUILD b/generated_api_shadow/envoy/extensions/filters/http/response_map/v4alpha/BUILD new file mode 100644 index 0000000000000..78acd50088226 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/response_map/v4alpha/BUILD @@ -0,0 +1,14 @@ +# DO NOT EDIT. This file is generated by tools/proto_format/proto_sync.py. + +load("@envoy_api//bazel:api_build_system.bzl", "api_proto_package") + +licenses(["notice"]) # Apache 2 + +api_proto_package( + deps = [ + "//envoy/config/accesslog/v4alpha:pkg", + "//envoy/config/core/v4alpha:pkg", + "//envoy/extensions/filters/http/response_map/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/http/response_map/v4alpha/response_map.proto b/generated_api_shadow/envoy/extensions/filters/http/response_map/v4alpha/response_map.proto new file mode 100644 index 0000000000000..df03c367eccd9 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/http/response_map/v4alpha/response_map.proto @@ -0,0 +1,112 @@ +syntax = "proto3"; + +package envoy.extensions.filters.http.response_map.v4alpha; + +import "envoy/config/accesslog/v4alpha/accesslog.proto"; +import "envoy/config/core/v4alpha/base.proto"; +import "envoy/config/core/v4alpha/substitution_format_string.proto"; + +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.http.response_map.v4alpha"; +option java_outer_classname = "ResponseMapProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: ResponseMap] +// Response map filter :ref:`configuration overview `. +// [#extension: envoy.filters.http.response_map] + +// The configuration to filter and change local response. +// [#next-free-field: 6] +message ResponseMapper { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.response_map.v3.ResponseMapper"; + + // Filter to determine if this mapper should apply. + config.accesslog.v4alpha.AccessLogFilter filter = 1 [(validate.rules).message = {required: true}]; + + // The new response status code if specified. + google.protobuf.UInt32Value status_code = 2 [(validate.rules).uint32 = {lt: 600 gte: 200}]; + + // The new body text if specified. It will be used in the `%LOCAL_REPLY_BODY%` + // command operator in the `body_format`. + config.core.v4alpha.DataSource body = 3; + + config.core.v4alpha.SubstitutionFormatString body_format_override = 4; + + // HTTP headers to add to a local reply. This allows the response mapper to append, to add + // or to override headers of any local reply before it is sent to a downstream client. + repeated config.core.v4alpha.HeaderValueOption headers_to_add = 5 + [(validate.rules).repeated = {max_items: 1000}]; +} + +// The configuration to customize HTTP responses read by Envoy. +message ResponseMap { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.response_map.v3.ResponseMap"; + + // Configuration of list of mappers which allows to filter and change HTTP response. + // The mappers will be checked by the specified order until one is matched. + repeated ResponseMapper mappers = 1; + + // The configuration to form response body from the :ref:`command operators ` + // and to specify response content type as one of: plain/text or application/json. + // + // Example one: plain/text body_format. + // + // .. code-block:: + // + // text_format: %LOCAL_REPLY_BODY%:%RESPONSE_CODE%:path=$REQ(:path)% + // + // The following response body in `plain/text` format will be generated for a request with + // local reply body of "upstream connection error", response_code=503 and path=/foo. + // + // .. code-block:: + // + // upstream connection error:503:path=/foo + // + // Example two: application/json body_format. + // + // .. code-block:: + // + // json_format: + // status: %RESPONSE_CODE% + // message: %LOCAL_REPLY_BODY% + // path: $REQ(:path)% + // + // The following response body in "application/json" format would be generated for a request with + // local reply body of "upstream connection error", response_code=503 and path=/foo. + // + // .. code-block:: json + // + // { + // "status": 503, + // "message": "upstream connection error", + // "path": "/foo" + // } + // + config.core.v4alpha.SubstitutionFormatString body_format = 2; +} + +// Extra settings on a per virtualhost/route/weighted-cluster level. +message ResponseMapPerRoute { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.http.response_map.v3.ResponseMapPerRoute"; + + oneof override { + option (validate.required) = true; + + // Disable the response map filter for this particular vhost or route. + // If disabled is specified in multiple per-filter-configs, the most specific one will be used. + bool disabled = 1 [(validate.rules).bool = {const: true}]; + + // Override the global configuration of the response map filter with this new config. + ResponseMap response_map = 2 [(validate.rules).message = {required: true}]; + } +} diff --git a/generated_api_shadow/envoy/service/auth/v3/external_auth.proto b/generated_api_shadow/envoy/service/auth/v3/external_auth.proto index 9e2bf8fccd5bd..4860be38c4b71 100644 --- a/generated_api_shadow/envoy/service/auth/v3/external_auth.proto +++ b/generated_api_shadow/envoy/service/auth/v3/external_auth.proto @@ -50,7 +50,7 @@ message DeniedHttpResponse { type.v3.HttpStatus status = 1 [(validate.rules).message = {required: true}]; // This field allows the authorization service to send HTTP response headers - // to the downstream client. Note that the `append` field in `HeaderValueOption` defaults to + // to the downstream client. Note that the :ref:`append field in HeaderValueOption ` defaults to // false when used in this message. repeated config.core.v3.HeaderValueOption headers = 2; @@ -60,14 +60,14 @@ message DeniedHttpResponse { } // HTTP attributes for an OK response. -// [#next-free-field: 6] +// [#next-free-field: 7] message OkHttpResponse { option (udpa.annotations.versioning).previous_message_type = "envoy.service.auth.v2.OkHttpResponse"; // HTTP entity headers in addition to the original request headers. This allows the authorization // service to append, to add or to override headers from the original request before - // dispatching it to the upstream. Note that the `append` field in `HeaderValueOption` defaults to + // dispatching it to the upstream. Note that the :ref:`append field in HeaderValueOption ` defaults to // false when used in this message. By setting the `append` field to `true`, // the filter will append the correspondent header value to the matched request header. // By leaving `append` as false, the filter will either add a new header, or override an existing @@ -96,6 +96,11 @@ message OkHttpResponse { // setting this field overrides :ref:`CheckResponse.dynamic_metadata // `. google.protobuf.Struct dynamic_metadata = 3 [deprecated = true]; + + // This field allows the authorization service to send HTTP response headers + // to the downstream client on success. Note that the :ref:`append field in HeaderValueOption ` + // defaults to false when used in this message. + repeated config.core.v3.HeaderValueOption response_headers_to_add = 6; } // Intended for gRPC and Network Authorization servers `only`. diff --git a/generated_api_shadow/envoy/service/auth/v4alpha/external_auth.proto b/generated_api_shadow/envoy/service/auth/v4alpha/external_auth.proto index dbb8dd61b3016..451a54bc677b9 100644 --- a/generated_api_shadow/envoy/service/auth/v4alpha/external_auth.proto +++ b/generated_api_shadow/envoy/service/auth/v4alpha/external_auth.proto @@ -50,7 +50,7 @@ message DeniedHttpResponse { type.v3.HttpStatus status = 1 [(validate.rules).message = {required: true}]; // This field allows the authorization service to send HTTP response headers - // to the downstream client. Note that the `append` field in `HeaderValueOption` defaults to + // to the downstream client. Note that the :ref:`append field in HeaderValueOption ` defaults to // false when used in this message. repeated config.core.v4alpha.HeaderValueOption headers = 2; @@ -60,14 +60,14 @@ message DeniedHttpResponse { } // HTTP attributes for an OK response. -// [#next-free-field: 6] +// [#next-free-field: 7] message OkHttpResponse { option (udpa.annotations.versioning).previous_message_type = "envoy.service.auth.v3.OkHttpResponse"; // HTTP entity headers in addition to the original request headers. This allows the authorization // service to append, to add or to override headers from the original request before - // dispatching it to the upstream. Note that the `append` field in `HeaderValueOption` defaults to + // dispatching it to the upstream. Note that the :ref:`append field in HeaderValueOption ` defaults to // false when used in this message. By setting the `append` field to `true`, // the filter will append the correspondent header value to the matched request header. // By leaving `append` as false, the filter will either add a new header, or override an existing @@ -96,6 +96,11 @@ message OkHttpResponse { // setting this field overrides :ref:`CheckResponse.dynamic_metadata // `. google.protobuf.Struct hidden_envoy_deprecated_dynamic_metadata = 3 [deprecated = true]; + + // This field allows the authorization service to send HTTP response headers + // to the downstream client on success. Note that the :ref:`append field in HeaderValueOption ` + // defaults to false when used in this message. + repeated config.core.v4alpha.HeaderValueOption response_headers_to_add = 6; } // Intended for gRPC and Network Authorization servers `only`. diff --git a/generated_api_shadow/envoy/service/ratelimit/v2/rls.proto b/generated_api_shadow/envoy/service/ratelimit/v2/rls.proto index 6d97718b4b329..8096dab2a21b3 100644 --- a/generated_api_shadow/envoy/service/ratelimit/v2/rls.proto +++ b/generated_api_shadow/envoy/service/ratelimit/v2/rls.proto @@ -5,6 +5,8 @@ package envoy.service.ratelimit.v2; import "envoy/api/v2/core/base.proto"; import "envoy/api/v2/ratelimit/ratelimit.proto"; +import "google/protobuf/struct.proto"; + import "udpa/annotations/migrate.proto"; import "udpa/annotations/status.proto"; import "validate/validate.proto"; @@ -46,6 +48,7 @@ message RateLimitRequest { } // A response from a ShouldRateLimit call. +// [#next-free-field: 7] message RateLimitResponse { enum Code { // The response code is not known. @@ -113,4 +116,15 @@ message RateLimitResponse { // A list of headers to add to the request when forwarded repeated api.v2.core.HeaderValue request_headers_to_add = 4; + + // A response body to send to the downstream client when the response code is not OK. + bytes raw_body = 5; + + // Optional response metadata that will be emitted as dynamic metadata to be consumed by the next + // filter. This metadata lives in a namespace specified by the canonical name of extension filter + // that requires it: + // + // - :ref:`envoy.filters.http.ratelimit ` for HTTP filter. + // - :ref:`envoy.filters.network.ratelimit ` for network filter. + google.protobuf.Struct dynamic_metadata = 6; } diff --git a/include/envoy/http/codec.h b/include/envoy/http/codec.h index 9ffa360bd72ed..8b9a1eb87359f 100644 --- a/include/envoy/http/codec.h +++ b/include/envoy/http/codec.h @@ -396,6 +396,8 @@ struct Http1Settings { // Performs proper casing of header keys: the first and all alpha characters following a // non-alphanumeric character is capitalized. ProperCase, + // Performs custom casing of header keys. Rules are defined as header_key_format_rules_. + Custom, }; // How header keys should be formatted when serializing HTTP/1.1 headers. @@ -405,6 +407,9 @@ struct Http1Settings { // - if true, the HTTP/1.1 connection is left open (where possible) // - if false, the HTTP/1.1 connection is terminated bool stream_error_on_invalid_http_message_{false}; + + // Rules to use when header_key_format_ == HeaderKeyFormat::Custom + std::map header_key_format_rules_{}; }; /** diff --git a/include/envoy/upstream/upstream.h b/include/envoy/upstream/upstream.h index b9de4977659b5..e2dd06270da7e 100644 --- a/include/envoy/upstream/upstream.h +++ b/include/envoy/upstream/upstream.h @@ -555,6 +555,7 @@ class PrioritySet { COUNTER(upstream_cx_http2_total) \ COUNTER(upstream_cx_idle_timeout) \ COUNTER(upstream_cx_max_requests) \ + COUNTER(upstream_cx_max_duration) \ COUNTER(upstream_cx_none_healthy) \ COUNTER(upstream_cx_overflow) \ COUNTER(upstream_cx_pool_overflow) \ @@ -750,6 +751,11 @@ class ClusterInfo { */ virtual float peekaheadRatio() const PURE; + /* + * @return the max duration for upstream connection pool connections. + */ + virtual const absl::optional maxConnectionDuration() const PURE; + /** * @return soft limit on size of the cluster's connections read and write buffers. */ diff --git a/source/common/conn_pool/conn_pool_base.cc b/source/common/conn_pool/conn_pool_base.cc index a5df5aadc0fd8..443ccbe840087 100644 --- a/source/common/conn_pool/conn_pool_base.cc +++ b/source/common/conn_pool/conn_pool_base.cc @@ -490,6 +490,11 @@ ActiveClient::ActiveClient(ConnPoolImplBase& parent, uint32_t lifetime_stream_li conn_length_ = std::make_unique( parent_.host()->cluster().stats().upstream_cx_length_ms_, parent_.dispatcher().timeSource()); connect_timer_->enableTimer(parent_.host()->cluster().connectTimeout()); + const auto max_connection_duration = parent_.host()->cluster().maxConnectionDuration(); + if (max_connection_duration) { + lifetime_timer_ = parent_.dispatcher().createTimer([this]() -> void { onLifetimeTimeout(); }); + lifetime_timer_->enableTimer(max_connection_duration.value()); + } parent_.host()->stats().cx_total_.inc(); parent_.host()->stats().cx_active_.inc(); parent_.host()->cluster().stats().upstream_cx_total_.inc(); @@ -522,5 +527,14 @@ void ActiveClient::onConnectTimeout() { close(); } +void ActiveClient::onLifetimeTimeout() { + if (state_ != ActiveClient::State::CLOSED) { + ENVOY_CONN_LOG(debug, "lifetime timeout, DRAINING", *this); + parent_.host()->cluster().stats().upstream_cx_max_duration_.inc(); + parent_.transitionActiveClientState(*this, + Envoy::ConnectionPool::ActiveClient::State::DRAINING); + } +} + } // namespace ConnectionPool } // namespace Envoy diff --git a/source/common/conn_pool/conn_pool_base.h b/source/common/conn_pool/conn_pool_base.h index c2d8ef7da67ef..ca93d9ab56bcd 100644 --- a/source/common/conn_pool/conn_pool_base.h +++ b/source/common/conn_pool/conn_pool_base.h @@ -55,6 +55,10 @@ class ActiveClient : public LinkedObject, return std::min(remaining_streams_, concurrent_stream_limit_ - numActiveStreams()); } + // Called if the maximum connection duration is reached. If set, this puts an upper + // bound on the lifetime of any connection. + void onLifetimeTimeout(); + // Closes the underlying connection. virtual void close() PURE; // Returns the ID of the underlying connection. @@ -81,6 +85,7 @@ class ActiveClient : public LinkedObject, Stats::TimespanPtr conn_connect_ms_; Stats::TimespanPtr conn_length_; Event::TimerPtr connect_timer_; + Event::TimerPtr lifetime_timer_; bool resources_released_{false}; bool timed_out_{false}; }; diff --git a/source/common/http/BUILD b/source/common/http/BUILD index e19d26c058f47..225e9c63998bc 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -159,6 +159,7 @@ envoy_cc_library( "//include/envoy/router:rds_interface", "//source/common/local_reply:local_reply_lib", "//source/common/network:utility_lib", + "//source/common/response_map:response_map_lib", "//source/common/stats:symbol_table_lib", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", "@envoy_api//envoy/type/v3:pkg_cc_proto", diff --git a/source/common/http/http1/codec_impl.cc b/source/common/http/http1/codec_impl.cc index f0ce0cfb5f888..f284d155a1550 100644 --- a/source/common/http/http1/codec_impl.cc +++ b/source/common/http/http1/codec_impl.cc @@ -61,8 +61,13 @@ const StringUtil::CaseUnorderedSet& caseUnorderdSetContainingUpgradeAndHttp2Sett } HeaderKeyFormatterPtr formatter(const Http::Http1Settings& settings) { - if (settings.header_key_format_ == Http1Settings::HeaderKeyFormat::ProperCase) { + switch (settings.header_key_format_) { + case Http1Settings::HeaderKeyFormat::ProperCase: return std::make_unique(); + case Http1Settings::HeaderKeyFormat::Custom: + return std::make_unique(settings.header_key_format_rules_); + default: + break; } return nullptr; diff --git a/source/common/http/http1/header_formatter.cc b/source/common/http/http1/header_formatter.cc index 514f2383dff02..4878ef202f0bc 100644 --- a/source/common/http/http1/header_formatter.cc +++ b/source/common/http/http1/header_formatter.cc @@ -19,6 +19,21 @@ std::string ProperCaseHeaderKeyFormatter::format(absl::string_view key) const { return copy; } + +std::string CustomHeaderKeyFormatter::format(absl::string_view key) const { + auto copy = std::string(key); + + // Check for a custom header key rewrite + const auto& rewrite = rules_.find(copy); + if (rewrite != rules_.end()) { + // Return a copy of the rewrite. + return rewrite->second; + } + + // Return a copy of the original key + return copy; +} + } // namespace Http1 } // namespace Http } // namespace Envoy diff --git a/source/common/http/http1/header_formatter.h b/source/common/http/http1/header_formatter.h index d99dc79cc741a..8891dbbbeefb0 100644 --- a/source/common/http/http1/header_formatter.h +++ b/source/common/http/http1/header_formatter.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include "envoy/common/pure.h" @@ -30,6 +31,18 @@ class ProperCaseHeaderKeyFormatter : public HeaderKeyFormatter { std::string format(absl::string_view key) const override; }; +/** + * A HeaderKeyFormatter that supports custom rules. + */ +class CustomHeaderKeyFormatter : public HeaderKeyFormatter { +public: + CustomHeaderKeyFormatter(const std::map& rules) : rules_(rules) {} + std::string format(absl::string_view key) const override; + +private: + const std::map& rules_; +}; + } // namespace Http1 } // namespace Http } // namespace Envoy diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index 86660479c683a..e653f3348dc44 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -478,6 +478,17 @@ Utility::parseHttp1Settings(const envoy::config::core::v3::Http1ProtocolOptions& if (config.header_key_format().has_proper_case_words()) { ret.header_key_format_ = Http1Settings::HeaderKeyFormat::ProperCase; + } else if (config.header_key_format().has_custom()) { + ret.header_key_format_ = Http1Settings::HeaderKeyFormat::Custom; + const auto& rules = config.header_key_format().custom().rules(); + // Transform rules, inserting new elements into ret.header_key_format_rules map... + std::transform(rules.cbegin(), rules.cend(), + std::inserter(ret.header_key_format_rules_, ret.header_key_format_rules_.end()), + [](const auto& pair) -> std::pair { + // ...by lower casing the key, and keeping the value as is. + const LowerCaseString lower(pair.first); + return std::pair(lower.get(), pair.second); + }); } else { ret.header_key_format_ = Http1Settings::HeaderKeyFormat::Default; } diff --git a/source/common/response_map/BUILD b/source/common/response_map/BUILD new file mode 100644 index 0000000000000..74e7095d5461e --- /dev/null +++ b/source/common/response_map/BUILD @@ -0,0 +1,31 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_library( + name = "response_map_lib", + srcs = ["response_map.cc"], + hdrs = ["response_map.h"], + deps = [ + "//include/envoy/api:api_interface", + "//include/envoy/http:codes_interface", + "//include/envoy/http:header_map_interface", + "//include/envoy/server:filter_config_interface", + "//include/envoy/stream_info:stream_info_interface", + "//source/common/access_log:access_log_lib", + "//source/common/common:enum_to_int", + "//source/common/config:datasource_lib", + "//source/common/formatter:substitution_format_string_lib", + "//source/common/formatter:substitution_formatter_lib", + "//source/common/http:header_map_lib", + "//source/common/router:header_parser_lib", + "//source/common/stream_info:stream_info_lib", + "@envoy_api//envoy/extensions/filters/http/response_map/v3:pkg_cc_proto", + ], +) diff --git a/source/common/response_map/response_map.cc b/source/common/response_map/response_map.cc new file mode 100644 index 0000000000000..bf094b86cf990 --- /dev/null +++ b/source/common/response_map/response_map.cc @@ -0,0 +1,203 @@ +#include "common/response_map/response_map.h" + +#include +#include + +#include "envoy/api/api.h" + +#include "common/access_log/access_log_impl.h" +#include "common/common/enum_to_int.h" +#include "common/config/datasource.h" +#include "common/formatter/substitution_format_string.h" +#include "common/formatter/substitution_formatter.h" +#include "common/http/header_map_impl.h" +#include "common/http/utility.h" +#include "common/router/header_parser.h" + +namespace Envoy { +namespace ResponseMap { + +class BodyFormatter { +public: + BodyFormatter() + : formatter_(std::make_unique("%LOCAL_REPLY_BODY%")), + content_type_(Http::Headers::get().ContentTypeValues.Text) {} + + BodyFormatter(const envoy::config::core::v3::SubstitutionFormatString& config, Api::Api& api) + : formatter_(Formatter::SubstitutionFormatStringUtils::fromProtoConfig(config, api)), + content_type_( + !config.content_type().empty() + ? config.content_type() + : config.format_case() == + envoy::config::core::v3::SubstitutionFormatString::FormatCase::kJsonFormat + ? Http::Headers::get().ContentTypeValues.Json + : Http::Headers::get().ContentTypeValues.Text) {} + + void format(const Http::RequestHeaderMap& request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap& response_trailers, + const StreamInfo::StreamInfo& stream_info, std::string& body, + absl::string_view& content_type) const { + body = + formatter_->format(request_headers, response_headers, response_trailers, stream_info, body); + content_type = content_type_; + } + +private: + const Formatter::FormatterPtr formatter_; + const std::string content_type_; +}; + +using BodyFormatterPtr = std::unique_ptr; +using HeaderParserPtr = std::unique_ptr; + +class ResponseMapper { +public: + ResponseMapper(const envoy::extensions::filters::http::response_map::v3::ResponseMapper& config, + Server::Configuration::CommonFactoryContext& context, + ProtobufMessage::ValidationVisitor& validationVisitor) + : filter_(AccessLog::FilterFactory::fromProto(config.filter(), context.runtime(), + context.api().randomGenerator(), + validationVisitor)) { + if (config.has_status_code()) { + status_code_ = static_cast(config.status_code().value()); + } + if (config.has_body()) { + body_ = Config::DataSource::read(config.body(), true, context.api()); + } + + if (config.has_body_format_override()) { + body_formatter_ = + std::make_unique(config.body_format_override(), context.api()); + } + + header_parser_ = Envoy::Router::HeaderParser::configure(config.headers_to_add()); + } + + // Decide if a request/response pair matches this mapper. + bool match(const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap& response_headers, + StreamInfo::StreamInfo& stream_info) const { + // Set response code on the stream_info because it's used by the StatusCode filter. + // Further, we know that the status header present on the upstream response headers + // is the status we want to match on. It may not be the status we send downstream + // to the client, though, because of rewrites below. + // + // Under normal circumstances we should have a response status by this point, because + // either the upstream set it or the router filter set it. If for whatever reason we + // don't, skip setting the stream info's response code and just let our evaluation + // logic do without it. We can't do much better, and we certaily don't want to throw + // an exception and crash here. + if (response_headers.Status() != nullptr) { + stream_info.setResponseCode( + static_cast(Http::Utility::getResponseStatus(response_headers))); + } + + if (request_headers == nullptr) { + request_headers = Http::StaticEmptyHeaders::get().request_headers.get(); + } + + return filter_->evaluate(stream_info, *request_headers, response_headers, + *Http::StaticEmptyHeaders::get().response_trailers); + } + + bool rewrite(const Http::RequestHeaderMap&, Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap&, StreamInfo::StreamInfo& stream_info, std::string& body, + BodyFormatter*& final_formatter) const { + if (body_.has_value()) { + body = body_.value(); + } + + header_parser_->evaluateHeaders(response_headers, stream_info); + + if (status_code_.has_value() && + Http::Utility::getResponseStatus(response_headers) != enumToInt(status_code_.value())) { + response_headers.setStatus(std::to_string(enumToInt(status_code_.value()))); + } + + if (body_formatter_) { + final_formatter = body_formatter_.get(); + } + return true; + } + +private: + const AccessLog::FilterPtr filter_; + absl::optional status_code_; + absl::optional body_; + HeaderParserPtr header_parser_; + BodyFormatterPtr body_formatter_; +}; + +using ResponseMapperPtr = std::unique_ptr; + +class ResponseMapImpl : public ResponseMap { +public: + ResponseMapImpl() : body_formatter_(std::make_unique()) {} + + ResponseMapImpl(const envoy::extensions::filters::http::response_map::v3::ResponseMap& config, + Server::Configuration::CommonFactoryContext& context, + ProtobufMessage::ValidationVisitor& validationVisitor) + : body_formatter_(config.has_body_format() + ? std::make_unique(config.body_format(), context.api()) + : std::make_unique()) { + for (const auto& mapper : config.mappers()) { + mappers_.emplace_back(std::make_unique(mapper, context, validationVisitor)); + } + } + + bool match(const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap& response_headers, + StreamInfo::StreamInfo& stream_info) const override { + for (const auto& mapper : mappers_) { + if (mapper->match(request_headers, response_headers, stream_info)) { + return true; + } + } + return false; + } + + void rewrite(const Http::RequestHeaderMap* request_headers, + Http::ResponseHeaderMap& response_headers, StreamInfo::StreamInfo& stream_info, + std::string& body, absl::string_view& content_type) const override { + if (request_headers == nullptr) { + request_headers = Http::StaticEmptyHeaders::get().request_headers.get(); + } + + BodyFormatter* final_formatter{}; + for (const auto& mapper : mappers_) { + if (!mapper->match(request_headers, response_headers, stream_info)) { + continue; + } + + if (mapper->rewrite(*request_headers, response_headers, + *Http::StaticEmptyHeaders::get().response_trailers, stream_info, body, + final_formatter)) { + break; + } + } + + if (!final_formatter) { + final_formatter = body_formatter_.get(); + } + return final_formatter->format(*request_headers, response_headers, + *Http::StaticEmptyHeaders::get().response_trailers, stream_info, + body, content_type); + } + +private: + std::list mappers_; + const BodyFormatterPtr body_formatter_; +}; + +ResponseMapPtr Factory::createDefault() { return std::make_unique(); } + +ResponseMapPtr +Factory::create(const envoy::extensions::filters::http::response_map::v3::ResponseMap& config, + Server::Configuration::CommonFactoryContext& context, + ProtobufMessage::ValidationVisitor& validationVisitor) { + return std::make_unique(config, context, validationVisitor); +} + +} // namespace ResponseMap +} // namespace Envoy diff --git a/source/common/response_map/response_map.h b/source/common/response_map/response_map.h new file mode 100644 index 0000000000000..13e82cf486df0 --- /dev/null +++ b/source/common/response_map/response_map.h @@ -0,0 +1,58 @@ +#pragma once + +#include "envoy/extensions/filters/http/response_map/v3/response_map.pb.h" +#include "envoy/http/codes.h" +#include "envoy/http/header_map.h" +#include "envoy/server/filter_config.h" + +#include "common/stream_info/stream_info_impl.h" + +namespace Envoy { +namespace ResponseMap { + +class ResponseMap { +public: + virtual ~ResponseMap() = default; + + /** + * rewrite the response status code, body and content_type. + * @param request_headers supplies the information about request headers required by filters. + * @param stream_info supplies the information about streams required by filters. + * @param code status code. + * @param body response body. + * @param content_type response content_type. + */ + virtual void rewrite(const Http::RequestHeaderMap* request_headers, + Http::ResponseHeaderMap& response_headers, + StreamInfo::StreamInfo& stream_info, std::string& body, + absl::string_view& content_type) const PURE; + + virtual bool match(const Http::RequestHeaderMap* request_headers, + const Http::ResponseHeaderMap& response_headers, + StreamInfo::StreamInfo& stream_info) const PURE; +}; + +using ResponseMapPtr = std::unique_ptr; + +/** + * Access log filter factory that reads from proto. + */ +class Factory { +public: + /** + * Create a ResponseMap object from ProtoConfig + */ + static ResponseMapPtr + create(const envoy::extensions::filters::http::response_map::v3::ResponseMap& config, + Server::Configuration::CommonFactoryContext& context, + ProtobufMessage::ValidationVisitor& validationVisitor); + + /** + * Create a default ResponseMap object with empty config. + * It is used at places without Server::Configuration::CommonFactoryContext. + */ + static ResponseMapPtr createDefault(); +}; + +} // namespace ResponseMap +} // namespace Envoy diff --git a/source/common/router/header_formatter.cc b/source/common/router/header_formatter.cc index f79c83b0de358..81c3186bfb806 100644 --- a/source/common/router/header_formatter.cc +++ b/source/common/router/header_formatter.cc @@ -43,6 +43,25 @@ std::string formatPerRequestStateParseException(absl::string_view params) { params); } +// Parses a substitution format field and returns a function that formats it. +std::function +parseSubstitutionFormatField(absl::string_view field_name, + StreamInfoHeaderFormatter::FormatterPtrMap& formatter_map) { + const std::string pattern = fmt::format("%{}%", field_name); + if (formatter_map.find(pattern) == formatter_map.end()) { + formatter_map.emplace( + std::make_pair(pattern, Formatter::FormatterPtr(new Formatter::FormatterImpl( + pattern, /*omit_empty_values=*/true)))); + } + return [&formatter_map, pattern](const Envoy::StreamInfo::StreamInfo& stream_info) { + const auto& formatter = formatter_map.at(pattern); + return formatter->format(*Http::StaticEmptyHeaders::get().request_headers, + *Http::StaticEmptyHeaders::get().response_headers, + *Http::StaticEmptyHeaders::get().response_trailers, stream_info, + absl::string_view()); + }; +} + // Parses the parameters for UPSTREAM_METADATA and returns a function suitable for accessing the // specified metadata from an StreamInfo::StreamInfo. Expects a string formatted as: // (["a", "b", "c"]) @@ -210,21 +229,6 @@ StreamInfoHeaderFormatter::FieldExtractor sslConnectionInfoStringHeaderExtractor }; } -// Helper that handles the case when the desired time field is empty. -StreamInfoHeaderFormatter::FieldExtractor sslConnectionInfoStringTimeHeaderExtractor( - std::function(const Ssl::ConnectionInfo& connection_info)> - time_extractor) { - return sslConnectionInfoStringHeaderExtractor( - [time_extractor](const Ssl::ConnectionInfo& connection_info) { - absl::optional time = time_extractor(connection_info); - if (!time.has_value()) { - return std::string(); - } - - return AccessLogDateTimeFormatter::fromTime(time.value()); - }); -} - } // namespace StreamInfoHeaderFormatter::StreamInfoHeaderFormatter(absl::string_view field_name, bool append) @@ -313,16 +317,10 @@ StreamInfoHeaderFormatter::StreamInfoHeaderFormatter(absl::string_view field_nam sslConnectionInfoStringHeaderExtractor([](const Ssl::ConnectionInfo& connection_info) { return connection_info.urlEncodedPemEncodedPeerCertificate(); }); - } else if (field_name == "DOWNSTREAM_PEER_CERT_V_START") { - field_extractor_ = - sslConnectionInfoStringTimeHeaderExtractor([](const Ssl::ConnectionInfo& connection_info) { - return connection_info.validFromPeerCertificate(); - }); - } else if (field_name == "DOWNSTREAM_PEER_CERT_V_END") { - field_extractor_ = - sslConnectionInfoStringTimeHeaderExtractor([](const Ssl::ConnectionInfo& connection_info) { - return connection_info.expirationPeerCertificate(); - }); + } else if (absl::StartsWith(field_name, "DOWNSTREAM_PEER_CERT_V_START")) { + field_extractor_ = parseSubstitutionFormatField(field_name, formatter_map_); + } else if (absl::StartsWith(field_name, "DOWNSTREAM_PEER_CERT_V_END")) { + field_extractor_ = parseSubstitutionFormatField(field_name, formatter_map_); } else if (field_name == "UPSTREAM_REMOTE_ADDRESS") { field_extractor_ = [](const Envoy::StreamInfo::StreamInfo& stream_info) -> std::string { if (stream_info.upstreamHost()) { @@ -331,23 +329,7 @@ StreamInfoHeaderFormatter::StreamInfoHeaderFormatter(absl::string_view field_nam return ""; }; } else if (absl::StartsWith(field_name, "START_TIME")) { - const std::string pattern = fmt::format("%{}%", field_name); - if (start_time_formatters_.find(pattern) == start_time_formatters_.end()) { - start_time_formatters_.emplace( - std::make_pair(pattern, Formatter::SubstitutionFormatParser::parse(pattern))); - } - field_extractor_ = [this, pattern](const Envoy::StreamInfo::StreamInfo& stream_info) { - const auto& formatters = start_time_formatters_.at(pattern); - std::string formatted; - for (const auto& formatter : formatters) { - const auto bit = formatter->format(*Http::StaticEmptyHeaders::get().request_headers, - *Http::StaticEmptyHeaders::get().response_headers, - *Http::StaticEmptyHeaders::get().response_trailers, - stream_info, absl::string_view()); - absl::StrAppend(&formatted, bit.value_or("-")); - } - return formatted; - }; + field_extractor_ = parseSubstitutionFormatField(field_name, formatter_map_); } else if (absl::StartsWith(field_name, "UPSTREAM_METADATA")) { field_extractor_ = parseMetadataField(field_name.substr(STATIC_STRLEN("UPSTREAM_METADATA"))); } else if (absl::StartsWith(field_name, "DYNAMIC_METADATA")) { diff --git a/source/common/router/header_formatter.h b/source/common/router/header_formatter.h index 65a996a5a9ebf..0aace90b36537 100644 --- a/source/common/router/header_formatter.h +++ b/source/common/router/header_formatter.h @@ -42,12 +42,18 @@ class StreamInfoHeaderFormatter : public HeaderFormatter { bool append() const override { return append_; } using FieldExtractor = std::function; + using FormatterPtrMap = absl::node_hash_map; private: FieldExtractor field_extractor_; const bool append_; - absl::node_hash_map> - start_time_formatters_; + + // Maps a string format pattern (including field name and any command operators between + // parenthesis) to the list of FormatterProviderPtrs that are capable of formatting that pattern. + // We use a map here to make sure that we only create a single parser for a given format pattern + // even if it appears multiple times in the larger formatting context (e.g. it shows up multiple + // times in a format string). + FormatterPtrMap formatter_map_; }; /** diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index 4d44e88846fa4..a6671f19458fb 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -820,6 +820,16 @@ ClusterInfoImpl::ClusterInfoImpl( idle_timeout_ = std::chrono::hours(1); } + if (config.common_http_protocol_options().has_max_connection_duration()) { + max_connection_duration_ = std::chrono::milliseconds(DurationUtil::durationToMilliseconds( + config.common_http_protocol_options().max_connection_duration())); + if (max_connection_duration_.value().count() == 0) { + max_connection_duration_ = absl::nullopt; + } + } else { + max_connection_duration_ = absl::nullopt; + } + if (config.has_eds_cluster_config()) { if (config.type() != envoy::config::cluster::v3::Cluster::EDS) { throw EnvoyException("eds_cluster_config set in a non-EDS cluster"); diff --git a/source/common/upstream/upstream_impl.h b/source/common/upstream/upstream_impl.h index 7a24014251612..6073c596e3507 100644 --- a/source/common/upstream/upstream_impl.h +++ b/source/common/upstream/upstream_impl.h @@ -552,6 +552,9 @@ class ClusterInfoImpl : public ClusterInfo, } float perUpstreamPreconnectRatio() const override { return per_upstream_preconnect_ratio_; } float peekaheadRatio() const override { return peekahead_ratio_; } + const absl::optional maxConnectionDuration() const override { + return max_connection_duration_; + } uint32_t perConnectionBufferLimitBytes() const override { return per_connection_buffer_limit_bytes_; } @@ -687,6 +690,7 @@ class ClusterInfoImpl : public ClusterInfo, absl::optional idle_timeout_; const float per_upstream_preconnect_ratio_; const float peekahead_ratio_; + absl::optional max_connection_duration_; const uint32_t per_connection_buffer_limit_bytes_; TransportSocketMatcherPtr socket_matcher_; Stats::ScopePtr stats_scope_; diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 1fa9f6293ea79..ac101008b54f3 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -81,6 +81,7 @@ EXTENSIONS = { "envoy.filters.http.original_src": "//source/extensions/filters/http/original_src:config", "envoy.filters.http.ratelimit": "//source/extensions/filters/http/ratelimit:config", "envoy.filters.http.rbac": "//source/extensions/filters/http/rbac:config", + "envoy.filters.http.response_map": "//source/extensions/filters/http/response_map:config", "envoy.filters.http.router": "//source/extensions/filters/http/router:config", "envoy.filters.http.squash": "//source/extensions/filters/http/squash:config", "envoy.filters.http.tap": "//source/extensions/filters/http/tap:config", diff --git a/source/extensions/filters/common/ext_authz/ext_authz.h b/source/extensions/filters/common/ext_authz/ext_authz.h index 6011752fc3feb..0d89e58031614 100644 --- a/source/extensions/filters/common/ext_authz/ext_authz.h +++ b/source/extensions/filters/common/ext_authz/ext_authz.h @@ -74,6 +74,10 @@ struct Response { // A set of HTTP headers returned by the authorization server, will be optionally added // (using "addCopy") to the request to the upstream server. Http::HeaderVector headers_to_add; + // A set of HTTP headers returned by the authorization server, will be optionally added + // (using "addCopy") to the response sent back to the downstream client on OK auth + // responses. + Http::HeaderVector response_headers_to_add; // A set of HTTP headers consumed by the authorization server, will be removed // from the request to the upstream server. std::vector headers_to_remove; diff --git a/source/extensions/filters/common/ext_authz/ext_authz_grpc_impl.cc b/source/extensions/filters/common/ext_authz/ext_authz_grpc_impl.cc index f065706a4f828..ee6027ca0ad9f 100644 --- a/source/extensions/filters/common/ext_authz/ext_authz_grpc_impl.cc +++ b/source/extensions/filters/common/ext_authz/ext_authz_grpc_impl.cc @@ -61,6 +61,12 @@ void GrpcClientImpl::onSuccess(std::unique_ptrheaders_to_remove.push_back(Http::LowerCaseString(header)); } } + if (response->ok_response().response_headers_to_add_size() > 0) { + for (const auto& header : response->ok_response().response_headers_to_add()) { + authz_response->response_headers_to_add.emplace_back( + Http::LowerCaseString(header.header().key()), header.header().value()); + } + } } } else { span.setTag(TracingConstants::get().TraceStatus, TracingConstants::get().TraceUnauthz); diff --git a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc index 4663563ada68a..11da8bc4f7dd8 100644 --- a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc +++ b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.cc @@ -35,6 +35,7 @@ const Response& errorResponse() { Http::HeaderVector{}, Http::HeaderVector{}, Http::HeaderVector{}, + Http::HeaderVector{}, {{}}, EMPTY_STRING, Http::Code::Forbidden, @@ -44,9 +45,10 @@ const Response& errorResponse() { // SuccessResponse used for creating either DENIED or OK authorization responses. struct SuccessResponse { SuccessResponse(const Http::HeaderMap& headers, const MatcherSharedPtr& matchers, - const MatcherSharedPtr& append_matchers, Response&& response) + const MatcherSharedPtr& append_matchers, + const MatcherSharedPtr& response_matchers, Response&& response) : headers_(headers), matchers_(matchers), append_matchers_(append_matchers), - response_(std::make_unique(response)) { + response_matchers_(response_matchers), response_(std::make_unique(response)) { headers_.iterate([this](const Http::HeaderEntry& header) -> Http::HeaderMap::Iterate { // UpstreamHeaderMatcher if (matchers_->matches(header.key().getStringView())) { @@ -64,6 +66,11 @@ struct SuccessResponse { Http::LowerCaseString{std::string(header.key().getStringView())}, std::string(header.value().getStringView())); } + if (response_matchers_->matches(header.key().getStringView())) { + response_->response_headers_to_add.emplace_back( + Http::LowerCaseString{std::string(header.key().getStringView())}, + std::string(header.value().getStringView())); + } return Http::HeaderMap::Iterate::Continue; }); } @@ -71,6 +78,7 @@ struct SuccessResponse { const Http::HeaderMap& headers_; const MatcherSharedPtr& matchers_; const MatcherSharedPtr& append_matchers_; + const MatcherSharedPtr& response_matchers_; ResponsePtr response_; }; @@ -106,6 +114,8 @@ ClientConfig::ClientConfig(const envoy::extensions::filters::http::ext_authz::v3 toRequestMatchers(config.http_service().authorization_request().allowed_headers())), client_header_matchers_(toClientMatchers( config.http_service().authorization_response().allowed_client_headers())), + client_header_on_success_matchers_(toClientMatchersOnSuccess( + config.http_service().authorization_response().allowed_client_headers_on_success())), upstream_header_matchers_(toUpstreamMatchers( config.http_service().authorization_response().allowed_upstream_headers())), upstream_header_to_append_matchers_(toUpstreamMatchers( @@ -132,6 +142,12 @@ ClientConfig::toRequestMatchers(const envoy::type::matcher::v3::ListStringMatche return std::make_shared(std::move(matchers)); } +MatcherSharedPtr +ClientConfig::toClientMatchersOnSuccess(const envoy::type::matcher::v3::ListStringMatcher& list) { + std::vector matchers(createStringMatchers(list)); + return std::make_shared(std::move(matchers)); +} + MatcherSharedPtr ClientConfig::toClientMatchers(const envoy::type::matcher::v3::ListStringMatcher& list) { std::vector matchers(createStringMatchers(list)); @@ -300,21 +316,24 @@ ResponsePtr RawHttpClientImpl::toResponse(Http::ResponseMessagePtr message) { // Create an Ok authorization response. if (status_code == enumToInt(Http::Code::OK)) { - SuccessResponse ok{message->headers(), config_->upstreamHeaderMatchers(), - config_->upstreamHeaderToAppendMatchers(), - Response{CheckStatus::OK, Http::HeaderVector{}, Http::HeaderVector{}, - Http::HeaderVector{}, std::move(headers_to_remove), EMPTY_STRING, - Http::Code::OK, ProtobufWkt::Struct{}}}; + SuccessResponse ok{ + message->headers(), config_->upstreamHeaderMatchers(), + config_->upstreamHeaderToAppendMatchers(), config_->clientHeaderOnSuccessMatchers(), + Response{CheckStatus::OK, Http::HeaderVector{}, Http::HeaderVector{}, Http::HeaderVector{}, + Http::HeaderVector{}, std::move(headers_to_remove), EMPTY_STRING, Http::Code::OK, + ProtobufWkt::Struct{}}}; return std::move(ok.response_); } // Create a Denied authorization response. SuccessResponse denied{message->headers(), config_->clientHeaderMatchers(), config_->upstreamHeaderToAppendMatchers(), + config_->clientHeaderOnSuccessMatchers(), Response{CheckStatus::Denied, Http::HeaderVector{}, Http::HeaderVector{}, Http::HeaderVector{}, + Http::HeaderVector{}, {{}}, message->bodyAsString(), static_cast(status_code), diff --git a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h index d197a9a342688..f2e6e5726973d 100644 --- a/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h +++ b/source/extensions/filters/common/ext_authz/ext_authz_http_impl.h @@ -92,6 +92,14 @@ class ClientConfig { */ const MatcherSharedPtr& clientHeaderMatchers() const { return client_header_matchers_; } + /** + * Returns a list of matchers used for selecting the authorization response headers that + * should be send back to the client on a successful (i.e. non-denied) response. + */ + const MatcherSharedPtr& clientHeaderOnSuccessMatchers() const { + return client_header_on_success_matchers_; + } + /** * Returns a list of matchers used for selecting the authorization response headers that * should be send to an the upstream server. @@ -122,13 +130,15 @@ class ClientConfig { toRequestMatchers(const envoy::type::matcher::v3::ListStringMatcher& list); static MatcherSharedPtr toClientMatchers(const envoy::type::matcher::v3::ListStringMatcher& list); static MatcherSharedPtr + toClientMatchersOnSuccess(const envoy::type::matcher::v3::ListStringMatcher& list); + static MatcherSharedPtr toUpstreamMatchers(const envoy::type::matcher::v3::ListStringMatcher& list); const MatcherSharedPtr request_header_matchers_; const MatcherSharedPtr client_header_matchers_; + const MatcherSharedPtr client_header_on_success_matchers_; const MatcherSharedPtr upstream_header_matchers_; const MatcherSharedPtr upstream_header_to_append_matchers_; - const Http::LowerCaseStrPairVector authorization_headers_to_add_; const std::string cluster_name_; const std::chrono::milliseconds timeout_; const std::string path_prefix_; diff --git a/source/extensions/filters/http/ext_authz/config.cc b/source/extensions/filters/http/ext_authz/config.cc index 5446cb7a55c22..c99894009d9ab 100644 --- a/source/extensions/filters/http/ext_authz/config.cc +++ b/source/extensions/filters/http/ext_authz/config.cc @@ -39,8 +39,7 @@ Http::FilterFactoryCb ExtAuthzFilterConfig::createFilterFactoryFromProtoTyped( &context](Http::FilterChainFactoryCallbacks& callbacks) { auto client = std::make_unique( context.clusterManager(), client_config); - callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr{ - std::make_shared(filter_config, std::move(client))}); + callbacks.addStreamFilter(std::make_shared(filter_config, std::move(client))); }; } else if (proto_config.grpc_service().has_google_grpc()) { // Google gRPC client. @@ -65,8 +64,7 @@ Http::FilterFactoryCb ExtAuthzFilterConfig::createFilterFactoryFromProtoTyped( auto client = std::make_unique( async_client_cache->getAsyncClient(), std::chrono::milliseconds(timeout_ms), transport_api_version); - callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr{ - std::make_shared(filter_config, std::move(client))}); + callbacks.addStreamFilter(std::make_shared(filter_config, std::move(client))); }; } else { // Envoy gRPC client. @@ -88,8 +86,7 @@ Http::FilterFactoryCb ExtAuthzFilterConfig::createFilterFactoryFromProtoTyped( auto client = std::make_unique( async_client_factory->create(), std::chrono::milliseconds(timeout_ms), transport_api_version); - callbacks.addStreamDecoderFilter(Http::StreamDecoderFilterSharedPtr{ - std::make_shared(filter_config, std::move(client))}); + callbacks.addStreamFilter(std::make_shared(filter_config, std::move(client))); }; } diff --git a/source/extensions/filters/http/ext_authz/ext_authz.cc b/source/extensions/filters/http/ext_authz/ext_authz.cc index 37247272d8796..fe8490f837524 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.cc +++ b/source/extensions/filters/http/ext_authz/ext_authz.cc @@ -53,7 +53,8 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers, // If metadata_context_namespaces is specified, pass matching metadata to the ext_authz service. envoy::config::core::v3::Metadata metadata_context; - const auto& request_metadata = callbacks_->streamInfo().dynamicMetadata().filter_metadata(); + const auto& request_metadata = + decoder_callbacks_->streamInfo().dynamicMetadata().filter_metadata(); for (const auto& context_key : config_->metadataContextNamespaces()) { const auto& metadata_it = request_metadata.find(context_key); if (metadata_it != request_metadata.end()) { @@ -62,36 +63,38 @@ void Filter::initiateCall(const Http::RequestHeaderMap& headers, } Filters::Common::ExtAuthz::CheckRequestUtils::createHttpCheck( - callbacks_, headers, std::move(context_extensions), std::move(metadata_context), + decoder_callbacks_, headers, std::move(context_extensions), std::move(metadata_context), check_request_, config_->maxRequestBytes(), config_->packAsBytes(), config_->includePeerCertificate()); - ENVOY_STREAM_LOG(trace, "ext_authz filter calling authorization server", *callbacks_); + ENVOY_STREAM_LOG(trace, "ext_authz filter calling authorization server", *decoder_callbacks_); state_ = State::Calling; filter_return_ = FilterReturn::StopDecoding; // Don't let the filter chain continue as we are // going to invoke check call. - cluster_ = callbacks_->clusterInfo(); + cluster_ = decoder_callbacks_->clusterInfo(); initiating_call_ = true; - client_->check(*this, check_request_, callbacks_->activeSpan(), callbacks_->streamInfo()); + client_->check(*this, check_request_, decoder_callbacks_->activeSpan(), + decoder_callbacks_->streamInfo()); initiating_call_ = false; } Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool end_stream) { - Router::RouteConstSharedPtr route = callbacks_->route(); + Router::RouteConstSharedPtr route = decoder_callbacks_->route(); const auto per_route_flags = getPerRouteFlags(route); skip_check_ = per_route_flags.skip_check_; if (skip_check_) { return Http::FilterHeadersStatus::Continue; } - if (!config_->filterEnabled(callbacks_->streamInfo().dynamicMetadata())) { + if (!config_->filterEnabled(decoder_callbacks_->streamInfo().dynamicMetadata())) { stats_.disabled_.inc(); if (config_->denyAtDisable()) { - ENVOY_STREAM_LOG(trace, "ext_authz filter is disabled. Deny the request.", *callbacks_); - callbacks_->streamInfo().setResponseFlag( + ENVOY_STREAM_LOG(trace, "ext_authz filter is disabled. Deny the request.", + *decoder_callbacks_); + decoder_callbacks_->streamInfo().setResponseFlag( StreamInfo::ResponseFlag::UnauthorizedExternalService); - callbacks_->sendLocalReply(config_->statusOnError(), EMPTY_STRING, nullptr, absl::nullopt, - RcDetails::get().AuthzError); + decoder_callbacks_->sendLocalReply(config_->statusOnError(), EMPTY_STRING, nullptr, + absl::nullopt, RcDetails::get().AuthzError); return Http::FilterHeadersStatus::StopIteration; } return Http::FilterHeadersStatus::Continue; @@ -103,9 +106,9 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, Http::Utility::isH2UpgradeRequest(headers)); if (buffer_data_) { - ENVOY_STREAM_LOG(debug, "ext_authz filter is buffering the request", *callbacks_); + ENVOY_STREAM_LOG(debug, "ext_authz filter is buffering the request", *decoder_callbacks_); if (!config_->allowPartialMessage()) { - callbacks_->setDecoderBufferLimit(config_->maxRequestBytes()); + decoder_callbacks_->setDecoderBufferLimit(config_->maxRequestBytes()); } return Http::FilterHeadersStatus::StopIteration; } @@ -122,12 +125,12 @@ Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_strea const bool buffer_is_full = isBufferFull(); if (end_stream || buffer_is_full) { ENVOY_STREAM_LOG(debug, "ext_authz filter finished buffering the request since {}", - *callbacks_, buffer_is_full ? "buffer is full" : "stream is ended"); + *decoder_callbacks_, buffer_is_full ? "buffer is full" : "stream is ended"); if (!buffer_is_full) { // Make sure data is available in initiateCall. - callbacks_->addDecodedData(data, true); + decoder_callbacks_->addDecodedData(data, true); } - initiateCall(*request_headers_, callbacks_->route()); + initiateCall(*request_headers_, decoder_callbacks_->route()); return filter_return_ == FilterReturn::StopDecoding ? Http::FilterDataStatus::StopIterationAndWatermark : Http::FilterDataStatus::Continue; @@ -142,8 +145,9 @@ Http::FilterDataStatus Filter::decodeData(Buffer::Instance& data, bool end_strea Http::FilterTrailersStatus Filter::decodeTrailers(Http::RequestTrailerMap&) { if (buffer_data_ && !skip_check_) { if (filter_return_ != FilterReturn::StopDecoding) { - ENVOY_STREAM_LOG(debug, "ext_authz filter finished buffering the request", *callbacks_); - initiateCall(*request_headers_, callbacks_->route()); + ENVOY_STREAM_LOG(debug, "ext_authz filter finished buffering the request", + *decoder_callbacks_); + initiateCall(*request_headers_, decoder_callbacks_->route()); } return filter_return_ == FilterReturn::StopDecoding ? Http::FilterTrailersStatus::StopIteration : Http::FilterTrailersStatus::Continue; @@ -152,8 +156,44 @@ Http::FilterTrailersStatus Filter::decodeTrailers(Http::RequestTrailerMap&) { return Http::FilterTrailersStatus::Continue; } +Http::FilterHeadersStatus Filter::encode100ContinueHeaders(Http::ResponseHeaderMap&) { + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterHeadersStatus Filter::encodeHeaders(Http::ResponseHeaderMap& headers, bool) { + ENVOY_STREAM_LOG(trace, + "ext_authz filter has {} response header(s) to add to the encoded response:", + *encoder_callbacks_, response_headers_to_add_.size()); + if (!response_headers_to_add_.empty()) { + ENVOY_STREAM_LOG( + trace, "ext_authz filter added header(s) to the encoded response:", *encoder_callbacks_); + for (const auto& header : response_headers_to_add_) { + ENVOY_STREAM_LOG(trace, "'{}':'{}'", *encoder_callbacks_, header.first.get(), header.second); + headers.addCopy(header.first, header.second); + } + } + + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterDataStatus Filter::encodeData(Buffer::Instance&, bool) { + return Http::FilterDataStatus::Continue; +} + +Http::FilterTrailersStatus Filter::encodeTrailers(Http::ResponseTrailerMap&) { + return Http::FilterTrailersStatus::Continue; +} + +Http::FilterMetadataStatus Filter::encodeMetadata(Http::MetadataMap&) { + return Http::FilterMetadataStatus::Continue; +} + void Filter::setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) { - callbacks_ = &callbacks; + decoder_callbacks_ = &callbacks; +} + +void Filter::setEncoderFilterCallbacks(Http::StreamEncoderFilterCallbacks& callbacks) { + encoder_callbacks_ = &callbacks; } void Filter::onDestroy() { @@ -176,17 +216,18 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { if (config_->clearRouteCache() && (!response->headers_to_set.empty() || !response->headers_to_append.empty() || !response->headers_to_remove.empty())) { - ENVOY_STREAM_LOG(debug, "ext_authz is clearing route cache", *callbacks_); - callbacks_->clearRouteCache(); + ENVOY_STREAM_LOG(debug, "ext_authz is clearing route cache", *decoder_callbacks_); + decoder_callbacks_->clearRouteCache(); } - ENVOY_STREAM_LOG(trace, "ext_authz filter added header(s) to the request:", *callbacks_); + ENVOY_STREAM_LOG(trace, + "ext_authz filter added header(s) to the request:", *decoder_callbacks_); for (const auto& header : response->headers_to_set) { - ENVOY_STREAM_LOG(trace, "'{}':'{}'", *callbacks_, header.first.get(), header.second); + ENVOY_STREAM_LOG(trace, "'{}':'{}'", *decoder_callbacks_, header.first.get(), header.second); request_headers_->setCopy(header.first, header.second); } for (const auto& header : response->headers_to_add) { - ENVOY_STREAM_LOG(trace, "'{}':'{}'", *callbacks_, header.first.get(), header.second); + ENVOY_STREAM_LOG(trace, "'{}':'{}'", *decoder_callbacks_, header.first.get(), header.second); request_headers_->addCopy(header.first, header.second); } for (const auto& header : response->headers_to_append) { @@ -200,28 +241,40 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { // combined headers: {{"original": "true"}, {"append": "1"}, {"append": "2"}}) to the request // to upstream server by only sets `headers_to_append`. if (!header_to_modify.empty()) { - ENVOY_STREAM_LOG(trace, "'{}':'{}'", *callbacks_, header.first.get(), header.second); + ENVOY_STREAM_LOG(trace, "'{}':'{}'", *decoder_callbacks_, header.first.get(), + header.second); // The current behavior of appending is by combining entries with the same key, into one // entry. The value of that combined entry is separated by ",". // TODO(dio): Consider to use addCopy instead. request_headers_->appendCopy(header.first, header.second); + } else { + // TODO(esmet): We allow adding a header even if the input config is for `append` headers. + // We should upstream this patch. + request_headers_->addCopy(header.first, header.second); } } - ENVOY_STREAM_LOG(trace, "ext_authz filter removed header(s) from the request:", *callbacks_); + ENVOY_STREAM_LOG(trace, + "ext_authz filter removed header(s) from the request:", *decoder_callbacks_); for (const auto& header : response->headers_to_remove) { // We don't allow removing any :-prefixed headers, nor Host, as removing // them would make the request malformed. if (!Http::HeaderUtility::isRemovableHeader(header.get())) { continue; } - ENVOY_STREAM_LOG(trace, "'{}'", *callbacks_, header.get()); + ENVOY_STREAM_LOG(trace, "'{}'", *decoder_callbacks_, header.get()); request_headers_->remove(header); } + if (!response->response_headers_to_add.empty()) { + ENVOY_STREAM_LOG(trace, "ext_authz filter saving {} header(s) to add to the response:", + *decoder_callbacks_, response->response_headers_to_add.size()); + response_headers_to_add_ = std::move(response->response_headers_to_add); + } + if (!response->dynamic_metadata.fields().empty()) { - callbacks_->streamInfo().setDynamicMetadata(HttpFilterNames::get().ExtAuthorization, - response->dynamic_metadata); + decoder_callbacks_->streamInfo().setDynamicMetadata(HttpFilterNames::get().ExtAuthorization, + response->dynamic_metadata); } if (cluster_) { @@ -234,7 +287,7 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { case CheckStatus::Denied: { ENVOY_STREAM_LOG(trace, "ext_authz filter rejected the request. Response status code: '{}", - *callbacks_, enumToInt(response->status_code)); + *decoder_callbacks_, enumToInt(response->status_code)); stats_.denied_.inc(); if (cluster_) { @@ -253,10 +306,10 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { config_->httpContext().codeStats().chargeResponseStat(info); } - callbacks_->sendLocalReply( + decoder_callbacks_->sendLocalReply( response->status_code, response->body, [&headers = response->headers_to_set, - &callbacks = *callbacks_](Http::HeaderMap& response_headers) -> void { + &callbacks = *decoder_callbacks_](Http::HeaderMap& response_headers) -> void { ENVOY_STREAM_LOG(trace, "ext_authz filter added header(s) to the local response:", callbacks); // Firstly, remove all headers requested by the ext_authz filter, to ensure that they will @@ -272,7 +325,8 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { } }, absl::nullopt, RcDetails::get().AuthzDenied); - callbacks_->streamInfo().setResponseFlag(StreamInfo::ResponseFlag::UnauthorizedExternalService); + decoder_callbacks_->streamInfo().setResponseFlag( + StreamInfo::ResponseFlag::UnauthorizedExternalService); break; } @@ -282,7 +336,8 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { } stats_.error_.inc(); if (config_->failureModeAllow()) { - ENVOY_STREAM_LOG(trace, "ext_authz filter allowed the request with error", *callbacks_); + ENVOY_STREAM_LOG(trace, "ext_authz filter allowed the request with error", + *decoder_callbacks_); stats_.failure_mode_allowed_.inc(); if (cluster_) { config_->incCounter(cluster_->statsScope(), config_->ext_authz_failure_mode_allowed_); @@ -291,11 +346,11 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { } else { ENVOY_STREAM_LOG( trace, "ext_authz filter rejected the request with an error. Response status code: {}", - *callbacks_, enumToInt(config_->statusOnError())); - callbacks_->streamInfo().setResponseFlag( + *decoder_callbacks_, enumToInt(config_->statusOnError())); + decoder_callbacks_->streamInfo().setResponseFlag( StreamInfo::ResponseFlag::UnauthorizedExternalService); - callbacks_->sendLocalReply(config_->statusOnError(), EMPTY_STRING, nullptr, absl::nullopt, - RcDetails::get().AuthzError); + decoder_callbacks_->sendLocalReply(config_->statusOnError(), EMPTY_STRING, nullptr, + absl::nullopt, RcDetails::get().AuthzError); } break; } @@ -307,7 +362,7 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { } bool Filter::isBufferFull() const { - const auto* buffer = callbacks_->decodingBuffer(); + const auto* buffer = decoder_callbacks_->decodingBuffer(); if (config_->allowPartialMessage() && buffer != nullptr) { return buffer->length() >= config_->maxRequestBytes(); } @@ -320,7 +375,7 @@ void Filter::continueDecoding() { filter_return_ = FilterReturn::ContinueDecoding; if (!initiating_call_) { - callbacks_->continueDecoding(); + decoder_callbacks_->continueDecoding(); } } diff --git a/source/extensions/filters/http/ext_authz/ext_authz.h b/source/extensions/filters/http/ext_authz/ext_authz.h index db93fa3d2eea5..9b1aca32f97af 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.h +++ b/source/extensions/filters/http/ext_authz/ext_authz.h @@ -229,7 +229,7 @@ class FilterConfigPerRoute : public Router::RouteSpecificFilterConfig { * ext_authz service before allowing further filter iteration. */ class Filter : public Logger::Loggable, - public Http::StreamDecoderFilter, + public Http::StreamFilter, public Filters::Common::ExtAuthz::RequestCallbacks { public: Filter(const FilterConfigSharedPtr& config, Filters::Common::ExtAuthz::ClientPtr&& client) @@ -245,6 +245,15 @@ class Filter : public Logger::Loggable, Http::FilterTrailersStatus decodeTrailers(Http::RequestTrailerMap& trailers) override; void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override; + // Http::StreamEncoderFilter + Http::FilterHeadersStatus encode100ContinueHeaders(Http::ResponseHeaderMap& headers) override; + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) override; + Http::FilterDataStatus encodeData(Buffer::Instance& data, bool end_stream) override; + Http::FilterTrailersStatus encodeTrailers(Http::ResponseTrailerMap& trailers) override; + Http::FilterMetadataStatus encodeMetadata(Http::MetadataMap& trailers) override; + void setEncoderFilterCallbacks(Http::StreamEncoderFilterCallbacks& callbacks) override; + // ExtAuthz::RequestCallbacks void onComplete(Filters::Common::ExtAuthz::ResponsePtr&&) override; @@ -275,8 +284,10 @@ class Filter : public Logger::Loggable, Http::HeaderMapPtr getHeaderMap(const Filters::Common::ExtAuthz::ResponsePtr& response); FilterConfigSharedPtr config_; Filters::Common::ExtAuthz::ClientPtr client_; - Http::StreamDecoderFilterCallbacks* callbacks_{}; + Http::StreamDecoderFilterCallbacks* decoder_callbacks_{}; + Http::StreamEncoderFilterCallbacks* encoder_callbacks_{}; Http::RequestHeaderMap* request_headers_; + Http::HeaderVector response_headers_to_add_{}; State state_{State::NotStarted}; FilterReturn filter_return_{FilterReturn::ContinueDecoding}; Upstream::ClusterInfoConstSharedPtr cluster_; diff --git a/source/extensions/filters/http/response_map/BUILD b/source/extensions/filters/http/response_map/BUILD new file mode 100644 index 0000000000000..0a67bc8d72370 --- /dev/null +++ b/source/extensions/filters/http/response_map/BUILD @@ -0,0 +1,46 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +# L7 HTTP filter which implements response_map processing +# Public docs: docs/root/configuration/http_filters/response_map_filter.rst + +envoy_extension_package() + +envoy_cc_library( + name = "response_map_filter_lib", + srcs = ["response_map_filter.cc"], + hdrs = ["response_map_filter.h"], + deps = [ + "//include/envoy/http:codes_interface", + "//include/envoy/http:filter_interface", + "//source/common/buffer:buffer_lib", + "//source/common/common:assert_lib", + "//source/common/common:enum_to_int", + "//source/common/http:header_map_lib", + "//source/common/http:headers_lib", + "//source/common/response_map:response_map_lib", + "//source/extensions/filters/http:well_known_names", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + security_posture = "robust_to_untrusted_downstream", + deps = [ + "//include/envoy/registry", + "//include/envoy/server:filter_config_interface", + "//source/common/response_map:response_map_lib", + "//source/extensions/filters/http:well_known_names", + "//source/extensions/filters/http/common:factory_base_lib", + "//source/extensions/filters/http/response_map:response_map_filter_lib", + "@envoy_api//envoy/extensions/filters/http/response_map/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/response_map/config.cc b/source/extensions/filters/http/response_map/config.cc new file mode 100644 index 0000000000000..9117499be3146 --- /dev/null +++ b/source/extensions/filters/http/response_map/config.cc @@ -0,0 +1,39 @@ +#include "extensions/filters/http/response_map/config.h" + +#include "envoy/registry/registry.h" + +#include "extensions/filters/http/response_map/response_map_filter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ResponseMapFilter { + +Http::FilterFactoryCb ResponseMapFilterFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::response_map::v3::ResponseMap& proto_config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) { + ResponseMapFilterConfigSharedPtr config = + std::make_shared(proto_config, stats_prefix, context); + return [config](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter(std::make_shared(config)); + }; +} + +Router::RouteSpecificFilterConfigConstSharedPtr +ResponseMapFilterFactory::createRouteSpecificFilterConfigTyped( + const envoy::extensions::filters::http::response_map::v3::ResponseMapPerRoute& proto_config, + Server::Configuration::ServerFactoryContext& context, + ProtobufMessage::ValidationVisitor& validationVisitor) { + return std::make_shared(proto_config, context, validationVisitor); +} + +/** + * Static registration for the response_map filter. @see RegisterFactory. + */ +REGISTER_FACTORY(ResponseMapFilterFactory, + Server::Configuration::NamedHttpFilterConfigFactory){"envoy.response_map"}; + +} // namespace ResponseMapFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/response_map/config.h b/source/extensions/filters/http/response_map/config.h new file mode 100644 index 0000000000000..7c38beba643c7 --- /dev/null +++ b/source/extensions/filters/http/response_map/config.h @@ -0,0 +1,39 @@ +#pragma once + +#include "envoy/extensions/filters/http/response_map/v3/response_map.pb.h" +#include "envoy/extensions/filters/http/response_map/v3/response_map.pb.validate.h" +#include "envoy/server/filter_config.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ResponseMapFilter { + +/** + * Config registration for the response_map filter. @see NamedHttpFilterConfigFactory. + */ +class ResponseMapFilterFactory + : public Common::FactoryBase< + envoy::extensions::filters::http::response_map::v3::ResponseMap, + envoy::extensions::filters::http::response_map::v3::ResponseMapPerRoute> { +public: + ResponseMapFilterFactory() : FactoryBase(HttpFilterNames::get().ResponseMap) {} + + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::http::response_map::v3::ResponseMap& proto_config, + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; + +private: + Router::RouteSpecificFilterConfigConstSharedPtr createRouteSpecificFilterConfigTyped( + const envoy::extensions::filters::http::response_map::v3::ResponseMapPerRoute& proto_config, + Server::Configuration::ServerFactoryContext& context, + ProtobufMessage::ValidationVisitor& validator) override; +}; + +} // namespace ResponseMapFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/response_map/response_map_filter.cc b/source/extensions/filters/http/response_map/response_map_filter.cc new file mode 100644 index 0000000000000..b0f092f0405fb --- /dev/null +++ b/source/extensions/filters/http/response_map/response_map_filter.cc @@ -0,0 +1,252 @@ +#include "extensions/filters/http/response_map/response_map_filter.h" + +#include "envoy/http/codes.h" +#include "envoy/http/header_map.h" + +#include "common/common/empty_string.h" +#include "common/common/enum_to_int.h" +#include "common/common/logger.h" +#include "common/http/header_map_impl.h" +#include "common/http/headers.h" +#include "common/http/utility.h" + +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ResponseMapFilter { + +ResponseMapFilterConfig::ResponseMapFilterConfig( + const envoy::extensions::filters::http::response_map::v3::ResponseMap& proto_config, + const std::string&, Server::Configuration::FactoryContext& context) + : response_map_(ResponseMap::Factory::create(proto_config, context, + context.messageValidationVisitor())) {} + +FilterConfigPerRoute::FilterConfigPerRoute( + const envoy::extensions::filters::http::response_map::v3::ResponseMapPerRoute& proto_config, + Server::Configuration::ServerFactoryContext& context, + ProtobufMessage::ValidationVisitor& validationVisitor) + : disabled_(proto_config.disabled()), + response_map_(proto_config.has_response_map() + ? ResponseMap::Factory::create(proto_config.response_map(), context, + validationVisitor) + : nullptr) {} + +ResponseMapFilter::ResponseMapFilter(ResponseMapFilterConfigSharedPtr config) : config_(config) {} + +/* + * Get FilterConfigPerRoute if one exists for the current route. + */ +const FilterConfigPerRoute* ResponseMapFilter::getRouteSpecificConfig(void) { + const auto& route = encoder_callbacks_->route(); + if (route == nullptr) { + return nullptr; + } + + const auto* per_route_config = + Http::Utility::resolveMostSpecificPerFilterConfig( + Extensions::HttpFilters::HttpFilterNames::get().ResponseMap, route); + ENVOY_LOG(trace, "response map filter: found route. has per_route_config? {}", + per_route_config != nullptr); + + return per_route_config; +} + +/* + * Called if there are any downstream headers to decode from the client or a previous filter. + * + * Not guaranteed to be called. In particular, if Envoy sends a local reply with HTTP 426 + * as an upgrade response for HTTP/1.0 requests, we may never decode any of the client's + * original headers, because the request was rejected at the initial HTTP/1.0 line. + */ +Http::FilterHeadersStatus ResponseMapFilter::decodeHeaders(Http::RequestHeaderMap& request_headers, + bool end_stream) { + ENVOY_LOG(trace, "response map filter: decodeHeaders with end_stream = {}", end_stream); + + // Save a pointer to the request headers. We need to pass them to the response_map_ + // matcher in encodeHeaders later. + request_headers_ = &request_headers; + return Http::FilterHeadersStatus::Continue; +} + +/** + * Called if there are any response headers to encode from the upstream or a later filter. + * + * Not guaranteed to be called, since it is theoretically possible for something to go wrong + * in the HTTP codec on the request path that means it isn't necessary or desirable to provide + * an HTTP response (which would otherwise contain headers). + */ +Http::FilterHeadersStatus ResponseMapFilter::encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) { + ENVOY_LOG(trace, "response map filter: encodeHeaders with http status = {}, end_stream = {}", + headers.getStatusValue(), end_stream); + + // Save a pointer to the response headers. We need to pass them to the response_map_ + // rewriter later. + response_headers_ = &headers; + + // Default to using the response map from the global filter config. If there's + // per-route config, use its response map, if set. + response_map_ = config_->response_map(); + + // Find per-route config if it exists... + const FilterConfigPerRoute* per_route_config = getRouteSpecificConfig(); + if (per_route_config != nullptr) { + // ...and disable the filter, if set... + if (per_route_config->disabled()) { + ENVOY_LOG(trace, "response map filter: disabling due to per_route_config"); + disabled_ = true; + return Http::FilterHeadersStatus::Continue; + } + + // ...or use a per-route response map, if set. + const ResponseMap::ResponseMapPtr* response_map = per_route_config->response_map(); + if (response_map != nullptr) { + ENVOY_LOG(trace, "response map filter: using per_route_config response map"); + response_map_ = response_map; + } + } + + // This should be impossible, because we only set response_map_ to the + // global filter config (guaranteed to exist if this filter exists) + // or to a non-null per-route config. Guard against it anyway. + if (response_map_ == nullptr) { + ENVOY_LOG(trace, + "response map filter: no response_map_ to match with, do_rewrite_ remains false"); + return Http::FilterHeadersStatus::Continue; + } + + // Use the response_map_ to match on the request/response pair... + const ResponseMap::ResponseMapPtr& response_map = *response_map_; + do_rewrite_ = response_map->match(request_headers_, headers, encoder_callbacks_->streamInfo()); + ENVOY_LOG(trace, "response map filter: used response_map_, do_rewrite_ = {}", do_rewrite_); + + // ...and if we decided not to do a rewrite, simply pass through to other filters. + if (!do_rewrite_) { + return Http::FilterHeadersStatus::Continue; + } + + // We know we're going to rewrite the response at this point. If the stream + // is not yet finished, we need to prevent other filters from iterating until + // we've had a chance to actually rewrite the body later in encodeData. + if (!end_stream) { + return Http::FilterHeadersStatus::StopIteration; + } + + // Now that the stream is complete, rewrite the response body. + ENVOY_LOG(trace, "response map filter: encodeHeaders doing rewrite"); + doRewrite(); + return Http::FilterHeadersStatus::Continue; +} + +Http::FilterDataStatus ResponseMapFilter::encodeData(Buffer::Instance& data, bool end_stream) { + // If this filter is disabled, continue without doing anything. + if (disabled_) { + return Http::FilterDataStatus::Continue; + } + + // If we decided not to rewrite the response, simply pass through to other + // filters. + if (!do_rewrite_) { + return Http::FilterDataStatus::Continue; + } + + // We decided to rewrite the response, so drain any data received from the + // upstream since we're rewriting it anyway. This both prevents unnecessary + // buffering and also prevents generating errors if the response is too big + // to be buffered completely. + data.drain(data.length()); + + // The stream is not yet complete, and we can't let other filters run + // until we've rewritten the response body. + if (!end_stream) { + return Http::FilterDataStatus::StopIterationAndBuffer; + } + + // Now that the stream is complete, rewrite the response body. + ENVOY_LOG(trace, "response map filter: encodeData doing rewrite"); + doRewrite(); + return Http::FilterDataStatus::Continue; +} + +void ResponseMapFilter::doRewrite(void) { + const Buffer::Instance* encoding_buffer = encoder_callbacks_->encodingBuffer(); + + ENVOY_LOG(trace, "response map filter: doRewrite with {} encoding_buffer", + encoding_buffer != nullptr ? "non-null" : "null"); + + // If this route is disabled, we should never be doing a rewrite. + // In fact, we never should have even checked if we should do + // a rewrite. + ASSERT(!disabled_); + + // We should either see no encoding buffer or an empty encoding buffer. + // + // We'll see no encoding buffer if the upstream response was never observed + // (ie: due to error) or if the upstream response was headers-only. Otherwise, + // if we did see a response, we should have drained any data we saw. + ASSERT(encoding_buffer == nullptr || encoding_buffer->length() == 0); + + // This should be impossible, because we only set response_map_ to the + // global filter config (guaranteed to exist if this filter exists) + // or to a non-null per-route config. Guard against it anyway. + if (response_map_ == nullptr) { + ENVOY_LOG(trace, + "response map filter: doRewrite has no response_map_ to rewrite with, doing nothing"); + return; + } + + const ResponseMap::ResponseMapPtr& response_map = *response_map_; + + // Fill in new_body and new_content_type using the response map rewrite. + // Pass in the request and response headers that we observed on the + // decodeHeaders and encodeHeaders paths, respectively. + std::string new_body; + absl::string_view new_content_type; + response_map->rewrite(request_headers_, *response_headers_, encoder_callbacks_->streamInfo(), + new_body, new_content_type); + + // Encoding buffer may be null here even if we saw data in encodeData above. This + // happens when sendLocalReply sends a response downstream. By adding encoded data + // here, we do successfully override the sendLocalReply body, but it's not clear + // how or why. + if (encoding_buffer == nullptr) { + // If we never saw a response body from the upstream, then we need to add encoded data + // instead of modifying the encoding buffer, as we do below. That's because headers-only + // upstream responses (or responses never received by the upstream) can only be transformed + // into responses with a body using this method. See `include/envoy/http/filter.h`. + // + // We're not streaming back this rewritten body (ie: it's already formed in memory) so + // we pass streaming_filter = false. + Buffer::OwnedImpl body{new_body}; + const bool streaming_filter = false; + encoder_callbacks_->addEncodedData(body, streaming_filter); + } else { + // We had a previous response from the upstream, so there's an existing encoding + // buffer that we should modify. Realistically, the encoding buffer should be + // empty here because we were draining data as it was received in encodeData, + // but that's more-or-less an optimization so we drain it again here for + // completeness. + // + // After we know the existing buffer has been drained of its existing data, we + // modify it by adding the rewritten body so that it gets passed downstream. + encoder_callbacks_->modifyEncodingBuffer([&new_body](Buffer::Instance& data) { + Buffer::OwnedImpl body{new_body}; + data.drain(data.length()); + data.move(body); + }); + } + + // Since we overwrote the response body, we need to set the content-length too. + encoding_buffer = encoder_callbacks_->encodingBuffer(); + response_headers_->setContentLength(encoding_buffer->length()); + if (!new_content_type.empty()) { + response_headers_->setContentType(new_content_type); + } +} + +} // namespace ResponseMapFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/response_map/response_map_filter.h b/source/extensions/filters/http/response_map/response_map_filter.h new file mode 100644 index 0000000000000..aeb37cd41e6dc --- /dev/null +++ b/source/extensions/filters/http/response_map/response_map_filter.h @@ -0,0 +1,147 @@ +#pragma once + +#include "envoy/http/filter.h" + +#include "common/buffer/buffer_impl.h" +#include "common/response_map/response_map.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ResponseMapFilter { + +/** + * Configuration for the response map filter. + */ +class ResponseMapFilterConfig { +public: + ResponseMapFilterConfig( + const envoy::extensions::filters::http::response_map::v3::ResponseMap& proto_config, + const std::string&, Server::Configuration::FactoryContext& context); + const ResponseMap::ResponseMapPtr* response_map() const { return &response_map_; } + +private: + const ResponseMap::ResponseMapPtr response_map_; +}; +using ResponseMapFilterConfigSharedPtr = std::shared_ptr; + +/* + * Per-route configuration for the response map filter. + * Allows the response map to be overriden or disabled. + */ +class FilterConfigPerRoute : public Router::RouteSpecificFilterConfig { +public: + FilterConfigPerRoute( + const envoy::extensions::filters::http::response_map::v3::ResponseMapPerRoute& proto_config, + Server::Configuration::ServerFactoryContext& context, + ProtobufMessage::ValidationVisitor& validationVisitor); + bool disabled() const { return disabled_; } + const ResponseMap::ResponseMapPtr* response_map() const { return &response_map_; } + +private: + bool disabled_; + const ResponseMap::ResponseMapPtr response_map_; +}; + +/** + * The response map filter. + * + * Filters on both the decoding stage (request moving upstream) and the encoding + * stage (response moving downstream). + * + * The request headers are captured in decodeHeaders to be later passed to a response + * map mapper for matching purposes. + * + * The response headers are captured in encodeHeaders to both be inspected by a + * response mapper for matching purposes. At this point, we decide if the response + * body (or, in theory, the headers too) will be rewritten. If the response is + * headers-only, we do the rewrite right away because encodeData won't be called. + * + * Otherwise, we wait until encodeData, drain anything we get from the upstream, + * and finally rewrite the response body. + * + * Not all filter stages are guaranteed to be called. For example, if there are + * no request headers to parse (because, for example, Envoy responds locally + * with HTTP 426 to upgrade an HTTP/1.0 request before parsing headers), then + * decodeHeaders will never be called. Similarly, if there is no upstream + * response body, then encodeData will not be called. + * + * The response map filter maintains three pieces of state: + * + * disabled_: set to true if a per-route config is found in the decode stage and + * the disabled flag is set. this disables rewrite behavior entirely. + * + * do_rewrite_: set to true if the chosen response mapper matched, and we should + * eventually do a response body (and/or header) rewrite. + * + * response_map_: set to a pointer to the response map that should be used to match + * and rewrite. if a per-route config is found and its mapper is set, + * use that. otherwise, use the globally configured mapper. + */ +class ResponseMapFilter : public Http::StreamFilter, Logger::Loggable { +public: + ResponseMapFilter(ResponseMapFilterConfigSharedPtr config); + + // Http::StreamFilterBase + void onDestroy() override {} + + // Http::StreamDecoderFilter + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap&, bool) override; + Http::FilterDataStatus decodeData(Buffer::Instance&, bool) override { + return Http::FilterDataStatus::Continue; + } + Http::FilterTrailersStatus decodeTrailers(Http::RequestTrailerMap&) override { + return Http::FilterTrailersStatus::Continue; + } + void setDecoderFilterCallbacks(Http::StreamDecoderFilterCallbacks& callbacks) override { + decoder_callbacks_ = &callbacks; + } + + // Http::StreamEncoderFilter + Http::FilterHeadersStatus encode100ContinueHeaders(Http::ResponseHeaderMap&) override { + return Http::FilterHeadersStatus::Continue; + } + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) override; + Http::FilterDataStatus encodeData(Buffer::Instance&, bool) override; + Http::FilterTrailersStatus encodeTrailers(Http::ResponseTrailerMap&) override { + return Http::FilterTrailersStatus::Continue; + } + Http::FilterMetadataStatus encodeMetadata(Http::MetadataMap&) override { + return Http::FilterMetadataStatus::Continue; + } + void setEncoderFilterCallbacks(Http::StreamEncoderFilterCallbacks& callbacks) override { + encoder_callbacks_ = &callbacks; + }; + + // Get the route specific config to use for this filter iteration. + const FilterConfigPerRoute* getRouteSpecificConfig(void); + + // Do the actual rewrite using the mapper we decided to use. + // + // Cannot be called if the filter was disabled (disabled_ == true) or if we decided + // not to do the rewrite (do_rewrite_ == false). Requires that response_map_ is set. + void doRewrite(); + +private: + ResponseMapFilterConfigSharedPtr config_; + Http::StreamDecoderFilterCallbacks* decoder_callbacks_{}; + Http::StreamEncoderFilterCallbacks* encoder_callbacks_{}; + Http::ResponseHeaderMap* response_headers_{}; + Http::RequestHeaderMap* request_headers_{}; + + // True if the response map is disabled on this iteration of the filter chain. + bool disabled_{}; + + // True if the response map matched the response and so we should rewrite + // the response. False otherwise. + bool do_rewrite_{}; + + // The response_map to use. May be the global response_map or a per-route map. + const ResponseMap::ResponseMapPtr* response_map_{}; +}; + +} // namespace ResponseMapFilter +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/well_known_names.h b/source/extensions/filters/http/well_known_names.h index 189640302f8db..5f81f2cf7e272 100644 --- a/source/extensions/filters/http/well_known_names.h +++ b/source/extensions/filters/http/well_known_names.h @@ -58,6 +58,8 @@ class HttpFilterNameValues { const std::string Squash = "envoy.filters.http.squash"; // External Authorization filter const std::string ExtAuthorization = "envoy.filters.http.ext_authz"; + // Response Map filter + const std::string ResponseMap = "envoy.filters.http.response_map"; // RBAC HTTP Authorization filter const std::string Rbac = "envoy.filters.http.rbac"; // JWT authentication filter diff --git a/test/common/router/header_formatter_test.cc b/test/common/router/header_formatter_test.cc index a99d7fbfe5587..f501a0d5ab356 100644 --- a/test/common/router/header_formatter_test.cc +++ b/test/common/router/header_formatter_test.cc @@ -463,6 +463,18 @@ TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVStart) { testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_START", "2018-12-18T01:50:34.000Z"); } +TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVStartCustom) { + NiceMock stream_info; + auto connection_info = std::make_shared>(); + absl::Time abslStartTime = + TestUtility::parseTime("Dec 18 01:50:34 2018 GMT", "%b %e %H:%M:%S %Y GMT"); + SystemTime startTime = absl::ToChronoTime(abslStartTime); + ON_CALL(*connection_info, validFromPeerCertificate()).WillByDefault(Return(startTime)); + EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info)); + testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_START(%b %e %H:%M:%S %Y %Z)", + "Dec 18 01:50:34 2018 UTC"); +} + TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVStartEmpty) { NiceMock stream_info; auto connection_info = std::make_shared>(); @@ -488,6 +500,18 @@ TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEnd) { testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_END", "2020-12-17T01:50:34.000Z"); } +TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEndCustom) { + NiceMock stream_info; + auto connection_info = std::make_shared>(); + absl::Time abslStartTime = + TestUtility::parseTime("Dec 17 01:50:34 2020 GMT", "%b %e %H:%M:%S %Y GMT"); + SystemTime startTime = absl::ToChronoTime(abslStartTime); + ON_CALL(*connection_info, expirationPeerCertificate()).WillByDefault(Return(startTime)); + EXPECT_CALL(stream_info, downstreamSslConnection()).WillRepeatedly(Return(connection_info)); + testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_END(%b %e %H:%M:%S %Y %Z)", + "Dec 17 01:50:34 2020 UTC"); +} + TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEndEmpty) { NiceMock stream_info; auto connection_info = std::make_shared>(); @@ -502,6 +526,24 @@ TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithDownstreamPeerCertVEndNoTls) testFormatting(stream_info, "DOWNSTREAM_PEER_CERT_V_END", EMPTY_STRING); } +TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithStartTime) { + NiceMock stream_info; + absl::Time abslStartTime = + TestUtility::parseTime("Dec 17 01:50:34 2020 GMT", "%b %e %H:%M:%S %Y GMT"); + SystemTime startTime = absl::ToChronoTime(abslStartTime); + EXPECT_CALL(stream_info, startTime()).WillRepeatedly(Return(startTime)); + testFormatting(stream_info, "START_TIME", "2020-12-17T01:50:34.000Z"); +} + +TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithStartTimeCustom) { + NiceMock stream_info; + absl::Time abslStartTime = + TestUtility::parseTime("Dec 17 01:50:34 2020 GMT", "%b %e %H:%M:%S %Y GMT"); + SystemTime startTime = absl::ToChronoTime(abslStartTime); + EXPECT_CALL(stream_info, startTime()).WillRepeatedly(Return(startTime)); + testFormatting(stream_info, "START_TIME(%b %e %H:%M:%S %Y %Z)", "Dec 17 01:50:34 2020 UTC"); +} + TEST_F(StreamInfoHeaderFormatterTest, TestFormatWithUpstreamMetadataVariable) { NiceMock stream_info; std::shared_ptr> host( diff --git a/test/extensions/filters/common/ext_authz/ext_authz_grpc_impl_test.cc b/test/extensions/filters/common/ext_authz/ext_authz_grpc_impl_test.cc index 98bce5a24108c..2601533b8d701 100644 --- a/test/extensions/filters/common/ext_authz/ext_authz_grpc_impl_test.cc +++ b/test/extensions/filters/common/ext_authz/ext_authz_grpc_impl_test.cc @@ -117,10 +117,13 @@ TEST_P(ExtAuthzGrpcClientTest, AuthorizationOkWithAllAtributes) { const std::string empty_body{}; const auto expected_headers = TestCommon::makeHeaderValueOption({{"foo", "bar", false}}); - auto check_response = TestCommon::makeCheckResponse( - Grpc::Status::WellKnownGrpcStatus::Ok, envoy::type::v3::OK, empty_body, expected_headers); - auto authz_response = - TestCommon::makeAuthzResponse(CheckStatus::OK, Http::Code::OK, empty_body, expected_headers); + const auto expected_downstream_headers = TestCommon::makeHeaderValueOption( + {{"authorized-by", "TestAuthService", false}, {"cookie", "authtoken=1234", true}}); + auto check_response = + TestCommon::makeCheckResponse(Grpc::Status::WellKnownGrpcStatus::Ok, envoy::type::v3::OK, + empty_body, expected_headers, expected_downstream_headers); + auto authz_response = TestCommon::makeAuthzResponse( + CheckStatus::OK, Http::Code::OK, empty_body, expected_headers, expected_downstream_headers); envoy::service::auth::v3::CheckRequest request; expectCallSend(request); @@ -190,11 +193,13 @@ TEST_P(ExtAuthzGrpcClientTest, AuthorizationDeniedWithAllAttributes) { const std::string expected_body{"test"}; const auto expected_headers = TestCommon::makeHeaderValueOption({{"foo", "bar", false}, {"foobar", "bar", true}}); - auto check_response = - TestCommon::makeCheckResponse(Grpc::Status::WellKnownGrpcStatus::PermissionDenied, - envoy::type::v3::Unauthorized, expected_body, expected_headers); - auto authz_response = TestCommon::makeAuthzResponse(CheckStatus::Denied, Http::Code::Unauthorized, - expected_body, expected_headers); + const auto expected_downstream_headers = TestCommon::makeHeaderValueOption({}); + auto check_response = TestCommon::makeCheckResponse( + Grpc::Status::WellKnownGrpcStatus::PermissionDenied, envoy::type::v3::Unauthorized, + expected_body, expected_headers, expected_downstream_headers); + auto authz_response = + TestCommon::makeAuthzResponse(CheckStatus::Denied, Http::Code::Unauthorized, expected_body, + expected_headers, expected_downstream_headers); envoy::service::auth::v3::CheckRequest request; expectCallSend(request); diff --git a/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc b/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc index 5798af81725a2..97e51e11a505c 100644 --- a/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc +++ b/test/extensions/filters/common/ext_authz/ext_authz_http_impl_test.cc @@ -89,6 +89,10 @@ class ExtAuthzHttpClientTest : public testing::Test { ignore_case: true - prefix: "X-" ignore_case: true + allowed_client_headers_on_success: + patterns: + - prefix: "X-Downstream-" + ignore_case: true )EOF"; TestUtility::loadFromYaml(default_yaml, proto_config); } else { @@ -296,8 +300,11 @@ using HeaderValuePair = std::paircheck(request_callbacks_, request, parent_span_, stream_info_); diff --git a/test/extensions/filters/common/ext_authz/test_common.cc b/test/extensions/filters/common/ext_authz/test_common.cc index 0f31d402750fa..67429f891e2ae 100644 --- a/test/extensions/filters/common/ext_authz/test_common.cc +++ b/test/extensions/filters/common/ext_authz/test_common.cc @@ -17,7 +17,8 @@ namespace ExtAuthz { CheckResponsePtr TestCommon::makeCheckResponse(Grpc::Status::GrpcStatus response_status, envoy::type::v3::StatusCode http_status_code, const std::string& body, - const HeaderValueOptionVector& headers) { + const HeaderValueOptionVector& headers, + const HeaderValueOptionVector& downstream_headers) { auto response = std::make_unique(); auto status = response->mutable_status(); status->set_code(response_status); @@ -46,13 +47,22 @@ CheckResponsePtr TestCommon::makeCheckResponse(Grpc::Status::GrpcStatus response item->CopyFrom(header); } } + if (!downstream_headers.empty()) { + const auto ok_response_headers_to_add = + response->mutable_ok_response()->mutable_response_headers_to_add(); + for (const auto& header : downstream_headers) { + auto* item = ok_response_headers_to_add->Add(); + item->CopyFrom(header); + } + } } return response; } Response TestCommon::makeAuthzResponse(CheckStatus status, Http::Code status_code, const std::string& body, - const HeaderValueOptionVector& headers) { + const HeaderValueOptionVector& headers, + const HeaderValueOptionVector& downstream_headers) { auto authz_response = Response{}; authz_response.status = status; authz_response.status_code = status_code; @@ -70,6 +80,12 @@ Response TestCommon::makeAuthzResponse(CheckStatus status, Http::Code status_cod } } } + if (!downstream_headers.empty()) { + for (auto& header : downstream_headers) { + authz_response.response_headers_to_add.emplace_back( + Http::LowerCaseString(header.header().key()), header.header().value()); + } + } return authz_response; } diff --git a/test/extensions/filters/common/ext_authz/test_common.h b/test/extensions/filters/common/ext_authz/test_common.h index 116973697fca4..358f3a9a5fb8e 100644 --- a/test/extensions/filters/common/ext_authz/test_common.h +++ b/test/extensions/filters/common/ext_authz/test_common.h @@ -32,16 +32,17 @@ class TestCommon { static Http::ResponseMessagePtr makeMessageResponse(const HeaderValueOptionVector& headers, const std::string& body = std::string{}); - static CheckResponsePtr makeCheckResponse( - Grpc::Status::GrpcStatus response_status = Grpc::Status::WellKnownGrpcStatus::Ok, - envoy::type::v3::StatusCode http_status_code = envoy::type::v3::OK, - const std::string& body = std::string{}, - const HeaderValueOptionVector& headers = HeaderValueOptionVector{}); + static CheckResponsePtr makeCheckResponse(Grpc::Status::GrpcStatus response_status, + envoy::type::v3::StatusCode http_status_code, + const std::string& body, + const HeaderValueOptionVector& headers, + const HeaderValueOptionVector& downstream_headers); static Response makeAuthzResponse(CheckStatus status, Http::Code status_code = Http::Code::OK, const std::string& body = std::string{}, - const HeaderValueOptionVector& headers = HeaderValueOptionVector{}); + const HeaderValueOptionVector& headers = HeaderValueOptionVector{}, + const HeaderValueOptionVector& downstream_headers = HeaderValueOptionVector{}); static HeaderValueOptionVector makeHeaderValueOption(KeyValueOptionVector&& headers); @@ -105,6 +106,12 @@ MATCHER_P(AuthzOkResponse, response, "") { return false; } + // Compare response_headers_to_add. + if (!TestCommon::compareHeaderVector(response.response_headers_to_add, + arg->response_headers_to_add)) { + return false; + } + return TestCommon::compareVectorOfHeaderName(response.headers_to_remove, arg->headers_to_remove); } diff --git a/test/extensions/filters/http/ext_authz/config_test.cc b/test/extensions/filters/http/ext_authz/config_test.cc index 0d48e3b481bae..2204f4985327b 100644 --- a/test/extensions/filters/http/ext_authz/config_test.cc +++ b/test/extensions/filters/http/ext_authz/config_test.cc @@ -58,7 +58,7 @@ void expectCorrectProtoGrpc(envoy::config::core::v3::ApiVersion api_version) { })); Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(*proto_config, "stats", context); Http::MockFilterChainFactoryCallbacks filter_callback; - EXPECT_CALL(filter_callback, addStreamDecoderFilter(_)); + EXPECT_CALL(filter_callback, addStreamFilter(_)); cb(filter_callback); } @@ -125,7 +125,7 @@ TEST(HttpExtAuthzConfigTest, CorrectProtoHttp) { EXPECT_CALL(context, scope()); Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(*proto_config, "stats", context); testing::StrictMock filter_callback; - EXPECT_CALL(filter_callback, addStreamDecoderFilter(_)); + EXPECT_CALL(filter_callback, addStreamFilter(_)); cb(filter_callback); } diff --git a/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc b/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc index c3ec137d8360f..727c2170bd36c 100644 --- a/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc +++ b/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc @@ -235,7 +235,8 @@ class ExtAuthzGrpcIntegrationTest : public Grpc::VersionedGrpcClientIntegrationP void sendExtAuthzResponse(const Headers& headers_to_add, const Headers& headers_to_append, const Headers& headers_to_remove, const Http::TestRequestHeaderMapImpl& new_headers_from_upstream, - const Http::TestRequestHeaderMapImpl& headers_to_append_multiple) { + const Http::TestRequestHeaderMapImpl& headers_to_append_multiple, + const Headers& response_headers_to_add) { ext_authz_request_->startGrpcStream(); envoy::service::auth::v3::CheckResponse check_response; check_response.mutable_status()->set_code(Grpc::Status::WellKnownGrpcStatus::Ok); @@ -285,6 +286,17 @@ class ExtAuthzGrpcIntegrationTest : public Grpc::VersionedGrpcClientIntegrationP return Http::HeaderMap::Iterate::Continue; }); + for (const auto& response_header_to_add : response_headers_to_add) { + auto* entry = check_response.mutable_ok_response()->mutable_response_headers_to_add()->Add(); + const auto key = std::string(response_header_to_add.first); + const auto value = std::string(response_header_to_add.second); + + entry->mutable_append()->set_value(false); + entry->mutable_header()->set_key(key); + entry->mutable_header()->set_value(value); + ENVOY_LOG_MISC(trace, "sendExtAuthzResponse: set response_header_to_add {}={}", key, value); + } + ext_authz_request_->sendGrpcMessage(check_response); ext_authz_request_->finishGrpcStream(Grpc::Status::Ok); } @@ -354,11 +366,12 @@ class ExtAuthzGrpcIntegrationTest : public Grpc::VersionedGrpcClientIntegrationP std::make_pair(header_to_append.first, header_to_append.second + "-appended")); } sendExtAuthzResponse(updated_headers_to_add, updated_headers_to_append, headers_to_remove, - new_headers_from_upstream, headers_to_append_multiple); + new_headers_from_upstream, headers_to_append_multiple, Headers{}); waitForSuccessfulUpstreamResponse("200", updated_headers_to_add, updated_headers_to_append, headers_to_remove, new_headers_from_upstream, headers_to_append_multiple); + cleanup(); } @@ -591,7 +604,8 @@ TEST_P(ExtAuthzGrpcIntegrationTest, HTTP2DownstreamRequestWithLargeBody) { } // Verifies that the original request headers will be added and appended when the authorization -// server returns headers_to_add and headers_to_append in OkResponse message. +// server returns headers_to_add, response_headers_to_add, and headers_to_append in OkResponse +// message. TEST_P(ExtAuthzGrpcIntegrationTest, SendHeadersToAddAndToAppendToUpstream) { XDS_DEPRECATED_FEATURE_TEST_SKIP; expectCheckRequestWithBodyWithHeaders( @@ -625,6 +639,38 @@ TEST_P(ExtAuthzGrpcIntegrationTest, DenyAtDisableWithMetadata) { expectFilterDisableCheck(/*deny_at_disable=*/true, /*disable_with_metadata=*/true, "403"); } +TEST_P(ExtAuthzGrpcIntegrationTest, DownstreamHeadersOnSuccess) { + XDS_DEPRECATED_FEATURE_TEST_SKIP; + // Set up ext_authz filter. + initializeConfig(); + + // Use h1, set up the test. + setDownstreamProtocol(Http::CodecClient::Type::HTTP1); + HttpIntegrationTest::initialize(); + + // Start a client connection and request. + initiateClientConnection(0); + + // Wait for the ext_authz request as a result of the client request. + waitForExtAuthzRequest(expectedCheckRequest(Http::CodecClient::Type::HTTP1)); + + // Send back an ext_authz response with response_headers_to_add set. + sendExtAuthzResponse(Headers{}, Headers{}, Headers{}, Http::TestRequestHeaderMapImpl{}, + Http::TestRequestHeaderMapImpl{}, + Headers{{"downstream2", "downstream-should-see-me"}}); + + // Wait for the upstream response. + waitForSuccessfulUpstreamResponse("200"); + + // Verify the response is HTTP 200 with the header from `response_headers_to_add` above. + const std::string expected_body(response_size_, 'a'); + verifyResponse(std::move(response_), "200", + Http::TestResponseHeaderMapImpl{{":status", "200"}, + {"downstream2", "downstream-should-see-me"}}, + expected_body); + cleanup(); +} + INSTANTIATE_TEST_SUITE_P(IpVersions, ExtAuthzHttpIntegrationTest, ValuesIn(TestEnvironment::getIpVersionsForTest()), TestUtility::ipTestParamsToString); @@ -766,7 +812,7 @@ TEST_P(ExtAuthzGrpcIntegrationTest, GoogleAsyncClientCreation) { initiateClientConnection(4, Headers{}, Headers{}); waitForExtAuthzRequest(expectedCheckRequest(Http::CodecClient::Type::HTTP2)); sendExtAuthzResponse(Headers{}, Headers{}, Headers{}, Http::TestRequestHeaderMapImpl{}, - Http::TestRequestHeaderMapImpl{}); + Http::TestRequestHeaderMapImpl{}, Headers{}); waitForSuccessfulUpstreamResponse("200"); @@ -796,7 +842,7 @@ TEST_P(ExtAuthzGrpcIntegrationTest, GoogleAsyncClientCreation) { test_server_->counter("grpc.ext_authz.google_grpc_client_creation")->value()); } sendExtAuthzResponse(Headers{}, Headers{}, Headers{}, Http::TestRequestHeaderMapImpl{}, - Http::TestRequestHeaderMapImpl{}); + Http::TestRequestHeaderMapImpl{}, Headers{}); result = fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_); RELEASE_ASSERT(result, result.message()); diff --git a/test/extensions/filters/http/ext_authz/ext_authz_test.cc b/test/extensions/filters/http/ext_authz/ext_authz_test.cc index 677349a277ead..358349aa7973e 100644 --- a/test/extensions/filters/http/ext_authz/ext_authz_test.cc +++ b/test/extensions/filters/http/ext_authz/ext_authz_test.cc @@ -63,6 +63,7 @@ template class HttpFilterTestBase : public T { client_ = new Filters::Common::ExtAuthz::MockClient(); filter_ = std::make_unique(config_, Filters::Common::ExtAuthz::ClientPtr{client_}); filter_->setDecoderFilterCallbacks(filter_callbacks_); + filter_->setEncoderFilterCallbacks(encoder_filter_callbacks_); addr_ = std::make_shared("1.2.3.4", 1111); } @@ -77,6 +78,7 @@ template class HttpFilterTestBase : public T { Filters::Common::ExtAuthz::MockClient* client_; std::unique_ptr filter_; NiceMock filter_callbacks_; + NiceMock encoder_filter_callbacks_; Filters::Common::ExtAuthz::RequestCallbacks* request_callbacks_; Http::TestRequestHeaderMapImpl request_headers_; Http::TestRequestTrailerMapImpl request_trailers_; @@ -1709,6 +1711,8 @@ TEST_P(HttpFilterTestParam, ImmediateOkResponseWithHttpAttributes) { response.headers_to_append = Http::HeaderVector{{request_header_key, "bar"}}; response.headers_to_set = Http::HeaderVector{{key_to_add, "foo"}, {key_to_override, "bar"}}; response.headers_to_remove = std::vector{key_to_remove}; + response.response_headers_to_add = + Http::HeaderVector{{Http::LowerCaseString{"cookie"}, "flavor=gingerbread"}}; auto response_ptr = std::make_unique(response); @@ -1726,6 +1730,16 @@ TEST_P(HttpFilterTestParam, ImmediateOkResponseWithHttpAttributes) { EXPECT_EQ(request_headers_.get_(key_to_add), "foo"); EXPECT_EQ(request_headers_.get_(key_to_override), "bar"); EXPECT_EQ(request_headers_.has(key_to_remove), false); + + Buffer::OwnedImpl response_data{}; + Http::TestResponseHeaderMapImpl response_headers{{":status", "200"}}; + Http::TestResponseTrailerMapImpl response_trailers{}; + Http::MetadataMap response_metadata{}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers, false)); + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(response_data, false)); + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers)); + EXPECT_EQ(Http::FilterMetadataStatus::Continue, filter_->encodeMetadata(response_metadata)); + EXPECT_EQ(response_headers.get_("cookie"), "flavor=gingerbread"); } // Test that an synchronous denied response from the authorization service, on the call stack, diff --git a/test/extensions/filters/network/common/fuzz/uber_per_readfilter.cc b/test/extensions/filters/network/common/fuzz/uber_per_readfilter.cc index f6976aedf6b9b..be8d0628743fe 100644 --- a/test/extensions/filters/network/common/fuzz/uber_per_readfilter.cc +++ b/test/extensions/filters/network/common/fuzz/uber_per_readfilter.cc @@ -68,9 +68,11 @@ void UberFilterFuzzer::perFilterSetup(const std::string& filter_name) { const std::string empty_body{}; const auto expected_headers = Filters::Common::ExtAuthz::TestCommon::makeHeaderValueOption({}); + const auto expected_downstream_headers = + Filters::Common::ExtAuthz::TestCommon::makeHeaderValueOption({}); auto check_response = Filters::Common::ExtAuthz::TestCommon::makeCheckResponse( Grpc::Status::WellKnownGrpcStatus::Ok, envoy::type::v3::OK, empty_body, - expected_headers); + expected_headers, expected_downstream_headers); // Give response to the grpc_client by calling onSuccess(). grpc_client_impl->onSuccess(std::move(check_response), span_); return async_request_.get(); diff --git a/test/mocks/upstream/cluster_info.cc b/test/mocks/upstream/cluster_info.cc index fd46bb9588465..ce3146a36d1a9 100644 --- a/test/mocks/upstream/cluster_info.cc +++ b/test/mocks/upstream/cluster_info.cc @@ -36,6 +36,13 @@ MockIdleTimeEnabledClusterInfo::MockIdleTimeEnabledClusterInfo() { MockIdleTimeEnabledClusterInfo::~MockIdleTimeEnabledClusterInfo() = default; +MockMaxConnectionDurationEnabledClusterInfo::MockMaxConnectionDurationEnabledClusterInfo() { + ON_CALL(*this, maxConnectionDuration()).WillByDefault(Return(std::chrono::milliseconds(1000))); +} + +MockMaxConnectionDurationEnabledClusterInfo::~MockMaxConnectionDurationEnabledClusterInfo() = + default; + MockClusterInfo::MockClusterInfo() : http2_options_(::Envoy::Http2::Utility::initializeAndValidateOptions( envoy::config::core::v3::Http2ProtocolOptions())), diff --git a/test/mocks/upstream/cluster_info.h b/test/mocks/upstream/cluster_info.h index 35b810b515ed3..a39d43c769aea 100644 --- a/test/mocks/upstream/cluster_info.h +++ b/test/mocks/upstream/cluster_info.h @@ -89,6 +89,7 @@ class MockClusterInfo : public ClusterInfo { MOCK_METHOD(bool, addedViaApi, (), (const)); MOCK_METHOD(std::chrono::milliseconds, connectTimeout, (), (const)); MOCK_METHOD(const absl::optional, idleTimeout, (), (const)); + MOCK_METHOD(const absl::optional, maxConnectionDuration, (), (const)); MOCK_METHOD(const absl::optional, maxStreamDuration, (), (const)); MOCK_METHOD(const absl::optional, grpcTimeoutHeaderMax, (), (const)); MOCK_METHOD(const absl::optional, grpcTimeoutHeaderOffset, (), @@ -200,5 +201,11 @@ class MockIdleTimeEnabledClusterInfo : public MockClusterInfo { ~MockIdleTimeEnabledClusterInfo() override; }; +class MockMaxConnectionDurationEnabledClusterInfo : public MockClusterInfo { +public: + MockMaxConnectionDurationEnabledClusterInfo(); + ~MockMaxConnectionDurationEnabledClusterInfo() override; +}; + } // namespace Upstream } // namespace Envoy