diff --git a/api/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto b/api/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto index 3082089202eef..007ccabc3e47d 100644 --- a/api/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto +++ b/api/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto @@ -15,11 +15,27 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // gRPC-JSON transcoder :ref:`configuration overview `. // [#extension: envoy.filters.http.grpc_json_transcoder] -// [#next-free-field: 10] +// [#next-free-field: 11] message GrpcJsonTranscoder { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.transcoder.v2.GrpcJsonTranscoder"; + enum UrlUnescapeSpec { + // URL path parameters will not decode RFC 6570 reserved characters. + // For example, segment `%2f%23/%20%2523` is unescaped to `%2f%23/ %23`. + ALL_CHARACTERS_EXCEPT_RESERVED = 0; + + // URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // For example, segment `%2f%23/%20%2523` is unescaped to `%2f#/ %23`. + ALL_CHARACTERS_EXCEPT_SLASH = 1; + + // URL path parameters will be fully URI-decoded. + // For example, segment `%2f%23/%20%2523` is unescaped to `/#/ %23`. + ALL_CHARACTERS = 2; + } + message PrintOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.transcoder.v2.GrpcJsonTranscoder.PrintOptions"; @@ -160,4 +176,11 @@ message GrpcJsonTranscoder { // the ``google/rpc/error_details.proto`` should be included in the configured // :ref:`proto descriptor set `. bool convert_grpc_status = 9; + + // URL unescaping policy. + // This spec is only applied when extracting variable with multiple segments. + // For example, in case of `/foo/{x=*}/bar/{y=prefix/*}/{z=**}` `x` variable is single segment and `y` and `z` are multiple segments. + // For a path with `/foo/first/bar/prefix/second/third/fourth`, `x=first`, `y=prefix/second`, `z=third/fourth`. + // If this setting is not specified, the value defaults to :ref:`ALL_CHARACTERS_EXCEPT_RESERVED`. + UrlUnescapeSpec url_unescape_spec = 10 [(validate.rules).enum = {defined_only: true}]; } diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index 015e604af0da9..62fef14148ae6 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -507,13 +507,13 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "grpc-httpjson-transcoding", project_desc = "Library that supports transcoding so that HTTP/JSON can be converted to gRPC", project_url = "https://github.com/grpc-ecosystem/grpc-httpjson-transcoding", - version = "b48d8aa15b3825e146168146755475ab918e95b7", - sha256 = "4147e992ec239fb78c435fdd9f68e8d93d89106f67278bf2995f3672dddba52b", + version = "4d095f048889d4fc3b8d4579aa80ca4290319802", + sha256 = "7af66e0674340932683ab4f04ea6f03e2550849a54741738d94310b84d396a2c", strip_prefix = "grpc-httpjson-transcoding-{version}", urls = ["https://github.com/grpc-ecosystem/grpc-httpjson-transcoding/archive/{version}.tar.gz"], use_category = ["dataplane_ext"], extensions = ["envoy.filters.http.grpc_json_transcoder"], - release_date = "2020-11-05", + release_date = "2020-11-12", cpe = "N/A", ), io_bazel_rules_go = dict( diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index 07412294aeb13..35e72b578e83b 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -50,6 +50,7 @@ New Features ------------ * config: added new runtime feature `envoy.features.enable_all_deprecated_features` that allows the use of all deprecated features. * grpc: implemented header value syntax support when defining :ref:`initial metadata ` for gRPC-based `ext_authz` :ref:`HTTP ` and :ref:`network ` filters, and :ref:`ratelimit ` filters. +* grpc-json: added support for configuring :ref:`unescaping behavior ` for path components. * hds: added support for delta updates in the :ref:`HealthCheckSpecifier `, making only the Endpoints and Health Checkers that changed be reconstructed on receiving a new message, rather than the entire HDS. * health_check: added option to use :ref:`no_traffic_healthy_interval ` which allows a different no traffic interval when the host is healthy. * http: added HCM :ref:`timeout config field ` to control how long a downstream has to finish sending headers before the stream is cancelled. diff --git a/generated_api_shadow/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto b/generated_api_shadow/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto index 3082089202eef..007ccabc3e47d 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/grpc_json_transcoder/v3/transcoder.proto @@ -15,11 +15,27 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // gRPC-JSON transcoder :ref:`configuration overview `. // [#extension: envoy.filters.http.grpc_json_transcoder] -// [#next-free-field: 10] +// [#next-free-field: 11] message GrpcJsonTranscoder { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.transcoder.v2.GrpcJsonTranscoder"; + enum UrlUnescapeSpec { + // URL path parameters will not decode RFC 6570 reserved characters. + // For example, segment `%2f%23/%20%2523` is unescaped to `%2f%23/ %23`. + ALL_CHARACTERS_EXCEPT_RESERVED = 0; + + // URL path parameters will be fully URI-decoded except in + // cases of single segment matches in reserved expansion, where "%2F" will be + // left encoded. + // For example, segment `%2f%23/%20%2523` is unescaped to `%2f#/ %23`. + ALL_CHARACTERS_EXCEPT_SLASH = 1; + + // URL path parameters will be fully URI-decoded. + // For example, segment `%2f%23/%20%2523` is unescaped to `/#/ %23`. + ALL_CHARACTERS = 2; + } + message PrintOptions { option (udpa.annotations.versioning).previous_message_type = "envoy.config.filter.http.transcoder.v2.GrpcJsonTranscoder.PrintOptions"; @@ -160,4 +176,11 @@ message GrpcJsonTranscoder { // the ``google/rpc/error_details.proto`` should be included in the configured // :ref:`proto descriptor set `. bool convert_grpc_status = 9; + + // URL unescaping policy. + // This spec is only applied when extracting variable with multiple segments. + // For example, in case of `/foo/{x=*}/bar/{y=prefix/*}/{z=**}` `x` variable is single segment and `y` and `z` are multiple segments. + // For a path with `/foo/first/bar/prefix/second/third/fourth`, `x=first`, `y=prefix/second`, `z=third/fourth`. + // If this setting is not specified, the value defaults to :ref:`ALL_CHARACTERS_EXCEPT_RESERVED`. + UrlUnescapeSpec url_unescape_spec = 10 [(validate.rules).enum = {defined_only: true}]; } diff --git a/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc b/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc index e2998a3f58661..d770b9feb51a9 100644 --- a/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc +++ b/source/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter.cc @@ -188,6 +188,24 @@ JsonTranscoderConfig::JsonTranscoderConfig( } } + switch (proto_config.url_unescape_spec()) { + case envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder:: + ALL_CHARACTERS_EXCEPT_RESERVED: + pmb.SetUrlUnescapeSpec( + google::grpc::transcoding::UrlUnescapeSpec::kAllCharactersExceptReserved); + break; + case envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder:: + ALL_CHARACTERS_EXCEPT_SLASH: + pmb.SetUrlUnescapeSpec(google::grpc::transcoding::UrlUnescapeSpec::kAllCharactersExceptSlash); + break; + case envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder:: + ALL_CHARACTERS: + pmb.SetUrlUnescapeSpec(google::grpc::transcoding::UrlUnescapeSpec::kAllCharacters); + break; + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } + path_matcher_ = pmb.Build(); const auto& print_config = proto_config.print_options(); diff --git a/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc b/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc index 1ec37be541b42..8082a846db893 100644 --- a/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc +++ b/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc @@ -1270,6 +1270,78 @@ INSTANTIATE_TEST_SUITE_P( })", R"({"id":"101","gender":"MALE","last_name":"Shakespeare"})"})); +struct GrpcJsonTranscoderFilterUnescapeTestParam { + std::string config_json_; + std::string expected_arg_; +}; + +class GrpcJsonTranscoderFilterUnescapeTest + : public testing::TestWithParam, + public GrpcJsonTranscoderFilterTestBase { +protected: + GrpcJsonTranscoderFilterUnescapeTest() { + envoy::extensions::filters::http::grpc_json_transcoder::v3::GrpcJsonTranscoder proto_config; + TestUtility::loadFromJson(TestEnvironment::substitute(GetParam().config_json_), proto_config); + config_ = std::make_unique(proto_config, *api_); + filter_ = std::make_unique(*config_); + filter_->setDecoderFilterCallbacks(decoder_callbacks_); + filter_->setEncoderFilterCallbacks(encoder_callbacks_); + } + + std::unique_ptr config_; + std::unique_ptr filter_; + NiceMock decoder_callbacks_; + NiceMock encoder_callbacks_; +}; + +TEST_P(GrpcJsonTranscoderFilterUnescapeTest, UnescapeSpec) { + Http::TestRequestHeaderMapImpl request_headers{ + {"content-type", "text/plain"}, {":method", "POST"}, {":path", "/wildcard/%2f%23/%20%2523"}}; + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers, false)); + + Buffer::OwnedImpl request_data{"{}"}; + EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(request_data, true)); + + Grpc::Decoder decoder; + std::vector frames; + decoder.decode(request_data, frames); + + EXPECT_EQ(1, frames.size()); + + bookstore::EchoBodyRequest expected_request; + expected_request.set_arg(GetParam().expected_arg_); + + bookstore::EchoBodyRequest request; + request.ParseFromString(frames[0].data_->toString()); + + EXPECT_EQ(expected_request.ByteSize(), frames[0].length_); + EXPECT_TRUE(MessageDifferencer::Equals(expected_request, request)); +} + +INSTANTIATE_TEST_SUITE_P(GrpcJsonTranscoderFilterUnescapeOptions, + GrpcJsonTranscoderFilterUnescapeTest, + ::testing::Values( + GrpcJsonTranscoderFilterUnescapeTestParam{ + R"({ + "proto_descriptor": "{{ test_rundir }}/test/proto/bookstore.descriptor", + "services": ["bookstore.Bookstore"] + })", + "%2f%23/ %23"}, + GrpcJsonTranscoderFilterUnescapeTestParam{ + R"({ + "proto_descriptor": "{{ test_rundir }}/test/proto/bookstore.descriptor", + "services": ["bookstore.Bookstore"], + "url_unescape_spec": "ALL_CHARACTERS_EXCEPT_SLASH" + })", + "%2f#/ %23"}, + GrpcJsonTranscoderFilterUnescapeTestParam{ + R"({ + "proto_descriptor": "{{ test_rundir }}/test/proto/bookstore.descriptor", + "services": ["bookstore.Bookstore"], + "url_unescape_spec": "ALL_CHARACTERS" + })", + "/#/ %23"})); + } // namespace } // namespace GrpcJsonTranscoder } // namespace HttpFilters diff --git a/test/proto/bookstore.proto b/test/proto/bookstore.proto index 62e697e219ee8..559ec4da0acee 100644 --- a/test/proto/bookstore.proto +++ b/test/proto/bookstore.proto @@ -134,6 +134,11 @@ service Bookstore { get: "/bigbook" }; } + rpc PostWildcard(EchoBodyRequest) returns (google.protobuf.Empty) { + option (google.api.http) = { + post: "/wildcard/{arg=**}" + }; + } } service ServiceWithResponseBody {