diff --git a/CODEOWNERS b/CODEOWNERS index 7f684161d704..524805d3ab11 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -32,6 +32,8 @@ extensions/filters/common/original_src @snowp @klarose /*/extensions/filters/http/ext_proc @gbrail @htuch # jwt_authn http filter extension /*/extensions/filters/http/jwt_authn @qiwzhang @lizan +# jres_proxy extension +/*/extensions/filters/network/jres_proxy @zyfjeff @lizan # grpc_http1_reverse_bridge http filter extension /*/extensions/filters/http/grpc_http1_reverse_bridge @snowp @zuercher # header_to_metadata extension diff --git a/api/BUILD b/api/BUILD index 5b4131922cd0..90740fc6a6ac 100644 --- a/api/BUILD +++ b/api/BUILD @@ -57,6 +57,7 @@ proto_library( "//envoy/config/filter/http/squash/v2:pkg", "//envoy/config/filter/http/tap/v2alpha:pkg", "//envoy/config/filter/http/transcoder/v2:pkg", + "//envoy/config/filter/jres/router/v2alpha1:pkg", "//envoy/config/filter/listener/http_inspector/v2:pkg", "//envoy/config/filter/listener/original_dst/v2:pkg", "//envoy/config/filter/listener/original_src/v2alpha1:pkg", @@ -68,6 +69,7 @@ proto_library( "//envoy/config/filter/network/echo/v2:pkg", "//envoy/config/filter/network/ext_authz/v2:pkg", "//envoy/config/filter/network/http_connection_manager/v2:pkg", + "//envoy/config/filter/network/jres_proxy/v2alpha1:pkg", "//envoy/config/filter/network/kafka_broker/v2alpha1:pkg", "//envoy/config/filter/network/local_rate_limit/v2alpha:pkg", "//envoy/config/filter/network/mongo_proxy/v2:pkg", @@ -218,6 +220,8 @@ proto_library( "//envoy/extensions/filters/network/echo/v3:pkg", "//envoy/extensions/filters/network/ext_authz/v3:pkg", "//envoy/extensions/filters/network/http_connection_manager/v3:pkg", + "//envoy/extensions/filters/network/jres_proxy/router/v3:pkg", + "//envoy/extensions/filters/network/jres_proxy/v3:pkg", "//envoy/extensions/filters/network/kafka_broker/v3:pkg", "//envoy/extensions/filters/network/local_ratelimit/v3:pkg", "//envoy/extensions/filters/network/mongo_proxy/v3:pkg", diff --git a/api/envoy/config/filter/jres/router/v2alpha1/BUILD b/api/envoy/config/filter/jres/router/v2alpha1/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/api/envoy/config/filter/jres/router/v2alpha1/BUILD @@ -0,0 +1,9 @@ +# 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 = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/api/envoy/config/filter/jres/router/v2alpha1/router.proto b/api/envoy/config/filter/jres/router/v2alpha1/router.proto new file mode 100644 index 000000000000..c4d31c800968 --- /dev/null +++ b/api/envoy/config/filter/jres/router/v2alpha1/router.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package envoy.config.filter.jres.router.v2alpha1; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.jres.router.v2alpha1"; +option java_outer_classname = "RouterProto"; +option java_multiple_files = true; +option (udpa.annotations.file_migrate).move_to_package = + "envoy.extensions.filters.network.jres_proxy.router.v3"; +option (udpa.annotations.file_status).package_version_status = FROZEN; + +// [#protodoc-title: Router] +// Jres router :ref:`configuration overview `. + +message Router { +} diff --git a/api/envoy/config/filter/network/jres_proxy/v2alpha1/BUILD b/api/envoy/config/filter/network/jres_proxy/v2alpha1/BUILD new file mode 100644 index 000000000000..5fe475a5dcf8 --- /dev/null +++ b/api/envoy/config/filter/network/jres_proxy/v2alpha1/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/api/v2/route:pkg", + "//envoy/type:pkg", + "//envoy/type/matcher:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/config/filter/network/jres_proxy/v2alpha1/README.md b/api/envoy/config/filter/network/jres_proxy/v2alpha1/README.md new file mode 100644 index 000000000000..ac867ca252de --- /dev/null +++ b/api/envoy/config/filter/network/jres_proxy/v2alpha1/README.md @@ -0,0 +1 @@ +Protocol buffer definitions for the Jres proxy. diff --git a/api/envoy/config/filter/network/jres_proxy/v2alpha1/jres_proxy.proto b/api/envoy/config/filter/network/jres_proxy/v2alpha1/jres_proxy.proto new file mode 100644 index 000000000000..d28c9be4457d --- /dev/null +++ b/api/envoy/config/filter/network/jres_proxy/v2alpha1/jres_proxy.proto @@ -0,0 +1,66 @@ +syntax = "proto3"; + +package envoy.config.filter.network.jres_proxy.v2alpha1; + +import "envoy/config/filter/network/jres_proxy/v2alpha1/route.proto"; + +import "google/protobuf/any.proto"; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.network.jres_proxy.v2alpha1"; +option java_outer_classname = "JresProxyProto"; +option java_multiple_files = true; +option (udpa.annotations.file_migrate).move_to_package = + "envoy.extensions.filters.network.jres_proxy.v3"; +option (udpa.annotations.file_status).package_version_status = FROZEN; + +// [#protodoc-title: Jres Proxy] +// Jres Proxy :ref:`configuration overview `. +// [#extension: envoy.filters.network.jres_proxy] + +// Jres Protocol types supported by Envoy. +enum ProtocolType { + // the default protocol. + Jres = 0; +} + +// Jres Serialization types supported by Envoy. +enum SerializationType { + // the default serialization protocol. + Hessian2 = 0; +} + +// [#next-free-field: 6] +message JresProxy { + // The human readable prefix to use when emitting statistics. + string stat_prefix = 1 [(validate.rules).string = {min_bytes: 1}]; + + // Configure the protocol used. + ProtocolType protocol_type = 2 [(validate.rules).enum = {defined_only: true}]; + + // Configure the serialization protocol used. + SerializationType serialization_type = 3 [(validate.rules).enum = {defined_only: true}]; + + // The route table for the connection manager is static and is specified in this property. + repeated RouteConfiguration route_config = 4; + + // A list of individual Jres filters that make up the filter chain for requests made to the + // Jres proxy. Order matters as the filters are processed sequentially. For backwards + // compatibility, if no jres_filters are specified, a default Jres router filter + // (`envoy.filters.jres.router`) is used. + repeated JresFilter jres_filters = 5; +} + +// JresFilter configures a Jres filter. +message JresFilter { + // The name of the filter to instantiate. The name must match a supported + // filter. + string name = 1 [(validate.rules).string = {min_bytes: 1}]; + + // Filter specific configuration which depends on the filter being + // instantiated. See the supported filters for further documentation. + google.protobuf.Any config = 2; +} diff --git a/api/envoy/config/filter/network/jres_proxy/v2alpha1/route.proto b/api/envoy/config/filter/network/jres_proxy/v2alpha1/route.proto new file mode 100644 index 000000000000..6b0d2ed0574c --- /dev/null +++ b/api/envoy/config/filter/network/jres_proxy/v2alpha1/route.proto @@ -0,0 +1,105 @@ +syntax = "proto3"; + +package envoy.config.filter.network.jres_proxy.v2alpha1; + +import "envoy/api/v2/route/route_components.proto"; +import "envoy/type/matcher/string.proto"; +import "envoy/type/range.proto"; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.network.jres_proxy.v2alpha1"; +option java_outer_classname = "RouteProto"; +option java_multiple_files = true; +option (udpa.annotations.file_migrate).move_to_package = + "envoy.extensions.filters.network.jres_proxy.v3"; +option (udpa.annotations.file_status).package_version_status = FROZEN; + +// [#protodoc-title: Jres Proxy Route Configuration] +// Jres Proxy :ref:`configuration overview `. + +// [#next-free-field: 6] +message RouteConfiguration { + // The name of the route configuration. Reserved for future use in asynchronous route discovery. + string name = 1; + + // The interface name of the service. + string interface = 2; + + // Which group does the interface belong to. + string group = 3; + + // The version number of the interface. + string version = 4; + + // The list of routes that will be matched, in order, against incoming requests. The first route + // that matches will be used. + repeated Route routes = 5; +} + +message Route { + // Route matching parameters. + RouteMatch match = 1 [(validate.rules).message = {required: true}]; + + // Route request to some upstream cluster. + RouteAction route = 2 [(validate.rules).message = {required: true}]; +} + +message RouteMatch { + // Method level routing matching. + MethodMatch method = 1; + + // Specifies a set of headers that the route should match on. The router will check the request’s + // headers against all the specified headers in the route config. A match will happen if all the + // headers in the route are present in the request with the same values (or based on presence if + // the value field is not in the config). + repeated api.v2.route.HeaderMatcher headers = 2; +} + +message RouteAction { + oneof cluster_specifier { + option (validate.required) = true; + + // Indicates the upstream cluster to which the request should be routed. + string cluster = 1; + + // Multiple upstream clusters can be specified for a given route. The + // request is routed to one of the upstream clusters based on weights + // assigned to each cluster. + // Currently ClusterWeight only supports the name and weight fields. + api.v2.route.WeightedCluster weighted_clusters = 2; + } +} + +message MethodMatch { + // The parameter matching type. + message ParameterMatchSpecifier { + oneof parameter_match_specifier { + // If specified, header match will be performed based on the value of the header. + string exact_match = 3; + + // If specified, header match will be performed based on range. + // The rule will match if the request header value is within this range. + // The entire request header value must represent an integer in base 10 notation: consisting + // of an optional plus or minus sign followed by a sequence of digits. The rule will not match + // if the header value does not represent an integer. Match will fail for empty values, + // floating point numbers or if only a subsequence of the header value is an integer. + // + // Examples: + // + // * For range [-10,0), route will match for header value -1, but not for 0, + // "somestring", 10.9, "-1somestring" + type.Int64Range range_match = 4; + } + } + + // The name of the method. + type.matcher.StringMatcher name = 1; + + // Method parameter definition. + // The key is the parameter index, starting from 0. + // The value is the parameter matching type. + map params_match = 2; +} diff --git a/api/envoy/extensions/filters/network/jres_proxy/router/v3/BUILD b/api/envoy/extensions/filters/network/jres_proxy/router/v3/BUILD new file mode 100644 index 000000000000..9c5334e09a38 --- /dev/null +++ b/api/envoy/extensions/filters/network/jres_proxy/router/v3/BUILD @@ -0,0 +1,12 @@ +# 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/filter/jres/router/v2alpha1:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/network/jres_proxy/router/v3/router.proto b/api/envoy/extensions/filters/network/jres_proxy/router/v3/router.proto new file mode 100644 index 000000000000..214a28af8eda --- /dev/null +++ b/api/envoy/extensions/filters/network/jres_proxy/router/v3/router.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.jres_proxy.router.v3; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.jres_proxy.router.v3"; +option java_outer_classname = "RouterProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Router] +// Jres router :ref:`configuration overview `. + +message Router { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.jres.router.v2alpha1.Router"; +} diff --git a/api/envoy/extensions/filters/network/jres_proxy/v3/BUILD b/api/envoy/extensions/filters/network/jres_proxy/v3/BUILD new file mode 100644 index 000000000000..a53416f3e2a5 --- /dev/null +++ b/api/envoy/extensions/filters/network/jres_proxy/v3/BUILD @@ -0,0 +1,15 @@ +# 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/filter/network/jres_proxy/v2alpha1:pkg", + "//envoy/config/route/v3:pkg", + "//envoy/type/matcher/v3:pkg", + "//envoy/type/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/network/jres_proxy/v3/README.md b/api/envoy/extensions/filters/network/jres_proxy/v3/README.md new file mode 100644 index 000000000000..ac867ca252de --- /dev/null +++ b/api/envoy/extensions/filters/network/jres_proxy/v3/README.md @@ -0,0 +1 @@ +Protocol buffer definitions for the Jres proxy. diff --git a/api/envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.proto b/api/envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.proto new file mode 100644 index 000000000000..3ce8410f584a --- /dev/null +++ b/api/envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.jres_proxy.v3; + +import "envoy/extensions/filters/network/jres_proxy/v3/route.proto"; + +import "google/protobuf/any.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.jres_proxy.v3"; +option java_outer_classname = "JresProxyProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Jres Proxy] +// Jres Proxy :ref:`configuration overview `. +// [#extension: envoy.filters.network.jres_proxy] + +// Jres Protocol types supported by Envoy. +enum ProtocolType { + // the default protocol. + Jres = 0; +} + +// Jres Serialization types supported by Envoy. +enum SerializationType { + // the default serialization protocol. + Hessian2 = 0; +} + +// [#next-free-field: 6] +message JresProxy { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.JresProxy"; + + // The human readable prefix to use when emitting statistics. + string stat_prefix = 1 [(validate.rules).string = {min_len: 1}]; + + // Configure the protocol used. + ProtocolType protocol_type = 2 [(validate.rules).enum = {defined_only: true}]; + + // Configure the serialization protocol used. + SerializationType serialization_type = 3 [(validate.rules).enum = {defined_only: true}]; + + // The route table for the connection manager is static and is specified in this property. + repeated RouteConfiguration route_config = 4; + + // A list of individual Jres filters that make up the filter chain for requests made to the + // Jres proxy. Order matters as the filters are processed sequentially. For backwards + // compatibility, if no jres_filters are specified, a default Jres router filter + // (`envoy.filters.jres.router`) is used. + repeated JresFilter jres_filters = 5; +} + +// JresFilter configures a Jres filter. +message JresFilter { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.JresFilter"; + + // The name of the filter to instantiate. The name must match a supported + // filter. + string name = 1 [(validate.rules).string = {min_len: 1}]; + + // Filter specific configuration which depends on the filter being + // instantiated. See the supported filters for further documentation. + google.protobuf.Any config = 2; +} diff --git a/api/envoy/extensions/filters/network/jres_proxy/v3/route.proto b/api/envoy/extensions/filters/network/jres_proxy/v3/route.proto new file mode 100644 index 000000000000..37cc3e63dfaf --- /dev/null +++ b/api/envoy/extensions/filters/network/jres_proxy/v3/route.proto @@ -0,0 +1,121 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.jres_proxy.v3; + +import "envoy/config/route/v3/route_components.proto"; +import "envoy/type/matcher/v3/string.proto"; +import "envoy/type/v3/range.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.jres_proxy.v3"; +option java_outer_classname = "RouteProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Jres Proxy Route Configuration] +// Jres Proxy :ref:`configuration overview `. + +// [#next-free-field: 6] +message RouteConfiguration { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.RouteConfiguration"; + + // The name of the route configuration. Reserved for future use in asynchronous route discovery. + string name = 1; + + // The interface name of the service. + string interface = 2; + + // Which group does the interface belong to. + string group = 3; + + // The version number of the interface. + string version = 4; + + // The list of routes that will be matched, in order, against incoming requests. The first route + // that matches will be used. + repeated Route routes = 5; +} + +message Route { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.Route"; + + // Route matching parameters. + RouteMatch match = 1 [(validate.rules).message = {required: true}]; + + // Route request to some upstream cluster. + RouteAction route = 2 [(validate.rules).message = {required: true}]; +} + +message RouteMatch { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.RouteMatch"; + + // Method level routing matching. + MethodMatch method = 1; + + // Specifies a set of headers that the route should match on. The router will check the request’s + // headers against all the specified headers in the route config. A match will happen if all the + // headers in the route are present in the request with the same values (or based on presence if + // the value field is not in the config). + repeated config.route.v3.HeaderMatcher headers = 2; +} + +message RouteAction { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.RouteAction"; + + oneof cluster_specifier { + option (validate.required) = true; + + // Indicates the upstream cluster to which the request should be routed. + string cluster = 1; + + // Multiple upstream clusters can be specified for a given route. The + // request is routed to one of the upstream clusters based on weights + // assigned to each cluster. + // Currently ClusterWeight only supports the name and weight fields. + config.route.v3.WeightedCluster weighted_clusters = 2; + } +} + +message MethodMatch { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.MethodMatch"; + + // The parameter matching type. + message ParameterMatchSpecifier { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.MethodMatch.ParameterMatchSpecifier"; + + oneof parameter_match_specifier { + // If specified, header match will be performed based on the value of the header. + string exact_match = 3; + + // If specified, header match will be performed based on range. + // The rule will match if the request header value is within this range. + // The entire request header value must represent an integer in base 10 notation: consisting + // of an optional plus or minus sign followed by a sequence of digits. The rule will not match + // if the header value does not represent an integer. Match will fail for empty values, + // floating point numbers or if only a subsequence of the header value is an integer. + // + // Examples: + // + // * For range [-10,0), route will match for header value -1, but not for 0, + // "somestring", 10.9, "-1somestring" + type.v3.Int64Range range_match = 4; + } + } + + // The name of the method. + type.matcher.v3.StringMatcher name = 1; + + // Method parameter definition. + // The key is the parameter index, starting from 0. + // The value is the parameter matching type. + map params_match = 2; +} diff --git a/api/envoy/extensions/filters/network/jres_proxy/v4alpha/BUILD b/api/envoy/extensions/filters/network/jres_proxy/v4alpha/BUILD new file mode 100644 index 000000000000..e30e37d73218 --- /dev/null +++ b/api/envoy/extensions/filters/network/jres_proxy/v4alpha/BUILD @@ -0,0 +1,15 @@ +# 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/route/v4alpha:pkg", + "//envoy/extensions/filters/network/jres_proxy/v3:pkg", + "//envoy/type/matcher/v4alpha:pkg", + "//envoy/type/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/network/jres_proxy/v4alpha/jres_proxy.proto b/api/envoy/extensions/filters/network/jres_proxy/v4alpha/jres_proxy.proto new file mode 100644 index 000000000000..e818a4292c43 --- /dev/null +++ b/api/envoy/extensions/filters/network/jres_proxy/v4alpha/jres_proxy.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.jres_proxy.v4alpha; + +import "envoy/extensions/filters/network/jres_proxy/v4alpha/route.proto"; + +import "google/protobuf/any.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.jres_proxy.v4alpha"; +option java_outer_classname = "JresProxyProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: Jres Proxy] +// Jres Proxy :ref:`configuration overview `. +// [#extension: envoy.filters.network.jres_proxy] + +// Jres Protocol types supported by Envoy. +enum ProtocolType { + // the default protocol. + Jres = 0; +} + +// Jres Serialization types supported by Envoy. +enum SerializationType { + // the default serialization protocol. + Hessian2 = 0; +} + +// [#next-free-field: 6] +message JresProxy { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.JresProxy"; + + // The human readable prefix to use when emitting statistics. + string stat_prefix = 1 [(validate.rules).string = {min_len: 1}]; + + // Configure the protocol used. + ProtocolType protocol_type = 2 [(validate.rules).enum = {defined_only: true}]; + + // Configure the serialization protocol used. + SerializationType serialization_type = 3 [(validate.rules).enum = {defined_only: true}]; + + // The route table for the connection manager is static and is specified in this property. + repeated RouteConfiguration route_config = 4; + + // A list of individual Jres filters that make up the filter chain for requests made to the + // Jres proxy. Order matters as the filters are processed sequentially. For backwards + // compatibility, if no jres_filters are specified, a default Jres router filter + // (`envoy.filters.jres.router`) is used. + repeated JresFilter jres_filters = 5; +} + +// JresFilter configures a Jres filter. +message JresFilter { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.JresFilter"; + + // The name of the filter to instantiate. The name must match a supported + // filter. + string name = 1 [(validate.rules).string = {min_len: 1}]; + + // Filter specific configuration which depends on the filter being + // instantiated. See the supported filters for further documentation. + google.protobuf.Any config = 2; +} diff --git a/api/envoy/extensions/filters/network/jres_proxy/v4alpha/route.proto b/api/envoy/extensions/filters/network/jres_proxy/v4alpha/route.proto new file mode 100644 index 000000000000..cbcce7d9397c --- /dev/null +++ b/api/envoy/extensions/filters/network/jres_proxy/v4alpha/route.proto @@ -0,0 +1,121 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.jres_proxy.v4alpha; + +import "envoy/config/route/v4alpha/route_components.proto"; +import "envoy/type/matcher/v4alpha/string.proto"; +import "envoy/type/v3/range.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.jres_proxy.v4alpha"; +option java_outer_classname = "RouteProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: Jres Proxy Route Configuration] +// Jres Proxy :ref:`configuration overview `. + +// [#next-free-field: 6] +message RouteConfiguration { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.RouteConfiguration"; + + // The name of the route configuration. Reserved for future use in asynchronous route discovery. + string name = 1; + + // The interface name of the service. + string interface = 2; + + // Which group does the interface belong to. + string group = 3; + + // The version number of the interface. + string version = 4; + + // The list of routes that will be matched, in order, against incoming requests. The first route + // that matches will be used. + repeated Route routes = 5; +} + +message Route { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.Route"; + + // Route matching parameters. + RouteMatch match = 1 [(validate.rules).message = {required: true}]; + + // Route request to some upstream cluster. + RouteAction route = 2 [(validate.rules).message = {required: true}]; +} + +message RouteMatch { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.RouteMatch"; + + // Method level routing matching. + MethodMatch method = 1; + + // Specifies a set of headers that the route should match on. The router will check the request’s + // headers against all the specified headers in the route config. A match will happen if all the + // headers in the route are present in the request with the same values (or based on presence if + // the value field is not in the config). + repeated config.route.v4alpha.HeaderMatcher headers = 2; +} + +message RouteAction { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.RouteAction"; + + oneof cluster_specifier { + option (validate.required) = true; + + // Indicates the upstream cluster to which the request should be routed. + string cluster = 1; + + // Multiple upstream clusters can be specified for a given route. The + // request is routed to one of the upstream clusters based on weights + // assigned to each cluster. + // Currently ClusterWeight only supports the name and weight fields. + config.route.v4alpha.WeightedCluster weighted_clusters = 2; + } +} + +message MethodMatch { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.MethodMatch"; + + // The parameter matching type. + message ParameterMatchSpecifier { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.MethodMatch.ParameterMatchSpecifier"; + + oneof parameter_match_specifier { + // If specified, header match will be performed based on the value of the header. + string exact_match = 3; + + // If specified, header match will be performed based on range. + // The rule will match if the request header value is within this range. + // The entire request header value must represent an integer in base 10 notation: consisting + // of an optional plus or minus sign followed by a sequence of digits. The rule will not match + // if the header value does not represent an integer. Match will fail for empty values, + // floating point numbers or if only a subsequence of the header value is an integer. + // + // Examples: + // + // * For range [-10,0), route will match for header value -1, but not for 0, + // "somestring", 10.9, "-1somestring" + type.v3.Int64Range range_match = 4; + } + } + + // The name of the method. + type.matcher.v4alpha.StringMatcher name = 1; + + // Method parameter definition. + // The key is the parameter index, starting from 0. + // The value is the parameter matching type. + map params_match = 2; +} diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 3204d928f1a9..4446f90295b3 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -101,6 +101,8 @@ proto_library( "//envoy/extensions/filters/network/echo/v3:pkg", "//envoy/extensions/filters/network/ext_authz/v3:pkg", "//envoy/extensions/filters/network/http_connection_manager/v3:pkg", + "//envoy/extensions/filters/network/jres_proxy/router/v3:pkg", + "//envoy/extensions/filters/network/jres_proxy/v3:pkg", "//envoy/extensions/filters/network/kafka_broker/v3:pkg", "//envoy/extensions/filters/network/local_ratelimit/v3:pkg", "//envoy/extensions/filters/network/mongo_proxy/v3:pkg", @@ -223,6 +225,7 @@ proto_library( "//envoy/config/filter/http/squash/v2:pkg", "//envoy/config/filter/http/tap/v2alpha:pkg", "//envoy/config/filter/http/transcoder/v2:pkg", + "//envoy/config/filter/jres/router/v2alpha1:pkg", "//envoy/config/filter/listener/http_inspector/v2:pkg", "//envoy/config/filter/listener/original_dst/v2:pkg", "//envoy/config/filter/listener/original_src/v2alpha1:pkg", @@ -234,6 +237,7 @@ proto_library( "//envoy/config/filter/network/echo/v2:pkg", "//envoy/config/filter/network/ext_authz/v2:pkg", "//envoy/config/filter/network/http_connection_manager/v2:pkg", + "//envoy/config/filter/network/jres_proxy/v2alpha1:pkg", "//envoy/config/filter/network/kafka_broker/v2alpha1:pkg", "//envoy/config/filter/network/local_rate_limit/v2alpha:pkg", "//envoy/config/filter/network/mongo_proxy/v2:pkg", diff --git a/docs/root/_static/jres_proxy.svg b/docs/root/_static/jres_proxy.svg new file mode 100644 index 000000000000..60a9cfcade0f --- /dev/null +++ b/docs/root/_static/jres_proxy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/docs/root/api-v2/config/filter/filter.rst b/docs/root/api-v2/config/filter/filter.rst index e7b8d2ff6f8e..d59d3194edf9 100644 --- a/docs/root/api-v2/config/filter/filter.rst +++ b/docs/root/api-v2/config/filter/filter.rst @@ -12,4 +12,5 @@ Filters accesslog/v2/accesslog.proto fault/v2/fault.proto dubbo/dubbo + jres/jres thrift/thrift diff --git a/docs/root/api-v2/config/filter/jres/jres.rst b/docs/root/api-v2/config/filter/jres/jres.rst new file mode 100644 index 000000000000..ecfec1541454 --- /dev/null +++ b/docs/root/api-v2/config/filter/jres/jres.rst @@ -0,0 +1,8 @@ +Jres filters +============== + +.. toctree:: + :glob: + :maxdepth: 2 + + */v2alpha1/* diff --git a/docs/root/api-v3/config/filter/filter.rst b/docs/root/api-v3/config/filter/filter.rst index 4bf4fe9f245e..60d94d5d5cec 100644 --- a/docs/root/api-v3/config/filter/filter.rst +++ b/docs/root/api-v3/config/filter/filter.rst @@ -10,4 +10,5 @@ Filters udp/udp http/http dubbo/dubbo + jres/jres thrift/thrift diff --git a/docs/root/api-v3/config/filter/jres/jres.rst b/docs/root/api-v3/config/filter/jres/jres.rst new file mode 100644 index 000000000000..ecfec1541454 --- /dev/null +++ b/docs/root/api-v3/config/filter/jres/jres.rst @@ -0,0 +1,8 @@ +Jres filters +============== + +.. toctree:: + :glob: + :maxdepth: 2 + + */v2alpha1/* diff --git a/docs/root/configuration/listeners/network_filters/jres_proxy_filter.rst b/docs/root/configuration/listeners/network_filters/jres_proxy_filter.rst new file mode 100644 index 000000000000..3ecff799f25b --- /dev/null +++ b/docs/root/configuration/listeners/network_filters/jres_proxy_filter.rst @@ -0,0 +1,83 @@ +.. _config_network_filters_jres_proxy: + +Jres proxy +============ + +The jres proxy filter decodes the RPC protocol between jres clients +and servers. the decoded RPC information is converted to metadata. +the metadata includes the basic request ID, request type, serialization type, +and the required service name, method name, parameter name, +and parameter value for routing. + +* :ref:`v3 API reference ` +* This filter should be configured with the name *envoy.filters.network.jres_proxy*. + +.. _config_network_filters_jres_proxy_stats: + +Statistics +---------- + +Every configured jres proxy filter has statistics rooted at *jres..* with the +following statistics: + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + request, Counter, Total requests + request_twoway, Counter, Total twoway requests + request_oneway, Counter, Total oneway requests + request_event, Counter, Total event requests + request_decoding_error, Counter, Total decoding error requests + request_decoding_success, Counter, Total decoding success requests + request_active, Gauge, Total active requests + response, Counter, Total responses + response_success, Counter, Total success responses + response_error, Counter, Total responses that protocol parse error + response_error_caused_connection_close, Counter, Total responses that caused by the downstream connection close + response_business_exception, Counter, Total responses that the protocol contains exception information returned by the business layer + response_decoding_error, Counter, Total decoding error responses + response_decoding_success, Counter, Total decoding success responses + response_error, Counter, Total responses that protocol parse error + local_response_success, Counter, Total local responses + local_response_error, Counter, Total local responses that encoding error + local_response_business_exception, Counter, Total local responses that the protocol contains business exception + cx_destroy_local_with_active_rq, Counter, Connections destroyed locally with an active query + cx_destroy_remote_with_active_rq, Counter, Connections destroyed remotely with an active query + + +Implement custom filter based on the jres proxy filter +-------------------------------------------------------- + +If you want to implement a custom filter based on the jres protocol, +the jres proxy filter like HTTP also provides a very convenient way to expand, +the first step is to implement the DecoderFilter interface, and give the filter named, such as testFilter, +the second step is to add your configuration, configuration method refer to the following sample + +.. code-block:: yaml + + filter_chains: + - filters: + - name: envoy.filters.network.jres_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.jres_proxy.v3.JresProxy + stat_prefix: jres_incomming_stats + protocol_type: Jres + serialization_type: Hessian2 + route_config: + name: local_route + interface: org.apache.jres.demo.DemoService + routes: + - match: + method: + name: + exact: sayHello + route: + cluster: user_service_jres_server + jres_filters: + - name: envoy.filters.jres.testFilter + typed_config: + "@type": type.googleapis.com/google.protobuf.Struct + value: + name: test_service + - name: envoy.filters.jres.router diff --git a/docs/root/configuration/listeners/network_filters/network_filters.rst b/docs/root/configuration/listeners/network_filters/network_filters.rst index f75a0f9c0e61..8205cca8cc15 100644 --- a/docs/root/configuration/listeners/network_filters/network_filters.rst +++ b/docs/root/configuration/listeners/network_filters/network_filters.rst @@ -11,6 +11,7 @@ filters. :maxdepth: 2 dubbo_proxy_filter + jres_proxy_filter client_ssl_auth_filter echo_filter direct_response_filter diff --git a/docs/root/configuration/other_protocols/jres_filters/jres_filters.rst b/docs/root/configuration/other_protocols/jres_filters/jres_filters.rst new file mode 100644 index 000000000000..83f9dc3281e9 --- /dev/null +++ b/docs/root/configuration/other_protocols/jres_filters/jres_filters.rst @@ -0,0 +1,11 @@ +.. _config_jres_filters: + +Jres filters +=============== + +Envoy has the following builtin Jres filters. + +.. toctree:: + :maxdepth: 2 + + router_filter diff --git a/docs/root/configuration/other_protocols/jres_filters/router_filter.rst b/docs/root/configuration/other_protocols/jres_filters/router_filter.rst new file mode 100644 index 000000000000..e544408dcb04 --- /dev/null +++ b/docs/root/configuration/other_protocols/jres_filters/router_filter.rst @@ -0,0 +1,11 @@ +.. _config_jres_filters_router: + +Router +====== + +The router filter implements Jres forwarding. It will be used in almost all Jres proxying +scenarios. The filter's main job is to follow the instructions specified in the configured +:ref:`route table `. + +* :ref:`v3 API reference ` +* This filter should be configured with the name *envoy.filters.jres.router*. diff --git a/docs/root/configuration/other_protocols/other_protocols.rst b/docs/root/configuration/other_protocols/other_protocols.rst index 8ad84b892de1..734ccf865c8a 100644 --- a/docs/root/configuration/other_protocols/other_protocols.rst +++ b/docs/root/configuration/other_protocols/other_protocols.rst @@ -6,3 +6,4 @@ Other protocols thrift_filters/thrift_filters dubbo_filters/dubbo_filters + jres_filters/jres_filters diff --git a/docs/root/intro/deployment_types/deployment_types.rst b/docs/root/intro/deployment_types/deployment_types.rst index 118b3b4c40bc..1b578a1b33ee 100644 --- a/docs/root/intro/deployment_types/deployment_types.rst +++ b/docs/root/intro/deployment_types/deployment_types.rst @@ -12,3 +12,4 @@ types in increasing order of complexity. service_to_service front_proxy double_proxy + jres_proxy diff --git a/docs/root/intro/deployment_types/jres_proxy.rst b/docs/root/intro/deployment_types/jres_proxy.rst new file mode 100644 index 000000000000..6dce1eaf049b --- /dev/null +++ b/docs/root/intro/deployment_types/jres_proxy.rst @@ -0,0 +1,25 @@ +.. _deployment_type_jres_proxy: + +Service to service, front proxy, and jres proxy +------------------------------------------------- + +.. image:: /_static/jres_proxy.svg + :width: 70% + +The above diagram shows the :ref:`front proxy ` configuration alongside +another Envoy cluster running as a *jres proxy*. The idea behind the jres proxy is that it is +more efficient to terminate TLS and client connections as close as possible to the user (shorter +round trip times for the TLS handshake, faster TCP CWND expansion, less chance for packet loss, +etc.). Connections that terminate in the jres proxy are then multiplexed onto long lived HTTP/2 +connections running in the main data center. + +In the above diagram, the front Envoy proxy running in region 1 authenticates itself with the front +Envoy proxy running in region 2 via TLS mutual authentication and pinned certificates. This allows +the front Envoy instances running in region 2 to trust elements of the incoming requests that +ordinarily would not be trustable (such as the x-forwarded-for HTTP header). + +Configuration template +^^^^^^^^^^^^^^^^^^^^^^ + +The source distribution includes an example jres proxy configuration. See +:ref:`here ` for more information. diff --git a/generated_api_shadow/envoy/config/filter/jres/router/v2alpha1/BUILD b/generated_api_shadow/envoy/config/filter/jres/router/v2alpha1/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/jres/router/v2alpha1/BUILD @@ -0,0 +1,9 @@ +# 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 = ["@com_github_cncf_udpa//udpa/annotations:pkg"], +) diff --git a/generated_api_shadow/envoy/config/filter/jres/router/v2alpha1/router.proto b/generated_api_shadow/envoy/config/filter/jres/router/v2alpha1/router.proto new file mode 100644 index 000000000000..c4d31c800968 --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/jres/router/v2alpha1/router.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package envoy.config.filter.jres.router.v2alpha1; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.jres.router.v2alpha1"; +option java_outer_classname = "RouterProto"; +option java_multiple_files = true; +option (udpa.annotations.file_migrate).move_to_package = + "envoy.extensions.filters.network.jres_proxy.router.v3"; +option (udpa.annotations.file_status).package_version_status = FROZEN; + +// [#protodoc-title: Router] +// Jres router :ref:`configuration overview `. + +message Router { +} diff --git a/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/BUILD b/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/BUILD new file mode 100644 index 000000000000..5fe475a5dcf8 --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/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/api/v2/route:pkg", + "//envoy/type:pkg", + "//envoy/type/matcher:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/README.md b/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/README.md new file mode 100644 index 000000000000..ac867ca252de --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/README.md @@ -0,0 +1 @@ +Protocol buffer definitions for the Jres proxy. diff --git a/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/jres_proxy.proto b/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/jres_proxy.proto new file mode 100644 index 000000000000..d28c9be4457d --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/jres_proxy.proto @@ -0,0 +1,66 @@ +syntax = "proto3"; + +package envoy.config.filter.network.jres_proxy.v2alpha1; + +import "envoy/config/filter/network/jres_proxy/v2alpha1/route.proto"; + +import "google/protobuf/any.proto"; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.network.jres_proxy.v2alpha1"; +option java_outer_classname = "JresProxyProto"; +option java_multiple_files = true; +option (udpa.annotations.file_migrate).move_to_package = + "envoy.extensions.filters.network.jres_proxy.v3"; +option (udpa.annotations.file_status).package_version_status = FROZEN; + +// [#protodoc-title: Jres Proxy] +// Jres Proxy :ref:`configuration overview `. +// [#extension: envoy.filters.network.jres_proxy] + +// Jres Protocol types supported by Envoy. +enum ProtocolType { + // the default protocol. + Jres = 0; +} + +// Jres Serialization types supported by Envoy. +enum SerializationType { + // the default serialization protocol. + Hessian2 = 0; +} + +// [#next-free-field: 6] +message JresProxy { + // The human readable prefix to use when emitting statistics. + string stat_prefix = 1 [(validate.rules).string = {min_bytes: 1}]; + + // Configure the protocol used. + ProtocolType protocol_type = 2 [(validate.rules).enum = {defined_only: true}]; + + // Configure the serialization protocol used. + SerializationType serialization_type = 3 [(validate.rules).enum = {defined_only: true}]; + + // The route table for the connection manager is static and is specified in this property. + repeated RouteConfiguration route_config = 4; + + // A list of individual Jres filters that make up the filter chain for requests made to the + // Jres proxy. Order matters as the filters are processed sequentially. For backwards + // compatibility, if no jres_filters are specified, a default Jres router filter + // (`envoy.filters.jres.router`) is used. + repeated JresFilter jres_filters = 5; +} + +// JresFilter configures a Jres filter. +message JresFilter { + // The name of the filter to instantiate. The name must match a supported + // filter. + string name = 1 [(validate.rules).string = {min_bytes: 1}]; + + // Filter specific configuration which depends on the filter being + // instantiated. See the supported filters for further documentation. + google.protobuf.Any config = 2; +} diff --git a/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/route.proto b/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/route.proto new file mode 100644 index 000000000000..6b0d2ed0574c --- /dev/null +++ b/generated_api_shadow/envoy/config/filter/network/jres_proxy/v2alpha1/route.proto @@ -0,0 +1,105 @@ +syntax = "proto3"; + +package envoy.config.filter.network.jres_proxy.v2alpha1; + +import "envoy/api/v2/route/route_components.proto"; +import "envoy/type/matcher/string.proto"; +import "envoy/type/range.proto"; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.config.filter.network.jres_proxy.v2alpha1"; +option java_outer_classname = "RouteProto"; +option java_multiple_files = true; +option (udpa.annotations.file_migrate).move_to_package = + "envoy.extensions.filters.network.jres_proxy.v3"; +option (udpa.annotations.file_status).package_version_status = FROZEN; + +// [#protodoc-title: Jres Proxy Route Configuration] +// Jres Proxy :ref:`configuration overview `. + +// [#next-free-field: 6] +message RouteConfiguration { + // The name of the route configuration. Reserved for future use in asynchronous route discovery. + string name = 1; + + // The interface name of the service. + string interface = 2; + + // Which group does the interface belong to. + string group = 3; + + // The version number of the interface. + string version = 4; + + // The list of routes that will be matched, in order, against incoming requests. The first route + // that matches will be used. + repeated Route routes = 5; +} + +message Route { + // Route matching parameters. + RouteMatch match = 1 [(validate.rules).message = {required: true}]; + + // Route request to some upstream cluster. + RouteAction route = 2 [(validate.rules).message = {required: true}]; +} + +message RouteMatch { + // Method level routing matching. + MethodMatch method = 1; + + // Specifies a set of headers that the route should match on. The router will check the request’s + // headers against all the specified headers in the route config. A match will happen if all the + // headers in the route are present in the request with the same values (or based on presence if + // the value field is not in the config). + repeated api.v2.route.HeaderMatcher headers = 2; +} + +message RouteAction { + oneof cluster_specifier { + option (validate.required) = true; + + // Indicates the upstream cluster to which the request should be routed. + string cluster = 1; + + // Multiple upstream clusters can be specified for a given route. The + // request is routed to one of the upstream clusters based on weights + // assigned to each cluster. + // Currently ClusterWeight only supports the name and weight fields. + api.v2.route.WeightedCluster weighted_clusters = 2; + } +} + +message MethodMatch { + // The parameter matching type. + message ParameterMatchSpecifier { + oneof parameter_match_specifier { + // If specified, header match will be performed based on the value of the header. + string exact_match = 3; + + // If specified, header match will be performed based on range. + // The rule will match if the request header value is within this range. + // The entire request header value must represent an integer in base 10 notation: consisting + // of an optional plus or minus sign followed by a sequence of digits. The rule will not match + // if the header value does not represent an integer. Match will fail for empty values, + // floating point numbers or if only a subsequence of the header value is an integer. + // + // Examples: + // + // * For range [-10,0), route will match for header value -1, but not for 0, + // "somestring", 10.9, "-1somestring" + type.Int64Range range_match = 4; + } + } + + // The name of the method. + type.matcher.StringMatcher name = 1; + + // Method parameter definition. + // The key is the parameter index, starting from 0. + // The value is the parameter matching type. + map params_match = 2; +} diff --git a/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/router/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/router/v3/BUILD new file mode 100644 index 000000000000..9c5334e09a38 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/router/v3/BUILD @@ -0,0 +1,12 @@ +# 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/filter/jres/router/v2alpha1:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/router/v3/router.proto b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/router/v3/router.proto new file mode 100644 index 000000000000..214a28af8eda --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/router/v3/router.proto @@ -0,0 +1,19 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.jres_proxy.router.v3; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.jres_proxy.router.v3"; +option java_outer_classname = "RouterProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Router] +// Jres router :ref:`configuration overview `. + +message Router { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.jres.router.v2alpha1.Router"; +} diff --git a/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v3/BUILD new file mode 100644 index 000000000000..a53416f3e2a5 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v3/BUILD @@ -0,0 +1,15 @@ +# 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/filter/network/jres_proxy/v2alpha1:pkg", + "//envoy/config/route/v3:pkg", + "//envoy/type/matcher/v3:pkg", + "//envoy/type/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.proto b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.proto new file mode 100644 index 000000000000..3ce8410f584a --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.jres_proxy.v3; + +import "envoy/extensions/filters/network/jres_proxy/v3/route.proto"; + +import "google/protobuf/any.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.jres_proxy.v3"; +option java_outer_classname = "JresProxyProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Jres Proxy] +// Jres Proxy :ref:`configuration overview `. +// [#extension: envoy.filters.network.jres_proxy] + +// Jres Protocol types supported by Envoy. +enum ProtocolType { + // the default protocol. + Jres = 0; +} + +// Jres Serialization types supported by Envoy. +enum SerializationType { + // the default serialization protocol. + Hessian2 = 0; +} + +// [#next-free-field: 6] +message JresProxy { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.JresProxy"; + + // The human readable prefix to use when emitting statistics. + string stat_prefix = 1 [(validate.rules).string = {min_len: 1}]; + + // Configure the protocol used. + ProtocolType protocol_type = 2 [(validate.rules).enum = {defined_only: true}]; + + // Configure the serialization protocol used. + SerializationType serialization_type = 3 [(validate.rules).enum = {defined_only: true}]; + + // The route table for the connection manager is static and is specified in this property. + repeated RouteConfiguration route_config = 4; + + // A list of individual Jres filters that make up the filter chain for requests made to the + // Jres proxy. Order matters as the filters are processed sequentially. For backwards + // compatibility, if no jres_filters are specified, a default Jres router filter + // (`envoy.filters.jres.router`) is used. + repeated JresFilter jres_filters = 5; +} + +// JresFilter configures a Jres filter. +message JresFilter { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.JresFilter"; + + // The name of the filter to instantiate. The name must match a supported + // filter. + string name = 1 [(validate.rules).string = {min_len: 1}]; + + // Filter specific configuration which depends on the filter being + // instantiated. See the supported filters for further documentation. + google.protobuf.Any config = 2; +} diff --git a/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v3/route.proto b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v3/route.proto new file mode 100644 index 000000000000..37cc3e63dfaf --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v3/route.proto @@ -0,0 +1,121 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.jres_proxy.v3; + +import "envoy/config/route/v3/route_components.proto"; +import "envoy/type/matcher/v3/string.proto"; +import "envoy/type/v3/range.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.jres_proxy.v3"; +option java_outer_classname = "RouteProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Jres Proxy Route Configuration] +// Jres Proxy :ref:`configuration overview `. + +// [#next-free-field: 6] +message RouteConfiguration { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.RouteConfiguration"; + + // The name of the route configuration. Reserved for future use in asynchronous route discovery. + string name = 1; + + // The interface name of the service. + string interface = 2; + + // Which group does the interface belong to. + string group = 3; + + // The version number of the interface. + string version = 4; + + // The list of routes that will be matched, in order, against incoming requests. The first route + // that matches will be used. + repeated Route routes = 5; +} + +message Route { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.Route"; + + // Route matching parameters. + RouteMatch match = 1 [(validate.rules).message = {required: true}]; + + // Route request to some upstream cluster. + RouteAction route = 2 [(validate.rules).message = {required: true}]; +} + +message RouteMatch { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.RouteMatch"; + + // Method level routing matching. + MethodMatch method = 1; + + // Specifies a set of headers that the route should match on. The router will check the request’s + // headers against all the specified headers in the route config. A match will happen if all the + // headers in the route are present in the request with the same values (or based on presence if + // the value field is not in the config). + repeated config.route.v3.HeaderMatcher headers = 2; +} + +message RouteAction { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.RouteAction"; + + oneof cluster_specifier { + option (validate.required) = true; + + // Indicates the upstream cluster to which the request should be routed. + string cluster = 1; + + // Multiple upstream clusters can be specified for a given route. The + // request is routed to one of the upstream clusters based on weights + // assigned to each cluster. + // Currently ClusterWeight only supports the name and weight fields. + config.route.v3.WeightedCluster weighted_clusters = 2; + } +} + +message MethodMatch { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.MethodMatch"; + + // The parameter matching type. + message ParameterMatchSpecifier { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.network.jres_proxy.v2alpha1.MethodMatch.ParameterMatchSpecifier"; + + oneof parameter_match_specifier { + // If specified, header match will be performed based on the value of the header. + string exact_match = 3; + + // If specified, header match will be performed based on range. + // The rule will match if the request header value is within this range. + // The entire request header value must represent an integer in base 10 notation: consisting + // of an optional plus or minus sign followed by a sequence of digits. The rule will not match + // if the header value does not represent an integer. Match will fail for empty values, + // floating point numbers or if only a subsequence of the header value is an integer. + // + // Examples: + // + // * For range [-10,0), route will match for header value -1, but not for 0, + // "somestring", 10.9, "-1somestring" + type.v3.Int64Range range_match = 4; + } + } + + // The name of the method. + type.matcher.v3.StringMatcher name = 1; + + // Method parameter definition. + // The key is the parameter index, starting from 0. + // The value is the parameter matching type. + map params_match = 2; +} diff --git a/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v4alpha/BUILD b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v4alpha/BUILD new file mode 100644 index 000000000000..e30e37d73218 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v4alpha/BUILD @@ -0,0 +1,15 @@ +# 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/route/v4alpha:pkg", + "//envoy/extensions/filters/network/jres_proxy/v3:pkg", + "//envoy/type/matcher/v4alpha:pkg", + "//envoy/type/v3:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v4alpha/jres_proxy.proto b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v4alpha/jres_proxy.proto new file mode 100644 index 000000000000..e818a4292c43 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v4alpha/jres_proxy.proto @@ -0,0 +1,70 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.jres_proxy.v4alpha; + +import "envoy/extensions/filters/network/jres_proxy/v4alpha/route.proto"; + +import "google/protobuf/any.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.jres_proxy.v4alpha"; +option java_outer_classname = "JresProxyProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: Jres Proxy] +// Jres Proxy :ref:`configuration overview `. +// [#extension: envoy.filters.network.jres_proxy] + +// Jres Protocol types supported by Envoy. +enum ProtocolType { + // the default protocol. + Jres = 0; +} + +// Jres Serialization types supported by Envoy. +enum SerializationType { + // the default serialization protocol. + Hessian2 = 0; +} + +// [#next-free-field: 6] +message JresProxy { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.JresProxy"; + + // The human readable prefix to use when emitting statistics. + string stat_prefix = 1 [(validate.rules).string = {min_len: 1}]; + + // Configure the protocol used. + ProtocolType protocol_type = 2 [(validate.rules).enum = {defined_only: true}]; + + // Configure the serialization protocol used. + SerializationType serialization_type = 3 [(validate.rules).enum = {defined_only: true}]; + + // The route table for the connection manager is static and is specified in this property. + repeated RouteConfiguration route_config = 4; + + // A list of individual Jres filters that make up the filter chain for requests made to the + // Jres proxy. Order matters as the filters are processed sequentially. For backwards + // compatibility, if no jres_filters are specified, a default Jres router filter + // (`envoy.filters.jres.router`) is used. + repeated JresFilter jres_filters = 5; +} + +// JresFilter configures a Jres filter. +message JresFilter { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.JresFilter"; + + // The name of the filter to instantiate. The name must match a supported + // filter. + string name = 1 [(validate.rules).string = {min_len: 1}]; + + // Filter specific configuration which depends on the filter being + // instantiated. See the supported filters for further documentation. + google.protobuf.Any config = 2; +} diff --git a/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v4alpha/route.proto b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v4alpha/route.proto new file mode 100644 index 000000000000..cbcce7d9397c --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/jres_proxy/v4alpha/route.proto @@ -0,0 +1,121 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.jres_proxy.v4alpha; + +import "envoy/config/route/v4alpha/route_components.proto"; +import "envoy/type/matcher/v4alpha/string.proto"; +import "envoy/type/v3/range.proto"; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.jres_proxy.v4alpha"; +option java_outer_classname = "RouteProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: Jres Proxy Route Configuration] +// Jres Proxy :ref:`configuration overview `. + +// [#next-free-field: 6] +message RouteConfiguration { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.RouteConfiguration"; + + // The name of the route configuration. Reserved for future use in asynchronous route discovery. + string name = 1; + + // The interface name of the service. + string interface = 2; + + // Which group does the interface belong to. + string group = 3; + + // The version number of the interface. + string version = 4; + + // The list of routes that will be matched, in order, against incoming requests. The first route + // that matches will be used. + repeated Route routes = 5; +} + +message Route { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.Route"; + + // Route matching parameters. + RouteMatch match = 1 [(validate.rules).message = {required: true}]; + + // Route request to some upstream cluster. + RouteAction route = 2 [(validate.rules).message = {required: true}]; +} + +message RouteMatch { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.RouteMatch"; + + // Method level routing matching. + MethodMatch method = 1; + + // Specifies a set of headers that the route should match on. The router will check the request’s + // headers against all the specified headers in the route config. A match will happen if all the + // headers in the route are present in the request with the same values (or based on presence if + // the value field is not in the config). + repeated config.route.v4alpha.HeaderMatcher headers = 2; +} + +message RouteAction { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.RouteAction"; + + oneof cluster_specifier { + option (validate.required) = true; + + // Indicates the upstream cluster to which the request should be routed. + string cluster = 1; + + // Multiple upstream clusters can be specified for a given route. The + // request is routed to one of the upstream clusters based on weights + // assigned to each cluster. + // Currently ClusterWeight only supports the name and weight fields. + config.route.v4alpha.WeightedCluster weighted_clusters = 2; + } +} + +message MethodMatch { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.MethodMatch"; + + // The parameter matching type. + message ParameterMatchSpecifier { + option (udpa.annotations.versioning).previous_message_type = + "envoy.extensions.filters.network.jres_proxy.v3.MethodMatch.ParameterMatchSpecifier"; + + oneof parameter_match_specifier { + // If specified, header match will be performed based on the value of the header. + string exact_match = 3; + + // If specified, header match will be performed based on range. + // The rule will match if the request header value is within this range. + // The entire request header value must represent an integer in base 10 notation: consisting + // of an optional plus or minus sign followed by a sequence of digits. The rule will not match + // if the header value does not represent an integer. Match will fail for empty values, + // floating point numbers or if only a subsequence of the header value is an integer. + // + // Examples: + // + // * For range [-10,0), route will match for header value -1, but not for 0, + // "somestring", 10.9, "-1somestring" + type.v3.Int64Range range_match = 4; + } + } + + // The name of the method. + type.matcher.v4alpha.StringMatcher name = 1; + + // Method parameter definition. + // The key is the parameter index, starting from 0. + // The value is the parameter matching type. + map params_match = 2; +} diff --git a/source/common/common/logger.h b/source/common/common/logger.h index 29453c290ae7..e0bcd1a9d63d 100644 --- a/source/common/common/logger.h +++ b/source/common/common/logger.h @@ -38,6 +38,7 @@ namespace Logger { FUNCTION(conn_handler) \ FUNCTION(decompression) \ FUNCTION(dubbo) \ + FUNCTION(jres) \ FUNCTION(envoy_bug) \ FUNCTION(ext_authz) \ FUNCTION(rocketmq) \ diff --git a/source/extensions/extensions_build_config.bzl b/source/extensions/extensions_build_config.bzl index 1fa9f6293ea7..b90d4f630cbb 100644 --- a/source/extensions/extensions_build_config.bzl +++ b/source/extensions/extensions_build_config.bzl @@ -107,6 +107,7 @@ EXTENSIONS = { "envoy.filters.network.client_ssl_auth": "//source/extensions/filters/network/client_ssl_auth:config", "envoy.filters.network.direct_response": "//source/extensions/filters/network/direct_response:config", "envoy.filters.network.dubbo_proxy": "//source/extensions/filters/network/dubbo_proxy:config", + "envoy.filters.network.jres_proxy": "//source/extensions/filters/network/jres_proxy:config", "envoy.filters.network.echo": "//source/extensions/filters/network/echo:config", "envoy.filters.network.ext_authz": "//source/extensions/filters/network/ext_authz:config", "envoy.filters.network.http_connection_manager": "//source/extensions/filters/network/http_connection_manager:config", diff --git a/source/extensions/filters/network/jres_proxy/BUILD b/source/extensions/filters/network/jres_proxy/BUILD new file mode 100644 index 000000000000..3dad63f244b0 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/BUILD @@ -0,0 +1,235 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_extension", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "buffer_helper_lib", + srcs = ["buffer_helper.cc"], + hdrs = ["buffer_helper.h"], + deps = [ + "//source/common/buffer:buffer_lib", + "//source/common/common:assert_lib", + "//source/common/common:byte_order_lib", + ], +) + +envoy_cc_library( + name = "hessian_utils_lib", + srcs = ["hessian_utils.cc"], + hdrs = ["hessian_utils.h"], + deps = [ + ":buffer_helper_lib", + "//source/common/singleton:const_singleton", + ], +) + +envoy_cc_library( + name = "protocol_interface", + hdrs = ["protocol.h"], + deps = [ + ":buffer_helper_lib", + ":message_lib", + ":metadata_lib", + ":serializer_interface", + "//include/envoy/config:typed_config_interface", + "//source/common/common:assert_lib", + "//source/common/config:utility_lib", + "//source/common/singleton:const_singleton", + ], +) + +envoy_cc_library( + name = "jres_protocol_impl_lib", + srcs = ["jres_protocol_impl.cc"], + hdrs = ["jres_protocol_impl.h"], + deps = [ + ":protocol_interface", + "//include/envoy/buffer:buffer_interface", + "//source/common/singleton:const_singleton", + ], +) + +envoy_cc_library( + name = "serializer_interface", + srcs = ["serializer_impl.cc"], + hdrs = [ + "protocol_constants.h", + "serializer.h", + "serializer_impl.h", + ], + deps = [ + ":message_lib", + ":metadata_lib", + "//include/envoy/buffer:buffer_interface", + "//include/envoy/config:typed_config_interface", + "//source/common/common:assert_lib", + "//source/common/config:utility_lib", + "//source/common/singleton:const_singleton", + ], +) + +envoy_cc_library( + name = "jres_hessian2_serializer_impl_lib", + srcs = ["jres_hessian2_serializer_impl.cc"], + hdrs = [ + "jres_hessian2_serializer_impl.h", + ], + deps = [ + ":buffer_helper_lib", + ":hessian_utils_lib", + ":serializer_interface", + "//source/common/singleton:const_singleton", + ], +) + +envoy_cc_library( + name = "decoder_lib", + srcs = ["decoder.cc"], + hdrs = ["decoder.h"], + deps = [ + ":decoder_events_lib", + ":jres_hessian2_serializer_impl_lib", + ":jres_protocol_impl_lib", + "//source/common/buffer:buffer_lib", + "//source/common/common:logger_lib", + ], +) + +envoy_cc_extension( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + security_posture = "requires_trusted_downstream_and_upstream", + status = "alpha", + deps = [ + ":conn_manager_lib", + "//include/envoy/registry", + "//include/envoy/stats:stats_interface", + "//include/envoy/stats:stats_macros", + "//source/common/common:utility_lib", + "//source/common/config:utility_lib", + "//source/extensions/filters/network:well_known_names", + "//source/extensions/filters/network/common:factory_base_lib", + "//source/extensions/filters/network/jres_proxy/filters:factory_base_lib", + "//source/extensions/filters/network/jres_proxy/filters:well_known_names", + "//source/extensions/filters/network/jres_proxy/router:config", + "//source/extensions/filters/network/jres_proxy/router:route_matcher", + "//source/extensions/filters/network/jres_proxy/router:router_lib", + "@envoy_api//envoy/extensions/filters/network/jres_proxy/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "metadata_lib", + hdrs = ["metadata.h"], + external_deps = ["abseil_optional"], + deps = [ + ":message_lib", + "//source/common/buffer:buffer_lib", + "//source/common/http:header_map_lib", + ], +) + +envoy_cc_library( + name = "message_lib", + hdrs = [ + "message.h", + "message_impl.h", + ], + deps = [ + "//source/common/buffer:buffer_lib", + ], +) + +envoy_cc_library( + name = "decoder_events_lib", + hdrs = ["decoder_event_handler.h"], + deps = [ + ":metadata_lib", + "//include/envoy/network:connection_interface", + "//include/envoy/network:filter_interface", + "//source/common/buffer:buffer_lib", + ], +) + +envoy_cc_library( + name = "stats_lib", + hdrs = ["stats.h"], + deps = [ + "//include/envoy/stats:stats_interface", + "//include/envoy/stats:stats_macros", + ], +) + +envoy_cc_library( + name = "app_exception_lib", + srcs = ["app_exception.cc"], + hdrs = ["app_exception.h"], + deps = [ + ":message_lib", + ":metadata_lib", + ":protocol_interface", + ":serializer_interface", + "//include/envoy/buffer:buffer_interface", + "//source/common/buffer:buffer_lib", + "//source/extensions/filters/network/jres_proxy/filters:filter_interface", + ], +) + +envoy_cc_library( + name = "heartbeat_response_lib", + srcs = ["heartbeat_response.cc"], + hdrs = ["heartbeat_response.h"], + deps = [ + ":metadata_lib", + ":protocol_interface", + ":serializer_interface", + "//include/envoy/buffer:buffer_interface", + "//source/extensions/filters/network/jres_proxy/filters:filter_interface", + ], +) + +envoy_cc_library( + name = "conn_manager_lib", + srcs = [ + "active_message.cc", + "conn_manager.cc", + ], + hdrs = [ + "active_message.h", + "conn_manager.h", + ], + deps = [ + ":app_exception_lib", + ":decoder_events_lib", + ":decoder_lib", + ":heartbeat_response_lib", + ":jres_hessian2_serializer_impl_lib", + ":jres_protocol_impl_lib", + ":stats_lib", + "//include/envoy/event:deferred_deletable", + "//include/envoy/event:dispatcher_interface", + "//include/envoy/network:connection_interface", + "//include/envoy/network:filter_interface", + "//include/envoy/stats:stats_interface", + "//include/envoy/stats:timespan_interface", + "//source/common/buffer:buffer_lib", + "//source/common/buffer:watermark_buffer_lib", + "//source/common/common:assert_lib", + "//source/common/common:linked_object", + "//source/common/common:logger_lib", + "//source/common/network:filter_lib", + "//source/common/stats:timespan_lib", + "//source/common/stream_info:stream_info_lib", + "//source/extensions/filters/network/jres_proxy/filters:filter_interface", + "//source/extensions/filters/network/jres_proxy/router:router_interface", + "@envoy_api//envoy/extensions/filters/network/jres_proxy/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/network/jres_proxy/active_message.cc b/source/extensions/filters/network/jres_proxy/active_message.cc new file mode 100644 index 000000000000..c2c366181b2e --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/active_message.cc @@ -0,0 +1,478 @@ +#include "extensions/filters/network/jres_proxy/active_message.h" + +#include "common/stats/timespan_impl.h" + +#include "extensions/filters/network/jres_proxy/app_exception.h" +#include "extensions/filters/network/jres_proxy/conn_manager.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +// class ActiveResponseDecoder +ActiveResponseDecoder::ActiveResponseDecoder(ActiveMessage& parent, JresFilterStats& stats, + Network::Connection& connection, + ProtocolPtr&& protocol) + : parent_(parent), stats_(stats), response_connection_(connection), + protocol_(std::move(protocol)), + decoder_(std::make_unique(*protocol_, *this)), complete_(false), + response_status_(JresFilters::UpstreamResponseStatus::MoreData) {} + +JresFilters::UpstreamResponseStatus ActiveResponseDecoder::onData(Buffer::Instance& data) { + ENVOY_LOG(debug, "jres response: the received reply data length is {}", data.length()); + + bool underflow = false; + decoder_->onData(data, underflow); + ASSERT(complete_ || underflow); + + return response_status_; +} + +void ActiveResponseDecoder::onStreamDecoded(MessageMetadataSharedPtr metadata, + ContextSharedPtr ctx) { + ASSERT(metadata->messageType() == MessageType::Response || + metadata->messageType() == MessageType::Exception); + ASSERT(metadata->hasResponseStatus()); + + metadata_ = metadata; + if (applyMessageEncodedFilters(metadata, ctx) != FilterStatus::Continue) { + response_status_ = JresFilters::UpstreamResponseStatus::Complete; + return; + } + + if (response_connection_.state() != Network::Connection::State::Open) { + throw DownstreamConnectionCloseException("Downstream has closed or closing"); + } + + response_connection_.write(ctx->messageOriginData(), false); + ENVOY_LOG(debug, + "jres response: the upstream response message has been forwarded to the downstream"); + + stats_.response_.inc(); + stats_.response_decoding_success_.inc(); + if (metadata->messageType() == MessageType::Exception) { + stats_.response_business_exception_.inc(); + } + + switch (metadata->responseStatus()) { + case ResponseStatus::Ok: + stats_.response_success_.inc(); + break; + default: + stats_.response_error_.inc(); + ENVOY_LOG(error, "jres response status: {}", static_cast(metadata->responseStatus())); + break; + } + + complete_ = true; + response_status_ = JresFilters::UpstreamResponseStatus::Complete; + + ENVOY_LOG(debug, "jres response: complete processing of upstream response messages, id is {}", + metadata->requestId()); +} + +FilterStatus ActiveResponseDecoder::applyMessageEncodedFilters(MessageMetadataSharedPtr metadata, + ContextSharedPtr ctx) { + parent_.encoder_filter_action_ = [metadata, + ctx](JresFilters::EncoderFilter* filter) -> FilterStatus { + return filter->onMessageEncoded(metadata, ctx); + }; + + auto status = parent_.applyEncoderFilters( + nullptr, ActiveMessage::FilterIterationStartState::CanStartFromCurrent); + switch (status) { + case FilterStatus::StopIteration: + break; + case FilterStatus::Retry: + response_status_ = JresFilters::UpstreamResponseStatus::Retry; + decoder_->reset(); + break; + default: + ASSERT(FilterStatus::Continue == status); + break; + } + + return status; +} + +// class ActiveMessageFilterBase +uint64_t ActiveMessageFilterBase::requestId() const { return parent_.requestId(); } + +uint64_t ActiveMessageFilterBase::streamId() const { return parent_.streamId(); } + +const Network::Connection* ActiveMessageFilterBase::connection() const { + return parent_.connection(); +} + +Router::RouteConstSharedPtr ActiveMessageFilterBase::route() { return parent_.route(); } + +SerializationType ActiveMessageFilterBase::serializationType() const { + return parent_.serializationType(); +} + +ProtocolType ActiveMessageFilterBase::protocolType() const { return parent_.protocolType(); } + +Event::Dispatcher& ActiveMessageFilterBase::dispatcher() { return parent_.dispatcher(); } + +void ActiveMessageFilterBase::resetStream() { parent_.resetStream(); } + +StreamInfo::StreamInfo& ActiveMessageFilterBase::streamInfo() { return parent_.streamInfo(); } + +// class ActiveMessageDecoderFilter +ActiveMessageDecoderFilter::ActiveMessageDecoderFilter(ActiveMessage& parent, + JresFilters::DecoderFilterSharedPtr filter, + bool dual_filter) + : ActiveMessageFilterBase(parent, dual_filter), handle_(filter) {} + +void ActiveMessageDecoderFilter::continueDecoding() { + ASSERT(parent_.context()); + auto state = ActiveMessage::FilterIterationStartState::AlwaysStartFromNext; + if (0 != parent_.context()->messageOriginData().length()) { + state = ActiveMessage::FilterIterationStartState::CanStartFromCurrent; + ENVOY_LOG(warn, "The original message data is not consumed, triggering the decoder filter from " + "the current location"); + } + const FilterStatus status = parent_.applyDecoderFilters(this, state); + if (status == FilterStatus::Continue) { + ENVOY_LOG(debug, "jres response: start upstream"); + // All filters have been executed for the current decoder state. + if (parent_.pendingStreamDecoded()) { + // If the filter stack was paused during messageEnd, handle end-of-request details. + parent_.finalizeRequest(); + } + parent_.continueDecoding(); + } +} + +void ActiveMessageDecoderFilter::sendLocalReply(const JresFilters::DirectResponse& response, + bool end_stream) { + parent_.sendLocalReply(response, end_stream); +} + +void ActiveMessageDecoderFilter::startUpstreamResponse() { parent_.startUpstreamResponse(); } + +JresFilters::UpstreamResponseStatus +ActiveMessageDecoderFilter::upstreamData(Buffer::Instance& buffer) { + return parent_.upstreamData(buffer); +} + +void ActiveMessageDecoderFilter::resetDownstreamConnection() { + parent_.resetDownstreamConnection(); +} + +// class ActiveMessageEncoderFilter +ActiveMessageEncoderFilter::ActiveMessageEncoderFilter(ActiveMessage& parent, + JresFilters::EncoderFilterSharedPtr filter, + bool dual_filter) + : ActiveMessageFilterBase(parent, dual_filter), handle_(filter) {} + +void ActiveMessageEncoderFilter::continueEncoding() { + ASSERT(parent_.context()); + auto state = ActiveMessage::FilterIterationStartState::AlwaysStartFromNext; + if (0 != parent_.context()->messageOriginData().length()) { + state = ActiveMessage::FilterIterationStartState::CanStartFromCurrent; + ENVOY_LOG(warn, "The original message data is not consumed, triggering the encoder filter from " + "the current location"); + } + const FilterStatus status = parent_.applyEncoderFilters(this, state); + if (FilterStatus::Continue == status) { + ENVOY_LOG(debug, "All encoding filters have been executed"); + } +} + +// class ActiveMessage +ActiveMessage::ActiveMessage(ConnectionManager& parent) + : parent_(parent), request_timer_(std::make_unique( + parent_.stats().request_time_ms_, parent.timeSystem())), + request_id_(-1), stream_id_(parent.randomGenerator().random()), + stream_info_(parent.timeSystem(), parent_.connection().addressProviderSharedPtr()), + pending_stream_decoded_(false), local_response_sent_(false) { + parent_.stats().request_active_.inc(); +} + +ActiveMessage::~ActiveMessage() { + parent_.stats().request_active_.dec(); + request_timer_->complete(); + for (auto& filter : decoder_filters_) { + ENVOY_LOG(debug, "destroy decoder filter"); + filter->handler()->onDestroy(); + } + + for (auto& filter : encoder_filters_) { + // Do not call on destroy twice for dual registered filters. + if (!filter->dual_filter_) { + ENVOY_LOG(debug, "destroy encoder filter"); + filter->handler()->onDestroy(); + } + } +} + +std::list::iterator +ActiveMessage::commonEncodePrefix(ActiveMessageEncoderFilter* filter, + FilterIterationStartState state) { + // Only do base state setting on the initial call. Subsequent calls for filtering do not touch + // the base state. + if (filter == nullptr) { + // ASSERT(!state_.local_complete_); + // state_.local_complete_ = end_stream; + return encoder_filters_.begin(); + } + + if (state == FilterIterationStartState::CanStartFromCurrent) { + // The filter iteration has been stopped for all frame types, and now the iteration continues. + // The current filter's encoding callback has not be called. Call it now. + return filter->entry(); + } + return std::next(filter->entry()); +} + +std::list::iterator +ActiveMessage::commonDecodePrefix(ActiveMessageDecoderFilter* filter, + FilterIterationStartState state) { + if (!filter) { + return decoder_filters_.begin(); + } + if (state == FilterIterationStartState::CanStartFromCurrent) { + // The filter iteration has been stopped for all frame types, and now the iteration continues. + // The current filter's callback function has not been called. Call it now. + return filter->entry(); + } + return std::next(filter->entry()); +} + +void ActiveMessage::onStreamDecoded(MessageMetadataSharedPtr metadata, ContextSharedPtr ctx) { + parent_.stats().request_decoding_success_.inc(); + + metadata_ = metadata; + context_ = ctx; + filter_action_ = [metadata, ctx](JresFilters::DecoderFilter* filter) -> FilterStatus { + return filter->onMessageDecoded(metadata, ctx); + }; + + auto status = applyDecoderFilters(nullptr, FilterIterationStartState::CanStartFromCurrent); + if (status == FilterStatus::StopIteration) { + ENVOY_LOG(debug, "jres request: stop calling decoder filter, id is {}", metadata->requestId()); + pending_stream_decoded_ = true; + return; + } + + finalizeRequest(); + + ENVOY_LOG(debug, "jres request: complete processing of downstream request messages, id is {}", + metadata->requestId()); +} + +void ActiveMessage::finalizeRequest() { + pending_stream_decoded_ = false; + parent_.stats().request_.inc(); + bool is_one_way = false; + switch (metadata_->messageType()) { + case MessageType::Request: + parent_.stats().request_twoway_.inc(); + break; + case MessageType::Oneway: + parent_.stats().request_oneway_.inc(); + is_one_way = true; + break; + default: + break; + } + + if (local_response_sent_ || is_one_way) { + parent_.deferredMessage(*this); + } +} + +void ActiveMessage::createFilterChain() { + parent_.config().filterFactory().createFilterChain(*this); +} + +JresProxy::Router::RouteConstSharedPtr ActiveMessage::route() { + if (cached_route_) { + return cached_route_.value(); + } + + if (metadata_ != nullptr) { + JresProxy::Router::RouteConstSharedPtr route = + parent_.config().routerConfig().route(*metadata_, stream_id_); + cached_route_ = route; + return cached_route_.value(); + } + + return nullptr; +} + +FilterStatus ActiveMessage::applyDecoderFilters(ActiveMessageDecoderFilter* filter, + FilterIterationStartState state) { + ASSERT(filter_action_ != nullptr); + if (!local_response_sent_) { + for (auto entry = commonDecodePrefix(filter, state); entry != decoder_filters_.end(); entry++) { + const FilterStatus status = filter_action_((*entry)->handler().get()); + if (local_response_sent_) { + break; + } + + if (status != FilterStatus::Continue) { + return status; + } + } + } + + filter_action_ = nullptr; + + return FilterStatus::Continue; +} + +FilterStatus ActiveMessage::applyEncoderFilters(ActiveMessageEncoderFilter* filter, + FilterIterationStartState state) { + ASSERT(encoder_filter_action_ != nullptr); + + if (!local_response_sent_) { + for (auto entry = commonEncodePrefix(filter, state); entry != encoder_filters_.end(); entry++) { + const FilterStatus status = encoder_filter_action_((*entry)->handler().get()); + if (local_response_sent_) { + break; + } + + if (status != FilterStatus::Continue) { + return status; + } + } + } + + encoder_filter_action_ = nullptr; + + return FilterStatus::Continue; +} + +void ActiveMessage::sendLocalReply(const JresFilters::DirectResponse& response, bool end_stream) { + ASSERT(metadata_); + metadata_->setRequestId(request_id_); + parent_.sendLocalReply(*metadata_, response, end_stream); + + if (end_stream) { + return; + } + + local_response_sent_ = true; +} + +void ActiveMessage::startUpstreamResponse() { + ENVOY_LOG(debug, "jres response: start upstream"); + + ASSERT(response_decoder_ == nullptr); + + auto protocol = + NamedProtocolConfigFactory::getFactory(protocolType()).createProtocol(serializationType()); + + // Create a response message decoder. + response_decoder_ = std::make_unique( + *this, parent_.stats(), parent_.connection(), std::move(protocol)); +} + +JresFilters::UpstreamResponseStatus ActiveMessage::upstreamData(Buffer::Instance& buffer) { + ASSERT(response_decoder_ != nullptr); + + try { + auto status = response_decoder_->onData(buffer); + if (status == JresFilters::UpstreamResponseStatus::Complete) { + if (requestId() != response_decoder_->requestId()) { + throw EnvoyException(fmt::format("jres response: request ID is not equal, {}:{}", + requestId(), response_decoder_->requestId())); + } + + // Completed upstream response. + parent_.deferredMessage(*this); + } else if (status == JresFilters::UpstreamResponseStatus::Retry) { + response_decoder_.reset(); + } + + return status; + } catch (const DownstreamConnectionCloseException& ex) { + ENVOY_CONN_LOG(error, "jres response: exception ({})", parent_.connection(), ex.what()); + onReset(); + parent_.stats().response_error_caused_connection_close_.inc(); + return JresFilters::UpstreamResponseStatus::Reset; + } catch (const EnvoyException& ex) { + ENVOY_CONN_LOG(error, "jres response: exception ({})", parent_.connection(), ex.what()); + parent_.stats().response_decoding_error_.inc(); + + onError(ex.what()); + return JresFilters::UpstreamResponseStatus::Reset; + } +} + +void ActiveMessage::resetDownstreamConnection() { + parent_.connection().close(Network::ConnectionCloseType::NoFlush); +} + +void ActiveMessage::resetStream() { parent_.deferredMessage(*this); } + +uint64_t ActiveMessage::requestId() const { + return metadata_ != nullptr ? metadata_->requestId() : 0; +} + +uint64_t ActiveMessage::streamId() const { return stream_id_; } + +void ActiveMessage::continueDecoding() { parent_.continueDecoding(); } + +SerializationType ActiveMessage::serializationType() const { + return parent_.downstreamSerializationType(); +} + +ProtocolType ActiveMessage::protocolType() const { return parent_.downstreamProtocolType(); } + +StreamInfo::StreamInfo& ActiveMessage::streamInfo() { return stream_info_; } + +Event::Dispatcher& ActiveMessage::dispatcher() { return parent_.connection().dispatcher(); } + +const Network::Connection* ActiveMessage::connection() const { return &parent_.connection(); } + +void ActiveMessage::addDecoderFilter(JresFilters::DecoderFilterSharedPtr filter) { + addDecoderFilterWorker(filter, false); +} + +void ActiveMessage::addEncoderFilter(JresFilters::EncoderFilterSharedPtr filter) { + addEncoderFilterWorker(filter, false); +} + +void ActiveMessage::addFilter(JresFilters::CodecFilterSharedPtr filter) { + addDecoderFilterWorker(filter, true); + addEncoderFilterWorker(filter, true); +} + +void ActiveMessage::addDecoderFilterWorker(JresFilters::DecoderFilterSharedPtr filter, + bool dual_filter) { + ActiveMessageDecoderFilterPtr wrapper = + std::make_unique(*this, filter, dual_filter); + filter->setDecoderFilterCallbacks(*wrapper); + LinkedList::moveIntoListBack(std::move(wrapper), decoder_filters_); +} +void ActiveMessage::addEncoderFilterWorker(JresFilters::EncoderFilterSharedPtr filter, + bool dual_filter) { + ActiveMessageEncoderFilterPtr wrapper = + std::make_unique(*this, filter, dual_filter); + filter->setEncoderFilterCallbacks(*wrapper); + LinkedList::moveIntoListBack(std::move(wrapper), encoder_filters_); +} + +void ActiveMessage::onReset() { parent_.deferredMessage(*this); } + +void ActiveMessage::onError(const std::string& what) { + if (!metadata_) { + // It's possible that an error occurred before the decoder generated metadata, + // and a metadata object needs to be created in order to generate a local reply. + metadata_ = std::make_shared(); + } + + ASSERT(metadata_); + ENVOY_LOG(error, "Bad response: {}", what); + sendLocalReply(AppException(ResponseStatus::BadResponse, what), false); + parent_.deferredMessage(*this); +} + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/active_message.h b/source/extensions/filters/network/jres_proxy/active_message.h new file mode 100644 index 000000000000..46cbe2e68ccb --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/active_message.h @@ -0,0 +1,228 @@ +#pragma once + +#include "envoy/event/deferred_deletable.h" +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/stats/timespan.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/linked_object.h" +#include "common/common/logger.h" +#include "common/stream_info/stream_info_impl.h" + +#include "extensions/filters/network/jres_proxy/decoder.h" +#include "extensions/filters/network/jres_proxy/decoder_event_handler.h" +#include "extensions/filters/network/jres_proxy/filters/filter.h" +#include "extensions/filters/network/jres_proxy/metadata.h" +#include "extensions/filters/network/jres_proxy/router/router.h" +#include "extensions/filters/network/jres_proxy/stats.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +class ConnectionManager; +class ActiveMessage; + +class ActiveResponseDecoder : public ResponseDecoderCallbacks, + public StreamHandler, + Logger::Loggable { +public: + ActiveResponseDecoder(ActiveMessage& parent, JresFilterStats& stats, + Network::Connection& connection, ProtocolPtr&& protocol); + ~ActiveResponseDecoder() override = default; + + JresFilters::UpstreamResponseStatus onData(Buffer::Instance& data); + + // StreamHandler + void onStreamDecoded(MessageMetadataSharedPtr metadata, ContextSharedPtr ctx) override; + + // ResponseDecoderCallbacks + StreamHandler& newStream() override { return *this; } + void onHeartbeat(MessageMetadataSharedPtr) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } + + uint64_t requestId() const { return metadata_ ? metadata_->requestId() : 0; } + +private: + FilterStatus applyMessageEncodedFilters(MessageMetadataSharedPtr metadata, ContextSharedPtr ctx); + + ActiveMessage& parent_; + JresFilterStats& stats_; + Network::Connection& response_connection_; + ProtocolPtr protocol_; + ResponseDecoderPtr decoder_; + MessageMetadataSharedPtr metadata_; + bool complete_ : 1; + JresFilters::UpstreamResponseStatus response_status_; +}; + +using ActiveResponseDecoderPtr = std::unique_ptr; + +class ActiveMessageFilterBase : public virtual JresFilters::FilterCallbacksBase { +public: + ActiveMessageFilterBase(ActiveMessage& parent, bool dual_filter) + : parent_(parent), dual_filter_(dual_filter) {} + ~ActiveMessageFilterBase() override = default; + + // JresFilters::FilterCallbacksBase + uint64_t requestId() const override; + uint64_t streamId() const override; + const Network::Connection* connection() const override; + JresProxy::Router::RouteConstSharedPtr route() override; + SerializationType serializationType() const override; + ProtocolType protocolType() const override; + StreamInfo::StreamInfo& streamInfo() override; + Event::Dispatcher& dispatcher() override; + void resetStream() override; + +protected: + ActiveMessage& parent_; + const bool dual_filter_ : 1; +}; + +// Wraps a DecoderFilter and acts as the DecoderFilterCallbacks for the filter, enabling filter +// chain continuation. +class ActiveMessageDecoderFilter : public JresFilters::DecoderFilterCallbacks, + public ActiveMessageFilterBase, + public LinkedObject, + Logger::Loggable { +public: + ActiveMessageDecoderFilter(ActiveMessage& parent, JresFilters::DecoderFilterSharedPtr filter, + bool dual_filter); + ~ActiveMessageDecoderFilter() override = default; + + void continueDecoding() override; + void sendLocalReply(const JresFilters::DirectResponse& response, bool end_stream) override; + void startUpstreamResponse() override; + JresFilters::UpstreamResponseStatus upstreamData(Buffer::Instance& buffer) override; + void resetDownstreamConnection() override; + + JresFilters::DecoderFilterSharedPtr handler() { return handle_; } + +private: + JresFilters::DecoderFilterSharedPtr handle_; +}; + +using ActiveMessageDecoderFilterPtr = std::unique_ptr; + +// Wraps a EncoderFilter and acts as the EncoderFilterCallbacks for the filter, enabling filter +// chain continuation. +class ActiveMessageEncoderFilter : public ActiveMessageFilterBase, + public JresFilters::EncoderFilterCallbacks, + public LinkedObject, + Logger::Loggable { +public: + ActiveMessageEncoderFilter(ActiveMessage& parent, JresFilters::EncoderFilterSharedPtr filter, + bool dual_filter); + ~ActiveMessageEncoderFilter() override = default; + + void continueEncoding() override; + JresFilters::EncoderFilterSharedPtr handler() { return handle_; } + +private: + JresFilters::EncoderFilterSharedPtr handle_; + + friend class ActiveMessage; +}; + +using ActiveMessageEncoderFilterPtr = std::unique_ptr; + +// ActiveMessage tracks downstream requests for which no response has been received. +class ActiveMessage : public LinkedObject, + public Event::DeferredDeletable, + public StreamHandler, + public JresFilters::DecoderFilterCallbacks, + public JresFilters::FilterChainFactoryCallbacks, + Logger::Loggable { +public: + ActiveMessage(ConnectionManager& parent); + ~ActiveMessage() override; + + // Indicates which filter to start the iteration with. + enum class FilterIterationStartState { AlwaysStartFromNext, CanStartFromCurrent }; + + // Returns the encoder filter to start iteration with. + std::list::iterator + commonEncodePrefix(ActiveMessageEncoderFilter* filter, FilterIterationStartState state); + // Returns the decoder filter to start iteration with. + std::list::iterator + commonDecodePrefix(ActiveMessageDecoderFilter* filter, FilterIterationStartState state); + + // Jres::FilterChainFactoryCallbacks + void addDecoderFilter(JresFilters::DecoderFilterSharedPtr filter) override; + void addEncoderFilter(JresFilters::EncoderFilterSharedPtr filter) override; + void addFilter(JresFilters::CodecFilterSharedPtr filter) override; + + // StreamHandler + void onStreamDecoded(MessageMetadataSharedPtr metadata, ContextSharedPtr ctx) override; + + // JresFilters::DecoderFilterCallbacks + uint64_t requestId() const override; + uint64_t streamId() const override; + const Network::Connection* connection() const override; + void continueDecoding() override; + SerializationType serializationType() const override; + ProtocolType protocolType() const override; + StreamInfo::StreamInfo& streamInfo() override; + Router::RouteConstSharedPtr route() override; + void sendLocalReply(const JresFilters::DirectResponse& response, bool end_stream) override; + void startUpstreamResponse() override; + JresFilters::UpstreamResponseStatus upstreamData(Buffer::Instance& buffer) override; + void resetDownstreamConnection() override; + Event::Dispatcher& dispatcher() override; + void resetStream() override; + + void createFilterChain(); + FilterStatus applyDecoderFilters(ActiveMessageDecoderFilter* filter, + FilterIterationStartState state); + FilterStatus applyEncoderFilters(ActiveMessageEncoderFilter* filter, + FilterIterationStartState state); + void finalizeRequest(); + void onReset(); + void onError(const std::string& what); + MessageMetadataSharedPtr metadata() const { return metadata_; } + ContextSharedPtr context() const { return context_; } + bool pendingStreamDecoded() const { return pending_stream_decoded_; } + +private: + void addDecoderFilterWorker(JresFilters::DecoderFilterSharedPtr filter, bool dual_filter); + void addEncoderFilterWorker(JresFilters::EncoderFilterSharedPtr, bool dual_filter); + + ConnectionManager& parent_; + + ContextSharedPtr context_; + MessageMetadataSharedPtr metadata_; + Stats::TimespanPtr request_timer_; + ActiveResponseDecoderPtr response_decoder_; + + absl::optional cached_route_; + + std::list decoder_filters_; + std::function filter_action_; + + std::list encoder_filters_; + std::function encoder_filter_action_; + + int32_t request_id_; + + // This value is used in the calculation of the weighted cluster. + uint64_t stream_id_; + StreamInfo::StreamInfoImpl stream_info_; + + Buffer::OwnedImpl response_buffer_; + + bool pending_stream_decoded_ : 1; + bool local_response_sent_ : 1; + + friend class ActiveResponseDecoder; +}; + +using ActiveMessagePtr = std::unique_ptr; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/app_exception.cc b/source/extensions/filters/network/jres_proxy/app_exception.cc new file mode 100644 index 000000000000..db593de8ff23 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/app_exception.cc @@ -0,0 +1,18 @@ +#include "extensions/filters/network/jres_proxy/app_exception.h" + +#include "common/buffer/buffer_impl.h" + +#include "extensions/filters/network/jres_proxy/message.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +DownstreamConnectionCloseException::DownstreamConnectionCloseException(const std::string& what) + : EnvoyException(what) {} + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/app_exception.h b/source/extensions/filters/network/jres_proxy/app_exception.h new file mode 100644 index 000000000000..d98bc7472d2d --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/app_exception.h @@ -0,0 +1,56 @@ +#pragma once + +#include "envoy/common/exception.h" + +#include "common/common/utility.h" + +#include "extensions/filters/network/jres_proxy/filters/filter.h" +#include "extensions/filters/network/jres_proxy/metadata.h" +#include "extensions/filters/network/jres_proxy/protocol.h" +#include "extensions/filters/network/jres_proxy/serializer.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +using ResponseType = JresFilters::DirectResponse::ResponseType; + +template +struct AppExceptionBase : public EnvoyException, + public JresFilters::DirectResponse, + Logger::Loggable { + AppExceptionBase(const AppExceptionBase& ex) = default; + AppExceptionBase(T status, const std::string& what) + : EnvoyException(what), status_(status), + response_type_(RpcResponseType::ResponseWithException) {} + + ResponseType encode(MessageMetadata& metadata, JresProxy::Protocol& protocol, + Buffer::Instance& buffer) const override { + ASSERT(buffer.length() == 0); + + ENVOY_LOG(debug, "Exception information: {}", what()); + + metadata.setResponseStatus(status_); + metadata.setMessageType(MessageType::Response); + if (!protocol.encode(buffer, metadata, what(), response_type_)) { + ExceptionUtil::throwEnvoyException("Failed to encode local reply message"); + } + + return ResponseType::Exception; + } + + const T status_; + const RpcResponseType response_type_; +}; + +using AppException = AppExceptionBase<>; + +struct DownstreamConnectionCloseException : public EnvoyException { + DownstreamConnectionCloseException(const std::string& what); +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/buffer_helper.cc b/source/extensions/filters/network/jres_proxy/buffer_helper.cc new file mode 100644 index 000000000000..782452a1eadb --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/buffer_helper.cc @@ -0,0 +1,30 @@ +#include "extensions/filters/network/jres_proxy/buffer_helper.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +double BufferHelper::peekDouble(Buffer::Instance& buffer, uint64_t offset) { + if (buffer.length() < offset + 8) { + throw EnvoyException("buffer underflow"); + } + double i; + uint64_t j = buffer.peekBEInt(offset); + std::memcpy(&i, &j, 8); + return i; +} + +float BufferHelper::peekFloat(Buffer::Instance& buffer, uint64_t offset) { + if (buffer.length() < offset + 4) { + throw EnvoyException("buffer underflow"); + } + float i; + uint32_t j = buffer.peekBEInt(offset); + std::memcpy(&i, &j, 4); + return i; +} +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/buffer_helper.h b/source/extensions/filters/network/jres_proxy/buffer_helper.h new file mode 100644 index 000000000000..96df280ba814 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/buffer_helper.h @@ -0,0 +1,39 @@ +#pragma once + +#include "envoy/buffer/buffer.h" +#include "envoy/common/exception.h" + +#include "common/common/assert.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +/** + * BufferHelper provides buffer operations for reading bytes and numbers in the various encodings + * used by protocols. + */ +class BufferHelper { +public: + /** + * Reads an double from the buffer at the given offset. + * @param buffer Buffer::Instance containing data to decode + * @param offset offset into buffer to peek at + * @return the double at offset in buffer + */ + static double peekDouble(Buffer::Instance& buffer, uint64_t offset = 0); + + /** + * Reads an float from the buffer at the given offset. + * @param buffer Buffer::Instance containing data to decode + * @param offset offset into buffer to peek at + * @return the float at offset in buffer + */ + static float peekFloat(Buffer::Instance& buffer, uint64_t offset = 0); +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/config.cc b/source/extensions/filters/network/jres_proxy/config.cc new file mode 100644 index 000000000000..6f1ba22f7127 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/config.cc @@ -0,0 +1,160 @@ +#include "extensions/filters/network/jres_proxy/config.h" + +#include "envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.pb.h" +#include "envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "common/config/utility.h" + +#include "extensions/filters/network/jres_proxy/conn_manager.h" +#include "extensions/filters/network/jres_proxy/filters/factory_base.h" +#include "extensions/filters/network/jres_proxy/filters/well_known_names.h" +#include "extensions/filters/network/jres_proxy/stats.h" + +#include "absl/container/flat_hash_map.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +Network::FilterFactoryCb JresProxyFilterConfigFactory::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::jres_proxy::v3::JresProxy& proto_config, + Server::Configuration::FactoryContext& context) { + std::shared_ptr filter_config(std::make_shared(proto_config, context)); + + return [filter_config, &context](Network::FilterManager& filter_manager) -> void { + filter_manager.addReadFilter(std::make_shared( + *filter_config, context.api().randomGenerator(), context.dispatcher().timeSource())); + }; +} + +/** + * Static registration for the jres filter. @see RegisterFactory. + */ +REGISTER_FACTORY(JresProxyFilterConfigFactory, + Server::Configuration::NamedNetworkFilterConfigFactory); + +class ProtocolTypeMapper { +public: + using ConfigProtocolType = envoy::extensions::filters::network::jres_proxy::v3::ProtocolType; + using ProtocolTypeMap = absl::flat_hash_map; + + static ProtocolType lookupProtocolType(ConfigProtocolType config_type) { + const auto& iter = protocolTypeMap().find(config_type); + ASSERT(iter != protocolTypeMap().end()); + return iter->second; + } + +private: + static const ProtocolTypeMap& protocolTypeMap() { + CONSTRUCT_ON_FIRST_USE(ProtocolTypeMap, { + {ConfigProtocolType::Jres, ProtocolType::Jres}, + }); + } +}; + +class SerializationTypeMapper { +public: + using ConfigSerializationType = + envoy::extensions::filters::network::jres_proxy::v3::SerializationType; + using SerializationTypeMap = absl::flat_hash_map; + + static SerializationType lookupSerializationType(ConfigSerializationType type) { + const auto& iter = serializationTypeMap().find(type); + ASSERT(iter != serializationTypeMap().end()); + return iter->second; + } + +private: + static const SerializationTypeMap& serializationTypeMap() { + CONSTRUCT_ON_FIRST_USE(SerializationTypeMap, + { + {ConfigSerializationType::Hessian2, SerializationType::Hessian2}, + }); + } +}; + +class RouteMatcherTypeMapper { +public: + using ConfigProtocolType = envoy::extensions::filters::network::jres_proxy::v3::ProtocolType; + using RouteMatcherTypeMap = absl::flat_hash_map; + + static Router::RouteMatcherType lookupRouteMatcherType(ConfigProtocolType type) { + const auto& iter = routeMatcherTypeMap().find(type); + ASSERT(iter != routeMatcherTypeMap().end()); + return iter->second; + } + +private: + static const RouteMatcherTypeMap& routeMatcherTypeMap() { + CONSTRUCT_ON_FIRST_USE(RouteMatcherTypeMap, + { + {ConfigProtocolType::Jres, Router::RouteMatcherType::Default}, + }); + } +}; + +// class ConfigImpl. +ConfigImpl::ConfigImpl(const JresProxyConfig& config, + Server::Configuration::FactoryContext& context) + : context_(context), stats_prefix_(fmt::format("jres.{}.", config.stat_prefix())), + stats_(JresFilterStats::generateStats(stats_prefix_, context_.scope())), + serialization_type_( + SerializationTypeMapper::lookupSerializationType(config.serialization_type())), + protocol_type_(ProtocolTypeMapper::lookupProtocolType(config.protocol_type())) { + auto type = RouteMatcherTypeMapper::lookupRouteMatcherType(config.protocol_type()); + route_matcher_ = Router::NamedRouteMatcherConfigFactory::getFactory(type).createRouteMatcher( + config.route_config(), context); + if (config.jres_filters().empty()) { + ENVOY_LOG(debug, "using default router filter"); + + envoy::extensions::filters::network::jres_proxy::v3::JresFilter router_config; + router_config.set_name(JresFilters::JresFilterNames::get().ROUTER); + registerFilter(router_config); + } else { + for (const auto& filter_config : config.jres_filters()) { + registerFilter(filter_config); + } + } +} + +void ConfigImpl::createFilterChain(JresFilters::FilterChainFactoryCallbacks& callbacks) { + for (const JresFilters::FilterFactoryCb& factory : filter_factories_) { + factory(callbacks); + } +} + +Router::RouteConstSharedPtr ConfigImpl::route(const MessageMetadata& metadata, + uint64_t random_value) const { + return route_matcher_->route(metadata, random_value); +} + +ProtocolPtr ConfigImpl::createProtocol() { + return NamedProtocolConfigFactory::getFactory(protocol_type_).createProtocol(serialization_type_); +} + +void ConfigImpl::registerFilter(const JresFilterConfig& proto_config) { + const auto& string_name = proto_config.name(); + ENVOY_LOG(debug, " jres filter #{}", filter_factories_.size()); + ENVOY_LOG(debug, " name: {}", string_name); + ENVOY_LOG(debug, " config: {}", + MessageUtil::getJsonStringFromMessage(proto_config.config(), true)); + + auto& factory = + Envoy::Config::Utility::getAndCheckFactoryByName( + string_name); + ProtobufTypes::MessagePtr message = factory.createEmptyConfigProto(); + Envoy::Config::Utility::translateOpaqueConfig(proto_config.config(), + ProtobufWkt::Struct::default_instance(), + context_.messageValidationVisitor(), *message); + JresFilters::FilterFactoryCb callback = + factory.createFilterFactoryFromProto(*message, stats_prefix_, context_); + + filter_factories_.push_back(callback); +} + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/config.h b/source/extensions/filters/network/jres_proxy/config.h new file mode 100644 index 000000000000..12f3c2cb08d0 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/config.h @@ -0,0 +1,74 @@ +#pragma once + +#include + +#include "envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.pb.h" +#include "envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.pb.validate.h" + +#include "extensions/filters/network/common/factory_base.h" +#include "extensions/filters/network/jres_proxy/conn_manager.h" +#include "extensions/filters/network/jres_proxy/filters/filter.h" +#include "extensions/filters/network/jres_proxy/router/route_matcher.h" +#include "extensions/filters/network/jres_proxy/router/router_impl.h" +#include "extensions/filters/network/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +/** + * Config registration for the jres proxy filter. @see NamedNetworkFilterConfigFactory. + */ +class JresProxyFilterConfigFactory + : public Common::FactoryBase { +public: + JresProxyFilterConfigFactory() : FactoryBase(NetworkFilterNames::get().JresProxy, true) {} + +private: + Network::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::jres_proxy::v3::JresProxy& proto_config, + Server::Configuration::FactoryContext& context) override; +}; + +class ConfigImpl : public Config, + public Router::Config, + public JresFilters::FilterChainFactory, + Logger::Loggable { +public: + using JresProxyConfig = envoy::extensions::filters::network::jres_proxy::v3::JresProxy; + using JresFilterConfig = envoy::extensions::filters::network::jres_proxy::v3::JresFilter; + + ConfigImpl(const JresProxyConfig& config, Server::Configuration::FactoryContext& context); + ~ConfigImpl() override = default; + + // JresFilters::FilterChainFactory + void createFilterChain(JresFilters::FilterChainFactoryCallbacks& callbacks) override; + + // Router::Config + Router::RouteConstSharedPtr route(const MessageMetadata& metadata, + uint64_t random_value) const override; + + // Config + JresFilterStats& stats() override { return stats_; } + JresFilters::FilterChainFactory& filterFactory() override { return *this; } + Router::Config& routerConfig() override { return *this; } + ProtocolPtr createProtocol() override; + +private: + void registerFilter(const JresFilterConfig& proto_config); + + Server::Configuration::FactoryContext& context_; + const std::string stats_prefix_; + JresFilterStats stats_; + const SerializationType serialization_type_; + const ProtocolType protocol_type_; + Router::RouteMatcherPtr route_matcher_; + + std::list filter_factories_; +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/conn_manager.cc b/source/extensions/filters/network/jres_proxy/conn_manager.cc new file mode 100644 index 000000000000..6053601ddac6 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/conn_manager.cc @@ -0,0 +1,208 @@ +#include "extensions/filters/network/jres_proxy/conn_manager.h" + +#include + +#include "envoy/common/exception.h" + +#include "common/common/fmt.h" + +#include "extensions/filters/network/jres_proxy/app_exception.h" +#include "extensions/filters/network/jres_proxy/heartbeat_response.h" +#include "extensions/filters/network/jres_proxy/jres_hessian2_serializer_impl.h" +#include "extensions/filters/network/jres_proxy/jres_protocol_impl.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +constexpr uint32_t BufferLimit = UINT32_MAX; + +ConnectionManager::ConnectionManager(Config& config, Random::RandomGenerator& random_generator, + TimeSource& time_system) + : config_(config), time_system_(time_system), stats_(config_.stats()), + random_generator_(random_generator), protocol_(config.createProtocol()), + decoder_(std::make_unique(*protocol_, *this)) {} + +Network::FilterStatus ConnectionManager::onData(Buffer::Instance& data, bool end_stream) { + ENVOY_LOG(trace, "jres: read {} bytes", data.length()); + request_buffer_.move(data); + dispatch(); + + if (end_stream) { + ENVOY_CONN_LOG(trace, "downstream half-closed", read_callbacks_->connection()); + + // Downstream has closed. Unless we're waiting for an upstream connection to complete a oneway + // request, close. The special case for oneway requests allows them to complete before the + // ConnectionManager is destroyed. + if (stopped_) { + ASSERT(!active_message_list_.empty()); + auto metadata = (*active_message_list_.begin())->metadata(); + if (metadata && metadata->messageType() == MessageType::Oneway) { + ENVOY_CONN_LOG(trace, "waiting for one-way completion", read_callbacks_->connection()); + half_closed_ = true; + return Network::FilterStatus::StopIteration; + } + } + + ENVOY_LOG(debug, "jres: end data processing"); + resetAllMessages(false); + read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWrite); + } + + return Network::FilterStatus::StopIteration; +} + +Network::FilterStatus ConnectionManager::onNewConnection() { + return Network::FilterStatus::Continue; +} + +void ConnectionManager::initializeReadFilterCallbacks(Network::ReadFilterCallbacks& callbacks) { + read_callbacks_ = &callbacks; + read_callbacks_->connection().addConnectionCallbacks(*this); + read_callbacks_->connection().enableHalfClose(true); + read_callbacks_->connection().setBufferLimits(BufferLimit); +} + +void ConnectionManager::onEvent(Network::ConnectionEvent event) { + resetAllMessages(event == Network::ConnectionEvent::LocalClose); +} + +void ConnectionManager::onAboveWriteBufferHighWatermark() { + ENVOY_CONN_LOG(debug, "onAboveWriteBufferHighWatermark", read_callbacks_->connection()); + read_callbacks_->connection().readDisable(true); +} + +void ConnectionManager::onBelowWriteBufferLowWatermark() { + ENVOY_CONN_LOG(debug, "onBelowWriteBufferLowWatermark", read_callbacks_->connection()); + read_callbacks_->connection().readDisable(false); +} + +StreamHandler& ConnectionManager::newStream() { + ENVOY_LOG(debug, "jres: create the new decoder event handler"); + + ActiveMessagePtr new_message(std::make_unique(*this)); + new_message->createFilterChain(); + LinkedList::moveIntoList(std::move(new_message), active_message_list_); + return **active_message_list_.begin(); +} + +void ConnectionManager::onHeartbeat(MessageMetadataSharedPtr metadata) { + stats_.request_event_.inc(); + + if (read_callbacks_->connection().state() != Network::Connection::State::Open) { + ENVOY_LOG(warn, "jres: downstream connection is closed or closing"); + return; + } + + metadata->setResponseStatus(ResponseStatus::Ok); + metadata->setMessageType(MessageType::HeartbeatResponse); + + HeartbeatResponse heartbeat; + Buffer::OwnedImpl response_buffer; + heartbeat.encode(*metadata, *protocol_, response_buffer); + + read_callbacks_->connection().write(response_buffer, false); +} + +void ConnectionManager::dispatch() { + if (0 == request_buffer_.length()) { + ENVOY_LOG(warn, "jres: it's empty data"); + return; + } + + if (stopped_) { + ENVOY_CONN_LOG(debug, "jres: jres filter stopped", read_callbacks_->connection()); + return; + } + + try { + bool underflow = false; + while (!underflow) { + decoder_->onData(request_buffer_, underflow); + } + return; + } catch (const EnvoyException& ex) { + ENVOY_CONN_LOG(error, "jres error: {}", read_callbacks_->connection(), ex.what()); + read_callbacks_->connection().close(Network::ConnectionCloseType::NoFlush); + stats_.request_decoding_error_.inc(); + } + resetAllMessages(true); +} + +void ConnectionManager::sendLocalReply(MessageMetadata& metadata, + const JresFilters::DirectResponse& response, + bool end_stream) { + if (read_callbacks_->connection().state() != Network::Connection::State::Open) { + return; + } + + JresFilters::DirectResponse::ResponseType result = + JresFilters::DirectResponse::ResponseType::ErrorReply; + + try { + Buffer::OwnedImpl buffer; + result = response.encode(metadata, *protocol_, buffer); + read_callbacks_->connection().write(buffer, end_stream); + } catch (const EnvoyException& ex) { + ENVOY_CONN_LOG(error, "jres error: {}", read_callbacks_->connection(), ex.what()); + } + + if (end_stream) { + read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWrite); + } + + switch (result) { + case JresFilters::DirectResponse::ResponseType::SuccessReply: + stats_.local_response_success_.inc(); + break; + case JresFilters::DirectResponse::ResponseType::ErrorReply: + stats_.local_response_error_.inc(); + break; + case JresFilters::DirectResponse::ResponseType::Exception: + stats_.local_response_business_exception_.inc(); + break; + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } +} + +void ConnectionManager::continueDecoding() { + ENVOY_CONN_LOG(debug, "jres filter continued", read_callbacks_->connection()); + stopped_ = false; + dispatch(); + + if (!stopped_ && half_closed_) { + // If we're half closed, but not stopped waiting for an upstream, + // reset any pending rpcs and close the connection. + resetAllMessages(false); + read_callbacks_->connection().close(Network::ConnectionCloseType::FlushWrite); + } +} + +void ConnectionManager::deferredMessage(ActiveMessage& message) { + if (!message.inserted()) { + return; + } + read_callbacks_->connection().dispatcher().deferredDelete( + message.removeFromList(active_message_list_)); +} + +void ConnectionManager::resetAllMessages(bool local_reset) { + while (!active_message_list_.empty()) { + if (local_reset) { + ENVOY_CONN_LOG(debug, "local close with active request", read_callbacks_->connection()); + stats_.cx_destroy_local_with_active_rq_.inc(); + } else { + ENVOY_CONN_LOG(debug, "remote close with active request", read_callbacks_->connection()); + stats_.cx_destroy_remote_with_active_rq_.inc(); + } + + active_message_list_.front()->onReset(); + } +} + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/conn_manager.h b/source/extensions/filters/network/jres_proxy/conn_manager.h new file mode 100644 index 000000000000..f802c1558d71 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/conn_manager.h @@ -0,0 +1,108 @@ +#pragma once + +#include "envoy/common/time.h" +#include "envoy/extensions/filters/network/jres_proxy/v3/jres_proxy.pb.h" +#include "envoy/network/connection.h" +#include "envoy/network/filter.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats.h" +#include "envoy/stats/stats_macros.h" +#include "envoy/stats/timespan.h" + +#include "common/common/logger.h" + +#include "extensions/filters/network/jres_proxy/active_message.h" +#include "extensions/filters/network/jres_proxy/decoder.h" +#include "extensions/filters/network/jres_proxy/decoder_event_handler.h" +#include "extensions/filters/network/jres_proxy/filters/filter.h" +#include "extensions/filters/network/jres_proxy/protocol.h" +#include "extensions/filters/network/jres_proxy/serializer.h" +#include "extensions/filters/network/jres_proxy/stats.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +/** + * Config is a configuration interface for ConnectionManager. + */ +class Config { +public: + virtual ~Config() = default; + + virtual JresFilters::FilterChainFactory& filterFactory() PURE; + virtual JresFilterStats& stats() PURE; + virtual ProtocolPtr createProtocol() PURE; + virtual Router::Config& routerConfig() PURE; +}; + +// class ActiveMessagePtr; +class ConnectionManager : public Network::ReadFilter, + public Network::ConnectionCallbacks, + public RequestDecoderCallbacks, + Logger::Loggable { +public: + using ConfigProtocolType = envoy::extensions::filters::network::jres_proxy::v3::ProtocolType; + using ConfigSerializationType = + envoy::extensions::filters::network::jres_proxy::v3::SerializationType; + + ConnectionManager(Config& config, Random::RandomGenerator& random_generator, + TimeSource& time_system); + ~ConnectionManager() override = default; + + // Network::ReadFilter + Network::FilterStatus onData(Buffer::Instance& data, bool end_stream) override; + Network::FilterStatus onNewConnection() override; + void initializeReadFilterCallbacks(Network::ReadFilterCallbacks&) override; + + // Network::ConnectionCallbacks + void onEvent(Network::ConnectionEvent) override; + void onAboveWriteBufferHighWatermark() override; + void onBelowWriteBufferLowWatermark() override; + + // RequestDecoderCallbacks + StreamHandler& newStream() override; + void onHeartbeat(MessageMetadataSharedPtr metadata) override; + + JresFilterStats& stats() const { return stats_; } + Network::Connection& connection() const { return read_callbacks_->connection(); } + TimeSource& timeSystem() const { return time_system_; } + Random::RandomGenerator& randomGenerator() const { return random_generator_; } + Config& config() const { return config_; } + SerializationType downstreamSerializationType() const { return protocol_->serializer()->type(); } + ProtocolType downstreamProtocolType() const { return protocol_->type(); } + + void continueDecoding(); + void deferredMessage(ActiveMessage& message); + void sendLocalReply(MessageMetadata& metadata, const JresFilters::DirectResponse& response, + bool end_stream); + + // This function is for testing only. + std::list& getActiveMessagesForTest() { return active_message_list_; } + +private: + void dispatch(); + void resetAllMessages(bool local_reset); + + Buffer::OwnedImpl request_buffer_; + std::list active_message_list_; + + bool stopped_{false}; + bool half_closed_{false}; + + Config& config_; + TimeSource& time_system_; + JresFilterStats& stats_; + Random::RandomGenerator& random_generator_; + + SerializerPtr serializer_; + ProtocolPtr protocol_; + RequestDecoderPtr decoder_; + Network::ReadFilterCallbacks* read_callbacks_{}; +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/decoder.cc b/source/extensions/filters/network/jres_proxy/decoder.cc new file mode 100644 index 000000000000..d6b73a8e4d72 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/decoder.cc @@ -0,0 +1,140 @@ +#include "extensions/filters/network/jres_proxy/decoder.h" + +#include "common/common/macros.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +DecoderStateMachine::DecoderStatus +DecoderStateMachine::onDecodeStreamHeader(Buffer::Instance& buffer) { + ASSERT(!active_stream_); + + auto metadata = std::make_shared(); + auto ret = protocol_.decodeHeader(buffer, metadata); + if (!ret.second) { + ENVOY_LOG(debug, "jres decoder: need more data for {} protocol", protocol_.name()); + return {ProtocolState::WaitForData}; + } + + auto context = ret.first; + if (metadata->messageType() == MessageType::HeartbeatRequest || + metadata->messageType() == MessageType::HeartbeatResponse) { + if (buffer.length() < (context->headerSize() + context->bodySize())) { + ENVOY_LOG(debug, "jres decoder: need more data for {} protocol heartbeat", protocol_.name()); + return {ProtocolState::WaitForData}; + } + + ENVOY_LOG(debug, "jres decoder: this is the {} heartbeat message", protocol_.name()); + buffer.drain(context->headerSize() + context->bodySize()); + delegate_.onHeartbeat(metadata); + return {ProtocolState::Done}; + } + + active_stream_ = delegate_.newStream(metadata, context); + ASSERT(active_stream_); + context->messageOriginData().move(buffer, context->headerSize()); + + return {ProtocolState::OnDecodeStreamData}; +} + +DecoderStateMachine::DecoderStatus +DecoderStateMachine::onDecodeStreamData(Buffer::Instance& buffer) { + ASSERT(active_stream_); + + if (!protocol_.decodeData(buffer, active_stream_->context_, active_stream_->metadata_)) { + ENVOY_LOG(debug, "jres decoder: need more data for {} serialization, current size {}", + protocol_.serializer()->name(), buffer.length()); + return {ProtocolState::WaitForData}; + } + + active_stream_->context_->messageOriginData().move(buffer, active_stream_->context_->bodySize()); + active_stream_->onStreamDecoded(); + active_stream_ = nullptr; + + ENVOY_LOG(debug, "jres decoder: ends the deserialization of the message"); + return {ProtocolState::Done}; +} + +DecoderStateMachine::DecoderStatus DecoderStateMachine::handleState(Buffer::Instance& buffer) { + switch (state_) { + case ProtocolState::OnDecodeStreamHeader: + return onDecodeStreamHeader(buffer); + case ProtocolState::OnDecodeStreamData: + return onDecodeStreamData(buffer); + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } +} + +ProtocolState DecoderStateMachine::run(Buffer::Instance& buffer) { + while (state_ != ProtocolState::Done) { + ENVOY_LOG(trace, "jres decoder: state {}, {} bytes available", + ProtocolStateNameValues::name(state_), buffer.length()); + + DecoderStatus s = handleState(buffer); + if (s.next_state_ == ProtocolState::WaitForData) { + return ProtocolState::WaitForData; + } + + state_ = s.next_state_; + } + + return state_; +} + +using DecoderStateMachinePtr = std::unique_ptr; + +DecoderBase::DecoderBase(Protocol& protocol) : protocol_(protocol) {} + +DecoderBase::~DecoderBase() { complete(); } + +FilterStatus DecoderBase::onData(Buffer::Instance& data, bool& buffer_underflow) { + ENVOY_LOG(debug, "jres decoder: {} bytes available", data.length()); + buffer_underflow = false; + + if (!decode_started_) { + start(); + } + + ASSERT(state_machine_ != nullptr); + + ENVOY_LOG(debug, "jres decoder: protocol {}, state {}, {} bytes available", protocol_.name(), + ProtocolStateNameValues::name(state_machine_->currentState()), data.length()); + + ProtocolState rv = state_machine_->run(data); + switch (rv) { + case ProtocolState::WaitForData: + ENVOY_LOG(debug, "jres decoder: wait for data"); + buffer_underflow = true; + return FilterStatus::Continue; + default: + break; + } + + ASSERT(rv == ProtocolState::Done); + + complete(); + buffer_underflow = (data.length() == 0); + ENVOY_LOG(debug, "jres decoder: data length {}", data.length()); + return FilterStatus::Continue; +} + +void DecoderBase::start() { + state_machine_ = std::make_unique(protocol_, *this); + decode_started_ = true; +} + +void DecoderBase::complete() { + state_machine_.reset(); + stream_.reset(); + decode_started_ = false; +} + +void DecoderBase::reset() { complete(); } + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/decoder.h b/source/extensions/filters/network/jres_proxy/decoder.h new file mode 100644 index 000000000000..3915bb81c7a4 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/decoder.h @@ -0,0 +1,192 @@ +#pragma once + +#include "envoy/buffer/buffer.h" + +#include "common/buffer/buffer_impl.h" +#include "common/common/logger.h" + +#include "extensions/filters/network/jres_proxy/decoder_event_handler.h" +#include "extensions/filters/network/jres_proxy/protocol.h" +#include "extensions/filters/network/jres_proxy/serializer.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +#define ALL_PROTOCOL_STATES(FUNCTION) \ + FUNCTION(StopIteration) \ + FUNCTION(WaitForData) \ + FUNCTION(OnDecodeStreamHeader) \ + FUNCTION(OnDecodeStreamData) \ + FUNCTION(Done) + +/** + * ProtocolState represents a set of states used in a state machine to decode Jres requests + * and responses. + */ +enum class ProtocolState { ALL_PROTOCOL_STATES(GENERATE_ENUM) }; + +class ProtocolStateNameValues { +public: + static const std::string& name(ProtocolState state) { + size_t i = static_cast(state); + ASSERT(i < names().size()); + return names()[i]; + } + +private: + static const std::vector& names() { + CONSTRUCT_ON_FIRST_USE(std::vector, {ALL_PROTOCOL_STATES(GENERATE_STRING)}); + } +}; + +struct ActiveStream { + ActiveStream(StreamHandler& handler, MessageMetadataSharedPtr metadata, ContextSharedPtr context) + : handler_(handler), metadata_(metadata), context_(context) {} + ~ActiveStream() { + metadata_.reset(); + context_.reset(); + } + + void onStreamDecoded() { + ASSERT(metadata_ && context_); + handler_.onStreamDecoded(metadata_, context_); + } + + StreamHandler& handler_; + MessageMetadataSharedPtr metadata_; + ContextSharedPtr context_; +}; + +using ActiveStreamPtr = std::unique_ptr; + +class DecoderStateMachine : public Logger::Loggable { +public: + class Delegate { + public: + virtual ~Delegate() = default; + virtual ActiveStream* newStream(MessageMetadataSharedPtr metadata, + ContextSharedPtr context) PURE; + virtual void onHeartbeat(MessageMetadataSharedPtr metadata) PURE; + }; + + DecoderStateMachine(Protocol& protocol, Delegate& delegate) + : protocol_(protocol), delegate_(delegate), state_(ProtocolState::OnDecodeStreamHeader) {} + + /** + * Consumes as much data from the configured Buffer as possible and executes the decoding state + * machine. Returns ProtocolState::WaitForData if more data is required to complete processing of + * a message. Returns ProtocolState::Done when the end of a message is successfully processed. + * Once the Done state is reached, further invocations of run return immediately with Done. + * + * @param buffer a buffer containing the remaining data to be processed + * @return ProtocolState returns with ProtocolState::WaitForData or ProtocolState::Done + * @throw Envoy Exception if thrown by the underlying Protocol + */ + ProtocolState run(Buffer::Instance& buffer); + + /** + * @return the current ProtocolState + */ + ProtocolState currentState() const { return state_; } + +private: + struct DecoderStatus { + DecoderStatus() = default; + DecoderStatus(ProtocolState next_state) : next_state_(next_state){}; + DecoderStatus(ProtocolState next_state, FilterStatus filter_status) + : next_state_(next_state), filter_status_(filter_status){}; + + ProtocolState next_state_; + absl::optional filter_status_; + }; + + // These functions map directly to the matching ProtocolState values. Each returns the next state + // or ProtocolState::WaitForData if more data is required. + DecoderStatus onDecodeStreamHeader(Buffer::Instance& buffer); + DecoderStatus onDecodeStreamData(Buffer::Instance& buffer); + + // handleState delegates to the appropriate method based on state_. + DecoderStatus handleState(Buffer::Instance& buffer); + + Protocol& protocol_; + Delegate& delegate_; + + ProtocolState state_; + ActiveStream* active_stream_{nullptr}; +}; + +using DecoderStateMachinePtr = std::unique_ptr; + +class DecoderBase : public DecoderStateMachine::Delegate, + public Logger::Loggable { +public: + DecoderBase(Protocol& protocol); + ~DecoderBase() override; + + /** + * Drains data from the given buffer + * + * @param data a Buffer containing Jres protocol data + * @throw EnvoyException on Jres protocol errors + */ + FilterStatus onData(Buffer::Instance& data, bool& buffer_underflow); + + const Protocol& protocol() { return protocol_; } + + // It is assumed that all of the protocol parsing are stateless, + // if there is a state of the need to provide the reset interface call here. + void reset(); + +protected: + void start(); + void complete(); + + Protocol& protocol_; + + ActiveStreamPtr stream_; + DecoderStateMachinePtr state_machine_; + + bool decode_started_{false}; +}; + +/** + * Decoder encapsulates a configured and ProtocolPtr and SerializationPtr. + */ +template class Decoder : public DecoderBase { +public: + Decoder(Protocol& protocol, T& callbacks) : DecoderBase(protocol), callbacks_(callbacks) {} + + ActiveStream* newStream(MessageMetadataSharedPtr metadata, ContextSharedPtr context) override { + ASSERT(!stream_); + stream_ = std::make_unique(callbacks_.newStream(), metadata, context); + return stream_.get(); + } + + void onHeartbeat(MessageMetadataSharedPtr metadata) override { callbacks_.onHeartbeat(metadata); } + +private: + T& callbacks_; +}; + +class RequestDecoder : public Decoder { +public: + RequestDecoder(Protocol& protocol, RequestDecoderCallbacks& callbacks) + : Decoder(protocol, callbacks) {} +}; + +using RequestDecoderPtr = std::unique_ptr; + +class ResponseDecoder : public Decoder { +public: + ResponseDecoder(Protocol& protocol, ResponseDecoderCallbacks& callbacks) + : Decoder(protocol, callbacks) {} +}; + +using ResponseDecoderPtr = std::unique_ptr; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/decoder_event_handler.h b/source/extensions/filters/network/jres_proxy/decoder_event_handler.h new file mode 100644 index 000000000000..12ae30d80392 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/decoder_event_handler.h @@ -0,0 +1,95 @@ +#pragma once + +#include "envoy/common/pure.h" +#include "envoy/network/filter.h" + +#include "common/buffer/buffer_impl.h" + +#include "extensions/filters/network/jres_proxy/message.h" +#include "extensions/filters/network/jres_proxy/metadata.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +enum class FilterStatus : uint8_t { + // Continue filter chain iteration. + Continue, + // Do not iterate to any of the remaining filters in the chain. Returning + // FilterDataStatus::Continue from decodeData()/encodeData() or calling + // continueDecoding()/continueEncoding() MUST be called if continued filter iteration is desired. + StopIteration, + // Indicates that a retry is required for the reply message received. + Retry, +}; + +class StreamDecoder { +public: + virtual ~StreamDecoder() = default; + + /** + * Indicates that the message had been decoded. + * @param metadata MessageMetadataSharedPtr describing the message + * @param ctx the message context information + * @return FilterStatus to indicate if filter chain iteration should continue + */ + virtual FilterStatus onMessageDecoded(MessageMetadataSharedPtr metadata, + ContextSharedPtr ctx) PURE; +}; + +using StreamDecoderSharedPtr = std::shared_ptr; + +class StreamEncoder { +public: + virtual ~StreamEncoder() = default; + + /** + * Indicates that the message had been encoded. + * @param metadata MessageMetadataSharedPtr describing the message + * @param ctx the message context information + * @return FilterStatus to indicate if filter chain iteration should continue + */ + virtual FilterStatus onMessageEncoded(MessageMetadataSharedPtr metadata, + ContextSharedPtr ctx) PURE; +}; + +using StreamEncoderSharedPtr = std::shared_ptr; + +class StreamHandler { +public: + virtual ~StreamHandler() = default; + + /** + * Indicates that the message had been decoded. + * @param metadata MessageMetadataSharedPtr describing the message + * @param ctx the message context information + * @return FilterStatus to indicate if filter chain iteration should continue + */ + virtual void onStreamDecoded(MessageMetadataSharedPtr metadata, ContextSharedPtr ctx) PURE; +}; + +using StreamDecoderSharedPtr = std::shared_ptr; + +class DecoderCallbacksBase { +public: + virtual ~DecoderCallbacksBase() = default; + + /** + * @return StreamDecoder* a new StreamDecoder for a message. + */ + virtual StreamHandler& newStream() PURE; + + /** + * Indicates that the message is a heartbeat. + */ + virtual void onHeartbeat(MessageMetadataSharedPtr) PURE; +}; + +class RequestDecoderCallbacks : public DecoderCallbacksBase {}; +class ResponseDecoderCallbacks : public DecoderCallbacksBase {}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/filters/BUILD b/source/extensions/filters/network/jres_proxy/filters/BUILD new file mode 100644 index 000000000000..b4e5b39018f9 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/filters/BUILD @@ -0,0 +1,53 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "filter_interface", + hdrs = ["filter.h"], + deps = [ + "//include/envoy/buffer:buffer_interface", + "//include/envoy/network:connection_interface", + "//include/envoy/stream_info:stream_info_interface", + "//source/extensions/filters/network/jres_proxy:decoder_events_lib", + "//source/extensions/filters/network/jres_proxy:metadata_lib", + "//source/extensions/filters/network/jres_proxy:protocol_interface", + "//source/extensions/filters/network/jres_proxy:serializer_interface", + "//source/extensions/filters/network/jres_proxy/router:router_interface", + ], +) + +envoy_cc_library( + name = "filter_config_interface", + hdrs = ["filter_config.h"], + deps = [ + ":filter_interface", + "//include/envoy/config:typed_config_interface", + "//include/envoy/server:filter_config_interface", + "//source/common/common:macros", + "//source/common/protobuf:cc_wkt_protos", + ], +) + +envoy_cc_library( + name = "factory_base_lib", + hdrs = ["factory_base.h"], + deps = [ + ":filter_config_interface", + "//source/common/protobuf:utility_lib", + ], +) + +envoy_cc_library( + name = "well_known_names", + hdrs = ["well_known_names.h"], + deps = [ + "//source/common/singleton:const_singleton", + ], +) diff --git a/source/extensions/filters/network/jres_proxy/filters/factory_base.h b/source/extensions/filters/network/jres_proxy/filters/factory_base.h new file mode 100644 index 000000000000..0d18faa94ac6 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/filters/factory_base.h @@ -0,0 +1,51 @@ +#pragma once + +#include +#include + +#include "envoy/common/pure.h" + +#include "common/protobuf/utility.h" + +#include "extensions/filters/network/jres_proxy/filters/filter_config.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace JresFilters { + +template class FactoryBase : public NamedJresFilterConfigFactory { +public: + FilterFactoryCb + createFilterFactoryFromProto(const Protobuf::Message& proto_config, + const std::string& stats_prefix, + Server::Configuration::FactoryContext& context) override { + return createFilterFactoryFromProtoTyped(MessageUtil::downcastAndValidate( + proto_config, context.messageValidationVisitor()), + stats_prefix, context); + } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + + std::string name() const override { return name_; } + +protected: + FactoryBase(const std::string& name) : name_(name) {} + +private: + virtual FilterFactoryCb + createFilterFactoryFromProtoTyped(const ConfigProto& proto_config, + const std::string& stats_prefix, + Server::Configuration::FactoryContext& context) PURE; + + const std::string name_; +}; + +} // namespace JresFilters +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/filters/filter.h b/source/extensions/filters/network/jres_proxy/filters/filter.h new file mode 100644 index 000000000000..6fbaf1878f44 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/filters/filter.h @@ -0,0 +1,290 @@ +#pragma once + +#include +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/common/pure.h" +#include "envoy/network/connection.h" +#include "envoy/stream_info/stream_info.h" + +#include "extensions/filters/network/jres_proxy/decoder_event_handler.h" +#include "extensions/filters/network/jres_proxy/message.h" +#include "extensions/filters/network/jres_proxy/metadata.h" +#include "extensions/filters/network/jres_proxy/protocol.h" +#include "extensions/filters/network/jres_proxy/router/router.h" +#include "extensions/filters/network/jres_proxy/serializer.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace JresFilters { + +enum class UpstreamResponseStatus : uint8_t { + MoreData = 0, // The upstream response requires more data. + Complete = 1, // The upstream response is complete. + Reset = 2, // The upstream response is invalid and its connection must be reset. + Retry = 3, // The upstream response is failure need to retry. +}; + +class DirectResponse { +public: + virtual ~DirectResponse() = default; + + enum class ResponseType : uint8_t { + // DirectResponse encodes MessageType::Reply with success payload + SuccessReply, + + // DirectResponse encodes MessageType::Reply with an exception payload + ErrorReply, + + // DirectResponse encodes MessageType::Exception + Exception, + }; + + /** + * Encodes the response via the given Protocol. + * @param metadata the MessageMetadata for the request that generated this response + * @param proto the Protocol to be used for message encoding + * @param buffer the Buffer into which the message should be encoded + * @return ResponseType indicating whether the message is a successful or error reply or an + * exception + */ + virtual ResponseType encode(MessageMetadata& metadata, Protocol& protocol, + Buffer::Instance& buffer) const PURE; +}; + +using DirectResponsePtr = std::unique_ptr; + +/** + * Decoder filter callbacks add additional callbacks. + */ +class FilterCallbacksBase { +public: + virtual ~FilterCallbacksBase() = default; + + /** + * @return uint64_t the ID of the originating request for logging purposes. + */ + virtual uint64_t requestId() const PURE; + + /** + * @return uint64_t the ID of the originating stream for logging purposes. + */ + virtual uint64_t streamId() const PURE; + + /** + * @return const Network::Connection* the originating connection, or nullptr if there is none. + */ + virtual const Network::Connection* connection() const PURE; + + /** + * @return RouteConstSharedPtr the route for the current request. + */ + virtual JresProxy::Router::RouteConstSharedPtr route() PURE; + + /** + * @return SerializationType the originating protocol. + */ + virtual SerializationType serializationType() const PURE; + + /** + * @return ProtocolType the originating protocol. + */ + virtual ProtocolType protocolType() const PURE; + + /** + * @return StreamInfo for logging purposes. + */ + virtual StreamInfo::StreamInfo& streamInfo() PURE; + + /** + * @return Event::Dispatcher& the thread local dispatcher for allocating timers, etc. + */ + virtual Event::Dispatcher& dispatcher() PURE; + + /** + * Reset the underlying stream. + */ + virtual void resetStream() PURE; +}; + +/** + * Decoder filter callbacks add additional callbacks. + */ +class DecoderFilterCallbacks : public virtual FilterCallbacksBase { +public: + ~DecoderFilterCallbacks() override = default; + + /** + * Continue iterating through the filter chain with buffered data. This routine can only be + * called if the filter has previously returned StopIteration from one of the DecoderFilter + * methods. The connection manager will callbacks to the next filter in the chain. Further note + * that if the request is not complete, the calling filter may receive further callbacks and must + * return an appropriate status code depending on what the filter needs to do. + */ + virtual void continueDecoding() PURE; + + /** + * Create a locally generated response using the provided response object. + * @param response DirectResponsePtr the response to send to the downstream client + */ + virtual void sendLocalReply(const DirectResponse& response, bool end_stream) PURE; + + /** + * Indicates the start of an upstream response. May only be called once. + * @param transport_type TransportType the upstream is using + * @param protocol_type ProtocolType the upstream is using + */ + virtual void startUpstreamResponse() PURE; + + /** + * Called with upstream response data. + * @param data supplies the upstream's data + * @return UpstreamResponseStatus indicating if the upstream response requires more data, is + * complete, or if an error occurred requiring the upstream connection to be reset. + */ + virtual UpstreamResponseStatus upstreamData(Buffer::Instance& data) PURE; + + /** + * Reset the downstream connection. + */ + virtual void resetDownstreamConnection() PURE; +}; + +/** + * Encoder filter callbacks add additional callbacks. + */ +class EncoderFilterCallbacks : public virtual FilterCallbacksBase { +public: + ~EncoderFilterCallbacks() override = default; + + /** + * Continue iterating through the filter chain with buffered data. This routine can only be + * called if the filter has previously returned StopIteration from one of the DecoderFilter + * methods. The connection manager will callbacks to the next filter in the chain. Further note + * that if the request is not complete, the calling filter may receive further callbacks and must + * return an appropriate status code depending on what the filter needs to do. + */ + virtual void continueEncoding() PURE; +}; + +/** + * Common base class for both decoder and encoder filters. + */ +class FilterBase { +public: + virtual ~FilterBase() = default; + + /** + * This routine is called prior to a filter being destroyed. This may happen after normal stream + * finish (both downstream and upstream) or due to reset. Every filter is responsible for making + * sure that any async events are cleaned up in the context of this routine. This includes timers, + * network calls, etc. The reason there is an onDestroy() method vs. doing this type of cleanup + * in the destructor is due to the deferred deletion model that Envoy uses to avoid stack unwind + * complications. Filters must not invoke either encoder or decoder filter callbacks after having + * onDestroy() invoked. + */ + virtual void onDestroy() PURE; +}; + +/** + * Decoder filter interface. + */ +class DecoderFilter : public StreamDecoder, public FilterBase { +public: + ~DecoderFilter() override = default; + + /** + * Called by the connection manager once to initialize the filter decoder callbacks that the + * filter should use. Callbacks will not be invoked by the filter after onDestroy() is called. + */ + virtual void setDecoderFilterCallbacks(DecoderFilterCallbacks& callbacks) PURE; +}; + +using DecoderFilterSharedPtr = std::shared_ptr; + +/** + * Encoder filter interface. + */ +class EncoderFilter : public StreamEncoder, public FilterBase { +public: + ~EncoderFilter() override = default; + + /** + * Called by the connection manager once to initialize the filter encoder callbacks that the + * filter should use. Callbacks will not be invoked by the filter after onDestroy() is called. + */ + virtual void setEncoderFilterCallbacks(EncoderFilterCallbacks& callbacks) PURE; +}; + +using EncoderFilterSharedPtr = std::shared_ptr; + +/** + * A filter that handles both encoding and decoding. + */ +class CodecFilter : public virtual DecoderFilter, public virtual EncoderFilter {}; + +using CodecFilterSharedPtr = std::shared_ptr; + +/** + * These callbacks are provided by the connection manager to the factory so that the factory can + * build the filter chain in an application specific way. + */ +class FilterChainFactoryCallbacks { +public: + virtual ~FilterChainFactoryCallbacks() = default; + + /** + * Add a decoder filter that is used when reading connection data. + * @param filter supplies the filter to add. + */ + virtual void addDecoderFilter(DecoderFilterSharedPtr filter) PURE; + + /** + * Add a encoder filter that is used when writing connection data. + * @param filter supplies the filter to add. + */ + virtual void addEncoderFilter(EncoderFilterSharedPtr filter) PURE; + + /** + * Add a decoder/encoder filter that is used both when reading and writing connection data. + * @param filter supplies the filter to add. + */ + virtual void addFilter(CodecFilterSharedPtr filter) PURE; +}; + +/** + * This function is used to wrap the creation of a Jres filter chain for new connections as they + * come in. Filter factories create the function at configuration initialization time, and then + * they are used at runtime. + * @param callbacks supplies the callbacks for the stream to install filters to. Typically the + * function will install a single filter, but it's technically possibly to install more than one + * if desired. + */ +using FilterFactoryCb = std::function; + +/** + * A FilterChainFactory is used by a connection manager to create a Jres level filter chain when + * a new connection is created. Typically it would be implemented by a configuration engine that + * would install a set of filters that are able to process an application scenario on top of a + * stream of Jres requests. + */ +class FilterChainFactory { +public: + virtual ~FilterChainFactory() = default; + + /** + * Called when a new Jres stream is created on the connection. + * @param callbacks supplies the "sink" that is used for actually creating the filter chain. @see + * FilterChainFactoryCallbacks. + */ + virtual void createFilterChain(FilterChainFactoryCallbacks& callbacks) PURE; +}; + +} // namespace JresFilters +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/filters/filter_config.h b/source/extensions/filters/network/jres_proxy/filters/filter_config.h new file mode 100644 index 000000000000..1094a952404a --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/filters/filter_config.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include "envoy/common/pure.h" +#include "envoy/config/typed_config.h" +#include "envoy/server/filter_config.h" + +#include "common/common/macros.h" +#include "common/protobuf/protobuf.h" + +#include "extensions/filters/network/jres_proxy/filters/filter.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace JresFilters { + +/** + * Implemented by each Jres filter and registered via Registry::registerFactory or the + * convenience class RegisterFactory. + */ +class NamedJresFilterConfigFactory : public Envoy::Config::TypedFactory { +public: + ~NamedJresFilterConfigFactory() override = default; + + /** + * Create a particular Jres filter factory implementation. If the implementation is unable to + * produce a factory with the provided parameters, it should throw an EnvoyException in the case + * of general error. The returned callback should always be initialized. + * @param config supplies the configuration for the filter + * @param stat_prefix prefix for stat logging + * @param context supplies the filter's context. + * @return FilterFactoryCb the factory creation function. + */ + virtual JresFilters::FilterFactoryCb + createFilterFactoryFromProto(const Protobuf::Message& config, const std::string& stat_prefix, + Server::Configuration::FactoryContext& context) PURE; + + std::string category() const override { return "envoy.jres_proxy.filters"; } +}; + +} // namespace JresFilters +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/filters/well_known_names.h b/source/extensions/filters/network/jres_proxy/filters/well_known_names.h new file mode 100644 index 000000000000..002718ae2221 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/filters/well_known_names.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "common/singleton/const_singleton.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace JresFilters { + +/** + * Well-known Jres filter names. + * NOTE: New filters should use the well known name: envoy.filters.jres.name. + */ +class JresFilterNameValues { +public: + // Router filter + const std::string ROUTER = "envoy.filters.jres.router"; +}; + +using JresFilterNames = ConstSingleton; + +} // namespace JresFilters +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/heartbeat_response.cc b/source/extensions/filters/network/jres_proxy/heartbeat_response.cc new file mode 100644 index 000000000000..a93a88616810 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/heartbeat_response.cc @@ -0,0 +1,25 @@ +#include "extensions/filters/network/jres_proxy/heartbeat_response.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +JresFilters::DirectResponse::ResponseType +HeartbeatResponse::encode(MessageMetadata& metadata, JresProxy::Protocol& protocol, + Buffer::Instance& buffer) const { + ASSERT(metadata.responseStatus() == ResponseStatus::Ok); + ASSERT(metadata.messageType() == MessageType::HeartbeatResponse); + + if (!protocol.encode(buffer, metadata, "")) { + throw EnvoyException("failed to encode heartbeat message"); + } + + ENVOY_LOG(debug, "buffer length {}", buffer.length()); + return DirectResponse::ResponseType::SuccessReply; +} + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/heartbeat_response.h b/source/extensions/filters/network/jres_proxy/heartbeat_response.h new file mode 100644 index 000000000000..b12c59d578f3 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/heartbeat_response.h @@ -0,0 +1,25 @@ +#pragma once + +#include "extensions/filters/network/jres_proxy/filters/filter.h" +#include "extensions/filters/network/jres_proxy/metadata.h" +#include "extensions/filters/network/jres_proxy/protocol.h" +#include "extensions/filters/network/jres_proxy/serializer.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +struct HeartbeatResponse : public JresFilters::DirectResponse, Logger::Loggable { + HeartbeatResponse() = default; + ~HeartbeatResponse() override = default; + + using ResponseType = JresFilters::DirectResponse::ResponseType; + ResponseType encode(MessageMetadata& metadata, Protocol& protocol, + Buffer::Instance& buffer) const override; +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/hessian_utils.cc b/source/extensions/filters/network/jres_proxy/hessian_utils.cc new file mode 100644 index 000000000000..180d7fd0d4b5 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/hessian_utils.cc @@ -0,0 +1,587 @@ +#include "extensions/filters/network/jres_proxy/hessian_utils.h" + +#include + +#include "common/common/assert.h" +#include "common/common/fmt.h" + +#include "extensions/filters/network/jres_proxy/buffer_helper.h" + +#include "absl/strings/str_cat.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +namespace { + +template +typename std::enable_if::value, T>::type leftShift(T left, uint16_t bit_number) { + if (left < 0) { + left = -left; + return -1 * (left << bit_number); + } + + return left << bit_number; +} + +inline void addByte(Buffer::Instance& buffer, const uint8_t value) { buffer.add(&value, 1); } + +void addSeq(Buffer::Instance& buffer, const std::initializer_list& values) { + for (const uint8_t& value : values) { + buffer.add(&value, 1); + } +} + +size_t doWriteString(Buffer::Instance& instance, absl::string_view str_view) { + const size_t length = str_view.length(); + constexpr size_t str_max_length = 0xffff; + constexpr size_t two_octet_max_lenth = 1024; + + if (length < 32) { + addByte(instance, static_cast(length)); + instance.add(str_view.data(), str_view.length()); + return length + sizeof(uint8_t); + } + + if (length < two_octet_max_lenth) { + const uint8_t code = length >> 8; // 0x30 + length / 0x100 must less than 0x34 + const uint8_t remain = length & 0xff; + std::initializer_list values{static_cast(0x30 + code), remain}; + addSeq(instance, values); + instance.add(str_view.data(), str_view.length()); + return length + values.size(); + } + + if (length <= str_max_length) { + const uint8_t code = length >> 8; + const uint8_t remain = length & 0xff; + std::initializer_list values{'S', code, remain}; + addSeq(instance, values); + instance.add(str_view.data(), str_view.length()); + return length + values.size(); + } + + std::initializer_list values{0x52, 0xff, 0xff}; + addSeq(instance, values); + instance.add(str_view.data(), str_max_length); + const size_t size = str_max_length + values.size(); + ASSERT(size == (str_max_length + values.size())); + + const size_t child_size = + doWriteString(instance, str_view.substr(str_max_length, length - str_max_length)); + return child_size + size; +} + +} // namespace + +/* + * Reference: + * https://cs.chromium.org/chromium/src/base/strings/string_util.h?q=WriteInto&sq=package:chromium&dr=CSs&l=426 + */ +char* allocStringBuffer(std::string* str, size_t length) { + str->reserve(length); + str->resize(length - 1); + return &((*str)[0]); +} + +std::string HessianUtils::peekString(Buffer::Instance& buffer, size_t* size, uint64_t offset) { + ASSERT(buffer.length() > offset); + const uint8_t code = buffer.peekInt(offset); + size_t delta_length = 0; + std::string result; + switch (code) { + case 0x00: + case 0x01: + case 0x02: + case 0x03: + case 0x04: + case 0x05: + case 0x06: + case 0x07: + case 0x08: + case 0x09: + case 0x0a: + case 0x0b: + case 0x0c: + case 0x0d: + case 0x0e: + case 0x0f: + case 0x10: + case 0x11: + case 0x12: + case 0x13: + case 0x14: + case 0x15: + case 0x16: + case 0x17: + case 0x18: + case 0x19: + case 0x1a: + case 0x1b: + case 0x1c: + case 0x1d: + case 0x1e: + case 0x1f: + delta_length = code - 0x00; + if (delta_length + 1 + offset > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + buffer.copyOut(offset + 1, delta_length, allocStringBuffer(&result, delta_length + 1)); + *size = delta_length + 1; + return result; + + case 0x30: + case 0x31: + case 0x32: + case 0x33: + if (offset + 2 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + delta_length = (code - 0x30) * 256 + buffer.peekInt(offset + 1); + if (delta_length + 2 + offset > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + buffer.copyOut(offset + 2, delta_length, allocStringBuffer(&result, delta_length + 1)); + *size = delta_length + 2; + return result; + + case 0x53: + if (offset + 3 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + delta_length = buffer.peekBEInt(offset + 1); + + if (delta_length + 3 + offset > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + buffer.copyOut(offset + 3, delta_length, allocStringBuffer(&result, delta_length + 1)); + *size = delta_length + 3; + return result; + + case 0x52: + if (offset + 3 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + delta_length = buffer.peekBEInt(offset + 1); + buffer.copyOut(offset + 3, delta_length, allocStringBuffer(&result, delta_length + 1)); + size_t next_size = 0; + result.append(peekString(buffer, &next_size, delta_length + 3 + offset)); + *size = next_size + delta_length + 3; + return result; + } + throw EnvoyException(absl::StrCat("hessian type is not string ", code)); +} + +std::string HessianUtils::readString(Buffer::Instance& buffer) { + size_t size; + std::string result(peekString(buffer, &size)); + buffer.drain(size); + return result; +} + +long HessianUtils::peekLong(Buffer::Instance& buffer, size_t* size, uint64_t offset) { + ASSERT(buffer.length() > offset); + long result; + uint8_t code = buffer.peekInt(offset); + switch (code) { + case 0xd8: + case 0xd9: + case 0xda: + case 0xdb: + case 0xdc: + case 0xdd: + case 0xde: + case 0xdf: + case 0xe0: + case 0xe1: + case 0xe2: + case 0xe3: + case 0xe4: + case 0xe5: + case 0xe6: + case 0xe7: + case 0xe8: + case 0xe9: + case 0xea: + case 0xeb: + case 0xec: + case 0xed: + case 0xee: + case 0xef: + + result = code - 0xe0; + *size = 1; + return result; + + case 0xf0: + case 0xf1: + case 0xf2: + case 0xf3: + case 0xf4: + case 0xf5: + case 0xf6: + case 0xf7: + case 0xf8: + case 0xf9: + case 0xfa: + case 0xfb: + case 0xfc: + case 0xfd: + case 0xfe: + case 0xff: + + if (offset + 2 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + result = leftShift(code - 0xf8, 8) + buffer.peekInt(offset + 1); + *size = 2; + return result; + + case 0x38: + case 0x39: + case 0x3a: + case 0x3b: + case 0x3c: + case 0x3d: + case 0x3e: + case 0x3f: + + if (offset + 3 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + result = leftShift(code - 0x3c, 16) + (buffer.peekInt(offset + 1) << 8) + + buffer.peekInt(offset + 2); + *size = 3; + return result; + + case 0x59: + + if (offset + 5 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + result = buffer.peekBEInt(offset + 1); + *size = 5; + return result; + + case 0x4c: + + if (offset + 9 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + result = buffer.peekBEInt(offset + 1); + *size = 9; + return result; + } + + throw EnvoyException(absl::StrCat("hessian type is not long ", code)); +} + +long HessianUtils::readLong(Buffer::Instance& buffer) { + size_t size; + const long result = peekLong(buffer, &size); + buffer.drain(size); + return result; +} + +bool HessianUtils::peekBool(Buffer::Instance& buffer, size_t* size, uint64_t offset) { + ASSERT(buffer.length() > offset); + bool result; + const uint8_t code = buffer.peekInt(offset); + if (code == 0x46) { + result = false; + *size = 1; + return result; + } + + if (code == 0x54) { + result = true; + *size = 1; + return result; + } + + throw EnvoyException(absl::StrCat("hessian type is not bool ", code)); +} + +bool HessianUtils::readBool(Buffer::Instance& buffer) { + size_t size; + bool result(peekBool(buffer, &size)); + buffer.drain(size); + return result; +} + +int HessianUtils::peekInt(Buffer::Instance& buffer, size_t* size, uint64_t offset) { + ASSERT(buffer.length() > offset); + const uint8_t code = buffer.peekInt(offset); + int result; + + // Compact int + if (code >= 0x80 && code <= 0xbf) { + result = (code - 0x90); + *size = 1; + return result; + } + + switch (code) { + case 0xc0: + case 0xc1: + case 0xc2: + case 0xc3: + case 0xc4: + case 0xc5: + case 0xc6: + case 0xc7: + case 0xc8: + case 0xc9: + case 0xca: + case 0xcb: + case 0xcc: + case 0xcd: + case 0xce: + case 0xcf: + if (offset + 2 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + result = leftShift(code - 0xc8, 8) + buffer.peekInt(offset + 1); + *size = 2; + return result; + + case 0xd0: + case 0xd1: + case 0xd2: + case 0xd3: + case 0xd4: + case 0xd5: + case 0xd6: + case 0xd7: + if (offset + 3 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + result = leftShift(code - 0xd4, 16) + (buffer.peekInt(offset + 1) << 8) + + buffer.peekInt(offset + 2); + *size = 3; + return result; + + case 0x49: + if (offset + 5 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + result = buffer.peekBEInt(offset + 1); + *size = 5; + return result; + } + + throw EnvoyException(absl::StrCat("hessian type is not int ", code)); +} + +int HessianUtils::readInt(Buffer::Instance& buffer) { + size_t size; + int result(peekInt(buffer, &size)); + buffer.drain(size); + return result; +} + +double HessianUtils::peekDouble(Buffer::Instance& buffer, size_t* size, uint64_t offset) { + ASSERT(buffer.length() > offset); + double result; + uint8_t code = buffer.peekInt(offset); + switch (code) { + case 0x5b: + result = 0.0; + *size = 1; + return result; + + case 0x5c: + result = 1.0; + *size = 1; + return result; + + case 0x5d: + if (offset + 2 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + result = static_cast(buffer.peekInt(offset + 1)); + *size = 2; + return result; + + case 0x5e: + if (offset + 3 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + result = static_cast(256 * buffer.peekInt(offset + 1) + + buffer.peekInt(offset + 2)); + *size = 3; + return result; + + case 0x5f: + if (offset + 5 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + result = BufferHelper::peekFloat(buffer, offset + 1); + *size = 5; + return result; + + case 0x44: + if (offset + 9 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + result = BufferHelper::peekDouble(buffer, offset + 1); + *size = 9; + return result; + } + + throw EnvoyException(absl::StrCat("hessian type is not double ", code)); +} + +double HessianUtils::readDouble(Buffer::Instance& buffer) { + size_t size; + double result(peekDouble(buffer, &size)); + buffer.drain(size); + return result; +} + +void HessianUtils::peekNull(Buffer::Instance& buffer, size_t* size, uint64_t offset) { + ASSERT(buffer.length() > offset); + uint8_t code = buffer.peekInt(offset); + if (code == 0x4e) { + *size = 1; + return; + } + + throw EnvoyException(absl::StrCat("hessian type is not null ", code)); +} + +void HessianUtils::readNull(Buffer::Instance& buffer) { + size_t size; + peekNull(buffer, &size); + buffer.drain(size); +} + +std::chrono::milliseconds HessianUtils::peekDate(Buffer::Instance& buffer, size_t* size, + uint64_t offset) { + ASSERT(buffer.length() > offset); + std::chrono::milliseconds result; + uint8_t code = buffer.peekInt(offset); + switch (code) { + case 0x4b: + if (offset + 5 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + result = std::chrono::minutes(buffer.peekBEInt(offset + 1)); + *size = 5; + return result; + + case 0x4a: + if (offset + 9 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + result = std::chrono::milliseconds(buffer.peekBEInt(offset + 1)); + *size = 9; + return result; + } + + throw EnvoyException(absl::StrCat("hessian type is not date ", code)); +} + +std::chrono::milliseconds HessianUtils::readDate(Buffer::Instance& buffer) { + size_t size; + std::chrono::milliseconds result; + result = peekDate(buffer, &size); + buffer.drain(size); + return result; +} + +std::string HessianUtils::peekByte(Buffer::Instance& buffer, size_t* size, uint64_t offset) { + ASSERT(buffer.length() > offset); + std::string result; + uint8_t code = buffer.peekInt(offset); + size_t delta_length = 0; + switch (code) { + case 0x20: + case 0x21: + case 0x22: + case 0x23: + case 0x24: + case 0x25: + case 0x26: + case 0x27: + case 0x28: + case 0x29: + case 0x2a: + case 0x2b: + case 0x2c: + case 0x2d: + case 0x2e: + case 0x2f: + delta_length = code - 0x20; + if (delta_length + 1 + offset > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + buffer.copyOut(offset + 1, delta_length, allocStringBuffer(&result, delta_length + 1)); + *size = delta_length + 1; + return result; + + case 0x42: + if (offset + 3 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + delta_length = buffer.peekBEInt(offset + 1); + if (delta_length + 3 + offset > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + buffer.copyOut(offset + 3, delta_length, allocStringBuffer(&result, delta_length + 1)); + *size = delta_length + 3; + return result; + + case 0x41: + if (offset + 3 > buffer.length()) { + throw EnvoyException("buffer underflow"); + } + + delta_length = buffer.peekBEInt(offset + 1); + buffer.copyOut(offset + 3, delta_length, allocStringBuffer(&result, delta_length + 1)); + size_t next_size; + result.append(peekByte(buffer, &next_size, delta_length + 3 + offset)); + *size = delta_length + 3 + next_size; + return result; + } + + throw EnvoyException(absl::StrCat("hessian type is not byte ", code)); +} + +std::string HessianUtils::readByte(Buffer::Instance& buffer) { + size_t size; + std::string result(peekByte(buffer, &size)); + buffer.drain(size); + return result; +} + +size_t HessianUtils::writeString(Buffer::Instance& buffer, absl::string_view str) { + return doWriteString(buffer, str); +} + +size_t HessianUtils::writeInt(Buffer::Instance& buffer, uint8_t value) { + // Compact int + buffer.writeByte(0x90 + value); + return sizeof(uint8_t); +} + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/hessian_utils.h b/source/extensions/filters/network/jres_proxy/hessian_utils.h new file mode 100644 index 000000000000..4fa0d1dc0725 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/hessian_utils.h @@ -0,0 +1,46 @@ +#pragma once + +#include +#include +#include + +#include "envoy/buffer/buffer.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +/* + * Hessian deserialization + * See http://hessian.caucho.com/doc/hessian-serialization.html + */ +class HessianUtils { +public: + static std::string peekString(Buffer::Instance& buffer, size_t* size, uint64_t offset = 0); + static long peekLong(Buffer::Instance& buffer, size_t* size, uint64_t offset = 0); + static bool peekBool(Buffer::Instance& buffer, size_t* size, uint64_t offset = 0); + static int peekInt(Buffer::Instance& buffer, size_t* size, uint64_t offset = 0); + static double peekDouble(Buffer::Instance& buffer, size_t* size, uint64_t offset = 0); + static void peekNull(Buffer::Instance& buffer, size_t* size, uint64_t offset = 0); + static std::chrono::milliseconds peekDate(Buffer::Instance& buffer, size_t* size, + uint64_t offset = 0); + static std::string peekByte(Buffer::Instance& buffer, size_t* size, uint64_t offset = 0); + + static std::string readString(Buffer::Instance& buffer); + static long readLong(Buffer::Instance& buffer); + static bool readBool(Buffer::Instance& buffer); + static int readInt(Buffer::Instance& buffer); + static double readDouble(Buffer::Instance& buffer); + static void readNull(Buffer::Instance& buffer); + static std::chrono::milliseconds readDate(Buffer::Instance& buffer); + static std::string readByte(Buffer::Instance& buffer); + + static size_t writeString(Buffer::Instance& buffer, absl::string_view str); + static size_t writeInt(Buffer::Instance& buffer, uint8_t value); +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/jres_hessian2_serializer_impl.cc b/source/extensions/filters/network/jres_proxy/jres_hessian2_serializer_impl.cc new file mode 100644 index 000000000000..8c59e4cdae42 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/jres_hessian2_serializer_impl.cc @@ -0,0 +1,118 @@ +#include "extensions/filters/network/jres_proxy/jres_hessian2_serializer_impl.h" + +#include "envoy/common/exception.h" + +#include "common/common/assert.h" +#include "common/common/macros.h" + +#include "extensions/filters/network/jres_proxy/hessian_utils.h" +#include "extensions/filters/network/jres_proxy/message_impl.h" +#include "extensions/filters/network/jres_proxy/serializer.h" +#include "extensions/filters/network/jres_proxy/serializer_impl.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +std::pair +JresHessian2SerializerImpl::deserializeRpcInvocation(Buffer::Instance& buffer, + ContextSharedPtr context) { + size_t total_size = 0, size; + // TODO(zyfjeff): Add format checker + std::string jres_version = HessianUtils::peekString(buffer, &size); + total_size += size; + std::string service_name = HessianUtils::peekString(buffer, &size, total_size); + total_size += size; + std::string service_version = HessianUtils::peekString(buffer, &size, total_size); + total_size += size; + std::string method_name = HessianUtils::peekString(buffer, &size, total_size); + total_size += size; + + if (static_cast(context->bodySize()) < total_size) { + throw EnvoyException(fmt::format("RpcInvocation size({}) large than body size({})", total_size, + context->bodySize())); + } + + auto invo = std::make_shared(); + invo->setServiceName(service_name); + invo->setServiceVersion(service_version); + invo->setMethodName(method_name); + + return std::pair(invo, true); +} + +std::pair +JresHessian2SerializerImpl::deserializeRpcResult(Buffer::Instance& buffer, + ContextSharedPtr context) { + ASSERT(buffer.length() >= context->bodySize()); + size_t total_size = 0; + bool has_value = true; + + auto result = std::make_shared(); + RpcResponseType type = static_cast(HessianUtils::peekInt(buffer, &total_size)); + + switch (type) { + case RpcResponseType::ResponseWithException: + case RpcResponseType::ResponseWithExceptionWithAttachments: + result->setException(true); + break; + case RpcResponseType::ResponseWithNullValue: + case RpcResponseType::ResponseNullValueWithAttachments: + has_value = false; + FALLTHRU; + case RpcResponseType::ResponseWithValue: + case RpcResponseType::ResponseValueWithAttachments: + result->setException(false); + break; + default: + throw EnvoyException(fmt::format("not supported return type {}", static_cast(type))); + } + + if (context->bodySize() < total_size) { + throw EnvoyException(fmt::format("RpcResult size({}) large than body size({})", total_size, + context->bodySize())); + } + + if (!has_value && context->bodySize() != total_size) { + throw EnvoyException( + fmt::format("RpcResult is no value, but the rest of the body size({}) not equal 0", + (context->bodySize() - total_size))); + } + + return std::pair(result, true); +} + +size_t JresHessian2SerializerImpl::serializeRpcResult(Buffer::Instance& output_buffer, + const std::string& content, + RpcResponseType type) { + size_t origin_length = output_buffer.length(); + + // The serialized response type is compact int. + size_t serialized_size = HessianUtils::writeInt( + output_buffer, static_cast::type>(type)); + + // Serialized response content. + serialized_size += HessianUtils::writeString(output_buffer, content); + + ASSERT((output_buffer.length() - origin_length) == serialized_size); + + return serialized_size; +} + +class JresHessian2SerializerConfigFactory + : public SerializerFactoryBase { +public: + JresHessian2SerializerConfigFactory() + : SerializerFactoryBase(ProtocolType::Jres, SerializationType::Hessian2) {} +}; + +/** + * Static registration for the Hessian protocol. @see RegisterFactory. + */ +REGISTER_FACTORY(JresHessian2SerializerConfigFactory, NamedSerializerConfigFactory); + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/jres_hessian2_serializer_impl.h b/source/extensions/filters/network/jres_proxy/jres_hessian2_serializer_impl.h new file mode 100644 index 000000000000..5a3d6041c47e --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/jres_hessian2_serializer_impl.h @@ -0,0 +1,30 @@ +#pragma once + +#include "extensions/filters/network/jres_proxy/serializer.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +class JresHessian2SerializerImpl : public Serializer { +public: + ~JresHessian2SerializerImpl() override = default; + const std::string& name() const override { + return ProtocolSerializerNames::get().fromType(ProtocolType::Jres, type()); + } + SerializationType type() const override { return SerializationType::Hessian2; } + + std::pair + deserializeRpcInvocation(Buffer::Instance& buffer, ContextSharedPtr context) override; + + std::pair deserializeRpcResult(Buffer::Instance& buffer, + ContextSharedPtr context) override; + + size_t serializeRpcResult(Buffer::Instance& output_buffer, const std::string& content, + RpcResponseType type) override; +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/jres_protocol_impl.cc b/source/extensions/filters/network/jres_proxy/jres_protocol_impl.cc new file mode 100644 index 000000000000..edf7edcc15ca --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/jres_protocol_impl.cc @@ -0,0 +1,228 @@ +#include "extensions/filters/network/jres_proxy/jres_protocol_impl.h" + +#include "envoy/registry/registry.h" + +#include "common/common/assert.h" + +#include "extensions/filters/network/jres_proxy/message_impl.h" +#include "extensions/filters/network/jres_proxy/serializer_impl.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace { + +constexpr uint16_t MagicNumber = 0xdabb; +constexpr uint8_t MessageTypeMask = 0x80; +constexpr uint8_t EventMask = 0x20; +constexpr uint8_t TwoWayMask = 0x40; +constexpr uint8_t SerializationTypeMask = 0x1f; +constexpr uint64_t FlagOffset = 2; +constexpr uint64_t StatusOffset = 3; +constexpr uint64_t RequestIDOffset = 4; +constexpr uint64_t BodySizeOffset = 12; + +} // namespace + +// Consistent with the SerializationType +bool isValidSerializationType(SerializationType type) { + switch (type) { + case SerializationType::Hessian2: + break; + default: + return false; + } + return true; +} + +// Consistent with the ResponseStatus +bool isValidResponseStatus(ResponseStatus status) { + switch (status) { + case ResponseStatus::Ok: + case ResponseStatus::ClientTimeout: + case ResponseStatus::ServerTimeout: + case ResponseStatus::BadRequest: + case ResponseStatus::BadResponse: + case ResponseStatus::ServiceNotFound: + case ResponseStatus::ServiceError: + case ResponseStatus::ClientError: + case ResponseStatus::ServerThreadpoolExhaustedError: + break; + default: + return false; + } + return true; +} + +void parseRequestInfoFromBuffer(Buffer::Instance& data, MessageMetadataSharedPtr metadata) { + ASSERT(data.length() >= JresProtocolImpl::MessageSize); + uint8_t flag = data.peekInt(FlagOffset); + bool is_two_way = (flag & TwoWayMask) == TwoWayMask ? true : false; + SerializationType type = static_cast(flag & SerializationTypeMask); + if (!isValidSerializationType(type)) { + throw EnvoyException( + absl::StrCat("invalid jres message serialization type ", + static_cast::type>(type))); + } + + if (!is_two_way && metadata->messageType() != MessageType::HeartbeatRequest) { + metadata->setMessageType(MessageType::Oneway); + } + + metadata->setSerializationType(type); +} + +void parseResponseInfoFromBuffer(Buffer::Instance& buffer, MessageMetadataSharedPtr metadata) { + ASSERT(buffer.length() >= JresProtocolImpl::MessageSize); + ResponseStatus status = static_cast(buffer.peekInt(StatusOffset)); + if (!isValidResponseStatus(status)) { + throw EnvoyException( + absl::StrCat("invalid jres message response status ", + static_cast::type>(status))); + } + + metadata->setResponseStatus(status); +} + +std::pair +JresProtocolImpl::decodeHeader(Buffer::Instance& buffer, MessageMetadataSharedPtr metadata) { + if (!metadata) { + throw EnvoyException("invalid metadata parameter"); + } + + if (buffer.length() < JresProtocolImpl::MessageSize) { + return std::pair(nullptr, false); + } + + uint16_t magic_number = buffer.peekBEInt(); + if (magic_number != MagicNumber) { + throw EnvoyException(absl::StrCat("invalid jres message magic number ", magic_number)); + } + + uint8_t flag = buffer.peekInt(FlagOffset); + MessageType type = + (flag & MessageTypeMask) == MessageTypeMask ? MessageType::Request : MessageType::Response; + bool is_event = (flag & EventMask) == EventMask ? true : false; + int64_t request_id = buffer.peekBEInt(RequestIDOffset); + int32_t body_size = buffer.peekBEInt(BodySizeOffset); + + // The body size of the heartbeat message is zero. + if (body_size > MaxBodySize || body_size < 0) { + throw EnvoyException(absl::StrCat("invalid jres message size ", body_size)); + } + + metadata->setRequestId(request_id); + + if (type == MessageType::Request) { + if (is_event) { + type = MessageType::HeartbeatRequest; + } + metadata->setMessageType(type); + parseRequestInfoFromBuffer(buffer, metadata); + } else { + if (is_event) { + type = MessageType::HeartbeatResponse; + } + metadata->setMessageType(type); + parseResponseInfoFromBuffer(buffer, metadata); + } + + auto context = std::make_shared(); + context->setHeaderSize(JresProtocolImpl::MessageSize); + context->setBodySize(body_size); + context->setHeartbeat(is_event); + + return std::pair(context, true); +} + +bool JresProtocolImpl::decodeData(Buffer::Instance& buffer, ContextSharedPtr context, + MessageMetadataSharedPtr metadata) { + ASSERT(serializer_); + + if ((buffer.length()) < static_cast(context->bodySize())) { + return false; + } + + switch (metadata->messageType()) { + case MessageType::Oneway: + case MessageType::Request: { + auto ret = serializer_->deserializeRpcInvocation(buffer, context); + if (!ret.second) { + return false; + } + metadata->setInvocationInfo(ret.first); + break; + } + case MessageType::Response: { + auto ret = serializer_->deserializeRpcResult(buffer, context); + if (!ret.second) { + return false; + } + if (ret.first->hasException()) { + metadata->setMessageType(MessageType::Exception); + } + break; + } + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } + + return true; +} + +bool JresProtocolImpl::encode(Buffer::Instance& buffer, const MessageMetadata& metadata, + const std::string& content, RpcResponseType type) { + ASSERT(serializer_); + + switch (metadata.messageType()) { + case MessageType::HeartbeatResponse: { + ASSERT(metadata.hasResponseStatus()); + ASSERT(content.empty()); + buffer.writeBEInt(MagicNumber); + uint8_t flag = static_cast(metadata.serializationType()); + flag = flag ^ EventMask; + buffer.writeByte(flag); + buffer.writeByte(static_cast(metadata.responseStatus())); + buffer.writeBEInt(metadata.requestId()); + buffer.writeBEInt(0); + return true; + } + case MessageType::Response: { + ASSERT(metadata.hasResponseStatus()); + ASSERT(!content.empty()); + Buffer::OwnedImpl body_buffer; + size_t serialized_body_size = serializer_->serializeRpcResult(body_buffer, content, type); + + buffer.writeBEInt(MagicNumber); + buffer.writeByte(static_cast(metadata.serializationType())); + buffer.writeByte(static_cast(metadata.responseStatus())); + buffer.writeBEInt(metadata.requestId()); + buffer.writeBEInt(serialized_body_size); + + buffer.move(body_buffer, serialized_body_size); + return true; + } + case MessageType::Request: + case MessageType::Oneway: + case MessageType::Exception: + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } +} + +class JresProtocolConfigFactory : public ProtocolFactoryBase { +public: + JresProtocolConfigFactory() : ProtocolFactoryBase(ProtocolType::Jres) {} +}; + +/** + * Static registration for the Jres protocol. @see RegisterFactory. + */ +REGISTER_FACTORY(JresProtocolConfigFactory, NamedProtocolConfigFactory); + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/jres_protocol_impl.h b/source/extensions/filters/network/jres_proxy/jres_protocol_impl.h new file mode 100644 index 000000000000..124b26e06e24 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/jres_protocol_impl.h @@ -0,0 +1,33 @@ +#pragma once + +#include "extensions/filters/network/jres_proxy/protocol.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +class JresProtocolImpl : public Protocol { +public: + JresProtocolImpl() = default; + ~JresProtocolImpl() override = default; + + const std::string& name() const override { return ProtocolNames::get().fromType(type()); } + ProtocolType type() const override { return ProtocolType::Jres; } + + std::pair decodeHeader(Buffer::Instance& buffer, + MessageMetadataSharedPtr metadata) override; + bool decodeData(Buffer::Instance& buffer, ContextSharedPtr context, + MessageMetadataSharedPtr metadata) override; + + bool encode(Buffer::Instance& buffer, const MessageMetadata& metadata, const std::string& content, + RpcResponseType type) override; + + static constexpr uint8_t MessageSize = 16; + static constexpr int32_t MaxBodySize = 16 * 1024 * 1024; +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/message.h b/source/extensions/filters/network/jres_proxy/message.h new file mode 100644 index 000000000000..c71630a452c2 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/message.h @@ -0,0 +1,145 @@ +#pragma once + +#include +#include + +#include "envoy/common/pure.h" + +#include "common/buffer/buffer_impl.h" + +#include "absl/container/node_hash_map.h" +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +/** + * Stream reset reasons. + */ +enum class StreamResetReason : uint8_t { + // If a local codec level reset was sent on the stream. + LocalReset, + // If a local codec level refused stream reset was sent on the stream (allowing for retry). + LocalRefusedStreamReset, + // If a remote codec level reset was received on the stream. + RemoteReset, + // If a remote codec level refused stream reset was received on the stream (allowing for retry). + RemoteRefusedStreamReset, + // If the stream was locally reset by a connection pool due to an initial connection failure. + ConnectionFailure, + // If the stream was locally reset due to connection termination. + ConnectionTermination, + // The stream was reset because of a resource overflow. + Overflow +}; + +// Supported protocol type +enum class ProtocolType : uint8_t { + Jres = 1, + + // ATTENTION: MAKE SURE THIS REMAINS EQUAL TO THE LAST PROTOCOL TYPE + LastProtocolType = Jres, +}; + +// Supported serialization type +enum class SerializationType : uint8_t { + Hessian2 = 2, +}; + +// Message Type +enum class MessageType : uint8_t { + Response = 0, + Request = 1, + Oneway = 2, + Exception = 3, + HeartbeatRequest = 4, + HeartbeatResponse = 5, + + // ATTENTION: MAKE SURE THIS REMAINS EQUAL TO THE LAST MESSAGE TYPE + LastMessageType = HeartbeatResponse, +}; + +/** + * Jres protocol response status types. + */ +enum class ResponseStatus : uint8_t { + Ok = 20, + ClientTimeout = 30, + ServerTimeout = 31, + BadRequest = 40, + BadResponse = 50, + ServiceNotFound = 60, + ServiceError = 70, + ServerError = 80, + ClientError = 90, + ServerThreadpoolExhaustedError = 100, +}; + +enum class RpcResponseType : uint8_t { + ResponseWithException = 0, + ResponseWithValue = 1, + ResponseWithNullValue = 2, + ResponseWithExceptionWithAttachments = 3, + ResponseValueWithAttachments = 4, + ResponseNullValueWithAttachments = 5, +}; + +class Context { +public: + using AttachmentMap = absl::node_hash_map; + + bool hasAttachments() const { return !attachments_.empty(); } + const AttachmentMap& attachments() const { return attachments_; } + + Buffer::Instance& messageOriginData() { return message_origin_buffer_; } + size_t messageSize() const { return headerSize() + bodySize(); } + + virtual size_t bodySize() const PURE; + virtual size_t headerSize() const PURE; + +protected: + Context() = default; + virtual ~Context() { attachments_.clear(); } + + AttachmentMap attachments_; + Buffer::OwnedImpl message_origin_buffer_; +}; + +using ContextSharedPtr = std::shared_ptr; + +/** + * RpcInvocation represent an rpc call + * See + * https://github.com/apache/incubator-jres/blob/master/jres-rpc/jres-rpc-api/src/main/java/org/apache/jres/rpc/RpcInvocation.java + */ +class RpcInvocation { +public: + virtual ~RpcInvocation() = default; + + virtual const std::string& serviceName() const PURE; + virtual const std::string& methodName() const PURE; + virtual const absl::optional& serviceVersion() const PURE; + virtual const absl::optional& serviceGroup() const PURE; +}; + +using RpcInvocationSharedPtr = std::shared_ptr; + +/** + * RpcResult represent the result of an rpc call + * See + * https://github.com/apache/incubator-jres/blob/master/jres-rpc/jres-rpc-api/src/main/java/org/apache/jres/rpc/RpcResult.java + */ +class RpcResult { +public: + virtual ~RpcResult() = default; + virtual bool hasException() const PURE; +}; + +using RpcResultSharedPtr = std::shared_ptr; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/message_impl.h b/source/extensions/filters/network/jres_proxy/message_impl.h new file mode 100644 index 000000000000..e53033d9a3d8 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/message_impl.h @@ -0,0 +1,65 @@ +#pragma once + +#include "extensions/filters/network/jres_proxy/message.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +class ContextBase : public Context { +public: + ContextBase() = default; + ~ContextBase() override = default; + + // Override from Context + size_t bodySize() const override { return body_size_; } + size_t headerSize() const override { return header_size_; } + + void setBodySize(size_t size) { body_size_ = size; } + void setHeaderSize(size_t size) { header_size_ = size; } + +protected: + size_t body_size_{0}; + size_t header_size_{0}; +}; + +class ContextImpl : public ContextBase { +public: + ContextImpl() = default; + ~ContextImpl() override = default; + + bool isHeartbeat() const { return is_heartbeat_; } + void setHeartbeat(bool is_heartbeat) { is_heartbeat_ = is_heartbeat; } + +private: + bool is_heartbeat_{false}; +}; + +class RpcInvocationBase : public RpcInvocation { +public: + ~RpcInvocationBase() override = default; + + void setServiceName(const std::string& name) { service_name_ = name; } + const std::string& serviceName() const override { return service_name_; } + + void setMethodName(const std::string& name) { method_name_ = name; } + const std::string& methodName() const override { return method_name_; } + + void setServiceVersion(const std::string& version) { service_version_ = version; } + const absl::optional& serviceVersion() const override { return service_version_; } + + void setServiceGroup(const std::string& group) { group_ = group; } + const absl::optional& serviceGroup() const override { return group_; } + +protected: + std::string service_name_; + std::string method_name_; + absl::optional service_version_; + absl::optional group_; +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/metadata.h b/source/extensions/filters/network/jres_proxy/metadata.h new file mode 100644 index 000000000000..ae243dd8c695 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/metadata.h @@ -0,0 +1,85 @@ +#pragma once + +#include +#include + +#include "common/common/assert.h" +#include "common/common/empty_string.h" +#include "common/http/header_map_impl.h" + +#include "extensions/filters/network/jres_proxy/message.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +class MessageMetadata { +public: + void setInvocationInfo(RpcInvocationSharedPtr invocation_info) { + invocation_info_ = invocation_info; + } + bool hasInvocationInfo() const { return invocation_info_ != nullptr; } + const RpcInvocation& invocationInfo() const { return *invocation_info_; } + + void setProtocolType(ProtocolType type) { proto_type_ = type; } + ProtocolType protocolType() const { return proto_type_; } + + void setProtocolVersion(uint8_t version) { protocol_version_ = version; } + uint8_t protocolVersion() const { return protocol_version_; } + + void setMessageType(MessageType type) { message_type_ = type; } + MessageType messageType() const { return message_type_; } + + void setRequestId(int64_t id) { request_id_ = id; } + int64_t requestId() const { return request_id_; } + + void setTimeout(uint32_t timeout) { timeout_ = timeout; } + absl::optional timeout() const { return timeout_; } + + void setTwoWayFlag(bool two_way) { is_two_way_ = two_way; } + bool isTwoWay() const { return is_two_way_; } + + template void setSerializationType(T type) { + ASSERT((std::is_same::type>::value)); + serialization_type_ = static_cast(type); + } + template T serializationType() const { + ASSERT((std::is_same::type>::value)); + return static_cast(serialization_type_); + } + + template void setResponseStatus(T status) { + ASSERT((std::is_same::type>::value)); + response_status_ = static_cast(status); + } + template T responseStatus() const { + ASSERT((std::is_same::type>::value)); + return static_cast(response_status_.value()); + } + bool hasResponseStatus() const { return response_status_.has_value(); } + +private: + bool is_two_way_{false}; + + MessageType message_type_{MessageType::Request}; + ProtocolType proto_type_{ProtocolType::Jres}; + + absl::optional response_status_; + absl::optional timeout_; + + RpcInvocationSharedPtr invocation_info_; + + uint8_t serialization_type_{static_cast(SerializationType::Hessian2)}; + uint8_t protocol_version_{1}; + int64_t request_id_ = 0; +}; + +using MessageMetadataSharedPtr = std::shared_ptr; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/protocol.h b/source/extensions/filters/network/jres_proxy/protocol.h new file mode 100644 index 000000000000..4577553d2e55 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/protocol.h @@ -0,0 +1,144 @@ +#pragma once + +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/config/typed_config.h" + +#include "common/common/assert.h" +#include "common/common/fmt.h" +#include "common/config/utility.h" +#include "common/singleton/const_singleton.h" + +#include "extensions/filters/network/jres_proxy/message.h" +#include "extensions/filters/network/jres_proxy/metadata.h" +#include "extensions/filters/network/jres_proxy/serializer.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +class Protocol { +public: + virtual ~Protocol() = default; + Protocol() = default; + + /** + * @return Initializes the serializer used by the protocol codec + */ + void initSerializer(SerializationType type) { + serializer_ = NamedSerializerConfigFactory::getFactory(this->type(), type).createSerializer(); + } + + /** + * @return Serializer the protocol Serializer + */ + virtual Serializer* serializer() const { return serializer_.get(); } + + virtual const std::string& name() const PURE; + + /** + * @return ProtocolType the protocol type + */ + virtual ProtocolType type() const PURE; + + /* + * decodes the jres protocol message header. + * + * @param buffer the currently buffered jres data. + * @param metadata the meta data of current messages + * @return ContextSharedPtr save the context data of current messages, + * nullptr if more data is required. + * bool true if a complete message was successfully consumed, false if more data + * is required. + * @throws EnvoyException if the data is not valid for this protocol. + */ + virtual std::pair decodeHeader(Buffer::Instance& buffer, + MessageMetadataSharedPtr metadata) PURE; + + /* + * decodes the jres protocol message body, potentially invoking callbacks. + * If successful, the message is removed from the buffer. + * + * @param buffer the currently buffered jres data. + * @param context save the meta data of current messages. + * @param metadata the meta data of current messages + * @return bool true if a complete message was successfully consumed, false if more data + * is required. + * @throws EnvoyException if the data is not valid for this protocol. + */ + virtual bool decodeData(Buffer::Instance& buffer, ContextSharedPtr context, + MessageMetadataSharedPtr metadata) PURE; + + /* + * encodes the jres protocol message. + * + * @param buffer save the currently buffered jres data. + * @param metadata the meta data of jres protocol + * @param content the body of jres protocol message + * @param type the type of jres protocol response message + * @return bool true if the protocol coding succeeds. + */ + virtual bool encode(Buffer::Instance& buffer, const MessageMetadata& metadata, + const std::string& content, + RpcResponseType type = RpcResponseType::ResponseWithValue) PURE; + +protected: + SerializerPtr serializer_; +}; + +using ProtocolPtr = std::unique_ptr; + +/** + * Implemented by each Jres protocol and registered via Registry::registerFactory or the + * convenience class RegisterFactory. + */ +class NamedProtocolConfigFactory : public Config::UntypedFactory { +public: + ~NamedProtocolConfigFactory() override = default; + + /** + * Create a particular Jres protocol. + * @param serialization_type the serialization type of the protocol body. + * @return protocol instance pointer. + */ + virtual ProtocolPtr createProtocol(SerializationType serialization_type) PURE; + + std::string category() const override { return "envoy.jres_proxy.protocols"; } + + /** + * Convenience method to lookup a factory by type. + * @param ProtocolType the protocol type. + * @return NamedProtocolConfigFactory& for the ProtocolType. + */ + static NamedProtocolConfigFactory& getFactory(ProtocolType type) { + const std::string& name = ProtocolNames::get().fromType(type); + return Envoy::Config::Utility::getAndCheckFactoryByName(name); + } +}; + +/** + * ProtocolFactoryBase provides a template for a trivial NamedProtocolConfigFactory. + */ +template class ProtocolFactoryBase : public NamedProtocolConfigFactory { +public: + ProtocolPtr createProtocol(SerializationType serialization_type) override { + auto protocol = std::make_unique(); + protocol->initSerializer(serialization_type); + return protocol; + } + + std::string name() const override { return name_; } + +protected: + ProtocolFactoryBase(ProtocolType type) : name_(ProtocolNames::get().fromType(type)) {} + +private: + const std::string name_; +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/protocol_constants.h b/source/extensions/filters/network/jres_proxy/protocol_constants.h new file mode 100644 index 000000000000..607067ed5cad --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/protocol_constants.h @@ -0,0 +1,98 @@ +#pragma once + +#include "common/common/assert.h" +#include "common/common/fmt.h" +#include "common/singleton/const_singleton.h" + +#include "extensions/filters/network/jres_proxy/message.h" + +#include "absl/container/node_hash_map.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +/** + * Names of available Protocol implementations. + */ +class ProtocolNameValues { +public: + struct ProtocolTypeHash { + template std::size_t operator()(T t) const { return static_cast(t); } + }; + + using ProtocolTypeNameMap = absl::node_hash_map; + + const ProtocolTypeNameMap protocolTypeNameMap = { + {ProtocolType::Jres, "jres"}, + }; + + const std::string& fromType(ProtocolType type) const { + const auto& itor = protocolTypeNameMap.find(type); + ASSERT(itor != protocolTypeNameMap.end()); + return itor->second; + } +}; + +using ProtocolNames = ConstSingleton; + +/** + * Names of available serializer implementations. + */ +class SerializerNameValues { +public: + struct SerializationTypeHash { + template std::size_t operator()(T t) const { return static_cast(t); } + }; + + using SerializerTypeNameMap = + absl::node_hash_map; + + const SerializerTypeNameMap serializerTypeNameMap = { + {SerializationType::Hessian2, "hessian2"}, + }; + + const std::string& fromType(SerializationType type) const { + const auto& itor = serializerTypeNameMap.find(type); + ASSERT(itor != serializerTypeNameMap.end()); + return itor->second; + } +}; + +using SerializerNames = ConstSingleton; + +class ProtocolSerializerNameValues { +public: + inline uint8_t generateKey(ProtocolType protocol_type, + SerializationType serialization_type) const { + return static_cast(serialization_type) ^ static_cast(protocol_type); + } + + inline std::string generateValue(ProtocolType protocol_type, + SerializationType serialization_type) const { + return fmt::format("{}.{}", ProtocolNames::get().fromType(protocol_type), + SerializerNames::get().fromType(serialization_type)); + } + +#define GENERATE_PAIR(X, Y) generateKey(X, Y), generateValue(X, Y) + + using ProtocolSerializerTypeNameMap = absl::node_hash_map; + + const ProtocolSerializerTypeNameMap protocolSerializerTypeNameMap = { + {GENERATE_PAIR(ProtocolType::Jres, SerializationType::Hessian2)}, + }; + + const std::string& fromType(ProtocolType protocol_type, SerializationType type) const { + const auto& itor = protocolSerializerTypeNameMap.find(generateKey(protocol_type, type)); + ASSERT(itor != protocolSerializerTypeNameMap.end()); + return itor->second; + } +}; + +using ProtocolSerializerNames = ConstSingleton; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/router/BUILD b/source/extensions/filters/network/jres_proxy/router/BUILD new file mode 100644 index 000000000000..18f9a4ccecdc --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/router/BUILD @@ -0,0 +1,87 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_library", + "envoy_extension_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_extension_package() + +envoy_cc_library( + name = "router_interface", + hdrs = ["router.h"], + deps = [ + "//include/envoy/router:router_interface", + "//source/extensions/filters/network/jres_proxy:metadata_lib", + ], +) + +envoy_cc_library( + name = "route_matcher_interface", + hdrs = ["route.h"], + deps = [ + ":router_interface", + "//include/envoy/config:typed_config_interface", + "//include/envoy/server:filter_config_interface", + "//source/common/config:utility_lib", + "//source/common/singleton:const_singleton", + "//source/extensions/filters/network/jres_proxy:metadata_lib", + "@envoy_api//envoy/extensions/filters/network/jres_proxy/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "route_matcher", + srcs = ["route_matcher.cc"], + hdrs = ["route_matcher.h"], + deps = [ + ":route_matcher_interface", + ":router_interface", + "//include/envoy/router:router_interface", + "//source/common/common:logger_lib", + "//source/common/common:matchers_lib", + "//source/common/http:header_utility_lib", + "//source/common/protobuf:utility_lib", + "//source/extensions/filters/network/jres_proxy:metadata_lib", + "//source/extensions/filters/network/jres_proxy:serializer_interface", + "@envoy_api//envoy/config/route/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/jres_proxy/v3:pkg_cc_proto", + "@envoy_api//envoy/type/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "config", + srcs = ["config.cc"], + hdrs = ["config.h"], + deps = [ + ":router_lib", + "//include/envoy/registry", + "//source/extensions/filters/network/jres_proxy/filters:factory_base_lib", + "//source/extensions/filters/network/jres_proxy/filters:filter_config_interface", + "//source/extensions/filters/network/jres_proxy/filters:well_known_names", + "@envoy_api//envoy/extensions/filters/network/jres_proxy/router/v3:pkg_cc_proto", + ], +) + +envoy_cc_library( + name = "router_lib", + srcs = ["router_impl.cc"], + hdrs = ["router_impl.h"], + deps = [ + ":router_interface", + "//include/envoy/tcp:conn_pool_interface", + "//include/envoy/upstream:cluster_manager_interface", + "//include/envoy/upstream:load_balancer_interface", + "//include/envoy/upstream:thread_local_cluster_interface", + "//source/common/common:logger_lib", + "//source/common/http:header_utility_lib", + "//source/common/router:metadatamatchcriteria_lib", + "//source/common/upstream:load_balancer_lib", + "//source/extensions/filters/network/jres_proxy:app_exception_lib", + "//source/extensions/filters/network/jres_proxy:protocol_interface", + "//source/extensions/filters/network/jres_proxy:serializer_interface", + "//source/extensions/filters/network/jres_proxy/filters:filter_interface", + ], +) diff --git a/source/extensions/filters/network/jres_proxy/router/config.cc b/source/extensions/filters/network/jres_proxy/router/config.cc new file mode 100644 index 000000000000..4f2ab5eed239 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/router/config.cc @@ -0,0 +1,32 @@ +#include "extensions/filters/network/jres_proxy/router/config.h" + +#include "envoy/extensions/filters/network/jres_proxy/router/v3/router.pb.h" +#include "envoy/extensions/filters/network/jres_proxy/router/v3/router.pb.validate.h" +#include "envoy/registry/registry.h" + +#include "extensions/filters/network/jres_proxy/router/router_impl.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace Router { + +JresFilters::FilterFactoryCb RouterFilterConfig::createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::jres_proxy::router::v3::Router&, const std::string&, + Server::Configuration::FactoryContext& context) { + return [&context](JresFilters::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addDecoderFilter(std::make_shared(context.clusterManager())); + }; +} + +/** + * Static registration for the router filter. @see RegisterFactory. + */ +REGISTER_FACTORY(RouterFilterConfig, JresFilters::NamedJresFilterConfigFactory); + +} // namespace Router +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/router/config.h b/source/extensions/filters/network/jres_proxy/router/config.h new file mode 100644 index 000000000000..9ea562bfa293 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/router/config.h @@ -0,0 +1,31 @@ +#pragma once + +#include "envoy/extensions/filters/network/jres_proxy/router/v3/router.pb.h" +#include "envoy/extensions/filters/network/jres_proxy/router/v3/router.pb.validate.h" + +#include "extensions/filters/network/jres_proxy/filters/factory_base.h" +#include "extensions/filters/network/jres_proxy/filters/well_known_names.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace Router { + +class RouterFilterConfig + : public JresFilters::FactoryBase< + envoy::extensions::filters::network::jres_proxy::router::v3::Router> { +public: + RouterFilterConfig() : FactoryBase(JresFilters::JresFilterNames::get().ROUTER) {} + +private: + JresFilters::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::filters::network::jres_proxy::router::v3::Router& proto_config, + const std::string& stat_prefix, Server::Configuration::FactoryContext& context) override; +}; + +} // namespace Router +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/router/route.h b/source/extensions/filters/network/jres_proxy/router/route.h new file mode 100644 index 000000000000..e9eb63344d96 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/router/route.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include + +#include "envoy/config/typed_config.h" +#include "envoy/extensions/filters/network/jres_proxy/v3/route.pb.h" +#include "envoy/router/router.h" +#include "envoy/server/filter_config.h" + +#include "common/config/utility.h" +#include "common/singleton/const_singleton.h" + +#include "extensions/filters/network/jres_proxy/metadata.h" +#include "extensions/filters/network/jres_proxy/router/router.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace Router { + +using RouteConfigurations = Protobuf::RepeatedPtrField< + envoy::extensions::filters::network::jres_proxy::v3::RouteConfiguration>; + +enum class RouteMatcherType : uint8_t { + Default, +}; + +/** + * Names of available Protocol implementations. + */ +class RouteMatcherNameValues { +public: + struct RouteMatcherTypeHash { + template std::size_t operator()(T t) const { return static_cast(t); } + }; + + using RouteMatcherNameMap = + absl::node_hash_map; + + const RouteMatcherNameMap routeMatcherNameMap = { + {RouteMatcherType::Default, "default"}, + }; + + const std::string& fromType(RouteMatcherType type) const { + const auto& itor = routeMatcherNameMap.find(type); + ASSERT(itor != routeMatcherNameMap.end()); + return itor->second; + } +}; + +using RouteMatcherNames = ConstSingleton; + +class RouteMatcher { +public: + virtual ~RouteMatcher() = default; + + virtual RouteConstSharedPtr route(const MessageMetadata& metadata, + uint64_t random_value) const PURE; +}; + +using RouteMatcherPtr = std::unique_ptr; +using RouteMatcherConstSharedPtr = std::shared_ptr; + +/** + * Implemented by each Jres protocol and registered via Registry::registerFactory or the + * convenience class RegisterFactory. + */ +class NamedRouteMatcherConfigFactory : public Envoy::Config::UntypedFactory { +public: + ~NamedRouteMatcherConfigFactory() override = default; + + /** + * Create a particular Jres protocol. + * @param serialization_type the serialization type of the protocol body. + * @return protocol instance pointer. + */ + virtual RouteMatcherPtr createRouteMatcher(const RouteConfigurations& route_configs, + Server::Configuration::FactoryContext& context) PURE; + + std::string category() const override { return "envoy.jres_proxy.route_matchers"; } + + /** + * Convenience method to lookup a factory by type. + * @param RouteMatcherType the protocol type. + * @return NamedRouteMatcherConfigFactory& for the RouteMatcherType. + */ + static NamedRouteMatcherConfigFactory& getFactory(RouteMatcherType type) { + const std::string& name = RouteMatcherNames::get().fromType(type); + return Envoy::Config::Utility::getAndCheckFactoryByName(name); + } +}; + +/** + * RouteMatcherFactoryBase provides a template for a trivial NamedProtocolConfigFactory. + */ +template +class RouteMatcherFactoryBase : public NamedRouteMatcherConfigFactory { +public: + RouteMatcherPtr createRouteMatcher(const RouteConfigurations& route_configs, + Server::Configuration::FactoryContext& context) override { + return std::make_unique(route_configs, context); + } + + std::string name() const override { return name_; } + +protected: + RouteMatcherFactoryBase(RouteMatcherType type) : name_(RouteMatcherNames::get().fromType(type)) {} + +private: + const std::string name_; +}; + +} // namespace Router +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/router/route_matcher.cc b/source/extensions/filters/network/jres_proxy/router/route_matcher.cc new file mode 100644 index 000000000000..667c404bae3c --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/router/route_matcher.cc @@ -0,0 +1,240 @@ +#include "extensions/filters/network/jres_proxy/router/route_matcher.h" + +#include "envoy/config/route/v3/route_components.pb.h" +#include "envoy/extensions/filters/network/jres_proxy/v3/route.pb.h" + +#include "common/protobuf/utility.h" + +#include "extensions/filters/network/jres_proxy/serializer_impl.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace Router { + +RouteEntryImplBase::RouteEntryImplBase( + const envoy::extensions::filters::network::jres_proxy::v3::Route& route) + : cluster_name_(route.route().cluster()), + config_headers_(Http::HeaderUtility::buildHeaderDataVector(route.match().headers())) { + if (route.route().cluster_specifier_case() == + envoy::extensions::filters::network::jres_proxy::v3::RouteAction::ClusterSpecifierCase:: + kWeightedClusters) { + total_cluster_weight_ = 0UL; + for (const auto& cluster : route.route().weighted_clusters().clusters()) { + weighted_clusters_.emplace_back(std::make_shared(*this, cluster)); + total_cluster_weight_ += weighted_clusters_.back()->clusterWeight(); + } + ENVOY_LOG(debug, "jres route matcher: weighted_clusters_size {}", weighted_clusters_.size()); + } +} + +const std::string& RouteEntryImplBase::clusterName() const { return cluster_name_; } + +const RouteEntry* RouteEntryImplBase::routeEntry() const { return this; } + +RouteConstSharedPtr RouteEntryImplBase::clusterEntry(uint64_t random_value) const { + if (weighted_clusters_.empty()) { + ENVOY_LOG(debug, "jres route matcher: weighted_clusters_size {}", weighted_clusters_.size()); + return shared_from_this(); + } + + return WeightedClusterUtil::pickCluster(weighted_clusters_, total_cluster_weight_, random_value, + false); +} + +bool RouteEntryImplBase::headersMatch(const Http::HeaderMap& headers) const { + ENVOY_LOG(debug, "jres route matcher: headers size {}, metadata headers size {}", + config_headers_.size(), headers.size()); + return Http::HeaderUtility::matchHeaders(headers, config_headers_); +} + +RouteEntryImplBase::WeightedClusterEntry::WeightedClusterEntry(const RouteEntryImplBase& parent, + const WeightedCluster& cluster) + : parent_(parent), cluster_name_(cluster.name()), + cluster_weight_(PROTOBUF_GET_WRAPPED_REQUIRED(cluster, weight)) {} + +ParameterRouteEntryImpl::ParameterRouteEntryImpl( + const envoy::extensions::filters::network::jres_proxy::v3::Route& route) + : RouteEntryImplBase(route) { + for (auto& config : route.match().method().params_match()) { + parameter_data_list_.emplace_back(config.first, config.second); + } +} + +ParameterRouteEntryImpl::~ParameterRouteEntryImpl() = default; + +bool ParameterRouteEntryImpl::matchParameter(absl::string_view request_data, + const ParameterData& config_data) const { + switch (config_data.match_type_) { + case Http::HeaderUtility::HeaderMatchType::Value: + return config_data.value_.empty() || request_data == config_data.value_; + case Http::HeaderUtility::HeaderMatchType::Range: { + int64_t value = 0; + return absl::SimpleAtoi(request_data, &value) && value >= config_data.range_.start() && + value < config_data.range_.end(); + } + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } +} + +RouteConstSharedPtr ParameterRouteEntryImpl::matches(const MessageMetadata& metadata, + uint64_t random_value) const { + ASSERT(metadata.hasInvocationInfo()); + const auto invocation = dynamic_cast(&metadata.invocationInfo()); + ASSERT(invocation); + if (!invocation->hasParameters()) { + return nullptr; + } + + ENVOY_LOG(debug, "jres route matcher: parameter name match"); + for (auto& config_data : parameter_data_list_) { + const std::string& data = invocation->getParameterValue(config_data.index_); + if (data.empty()) { + ENVOY_LOG(debug, + "jres route matcher: parameter matching failed, there are no parameters in the " + "user request, index '{}'", + config_data.index_); + return nullptr; + } + + if (!matchParameter(data, config_data)) { + ENVOY_LOG(debug, "jres route matcher: parameter matching failed, index '{}', value '{}'", + config_data.index_, data); + return nullptr; + } + } + + return clusterEntry(random_value); +} + +ParameterRouteEntryImpl::ParameterData::ParameterData(uint32_t index, + const ParameterMatchSpecifier& config) { + index_ = index; + switch (config.parameter_match_specifier_case()) { + case ParameterMatchSpecifier::kExactMatch: + match_type_ = Http::HeaderUtility::HeaderMatchType::Value; + value_ = config.exact_match(); + break; + case ParameterMatchSpecifier::kRangeMatch: + match_type_ = Http::HeaderUtility::HeaderMatchType::Range; + range_.set_start(config.range_match().start()); + range_.set_end(config.range_match().end()); + break; + default: + match_type_ = Http::HeaderUtility::HeaderMatchType::Value; + break; + } +} + +MethodRouteEntryImpl::MethodRouteEntryImpl( + const envoy::extensions::filters::network::jres_proxy::v3::Route& route) + : RouteEntryImplBase(route), method_name_(route.match().method().name()) { + if (route.match().method().params_match_size() != 0) { + parameter_route_ = std::make_shared(route); + } +} + +MethodRouteEntryImpl::~MethodRouteEntryImpl() = default; + +RouteConstSharedPtr MethodRouteEntryImpl::matches(const MessageMetadata& metadata, + uint64_t random_value) const { + ASSERT(metadata.hasInvocationInfo()); + const auto invocation = dynamic_cast(&metadata.invocationInfo()); + ASSERT(invocation); + + if (invocation->hasHeaders() && !RouteEntryImplBase::headersMatch(invocation->headers())) { + ENVOY_LOG(error, "jres route matcher: headers not match"); + return nullptr; + } + + if (invocation->methodName().empty()) { + ENVOY_LOG(error, "jres route matcher: there is no method name in the metadata"); + return nullptr; + } + + if (!method_name_.match(invocation->methodName())) { + ENVOY_LOG(debug, "jres route matcher: method matching failed, input method '{}'", + invocation->methodName()); + return nullptr; + } + + if (parameter_route_) { + ENVOY_LOG(debug, "jres route matcher: parameter matching is required"); + return parameter_route_->matches(metadata, random_value); + } + + return clusterEntry(random_value); +} + +SingleRouteMatcherImpl::SingleRouteMatcherImpl(const RouteConfig& config, + Server::Configuration::FactoryContext&) + : service_name_(config.interface()), group_(config.group()), version_(config.version()) { + using envoy::extensions::filters::network::jres_proxy::v3::RouteMatch; + + for (const auto& route : config.routes()) { + routes_.emplace_back(std::make_shared(route)); + } + ENVOY_LOG(debug, "jres route matcher: routes list size {}", routes_.size()); +} + +RouteConstSharedPtr SingleRouteMatcherImpl::route(const MessageMetadata& metadata, + uint64_t random_value) const { + ASSERT(metadata.hasInvocationInfo()); + const auto& invocation = metadata.invocationInfo(); + + if (service_name_ == invocation.serviceName() && + (group_.value().empty() || + (invocation.serviceGroup().has_value() && invocation.serviceGroup().value() == group_)) && + (version_.value().empty() || (invocation.serviceVersion().has_value() && + invocation.serviceVersion().value() == version_))) { + for (const auto& route : routes_) { + RouteConstSharedPtr route_entry = route->matches(metadata, random_value); + if (nullptr != route_entry) { + return route_entry; + } + } + } else { + ENVOY_LOG(debug, "jres route matcher: interface matching failed"); + } + + return nullptr; +} + +MultiRouteMatcher::MultiRouteMatcher(const RouteConfigList& route_config_list, + Server::Configuration::FactoryContext& context) { + for (const auto& route_config : route_config_list) { + route_matcher_list_.emplace_back( + std::make_unique(route_config, context)); + } + ENVOY_LOG(debug, "route matcher list size {}", route_matcher_list_.size()); +} + +RouteConstSharedPtr MultiRouteMatcher::route(const MessageMetadata& metadata, + uint64_t random_value) const { + for (const auto& route_matcher : route_matcher_list_) { + auto route = route_matcher->route(metadata, random_value); + if (nullptr != route) { + return route; + } + } + + return nullptr; +} + +class DefaultRouteMatcherConfigFactory : public RouteMatcherFactoryBase { +public: + DefaultRouteMatcherConfigFactory() : RouteMatcherFactoryBase(RouteMatcherType::Default) {} +}; + +/** + * Static registration for the Jres protocol. @see RegisterFactory. + */ +REGISTER_FACTORY(DefaultRouteMatcherConfigFactory, NamedRouteMatcherConfigFactory); + +} // namespace Router +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/router/route_matcher.h b/source/extensions/filters/network/jres_proxy/router/route_matcher.h new file mode 100644 index 000000000000..db406973ff84 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/router/route_matcher.h @@ -0,0 +1,161 @@ +#pragma once + +#include +#include +#include + +#include "envoy/config/route/v3/route_components.pb.h" +#include "envoy/extensions/filters/network/jres_proxy/v3/route.pb.h" +#include "envoy/type/v3/range.pb.h" + +#include "common/common/logger.h" +#include "common/common/matchers.h" +#include "common/http/header_utility.h" +#include "common/protobuf/protobuf.h" + +#include "extensions/filters/network/jres_proxy/metadata.h" +#include "extensions/filters/network/jres_proxy/router/route.h" +#include "extensions/filters/network/jres_proxy/router/router.h" + +#include "absl/types/optional.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace Router { + +class RouteEntryImplBase : public RouteEntry, + public Route, + public std::enable_shared_from_this, + public Logger::Loggable { +public: + RouteEntryImplBase(const envoy::extensions::filters::network::jres_proxy::v3::Route& route); + ~RouteEntryImplBase() override = default; + + // Router::RouteEntry + const std::string& clusterName() const override; + const Envoy::Router::MetadataMatchCriteria* metadataMatchCriteria() const override { + return metadata_match_criteria_.get(); + } + + // Router::Route + const RouteEntry* routeEntry() const override; + + virtual RouteConstSharedPtr matches(const MessageMetadata& metadata, + uint64_t random_value) const PURE; + +protected: + RouteConstSharedPtr clusterEntry(uint64_t random_value) const; + bool headersMatch(const Http::HeaderMap& headers) const; + +private: + class WeightedClusterEntry : public RouteEntry, public Route { + public: + using WeightedCluster = envoy::config::route::v3::WeightedCluster::ClusterWeight; + WeightedClusterEntry(const RouteEntryImplBase& parent, const WeightedCluster& cluster); + + uint64_t clusterWeight() const { return cluster_weight_; } + + // Router::RouteEntry + const std::string& clusterName() const override { return cluster_name_; } + const Envoy::Router::MetadataMatchCriteria* metadataMatchCriteria() const override { + return metadata_match_criteria_ ? metadata_match_criteria_.get() + : parent_.metadataMatchCriteria(); + } + + // Router::Route + const RouteEntry* routeEntry() const override { return this; } + + private: + const RouteEntryImplBase& parent_; + const std::string cluster_name_; + const uint64_t cluster_weight_; + Envoy::Router::MetadataMatchCriteriaConstPtr metadata_match_criteria_; + }; + + using WeightedClusterEntrySharedPtr = std::shared_ptr; + + uint64_t total_cluster_weight_; + const std::string cluster_name_; + const std::vector config_headers_; + std::vector weighted_clusters_; + + // TODO(gengleilei) Implement it. + Envoy::Router::MetadataMatchCriteriaConstPtr metadata_match_criteria_; +}; + +using RouteEntryImplBaseConstSharedPtr = std::shared_ptr; + +class ParameterRouteEntryImpl : public RouteEntryImplBase { +public: + ParameterRouteEntryImpl(const envoy::extensions::filters::network::jres_proxy::v3::Route& route); + ~ParameterRouteEntryImpl() override; + + struct ParameterData { + using ParameterMatchSpecifier = + envoy::extensions::filters::network::jres_proxy::v3::MethodMatch::ParameterMatchSpecifier; + ParameterData(uint32_t index, const ParameterMatchSpecifier& config); + + Http::HeaderUtility::HeaderMatchType match_type_; + std::string value_; + envoy::type::v3::Int64Range range_; + uint32_t index_; + }; + + // RoutEntryImplBase + RouteConstSharedPtr matches(const MessageMetadata& metadata, + uint64_t random_value) const override; + +private: + bool matchParameter(absl::string_view request_data, const ParameterData& config_data) const; + + std::vector parameter_data_list_; +}; + +class MethodRouteEntryImpl : public RouteEntryImplBase { +public: + MethodRouteEntryImpl(const envoy::extensions::filters::network::jres_proxy::v3::Route& route); + ~MethodRouteEntryImpl() override; + + // RoutEntryImplBase + RouteConstSharedPtr matches(const MessageMetadata& metadata, + uint64_t random_value) const override; + +private: + const Matchers::StringMatcherImpl method_name_; + std::shared_ptr parameter_route_; +}; + +class SingleRouteMatcherImpl : public RouteMatcher, public Logger::Loggable { +public: + using RouteConfig = envoy::extensions::filters::network::jres_proxy::v3::RouteConfiguration; + SingleRouteMatcherImpl(const RouteConfig& config, Server::Configuration::FactoryContext& context); + + RouteConstSharedPtr route(const MessageMetadata& metadata, uint64_t random_value) const override; + +private: + std::vector routes_; + const std::string service_name_; + const absl::optional group_; + const absl::optional version_; +}; + +class MultiRouteMatcher : public RouteMatcher, public Logger::Loggable { +public: + using RouteConfigList = Envoy::Protobuf::RepeatedPtrField< + envoy::extensions::filters::network::jres_proxy::v3::RouteConfiguration>; + MultiRouteMatcher(const RouteConfigList& route_config_list, + Server::Configuration::FactoryContext& context); + + RouteConstSharedPtr route(const MessageMetadata& metadata, uint64_t random_value) const override; + +private: + std::vector route_matcher_list_; +}; + +} // namespace Router +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/router/router.h b/source/extensions/filters/network/jres_proxy/router/router.h new file mode 100644 index 000000000000..5b1a417525b2 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/router/router.h @@ -0,0 +1,77 @@ +#pragma once + +#include +#include + +#include "envoy/router/router.h" + +#include "extensions/filters/network/jres_proxy/metadata.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace Router { + +/** + * RouteEntry is an individual resolved route entry. + */ +class RouteEntry { +public: + virtual ~RouteEntry() = default; + + /** + * @return const std::string& the upstream cluster that owns the route. + */ + virtual const std::string& clusterName() const PURE; + + /** + * @return MetadataMatchCriteria* the metadata that a subset load balancer should match when + * selecting an upstream host + */ + virtual const Envoy::Router::MetadataMatchCriteria* metadataMatchCriteria() const PURE; +}; + +using RouteEntryPtr = std::shared_ptr; + +/** + * Route holds the RouteEntry for a request. + */ +class Route { +public: + virtual ~Route() = default; + + /** + * @return the route entry or nullptr if there is no matching route for the request. + */ + virtual const RouteEntry* routeEntry() const PURE; +}; + +using RouteConstSharedPtr = std::shared_ptr; +using RouteSharedPtr = std::shared_ptr; + +/** + * The router configuration. + */ +class Config { +public: + virtual ~Config() = default; + + /** + * Based on the incoming Jres request transport and/or protocol data, determine the target + * route for the request. + * @param metadata MessageMetadata for the message to route + * @param random_value uint64_t used to select cluster affinity + * @return the route or nullptr if there is no matching route for the request. + */ + virtual RouteConstSharedPtr route(const MessageMetadata& metadata, + uint64_t random_value) const PURE; +}; + +using ConfigConstSharedPtr = std::shared_ptr; + +} // namespace Router +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/router/router_impl.cc b/source/extensions/filters/network/jres_proxy/router/router_impl.cc new file mode 100644 index 000000000000..eeb2ec5032f3 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/router/router_impl.cc @@ -0,0 +1,321 @@ +#include "extensions/filters/network/jres_proxy/router/router_impl.h" + +#include "envoy/upstream/cluster_manager.h" +#include "envoy/upstream/thread_local_cluster.h" + +#include "extensions/filters/network/jres_proxy/app_exception.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace Router { + +void Router::onDestroy() { + if (upstream_request_) { + upstream_request_->resetStream(); + } + cleanup(); +} + +void Router::setDecoderFilterCallbacks(JresFilters::DecoderFilterCallbacks& callbacks) { + callbacks_ = &callbacks; +} + +FilterStatus Router::onMessageDecoded(MessageMetadataSharedPtr metadata, ContextSharedPtr ctx) { + ASSERT(metadata->hasInvocationInfo()); + const auto& invocation = metadata->invocationInfo(); + + route_ = callbacks_->route(); + if (!route_) { + ENVOY_STREAM_LOG(debug, "jres router: no cluster match for interface '{}'", *callbacks_, + invocation.serviceName()); + callbacks_->sendLocalReply(AppException(ResponseStatus::ServiceNotFound, + fmt::format("jres router: no route for interface '{}'", + invocation.serviceName())), + false); + return FilterStatus::StopIteration; + } + + route_entry_ = route_->routeEntry(); + + Upstream::ThreadLocalCluster* cluster = + cluster_manager_.getThreadLocalCluster(route_entry_->clusterName()); + if (!cluster) { + ENVOY_STREAM_LOG(debug, "jres router: unknown cluster '{}'", *callbacks_, + route_entry_->clusterName()); + callbacks_->sendLocalReply( + AppException(ResponseStatus::ServerError, + fmt::format("jres router: unknown cluster '{}'", route_entry_->clusterName())), + false); + return FilterStatus::StopIteration; + } + + cluster_ = cluster->info(); + ENVOY_STREAM_LOG(debug, "jres router: cluster '{}' match for interface '{}'", *callbacks_, + route_entry_->clusterName(), invocation.serviceName()); + + if (cluster_->maintenanceMode()) { + callbacks_->sendLocalReply( + AppException(ResponseStatus::ServerError, + fmt::format("jres router: maintenance mode for cluster '{}'", + route_entry_->clusterName())), + false); + return FilterStatus::StopIteration; + } + + Tcp::ConnectionPool::Instance* conn_pool = + cluster->tcpConnPool(Upstream::ResourcePriority::Default, this); + if (!conn_pool) { + callbacks_->sendLocalReply(AppException(ResponseStatus::ServerError, + fmt::format("jres router: no healthy upstream for '{}'", + route_entry_->clusterName())), + false); + return FilterStatus::StopIteration; + } + + ENVOY_STREAM_LOG(debug, "jres router: decoding request", *callbacks_); + upstream_request_buffer_.move(ctx->messageOriginData(), ctx->messageSize()); + + upstream_request_ = std::make_unique( + *this, *conn_pool, metadata, callbacks_->serializationType(), callbacks_->protocolType()); + return upstream_request_->start(); +} + +void Router::onUpstreamData(Buffer::Instance& data, bool end_stream) { + ASSERT(!upstream_request_->response_complete_); + + ENVOY_STREAM_LOG(trace, "jres router: reading response: {} bytes", *callbacks_, data.length()); + + // Handle normal response. + if (!upstream_request_->response_started_) { + callbacks_->startUpstreamResponse(); + upstream_request_->response_started_ = true; + } + + JresFilters::UpstreamResponseStatus status = callbacks_->upstreamData(data); + if (status == JresFilters::UpstreamResponseStatus::Complete) { + ENVOY_STREAM_LOG(debug, "jres router: response complete", *callbacks_); + upstream_request_->onResponseComplete(); + cleanup(); + return; + } else if (status == JresFilters::UpstreamResponseStatus::Reset) { + ENVOY_STREAM_LOG(debug, "jres router: upstream reset", *callbacks_); + // When the upstreamData function returns Reset, + // the current stream is already released from the upper layer, + // so there is no need to call callbacks_->resetStream() to notify + // the upper layer to release the stream. + upstream_request_->resetStream(); + return; + } + + if (end_stream) { + // Response is incomplete, but no more data is coming. + ENVOY_STREAM_LOG(debug, "jres router: response underflow", *callbacks_); + upstream_request_->onResetStream(ConnectionPool::PoolFailureReason::RemoteConnectionFailure); + upstream_request_->onResponseComplete(); + cleanup(); + } +} + +void Router::onEvent(Network::ConnectionEvent event) { + if (!upstream_request_ || upstream_request_->response_complete_) { + // Client closed connection after completing response. + ENVOY_LOG(debug, "jres upstream request: the upstream request had completed"); + return; + } + + if (upstream_request_->stream_reset_ && event == Network::ConnectionEvent::LocalClose) { + ENVOY_LOG(debug, "jres upstream request: the stream reset"); + return; + } + + switch (event) { + case Network::ConnectionEvent::RemoteClose: + upstream_request_->onResetStream(ConnectionPool::PoolFailureReason::RemoteConnectionFailure); + break; + case Network::ConnectionEvent::LocalClose: + upstream_request_->onResetStream(ConnectionPool::PoolFailureReason::LocalConnectionFailure); + break; + default: + // Connected is consumed by the connection pool. + NOT_REACHED_GCOVR_EXCL_LINE; + } +} + +const Network::Connection* Router::downstreamConnection() const { + return callbacks_ != nullptr ? callbacks_->connection() : nullptr; +} + +void Router::cleanup() { + if (upstream_request_) { + upstream_request_.reset(); + } +} + +Router::UpstreamRequest::UpstreamRequest(Router& parent, Tcp::ConnectionPool::Instance& pool, + MessageMetadataSharedPtr& metadata, + SerializationType serialization_type, + ProtocolType protocol_type) + : parent_(parent), conn_pool_(pool), metadata_(metadata), + protocol_( + NamedProtocolConfigFactory::getFactory(protocol_type).createProtocol(serialization_type)), + request_complete_(false), response_started_(false), response_complete_(false), + stream_reset_(false) {} + +Router::UpstreamRequest::~UpstreamRequest() = default; + +FilterStatus Router::UpstreamRequest::start() { + Tcp::ConnectionPool::Cancellable* handle = conn_pool_.newConnection(*this); + if (handle) { + // Pause while we wait for a connection. + conn_pool_handle_ = handle; + return FilterStatus::StopIteration; + } + + return FilterStatus::Continue; +} + +void Router::UpstreamRequest::resetStream() { + stream_reset_ = true; + + if (conn_pool_handle_) { + ASSERT(!conn_data_); + conn_pool_handle_->cancel(Tcp::ConnectionPool::CancelPolicy::Default); + conn_pool_handle_ = nullptr; + ENVOY_LOG(debug, "jres upstream request: reset connection pool handler"); + } + + if (conn_data_) { + ASSERT(!conn_pool_handle_); + conn_data_->connection().close(Network::ConnectionCloseType::NoFlush); + conn_data_.reset(); + ENVOY_LOG(debug, "jres upstream request: reset connection data"); + } +} + +void Router::UpstreamRequest::encodeData(Buffer::Instance& data) { + ASSERT(conn_data_); + ASSERT(!conn_pool_handle_); + + ENVOY_STREAM_LOG(trace, "proxying {} bytes", *parent_.callbacks_, data.length()); + conn_data_->connection().write(data, false); +} + +void Router::UpstreamRequest::onPoolFailure(ConnectionPool::PoolFailureReason reason, + Upstream::HostDescriptionConstSharedPtr host) { + conn_pool_handle_ = nullptr; + + // Mimic an upstream reset. + onUpstreamHostSelected(host); + onResetStream(reason); + + parent_.upstream_request_buffer_.drain(parent_.upstream_request_buffer_.length()); + + // If it is a connection error, it means that the connection pool returned + // the error asynchronously and the upper layer needs to be notified to continue decoding. + // If it is a non-connection error, it is returned synchronously from the connection pool + // and is still in the callback at the current Filter, nothing to do. + if (reason == ConnectionPool::PoolFailureReason::Timeout || + reason == ConnectionPool::PoolFailureReason::LocalConnectionFailure || + reason == ConnectionPool::PoolFailureReason::RemoteConnectionFailure) { + parent_.callbacks_->continueDecoding(); + } +} + +void Router::UpstreamRequest::onPoolReady(Tcp::ConnectionPool::ConnectionDataPtr&& conn_data, + Upstream::HostDescriptionConstSharedPtr host) { + ENVOY_LOG(debug, "jres upstream request: tcp connection has ready"); + + // Only invoke continueDecoding if we'd previously stopped the filter chain. + bool continue_decoding = conn_pool_handle_ != nullptr; + + onUpstreamHostSelected(host); + conn_data_ = std::move(conn_data); + conn_data_->addUpstreamCallbacks(parent_); + conn_pool_handle_ = nullptr; + + onRequestStart(continue_decoding); + encodeData(parent_.upstream_request_buffer_); +} + +void Router::UpstreamRequest::onRequestStart(bool continue_decoding) { + ENVOY_LOG(debug, "jres upstream request: start sending data to the server {}", + upstream_host_->address()->asString()); + + if (continue_decoding) { + parent_.callbacks_->continueDecoding(); + } + onRequestComplete(); +} + +void Router::UpstreamRequest::onRequestComplete() { request_complete_ = true; } + +void Router::UpstreamRequest::onResponseComplete() { + response_complete_ = true; + conn_data_.reset(); +} + +void Router::UpstreamRequest::onUpstreamHostSelected(Upstream::HostDescriptionConstSharedPtr host) { + ENVOY_LOG(debug, "jres upstream request: selected upstream {}", host->address()->asString()); + upstream_host_ = host; +} + +void Router::UpstreamRequest::onResetStream(ConnectionPool::PoolFailureReason reason) { + if (metadata_->messageType() == MessageType::Oneway) { + // For oneway requests, we should not attempt a response. Reset the downstream to signal + // an error. + ENVOY_LOG(debug, "jres upstream request: the request is oneway, reset downstream stream"); + parent_.callbacks_->resetStream(); + return; + } + + // When the filter's callback does not end, the sendLocalReply function call + // triggers the release of the current stream at the end of the filter's callback. + switch (reason) { + case ConnectionPool::PoolFailureReason::Overflow: + parent_.callbacks_->sendLocalReply( + AppException(ResponseStatus::ServerError, + fmt::format("jres upstream request: too many connections")), + false); + break; + case ConnectionPool::PoolFailureReason::LocalConnectionFailure: + // Should only happen if we closed the connection, due to an error condition, in which case + // we've already handled any possible downstream response. + parent_.callbacks_->sendLocalReply( + AppException(ResponseStatus::ServerError, + fmt::format("jres upstream request: local connection failure '{}'", + upstream_host_->address()->asString())), + false); + break; + case ConnectionPool::PoolFailureReason::RemoteConnectionFailure: + parent_.callbacks_->sendLocalReply( + AppException(ResponseStatus::ServerError, + fmt::format("jres upstream request: remote connection failure '{}'", + upstream_host_->address()->asString())), + false); + break; + case ConnectionPool::PoolFailureReason::Timeout: + parent_.callbacks_->sendLocalReply( + AppException(ResponseStatus::ServerError, + fmt::format("jres upstream request: connection failure '{}' due to timeout", + upstream_host_->address()->asString())), + false); + break; + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } + + if (parent_.filter_complete_ && !response_complete_) { + // When the filter's callback has ended and the reply message has not been processed, + // call resetStream to release the current stream. + // the resetStream eventually triggers the onDestroy function call. + parent_.callbacks_->resetStream(); + } +} + +} // namespace Router +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/router/router_impl.h b/source/extensions/filters/network/jres_proxy/router/router_impl.h new file mode 100644 index 000000000000..0956a95c130d --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/router/router_impl.h @@ -0,0 +1,103 @@ +#pragma once + +#include +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/tcp/conn_pool.h" + +#include "common/common/logger.h" +#include "common/upstream/load_balancer_impl.h" + +#include "extensions/filters/network/jres_proxy/filters/filter.h" +#include "extensions/filters/network/jres_proxy/router/router.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { +namespace Router { + +class Router : public Tcp::ConnectionPool::UpstreamCallbacks, + public Upstream::LoadBalancerContextBase, + public JresFilters::DecoderFilter, + Logger::Loggable { +public: + Router(Upstream::ClusterManager& cluster_manager) : cluster_manager_(cluster_manager) {} + ~Router() override = default; + + // JresFilters::DecoderFilter + void onDestroy() override; + void setDecoderFilterCallbacks(JresFilters::DecoderFilterCallbacks& callbacks) override; + + FilterStatus onMessageDecoded(MessageMetadataSharedPtr metadata, ContextSharedPtr ctx) override; + + // Upstream::LoadBalancerContextBase + const Envoy::Router::MetadataMatchCriteria* metadataMatchCriteria() override { return nullptr; } + const Network::Connection* downstreamConnection() const override; + + // Tcp::ConnectionPool::UpstreamCallbacks + void onUpstreamData(Buffer::Instance& data, bool end_stream) override; + void onEvent(Network::ConnectionEvent event) override; + void onAboveWriteBufferHighWatermark() override {} + void onBelowWriteBufferLowWatermark() override {} + +private: + struct UpstreamRequest : public Tcp::ConnectionPool::Callbacks { + UpstreamRequest(Router& parent, Tcp::ConnectionPool::Instance& pool, + MessageMetadataSharedPtr& metadata, SerializationType serialization_type, + ProtocolType protocol_type); + ~UpstreamRequest() override; + + FilterStatus start(); + void resetStream(); + void encodeData(Buffer::Instance& data); + + // Tcp::ConnectionPool::Callbacks + void onPoolFailure(ConnectionPool::PoolFailureReason reason, + Upstream::HostDescriptionConstSharedPtr host) override; + void onPoolReady(Tcp::ConnectionPool::ConnectionDataPtr&& conn, + Upstream::HostDescriptionConstSharedPtr host) override; + + void onRequestStart(bool continue_decoding); + void onRequestComplete(); + void onResponseComplete(); + void onUpstreamHostSelected(Upstream::HostDescriptionConstSharedPtr host); + void onResetStream(ConnectionPool::PoolFailureReason reason); + + Router& parent_; + Tcp::ConnectionPool::Instance& conn_pool_; + MessageMetadataSharedPtr metadata_; + + Tcp::ConnectionPool::Cancellable* conn_pool_handle_{}; + Tcp::ConnectionPool::ConnectionDataPtr conn_data_; + Upstream::HostDescriptionConstSharedPtr upstream_host_; + SerializerPtr serializer_; + ProtocolPtr protocol_; + + bool request_complete_ : 1; + bool response_started_ : 1; + bool response_complete_ : 1; + bool stream_reset_ : 1; + }; + + void cleanup(); + + Upstream::ClusterManager& cluster_manager_; + + JresFilters::DecoderFilterCallbacks* callbacks_{}; + RouteConstSharedPtr route_{}; + const RouteEntry* route_entry_{}; + Upstream::ClusterInfoConstSharedPtr cluster_; + + std::unique_ptr upstream_request_; + Envoy::Buffer::OwnedImpl upstream_request_buffer_; + + bool filter_complete_{false}; +}; + +} // namespace Router +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/serializer.h b/source/extensions/filters/network/jres_proxy/serializer.h new file mode 100644 index 000000000000..95b2f1aecc5c --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/serializer.h @@ -0,0 +1,125 @@ +#pragma once + +#include + +#include "envoy/buffer/buffer.h" +#include "envoy/config/typed_config.h" + +#include "common/common/assert.h" +#include "common/config/utility.h" +#include "common/singleton/const_singleton.h" + +#include "extensions/filters/network/jres_proxy/message.h" +#include "extensions/filters/network/jres_proxy/metadata.h" +#include "extensions/filters/network/jres_proxy/protocol_constants.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +class Serializer { +public: + virtual ~Serializer() = default; + + /** + * Return this Serializer's name + * + * @return std::string containing the serialization name. + */ + virtual const std::string& name() const PURE; + + /** + * @return SerializationType the serializer type + */ + virtual SerializationType type() const PURE; + + /** + * deserialize an rpc call + * If successful, the RpcInvocation removed from the buffer + * + * @param buffer the currently buffered jres data + * @param context context information for RPC messages + * @return a pair containing the deserialized result of the message and the deserialized + * invocation information. + * @throws EnvoyException if the data is not valid for this serialization + */ + virtual std::pair + deserializeRpcInvocation(Buffer::Instance& buffer, ContextSharedPtr context) PURE; + + /** + * deserialize result of an rpc call + * + * @param buffer the currently buffered jres data + * @param context context information for RPC messages + * @return a pair containing the deserialized result of the message and the deserialized + * result information. + * @throws EnvoyException if the data is not valid for this serialization + */ + virtual std::pair deserializeRpcResult(Buffer::Instance& buffer, + ContextSharedPtr context) PURE; + + /** + * serialize result of an rpc call + * If successful, the output_buffer is written to the serialized data + * + * @param output_buffer store the serialized data + * @param content the rpc response content + * @param type the rpc response type + * @return size_t the length of the serialized content + */ + virtual size_t serializeRpcResult(Buffer::Instance& output_buffer, const std::string& content, + RpcResponseType type) PURE; +}; + +using SerializerPtr = std::unique_ptr; + +/** + * Implemented by each Jres serialize and registered via Registry::registerFactory or the + * convenience class RegisterFactory. + */ +class NamedSerializerConfigFactory : public Config::UntypedFactory { +public: + ~NamedSerializerConfigFactory() override = default; + + /** + * Create a particular Jres serializer. + * @return SerializerPtr the transport + */ + virtual SerializerPtr createSerializer() PURE; + + std::string category() const override { return "envoy.jres_proxy.serializers"; } + + /** + * Convenience method to lookup a factory by type. + * @param TransportType the transport type + * @return NamedSerializerConfigFactory& for the TransportType + */ + static NamedSerializerConfigFactory& getFactory(ProtocolType protocol_type, + SerializationType type) { + const std::string& name = ProtocolSerializerNames::get().fromType(protocol_type, type); + return Envoy::Config::Utility::getAndCheckFactoryByName(name); + } +}; + +/** + * SerializerFactoryBase provides a template for a trivial NamedSerializerConfigFactory. + */ +template class SerializerFactoryBase : public NamedSerializerConfigFactory { +public: + SerializerPtr createSerializer() override { return std::make_unique(); } + + std::string name() const override { return name_; } + +protected: + SerializerFactoryBase(ProtocolType protocol_type, SerializationType type) + : name_(ProtocolSerializerNames::get().fromType(protocol_type, type)) {} + +private: + const std::string name_; +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/serializer_impl.cc b/source/extensions/filters/network/jres_proxy/serializer_impl.cc new file mode 100644 index 000000000000..4c4d0f384125 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/serializer_impl.cc @@ -0,0 +1,48 @@ +#include "extensions/filters/network/jres_proxy/serializer_impl.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +void RpcInvocationImpl::addParameterValue(uint32_t index, const std::string& value) { + assignParameterIfNeed(); + parameter_map_->emplace(index, value); +} + +const std::string& RpcInvocationImpl::getParameterValue(uint32_t index) const { + if (parameter_map_) { + auto itor = parameter_map_->find(index); + if (itor != parameter_map_->end()) { + return itor->second; + } + } + + return EMPTY_STRING; +} + +const RpcInvocationImpl::ParameterValueMap& RpcInvocationImpl::parameters() { + ASSERT(hasParameters()); + return *parameter_map_; +} + +const Http::HeaderMap& RpcInvocationImpl::headers() const { + ASSERT(hasHeaders()); + return *headers_; +} + +void RpcInvocationImpl::addHeader(const std::string& key, const std::string& value) { + assignHeaderIfNeed(); + headers_->addCopy(Http::LowerCaseString(key), value); +} + +void RpcInvocationImpl::addHeaderReference(const Http::LowerCaseString& key, + const std::string& value) { + assignHeaderIfNeed(); + headers_->addReference(key, value); +} + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/serializer_impl.h b/source/extensions/filters/network/jres_proxy/serializer_impl.h new file mode 100644 index 000000000000..3610a242ad97 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/serializer_impl.h @@ -0,0 +1,62 @@ +#pragma once + +#include "extensions/filters/network/jres_proxy/message_impl.h" +#include "extensions/filters/network/jres_proxy/serializer.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +class RpcInvocationImpl : public RpcInvocationBase { +public: + // TODO(gengleilei) Add parameter data types and implement Jres data type mapping. + using ParameterValueMap = absl::node_hash_map; + using ParameterValueMapPtr = std::unique_ptr; + + RpcInvocationImpl() = default; + ~RpcInvocationImpl() override = default; + + void addParameterValue(uint32_t index, const std::string& value); + const ParameterValueMap& parameters(); + const std::string& getParameterValue(uint32_t index) const; + bool hasParameters() const { return parameter_map_ != nullptr; } + + void addHeader(const std::string& key, const std::string& value); + void addHeaderReference(const Http::LowerCaseString& key, const std::string& value); + const Http::HeaderMap& headers() const; + bool hasHeaders() const { return headers_ != nullptr; } + +private: + inline void assignHeaderIfNeed() { + if (!headers_) { + headers_ = Http::RequestHeaderMapImpl::create(); + } + } + + inline void assignParameterIfNeed() { + if (!parameter_map_) { + parameter_map_ = std::make_unique(); + } + } + + ParameterValueMapPtr parameter_map_; + Http::HeaderMapPtr headers_; // attachment +}; + +class RpcResultImpl : public RpcResult { +public: + RpcResultImpl() = default; + ~RpcResultImpl() override = default; + + bool hasException() const override { return has_exception_; } + void setException(bool has_exception) { has_exception_ = has_exception; } + +private: + bool has_exception_ = false; +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/jres_proxy/stats.h b/source/extensions/filters/network/jres_proxy/stats.h new file mode 100644 index 000000000000..dc029aae5ef7 --- /dev/null +++ b/source/extensions/filters/network/jres_proxy/stats.h @@ -0,0 +1,54 @@ +#pragma once + +#include + +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace JresProxy { + +/** + * All jres filter stats. @see stats_macros.h + */ +#define ALL_JRES_FILTER_STATS(COUNTER, GAUGE, HISTOGRAM) \ + COUNTER(cx_destroy_local_with_active_rq) \ + COUNTER(cx_destroy_remote_with_active_rq) \ + COUNTER(local_response_business_exception) \ + COUNTER(local_response_error) \ + COUNTER(local_response_success) \ + COUNTER(request) \ + COUNTER(request_decoding_error) \ + COUNTER(request_decoding_success) \ + COUNTER(request_event) \ + COUNTER(request_oneway) \ + COUNTER(request_twoway) \ + COUNTER(response) \ + COUNTER(response_business_exception) \ + COUNTER(response_decoding_error) \ + COUNTER(response_decoding_success) \ + COUNTER(response_error) \ + COUNTER(response_error_caused_connection_close) \ + COUNTER(response_success) \ + GAUGE(request_active, Accumulate) \ + HISTOGRAM(request_time_ms, Milliseconds) + +/** + * Struct definition for all jres proxy stats. @see stats_macros.h + */ +struct JresFilterStats { + ALL_JRES_FILTER_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT, GENERATE_HISTOGRAM_STRUCT) + + static JresFilterStats generateStats(const std::string& prefix, Stats::Scope& scope) { + return JresFilterStats{ALL_JRES_FILTER_STATS(POOL_COUNTER_PREFIX(scope, prefix), + POOL_GAUGE_PREFIX(scope, prefix), + POOL_HISTOGRAM_PREFIX(scope, prefix))}; + } +}; + +} // namespace JresProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/network/well_known_names.h b/source/extensions/filters/network/well_known_names.h index 7f62193c7896..ec51e4840f5e 100644 --- a/source/extensions/filters/network/well_known_names.h +++ b/source/extensions/filters/network/well_known_names.h @@ -22,6 +22,8 @@ class NetworkFilterNameValues { const std::string RocketmqProxy = "envoy.filters.network.rocketmq_proxy"; // Dubbo proxy filter const std::string DubboProxy = "envoy.filters.network.dubbo_proxy"; + // Jres proxy filter + const std::string JresProxy = "envoy.filters.network.jres_proxy"; // HTTP connection manager filter const std::string HttpConnectionManager = "envoy.filters.network.http_connection_manager"; // Local rate limit filter diff --git a/tools/api/generate_go_protobuf.py b/tools/api/generate_go_protobuf.py index 5b25de2dbb0a..158a0a5d4716 100755 --- a/tools/api/generate_go_protobuf.py +++ b/tools/api/generate_go_protobuf.py @@ -16,10 +16,10 @@ IMPORT_BASE = 'github.com/envoyproxy/go-control-plane' OUTPUT_BASE = 'build_go' REPO_BASE = 'go-control-plane' -BRANCH = 'master' +BRANCH = 'istio1.9.2' MIRROR_MSG = 'Mirrored from envoyproxy/envoy @ ' -USER_NAME = 'go-control-plane(Azure Pipelines)' -USER_EMAIL = 'go-control-plane@users.noreply.github.com' +USER_NAME = 'FeiYing9' +USER_EMAIL = '605692769@qq.com' def generateProtobufs(output): @@ -71,7 +71,7 @@ def git(repo, *args): def cloneGoProtobufs(repo): # Create a local clone of go-control-plane - git(None, 'clone', 'git@github.com:envoyproxy/go-control-plane', repo, '-b', BRANCH) + git(None, 'clone', 'git@github.com:FeiYing9/go-control-plane', repo, '-b', BRANCH) def findLastSyncSHA(repo): diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index ecf2e82a8c2a..c212140cf89c 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -29,6 +29,7 @@ CEL DSR HEXDIG HEXDIGIT +JRES OWS Preconnecting STATNAME