From bcb6cf2431323c538568b1223610bce646147266 Mon Sep 17 00:00:00 2001 From: qinggniq Date: Fri, 15 Jan 2021 17:02:54 +0800 Subject: [PATCH] feat bug Signed-off-by: qinggniq add auth helper Signed-off-by: qinggniq refactor mysql server greeting codec Signed-off-by: qinggniq codec encode Signed-off-by: qinggniq add unit test for mysql greeting codec Signed-off-by: qinggniq complete login resp codec Signed-off-by: qinggniq add login message test Signed-off-by: qinggniq feat link error Signed-off-by: qinggniq refactor mysql login resp Signed-off-by: qinggniq pass all codec test Signed-off-by: qinggniq only contain codec change Signed-off-by: qinggniq remove header Signed-off-by: qinggniq http: expose encoded headers/trailers via callbacks (#14544) In order to support upstream filters passing ownership of headers and then being able to reference them after the fact, expose a HTTP filter function that allows reading the header maps back. Signed-off-by: Snow Pettersen Implement request header processing in ext_proc (#14385) Send request headers to the server and apply header mutations based on the response. The rest of the protocol is still ignored. Signed-off-by: Gregory Brail 1.17.0 release (#14624) Signed-off-by: Matt Klein kick off v1.18.0 (#14637) Signed-off-by: Matt Klein filter manager: drop assert (#14633) Signed-off-by: Raul Gutierrez Segales docs: update ext_proc docs to reflect implementation status (#14636) Signed-off-by: Gregory Brail [tls] Expose ServerContextImpl::selectTlsContext (#14592) Signed-off-by: Chad Retz http: support creating filters with match tree (#14430) Adds support for wrapping a HTTP filter with an ExtensionWithMatcher proto to create the filters with an associated match tree. Under the hood this makes use of a wrapper filter factory that manages creating the match tree and adding it to the FM alongside the associated filter. Also includes some code to register factories for input/actions, allowing them to be referenced in the proto configuration. Signed-off-by: Snow Pettersen fix empty connection debug logs (#14666) Fixes #14661 Signed-off-by: Rama Chavali tcp_proxy: ignore transfer encoding in HTTP/1.1 CONNECT responses (#14623) Commit Message: Ignore the transfer encoding header in CONNECT responses Additional Description: NONE Risk Level: low Testing: integration test Docs Changes: NONE Release Notes: https://github.com/irozzo-1A/envoy/blob/ignore-transfer-encoding/docs/root/version_history/current.rst#new-features Platform Specific Features: NONE Fixes #11308 Signed-off-by: Iacopo Rozzo ci: fix docs tag build (#14653) Signed-off-by: Lizan Zhou HTTP health checker: handle GOAWAY from HTTP2 upstreams (#13599) Makes the HTTP health checker handle GOAWAY properly. When the NO_ERROR code is received, any in flight request will be allowed to complete, at which time the connection will be closed and a new connection created on the next interval. GOAWAY frames with codes other than NO_ERROR are treated as a health check failure, and immediately close the connection. Signed-off-by: Michael Puncel upstream: clean up feature parsing code (#14629) Fixing a perfectly safe and fairly terrible version merge in the ALPN pr the "refactor all upstream config" PRs. the original code created the new options for new config, and parseFeatures handled parsing features from either the new options, or the old config. I decided that was too complicated, changed the code to always create the new options struct and forgot to clean up parseFeatures to assume the presence of the new options struct and remove handling things the old style way. Risk Level: low (clean up inaccessible code) Testing: added one extra unit test just because Docs Changes: n/a Release Notes: n/a Signed-off-by: Alyssa Wilk upstream: force a full rebuild on host weight changes (#14569) This will allow us to build load balancers that pre-compute data structures based on host weights (for example using weighted queues), to work around some of the deficiencies of EDF scheduling. This behavior can be temporarily disabled by setting the envoy.reloadable_features.upstream_host_weight_change_causes_rebuild feature flag to false. Fixes https://github.com/envoyproxy/envoy/issues/14360 Signed-off-by: Matt Klein access log: add support for command formatter extensions (#14512) Signed-off-by: Raul Gutierrez Segales test: improving dynamic_forward_proxy coverage (#14672) Risk Level: n/a (test only) Signed-off-by: Alyssa Wilk access_logs: removing disallow_unbounded_access_logs (#14677) Signed-off-by: Alyssa Wilk wasm: replace the obsolete contents in wasm-cc's README with docs link (#14628) Signed-off-by: Kenjiro Nakayama grpc-json-transcoder: support root path (#14585) Signed-off-by: Xuyang Tao ecds: add config source for network filter configs (#14674) Signed-off-by: Kuat Yessenov fix comment for parameters end_stream of decodeData/encodeData. (#14620) Signed-off-by: wangfakang [fuzz] Fix bugs in HPACK fuzz test (#14638) - Use after free because nghttp2_nv object has pointers to the underlying strings and copying them resulted in a use after free when the copy was used after the original was destroyed - Fixed sorting issues and tested leading/trailing whitespace headers (I can no longer reproduce an issue I saw where a null byte appeared after decoding whitespace, maybe the former fix fixed this) Risk Level: Low Testing: Added regression tests and cases for whitespace headers Fixes https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=28880 https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=28869 Signed-off-by: Asra Ali v3 packages updates for omit_canary_hosts proto (#14117) Risk Level: LOW Testing: unit ( proto_format and docs ) part of #12841 Signed-off-by: Abhay Narayan Katare streaminfo/mocks: delay filter_state_ dereference (#14612) By dereferencing filter_state_ in the constructor, any test that sets filter_state_ will dereference an invalid pointer. This may not be a common use-case, but it came up when writing some microbenchmarks for a custom filter where I needed to reset the FilterState on each iteration of the benchmark. Signed-off-by: Brian Wolfe http: support passing match result action to filter (#14462) Adds support for passing through a match action from a match tree to the associated HTTP filter. Some care has to be taken here around dual filters, so we introduce an abstraction that moves handling HttpMatchingData updates and applying the match result into a FilterMatchState object that is shared between all filter wrappers for a given filter. This should also avoid having to match twice for dual filters: the match result is shared for both filters, instead of both of them having to independently arrive at it with the same data. Signed-off-by: Snow Pettersen refactor: use unitfloat in more places (#14396) Commit Message: Use UnitFloat in place of float in more locations Additional Description: UnitFloat represents a floating point value that is guaranteed to be in the range [0, 1]. Use it in place of floats that also have the same expectation in OverloadActionState and connection listeners. This PR introduces no functional changes. Risk Level: low Testing: ran affected tests Docs Changes: n/a Release Notes: n/a Platform Specific Features: n/a Signed-off-by: Alex Konradi [tls] add missing built in cipher stat names (#14676) * add missing ciphers Signed-off-by: Asra Ali docs: adding coverage walkthroguh (#14688) Risk Level: n/a Testing: n/a Docs Changes: adding developer docs Release Notes: n/a Signed-off-by: Alyssa Wilk local ratelimit: Add descriptor support in HTTP Local Rate Limiting (#14588) Signed-off-by: Kuat Yessenov Co-authored-by: gargnupur http: prefetch for upstreams (#14143) Commit Message: Adding predictive prefetch (useful mainly for HTTP/1.1 and TCP proxying) and uncommenting prefetch config. Additional Description: Risk Level: low (mostly config guarded) Testing: unit, integration tests Docs Changes: APIs unhidden Release Notes: inline Fixes #2755 Signed-off-by: Alyssa Wilk docs: Give a hint to specify type_url instead (#14562) Signed-off-by: Dhi Aurrahman Remove flaky_on_windows tag from proxy_filter_integration_test (#14680) Testing: Ran proxy_filter_integration_test thousands of times Signed-off-by: Randy Miller upstream: Fix moving EDS hosts between priorities. (#14483) At present if health checks are enabled and passing then moving an EDS host from P0->P1 is a NOOP, and P1->P0 results in an abort. In the first case: * P0 processing treats A as being removed because it's not in P0's list of endpoints anymore. * P0 marks A's existing Host as PENDING_DYNAMIC_REMOVAL. It marks A as having been updated in this config apply. * P1 skips over A because it is marked as updated in this update cycle already. In the second case: * P0 updates the priority on the existing Host. It is appended to the vector of added hosts. * P1 marks A's existing Host as PENDING_DYNAMIC_REMOVAL. It does adjust the removed host vector as the host is still pending removal. * A's Host is now in both priorities and is PENDING_DYNAMIC_REMOVAL. This is wrong, and would cause problems later but doesn't have a chance to because: * onClusterMemberUpdate fires with A's existing Host in the added vector (remember it wasn't removed from P1!) * HealthChecker attempts to create a new health check session on A, which results in an abort from the destructor of the already existing one. This was masked in tests by the tests enabling ignore_health_on_host_removal. We fix this by passing in the set of addresses that appear in the endpoint update. If a host being considered for removal appears in this set, and it isn't being duplicated into the current priority as a result of a health check address change, then we assume it's being moved and will immediately remove it. To simplify the case where a host's health check address is being changed AND it is being moved between priorities we always apply priority moves in place before we attempt any other modifications. This means such a case is treated as a separate priority move, followed by the health check address change. fixes #11517 Signed-off-by: Jonathan Oddy examples: Add TLS SNI sandbox (#13975) Signed-off-by: Ryan Northey [utility]: Change behavior of main thread verification utility (#14660) Currently, the isMainThread function can only be called during the lifetime of thread local instance because the singleton that store main thread id is initialized in the constructor of tls instance and cleared in the destructor of tls instance. Change the utility so that outside the lifetime of tls instance, the function return true by default because everything is in main thread when threading is off. Risk Level: low Testing: change unit to reflect change of behavior. Signed-off-by: chaoqin-li1123 Windows build: Add repository cache to CI (#14678) Signed-off-by: Sunjay Bhatia ext-proc: Support "immediate_response" options for request headers (#14652) This lets ext_proc servers return an immediate HTTP response (such as to indicate an error) in response to a request_headers message. Signed-off-by: Gregory Brail stats: convert tag extractor regexs to Re2 (#14519) Risk Level: high, the regexes are updated to match more specific patterns. Testing: unit tests Fixes #14439 Signed-off-by: Dmitry Rozhkov hcm: removing envoy.reloadable_features.early_errors_via_hcm #14641 (#14684) Risk Level: Low (removal of deprecated disabled code) Testing: n/a Docs Changes: n/a Release Notes: inline Fixes #14641 Signed-off-by: Alyssa Wilk test: Fix O(1/32k) flakiness in H2 flood tests that disable writes based on source port of outgoing connections. (#14695) It is possible for the kernel to assign the same source port to both the client connection used by the test framework to connect to the Envoy and the Envoy's client connection to the upstream. When the source port is reused by both connections, the test client times out while trying to send the request because disabling write on the upstream connection also disabled writes on the test's client connection. Signed-off-by: Antonio Vicente tls: add missing stats for signature algorithms. (#14703) While there, refresh supported cipher suites and add more warnings. Signed-off-by: Piotr Sikora connection: tighten network connection buffer limits (#14333) Signed-off-by: Antonio Vicente xdstp: LDS glob collection support. (#14311) This patch introduces support for LDS xdstp:// collection URLs for glob collections over ADS. Context parameters are currently computed from node and resource URLs. Followup PRs will add support for other collection types (CDS, SRDS), non-ADS, provide dynamic context parameter update, extend support to singleton resources and then other xdstp:// features (list collections, redirects, alternatives, etc.) Part of #11264. Risk level: Low (opt-in) Testing: ADS integration test added. Various unit tests following implementation. Signed-off-by: Harvey Tuch listener manager: avoid unique -> shared conversion (#14693) buildFilterChainInternal() returns a shared_ptr, so let's make that instead of unique_ptr. Signed-off-by: Raul Gutierrez Segales proto: re-implement RepeatedPtrUtil::hash(). (#14701) This changes RepeatedPtrUtil::hash() implementation to match MessageUtil::hash(), which was re-implemented in #8231. Reported by Tomoaki Fujii (Google). Signed-off-by: Piotr Sikora tcp: setting nodelay on all connections (#14574) This should have minimal effect, new server side connections had no-delay, codecs set no-delay, and upstream pools set no-delay. Traffic not using the tcp connection pool may be affected as well as raw use of the TCP client. Risk Level: Medium (data plane) Testing: new unit tests Docs Changes: n/a Release Notes: inline Runtime guard: envoy.reloadable_features.always_nodelay Signed-off-by: Alyssa Wilk test: Add multiheader TE + Content-Length test (#14686) Signed-off-by: Yan Avlasov http2: Flip the upstream H2 frame flood and abuse checks to ON by default (#14443) Signed-off-by: Yan Avlasov Fix the emsdk patching. (#14673) If the patch fails, because of `|| true`, bazel continues the build. Signed-off-by: Jonh Wendell test: print test parameters meaningfully (#14604) Signed-off-by: Alex Konradi Migrate v2 thrift_filter to v3 api and corresponding docs changes. (#13885) part of #12841 Signed-off-by: Abhay Narayan Katare http: reinstating prior connect timeout behavior (#14685) Signed-off-by: Alyssa Wilk Fix typo (#14716) Signed-off-by: Hu Shuai master -> main (#14729) Various fixes Signed-off-by: Matt Klein readme: fix logo URL (#14733) Signed-off-by: Matt Klein Bump nghttp2 to 1.42.0 (#14730) - Drops nghttp2 PR1468 patch - Requires bazel_external_cmake to support copts, defines to drop the rest Risk Level: low Testing: CI Fixes #1417 Signed-off-by: William A Rowe Jr Pick up current bazel-build-tools tag (#14734) Signed-off-by: William A Rowe Jr access-logger: support request/response headers size (#14692) Add following command operator in access logger %REQUEST_HEADER_BYTES% %RESPONSE_HEADER_BYTES% %RESPONSE_TRAILER_BYTES% Risk Level: Low Testing: unit test Docs Changes: done Release Notes: done Signed-off-by: Xuyang Tao dynamic_forward_proxy: envoy.reloadable_features.enable_dns_cache_circuit_breakers deprecation (#14683) * dynamic_forward_proxy: deprecation Signed-off-by: Shikugawa Add support for google::protobuf::ListValue formatting (#14518) Signed-off-by: Itamar Kaminski tls: improve TLS handshake/read/write error log (#14600) Signed-off-by: Shikugawa config: switch from std::set to absl::flat_hash_set for resource names. (#14739) This was a cleanup deferred from the review of #14311. The idea is to switch to the more efficient unordered absl::flat_hash_set across the resource subscription code base. Internally, we still use std::set (and even explicitly sort in the http_subscription_impl) to avoid changing any wire ordering. It seems desirable to preserve this for two reasons: (1) this derisks this PR as an internal-only change and (2) having deterministic wire ordering makes debug of xDS issues somewhat easier. Risk level: Low Testing: Updated tests. Signed-off-by: Harvey Tuch network filters: avoid unnecessary std::shared_ptrs (#14711) While debugging a crash in: https://github.com/envoyproxy/envoy/pull/13592 I ended up discussing with @lambdai and @mattklein123 whether network filters can hold references to things owned by their corresponding FactoryFilterCb. The answer is yes and the HCM and some other notable filters already use references instead of std::shared_ptrs. So let's consistently do this everywhere to avoid someone else asking this same question in the future. Plus, it's always nice to create fewer std::shared_ptrs. Follow-up on: https://github.com/envoyproxy/envoy/issues/8633 Signed-off-by: Raul Gutierrez Segales docs: Updated version history with 1.13.8 release notes. (#14742) Signed-off-by: Christoph Pakulski Dispatcher: keeps a stack of tracked objects. (#14573) Dispatcher will now keep a stack of tracked objects; on crash it'll "unwind" and have those objects dump their state. Moreover, it'll invoke fatal actions with the tracked objects. This allows us to dump more information during crash. See related PR: #14509 Will follow up with another PR dumping information at the codec/parser level. Signed-off-by: Kevin Baichoo bootstrap-extensions: fix a crash on http callout (#14478) Currently when the ServerFactoryContext is passed to bootstrap extensions, it is only partially initialized. Specifically, attempting to access the cluster manager will cause a nullptr access (and hence a crash) This PR splits the creation and initialized to 2 seperate fucntions. Early creation is required to not break the `default_socket_interface` feature. Once created, the extension will receive the ServerFactoryContext in a different callback (the newly added `serverInitialized`), once they are fully initialized. Commit Message: Fix a crash that happens when bootstrap extensions perform http calls. Additional Description: Risk Level: Low (small bug-fix) Testing: Unit tests updated; tested manually with the changes as well. Docs Changes: N/A Release Notes: N/A Fixes https://github.com/envoyproxy/envoy/issues/14420 Signed-off-by: Yuval Kohavi overload: create scaled timers via the dispatcher (#14679) Refactor the existing pathway for creating scaled Timer objects away from the ThreadLocalOverloadState and into the Dispatcher interface. This allows scaled timers to be created without plumbing through a bunch of extra state. Signed-off-by: Alex Konradi http: removing nvoy.reloadable_features.fix_upgrade_response #14643 (#14706) Risk Level: Low (removing deprecated disabled code) Testing: n/a Docs Changes: n/a Release Notes: inline Fixes #14643 Signed-off-by: Alyssa Wilk http: removing envoy.reloadable_features.fixed_connection_close (#14705) Risk Level: Low (removing deprecated guarded code) Testing: n/a Docs Changes: n/a Release Notes: inline Fixes #14645 Signed-off-by: Alyssa Wilk Revert "network filters: avoid unnecessary std::shared_ptrs (#14711)" (#14755) This reverts commit 72db81d3e61818012b92fa43d7cdd9ca31c2277e. Per discussion in #14717 and via Slack, we'll come up with a different approach since using a std::function to keep state presents a few challenges. Signed-off-by: Raul Gutierrez Segales Clarify Consecutive Gateway Failure docs (#14738) It was initially unclear to me that when split_external_local_origin_errors is in the default setting of false that local origin failures will be counted as Consecutive Gateway Failures. It is clear above that they are counted by the Consecutive 5xx detection type but since I had that disabled I was surprised to find them counted in Consecutive Gateway Failure. I think the logic makes sense though so just attempting to clarify the docs here. Signed-off-by: Matthew Mead-Briggs thrift proxy: fix crash when using payload_passthrough (#14723) We started seeing crashes triggered by ConnectionManager::passthroughEnabled() once we enabled `payload_passthrough`. That code assumes that there will _always_ be an active RPC. However, this is not true after a local response has been sent (e.g.: no healthy upstream, no cluster, no route, etc.). Risk Level: low Testing: unit tests added Doc Changes: n/a Release Notes: n/a Signed-off-by: Raul Gutierrez Segales thrift proxy: add comments explaining local replies (#14754) Risk Level: low Testing: n/a Docs Changes: n/a Release Notes: n/a Signed-off-by: Raul Gutierrez Segales wasm: update V8 to v8.9.255.6. (#14764) Signed-off-by: Piotr Sikora Request headers to add (#14747) Being consistent about treating Host: and :authority the same way in Envoy header modification. Risk Level: Medium (changes allowed modifiable headers) Testing: new unit tests Docs Changes: yes Release Notes: inline Signed-off-by: Alyssa Wilk tls: update BoringSSL to fbbf8781 (4324). (#14763) Signed-off-by: Piotr Sikora docs: change getting started order (#14774) Sandboxes are more relevant to new users than the other sections. Signed-off-by: Matt Klein oauth2: set accept header on access_token request (#14538) Co-authored-by: Dhi Aurrahman Signed-off-by: Richard Patel router: Remove envoy.reloadable_features.consume_all_retry_headers (#14662) This patch removes the envoy.reloadable_features.consume_all_retry_headers runtime flag. Signed-off-by: Martin Matusiak docs: API review checklist (#14399) * API review checklist Signed-off-by: Mark D. Roth [docs] Add guidance on ENVOY_BUG in STYLE.md (#14575) * Add guidanceon ENVOY_BUG and macro usage to STYLE.md Signed-off-by: Asra Ali filters: Add test/server:filter_config_test (#14746) As part of #14470, I'll be modifying the base filter interface to include an overridable dependencies() function. This is prep work. Risk Level: Low (test only) Doc Change: n/a Release Notes: n/a Signed-off-by: Auni Ahsan dns: removing envoy.reloadable_features.fix_wildcard_matching #14644 (#14768) Signed-off-by: Alyssa Wilk test: FakeUpstream threading fixes (#14526) Signed-off-by: Antonio Vicente server: add FIPS mode statistic indicating FIPS compliance (#14719) Signed-off-by: Ravindra Akella Add error_state to all config dump resources (#14689) Store the NACKed resource in each resources Risk Level: None Fixes: #14431 Signed-off-by: Lidi Zheng docs: fix two typos in jwt_authn_filter (#14796) Signed-off-by: Lukasz Jernas tcp: adding logs and debug checks (#14771) Adding some logs and one ENVOY_BUG around the new TCP pool. Signed-off-by: Alyssa Wilk google_grpc: attempt to reduce lock contention between completionThread() and onCompletedOps() (#14777) Holding a stream's lock while running handleOpCompletion can result in the completion queue having to wait until the lock is released before adding messages on that stream to completed_ops_. In cases where the completion queue is shared across multiple gRPC streams, delivery of new messages on all streams is blocked until the lock held by the first stream while executing onCompletedOps. Signed-off-by: Antonio Vicente filters: Add dependencies.proto (#14750) Introduces the FilterDependency proto. This isn't quite an extension, but it's a common proto to be used by all filter extensions. Risk Level: Low (proto addition only) Signed-off-by: Auni Ahsan tools: Syncing api/BUILD file to generated_api_shadow (#14792) After chatting with @akonradi on Slack, it seems the generated_api_shadow/BUILD file was not being updated by proto_format since PR #9719. This PR copies the api/BUILD file to generated_api_shadow. Risk Level: Low (relevant for development) Signed-off-by: Adi Suissa-Peleg Add debug log for slow config updates for GRPC subscriptions (#14343) Risk Level: Low Testing: Docs Changes: N/A Release Notes: N/A Signed-off-by: Adam Schaub oauth2 filter: Make OAuth scopes configurable. (#14168) New optional parameter 'auth_scopes' added to the filter. The default value is 'user' (if not provided) to avoid breaking changes to users updating to the latest version. Signed-off-by: andreyprezotto Co-authored-by: Nitin Goyal upstream: Optimize LoadStatsReporter::startLoadReportPeriod implementation (#14803) cm_.clusters() is not O(1) in part due to it creating maps and returning by value. This means that startLoadReportPeriod was effectively O(n**2) on number of clusters since cm_.clusters() is called for every active cluster. Risk Level: low, functional no-op Testing: Existing tests. We may want a benchmark. Signed-off-by: Antonio Vicente ext_proc: Implement response path for headers only (#14713) Implement header processing on the response path by sending the response_headers message to the processor and handling the result. Also update the docs in the .proto file. Signed-off-by: Gregory Brail reformat code Signed-off-by: qinggniq --- .azure-pipelines/cve_scan.yml | 2 +- .azure-pipelines/pipelines.yml | 10 +- .bazelrc | 4 +- .devcontainer/Dockerfile | 2 +- .../non--crash-security--bug.md | 4 +- .github/workflows/get_build_targets.sh | 4 +- CONTRIBUTING.md | 23 +- DEPENDENCY_POLICY.md | 4 +- DEVELOPER.md | 30 +- EXTENSION_POLICY.md | 8 +- GOVERNANCE.md | 8 +- PULL_REQUESTS.md | 8 + PULL_REQUEST_TEMPLATE.md | 3 +- README.md | 6 +- RELEASES.md | 16 +- REPO_LAYOUT.md | 2 + SECURITY.md | 16 +- STYLE.md | 118 ++- VERSION | 2 +- api/API_VERSIONING.md | 4 +- api/BUILD | 3 + api/CONTRIBUTING.md | 2 +- api/README.md | 4 +- api/STYLE.md | 6 + api/envoy/admin/v3/config_dump.proto | 45 + api/envoy/admin/v4alpha/config_dump.proto | 45 + api/envoy/api/v2/core/protocol.proto | 2 +- api/envoy/config/cluster/v3/cluster.proto | 18 +- .../config/cluster/v4alpha/cluster.proto | 18 +- api/envoy/config/core/v3/protocol.proto | 2 +- .../core/v3/substitution_format_string.proto | 8 +- api/envoy/config/core/v4alpha/protocol.proto | 2 +- .../v4alpha/substitution_format_string.proto | 8 +- .../filter/http/ext_authz/v2/ext_authz.proto | 5 +- .../listener/v3/listener_components.proto | 12 +- .../v4alpha/listener_components.proto | 12 +- .../config/retry/omit_canary_hosts/v3/BUILD | 9 + .../v3/omit_canary_hosts.proto | 16 + .../config/route/v3/route_components.proto | 1 + .../route/v4alpha/route_components.proto | 1 + .../dynamic_forward_proxy/v3/dns_cache.proto | 3 +- .../common/ratelimit/v3/ratelimit.proto | 9 + .../filters/common/dependency/v3/BUILD | 9 + .../common/dependency/v3/dependency.proto | 45 + .../http/ext_proc/v3alpha/ext_proc.proto | 18 +- .../filters/http/local_ratelimit/v3/BUILD | 1 + .../local_ratelimit/v3/local_rate_limit.proto | 27 +- .../filters/http/oauth2/v3alpha/oauth.proto | 7 +- .../filters/http/oauth2/v4alpha/oauth.proto | 7 +- .../network/thrift_proxy/router/v3/BUILD | 12 + .../thrift_proxy/router/v3/router.proto | 20 + .../ext_proc/v3alpha/external_processor.proto | 8 +- api/envoy/type/matcher/v3/http_inputs.proto | 35 + .../type/matcher/v4alpha/http_inputs.proto | 40 + api/review_checklist.md | 154 +++ api/versioning/BUILD | 3 + bazel/EXTERNAL_DEPS.md | 4 +- bazel/PPROF.md | 6 +- bazel/README.md | 24 +- bazel/foreign_cc/nghttp2.patch | 80 +- bazel/repositories.bzl | 5 +- bazel/repository_locations.bzl | 28 +- ci/README.md | 16 +- ci/api_mirror.sh | 4 +- ci/docker_ci.sh | 14 +- ci/filter_example_mirror.sh | 4 +- ci/go_mirror.sh | 2 +- ci/repokitteh/modules/newcontributor.star | 2 +- ci/upload_gcs_artifact.sh | 2 +- ci/verify_examples.sh | 2 +- ci/windows_ci_steps.sh | 1 + docs/README.md | 4 +- docs/publish.sh | 4 +- .../common_messages/common_messages.rst | 1 + docs/root/api-v3/config/common/common.rst | 1 + docs/root/api-v3/config/retry/retry.rst | 1 + docs/root/api-v3/types/types.rst | 1 + .../http/http_conn_man/headers.rst | 2 +- .../dynamic_forward_proxy_filter.rst | 2 - .../http/http_filters/ext_proc_filter.rst | 21 + .../http/http_filters/jwt_authn_filter.rst | 4 +- .../http_filters/local_rate_limit_filter.rst | 87 ++ .../http/http_filters/oauth2_filter.rst | 10 + .../listener_filters/tls_inspector.rst | 18 +- .../observability/access_log/usage.rst | 21 + .../observability/statistics.rst | 10 + .../thrift_filters/router_filter.rst | 2 +- .../cluster_manager/cluster_stats.rst | 1 + docs/root/extending/extending.rst | 1 + .../intro/arch_overview/http/upgrades.rst | 2 + .../intro/arch_overview/upstream/outlier.rst | 5 +- docs/root/start/sandboxes/index.rst | 1 + docs/root/start/sandboxes/tls-sni.rst | 175 ++++ docs/root/start/sandboxes/tls.rst | 4 + docs/root/start/start.rst | 7 +- docs/root/version_history/current.rst | 126 +-- docs/root/version_history/v1.11.0.rst | 2 +- docs/root/version_history/v1.13.8.rst | 9 + docs/root/version_history/v1.14.0.rst | 2 +- docs/root/version_history/v1.17.0.rst | 121 +++ docs/root/version_history/v1.4.0.rst | 2 +- docs/root/version_history/v1.8.0.rst | 16 +- docs/root/version_history/v1.9.0.rst | 10 +- docs/root/version_history/version_history.rst | 4 +- examples/BUILD | 1 + examples/_extra_certs/README.md | 7 + examples/_extra_certs/domain1.crt.pem | 21 + examples/_extra_certs/domain1.key.pem | 28 + examples/_extra_certs/domain2.crt.pem | 21 + examples/_extra_certs/domain2.key.pem | 28 + examples/_extra_certs/domain3.crt.pem | 21 + examples/_extra_certs/domain3.key.pem | 28 + .../grpc-bridge/docker-compose-protos.yaml | 2 +- examples/tls-sni/Dockerfile | 9 + examples/tls-sni/Dockerfile-client | 6 + examples/tls-sni/README.md | 2 + examples/tls-sni/docker-compose.yaml | 34 + examples/tls-sni/envoy-client.yaml | 92 ++ examples/tls-sni/envoy.yaml | 127 +++ examples/tls-sni/verify.sh | 51 + examples/wasm-cc/README.md | 61 +- examples/wasm-cc/docker-compose-wasm.yaml | 4 +- generated_api_shadow/BUILD | 140 ++- .../envoy/admin/v3/config_dump.proto | 45 + .../envoy/admin/v4alpha/config_dump.proto | 45 + .../envoy/api/v2/core/protocol.proto | 2 +- .../envoy/config/cluster/v3/cluster.proto | 18 +- .../config/cluster/v4alpha/cluster.proto | 18 +- .../envoy/config/core/v3/protocol.proto | 2 +- .../core/v3/substitution_format_string.proto | 8 +- .../envoy/config/core/v4alpha/protocol.proto | 2 +- .../v4alpha/substitution_format_string.proto | 8 +- .../filter/http/ext_authz/v2/ext_authz.proto | 5 +- .../listener/v3/listener_components.proto | 12 +- .../v4alpha/listener_components.proto | 12 +- .../config/retry/omit_canary_hosts/v3/BUILD | 9 + .../v3/omit_canary_hosts.proto | 16 + .../config/route/v3/route_components.proto | 1 + .../route/v4alpha/route_components.proto | 1 + .../dynamic_forward_proxy/v3/dns_cache.proto | 3 +- .../common/ratelimit/v3/ratelimit.proto | 9 + .../filters/common/dependency/v3/BUILD | 9 + .../common/dependency/v3/dependency.proto | 45 + .../http/ext_proc/v3alpha/ext_proc.proto | 18 +- .../filters/http/local_ratelimit/v3/BUILD | 1 + .../local_ratelimit/v3/local_rate_limit.proto | 27 +- .../filters/http/oauth2/v3alpha/oauth.proto | 7 +- .../filters/http/oauth2/v4alpha/oauth.proto | 7 +- .../network/thrift_proxy/router/v3/BUILD | 12 + .../thrift_proxy/router/v3/router.proto | 20 + .../ext_proc/v3alpha/external_processor.proto | 8 +- .../envoy/type/matcher/v3/http_inputs.proto | 35 + .../type/matcher/v4alpha/http_inputs.proto | 40 + include/envoy/api/BUILD | 1 + include/envoy/api/api.h | 13 + include/envoy/common/BUILD | 1 + include/envoy/common/random_generator.h | 10 +- include/envoy/config/BUILD | 6 + include/envoy/config/context_provider.h | 26 + include/envoy/config/grpc_mux.h | 6 +- include/envoy/config/subscription.h | 18 +- include/envoy/config/subscription_factory.h | 10 +- include/envoy/event/BUILD | 9 +- include/envoy/event/dispatcher.h | 32 +- .../envoy/event/scaled_range_timer_manager.h | 15 +- include/envoy/event/scaled_timer.h | 84 ++ include/envoy/event/scaled_timer_minimum.h | 60 -- .../envoy/formatter/substitution_formatter.h | 52 + include/envoy/http/filter.h | 30 +- include/envoy/http/header_map.h | 1 + include/envoy/local_info/BUILD | 1 + include/envoy/local_info/local_info.h | 6 + include/envoy/matcher/BUILD | 1 + include/envoy/matcher/matcher.h | 5 +- include/envoy/network/BUILD | 2 + include/envoy/network/connection_handler.h | 4 +- include/envoy/network/listener.h | 4 +- include/envoy/ratelimit/ratelimit.h | 40 +- include/envoy/router/router_ratelimit.h | 12 + .../envoy/server/bootstrap_extension_config.h | 8 +- include/envoy/server/fatal_action_config.h | 8 +- include/envoy/server/overload/BUILD | 2 +- .../envoy/server/overload/overload_manager.h | 6 + .../overload/thread_local_overload_state.h | 23 +- include/envoy/upstream/upstream.h | 2 +- security/email-templates.md | 6 +- security/postmortems/cve-2019-15225.md | 2 +- security/postmortems/cve-2019-9900.md | 2 +- source/common/api/api_impl.cc | 7 + source/common/api/api_impl.h | 3 + source/common/common/interval_value.h | 20 + source/common/common/scope_tracker.h | 25 +- source/common/common/thread.h | 9 +- source/common/common/utility.h | 13 + source/common/config/BUILD | 14 + source/common/config/README.md | 47 - source/common/config/context_provider_impl.h | 23 + .../common/config/delta_subscription_state.cc | 5 +- .../common/config/delta_subscription_state.h | 10 +- .../config/filesystem_subscription_impl.cc | 4 +- .../config/filesystem_subscription_impl.h | 6 +- source/common/config/grpc_mux_impl.cc | 2 +- source/common/config/grpc_mux_impl.h | 28 +- .../common/config/grpc_subscription_impl.cc | 59 +- source/common/config/grpc_subscription_impl.h | 37 +- .../common/config/http_subscription_impl.cc | 8 +- source/common/config/http_subscription_impl.h | 8 +- source/common/config/new_grpc_mux_impl.cc | 37 +- source/common/config/new_grpc_mux_impl.h | 11 +- .../config/subscription_factory_impl.cc | 40 +- .../common/config/subscription_factory_impl.h | 5 +- source/common/config/utility.cc | 7 + source/common/config/utility.h | 5 +- source/common/config/watch_map.cc | 54 +- source/common/config/watch_map.h | 18 +- source/common/config/well_known_names.cc | 129 ++- source/common/config/well_known_names.h | 1 - source/common/config/xDS_code_diagram.png | Bin 97291 -> 0 bytes source/common/config/xds_context_params.cc | 59 +- source/common/config/xds_context_params.h | 31 +- source/common/config/xds_resource.cc | 8 +- source/common/config/xds_resource.h | 6 + source/common/conn_pool/conn_pool_base.cc | 97 +- source/common/conn_pool/conn_pool_base.h | 38 +- source/common/event/BUILD | 4 + source/common/event/dispatcher_impl.cc | 67 +- source/common/event/dispatcher_impl.h | 38 +- .../event/scaled_range_timer_manager_impl.cc | 21 +- .../event/scaled_range_timer_manager_impl.h | 12 +- .../http/filter_config_discovery_impl.cc | 3 +- source/common/formatter/BUILD | 1 + .../formatter/substitution_format_string.cc | 17 +- .../formatter/substitution_formatter.cc | 429 +++++--- .../common/formatter/substitution_formatter.h | 97 +- .../common/grpc/google_async_client_impl.cc | 18 +- source/common/http/BUILD | 3 + source/common/http/async_client_impl.h | 4 + source/common/http/codec_client.cc | 4 +- source/common/http/conn_manager_impl.cc | 48 +- source/common/http/conn_manager_utility.cc | 4 +- source/common/http/filter_manager.cc | 81 +- source/common/http/filter_manager.h | 177 +++- source/common/http/header_utility.cc | 11 + source/common/http/header_utility.h | 14 + source/common/http/http1/codec_impl.cc | 13 - source/common/http/http1/conn_pool.cc | 24 +- source/common/http/http2/codec_impl.cc | 9 + source/common/http/http2/codec_impl.h | 1 + source/common/http/match_wrapper/BUILD | 23 + source/common/http/match_wrapper/config.cc | 90 ++ source/common/http/match_wrapper/config.h | 28 + source/common/http/utility.cc | 2 +- source/common/init/target_impl.cc | 10 +- source/common/init/target_impl.h | 10 +- source/common/local_info/BUILD | 1 + source/common/local_info/local_info_impl.h | 46 +- source/common/matcher/matcher.h | 2 +- source/common/network/connection_impl.cc | 30 +- source/common/network/socket_interface.h | 2 + source/common/network/socket_interface_impl.h | 1 + source/common/network/tcp_listener_impl.cc | 3 +- source/common/network/tcp_listener_impl.h | 6 +- source/common/network/udp_listener_impl.h | 2 +- source/common/protobuf/utility.h | 16 +- source/common/router/BUILD | 2 +- source/common/router/header_parser.cc | 9 +- source/common/router/rds_impl.cc | 2 +- source/common/router/retry_state_impl.cc | 11 +- source/common/router/router_ratelimit.cc | 80 +- source/common/router/router_ratelimit.h | 18 +- source/common/router/scoped_rds.cc | 2 +- source/common/router/upstream_request.cc | 7 +- source/common/router/vhds.cc | 7 +- source/common/runtime/runtime_features.cc | 15 +- source/common/runtime/runtime_impl.cc | 4 +- source/common/secret/sds_api.cc | 2 +- source/common/stats/symbol_table_impl.cc | 4 +- source/common/stats/tag_extractor_impl.cc | 14 +- source/common/stats/tag_extractor_impl.h | 36 + source/common/stats/thread_local_store.h | 2 +- source/common/stats/utility.h | 10 +- source/common/tcp/conn_pool.cc | 6 +- source/common/tcp/original_conn_pool.cc | 5 +- source/common/upstream/cds_api_impl.cc | 2 +- .../common/upstream/cluster_manager_impl.cc | 60 +- source/common/upstream/cluster_manager_impl.h | 1 + source/common/upstream/eds.cc | 39 +- source/common/upstream/eds.h | 4 +- source/common/upstream/health_checker_impl.cc | 76 +- source/common/upstream/health_checker_impl.h | 14 + source/common/upstream/load_balancer_impl.cc | 33 +- source/common/upstream/load_balancer_impl.h | 5 +- source/common/upstream/load_stats_reporter.cc | 33 +- source/common/upstream/strict_dns_cluster.cc | 7 +- source/common/upstream/upstream_impl.cc | 43 +- source/common/upstream/upstream_impl.h | 6 +- source/common/version/version.cc | 8 + source/common/version/version.h | 2 + source/docs/aclick.png | Bin 0 -> 70109 bytes source/docs/click.png | Bin 0 -> 120611 bytes source/docs/coverage.md | 49 + source/docs/eclick.png | Bin 0 -> 113057 bytes source/docs/file.png | Bin 0 -> 190636 bytes source/docs/network_filter_fuzzing.md | 32 +- source/docs/pr_view.png | Bin 0 -> 77503 bytes source/docs/repokitteh.md | 4 +- source/docs/report.png | Bin 0 -> 169953 bytes source/docs/results.png | Bin 0 -> 231768 bytes source/docs/stats.md | 20 +- source/docs/upload.png | Bin 0 -> 173959 bytes source/docs/xds.md | 93 ++ source/extensions/access_loggers/common/BUILD | 1 - .../common/grpc_access_logger.h | 10 +- source/extensions/bootstrap/wasm/config.cc | 33 +- source/extensions/bootstrap/wasm/config.h | 22 +- .../clusters/redis/redis_cluster.cc | 7 +- .../common/dynamic_forward_proxy/dns_cache.h | 5 +- .../dynamic_forward_proxy/dns_cache_impl.cc | 11 +- .../dynamic_forward_proxy/dns_cache_impl.h | 3 +- .../filters/common/local_ratelimit/BUILD | 3 + .../local_ratelimit/local_ratelimit_impl.cc | 103 +- .../local_ratelimit/local_ratelimit_impl.h | 53 +- .../filters/http/dynamic_forward_proxy/BUILD | 1 - .../dynamic_forward_proxy/proxy_filter.cc | 24 +- .../filters/http/ext_authz/ext_authz.cc | 3 +- source/extensions/filters/http/ext_proc/BUILD | 19 + .../filters/http/ext_proc/config.cc | 17 +- .../extensions/filters/http/ext_proc/config.h | 2 + .../filters/http/ext_proc/ext_proc.cc | 174 +++- .../filters/http/ext_proc/ext_proc.h | 100 +- .../filters/http/ext_proc/mutation_utils.cc | 80 ++ .../filters/http/ext_proc/mutation_utils.h | 34 + .../filters/http/local_ratelimit/BUILD | 1 + .../filters/http/local_ratelimit/config.cc | 5 +- .../http/local_ratelimit/local_ratelimit.cc | 50 +- .../http/local_ratelimit/local_ratelimit.h | 20 +- .../extensions/filters/http/oauth2/filter.cc | 30 +- .../extensions/filters/http/oauth2/filter.h | 2 + .../filters/http/oauth2/oauth_client.cc | 1 + .../filters/http/oauth2/oauth_client.h | 8 +- .../network/common/redis/client_impl.cc | 6 +- .../local_ratelimit/local_ratelimit.cc | 6 +- .../network/local_ratelimit/local_ratelimit.h | 1 + .../filters/network/mysql_proxy/mysql_codec.h | 26 +- .../network/mysql_proxy/mysql_codec_clogin.cc | 250 +++-- .../network/mysql_proxy/mysql_codec_clogin.h | 49 +- .../mysql_proxy/mysql_codec_clogin_resp.cc | 451 +++++++- .../mysql_proxy/mysql_codec_clogin_resp.h | 165 ++- .../mysql_proxy/mysql_codec_command.cc | 13 +- .../network/mysql_proxy/mysql_codec_command.h | 9 +- .../mysql_proxy/mysql_codec_greeting.cc | 186 +++- .../mysql_proxy/mysql_codec_greeting.h | 65 +- .../mysql_proxy/mysql_codec_switch_resp.cc | 10 +- .../mysql_proxy/mysql_codec_switch_resp.h | 5 +- .../network/mysql_proxy/mysql_decoder.cc | 36 +- .../network/mysql_proxy/mysql_filter.cc | 6 +- .../network/mysql_proxy/mysql_utils.cc | 88 +- .../filters/network/mysql_proxy/mysql_utils.h | 13 +- .../sni_dynamic_forward_proxy/proxy_filter.cc | 2 +- .../network/thrift_proxy/conn_manager.cc | 30 +- .../rate_limit_descriptors/expr/config.cc | 4 +- .../previous_priorities/previous_priorities.h | 4 +- source/extensions/transport_sockets/tls/BUILD | 1 + .../transport_sockets/tls/context_impl.cc | 56 +- .../transport_sockets/tls/context_impl.h | 8 +- .../transport_sockets/tls/ssl_handshaker.cc | 2 + .../transport_sockets/tls/ssl_handshaker.h | 4 +- .../transport_sockets/tls/ssl_socket.cc | 8 +- .../transport_sockets/tls/utility.cc | 45 + .../transport_sockets/tls/utility.h | 30 + source/extensions/upstreams/http/config.cc | 32 +- source/extensions/upstreams/http/config.h | 2 +- source/server/admin/admin.h | 10 +- source/server/config_validation/api.cc | 6 + source/server/config_validation/api.h | 2 + source/server/config_validation/server.cc | 4 +- source/server/connection_handler_impl.cc | 7 +- source/server/connection_handler_impl.h | 4 +- source/server/lds_api.cc | 5 +- source/server/listener_manager_impl.cc | 2 +- source/server/overload_manager_impl.cc | 80 +- source/server/overload_manager_impl.h | 9 +- source/server/server.cc | 24 +- source/server/server.h | 14 + source/server/worker_impl.cc | 5 +- test/README.md | 8 +- .../common/access_log/access_log_impl_test.cc | 34 + test/common/common/BUILD | 13 + test/common/common/random_generator_test.cc | 9 +- test/common/common/scope_tracker_test.cc | 37 + test/common/common/utility_test.cc | 25 + test/common/config/BUILD | 11 + .../config/context_provider_impl_test.cc | 42 + .../config/delta_subscription_impl_test.cc | 2 +- .../config/delta_subscription_test_harness.h | 11 +- .../filesystem_subscription_test_harness.h | 4 +- .../config/grpc_subscription_test_harness.h | 6 +- .../config/http_subscription_test_harness.h | 4 +- test/common/config/new_grpc_mux_impl_test.cc | 39 + .../config/subscription_factory_impl_test.cc | 67 +- test/common/config/subscription_impl_test.cc | 18 + .../common/config/subscription_test_harness.h | 8 + test/common/config/watch_map_test.cc | 68 +- test/common/config/xds_context_params_test.cc | 48 +- test/common/conn_pool/conn_pool_base_test.cc | 10 + test/common/event/dispatcher_impl_test.cc | 112 +- .../scaled_range_timer_manager_impl_test.cc | 58 +- .../http/filter_config_discovery_impl_test.cc | 3 +- test/common/formatter/BUILD | 14 + test/common/formatter/command_extension.cc | 45 + test/common/formatter/command_extension.h | 37 + .../substitution_format_string_test.cc | 40 +- .../formatter/substitution_formatter_test.cc | 224 +++- test/common/grpc/grpc_client_integration.h | 4 +- test/common/http/conn_manager_impl_test.cc | 77 +- test/common/http/conn_manager_impl_test_2.cc | 23 +- .../http/conn_manager_impl_test_base.cc | 2 +- test/common/http/conn_manager_utility_test.cc | 36 - test/common/http/filter_manager_test.cc | 100 +- test/common/http/header_utility_test.cc | 18 + test/common/http/http1/codec_impl_test.cc | 18 - test/common/http/http1/conn_pool_test.cc | 79 -- test/common/http/http2/codec_impl_test.cc | 18 + .../http/http2/hpack_corpus/use_after_free | 1 + .../common/http/http2/hpack_corpus/whitespace | 26 + test/common/http/http2/hpack_fuzz_test.cc | 50 +- test/common/http/match_wrapper/BUILD | 19 + test/common/http/match_wrapper/config_test.cc | 91 ++ test/common/matcher/test_utility.h | 4 +- test/common/network/BUILD | 1 + test/common/network/connection_impl_test.cc | 120 ++- test/common/network/listener_impl_test.cc | 6 +- test/common/router/config_impl_test.cc | 31 +- test/common/router/rds_impl_test.cc | 10 +- test/common/router/retry_state_impl_test.cc | 52 - test/common/router/router_ratelimit_test.cc | 102 +- test/common/router/router_test.cc | 86 +- .../common/router/router_upstream_log_test.cc | 41 +- test/common/router/scoped_rds_test.cc | 10 +- test/common/runtime/runtime_impl_test.cc | 14 +- test/common/secret/sds_api_test.cc | 6 +- test/common/signal/fatal_action_test.cc | 12 +- test/common/signal/signals_test.cc | 17 +- test/common/stats/BUILD | 19 + .../stats/tag_extractor_impl_speed_test.cc | 110 ++ test/common/tcp/conn_pool_test.cc | 1 - .../thread_local/thread_local_impl_test.cc | 21 +- test/common/upstream/BUILD | 1 + test/common/upstream/cds_api_impl_test.cc | 2 +- .../upstream/cluster_manager_impl_test.cc | 101 +- test/common/upstream/eds_speed_test.cc | 4 +- test/common/upstream/eds_test.cc | 354 ++++++- test/common/upstream/health_check_fuzz.cc | 2 +- .../upstream/health_check_fuzz_test_utils.cc | 3 +- .../upstream/health_check_fuzz_test_utils.h | 1 + .../upstream/health_checker_impl_test.cc | 402 +++++++- .../upstream/load_balancer_impl_test.cc | 9 +- test/config/utility.cc | 21 +- test/config/utility.h | 6 +- .../common/grpc_access_logger_test.cc | 36 - .../http_grpc_access_log_integration_test.cc | 3 +- .../tcp_grpc_access_log_integration_test.cc | 3 +- test/extensions/bootstrap/wasm/BUILD | 15 + test/extensions/bootstrap/wasm/config_test.cc | 1 + .../extensions/bootstrap/wasm/test_data/BUILD | 5 + .../bootstrap/wasm/test_data/http_cpp.cc | 46 + .../bootstrap/wasm/wasm_integration_test.cc | 95 ++ .../clusters/redis/redis_cluster_test.cc | 67 +- .../dns_cache_impl_test.cc | 14 +- .../dns_cache_resource_manager_test.cc | 5 +- .../common/dynamic_forward_proxy/mocks.cc | 2 +- .../common/dynamic_forward_proxy/mocks.h | 7 +- test/extensions/common/wasm/wasm_runtime.cc | 4 + test/extensions/common/wasm/wasm_runtime.h | 2 + .../local_ratelimit/local_ratelimit_test.cc | 293 +++++- .../filters/http/dynamic_forward_proxy/BUILD | 3 - .../proxy_filter_integration_test.cc | 49 - .../proxy_filter_test.cc | 24 +- .../ext_authz/ext_authz_integration_test.cc | 3 +- test/extensions/filters/http/ext_proc/BUILD | 66 ++ .../filters/http/ext_proc/config_test.cc | 2 +- .../ext_proc/ext_proc_integration_test.cc | 403 ++++++++ .../filters/http/ext_proc/filter_test.cc | 711 ++++++++++++- .../filters/http/ext_proc/mock_server.cc | 17 + .../filters/http/ext_proc/mock_server.h | 31 + .../http/ext_proc/mutation_utils_test.cc | 125 +++ .../filters/http/ext_proc/ordering_test.cc | 348 +++++++ .../extensions/filters/http/ext_proc/utils.cc | 24 + test/extensions/filters/http/ext_proc/utils.h | 41 + .../filters/http/fault/fault_filter_test.cc | 58 +- .../json_transcoder_filter_test.cc | 35 + .../filters/http/local_ratelimit/BUILD | 1 + .../http/local_ratelimit/config_test.cc | 154 ++- .../http/local_ratelimit/filter_test.cc | 193 +++- .../filters/http/oauth2/config_test.cc | 8 + .../filters/http/oauth2/filter_test.cc | 133 ++- .../http/oauth2/oauth_integration_test.cc | 4 + .../filters/http/oauth2/oauth_test.cc | 20 +- .../ratelimit/ratelimit_integration_test.cc | 12 +- .../filters/http/squash/squash_filter_test.cc | 17 +- .../filters/network/common/fuzz/README.md | 4 +- .../filters/network/common/redis/BUILD | 1 + .../network/common/redis/client_impl_test.cc | 63 +- .../filters/network/mysql_proxy/BUILD | 37 + .../mysql_proxy/mysql_clogin_resp_test.cc | 456 +++++++++ .../network/mysql_proxy/mysql_clogin_test.cc | 584 +++++++++++ .../network/mysql_proxy/mysql_codec_test.cc | 960 +----------------- .../network/mysql_proxy/mysql_command_test.cc | 14 +- .../network/mysql_proxy/mysql_filter_test.cc | 121 ++- .../network/mysql_proxy/mysql_greet_test.cc | 469 +++++++++ .../mysql_proxy/mysql_integration_test.cc | 4 +- .../network/mysql_proxy/mysql_test_utils.cc | 86 +- .../network/mysql_proxy/mysql_test_utils.h | 16 +- .../proxy_filter_test.cc | 8 +- .../network/thrift_proxy/conn_manager_test.cc | 49 + .../integration/quic_http_integration_test.cc | 3 + .../metrics_service_integration_test.cc | 3 +- .../proxy_protocol_integration_test.cc | 1 + .../tls/context_impl_test.cc | 63 +- .../transport_sockets/tls/handshaker_test.cc | 5 +- .../transport_sockets/tls/utility_test.cc | 33 + test/integration/BUILD | 15 + test/integration/ads_integration_test.cc | 118 +++ test/integration/autonomous_upstream.cc | 3 +- ...nd_formatter_extension_integration_test.cc | 40 + .../extension_discovery_integration_test.cc | 158 ++- test/integration/fake_upstream.cc | 108 +- test/integration/fake_upstream.h | 45 +- test/integration/hds_integration_test.cc | 3 +- .../health_check_integration_test.cc | 122 ++- .../http2_flood_integration_test.cc | 46 +- test/integration/http2_integration_test.cc | 4 +- test/integration/http_integration.cc | 52 +- test/integration/http_integration.h | 8 +- test/integration/integration_test.cc | 195 +++- .../load_stats_integration_test.cc | 3 +- test/integration/stats_integration_test.cc | 4 +- .../integration/tcp_proxy_integration_test.cc | 2 + .../tcp_tunneling_integration_test.cc | 16 +- test/mocks/api/mocks.cc | 8 +- test/mocks/api/mocks.h | 8 +- test/mocks/config/mocks.cc | 13 +- test/mocks/config/mocks.h | 28 +- test/mocks/event/mocks.cc | 3 + test/mocks/event/mocks.h | 26 +- test/mocks/event/wrapped_dispatcher.h | 15 +- test/mocks/http/mocks.h | 9 + test/mocks/local_info/mocks.h | 1 + test/mocks/network/mocks.h | 6 +- test/mocks/ratelimit/mocks.h | 4 - test/mocks/router/mocks.h | 5 + test/mocks/router/router_filter_interface.cc | 3 +- .../server/bootstrap_extension_factory.cc | 5 + .../server/bootstrap_extension_factory.h | 9 + test/mocks/server/overload_manager.cc | 12 - test/mocks/server/overload_manager.h | 7 +- test/mocks/stream_info/mocks.cc | 4 +- test/per_file_coverage.sh | 3 +- test/proto/bookstore.proto | 5 + test/server/BUILD | 10 + .../config_validation/dispatcher_test.cc | 9 + test/server/connection_handler_test.cc | 10 +- test/server/filter_config_test.cc | 42 + test/server/lds_api_test.cc | 2 +- test/server/overload_manager_impl_test.cc | 139 +-- test/server/server_test.cc | 40 +- tools/api/generate_go_protobuf.py | 2 +- tools/code_format/check_format.py | 1 + tools/git/last_github_commit.sh | 4 +- tools/proto_format/proto_format.sh | 1 + tools/proto_format/proto_sync.py | 2 +- tools/spelling/spelling_dictionary.txt | 6 + 572 files changed, 15328 insertions(+), 4262 deletions(-) create mode 100644 api/envoy/config/retry/omit_canary_hosts/v3/BUILD create mode 100644 api/envoy/config/retry/omit_canary_hosts/v3/omit_canary_hosts.proto create mode 100644 api/envoy/extensions/filters/common/dependency/v3/BUILD create mode 100644 api/envoy/extensions/filters/common/dependency/v3/dependency.proto create mode 100644 api/envoy/extensions/filters/network/thrift_proxy/router/v3/BUILD create mode 100644 api/envoy/extensions/filters/network/thrift_proxy/router/v3/router.proto create mode 100644 api/envoy/type/matcher/v3/http_inputs.proto create mode 100644 api/envoy/type/matcher/v4alpha/http_inputs.proto create mode 100644 api/review_checklist.md create mode 100644 docs/root/start/sandboxes/tls-sni.rst create mode 100644 docs/root/version_history/v1.13.8.rst create mode 100644 docs/root/version_history/v1.17.0.rst create mode 100644 examples/_extra_certs/README.md create mode 100644 examples/_extra_certs/domain1.crt.pem create mode 100644 examples/_extra_certs/domain1.key.pem create mode 100644 examples/_extra_certs/domain2.crt.pem create mode 100644 examples/_extra_certs/domain2.key.pem create mode 100644 examples/_extra_certs/domain3.crt.pem create mode 100644 examples/_extra_certs/domain3.key.pem create mode 100644 examples/tls-sni/Dockerfile create mode 100644 examples/tls-sni/Dockerfile-client create mode 100644 examples/tls-sni/README.md create mode 100644 examples/tls-sni/docker-compose.yaml create mode 100644 examples/tls-sni/envoy-client.yaml create mode 100644 examples/tls-sni/envoy.yaml create mode 100755 examples/tls-sni/verify.sh create mode 100644 generated_api_shadow/envoy/config/retry/omit_canary_hosts/v3/BUILD create mode 100644 generated_api_shadow/envoy/config/retry/omit_canary_hosts/v3/omit_canary_hosts.proto create mode 100644 generated_api_shadow/envoy/extensions/filters/common/dependency/v3/BUILD create mode 100644 generated_api_shadow/envoy/extensions/filters/common/dependency/v3/dependency.proto create mode 100644 generated_api_shadow/envoy/extensions/filters/network/thrift_proxy/router/v3/BUILD create mode 100644 generated_api_shadow/envoy/extensions/filters/network/thrift_proxy/router/v3/router.proto create mode 100644 generated_api_shadow/envoy/type/matcher/v3/http_inputs.proto create mode 100644 generated_api_shadow/envoy/type/matcher/v4alpha/http_inputs.proto create mode 100644 include/envoy/config/context_provider.h create mode 100644 include/envoy/event/scaled_timer.h delete mode 100644 include/envoy/event/scaled_timer_minimum.h delete mode 100644 source/common/config/README.md create mode 100644 source/common/config/context_provider_impl.h delete mode 100644 source/common/config/xDS_code_diagram.png create mode 100644 source/common/http/match_wrapper/BUILD create mode 100644 source/common/http/match_wrapper/config.cc create mode 100644 source/common/http/match_wrapper/config.h create mode 100644 source/docs/aclick.png create mode 100644 source/docs/click.png create mode 100644 source/docs/coverage.md create mode 100644 source/docs/eclick.png create mode 100644 source/docs/file.png create mode 100644 source/docs/pr_view.png create mode 100644 source/docs/report.png create mode 100644 source/docs/results.png create mode 100644 source/docs/upload.png create mode 100644 source/docs/xds.md create mode 100644 source/extensions/filters/http/ext_proc/mutation_utils.cc create mode 100644 source/extensions/filters/http/ext_proc/mutation_utils.h create mode 100644 test/common/common/scope_tracker_test.cc create mode 100644 test/common/config/context_provider_impl_test.cc create mode 100644 test/common/formatter/command_extension.cc create mode 100644 test/common/formatter/command_extension.h create mode 100644 test/common/http/http2/hpack_corpus/use_after_free create mode 100644 test/common/http/http2/hpack_corpus/whitespace create mode 100644 test/common/http/match_wrapper/BUILD create mode 100644 test/common/http/match_wrapper/config_test.cc create mode 100644 test/common/stats/tag_extractor_impl_speed_test.cc create mode 100644 test/extensions/bootstrap/wasm/test_data/http_cpp.cc create mode 100644 test/extensions/bootstrap/wasm/wasm_integration_test.cc create mode 100644 test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc create mode 100644 test/extensions/filters/http/ext_proc/mock_server.cc create mode 100644 test/extensions/filters/http/ext_proc/mock_server.h create mode 100644 test/extensions/filters/http/ext_proc/mutation_utils_test.cc create mode 100644 test/extensions/filters/http/ext_proc/ordering_test.cc create mode 100644 test/extensions/filters/http/ext_proc/utils.cc create mode 100644 test/extensions/filters/http/ext_proc/utils.h create mode 100644 test/extensions/filters/network/mysql_proxy/mysql_clogin_resp_test.cc create mode 100644 test/extensions/filters/network/mysql_proxy/mysql_clogin_test.cc create mode 100644 test/extensions/filters/network/mysql_proxy/mysql_greet_test.cc create mode 100644 test/integration/command_formatter_extension_integration_test.cc create mode 100644 test/server/filter_config_test.cc diff --git a/.azure-pipelines/cve_scan.yml b/.azure-pipelines/cve_scan.yml index 9cfe24497984..322adae2bb71 100644 --- a/.azure-pipelines/cve_scan.yml +++ b/.azure-pipelines/cve_scan.yml @@ -9,7 +9,7 @@ schedules: displayName: Hourly CVE scan branches: include: - - master + - main always: true pool: diff --git a/.azure-pipelines/pipelines.yml b/.azure-pipelines/pipelines.yml index ad0ef9b4636f..79e34999caa4 100644 --- a/.azure-pipelines/pipelines.yml +++ b/.azure-pipelines/pipelines.yml @@ -1,7 +1,7 @@ trigger: branches: include: - - "master" + - "main" - "release/v*" tags: include: @@ -54,6 +54,7 @@ stages: - script: ci/run_envoy_docker.sh 'ci/do_ci.sh docs' workingDirectory: $(Build.SourcesDirectory) env: + AZP_BRANCH: $(Build.SourceBranch) ENVOY_DOCKER_BUILD_DIR: $(Build.StagingDirectory) BAZEL_REMOTE_CACHE: grpcs://remotebuildexecution.googleapis.com BAZEL_REMOTE_INSTANCE: projects/envoy-ci/instances/default_instance @@ -303,6 +304,7 @@ stages: - script: ci/run_envoy_docker.sh 'ci/do_ci.sh docs' workingDirectory: $(Build.SourcesDirectory) env: + AZP_BRANCH: $(Build.SourceBranch) ENVOY_DOCKER_BUILD_DIR: $(Build.StagingDirectory) BAZEL_REMOTE_CACHE: grpcs://remotebuildexecution.googleapis.com BAZEL_REMOTE_INSTANCE: projects/envoy-ci/instances/default_instance @@ -321,7 +323,6 @@ stages: workingDirectory: $(Build.SourcesDirectory) env: AZP_BRANCH: $(Build.SourceBranch) - AZP_SHA1: $(Build.SourceVersion) - stage: verify dependsOn: ["docker"] @@ -384,6 +385,11 @@ stages: pool: vmImage: "windows-latest" steps: + - task: Cache@2 + inputs: + key: '"windows.release" | ./WORKSPACE | **/*.bzl' + path: $(Build.StagingDirectory)/repository_cache + continueOnError: true - bash: ci/run_envoy_docker.sh ci/windows_ci_steps.sh displayName: "Run Windows CI" env: diff --git a/.bazelrc b/.bazelrc index d43694e4ccf9..faa93d0bff45 100644 --- a/.bazelrc +++ b/.bazelrc @@ -245,8 +245,8 @@ build:remote-clang-cl --config=clang-cl build:remote-clang-cl --config=rbe-toolchain-clang-cl # Docker sandbox -# NOTE: Update this from https://github.com/envoyproxy/envoy-build-tools/blob/master/toolchains/rbe_toolchains_config.bzl#L8 -build:docker-sandbox --experimental_docker_image=envoyproxy/envoy-build-ubuntu:11efa5680d987fff33fde4af3cc5ece105015d04 +# NOTE: Update this from https://github.com/envoyproxy/envoy-build-tools/blob/main/toolchains/rbe_toolchains_config.bzl#L8 +build:docker-sandbox --experimental_docker_image=envoyproxy/envoy-build-ubuntu:c8fa4235714003ba0896287ee2f91cae06e0e407 build:docker-sandbox --spawn_strategy=docker build:docker-sandbox --strategy=Javac=docker build:docker-sandbox --strategy=Closure=docker diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 77d7228bfbd0..bd72c0ebde6b 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM gcr.io/envoy-ci/envoy-build:11efa5680d987fff33fde4af3cc5ece105015d04 +FROM gcr.io/envoy-ci/envoy-build:c8fa4235714003ba0896287ee2f91cae06e0e407 ARG USERNAME=vscode ARG USER_UID=501 diff --git a/.github/ISSUE_TEMPLATE/non--crash-security--bug.md b/.github/ISSUE_TEMPLATE/non--crash-security--bug.md index 15c784680ecf..5b80df378a02 100644 --- a/.github/ISSUE_TEMPLATE/non--crash-security--bug.md +++ b/.github/ISSUE_TEMPLATE/non--crash-security--bug.md @@ -22,7 +22,7 @@ returned, etc. > Include sample requests, environment, etc. All data and inputs required to reproduce the bug. ->**Note**: The [Envoy_collect tool](https://github.com/envoyproxy/envoy/blob/master/tools/envoy_collect/README.md) +>**Note**: The [Envoy_collect tool](https://github.com/envoyproxy/envoy/blob/main/tools/envoy_collect/README.md) gathers a tarball with debug logs, config and the following admin endpoints: /stats, /clusters and /server_info. Please note if there are privacy concerns, sanitize the data prior to sharing the tarball/pasting. @@ -46,4 +46,4 @@ sharing. *Call Stack*: > If the Envoy binary is crashing, a call stack is **required**. -Please refer to the [Bazel Stack trace documentation](https://github.com/envoyproxy/envoy/tree/master/bazel#stack-trace-symbol-resolution). +Please refer to the [Bazel Stack trace documentation](https://github.com/envoyproxy/envoy/tree/main/bazel#stack-trace-symbol-resolution). diff --git a/.github/workflows/get_build_targets.sh b/.github/workflows/get_build_targets.sh index 25f67b74af50..874af4aefcc1 100755 --- a/.github/workflows/get_build_targets.sh +++ b/.github/workflows/get_build_targets.sh @@ -6,7 +6,7 @@ readonly SEARCH_FOLDER="//source/common/..." set -e -o pipefail function get_targets() { - # Comparing the PR HEAD with the upstream master HEAD. + # Comparing the PR HEAD with the upstream main HEAD. git diff --name-only HEAD FETCH_HEAD | while IFS= read -r line do # Only targets under those folders. @@ -23,6 +23,6 @@ function get_targets() { } # Fetching the upstream HEAD to compare with and stored in FETCH_HEAD. -git fetch https://github.com/envoyproxy/envoy.git master 2>/dev/null +git fetch https://github.com/envoyproxy/envoy.git main 2>/dev/null export BUILD_TARGETS_LOCAL=$(echo $(get_targets)) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 91642701329d..48c54fd9c6f4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -47,7 +47,7 @@ versioning guidelines: * Features may be marked as deprecated in a given versioned API at any point in time, but this may only be done when a replacement implementation and configuration path is available in Envoy on - master. Deprecators must implement a conversion from the deprecated configuration to the latest + main. Deprecators must implement a conversion from the deprecated configuration to the latest `vNalpha` (with the deprecated field) that Envoy uses internally. A field may be deprecated if this tool would be able to perform the conversion. For example, removing a field to describe HTTP/2 window settings is valid if a more comprehensive HTTP/2 protocol options field is being @@ -73,7 +73,7 @@ versioning guidelines: `envoy.features.enable_all_deprecated_features` is set to true. Finally, following the deprecation of the API major version where the field was first marked deprecated, the entire implementation code will be removed from the Envoy implementation. -* This policy means that organizations deploying master should have some time to get ready for +* This policy means that organizations deploying main should have some time to get ready for breaking changes at the next major API version. This is typically a window of at least 12 months or until the organization moves to the next major API version. * The breaking change policy also applies to source level extensions (e.g., filters). Code that @@ -99,7 +99,7 @@ versioning guidelines: Please see [support/README.md](support/README.md) for more information on these hooks. -* Create your PR. +* Create your PR. If your PR adds new code, it should include tests [covering](source/docs/coverage.md) the new code. * Tests will automatically run for you. * We will **not** merge any PR that is not passing tests. * PRs are expected to have 100% test coverage for added code. This can be verified with a coverage @@ -144,7 +144,7 @@ versioning guidelines: * If your PR involves any changes to [envoy-filter-example](https://github.com/envoyproxy/envoy-filter-example) (for example making a new branch so that CI can pass) it is your responsibility to follow through with merging those - changes back to master once the CI dance is done. + changes back to main once the CI dance is done. * If your PR is a high risk change, the reviewer may ask that you runtime guard it. See the section on runtime guarding below. @@ -189,18 +189,18 @@ maintainer's discretion. Generally all runtime guarded features will be set true release is cut. Old code paths for refactors can be cleaned up after a release and there has been some production run time. Old code for behavioral changes will be deprecated after six months. Runtime features are set true by default by inclusion in -[source/common/runtime/runtime_features.cc](https://github.com/envoyproxy/envoy/blob/master/source/common/runtime/runtime_features.cc) +[source/common/runtime/runtime_features.cc](https://github.com/envoyproxy/envoy/blob/main/source/common/runtime/runtime_features.cc) There are four suggested options for testing new runtime features: -1. Create a per-test Runtime::LoaderSingleton as done in [DeprecatedFieldsTest.IndividualFieldDisallowedWithRuntimeOverride](https://github.com/envoyproxy/envoy/blob/master/test/common/protobuf/utility_test.cc) +1. Create a per-test Runtime::LoaderSingleton as done in [DeprecatedFieldsTest.IndividualFieldDisallowedWithRuntimeOverride](https://github.com/envoyproxy/envoy/blob/main/test/common/protobuf/utility_test.cc) 2. Create a [parameterized test](https://github.com/google/googletest/blob/master/googletest/docs/advanced.md#how-to-write-value-parameterized-tests) where the set up of the test sets the new runtime value explicitly to GetParam() as outlined in (1). 3. Set up integration tests with custom runtime defaults as documented in the - [integration test README](https://github.com/envoyproxy/envoy/blob/master/test/integration/README.md) + [integration test README](https://github.com/envoyproxy/envoy/blob/main/test/integration/README.md) 4. Run a given unit test with the new runtime value explicitly set true or false as done - for [runtime_flag_override_test](https://github.com/envoyproxy/envoy/blob/master/test/common/runtime/BUILD) + for [runtime_flag_override_test](https://github.com/envoyproxy/envoy/blob/main/test/common/runtime/BUILD) Runtime code is held to the same standard as regular Envoy code, so both the old path and the new should have 100% coverage both with the feature defaulting true @@ -233,6 +233,13 @@ and false. * If a PR includes a deprecation/breaking change, notification should be sent to the [envoy-announce](https://groups.google.com/forum/#!forum/envoy-announce) email list. +# API changes + +If you change anything in the [api tree](https://github.com/envoyproxy/envoy/tree/master/api), +please read the [API Review +Checklist](https://github.com/envoyproxy/envoy/tree/master/api/review_checklist.md) +and make sure that your changes have addressed all of the considerations listed there. + # Adding new extensions For developers adding a new extension, one can take an existing extension as the starting point. diff --git a/DEPENDENCY_POLICY.md b/DEPENDENCY_POLICY.md index 588dad437ad2..abefac1fe550 100644 --- a/DEPENDENCY_POLICY.md +++ b/DEPENDENCY_POLICY.md @@ -40,7 +40,7 @@ Dependency declarations must: and `urls` to reference the version. If you need to reference version `X.Y.Z` as `X_Y_Z`, this may appear in a string as `{underscore_version}`, similarly for `X-Y-Z` you can use `{dash_version}`. -* Versions should prefer release versions over master branch GitHub SHA tarballs. A comment is +* Versions should prefer release versions over main branch GitHub SHA tarballs. A comment is necessary if the latter is used. This comment should contain the reason that a non-release version is being used. * Provide accurate entries for `use_category`. Please think carefully about whether there are data @@ -112,7 +112,7 @@ basis: * Extension [CODEOWNERS](CODEOWNERS) should update extension specific dependencies. -Where possible, we prefer the latest release version for external dependencies, rather than master +Where possible, we prefer the latest release version for external dependencies, rather than main branch GitHub SHA tarballs. ## Dependency shepherds diff --git a/DEVELOPER.md b/DEVELOPER.md index 6786925fa7e8..7a4ec69f54c6 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -1,38 +1,38 @@ # Developer documentation Envoy is built using the Bazel build system. Our CI on Azure Pipelines builds, tests, and runs coverage against -all pull requests and the master branch. +all pull requests and the main branch. -To get started building Envoy locally, see the [Bazel quick start](https://github.com/envoyproxy/envoy/blob/master/bazel/README.md#quick-start-bazel-build-for-developers). -To run tests, there are Bazel [targets](https://github.com/envoyproxy/envoy/blob/master/bazel/README.md#testing-envoy-with-bazel) for Google Test. -To generate a coverage report, there is a [coverage build script](https://github.com/envoyproxy/envoy/blob/master/bazel/README.md#coverage-builds). +To get started building Envoy locally, see the [Bazel quick start](https://github.com/envoyproxy/envoy/blob/main/bazel/README.md#quick-start-bazel-build-for-developers). +To run tests, there are Bazel [targets](https://github.com/envoyproxy/envoy/blob/main/bazel/README.md#testing-envoy-with-bazel) for Google Test. +To generate a coverage report, there is a [coverage build script](https://github.com/envoyproxy/envoy/blob/main/bazel/README.md#coverage-builds). -If you plan to contribute to Envoy, you may find it useful to install the Envoy [development support toolchain](https://github.com/envoyproxy/envoy/blob/master/support/README.md), which helps automate parts of the development process, particularly those involving code review. +If you plan to contribute to Envoy, you may find it useful to install the Envoy [development support toolchain](https://github.com/envoyproxy/envoy/blob/main/support/README.md), which helps automate parts of the development process, particularly those involving code review. Below is a list of additional documentation to aid the development process: - [General build and installation documentation](https://www.envoyproxy.io/docs/envoy/latest/start/start) -- [Building and testing Envoy with Bazel](https://github.com/envoyproxy/envoy/blob/master/bazel/README.md) +- [Building and testing Envoy with Bazel](https://github.com/envoyproxy/envoy/blob/main/bazel/README.md) -- [Managing external dependencies with Bazel](https://github.com/envoyproxy/envoy/blob/master/bazel/EXTERNAL_DEPS.md) +- [Managing external dependencies with Bazel](https://github.com/envoyproxy/envoy/blob/main/bazel/EXTERNAL_DEPS.md) -- [Guide to Envoy Bazel rules (managing `BUILD` files)](https://github.com/envoyproxy/envoy/blob/master/bazel/DEVELOPER.md) +- [Guide to Envoy Bazel rules (managing `BUILD` files)](https://github.com/envoyproxy/envoy/blob/main/bazel/DEVELOPER.md) -- [Using Docker for building and testing](https://github.com/envoyproxy/envoy/tree/master/ci) +- [Using Docker for building and testing](https://github.com/envoyproxy/envoy/tree/main/ci) -- [Guide to contributing to Envoy](https://github.com/envoyproxy/envoy/blob/master/CONTRIBUTING.md) +- [Guide to contributing to Envoy](https://github.com/envoyproxy/envoy/blob/main/CONTRIBUTING.md) -- [Overview of Envoy's testing frameworks](https://github.com/envoyproxy/envoy/blob/master/test/README.md) +- [Overview of Envoy's testing frameworks](https://github.com/envoyproxy/envoy/blob/main/test/README.md) -- [Overview of how to write integration tests for new code](https://github.com/envoyproxy/envoy/blob/master/test/integration/README.md) +- [Overview of how to write integration tests for new code](https://github.com/envoyproxy/envoy/blob/main/test/integration/README.md) - [Envoy filter example project (how to consume and extend Envoy as a submodule)](https://github.com/envoyproxy/envoy-filter-example) -- [Performance testing Envoy with `tcmalloc`/`pprof`](https://github.com/envoyproxy/envoy/blob/master/bazel/PPROF.md) +- [Performance testing Envoy with `tcmalloc`/`pprof`](https://github.com/envoyproxy/envoy/blob/main/bazel/PPROF.md) And some documents on components of Envoy architecture: -- [Envoy flow control](https://github.com/envoyproxy/envoy/blob/master/source/docs/flow_control.md) +- [Envoy flow control](https://github.com/envoyproxy/envoy/blob/main/source/docs/flow_control.md) -- [Envoy's subset load balancer](https://github.com/envoyproxy/envoy/blob/master/source/docs/subset_load_balancer.md) +- [Envoy's subset load balancer](https://github.com/envoyproxy/envoy/blob/main/source/docs/subset_load_balancer.md) diff --git a/EXTENSION_POLICY.md b/EXTENSION_POLICY.md index 2f216813a6ff..39d41f14f875 100644 --- a/EXTENSION_POLICY.md +++ b/EXTENSION_POLICY.md @@ -15,7 +15,7 @@ The following procedure will be used when proposing new extensions for inclusion 2. All extensions must be sponsored by an existing maintainer. Sponsorship means that the maintainer will shepherd the extension through design/code reviews. Maintainers can self-sponsor extensions if they are going to write them, shepherd them, and maintain them. - + Sponsorship serves two purposes: * It ensures that the extension will ultimately meet the Envoy quality bar. * It makes sure that incentives are aligned and that extensions are not added to the repo without @@ -24,7 +24,7 @@ The following procedure will be used when proposing new extensions for inclusion *If sponsorship cannot be found from an existing maintainer, an organization can consider [doing the work to become a maintainer](./GOVERNANCE.md#process-for-becoming-a-maintainer) in order to be able to self-sponsor extensions.* - + 3. Each extension must have two reviewers proposed for reviewing PRs to the extension. Neither of the reviewers must be a senior maintainer. Existing maintainers (including the sponsor) and other contributors can count towards this number. The initial reviewers will be codified in the @@ -105,7 +105,7 @@ The `security_posture` is one of: * `unknown`: This is functionally equivalent to `requires_trusted_downstream_and_upstream`, but acts as a placeholder to allow us to identify extensions that need classifying. * `data_plane_agnostic`: Not relevant to data plane threats, e.g. stats sinks. - + An assessment of a robust security posture for an extension is subject to the following guidelines: * Does the extension have fuzz coverage? If it's only receiving fuzzing @@ -122,7 +122,7 @@ An assessment of a robust security posture for an extension is subject to the fo * Does the extension have active [CODEOWNERS](CODEOWNERS) who are willing to vouch for the robustness of the extension? * Is the extension absent a [low coverage - exception](https://github.com/envoyproxy/envoy/blob/master/test/per_file_coverage.sh#L5)? + exception](https://github.com/envoyproxy/envoy/blob/main/test/per_file_coverage.sh#L5)? The current stability and security posture of all extensions can be seen [here](https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/threat_model#core-and-extensions). diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 1cb5157443ea..593a1c02e25a 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -38,7 +38,7 @@ The areas of specialization listed in [OWNERS.md](OWNERS.md) can be used to help with routing an issue/question to the right person. * Triage build issues - file issues for known flaky builds or bugs, and either fix or find someone - to fix any master build breakages. + to fix any main build breakages. * During GitHub issue triage, apply all applicable [labels](https://github.com/envoyproxy/envoy/labels) to each new issue. Labels are extremely useful for future issue follow up. Which labels to apply is somewhat subjective so just use your best judgment. A few of the most important labels that are @@ -77,7 +77,7 @@ or you can subscribe to the iCal feed [here](webcal://kubernetes.app.opsgenie.co "is:open is:issue milestone:[current milestone]" and either hold off until they are fixed or bump them to the next milestone. * Begin marshalling the ongoing PR flow in this repo. Ask maintainers to hold off merging any - particularly risky PRs until after the release is tagged. This is because we aim for master to be + particularly risky PRs until after the release is tagged. This is because we aim for main to be at release candidate quality at all times. * Do a final check of the [release notes](docs/root/version_history/current.rst): * Make any needed corrections (grammar, punctuation, formatting, etc.). @@ -91,7 +91,7 @@ or you can subscribe to the iCal feed [here](webcal://kubernetes.app.opsgenie.co release, please also make sure there's a stable maintainer signed up for next quarter, and the deadline for the next release is documented in the release schedule. * Get a review and merge. -* Wait for tests to pass on [master](https://dev.azure.com/cncf/envoy/_build). +* Wait for tests to pass on [main](https://dev.azure.com/cncf/envoy/_build). * Create a [tagged release](https://github.com/envoyproxy/envoy/releases). The release should start with "v" and be followed by the version number. E.g., "v1.6.0". **This must match the [VERSION](VERSION).** @@ -106,7 +106,7 @@ or you can subscribe to the iCal feed [here](webcal://kubernetes.app.opsgenie.co * Make sure we tweet the new release: either have Matt do it or email social@cncf.io and ask them to do an Envoy account post. * Do a new PR to setup the next version - * Update [VERSION](VERSION) to the next development release. E.g., "1.7.0-dev". + * Update [VERSION](VERSION) to the next development release. E.g., "1.7.0-dev". * `git mv docs/root/version_history/current.rst docs/root/version_history/v1.6.0.rst`, filling in the previous release version number in the filename and delete empty sections (like Incompatible Behavior Changes, Minor Bahavior Changes, etc). Add an entry for the new file in the `toctree` in diff --git a/PULL_REQUESTS.md b/PULL_REQUESTS.md index 3fb17835ebd0..e8a5a390b048 100644 --- a/PULL_REQUESTS.md +++ b/PULL_REQUESTS.md @@ -111,3 +111,11 @@ PR description. If you mark existing APIs or code as deprecated, when the next release is cut, the deprecation script will create and assign an issue to you for cleaning up the deprecated code path. + +### API Changes + +If this PR changes anything in the [api tree](https://github.com/envoyproxy/envoy/tree/master/api), +please read the [API Review +Checklist](https://github.com/envoyproxy/envoy/tree/master/api/review_checklist.md) +and make sure that your changes have addressed all of the considerations listed there. +Any relevant considerations should be documented under "API Considerations" in the PR description. diff --git a/PULL_REQUEST_TEMPLATE.md b/PULL_REQUEST_TEMPLATE.md index 366209eed929..74af96b6e142 100644 --- a/PULL_REQUEST_TEMPLATE.md +++ b/PULL_REQUEST_TEMPLATE.md @@ -10,7 +10,7 @@ Thank you in advance for helping to keep Envoy secure. --> For an explanation of how to fill out the fields, please see the relevant section -in [PULL_REQUESTS.md](https://github.com/envoyproxy/envoy/blob/master/PULL_REQUESTS.md) +in [PULL_REQUESTS.md](https://github.com/envoyproxy/envoy/blob/main/PULL_REQUESTS.md) Commit Message: Additional Description: @@ -22,3 +22,4 @@ Platform Specific Features: [Optional Runtime guard:] [Optional Fixes #Issue] [Optional Deprecated:] +[Optional [API Considerations](https://github.com/envoyproxy/envoy/blob/master/api/review_checklist.md):] diff --git a/README.md b/README.md index 03c0aa0432d6..e0347b08e328 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![Envoy Logo](https://github.com/envoyproxy/artwork/blob/master/PNG/Envoy_Logo_Final_PANTONE.png) +![Envoy Logo](https://github.com/envoyproxy/artwork/blob/main/PNG/Envoy_Logo_Final_PANTONE.png) [Cloud-native high-performance edge/middle/service proxy](https://www.envoyproxy.io/) @@ -9,7 +9,7 @@ involved and how Envoy plays a role, read the CNCF [announcement](https://www.cncf.io/blog/2017/09/13/cncf-hosts-envoy/). [![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/1266/badge)](https://bestpractices.coreinfrastructure.org/projects/1266) -[![Azure Pipelines](https://dev.azure.com/cncf/envoy/_apis/build/status/11?branchName=master)](https://dev.azure.com/cncf/envoy/_build/latest?definitionId=11&branchName=master) +[![Azure Pipelines](https://dev.azure.com/cncf/envoy/_apis/build/status/11?branchName=main)](https://dev.azure.com/cncf/envoy/_build/latest?definitionId=11&branchName=main) [![Fuzzing Status](https://oss-fuzz-build-logs.storage.googleapis.com/badges/envoy.svg)](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&can=1&q=proj:envoy) [![Jenkins](https://powerci.osuosl.org/buildStatus/icon?job=build-envoy-static-master&subject=ppc64le%20build)](https://powerci.osuosl.org/job/build-envoy-static-master/) @@ -65,7 +65,7 @@ have prior experience. To get started: * [Beginner issues](https://github.com/envoyproxy/envoy/issues?q=is%3Aopen+is%3Aissue+label%3Abeginner) * [Build/test quick start using docker](ci#building-and-running-tests-as-a-developer) * [Developer guide](DEVELOPER.md) -* Consider installing the Envoy [development support toolchain](https://github.com/envoyproxy/envoy/blob/master/support/README.md), which helps automate parts of the development process, particularly those involving code review. +* Consider installing the Envoy [development support toolchain](https://github.com/envoyproxy/envoy/blob/main/support/README.md), which helps automate parts of the development process, particularly those involving code review. * Please make sure that you let us know if you are working on an issue so we don't duplicate work! ## Community Meeting diff --git a/RELEASES.md b/RELEASES.md index a1d7929ad673..a630f49633e7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -2,7 +2,7 @@ ## Active development -Active development is happening on the `master` branch, and a new version is released from it +Active development is happening on the `main` branch, and a new version is released from it at the end of each quarter. ## Stable releases @@ -10,23 +10,23 @@ at the end of each quarter. Stable releases of Envoy include: * Extended maintenance window (any version released in the last 12 months). -* Security fixes backported from the `master` branch (including those deemed not worthy +* Security fixes backported from the `main` branch (including those deemed not worthy of creating a CVE). -* Stability fixes backported from the `master` branch (anything that can result in a crash, +* Stability fixes backported from the `main` branch (anything that can result in a crash, including crashes triggered by a trusted control plane). * Bugfixes, deemed worthwhile by the maintainers of stable releases. ### Hand-off Hand-off to the maintainers of stable releases happens after Envoy maintainers release a new -version from the `master` branch by creating a `vX.Y.0` tag and a corresponding `release/vX.Y` +version from the `main` branch by creating a `vX.Y.0` tag and a corresponding `release/vX.Y` branch, with merge permissions given to the release manager of stable releases, and CI configured to execute tests on it. ### Security releases Critical security fixes are owned by the Envoy security team, which provides fixes for the -`master` branch, and the latest release branch. Once those fixes are ready, the maintainers +`main` branch, and the latest release branch. Once those fixes are ready, the maintainers of stable releases backport them to the remaining supported stable releases. ### Backports @@ -37,7 +37,7 @@ by adding the `backport/review` or `backport/approved` label (this can be done u `/backport` command). Changes nominated by the change author and/or members of the Envoy community are evaluated for backporting on a case-by-case basis, and require approval from either the release manager of stable release, Envoy maintainers, or Envoy security team. Once approved, those fixes -are backported from the `master` branch to all supported stable branches by the maintainers of +are backported from the `main` branch to all supported stable branches by the maintainers of stable releases. New stable versions from non-critical security fixes are released on a regular schedule, initially aiming for the bi-weekly releases. @@ -67,7 +67,7 @@ deadline of 3 weeks. | 1.14.0 | 2020/03/31 | 2020/04/08 | +8 days | 2021/04/08 | | 1.15.0 | 2020/06/30 | 2020/07/07 | +7 days | 2021/07/07 | | 1.16.0 | 2020/09/30 | 2020/10/08 | +8 days | 2021/10/08 | -| 1.17.0 | 2020/12/31 | | | | - +| 1.17.0 | 2020/12/31 | 2021/01/11 | +11 days | 2022/01/11 | +| 1.18.0 | 2021/03/31 | | | | [repokitteh]: https://github.com/repokitteh diff --git a/REPO_LAYOUT.md b/REPO_LAYOUT.md index fff64a494971..b8f509b2194c 100644 --- a/REPO_LAYOUT.md +++ b/REPO_LAYOUT.md @@ -89,6 +89,8 @@ code/extensions, and allows us specify extension owners in [CODEOWNERS](CODEOWNE `Envoy::Extensions::ListenerFilters` namespace. * [filters/network/](/source/extensions/filters/network): L4 network filters which use the `Envoy::Extensions::NetworkFilters` namespace. + * [formatters](/source/extensions/formatters): Access log formatters which use the + `Envoy::Extensions::Formatters` namespace. * [grpc_credentials/](/source/extensions/grpc_credentials): Custom gRPC credentials which use the `Envoy::Extensions::GrpcCredentials` namespace. * [health_checker/](/source/extensions/health_checker): Custom health checkers which use the diff --git a/SECURITY.md b/SECURITY.md index 34138877e6b3..53b4de82b780 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -63,31 +63,31 @@ score >= 4; see below). If the fix relies on another upstream project's disclosu will adjust the process as well. We will work with the upstream project to fit their timeline and best protect our users. -### Released versions and master branch +### Released versions and main branch If the vulnerability affects the last point release version, e.g. 1.10, then the full security release process described in this document will be activated. A security point release will be -created for 1.10, e.g. 1.10.1, together with a fix to master if necessary. Older point releases, +created for 1.10, e.g. 1.10.1, together with a fix to main if necessary. Older point releases, e.g. 1.9, are not supported by the Envoy project and will not have any security release created. -If a security vulnerability affects only these older versions but not master or the last supported +If a security vulnerability affects only these older versions but not main or the last supported point release, the Envoy security team will share this information with the private distributor list, following the standard embargo process, but not create a security release. After the embargo expires, the vulnerability will be described as a GitHub issue. A CVE will be filed if warranted by severity. -If a vulnerability does not affect any point release but only master, additional caveats apply: +If a vulnerability does not affect any point release but only main, additional caveats apply: * If the issue is detected and a fix is available within 7 days of the introduction of the vulnerability, or the issue is deemed a low severity vulnerability by the Envoy maintainer and - security teams, the fix will be publicly reviewed and landed on master. If the severity is at least + security teams, the fix will be publicly reviewed and landed on main. If the severity is at least medium or at maintainer discretion a courtesy e-mail will be sent to envoy-users@googlegroups.com, envoy-dev@googlegroups.com, envoy-security-announce@googlegroups.com and cncf-envoy-distributors-announce@lists.cncf.io. * If the vulnerability has been in existence for more than 7 days and is medium or higher, we will activate the security release process. -We advise distributors and operators working from the master branch to allow at least 5 days soak +We advise distributors and operators working from the main branch to allow at least 5 days soak time after cutting a binary release before distribution or rollout, to allow time for our fuzzers to detect issues during their execution on ClusterFuzz. A soak period of 7 days provides an even stronger guarantee, since we will invoke the security release process for medium or higher severity issues @@ -181,7 +181,7 @@ patches, understand exact mitigation steps, etc. should be reserved for remotely exploitable or privilege escalation issues. Otherwise, this process can be skipped. - The Fix Lead will email the patches to cncf-envoy-distributors-announce@lists.cncf.io so - distributors can prepare builds to be available to users on the day of the issue's announcement. Any + distributors can prepare builds to be available to users on the day of the issue's announcement. Any patches against main will be updated and resent weekly. Distributors should read about the [Private Distributors List](#private-distributors-list) to find out the requirements for being added to this list. @@ -193,7 +193,7 @@ patches, understand exact mitigation steps, etc. - The maintainers will create a new patch release branch from the latest patch release tag + the fix from the security branch. As a practical example if v1.5.3 is the latest patch release in Envoy.git a new branch will be created called v1.5.4 which includes only patches required to fix the issue. -- The Fix Lead will cherry-pick the patches onto the master branch and all relevant release branches. +- The Fix Lead will cherry-pick the patches onto the main branch and all relevant release branches. The Fix Team will LGTM and merge. Maintainers will merge these PRs as quickly as possible. Changes shouldn't be made to the commits even for a typo in the CHANGELOG as this will change the git sha of the commits leading to confusion and potentially conflicts as the fix is cherry-picked around diff --git a/STYLE.md b/STYLE.md index 3feafbcc10a0..7156614b1d48 100644 --- a/STYLE.md +++ b/STYLE.md @@ -106,24 +106,35 @@ A few general notes on our error handling philosophy: * All error code returns should be checked. * At a very high level, our philosophy is that errors should be handled gracefully when caused by: - - Untrusted network traffic OR + - Untrusted network traffic (from downstream, upstream, or extensions like filters) - Raised by the Envoy process environment and are *likely* to happen + - Third party dependency return codes * Examples of likely environnmental errors include any type of network error, disk IO error, bad - data returned by an API call, bad data read from runtime files, etc. Errors in the Envoy - environment that are *unlikely* to happen after process initialization, should lead to process - death, under the assumption that the additional burden of defensive coding and testing is not an - effective use of time for an error that should not happen given proper system setup. Examples of - these types of errors include not being able to open the shared memory region, system calls that - should not fail assuming correct parameters (which should be validated via tests), etc. Examples - of system calls that should not fail when passed valid parameters include the kernel returning a - valid `sockaddr` after a successful call to `accept()`, `pthread_create()`, `pthread_join()`, etc. -* OOM events (both memory and FDs) are considered fatal crashing errors. An OOM error should never - silently be ignored and should crash the process either via the C++ allocation error exception, an - explicit `RELEASE_ASSERT` following a third party library call, or an obvious crash on a subsequent - line via null pointer dereference. This rule is again based on the philosophy that the engineering - costs of properly handling these cases are not worth it. Time is better spent designing proper system - controls that shed load if resource usage becomes too high, etc. -* The "less is more" error handling philosophy described in the previous two points is primarily + data returned by an API call, bad data read from runtime files, etc. This includes loading + configuration at runtime. +* Third party dependency return codes should be checked and gracefully handled. Examples include + HTTP/2 or JSON parsers. Some return codes may be handled by continuing, for example, in case of an + out of process RPC failure. +* Testing should cover any serious cases that may result in infinite loops, crashes, or serious + errors. Non-trivial invariants are also encouraged to have testing. Internal, localized invariants + may not need testing. +* Errors in the Envoy environment that are *unlikely* to happen after process initialization, should + lead to process death, under the assumption that the additional burden of defensive coding and + testing is not an effective use of time for an error that should not happen given proper system + setup. Examples of these types of errors include not being able to open the shared memory region, + system calls that should not fail assuming correct parameters (which should be validated via + tests), etc. Examples of system calls that should not fail when passed valid parameters include + the kernel returning a valid `sockaddr` after a successful call to `accept()`, `pthread_create()`, + `pthread_join()`, etc. However, system calls that require permissions may cause likely errors in + some deployments and need graceful error handling. +* OOM events (both memory and FDs) or ENOMEM errors are considered fatal crashing errors. An OOM + error should never silently be ignored and should crash the process either via the C++ allocation + error exception, an explicit `RELEASE_ASSERT` following a third party library call, or an obvious + crash on a subsequent line via null pointer dereference. This rule is again based on the + philosophy that the engineering costs of properly handling these cases are not worth it. Time is + better spent designing proper system controls that shed load if resource usage becomes too high, + etc. +* The "less is more" error handling philosophy described in the previous points is primarily based on the fact that restarts are designed to be fast, reliable and cheap. * Although we strongly recommend that any type of startup error leads to a fatal error, since this is almost always a result of faulty configuration which should be caught during a canary process, @@ -148,20 +159,67 @@ A few general notes on our error handling philosophy: continue seems ridiculous because *"this should never happen"*, it's a very good indication that the appropriate behavior is to terminate the process and not handle the error. When in doubt, please discuss. -* Per above it's acceptable to turn failures into crash semantics - via `RELEASE_ASSERT(condition)` or `PANIC(message)` if there is no other sensible behavior, e.g. - in OOM (memory/FD) scenarios. Only `RELEASE_ASSERT(condition)` should be used to validate - conditions that might be imposed by the external environment. `ASSERT(condition)` should be used - to document (and check in debug-only builds) program invariants. Use `ASSERT` liberally, but do - not use it for things that will crash in an obvious way in a subsequent line. E.g., do not do - `ASSERT(foo != nullptr); foo->doSomething();`. Note that there is a gray line between external - environment failures and program invariant violations. For example, memory corruption due to a - security issue (a bug, deliberate buffer overflow etc.) might manifest as a violation of program - invariants or as a detectable condition in the external environment (e.g. some library returning a - highly unexpected error code or buffer contents). Unfortunately no rule can cleanly cover when to - use `RELEASE_ASSERT` vs. `ASSERT`. In general we view `ASSERT` as the common case and - `RELEASE_ASSERT` as the uncommon case, but experience and judgment may dictate a particular approach - depending on the situation. + +# Macro Usage + +* The following macros are available: + - `RELEASE_ASSERT`: fatal check. + - `ASSERT`: fatal check in debug-only builds. These should be used to document (and check in + debug-only builds) program invariants. + - `ENVOY_BUG`: logs and increments a stat in release mode, fatal check in debug builds. These + should be used where it may be useful to detect if an efficient condition is violated in + production (and fatal check in debug-only builds). + +* Sub-macros alias the macros above and can be used to annotate specific situations: + - `ENVOY_BUG_ALPHA` (alias `ENVOY_BUG`): Used for alpha or rapidly changing protocols that need + detectability on probable conditions or invariants. + +* Per above it's acceptable to turn failures into crash semantics via `RELEASE_ASSERT(condition)` or + `PANIC(message)` if there is no other sensible behavior, e.g. in OOM (memory/FD) scenarios. +* Do not `ASSERT` on conditions imposed by the external environment. Either add error handling + (potentially with an `ENVOY_BUG` for detectability) or `RELEASE_ASSERT` if the condition indicates + that the process is unrecoverable. +* Use `ASSERT` and `ENVOY_BUG` liberally, but do not use them for things that will crash in an obvious + way in a subsequent line. E.g., do not do `ASSERT(foo != nullptr); foo->doSomething();`. +* Use `ASSERT`s for true invariants and well-defined conditions that are useful for tests, + debug-only checks and documentation. They may be `ENVOY_BUG`s if performance allows, see point + below. +* `ENVOY_BUG`s provide detectability and more confidence than an `ASSERT`. They are useful for + non-trivial conditions, those with complex control flow, and rapidly changing protocols. Testing + should be added to ensure that Envoy can continue to operate even if an `ENVOY_BUG` condition is + violated. +* Annotate conditions with comments on belief or reasoning, for example `Condition is guaranteed by + caller foo` or `Condition is likely to hold after processing through external library foo`. +* Macro usage should be understandable to a reader. Add comments if not. They should be robust to + future changes. +* Note that there is a gray line between external environment failures and program invariant + violations. For example, memory corruption due to a security issue (a bug, deliberate buffer + overflow etc.) might manifest as a violation of program invariants or as a detectable condition in + the external environment (e.g. some library returning a highly unexpected error code or buffer + contents). Unfortunately no rule can cleanly cover when to use `RELEASE_ASSERT` vs. `ASSERT`. In + general we view `ASSERT` as the common case and `RELEASE_ASSERT` as the uncommon case, but + experience and judgment may dictate a particular approach depending on the situation. The risk of + process death from `RELEASE_ASSERT` should be justified with the severity and possibility of the + condition to avoid unintentional crashes. You may use the following guide: + * If a violation is high risk (will cause a crash in subsequent data processing or indicates a + failure state beyond recovery), use `RELEASE_ASSERT`. + * If a violation is medium or low risk (Envoy can continue safely) and is not expensive, + consider `ENVOY_BUG`. + * Otherwise (if a condition is expensive or test-only), use `ASSERT`. + +Below is a guideline for macro usage. The left side of the table has invariants and the right side +has error conditions that can be triggered and should be gracefully handled. `ENVOY_BUG` represents +a middle ground that can be used for uncertain conditions that need detectability. `ENVOY_BUG`s can +also be added for errors if they warrant detection. + +| `ASSERT`/`RELEASE_ASSERT` | `ENVOY_BUG` | Error handling and Testing | +| --- | --- | --- | +| Low level invariants in data structures | | | +| Simple, provable internal class invariants | Complex, uncertain internal class invariants (e.g. need detectability if violated) | | +| Provable (pre/post)-conditions | Complicated but likely (pre-/post-) conditions that are low-risk (Envoy can continue safely) | Triggerable or uncertain conditions, may be based on untrusted data plane traffic or an extensions’ contract. | +| | Conditions in alpha or changing extensions that need detectability. (`ENVOY_BUG_ALPHA`) | | +| Unlikely environment errors after process initialization that would otherwise crash | | Likely environment errors, e.g. return codes from untrusted extensions, dependencies or system calls, network error, bad data read, permission based errors, etc. | +| Fatal crashing events. e.g. OOMs, deadlocks, no process recovery possible | | | # Hermetic and deterministic tests diff --git a/VERSION b/VERSION index ee8855caa4a7..ee017091ff37 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.17.0-dev +1.18.0-dev diff --git a/api/API_VERSIONING.md b/api/API_VERSIONING.md index 1757b0f2efce..6952ca0954ef 100644 --- a/api/API_VERSIONING.md +++ b/api/API_VERSIONING.md @@ -105,7 +105,7 @@ Envoy will support at most three major versions of any API package at all times: for the next stable major version. This is only generated when the current stable major version requires a breaking change at the next cycle, e.g. a deprecation or field rename. This release candidate is mechanically generated via the - [protoxform](https://github.com/envoyproxy/envoy/tree/master/tools/protoxform) tool from the + [protoxform](https://github.com/envoyproxy/envoy/tree/main/tools/protoxform) tool from the current stable major version, making use of annotations such as `deprecated = true`. This is not a human editable artifact. @@ -161,7 +161,7 @@ methods, depending on whether the change is mechanical or manual. ## Mechanical breaking changes Field deprecations, renames, etc. are mechanical changes that are supported by the -[protoxform](https://github.com/envoyproxy/envoy/tree/master/tools/protoxform) tool. These are +[protoxform](https://github.com/envoyproxy/envoy/tree/main/tools/protoxform) tool. These are guided by [annotations](STYLE.md#api-annotations). ## Manual breaking changes diff --git a/api/BUILD b/api/BUILD index 6b8fc64edab1..effde23bad70 100644 --- a/api/BUILD +++ b/api/BUILD @@ -144,6 +144,7 @@ proto_library( "//envoy/config/resource_monitor/fixed_heap/v2alpha:pkg", "//envoy/config/resource_monitor/injected_resource/v2alpha:pkg", "//envoy/config/retry/omit_canary_hosts/v2:pkg", + "//envoy/config/retry/omit_canary_hosts/v3:pkg", "//envoy/config/retry/previous_hosts/v2:pkg", "//envoy/config/route/v3:pkg", "//envoy/config/tap/v3:pkg", @@ -165,6 +166,7 @@ proto_library( "//envoy/extensions/common/tap/v3:pkg", "//envoy/extensions/compression/gzip/compressor/v3:pkg", "//envoy/extensions/compression/gzip/decompressor/v3:pkg", + "//envoy/extensions/filters/common/dependency/v3:pkg", "//envoy/extensions/filters/common/fault/v3:pkg", "//envoy/extensions/filters/common/matcher/action/v3:pkg", "//envoy/extensions/filters/http/adaptive_concurrency/v3:pkg", @@ -230,6 +232,7 @@ proto_library( "//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3alpha:pkg", "//envoy/extensions/filters/network/tcp_proxy/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/filters/ratelimit/v3:pkg", + "//envoy/extensions/filters/network/thrift_proxy/router/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/v3:pkg", "//envoy/extensions/filters/network/wasm/v3:pkg", "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", diff --git a/api/CONTRIBUTING.md b/api/CONTRIBUTING.md index 01ba39b500b8..847cdf74dc55 100644 --- a/api/CONTRIBUTING.md +++ b/api/CONTRIBUTING.md @@ -9,7 +9,7 @@ changes. They may be as part of a larger implementation PR. Please follow the st process for validating build/test sanity of `api/` before submitting a PR. *Note: New .proto files should be added to -[BUILD](https://github.com/envoyproxy/envoy/blob/master/api/versioning/BUILD) in order to get the RSTs generated.* +[BUILD](https://github.com/envoyproxy/envoy/blob/main/api/versioning/BUILD) in order to get the RSTs generated.* ## Documentation changes diff --git a/api/README.md b/api/README.md index fa6f3bb1eeba..94fce1159e46 100644 --- a/api/README.md +++ b/api/README.md @@ -9,9 +9,9 @@ blog post for more information on the universal data plane concept. # Repository structure The API tree can be found at two locations: -* https://github.com/envoyproxy/envoy/tree/master/api - canonical read/write home for the APIs. +* https://github.com/envoyproxy/envoy/tree/main/api - canonical read/write home for the APIs. * https://github.com/envoyproxy/data-plane-api - read-only mirror of - https://github.com/envoyproxy/envoy/tree/master/api, providing the ability to consume the data + https://github.com/envoyproxy/envoy/tree/main/api, providing the ability to consume the data plane APIs without the Envoy implementation. # Further API reading diff --git a/api/STYLE.md b/api/STYLE.md index 7e15d2ec9f1b..79a08ee67f79 100644 --- a/api/STYLE.md +++ b/api/STYLE.md @@ -81,6 +81,12 @@ In addition, the following conventions should be followed: pattern forces developers to explicitly choose the correct enum value for their use case, and avoid misunderstanding of the default behavior. +* For time-related fields, prefer using the well-known types `google.protobuf.Duration` or + `google.protobuf.Timestamp` instead of raw integers for seconds. + +* If a field is going to contain raw bytes rather than a human-readable string, the field should + be of type `bytes` instead of `string`. + * Proto fields should be sorted logically, not by field number. ## Package organization diff --git a/api/envoy/admin/v3/config_dump.proto b/api/envoy/admin/v3/config_dump.proto index 73156697fdb2..b3e5510a700c 100644 --- a/api/envoy/admin/v3/config_dump.proto +++ b/api/envoy/admin/v3/config_dump.proto @@ -49,6 +49,7 @@ message UpdateFailureState { "envoy.admin.v2alpha.UpdateFailureState"; // What the component configuration would have been if the update had succeeded. + // This field may not be populated by xDS clients due to storage overhead. google.protobuf.Any failed_configuration = 1; // Time of the latest failed update attempt. @@ -56,6 +57,10 @@ message UpdateFailureState { // Details about the last failed update attempt. string details = 3; + + // This is the version of the rejected resource. + // [#not-implemented-hide:] + string version_info = 4; } // This message describes the bootstrap configuration that Envoy was started with. This includes @@ -134,6 +139,9 @@ message ListenersConfigDump { DynamicListenerState draining_state = 4; // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. UpdateFailureState error_state = 5; } @@ -184,6 +192,13 @@ message ClustersConfigDump { // The timestamp when the Cluster was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // This is the :ref:`version_info ` in the @@ -239,6 +254,13 @@ message RoutesConfigDump { // The timestamp when the Route was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // The statically loaded route configs. @@ -270,6 +292,7 @@ message ScopedRoutesConfigDump { google.protobuf.Timestamp last_updated = 3; } + // [#next-free-field: 6] message DynamicScopedRouteConfigs { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v2alpha.ScopedRoutesConfigDump.DynamicScopedRouteConfigs"; @@ -287,6 +310,13 @@ message ScopedRoutesConfigDump { // The timestamp when the scoped route config set was last updated. google.protobuf.Timestamp last_updated = 4; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 5; } // The statically loaded scoped route configs. @@ -302,6 +332,7 @@ message SecretsConfigDump { "envoy.admin.v2alpha.SecretsConfigDump"; // DynamicSecret contains secret information fetched via SDS. + // [#next-free-field: 6] message DynamicSecret { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v2alpha.SecretsConfigDump.DynamicSecret"; @@ -319,6 +350,13 @@ message SecretsConfigDump { // Security sensitive information is redacted (replaced with "[redacted]") for // private keys and passwords in TLS certificates. google.protobuf.Any secret = 4; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 5; } // StaticSecret specifies statically loaded secret in bootstrap. @@ -373,6 +411,13 @@ message EndpointsConfigDump { // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // The statically loaded endpoint configs. diff --git a/api/envoy/admin/v4alpha/config_dump.proto b/api/envoy/admin/v4alpha/config_dump.proto index 7fed09631d75..3f5610be23e0 100644 --- a/api/envoy/admin/v4alpha/config_dump.proto +++ b/api/envoy/admin/v4alpha/config_dump.proto @@ -48,6 +48,7 @@ message UpdateFailureState { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v3.UpdateFailureState"; // What the component configuration would have been if the update had succeeded. + // This field may not be populated by xDS clients due to storage overhead. google.protobuf.Any failed_configuration = 1; // Time of the latest failed update attempt. @@ -55,6 +56,10 @@ message UpdateFailureState { // Details about the last failed update attempt. string details = 3; + + // This is the version of the rejected resource. + // [#not-implemented-hide:] + string version_info = 4; } // This message describes the bootstrap configuration that Envoy was started with. This includes @@ -131,6 +136,9 @@ message ListenersConfigDump { DynamicListenerState draining_state = 4; // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. UpdateFailureState error_state = 5; } @@ -180,6 +188,13 @@ message ClustersConfigDump { // The timestamp when the Cluster was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // This is the :ref:`version_info ` in the @@ -234,6 +249,13 @@ message RoutesConfigDump { // The timestamp when the Route was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // The statically loaded route configs. @@ -265,6 +287,7 @@ message ScopedRoutesConfigDump { google.protobuf.Timestamp last_updated = 3; } + // [#next-free-field: 6] message DynamicScopedRouteConfigs { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v3.ScopedRoutesConfigDump.DynamicScopedRouteConfigs"; @@ -282,6 +305,13 @@ message ScopedRoutesConfigDump { // The timestamp when the scoped route config set was last updated. google.protobuf.Timestamp last_updated = 4; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 5; } // The statically loaded scoped route configs. @@ -296,6 +326,7 @@ message SecretsConfigDump { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v3.SecretsConfigDump"; // DynamicSecret contains secret information fetched via SDS. + // [#next-free-field: 6] message DynamicSecret { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v3.SecretsConfigDump.DynamicSecret"; @@ -313,6 +344,13 @@ message SecretsConfigDump { // Security sensitive information is redacted (replaced with "[redacted]") for // private keys and passwords in TLS certificates. google.protobuf.Any secret = 4; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 5; } // StaticSecret specifies statically loaded secret in bootstrap. @@ -375,6 +413,13 @@ message EndpointsConfigDump { // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // The statically loaded endpoint configs. diff --git a/api/envoy/api/v2/core/protocol.proto b/api/envoy/api/v2/core/protocol.proto index 9c47e388ee1a..ae1a86424cf0 100644 --- a/api/envoy/api/v2/core/protocol.proto +++ b/api/envoy/api/v2/core/protocol.proto @@ -201,7 +201,7 @@ message Http2ProtocolOptions { // Still under implementation. DO NOT USE. // // Allows metadata. See [metadata - // docs](https://github.com/envoyproxy/envoy/blob/master/source/docs/h2_metadata.md) for more + // docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more // information. bool allow_metadata = 6; diff --git a/api/envoy/config/cluster/v3/cluster.proto b/api/envoy/config/cluster/v3/cluster.proto index f4e3386cfac9..90cd990a340a 100644 --- a/api/envoy/config/cluster/v3/cluster.proto +++ b/api/envoy/config/cluster/v3/cluster.proto @@ -583,11 +583,10 @@ message Cluster { google.protobuf.Duration max_interval = 2 [(validate.rules).duration = {gt {nanos: 1000000}}]; } - // [#not-implemented-hide:] message PreconnectPolicy { // Indicates how many streams (rounded up) can be anticipated per-upstream for each // incoming stream. This is useful for high-QPS or latency-sensitive services. Preconnecting - // will only be done if the upstream is healthy. + // will only be done if the upstream is healthy and the cluster has traffic. // // For example if this is 2, for an incoming HTTP/1.1 stream, 2 connections will be // established, one for the new incoming stream, and one for a presumed follow-up stream. For @@ -605,8 +604,7 @@ message Cluster { // // If this value is not set, or set explicitly to one, Envoy will fetch as many connections // as needed to serve streams in flight. This means in steady state if a connection is torn down, - // a subsequent streams will pay an upstream-rtt latency penalty waiting for streams to be - // preconnected. + // a subsequent streams will pay an upstream-rtt latency penalty waiting for a new connection. // // This is limited somewhat arbitrarily to 3 because preconnecting too aggressively can // harm latency more than the preconnecting helps. @@ -616,24 +614,25 @@ message Cluster { // Indicates how many many streams (rounded up) can be anticipated across a cluster for each // stream, useful for low QPS services. This is currently supported for a subset of // deterministic non-hash-based load-balancing algorithms (weighted round robin, random). - // Unlike per_upstream_preconnect_ratio this preconnects across the upstream instances in a + // Unlike *per_upstream_preconnect_ratio* this preconnects across the upstream instances in a // cluster, doing best effort predictions of what upstream would be picked next and // pre-establishing a connection. // + // Preconnecting will be limited to one preconnect per configured upstream in the cluster and will + // only be done if there are healthy upstreams and the cluster has traffic. + // // For example if preconnecting is set to 2 for a round robin HTTP/2 cluster, on the first // incoming stream, 2 connections will be preconnected - one to the first upstream for this // cluster, one to the second on the assumption there will be a follow-up stream. // - // Preconnecting will be limited to one preconnect per configured upstream in the cluster. - // // If this value is not set, or set explicitly to one, Envoy will fetch as many connections // as needed to serve streams in flight, so during warm up and in steady state if a connection // is closed (and per_upstream_preconnect_ratio is not set), there will be a latency hit for // connection establishment. // // If both this and preconnect_ratio are set, Envoy will make sure both predicted needs are met, - // basically preconnecting max(predictive-preconnect, per-upstream-preconnect), for each upstream. - // TODO(alyssawilk) per LB docs and LB overview docs when unhiding. + // basically preconnecting max(predictive-preconnect, per-upstream-preconnect), for each + // upstream. google.protobuf.DoubleValue predictive_preconnect_ratio = 2 [(validate.rules).double = {lte: 3.0 gte: 1.0}]; } @@ -1028,7 +1027,6 @@ message Cluster { // Configuration to track optional cluster stats. TrackClusterStats track_cluster_stats = 49; - // [#not-implemented-hide:] // Preconnect configuration for this cluster. PreconnectPolicy preconnect_policy = 50; diff --git a/api/envoy/config/cluster/v4alpha/cluster.proto b/api/envoy/config/cluster/v4alpha/cluster.proto index 8b30ab23f265..2d8aa4369b40 100644 --- a/api/envoy/config/cluster/v4alpha/cluster.proto +++ b/api/envoy/config/cluster/v4alpha/cluster.proto @@ -588,14 +588,13 @@ message Cluster { google.protobuf.Duration max_interval = 2 [(validate.rules).duration = {gt {nanos: 1000000}}]; } - // [#not-implemented-hide:] message PreconnectPolicy { option (udpa.annotations.versioning).previous_message_type = "envoy.config.cluster.v3.Cluster.PreconnectPolicy"; // Indicates how many streams (rounded up) can be anticipated per-upstream for each // incoming stream. This is useful for high-QPS or latency-sensitive services. Preconnecting - // will only be done if the upstream is healthy. + // will only be done if the upstream is healthy and the cluster has traffic. // // For example if this is 2, for an incoming HTTP/1.1 stream, 2 connections will be // established, one for the new incoming stream, and one for a presumed follow-up stream. For @@ -613,8 +612,7 @@ message Cluster { // // If this value is not set, or set explicitly to one, Envoy will fetch as many connections // as needed to serve streams in flight. This means in steady state if a connection is torn down, - // a subsequent streams will pay an upstream-rtt latency penalty waiting for streams to be - // preconnected. + // a subsequent streams will pay an upstream-rtt latency penalty waiting for a new connection. // // This is limited somewhat arbitrarily to 3 because preconnecting too aggressively can // harm latency more than the preconnecting helps. @@ -624,24 +622,25 @@ message Cluster { // Indicates how many many streams (rounded up) can be anticipated across a cluster for each // stream, useful for low QPS services. This is currently supported for a subset of // deterministic non-hash-based load-balancing algorithms (weighted round robin, random). - // Unlike per_upstream_preconnect_ratio this preconnects across the upstream instances in a + // Unlike *per_upstream_preconnect_ratio* this preconnects across the upstream instances in a // cluster, doing best effort predictions of what upstream would be picked next and // pre-establishing a connection. // + // Preconnecting will be limited to one preconnect per configured upstream in the cluster and will + // only be done if there are healthy upstreams and the cluster has traffic. + // // For example if preconnecting is set to 2 for a round robin HTTP/2 cluster, on the first // incoming stream, 2 connections will be preconnected - one to the first upstream for this // cluster, one to the second on the assumption there will be a follow-up stream. // - // Preconnecting will be limited to one preconnect per configured upstream in the cluster. - // // If this value is not set, or set explicitly to one, Envoy will fetch as many connections // as needed to serve streams in flight, so during warm up and in steady state if a connection // is closed (and per_upstream_preconnect_ratio is not set), there will be a latency hit for // connection establishment. // // If both this and preconnect_ratio are set, Envoy will make sure both predicted needs are met, - // basically preconnecting max(predictive-preconnect, per-upstream-preconnect), for each upstream. - // TODO(alyssawilk) per LB docs and LB overview docs when unhiding. + // basically preconnecting max(predictive-preconnect, per-upstream-preconnect), for each + // upstream. google.protobuf.DoubleValue predictive_preconnect_ratio = 2 [(validate.rules).double = {lte: 3.0 gte: 1.0}]; } @@ -968,7 +967,6 @@ message Cluster { // Configuration to track optional cluster stats. TrackClusterStats track_cluster_stats = 49; - // [#not-implemented-hide:] // Preconnect configuration for this cluster. PreconnectPolicy preconnect_policy = 50; diff --git a/api/envoy/config/core/v3/protocol.proto b/api/envoy/config/core/v3/protocol.proto index c294370366df..80d971c1466b 100644 --- a/api/envoy/config/core/v3/protocol.proto +++ b/api/envoy/config/core/v3/protocol.proto @@ -266,7 +266,7 @@ message Http2ProtocolOptions { // Still under implementation. DO NOT USE. // // Allows metadata. See [metadata - // docs](https://github.com/envoyproxy/envoy/blob/master/source/docs/h2_metadata.md) for more + // docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more // information. bool allow_metadata = 6; diff --git a/api/envoy/config/core/v3/substitution_format_string.proto b/api/envoy/config/core/v3/substitution_format_string.proto index fa14db45ddda..9802309df60d 100644 --- a/api/envoy/config/core/v3/substitution_format_string.proto +++ b/api/envoy/config/core/v3/substitution_format_string.proto @@ -3,7 +3,9 @@ syntax = "proto3"; package envoy.config.core.v3; import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/extension.proto"; +import "google/protobuf/any.proto"; import "google/protobuf/struct.proto"; import "udpa/annotations/status.proto"; @@ -18,7 +20,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Configuration to use multiple :ref:`command operators ` // to generate a new string in either plain text or JSON format. -// [#next-free-field: 6] +// [#next-free-field: 7] message SubstitutionFormatString { oneof format { option (validate.required) = true; @@ -103,4 +105,8 @@ message SubstitutionFormatString { // content_type: "text/html; charset=UTF-8" // string content_type = 4; + + // Specifies a collection of Formatter plugins that can be called from the access log configuration. + // See the formatters extensions documentation for details. + repeated TypedExtensionConfig formatters = 6; } diff --git a/api/envoy/config/core/v4alpha/protocol.proto b/api/envoy/config/core/v4alpha/protocol.proto index e33d83442afc..60f0b3210805 100644 --- a/api/envoy/config/core/v4alpha/protocol.proto +++ b/api/envoy/config/core/v4alpha/protocol.proto @@ -273,7 +273,7 @@ message Http2ProtocolOptions { // Still under implementation. DO NOT USE. // // Allows metadata. See [metadata - // docs](https://github.com/envoyproxy/envoy/blob/master/source/docs/h2_metadata.md) for more + // docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more // information. bool allow_metadata = 6; diff --git a/api/envoy/config/core/v4alpha/substitution_format_string.proto b/api/envoy/config/core/v4alpha/substitution_format_string.proto index 383d5dfd5e6d..abd8089362c6 100644 --- a/api/envoy/config/core/v4alpha/substitution_format_string.proto +++ b/api/envoy/config/core/v4alpha/substitution_format_string.proto @@ -3,7 +3,9 @@ syntax = "proto3"; package envoy.config.core.v4alpha; import "envoy/config/core/v4alpha/base.proto"; +import "envoy/config/core/v4alpha/extension.proto"; +import "google/protobuf/any.proto"; import "google/protobuf/struct.proto"; import "udpa/annotations/status.proto"; @@ -19,7 +21,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // Configuration to use multiple :ref:`command operators ` // to generate a new string in either plain text or JSON format. -// [#next-free-field: 6] +// [#next-free-field: 7] message SubstitutionFormatString { option (udpa.annotations.versioning).previous_message_type = "envoy.config.core.v3.SubstitutionFormatString"; @@ -92,4 +94,8 @@ message SubstitutionFormatString { // content_type: "text/html; charset=UTF-8" // string content_type = 4; + + // Specifies a collection of Formatter plugins that can be called from the access log configuration. + // See the formatters extensions documentation for details. + repeated TypedExtensionConfig formatters = 6; } diff --git a/api/envoy/config/filter/http/ext_authz/v2/ext_authz.proto b/api/envoy/config/filter/http/ext_authz/v2/ext_authz.proto index db188a572ae0..b9a807d82edb 100644 --- a/api/envoy/config/filter/http/ext_authz/v2/ext_authz.proto +++ b/api/envoy/config/filter/http/ext_authz/v2/ext_authz.proto @@ -49,10 +49,7 @@ message ExtAuthz { // `. bool failure_mode_allow = 2; - // Sets the package version the gRPC service should use. This is particularly - // useful when transitioning from alpha to release versions assuming that both definitions are - // semantically compatible. Deprecation note: This field is deprecated and should only be used for - // version upgrade. See release notes for more details. + // [#not-implemented-hide: Support for this field has been removed.] bool use_alpha = 4 [deprecated = true, (envoy.annotations.disallowed_by_default) = true]; // Enables filter to buffer the client request body and send it within the authorization request. diff --git a/api/envoy/config/listener/v3/listener_components.proto b/api/envoy/config/listener/v3/listener_components.proto index c2236a34d3c4..c73103bfad5d 100644 --- a/api/envoy/config/listener/v3/listener_components.proto +++ b/api/envoy/config/listener/v3/listener_components.proto @@ -4,6 +4,7 @@ package envoy.config.listener.v3; import "envoy/config/core/v3/address.proto"; import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/extension.proto"; import "envoy/type/v3/range.proto"; import "google/protobuf/any.proto"; @@ -23,6 +24,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Listener components] // Listener :ref:`configuration overview ` +// [#next-free-field: 6] message Filter { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.listener.Filter"; @@ -34,10 +36,16 @@ message Filter { // :ref:`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. oneof config_type { + // Filter specific configuration which depends on the filter being + // instantiated. See the supported filters for further documentation. google.protobuf.Any typed_config = 4; + + // Configuration source specifier for an extension configuration discovery + // service. In case of a failure and without the default configuration, the + // listener closes the connections. + // [#not-implemented-hide:] + core.v3.ExtensionConfigSource config_discovery = 5; } } diff --git a/api/envoy/config/listener/v4alpha/listener_components.proto b/api/envoy/config/listener/v4alpha/listener_components.proto index 021aadc928c3..3f338cef736c 100644 --- a/api/envoy/config/listener/v4alpha/listener_components.proto +++ b/api/envoy/config/listener/v4alpha/listener_components.proto @@ -4,6 +4,7 @@ package envoy.config.listener.v4alpha; import "envoy/config/core/v4alpha/address.proto"; import "envoy/config/core/v4alpha/base.proto"; +import "envoy/config/core/v4alpha/extension.proto"; import "envoy/type/v3/range.proto"; import "google/protobuf/any.proto"; @@ -23,6 +24,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // [#protodoc-title: Listener components] // Listener :ref:`configuration overview ` +// [#next-free-field: 6] message Filter { option (udpa.annotations.versioning).previous_message_type = "envoy.config.listener.v3.Filter"; @@ -34,10 +36,16 @@ message Filter { // :ref:`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. oneof config_type { + // Filter specific configuration which depends on the filter being + // instantiated. See the supported filters for further documentation. google.protobuf.Any typed_config = 4; + + // Configuration source specifier for an extension configuration discovery + // service. In case of a failure and without the default configuration, the + // listener closes the connections. + // [#not-implemented-hide:] + core.v4alpha.ExtensionConfigSource config_discovery = 5; } } diff --git a/api/envoy/config/retry/omit_canary_hosts/v3/BUILD b/api/envoy/config/retry/omit_canary_hosts/v3/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/api/envoy/config/retry/omit_canary_hosts/v3/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/retry/omit_canary_hosts/v3/omit_canary_hosts.proto b/api/envoy/config/retry/omit_canary_hosts/v3/omit_canary_hosts.proto new file mode 100644 index 000000000000..fe928ba733db --- /dev/null +++ b/api/envoy/config/retry/omit_canary_hosts/v3/omit_canary_hosts.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package envoy.config.retry.omit_canary_hosts.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.config.retry.omit_canary_hosts.v3"; +option java_outer_classname = "OmitCanaryHostsProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Omit Canary Hosts Predicate] +// [#extension: envoy.retry_host_predicates.omit_canary_hosts] + +message OmitCanaryHostsPredicate { +} diff --git a/api/envoy/config/route/v3/route_components.proto b/api/envoy/config/route/v3/route_components.proto index 53b351b8d3aa..9538a417eb7c 100644 --- a/api/envoy/config/route/v3/route_components.proto +++ b/api/envoy/config/route/v3/route_components.proto @@ -1541,6 +1541,7 @@ message VirtualCluster { } // Global rate limiting :ref:`architecture overview `. +// Also applies to Local rate limiting :ref:`using descriptors `. message RateLimit { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RateLimit"; diff --git a/api/envoy/config/route/v4alpha/route_components.proto b/api/envoy/config/route/v4alpha/route_components.proto index 577282595d84..319c65c6e2ff 100644 --- a/api/envoy/config/route/v4alpha/route_components.proto +++ b/api/envoy/config/route/v4alpha/route_components.proto @@ -1490,6 +1490,7 @@ message VirtualCluster { } // Global rate limiting :ref:`architecture overview `. +// Also applies to Local rate limiting :ref:`using descriptors `. message RateLimit { option (udpa.annotations.versioning).previous_message_type = "envoy.config.route.v3.RateLimit"; diff --git a/api/envoy/extensions/common/dynamic_forward_proxy/v3/dns_cache.proto b/api/envoy/extensions/common/dynamic_forward_proxy/v3/dns_cache.proto index 5579cc16bd97..4b182a29711b 100644 --- a/api/envoy/extensions/common/dynamic_forward_proxy/v3/dns_cache.proto +++ b/api/envoy/extensions/common/dynamic_forward_proxy/v3/dns_cache.proto @@ -92,8 +92,7 @@ message DnsCacheConfig { config.cluster.v3.Cluster.RefreshRate dns_failure_refresh_rate = 6; // The config of circuit breakers for resolver. It provides a configurable threshold. - // If `envoy.reloadable_features.enable_dns_cache_circuit_breakers` is enabled, - // envoy will use dns cache circuit breakers with default settings even if this value is not set. + // Envoy will use dns cache circuit breakers with default settings even if this value is not set. DnsCacheCircuitBreakers dns_cache_circuit_breaker = 7; // [#next-major-version: Reconcile DNS options in a single message.] diff --git a/api/envoy/extensions/common/ratelimit/v3/ratelimit.proto b/api/envoy/extensions/common/ratelimit/v3/ratelimit.proto index 30efa6026218..6bb771d25af9 100644 --- a/api/envoy/extensions/common/ratelimit/v3/ratelimit.proto +++ b/api/envoy/extensions/common/ratelimit/v3/ratelimit.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package envoy.extensions.common.ratelimit.v3; import "envoy/type/v3/ratelimit_unit.proto"; +import "envoy/type/v3/token_bucket.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; @@ -92,3 +93,11 @@ message RateLimitDescriptor { // Optional rate limit override to supply to the ratelimit service. RateLimitOverride limit = 2; } + +message LocalRateLimitDescriptor { + // Descriptor entries. + repeated v3.RateLimitDescriptor.Entry entries = 1 [(validate.rules).repeated = {min_items: 1}]; + + // Token Bucket algorithm for local ratelimiting. + type.v3.TokenBucket token_bucket = 2 [(validate.rules).message = {required: true}]; +} diff --git a/api/envoy/extensions/filters/common/dependency/v3/BUILD b/api/envoy/extensions/filters/common/dependency/v3/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/api/envoy/extensions/filters/common/dependency/v3/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/extensions/filters/common/dependency/v3/dependency.proto b/api/envoy/extensions/filters/common/dependency/v3/dependency.proto new file mode 100644 index 000000000000..c21408dc0701 --- /dev/null +++ b/api/envoy/extensions/filters/common/dependency/v3/dependency.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package envoy.extensions.filters.common.dependency.v3; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.common.dependency.v3"; +option java_outer_classname = "DependencyProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Filter dependency specification] + +// Dependency specification and string identifier. +message Dependency { + enum DependencyType { + HEADER = 0; + FILTER_STATE_KEY = 1; + DYNAMIC_METADATA = 2; + } + + // The kind of dependency. + DependencyType type = 1; + + // The string identifier for the dependency. + string name = 2 [(validate.rules).string = {min_len: 1}]; +} + +// Dependency specification for a filter. For a filter chain to be valid, any +// dependency that is required must be provided by an earlier filter. +message FilterDependencies { + // A list of dependencies required on the decode path. + repeated Dependency decode_required = 1; + + // A list of dependencies provided on the encode path. + repeated Dependency decode_provided = 2; + + // A list of dependencies required on the decode path. + repeated Dependency encode_required = 3; + + // A list of dependencies provided on the encode path. + repeated Dependency encode_provided = 4; +} diff --git a/api/envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.proto b/api/envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.proto index 1e64376add9b..ad5ab5d507bb 100644 --- a/api/envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.proto +++ b/api/envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.proto @@ -21,7 +21,22 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#extension: envoy.filters.http.ext_proc] // The External Processing filter allows an external service to act on HTTP traffic in a flexible way. -// It communicates with an external gRPC service that can use it to do a variety of things + +// **Current Implementation Status:** +// At this time, the filter will send the "request_headers" and "response_headers" messages +// to the server when the filter is invoked, and apply any header mutations returned by the +// server, and respond to "immediate_response" messages. No other parts of the protocol are implemented yet. + +// As designed, the filter supports up to six different processing steps, which are in the +// process of being implemented: +// * Request headers: IMPLEMENTED +// * Request body: NOT IMPLEMENTED +// * Request trailers: NOT IMPLEMENTED +// * Response headers: IMPLEMENTED +// * Response body: NOT IMPLEMENTED +// * Response trailers: NOT IMPLEMENTED + +// The filter communicates with an external gRPC service that can use it to do a variety of things // with the request and response: // // * Access and modify the HTTP headers on the request, response, or both @@ -63,7 +78,6 @@ message ExternalProcessor { // The filter supports both the "Envoy" and "Google" gRPC clients. config.core.v3.GrpcService grpc_service = 1; - // [#not-implemented-hide:] // By default, if the gRPC stream cannot be established, or if it is closed // prematurely with an error, the filter will fail. Specifically, if the // response headers have not yet been delivered, then it will return a 500 diff --git a/api/envoy/extensions/filters/http/local_ratelimit/v3/BUILD b/api/envoy/extensions/filters/http/local_ratelimit/v3/BUILD index ad2fc9a9a84f..6c58a43e4ff6 100644 --- a/api/envoy/extensions/filters/http/local_ratelimit/v3/BUILD +++ b/api/envoy/extensions/filters/http/local_ratelimit/v3/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ "//envoy/config/core/v3:pkg", + "//envoy/extensions/common/ratelimit/v3:pkg", "//envoy/type/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", ], diff --git a/api/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto b/api/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto index 94f21edd3eed..546ff26bac79 100644 --- a/api/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto +++ b/api/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package envoy.extensions.filters.http.local_ratelimit.v3; import "envoy/config/core/v3/base.proto"; +import "envoy/extensions/common/ratelimit/v3/ratelimit.proto"; import "envoy/type/v3/http_status.proto"; import "envoy/type/v3/token_bucket.proto"; @@ -19,7 +20,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Local Rate limit :ref:`configuration overview `. // [#extension: envoy.filters.http.local_ratelimit] -// [#next-free-field: 7] +// [#next-free-field: 10] message LocalRateLimit { // The human readable prefix to use when emitting stats. string stat_prefix = 1 [(validate.rules).string = {min_len: 1}]; @@ -67,4 +68,28 @@ message LocalRateLimit { // have been rate limited. repeated config.core.v3.HeaderValueOption response_headers_to_add = 6 [(validate.rules).repeated = {max_items: 10}]; + + // The rate limit descriptor list to use in the local rate limit to override + // on. The rate limit descriptor is selected by the first full match from the + // request descriptors. + // + // Example on how to use ::ref:`this ` + // + // .. note:: + // + // In the current implementation the descriptor's token bucket :ref:`fill_interval + // ` must be a multiple + // global :ref:`token bucket's` fill interval. + // + // The descriptors must match verbatim for rate limiting to apply. There is no partial + // match by a subset of descriptor entries in the current implementation. + repeated common.ratelimit.v3.LocalRateLimitDescriptor descriptors = 8; + + // Specifies the rate limit configurations to be applied with the same + // stage number. If not set, the default stage number is 0. + // + // .. note:: + // + // The filter supports a range of 0 - 10 inclusively for stage numbers. + uint32 stage = 9 [(validate.rules).uint32 = {lte: 10}]; } diff --git a/api/envoy/extensions/filters/http/oauth2/v3alpha/oauth.proto b/api/envoy/extensions/filters/http/oauth2/v3alpha/oauth.proto index e4be64167ed2..11fe12695865 100644 --- a/api/envoy/extensions/filters/http/oauth2/v3alpha/oauth.proto +++ b/api/envoy/extensions/filters/http/oauth2/v3alpha/oauth.proto @@ -44,7 +44,7 @@ message OAuth2Credentials { // OAuth config // -// [#next-free-field: 9] +// [#next-free-field: 10] message OAuth2Config { // Endpoint on the authorization server to retrieve the access token from. config.core.v3.HttpUri token_endpoint = 1; @@ -74,6 +74,11 @@ message OAuth2Config { // Any request that matches any of the provided matchers will be passed through without OAuth validation. repeated config.route.v3.HeaderMatcher pass_through_matcher = 8; + + // Optional list of OAuth scopes to be claimed in the authorization request. If not specified, + // defaults to "user" scope. + // OAuth RFC https://tools.ietf.org/html/rfc6749#section-3.3 + repeated string auth_scopes = 9; } // Filter config. diff --git a/api/envoy/extensions/filters/http/oauth2/v4alpha/oauth.proto b/api/envoy/extensions/filters/http/oauth2/v4alpha/oauth.proto index ee51e1f96099..af1f0944ed34 100644 --- a/api/envoy/extensions/filters/http/oauth2/v4alpha/oauth.proto +++ b/api/envoy/extensions/filters/http/oauth2/v4alpha/oauth.proto @@ -47,7 +47,7 @@ message OAuth2Credentials { // OAuth config // -// [#next-free-field: 9] +// [#next-free-field: 10] message OAuth2Config { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.http.oauth2.v3alpha.OAuth2Config"; @@ -80,6 +80,11 @@ message OAuth2Config { // Any request that matches any of the provided matchers will be passed through without OAuth validation. repeated config.route.v4alpha.HeaderMatcher pass_through_matcher = 8; + + // Optional list of OAuth scopes to be claimed in the authorization request. If not specified, + // defaults to "user" scope. + // OAuth RFC https://tools.ietf.org/html/rfc6749#section-3.3 + repeated string auth_scopes = 9; } // Filter config. diff --git a/api/envoy/extensions/filters/network/thrift_proxy/router/v3/BUILD b/api/envoy/extensions/filters/network/thrift_proxy/router/v3/BUILD new file mode 100644 index 000000000000..c24f669b9bbd --- /dev/null +++ b/api/envoy/extensions/filters/network/thrift_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/thrift/router/v2alpha1:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/api/envoy/extensions/filters/network/thrift_proxy/router/v3/router.proto b/api/envoy/extensions/filters/network/thrift_proxy/router/v3/router.proto new file mode 100644 index 000000000000..860622cb61e4 --- /dev/null +++ b/api/envoy/extensions/filters/network/thrift_proxy/router/v3/router.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.thrift_proxy.router.v3; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.thrift_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] +// Thrift router :ref:`configuration overview `. +// [#extension: envoy.filters.thrift.router] + +message Router { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.thrift.router.v2alpha1.Router"; +} diff --git a/api/envoy/service/ext_proc/v3alpha/external_processor.proto b/api/envoy/service/ext_proc/v3alpha/external_processor.proto index cac1dde34d9e..5b0696bfc3b0 100644 --- a/api/envoy/service/ext_proc/v3alpha/external_processor.proto +++ b/api/envoy/service/ext_proc/v3alpha/external_processor.proto @@ -6,7 +6,6 @@ import "envoy/config/core/v3/base.proto"; import "envoy/extensions/filters/http/ext_proc/v3alpha/processing_mode.proto"; import "envoy/type/v3/http_status.proto"; -import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "udpa/annotations/status.proto"; @@ -289,10 +288,13 @@ message GrpcStatus { // Change HTTP headers or trailers by appending, replacing, or removing // headers. message HeaderMutation { - // Add or replace HTTP headers. + // Add or replace HTTP headers. Attempts to set the value of + // any "x-envoy" header, and attempts to set the ":method", + // ":authority", ":scheme", or "host" headers will be ignored. repeated config.core.v3.HeaderValueOption set_headers = 1; - // Remove these HTTP headers. + // Remove these HTTP headers. Attempts to remove system headers -- + // any header starting with ":", plus "host" -- will be ignored. repeated string remove_headers = 2; } diff --git a/api/envoy/type/matcher/v3/http_inputs.proto b/api/envoy/type/matcher/v3/http_inputs.proto new file mode 100644 index 000000000000..48eb4c43154a --- /dev/null +++ b/api/envoy/type/matcher/v3/http_inputs.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package envoy.type.matcher.v3; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type.matcher.v3"; +option java_outer_classname = "HttpInputsProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Common HTTP Inputs] + +// Match input indicates that matching should be done on a specific request header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the request contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpRequestHeaderMatchInput { + // The request header to match on. + string header_name = 1; +} + +// Match input indicating that matching should be done on a specific response header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the response contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpResponseHeaderMatchInput { + // The response header to match on. + string header_name = 1; +} diff --git a/api/envoy/type/matcher/v4alpha/http_inputs.proto b/api/envoy/type/matcher/v4alpha/http_inputs.proto new file mode 100644 index 000000000000..f15e9ceb70e2 --- /dev/null +++ b/api/envoy/type/matcher/v4alpha/http_inputs.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package envoy.type.matcher.v4alpha; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type.matcher.v4alpha"; +option java_outer_classname = "HttpInputsProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: Common HTTP Inputs] + +// Match input indicates that matching should be done on a specific request header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the request contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpRequestHeaderMatchInput { + option (udpa.annotations.versioning).previous_message_type = + "envoy.type.matcher.v3.HttpRequestHeaderMatchInput"; + + // The request header to match on. + string header_name = 1; +} + +// Match input indicating that matching should be done on a specific response header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the response contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpResponseHeaderMatchInput { + option (udpa.annotations.versioning).previous_message_type = + "envoy.type.matcher.v3.HttpResponseHeaderMatchInput"; + + // The response header to match on. + string header_name = 1; +} diff --git a/api/review_checklist.md b/api/review_checklist.md new file mode 100644 index 000000000000..bce9f188dad2 --- /dev/null +++ b/api/review_checklist.md @@ -0,0 +1,154 @@ +# API Review Checklist + +This checklist is intended to be used when reviewing xDS API changes. +Users who wish to contribute API changes should read this and proactively +consider the answers to these questions before sending a PR. + +## Feature Enablement +- Are the default values going to cause behavior changes for existing users + who do not know about the change and have not updated the resources being + served by their control plane? + - If yes, do we have some estimate of how many users will be affected? + - Why is it justified to change the default behavior, rather than making + this feature opt-in? + - Some possible justifications include security concerns with existing + behavior, or a desire to eliminate legacy behavior. + - What is the plan to make this change in a safe way? For example, is the + transition going to be staged over the course of several minor xDS versions? + - How will we warn users about this change? + - Possible ways to do this include release notes, announcements, warnings + from the code, etc. +- Will users have a way to disable this change if it causes problems? + - If not, why do we think that's okay? (It might be the case that we think + it will not actually affect anyone, or no one will care.) + - If so, is the mechanism to disable it part of the xDS API, or is it + acceptable to have a separate knob for this in the client? (See also + "Genericness" below -- if this is not part of the API, will every xDS + client need to add a different knob? Is consistency across clients + important for this?) + +## Style +- Is the PR aligned with the [API style guidelines](STYLE.md)? + +## Validation Rules +- Does the field have protocgen-validate rules to enforce constraints on + its value? +- Is the field mandatory? Does it have the required rule? +- Is the field numeric? Is it bounded correctly? +- Is the field repeated? Does it have the correct min/max items numbers? Are + the values unique? +- If a field may eventually be repeated but initially there is a desire to + cap it to a single value, consider using repeated with a max length of 1, + which is easier to relax in the future. + +## Deprecations +- When a field or enum value is deprecated, according to the minor/patch + versioning policy this implies a minor version for support removal. Is the + work necessary to add support for the newer replacement field acceptable to + known xDS clients in this time frame? +- No deprecations are allowed when an alternative is "not ready yet" in any + major known xDS client (Envoy or gRPC at this point), unless the + maintainers of that xDS client have signed off on the change. If you are not + sure about the current state of a feature in the major known xDS clients, + ask one of the API shepherds. +- Is this deprecated field documented in a way that points to the preferred + newer method of configuration? + +## Extensibility +- Is this feature being directly added into the API itself, or is it being + introduced via an extension point (i.e., using `TypedExtensionConfig`)? +- If not via an extension point, why not? +- If no appropriate extension point currently exists, is this a good + opportunity to add one? Can we move some existing "core" functionality + into a set of standardized plugins for an extension point? +- Do we have good documentation showing what to plug into the extension point? + (At minimum, it should have a comment pointing to the common protos to + be plugged into the extension point.) +- If an enum is being introduced, should this be a oneof with empty messages + for future API growth? +- When a new field is introduced for a distinct purpose, should this be a + message to allow for future API growth? + +## Consistency +- Can the proposed API reuse part or all of other APIs? + - Can some other API be refactored to be part of it, or vice versa? + - Example: Can it use common types such as matcher or number? +- Are there similar structures that already exist? +- Is the naming convention consistent with other APIs? +- If there are new proto messages being added, are they in the right + place in the tree? Consider not just the current users of this proto + but also other possible uses in the future. Would it make more sense + to make the proto a generic type and put it in a common location, so + that it can be used in other places in the future? + +## Interactions With Other Features +- Will this feature interact in strange ways with other features, either + existing or planned? + - For example, if you are defining a new cluster type, how will the + new type implement all of the features currently configured via CDS? + - If this is a change in the upstream side of the API, will it work properly + with LRS load reporting? +- Will there be combinations of features that won't work properly? If so, + please document each combination that won't work and justify why this is + okay. Is there some other way to structure this feature that would not + cause conflicts with other features? +- If this change involves extension configuration, how will it interact + with ECDS? + +## Genericness +- Is this an Envoy-specific or proxy-specific feature? How will it apply to + xDS clients other than Envoy (e.g., gRPC)? + +## Dependencies +- Does this feature pull in any new dependencies for clients? +- Are these dependencies optional or required? +- Will the dependencies cause problems for any known xDS clients (e.g., + [Envoy's dependency policy](https://github.com/envoyproxy/envoy/blob/master/DEPENDENCY_POLICY.md))? + +## Failure Modes +- What is the failure mode if this feature is configured but is not working + for some reason? +- Is the failure mode what users will expect/want? +- Is this failure mode specified in the API, or is each client expected to + handle it on its own? Consistency across clients is preferred; if there's + a reason this isn't feasible, please explain. + +## Scalability +- Does this feature add new per-request functionality? How much overhead does + it add on the per-request path? +- Are there ways that the API could be structured differently to make it + possible to implement the feature in more efficient ways? (Even if this + efficiency is not needed now, it may be something we will need in the future, + and we will save pain in the long run if we structure the API in a way that + gives us the flexibility to change the implementation later.) +- How does this feature affect config size? For example, instead of + adding a huge mandatory proto to every single route entry, consider + ways of setting it at the virtual host level and then overriding only + the parts that change on a per-route basis. +- Will the change require multiple round trips via the REST API? + +## Monitoring +- Is there any behavior associated with this feature that will require + monitoring? +- How will the data be exposed to monitoring? +- Is the monitoring configuration part of the xDS API, or is it client-specific? + +## Documentation +- Can a user look at the docs and understand it without a bunch of extra + context? +- Pay special attention to documentation around extensions and `typed_config`. + Users generally find this extremely confusing. There should be examples + showing how to configure extension points and optimally all known public + extensions (there is tooling work in progress to automate this). +- Larger features should contain architecture overview documentation with + relevant cross-linking. +- Relevant differences between clients need to be documented (in the future + we will build tooling to allow for common documentation as well as per-client + documentation). + +## Where to Put New Protos +- The xDS API is currently partly in the Envoy repo and partly in the + cncf/xds repo. We will move pieces of the API to the latter repo + slowly over time, as they become less Envoy-specific, until eventually + the whole API has been moved there. If your change involves adding + new protos, they should generally go in the new repo. diff --git a/api/versioning/BUILD b/api/versioning/BUILD index 6f6762722148..ba5c589d6fc3 100644 --- a/api/versioning/BUILD +++ b/api/versioning/BUILD @@ -27,6 +27,7 @@ proto_library( "//envoy/config/resource_monitor/fixed_heap/v2alpha:pkg", "//envoy/config/resource_monitor/injected_resource/v2alpha:pkg", "//envoy/config/retry/omit_canary_hosts/v2:pkg", + "//envoy/config/retry/omit_canary_hosts/v3:pkg", "//envoy/config/retry/previous_hosts/v2:pkg", "//envoy/config/route/v3:pkg", "//envoy/config/tap/v3:pkg", @@ -48,6 +49,7 @@ proto_library( "//envoy/extensions/common/tap/v3:pkg", "//envoy/extensions/compression/gzip/compressor/v3:pkg", "//envoy/extensions/compression/gzip/decompressor/v3:pkg", + "//envoy/extensions/filters/common/dependency/v3:pkg", "//envoy/extensions/filters/common/fault/v3:pkg", "//envoy/extensions/filters/common/matcher/action/v3:pkg", "//envoy/extensions/filters/http/adaptive_concurrency/v3:pkg", @@ -113,6 +115,7 @@ proto_library( "//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3alpha:pkg", "//envoy/extensions/filters/network/tcp_proxy/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/filters/ratelimit/v3:pkg", + "//envoy/extensions/filters/network/thrift_proxy/router/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/v3:pkg", "//envoy/extensions/filters/network/wasm/v3:pkg", "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", diff --git a/bazel/EXTERNAL_DEPS.md b/bazel/EXTERNAL_DEPS.md index 4f66ef80eac8..02ba28e30674 100644 --- a/bazel/EXTERNAL_DEPS.md +++ b/bazel/EXTERNAL_DEPS.md @@ -78,7 +78,7 @@ documentation for further references. # Updating an external dependency version 1. Update the corresponding entry in -[the repository locations file.](https://github.com/envoyproxy/envoy/blob/master/bazel/repository_locations.bzl) +[the repository locations file.](https://github.com/envoyproxy/envoy/blob/main/bazel/repository_locations.bzl) 2. `bazel test //test/...` # Overriding an external dependency temporarily @@ -88,7 +88,7 @@ specifying Bazel option [`--override_repository`](https://docs.bazel.build/versions/master/command-line-reference.html) to point to a local copy. The option can used multiple times to override multiple dependencies. The name of the dependency can be found in -[the repository locations file.](https://github.com/envoyproxy/envoy/blob/master/bazel/repository_locations.bzl) +[the repository locations file.](https://github.com/envoyproxy/envoy/blob/main/bazel/repository_locations.bzl) The path of the local copy has to be absolute path. For repositories built by `envoy_cmake_external()` in `bazel/foreign_cc/BUILD`, diff --git a/bazel/PPROF.md b/bazel/PPROF.md index 74987b1986b4..2565a807b5e4 100644 --- a/bazel/PPROF.md +++ b/bazel/PPROF.md @@ -29,7 +29,7 @@ specific place yourself. Static linking is already available (because of a `HeapProfilerDump()` call inside -[`Envoy::Profiler::Heap::stopProfiler())`](https://github.com/envoyproxy/envoy/blob/master/source/common/profiler/profiler.cc#L32-L39)). +[`Envoy::Profiler::Heap::stopProfiler())`](https://github.com/envoyproxy/envoy/blob/main/source/common/profiler/profiler.cc#L32-L39)). ### Compiling a statically-linked Envoy @@ -82,7 +82,7 @@ is controlled by `ProfilerStart()`/`ProfilerStop()`, and the [Gperftools Heap Profiler](https://gperftools.github.io/gperftools/heapprofile.html) is controlled by `HeapProfilerStart()`, `HeapProfilerStop()` and `HeapProfilerDump()`. -These functions are wrapped by Envoy objects defined in [`source/common/profiler/profiler.h`](https://github.com/envoyproxy/envoy/blob/master/source/common/profiler/profiler.h)). +These functions are wrapped by Envoy objects defined in [`source/common/profiler/profiler.h`](https://github.com/envoyproxy/envoy/blob/main/source/common/profiler/profiler.h)). To enable profiling programmatically: @@ -258,7 +258,7 @@ account other memory allocating functions. In case there is a need to measure how long a code path takes time to execute in Envoy you may resort to instrumenting the code with the -[performance annotations](https://github.com/envoyproxy/envoy/blob/master/source/common/common/perf_annotation.h). +[performance annotations](https://github.com/envoyproxy/envoy/blob/main/source/common/common/perf_annotation.h). There are two types of the annotations. The first one is used to measure operations limited by a common lexical scope. For example: diff --git a/bazel/README.md b/bazel/README.md index e4d4d4a9cf22..8f9203d693d6 100644 --- a/bazel/README.md +++ b/bazel/README.md @@ -40,13 +40,13 @@ independently sourced, the following steps should be followed: This section describes how to and what dependencies to install to get started building Envoy with Bazel. If you would rather use a pre-build Docker image with required tools installed, skip to [this section](#building-envoy-with-the-ci-docker-image). -As a developer convenience, a [WORKSPACE](https://github.com/envoyproxy/envoy/blob/master/WORKSPACE) and +As a developer convenience, a [WORKSPACE](https://github.com/envoyproxy/envoy/blob/main/WORKSPACE) and [rules for building a recent -version](https://github.com/envoyproxy/envoy/blob/master/bazel/repositories.bzl) of the various Envoy +version](https://github.com/envoyproxy/envoy/blob/main/bazel/repositories.bzl) of the various Envoy dependencies are provided. These are provided as is, they are only suitable for development and testing purposes. The specific versions of the Envoy dependencies used in this build may not be up-to-date with the latest security patches. See -[this doc](https://github.com/envoyproxy/envoy/blob/master/bazel/EXTERNAL_DEPS.md#updating-an-external-dependency-version) +[this doc](https://github.com/envoyproxy/envoy/blob/main/bazel/EXTERNAL_DEPS.md#updating-an-external-dependency-version) for how to update or override dependencies. 1. Install external dependencies. @@ -84,14 +84,14 @@ for how to update or override dependencies. ``` Note: Either `libc++` or `libstdc++-7-dev` (or higher) must be installed. - + #### Config Flag Choices - Different [config](https://docs.bazel.build/versions/master/guide.html#--config) flags specify the compiler libraries: - + Different [config](https://docs.bazel.build/versions/master/guide.html#--config) flags specify the compiler libraries: + - `--config=libc++` means using `clang` + `libc++` - `--config=clang` means using `clang` + `libstdc++` - no config flag means using `gcc` + `libstdc++` - + ### macOS On macOS, you'll need to install several dependencies. This can be accomplished via [Homebrew](https://brew.sh/): @@ -256,7 +256,7 @@ MSYS2 or Git bash), run: ./ci/run_envoy_docker.sh './ci/windows_ci_steps.sh' ``` -See also the [documentation](https://github.com/envoyproxy/envoy/tree/master/ci) for developer use of the +See also the [documentation](https://github.com/envoyproxy/envoy/tree/main/ci) for developer use of the CI Docker image. ## Building Envoy with Remote Execution @@ -348,7 +348,7 @@ bazel test //test/... An individual test target can be run with a more specific Bazel [label](https://bazel.build/versions/master/docs/build-ref.html#Labels), e.g. to build and run only the units tests in -[test/common/http/async_client_impl_test.cc](https://github.com/envoyproxy/envoy/blob/master/test/common/http/async_client_impl_test.cc): +[test/common/http/async_client_impl_test.cc](https://github.com/envoyproxy/envoy/blob/main/test/common/http/async_client_impl_test.cc): ``` bazel test //test/common/http:async_client_impl_test @@ -749,7 +749,7 @@ test/run_envoy_bazel_coverage.sh **Note** that it is important to ensure that the versions of `clang`, `llvm-cov` and `llvm-profdata` are consistent and that they match the most recent Clang/LLVM toolchain version in use by Envoy (see the [build container -toolchain](https://github.com/envoyproxy/envoy-build-tools/blob/master/build_container/build_container_ubuntu.sh) for reference). +toolchain](https://github.com/envoyproxy/envoy-build-tools/blob/main/build_container/build_container_ubuntu.sh) for reference). The summary results are printed to the standard output and the full coverage report is available in `generated/coverage/coverage.html`. @@ -765,8 +765,8 @@ need to navigate down and open "coverage.html" but then you can navigate per nor have seen some issues with seeing the artifacts tab. If you can't see it, log out of Circle, and then log back in and it should start working. -The latest coverage report for master is available -[here](https://storage.googleapis.com/envoy-postsubmit/master/coverage/index.html). The latest fuzz coverage report for master is available [here](https://storage.googleapis.com/envoy-postsubmit/master/fuzz_coverage/index.html). +The latest coverage report for main is available +[here](https://storage.googleapis.com/envoy-postsubmit/main/coverage/index.html). The latest fuzz coverage report for main is available [here](https://storage.googleapis.com/envoy-postsubmit/main/fuzz_coverage/index.html). It's also possible to specialize the coverage build to a specified test or test dir. This is useful when doing things like exploring the coverage of a fuzzer over its corpus. This can be done by diff --git a/bazel/foreign_cc/nghttp2.patch b/bazel/foreign_cc/nghttp2.patch index 91ddf1898e45..9eacb4500e37 100644 --- a/bazel/foreign_cc/nghttp2.patch +++ b/bazel/foreign_cc/nghttp2.patch @@ -1,8 +1,7 @@ -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 35c77d1d..47bd63f5 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -273,7 +273,11 @@ check_type_size("ssize_t" SIZEOF_SSIZE_T) +diff -u -r a/CMakeLists.txt b/CMakeLists.txt +--- a/CMakeLists.txt 2020-11-23 08:59:08.000000000 -0600 ++++ b/CMakeLists.txt 2021-01-15 17:15:43.665745800 -0600 +@@ -271,7 +271,11 @@ if(SIZEOF_SSIZE_T STREQUAL "") # ssize_t is a signed type in POSIX storing at least -1. # Set it to "int" to match the behavior of AC_TYPE_SSIZE_T (autotools). @@ -15,73 +14,4 @@ index 35c77d1d..47bd63f5 100644 endif() # AC_TYPE_UINT8_T # AC_TYPE_UINT16_T -# https://github.com/nghttp2/nghttp2/pull/1468 -diff --git a/lib/nghttp2_buf.c b/lib/nghttp2_buf.c -index 2a435bebf..92f97f7f2 100644 ---- a/lib/nghttp2_buf.c -+++ b/lib/nghttp2_buf.c -@@ -82,8 +82,10 @@ void nghttp2_buf_reset(nghttp2_buf *buf) { - } - - void nghttp2_buf_wrap_init(nghttp2_buf *buf, uint8_t *begin, size_t len) { -- buf->begin = buf->pos = buf->last = buf->mark = begin; -- buf->end = begin + len; -+ buf->begin = buf->pos = buf->last = buf->mark = buf->end = begin; -+ if (buf->end != NULL) { -+ buf->end += len; -+ } - } - - static int buf_chain_new(nghttp2_buf_chain **chain, size_t chunk_length, -diff --git a/lib/nghttp2_frame.c b/lib/nghttp2_frame.c -index 4821de408..940c723b0 100644 ---- a/lib/nghttp2_frame.c -+++ b/lib/nghttp2_frame.c -@@ -818,8 +818,10 @@ int nghttp2_frame_unpack_origin_payload(nghttp2_extension *frame, - size_t len = 0; - - origin = frame->payload; -- p = payload; -- end = p + payloadlen; -+ p = end = payload; -+ if (end != NULL) { -+ end += payloadlen; -+ } - - for (; p != end;) { - if (end - p < 2) { -diff --git a/lib/nghttp2_session.c b/lib/nghttp2_session.c -index 563ccd7de..794f141a1 100644 ---- a/lib/nghttp2_session.c -+++ b/lib/nghttp2_session.c -@@ -5349,7 +5349,7 @@ static ssize_t inbound_frame_effective_readlen(nghttp2_inbound_frame *iframe, - - ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in, - size_t inlen) { -- const uint8_t *first = in, *last = in + inlen; -+ const uint8_t *first = in, *last = in; - nghttp2_inbound_frame *iframe = &session->iframe; - size_t readlen; - ssize_t padlen; -@@ -5360,6 +5360,10 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in, - size_t pri_fieldlen; - nghttp2_mem *mem; - -+ if (in != NULL) { -+ last += inlen; -+ } -+ - DEBUGF("recv: connection recv_window_size=%d, local_window=%d\n", - session->recv_window_size, session->local_window_size); - -@@ -5389,7 +5393,9 @@ ssize_t nghttp2_session_mem_recv(nghttp2_session *session, const uint8_t *in, - } - - iframe->payloadleft -= readlen; -- in += readlen; -+ if (in != NULL) { -+ in += readlen; -+ } - - if (iframe->payloadleft == 0) { - session_inbound_frame_reset(session); +Only in b: CMakeLists.txt.orig diff --git a/bazel/repositories.bzl b/bazel/repositories.bzl index e1b65d3aadb1..e9e2beceb1f0 100644 --- a/bazel/repositories.bzl +++ b/bazel/repositories.bzl @@ -378,8 +378,7 @@ def _com_github_nghttp2_nghttp2(): name = "com_github_nghttp2_nghttp2", build_file_content = BUILD_ALL_CONTENT, patch_args = ["-p1"], - # This patch cannot be picked up due to ABI rules. Better - # solve is likely at the next version-major. Discussion at; + # This patch cannot be picked up due to ABI rules. Discussion at; # https://github.com/nghttp2/nghttp2/pull/1395 # https://github.com/envoyproxy/envoy/pull/8572#discussion_r334067786 patches = ["@envoy//bazel/foreign_cc:nghttp2.patch"], @@ -784,7 +783,7 @@ def _emscripten_toolchain(): ".emscripten_sanity", ]), patch_cmds = [ - "[[ \"$(uname -m)\" == \"x86_64\" ]] && ./emsdk install 2.0.7 && ./emsdk activate --embedded 2.0.7 || true", + "if [[ \"$(uname -m)\" == \"x86_64\" ]]; then ./emsdk install 2.0.7 && ./emsdk activate --embedded 2.0.7; fi", ], ) diff --git a/bazel/repository_locations.bzl b/bazel/repository_locations.bzl index e6f1b1afe8dd..8ce15bb5d239 100644 --- a/bazel/repository_locations.bzl +++ b/bazel/repository_locations.bzl @@ -60,18 +60,18 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "BoringSSL", project_desc = "Minimal OpenSSL fork", project_url = "https://github.com/google/boringssl", - version = "1ce6682c7f6cfe0426ed54a37c10775bea9d3502", - sha256 = "b878d84f90b9a95fa1e53f46f1b69a5116621e117a6d4dbf602d884311ee6aa7", - strip_prefix = "boringssl-{version}", # To update BoringSSL, which tracks Chromium releases: # 1. Open https://omahaproxy.appspot.com/ and note of linux/stable release. # 2. Open https://chromium.googlesource.com/chromium/src/+/refs/tags//DEPS and note . # 3. Find a commit in BoringSSL's "master-with-bazel" branch that merges . # - # chromium-87.0.4280.66 + # chromium-88.0.4324.96 + version = "fbbf8781456c38a90f674d4771126ed39132855b", + sha256 = "76a571c1c3f6f6618d538384319ccc2b7ba9f7ae3f60087ba67894faba11a6b6", + strip_prefix = "boringssl-{version}", urls = ["https://github.com/google/boringssl/archive/{version}.tar.gz"], use_category = ["controlplane", "dataplane_core"], - release_date = "2020-09-21", + release_date = "2020-11-04", cpe = "cpe:2.3:a:google:boringssl:*", ), boringssl_fips = dict( @@ -263,12 +263,12 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "Nghttp2", project_desc = "Implementation of HTTP/2 and its header compression algorithm HPACK in Cimplementation of HTTP/2 and its header compression algorithm HPACK in C", project_url = "https://nghttp2.org", - version = "1.41.0", - sha256 = "eacc6f0f8543583ecd659faf0a3f906ed03826f1d4157b536b4b385fe47c5bb8", + version = "1.42.0", + sha256 = "884d18a0158908125d58b1b61d475c0325e5a004e3d61a56b5fcc55d5f4b7af5", strip_prefix = "nghttp2-{version}", urls = ["https://github.com/nghttp2/nghttp2/releases/download/v{version}/nghttp2-{version}.tar.gz"], use_category = ["controlplane", "dataplane_core"], - release_date = "2020-06-02", + release_date = "2020-11-23", cpe = "cpe:2.3:a:nghttp2:nghttp2:*", ), io_opentracing_cpp = dict( @@ -505,13 +505,13 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "grpc-httpjson-transcoding", project_desc = "Library that supports transcoding so that HTTP/JSON can be converted to gRPC", project_url = "https://github.com/grpc-ecosystem/grpc-httpjson-transcoding", - version = "4d095f048889d4fc3b8d4579aa80ca4290319802", - sha256 = "7af66e0674340932683ab4f04ea6f03e2550849a54741738d94310b84d396a2c", + version = "22160afc7e67b9becce3198f8a6a321f01a3cef8", + sha256 = "d761b6442f600b628f5e420b601824757dfdbea6c12ac86cd59dbaa53d20d343", strip_prefix = "grpc-httpjson-transcoding-{version}", urls = ["https://github.com/grpc-ecosystem/grpc-httpjson-transcoding/archive/{version}.tar.gz"], use_category = ["dataplane_ext"], extensions = ["envoy.filters.http.grpc_json_transcoder"], - release_date = "2020-11-13", + release_date = "2021-01-06", cpe = "N/A", ), io_bazel_rules_go = dict( @@ -665,14 +665,14 @@ REPOSITORY_LOCATIONS_SPEC = dict( project_name = "V8", project_desc = "Google’s open source high-performance JavaScript and WebAssembly engine, written in C++", project_url = "https://v8.dev", - version = "8.8.278.8", + version = "8.9.255.6", # This archive was created using https://storage.googleapis.com/envoyproxy-wee8/wee8-archive.sh # and contains complete checkout of V8 with all dependencies necessary to build wee8. - sha256 = "0c5c7b534a619d3f6077dd3583b7976a2cfe7f8ea71ca1e2d81a9de1d40131f9", + sha256 = "34c41e1f62a68f94cc0715fa3dc02e8bdcd35b5ca45a038975d9aa6a3e2688b5", urls = ["https://storage.googleapis.com/envoyproxy-wee8/wee8-{version}.tar.gz"], use_category = ["dataplane_ext"], extensions = ["envoy.wasm.runtime.v8"], - release_date = "2020-12-04", + release_date = "2021-01-18", cpe = "cpe:2.3:a:google:v8:*", ), com_googlesource_quiche = dict( diff --git a/ci/README.md b/ci/README.md index 028e31263b30..9cde25759f94 100644 --- a/ci/README.md +++ b/ci/README.md @@ -6,24 +6,24 @@ and an image based on Windows2019. ## Ubuntu Envoy image The Ubuntu based Envoy Docker image at [`envoyproxy/envoy-build:`](https://hub.docker.com/r/envoyproxy/envoy-build/) is used for CI checks, -where `` is specified in [`envoy_build_sha.sh`](https://github.com/envoyproxy/envoy/blob/master/ci/envoy_build_sha.sh). Developers -may work with the latest build image SHA in [envoy-build-tools](https://github.com/envoyproxy/envoy-build-tools/blob/master/toolchains/rbe_toolchains_config.bzl#L8) +where `` is specified in [`envoy_build_sha.sh`](https://github.com/envoyproxy/envoy/blob/main/ci/envoy_build_sha.sh). Developers +may work with the latest build image SHA in [envoy-build-tools](https://github.com/envoyproxy/envoy-build-tools/blob/main/toolchains/rbe_toolchains_config.bzl#L8) repo to provide a self-contained environment for building Envoy binaries and running tests that reflects the latest built Ubuntu Envoy image. Moreover, the Docker image at [`envoyproxy/envoy-dev:`](https://hub.docker.com/r/envoyproxy/envoy-dev/) is an image that has an Envoy binary at `/usr/local/bin/envoy`. -The `` corresponds to the master commit at which the binary was compiled. Lastly, `envoyproxy/envoy-dev:latest` contains an Envoy -binary built from the latest tip of master that passed tests. +The `` corresponds to the main commit at which the binary was compiled. Lastly, `envoyproxy/envoy-dev:latest` contains an Envoy +binary built from the latest tip of main that passed tests. ## Alpine Envoy image Minimal images based on Alpine Linux allow for quicker deployment of Envoy. The Alpine base image is only built with symbols stripped. To get the binary with symbols, use the corresponding Ubuntu based debug image. The image is pushed with two different tags: `` and `latest`. Parallel to the Ubuntu images above, `` corresponds to the -master commit at which the binary was compiled, and `latest` corresponds to a binary built from the latest tip of master that passed tests. +main commit at which the binary was compiled, and `latest` corresponds to a binary built from the latest tip of main that passed tests. ## Windows 2019 Envoy image The Windows 2019 based Envoy Docker image at [`envoyproxy/envoy-build-windows2019:`](https://hub.docker.com/r/envoyproxy/envoy-build-windows2019/) -is used for CI checks, where `` is specified in [`envoy_build_sha.sh`](https://github.com/envoyproxy/envoy/blob/master/ci/envoy_build_sha.sh). +is used for CI checks, where `` is specified in [`envoy_build_sha.sh`](https://github.com/envoyproxy/envoy/blob/main/ci/envoy_build_sha.sh). Developers may work with the most recent `envoyproxy/envoy-build-windows2019` image to provide a self-contained environment for building Envoy binaries and running tests that reflects the latest built Windows 2019 Envoy image. @@ -44,7 +44,7 @@ We use the Clang compiler for all Linux CI runs with tests. We have an additiona # C++ standard library As of November 2019 after [#8859](https://github.com/envoyproxy/envoy/pull/8859) the official released binary is -[linked against libc++ on Linux](https://github.com/envoyproxy/envoy/blob/master/bazel/README.md#linking-against-libc-on-linux). +[linked against libc++ on Linux](https://github.com/envoyproxy/envoy/blob/main/bazel/README.md#linking-against-libc-on-linux). To override the C++ standard library in your build, set environment variable `ENVOY_STDLIB` to `libstdc++` or `libc++` and run `./ci/do_ci.sh` as described below. @@ -98,7 +98,7 @@ For a debug version of the Envoy binary you can run: The build artifact can be found in `/tmp/envoy-docker-build/envoy/source/exe/envoy-debug` (or wherever `$ENVOY_DOCKER_BUILD_DIR` points). -To leverage a [bazel remote cache](https://github.com/envoyproxy/envoy/tree/master/bazel#advanced-caching-setup) add the http_remote_cache endpoint to +To leverage a [bazel remote cache](https://github.com/envoyproxy/envoy/tree/main/bazel#advanced-caching-setup) add the http_remote_cache endpoint to the BAZEL_BUILD_EXTRA_OPTIONS environment variable ```bash diff --git a/ci/api_mirror.sh b/ci/api_mirror.sh index 03e8ab85d80c..8a3022b72431 100755 --- a/ci/api_mirror.sh +++ b/ci/api_mirror.sh @@ -3,8 +3,8 @@ set -e CHECKOUT_DIR=../data-plane-api -MAIN_BRANCH="refs/heads/master" -API_MAIN_BRANCH="master" +MAIN_BRANCH="refs/heads/main" +API_MAIN_BRANCH="main" if [[ "${AZP_BRANCH}" == "${MAIN_BRANCH}" ]]; then echo "Cloning..." diff --git a/ci/docker_ci.sh b/ci/docker_ci.sh index 3bd584923bdf..ab1283745037 100755 --- a/ci/docker_ci.sh +++ b/ci/docker_ci.sh @@ -101,11 +101,11 @@ push_images() { docker push "${BUILD_TAG}" } -MASTER_BRANCH="refs/heads/master" +MAIN_BRANCH="refs/heads/main" RELEASE_BRANCH_REGEX="^refs/heads/release/v.*" RELEASE_TAG_REGEX="^refs/tags/v.*" -# For master builds and release branch builds use the dev repo. Otherwise we assume it's a tag and +# For main builds and release branch builds use the dev repo. Otherwise we assume it's a tag and # we push to the primary repo. if [[ "${AZP_BRANCH}" =~ ${RELEASE_TAG_REGEX} ]]; then IMAGE_POSTFIX="" @@ -146,11 +146,11 @@ ENVOY_DOCKER_TAR="${ENVOY_DOCKER_IMAGE_DIRECTORY}/envoy-docker-images.tar.xz" echo "Saving built images to ${ENVOY_DOCKER_TAR}." docker save "${IMAGES_TO_SAVE[@]}" | xz -T0 -2 >"${ENVOY_DOCKER_TAR}" -# Only push images for master builds, release branch builds, and tag builds. -if [[ "${AZP_BRANCH}" != "${MASTER_BRANCH}" ]] && +# Only push images for main builds, release branch builds, and tag builds. +if [[ "${AZP_BRANCH}" != "${MAIN_BRANCH}" ]] && ! [[ "${AZP_BRANCH}" =~ ${RELEASE_BRANCH_REGEX} ]] && ! [[ "${AZP_BRANCH}" =~ ${RELEASE_TAG_REGEX} ]]; then - echo 'Ignoring non-master branch or tag for docker push.' + echo 'Ignoring non-main branch or tag for docker push.' exit 0 fi @@ -159,8 +159,8 @@ docker login -u "$DOCKERHUB_USERNAME" -p "$DOCKERHUB_PASSWORD" for BUILD_TYPE in "${BUILD_TYPES[@]}"; do push_images "${BUILD_TYPE}" "${DOCKER_IMAGE_PREFIX}${BUILD_TYPE}${IMAGE_POSTFIX}:${IMAGE_NAME}" - # Only push latest on master builds. - if [[ "${AZP_BRANCH}" == "${MASTER_BRANCH}" ]]; then + # Only push latest on main builds. + if [[ "${AZP_BRANCH}" == "${MAIN_BRANCH}" ]]; then docker tag "${DOCKER_IMAGE_PREFIX}${BUILD_TYPE}${IMAGE_POSTFIX}:${IMAGE_NAME}" "${DOCKER_IMAGE_PREFIX}${BUILD_TYPE}${IMAGE_POSTFIX}:latest" push_images "${BUILD_TYPE}" "${DOCKER_IMAGE_PREFIX}${BUILD_TYPE}${IMAGE_POSTFIX}:latest" fi diff --git a/ci/filter_example_mirror.sh b/ci/filter_example_mirror.sh index 8602b1677e4b..a51122771403 100755 --- a/ci/filter_example_mirror.sh +++ b/ci/filter_example_mirror.sh @@ -4,8 +4,8 @@ set -e ENVOY_SRCDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/../" && pwd) CHECKOUT_DIR=../envoy-filter-example -MAIN_BRANCH="refs/heads/master" -FILTER_EXAMPLE_MAIN_BRANCH="master" +MAIN_BRANCH="refs/heads/main" +FILTER_EXAMPLE_MAIN_BRANCH="main" if [[ "${AZP_BRANCH}" == "${MAIN_BRANCH}" ]]; then echo "Cloning..." diff --git a/ci/go_mirror.sh b/ci/go_mirror.sh index 63f96d0d7969..96743eef6262 100755 --- a/ci/go_mirror.sh +++ b/ci/go_mirror.sh @@ -2,7 +2,7 @@ set -e -MAIN_BRANCH="refs/heads/master" +MAIN_BRANCH="refs/heads/main" # shellcheck source=ci/setup_cache.sh . "$(dirname "$0")"/setup_cache.sh diff --git a/ci/repokitteh/modules/newcontributor.star b/ci/repokitteh/modules/newcontributor.star index a192bd8613e7..6bc5fc7071b2 100644 --- a/ci/repokitteh/modules/newcontributor.star +++ b/ci/repokitteh/modules/newcontributor.star @@ -4,7 +4,7 @@ Hi @%s, welcome and thank you for your contribution. We will try to review your Pull Request as quickly as possible. -In the meantime, please take a look at the [contribution guidelines](https://github.com/envoyproxy/envoy/blob/master/CONTRIBUTING.md) if you have not done so already. +In the meantime, please take a look at the [contribution guidelines](https://github.com/envoyproxy/envoy/blob/main/CONTRIBUTING.md) if you have not done so already. """ diff --git a/ci/upload_gcs_artifact.sh b/ci/upload_gcs_artifact.sh index aea9dbc3f24f..4a4bba6e4603 100755 --- a/ci/upload_gcs_artifact.sh +++ b/ci/upload_gcs_artifact.sh @@ -19,7 +19,7 @@ if [ ! -d "${SOURCE_DIRECTORY}" ]; then fi if [[ "$BUILD_REASON" == "PullRequest" ]]; then - # non-master upload to the last commit sha (first 7 chars) in the developers branch + # non-main upload to the last commit sha (first 7 chars) in the developers branch UPLOAD_PATH="$(git log --pretty=%P -n 1 | cut -d' ' -f2 | head -c7)" else UPLOAD_PATH="${SYSTEM_PULLREQUEST_PULLREQUESTNUMBER:-${BUILD_SOURCEBRANCHNAME}}" diff --git a/ci/verify_examples.sh b/ci/verify_examples.sh index 03a26be026a3..974e46b7ad89 100755 --- a/ci/verify_examples.sh +++ b/ci/verify_examples.sh @@ -29,7 +29,7 @@ trap exit 1 INT run_examples () { local examples example cd "${SRCDIR}/examples" || exit 1 - examples=$(find . -mindepth 1 -maxdepth 1 -type d -name "$TESTFILTER" | sort) + examples=$(find . -mindepth 1 -maxdepth 1 -type d -name "$TESTFILTER" ! -iname "_*" | sort) for example in $examples; do pushd "$example" > /dev/null || return 1 ./verify.sh diff --git a/ci/windows_ci_steps.sh b/ci/windows_ci_steps.sh index fd3497e8474d..3a8f5bb3ae12 100755 --- a/ci/windows_ci_steps.sh +++ b/ci/windows_ci_steps.sh @@ -50,6 +50,7 @@ BAZEL_BUILD_OPTIONS=( --show_task_finish --verbose_failures "--test_output=errors" + "--repository_cache=${BUILD_DIR/\/c/c:}/repository_cache" "${BAZEL_BUILD_EXTRA_OPTIONS[@]}" "${BAZEL_EXTRA_TEST_OPTIONS[@]}") diff --git a/docs/README.md b/docs/README.md index 5cd5444d670b..27feeffc1564 100644 --- a/docs/README.md +++ b/docs/README.md @@ -6,7 +6,7 @@ In both cases, the generated output can be found in `generated/docs`. ## Building in an existing Envoy development environment -If you have an [existing Envoy development environment](https://github.com/envoyproxy/envoy/tree/master/bazel#quick-start-bazel-build-for-developers), you should have the necessary dependencies and requirements and be able to build the documentation directly. +If you have an [existing Envoy development environment](https://github.com/envoyproxy/envoy/tree/main/bazel#quick-start-bazel-build-for-developers), you should have the necessary dependencies and requirements and be able to build the documentation directly. ```bash ./docs/build.sh @@ -45,7 +45,7 @@ To do this: 1. The docs are published to [docs/envoy/latest](https://github.com/envoyproxy/envoyproxy.github.io/tree/master/docs/envoy/latest) on every commit to master. This process is handled by Azure Pipelines with the - [`publish.sh`](https://github.com/envoyproxy/envoy/blob/master/docs/publish.sh) script. + [`publish.sh`](https://github.com/envoyproxy/envoy/blob/main/docs/publish.sh) script. 2. The docs are published to [docs/envoy](https://github.com/envoyproxy/envoyproxy.github.io/tree/master/docs/envoy) in a directory named after every tagged commit in this repo. Thus, on every tagged release there diff --git a/docs/publish.sh b/docs/publish.sh index 11b75f1b77c9..1c65cdecae50 100755 --- a/docs/publish.sh +++ b/docs/publish.sh @@ -13,7 +13,7 @@ DOCS_DIR=generated/docs CHECKOUT_DIR=envoy-docs BUILD_SHA=$(git rev-parse HEAD) -MAIN_BRANCH="refs/heads/master" +MAIN_BRANCH="refs/heads/main" RELEASE_TAG_REGEX="^refs/tags/v.*" if [[ "${AZP_BRANCH}" =~ ${RELEASE_TAG_REGEX} ]]; then @@ -25,7 +25,7 @@ else exit 0 fi -DOCS_MAIN_BRANCH="master" +DOCS_MAIN_BRANCH="main" echo 'cloning' git clone git@github.com:envoyproxy/envoyproxy.github.io "${CHECKOUT_DIR}" -b "${DOCS_MAIN_BRANCH}" --depth 1 diff --git a/docs/root/api-v3/common_messages/common_messages.rst b/docs/root/api-v3/common_messages/common_messages.rst index 24309a9d7815..1e04318e4736 100644 --- a/docs/root/api-v3/common_messages/common_messages.rst +++ b/docs/root/api-v3/common_messages/common_messages.rst @@ -22,4 +22,5 @@ Common messages ../extensions/filters/common/fault/v3/fault.proto ../extensions/network/socket_interface/v3/default_socket_interface.proto ../extensions/common/matching/v3/extension_matcher.proto + ../extensions/filters/common/dependency/v3/dependency.proto ../extensions/filters/common/matcher/action/v3/skip_action.proto diff --git a/docs/root/api-v3/config/common/common.rst b/docs/root/api-v3/config/common/common.rst index bb6965a5f149..f286ba06c4e9 100644 --- a/docs/root/api-v3/config/common/common.rst +++ b/docs/root/api-v3/config/common/common.rst @@ -8,3 +8,4 @@ Common matcher/v3/* ../../extensions/common/dynamic_forward_proxy/v3/* ../../extensions/common/tap/v3/* + ../../extensions/common/ratelimit/v3/* diff --git a/docs/root/api-v3/config/retry/retry.rst b/docs/root/api-v3/config/retry/retry.rst index 2ba4572ab2ed..75b69edd2bc1 100644 --- a/docs/root/api-v3/config/retry/retry.rst +++ b/docs/root/api-v3/config/retry/retry.rst @@ -7,4 +7,5 @@ Retry Predicates */empty/* */v2/* + */v3/* ../../extensions/retry/**/v3/* diff --git a/docs/root/api-v3/types/types.rst b/docs/root/api-v3/types/types.rst index 3e6af53865bd..4321708bdb82 100644 --- a/docs/root/api-v3/types/types.rst +++ b/docs/root/api-v3/types/types.rst @@ -21,5 +21,6 @@ Types ../type/matcher/v3/string.proto ../type/matcher/v3/struct.proto ../type/matcher/v3/value.proto + ../type/matcher/v3/http_inputs.proto ../type/metadata/v3/metadata.proto ../type/tracing/v3/custom_tag.proto diff --git a/docs/root/configuration/http/http_conn_man/headers.rst b/docs/root/configuration/http/http_conn_man/headers.rst index 143def096e48..6763707f1aac 100644 --- a/docs/root/configuration/http/http_conn_man/headers.rst +++ b/docs/root/configuration/http/http_conn_man/headers.rst @@ -479,7 +479,7 @@ Custom request/response headers can be added to a request/response at the weight route, virtual host, and/or global route configuration level. See the :ref:`v3 ` API documentation. -No *:-prefixed* pseudo-header may be modified via this mechanism. The *:path* +Neither *:-prefixed* pseudo-headers nor the Host: header may be modified via this mechanism. The *:path* and *:authority* headers may instead be modified via mechanisms such as :ref:`prefix_rewrite `, :ref:`regex_rewrite `, and diff --git a/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst b/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst index 4e1ca3526341..3699fb96d3ef 100644 --- a/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst +++ b/docs/root/configuration/http/http_filters/dynamic_forward_proxy_filter.rst @@ -31,8 +31,6 @@ host when forwarding. See the example below within the configured routes. .. _dns_cache_circuit_breakers: Dynamic forward proxy uses circuit breakers built in to the DNS cache with the configuration - of :ref:`DNS cache circuit breakers `. By default, this behavior is enabled by the runtime feature `envoy.reloadable_features.enable_dns_cache_circuit_breakers`. - If this runtime feature is disabled, cluster circuit breakers will be used even when setting the configuration of :ref:`DNS cache circuit breakers `. .. literalinclude:: _include/dns-cache-circuit-breaker.yaml diff --git a/docs/root/configuration/http/http_filters/ext_proc_filter.rst b/docs/root/configuration/http/http_filters/ext_proc_filter.rst index 851dea86e1c4..f00eb27d1bf1 100644 --- a/docs/root/configuration/http/http_filters/ext_proc_filter.rst +++ b/docs/root/configuration/http/http_filters/ext_proc_filter.rst @@ -17,3 +17,24 @@ messages, and the server must reply with :ref:`ProcessingResponse `. This filter is a work in progress. In its current state, it actually does nothing. + +Statistics +---------- +This filter outputs statistics in the +*http..ext_proc.* namespace. The :ref:`stat prefix +` +comes from the owning HTTP connection manager. + +The following statistics are supported: + +.. csv-table:: + :header: Name, Type, Description + :widths: auto + + streams_started, Counter, The number of gRPC streams that have been started to send to the external processing service + streams_msgs_sent, Counter, The number of messages sent on those streams + streams_msgs_received, Counter, The number of messages received on those streams + spurious_msgs_received, Counter, The number of unexpected messages received that violated the protocol + streams_closed, Counter, The number of streams successfully closed on either end + streams_failed, Counter, The number of times a stream produced a gRPC error + failure_mode_allowed, Counter, The number of times an error was ignored due to configuration diff --git a/docs/root/configuration/http/http_filters/jwt_authn_filter.rst b/docs/root/configuration/http/http_filters/jwt_authn_filter.rst index f8634e2971bc..814deed6b16c 100644 --- a/docs/root/configuration/http/http_filters/jwt_authn_filter.rst +++ b/docs/root/configuration/http/http_filters/jwt_authn_filter.rst @@ -45,7 +45,7 @@ JwtProvider Default Extract Location ~~~~~~~~~~~~~~~~~~~~~~~~ -If *from_headers* and *from_params* is empty, the default location to extract JWT is from HTTP header:: +If *from_headers* and *from_params* is empty, the default location to extract JWT is from HTTP header:: Authorization: Bearer @@ -81,7 +81,7 @@ Remote JWKS config example cache_duration: seconds: 300 -Above example fetches JWSK from a remote server with URL https://example.com/jwks.json. The token will be extracted from the default extract locations. The token will not be forwarded to upstream. JWT payload will not be added to the request header. +Above example fetches JWKS from a remote server with URL https://example.com/jwks.json. The token will be extracted from the default extract locations. The token will not be forwarded to upstream. JWT payload will not be added to the request header. Following cluster **example_jwks_cluster** is needed to fetch JWKS. diff --git a/docs/root/configuration/http/http_filters/local_rate_limit_filter.rst b/docs/root/configuration/http/http_filters/local_rate_limit_filter.rst index 78bbc806a78e..3903eefe8b33 100644 --- a/docs/root/configuration/http/http_filters/local_rate_limit_filter.rst +++ b/docs/root/configuration/http/http_filters/local_rate_limit_filter.rst @@ -103,6 +103,93 @@ The route specific configuration: Note that if this filter is configured as globally disabled and there are no virtual host or route level token buckets, no rate limiting will be applied. +.. _config_http_filters_local_rate_limit_descriptors: + +Using rate limit descriptors for local rate limiting +---------------------------------------------------- + +Rate limit descriptors can be used to override local per-route rate limiting. +A route's :ref:`rate limit action ` +is used to match up a :ref:`local descriptor +` in +the filter config descriptor list. The local descriptor's token bucket +settings are then used to decide if the request should be rate limited or not +depending on whether the local descriptor's entries match the route's rate +limit actions descriptor entries. If there is no matching descriptor entries, +the default token bucket is used. + +Example filter configuration using descriptors: + +.. validated-code-block:: yaml + :type-name: envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + + route_config: + name: local_route + virtual_hosts: + - name: local_service + domains: ["*"] + routes: + - match: { prefix: "/foo" } + route: { cluster: service_protected_by_rate_limit } + typed_per_filter_config: + envoy.filters.http.local_ratelimit: + "@type": type.googleapis.com/envoy.extensions.filters.http.local_ratelimit.v3.LocalRateLimit + stat_prefix: test + token_bucket: + max_tokens: 1000 + tokens_per_fill: 1000 + fill_interval: 60s + filter_enabled: + runtime_key: test_enabled + default_value: + numerator: 100 + denominator: HUNDRED + filter_enforced: + runtime_key: test_enforced + default_value: + numerator: 100 + denominator: HUNDRED + response_headers_to_add: + - append: false + header: + key: x-test-rate-limit + value: 'true' + descriptors: + - entries: + - key: client_cluster + value: foo + - key: path + value: /foo/bar + token_bucket: + max_tokens: 10 + tokens_per_fill: 10 + fill_interval: 60s + - entries: + - key: client_cluster + value: foo + - key: path + value: /foo/bar2 + token_bucket: + max_tokens: 100 + tokens_per_fill: 100 + fill_interval: 60s + - match: { prefix: "/" } + route: { cluster: default_service } + rate_limits: + - actions: # any actions in here + - request_headers: + header_name: x-envoy-downstream-service-cluster + descriptor_key: client_cluster + - request_headers: + header_name: ":path" + descriptor_key: path + +In this example, requests are rate-limited for routes prefixed with "/foo" as +follow. If requests come from a downstream service cluster "foo" for "/foo/bar" +path, then 10 req/min are allowed. But if they come from a downstream service +cluster "foo" for "/foo/bar2" path, then 100 req/min are allowed. Otherwise, +1000 req/min are allowed. + Statistics ---------- diff --git a/docs/root/configuration/http/http_filters/oauth2_filter.rst b/docs/root/configuration/http/http_filters/oauth2_filter.rst index 6b8b9789a5c7..ebd2f9cdff5f 100644 --- a/docs/root/configuration/http/http_filters/oauth2_filter.rst +++ b/docs/root/configuration/http/http_filters/oauth2_filter.rst @@ -71,6 +71,11 @@ The following is an example configuring the filter. name: hmac sds_config: path: "/etc/envoy/hmac.yaml" + # (Optional): defaults to 'user' scope if not provided + auth_scopes: + - user + - openid + - email Below is a complete code example of how we employ the filter as one of :ref:`HttpConnectionManager HTTP filters @@ -114,6 +119,11 @@ Below is a complete code example of how we employ the filter as one of name: hmac sds_config: path: "/etc/envoy/hmac.yaml" + # (Optional): defaults to 'user' scope if not provided + auth_scopes: + - user + - openid + - email - name: envoy.router tracing: {} codec_type: "AUTO" diff --git a/docs/root/configuration/listeners/listener_filters/tls_inspector.rst b/docs/root/configuration/listeners/listener_filters/tls_inspector.rst index e9897435e880..9e6d424485e5 100644 --- a/docs/root/configuration/listeners/listener_filters/tls_inspector.rst +++ b/docs/root/configuration/listeners/listener_filters/tls_inspector.rst @@ -15,8 +15,10 @@ from the client. This can be used to select a of a :ref:`FilterChainMatch `. * :ref:`SNI ` -* :ref:`v2 API reference ` -* This filter should be configured with the name *envoy.filters.listener.tls_inspector*. +* :ref:`v3 API reference ` +* This filter may be configured with the name *envoy.filters.listener.tls_inspector* or + *type.googleapis.com/envoy.extensions.listeners.tls_inspector.v3.TlsInspector* as the + `type_url `_. Example ------- @@ -29,10 +31,20 @@ A sample filter configuration could be: - name: "envoy.filters.listener.tls_inspector" typed_config: {} +Or by specifying the `type_url `_ +of the *typed_config*: + +.. code-block:: yaml + + listener_filters: + - name: "tls_inspector" + typed_config: + "@type": type.googleapis.com/envoy.extensions.listeners.tls_inspector.v3.TlsInspector + Statistics ---------- -This filter has a statistics tree rooted at *tls_inspector* with the following statistics: +This filter has a statistics tree rooted at *tls_inspector* with the following statistics: .. csv-table:: :header: Name, Type, Description diff --git a/docs/root/configuration/observability/access_log/usage.rst b/docs/root/configuration/observability/access_log/usage.rst index 9a00ac1a29d9..6497767fde45 100644 --- a/docs/root/configuration/observability/access_log/usage.rst +++ b/docs/root/configuration/observability/access_log/usage.rst @@ -163,6 +163,13 @@ The following command operators are supported: In typed JSON logs, START_TIME is always rendered as a string. +%REQUEST_HEADERS_BYTES% + HTTP + Uncompressed bytes of request headers. + + TCP + Not implemented (0). + %BYTES_RECEIVED% HTTP Body bytes received. @@ -213,6 +220,20 @@ The following command operators are supported: Connection termination details may provide additional information about why the connection was terminated by Envoy for L4 reasons. +%RESPONSE_HEADERS_BYTES% + HTTP + Uncompressed bytes of response headers. + + TCP + Not implemented (0). + +%RESPONSE_TRAILERS_BYTES% + HTTP + Uncompressed bytes of response trailers. + + TCP + Not implemented (0). + %BYTES_SENT% HTTP Body bytes sent. For WebSocket connection it will also include response header bytes. diff --git a/docs/root/configuration/observability/statistics.rst b/docs/root/configuration/observability/statistics.rst index ed25a3e2de8d..4732d88d25c1 100644 --- a/docs/root/configuration/observability/statistics.rst +++ b/docs/root/configuration/observability/statistics.rst @@ -34,3 +34,13 @@ Server related statistics are rooted at *server.* with following statistics: static_unknown_fields, Counter, Number of messages in static configuration with unknown fields dynamic_unknown_fields, Counter, Number of messages in dynamic configuration with unknown fields +Server Compilation Settings +--------------------------- + +Server Compilation Settings related statistics are rooted at *server.compilation_settings.* with following statistics: + +.. csv-table:: + :header: Name, Type, Description + :widths: 1, 1, 2 + + fips_mode, Gauge, Integer representing whether the envoy build is FIPS compliant or not \ No newline at end of file diff --git a/docs/root/configuration/other_protocols/thrift_filters/router_filter.rst b/docs/root/configuration/other_protocols/thrift_filters/router_filter.rst index 22ce7bcbf137..c08a02f80630 100644 --- a/docs/root/configuration/other_protocols/thrift_filters/router_filter.rst +++ b/docs/root/configuration/other_protocols/thrift_filters/router_filter.rst @@ -7,7 +7,7 @@ The router filter implements Thrift forwarding. It will be used in almost all Th scenarios. The filter's main job is to follow the instructions specified in the configured :ref:`route table `. -* :ref:`v3 API reference ` +* :ref:`v3 API reference ` * This filter should be configured with the name *envoy.filters.thrift.router*. Statistics diff --git a/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst b/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst index 874b9d9f28fa..b6e5389e7da3 100644 --- a/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst +++ b/docs/root/configuration/upstream/cluster_manager/cluster_stats.rst @@ -94,6 +94,7 @@ Every cluster has a statistics tree rooted at *cluster..* with the followi update_attempt, Counter, Total attempted cluster membership updates by service discovery update_success, Counter, Total successful cluster membership updates by service discovery update_failure, Counter, Total failed cluster membership updates by service discovery + update_duration, Histogram, Amount of time spent updating configs update_empty, Counter, Total cluster membership updates ending with empty cluster load assignment and continuing with previous config update_no_rebuild, Counter, Total successful cluster membership updates that didn't result in any cluster load balancing structure rebuilds version, Gauge, Hash of the contents from the last successful API fetch diff --git a/docs/root/extending/extending.rst b/docs/root/extending/extending.rst index 7f2e676e20eb..aaf83ce7b516 100644 --- a/docs/root/extending/extending.rst +++ b/docs/root/extending/extending.rst @@ -26,6 +26,7 @@ types including: * :ref:`Compression libraries ` * :ref:`Bootstrap extensions ` * :ref:`Fatal actions ` +* :ref:`Formatters ` As of this writing there is no high level extension developer documentation. The :repo:`existing extensions ` are a good way to learn what is possible. diff --git a/docs/root/intro/arch_overview/http/upgrades.rst b/docs/root/intro/arch_overview/http/upgrades.rst index 6a4338d39ee5..ba015ff838a9 100644 --- a/docs/root/intro/arch_overview/http/upgrades.rst +++ b/docs/root/intro/arch_overview/http/upgrades.rst @@ -94,6 +94,8 @@ will synthesize 200 response headers, and then forward the TCP data as the HTTP For an example of proxying connect, please see :repo:`configs/proxy_connect.yaml ` For an example of terminating connect, please see :repo:`configs/terminate_connect.yaml ` +.. _tunneling-tcp-over-http: + Tunneling TCP over HTTP ^^^^^^^^^^^^^^^^^^^^^^^ Envoy also has support for tunneling raw TCP over HTTP CONNECT requests. Find diff --git a/docs/root/intro/arch_overview/upstream/outlier.rst b/docs/root/intro/arch_overview/upstream/outlier.rst index fcfb6f6da837..bbc8e960c675 100644 --- a/docs/root/intro/arch_overview/upstream/outlier.rst +++ b/docs/root/intro/arch_overview/upstream/outlier.rst @@ -110,8 +110,9 @@ the :ref:`outlier_detection.consecutive_5xx`. +In the default mode (:ref:`outlier_detection.split_external_local_origin_errors` is *false*) this detection type takes into account a subset of 5xx errors, called "gateway errors" (502, 503 or 504 status code) and local origin failures, such as timeout, TCP reset etc. + +In split mode (:ref:`outlier_detection.split_external_local_origin_errors` is *true*) this detection type takes into account a subset of 5xx errors, called "gateway errors" (502, 503 or 504 status code) and is supported only by the :ref:`http router `. If an upstream host returns some number of consecutive "gateway errors" (502, 503 or 504 status code), it will be ejected. diff --git a/docs/root/start/sandboxes/index.rst b/docs/root/start/sandboxes/index.rst index d1b686d01820..4bf8c5f8c102 100644 --- a/docs/root/start/sandboxes/index.rst +++ b/docs/root/start/sandboxes/index.rst @@ -63,6 +63,7 @@ The following sandboxes are available: postgres redis skywalking_tracing + tls-sni tls wasm-cc websocket diff --git a/docs/root/start/sandboxes/tls-sni.rst b/docs/root/start/sandboxes/tls-sni.rst new file mode 100644 index 000000000000..f61817027b5a --- /dev/null +++ b/docs/root/start/sandboxes/tls-sni.rst @@ -0,0 +1,175 @@ +.. _install_sandboxes_tls_sni: + +TLS Server name indication (``SNI``) +==================================== + +.. sidebar:: Requirements + + .. include:: _include/docker-env-setup-link.rst + + :ref:`curl ` + Used to make ``HTTP`` requests. + + :ref:`jq ` + Parse ``json`` output from the upstream echo servers. + +This example demonstrates an Envoy proxy that listens on three ``TLS`` domains +on the same ``IP`` address. + +The first two domains (``domain1`` and ``domain2``) terminate the ``TLS`` and proxy +to upstream ``HTTP`` hosts. + +The other domain (``domain3``) is proxied unterminated, based on the ``SNI`` headers. + +It also demonstrates Envoy acting as a client proxy connecting to upstream ``SNI`` services. + +.. _install_sandboxes_tls_sni_step1: + +Step 1: Create keypairs for each of the domain endpoints +******************************************************** + +Change directory to ``examples/tls-sni`` in the Envoy repository. + +The example creates two Envoy ``TLS`` endpoints and they will require their own +keypairs. + +Create self-signed certificates for these endpoints as follows: + +.. code-block:: console + + $ pwd + envoy/examples/tls-sni + + $ mkdir -p certs + + $ openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \ + -subj "/C=US/ST=CA/O=MyExample, Inc./CN=domain1.example.com" \ + -keyout certs/domain1.key.pem \ + -out certs/domain1.crt.pem + Generating a RSA private key + .............+++++ + ...................+++++ + writing new private key to 'certs/domain1.key.pem' + ----- + + $ openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \ + -subj "/C=US/ST=CA/O=MyExample, Inc./CN=domain2.example.com" \ + -keyout certs/domain2.key.pem \ + -out certs/domain2.crt.pem + Generating a RSA private key + .............+++++ + ...................+++++ + writing new private key to 'certs/domain2.key.pem' + ----- + +.. warning:: + + ``SNI`` does *not* validate that the certificates presented are correct for the domain, or that they + were issued by a recognised certificate authority. + + See the :ref:`Securing Envoy quick start guide ` for more information about + :ref:`validating cerfificates `. + +.. _install_sandboxes_tls_sni_step2: + +Step 2: Start the containers +**************************** + +Build and start the containers. + +This starts two upstream ``HTTP`` containers listening on the internal Docker network on port ``80``, and +an upstream ``HTTPS`` service listening on internal port ``443`` + +In front of these is an Envoy proxy that listens on https://localhost:10000 and serves three ``SNI`` routed +``TLS`` domains: + +- ``domain1.example.com`` +- ``domain2.example.com`` +- ``domain3.example.com`` + +The first two domains use the keys and certificates :ref:`you created in step 1 ` to terminate ``TLS`` and +proxy to the two upstream ``HTTP`` servers. + +The third domain proxies to the upstream ``TLS`` server based on the requested ``SNI`` address, but does no ``TLS`` termination itself. + +The composition also starts an Envoy proxy client which listens on http://localhost:20000. + +The client proxy has no ``TLS`` termination but instead proxies three routed paths - +``/domain1``, ``/domain2`` and ``/domain3`` - to the ``SNI``-enabled proxy. + +.. code-block:: console + + $ pwd + envoy/examples/tls-sni + $ docker-compose build --pull + $ docker-compose up -d + $ docker-compose ps + + Name Command State Ports + ------------------------------------------------------------------------------------------- + tls-sni_http-upstream1_1 node ./index.js Up + tls-sni_http-upstream2_1 node ./index.js Up + tls-sni_http-upstream3_1 node ./index.js Up + tls-sni_proxy_1 /docker-entrypoint.sh /usr ... Up 0.0.0.0:10000->10000/tcp + tls-sni_proxy-client_1 /docker-entrypoint.sh /usr ... Up 0.0.0.0:20000->10000/tcp + +Step 2: Query the ``SNI`` endpoints directly with curl +****************************************************** + +You can use curl to query the ``SNI``-routed ``HTTPS`` endpoints of the Envoy proxy directly. + +To do this you must explicitly tell curl to resolve the ``DNS`` for the endpoints correctly. + +Each endpoint should proxy to the respective ``http-upstream`` or ``https-upstream`` service. + +.. code-block:: console + + $ curl -sk --resolve domain1.example.com:10000:127.0.0.1 \ + https://domain1.example.com:10000 \ + | jq -r '.os.hostname' + http-upstream1 + + $ curl -sk --resolve domain2.example.com:10000:127.0.0.1 \ + https://domain2.example.com:10000 \ + | jq -r '.os.hostname' + http-upstream2 + + $ curl -sk --resolve domain3.example.com:10000:127.0.0.1 \ + https://domain3.example.com:10000 \ + | jq -r '.os.hostname' + https-upstream3 + +Step 3: Query the ``SNI`` endpoints via an Envoy proxy client +************************************************************* + +Next, query the Envoy proxy client using the routed paths. + +These route via the ``SNI`` proxy endpoints to the respective ``http-upstream`` or +``https-upstream`` services. + +.. code-block:: console + + $ curl -s http://localhost:20000/domain1 \ + | jq '.os.hostname' + http-upstream1 + + $ curl -s http://localhost:20000/domain2 \ + | jq '.os.hostname' + http-upstream2 + + $ curl -s http://localhost:20000/domain3 \ + | jq '.os.hostname' + https-upstream3 + +.. seealso:: + + :ref:`Securing Envoy quick start guide ` + Outline of key concepts for securing Envoy. + + :ref:`TLS sandbox ` + Sandbox featuring examples of how Envoy can be configured to make + use of encrypted connections using ``HTTP`` over ``TLS``. + + :ref:`Double proxy sandbox ` + An example of securing traffic between proxies with validation and + mutual authentication using ``mTLS`` with non-``HTTP`` traffic. diff --git a/docs/root/start/sandboxes/tls.rst b/docs/root/start/sandboxes/tls.rst index 84942094bb81..ecfbcaf4e3cb 100644 --- a/docs/root/start/sandboxes/tls.rst +++ b/docs/root/start/sandboxes/tls.rst @@ -170,6 +170,10 @@ The upstream ``service-https`` handles the request. :ref:`Securing Envoy quick start guide ` Outline of key concepts for securing Envoy. + :ref:`TLS SNI sandbox ` + Example of using Envoy to serve multiple domains protected by TLS and + served from the same ``IP`` address. + :ref:`Double proxy sandbox ` An example of securing traffic between proxies with validation and mutual authentication using ``mTLS`` with non-``HTTP`` traffic. diff --git a/docs/root/start/start.rst b/docs/root/start/start.rst index 2f7fdb876499..01d8b0c662cb 100644 --- a/docs/root/start/start.rst +++ b/docs/root/start/start.rst @@ -11,14 +11,11 @@ You can also :ref:`build it ` from source. These examples use the :ref:`v3 Envoy API `. .. toctree:: - :maxdepth: 3 + :maxdepth: 2 install quick-start/index + sandboxes/index docker building -.. toctree:: - :maxdepth: 2 - - sandboxes/index diff --git a/docs/root/version_history/current.rst b/docs/root/version_history/current.rst index f0f0ac3d5a53..2bf22571c14e 100644 --- a/docs/root/version_history/current.rst +++ b/docs/root/version_history/current.rst @@ -1,124 +1,54 @@ -1.17.0 (pending) +1.18.0 (Pending) ================ Incompatible Behavior Changes ----------------------------- *Changes that are expected to cause an incompatibility if applicable; deployment changes are likely required* -* config: v2 is now fatal-by-default. This may be overridden by setting :option:`--bootstrap-version` 2 on the CLI for a v2 bootstrap file and also enabling the runtime `envoy.reloadable_features.enable_deprecated_v2_api` feature. See - the :ref:`FAQ entry ` for further details. - Minor Behavior Changes ---------------------- *Changes that may cause incompatibilities for some users, but should not for most* -* build: the Alpine based debug images are no longer built in CI, use Ubuntu based images instead. -* cluster manager: the cluster which can't extract secret entity by SDS to be warming and never activate. This feature is disabled by default and is controlled by runtime guard `envoy.reloadable_features.cluster_keep_warming_no_secret_entity`. -* decompressor: set the default value of window_bits of the decompressor to 15 to be able to decompress responses compressed by a compressor with any window size. -* expr filter: added `connection.termination_details` property support. -* ext_authz filter: disable `envoy.reloadable_features.ext_authz_measure_timeout_on_check_created` by default. -* ext_authz filter: the deprecated field :ref:`use_alpha ` is no longer supported and cannot be set anymore. -* formatter: the :ref:`text_format ` field no longer requires at least one byte, and may now be the empty string. It has also become deprecated: see Deprecated section. -* grpc_web filter: if a `grpc-accept-encoding` header is present it's passed as-is to the upstream and if it isn't `grpc-accept-encoding:identity` is sent instead. The header was always overwriten with `grpc-accept-encoding:identity,deflate,gzip` before. -* http: upstream protocol will now only be logged if an upstream stream was established. -* jwt_authn filter: added support of Jwt time constraint verification with a clock skew (default to 60 seconds) and added a filter config field :ref:`clock_skew_seconds ` to configure it. -* kill_request: enable a way to configure kill header name in KillRequest proto. -* listener: injection of the :ref:`TLS inspector ` has been disabled by default. This feature is controlled by the runtime guard `envoy.reloadable_features.disable_tls_inspector_injection`. -* lua: add `always_wrap_body` argument to `body()` API to always return a :ref:`buffer object ` even if the body is empty. -* memory: enable new tcmalloc with restartable sequences for aarch64 builds. -* mongo proxy metrics: swapped network connection remote and local closed counters previously set reversed (`cx_destroy_local_with_active_rq` and `cx_destroy_remote_with_active_rq`). -* outlier detection: added :ref:`max_ejection_time ` to limit ejection time growth when a node stays unhealthy for extended period of time. By default :ref:`max_ejection_time ` limits ejection time to 5 minutes. Additionally, when the node stays healthy, ejection time decreases. See :ref:`ejection algorithm` for more info. Previously, ejection time could grow without limit and never decreased. -* performance: improve performance when handling large HTTP/1 bodies. -* tcp_proxy: now waits for HTTP tunnel to be established before start streaming the downstream data, the runtime guard `envoy.reloadable_features.http_upstream_wait_connect_response` can be set to "false" to disable this behavior. -* tls: removed RSA key transport and SHA-1 cipher suites from the client-side defaults. -* watchdog: the watchdog action :ref:`abort_action ` is now the default action to terminate the process if watchdog kill / multikill is enabled. -* xds: to support TTLs, heartbeating has been added to xDS. As a result, responses that contain empty resources without updating the version will no longer be propagated to the - subscribers. To undo this for VHDS (which is the only subscriber that wants empty resources), the `envoy.reloadable_features.vhds_heartbeats` can be set to "false". +* oauth filter: added the optional parameter :ref:`auth_scopes ` with default value of 'user' if not provided. Enables this value to be overridden in the Authorization request to the OAuth provider. +* tcp: setting NODELAY in the base connection class. This should have no effect for TCP or HTTP proxying, but may improve throughput in other areas. This behavior can be temporarily reverted by setting `envoy.reloadable_features.always_nodelay` to false. +* upstream: host weight changes now cause a full load balancer rebuild as opposed to happening + atomically inline. This change has been made to support load balancer pre-computation of data + structures based on host weight, but may have performance implications if host weight changes + are very frequent. This change can be disabled by setting the `envoy.reloadable_features.upstream_host_weight_change_causes_rebuild` + feature flag to false. If setting this flag to false is required in a deployment please open an + issue against the project. Bug Fixes --------- *Changes expected to improve the state of the world and are unlikely to have negative effects* -* config: validate that upgrade configs have a non-empty :ref:`upgrade_type `, fixing a bug where an errant "-" could result in unexpected behavior. -* dns: fix a bug where custom resolvers provided in configuration were not preserved after network issues. -* dns_filter: correctly associate DNS response IDs when multiple queries are received. -* grpc mux: fix sending node again after stream is reset when ::ref:`set_node_on_first_message_only ` is set. -* http: fixed URL parsing for HTTP/1.1 fully qualified URLs and connect requests containing IPv6 addresses. -* http: reject requests with missing required headers after filter chain processing. -* http: sending CONNECT_ERROR for HTTP/2 where appropriate during CONNECT requests. -* proxy_proto: fixed a bug where the wrong downstream address got sent to upstream connections. -* proxy_proto: fixed a bug where network filters would not have the correct downstreamRemoteAddress() when accessed from the StreamInfo. This could result in incorrect enforcement of RBAC rules in the RBAC network filter (but not in the RBAC HTTP filter), or incorrect access log addresses from tcp_proxy. -* sds: fix a bug that clusters sharing same sds target are marked active immediately. -* tls: fix detection of the upstream connection close event. -* tls: fix read resumption after triggering buffer high-watermark and all remaining request/response bytes are stored in the SSL connection's internal buffers. -* udp: fixed issue in which receiving truncated UDP datagrams would cause Envoy to crash. -* watchdog: touch the watchdog before most event loop operations to avoid misses when handling bursts of callbacks. +* active http health checks: properly handles HTTP/2 GOAWAY frames from the upstream. Previously a GOAWAY frame due to a graceful listener drain could cause improper failed health checks due to streams being refused by the upstream on a connection that is going away. To revert to old GOAWAY handling behavior, set the runtime feature `envoy.reloadable_features.health_check.graceful_goaway_handling` to false. +* buffer: tighten network connection read and write buffer high watermarks in preparation to more careful enforcement of read limits. Buffer high-watermark is now set to the exact configured value; previously it was set to value + 1. +* http: disallowing "host:" in request_headers_to_add for behavioral consistency with rejecting :authority header. This behavior can be temporarily reverted by setting `envoy.reloadable_features.treat_host_like_authority` to false. +* http: reverting a behavioral change where upstream connect timeouts were temporarily treated differently from other connection failures. The change back to the original behavior can be temporarily reverted by setting `envoy.reloadable_features.treat_upstream_connect_timeout_as_connect_failure` to false. +* upstream: fix handling of moving endpoints between priorities when active health checks are enabled. Previously moving to a higher numbered priority was a NOOP, and moving to a lower numbered priority caused an abort. Removed Config or Runtime ------------------------- *Normally occurs at the end of the* :ref:`deprecation period ` -* dispatcher: removed legacy socket read/write resumption code path and runtime guard `envoy.reloadable_features.activate_fds_next_event_loop`. -* ext_authz: removed auto ignore case in HTTP-based `ext_authz` header matching and the runtime guard `envoy.reloadable_features.ext_authz_http_service_enable_case_sensitive_string_matcher`. To ignore case, set the :ref:`ignore_case ` field to true. -* http: flip default HTTP/1 and HTTP/2 server codec implementations to new codecs that remove the use of exceptions for control flow. To revert to old codec behavior, set the runtime feature `envoy.reloadable_features.new_codec_behavior` to false. -* http: removed `envoy.reloadable_features.http1_flood_protection` and legacy code path for turning flood protection off. -* http: removed `envoy.reloadable_features.new_codec_behavior` and legacy codecs. +* access_logs: removed legacy unbounded access logs and runtime guard `envoy.reloadable_features.disallow_unbounded_access_logs`. +* dns: removed legacy buggy wildcard matching path and runtime guard `envoy.reloadable_features.fix_wildcard_matching`. +* dynamic_forward_proxy: removed `envoy.reloadable_features.enable_dns_cache_circuit_breakers` and legacy code path. +* http: removed legacy connection close behavior and runtime guard `envoy.reloadable_features.fixed_connection_close`. +* http: removed legacy HTTP/1.1 error reporting path and runtime guard `envoy.reloadable_features.early_errors_via_hcm`. +* http: removed legacy sanitization path for upgrade response headers and runtime guard `envoy.reloadable_features.fix_upgrade_response`. +* router: removed `envoy.reloadable_features.consume_all_retry_headers` and legacy code path. New Features ------------ -* compression: the :ref:`compressor ` filter adds support for compressing request payloads. Its configuration is unified with the :ref:`decompressor ` filter with two new fields for different directions - :ref:`requests ` and :ref:`responses `. The latter deprecates the old response-specific fields and, if used, roots the response-specific stats in `.compressor...response.*` instead of `.compressor...*`. -* config: added ability to flush stats when the admin's :ref:`/stats endpoint ` is hit instead of on a timer via :ref:`stats_flush_on_admin `. -* config: added new runtime feature `envoy.features.enable_all_deprecated_features` that allows the use of all deprecated features. -* crash support: added the ability to dump L4 connection data on crash. -* formatter: added new :ref:`text_format_source ` field to support format strings both inline and from a file. -* formatter: added support for custom date formatting to :ref:`%DOWNSTREAM_PEER_CERT_V_START% ` and :ref:`%DOWNSTREAM_PEER_CERT_V_END% `, similar to :ref:`%START_TIME% `. -* grpc: implemented header value syntax support when defining :ref:`initial metadata ` for gRPC-based `ext_authz` :ref:`HTTP ` and :ref:`network ` filters, and :ref:`ratelimit ` filters. -* grpc-json: added support for configuring :ref:`unescaping behavior ` for path components. -* hds: added support for delta updates in the :ref:`HealthCheckSpecifier `, making only the Endpoints and Health Checkers that changed be reconstructed on receiving a new message, rather than the entire HDS. -* health_check: added option to use :ref:`no_traffic_healthy_interval ` which allows a different no traffic interval when the host is healthy. -* http: added HCM :ref:`timeout config field ` to control how long a downstream has to finish sending headers before the stream is cancelled. -* http: added frame flood and abuse checks to the upstream HTTP/2 codec. This check is off by default and can be enabled by setting the `envoy.reloadable_features.upstream_http2_flood_checks` runtime key to true. -* http: added :ref:`stripping any port from host header ` support. -* http: clusters now support selecting HTTP/1 or HTTP/2 based on ALPN, configurable via :ref:`alpn_config ` in the :ref:`http_protocol_options ` message. -* jwt_authn: added support for :ref:`per-route config `. -* jwt_authn: changed config field :ref:`issuer ` to be optional to comply with JWT `RFC `_ requirements. -* kill_request: added new :ref:`HTTP kill request filter `. -* listener: added an optional :ref:`default filter chain `. If this field is supplied, and none of the :ref:`filter_chains ` matches, this default filter chain is used to serve the connection. -* listener: added back the :ref:`use_original_dst field `. -* listener: added the :ref:`Listener.bind_to_port field `. -* log: added a new custom flag ``%_`` to the log pattern to print the actual message to log, but with escaped newlines. -* lua: added `downstreamDirectRemoteAddress()` and `downstreamLocalAddress()` APIs to :ref:`streamInfo() `. -* mongo_proxy: the list of commands to produce metrics for is now :ref:`configurable `. -* network: added a :ref:`timeout ` for incoming connections completing transport-level negotiation, including TLS and ALTS hanshakes. -* overload: add :ref:`envoy.overload_actions.reduce_timeouts ` overload action to enable scaling timeouts down with load. Scaling support :ref:`is limited ` to the HTTP connection and stream idle timeouts. -* ratelimit: added support for use of various :ref:`metadata ` as a ratelimit action. -* ratelimit: added :ref:`disable_x_envoy_ratelimited_header ` option to disable `X-Envoy-RateLimited` header. -* ratelimit: added :ref:`body ` field to support custom response bodies for non-OK responses from the external ratelimit service. -* ratelimit: added :ref:`descriptor extensions `. -* ratelimit: added :ref:`computed descriptors `. -* ratelimit: added :ref:`dynamic_metadata ` field to support setting dynamic metadata from the ratelimit service. -* router: added support for regex rewrites during HTTP redirects using :ref:`regex_rewrite `. -* sds: improved support for atomic :ref:`key rotations ` and added configurable rotation triggers for - :ref:`TlsCertificate ` and - :ref:`CertificateValidationContext `. -* signal: added an extension point for custom actions to run on the thread that has encountered a fatal error. Actions are configurable via :ref:`fatal_actions `. -* start_tls: :ref:`transport socket` which starts in clear-text but may programatically be converted to use tls. -* tcp: added a new :ref:`envoy.overload_actions.reject_incoming_connections ` action to reject incoming TCP connections. -* thrift_proxy: added a new :ref: `payload_passthrough ` option to skip decoding body in the Thrift message. -* tls: added support for RSA certificates with 4096-bit keys in FIPS mode. -* tracing: added SkyWalking tracer. -* tracing: added support for setting the hostname used when sending spans to a Zipkin collector using the :ref:`collector_hostname ` field. -* xds: added support for resource TTLs. A TTL is specified on the :ref:`Resource `. For SotW, a :ref:`Resource ` can be embedded - in the list of resources to specify the TTL. +* access log: added the :ref:`formatters ` extension point for custom formatters (command operators). +* access log: support command operator: %REQUEST_HEADERS_BYTES%, %RESPONSE_HEADERS_BYTES% and %RESPONSE_TRAILERS_BYTES%. +* dispatcher: supports a stack of `Envoy::ScopeTrackedObject` instead of a single tracked object. This will allow Envoy to dump more debug information on crash. +* http: added support for :ref:`:ref:`preconnecting `. Preconnecting is off by default, but recommended for clusters serving latency-sensitive traffic, especially if using HTTP/1.1. +* http: change frame flood and abuse checks to the upstream HTTP/2 codec to ON by default. It can be disabled by setting the `envoy.reloadable_features.upstream_http2_flood_checks` runtime key to false. +* server: added :ref:`fips_mode ` statistic. +* tcp_proxy: add support for converting raw TCP streams into HTTP/1.1 CONNECT requests. See :ref:`upgrade documentation ` for details. Deprecated ---------- -* cluster: HTTP configuration for upstream clusters has beem reworked. HTTP-specific configuration is now done in the new :ref:`http_protocol_options ` message, configured via the cluster's :ref:`extension_protocol_options`. This replaces explicit HTTP configuration in cluster config, including :ref:`upstream_http_protocol_options` :ref:`common_http_protocol_options` :ref:`http_protocol_options` :ref:`http2_protocol_options` and :ref:`protocol_selection`. Examples of before-and-after configuration can be found in the :ref:`http_protocol_options docs ` and all of Envoy's example configurations have been updated to the new style of config. -* compression: the fields :ref:`content_length `, :ref:`content_type `, :ref:`disable_on_etag_header `, :ref:`remove_accept_encoding_header ` and :ref:`runtime_enabled ` of the :ref:`Compressor ` message have been deprecated in favor of :ref:`response_direction_config `. -* formatter: :ref:`text_format ` is now deprecated in favor of :ref:`text_format_source `. To migrate existing text format strings, use the :ref:`inline_string ` field. -* gzip: :ref:`HTTP Gzip filter ` is rejected now unless explicitly allowed with :ref:`runtime override ` `envoy.deprecated_features.allow_deprecated_gzip_http_filter` set to `true`. -* listener: :ref:`use_proxy_proto ` has been deprecated in favor of adding a :ref:`PROXY protocol listener filter ` explicitly. -* logging: the `--log-format-prefix-with-location` option is removed. -* ratelimit: the :ref:`dynamic metadata ` action is deprecated in favor of the more generic :ref:`metadata ` action. -* stats: the `--use-fake-symbol-table` option is removed. -* tracing: OpenCensus :ref:`Zipkin configuration ` is now deprecated, the preferred Zipkin export is via Envoy's :ref:`native Zipkin tracer `. diff --git a/docs/root/version_history/v1.11.0.rst b/docs/root/version_history/v1.11.0.rst index 1bc4051b7da4..10b48736b2db 100644 --- a/docs/root/version_history/v1.11.0.rst +++ b/docs/root/version_history/v1.11.0.rst @@ -80,7 +80,7 @@ Changes * runtime: :ref:`Runtime Discovery Service (RTDS) ` support added to layered runtime configuration. * sandbox: added :ref:`CSRF sandbox `. * server: ``--define manual_stamp=manual_stamp`` was added to allow server stamping outside of binary rules. - more info in the `bazel docs `_. + more info in the `bazel docs `_. * server: added :ref:`server state ` statistic. * server: added :ref:`initialization_time_ms` statistic. * subset: added :ref:`list_as_any` option to diff --git a/docs/root/version_history/v1.13.8.rst b/docs/root/version_history/v1.13.8.rst new file mode 100644 index 000000000000..d4d0e70fb18c --- /dev/null +++ b/docs/root/version_history/v1.13.8.rst @@ -0,0 +1,9 @@ +1.13.8 (January 15, 2021) +========================= + +Changes +------- + +* http: fixed URL parsing for HTTP/1.1 fully qualified URLs and connect requests containing IPv6 addresses. +* tls: fix detection of the upstream connection close event. + diff --git a/docs/root/version_history/v1.14.0.rst b/docs/root/version_history/v1.14.0.rst index 2db9566c6788..649a34d1cce3 100644 --- a/docs/root/version_history/v1.14.0.rst +++ b/docs/root/version_history/v1.14.0.rst @@ -187,6 +187,6 @@ Deprecated and the previous default can be enabled until the end of the deprecation period by enabling runtime feature `envoy.deprecated_features.grpc_stats_filter_enable_stats_for_all_methods_by_default`. * The :ref:`source_ip ` field in - `RBAC `_ has been deprecated + `RBAC `_ has been deprecated in favor of :ref:`direct_remote_ip ` and :ref:`remote_ip `. diff --git a/docs/root/version_history/v1.17.0.rst b/docs/root/version_history/v1.17.0.rst new file mode 100644 index 000000000000..dc4a84c02a2f --- /dev/null +++ b/docs/root/version_history/v1.17.0.rst @@ -0,0 +1,121 @@ +1.17.0 (January 11, 2021) +========================= + +Incompatible Behavior Changes +----------------------------- +*Changes that are expected to cause an incompatibility if applicable; deployment changes are likely required* + +* config: v2 is now fatal-by-default. This may be overridden by setting :option:`--bootstrap-version` 2 on the CLI for a v2 bootstrap file and also enabling the runtime `envoy.reloadable_features.enable_deprecated_v2_api` feature. See + the :ref:`FAQ entry ` for further details. + +Minor Behavior Changes +---------------------- +*Changes that may cause incompatibilities for some users, but should not for most* + +* build: the Alpine based debug images are no longer built in CI, use Ubuntu based images instead. +* decompressor: set the default value of window_bits of the decompressor to 15 to be able to decompress responses compressed by a compressor with any window size. +* expr filter: added `connection.termination_details` property support. +* formatter: the :ref:`text_format ` field no longer requires at least one byte, and may now be the empty string. It has also become :ref:`deprecated <1_17_deprecated>`. +* grpc_web filter: if a `grpc-accept-encoding` header is present it's passed as-is to the upstream and if it isn't `grpc-accept-encoding:identity` is sent instead. The header was always overwriten with `grpc-accept-encoding:identity,deflate,gzip` before. +* http: upstream protocol will now only be logged if an upstream stream was established. +* jwt_authn filter: added support of JWT time constraint verification with a clock skew (default to 60 seconds) and added a filter config field :ref:`clock_skew_seconds ` to configure it. +* listener: injection of the :ref:`TLS inspector ` has been disabled by default. This feature is controlled by the runtime guard `envoy.reloadable_features.disable_tls_inspector_injection`. +* lua: added `always_wrap_body` argument to `body()` API to always return a :ref:`buffer object ` even if the body is empty. +* memory: enabled new tcmalloc with restartable sequences for aarch64 builds. +* mongo proxy metrics: swapped network connection remote and local closed counters previously set reversed (`cx_destroy_local_with_active_rq` and `cx_destroy_remote_with_active_rq`). +* outlier detection: added :ref:`max_ejection_time ` to limit ejection time growth when a node stays unhealthy for extended period of time. By default :ref:`max_ejection_time ` limits ejection time to 5 minutes. Additionally, when the node stays healthy, ejection time decreases. See :ref:`ejection algorithm` for more info. Previously, ejection time could grow without limit and never decreased. +* performance: improved performance when handling large HTTP/1 bodies. +* tcp_proxy: now waits for HTTP tunnel to be established before start streaming the downstream data, the runtime guard `envoy.reloadable_features.http_upstream_wait_connect_response` can be set to "false" to disable this behavior. +* tls: removed RSA key transport and SHA-1 cipher suites from the client-side defaults. +* watchdog: the watchdog action :ref:`abort_action ` is now the default action to terminate the process if watchdog kill / multikill is enabled. +* xds: to support TTLs, heartbeating has been added to xDS. As a result, responses that contain empty resources without updating the version will no longer be propagated to the + subscribers. To undo this for VHDS (which is the only subscriber that wants empty resources), the `envoy.reloadable_features.vhds_heartbeats` can be set to "false". + +Bug Fixes +--------- +*Changes expected to improve the state of the world and are unlikely to have negative effects* + +* config: validate that upgrade configs have a non-empty :ref:`upgrade_type `, fixing a bug where an errant "-" could result in unexpected behavior. +* dns: fixed a bug where custom resolvers provided in configuration were not preserved after network issues. +* dns_filter: correctly associate DNS response IDs when multiple queries are received. +* grpc mux: fixed sending node again after stream is reset when :ref:`set_node_on_first_message_only ` is set. +* http: fixed URL parsing for HTTP/1.1 fully qualified URLs and connect requests containing IPv6 addresses. +* http: reject requests with missing required headers after filter chain processing. +* http: sending CONNECT_ERROR for HTTP/2 where appropriate during CONNECT requests. +* proxy_proto: fixed a bug where the wrong downstream address got sent to upstream connections. +* proxy_proto: fixed a bug where network filters would not have the correct downstreamRemoteAddress() when accessed from the StreamInfo. This could result in incorrect enforcement of RBAC rules in the RBAC network filter (but not in the RBAC HTTP filter), or incorrect access log addresses from tcp_proxy. +* sds: fixed a bug that clusters sharing same sds target are marked active immediately. +* tls: fixed detection of the upstream connection close event. +* tls: fixed read resumption after triggering buffer high-watermark and all remaining request/response bytes are stored in the SSL connection's internal buffers. +* udp: fixed issue in which receiving truncated UDP datagrams would cause Envoy to crash. +* watchdog: touch the watchdog before most event loop operations to avoid misses when handling bursts of callbacks. + +Removed Config or Runtime +------------------------- +*Normally occurs at the end of the* :ref:`deprecation period ` + +* dispatcher: removed legacy socket read/write resumption code path and runtime guard `envoy.reloadable_features.activate_fds_next_event_loop`. +* ext_authz: removed auto ignore case in HTTP-based `ext_authz` header matching and the runtime guard `envoy.reloadable_features.ext_authz_http_service_enable_case_sensitive_string_matcher`. To ignore case, set the :ref:`ignore_case ` field to true. +* ext_authz: the deprecated field `use_alpha` is no longer supported and cannot be set anymore. +* http: removed `envoy.reloadable_features.http1_flood_protection` and legacy code path for turning flood protection off. +* http: removed `envoy.reloadable_features.new_codec_behavior` and legacy codecs. + +New Features +------------ +* compression: the :ref:`compressor ` filter added support for compressing request payloads. Its configuration is unified with the :ref:`decompressor ` filter with two new fields for different directions - :ref:`requests ` and :ref:`responses `. The latter deprecates the old response-specific fields and, if used, roots the response-specific stats in `.compressor...response.*` instead of `.compressor...*`. +* config: added ability to flush stats when the admin's :ref:`/stats endpoint ` is hit instead of on a timer via :ref:`stats_flush_on_admin `. +* config: added new runtime feature `envoy.features.enable_all_deprecated_features` that allows the use of all deprecated features. +* crash support: added the ability to dump L4 connection data on crash. +* formatter: added new :ref:`text_format_source ` field to support format strings both inline and from a file. +* formatter: added support for custom date formatting to :ref:`%DOWNSTREAM_PEER_CERT_V_START% ` and :ref:`%DOWNSTREAM_PEER_CERT_V_END% `, similar to :ref:`%START_TIME% `. +* grpc: implemented header value syntax support when defining :ref:`initial metadata ` for gRPC-based `ext_authz` :ref:`HTTP ` and :ref:`network ` filters, and :ref:`ratelimit ` filters. +* grpc-json: added support for configuring :ref:`unescaping behavior ` for path components. +* hds: added support for delta updates in the :ref:`HealthCheckSpecifier `, making only the Endpoints and Health Checkers that changed be reconstructed on receiving a new message, rather than the entire HDS. +* health_check: added option to use :ref:`no_traffic_healthy_interval ` which allows a different no traffic interval when the host is healthy. +* http: added HCM :ref:`request_headers_timeout config field ` to control how long a downstream has to finish sending headers before the stream is cancelled. +* http: added frame flood and abuse checks to the upstream HTTP/2 codec. This check is off by default and can be enabled by setting the `envoy.reloadable_features.upstream_http2_flood_checks` runtime key to true. +* http: added :ref:`stripping any port from host header ` support. +* http: clusters added support for selecting HTTP/1 or HTTP/2 based on ALPN, configurable via :ref:`alpn_config ` in the :ref:`http_protocol_options ` message. +* jwt_authn: added support for :ref:`per-route config `. +* jwt_authn: changed config field :ref:`issuer ` to be optional to comply with JWT `RFC `_ requirements. +* kill_request: added new :ref:`HTTP kill request filter `. +* listener: added an optional :ref:`default filter chain `. If this field is supplied, and none of the :ref:`filter_chains ` matches, this default filter chain is used to serve the connection. +* listener: added back the :ref:`use_original_dst field `. +* listener: added the :ref:`Listener.bind_to_port field `. +* log: added a new custom flag ``%_`` to the log pattern to print the actual message to log, but with escaped newlines. +* lua: added `downstreamDirectRemoteAddress()` and `downstreamLocalAddress()` APIs to :ref:`streamInfo() `. +* mongo_proxy: the list of commands to produce metrics for is now :ref:`configurable `. +* network: added a :ref:`transport_socket_connect_timeout config field ` for incoming connections completing transport-level negotiation, including TLS and ALTS hanshakes. +* overload: added :ref:`envoy.overload_actions.reduce_timeouts ` overload action to enable scaling timeouts down with load. Scaling support :ref:`is limited ` to the HTTP connection and stream idle timeouts. +* ratelimit: added support for use of various :ref:`metadata ` as a ratelimit action. +* ratelimit: added :ref:`disable_x_envoy_ratelimited_header ` option to disable `X-Envoy-RateLimited` header. +* ratelimit: added :ref:`body ` field to support custom response bodies for non-OK responses from the external ratelimit service. +* ratelimit: added :ref:`descriptor extensions `. +* ratelimit: added :ref:`computed descriptors `. +* ratelimit: added :ref:`dynamic_metadata ` field to support setting dynamic metadata from the ratelimit service. +* router: added support for regex rewrites during HTTP redirects using :ref:`regex_rewrite `. +* sds: improved support for atomic :ref:`key rotations ` and added configurable rotation triggers for + :ref:`TlsCertificate ` and + :ref:`CertificateValidationContext `. +* signal: added an extension point for custom actions to run on the thread that has encountered a fatal error. Actions are configurable via :ref:`fatal_actions `. +* start_tls: added new :ref:`transport socket` which starts in clear-text but may programatically be converted to use tls. +* tcp: added a new :ref:`envoy.overload_actions.reject_incoming_connections ` action to reject incoming TCP connections. +* thrift_proxy: added a new :ref:`payload_passthrough ` option to skip decoding body in the Thrift message. +* tls: added support for RSA certificates with 4096-bit keys in FIPS mode. +* tracing: added :ref:`SkyWalking tracer `. +* tracing: added support for setting the hostname used when sending spans to a Zipkin collector using the :ref:`collector_hostname ` field. +* xds: added support for resource TTLs. A TTL is specified on the :ref:`Resource `. For SotW, a :ref:`Resource ` can be embedded in the list of resources to specify the TTL. + +.. _1_17_deprecated: + +Deprecated +---------- +* cluster: HTTP configuration for upstream clusters has been reworked. HTTP-specific configuration is now done in the new :ref:`http_protocol_options ` message, configured via the cluster's :ref:`extension_protocol_options`. This replaces explicit HTTP configuration in cluster config, including :ref:`upstream_http_protocol_options` :ref:`common_http_protocol_options` :ref:`http_protocol_options` :ref:`http2_protocol_options` and :ref:`protocol_selection`. Examples of before-and-after configuration can be found in the :ref:`http_protocol_options docs ` and all of Envoy's example configurations have been updated to the new style of config. +* compression: the fields :ref:`content_length `, :ref:`content_type `, :ref:`disable_on_etag_header `, :ref:`remove_accept_encoding_header ` and :ref:`runtime_enabled ` of the :ref:`Compressor ` message have been deprecated in favor of :ref:`response_direction_config `. +* formatter: :ref:`text_format ` is now deprecated in favor of :ref:`text_format_source `. To migrate existing text format strings, use the :ref:`inline_string ` field. +* gzip: :ref:`HTTP Gzip filter ` is rejected now unless explicitly allowed with :ref:`runtime override ` `envoy.deprecated_features.allow_deprecated_gzip_http_filter` set to `true`. Use the :ref:`compressor filter `. +* listener: :ref:`use_proxy_proto ` has been deprecated in favor of adding a :ref:`PROXY protocol listener filter ` explicitly. +* logging: the `--log-format-prefix-with-location` option is removed. +* ratelimit: the :ref:`dynamic metadata ` action is deprecated in favor of the more generic :ref:`metadata ` action. +* stats: the `--use-fake-symbol-table` option is removed. +* tracing: OpenCensus :ref:`Zipkin configuration ` is now deprecated, the preferred Zipkin export is via Envoy's :ref:`native Zipkin tracer `. diff --git a/docs/root/version_history/v1.4.0.rst b/docs/root/version_history/v1.4.0.rst index f940deb1b5a6..fb8105538627 100644 --- a/docs/root/version_history/v1.4.0.rst +++ b/docs/root/version_history/v1.4.0.rst @@ -56,7 +56,7 @@ Deprecated * The following log macros have been deprecated: `log_trace`, `log_debug`, `conn_log`, `conn_log_info`, `conn_log_debug`, `conn_log_trace`, `stream_log`, `stream_log_info`, `stream_log_debug`, `stream_log_trace`. For replacements, please see - `logger.h `_. + `logger.h `_. * The connectionId() and ssl() callbacks of StreamFilterCallbacks have been deprecated and replaced with a more general connection() callback, which, when not returning a nullptr, can be used to get the connection id and SSL connection from the returned Connection object pointer. diff --git a/docs/root/version_history/v1.8.0.rst b/docs/root/version_history/v1.8.0.rst index 5f05f5d7d8eb..e99c0e2c459c 100644 --- a/docs/root/version_history/v1.8.0.rst +++ b/docs/root/version_history/v1.8.0.rst @@ -106,24 +106,24 @@ Deprecated * Use of the legacy `ratelimit.proto `_ is deprecated, in favor of the proto defined in - `date-plane-api `_ + `date-plane-api `_ Prior to 1.8.0, Envoy can use either proto to send client requests to a ratelimit server with the use of the - `use_data_plane_proto` boolean flag in the `ratelimit configuration `_. + `use_data_plane_proto` boolean flag in the `ratelimit configuration `_. However, when using the deprecated client a warning is logged. * Use of the --v2-config-only flag. * Use of both `use_websocket` and `websocket_config` in - `route.proto `_ + `route.proto `_ is deprecated. Please use the new `upgrade_configs` in the - `HttpConnectionManager `_ + `HttpConnectionManager `_ instead. -* Use of the integer `percent` field in `FaultDelay `_ - and in `FaultAbort `_ is deprecated in favor +* Use of the integer `percent` field in `FaultDelay `_ + and in `FaultAbort `_ is deprecated in favor of the new `FractionalPercent` based `percentage` field. * Setting hosts via `hosts` field in `Cluster` is deprecated. Use `load_assignment` instead. * Use of `response_headers_to_*` and `request_headers_to_add` are deprecated at the `RouteAction` level. Please use the configuration options at the `Route` level. * Use of `runtime` in `RouteMatch`, found in - `route.proto `_. + `route.proto `_. Set the `runtime_fraction` field instead. -* Use of the string `user` field in `Authenticated` in `rbac.proto `_ +* Use of the string `user` field in `Authenticated` in `rbac.proto `_ is deprecated in favor of the new `StringMatcher` based `principal_name` field. diff --git a/docs/root/version_history/v1.9.0.rst b/docs/root/version_history/v1.9.0.rst index 54cd1e478336..12614ca1497b 100644 --- a/docs/root/version_history/v1.9.0.rst +++ b/docs/root/version_history/v1.9.0.rst @@ -102,12 +102,12 @@ Changes Deprecated ---------- -* Order of execution of the network write filter chain has been reversed. Prior to this release cycle it was incorrect, see `#4599 `_. In the 1.9.0 release cycle we introduced `bugfix_reverse_write_filter_order` in `lds.proto `_ to temporarily support both old and new behaviors. Note this boolean field is deprecated. -* Order of execution of the HTTP encoder filter chain has been reversed. Prior to this release cycle it was incorrect, see `#4599 `_. In the 1.9.0 release cycle we introduced `bugfix_reverse_encode_order` in `http_connection_manager.proto `_ to temporarily support both old and new behaviors. Note this boolean field is deprecated. +* Order of execution of the network write filter chain has been reversed. Prior to this release cycle it was incorrect, see `#4599 `_. In the 1.9.0 release cycle we introduced `bugfix_reverse_write_filter_order` in `lds.proto `_ to temporarily support both old and new behaviors. Note this boolean field is deprecated. +* Order of execution of the HTTP encoder filter chain has been reversed. Prior to this release cycle it was incorrect, see `#4599 `_. In the 1.9.0 release cycle we introduced `bugfix_reverse_encode_order` in `http_connection_manager.proto `_ to temporarily support both old and new behaviors. Note this boolean field is deprecated. * Use of the v1 REST_LEGACY ApiConfigSource is deprecated. * Use of std::hash in the ring hash load balancer is deprecated. -* Use of `rate_limit_service` configuration in the `bootstrap configuration `_ is deprecated. +* Use of `rate_limit_service` configuration in the `bootstrap configuration `_ is deprecated. * Use of `runtime_key` in `RequestMirrorPolicy`, found in - `route.proto `_ + `route.proto `_ is deprecated. Set the `runtime_fraction` field instead. -* Use of buffer filter `max_request_time` is deprecated in favor of the request timeout found in `HttpConnectionManager `_ +* Use of buffer filter `max_request_time` is deprecated in favor of the request timeout found in `HttpConnectionManager `_ diff --git a/docs/root/version_history/version_history.rst b/docs/root/version_history/version_history.rst index b25ef731208a..05c5ffd15568 100644 --- a/docs/root/version_history/version_history.rst +++ b/docs/root/version_history/version_history.rst @@ -7,6 +7,7 @@ Version history :titlesonly: current + v1.17.0 v1.16.2 v1.16.1 v1.16.0 @@ -21,6 +22,7 @@ Version history v1.14.2 v1.14.1 v1.14.0 + v1.13.8 v1.13.7 v1.13.6 v1.13.5 @@ -59,7 +61,7 @@ Deprecation Policy ^^^^^^^^^^^^^^^^^^ As of release 1.3.0, Envoy will follow a -`Breaking Change Policy `_. +`Breaking Change Policy `_. Features in the deprecated list for each version have been DEPRECATED and will be removed in the specified release cycle. A logged warning diff --git a/examples/BUILD b/examples/BUILD index 59d945583b06..47e7740bc91c 100644 --- a/examples/BUILD +++ b/examples/BUILD @@ -15,6 +15,7 @@ filegroup( [ "**/*.yaml", "**/*.lua", + "_extra_certs/*.pem", ], exclude = [ "cache/responses.yaml", diff --git a/examples/_extra_certs/README.md b/examples/_extra_certs/README.md new file mode 100644 index 000000000000..3e4b69997626 --- /dev/null +++ b/examples/_extra_certs/README.md @@ -0,0 +1,7 @@ +Extra certificates for config validation testing +================================================ + +This folder contains certs that are referenced in the sandbox examples, that end users are +expected to create themselves. + +In order to test the related configs we need to provide the certs to CI. diff --git a/examples/_extra_certs/domain1.crt.pem b/examples/_extra_certs/domain1.crt.pem new file mode 100644 index 000000000000..33be12e8b02c --- /dev/null +++ b/examples/_extra_certs/domain1.crt.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhDCCAmygAwIBAgITAJlvbEs3wtayr3rx+TyuBtu0mTANBgkqhkiG9w0BAQsF +ADBSMQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExGDAWBgNVBAoMD015RXhhbXBs +ZSwgSW5jLjEcMBoGA1UEAwwTZG9tYWluMS5leGFtcGxlLmNvbTAeFw0yMDExMTIx +MTA3MDdaFw0yMTExMTIxMTA3MDdaMFIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD +QTEYMBYGA1UECgwPTXlFeGFtcGxlLCBJbmMuMRwwGgYDVQQDDBNkb21haW4xLmV4 +YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA15Q63skf +pc5o2mhBE0dOcJaTqLS+nmIO5jK8QUKctpbOQz2p7j9zi9ZUh++4N84yjF56GQEw +/KqPvPHNA/tJKpkDugWHq4IFPU+o1k2AJKLVEvN3wXpbiae77eqgUCg0aS6kWDaT +LrCie/laxnSpnfRGDo1xsLRqNLzZxF3CPvA/WbgpR1JXYUAnoXZGHISrnXLzyI1O +DaDdDoi8Nn54neZ9jXtkeDWfuO5NkXK/U1dNnCez9a7EGO+h8ZF0Uc12UqPiX86L +frK0v25n94lPTGq5SOgswATMSOfN6g4pGaUFofZIyenHamUngzqm55M+/tMeiaF7 +Pwf4wcTyXEaXcQIDAQABo1MwUTAdBgNVHQ4EFgQUTSuIMFANakAWSPIUiqdMUrFq +66YwHwYDVR0jBBgwFoAUTSuIMFANakAWSPIUiqdMUrFq66YwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAdbdIBKAomEsGtcuWWc8vI4r0l+AMegGK +yg86byKm9WRHtpYnO+iZ+SopTLTFhgLsGfEMoN+HGeUIexUvwDzb384EJ4kLPr3E +Yqt5uNz9YMuFpkhuFTL+V5RczcPKfir5hzAgvAtj6eaRf9WPlObF+Rr0t8pJZG0k +9dEtBqE87XVUDvj6waMCpTFxwv22E/xjRJ5nSDjfk9y8LDpIF5SOunncVMRVfcjg +Qp0Q9KpZpbxXFMYVBfMxp4Z/KQd0W5nVWZlwg/D03n0IkS0e8irUyrerFLdOTwxf +G5M/n/VeCwC2GPlT8Eo/3BUa+SeX2iHl93/osqfWNQAY3riaN0y+FA== +-----END CERTIFICATE----- diff --git a/examples/_extra_certs/domain1.key.pem b/examples/_extra_certs/domain1.key.pem new file mode 100644 index 000000000000..eefcca26453f --- /dev/null +++ b/examples/_extra_certs/domain1.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDXlDreyR+lzmja +aEETR05wlpOotL6eYg7mMrxBQpy2ls5DPanuP3OL1lSH77g3zjKMXnoZATD8qo+8 +8c0D+0kqmQO6BYerggU9T6jWTYAkotUS83fBeluJp7vt6qBQKDRpLqRYNpMusKJ7 ++VrGdKmd9EYOjXGwtGo0vNnEXcI+8D9ZuClHUldhQCehdkYchKudcvPIjU4NoN0O +iLw2fnid5n2Ne2R4NZ+47k2Rcr9TV02cJ7P1rsQY76HxkXRRzXZSo+Jfzot+srS/ +bmf3iU9MarlI6CzABMxI583qDikZpQWh9kjJ6cdqZSeDOqbnkz7+0x6JoXs/B/jB +xPJcRpdxAgMBAAECggEBALvIi+tKaH3mqaEuVRk08NfT4jV/k9ek9POCWEfEfXvz +KyKZUS+OJ9k0TpfGscIypAdvuI2VYxWEgQaF3h7MwfQQK5XbgU1dSbEZdamPAsNm +75G9cKChM6FZ8bKRwSlxjA3fKhsJFvYBuNei4naiYqmLgYbloJXa4fSkWFDblvt4 +cmsP9iEZL7tBJ4bIGmugpPR83PPlfu/EQY2w8T+Rw8/JAXDd80V1egCucYpwOx94 +esXVpWzTA3xZyPTlQrFmOe9NEb2C5oqOx1s/zmfQpytKPjF7YojnHnYeHrDL5Y+j +sVP753celaYncWoANfAyV4FOxEsOa1OCKbF5OOuWPI0CgYEA/KyTK4NyRHqXrhuO +J/rRDhhZBomP4LkY2uTMdOH+n0hvy0m5eV5+88CQF6atSfKQ8h+zbhJVNeVilZDK +NhjAEm+x2vME41Wp1vqsALpPtuGFQm3EcwKDvTgyvm04X2RfZSZ4MT0993M/g07u +x+VQiZu127PjcNibDXXoDwM5p/MCgYEA2mqoRnTr/DCrO8u678ccv8MYHhh0ISOs +Tbmh83qROWehdoB6kRQ/i+kefbL6Rw0bY5+3rlvQ+3B3MvVoYLWUWuyhtYK2pt3i +R071WPCuR3PIVOEK+wuHi85peiGSHxfEDiUb3AvNnd8dZGBFnHe8mZObccc7b4uy +jT4VLJ56IAsCgYEAlg3GuKivS4uiWHt0yLljPYOoGwHGuCY0ZIpMAX3UwLM78PYv +d6xuqENLT0Bk2O18ts2suUmZ4RAAo+IAtG+uYUSD0wtPc9KDsm/bhfMfM/RqNzEI +4WQ06EJfoEcsmzn4jRFzf4pnKnT+2vQdSgkc8xvNvFPwVivMqQnEbmXz75ECgYBr +BTfOzhuTRoWglwLR2k5L59w5YuIEGuaibwLbuoLODekfl3R3AeThOSinjrrzdYim +F+x4kqSjj0fYwEaUnGRE6Q2TUqkMukvVhOrS2ZuLhz/x1xL6T3vrFQi5vxlKAusd +wzETcPUfFePg+wsgz8qptZnE9ko5LcofSvw1ELHmYQKBgBfT1GtRlYCEMbuSJY20 +AtoOg5vN2b6s2nqQGff7J8UOywPDk9hyboL4ByS9Udemap0USisGAZiEfq+VbA+2 +lPhV/gmBFDidCCYRXKi6qfcDG9ssJ5Gylg/8XaaMKAQ7vp73sQYTDlcgUcPx90ue +GMITMZWQr8Qs/u9zl22tnxAb +-----END PRIVATE KEY----- diff --git a/examples/_extra_certs/domain2.crt.pem b/examples/_extra_certs/domain2.crt.pem new file mode 100644 index 000000000000..9f983682ae6b --- /dev/null +++ b/examples/_extra_certs/domain2.crt.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhTCCAm2gAwIBAgIUXBZV/SLVGAdsb1mJrpuahYegPLEwDQYJKoZIhvcNAQEL +BQAwUjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRgwFgYDVQQKDA9NeUV4YW1w +bGUsIEluYy4xHDAaBgNVBAMME2RvbWFpbjIuZXhhbXBsZS5jb20wHhcNMjAxMTEy +MTEwNzA3WhcNMjExMTEyMTEwNzA3WjBSMQswCQYDVQQGEwJVUzELMAkGA1UECAwC +Q0ExGDAWBgNVBAoMD015RXhhbXBsZSwgSW5jLjEcMBoGA1UEAwwTZG9tYWluMi5l +eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAJfPje1r +nUfVnHobHRKReisGdGWHQhngJlp3II7sPk0vRuvOLsyLp0AKA/pS0isW1WFUh+Kz +BpOVrU/LPqi1ZW38mzCgGr91fc999vtsZyXOCEh7Vi3UVfzVtSNwnijlG3wovjGK +DqLlne03/lPFT1x9coIy7XvA1ZICfX8EfP5ajt60+UWYXAKN2FLas9K+3lzzbHx5 +F/iI134A695ozLNHT1qe+IOA6NwW5LoTwzoHRVMoJz2cvSRr0vCVkt0IvO5ARyyr +400Nx0vKkxhf0Z+yXGSowWVN8VtSPiRSeC4vGmPRl6O6XoiPwjus2jlXrJifcIyg +hNDrOQnYbYK5dA8CAwEAAaNTMFEwHQYDVR0OBBYEFIQfS5xxWYX7pWgc59p+h6y8 +sQMFMB8GA1UdIwQYMBaAFIQfS5xxWYX7pWgc59p+h6y8sQMFMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBAIHDjt+wxYuOJkI8VncVR6VpGy9hqntd +rnNxupReenhocPN/QIl1TQva/gGq4gz1vNWhHz1B5bxPoyPESed5+QQvJMo3/5Ub +OyDIKwspwRy6PUoyJDjhC/z7B2FhZPmxVmbHfhL0wiQjI7j/u+/c8Jq9YDr8ZsZs +whXjvSOl9+I0xWZFRN0O+cFszTnoucmLRdFVl648ghUlW3m6/YNWF+mLucleZVt3 +wFUKGwq88Z4sU6kqcXXG4GykZYmSwB3BmaaamQKq06v+k9Qjrj5gJD8S1Ygznc6/ +Z+ZzAb/FfHXHV6QbY5/35wVFO3OMk6NHy9oLZrfPxBn/C5brUz+dXnI= +-----END CERTIFICATE----- diff --git a/examples/_extra_certs/domain2.key.pem b/examples/_extra_certs/domain2.key.pem new file mode 100644 index 000000000000..12602841945f --- /dev/null +++ b/examples/_extra_certs/domain2.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCXz43ta51H1Zx6 +Gx0SkXorBnRlh0IZ4CZadyCO7D5NL0brzi7Mi6dACgP6UtIrFtVhVIfiswaTla1P +yz6otWVt/JswoBq/dX3Pffb7bGclzghIe1Yt1FX81bUjcJ4o5Rt8KL4xig6i5Z3t +N/5TxU9cfXKCMu17wNWSAn1/BHz+Wo7etPlFmFwCjdhS2rPSvt5c82x8eRf4iNd+ +AOveaMyzR09anviDgOjcFuS6E8M6B0VTKCc9nL0ka9LwlZLdCLzuQEcsq+NNDcdL +ypMYX9GfslxkqMFlTfFbUj4kUnguLxpj0Zejul6Ij8I7rNo5V6yYn3CMoITQ6zkJ +2G2CuXQPAgMBAAECggEAJ089Rv8YqOMtM4kVzBsTcVSoiyms+hpKlB5ItfmCYGYf +jSvEfn6i/jgZs5YCidnNwvgqf48v4sNdL05HmVPvQb2pSbwLcQwxWasaaxw00Vs6 +VdpqBE/5PBDyaIzex2Qb69h490byZ0fhzu0y0+pBlId/QSuCxwq1wqsWZ+93ljzi +rDDoPSWcty0R3QWrMUmbihi6i8v8fbFz174jxGH1TRJ0+a9IuGZ9Yap1mSg9YC/0 +oM20lsvmQvczXYdJMUhg0CYJ3weThO9pK4fnDa7pvkBgxgOTOY2TZh3PUs7DmYa7 +YCE5xp8CviKZywaAqvowjfdWj8yCU5ZFzN4LdDmhMQKBgQDHL/dUg61+viWR7GyD +Kb9E9FjktwSu52Ec1jfcjFe8UkRJa7JiruagVXuekAVvA+R7ZifSb0Yfs1QaHwA3 +NPvVpwb3omW02gfbXR9AJ/eEJfgkcliPrJsL0QqbLu+w+5CFaj/iCN5SxE822WLx +3dGNDOrQEr0A+K3Hmj5SCtCmbQKBgQDDHEwxzjq6jw2n+K0ArIvpXa1/hteT6h1q +7Qcg9nEaSiVbYAfp+1qgQqoCMe7aSNJe/RuGP59mwlIgt1rn+QFgb8K9IoZYvb4N +jwLmuOx1tLWtbRLHHFdYA14XlwKOl79NwjJPepCZIU8eOzaDLBUUeH0DkgJgwSZ2 +TnJOmp6m6wKBgHL0tNpa0INoPAiWmR2tt0yVdMQy+An1UW+yFjU77dqq4+w3spEP +fdyk2R5u4iPq7C9niq4BOEhNV8lngNlbw8fPiM7cM7SHbKdmfAWry0bCHw7xyzjI +Fgdg0q0zDnRnC0ZkRpAuLBk6YLk4BsmuCiVMgiwp1Fi+LJUY6MSypy6VAoGAUFun +RhwaNBwXE8dn+Y8XUNY0TwHKaDFUTGWzOfBGRP2kxS2YFNZhTQAn5R+LsHutqVG1 +tGUf0cLW8IKT/lagKofdPOirTIFZdVwhZcVkHlZ/PR5fTYJutuEsL6sScogtUmlZ +L0LbqzX80AazPPM6+2NkmcPZFuB2ZuOIULd+AGECgYBt40PiCf1WmAQK9J3pylOD +s7kWwzapIAKf92JBJmo5sOushYsPXAnqUJaZERpBBCmsdtYyjQV8VYxg/CrU5CkE +0zdFmFcfw7swwaE+aJLwueV1qR7lKi89DYZ9OFI+Z0JoekQc/TPeJLg1MctK76qA +OMswum0oXiu/zZJGGtCegg== +-----END PRIVATE KEY----- diff --git a/examples/_extra_certs/domain3.crt.pem b/examples/_extra_certs/domain3.crt.pem new file mode 100644 index 000000000000..fdfbd07e9321 --- /dev/null +++ b/examples/_extra_certs/domain3.crt.pem @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDhTCCAm2gAwIBAgIUI/iTc/yKhX+HkiDCKAuCa25fxQwwDQYJKoZIhvcNAQEL +BQAwUjELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRgwFgYDVQQKDA9NeUV4YW1w +bGUsIEluYy4xHDAaBgNVBAMME2RvbWFpbjMuZXhhbXBsZS5jb20wHhcNMjAxMTEy +MTEwNzA3WhcNMjExMTEyMTEwNzA3WjBSMQswCQYDVQQGEwJVUzELMAkGA1UECAwC +Q0ExGDAWBgNVBAoMD015RXhhbXBsZSwgSW5jLjEcMBoGA1UEAwwTZG9tYWluMy5l +eGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMOO0k18 +6k3164UpYGVOC1oj8jbmdZeeg57mkamYE9CUk0W9KKgHOCSNMZXUxPtnqtqJgJ92 +ccawTe0WrbOKQA3ARK7WePbX3HepJfTFZgCC5d5njRPDfIgsk4MP89nc2p8qO5Vv +SvCTae8/ykTyfz5fKwaGMYwvrSGUpuhFD8OFMOAsvnoIXZd+ixLkDATupP/IaV0a +6tHs6BnC3vxn+baC30fuHKErfOh7Jlo3FDqXNMwfes6MJ7/u8odeFfuGOaaO4eRT +EAhy4VQBJkCtS3yCFEv3kCRXjmgEBDSQ9jDjtnykOqViO5euibeKxnz+7xjRWVGY +bT5Z+s6eUHkiraMCAwEAAaNTMFEwHQYDVR0OBBYEFDVYD/F+NzsKgfYsRM6XfMuB +qXmqMB8GA1UdIwQYMBaAFDVYD/F+NzsKgfYsRM6XfMuBqXmqMA8GA1UdEwEB/wQF +MAMBAf8wDQYJKoZIhvcNAQELBQADggEBABwiF5AVDmQTLYE4wuHxq245qOj/vKfi +1L2lNgZ7G2Luobbvli2SQo7g8UYMSrwNF3Y1TDoEryeYMKYr2udb8WvdzhlL3z/J +a/qBElwWsATnpRfBAqxeWkx0x0E0C4nrjXM7PbAEjvEZ2AQKc5zmvii1Ek4h/+Sa +h2+Tmm5zg0Lo410CqujRmGtHU2AtkqguOhNrvJcRxEH4iLDB87WfUlLW6JrN+CLB +qIxkyLlhMUNMa200mpsfwQQRdImTjdn+VgpFR9BeZYU2gPZxqdxKcyrGfYXim1oJ +dC34TKistMWFs0C3l+Xs7unqSkqk5s1Nkdh6vnMF39PkwFoVP3Nn2wY= +-----END CERTIFICATE----- diff --git a/examples/_extra_certs/domain3.key.pem b/examples/_extra_certs/domain3.key.pem new file mode 100644 index 000000000000..84f259bd0a8a --- /dev/null +++ b/examples/_extra_certs/domain3.key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDDjtJNfOpN9euF +KWBlTgtaI/I25nWXnoOe5pGpmBPQlJNFvSioBzgkjTGV1MT7Z6raiYCfdnHGsE3t +Fq2zikANwESu1nj219x3qSX0xWYAguXeZ40Tw3yILJODD/PZ3NqfKjuVb0rwk2nv +P8pE8n8+XysGhjGML60hlKboRQ/DhTDgLL56CF2XfosS5AwE7qT/yGldGurR7OgZ +wt78Z/m2gt9H7hyhK3zoeyZaNxQ6lzTMH3rOjCe/7vKHXhX7hjmmjuHkUxAIcuFU +ASZArUt8ghRL95AkV45oBAQ0kPYw47Z8pDqlYjuXrom3isZ8/u8Y0VlRmG0+WfrO +nlB5Iq2jAgMBAAECggEBALe2MUzIP9kTYLlNIJiq07FPuZjnsarJKD8bvdWD34GA +QkYuqMYJWi3EUsO+CXtgbTo2GJY1kDcmo15Kgs3636fLavqQ0zyZlyz2w4iJ9QQf +9FCWGQtrB09qCP4D+4I8n0kNRMJithUBd3BiDePtp6nxf5r2cA+RLmUwoAft8Rws +GoZF5vm93iHF29H/nbP7KNTyBAoRq95y+SiXvcLOb9pN5IwAiqkAcVF8YpUCqsQL +PRTJZupSRU3Cg0Asq9nvqxeTBrySeOkoRid9b/5/MKP2CXNkGoOl7TCiHJ1SFYCd +waN+516Nd6Hu8WpH7n3eqcc/JhmP7jLv3vqLYAWpJ/ECgYEA9XigdnEiGFuP/SAE +l8InIGfNxGegEb9AMifZdir4lI1BKC+Ke84TD0V5tgHS5zsk8SOAIERHrclOTtHR +WPEx+GDrsukl3QDisdFE6sU1ktZ9S0uim9hYGaAtxME99U3EhDTBLApNyhxe2REC +yZoCuObqz7OVCu+SZ6/etw5mk+kCgYEAy/IgXJxCD3ais1Jy0DOfrzSExXulOi4X ++EWtMNcBkcYxE0Jl1mpsgd7GZNnJCf8ThajetREPFYzMCYiI6KOtOJBi+Bil6hpI +N2U29LD/dxIotHzdIau4ESFODJdgP2agzFx72JDKEBxGsuc7x5bd7P8hex+XURTW +KVuawJPXOasCgYEAoYXCcK149fYqBTGwU/vZqyUi7P4TAhqKr3YxTeRwta9NFJhT +06uCNyZMNEt279inMlVd1d2YHO69rHe7/X6YlwuPjKaF16rhgIhnhORHoFurDoSy +d0IglpwkAbf2gRevHB9qjQQqs7d/Ye4jm2zQJcMs94b/p7aE6915+5JqRSECgYAo +uC4n73btGXXAsfyEf1oppCXCPD6wEBXvFxJORw9kKJsRylcE6XjCsVURO759BXXD +YQUeR8qoNdVjLeSP9mYWfhWUjW9K/3ZdwRKo5lILVw/TgX6xQ1Tb7rdjojGwVvBR +/UEo6ze84bhn7e0sm32x3Pq1V4hhwvRDi6upOZtmQwKBgQDbjiPYdqKkbrC7YFsH +4m9VenSmnOppugcYU5h3zLORERfuTBT4cT2TEZT4zlz1vUZFjoQ5ML97xSGeCKzN +0Y0uh6zRgoy2zAQzwmtrNi80GuFEv1CJq7qxzz3aDU2Y/qYFmnl2cTwLNC+MxpbJ +lpvH9Ufkaj4vu/Iuw/Dnc2BPQg== +-----END PRIVATE KEY----- diff --git a/examples/grpc-bridge/docker-compose-protos.yaml b/examples/grpc-bridge/docker-compose-protos.yaml index 42da7d7407c7..543fe4bf5aca 100644 --- a/examples/grpc-bridge/docker-compose-protos.yaml +++ b/examples/grpc-bridge/docker-compose-protos.yaml @@ -1,7 +1,7 @@ version: "3.7" # This is the conversion from a script to a dockerized version of the script -# https://github.com/envoyproxy/envoy/blob/master/examples/grpc-bridge/service/script/gen +# https://github.com/envoyproxy/envoy/blob/main/examples/grpc-bridge/service/script/gen services: # $ docker run -ti -v $(pwd):/protos -v $(pwd)/stubs:/stubs grpc/go protoc --go_out=plugins=grpc:/stubs -I/protos /protos/kv.proto diff --git a/examples/tls-sni/Dockerfile b/examples/tls-sni/Dockerfile new file mode 100644 index 000000000000..e7d9edb34d93 --- /dev/null +++ b/examples/tls-sni/Dockerfile @@ -0,0 +1,9 @@ +FROM envoyproxy/envoy-dev:latest + +COPY ./envoy.yaml /etc/envoy.yaml +COPY ./certs /certs +RUN chmod go+r /etc/envoy.yaml \ + && chmod go+x /certs \ + && chmod go+r /certs/* + +CMD ["/usr/local/bin/envoy", "-c", "/etc/envoy.yaml", "-l", "debug"] diff --git a/examples/tls-sni/Dockerfile-client b/examples/tls-sni/Dockerfile-client new file mode 100644 index 000000000000..ef7428f83f6b --- /dev/null +++ b/examples/tls-sni/Dockerfile-client @@ -0,0 +1,6 @@ +FROM envoyproxy/envoy-dev:latest + +COPY ./envoy-client.yaml /etc/envoy.yaml +RUN chmod go+r /etc/envoy.yaml + +CMD ["/usr/local/bin/envoy", "-c /etc/envoy.yaml"] diff --git a/examples/tls-sni/README.md b/examples/tls-sni/README.md new file mode 100644 index 000000000000..8f43b19a059f --- /dev/null +++ b/examples/tls-sni/README.md @@ -0,0 +1,2 @@ +To learn about this sandbox and for instructions on how to run it please head over +to the [Envoy docs](https://www.envoyproxy.io/docs/envoy/latest/start/sandboxes/tls-sni.html). diff --git a/examples/tls-sni/docker-compose.yaml b/examples/tls-sni/docker-compose.yaml new file mode 100644 index 000000000000..0c5fbd4ac516 --- /dev/null +++ b/examples/tls-sni/docker-compose.yaml @@ -0,0 +1,34 @@ +version: "3.7" +services: + + proxy: + build: + context: . + dockerfile: Dockerfile + ports: + - "10000:10000" + + proxy-client: + build: + context: . + dockerfile: Dockerfile-client + ports: + - "20000:10000" + + http-upstream1: + image: mendhak/http-https-echo + hostname: http-upstream1 + environment: + - HTTPS_PORT=0 + + http-upstream2: + image: mendhak/http-https-echo + hostname: http-upstream2 + environment: + - HTTPS_PORT=0 + + https-upstream3: + image: mendhak/http-https-echo + hostname: https-upstream3 + environment: + - HTTP_PORT=0 diff --git a/examples/tls-sni/envoy-client.yaml b/examples/tls-sni/envoy-client.yaml new file mode 100644 index 000000000000..0ba5b2ebc8e2 --- /dev/null +++ b/examples/tls-sni/envoy-client.yaml @@ -0,0 +1,92 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 10000 + filter_chains: + - filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: app + domains: + - "*" + routes: + - match: + prefix: "/domain1" + route: + cluster: proxy-client-domain1 + - match: + prefix: "/domain2" + route: + cluster: proxy-client-domain2 + - match: + prefix: "/domain3" + route: + cluster: proxy-client-domain3 + http_filters: + - name: envoy.filters.http.router + + clusters: + - name: proxy-client-domain1 + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + load_assignment: + cluster_name: proxy-client-domain1 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: proxy + port_value: 10000 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + sni: domain1.example.com + + - name: proxy-client-domain2 + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + load_assignment: + cluster_name: proxy-client-domain2 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: proxy + port_value: 10000 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + sni: domain2.example.com + + - name: proxy-client-domain3 + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + load_assignment: + cluster_name: proxy-client-domain3 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: proxy + port_value: 10000 + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + sni: domain3.example.com diff --git a/examples/tls-sni/envoy.yaml b/examples/tls-sni/envoy.yaml new file mode 100644 index 000000000000..cd6245e8209b --- /dev/null +++ b/examples/tls-sni/envoy.yaml @@ -0,0 +1,127 @@ +static_resources: + listeners: + - address: + socket_address: + address: 0.0.0.0 + port_value: 10000 + listener_filters: + - name: "envoy.filters.listener.tls_inspector" + filter_chains: + - filter_chain_match: + server_names: + - domain1.example.com + filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: app + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: proxy-domain1 + http_filters: + - name: envoy.filters.http.router + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + common_tls_context: + tls_certificates: + certificate_chain: + filename: certs/domain1.crt.pem + private_key: + filename: certs/domain1.key.pem + + - filter_chain_match: + server_names: + - domain2.example.com + filters: + - name: envoy.filters.network.http_connection_manager + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + codec_type: auto + stat_prefix: ingress_http + route_config: + name: local_route + virtual_hosts: + - name: app + domains: + - "*" + routes: + - match: + prefix: "/" + route: + cluster: proxy-domain2 + http_filters: + - name: envoy.filters.http.router + transport_socket: + name: envoy.transport_sockets.tls + typed_config: + "@type": type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + common_tls_context: + tls_certificates: + certificate_chain: + filename: certs/domain2.crt.pem + private_key: + filename: certs/domain2.key.pem + + - filter_chain_match: + server_names: + - domain3.example.com + filters: + - name: envoy.filters.network.tcp_proxy + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy + cluster: proxy-domain3 + stat_prefix: ingress_domain3 + + clusters: + - name: proxy-domain1 + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + load_assignment: + cluster_name: proxy-domain1 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: http-upstream1 + port_value: 80 + + - name: proxy-domain2 + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + load_assignment: + cluster_name: proxy-domain2 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: http-upstream2 + port_value: 80 + + - name: proxy-domain3 + connect_timeout: 0.25s + type: strict_dns + lb_policy: round_robin + load_assignment: + cluster_name: proxy-domain3 + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: https-upstream3 + port_value: 443 diff --git a/examples/tls-sni/verify.sh b/examples/tls-sni/verify.sh new file mode 100755 index 000000000000..32f639cd7104 --- /dev/null +++ b/examples/tls-sni/verify.sh @@ -0,0 +1,51 @@ +#!/bin/bash -e + +export NAME=tls-sni +export MANUAL=true + +# shellcheck source=examples/verify-common.sh +. "$(dirname "${BASH_SOURCE[0]}")/../verify-common.sh" + + +create_self_signed_certs () { + local domain="$1" + openssl req -new -newkey rsa:2048 -days 365 -nodes -x509 \ + -subj "/C=US/ST=CA/O=MyExample, Inc./CN=${domain}.example.com" \ + -keyout "certs/${domain}.key.pem" \ + -out "certs/${domain}.crt.pem" +} + +mkdir -p certs + +run_log "Create certificates for each of the services" +create_self_signed_certs domain1 +create_self_signed_certs domain2 + +bring_up_example + +run_log "Query domain1 with curl and tls/sni" +curl -sk --resolve domain1.example.com:10000:127.0.0.1 \ + https://domain1.example.com:10000 \ + | jq '.os.hostname' | grep http-upstream1 + +run_log "Query domain2 with curl and tls/sni" +curl -sk --resolve domain2.example.com:10000:127.0.0.1 \ + https://domain2.example.com:10000 \ + | jq '.os.hostname' | grep http-upstream2 + +run_log "Query domain3 with curl and tls/sni" +curl -sk --resolve domain3.example.com:10000:127.0.0.1 \ + https://domain3.example.com:10000 \ + | jq '.os.hostname' | grep https-upstream3 + +run_log "Query domain1 via Envoy sni client" +curl -s http://localhost:20000/domain1 \ + | jq '.os.hostname' | grep http-upstream1 + +run_log "Query domain2 via Envoy sni client" +curl -s http://localhost:20000/domain2 \ + | jq '.os.hostname' | grep http-upstream2 + +run_log "Query domain3 via Envoy sni client" +curl -s http://localhost:20000/domain3 \ + | jq '.os.hostname' | grep https-upstream3 diff --git a/examples/wasm-cc/README.md b/examples/wasm-cc/README.md index 2922b607f9b7..f5e04fd7503a 100644 --- a/examples/wasm-cc/README.md +++ b/examples/wasm-cc/README.md @@ -1,59 +1,2 @@ -# Envoy WebAssembly Filter - -In this example, we show how a WebAssembly(WASM) filter can be used with the Envoy -proxy. The Envoy proxy [configuration](./envoy.yaml) includes a Webassembly filter -as documented [here](https://www.envoyproxy.io/docs/envoy/latest/). - - - - -## Quick Start - -1. `docker-compose build` -2. `docker-compose up` -3. `curl -v localhost:18000` - -Curl output should include our headers: - -``` -# curl -v localhost:8000 -* Rebuilt URL to: localhost:18000/ -* Trying 127.0.0.1... -* TCP_NODELAY set -* Connected to localhost (127.0.0.1) port 18000 (#0) -> GET / HTTP/1.1 -> Host: localhost:18000 -> User-Agent: curl/7.58.0 -> Accept: */* -> -< HTTP/1.1 200 OK -< content-length: 13 -< content-type: text/plain -< location: envoy-wasm -< date: Tue, 09 Jul 2019 00:47:14 GMT -< server: envoy -< x-envoy-upstream-service-time: 0 -< newheader: newheadervalue -< -example body -* Connection #0 to host localhost left intact -``` - -## Build WASM Module - -Now you want to make changes to the C++ filter ([envoy_filter_http_wasm_example.cc](envoy_filter_http_wasm_example.cc)) -and build the WASM module ([envoy_filter_http_wasm_example.wasm](envoy_filter_http_wasm_example.wasm)). - -1. Build WASM module - ```shell - bazel build //examples/wasm:envoy_filter_http_wasm_example.wasm - ``` - -## Build the Envoy WASM Image - - - -For Envoy WASM runtime developers, if you want to make changes, please - -1. Follow [instructions](https://github.com/envoyproxy/envoy-wasm/blob/master/WASM.md). -2. Modify `docker-compose.yaml` to mount your own Envoy. +To learn about this sandbox and for instructions on how to run it please head over +to the [envoy docs](https://www.envoyproxy.io/docs/envoy/latest/start/sandboxes/wasm-cc). diff --git a/examples/wasm-cc/docker-compose-wasm.yaml b/examples/wasm-cc/docker-compose-wasm.yaml index 7dd2490bb4ed..5e30327cb98e 100644 --- a/examples/wasm-cc/docker-compose-wasm.yaml +++ b/examples/wasm-cc/docker-compose-wasm.yaml @@ -3,7 +3,7 @@ version: "3.7" services: wasm_compile_update: - image: envoyproxy/envoy-build-ubuntu:11efa5680d987fff33fde4af3cc5ece105015d04 + image: envoyproxy/envoy-build-ubuntu:c8fa4235714003ba0896287ee2f91cae06e0e407 command: | bash -c "bazel build //examples/wasm-cc:envoy_filter_http_wasm_updated_example.wasm \ && cp -a bazel-bin/examples/wasm-cc/* /build" @@ -13,7 +13,7 @@ services: - ./lib:/build wasm_compile: - image: envoyproxy/envoy-build-ubuntu:11efa5680d987fff33fde4af3cc5ece105015d04 + image: envoyproxy/envoy-build-ubuntu:c8fa4235714003ba0896287ee2f91cae06e0e407 command: | bash -c "bazel build //examples/wasm-cc:envoy_filter_http_wasm_example.wasm \ && cp -a bazel-bin/examples/wasm-cc/* /build" diff --git a/generated_api_shadow/BUILD b/generated_api_shadow/BUILD index c93cdaf5db22..5b4131922cd0 100644 --- a/generated_api_shadow/BUILD +++ b/generated_api_shadow/BUILD @@ -5,11 +5,10 @@ load("@rules_proto//proto:defs.bzl", "proto_library") licenses(["notice"]) # Apache 2 proto_library( - name = "protos", + name = "v2_protos", visibility = ["//visibility:public"], deps = [ "//envoy/admin/v2alpha:pkg", - "//envoy/admin/v3:pkg", "//envoy/api/v2:pkg", "//envoy/api/v2/auth:pkg", "//envoy/api/v2/cluster:pkg", @@ -19,23 +18,20 @@ proto_library( "//envoy/api/v2/ratelimit:pkg", "//envoy/api/v2/route:pkg", "//envoy/config/accesslog/v2:pkg", - "//envoy/config/accesslog/v3:pkg", "//envoy/config/bootstrap/v2:pkg", - "//envoy/config/bootstrap/v3:pkg", "//envoy/config/cluster/aggregate/v2alpha:pkg", "//envoy/config/cluster/dynamic_forward_proxy/v2alpha:pkg", "//envoy/config/cluster/redis:pkg", - "//envoy/config/cluster/v3:pkg", "//envoy/config/common/dynamic_forward_proxy/v2alpha:pkg", "//envoy/config/common/tap/v2alpha:pkg", - "//envoy/config/core/v3:pkg", - "//envoy/config/endpoint/v3:pkg", "//envoy/config/filter/accesslog/v2:pkg", "//envoy/config/filter/dubbo/router/v2alpha1:pkg", "//envoy/config/filter/fault/v2:pkg", "//envoy/config/filter/http/adaptive_concurrency/v2alpha:pkg", - "//envoy/config/filter/http/admission_control/v2alpha:pkg", + "//envoy/config/filter/http/aws_lambda/v2alpha:pkg", + "//envoy/config/filter/http/aws_request_signing/v2alpha:pkg", "//envoy/config/filter/http/buffer/v2:pkg", + "//envoy/config/filter/http/cache/v2alpha:pkg", "//envoy/config/filter/http/compressor/v2:pkg", "//envoy/config/filter/http/cors/v2:pkg", "//envoy/config/filter/http/csrf/v2:pkg", @@ -67,6 +63,7 @@ proto_library( "//envoy/config/filter/listener/proxy_protocol/v2:pkg", "//envoy/config/filter/listener/tls_inspector/v2:pkg", "//envoy/config/filter/network/client_ssl_auth/v2:pkg", + "//envoy/config/filter/network/direct_response/v2:pkg", "//envoy/config/filter/network/dubbo_proxy/v2alpha1:pkg", "//envoy/config/filter/network/echo/v2:pkg", "//envoy/config/filter/network/ext_authz/v2:pkg", @@ -78,7 +75,6 @@ proto_library( "//envoy/config/filter/network/rate_limit/v2:pkg", "//envoy/config/filter/network/rbac/v2:pkg", "//envoy/config/filter/network/redis_proxy/v2:pkg", - "//envoy/config/filter/network/rocketmq_proxy/v3:pkg", "//envoy/config/filter/network/sni_cluster/v2:pkg", "//envoy/config/filter/network/tcp_proxy/v2:pkg", "//envoy/config/filter/network/thrift_proxy/v2alpha1:pkg", @@ -87,57 +83,106 @@ proto_library( "//envoy/config/filter/thrift/router/v2alpha1:pkg", "//envoy/config/filter/udp/udp_proxy/v2alpha:pkg", "//envoy/config/grpc_credential/v2alpha:pkg", - "//envoy/config/grpc_credential/v3:pkg", "//envoy/config/health_checker/redis/v2:pkg", "//envoy/config/listener/v2:pkg", - "//envoy/config/listener/v3:pkg", "//envoy/config/metrics/v2:pkg", - "//envoy/config/metrics/v3:pkg", "//envoy/config/overload/v2alpha:pkg", - "//envoy/config/overload/v3:pkg", "//envoy/config/ratelimit/v2:pkg", - "//envoy/config/ratelimit/v3:pkg", "//envoy/config/rbac/v2:pkg", - "//envoy/config/rbac/v3:pkg", "//envoy/config/resource_monitor/fixed_heap/v2alpha:pkg", "//envoy/config/resource_monitor/injected_resource/v2alpha:pkg", "//envoy/config/retry/omit_canary_hosts/v2:pkg", "//envoy/config/retry/omit_host_metadata/v2:pkg", "//envoy/config/retry/previous_hosts/v2:pkg", "//envoy/config/retry/previous_priorities:pkg", - "//envoy/config/route/v3:pkg", - "//envoy/config/tap/v3:pkg", "//envoy/config/trace/v2:pkg", "//envoy/config/trace/v2alpha:pkg", - "//envoy/config/trace/v3:pkg", "//envoy/config/transport_socket/alts/v2alpha:pkg", "//envoy/config/transport_socket/raw_buffer/v2:pkg", "//envoy/config/transport_socket/tap/v2alpha:pkg", "//envoy/data/accesslog/v2:pkg", - "//envoy/data/accesslog/v3:pkg", "//envoy/data/cluster/v2alpha:pkg", "//envoy/data/core/v2alpha:pkg", - "//envoy/data/core/v3:pkg", + "//envoy/data/dns/v2alpha:pkg", "//envoy/data/tap/v2alpha:pkg", + "//envoy/service/accesslog/v2:pkg", + "//envoy/service/auth/v2:pkg", + "//envoy/service/discovery/v2:pkg", + "//envoy/service/event_reporting/v2alpha:pkg", + "//envoy/service/load_stats/v2:pkg", + "//envoy/service/metrics/v2:pkg", + "//envoy/service/ratelimit/v2:pkg", + "//envoy/service/status/v2:pkg", + "//envoy/service/tap/v2alpha:pkg", + "//envoy/service/trace/v2:pkg", + "//envoy/type:pkg", + "//envoy/type/matcher:pkg", + "//envoy/type/metadata/v2:pkg", + "//envoy/type/tracing/v2:pkg", + ], +) + +proto_library( + name = "v3_protos", + visibility = ["//visibility:public"], + deps = [ + "//envoy/admin/v3:pkg", + "//envoy/config/accesslog/v3:pkg", + "//envoy/config/bootstrap/v3:pkg", + "//envoy/config/cluster/v3:pkg", + "//envoy/config/common/matcher/v3:pkg", + "//envoy/config/core/v3:pkg", + "//envoy/config/endpoint/v3:pkg", + "//envoy/config/filter/thrift/router/v2alpha1:pkg", + "//envoy/config/grpc_credential/v3:pkg", + "//envoy/config/health_checker/redis/v2:pkg", + "//envoy/config/listener/v3:pkg", + "//envoy/config/metrics/v3:pkg", + "//envoy/config/overload/v3:pkg", + "//envoy/config/ratelimit/v3:pkg", + "//envoy/config/rbac/v3:pkg", + "//envoy/config/resource_monitor/fixed_heap/v2alpha:pkg", + "//envoy/config/resource_monitor/injected_resource/v2alpha:pkg", + "//envoy/config/retry/omit_canary_hosts/v2:pkg", + "//envoy/config/retry/omit_canary_hosts/v3:pkg", + "//envoy/config/retry/previous_hosts/v2:pkg", + "//envoy/config/route/v3:pkg", + "//envoy/config/tap/v3:pkg", + "//envoy/config/trace/v3:pkg", + "//envoy/data/accesslog/v3:pkg", + "//envoy/data/cluster/v3:pkg", + "//envoy/data/core/v3:pkg", + "//envoy/data/dns/v3:pkg", "//envoy/data/tap/v3:pkg", "//envoy/extensions/access_loggers/file/v3:pkg", "//envoy/extensions/access_loggers/grpc/v3:pkg", + "//envoy/extensions/access_loggers/wasm/v3:pkg", "//envoy/extensions/clusters/aggregate/v3:pkg", "//envoy/extensions/clusters/dynamic_forward_proxy/v3:pkg", "//envoy/extensions/clusters/redis/v3:pkg", "//envoy/extensions/common/dynamic_forward_proxy/v3:pkg", + "//envoy/extensions/common/matching/v3:pkg", "//envoy/extensions/common/ratelimit/v3:pkg", "//envoy/extensions/common/tap/v3:pkg", + "//envoy/extensions/compression/gzip/compressor/v3:pkg", + "//envoy/extensions/compression/gzip/decompressor/v3:pkg", "//envoy/extensions/filters/common/fault/v3:pkg", + "//envoy/extensions/filters/common/matcher/action/v3:pkg", "//envoy/extensions/filters/http/adaptive_concurrency/v3:pkg", "//envoy/extensions/filters/http/admission_control/v3alpha:pkg", + "//envoy/extensions/filters/http/aws_lambda/v3:pkg", + "//envoy/extensions/filters/http/aws_request_signing/v3:pkg", "//envoy/extensions/filters/http/buffer/v3:pkg", + "//envoy/extensions/filters/http/cache/v3alpha:pkg", + "//envoy/extensions/filters/http/cdn_loop/v3alpha:pkg", "//envoy/extensions/filters/http/compressor/v3:pkg", "//envoy/extensions/filters/http/cors/v3:pkg", "//envoy/extensions/filters/http/csrf/v3:pkg", + "//envoy/extensions/filters/http/decompressor/v3:pkg", "//envoy/extensions/filters/http/dynamic_forward_proxy/v3:pkg", "//envoy/extensions/filters/http/dynamo/v3:pkg", "//envoy/extensions/filters/http/ext_authz/v3:pkg", + "//envoy/extensions/filters/http/ext_proc/v3alpha:pkg", "//envoy/extensions/filters/http/fault/v3:pkg", "//envoy/extensions/filters/http/grpc_http1_bridge/v3:pkg", "//envoy/extensions/filters/http/grpc_http1_reverse_bridge/v3:pkg", @@ -149,7 +194,10 @@ proto_library( "//envoy/extensions/filters/http/health_check/v3:pkg", "//envoy/extensions/filters/http/ip_tagging/v3:pkg", "//envoy/extensions/filters/http/jwt_authn/v3:pkg", + "//envoy/extensions/filters/http/kill_request/v3:pkg", + "//envoy/extensions/filters/http/local_ratelimit/v3:pkg", "//envoy/extensions/filters/http/lua/v3:pkg", + "//envoy/extensions/filters/http/oauth2/v3alpha:pkg", "//envoy/extensions/filters/http/on_demand/v3:pkg", "//envoy/extensions/filters/http/original_src/v3:pkg", "//envoy/extensions/filters/http/ratelimit/v3:pkg", @@ -157,12 +205,14 @@ proto_library( "//envoy/extensions/filters/http/router/v3:pkg", "//envoy/extensions/filters/http/squash/v3:pkg", "//envoy/extensions/filters/http/tap/v3:pkg", + "//envoy/extensions/filters/http/wasm/v3:pkg", "//envoy/extensions/filters/listener/http_inspector/v3:pkg", "//envoy/extensions/filters/listener/original_dst/v3:pkg", "//envoy/extensions/filters/listener/original_src/v3:pkg", "//envoy/extensions/filters/listener/proxy_protocol/v3:pkg", "//envoy/extensions/filters/listener/tls_inspector/v3:pkg", "//envoy/extensions/filters/network/client_ssl_auth/v3:pkg", + "//envoy/extensions/filters/network/direct_response/v3:pkg", "//envoy/extensions/filters/network/dubbo_proxy/router/v3:pkg", "//envoy/extensions/filters/network/dubbo_proxy/v3:pkg", "//envoy/extensions/filters/network/echo/v3:pkg", @@ -172,51 +222,75 @@ proto_library( "//envoy/extensions/filters/network/local_ratelimit/v3:pkg", "//envoy/extensions/filters/network/mongo_proxy/v3:pkg", "//envoy/extensions/filters/network/mysql_proxy/v3:pkg", + "//envoy/extensions/filters/network/postgres_proxy/v3alpha:pkg", "//envoy/extensions/filters/network/ratelimit/v3:pkg", "//envoy/extensions/filters/network/rbac/v3:pkg", "//envoy/extensions/filters/network/redis_proxy/v3:pkg", + "//envoy/extensions/filters/network/rocketmq_proxy/v3:pkg", "//envoy/extensions/filters/network/sni_cluster/v3:pkg", + "//envoy/extensions/filters/network/sni_dynamic_forward_proxy/v3alpha:pkg", "//envoy/extensions/filters/network/tcp_proxy/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/filters/ratelimit/v3:pkg", + "//envoy/extensions/filters/network/thrift_proxy/router/v3:pkg", "//envoy/extensions/filters/network/thrift_proxy/v3:pkg", + "//envoy/extensions/filters/network/wasm/v3:pkg", "//envoy/extensions/filters/network/zookeeper_proxy/v3:pkg", + "//envoy/extensions/filters/udp/dns_filter/v3alpha:pkg", + "//envoy/extensions/filters/udp/udp_proxy/v3:pkg", + "//envoy/extensions/internal_redirect/allow_listed_routes/v3:pkg", + "//envoy/extensions/internal_redirect/previous_routes/v3:pkg", + "//envoy/extensions/internal_redirect/safe_cross_scheme/v3:pkg", + "//envoy/extensions/network/socket_interface/v3:pkg", + "//envoy/extensions/rate_limit_descriptors/expr/v3:pkg", "//envoy/extensions/retry/host/omit_host_metadata/v3:pkg", "//envoy/extensions/retry/priority/previous_priorities/v3:pkg", + "//envoy/extensions/stat_sinks/wasm/v3:pkg", "//envoy/extensions/transport_sockets/alts/v3:pkg", + "//envoy/extensions/transport_sockets/proxy_protocol/v3:pkg", + "//envoy/extensions/transport_sockets/quic/v3:pkg", "//envoy/extensions/transport_sockets/raw_buffer/v3:pkg", + "//envoy/extensions/transport_sockets/starttls/v3:pkg", "//envoy/extensions/transport_sockets/tap/v3:pkg", "//envoy/extensions/transport_sockets/tls/v3:pkg", + "//envoy/extensions/upstreams/http/generic/v3:pkg", + "//envoy/extensions/upstreams/http/http/v3:pkg", + "//envoy/extensions/upstreams/http/tcp/v3:pkg", + "//envoy/extensions/upstreams/http/v3:pkg", + "//envoy/extensions/upstreams/tcp/generic/v3:pkg", "//envoy/extensions/wasm/v3:pkg", - "//envoy/service/accesslog/v2:pkg", + "//envoy/extensions/watchdog/profile_action/v3alpha:pkg", "//envoy/service/accesslog/v3:pkg", - "//envoy/service/auth/v2:pkg", "//envoy/service/auth/v3:pkg", "//envoy/service/cluster/v3:pkg", - "//envoy/service/discovery/v2:pkg", "//envoy/service/discovery/v3:pkg", "//envoy/service/endpoint/v3:pkg", + "//envoy/service/event_reporting/v3:pkg", + "//envoy/service/ext_proc/v3alpha:pkg", + "//envoy/service/extension/v3:pkg", "//envoy/service/health/v3:pkg", "//envoy/service/listener/v3:pkg", - "//envoy/service/load_stats/v2:pkg", "//envoy/service/load_stats/v3:pkg", - "//envoy/service/metrics/v2:pkg", "//envoy/service/metrics/v3:pkg", - "//envoy/service/ratelimit/v2:pkg", "//envoy/service/ratelimit/v3:pkg", "//envoy/service/route/v3:pkg", "//envoy/service/runtime/v3:pkg", "//envoy/service/secret/v3:pkg", - "//envoy/service/status/v2:pkg", "//envoy/service/status/v3:pkg", - "//envoy/service/tap/v2alpha:pkg", "//envoy/service/tap/v3:pkg", - "//envoy/service/trace/v2:pkg", "//envoy/service/trace/v3:pkg", - "//envoy/type:pkg", - "//envoy/type/matcher:pkg", "//envoy/type/matcher/v3:pkg", - "//envoy/type/metadata/v2:pkg", - "//envoy/type/tracing/v2:pkg", + "//envoy/type/metadata/v3:pkg", + "//envoy/type/tracing/v3:pkg", "//envoy/type/v3:pkg", + "//envoy/watchdog/v3alpha:pkg", + ], +) + +proto_library( + name = "all_protos", + visibility = ["//visibility:public"], + deps = [ + ":v2_protos", + ":v3_protos", ], ) diff --git a/generated_api_shadow/envoy/admin/v3/config_dump.proto b/generated_api_shadow/envoy/admin/v3/config_dump.proto index 73156697fdb2..b3e5510a700c 100644 --- a/generated_api_shadow/envoy/admin/v3/config_dump.proto +++ b/generated_api_shadow/envoy/admin/v3/config_dump.proto @@ -49,6 +49,7 @@ message UpdateFailureState { "envoy.admin.v2alpha.UpdateFailureState"; // What the component configuration would have been if the update had succeeded. + // This field may not be populated by xDS clients due to storage overhead. google.protobuf.Any failed_configuration = 1; // Time of the latest failed update attempt. @@ -56,6 +57,10 @@ message UpdateFailureState { // Details about the last failed update attempt. string details = 3; + + // This is the version of the rejected resource. + // [#not-implemented-hide:] + string version_info = 4; } // This message describes the bootstrap configuration that Envoy was started with. This includes @@ -134,6 +139,9 @@ message ListenersConfigDump { DynamicListenerState draining_state = 4; // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. UpdateFailureState error_state = 5; } @@ -184,6 +192,13 @@ message ClustersConfigDump { // The timestamp when the Cluster was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // This is the :ref:`version_info ` in the @@ -239,6 +254,13 @@ message RoutesConfigDump { // The timestamp when the Route was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // The statically loaded route configs. @@ -270,6 +292,7 @@ message ScopedRoutesConfigDump { google.protobuf.Timestamp last_updated = 3; } + // [#next-free-field: 6] message DynamicScopedRouteConfigs { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v2alpha.ScopedRoutesConfigDump.DynamicScopedRouteConfigs"; @@ -287,6 +310,13 @@ message ScopedRoutesConfigDump { // The timestamp when the scoped route config set was last updated. google.protobuf.Timestamp last_updated = 4; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 5; } // The statically loaded scoped route configs. @@ -302,6 +332,7 @@ message SecretsConfigDump { "envoy.admin.v2alpha.SecretsConfigDump"; // DynamicSecret contains secret information fetched via SDS. + // [#next-free-field: 6] message DynamicSecret { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v2alpha.SecretsConfigDump.DynamicSecret"; @@ -319,6 +350,13 @@ message SecretsConfigDump { // Security sensitive information is redacted (replaced with "[redacted]") for // private keys and passwords in TLS certificates. google.protobuf.Any secret = 4; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 5; } // StaticSecret specifies statically loaded secret in bootstrap. @@ -373,6 +411,13 @@ message EndpointsConfigDump { // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // The statically loaded endpoint configs. diff --git a/generated_api_shadow/envoy/admin/v4alpha/config_dump.proto b/generated_api_shadow/envoy/admin/v4alpha/config_dump.proto index 7fed09631d75..3f5610be23e0 100644 --- a/generated_api_shadow/envoy/admin/v4alpha/config_dump.proto +++ b/generated_api_shadow/envoy/admin/v4alpha/config_dump.proto @@ -48,6 +48,7 @@ message UpdateFailureState { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v3.UpdateFailureState"; // What the component configuration would have been if the update had succeeded. + // This field may not be populated by xDS clients due to storage overhead. google.protobuf.Any failed_configuration = 1; // Time of the latest failed update attempt. @@ -55,6 +56,10 @@ message UpdateFailureState { // Details about the last failed update attempt. string details = 3; + + // This is the version of the rejected resource. + // [#not-implemented-hide:] + string version_info = 4; } // This message describes the bootstrap configuration that Envoy was started with. This includes @@ -131,6 +136,9 @@ message ListenersConfigDump { DynamicListenerState draining_state = 4; // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. UpdateFailureState error_state = 5; } @@ -180,6 +188,13 @@ message ClustersConfigDump { // The timestamp when the Cluster was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // This is the :ref:`version_info ` in the @@ -234,6 +249,13 @@ message RoutesConfigDump { // The timestamp when the Route was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // The statically loaded route configs. @@ -265,6 +287,7 @@ message ScopedRoutesConfigDump { google.protobuf.Timestamp last_updated = 3; } + // [#next-free-field: 6] message DynamicScopedRouteConfigs { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v3.ScopedRoutesConfigDump.DynamicScopedRouteConfigs"; @@ -282,6 +305,13 @@ message ScopedRoutesConfigDump { // The timestamp when the scoped route config set was last updated. google.protobuf.Timestamp last_updated = 4; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 5; } // The statically loaded scoped route configs. @@ -296,6 +326,7 @@ message SecretsConfigDump { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v3.SecretsConfigDump"; // DynamicSecret contains secret information fetched via SDS. + // [#next-free-field: 6] message DynamicSecret { option (udpa.annotations.versioning).previous_message_type = "envoy.admin.v3.SecretsConfigDump.DynamicSecret"; @@ -313,6 +344,13 @@ message SecretsConfigDump { // Security sensitive information is redacted (replaced with "[redacted]") for // private keys and passwords in TLS certificates. google.protobuf.Any secret = 4; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 5; } // StaticSecret specifies statically loaded secret in bootstrap. @@ -375,6 +413,13 @@ message EndpointsConfigDump { // [#not-implemented-hide:] The timestamp when the Endpoint was last updated. google.protobuf.Timestamp last_updated = 3; + + // Set if the last update failed, cleared after the next successful update. + // The *error_state* field contains the rejected version of this particular + // resource along with the reason and timestamp. For successfully updated or + // acknowledged resource, this field should be empty. + // [#not-implemented-hide:] + UpdateFailureState error_state = 4; } // The statically loaded endpoint configs. diff --git a/generated_api_shadow/envoy/api/v2/core/protocol.proto b/generated_api_shadow/envoy/api/v2/core/protocol.proto index 9c47e388ee1a..ae1a86424cf0 100644 --- a/generated_api_shadow/envoy/api/v2/core/protocol.proto +++ b/generated_api_shadow/envoy/api/v2/core/protocol.proto @@ -201,7 +201,7 @@ message Http2ProtocolOptions { // Still under implementation. DO NOT USE. // // Allows metadata. See [metadata - // docs](https://github.com/envoyproxy/envoy/blob/master/source/docs/h2_metadata.md) for more + // docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more // information. bool allow_metadata = 6; diff --git a/generated_api_shadow/envoy/config/cluster/v3/cluster.proto b/generated_api_shadow/envoy/config/cluster/v3/cluster.proto index cce56c9a1e33..f3859bf35d22 100644 --- a/generated_api_shadow/envoy/config/cluster/v3/cluster.proto +++ b/generated_api_shadow/envoy/config/cluster/v3/cluster.proto @@ -583,11 +583,10 @@ message Cluster { google.protobuf.Duration max_interval = 2 [(validate.rules).duration = {gt {nanos: 1000000}}]; } - // [#not-implemented-hide:] message PreconnectPolicy { // Indicates how many streams (rounded up) can be anticipated per-upstream for each // incoming stream. This is useful for high-QPS or latency-sensitive services. Preconnecting - // will only be done if the upstream is healthy. + // will only be done if the upstream is healthy and the cluster has traffic. // // For example if this is 2, for an incoming HTTP/1.1 stream, 2 connections will be // established, one for the new incoming stream, and one for a presumed follow-up stream. For @@ -605,8 +604,7 @@ message Cluster { // // If this value is not set, or set explicitly to one, Envoy will fetch as many connections // as needed to serve streams in flight. This means in steady state if a connection is torn down, - // a subsequent streams will pay an upstream-rtt latency penalty waiting for streams to be - // preconnected. + // a subsequent streams will pay an upstream-rtt latency penalty waiting for a new connection. // // This is limited somewhat arbitrarily to 3 because preconnecting too aggressively can // harm latency more than the preconnecting helps. @@ -616,24 +614,25 @@ message Cluster { // Indicates how many many streams (rounded up) can be anticipated across a cluster for each // stream, useful for low QPS services. This is currently supported for a subset of // deterministic non-hash-based load-balancing algorithms (weighted round robin, random). - // Unlike per_upstream_preconnect_ratio this preconnects across the upstream instances in a + // Unlike *per_upstream_preconnect_ratio* this preconnects across the upstream instances in a // cluster, doing best effort predictions of what upstream would be picked next and // pre-establishing a connection. // + // Preconnecting will be limited to one preconnect per configured upstream in the cluster and will + // only be done if there are healthy upstreams and the cluster has traffic. + // // For example if preconnecting is set to 2 for a round robin HTTP/2 cluster, on the first // incoming stream, 2 connections will be preconnected - one to the first upstream for this // cluster, one to the second on the assumption there will be a follow-up stream. // - // Preconnecting will be limited to one preconnect per configured upstream in the cluster. - // // If this value is not set, or set explicitly to one, Envoy will fetch as many connections // as needed to serve streams in flight, so during warm up and in steady state if a connection // is closed (and per_upstream_preconnect_ratio is not set), there will be a latency hit for // connection establishment. // // If both this and preconnect_ratio are set, Envoy will make sure both predicted needs are met, - // basically preconnecting max(predictive-preconnect, per-upstream-preconnect), for each upstream. - // TODO(alyssawilk) per LB docs and LB overview docs when unhiding. + // basically preconnecting max(predictive-preconnect, per-upstream-preconnect), for each + // upstream. google.protobuf.DoubleValue predictive_preconnect_ratio = 2 [(validate.rules).double = {lte: 3.0 gte: 1.0}]; } @@ -1026,7 +1025,6 @@ message Cluster { // Configuration to track optional cluster stats. TrackClusterStats track_cluster_stats = 49; - // [#not-implemented-hide:] // Preconnect configuration for this cluster. PreconnectPolicy preconnect_policy = 50; diff --git a/generated_api_shadow/envoy/config/cluster/v4alpha/cluster.proto b/generated_api_shadow/envoy/config/cluster/v4alpha/cluster.proto index 172be74b46bc..9fb018b4ee69 100644 --- a/generated_api_shadow/envoy/config/cluster/v4alpha/cluster.proto +++ b/generated_api_shadow/envoy/config/cluster/v4alpha/cluster.proto @@ -589,14 +589,13 @@ message Cluster { google.protobuf.Duration max_interval = 2 [(validate.rules).duration = {gt {nanos: 1000000}}]; } - // [#not-implemented-hide:] message PreconnectPolicy { option (udpa.annotations.versioning).previous_message_type = "envoy.config.cluster.v3.Cluster.PreconnectPolicy"; // Indicates how many streams (rounded up) can be anticipated per-upstream for each // incoming stream. This is useful for high-QPS or latency-sensitive services. Preconnecting - // will only be done if the upstream is healthy. + // will only be done if the upstream is healthy and the cluster has traffic. // // For example if this is 2, for an incoming HTTP/1.1 stream, 2 connections will be // established, one for the new incoming stream, and one for a presumed follow-up stream. For @@ -614,8 +613,7 @@ message Cluster { // // If this value is not set, or set explicitly to one, Envoy will fetch as many connections // as needed to serve streams in flight. This means in steady state if a connection is torn down, - // a subsequent streams will pay an upstream-rtt latency penalty waiting for streams to be - // preconnected. + // a subsequent streams will pay an upstream-rtt latency penalty waiting for a new connection. // // This is limited somewhat arbitrarily to 3 because preconnecting too aggressively can // harm latency more than the preconnecting helps. @@ -625,24 +623,25 @@ message Cluster { // Indicates how many many streams (rounded up) can be anticipated across a cluster for each // stream, useful for low QPS services. This is currently supported for a subset of // deterministic non-hash-based load-balancing algorithms (weighted round robin, random). - // Unlike per_upstream_preconnect_ratio this preconnects across the upstream instances in a + // Unlike *per_upstream_preconnect_ratio* this preconnects across the upstream instances in a // cluster, doing best effort predictions of what upstream would be picked next and // pre-establishing a connection. // + // Preconnecting will be limited to one preconnect per configured upstream in the cluster and will + // only be done if there are healthy upstreams and the cluster has traffic. + // // For example if preconnecting is set to 2 for a round robin HTTP/2 cluster, on the first // incoming stream, 2 connections will be preconnected - one to the first upstream for this // cluster, one to the second on the assumption there will be a follow-up stream. // - // Preconnecting will be limited to one preconnect per configured upstream in the cluster. - // // If this value is not set, or set explicitly to one, Envoy will fetch as many connections // as needed to serve streams in flight, so during warm up and in steady state if a connection // is closed (and per_upstream_preconnect_ratio is not set), there will be a latency hit for // connection establishment. // // If both this and preconnect_ratio are set, Envoy will make sure both predicted needs are met, - // basically preconnecting max(predictive-preconnect, per-upstream-preconnect), for each upstream. - // TODO(alyssawilk) per LB docs and LB overview docs when unhiding. + // basically preconnecting max(predictive-preconnect, per-upstream-preconnect), for each + // upstream. google.protobuf.DoubleValue predictive_preconnect_ratio = 2 [(validate.rules).double = {lte: 3.0 gte: 1.0}]; } @@ -1040,7 +1039,6 @@ message Cluster { // Configuration to track optional cluster stats. TrackClusterStats track_cluster_stats = 49; - // [#not-implemented-hide:] // Preconnect configuration for this cluster. PreconnectPolicy preconnect_policy = 50; diff --git a/generated_api_shadow/envoy/config/core/v3/protocol.proto b/generated_api_shadow/envoy/config/core/v3/protocol.proto index c294370366df..80d971c1466b 100644 --- a/generated_api_shadow/envoy/config/core/v3/protocol.proto +++ b/generated_api_shadow/envoy/config/core/v3/protocol.proto @@ -266,7 +266,7 @@ message Http2ProtocolOptions { // Still under implementation. DO NOT USE. // // Allows metadata. See [metadata - // docs](https://github.com/envoyproxy/envoy/blob/master/source/docs/h2_metadata.md) for more + // docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more // information. bool allow_metadata = 6; diff --git a/generated_api_shadow/envoy/config/core/v3/substitution_format_string.proto b/generated_api_shadow/envoy/config/core/v3/substitution_format_string.proto index fa14db45ddda..9802309df60d 100644 --- a/generated_api_shadow/envoy/config/core/v3/substitution_format_string.proto +++ b/generated_api_shadow/envoy/config/core/v3/substitution_format_string.proto @@ -3,7 +3,9 @@ syntax = "proto3"; package envoy.config.core.v3; import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/extension.proto"; +import "google/protobuf/any.proto"; import "google/protobuf/struct.proto"; import "udpa/annotations/status.proto"; @@ -18,7 +20,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Configuration to use multiple :ref:`command operators ` // to generate a new string in either plain text or JSON format. -// [#next-free-field: 6] +// [#next-free-field: 7] message SubstitutionFormatString { oneof format { option (validate.required) = true; @@ -103,4 +105,8 @@ message SubstitutionFormatString { // content_type: "text/html; charset=UTF-8" // string content_type = 4; + + // Specifies a collection of Formatter plugins that can be called from the access log configuration. + // See the formatters extensions documentation for details. + repeated TypedExtensionConfig formatters = 6; } diff --git a/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto b/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto index 2063d6d6793e..c9fc21d4cfa4 100644 --- a/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto +++ b/generated_api_shadow/envoy/config/core/v4alpha/protocol.proto @@ -269,7 +269,7 @@ message Http2ProtocolOptions { // Still under implementation. DO NOT USE. // // Allows metadata. See [metadata - // docs](https://github.com/envoyproxy/envoy/blob/master/source/docs/h2_metadata.md) for more + // docs](https://github.com/envoyproxy/envoy/blob/main/source/docs/h2_metadata.md) for more // information. bool allow_metadata = 6; diff --git a/generated_api_shadow/envoy/config/core/v4alpha/substitution_format_string.proto b/generated_api_shadow/envoy/config/core/v4alpha/substitution_format_string.proto index 8fc44f05a9c3..922fd6b819a0 100644 --- a/generated_api_shadow/envoy/config/core/v4alpha/substitution_format_string.proto +++ b/generated_api_shadow/envoy/config/core/v4alpha/substitution_format_string.proto @@ -3,7 +3,9 @@ syntax = "proto3"; package envoy.config.core.v4alpha; import "envoy/config/core/v4alpha/base.proto"; +import "envoy/config/core/v4alpha/extension.proto"; +import "google/protobuf/any.proto"; import "google/protobuf/struct.proto"; import "udpa/annotations/status.proto"; @@ -19,7 +21,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // Configuration to use multiple :ref:`command operators ` // to generate a new string in either plain text or JSON format. -// [#next-free-field: 6] +// [#next-free-field: 7] message SubstitutionFormatString { option (udpa.annotations.versioning).previous_message_type = "envoy.config.core.v3.SubstitutionFormatString"; @@ -107,4 +109,8 @@ message SubstitutionFormatString { // content_type: "text/html; charset=UTF-8" // string content_type = 4; + + // Specifies a collection of Formatter plugins that can be called from the access log configuration. + // See the formatters extensions documentation for details. + repeated TypedExtensionConfig formatters = 6; } diff --git a/generated_api_shadow/envoy/config/filter/http/ext_authz/v2/ext_authz.proto b/generated_api_shadow/envoy/config/filter/http/ext_authz/v2/ext_authz.proto index db188a572ae0..b9a807d82edb 100644 --- a/generated_api_shadow/envoy/config/filter/http/ext_authz/v2/ext_authz.proto +++ b/generated_api_shadow/envoy/config/filter/http/ext_authz/v2/ext_authz.proto @@ -49,10 +49,7 @@ message ExtAuthz { // `. bool failure_mode_allow = 2; - // Sets the package version the gRPC service should use. This is particularly - // useful when transitioning from alpha to release versions assuming that both definitions are - // semantically compatible. Deprecation note: This field is deprecated and should only be used for - // version upgrade. See release notes for more details. + // [#not-implemented-hide: Support for this field has been removed.] bool use_alpha = 4 [deprecated = true, (envoy.annotations.disallowed_by_default) = true]; // Enables filter to buffer the client request body and send it within the authorization request. diff --git a/generated_api_shadow/envoy/config/listener/v3/listener_components.proto b/generated_api_shadow/envoy/config/listener/v3/listener_components.proto index 907f25b66304..f96647e5d0b1 100644 --- a/generated_api_shadow/envoy/config/listener/v3/listener_components.proto +++ b/generated_api_shadow/envoy/config/listener/v3/listener_components.proto @@ -4,6 +4,7 @@ package envoy.config.listener.v3; import "envoy/config/core/v3/address.proto"; import "envoy/config/core/v3/base.proto"; +import "envoy/config/core/v3/extension.proto"; import "envoy/extensions/transport_sockets/tls/v3/tls.proto"; import "envoy/type/v3/range.proto"; @@ -24,6 +25,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: Listener components] // Listener :ref:`configuration overview ` +// [#next-free-field: 6] message Filter { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.listener.Filter"; @@ -33,11 +35,17 @@ message Filter { // :ref:`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. oneof config_type { + // Filter specific configuration which depends on the filter being + // instantiated. See the supported filters for further documentation. google.protobuf.Any typed_config = 4; + // Configuration source specifier for an extension configuration discovery + // service. In case of a failure and without the default configuration, the + // listener closes the connections. + // [#not-implemented-hide:] + core.v3.ExtensionConfigSource config_discovery = 5; + google.protobuf.Struct hidden_envoy_deprecated_config = 2 [deprecated = true]; } } diff --git a/generated_api_shadow/envoy/config/listener/v4alpha/listener_components.proto b/generated_api_shadow/envoy/config/listener/v4alpha/listener_components.proto index 7cc1956a1b42..9e95e23542d0 100644 --- a/generated_api_shadow/envoy/config/listener/v4alpha/listener_components.proto +++ b/generated_api_shadow/envoy/config/listener/v4alpha/listener_components.proto @@ -4,6 +4,7 @@ package envoy.config.listener.v4alpha; import "envoy/config/core/v4alpha/address.proto"; import "envoy/config/core/v4alpha/base.proto"; +import "envoy/config/core/v4alpha/extension.proto"; import "envoy/type/v3/range.proto"; import "google/protobuf/any.proto"; @@ -23,6 +24,7 @@ option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSIO // [#protodoc-title: Listener components] // Listener :ref:`configuration overview ` +// [#next-free-field: 6] message Filter { option (udpa.annotations.versioning).previous_message_type = "envoy.config.listener.v3.Filter"; @@ -34,10 +36,16 @@ message Filter { // :ref:`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. oneof config_type { + // Filter specific configuration which depends on the filter being + // instantiated. See the supported filters for further documentation. google.protobuf.Any typed_config = 4; + + // Configuration source specifier for an extension configuration discovery + // service. In case of a failure and without the default configuration, the + // listener closes the connections. + // [#not-implemented-hide:] + core.v4alpha.ExtensionConfigSource config_discovery = 5; } } diff --git a/generated_api_shadow/envoy/config/retry/omit_canary_hosts/v3/BUILD b/generated_api_shadow/envoy/config/retry/omit_canary_hosts/v3/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/generated_api_shadow/envoy/config/retry/omit_canary_hosts/v3/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/retry/omit_canary_hosts/v3/omit_canary_hosts.proto b/generated_api_shadow/envoy/config/retry/omit_canary_hosts/v3/omit_canary_hosts.proto new file mode 100644 index 000000000000..fe928ba733db --- /dev/null +++ b/generated_api_shadow/envoy/config/retry/omit_canary_hosts/v3/omit_canary_hosts.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package envoy.config.retry.omit_canary_hosts.v3; + +import "udpa/annotations/status.proto"; + +option java_package = "io.envoyproxy.envoy.config.retry.omit_canary_hosts.v3"; +option java_outer_classname = "OmitCanaryHostsProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Omit Canary Hosts Predicate] +// [#extension: envoy.retry_host_predicates.omit_canary_hosts] + +message OmitCanaryHostsPredicate { +} diff --git a/generated_api_shadow/envoy/config/route/v3/route_components.proto b/generated_api_shadow/envoy/config/route/v3/route_components.proto index bfb296e47836..afdd534094e3 100644 --- a/generated_api_shadow/envoy/config/route/v3/route_components.proto +++ b/generated_api_shadow/envoy/config/route/v3/route_components.proto @@ -1553,6 +1553,7 @@ message VirtualCluster { } // Global rate limiting :ref:`architecture overview `. +// Also applies to Local rate limiting :ref:`using descriptors `. message RateLimit { option (udpa.annotations.versioning).previous_message_type = "envoy.api.v2.route.RateLimit"; diff --git a/generated_api_shadow/envoy/config/route/v4alpha/route_components.proto b/generated_api_shadow/envoy/config/route/v4alpha/route_components.proto index 586527865d5b..5b904936572a 100644 --- a/generated_api_shadow/envoy/config/route/v4alpha/route_components.proto +++ b/generated_api_shadow/envoy/config/route/v4alpha/route_components.proto @@ -1557,6 +1557,7 @@ message VirtualCluster { } // Global rate limiting :ref:`architecture overview `. +// Also applies to Local rate limiting :ref:`using descriptors `. message RateLimit { option (udpa.annotations.versioning).previous_message_type = "envoy.config.route.v3.RateLimit"; diff --git a/generated_api_shadow/envoy/extensions/common/dynamic_forward_proxy/v3/dns_cache.proto b/generated_api_shadow/envoy/extensions/common/dynamic_forward_proxy/v3/dns_cache.proto index 5579cc16bd97..4b182a29711b 100644 --- a/generated_api_shadow/envoy/extensions/common/dynamic_forward_proxy/v3/dns_cache.proto +++ b/generated_api_shadow/envoy/extensions/common/dynamic_forward_proxy/v3/dns_cache.proto @@ -92,8 +92,7 @@ message DnsCacheConfig { config.cluster.v3.Cluster.RefreshRate dns_failure_refresh_rate = 6; // The config of circuit breakers for resolver. It provides a configurable threshold. - // If `envoy.reloadable_features.enable_dns_cache_circuit_breakers` is enabled, - // envoy will use dns cache circuit breakers with default settings even if this value is not set. + // Envoy will use dns cache circuit breakers with default settings even if this value is not set. DnsCacheCircuitBreakers dns_cache_circuit_breaker = 7; // [#next-major-version: Reconcile DNS options in a single message.] diff --git a/generated_api_shadow/envoy/extensions/common/ratelimit/v3/ratelimit.proto b/generated_api_shadow/envoy/extensions/common/ratelimit/v3/ratelimit.proto index 30efa6026218..6bb771d25af9 100644 --- a/generated_api_shadow/envoy/extensions/common/ratelimit/v3/ratelimit.proto +++ b/generated_api_shadow/envoy/extensions/common/ratelimit/v3/ratelimit.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package envoy.extensions.common.ratelimit.v3; import "envoy/type/v3/ratelimit_unit.proto"; +import "envoy/type/v3/token_bucket.proto"; import "udpa/annotations/status.proto"; import "udpa/annotations/versioning.proto"; @@ -92,3 +93,11 @@ message RateLimitDescriptor { // Optional rate limit override to supply to the ratelimit service. RateLimitOverride limit = 2; } + +message LocalRateLimitDescriptor { + // Descriptor entries. + repeated v3.RateLimitDescriptor.Entry entries = 1 [(validate.rules).repeated = {min_items: 1}]; + + // Token Bucket algorithm for local ratelimiting. + type.v3.TokenBucket token_bucket = 2 [(validate.rules).message = {required: true}]; +} diff --git a/generated_api_shadow/envoy/extensions/filters/common/dependency/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/common/dependency/v3/BUILD new file mode 100644 index 000000000000..ee92fb652582 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/common/dependency/v3/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/extensions/filters/common/dependency/v3/dependency.proto b/generated_api_shadow/envoy/extensions/filters/common/dependency/v3/dependency.proto new file mode 100644 index 000000000000..c21408dc0701 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/common/dependency/v3/dependency.proto @@ -0,0 +1,45 @@ +syntax = "proto3"; + +package envoy.extensions.filters.common.dependency.v3; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.common.dependency.v3"; +option java_outer_classname = "DependencyProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Filter dependency specification] + +// Dependency specification and string identifier. +message Dependency { + enum DependencyType { + HEADER = 0; + FILTER_STATE_KEY = 1; + DYNAMIC_METADATA = 2; + } + + // The kind of dependency. + DependencyType type = 1; + + // The string identifier for the dependency. + string name = 2 [(validate.rules).string = {min_len: 1}]; +} + +// Dependency specification for a filter. For a filter chain to be valid, any +// dependency that is required must be provided by an earlier filter. +message FilterDependencies { + // A list of dependencies required on the decode path. + repeated Dependency decode_required = 1; + + // A list of dependencies provided on the encode path. + repeated Dependency decode_provided = 2; + + // A list of dependencies required on the decode path. + repeated Dependency encode_required = 3; + + // A list of dependencies provided on the encode path. + repeated Dependency encode_provided = 4; +} diff --git a/generated_api_shadow/envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.proto b/generated_api_shadow/envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.proto index 1e64376add9b..ad5ab5d507bb 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.proto @@ -21,7 +21,22 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#extension: envoy.filters.http.ext_proc] // The External Processing filter allows an external service to act on HTTP traffic in a flexible way. -// It communicates with an external gRPC service that can use it to do a variety of things + +// **Current Implementation Status:** +// At this time, the filter will send the "request_headers" and "response_headers" messages +// to the server when the filter is invoked, and apply any header mutations returned by the +// server, and respond to "immediate_response" messages. No other parts of the protocol are implemented yet. + +// As designed, the filter supports up to six different processing steps, which are in the +// process of being implemented: +// * Request headers: IMPLEMENTED +// * Request body: NOT IMPLEMENTED +// * Request trailers: NOT IMPLEMENTED +// * Response headers: IMPLEMENTED +// * Response body: NOT IMPLEMENTED +// * Response trailers: NOT IMPLEMENTED + +// The filter communicates with an external gRPC service that can use it to do a variety of things // with the request and response: // // * Access and modify the HTTP headers on the request, response, or both @@ -63,7 +78,6 @@ message ExternalProcessor { // The filter supports both the "Envoy" and "Google" gRPC clients. config.core.v3.GrpcService grpc_service = 1; - // [#not-implemented-hide:] // By default, if the gRPC stream cannot be established, or if it is closed // prematurely with an error, the filter will fail. Specifically, if the // response headers have not yet been delivered, then it will return a 500 diff --git a/generated_api_shadow/envoy/extensions/filters/http/local_ratelimit/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/http/local_ratelimit/v3/BUILD index ad2fc9a9a84f..6c58a43e4ff6 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/local_ratelimit/v3/BUILD +++ b/generated_api_shadow/envoy/extensions/filters/http/local_ratelimit/v3/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ "//envoy/config/core/v3:pkg", + "//envoy/extensions/common/ratelimit/v3:pkg", "//envoy/type/v3:pkg", "@com_github_cncf_udpa//udpa/annotations:pkg", ], diff --git a/generated_api_shadow/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto b/generated_api_shadow/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto index 94f21edd3eed..546ff26bac79 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package envoy.extensions.filters.http.local_ratelimit.v3; import "envoy/config/core/v3/base.proto"; +import "envoy/extensions/common/ratelimit/v3/ratelimit.proto"; import "envoy/type/v3/http_status.proto"; import "envoy/type/v3/token_bucket.proto"; @@ -19,7 +20,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // Local Rate limit :ref:`configuration overview `. // [#extension: envoy.filters.http.local_ratelimit] -// [#next-free-field: 7] +// [#next-free-field: 10] message LocalRateLimit { // The human readable prefix to use when emitting stats. string stat_prefix = 1 [(validate.rules).string = {min_len: 1}]; @@ -67,4 +68,28 @@ message LocalRateLimit { // have been rate limited. repeated config.core.v3.HeaderValueOption response_headers_to_add = 6 [(validate.rules).repeated = {max_items: 10}]; + + // The rate limit descriptor list to use in the local rate limit to override + // on. The rate limit descriptor is selected by the first full match from the + // request descriptors. + // + // Example on how to use ::ref:`this ` + // + // .. note:: + // + // In the current implementation the descriptor's token bucket :ref:`fill_interval + // ` must be a multiple + // global :ref:`token bucket's` fill interval. + // + // The descriptors must match verbatim for rate limiting to apply. There is no partial + // match by a subset of descriptor entries in the current implementation. + repeated common.ratelimit.v3.LocalRateLimitDescriptor descriptors = 8; + + // Specifies the rate limit configurations to be applied with the same + // stage number. If not set, the default stage number is 0. + // + // .. note:: + // + // The filter supports a range of 0 - 10 inclusively for stage numbers. + uint32 stage = 9 [(validate.rules).uint32 = {lte: 10}]; } diff --git a/generated_api_shadow/envoy/extensions/filters/http/oauth2/v3alpha/oauth.proto b/generated_api_shadow/envoy/extensions/filters/http/oauth2/v3alpha/oauth.proto index e4be64167ed2..11fe12695865 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/oauth2/v3alpha/oauth.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/oauth2/v3alpha/oauth.proto @@ -44,7 +44,7 @@ message OAuth2Credentials { // OAuth config // -// [#next-free-field: 9] +// [#next-free-field: 10] message OAuth2Config { // Endpoint on the authorization server to retrieve the access token from. config.core.v3.HttpUri token_endpoint = 1; @@ -74,6 +74,11 @@ message OAuth2Config { // Any request that matches any of the provided matchers will be passed through without OAuth validation. repeated config.route.v3.HeaderMatcher pass_through_matcher = 8; + + // Optional list of OAuth scopes to be claimed in the authorization request. If not specified, + // defaults to "user" scope. + // OAuth RFC https://tools.ietf.org/html/rfc6749#section-3.3 + repeated string auth_scopes = 9; } // Filter config. diff --git a/generated_api_shadow/envoy/extensions/filters/http/oauth2/v4alpha/oauth.proto b/generated_api_shadow/envoy/extensions/filters/http/oauth2/v4alpha/oauth.proto index ee51e1f96099..af1f0944ed34 100644 --- a/generated_api_shadow/envoy/extensions/filters/http/oauth2/v4alpha/oauth.proto +++ b/generated_api_shadow/envoy/extensions/filters/http/oauth2/v4alpha/oauth.proto @@ -47,7 +47,7 @@ message OAuth2Credentials { // OAuth config // -// [#next-free-field: 9] +// [#next-free-field: 10] message OAuth2Config { option (udpa.annotations.versioning).previous_message_type = "envoy.extensions.filters.http.oauth2.v3alpha.OAuth2Config"; @@ -80,6 +80,11 @@ message OAuth2Config { // Any request that matches any of the provided matchers will be passed through without OAuth validation. repeated config.route.v4alpha.HeaderMatcher pass_through_matcher = 8; + + // Optional list of OAuth scopes to be claimed in the authorization request. If not specified, + // defaults to "user" scope. + // OAuth RFC https://tools.ietf.org/html/rfc6749#section-3.3 + repeated string auth_scopes = 9; } // Filter config. diff --git a/generated_api_shadow/envoy/extensions/filters/network/thrift_proxy/router/v3/BUILD b/generated_api_shadow/envoy/extensions/filters/network/thrift_proxy/router/v3/BUILD new file mode 100644 index 000000000000..c24f669b9bbd --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/thrift_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/thrift/router/v2alpha1:pkg", + "@com_github_cncf_udpa//udpa/annotations:pkg", + ], +) diff --git a/generated_api_shadow/envoy/extensions/filters/network/thrift_proxy/router/v3/router.proto b/generated_api_shadow/envoy/extensions/filters/network/thrift_proxy/router/v3/router.proto new file mode 100644 index 000000000000..860622cb61e4 --- /dev/null +++ b/generated_api_shadow/envoy/extensions/filters/network/thrift_proxy/router/v3/router.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package envoy.extensions.filters.network.thrift_proxy.router.v3; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; + +option java_package = "io.envoyproxy.envoy.extensions.filters.network.thrift_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] +// Thrift router :ref:`configuration overview `. +// [#extension: envoy.filters.thrift.router] + +message Router { + option (udpa.annotations.versioning).previous_message_type = + "envoy.config.filter.thrift.router.v2alpha1.Router"; +} diff --git a/generated_api_shadow/envoy/service/ext_proc/v3alpha/external_processor.proto b/generated_api_shadow/envoy/service/ext_proc/v3alpha/external_processor.proto index cac1dde34d9e..5b0696bfc3b0 100644 --- a/generated_api_shadow/envoy/service/ext_proc/v3alpha/external_processor.proto +++ b/generated_api_shadow/envoy/service/ext_proc/v3alpha/external_processor.proto @@ -6,7 +6,6 @@ import "envoy/config/core/v3/base.proto"; import "envoy/extensions/filters/http/ext_proc/v3alpha/processing_mode.proto"; import "envoy/type/v3/http_status.proto"; -import "google/protobuf/duration.proto"; import "google/protobuf/struct.proto"; import "udpa/annotations/status.proto"; @@ -289,10 +288,13 @@ message GrpcStatus { // Change HTTP headers or trailers by appending, replacing, or removing // headers. message HeaderMutation { - // Add or replace HTTP headers. + // Add or replace HTTP headers. Attempts to set the value of + // any "x-envoy" header, and attempts to set the ":method", + // ":authority", ":scheme", or "host" headers will be ignored. repeated config.core.v3.HeaderValueOption set_headers = 1; - // Remove these HTTP headers. + // Remove these HTTP headers. Attempts to remove system headers -- + // any header starting with ":", plus "host" -- will be ignored. repeated string remove_headers = 2; } diff --git a/generated_api_shadow/envoy/type/matcher/v3/http_inputs.proto b/generated_api_shadow/envoy/type/matcher/v3/http_inputs.proto new file mode 100644 index 000000000000..48eb4c43154a --- /dev/null +++ b/generated_api_shadow/envoy/type/matcher/v3/http_inputs.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package envoy.type.matcher.v3; + +import "udpa/annotations/migrate.proto"; +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type.matcher.v3"; +option java_outer_classname = "HttpInputsProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = ACTIVE; + +// [#protodoc-title: Common HTTP Inputs] + +// Match input indicates that matching should be done on a specific request header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the request contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpRequestHeaderMatchInput { + // The request header to match on. + string header_name = 1; +} + +// Match input indicating that matching should be done on a specific response header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the response contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpResponseHeaderMatchInput { + // The response header to match on. + string header_name = 1; +} diff --git a/generated_api_shadow/envoy/type/matcher/v4alpha/http_inputs.proto b/generated_api_shadow/envoy/type/matcher/v4alpha/http_inputs.proto new file mode 100644 index 000000000000..f15e9ceb70e2 --- /dev/null +++ b/generated_api_shadow/envoy/type/matcher/v4alpha/http_inputs.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package envoy.type.matcher.v4alpha; + +import "udpa/annotations/status.proto"; +import "udpa/annotations/versioning.proto"; +import "validate/validate.proto"; + +option java_package = "io.envoyproxy.envoy.type.matcher.v4alpha"; +option java_outer_classname = "HttpInputsProto"; +option java_multiple_files = true; +option (udpa.annotations.file_status).package_version_status = NEXT_MAJOR_VERSION_CANDIDATE; + +// [#protodoc-title: Common HTTP Inputs] + +// Match input indicates that matching should be done on a specific request header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the request contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpRequestHeaderMatchInput { + option (udpa.annotations.versioning).previous_message_type = + "envoy.type.matcher.v3.HttpRequestHeaderMatchInput"; + + // The request header to match on. + string header_name = 1; +} + +// Match input indicating that matching should be done on a specific response header. +// The resulting input string will be all headers for the given key joined by a comma, +// e.g. if the response contains two 'foo' headers with value 'bar' and 'baz', the input +// string will be 'bar,baz'. +// [#comment:TODO(snowp): Link to unified matching docs.] +message HttpResponseHeaderMatchInput { + option (udpa.annotations.versioning).previous_message_type = + "envoy.type.matcher.v3.HttpResponseHeaderMatchInput"; + + // The response header to match on. + string header_name = 1; +} diff --git a/include/envoy/api/BUILD b/include/envoy/api/BUILD index 2d9c47ddc0b6..36736838c6d8 100644 --- a/include/envoy/api/BUILD +++ b/include/envoy/api/BUILD @@ -14,6 +14,7 @@ envoy_cc_library( deps = [ "//include/envoy/common:random_generator_interface", "//include/envoy/event:dispatcher_interface", + "//include/envoy/event:scaled_range_timer_manager_interface", "//include/envoy/filesystem:filesystem_interface", "//include/envoy/server:process_context_interface", "//include/envoy/thread:thread_interface", diff --git a/include/envoy/api/api.h b/include/envoy/api/api.h index aa310d2c40c2..89336aaf5734 100644 --- a/include/envoy/api/api.h +++ b/include/envoy/api/api.h @@ -6,6 +6,7 @@ #include "envoy/common/random_generator.h" #include "envoy/common/time.h" #include "envoy/event/dispatcher.h" +#include "envoy/event/scaled_range_timer_manager.h" #include "envoy/filesystem/filesystem.h" #include "envoy/server/process_context.h" #include "envoy/stats/store.h" @@ -30,6 +31,18 @@ class Api { */ virtual Event::DispatcherPtr allocateDispatcher(const std::string& name) PURE; + /** + * Allocate a dispatcher. + * @param name the identity name for a dispatcher, e.g. "worker_2" or "main_thread". + * This name will appear in per-handler/worker statistics, such as + * "server.worker_2.watchdog_miss". + * @param scaled_timer_factory the factory to use when creating the scaled timer manager. + * @return Event::DispatcherPtr which is owned by the caller. + */ + virtual Event::DispatcherPtr + allocateDispatcher(const std::string& name, + const Event::ScaledRangeTimerManagerFactory& scaled_timer_factory) PURE; + /** * Allocate a dispatcher. * @param name the identity name for a dispatcher, e.g. "worker_2" or "main_thread". diff --git a/include/envoy/common/BUILD b/include/envoy/common/BUILD index d120b7c0cc54..f831dcd62708 100644 --- a/include/envoy/common/BUILD +++ b/include/envoy/common/BUILD @@ -38,6 +38,7 @@ envoy_cc_library( envoy_cc_library( name = "random_generator_interface", hdrs = ["random_generator.h"], + deps = ["//source/common/common:interval_value"], ) envoy_cc_library( diff --git a/include/envoy/common/random_generator.h b/include/envoy/common/random_generator.h index f778d909a49b..ed4c079d2fb0 100644 --- a/include/envoy/common/random_generator.h +++ b/include/envoy/common/random_generator.h @@ -6,6 +6,8 @@ #include "envoy/common/pure.h" +#include "common/common/interval_value.h" + namespace Envoy { namespace Random { @@ -50,13 +52,13 @@ class RandomGenerator { /** * @return a random boolean value, with probability `p` equaling true. */ - bool bernoulli(float p) { - if (p <= 0) { + bool bernoulli(UnitFloat p) { + if (p == UnitFloat::min()) { return false; - } else if (p >= 1) { + } else if (p == UnitFloat::max()) { return true; } - return random() < static_cast(p * static_cast(max())); + return random() < static_cast(p.value() * static_cast(max())); } }; diff --git a/include/envoy/config/BUILD b/include/envoy/config/BUILD index 4197cd1c5ed8..be598953175c 100644 --- a/include/envoy/config/BUILD +++ b/include/envoy/config/BUILD @@ -29,6 +29,12 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "context_provider_interface", + hdrs = ["context_provider.h"], + deps = ["@com_github_cncf_udpa//xds/core/v3:pkg_cc_proto"], +) + envoy_cc_library( name = "extension_config_provider_interface", hdrs = ["extension_config_provider.h"], diff --git a/include/envoy/config/context_provider.h b/include/envoy/config/context_provider.h new file mode 100644 index 000000000000..a1d68d6480d8 --- /dev/null +++ b/include/envoy/config/context_provider.h @@ -0,0 +1,26 @@ +#pragma once + +#include "envoy/common/pure.h" + +#include "xds/core/v3/context_params.pb.h" + +namespace Envoy { +namespace Config { + +/** + * A provider for xDS context parameters. These are currently derived from the bootstrap, but will + * be set dynamically at runtime in the near future as we add support for dynamic context parameter + * discovery and updates. + */ +class ContextProvider { +public: + virtual ~ContextProvider() = default; + + /** + * @return const xds::core::v3::ContextParams& node-level context parameters. + */ + virtual const xds::core::v3::ContextParams& nodeContext() const PURE; +}; + +} // namespace Config +} // namespace Envoy diff --git a/include/envoy/config/grpc_mux.h b/include/envoy/config/grpc_mux.h index 43725cb30233..fc1c603b74ac 100644 --- a/include/envoy/config/grpc_mux.h +++ b/include/envoy/config/grpc_mux.h @@ -42,7 +42,7 @@ class GrpcMuxWatch { * Updates the set of resources that the watch is interested in. * @param resources set of resource names to watch for */ - virtual void update(const std::set& resources) PURE; + virtual void update(const absl::flat_hash_set& resources) PURE; }; using GrpcMuxWatchPtr = std::unique_ptr; @@ -100,13 +100,13 @@ class GrpcMux { * away, its EDS updates should be cancelled by destroying the GrpcMuxWatchPtr. */ virtual GrpcMuxWatchPtr addWatch(const std::string& type_url, - const std::set& resources, + const absl::flat_hash_set& resources, SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder, const bool use_namespace_matching) PURE; virtual void requestOnDemandUpdate(const std::string& type_url, - const std::set& for_update) PURE; + const absl::flat_hash_set& for_update) PURE; using TypeUrlMap = absl::flat_hash_map; static TypeUrlMap& typeUrlMap() { MUTABLE_CONSTRUCT_ON_FIRST_USE(TypeUrlMap, {}); } diff --git a/include/envoy/config/subscription.h b/include/envoy/config/subscription.h index e768482f2ee5..05e1967f3e9d 100644 --- a/include/envoy/config/subscription.h +++ b/include/envoy/config/subscription.h @@ -179,24 +179,21 @@ class Subscription { * Start a configuration subscription asynchronously. This should be called once and will continue * to fetch throughout the lifetime of the Subscription object. * @param resources set of resource names to fetch. - * @param use_namespace_matching if the subscription is for a collection of resources. In such a - * case a namespace watch will be created. */ - virtual void start(const std::set& resource_names, - const bool use_namespace_matching = false) PURE; + virtual void start(const absl::flat_hash_set& resource_names) PURE; /** * Update the resources to fetch. - * @param resources vector of resource names to fetch. It's a (not unordered_)set so that it can - * be passed to std::set_difference, which must be given sorted collections. + * @param resources vector of resource names to fetch. */ - virtual void updateResourceInterest(const std::set& update_to_these_names) PURE; + virtual void + updateResourceInterest(const absl::flat_hash_set& update_to_these_names) PURE; /** * Creates a discovery request for resources. * @param add_these_names resource ids for inclusion in the discovery request. */ - virtual void requestOnDemandUpdate(const std::set& add_these_names) PURE; + virtual void requestOnDemandUpdate(const absl::flat_hash_set& add_these_names) PURE; }; using SubscriptionPtr = std::unique_ptr; @@ -204,7 +201,7 @@ using SubscriptionPtr = std::unique_ptr; /** * Per subscription stats. @see stats_macros.h */ -#define ALL_SUBSCRIPTION_STATS(COUNTER, GAUGE, TEXT_READOUT) \ +#define ALL_SUBSCRIPTION_STATS(COUNTER, GAUGE, TEXT_READOUT, HISTOGRAM) \ COUNTER(init_fetch_timeout) \ COUNTER(update_attempt) \ COUNTER(update_failure) \ @@ -212,6 +209,7 @@ using SubscriptionPtr = std::unique_ptr; COUNTER(update_success) \ GAUGE(update_time, NeverImport) \ GAUGE(version, NeverImport) \ + HISTOGRAM(update_duration, Milliseconds) \ TEXT_READOUT(version_text) /** @@ -219,7 +217,7 @@ using SubscriptionPtr = std::unique_ptr; */ struct SubscriptionStats { ALL_SUBSCRIPTION_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT, - GENERATE_TEXT_READOUT_STRUCT) + GENERATE_TEXT_READOUT_STRUCT, GENERATE_HISTOGRAM_STRUCT) }; } // namespace Config diff --git a/include/envoy/config/subscription_factory.h b/include/envoy/config/subscription_factory.h index ef475252f4a7..8ad7473599a1 100644 --- a/include/envoy/config/subscription_factory.h +++ b/include/envoy/config/subscription_factory.h @@ -22,14 +22,14 @@ class SubscriptionFactory { * @param callbacks the callbacks needed by all Subscription objects, to deliver config updates. * The callbacks must not result in the deletion of the Subscription object. * @param resource_decoder how incoming opaque resource objects are to be decoded. + * @param use_namespace_matching whether to use namespace match semantics on subscription. * * @return SubscriptionPtr subscription object corresponding for config and type_url. */ - virtual SubscriptionPtr - subscriptionFromConfigSource(const envoy::config::core::v3::ConfigSource& config, - absl::string_view type_url, Stats::Scope& scope, - SubscriptionCallbacks& callbacks, - OpaqueResourceDecoder& resource_decoder) PURE; + virtual SubscriptionPtr subscriptionFromConfigSource( + const envoy::config::core::v3::ConfigSource& config, absl::string_view type_url, + Stats::Scope& scope, SubscriptionCallbacks& callbacks, + OpaqueResourceDecoder& resource_decoder, bool use_namespace_matching) PURE; /** * Collection subscription factory interface for xDS-TP URLs. diff --git a/include/envoy/event/BUILD b/include/envoy/event/BUILD index a4c865a2d225..91d13f4d113c 100644 --- a/include/envoy/event/BUILD +++ b/include/envoy/event/BUILD @@ -19,11 +19,12 @@ envoy_cc_library( deps = [ ":deferred_deletable", ":file_event_interface", + ":scaled_timer", ":schedulable_cb_interface", ":signal_interface", + ":timer_interface", "//include/envoy/common:scope_tracker_interface", "//include/envoy/common:time_interface", - "//include/envoy/event:timer_interface", "//include/envoy/filesystem:watcher_interface", "//include/envoy/network:connection_handler_interface", "//include/envoy/network:connection_interface", @@ -42,8 +43,8 @@ envoy_cc_library( ) envoy_cc_library( - name = "scaled_timer_minimum", - hdrs = ["scaled_timer_minimum.h"], + name = "scaled_timer", + hdrs = ["scaled_timer.h"], deps = [ "//source/common/common:interval_value", ], @@ -53,7 +54,7 @@ envoy_cc_library( name = "scaled_range_timer_manager_interface", hdrs = ["scaled_range_timer_manager.h"], deps = [ - ":scaled_timer_minimum", + ":scaled_timer", ":timer_interface", ], ) diff --git a/include/envoy/event/dispatcher.h b/include/envoy/event/dispatcher.h index 599617e87d28..36e34f78bd1a 100644 --- a/include/envoy/event/dispatcher.h +++ b/include/envoy/event/dispatcher.h @@ -9,6 +9,7 @@ #include "envoy/common/scope_tracker.h" #include "envoy/common/time.h" #include "envoy/event/file_event.h" +#include "envoy/event/scaled_timer.h" #include "envoy/event/schedulable_cb.h" #include "envoy/event/signal.h" #include "envoy/event/timer.h" @@ -77,6 +78,20 @@ class DispatcherBase { */ virtual Event::TimerPtr createTimer(TimerCb cb) PURE; + /** + * Allocates a scaled timer. @see Timer for docs on how to use the timer. + * @param timer_type the type of timer to create. + * @param cb supplies the callback to invoke when the timer fires. + */ + virtual Event::TimerPtr createScaledTimer(Event::ScaledTimerType timer_type, TimerCb cb) PURE; + + /** + * Allocates a scaled timer. @see Timer for docs on how to use the timer. + * @param minimum the rule for computing the minimum value of the timer. + * @param cb supplies the callback to invoke when the timer fires. + */ + virtual Event::TimerPtr createScaledTimer(Event::ScaledTimerMinimum minimum, TimerCb cb) PURE; + /** * Allocates a schedulable callback. @see SchedulableCallback for docs on how to use the wrapped * callback. @@ -86,15 +101,18 @@ class DispatcherBase { virtual Event::SchedulableCallbackPtr createSchedulableCallback(std::function cb) PURE; /** - * Sets a tracked object, which is currently operating in this Dispatcher. - * This should be cleared with another call to setTrackedObject() when the object is done doing - * work. Calling setTrackedObject(nullptr) results in no object being tracked. - * - * This is optimized for performance, to avoid allocation where we do scoped object tracking. + * Appends a tracked object to the current stack of tracked objects operating + * in the dispatcher. * - * @return The previously tracked object or nullptr if there was none. + * It's recommended to use ScopeTrackerScopeState to manage the object's tracking. If directly + * invoking, there needs to be a subsequent call to popTrackedObject(). + */ + virtual void pushTrackedObject(const ScopeTrackedObject* object) PURE; + + /** + * Removes the top of the stack of tracked object and asserts that it was expected. */ - virtual const ScopeTrackedObject* setTrackedObject(const ScopeTrackedObject* object) PURE; + virtual void popTrackedObject(const ScopeTrackedObject* expected_object) PURE; /** * Validates that an operation is thread-safe with respect to this dispatcher; i.e. that the diff --git a/include/envoy/event/scaled_range_timer_manager.h b/include/envoy/event/scaled_range_timer_manager.h index 7defc1b45be7..8b91ec5758fc 100644 --- a/include/envoy/event/scaled_range_timer_manager.h +++ b/include/envoy/event/scaled_range_timer_manager.h @@ -1,9 +1,11 @@ #pragma once #include "envoy/common/pure.h" -#include "envoy/event/scaled_timer_minimum.h" +#include "envoy/event/scaled_timer.h" #include "envoy/event/timer.h" +#include "common/common/interval_value.h" + #include "absl/types/variant.h" namespace Envoy { @@ -28,6 +30,12 @@ class ScaledRangeTimerManager { */ virtual TimerPtr createTimer(ScaledTimerMinimum minimum, TimerCb callback) PURE; + /** + * Creates a new timer backed by the manager using the provided timer type to + * determine the minimum. + */ + virtual TimerPtr createTimer(ScaledTimerType timer_type, TimerCb callback) PURE; + /** * Sets the scale factor for all timers created through this manager. The value should be between * 0 and 1, inclusive. The scale factor affects the amount of time timers spend in their target @@ -36,10 +44,13 @@ class ScaledRangeTimerManager { * factor of 0.5 causes firing halfway between min and max, and a factor of 1 causes firing at * max. */ - virtual void setScaleFactor(double scale_factor) PURE; + virtual void setScaleFactor(UnitFloat scale_factor) PURE; }; using ScaledRangeTimerManagerPtr = std::unique_ptr; +class Dispatcher; +using ScaledRangeTimerManagerFactory = std::function; + } // namespace Event } // namespace Envoy diff --git a/include/envoy/event/scaled_timer.h b/include/envoy/event/scaled_timer.h new file mode 100644 index 000000000000..63ac8a5f421e --- /dev/null +++ b/include/envoy/event/scaled_timer.h @@ -0,0 +1,84 @@ +#pragma once + +#include + +#include "common/common/interval_value.h" + +#include "absl/container/flat_hash_map.h" +#include "absl/types/variant.h" + +namespace Envoy { +namespace Event { + +/** + * Describes a minimum timer value that is equal to a scale factor applied to the maximum. + */ +struct ScaledMinimum { + explicit constexpr ScaledMinimum(UnitFloat scale_factor) : scale_factor_(scale_factor) {} + inline bool operator==(const ScaledMinimum& other) const { + return other.scale_factor_.value() == scale_factor_.value(); + } + const UnitFloat scale_factor_; +}; + +/** + * Describes a minimum timer value that is an absolute duration. + */ +struct AbsoluteMinimum { + explicit constexpr AbsoluteMinimum(std::chrono::milliseconds value) : value_(value) {} + inline bool operator==(const AbsoluteMinimum& other) const { return other.value_ == value_; } + const std::chrono::milliseconds value_; +}; + +/** + * Class that describes how to compute a minimum timeout given a maximum timeout value. It wraps + * ScaledMinimum and AbsoluteMinimum and provides a single computeMinimum() method. + */ +class ScaledTimerMinimum { +public: + // Forward arguments to impl_'s constructor. + constexpr ScaledTimerMinimum(AbsoluteMinimum arg) : impl_(arg) {} + constexpr ScaledTimerMinimum(ScaledMinimum arg) : impl_(arg) {} + + // Computes the minimum value for a given maximum timeout. If this object was constructed with a + // - ScaledMinimum value: + // the return value is the scale factor applied to the provided maximum. + // - AbsoluteMinimum: + // the return value is that minimum, and the provided maximum is ignored. + std::chrono::milliseconds computeMinimum(std::chrono::milliseconds maximum) const { + struct Visitor { + explicit Visitor(std::chrono::milliseconds value) : value_(value) {} + std::chrono::milliseconds operator()(ScaledMinimum scale_factor) { + return std::chrono::duration_cast( + scale_factor.scale_factor_.value() * value_); + } + std::chrono::milliseconds operator()(AbsoluteMinimum absolute_value) { + return absolute_value.value_; + } + const std::chrono::milliseconds value_; + }; + return absl::visit(Visitor(maximum), impl_); + } + + inline bool operator==(const ScaledTimerMinimum& other) const { return impl_ == other.impl_; } + +private: + absl::variant impl_; +}; + +enum class ScaledTimerType { + // Timers created with this type will never be scaled. This should only be used for testing. + UnscaledRealTimerForTest, + // The amount of time an HTTP connection to a downstream client can remain idle (no streams). This + // corresponds to the HTTP_DOWNSTREAM_CONNECTION_IDLE TimerType in overload.proto. + HttpDownstreamIdleConnectionTimeout, + // The amount of time an HTTP stream from a downstream client can remain idle. This corresponds to + // the HTTP_DOWNSTREAM_STREAM_IDLE TimerType in overload.proto. + HttpDownstreamIdleStreamTimeout, +}; + +using ScaledTimerTypeMap = absl::flat_hash_map; +using ScaledTimerTypeMapConstSharedPtr = std::shared_ptr; + +} // namespace Event +} // namespace Envoy diff --git a/include/envoy/event/scaled_timer_minimum.h b/include/envoy/event/scaled_timer_minimum.h deleted file mode 100644 index edf3b1777b7f..000000000000 --- a/include/envoy/event/scaled_timer_minimum.h +++ /dev/null @@ -1,60 +0,0 @@ -#pragma once - -#include - -#include "common/common/interval_value.h" - -#include "absl/types/variant.h" - -namespace Envoy { -namespace Event { - -/** - * Describes a minimum timer value that is equal to a scale factor applied to the maximum. - */ -struct ScaledMinimum { - explicit ScaledMinimum(UnitFloat scale_factor) : scale_factor_(scale_factor) {} - const UnitFloat scale_factor_; -}; - -/** - * Describes a minimum timer value that is an absolute duration. - */ -struct AbsoluteMinimum { - explicit AbsoluteMinimum(std::chrono::milliseconds value) : value_(value) {} - const std::chrono::milliseconds value_; -}; - -/** - * Class that describes how to compute a minimum timeout given a maximum timeout value. It wraps - * ScaledMinimum and AbsoluteMinimum and provides a single computeMinimum() method. - */ -class ScaledTimerMinimum : private absl::variant { -public: - // Use base class constructor. - using absl::variant::variant; - - // Computes the minimum value for a given maximum timeout. If this object was constructed with a - // - ScaledMinimum value: - // the return value is the scale factor applied to the provided maximum. - // - AbsoluteMinimum: - // the return value is that minimum, and the provided maximum is ignored. - std::chrono::milliseconds computeMinimum(std::chrono::milliseconds maximum) const { - struct Visitor { - explicit Visitor(std::chrono::milliseconds value) : value_(value) {} - std::chrono::milliseconds operator()(ScaledMinimum scale_factor) { - return std::chrono::duration_cast( - scale_factor.scale_factor_.value() * value_); - } - std::chrono::milliseconds operator()(AbsoluteMinimum absolute_value) { - return absolute_value.value_; - } - const std::chrono::milliseconds value_; - }; - return absl::visit&>( - Visitor(maximum), *this); - } -}; - -} // namespace Event -} // namespace Envoy diff --git a/include/envoy/formatter/substitution_formatter.h b/include/envoy/formatter/substitution_formatter.h index 0d78ce9aed0e..48541a0a98c2 100644 --- a/include/envoy/formatter/substitution_formatter.h +++ b/include/envoy/formatter/substitution_formatter.h @@ -4,6 +4,7 @@ #include #include "envoy/common/pure.h" +#include "envoy/config/typed_config.h" #include "envoy/http/header_map.h" #include "envoy/stream_info/stream_info.h" @@ -78,5 +79,56 @@ class FormatterProvider { using FormatterProviderPtr = std::unique_ptr; +/** + * Interface for command parser. + * CommandParser returns a FormatterProviderPtr after successfully parsing an access log format + * token, nullptr otherwise. + */ +class CommandParser { +public: + virtual ~CommandParser() = default; + + /** + * Return a FormatterProviderPtr if this command is parsed from the token. + * @param token the token to parse + * @param pos current position in the entire format string + * @param command_end_position position at the end of the command token + * + * Given the following format line using an extension called %CMD()%: + * + * %CMD()% %START_TIME(%Y/%m/%d)% ... + * + * The call to parse() for that extension would look like this: + * + * parse("CMD()", 1, 5) + * + * @return FormattterProviderPtr substitution provider for the parsed command + */ + virtual FormatterProviderPtr parse(const std::string& token, size_t pos, + size_t command_end_position) const PURE; +}; + +using CommandParserPtr = std::unique_ptr; + +/** + * Implemented by each custom CommandParser and registered via Registry::registerFactory() + * or the convenience class RegisterFactory. + */ +class CommandParserFactory : public Config::TypedFactory { +public: + ~CommandParserFactory() override = default; + + /** + * Creates a particular CommandParser implementation. + * + * @param config supplies the configuration for the command parser. + * @return CommandParserPtr the CommandParser which will be used in + * SubstitutionFormatParser::parse() when evaluating an access log format string. + */ + virtual CommandParserPtr createCommandParserFromProto(const Protobuf::Message& config) PURE; + + std::string category() const override { return "envoy.formatter"; } +}; + } // namespace Formatter } // namespace Envoy diff --git a/include/envoy/http/filter.h b/include/envoy/http/filter.h index 4157dfc6819f..605177c2c8df 100644 --- a/include/envoy/http/filter.h +++ b/include/envoy/http/filter.h @@ -49,7 +49,7 @@ enum class FilterHeadersStatus { // injectDecodedDataToFilterChain()/injectEncodedDataToFilterChain(), possibly multiple times // if the body needs to be divided into several chunks. The filter may need to handle // watermark events when injecting a body, see: - // https://github.com/envoyproxy/envoy/blob/master/source/docs/flow_control.md. + // https://github.com/envoyproxy/envoy/blob/main/source/docs/flow_control.md. // // The last call to inject data MUST have end_stream set to true to conclude the stream. // If the filter cannot provide a body the stream should be reset. @@ -411,6 +411,12 @@ class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks { */ virtual void encode100ContinueHeaders(ResponseHeaderMapPtr&& headers) PURE; + /** + * Returns the 100-Continue headers provided to encode100ContinueHeaders. Returns absl::nullopt if + * no headers have been provided yet. + */ + virtual ResponseHeaderMapOptRef continueHeaders() const PURE; + /** * Called with headers to be encoded, optionally indicating end of stream. * @@ -427,6 +433,12 @@ class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks { virtual void encodeHeaders(ResponseHeaderMapPtr&& headers, bool end_stream, absl::string_view details) PURE; + /** + * Returns the headers provided to encodeHeaders. Returns absl::nullopt if no headers have been + * provided yet. + */ + virtual ResponseHeaderMapOptRef responseHeaders() const PURE; + /** * Called with data to be encoded, optionally indicating end of stream. * @param data supplies the data to be encoded. @@ -440,6 +452,12 @@ class StreamDecoderFilterCallbacks : public virtual StreamFilterCallbacks { */ virtual void encodeTrailers(ResponseTrailerMapPtr&& trailers) PURE; + /** + * Returns the trailers provided to encodeTrailers. Returns absl::nullopt if no headers have been + * provided yet. + */ + virtual ResponseTrailerMapOptRef responseTrailers() const PURE; + /** * Called with metadata to be encoded. * @@ -572,6 +590,12 @@ class StreamFilterBase { * onDestroy(). */ virtual void onDestroy() PURE; + + /** + * Called when a match result occurs that isn't handled by the filter manager. + * @param action the resulting match action + */ + virtual void onMatchCallback(const Matcher::Action&) {} }; /** @@ -591,6 +615,7 @@ class StreamDecoderFilter : public StreamFilterBase { * Called with a decoded data frame. * @param data supplies the decoded data. * @param end_stream supplies whether this is the last data frame. + * Further note that end_stream is only true if there are no trailers. * @return FilterDataStatus determines how filter chain iteration proceeds. */ virtual FilterDataStatus decodeData(Buffer::Instance& data, bool end_stream) PURE; @@ -825,6 +850,7 @@ class StreamEncoderFilter : public StreamFilterBase { * Called with data to be encoded, optionally indicating end of stream. * @param data supplies the data to be encoded. * @param end_stream supplies whether this is the last data frame. + * Further note that end_stream is only true if there are no trailers. * @return FilterDataStatus determines how filter chain iteration proceeds. */ virtual FilterDataStatus encodeData(Buffer::Instance& data, bool end_stream) PURE; @@ -867,6 +893,8 @@ using StreamFilterSharedPtr = std::shared_ptr; class HttpMatchingData { public: + static absl::string_view name() { return "http"; } + virtual ~HttpMatchingData() = default; virtual RequestHeaderMapOptConstRef requestHeaders() const PURE; diff --git a/include/envoy/http/header_map.h b/include/envoy/http/header_map.h index 8df40a404e82..e3262e3ecfb0 100644 --- a/include/envoy/http/header_map.h +++ b/include/envoy/http/header_map.h @@ -786,6 +786,7 @@ class ResponseTrailerMap public CustomInlineHeaderBase {}; using ResponseTrailerMapPtr = std::unique_ptr; using ResponseTrailerMapOptRef = OptRef; +using ResponseTrailerMapOptConstRef = OptRef; /** * Convenient container type for storing Http::LowerCaseString and std::string key/value pairs. diff --git a/include/envoy/local_info/BUILD b/include/envoy/local_info/BUILD index 009f1ab020b7..20cf5b66d0bb 100644 --- a/include/envoy/local_info/BUILD +++ b/include/envoy/local_info/BUILD @@ -12,6 +12,7 @@ envoy_cc_library( name = "local_info_interface", hdrs = ["local_info.h"], deps = [ + "//include/envoy/config:context_provider_interface", "//include/envoy/network:address_interface", "//include/envoy/stats:symbol_table_interface", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", diff --git a/include/envoy/local_info/local_info.h b/include/envoy/local_info/local_info.h index 473f7510a81f..e32f4387915d 100644 --- a/include/envoy/local_info/local_info.h +++ b/include/envoy/local_info/local_info.h @@ -3,6 +3,7 @@ #include #include "envoy/common/pure.h" +#include "envoy/config/context_provider.h" #include "envoy/config/core/v3/base.pb.h" #include "envoy/network/address.h" #include "envoy/stats/symbol_table.h" @@ -46,6 +47,11 @@ class LocalInfo { * @return the full node identity presented to management servers. */ virtual const envoy::config::core::v3::Node& node() const PURE; + + /** + * @return the xDS context provider for the node. + */ + virtual const Config::ContextProvider& contextProvider() const PURE; }; using LocalInfoPtr = std::unique_ptr; diff --git a/include/envoy/matcher/BUILD b/include/envoy/matcher/BUILD index 838e7fd5e0c1..fadfc109afb0 100644 --- a/include/envoy/matcher/BUILD +++ b/include/envoy/matcher/BUILD @@ -17,6 +17,7 @@ envoy_cc_library( ], deps = [ "//include/envoy/config:typed_config_interface", + "//include/envoy/protobuf:message_validator_interface", "@envoy_api//envoy/config/common/matcher/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], diff --git a/include/envoy/matcher/matcher.h b/include/envoy/matcher/matcher.h index 613274ce389a..678eed64023b 100644 --- a/include/envoy/matcher/matcher.h +++ b/include/envoy/matcher/matcher.h @@ -7,6 +7,7 @@ #include "envoy/config/common/matcher/v3/matcher.pb.h" #include "envoy/config/core/v3/extension.pb.h" #include "envoy/config/typed_config.h" +#include "envoy/protobuf/message_validator.h" #include "absl/strings/string_view.h" #include "absl/types/optional.h" @@ -209,7 +210,9 @@ template class DataInputFactory : public Config::TypedFactory { /** * Creates a DataInput from the provided config. */ - virtual DataInputPtr createDataInput(const Protobuf::Message& config) PURE; + virtual DataInputPtr + createDataInput(const Protobuf::Message& config, + ProtobufMessage::ValidationVisitor& validation_visitor) PURE; /** * The category of this factory depends on the DataType, so we require a name() function to exist diff --git a/include/envoy/network/BUILD b/include/envoy/network/BUILD index 64cdece4ab9f..ccb27c6eb794 100644 --- a/include/envoy/network/BUILD +++ b/include/envoy/network/BUILD @@ -38,6 +38,7 @@ envoy_cc_library( ":listen_socket_interface", ":listener_interface", "//include/envoy/ssl:context_interface", + "//source/common/common:interval_value", ], ) @@ -173,6 +174,7 @@ envoy_cc_library( "//include/envoy/common:resource_interface", "//include/envoy/init:manager_interface", "//include/envoy/stats:stats_interface", + "//source/common/common:interval_value", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) diff --git a/include/envoy/network/connection_handler.h b/include/envoy/network/connection_handler.h index c42cc290cd61..a7afc67b20c8 100644 --- a/include/envoy/network/connection_handler.h +++ b/include/envoy/network/connection_handler.h @@ -9,6 +9,8 @@ #include "envoy/network/listener.h" #include "envoy/ssl/context.h" +#include "common/common/interval_value.h" + namespace Envoy { namespace Network { @@ -98,7 +100,7 @@ class ConnectionHandler { * Set the fraction of connections the listeners should reject. * @param reject_fraction a value between 0 (reject none) and 1 (reject all). */ - virtual void setListenerRejectFraction(float reject_fraction) PURE; + virtual void setListenerRejectFraction(UnitFloat reject_fraction) PURE; /** * @return the stat prefix used for per-handler stats. diff --git a/include/envoy/network/listener.h b/include/envoy/network/listener.h index 73edb4e7e9d5..5a182c0f44a7 100644 --- a/include/envoy/network/listener.h +++ b/include/envoy/network/listener.h @@ -16,6 +16,8 @@ #include "envoy/network/udp_packet_writer_handler.h" #include "envoy/stats/scope.h" +#include "common/common/interval_value.h" + namespace Envoy { namespace Network { @@ -332,7 +334,7 @@ class Listener { * Set the fraction of incoming connections that will be closed immediately * after being opened. */ - virtual void setRejectFraction(float reject_fraction) PURE; + virtual void setRejectFraction(UnitFloat reject_fraction) PURE; }; using ListenerPtr = std::unique_ptr; diff --git a/include/envoy/ratelimit/ratelimit.h b/include/envoy/ratelimit/ratelimit.h index e10641400310..00646eda753e 100644 --- a/include/envoy/ratelimit/ratelimit.h +++ b/include/envoy/ratelimit/ratelimit.h @@ -9,6 +9,7 @@ #include "envoy/stream_info/stream_info.h" #include "envoy/type/v3/ratelimit_unit.pb.h" +#include "absl/time/time.h" #include "absl/types/optional.h" namespace Envoy { @@ -28,6 +29,15 @@ struct RateLimitOverride { struct DescriptorEntry { std::string key_; std::string value_; + + friend bool operator==(const DescriptorEntry& lhs, const DescriptorEntry& rhs) { + return lhs.key_ == rhs.key_ && lhs.value_ == rhs.value_; + } + template + friend H AbslHashValue(H h, // NOLINT(readability-identifier-naming) + const DescriptorEntry& entry) { + return H::combine(std::move(h), entry.key_, entry.value_); + } }; /** @@ -39,6 +49,25 @@ struct Descriptor { }; /** + * A single token bucket. See token_bucket.proto. + */ +struct TokenBucket { + uint32_t max_tokens_; + uint32_t tokens_per_fill_; + absl::Duration fill_interval_; +}; + +/** + * A single rate limit request descriptor. See ratelimit.proto. + */ +struct LocalDescriptor { + std::vector entries_; + friend bool operator==(const LocalDescriptor& lhs, const LocalDescriptor& rhs) { + return lhs.entries_ == rhs.entries_; + } +}; + +/* * Base interface for generic rate limit descriptor producer. */ class DescriptorProducer { @@ -46,14 +75,15 @@ class DescriptorProducer { virtual ~DescriptorProducer() = default; /** - * Potentially append a descriptor entry to the end of descriptor. - * @param descriptor supplies the descriptor to optionally fill. + * Potentially fill a descriptor entry to the end of descriptor. + * @param descriptor_entry supplies the descriptor entry to optionally fill. * @param local_service_cluster supplies the name of the local service cluster. * @param headers supplies the header for the request. * @param info stream info associated with the request * @return true if the producer populated the descriptor. */ - virtual bool populateDescriptor(Descriptor& descriptor, const std::string& local_service_cluster, + virtual bool populateDescriptor(DescriptorEntry& descriptor_entry, + const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const PURE; }; @@ -73,8 +103,8 @@ class DescriptorProducerFactory : public Config::TypedFactory { * * @param config supplies the configuration for the descriptor extension. * @param validator configuration validation visitor. - * @return DescriptorProducerPtr the rate limit descriptor producer which will be used to populate - * rate limit descriptors. + * @return DescriptorProducerPtr the rate limit descriptor producer which will be used to + * populate rate limit descriptors. */ virtual DescriptorProducerPtr createDescriptorProducerFromProto(const Protobuf::Message& config, diff --git a/include/envoy/router/router_ratelimit.h b/include/envoy/router/router_ratelimit.h index 8a4ef25f6d6d..199b3248130e 100644 --- a/include/envoy/router/router_ratelimit.h +++ b/include/envoy/router/router_ratelimit.h @@ -59,6 +59,18 @@ class RateLimitPolicyEntry { const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const PURE; + + /** + * Potentially populate the local descriptor array with new descriptors to query. + * @param descriptors supplies the descriptor array to optionally fill. + * @param local_service_cluster supplies the name of the local service cluster. + * @param headers supplies the header for the request. + * @param info stream info associated with the request + */ + virtual void populateLocalDescriptors(std::vector& descriptors, + const std::string& local_service_cluster, + const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& info) const PURE; }; /** diff --git a/include/envoy/server/bootstrap_extension_config.h b/include/envoy/server/bootstrap_extension_config.h index 7eaf4dcb2530..f674540d19fb 100644 --- a/include/envoy/server/bootstrap_extension_config.h +++ b/include/envoy/server/bootstrap_extension_config.h @@ -15,6 +15,11 @@ namespace Server { class BootstrapExtension { public: virtual ~BootstrapExtension() = default; + + /** + * Called when server is done initializing and we have the ServerFactoryContext fully initialized. + */ + virtual void onServerInitialized() PURE; }; using BootstrapExtensionPtr = std::unique_ptr; @@ -34,7 +39,8 @@ class BootstrapExtensionFactory : public Config::TypedFactory { * implementation is unable to produce a factory with the provided parameters, it should throw an * EnvoyException. The returned pointer should never be nullptr. * @param config the custom configuration for this bootstrap extension type. - * @param context general filter context through which persistent resources can be accessed. + * @param context is the context to use for the extension. Note that the clusterManager is not + * yet initialized at this point and **must not** be used. */ virtual BootstrapExtensionPtr createBootstrapExtension(const Protobuf::Message& config, ServerFactoryContext& context) PURE; diff --git a/include/envoy/server/fatal_action_config.h b/include/envoy/server/fatal_action_config.h index c8768dced40a..1e5914ac2592 100644 --- a/include/envoy/server/fatal_action_config.h +++ b/include/envoy/server/fatal_action_config.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include "envoy/common/pure.h" #include "envoy/config/bootstrap/v3/bootstrap.pb.h" @@ -17,11 +18,10 @@ class FatalAction { public: virtual ~FatalAction() = default; /** - * Callback function to run when we are crashing. - * @param current_object the object we were working on when we started - * crashing. + * Callback function to run when Envoy is crashing. + * @param tracked_objects a span of objects Envoy was working on when Envoy started crashing. */ - virtual void run(const ScopeTrackedObject* current_object) PURE; + virtual void run(absl::Span tracked_objects) PURE; /** * @return whether the action is async-signal-safe. diff --git a/include/envoy/server/overload/BUILD b/include/envoy/server/overload/BUILD index f6f4a90933e9..a6e3f789c444 100644 --- a/include/envoy/server/overload/BUILD +++ b/include/envoy/server/overload/BUILD @@ -23,7 +23,7 @@ envoy_cc_library( name = "thread_local_overload_state", hdrs = ["thread_local_overload_state.h"], deps = [ - "//include/envoy/event:scaled_timer_minimum", + "//include/envoy/event:scaled_range_timer_manager_interface", "//include/envoy/event:timer_interface", "//include/envoy/thread_local:thread_local_object", "//source/common/common:interval_value", diff --git a/include/envoy/server/overload/overload_manager.h b/include/envoy/server/overload/overload_manager.h index a4b42a7c9fad..a9092b64341b 100644 --- a/include/envoy/server/overload/overload_manager.h +++ b/include/envoy/server/overload/overload_manager.h @@ -4,6 +4,7 @@ #include "envoy/common/pure.h" #include "envoy/event/dispatcher.h" +#include "envoy/event/scaled_range_timer_manager.h" #include "envoy/server/overload/thread_local_overload_state.h" #include "common/singleton/const_singleton.h" @@ -69,6 +70,11 @@ class OverloadManager { * an alternative to registering a callback for overload action state changes. */ virtual ThreadLocalOverloadState& getThreadLocalOverloadState() PURE; + + /** + * Get a factory for constructing scaled timer managers that respond to overload state. + */ + virtual Event::ScaledRangeTimerManagerFactory scaledTimerFactory() PURE; }; } // namespace Server diff --git a/include/envoy/server/overload/thread_local_overload_state.h b/include/envoy/server/overload/thread_local_overload_state.h index ef7a701618d9..23c5e7add043 100644 --- a/include/envoy/server/overload/thread_local_overload_state.h +++ b/include/envoy/server/overload/thread_local_overload_state.h @@ -4,7 +4,7 @@ #include #include "envoy/common/pure.h" -#include "envoy/event/scaled_timer_minimum.h" +#include "envoy/event/scaled_range_timer_manager.h" #include "envoy/event/timer.h" #include "envoy/thread_local/thread_local_object.h" @@ -28,7 +28,7 @@ class OverloadActionState { explicit constexpr OverloadActionState(UnitFloat value) : action_value_(value) {} - float value() const { return action_value_.value(); } + UnitFloat value() const { return action_value_; } bool isSaturated() const { return action_value_.value() == UnitFloat::max().value(); } private: @@ -40,17 +40,6 @@ class OverloadActionState { */ using OverloadActionCb = std::function; -enum class OverloadTimerType { - // Timers created with this type will never be scaled. This should only be used for testing. - UnscaledRealTimerForTest, - // The amount of time an HTTP connection to a downstream client can remain idle (no streams). This - // corresponds to the HTTP_DOWNSTREAM_CONNECTION_IDLE TimerType in overload.proto. - HttpDownstreamIdleConnectionTimeout, - // The amount of time an HTTP stream from a downstream client can remain idle. This corresponds to - // the HTTP_DOWNSTREAM_STREAM_IDLE TimerType in overload.proto. - HttpDownstreamIdleStreamTimeout, -}; - /** * Thread-local copy of the state of each configured overload action. */ @@ -58,14 +47,6 @@ class ThreadLocalOverloadState : public ThreadLocal::ThreadLocalObject { public: // Get a thread-local reference to the value for the given action key. virtual const OverloadActionState& getState(const std::string& action) PURE; - - // Get a scaled timer whose minimum corresponds to the configured value for the given timer type. - virtual Event::TimerPtr createScaledTimer(OverloadTimerType timer_type, - Event::TimerCb callback) PURE; - - // Get a scaled timer whose minimum is determined by the given scaling rule. - virtual Event::TimerPtr createScaledTimer(Event::ScaledTimerMinimum minimum, - Event::TimerCb callback) PURE; }; } // namespace Server diff --git a/include/envoy/upstream/upstream.h b/include/envoy/upstream/upstream.h index b9de4977659b..74582b1c41a6 100644 --- a/include/envoy/upstream/upstream.h +++ b/include/envoy/upstream/upstream.h @@ -211,7 +211,7 @@ using HostVector = std::vector; using HealthyHostVector = Phantom; using DegradedHostVector = Phantom; using ExcludedHostVector = Phantom; -using HostMap = absl::node_hash_map; +using HostMap = absl::flat_hash_map; using HostVectorSharedPtr = std::shared_ptr; using HostVectorConstSharedPtr = std::shared_ptr; diff --git a/security/email-templates.md b/security/email-templates.md index ec76e6d82748..3e6c17897866 100644 --- a/security/email-templates.md +++ b/security/email-templates.md @@ -36,14 +36,14 @@ Hello Envoy Distributors, The Envoy security team would like to provide advanced notice to the Envoy Private Distributors List of some details on the pending Envoy $VERSION security release, following the process described at -https://github.com/envoyproxy/envoy/blob/master/SECURITY.md. +https://github.com/envoyproxy/envoy/blob/main/SECURITY.md. This release will be made available on the $ORDINALDAY of $MONTH $YEAR at $PDTHOUR PDT ($GMTHOUR GMT). This release will fix $NUMDEFECTS security defect(s). The highest rated security defect is considered $SEVERITY severity. Below we provide details of these vulnerabilities under our embargo policy -(https://github.com/envoyproxy/envoy/blob/master/SECURITY.md#embargo-policy). +(https://github.com/envoyproxy/envoy/blob/main/SECURITY.md#embargo-policy). This information should be treated as confidential until public release by the Envoy maintainers on the Envoy GitHub. @@ -86,7 +86,7 @@ As a reminder, these patches are under embargo until $ORDINALDAY of $MONTH $YEAR at $PDTHOUR PDT ($GMTHOUR GMT). The information below should be treated as confidential and shared only on a need-to-know basis. The rules outline in our embargo policy -(https://github.com/envoyproxy/envoy/blob/master/SECURITY.md#embargo-policy) +(https://github.com/envoyproxy/envoy/blob/main/SECURITY.md#embargo-policy) still apply, and it is extremely important that any communication related to these CVEs are not forwarded further. diff --git a/security/postmortems/cve-2019-15225.md b/security/postmortems/cve-2019-15225.md index 1fbd02727822..7b3635ca70c1 100644 --- a/security/postmortems/cve-2019-15225.md +++ b/security/postmortems/cve-2019-15225.md @@ -130,7 +130,7 @@ amplify the effect of the O(n^2) process enough to produce a timeout. * The fixes for CVE-2019-15226 were straightforward and localized. * The security release occurred on time and followed the guidelines established in - https://github.com/envoyproxy/envoy/blob/master/SECURITY.md + https://github.com/envoyproxy/envoy/blob/main/SECURITY.md ### What went wrong diff --git a/security/postmortems/cve-2019-9900.md b/security/postmortems/cve-2019-9900.md index d6b6d38af579..2766d1a647ba 100644 --- a/security/postmortems/cve-2019-9900.md +++ b/security/postmortems/cve-2019-9900.md @@ -303,7 +303,7 @@ All times US/Pacific number of users. 2019-04-04: -* 15:41 The Envoy master branch was frozen to prepare for the security release. PRs were rebased +* 15:41 The Envoy main branch was frozen to prepare for the security release. PRs were rebased against master and prepared for the release push. * 18:33 Envoy security team was contacted by a distributor who had noticed public visibility of binary images with the fix patch by other vendors. After discussion, we agreed on a general diff --git a/source/common/api/api_impl.cc b/source/common/api/api_impl.cc index 2ac6bd73ae05..f91e5c6a1c6f 100644 --- a/source/common/api/api_impl.cc +++ b/source/common/api/api_impl.cc @@ -21,6 +21,13 @@ Event::DispatcherPtr Impl::allocateDispatcher(const std::string& name) { return std::make_unique(name, *this, time_system_, watermark_factory_); } +Event::DispatcherPtr +Impl::allocateDispatcher(const std::string& name, + const Event::ScaledRangeTimerManagerFactory& scaled_timer_factory) { + return std::make_unique(name, *this, time_system_, scaled_timer_factory, + watermark_factory_); +} + Event::DispatcherPtr Impl::allocateDispatcher(const std::string& name, Buffer::WatermarkFactoryPtr&& factory) { return std::make_unique(name, *this, time_system_, std::move(factory)); diff --git a/source/common/api/api_impl.h b/source/common/api/api_impl.h index c7bea1a73f24..0bec3b866562 100644 --- a/source/common/api/api_impl.h +++ b/source/common/api/api_impl.h @@ -24,6 +24,9 @@ class Impl : public Api { // Api::Api Event::DispatcherPtr allocateDispatcher(const std::string& name) override; + Event::DispatcherPtr + allocateDispatcher(const std::string& name, + const Event::ScaledRangeTimerManagerFactory& scaled_timer_factory) override; Event::DispatcherPtr allocateDispatcher(const std::string& name, Buffer::WatermarkFactoryPtr&& watermark_factory) override; Thread::ThreadFactory& threadFactory() override { return thread_factory_; } diff --git a/source/common/common/interval_value.h b/source/common/common/interval_value.h index 454c493e0d25..3a058eaae4b5 100644 --- a/source/common/common/interval_value.h +++ b/source/common/common/interval_value.h @@ -22,6 +22,26 @@ template class ClosedIntervalValue { T value() const { return value_; } + // Returns a value that is as far from max as the original value is from min. + // This guarantees that max().invert() == min() and min().invert() == max(). + ClosedIntervalValue invert() const { + return ClosedIntervalValue(value_ == Interval::max_value + ? Interval::min_value + : value_ == Interval::min_value + ? Interval::max_value + : Interval::max_value - (value_ - Interval::min_value)); + } + + // Comparisons are performed using the same operators on the underlying value + // type, with the same exactness guarantees. + + bool operator==(ClosedIntervalValue other) const { return value_ == other.value(); } + bool operator!=(ClosedIntervalValue other) const { return value_ != other.value(); } + bool operator<(ClosedIntervalValue other) const { return value_ < other.value(); } + bool operator<=(ClosedIntervalValue other) const { return value_ <= other.value(); } + bool operator>=(ClosedIntervalValue other) const { return value_ >= other.value(); } + bool operator>(ClosedIntervalValue other) const { return value_ > other.value(); } + private: T value_; }; diff --git a/source/common/common/scope_tracker.h b/source/common/common/scope_tracker.h index bed58c3fa8c0..4426bbaca5cc 100644 --- a/source/common/common/scope_tracker.h +++ b/source/common/common/scope_tracker.h @@ -3,24 +3,35 @@ #include "envoy/common/scope_tracker.h" #include "envoy/event/dispatcher.h" +#include "common/common/assert.h" + namespace Envoy { -// A small class for tracking the scope of the object which is currently having +// A small class for managing the scope of a tracked object which is currently having // work done in this thread. // -// When created, it sets the tracked object in the dispatcher, and when destroyed it points the -// dispatcher at the previously tracked object. +// When created, it appends the tracked object to the dispatcher's stack of tracked objects, and +// when destroyed it pops the dispatcher's stack of tracked object, which should be the object it +// registered. class ScopeTrackerScopeState { public: ScopeTrackerScopeState(const ScopeTrackedObject* object, Event::Dispatcher& dispatcher) - : dispatcher_(dispatcher) { - latched_object_ = dispatcher_.setTrackedObject(object); + : registered_object_(object), dispatcher_(dispatcher) { + dispatcher_.pushTrackedObject(registered_object_); + } + + ~ScopeTrackerScopeState() { + // If ScopeTrackerScopeState is always used for managing tracked objects, + // then the object popped off should be the object we registered. + dispatcher_.popTrackedObject(registered_object_); } - ~ScopeTrackerScopeState() { dispatcher_.setTrackedObject(latched_object_); } + // Make this object stack-only, it doesn't make sense for it + // to be on the heap since it's tracking a stack of active operations. + void* operator new(std::size_t) = delete; private: - const ScopeTrackedObject* latched_object_; + const ScopeTrackedObject* registered_object_; Event::Dispatcher& dispatcher_; }; diff --git a/source/common/common/thread.h b/source/common/common/thread.h index bb86e0de3377..819e5902b746 100644 --- a/source/common/common/thread.h +++ b/source/common/common/thread.h @@ -177,7 +177,14 @@ struct MainThread { delete MainThreadSingleton::getExisting(); MainThreadSingleton::clear(); } - static bool isMainThread() { return MainThreadSingleton::get().inMainThread(); } + static bool isMainThread() { + // If threading is off, only main thread is running. + if (MainThreadSingleton::getExisting() == nullptr) { + return true; + } + // When threading is on, compare thread id with main thread id. + return MainThreadSingleton::get().inMainThread(); + } private: std::thread::id main_thread_id_{std::this_thread::get_id()}; diff --git a/source/common/common/utility.h b/source/common/common/utility.h index e4eba5b37117..c995fed2db38 100644 --- a/source/common/common/utility.h +++ b/source/common/common/utility.h @@ -752,4 +752,17 @@ class InlineString : public InlineStorage { char data_[]; }; +class SetUtil { +public: + // Use instead of std::set_difference for unordered absl::flat_hash_set containers. + template + static void setDifference(const absl::flat_hash_set& original_set, + const absl::flat_hash_set& remove_set, + absl::flat_hash_set& result_set) { + std::copy_if(original_set.begin(), original_set.end(), + std::inserter(result_set, result_set.begin()), + [&remove_set](const T& v) -> bool { return remove_set.count(v) == 0; }); + } +}; + } // namespace Envoy diff --git a/source/common/config/BUILD b/source/common/config/BUILD index 067f1d9ada0f..1454159381ec 100644 --- a/source/common/config/BUILD +++ b/source/common/config/BUILD @@ -44,6 +44,15 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "context_provider_lib", + hdrs = ["context_provider_impl.h"], + deps = [ + ":xds_context_params_lib", + "//include/envoy/config:context_provider_interface", + ], +) + envoy_cc_library( name = "datasource_lib", srcs = ["datasource.cc"], @@ -173,6 +182,7 @@ envoy_cc_library( hdrs = ["grpc_subscription_impl.h"], deps = [ ":grpc_mux_lib", + ":xds_resource_lib", "//include/envoy/config:subscription_interface", "//include/envoy/event:dispatcher_interface", "//include/envoy/grpc:async_client_interface", @@ -189,6 +199,8 @@ envoy_cc_library( ":pausable_ack_queue_lib", ":version_converter_lib", ":watch_map_lib", + ":xds_context_params_lib", + ":xds_resource_lib", "//include/envoy/event:dispatcher_interface", "//include/envoy/grpc:async_client_interface", "//source/common/memory:utils_lib", @@ -426,10 +438,12 @@ envoy_cc_library( hdrs = ["watch_map.h"], deps = [ ":decoded_resource_lib", + ":xds_resource_lib", "//include/envoy/config:subscription_interface", "//source/common/common:assert_lib", "//source/common/common:cleanup_lib", "//source/common/common:minimal_logger_lib", + "//source/common/common:utility_lib", "//source/common/protobuf", "@envoy_api//envoy/service/discovery/v3:pkg_cc_proto", ], diff --git a/source/common/config/README.md b/source/common/config/README.md deleted file mode 100644 index 3135bd56bdaa..000000000000 --- a/source/common/config/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# xDS - -xDS stands for [fill in the blank] Discovery Service. It provides dynamic config discovery/updates. - -tldr: xDS can use the filesystem, REST, or gRPC. gRPC xDS comes in four flavors. -However, Envoy code uses all of that via the same Subscription interface. -If you are an Envoy developer with your hands on a valid Subscription object, -you can mostly forget the filesystem/REST/gRPC distinction, and you can -especially forget about the gRPC flavors. All of that is specified in the -bootstrap config, which is read and put into action by ClusterManagerImpl. - -Note that there can be multiple active gRPC subscriptions for a single resource -type. This concept is called "resource watches". If one EDS subscription -subscribes to X and Y, and another subscribes to Y and Z, the underlying -subscription logic will maintain a subscription to the union: X Y and Z. Updates -to X will be delivered to the first object, Y to both, Z to the second. This -logic is implemented by WatchMap. - -### If you are working on Envoy's gRPC xDS client logic itself, read on. - -When using gRPC, xDS has two pairs of options: aggregated/non-aggregated, and -delta/state-of-the-world updates. All four combinations of these are usable. - -"Aggregated" means that EDS, CDS, etc resources are all carried by the same gRPC stream. -For Envoy's implementation of xDS client logic, there is effectively no difference -between aggregated xDS and non-aggregated: they both use the same request/response protos. The -non-aggregated case is handled by running the aggregated logic, and just happening to only have 1 -xDS subscription type to "aggregate", i.e., NewGrpcMuxImpl only has one -DeltaSubscriptionState entry in its map. - -However, to the config server, there is a huge difference: when using ADS (caused -by the user providing an ads_config in the bootstrap config), the gRPC client sets -its method string to {Delta,Stream}AggregatedResources, as opposed to {Delta,Stream}Clusters, -{Delta,Stream}Routes, etc. So, despite using the same request/response protos, -and having identical client code, they're actually different gRPC services. - -Delta vs state-of-the-world is a question of wire format: the protos in question are named -[Delta]Discovery{Request,Response}. That is what the GrpcMux interface is useful for: its -NewGrpcMuxImpl (TODO may be renamed) implementation works with DeltaDiscovery{Request,Response} and has -delta-specific logic; its GrpxMuxImpl implementation (TODO will be merged into NewGrpcMuxImpl) works with Discovery{Request,Response} -and has SotW-specific logic. Both the delta and SotW Subscription implementations (TODO will be merged) hold a shared_ptr. -The shared_ptr allows for both non- and aggregated: if non-aggregated, you'll be the only holder of that shared_ptr. - -![xDS_code_diagram](xDS_code_diagram.png) - -Note that the orange flow does not necessarily have to happen in response to the blue flow; there can be spontaneous updates. ACKs are not shown in this diagram; they are also carred by the [Delta]DiscoveryRequest protos. -What does GrpcXdsContext even do in this diagram? Just own things and pass through function calls? Answer: it sequences the requests and ACKs that the various type_urls send. diff --git a/source/common/config/context_provider_impl.h b/source/common/config/context_provider_impl.h new file mode 100644 index 000000000000..e5909b83f193 --- /dev/null +++ b/source/common/config/context_provider_impl.h @@ -0,0 +1,23 @@ +#pragma once + +#include "envoy/config/context_provider.h" + +#include "common/config/xds_context_params.h" + +namespace Envoy { +namespace Config { + +class ContextProviderImpl : public ContextProvider { +public: + ContextProviderImpl(const envoy::config::core::v3::Node& node, + const Protobuf::RepeatedPtrField& node_context_params) + : node_context_(XdsContextParams::encodeNodeContext(node, node_context_params)) {} + + const xds::core::v3::ContextParams& nodeContext() const override { return node_context_; } + +private: + const xds::core::v3::ContextParams node_context_; +}; + +} // namespace Config +} // namespace Envoy diff --git a/source/common/config/delta_subscription_state.cc b/source/common/config/delta_subscription_state.cc index 7702081602a1..8821427c0c37 100644 --- a/source/common/config/delta_subscription_state.cc +++ b/source/common/config/delta_subscription_state.cc @@ -32,8 +32,9 @@ DeltaSubscriptionState::DeltaSubscriptionState(std::string type_url, type_url_(std::move(type_url)), watch_map_(watch_map), local_info_(local_info), dispatcher_(dispatcher) {} -void DeltaSubscriptionState::updateSubscriptionInterest(const std::set& cur_added, - const std::set& cur_removed) { +void DeltaSubscriptionState::updateSubscriptionInterest( + const absl::flat_hash_set& cur_added, + const absl::flat_hash_set& cur_removed) { for (const auto& a : cur_added) { setResourceWaitingForServer(a); // If interest in a resource is removed-then-added (all before a discovery request diff --git a/source/common/config/delta_subscription_state.h b/source/common/config/delta_subscription_state.h index 089b632c9a01..ac073d777705 100644 --- a/source/common/config/delta_subscription_state.h +++ b/source/common/config/delta_subscription_state.h @@ -29,9 +29,9 @@ class DeltaSubscriptionState : public Logger::Loggable { const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher); // Update which resources we're interested in subscribing to. - void updateSubscriptionInterest(const std::set& cur_added, - const std::set& cur_removed); - void addAliasesToResolve(const std::set& aliases); + void updateSubscriptionInterest(const absl::flat_hash_set& cur_added, + const absl::flat_hash_set& cur_removed); + void addAliasesToResolve(const absl::flat_hash_set& aliases); // Whether there was a change in our subscription interest we have yet to inform the server of. bool subscriptionUpdatePending() const; @@ -98,8 +98,8 @@ class DeltaSubscriptionState : public Logger::Loggable { const bool supports_heartbeats_; TtlManager ttl_; // The keys of resource_versions_. Only tracked separately because std::map does not provide an - // iterator into just its keys, e.g. for use in std::set_difference. - std::set resource_names_; + // iterator into just its keys. + absl::flat_hash_set resource_names_; const std::string type_url_; UntypedConfigUpdateCallbacks& watch_map_; diff --git a/source/common/config/filesystem_subscription_impl.cc b/source/common/config/filesystem_subscription_impl.cc index 35feaa31e5f0..537fa8044260 100644 --- a/source/common/config/filesystem_subscription_impl.cc +++ b/source/common/config/filesystem_subscription_impl.cc @@ -27,13 +27,13 @@ FilesystemSubscriptionImpl::FilesystemSubscriptionImpl( } // Config::Subscription -void FilesystemSubscriptionImpl::start(const std::set&, const bool) { +void FilesystemSubscriptionImpl::start(const absl::flat_hash_set&) { started_ = true; // Attempt to read in case there is a file there already. refresh(); } -void FilesystemSubscriptionImpl::updateResourceInterest(const std::set&) { +void FilesystemSubscriptionImpl::updateResourceInterest(const absl::flat_hash_set&) { // Bump stats for consistent behavior with other xDS. stats_.update_attempt_.inc(); } diff --git a/source/common/config/filesystem_subscription_impl.h b/source/common/config/filesystem_subscription_impl.h index 2a817180d2a8..1c8d9d041ccf 100644 --- a/source/common/config/filesystem_subscription_impl.h +++ b/source/common/config/filesystem_subscription_impl.h @@ -27,9 +27,9 @@ class FilesystemSubscriptionImpl : public Config::Subscription, // Config::Subscription // We report all discovered resources in the watched file, so the resource names arguments are // unused, and updateResourceInterest is a no-op (other than updating a stat). - void start(const std::set&, const bool use_namespace_matching = false) override; - void updateResourceInterest(const std::set&) override; - void requestOnDemandUpdate(const std::set&) override { + void start(const absl::flat_hash_set&) override; + void updateResourceInterest(const absl::flat_hash_set&) override; + void requestOnDemandUpdate(const absl::flat_hash_set&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } diff --git a/source/common/config/grpc_mux_impl.cc b/source/common/config/grpc_mux_impl.cc index 2734867446fe..ffd1e31319ef 100644 --- a/source/common/config/grpc_mux_impl.cc +++ b/source/common/config/grpc_mux_impl.cc @@ -68,7 +68,7 @@ void GrpcMuxImpl::sendDiscoveryRequest(const std::string& type_url) { } GrpcMuxWatchPtr GrpcMuxImpl::addWatch(const std::string& type_url, - const std::set& resources, + const absl::flat_hash_set& resources, SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder, const bool) { auto watch = diff --git a/source/common/config/grpc_mux_impl.h b/source/common/config/grpc_mux_impl.h index bd554891461d..4b46dbb67317 100644 --- a/source/common/config/grpc_mux_impl.h +++ b/source/common/config/grpc_mux_impl.h @@ -47,12 +47,13 @@ class GrpcMuxImpl : public GrpcMux, ScopedResume pause(const std::string& type_url) override; ScopedResume pause(const std::vector type_urls) override; - GrpcMuxWatchPtr addWatch(const std::string& type_url, const std::set& resources, + GrpcMuxWatchPtr addWatch(const std::string& type_url, + const absl::flat_hash_set& resources, SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder, const bool use_namespace_matching = false) override; - void requestOnDemandUpdate(const std::string&, const std::set&) override { + void requestOnDemandUpdate(const std::string&, const absl::flat_hash_set&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } @@ -80,11 +81,12 @@ class GrpcMuxImpl : public GrpcMux, void sendDiscoveryRequest(const std::string& type_url); struct GrpcMuxWatchImpl : public GrpcMuxWatch { - GrpcMuxWatchImpl(const std::set& resources, SubscriptionCallbacks& callbacks, - OpaqueResourceDecoder& resource_decoder, const std::string& type_url, - GrpcMuxImpl& parent) - : resources_(resources), callbacks_(callbacks), resource_decoder_(resource_decoder), - type_url_(type_url), parent_(parent), watches_(parent.apiStateFor(type_url).watches_) { + GrpcMuxWatchImpl(const absl::flat_hash_set& resources, + SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder, + const std::string& type_url, GrpcMuxImpl& parent) + : callbacks_(callbacks), resource_decoder_(resource_decoder), type_url_(type_url), + parent_(parent), watches_(parent.apiStateFor(type_url).watches_) { + std::copy(resources.begin(), resources.end(), std::inserter(resources_, resources_.begin())); watches_.emplace(watches_.begin(), this); } @@ -95,17 +97,19 @@ class GrpcMuxImpl : public GrpcMux, } } - void update(const std::set& resources) override { + void update(const absl::flat_hash_set& resources) override { watches_.remove(this); if (!resources_.empty()) { parent_.queueDiscoveryRequest(type_url_); } - resources_ = resources; + resources_.clear(); + std::copy(resources.begin(), resources.end(), std::inserter(resources_, resources_.begin())); // move this watch to the beginning of the list watches_.emplace(watches_.begin(), this); parent_.queueDiscoveryRequest(type_url_); } + // Maintain deterministic wire ordering via ordered std::set. std::set resources_; SubscriptionCallbacks& callbacks_; OpaqueResourceDecoder& resource_decoder_; @@ -184,12 +188,12 @@ class NullGrpcMuxImpl : public GrpcMux, return std::make_unique([] {}); } - GrpcMuxWatchPtr addWatch(const std::string&, const std::set&, SubscriptionCallbacks&, - OpaqueResourceDecoder&, const bool) override { + GrpcMuxWatchPtr addWatch(const std::string&, const absl::flat_hash_set&, + SubscriptionCallbacks&, OpaqueResourceDecoder&, const bool) override { ExceptionUtil::throwEnvoyException("ADS must be configured to support an ADS config source"); } - void requestOnDemandUpdate(const std::string&, const std::set&) override { + void requestOnDemandUpdate(const std::string&, const absl::flat_hash_set&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } diff --git a/source/common/config/grpc_subscription_impl.cc b/source/common/config/grpc_subscription_impl.cc index 83c21e2208dc..62a6c5387651 100644 --- a/source/common/config/grpc_subscription_impl.cc +++ b/source/common/config/grpc_subscription_impl.cc @@ -1,26 +1,35 @@ #include "common/config/grpc_subscription_impl.h" +#include + #include "common/common/assert.h" #include "common/common/logger.h" #include "common/common/utility.h" +#include "common/config/xds_resource.h" #include "common/grpc/common.h" #include "common/protobuf/protobuf.h" +#include "common/protobuf/type_util.h" #include "common/protobuf/utility.h" namespace Envoy { namespace Config { -GrpcSubscriptionImpl::GrpcSubscriptionImpl( - GrpcMuxSharedPtr grpc_mux, SubscriptionCallbacks& callbacks, - OpaqueResourceDecoder& resource_decoder, SubscriptionStats stats, absl::string_view type_url, - Event::Dispatcher& dispatcher, std::chrono::milliseconds init_fetch_timeout, bool is_aggregated) +constexpr std::chrono::milliseconds UpdateDurationLogThreshold = std::chrono::milliseconds(50); + +GrpcSubscriptionImpl::GrpcSubscriptionImpl(GrpcMuxSharedPtr grpc_mux, + SubscriptionCallbacks& callbacks, + OpaqueResourceDecoder& resource_decoder, + SubscriptionStats stats, absl::string_view type_url, + Event::Dispatcher& dispatcher, + std::chrono::milliseconds init_fetch_timeout, + bool is_aggregated, bool use_namespace_matching) : grpc_mux_(grpc_mux), callbacks_(callbacks), resource_decoder_(resource_decoder), stats_(stats), type_url_(type_url), dispatcher_(dispatcher), - init_fetch_timeout_(init_fetch_timeout), is_aggregated_(is_aggregated) {} + init_fetch_timeout_(init_fetch_timeout), is_aggregated_(is_aggregated), + use_namespace_matching_(use_namespace_matching) {} // Config::Subscription -void GrpcSubscriptionImpl::start(const std::set& resources, - const bool use_namespace_matching) { +void GrpcSubscriptionImpl::start(const absl::flat_hash_set& resources) { if (init_fetch_timeout_.count() > 0) { init_fetch_timeout_timer_ = dispatcher_.createTimer([this]() -> void { callbacks_.onConfigUpdateFailed(Envoy::Config::ConfigUpdateFailureReason::FetchTimedout, @@ -30,7 +39,7 @@ void GrpcSubscriptionImpl::start(const std::set& resources, } watch_ = - grpc_mux_->addWatch(type_url_, resources, *this, resource_decoder_, use_namespace_matching); + grpc_mux_->addWatch(type_url_, resources, *this, resource_decoder_, use_namespace_matching_); // The attempt stat here is maintained for the purposes of having consistency between ADS and // gRPC/filesystem/REST Subscriptions. Since ADS is push based and muxed, the notion of an @@ -45,12 +54,13 @@ void GrpcSubscriptionImpl::start(const std::set& resources, } void GrpcSubscriptionImpl::updateResourceInterest( - const std::set& update_to_these_names) { + const absl::flat_hash_set& update_to_these_names) { watch_->update(update_to_these_names); stats_.update_attempt_.inc(); } -void GrpcSubscriptionImpl::requestOnDemandUpdate(const std::set& for_update) { +void GrpcSubscriptionImpl::requestOnDemandUpdate( + const absl::flat_hash_set& for_update) { grpc_mux_->requestOnDemandUpdate(type_url_, for_update); stats_.update_attempt_.inc(); } @@ -63,14 +73,23 @@ void GrpcSubscriptionImpl::onConfigUpdate(const std::vector( + dispatcher_.timeSource().monotonicTime() - start); stats_.update_success_.inc(); stats_.update_attempt_.inc(); stats_.update_time_.set(DateUtil::nowToMilliseconds(dispatcher_.timeSource())); stats_.version_.set(HashUtil::xxHash64(version_info)); stats_.version_text_.set(version_info); + stats_.update_duration_.recordValue(update_duration.count()); ENVOY_LOG(debug, "gRPC config for {} accepted with {} resources with version {}", type_url_, resources.size(), version_info); + + if (update_duration > UpdateDurationLogThreshold) { + ENVOY_LOG(debug, "gRPC config update took {} ms! Resources names: {}", update_duration.count(), + absl::StrJoin(resources, ",", ResourceNameFormatter())); + } } void GrpcSubscriptionImpl::onConfigUpdate( @@ -79,11 +98,15 @@ void GrpcSubscriptionImpl::onConfigUpdate( const std::string& system_version_info) { disableInitFetchTimeoutTimer(); stats_.update_attempt_.inc(); + auto start = dispatcher_.timeSource().monotonicTime(); callbacks_.onConfigUpdate(added_resources, removed_resources, system_version_info); + std::chrono::milliseconds update_duration = std::chrono::duration_cast( + dispatcher_.timeSource().monotonicTime() - start); stats_.update_success_.inc(); stats_.update_time_.set(DateUtil::nowToMilliseconds(dispatcher_.timeSource())); stats_.version_.set(HashUtil::xxHash64(system_version_info)); stats_.version_text_.set(system_version_info); + stats_.update_duration_.recordValue(update_duration.count()); } void GrpcSubscriptionImpl::onConfigUpdateFailed(ConfigUpdateFailureReason reason, @@ -121,5 +144,21 @@ void GrpcSubscriptionImpl::disableInitFetchTimeoutTimer() { } } +GrpcCollectionSubscriptionImpl::GrpcCollectionSubscriptionImpl( + const xds::core::v3::ResourceLocator& collection_locator, GrpcMuxSharedPtr grpc_mux, + SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder, + SubscriptionStats stats, Event::Dispatcher& dispatcher, + std::chrono::milliseconds init_fetch_timeout, bool is_aggregated) + : GrpcSubscriptionImpl( + grpc_mux, callbacks, resource_decoder, stats, + TypeUtil::descriptorFullNameToTypeUrl(collection_locator.resource_type()), dispatcher, + init_fetch_timeout, is_aggregated, false), + collection_locator_(collection_locator) {} + +void GrpcCollectionSubscriptionImpl::start(const absl::flat_hash_set& resource_names) { + ASSERT(resource_names.empty()); + GrpcSubscriptionImpl::start({XdsResourceIdentifier::encodeUrl(collection_locator_)}); +} + } // namespace Config } // namespace Envoy diff --git a/source/common/config/grpc_subscription_impl.h b/source/common/config/grpc_subscription_impl.h index a7fa247eeebf..b1892d4ac046 100644 --- a/source/common/config/grpc_subscription_impl.h +++ b/source/common/config/grpc_subscription_impl.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "envoy/config/grpc_mux.h" @@ -8,6 +9,8 @@ #include "common/common/logger.h" +#include "xds/core/v3/resource_locator.pb.h" + namespace Envoy { namespace Config { @@ -15,19 +18,20 @@ namespace Config { * Adapter from typed Subscription to untyped GrpcMux. Also handles per-xDS API stats/logging. */ class GrpcSubscriptionImpl : public Subscription, - SubscriptionCallbacks, + protected SubscriptionCallbacks, Logger::Loggable { public: GrpcSubscriptionImpl(GrpcMuxSharedPtr grpc_mux, SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder, SubscriptionStats stats, absl::string_view type_url, Event::Dispatcher& dispatcher, - std::chrono::milliseconds init_fetch_timeout, bool is_aggregated); + std::chrono::milliseconds init_fetch_timeout, bool is_aggregated, + bool use_namespace_matching); // Config::Subscription - void start(const std::set& resource_names, - const bool use_namespace_matching = false) override; - void updateResourceInterest(const std::set& update_to_these_names) override; - void requestOnDemandUpdate(const std::set& add_these_names) override; + void start(const absl::flat_hash_set& resource_names) override; + void + updateResourceInterest(const absl::flat_hash_set& update_to_these_names) override; + void requestOnDemandUpdate(const absl::flat_hash_set& add_these_names) override; // Config::SubscriptionCallbacks (all pass through to callbacks_!) void onConfigUpdate(const std::vector& resources, const std::string& version_info) override; @@ -55,10 +59,31 @@ class GrpcSubscriptionImpl : public Subscription, std::chrono::milliseconds init_fetch_timeout_; Event::TimerPtr init_fetch_timeout_timer_; const bool is_aggregated_; + const bool use_namespace_matching_; + + struct ResourceNameFormatter { + void operator()(std::string* out, const Config::DecodedResourceRef& resource) { + out->append(resource.get().name()); + } + }; }; using GrpcSubscriptionImplPtr = std::unique_ptr; using GrpcSubscriptionImplSharedPtr = std::shared_ptr; +class GrpcCollectionSubscriptionImpl : public GrpcSubscriptionImpl { +public: + GrpcCollectionSubscriptionImpl(const xds::core::v3::ResourceLocator& collection_locator, + GrpcMuxSharedPtr grpc_mux, SubscriptionCallbacks& callbacks, + OpaqueResourceDecoder& resource_decoder, SubscriptionStats stats, + Event::Dispatcher& dispatcher, + std::chrono::milliseconds init_fetch_timeout, bool is_aggregated); + + void start(const absl::flat_hash_set& resource_names) override; + +private: + xds::core::v3::ResourceLocator collection_locator_; +}; + } // namespace Config } // namespace Envoy diff --git a/source/common/config/http_subscription_impl.cc b/source/common/config/http_subscription_impl.cc index 534af337b0f7..054b4cc49946 100644 --- a/source/common/config/http_subscription_impl.cc +++ b/source/common/config/http_subscription_impl.cc @@ -43,7 +43,7 @@ HttpSubscriptionImpl::HttpSubscriptionImpl( } // Config::Subscription -void HttpSubscriptionImpl::start(const std::set& resource_names, const bool) { +void HttpSubscriptionImpl::start(const absl::flat_hash_set& resource_names) { if (init_fetch_timeout_.count() > 0) { init_fetch_timeout_timer_ = dispatcher_.createTimer([this]() -> void { handleFailure(Config::ConfigUpdateFailureReason::FetchTimedout, nullptr); @@ -53,14 +53,18 @@ void HttpSubscriptionImpl::start(const std::set& resource_names, co Protobuf::RepeatedPtrField resources_vector(resource_names.begin(), resource_names.end()); + // Sort to provide stable wire ordering. + std::sort(resources_vector.begin(), resources_vector.end()); request_.mutable_resource_names()->Swap(&resources_vector); initialize(); } void HttpSubscriptionImpl::updateResourceInterest( - const std::set& update_to_these_names) { + const absl::flat_hash_set& update_to_these_names) { Protobuf::RepeatedPtrField resources_vector(update_to_these_names.begin(), update_to_these_names.end()); + // Sort to provide stable wire ordering. + std::sort(resources_vector.begin(), resources_vector.end()); request_.mutable_resource_names()->Swap(&resources_vector); } diff --git a/source/common/config/http_subscription_impl.h b/source/common/config/http_subscription_impl.h index 73bdae509493..33263e0ef3eb 100644 --- a/source/common/config/http_subscription_impl.h +++ b/source/common/config/http_subscription_impl.h @@ -34,10 +34,10 @@ class HttpSubscriptionImpl : public Http::RestApiFetcher, ProtobufMessage::ValidationVisitor& validation_visitor); // Config::Subscription - void start(const std::set& resource_names, - const bool use_namespace_matching = false) override; - void updateResourceInterest(const std::set& update_to_these_names) override; - void requestOnDemandUpdate(const std::set&) override { + void start(const absl::flat_hash_set& resource_names) override; + void + updateResourceInterest(const absl::flat_hash_set& update_to_these_names) override; + void requestOnDemandUpdate(const absl::flat_hash_set&) override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } diff --git a/source/common/config/new_grpc_mux_impl.cc b/source/common/config/new_grpc_mux_impl.cc index dc8d5c3b14dc..0ace0efe045d 100644 --- a/source/common/config/new_grpc_mux_impl.cc +++ b/source/common/config/new_grpc_mux_impl.cc @@ -7,6 +7,8 @@ #include "common/common/token_bucket_impl.h" #include "common/config/utility.h" #include "common/config/version_converter.h" +#include "common/config/xds_context_params.h" +#include "common/config/xds_resource.h" #include "common/memory/utils.h" #include "common/protobuf/protobuf.h" #include "common/protobuf/utility.h" @@ -128,7 +130,7 @@ void NewGrpcMuxImpl::kickOffAck(UpdateAck ack) { void NewGrpcMuxImpl::start() { grpc_stream_.establishNewStream(); } GrpcMuxWatchPtr NewGrpcMuxImpl::addWatch(const std::string& type_url, - const std::set& resources, + const absl::flat_hash_set& resources, SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder, const bool use_namespace_matching) { @@ -152,14 +154,37 @@ GrpcMuxWatchPtr NewGrpcMuxImpl::addWatch(const std::string& type_url, // the whole subscription, or if a removed name has no other watch interested in it, then the // subscription will enqueue and attempt to send an appropriate discovery request. void NewGrpcMuxImpl::updateWatch(const std::string& type_url, Watch* watch, - const std::set& resources, - bool creating_namespace_watch) { + const absl::flat_hash_set& resources, + const bool creating_namespace_watch) { ASSERT(watch != nullptr); auto sub = subscriptions_.find(type_url); RELEASE_ASSERT(sub != subscriptions_.end(), fmt::format("Watch of {} has no subscription to update.", type_url)); - auto added_removed = sub->second->watch_map_.updateWatchInterest(watch, resources); - if (creating_namespace_watch) { + // If this is a glob collection subscription, we need to compute actual context parameters. + absl::flat_hash_set xdstp_resources; + // TODO(htuch): add support for resources beyond glob collections, the constraints below around + // resource size and ID reflect the progress of the xdstp:// implementation. + if (!resources.empty() && XdsResourceIdentifier::hasXdsTpScheme(*resources.begin())) { + // Callers must be asking for a single resource, the collection. + ASSERT(resources.size() == 1); + auto resource = XdsResourceIdentifier::decodeUrn(*resources.begin()); + // We only know how to deal with glob collections and static context parameters right now. + // TODO(htuch): add support for dynamic context params and list collections in the future. + if (absl::EndsWith(resource.id(), "/*")) { + auto encoded_context = XdsContextParams::encodeResource( + local_info_.contextProvider().nodeContext(), resource.context(), {}, {}); + resource.mutable_context()->CopyFrom(encoded_context); + XdsResourceIdentifier::EncodeOptions encode_options; + encode_options.sort_context_params_ = true; + xdstp_resources.insert(XdsResourceIdentifier::encodeUrn(resource, encode_options)); + } else { + // TODO(htuch): We will handle list collections here in future work. + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; + } + } + auto added_removed = sub->second->watch_map_.updateWatchInterest( + watch, xdstp_resources.empty() ? resources : xdstp_resources); + if (creating_namespace_watch && xdstp_resources.empty()) { // This is to prevent sending out of requests that contain prefixes instead of resource names sub->second->sub_state_.updateSubscriptionInterest({}, {}); } else { @@ -173,7 +198,7 @@ void NewGrpcMuxImpl::updateWatch(const std::string& type_url, Watch* watch, } void NewGrpcMuxImpl::requestOnDemandUpdate(const std::string& type_url, - const std::set& for_update) { + const absl::flat_hash_set& for_update) { auto sub = subscriptions_.find(type_url); RELEASE_ASSERT(sub != subscriptions_.end(), fmt::format("Watch of {} has no subscription to update.", type_url)); diff --git a/source/common/config/new_grpc_mux_impl.h b/source/common/config/new_grpc_mux_impl.h index f39308748df6..392be536fe2c 100644 --- a/source/common/config/new_grpc_mux_impl.h +++ b/source/common/config/new_grpc_mux_impl.h @@ -38,13 +38,14 @@ class NewGrpcMuxImpl const RateLimitSettings& rate_limit_settings, const LocalInfo::LocalInfo& local_info); - GrpcMuxWatchPtr addWatch(const std::string& type_url, const std::set& resources, + GrpcMuxWatchPtr addWatch(const std::string& type_url, + const absl::flat_hash_set& resources, SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder, const bool use_namespace_matching = false) override; void requestOnDemandUpdate(const std::string& type_url, - const std::set& for_update) override; + const absl::flat_hash_set& for_update) override; ScopedResume pause(const std::string& type_url) override; ScopedResume pause(const std::vector type_urls) override; @@ -101,7 +102,7 @@ class NewGrpcMuxImpl } } - void update(const std::set& resources) override { + void update(const absl::flat_hash_set& resources) override { parent_.updateWatch(type_url_, watch_, resources); } @@ -117,8 +118,8 @@ class NewGrpcMuxImpl // the whole subscription, or if a removed name has no other watch interested in it, then the // subscription will enqueue and attempt to send an appropriate discovery request. void updateWatch(const std::string& type_url, Watch* watch, - const std::set& resources, - const bool creating_namespace_watch = false); + const absl::flat_hash_set& resources, + bool creating_namespace_watch = false); void addSubscription(const std::string& type_url, const bool use_namespace_matching); diff --git a/source/common/config/subscription_factory_impl.cc b/source/common/config/subscription_factory_impl.cc index ba40d3ec99dc..4e125e115a2e 100644 --- a/source/common/config/subscription_factory_impl.cc +++ b/source/common/config/subscription_factory_impl.cc @@ -25,8 +25,8 @@ SubscriptionFactoryImpl::SubscriptionFactoryImpl( SubscriptionPtr SubscriptionFactoryImpl::subscriptionFromConfigSource( const envoy::config::core::v3::ConfigSource& config, absl::string_view type_url, - Stats::Scope& scope, SubscriptionCallbacks& callbacks, - OpaqueResourceDecoder& resource_decoder) { + Stats::Scope& scope, SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder, + bool use_namespace_matching) { Config::Utility::checkLocalInfo(type_url, local_info_); std::unique_ptr result; SubscriptionStats stats = Utility::generateStats(scope); @@ -68,7 +68,7 @@ SubscriptionPtr SubscriptionFactoryImpl::subscriptionFromConfigSource( api_config_source.set_node_on_first_message_only()), callbacks, resource_decoder, stats, type_url, dispatcher_, Utility::configSourceInitialFetchTimeout(config), - /*is_aggregated*/ false); + /*is_aggregated*/ false, use_namespace_matching); case envoy::config::core::v3::ApiConfigSource::DELTA_GRPC: { return std::make_unique( std::make_shared( @@ -79,7 +79,8 @@ SubscriptionPtr SubscriptionFactoryImpl::subscriptionFromConfigSource( api_.randomGenerator(), scope, Utility::parseRateLimitSettings(api_config_source), local_info_), callbacks, resource_decoder, stats, type_url, dispatcher_, - Utility::configSourceInitialFetchTimeout(config), false); + Utility::configSourceInitialFetchTimeout(config), /*is_aggregated*/ false, + use_namespace_matching); } default: NOT_REACHED_GCOVR_EXCL_LINE; @@ -88,7 +89,7 @@ SubscriptionPtr SubscriptionFactoryImpl::subscriptionFromConfigSource( case envoy::config::core::v3::ConfigSource::ConfigSourceSpecifierCase::kAds: { return std::make_unique( cm_.adsMux(), callbacks, resource_decoder, stats, type_url, dispatcher_, - Utility::configSourceInitialFetchTimeout(config), true); + Utility::configSourceInitialFetchTimeout(config), true, use_namespace_matching); } default: throw EnvoyException( @@ -99,7 +100,7 @@ SubscriptionPtr SubscriptionFactoryImpl::subscriptionFromConfigSource( SubscriptionPtr SubscriptionFactoryImpl::collectionSubscriptionFromUrl( const xds::core::v3::ResourceLocator& collection_locator, - const envoy::config::core::v3::ConfigSource& /*config*/, absl::string_view /*type_url*/, + const envoy::config::core::v3::ConfigSource& config, absl::string_view resource_type, Stats::Scope& scope, SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder) { std::unique_ptr result; @@ -112,9 +113,32 @@ SubscriptionPtr SubscriptionFactoryImpl::collectionSubscriptionFromUrl( return std::make_unique( dispatcher_, path, callbacks, resource_decoder, stats, validation_visitor_, api_); } + case xds::core::v3::ResourceLocator::XDSTP: { + if (resource_type != collection_locator.resource_type()) { + throw EnvoyException( + fmt::format("xdstp:// type does not match {} in {}", resource_type, + Config::XdsResourceIdentifier::encodeUrl(collection_locator))); + } + const envoy::config::core::v3::ApiConfigSource& api_config_source = config.api_config_source(); + Utility::checkApiConfigSourceSubscriptionBackingCluster(cm_.primaryClusters(), + api_config_source); + + switch (api_config_source.api_type()) { + case envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC: { + return std::make_unique( + collection_locator, cm_.adsMux(), callbacks, resource_decoder, stats, dispatcher_, + Utility::configSourceInitialFetchTimeout(config), false); + } + default: + // TODO(htuch): Add support for non-aggregated delta gRPC, but there will always be options, + // e.g. v2 REST, that don't make sense for xdstp:// ResourceLocators. + throw EnvoyException(fmt::format("Unknown xdstp:// transport API type in {}", + api_config_source.DebugString())); + } + } default: - throw EnvoyException(fmt::format("Unsupported collection resource locator: {}", - XdsResourceIdentifier::encodeUrl(collection_locator))); + // TODO(htuch): Implement HTTP semantics for collection ResourceLocators. + NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } NOT_REACHED_GCOVR_EXCL_LINE; } diff --git a/source/common/config/subscription_factory_impl.h b/source/common/config/subscription_factory_impl.h index 5c0555533a50..1bb8a79feb59 100644 --- a/source/common/config/subscription_factory_impl.h +++ b/source/common/config/subscription_factory_impl.h @@ -23,11 +23,12 @@ class SubscriptionFactoryImpl : public SubscriptionFactory, Logger::Loggable& update_to_these_names) { +AddedRemoved +WatchMap::updateWatchInterest(Watch* watch, + const absl::flat_hash_set& update_to_these_names) { if (update_to_these_names.empty()) { wildcard_watches_.insert(watch); } else { wildcard_watches_.erase(watch); } - std::vector newly_added_to_watch; - std::set_difference(update_to_these_names.begin(), update_to_these_names.end(), - watch->resource_names_.begin(), watch->resource_names_.end(), - std::inserter(newly_added_to_watch, newly_added_to_watch.begin())); + absl::flat_hash_set newly_added_to_watch; + SetUtil::setDifference(update_to_these_names, watch->resource_names_, newly_added_to_watch); - std::vector newly_removed_from_watch; - std::set_difference(watch->resource_names_.begin(), watch->resource_names_.end(), - update_to_these_names.begin(), update_to_these_names.end(), - std::inserter(newly_removed_from_watch, newly_removed_from_watch.begin())); + absl::flat_hash_set newly_removed_from_watch; + SetUtil::setDifference(watch->resource_names_, update_to_these_names, newly_removed_from_watch); watch->resource_names_ = update_to_these_names; @@ -74,7 +90,9 @@ absl::flat_hash_set WatchMap::watchesInterestedIn(const std::string& res } const auto prefix = namespaceFromName(resource_name); - const auto resource_key = use_namespace_matching_ && !prefix.empty() ? prefix : resource_name; + const bool is_xdstp = XdsResourceIdentifier::hasXdsTpScheme(resource_name); + const auto resource_key = + (use_namespace_matching_ || is_xdstp) && !prefix.empty() ? prefix : resource_name; const auto watches_interested = watch_interest_.find(resource_key); if (watches_interested != watch_interest_.end()) { for (const auto& watch : watches_interested->second) { @@ -206,9 +224,10 @@ void WatchMap::onConfigUpdateFailed(ConfigUpdateFailureReason reason, const Envo } } -std::set WatchMap::findAdditions(const std::vector& newly_added_to_watch, - Watch* watch) { - std::set newly_added_to_subscription; +absl::flat_hash_set +WatchMap::findAdditions(const absl::flat_hash_set& newly_added_to_watch, + Watch* watch) { + absl::flat_hash_set newly_added_to_subscription; for (const auto& name : newly_added_to_watch) { auto entry = watch_interest_.find(name); if (entry == watch_interest_.end()) { @@ -222,9 +241,10 @@ std::set WatchMap::findAdditions(const std::vector& ne return newly_added_to_subscription; } -std::set -WatchMap::findRemovals(const std::vector& newly_removed_from_watch, Watch* watch) { - std::set newly_removed_from_subscription; +absl::flat_hash_set +WatchMap::findRemovals(const absl::flat_hash_set& newly_removed_from_watch, + Watch* watch) { + absl::flat_hash_set newly_removed_from_subscription; for (const auto& name : newly_removed_from_watch) { auto entry = watch_interest_.find(name); RELEASE_ASSERT( diff --git a/source/common/config/watch_map.h b/source/common/config/watch_map.h index d0d9e822b28e..768117e93be6 100644 --- a/source/common/config/watch_map.h +++ b/source/common/config/watch_map.h @@ -17,10 +17,10 @@ namespace Envoy { namespace Config { struct AddedRemoved { - AddedRemoved(std::set&& added, std::set&& removed) + AddedRemoved(absl::flat_hash_set&& added, absl::flat_hash_set&& removed) : added_(std::move(added)), removed_(std::move(removed)) {} - std::set added_; - std::set removed_; + absl::flat_hash_set added_; + absl::flat_hash_set removed_; }; struct Watch { @@ -28,7 +28,7 @@ struct Watch { : callbacks_(callbacks), resource_decoder_(resource_decoder) {} SubscriptionCallbacks& callbacks_; OpaqueResourceDecoder& resource_decoder_; - std::set resource_names_; // must be sorted set, for set_difference. + absl::flat_hash_set resource_names_; // Needed only for state-of-the-world. // Whether the most recent update contained any resources this watch cares about. // If true, a new update that also contains no resources can skip this watch. @@ -73,7 +73,7 @@ class WatchMap : public UntypedConfigUpdateCallbacks, public Logger::Loggable& update_to_these_names); + const absl::flat_hash_set& update_to_these_names); // Expects that the watch to be removed has already had all of its resource names removed via // updateWatchInterest(). @@ -96,13 +96,13 @@ class WatchMap : public UntypedConfigUpdateCallbacks, public Logger::Loggable findAdditions(const std::vector& newly_added_to_watch, - Watch* watch); + absl::flat_hash_set + findAdditions(const absl::flat_hash_set& newly_added_to_watch, Watch* watch); // Given a list of names that an individual watch no longer cares about, returns those names that // in fact the entire subscription no longer cares about. - std::set findRemovals(const std::vector& newly_removed_from_watch, - Watch* watch); + absl::flat_hash_set + findRemovals(const absl::flat_hash_set& newly_removed_from_watch, Watch* watch); // Returns the union of watch_interest_[resource_name] and wildcard_watches_. absl::flat_hash_set watchesInterestedIn(const std::string& resource_name); diff --git a/source/common/config/well_known_names.cc b/source/common/config/well_known_names.cc index e8fc767c41a3..6d9ef5308e40 100644 --- a/source/common/config/well_known_names.cc +++ b/source/common/config/well_known_names.cc @@ -1,8 +1,31 @@ #include "common/config/well_known_names.h" +#include "absl/strings/str_replace.h" + namespace Envoy { namespace Config { +namespace { + +// To allow for more readable regular expressions to be declared below, and to +// reduce duplication, define a few common pattern substitutions for regex +// segments. +std::string expandRegex(const std::string& regex) { + return absl::StrReplaceAll( + regex, {// Regex to look for either IPv4 or IPv6 addresses plus port number after underscore. + {"
", R"((?:(?:\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[[a-fA-F_\d]+\])_\d+))"}, + // Cipher names can contain alphanumerics with dashes and + // underscores. + {"", R"([\w-]+)"}, + // A generic name can contain any character except dots. + {"", R"([^\.]+)"}, + // Route names may contain dots in addition to alphanumerics and + // dashes with underscores. + {"", R"([\w-\.]+)"}}); +} + +} // namespace + TagNameValues::TagNameValues() { // Note: the default regexes are defined below in the order that they will typically be matched // (see the TagExtractor class definition for an explanation of the iterative matching process). @@ -24,107 +47,101 @@ TagNameValues::TagNameValues() { // - Typical * notation will be used to denote an arbitrary set of characters. // *_rq(_) - addRegex(RESPONSE_CODE, "_rq(_(\\d{3}))$", "_rq_"); + addRe2(RESPONSE_CODE, R"(_rq(_(\d{3}))$)", "_rq_"); // *_rq_()xx - addRegex(RESPONSE_CODE_CLASS, "_rq_(\\d)xx$", "_rq_"); + addRe2(RESPONSE_CODE_CLASS, R"(_rq_((\d))xx$)", "_rq_"); // http.[.]dynamodb.table.[.]capacity.[.](__partition_id=) - addRegex(DYNAMO_PARTITION_ID, - "^http(?=\\.).*?\\.dynamodb\\.table(?=\\.).*?\\." - "capacity(?=\\.).*?(\\.__partition_id=(\\w{7}))$", - ".dynamodb.table."); + addRe2(DYNAMO_PARTITION_ID, + R"(^http\.\.dynamodb\.table\.\.capacity\.(\.__partition_id=(\w{7}))$)", + ".dynamodb.table."); - // http.[.]dynamodb.operation.(.) or + // http.[.]dynamodb.operation.(.)* or // http.[.]dynamodb.table.[.]capacity.(.)[] - addRegex(DYNAMO_OPERATION, - "^http(?=\\.).*?\\.dynamodb.(?:operation|table(?=" - "\\.).*?\\.capacity)(\\.(.*?))(?:\\.|$)", - ".dynamodb."); + addRe2(DYNAMO_OPERATION, + R"(^http\.\.dynamodb.(?:operation|table\.\.capacity)(\.())(?:\.|$))", + ".dynamodb."); - // mongo.[.]collection.[.]callsite.(.)query. - addRegex(MONGO_CALLSITE, - R"(^mongo(?=\.).*?\.collection(?=\.).*?\.callsite\.((.*?)\.).*?query.\w+?$)", - ".collection."); + // mongo.[.]collection.[.]callsite.(.)query.* + addRe2(MONGO_CALLSITE, R"(^mongo\.\.collection\.\.callsite\.(()\.)query\.)", + ".collection."); - // http.[.]dynamodb.table.(.) or + // http.[.]dynamodb.table.(.)* or // http.[.]dynamodb.error.(.)* - addRegex(DYNAMO_TABLE, R"(^http(?=\.).*?\.dynamodb.(?:table|error)\.((.*?)\.))", ".dynamodb."); + addRe2(DYNAMO_TABLE, R"(^http\.\.dynamodb.(?:table|error)\.(()\.))", ".dynamodb."); - // mongo.[.]collection.(.)query. - addRegex(MONGO_COLLECTION, R"(^mongo(?=\.).*?\.collection\.((.*?)\.).*?query.\w+?$)", - ".collection."); + // mongo.[.]collection.(.)query.* + addRe2(MONGO_COLLECTION, R"(^mongo\.\.collection\.(()\.).*?query\.)", ".collection."); - // mongo.[.]cmd.(.) - addRegex(MONGO_CMD, R"(^mongo(?=\.).*?\.cmd\.((.*?)\.)\w+?$)", ".cmd."); + // mongo.[.]cmd.(.)* + addRe2(MONGO_CMD, R"(^mongo\.\.cmd\.(()\.))", ".cmd."); - // cluster.[.]grpc.[.](.) - addRegex(GRPC_BRIDGE_METHOD, R"(^cluster(?=\.).*?\.grpc(?=\.).*\.((.*?)\.)\w+?$)", ".grpc."); + // cluster.[.]grpc.[.](.)* + addRe2(GRPC_BRIDGE_METHOD, R"(^cluster\.\.grpc\.\.(()\.))", ".grpc."); - // http.[.]user_agent.(.) - addRegex(HTTP_USER_AGENT, R"(^http(?=\.).*?\.user_agent\.((.*?)\.)\w+?$)", ".user_agent."); + // http.[.]user_agent.(.)* + addRe2(HTTP_USER_AGENT, R"(^http\.\.user_agent\.(()\.))", ".user_agent."); - // vhost.[.]vcluster.(.) - addRegex(VIRTUAL_CLUSTER, R"(^vhost(?=\.).*?\.vcluster\.((.*?)\.)\w+?$)", ".vcluster."); + // vhost.[.]vcluster.(.)* + addRe2(VIRTUAL_CLUSTER, R"(^vhost\.\.vcluster\.(()\.))", ".vcluster."); - // http.[.]fault.(.) - addRegex(FAULT_DOWNSTREAM_CLUSTER, R"(^http(?=\.).*?\.fault\.((.*?)\.)\w+?$)", ".fault."); + // http.[.]fault.(.)* + addRe2(FAULT_DOWNSTREAM_CLUSTER, R"(^http\.\.fault\.(()\.))", ".fault."); // listener.[
.]ssl.cipher.() - addRegex(SSL_CIPHER, R"(^listener(?=\.).*?\.ssl\.cipher(\.(.*?))$)"); + addRe2(SSL_CIPHER, R"(^listener\..*?\.ssl\.cipher(\.())$)"); // cluster.[.]ssl.ciphers.() - addRegex(SSL_CIPHER_SUITE, R"(^cluster(?=\.).*?\.ssl\.ciphers(\.(.*?))$)", ".ssl.ciphers."); + addRe2(SSL_CIPHER_SUITE, R"(^cluster\.\.ssl\.ciphers(\.())$)", ".ssl.ciphers."); // cluster.[.]grpc.(.)* - addRegex(GRPC_BRIDGE_SERVICE, R"(^cluster(?=\.).*?\.grpc\.((.*?)\.))", ".grpc."); + addRe2(GRPC_BRIDGE_SERVICE, R"(^cluster\.\.grpc\.(()\.))", ".grpc."); - // tcp.(.) - addRegex(TCP_PREFIX, R"(^tcp\.((.*?)\.)\w+?$)"); + // tcp.(.)* + addRe2(TCP_PREFIX, R"(^tcp\.(()\.))"); - // udp.(.) - addRegex(UDP_PREFIX, R"(^udp\.((.*?)\.)\w+?$)"); + // udp.(.)* + addRe2(UDP_PREFIX, R"(^udp\.(()\.))"); - // auth.clientssl.(.) - addRegex(CLIENTSSL_PREFIX, R"(^auth\.clientssl\.((.*?)\.)\w+?$)"); + // auth.clientssl.(.)* + addRe2(CLIENTSSL_PREFIX, R"(^auth\.clientssl\.(()\.))"); - // ratelimit.(.) - addRegex(RATELIMIT_PREFIX, R"(^ratelimit\.((.*?)\.)\w+?$)"); + // ratelimit.(.)* + addRe2(RATELIMIT_PREFIX, R"(^ratelimit\.(()\.))"); // cluster.(.)* - addRe2(CLUSTER_NAME, "^cluster\\.(([^\\.]+)\\.).*"); + addRe2(CLUSTER_NAME, R"(^cluster\.(()\.))"); // listener.[
.]http.(.)* - addRegex(HTTP_CONN_MANAGER_PREFIX, R"(^listener(?=\.).*?\.http\.((.*?)\.))", ".http."); + // The
part can be anything here (.*?) for the sake of a simpler + // internal state of the regex which performs better. + addRe2(HTTP_CONN_MANAGER_PREFIX, R"(^listener\..*?\.http\.(()\.))", ".http."); // http.(.)* - addRegex(HTTP_CONN_MANAGER_PREFIX, "^http\\.((.*?)\\.)"); + addRe2(HTTP_CONN_MANAGER_PREFIX, R"(^http\.(()\.))"); // listener.(
.)* - addRegex(LISTENER_ADDRESS, - R"(^listener\.(((?:[_.[:digit:]]*|[_\[\]aAbBcCdDeEfF[:digit:]]*))\.))"); + addRe2(LISTENER_ADDRESS, R"(^listener\.((
)\.))"); // vhost.(.)* - addRegex(VIRTUAL_HOST, "^vhost\\.((.*?)\\.)"); + addRe2(VIRTUAL_HOST, R"(^vhost\.(()\.))"); // mongo.(.)* - addRegex(MONGO_PREFIX, "^mongo\\.((.*?)\\.)"); + addRe2(MONGO_PREFIX, R"(^mongo\.(()\.))"); // http.[.]rds.(.) - addRegex(RDS_ROUTE_CONFIG, R"(^http(?=\.).*?\.rds\.((.*?)\.)\w+?$)", ".rds."); + // Note: can contain dots thus we have to maintain full + // match. + addRe2(RDS_ROUTE_CONFIG, R"(^http\.\.rds\.(()\.)\w+?$)", ".rds."); // listener_manager.(worker_.)* - addRegex(WORKER_ID, R"(^listener_manager\.((worker_\d+)\.))", "listener_manager.worker_"); -} - -void TagNameValues::addRegex(const std::string& name, const std::string& regex, - const std::string& substr) { - descriptor_vec_.emplace_back(Descriptor{name, regex, substr, Regex::Type::StdRegex}); + addRe2(WORKER_ID, R"(^listener_manager\.((worker_\d+)\.))", "listener_manager.worker_"); } void TagNameValues::addRe2(const std::string& name, const std::string& regex, const std::string& substr) { - descriptor_vec_.emplace_back(Descriptor{name, regex, substr, Regex::Type::Re2}); + descriptor_vec_.emplace_back(Descriptor{name, expandRegex(regex), substr, Regex::Type::Re2}); } } // namespace Config diff --git a/source/common/config/well_known_names.h b/source/common/config/well_known_names.h index 97ce58fd7265..918360aff1f6 100644 --- a/source/common/config/well_known_names.h +++ b/source/common/config/well_known_names.h @@ -129,7 +129,6 @@ class TagNameValues { const std::vector& descriptorVec() const { return descriptor_vec_; } private: - void addRegex(const std::string& name, const std::string& regex, const std::string& substr = ""); void addRe2(const std::string& name, const std::string& regex, const std::string& substr = ""); // Collection of tag descriptors. diff --git a/source/common/config/xDS_code_diagram.png b/source/common/config/xDS_code_diagram.png deleted file mode 100644 index ef4df79cc1e30f59f865d5c29abc91ca05b19f72..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 97291 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%0A07Moj7KS0o@imd+yv_n<&&CTakng_yb3_4Oj{ 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 L(bFhWv%dB}cK|?= diff --git a/source/common/config/xds_context_params.cc b/source/common/config/xds_context_params.cc index bb92f2ba8d67..e4241f9777ce 100644 --- a/source/common/config/xds_context_params.cc +++ b/source/common/config/xds_context_params.cc @@ -48,28 +48,17 @@ void mergeMetadataJson(Protobuf::Map& params, } // namespace -xds::core::v3::ContextParams XdsContextParams::encode( - const envoy::config::core::v3::Node& node, const std::vector& node_context_params, +xds::core::v3::ContextParams XdsContextParams::encodeResource( + const xds::core::v3::ContextParams& node_context_params, const xds::core::v3::ContextParams& resource_context_params, const std::vector& client_features, const absl::flat_hash_map& extra_resource_params) { xds::core::v3::ContextParams context_params; - auto& mutable_params = *context_params.mutable_params(); // 1. Establish base layer of per-node context parameters. - for (const std::string& ncp : node_context_params) { - // First attempt field accessors known ahead of time, if that fails we consider the cases of - // metadata, either directly in the Node message, or nested in the user_agent_build_version. - if (nodeParamCbs().count(ncp) > 0) { - mutable_params["xds.node." + ncp] = nodeParamCbs().at(ncp)(node); - } else if (ncp == "metadata") { - mergeMetadataJson(mutable_params, node.metadata(), "xds.node.metadata."); - } else if (ncp == "user_agent_build_version.metadata") { - mergeMetadataJson(mutable_params, node.user_agent_build_version().metadata(), - "xds.node.user_agent_build_version.metadata."); - } - } + context_params.MergeFrom(node_context_params); // 2. Overlay with context parameters from resource name. + auto& mutable_params = *context_params.mutable_params(); for (const auto& it : resource_context_params.params()) { mutable_params[it.first] = it.second; } @@ -87,5 +76,45 @@ xds::core::v3::ContextParams XdsContextParams::encode( return context_params; } +xds::core::v3::ContextParams XdsContextParams::encodeNodeContext( + const envoy::config::core::v3::Node& node, + const Protobuf::RepeatedPtrField& node_context_params) { + // E.g. if we have a node instance with contents + // + // user_agent_build_version: + // version: + // major_number: 1 + // minor_number: 2 + // patch: 3 + // metadata: + // foo: true + // bar: "a" + // baz: 42 + // + // and node_context_params [user_agent_build_version.version, user_agent_build_version.metadata], + // we end up with the map: + // + // xds.node.user_agent_build_version.metadata.bar: "\"a\"" + // xds.node.user_agent_build_version.metadata.baz: "42" + // xds.node.user_agent_build_version.metadata.foo: "true" + // xds.node.user_agent_build_version.version: "1.2.3" + xds::core::v3::ContextParams context_params; + auto& mutable_params = *context_params.mutable_params(); + for (const std::string& ncp : node_context_params) { + // First attempt field accessors known ahead of time, if that fails we consider the cases of + // metadata, either directly in the Node message, or nested in the user_agent_build_version. + auto it = nodeParamCbs().find(ncp); + if (it != nodeParamCbs().end()) { + mutable_params["xds.node." + ncp] = (it->second)(node); + } else if (ncp == "metadata") { + mergeMetadataJson(mutable_params, node.metadata(), "xds.node.metadata."); + } else if (ncp == "user_agent_build_version.metadata") { + mergeMetadataJson(mutable_params, node.user_agent_build_version().metadata(), + "xds.node.user_agent_build_version.metadata."); + } + } + return context_params; +} + } // namespace Config } // namespace Envoy diff --git a/source/common/config/xds_context_params.h b/source/common/config/xds_context_params.h index 72991cf2e3db..2627ba8e1513 100644 --- a/source/common/config/xds_context_params.h +++ b/source/common/config/xds_context_params.h @@ -2,6 +2,8 @@ #include "envoy/config/core/v3/base.pb.h" +#include "common/protobuf/protobuf.h" + #include "absl/container/flat_hash_map.h" #include "xds/core/v3/context_params.pb.h" @@ -12,21 +14,32 @@ namespace Config { class XdsContextParams { public: /** - * Encode context parameters by following the xDS transport precedence algorithm and applying - * parameter prefixes. - * @param node reference to the local Node information. - * @param node_context_params a list of node fields to include in context parameters. + * Encode resource context parameters by following the xDS transport precedence algorithm and + * applying parameter prefixes. + * + * @param node_context_params node context parameters. * @param resource_context_params context parameters from resource locator. * @param client_features client feature capabilities. * @param extra_resource_param per-resource type well known attributes. * @return xds::core::v3::ContextParams encoded context parameters. */ static xds::core::v3::ContextParams - encode(const envoy::config::core::v3::Node& node, - const std::vector& node_context_params, - const xds::core::v3::ContextParams& resource_context_params, - const std::vector& client_features, - const absl::flat_hash_map& extra_resource_params); + encodeResource(const xds::core::v3::ContextParams& node_context_params, + const xds::core::v3::ContextParams& resource_context_params, + const std::vector& client_features, + const absl::flat_hash_map& extra_resource_params); + + /** + * Encode node context parameters. + * + * @param node reference to the local Node information. + * @param node_context_params a list of node fields to include in context parameters. + * + * @return xds::core::v3::ContextParams encoded node context parameters. + */ + static xds::core::v3::ContextParams + encodeNodeContext(const envoy::config::core::v3::Node& node, + const Protobuf::RepeatedPtrField& node_context_params); }; } // namespace Config diff --git a/source/common/config/xds_resource.cc b/source/common/config/xds_resource.cc index 5216f96c72f4..414d2f6b3018 100644 --- a/source/common/config/xds_resource.cc +++ b/source/common/config/xds_resource.cc @@ -151,7 +151,7 @@ void decodeFragment( } // namespace xds::core::v3::ResourceName XdsResourceIdentifier::decodeUrn(absl::string_view resource_urn) { - if (!absl::StartsWith(resource_urn, "xdstp:")) { + if (!hasXdsTpScheme(resource_urn)) { throw XdsResourceIdentifier::DecodeException( fmt::format("{} does not have an xdstp: scheme", resource_urn)); } @@ -178,7 +178,7 @@ xds::core::v3::ResourceLocator XdsResourceIdentifier::decodeUrl(absl::string_vie decodeFragment(path.substr(fragment_start + 1), *decoded_resource_locator.mutable_directives()); path = path.substr(0, fragment_start); } - if (absl::StartsWith(resource_url, "xdstp:")) { + if (hasXdsTpScheme(resource_url)) { decoded_resource_locator.set_scheme(xds::core::v3::ResourceLocator::XDSTP); } else if (absl::StartsWith(resource_url, "http:")) { decoded_resource_locator.set_scheme(xds::core::v3::ResourceLocator::HTTP); @@ -203,5 +203,9 @@ xds::core::v3::ResourceLocator XdsResourceIdentifier::decodeUrl(absl::string_vie return decoded_resource_locator; } +bool XdsResourceIdentifier::hasXdsTpScheme(absl::string_view resource_name) { + return absl::StartsWith(resource_name, "xdstp:"); +} + } // namespace Config } // namespace Envoy diff --git a/source/common/config/xds_resource.h b/source/common/config/xds_resource.h index b62d65db5196..9011fd96cd4c 100644 --- a/source/common/config/xds_resource.h +++ b/source/common/config/xds_resource.h @@ -67,6 +67,12 @@ class XdsResourceIdentifier { * @throws DecodeException when parsing fails. */ static xds::core::v3::ResourceLocator decodeUrl(absl::string_view resource_url); + + /** + * @param resource_name resource name. + * @return bool does resource_name have a xdstp: scheme? + */ + static bool hasXdsTpScheme(absl::string_view resource_name); }; } // namespace Config diff --git a/source/common/conn_pool/conn_pool_base.cc b/source/common/conn_pool/conn_pool_base.cc index a5df5aadc0fd..06b82d8eb5e4 100644 --- a/source/common/conn_pool/conn_pool_base.cc +++ b/source/common/conn_pool/conn_pool_base.cc @@ -35,6 +35,28 @@ void ConnPoolImplBase::destructAllConnections() { dispatcher_.clearDeferredDeleteList(); } +bool ConnPoolImplBase::shouldConnect(size_t pending_streams, size_t active_streams, + uint32_t connecting_and_connected_capacity, + float preconnect_ratio, bool anticipate_incoming_stream) { + // This is set to true any time global preconnect is being calculated. + // ClusterManagerImpl::maybePreconnect is called directly before a stream is created, so the + // stream must be anticipated. + // + // Also without this, we would never pre-establish a connection as the first + // connection in a pool because pending/active streams could both be 0. + int anticipated_streams = anticipate_incoming_stream ? 1 : 0; + + // The number of streams we want to be provisioned for is the number of + // pending, active, and anticipated streams times the preconnect ratio. + // The number of streams we are (theoretically) provisioned for is the + // connecting stream capacity plus the number of active streams. + // + // If preconnect ratio is not set, it defaults to 1, and this simplifies to the + // legacy value of pending_streams_.size() > connecting_stream_capacity_ + return (pending_streams + active_streams + anticipated_streams) * preconnect_ratio > + connecting_and_connected_capacity + active_streams; +} + bool ConnPoolImplBase::shouldCreateNewConnection(float global_preconnect_ratio) const { // If the host is not healthy, don't make it do extra work, especially as // upstream selection logic may result in bypassing this upstream entirely. @@ -44,25 +66,25 @@ bool ConnPoolImplBase::shouldCreateNewConnection(float global_preconnect_ratio) return pending_streams_.size() > connecting_stream_capacity_; } - // If global preconnecting is on, and this connection is within the global - // preconnect limit, preconnect. - // We may eventually want to track preconnect_attempts to allow more preconnecting for - // heavily weighted upstreams or sticky picks. - if (global_preconnect_ratio > 1.0 && - ((pending_streams_.size() + 1 + num_active_streams_) * global_preconnect_ratio > - (connecting_stream_capacity_ + num_active_streams_))) { - return true; + // Determine if we are trying to prefetch for global preconnect or local preconnect. + if (global_preconnect_ratio != 0) { + // If global preconnecting is on, and this connection is within the global + // preconnect limit, preconnect. + // For global preconnect, we anticipate an incoming stream to this pool, since it is + // prefetching for the next upcoming stream, which will likely be assigned to this pool. + // We may eventually want to track preconnect_attempts to allow more preconnecting for + // heavily weighted upstreams or sticky picks. + return shouldConnect(pending_streams_.size(), num_active_streams_, connecting_stream_capacity_, + global_preconnect_ratio, true); + } else { + // Ensure this local pool has adequate connections for the given load. + // + // Local preconnect does not need to anticipate a stream. It is called as + // new streams are established or torn down and simply attempts to maintain + // the correct ratio of streams and anticipated capacity. + return shouldConnect(pending_streams_.size(), num_active_streams_, connecting_stream_capacity_, + perUpstreamPreconnectRatio()); } - - // The number of streams we want to be provisioned for is the number of - // pending and active streams times the preconnect ratio. - // The number of streams we are (theoretically) provisioned for is the - // connecting stream capacity plus the number of active streams. - // - // If preconnect ratio is not set, it defaults to 1, and this simplifies to the - // legacy value of pending_streams_.size() > connecting_stream_capacity_ - return (pending_streams_.size() + num_active_streams_) * perUpstreamPreconnectRatio() > - (connecting_stream_capacity_ + num_active_streams_); } float ConnPoolImplBase::perUpstreamPreconnectRatio() const { @@ -73,7 +95,8 @@ float ConnPoolImplBase::perUpstreamPreconnectRatio() const { } } -void ConnPoolImplBase::tryCreateNewConnections() { +ConnPoolImplBase::ConnectionResult ConnPoolImplBase::tryCreateNewConnections() { + ConnPoolImplBase::ConnectionResult result; // Somewhat arbitrarily cap the number of connections preconnected due to new // incoming connections. The preconnect ratio is capped at 3, so in steady // state, no more than 3 connections should be preconnected. If hosts go @@ -81,16 +104,19 @@ void ConnPoolImplBase::tryCreateNewConnections() { // many connections are desired when the host becomes healthy again, but // overwhelming it with connections is not desirable. for (int i = 0; i < 3; ++i) { - if (!tryCreateNewConnection()) { - return; + result = tryCreateNewConnection(); + if (result != ConnectionResult::CreatedNewConnection) { + break; } } + return result; } -bool ConnPoolImplBase::tryCreateNewConnection(float global_preconnect_ratio) { +ConnPoolImplBase::ConnectionResult +ConnPoolImplBase::tryCreateNewConnection(float global_preconnect_ratio) { // There are already enough CONNECTING connections for the number of queued streams. if (!shouldCreateNewConnection(global_preconnect_ratio)) { - return false; + return ConnectionResult::ShouldNotConnect; } const bool can_create_connection = @@ -113,8 +139,12 @@ bool ConnPoolImplBase::tryCreateNewConnection(float global_preconnect_ratio) { state_.incrConnectingStreamCapacity(client->effectiveConcurrentStreamLimit()); connecting_stream_capacity_ += client->effectiveConcurrentStreamLimit(); LinkedList::moveIntoList(std::move(client), owningList(client->state_)); + return can_create_connection ? ConnectionResult::CreatedNewConnection + : ConnectionResult::CreatedButRateLimited; + } else { + ENVOY_LOG(trace, "not creating a new connection: connection constrained"); + return ConnectionResult::NoConnectionRateLimited; } - return can_create_connection; } void ConnPoolImplBase::attachStreamToClient(Envoy::ConnectionPool::ActiveClient& client, @@ -189,18 +219,25 @@ ConnectionPool::Cancellable* ConnPoolImplBase::newStream(AttachContext& context) ActiveClient& client = *ready_clients_.front(); ENVOY_CONN_LOG(debug, "using existing connection", client); attachStreamToClient(client, context); - // Even if there's a ready client, we may want to preconnect a new connection - // to handle the next incoming stream. + // Even if there's a ready client, we may want to preconnect to handle the next incoming stream. tryCreateNewConnections(); return nullptr; } if (host_->cluster().resourceManager(priority_).pendingRequests().canCreate()) { ConnectionPool::Cancellable* pending = newPendingStream(context); + ENVOY_LOG(debug, "trying to create new connection"); + + auto old_capacity = connecting_stream_capacity_; // This must come after newPendingStream() because this function uses the // length of pending_streams_ to determine if a new connection is needed. - tryCreateNewConnections(); - + const ConnectionResult result = tryCreateNewConnections(); + // If there is not enough connecting capacity, the only reason to not + // increase capacity is if the connection limits are exceeded. + ENVOY_BUG(pending_streams_.size() <= connecting_stream_capacity_ || + connecting_stream_capacity_ > old_capacity || + result == ConnectionResult::NoConnectionRateLimited, + fmt::format("Failed to create expected connection: {}", *this)); return pending; } else { ENVOY_LOG(debug, "max pending streams overflow"); @@ -212,7 +249,7 @@ ConnectionPool::Cancellable* ConnPoolImplBase::newStream(AttachContext& context) } bool ConnPoolImplBase::maybePreconnect(float global_preconnect_ratio) { - return tryCreateNewConnection(global_preconnect_ratio); + return tryCreateNewConnection(global_preconnect_ratio) == ConnectionResult::CreatedNewConnection; } void ConnPoolImplBase::scheduleOnUpstreamReady() { @@ -368,7 +405,7 @@ void ConnPoolImplBase::onConnectionEvent(ActiveClient& client, absl::string_view // NOTE: We move the existing pending streams to a temporary list. This is done so that // if retry logic submits a new stream to the pool, we don't fail it inline. purgePendingStreams(client.real_host_description_, failure_reason, reason); - // See if we should preconnect another connection based on active connections. + // See if we should preconnect based on active connections. tryCreateNewConnections(); } diff --git a/source/common/conn_pool/conn_pool_base.h b/source/common/conn_pool/conn_pool_base.h index c2d8ef7da67e..ac8da17e980a 100644 --- a/source/common/conn_pool/conn_pool_base.h +++ b/source/common/conn_pool/conn_pool_base.h @@ -6,6 +6,7 @@ #include "envoy/stats/timespan.h" #include "envoy/upstream/cluster_manager.h" +#include "common/common/dump_state_utils.h" #include "common/common/linked_object.h" #include "absl/strings/string_view.h" @@ -122,6 +123,15 @@ class ConnPoolImplBase : protected Logger::Loggable { return *static_cast(&context); } + // Determines if prefetching is warranted based on the number of streams in + // use, pending streams, anticipated capacity, and preconnect configuration. + // + // If anticipate_incoming_stream is true this assumes a call to newStream is + // pending, which is true for global preconnect. + static bool shouldConnect(size_t pending_streams, size_t active_streams, + uint32_t connecting_and_connected_capacity, float preconnect_ratio, + bool anticipate_incoming_stream = false); + void addDrainedCallbackImpl(Instance::DrainedCb cb); void drainConnectionsImpl(); @@ -158,8 +168,7 @@ class ConnPoolImplBase : protected Logger::Loggable { void checkForDrained(); void scheduleOnUpstreamReady(); ConnectionPool::Cancellable* newStream(AttachContext& context); - // Called if this pool is likely to be picked soon, to determine if it's worth - // preconnecting a connection. + // Called if this pool is likely to be picked soon, to determine if it's worth preconnecting. bool maybePreconnect(float global_preconnect_ratio); virtual ConnectionPool::Cancellable* newPendingStream(AttachContext& context) PURE; @@ -184,17 +193,38 @@ class ConnPoolImplBase : protected Logger::Loggable { } bool hasPendingStreams() const { return !pending_streams_.empty(); } + void dumpState(std::ostream& os, int indent_level = 0) const { + const char* spaces = spacesForLevel(indent_level); + os << spaces << "ConnPoolImplBase " << this << DUMP_MEMBER(ready_clients_.size()) + << DUMP_MEMBER(busy_clients_.size()) << DUMP_MEMBER(connecting_clients_.size()) + << DUMP_MEMBER(connecting_stream_capacity_) << DUMP_MEMBER(num_active_streams_); + } + + friend std::ostream& operator<<(std::ostream& os, const ConnPoolImplBase& s) { + s.dumpState(os); + return os; + } + protected: + // Creates up to 3 connections, based on the preconnect ratio. virtual void onConnected(Envoy::ConnectionPool::ActiveClient&) {} + enum class ConnectionResult { + CreatedNewConnection, + ShouldNotConnect, + NoConnectionRateLimited, + CreatedButRateLimited, + }; + // Creates up to 3 connections, based on the preconnect ratio. - void tryCreateNewConnections(); + // Returns the ConnectionResult of the last attempt. + ConnectionResult tryCreateNewConnections(); // Creates a new connection if there is sufficient demand, it is allowed by resourceManager, or // to avoid starving this pool. // Demand is determined either by perUpstreamPreconnectRatio() or global_preconnect_ratio // if this is called by maybePreconnect() - bool tryCreateNewConnection(float global_preconnect_ratio = 0); + ConnectionResult tryCreateNewConnection(float global_preconnect_ratio = 0); // A helper function which determines if a canceled pending connection should // be closed as excess or not. diff --git a/source/common/event/BUILD b/source/common/event/BUILD index c14a6ee08e99..333755eeac35 100644 --- a/source/common/event/BUILD +++ b/source/common/event/BUILD @@ -34,6 +34,7 @@ envoy_cc_library( ":libevent_scheduler_lib", ":real_time_system_lib", ":signal_lib", + ":scaled_range_timer_manager_lib", "//include/envoy/common:scope_tracker_interface", "//include/envoy/common:time_interface", "//include/envoy/event:signal_interface", @@ -112,6 +113,9 @@ envoy_cc_library( "file_event_impl.h", "schedulable_cb_impl.h", ], + external_deps = [ + "abseil_inlined_vector", + ], deps = [ ":libevent_lib", ":libevent_scheduler_lib", diff --git a/source/common/event/dispatcher_impl.cc b/source/common/event/dispatcher_impl.cc index 558d82b9230d..f67d7787abc9 100644 --- a/source/common/event/dispatcher_impl.cc +++ b/source/common/event/dispatcher_impl.cc @@ -7,14 +7,17 @@ #include #include "envoy/api/api.h" +#include "envoy/common/scope_tracker.h" #include "envoy/network/listen_socket.h" #include "envoy/network/listener.h" #include "common/buffer/buffer_impl.h" +#include "common/common/assert.h" #include "common/common/lock_guard.h" #include "common/common/thread.h" #include "common/event/file_event_impl.h" #include "common/event/libevent_scheduler.h" +#include "common/event/scaled_range_timer_manager_impl.h" #include "common/event/signal_impl.h" #include "common/event/timer_impl.h" #include "common/filesystem/watcher_impl.h" @@ -37,17 +40,33 @@ namespace Envoy { namespace Event { +DispatcherImpl::DispatcherImpl(const std::string& name, Api::Api& api, + Event::TimeSystem& time_system) + : DispatcherImpl(name, api, time_system, {}) {} + +DispatcherImpl::DispatcherImpl(const std::string& name, Api::Api& api, + Event::TimeSystem& time_system, + const Buffer::WatermarkFactorySharedPtr& watermark_factory) + : DispatcherImpl( + name, api, time_system, + [](Dispatcher& dispatcher) { + return std::make_unique(dispatcher); + }, + watermark_factory) {} + DispatcherImpl::DispatcherImpl(const std::string& name, Api::Api& api, Event::TimeSystem& time_system, - const Buffer::WatermarkFactorySharedPtr& factory) + const ScaledRangeTimerManagerFactory& scaled_timer_factory, + const Buffer::WatermarkFactorySharedPtr& watermark_factory) : name_(name), api_(api), - buffer_factory_(factory != nullptr ? factory - : std::make_shared()), + buffer_factory_(watermark_factory != nullptr + ? watermark_factory + : std::make_shared()), scheduler_(time_system.createScheduler(base_scheduler_, base_scheduler_)), deferred_delete_cb_(base_scheduler_.createSchedulableCallback( [this]() -> void { clearDeferredDeleteList(); })), post_cb_(base_scheduler_.createSchedulableCallback([this]() -> void { runPostCallbacks(); })), - current_to_delete_(&to_delete_1_) { + current_to_delete_(&to_delete_1_), scaled_timer_manager_(scaled_timer_factory(*this)) { ASSERT(!name_.empty()); FatalErrorHandler::registerFatalErrorHandler(*this); updateApproximateMonotonicTimeInternal(); @@ -190,6 +209,16 @@ TimerPtr DispatcherImpl::createTimer(TimerCb cb) { return createTimerInternal(cb); } +TimerPtr DispatcherImpl::createScaledTimer(ScaledTimerType timer_type, TimerCb cb) { + ASSERT(isThreadSafe()); + return scaled_timer_manager_->createTimer(timer_type, std::move(cb)); +} + +TimerPtr DispatcherImpl::createScaledTimer(ScaledTimerMinimum minimum, TimerCb cb) { + ASSERT(isThreadSafe()); + return scaled_timer_manager_->createTimer(minimum, std::move(cb)); +} + Event::SchedulableCallbackPtr DispatcherImpl::createSchedulableCallback(std::function cb) { ASSERT(isThreadSafe()); return base_scheduler_.createSchedulableCallback([this, cb]() { @@ -287,6 +316,16 @@ void DispatcherImpl::runPostCallbacks() { } } +void DispatcherImpl::onFatalError(std::ostream& os) const { + // Dump the state of the tracked objects in the dispatcher if thread safe. This generally + // results in dumping the active state only for the thread which caused the fatal error. + if (isThreadSafe()) { + for (auto iter = tracked_object_stack_.rbegin(); iter != tracked_object_stack_.rend(); ++iter) { + (*iter)->dumpState(os); + } + } +} + void DispatcherImpl::runFatalActionsOnTrackedObject( const FatalAction::FatalActionPtrList& actions) const { // Only run if this is the dispatcher of the current thread and @@ -296,7 +335,7 @@ void DispatcherImpl::runFatalActionsOnTrackedObject( } for (const auto& action : actions) { - action->run(current_object_); + action->run(tracked_object_stack_); } } @@ -306,5 +345,23 @@ void DispatcherImpl::touchWatchdog() { } } +void DispatcherImpl::pushTrackedObject(const ScopeTrackedObject* object) { + ASSERT(isThreadSafe()); + ASSERT(object != nullptr); + tracked_object_stack_.push_back(object); + ASSERT(tracked_object_stack_.size() <= ExpectedMaxTrackedObjectStackDepth); +} + +void DispatcherImpl::popTrackedObject(const ScopeTrackedObject* expected_object) { + ASSERT(isThreadSafe()); + ASSERT(expected_object != nullptr); + RELEASE_ASSERT(!tracked_object_stack_.empty(), "Tracked Object Stack is empty, nothing to pop!"); + + const ScopeTrackedObject* top = tracked_object_stack_.back(); + tracked_object_stack_.pop_back(); + ASSERT(top == expected_object, + "Popped the top of the tracked object stack, but it wasn't the expected object!"); +} + } // namespace Event } // namespace Envoy diff --git a/source/common/event/dispatcher_impl.h b/source/common/event/dispatcher_impl.h index bd3b698af11f..f94107ec79bb 100644 --- a/source/common/event/dispatcher_impl.h +++ b/source/common/event/dispatcher_impl.h @@ -20,9 +20,16 @@ #include "common/event/libevent_scheduler.h" #include "common/signal/fatal_error_handler.h" +#include "absl/container/inlined_vector.h" + namespace Envoy { namespace Event { +// The tracked object stack likely won't grow larger than this initial +// reservation; this should make appends constant time since the stack +// shouldn't have to grow larger. +inline constexpr size_t ExpectedMaxTrackedObjectStackDepth = 10; + /** * libevent implementation of Event::Dispatcher. */ @@ -30,8 +37,12 @@ class DispatcherImpl : Logger::Loggable, public Dispatcher, public FatalErrorHandlerInterface { public: + DispatcherImpl(const std::string& name, Api::Api& api, Event::TimeSystem& time_system); + DispatcherImpl(const std::string& name, Api::Api& api, Event::TimeSystem& time_systems, + const Buffer::WatermarkFactorySharedPtr& watermark_factory); DispatcherImpl(const std::string& name, Api::Api& api, Event::TimeSystem& time_system, - const Buffer::WatermarkFactorySharedPtr& factory = nullptr); + const ScaledRangeTimerManagerFactory& scaled_timer_factory, + const Buffer::WatermarkFactorySharedPtr& watermark_factory); ~DispatcherImpl() override; /** @@ -67,6 +78,9 @@ class DispatcherImpl : Logger::Loggable, Network::UdpListenerPtr createUdpListener(Network::SocketSharedPtr socket, Network::UdpListenerCallbacks& cb) override; TimerPtr createTimer(TimerCb cb) override; + TimerPtr createScaledTimer(ScaledTimerType timer_type, TimerCb cb) override; + TimerPtr createScaledTimer(ScaledTimerMinimum minimum, TimerCb cb) override; + Event::SchedulableCallbackPtr createSchedulableCallback(std::function cb) override; void deferredDelete(DeferredDeletablePtr&& to_delete) override; void exit() override; @@ -74,25 +88,13 @@ class DispatcherImpl : Logger::Loggable, void post(std::function callback) override; void run(RunType type) override; Buffer::WatermarkFactory& getWatermarkFactory() override { return *buffer_factory_; } - const ScopeTrackedObject* setTrackedObject(const ScopeTrackedObject* object) override { - const ScopeTrackedObject* return_object = current_object_; - current_object_ = object; - return return_object; - } + void pushTrackedObject(const ScopeTrackedObject* object) override; + void popTrackedObject(const ScopeTrackedObject* expected_object) override; MonotonicTime approximateMonotonicTime() const override; void updateApproximateMonotonicTime() override; // FatalErrorInterface - void onFatalError(std::ostream& os) const override { - // Dump the state of the tracked object if it is in the current thread. This generally results - // in dumping the active state only for the thread which caused the fatal error. - if (isThreadSafe()) { - if (current_object_) { - current_object_->dumpState(os); - } - } - } - + void onFatalError(std::ostream& os) const override; void runFatalActionsOnTrackedObject(const FatalAction::FatalActionPtrList& actions) const override; @@ -150,10 +152,12 @@ class DispatcherImpl : Logger::Loggable, std::vector* current_to_delete_; Thread::MutexBasicLockable post_lock_; std::list> post_callbacks_ ABSL_GUARDED_BY(post_lock_); - const ScopeTrackedObject* current_object_{}; + absl::InlinedVector + tracked_object_stack_; bool deferred_deleting_{}; MonotonicTime approximate_monotonic_time_; WatchdogRegistrationPtr watchdog_registration_; + const ScaledRangeTimerManagerPtr scaled_timer_manager_; }; } // namespace Event diff --git a/source/common/event/scaled_range_timer_manager_impl.cc b/source/common/event/scaled_range_timer_manager_impl.cc index 6aab81486929..baea9de390bf 100644 --- a/source/common/event/scaled_range_timer_manager_impl.cc +++ b/source/common/event/scaled_range_timer_manager_impl.cc @@ -137,8 +137,12 @@ class ScaledRangeTimerManagerImpl::RangeTimerImpl final : public Timer { const ScopeTrackedObject* scope_; }; -ScaledRangeTimerManagerImpl::ScaledRangeTimerManagerImpl(Dispatcher& dispatcher) - : dispatcher_(dispatcher), scale_factor_(1.0) {} +ScaledRangeTimerManagerImpl::ScaledRangeTimerManagerImpl( + Dispatcher& dispatcher, const ScaledTimerTypeMapConstSharedPtr& timer_minimums) + : dispatcher_(dispatcher), + timer_minimums_(timer_minimums != nullptr ? timer_minimums + : std::make_shared()), + scale_factor_(1.0) {} ScaledRangeTimerManagerImpl::~ScaledRangeTimerManagerImpl() { // Scaled timers created by the manager shouldn't outlive it. This is @@ -146,13 +150,22 @@ ScaledRangeTimerManagerImpl::~ScaledRangeTimerManagerImpl() { ASSERT(queues_.empty()); } +TimerPtr ScaledRangeTimerManagerImpl::createTimer(ScaledTimerType timer_type, TimerCb callback) { + const auto minimum_it = timer_minimums_->find(timer_type); + const Event::ScaledTimerMinimum minimum = + minimum_it != timer_minimums_->end() + ? minimum_it->second + : Event::ScaledTimerMinimum(Event::ScaledMinimum(UnitFloat::max())); + return createTimer(minimum, std::move(callback)); +} + TimerPtr ScaledRangeTimerManagerImpl::createTimer(ScaledTimerMinimum minimum, TimerCb callback) { return std::make_unique(minimum, callback, *this); } -void ScaledRangeTimerManagerImpl::setScaleFactor(double scale_factor) { +void ScaledRangeTimerManagerImpl::setScaleFactor(UnitFloat scale_factor) { const MonotonicTime now = dispatcher_.approximateMonotonicTime(); - scale_factor_ = UnitFloat(scale_factor); + scale_factor_ = scale_factor; for (auto& queue : queues_) { resetQueueTimer(*queue, now); } diff --git a/source/common/event/scaled_range_timer_manager_impl.h b/source/common/event/scaled_range_timer_manager_impl.h index f9bcc7242b7d..4c36b50f5de0 100644 --- a/source/common/event/scaled_range_timer_manager_impl.h +++ b/source/common/event/scaled_range_timer_manager_impl.h @@ -1,3 +1,5 @@ +#pragma once + #include #include @@ -21,12 +23,15 @@ namespace Event { */ class ScaledRangeTimerManagerImpl : public ScaledRangeTimerManager { public: - explicit ScaledRangeTimerManagerImpl(Dispatcher& dispatcher); + // Takes a Dispatcher and a map from timer type to scaled minimum value. + ScaledRangeTimerManagerImpl(Dispatcher& dispatcher, + const ScaledTimerTypeMapConstSharedPtr& timer_minimums = nullptr); ~ScaledRangeTimerManagerImpl() override; // ScaledRangeTimerManager impl TimerPtr createTimer(ScaledTimerMinimum minimum, TimerCb callback) override; - void setScaleFactor(double scale_factor) override; + TimerPtr createTimer(ScaledTimerType timer_type, TimerCb callback) override; + void setScaleFactor(UnitFloat scale_factor) override; private: class RangeTimerImpl; @@ -114,9 +119,10 @@ class ScaledRangeTimerManagerImpl : public ScaledRangeTimerManager { void onQueueTimerFired(Queue& queue); Dispatcher& dispatcher_; + const ScaledTimerTypeMapConstSharedPtr timer_minimums_; UnitFloat scale_factor_; absl::flat_hash_set, Hash, Eq> queues_; }; } // namespace Event -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/source/common/filter/http/filter_config_discovery_impl.cc b/source/common/filter/http/filter_config_discovery_impl.cc index aef0519b4a88..feae7528f4c2 100644 --- a/source/common/filter/http/filter_config_discovery_impl.cc +++ b/source/common/filter/http/filter_config_discovery_impl.cc @@ -87,7 +87,8 @@ FilterConfigSubscription::FilterConfigSubscription( const auto resource_name = getResourceName(); subscription_ = factory_context.clusterManager().subscriptionFactory().subscriptionFromConfigSource( - config_source, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_); + config_source, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_, + false); } void FilterConfigSubscription::start() { diff --git a/source/common/formatter/BUILD b/source/common/formatter/BUILD index 5747f9d1e7f9..7b36bb0b7b28 100644 --- a/source/common/formatter/BUILD +++ b/source/common/formatter/BUILD @@ -35,6 +35,7 @@ envoy_cc_library( hdrs = ["substitution_format_string.h"], deps = [ ":substitution_formatter_lib", + "//source/common/config:utility_lib", "//source/common/protobuf", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], diff --git a/source/common/formatter/substitution_format_string.cc b/source/common/formatter/substitution_format_string.cc index 372b9fc6a612..bddaac039a7f 100644 --- a/source/common/formatter/substitution_format_string.cc +++ b/source/common/formatter/substitution_format_string.cc @@ -3,6 +3,7 @@ #include "envoy/api/api.h" #include "common/config/datasource.h" +#include "common/config/utility.h" #include "common/formatter/substitution_formatter.h" namespace Envoy { @@ -16,14 +17,26 @@ SubstitutionFormatStringUtils::createJsonFormatter(const ProtobufWkt::Struct& st FormatterPtr SubstitutionFormatStringUtils::fromProtoConfig( const envoy::config::core::v3::SubstitutionFormatString& config, Api::Api& api) { + // Instantiate formatter extensions. + std::vector commands; + for (const auto& formatter : config.formatters()) { + auto* factory = Envoy::Config::Utility::getFactory(formatter); + if (!factory) { + throw EnvoyException(absl::StrCat("Formatter not found: ", formatter.name())); + } + auto parser = factory->createCommandParserFromProto(formatter.typed_config()); + commands.push_back(std::move(parser)); + } + switch (config.format_case()) { case envoy::config::core::v3::SubstitutionFormatString::FormatCase::kTextFormat: - return std::make_unique(config.text_format(), config.omit_empty_values()); + return std::make_unique(config.text_format(), config.omit_empty_values(), + commands); case envoy::config::core::v3::SubstitutionFormatString::FormatCase::kJsonFormat: { return createJsonFormatter(config.json_format(), true, config.omit_empty_values()); case envoy::config::core::v3::SubstitutionFormatString::FormatCase::kTextFormatSource: return std::make_unique( - Config::DataSource::read(config.text_format_source(), true, api)); + Config::DataSource::read(config.text_format_source(), true, api), false, commands); } default: NOT_REACHED_GCOVR_EXCL_LINE; diff --git a/source/common/formatter/substitution_formatter.cc b/source/common/formatter/substitution_formatter.cc index 1072ae132f6f..2f4737bf3fb4 100644 --- a/source/common/formatter/substitution_formatter.cc +++ b/source/common/formatter/substitution_formatter.cc @@ -49,9 +49,6 @@ const std::regex& getSystemTimeFormatNewlinePattern() { } const std::regex& getNewlinePattern() { CONSTRUCT_ON_FIRST_USE(std::regex, "\n"); } -template struct StructFormatMapVisitor : Ts... { using Ts::operator()...; }; -template StructFormatMapVisitor(Ts...) -> StructFormatMapVisitor; - } // namespace const std::string SubstitutionFormatUtils::DEFAULT_FORMAT = @@ -113,6 +110,12 @@ FormatterImpl::FormatterImpl(const std::string& format, bool omit_empty_values) providers_ = SubstitutionFormatParser::parse(format); } +FormatterImpl::FormatterImpl(const std::string& format, bool omit_empty_values, + const std::vector& command_parsers) + : empty_value_string_(omit_empty_values ? EMPTY_STRING : DefaultUnspecifiedValueString) { + providers_ = SubstitutionFormatParser::parse(format, command_parsers); +} + std::string FormatterImpl::format(const Http::RequestHeaderMap& request_headers, const Http::ResponseHeaderMap& response_headers, const Http::ResponseTrailerMap& response_trailers, @@ -142,76 +145,145 @@ std::string JsonFormatterImpl::format(const Http::RequestHeaderMap& request_head return absl::StrCat(log_line, "\n"); } +StructFormatter::StructFormatter(const ProtobufWkt::Struct& format_mapping, bool preserve_types, + bool omit_empty_values) + : omit_empty_values_(omit_empty_values), preserve_types_(preserve_types), + empty_value_(omit_empty_values_ ? EMPTY_STRING : DefaultUnspecifiedValueString), + struct_output_format_(toFormatMapValue(format_mapping)) {} + StructFormatter::StructFormatMapWrapper -StructFormatter::toFormatMap(const ProtobufWkt::Struct& struct_format) const { +StructFormatter::toFormatMapValue(const ProtobufWkt::Struct& struct_format) const { auto output = std::make_unique(); for (const auto& pair : struct_format.fields()) { switch (pair.second.kind_case()) { case ProtobufWkt::Value::kStringValue: - output->emplace(pair.first, SubstitutionFormatParser::parse(pair.second.string_value())); + output->emplace(pair.first, toFormatStringValue(pair.second.string_value())); + break; + + case ProtobufWkt::Value::kStructValue: + output->emplace(pair.first, toFormatMapValue(pair.second.struct_value())); + break; + + case ProtobufWkt::Value::kListValue: + output->emplace(pair.first, toFormatListValue(pair.second.list_value())); + break; + + default: + throw EnvoyException("Only string values, nested structs and list values are " + "supported in structured access log format."); + } + } + return {std::move(output)}; +}; + +StructFormatter::StructFormatListWrapper +StructFormatter::toFormatListValue(const ProtobufWkt::ListValue& list_value_format) const { + auto output = std::make_unique(); + for (const auto& value : list_value_format.values()) { + switch (value.kind_case()) { + case ProtobufWkt::Value::kStringValue: + output->emplace_back(toFormatStringValue(value.string_value())); break; + case ProtobufWkt::Value::kStructValue: - output->emplace(pair.first, toFormatMap(pair.second.struct_value())); + output->emplace_back(toFormatMapValue(value.struct_value())); + break; + + case ProtobufWkt::Value::kListValue: + output->emplace_back(toFormatListValue(value.list_value())); break; default: - throw EnvoyException( - "Only string values or nested structs are supported in structured access log format."); + throw EnvoyException("Only string values, nested structs and list values are " + "supported in structured access log format."); } } return {std::move(output)}; +} + +std::vector +StructFormatter::toFormatStringValue(const std::string& string_format) const { + return SubstitutionFormatParser::parse(string_format); }; +ProtobufWkt::Value StructFormatter::providersCallback( + const std::vector& providers, + const Http::RequestHeaderMap& request_headers, const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap& response_trailers, const StreamInfo::StreamInfo& stream_info, + absl::string_view local_reply_body) const { + ASSERT(!providers.empty()); + if (providers.size() == 1) { + const auto& provider = providers.front(); + if (preserve_types_) { + return provider->formatValue(request_headers, response_headers, response_trailers, + stream_info, local_reply_body); + } + + if (omit_empty_values_) { + return ValueUtil::optionalStringValue(provider->format( + request_headers, response_headers, response_trailers, stream_info, local_reply_body)); + } + + const auto str = provider->format(request_headers, response_headers, response_trailers, + stream_info, local_reply_body); + return ValueUtil::stringValue(str.value_or(DefaultUnspecifiedValueString)); + } + // Multiple providers forces string output. + std::string str; + for (const auto& provider : providers) { + const auto bit = provider->format(request_headers, response_headers, response_trailers, + stream_info, local_reply_body); + str += bit.value_or(empty_value_); + } + return ValueUtil::stringValue(str); +} + +ProtobufWkt::Value StructFormatter::structFormatMapCallback( + const StructFormatter::StructFormatMapWrapper& format_map, + const StructFormatter::StructFormatMapVisitor& visitor) const { + ProtobufWkt::Struct output; + auto* fields = output.mutable_fields(); + for (const auto& pair : *format_map.value_) { + ProtobufWkt::Value value = absl::visit(visitor, pair.second); + if (omit_empty_values_ && value.kind_case() == ProtobufWkt::Value::kNullValue) { + continue; + } + (*fields)[pair.first] = value; + } + return ValueUtil::structValue(output); +} + +ProtobufWkt::Value StructFormatter::structFormatListCallback( + const StructFormatter::StructFormatListWrapper& format_list, + const StructFormatter::StructFormatMapVisitor& visitor) const { + std::vector output; + for (const auto& val : *format_list.value_) { + ProtobufWkt::Value value = absl::visit(visitor, val); + if (omit_empty_values_ && value.kind_case() == ProtobufWkt::Value::kNullValue) { + continue; + } + output.push_back(value); + } + return ValueUtil::listValue(output); +} + ProtobufWkt::Struct StructFormatter::format(const Http::RequestHeaderMap& request_headers, const Http::ResponseHeaderMap& response_headers, const Http::ResponseTrailerMap& response_trailers, const StreamInfo::StreamInfo& stream_info, absl::string_view local_reply_body) const { - const std::string& empty_value = - omit_empty_values_ ? EMPTY_STRING : DefaultUnspecifiedValueString; - const std::function&)> - providers_callback = [&](const std::vector& providers) { - ASSERT(!providers.empty()); - if (providers.size() == 1) { - const auto& provider = providers.front(); - if (preserve_types_) { - return provider->formatValue(request_headers, response_headers, response_trailers, - stream_info, local_reply_body); - } - - if (omit_empty_values_) { - return ValueUtil::optionalStringValue( - provider->format(request_headers, response_headers, response_trailers, stream_info, - local_reply_body)); - } - - const auto str = provider->format(request_headers, response_headers, response_trailers, - stream_info, local_reply_body); - return ValueUtil::stringValue(str.value_or(DefaultUnspecifiedValueString)); - } - // Multiple providers forces string output. - std::string str; - for (const auto& provider : providers) { - const auto bit = provider->format(request_headers, response_headers, response_trailers, - stream_info, local_reply_body); - str += bit.value_or(empty_value); - } - return ValueUtil::stringValue(str); - }; - const std::function - struct_format_map_callback = [&](const StructFormatter::StructFormatMapWrapper& format) { - ProtobufWkt::Struct output; - auto* fields = output.mutable_fields(); - StructFormatMapVisitor visitor{struct_format_map_callback, providers_callback}; - for (const auto& pair : *format.value_) { - ProtobufWkt::Value value = absl::visit(visitor, pair.second); - if (omit_empty_values_ && value.kind_case() == ProtobufWkt::Value::kNullValue) { - continue; - } - (*fields)[pair.first] = value; - } - return ValueUtil::structValue(output); - }; - return struct_format_map_callback(struct_output_format_).struct_value(); + StructFormatMapVisitor visitor{ + [&](const std::vector& providers) { + return providersCallback(providers, request_headers, response_headers, response_trailers, + stream_info, local_reply_body); + }, + [&, this](const StructFormatter::StructFormatMapWrapper& format_map) { + return structFormatMapCallback(format_map, visitor); + }, + [&, this](const StructFormatter::StructFormatListWrapper& format_list) { + return structFormatListCallback(format_list, visitor); + }, + }; + return structFormatMapCallback(struct_output_format_, visitor).struct_value(); } void SubstitutionFormatParser::parseCommandHeader(const std::string& token, const size_t start, @@ -282,109 +354,143 @@ void SubstitutionFormatParser::parseCommand(const std::string& token, const size } } -// TODO(derekargueta): #2967 - Rewrite SubstitutionFormatter with parser library & formal grammar std::vector SubstitutionFormatParser::parse(const std::string& format) { - std::string current_token; - std::vector formatters; + return SubstitutionFormatParser::parse(format, {}); +} + +FormatterProviderPtr SubstitutionFormatParser::parseBuiltinCommand(const std::string& token) { static constexpr absl::string_view DYNAMIC_META_TOKEN{"DYNAMIC_METADATA("}; static constexpr absl::string_view FILTER_STATE_TOKEN{"FILTER_STATE("}; - const std::regex command_w_args_regex(R"EOF(^%([A-Z]|_)+(\([^\)]*\))?(:[0-9]+)?(%))EOF"); - static constexpr absl::string_view PLAIN_SERIALIZATION{"PLAIN"}; static constexpr absl::string_view TYPED_SERIALIZATION{"TYPED"}; + if (absl::StartsWith(token, "REQ(")) { + std::string main_header, alternative_header; + absl::optional max_length; + + parseCommandHeader(token, ReqParamStart, main_header, alternative_header, max_length); + + return std::make_unique(main_header, alternative_header, max_length); + } else if (absl::StartsWith(token, "RESP(")) { + std::string main_header, alternative_header; + absl::optional max_length; + + parseCommandHeader(token, RespParamStart, main_header, alternative_header, max_length); + + return std::make_unique(main_header, alternative_header, max_length); + } else if (absl::StartsWith(token, "TRAILER(")) { + std::string main_header, alternative_header; + absl::optional max_length; + + parseCommandHeader(token, TrailParamStart, main_header, alternative_header, max_length); + + return std::make_unique(main_header, alternative_header, max_length); + } else if (absl::StartsWith(token, "LOCAL_REPLY_BODY")) { + return std::make_unique(); + } else if (absl::StartsWith(token, DYNAMIC_META_TOKEN)) { + std::string filter_namespace; + absl::optional max_length; + std::vector path; + const size_t start = DYNAMIC_META_TOKEN.size(); + + parseCommand(token, start, ":", filter_namespace, path, max_length); + return std::make_unique(filter_namespace, path, max_length); + } else if (absl::StartsWith(token, FILTER_STATE_TOKEN)) { + std::string key; + absl::optional max_length; + std::vector path; + const size_t start = FILTER_STATE_TOKEN.size(); + + parseCommand(token, start, ":", key, path, max_length); + if (key.empty()) { + throw EnvoyException("Invalid filter state configuration, key cannot be empty."); + } + + const absl::string_view serialize_type = + !path.empty() ? path[path.size() - 1] : TYPED_SERIALIZATION; + + if (serialize_type != PLAIN_SERIALIZATION && serialize_type != TYPED_SERIALIZATION) { + throw EnvoyException("Invalid filter state serialize type, only support PLAIN/TYPED."); + } + const bool serialize_as_string = serialize_type == PLAIN_SERIALIZATION; + + return std::make_unique(key, max_length, serialize_as_string); + } else if (absl::StartsWith(token, "START_TIME")) { + return std::make_unique(token); + } else if (absl::StartsWith(token, "DOWNSTREAM_PEER_CERT_V_START")) { + return std::make_unique(token); + } else if (absl::StartsWith(token, "DOWNSTREAM_PEER_CERT_V_END")) { + return std::make_unique(token); + } else if (absl::StartsWith(token, "GRPC_STATUS")) { + return std::make_unique("grpc-status", "", absl::optional()); + } else if (absl::StartsWith(token, "REQUEST_HEADERS_BYTES")) { + return std::make_unique( + HeadersByteSizeFormatter::HeaderType::RequestHeaders); + } else if (absl::StartsWith(token, "RESPONSE_HEADERS_BYTES")) { + return std::make_unique( + HeadersByteSizeFormatter::HeaderType::ResponseHeaders); + } else if (absl::StartsWith(token, "RESPONSE_TRAILERS_BYTES")) { + return std::make_unique( + HeadersByteSizeFormatter::HeaderType::ResponseTrailers); + } + + return nullptr; +} + +// TODO(derekargueta): #2967 - Rewrite SubstitutionFormatter with parser library & formal grammar +std::vector +SubstitutionFormatParser::parse(const std::string& format, + const std::vector& commands) { + std::string current_token; + std::vector formatters; + const std::regex command_w_args_regex(R"EOF(^%([A-Z]|_)+(\([^\)]*\))?(:[0-9]+)?(%))EOF"); + for (size_t pos = 0; pos < format.length(); ++pos) { - if (format[pos] == '%') { - if (!current_token.empty()) { - formatters.emplace_back(FormatterProviderPtr{new PlainStringFormatter(current_token)}); - current_token = ""; - } + if (format[pos] != '%') { + current_token += format[pos]; + continue; + } - std::smatch m; - const std::string search_space = format.substr(pos); - if (!std::regex_search(search_space, m, command_w_args_regex)) { - throw EnvoyException( - fmt::format("Incorrect configuration: {}. Couldn't find valid command at position {}", - format, pos)); - } + if (!current_token.empty()) { + formatters.emplace_back(FormatterProviderPtr{new PlainStringFormatter(current_token)}); + current_token = ""; + } - const std::string match = m.str(0); - const std::string token = match.substr(1, match.length() - 2); - pos += 1; - const int command_end_position = pos + token.length(); - - if (absl::StartsWith(token, "REQ(")) { - std::string main_header, alternative_header; - absl::optional max_length; - - parseCommandHeader(token, ReqParamStart, main_header, alternative_header, max_length); - - formatters.emplace_back(FormatterProviderPtr{ - new RequestHeaderFormatter(main_header, alternative_header, max_length)}); - } else if (absl::StartsWith(token, "RESP(")) { - std::string main_header, alternative_header; - absl::optional max_length; - - parseCommandHeader(token, RespParamStart, main_header, alternative_header, max_length); - - formatters.emplace_back(FormatterProviderPtr{ - new ResponseHeaderFormatter(main_header, alternative_header, max_length)}); - } else if (absl::StartsWith(token, "TRAILER(")) { - std::string main_header, alternative_header; - absl::optional max_length; - - parseCommandHeader(token, TrailParamStart, main_header, alternative_header, max_length); - - formatters.emplace_back(FormatterProviderPtr{ - new ResponseTrailerFormatter(main_header, alternative_header, max_length)}); - } else if (absl::StartsWith(token, "LOCAL_REPLY_BODY")) { - formatters.emplace_back(std::make_unique()); - } else if (absl::StartsWith(token, DYNAMIC_META_TOKEN)) { - std::string filter_namespace; - absl::optional max_length; - std::vector path; - const size_t start = DYNAMIC_META_TOKEN.size(); - - parseCommand(token, start, ":", filter_namespace, path, max_length); - formatters.emplace_back( - FormatterProviderPtr{new DynamicMetadataFormatter(filter_namespace, path, max_length)}); - } else if (absl::StartsWith(token, FILTER_STATE_TOKEN)) { - std::string key; - absl::optional max_length; - std::vector path; - const size_t start = FILTER_STATE_TOKEN.size(); - - parseCommand(token, start, ":", key, path, max_length); - if (key.empty()) { - throw EnvoyException("Invalid filter state configuration, key cannot be empty."); - } + std::smatch m; + const std::string search_space = format.substr(pos); + if (!std::regex_search(search_space, m, command_w_args_regex)) { + throw EnvoyException(fmt::format( + "Incorrect configuration: {}. Couldn't find valid command at position {}", format, pos)); + } - const absl::string_view serialize_type = - !path.empty() ? path[path.size() - 1] : TYPED_SERIALIZATION; + const std::string match = m.str(0); + const std::string token = match.substr(1, match.length() - 2); + pos += 1; + const size_t command_end_position = pos + token.length(); - if (serialize_type != PLAIN_SERIALIZATION && serialize_type != TYPED_SERIALIZATION) { - throw EnvoyException("Invalid filter state serialize type, only support PLAIN/TYPED."); + auto formatter = parseBuiltinCommand(token); + if (formatter) { + formatters.push_back(std::move(formatter)); + } else { + // Check formatter extensions. These are used for anything not provided by the built-in + // operators, e.g.: specialized formatting, computing stats from request/response headers + // or from stream info, etc. + bool added = false; + for (const auto& cmd : commands) { + auto formatter = cmd->parse(token, pos, command_end_position); + if (formatter) { + formatters.push_back(std::move(formatter)); + added = true; + break; } - const bool serialize_as_string = serialize_type == PLAIN_SERIALIZATION; - - formatters.push_back( - std::make_unique(key, max_length, serialize_as_string)); - } else if (absl::StartsWith(token, "START_TIME")) { - formatters.emplace_back(FormatterProviderPtr{new StartTimeFormatter(token)}); - } else if (absl::StartsWith(token, "DOWNSTREAM_PEER_CERT_V_START")) { - formatters.emplace_back(FormatterProviderPtr{new DownstreamPeerCertVStartFormatter(token)}); - } else if (absl::StartsWith(token, "DOWNSTREAM_PEER_CERT_V_END")) { - formatters.emplace_back(FormatterProviderPtr{new DownstreamPeerCertVEndFormatter(token)}); - } else if (absl::StartsWith(token, "GRPC_STATUS")) { - formatters.emplace_back(FormatterProviderPtr{ - new GrpcStatusFormatter("grpc-status", "", absl::optional())}); - } else { + } + + if (!added) { formatters.emplace_back(FormatterProviderPtr{new StreamInfoFormatter(token)}); } - pos = command_end_position; - } else { - current_token += format[pos]; } + + pos = command_end_position; } if (!current_token.empty()) { @@ -924,6 +1030,41 @@ ResponseTrailerFormatter::formatValue(const Http::RequestHeaderMap&, const Http: return HeaderFormatter::formatValue(response_trailers); } +HeadersByteSizeFormatter::HeadersByteSizeFormatter(const HeaderType header_type) + : header_type_(header_type) {} + +uint64_t HeadersByteSizeFormatter::extractHeadersByteSize( + const Http::RequestHeaderMap& request_headers, const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap& response_trailers) const { + switch (header_type_) { + case HeaderType::RequestHeaders: + return request_headers.byteSize(); + case HeaderType::ResponseHeaders: + return response_headers.byteSize(); + case HeaderType::ResponseTrailers: + return response_trailers.byteSize(); + default: + NOT_REACHED_GCOVR_EXCL_LINE; + } +} + +absl::optional +HeadersByteSizeFormatter::format(const Http::RequestHeaderMap& request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap& response_trailers, + const StreamInfo::StreamInfo&, absl::string_view) const { + return absl::StrCat(extractHeadersByteSize(request_headers, response_headers, response_trailers)); +} + +ProtobufWkt::Value +HeadersByteSizeFormatter::formatValue(const Http::RequestHeaderMap& request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap& response_trailers, + const StreamInfo::StreamInfo&, absl::string_view) const { + return ValueUtil::numberValue( + extractHeadersByteSize(request_headers, response_headers, response_trailers)); +} + GrpcStatusFormatter::GrpcStatusFormatter(const std::string& main_header, const std::string& alternative_header, absl::optional max_length) @@ -1000,8 +1141,8 @@ MetadataFormatter::formatMetadataValue(const envoy::config::core::v3::Metadata& return val; } -// TODO(glicht): Consider adding support for route/listener/cluster metadata as suggested by @htuch. -// See: https://github.com/envoyproxy/envoy/issues/3006 +// TODO(glicht): Consider adding support for route/listener/cluster metadata as suggested by +// @htuch. See: https://github.com/envoyproxy/envoy/issues/3006 DynamicMetadataFormatter::DynamicMetadataFormatter(const std::string& filter_namespace, const std::vector& path, absl::optional max_length) diff --git a/source/common/formatter/substitution_formatter.h b/source/common/formatter/substitution_formatter.h index dc0d39689e9b..19d6b68f19fb 100644 --- a/source/common/formatter/substitution_formatter.h +++ b/source/common/formatter/substitution_formatter.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include @@ -23,8 +24,9 @@ namespace Formatter { class SubstitutionFormatParser { public: static std::vector parse(const std::string& format); + static std::vector + parse(const std::string& format, const std::vector& command_parsers); -private: /** * Parse a header format rule of the form: %REQ(X?Y):Z% . * Will populate a main_header and an optional alternative header if specified. @@ -60,6 +62,19 @@ class SubstitutionFormatParser { const std::string& separator, std::string& main, std::vector& sub_items, absl::optional& max_length); + /** + * Return a FormatterProviderPtr if a built-in command is parsed from the token. This method + * handles mapping the command name to an appropriate formatter after parsing. + * + * TODO(rgs1): this can be refactored into a dispatch table using the command name as the key and + * the parsing parameters as the value. + * + * @param token the token to parse + * @return FormattterProviderPtr substitution provider for the parsed command or nullptr + */ + static FormatterProviderPtr parseBuiltinCommand(const std::string& token); + +private: // the indexes of where the parameters for each directive is expected to begin static const size_t ReqParamStart{sizeof("REQ(") - 1}; static const size_t RespParamStart{sizeof("RESP(") - 1}; @@ -93,6 +108,8 @@ class SubstitutionFormatUtils { class FormatterImpl : public Formatter { public: FormatterImpl(const std::string& format, bool omit_empty_values = false); + FormatterImpl(const std::string& format, bool omit_empty_values, + const std::vector& command_parsers); // Formatter::format std::string format(const Http::RequestHeaderMap& request_headers, @@ -106,6 +123,10 @@ class FormatterImpl : public Formatter { std::vector providers_; }; +// Helper classes for StructFormatter::StructFormatMapVisitor. +template struct StructFormatMapVisitorHelper : Ts... { using Ts::operator()...; }; +template StructFormatMapVisitorHelper(Ts...) -> StructFormatMapVisitorHelper; + /** * An formatter for structured log formats, which returns a Struct proto that * can be converted easily into multiple formats. @@ -113,9 +134,7 @@ class FormatterImpl : public Formatter { class StructFormatter { public: StructFormatter(const ProtobufWkt::Struct& format_mapping, bool preserve_types, - bool omit_empty_values) - : omit_empty_values_(omit_empty_values), preserve_types_(preserve_types), - struct_output_format_(toFormatMap(format_mapping)) {} + bool omit_empty_values); ProtobufWkt::Struct format(const Http::RequestHeaderMap& request_headers, const Http::ResponseHeaderMap& response_headers, @@ -125,22 +144,55 @@ class StructFormatter { private: struct StructFormatMapWrapper; - using StructFormatMapValue = - absl::variant, const StructFormatMapWrapper>; + struct StructFormatListWrapper; + using StructFormatValue = + absl::variant, const StructFormatMapWrapper, + const StructFormatListWrapper>; // Although not required for Struct/JSON, it is nice to have the order of // properties preserved between the format and the log entry, thus std::map. - using StructFormatMap = std::map; + using StructFormatMap = std::map; using StructFormatMapPtr = std::unique_ptr; struct StructFormatMapWrapper { StructFormatMapPtr value_; }; - StructFormatMapWrapper toFormatMap(const ProtobufWkt::Struct& struct_format) const; + using StructFormatList = std::list; + using StructFormatListPtr = std::unique_ptr; + struct StructFormatListWrapper { + StructFormatListPtr value_; + }; + + using StructFormatMapVisitor = StructFormatMapVisitorHelper< + const std::function&)>, + const std::function, + const std::function>; + + // Methods for building the format map. + std::vector toFormatStringValue(const std::string& string_format) const; + StructFormatMapWrapper toFormatMapValue(const ProtobufWkt::Struct& struct_format) const; + StructFormatListWrapper toFormatListValue(const ProtobufWkt::ListValue& list_value_format) const; + + // Methods for doing the actual formatting. + ProtobufWkt::Value providersCallback(const std::vector& providers, + const Http::RequestHeaderMap& request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap& response_trailers, + const StreamInfo::StreamInfo& stream_info, + absl::string_view local_reply_body) const; + ProtobufWkt::Value + structFormatMapCallback(const StructFormatter::StructFormatMapWrapper& format_map, + const StructFormatMapVisitor& visitor) const; + ProtobufWkt::Value + structFormatListCallback(const StructFormatter::StructFormatListWrapper& format_list, + const StructFormatMapVisitor& visitor) const; const bool omit_empty_values_; const bool preserve_types_; + const std::string empty_value_; const StructFormatMapWrapper struct_output_format_; -}; +}; // namespace Formatter + +using StructFormatterPtr = std::unique_ptr; class JsonFormatterImpl : public Formatter { public: @@ -212,6 +264,33 @@ class HeaderFormatter { absl::optional max_length_; }; +/** + * FormatterProvider for headers byte size. + */ +class HeadersByteSizeFormatter : public FormatterProvider { +public: + // TODO(taoxuy): Add RequestTrailers here. + enum class HeaderType { RequestHeaders, ResponseHeaders, ResponseTrailers }; + + HeadersByteSizeFormatter(const HeaderType header_type); + + absl::optional format(const Http::RequestHeaderMap& request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap& response_trailers, + const StreamInfo::StreamInfo&, + absl::string_view) const override; + ProtobufWkt::Value formatValue(const Http::RequestHeaderMap& request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap& response_trailers, + const StreamInfo::StreamInfo&, absl::string_view) const override; + +private: + uint64_t extractHeadersByteSize(const Http::RequestHeaderMap& request_headers, + const Http::ResponseHeaderMap& response_headers, + const Http::ResponseTrailerMap& response_trailers) const; + HeaderType header_type_; +}; + /** * FormatterProvider for request headers. */ diff --git a/source/common/grpc/google_async_client_impl.cc b/source/common/grpc/google_async_client_impl.cc index 8f94df661e21..c3d1cdd89bc4 100644 --- a/source/common/grpc/google_async_client_impl.cc +++ b/source/common/grpc/google_async_client_impl.cc @@ -258,12 +258,22 @@ void GoogleAsyncStreamImpl::writeQueued() { } void GoogleAsyncStreamImpl::onCompletedOps() { - Thread::LockGuard lock(completed_ops_lock_); - while (!completed_ops_.empty()) { + // The items in completed_ops_ execute in the order they were originally added to the queue since + // both the post callback scheduled by the completionThread and the deferred deletion of the + // GoogleAsyncClientThreadLocal happen on the dispatcher thread. + std::deque> completed_ops; + { + Thread::LockGuard lock(completed_ops_lock_); + completed_ops = std::move(completed_ops_); + // completed_ops_ should be empty after the move. + ASSERT(completed_ops_.empty()); + } + + while (!completed_ops.empty()) { GoogleAsyncTag::Operation op; bool ok; - std::tie(op, ok) = completed_ops_.front(); - completed_ops_.pop_front(); + std::tie(op, ok) = completed_ops.front(); + completed_ops.pop_front(); handleOpCompletion(op, ok); } } diff --git a/source/common/http/BUILD b/source/common/http/BUILD index e19d26c058f4..a1917b42809c 100644 --- a/source/common/http/BUILD +++ b/source/common/http/BUILD @@ -184,6 +184,8 @@ envoy_cc_library( "//source/common/local_reply:local_reply_lib", "//source/common/matcher:matcher_lib", "@envoy_api//envoy/extensions/filters/common/matcher/action/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", + "@envoy_api//envoy/type/matcher/v3:pkg_cc_proto", ], ) @@ -247,6 +249,7 @@ envoy_cc_library( "//source/common/http/http2:codec_lib", "//source/common/http/http3:quic_codec_factory_lib", "//source/common/http/http3:well_known_names", + "//source/common/http/match_wrapper:config", "//source/common/network:utility_lib", "//source/common/router:config_lib", "//source/common/router:scoped_rds_lib", diff --git a/source/common/http/async_client_impl.h b/source/common/http/async_client_impl.h index 00fc7d09a5dc..9cee1e0399e7 100644 --- a/source/common/http/async_client_impl.h +++ b/source/common/http/async_client_impl.h @@ -31,6 +31,7 @@ #include "envoy/upstream/load_balancer.h" #include "envoy/upstream/upstream.h" +#include "common/common/assert.h" #include "common/common/empty_string.h" #include "common/common/linked_object.h" #include "common/http/message_impl.h" @@ -398,10 +399,13 @@ class AsyncStreamImpl : public AsyncClient::Stream, // The async client won't pause if sending an Expect: 100-Continue so simply // swallows any incoming encode100Continue. void encode100ContinueHeaders(ResponseHeaderMapPtr&&) override {} + ResponseHeaderMapOptRef continueHeaders() const override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } void encodeHeaders(ResponseHeaderMapPtr&& headers, bool end_stream, absl::string_view details) override; + ResponseHeaderMapOptRef responseHeaders() const override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } void encodeData(Buffer::Instance& data, bool end_stream) override; void encodeTrailers(ResponseTrailerMapPtr&& trailers) override; + ResponseTrailerMapOptRef responseTrailers() const override { NOT_IMPLEMENTED_GCOVR_EXCL_LINE; } void encodeMetadata(MetadataMapPtr&&) override {} void onDecoderFilterAboveWriteBufferHighWatermark() override { ++high_watermark_calls_; } void onDecoderFilterBelowWriteBufferLowWatermark() override { diff --git a/source/common/http/codec_client.cc b/source/common/http/codec_client.cc index 47a7fd33f8f1..fe9225a48059 100644 --- a/source/common/http/codec_client.cc +++ b/source/common/http/codec_client.cc @@ -49,7 +49,9 @@ CodecClient::CodecClient(Type type, Network::ClientConnectionPtr&& connection, // We just universally set no delay on connections. Theoretically we might at some point want // to make this configurable. - connection_->noDelay(true); + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) { + connection_->noDelay(true); + } } CodecClient::~CodecClient() = default; diff --git a/source/common/http/conn_manager_impl.cc b/source/common/http/conn_manager_impl.cc index d6f501985486..ea2cc3516622 100644 --- a/source/common/http/conn_manager_impl.cc +++ b/source/common/http/conn_manager_impl.cc @@ -10,6 +10,7 @@ #include "envoy/buffer/buffer.h" #include "envoy/common/time.h" #include "envoy/event/dispatcher.h" +#include "envoy/event/scaled_range_timer_manager.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" #include "envoy/http/header_map.h" #include "envoy/network/drain_decision.h" @@ -121,8 +122,8 @@ void ConnectionManagerImpl::initializeReadFilterCallbacks(Network::ReadFilterCal read_callbacks_->connection().addConnectionCallbacks(*this); if (config_.idleTimeout()) { - connection_idle_timer_ = overload_state_.createScaledTimer( - Server::OverloadTimerType::HttpDownstreamIdleConnectionTimeout, + connection_idle_timer_ = read_callbacks_->connection().dispatcher().createScaledTimer( + Event::ScaledTimerType::HttpDownstreamIdleConnectionTimeout, [this]() -> void { onIdleTimeout(); }); connection_idle_timer_->enableTimer(config_.idleTimeout().value()); } @@ -627,9 +628,10 @@ ConnectionManagerImpl::ActiveStream::ActiveStream(ConnectionManagerImpl& connect if (connection_manager_.config_.streamIdleTimeout().count()) { idle_timeout_ms_ = connection_manager_.config_.streamIdleTimeout(); - stream_idle_timer_ = connection_manager_.overload_state_.createScaledTimer( - Server::OverloadTimerType::HttpDownstreamIdleStreamTimeout, - [this]() -> void { onIdleTimeout(); }); + stream_idle_timer_ = + connection_manager_.read_callbacks_->connection().dispatcher().createScaledTimer( + Event::ScaledTimerType::HttpDownstreamIdleStreamTimeout, + [this]() -> void { onIdleTimeout(); }); resetIdleTimer(); } @@ -858,12 +860,7 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&& he // Both saw_connection_close_ and is_head_request_ affect local replies: set // them as early as possible. const Protocol protocol = connection_manager_.codec_->protocol(); - const bool fixed_connection_close = - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.fixed_connection_close"); - if (fixed_connection_close) { - state_.saw_connection_close_ = - HeaderUtility::shouldCloseConnection(protocol, *request_headers_); - } + state_.saw_connection_close_ = HeaderUtility::shouldCloseConnection(protocol, *request_headers_); if (HeaderUtility::isConnect(*request_headers_) && !request_headers_->Path() && !Runtime::runtimeFeatureEnabled("envoy.reloadable_features.stop_faking_paths")) { request_headers_->setPath("/"); @@ -930,14 +927,6 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&& he sendLocalReply(false, Code::UpgradeRequired, "", nullptr, absl::nullopt, StreamInfo::ResponseCodeDetails::get().LowVersion); return; - } else if (!fixed_connection_close) { - // HTTP/1.0 defaults to single-use connections. Make sure the connection - // will be closed unless Keep-Alive is present. - state_.saw_connection_close_ = true; - if (absl::EqualsIgnoreCase(request_headers_->getConnectionValue(), - Http::Headers::get().ConnectionValues.KeepAlive)) { - state_.saw_connection_close_ = false; - } } if (!request_headers_->Host() && !connection_manager_.config_.http1Settings().default_host_for_http_10_.empty()) { @@ -988,20 +977,6 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&& he ConnectionManagerUtility::maybeNormalizeHost(*request_headers_, connection_manager_.config_, localPort()); - if (!fixed_connection_close && protocol == Protocol::Http11 && - absl::EqualsIgnoreCase(request_headers_->getConnectionValue(), - Http::Headers::get().ConnectionValues.Close)) { - state_.saw_connection_close_ = true; - } - // Note: Proxy-Connection is not a standard header, but is supported here - // since it is supported by http-parser the underlying parser for http - // requests. - if (!fixed_connection_close && protocol < Protocol::Http2 && !state_.saw_connection_close_ && - absl::EqualsIgnoreCase(request_headers_->getProxyConnectionValue(), - Http::Headers::get().ConnectionValues.Close)) { - state_.saw_connection_close_ = true; - } - if (!state_.is_internally_created_) { // Only sanitize headers on first pass. // Modify the downstream remote address depending on configuration and headers. filter_manager_.setDownstreamRemoteAddress(ConnectionManagerUtility::mutateRequestHeaders( @@ -1051,9 +1026,10 @@ void ConnectionManagerImpl::ActiveStream::decodeHeaders(RequestHeaderMapPtr&& he if (idle_timeout_ms_.count()) { // If we have a route-level idle timeout but no global stream idle timeout, create a timer. if (stream_idle_timer_ == nullptr) { - stream_idle_timer_ = connection_manager_.overload_state_.createScaledTimer( - Server::OverloadTimerType::HttpDownstreamIdleStreamTimeout, - [this]() -> void { onIdleTimeout(); }); + stream_idle_timer_ = + connection_manager_.read_callbacks_->connection().dispatcher().createScaledTimer( + Event::ScaledTimerType::HttpDownstreamIdleStreamTimeout, + [this]() -> void { onIdleTimeout(); }); } } else if (stream_idle_timer_ != nullptr) { // If we had a global stream idle timeout but the route-level idle timeout is set to zero diff --git a/source/common/http/conn_manager_utility.cc b/source/common/http/conn_manager_utility.cc index 265079582e18..db2c8a7d3353 100644 --- a/source/common/http/conn_manager_utility.cc +++ b/source/common/http/conn_manager_utility.cc @@ -390,9 +390,7 @@ void ConnectionManagerUtility::mutateResponseHeaders(ResponseHeaderMap& response } } else { response_headers.removeConnection(); - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.fix_upgrade_response")) { - response_headers.removeUpgrade(); - } + response_headers.removeUpgrade(); } response_headers.removeTransferEncoding(); diff --git a/source/common/http/filter_manager.cc b/source/common/http/filter_manager.cc index fe579cebefdd..6caf33f7536f 100644 --- a/source/common/http/filter_manager.cc +++ b/source/common/http/filter_manager.cc @@ -15,6 +15,8 @@ namespace Envoy { namespace Http { namespace { +REGISTER_FACTORY(HttpRequestHeadersDataInputFactory, Matcher::DataInputFactory); +REGISTER_FACTORY(SkipActionFactory, Matcher::ActionFactory); template using FilterList = std::list>; @@ -242,7 +244,7 @@ void ActiveStreamFilterBase::clearRouteCache() { parent_.filter_manager_callbacks_.clearRouteCache(); } -void ActiveStreamFilterBase::evaluateMatchTreeWithNewData( +void FilterMatchState::evaluateMatchTreeWithNewData( std::function update_func) { if (match_tree_evaluated_ || !matching_data_) { return; @@ -256,7 +258,9 @@ void ActiveStreamFilterBase::evaluateMatchTreeWithNewData( if (match_tree_evaluated_ && match_result.result_) { if (SkipAction().typeUrl() == match_result.result_->typeUrl()) { - skip_ = true; + skip_filter_ = true; + } else { + filter_->onMatchCallback(*match_result.result_); } } } @@ -368,6 +372,10 @@ void ActiveStreamDecoderFilter::encode100ContinueHeaders(ResponseHeaderMapPtr&& } } +ResponseHeaderMapOptRef ActiveStreamDecoderFilter::continueHeaders() const { + return parent_.filter_manager_callbacks_.continueHeaders(); +} + void ActiveStreamDecoderFilter::encodeHeaders(ResponseHeaderMapPtr&& headers, bool end_stream, absl::string_view details) { parent_.stream_info_.setResponseCodeDetails(details); @@ -375,6 +383,10 @@ void ActiveStreamDecoderFilter::encodeHeaders(ResponseHeaderMapPtr&& headers, bo parent_.encodeHeaders(nullptr, *parent_.filter_manager_callbacks_.responseHeaders(), end_stream); } +ResponseHeaderMapOptRef ActiveStreamDecoderFilter::responseHeaders() const { + return parent_.filter_manager_callbacks_.responseHeaders(); +} + void ActiveStreamDecoderFilter::encodeData(Buffer::Instance& data, bool end_stream) { parent_.encodeData(nullptr, data, end_stream, FilterManager::FilterIterationStartState::CanStartFromCurrent); @@ -385,6 +397,10 @@ void ActiveStreamDecoderFilter::encodeTrailers(ResponseTrailerMapPtr&& trailers) parent_.encodeTrailers(nullptr, *parent_.filter_manager_callbacks_.responseTrailers()); } +ResponseTrailerMapOptRef ActiveStreamDecoderFilter::responseTrailers() const { + return parent_.filter_manager_callbacks_.responseTrailers(); +} + void ActiveStreamDecoderFilter::encodeMetadata(MetadataMapPtr&& metadata_map_ptr) { parent_.encodeMetadata(nullptr, std::move(metadata_map_ptr)); } @@ -404,11 +420,18 @@ void ActiveStreamDecoderFilter::requestDataTooLarge() { } } -void FilterManager::addStreamDecoderFilterWorker( - StreamDecoderFilterSharedPtr filter, Matcher::MatchTreeSharedPtr matcher, - HttpMatchingDataImplSharedPtr matching_data, bool dual_filter) { - ActiveStreamDecoderFilterPtr wrapper(new ActiveStreamDecoderFilter( - *this, filter, std::move(matcher), std::move(matching_data), dual_filter)); +void FilterManager::addStreamDecoderFilterWorker(StreamDecoderFilterSharedPtr filter, + FilterMatchStateSharedPtr match_state, + bool dual_filter) { + ActiveStreamDecoderFilterPtr wrapper( + new ActiveStreamDecoderFilter(*this, filter, match_state, dual_filter)); + + // If we're a dual handling filter, have the encoding wrapper be the only thing registering itself + // as the handling filter. + if (match_state) { + match_state->filter_ = filter.get(); + } + filter->setDecoderFilterCallbacks(*wrapper); // Note: configured decoder filters are appended to decoder_filters_. // This means that if filters are configured in the following order (assume all three filters are @@ -421,11 +444,16 @@ void FilterManager::addStreamDecoderFilterWorker( LinkedList::moveIntoListBack(std::move(wrapper), decoder_filters_); } -void FilterManager::addStreamEncoderFilterWorker( - StreamEncoderFilterSharedPtr filter, Matcher::MatchTreeSharedPtr match_tree, - HttpMatchingDataImplSharedPtr matching_data, bool dual_filter) { - ActiveStreamEncoderFilterPtr wrapper(new ActiveStreamEncoderFilter( - *this, filter, std::move(match_tree), std::move(matching_data), dual_filter)); +void FilterManager::addStreamEncoderFilterWorker(StreamEncoderFilterSharedPtr filter, + FilterMatchStateSharedPtr match_state, + bool dual_filter) { + ActiveStreamEncoderFilterPtr wrapper( + new ActiveStreamEncoderFilter(*this, filter, match_state, dual_filter)); + + if (match_state) { + match_state->filter_ = filter.get(); + } + filter->setEncoderFilterCallbacks(*wrapper); // Note: configured encoder filters are prepended to encoder_filters_. // This means that if filters are configured in the following order (assume all three filters are @@ -463,9 +491,12 @@ void FilterManager::decodeHeaders(ActiveStreamDecoderFilter* filter, RequestHead std::list::iterator continue_data_entry = decoder_filters_.end(); for (; entry != decoder_filters_.end(); entry++) { - (*entry)->evaluateMatchTreeWithNewData( - [&](auto& matching_data) { matching_data.onRequestHeaders(headers); }); - if ((*entry)->skip_) { + if ((*entry)->filter_match_state_) { + (*entry)->filter_match_state_->evaluateMatchTreeWithNewData( + [&](auto& matching_data) { matching_data.onRequestHeaders(headers); }); + } + + if ((*entry)->skipFilter()) { continue; } @@ -549,7 +580,7 @@ void FilterManager::decodeData(ActiveStreamDecoderFilter* filter, Buffer::Instan commonDecodePrefix(filter, filter_iteration_start_state); for (; entry != decoder_filters_.end(); entry++) { - if ((*entry)->skip_) { + if ((*entry)->skipFilter()) { continue; } // If the filter pointed by entry has stopped for all frame types, return now. @@ -683,7 +714,7 @@ void FilterManager::decodeTrailers(ActiveStreamDecoderFilter* filter, RequestTra commonDecodePrefix(filter, FilterIterationStartState::CanStartFromCurrent); for (; entry != decoder_filters_.end(); entry++) { - if ((*entry)->skip_) { + if ((*entry)->skipFilter()) { continue; } @@ -715,7 +746,7 @@ void FilterManager::decodeMetadata(ActiveStreamDecoderFilter* filter, MetadataMa commonDecodePrefix(filter, FilterIterationStartState::CanStartFromCurrent); for (; entry != decoder_filters_.end(); entry++) { - if ((*entry)->skip_) { + if ((*entry)->skipFilter()) { continue; } // If the filter pointed by entry has stopped for all frame type, stores metadata and returns. @@ -925,7 +956,7 @@ void FilterManager::encode100ContinueHeaders(ActiveStreamEncoderFilter* filter, std::list::iterator entry = commonEncodePrefix(filter, false, FilterIterationStartState::AlwaysStartFromNext); for (; entry != encoder_filters_.end(); entry++) { - if ((*entry)->skip_) { + if ((*entry)->skipFilter()) { continue; } @@ -970,9 +1001,11 @@ void FilterManager::encodeHeaders(ActiveStreamEncoderFilter* filter, ResponseHea std::list::iterator continue_data_entry = encoder_filters_.end(); for (; entry != encoder_filters_.end(); entry++) { - (*entry)->evaluateMatchTreeWithNewData( - [&headers](auto& matching_data) { matching_data.onResponseHeaders(headers); }); - if ((*entry)->skip_) { + if ((*entry)->filter_match_state_) { + (*entry)->filter_match_state_->evaluateMatchTreeWithNewData( + [&headers](auto& matching_data) { matching_data.onResponseHeaders(headers); }); + } + if ((*entry)->skipFilter()) { continue; } ASSERT(!(state_.filter_call_state_ & FilterCallState::EncodeHeaders)); @@ -1029,7 +1062,7 @@ void FilterManager::encodeMetadata(ActiveStreamEncoderFilter* filter, commonEncodePrefix(filter, false, FilterIterationStartState::CanStartFromCurrent); for (; entry != encoder_filters_.end(); entry++) { - if ((*entry)->skip_) { + if ((*entry)->skipFilter()) { continue; } // If the filter pointed by entry has stopped for all frame type, stores metadata and returns. @@ -1100,7 +1133,7 @@ void FilterManager::encodeData(ActiveStreamEncoderFilter* filter, Buffer::Instan const bool trailers_exists_at_start = filter_manager_callbacks_.responseTrailers().has_value(); for (; entry != encoder_filters_.end(); entry++) { - if ((*entry)->skip_) { + if ((*entry)->skipFilter()) { continue; } // If the filter pointed by entry has stopped for all frame type, return now. diff --git a/source/common/http/filter_manager.h b/source/common/http/filter_manager.h index 3047772768b4..80845e0f9375 100644 --- a/source/common/http/filter_manager.h +++ b/source/common/http/filter_manager.h @@ -4,10 +4,14 @@ #include "envoy/common/optref.h" #include "envoy/extensions/filters/common/matcher/action/v3/skip_action.pb.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" +#include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.validate.h" #include "envoy/http/filter.h" #include "envoy/http/header_map.h" #include "envoy/matcher/matcher.h" #include "envoy/network/socket.h" +#include "envoy/protobuf/message_validator.h" +#include "envoy/type/matcher/v3/http_inputs.pb.validate.h" #include "common/buffer/watermark_buffer.h" #include "common/common/dump_state_utils.h" @@ -18,6 +22,7 @@ #include "common/http/headers.h" #include "common/local_reply/local_reply.h" #include "common/matcher/matcher.h" +#include "common/protobuf/utility.h" namespace Envoy { namespace Http { @@ -97,6 +102,36 @@ class HttpRequestHeadersDataInput : public HttpHeadersDataInputBase +class HttpHeadersDataInputFactoryBase : public Matcher::DataInputFactory { +public: + explicit HttpHeadersDataInputFactoryBase(const std::string& name) : name_(name) {} + + std::string name() const override { return name_; } + + Matcher::DataInputPtr + createDataInput(const Protobuf::Message& config, + ProtobufMessage::ValidationVisitor& validation_visitor) override { + const auto& typed_config = + MessageUtil::downcastAndValidate(config, validation_visitor); + + return std::make_unique(typed_config.header_name()); + }; + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + +private: + const std::string name_; +}; + +class HttpRequestHeadersDataInputFactory + : public HttpHeadersDataInputFactoryBase< + HttpRequestHeadersDataInput, envoy::type::matcher::v3::HttpRequestHeaderMatchInput> { +public: + HttpRequestHeadersDataInputFactory() : HttpHeadersDataInputFactoryBase("request-headers") {} +}; + class HttpResponseHeadersDataInput : public HttpHeadersDataInputBase { public: explicit HttpResponseHeadersDataInput(const std::string& name) : HttpHeadersDataInputBase(name) {} @@ -107,9 +142,58 @@ class HttpResponseHeadersDataInput : public HttpHeadersDataInputBase { +public: + HttpResponseHeadersDataInputFactory() : HttpHeadersDataInputFactoryBase("response-headers") {} +}; + class SkipAction : public Matcher::ActionBase< envoy::extensions::filters::common::matcher::action::v3::SkipFilter> {}; +struct ActiveStreamFilterBase; + +/** + * Manages the shared match state between one or two filters. + * The need for this class comes from the fact that a single instantiated filter can be wrapped by + * two different ActiveStreamFilters, one for encoding and one for decoding. Certain match actions + * should be made visible to both wrappers (e.g. the skip action), while other actions should be + * sent to the underlying filter exactly once. + */ +class FilterMatchState { +public: + FilterMatchState(Matcher::MatchTreeSharedPtr match_tree, + HttpMatchingDataImplSharedPtr matching_data) + : match_tree_(std::move(match_tree)), matching_data_(std::move(matching_data)), + match_tree_evaluated_(false), skip_filter_(false) {} + + void evaluateMatchTreeWithNewData(std::function update_func); + + StreamFilterBase* filter_{}; + + bool skipFilter() const { return skip_filter_; } + +private: + Matcher::MatchTreeSharedPtr match_tree_; + HttpMatchingDataImplSharedPtr matching_data_; + bool match_tree_evaluated_ : 1; + bool skip_filter_ : 1; +}; + +using FilterMatchStateSharedPtr = std::shared_ptr; + +class SkipActionFactory : public Matcher::ActionFactory { +public: + std::string name() const override { return "skip"; } + Matcher::ActionFactoryCb createActionFactoryCb(const Protobuf::Message&) override { + return []() { return std::make_unique(); }; + } + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } +}; + /** * Base class wrapper for both stream encoder and decoder filters. * @@ -120,14 +204,11 @@ class SkipAction : public Matcher::ActionBase< struct ActiveStreamFilterBase : public virtual StreamFilterCallbacks, Logger::Loggable { ActiveStreamFilterBase(FilterManager& parent, bool dual_filter, - Matcher::MatchTreeSharedPtr match_tree, - HttpMatchingDataImplSharedPtr matching_data) + FilterMatchStateSharedPtr match_state) : parent_(parent), iteration_state_(IterationState::Continue), - match_tree_(std::move(match_tree)), matching_data_(std::move(matching_data)), - iterate_from_current_filter_(false), headers_continued_(false), - continue_headers_continued_(false), end_stream_(false), dual_filter_(dual_filter), - decode_headers_called_(false), encode_headers_called_(false), match_tree_evaluated_(false), - skip_(false) {} + filter_match_state_(std::move(match_state)), iterate_from_current_filter_(false), + headers_continued_(false), continue_headers_continued_(false), end_stream_(false), + dual_filter_(dual_filter), decode_headers_called_(false), encode_headers_called_(false) {} // Functions in the following block are called after the filter finishes processing // corresponding data. Those functions handle state updates and data storage (if needed) @@ -160,6 +241,8 @@ struct ActiveStreamFilterBase : public virtual StreamFilterCallbacks, // TODO(soya3129): make this pure when adding impl to encoder filter. virtual void handleMetadataAfterHeadersCallback() PURE; + virtual void onMatchCallback(const Matcher::Action& action) PURE; + // Http::StreamFilterCallbacks const Network::Connection* connection() override; Event::Dispatcher& dispatcher() override; @@ -196,8 +279,7 @@ struct ActiveStreamFilterBase : public virtual StreamFilterCallbacks, } return saved_response_metadata_.get(); } - - void evaluateMatchTreeWithNewData(std::function update_func); + bool skipFilter() const { return filter_match_state_ && filter_match_state_->skipFilter(); } // A vector to save metadata when the current filter's [de|en]codeMetadata() can not be called, // either because [de|en]codeHeaders() of the current filter returns StopAllIteration or because @@ -217,9 +299,7 @@ struct ActiveStreamFilterBase : public virtual StreamFilterCallbacks, FilterManager& parent_; IterationState iteration_state_; - Matcher::MatchTreeSharedPtr match_tree_; - HttpMatchingDataImplSharedPtr matching_data_; - + FilterMatchStateSharedPtr filter_match_state_; // If the filter resumes iteration from a StopAllBuffer/Watermark state, the current filter // hasn't parsed data and trailers. As a result, the filter iteration should start with the // current filter instead of the next one. If true, filter iteration starts with the current @@ -232,8 +312,8 @@ struct ActiveStreamFilterBase : public virtual StreamFilterCallbacks, const bool dual_filter_ : 1; bool decode_headers_called_ : 1; bool encode_headers_called_ : 1; - bool match_tree_evaluated_ : 1; - bool skip_ : 1; + + friend FilterMatchState; }; /** @@ -243,11 +323,8 @@ struct ActiveStreamDecoderFilter : public ActiveStreamFilterBase, public StreamDecoderFilterCallbacks, LinkedObject { ActiveStreamDecoderFilter(FilterManager& parent, StreamDecoderFilterSharedPtr filter, - Matcher::MatchTreeSharedPtr match_tree, - HttpMatchingDataImplSharedPtr matching_data, bool dual_filter) - : ActiveStreamFilterBase(parent, dual_filter, std::move(match_tree), - std::move(matching_data)), - handle_(filter) {} + FilterMatchStateSharedPtr match_state, bool dual_filter) + : ActiveStreamFilterBase(parent, dual_filter, std::move(match_state)), handle_(filter) {} // ActiveStreamFilterBase bool canContinue() override; @@ -269,6 +346,9 @@ struct ActiveStreamDecoderFilter : public ActiveStreamFilterBase, void drainSavedRequestMetadata(); // This function is called after the filter calls decodeHeaders() to drain accumulated metadata. void handleMetadataAfterHeadersCallback() override; + void onMatchCallback(const Matcher::Action& action) override { + handle_->onMatchCallback(std::move(action)); + } // Http::StreamDecoderFilterCallbacks void addDecodedData(Buffer::Instance& data, bool streaming) override; @@ -285,10 +365,13 @@ struct ActiveStreamDecoderFilter : public ActiveStreamFilterBase, const absl::optional grpc_status, absl::string_view details) override; void encode100ContinueHeaders(ResponseHeaderMapPtr&& headers) override; + ResponseHeaderMapOptRef continueHeaders() const override; void encodeHeaders(ResponseHeaderMapPtr&& headers, bool end_stream, absl::string_view details) override; + ResponseHeaderMapOptRef responseHeaders() const override; void encodeData(Buffer::Instance& data, bool end_stream) override; void encodeTrailers(ResponseTrailerMapPtr&& trailers) override; + ResponseTrailerMapOptRef responseTrailers() const override; void encodeMetadata(MetadataMapPtr&& metadata_map_ptr) override; void onDecoderFilterAboveWriteBufferHighWatermark() override; void onDecoderFilterBelowWriteBufferLowWatermark() override; @@ -332,11 +415,8 @@ struct ActiveStreamEncoderFilter : public ActiveStreamFilterBase, public StreamEncoderFilterCallbacks, LinkedObject { ActiveStreamEncoderFilter(FilterManager& parent, StreamEncoderFilterSharedPtr filter, - Matcher::MatchTreeSharedPtr match_tree, - HttpMatchingDataImplSharedPtr matching_data, bool dual_filter) - : ActiveStreamFilterBase(parent, dual_filter, std::move(match_tree), - std::move(matching_data)), - handle_(filter) {} + FilterMatchStateSharedPtr match_state, bool dual_filter) + : ActiveStreamFilterBase(parent, dual_filter, std::move(match_state)), handle_(filter) {} // ActiveStreamFilterBase bool canContinue() override { return true; } @@ -349,6 +429,7 @@ struct ActiveStreamEncoderFilter : public ActiveStreamFilterBase, void doData(bool end_stream) override; void drainSavedResponseMetadata(); void handleMetadataAfterHeadersCallback() override; + void onMatchCallback(const Matcher::Action& action) override { handle_->onMatchCallback(action); } void doMetadata() override { if (saved_response_metadata_ != nullptr) { @@ -608,7 +689,10 @@ class OverridableRemoteSocketAddressSetterStreamInfo : public StreamInfo::Stream void setDownstreamRemoteAddress( const Network::Address::InstanceConstSharedPtr& downstream_remote_address) { - ASSERT(overridden_downstream_remote_address_ == nullptr); + // TODO(rgs1): we should assert overridden_downstream_remote_address_ is nullptr, + // but we are currently relaxing this as a workaround to: + // + // https://github.com/envoyproxy/envoy/pull/14432#issuecomment-758167614 overridden_downstream_remote_address_ = downstream_remote_address; } @@ -674,34 +758,40 @@ class FilterManager : public ScopeTrackedObject, // Http::FilterChainFactoryCallbacks void addStreamDecoderFilter(StreamDecoderFilterSharedPtr filter) override { - addStreamDecoderFilterWorker(filter, nullptr, nullptr, false); + addStreamDecoderFilterWorker(filter, nullptr, false); } void addStreamDecoderFilter(StreamDecoderFilterSharedPtr filter, Matcher::MatchTreeSharedPtr match_tree) override { if (match_tree) { - auto matching_data = std::make_shared(); - addStreamDecoderFilterWorker(filter, std::move(match_tree), std::move(matching_data), false); + addStreamDecoderFilterWorker( + filter, + std::make_shared(std::move(match_tree), + std::make_shared()), + false); return; } - addStreamDecoderFilterWorker(filter, nullptr, nullptr, false); + addStreamDecoderFilterWorker(filter, nullptr, false); } void addStreamEncoderFilter(StreamEncoderFilterSharedPtr filter) override { - addStreamEncoderFilterWorker(filter, nullptr, nullptr, false); + addStreamEncoderFilterWorker(filter, nullptr, false); } void addStreamEncoderFilter(StreamEncoderFilterSharedPtr filter, Matcher::MatchTreeSharedPtr match_tree) override { if (match_tree) { - addStreamEncoderFilterWorker(filter, std::move(match_tree), - std::make_shared(), false); + addStreamEncoderFilterWorker( + filter, + std::make_shared(std::move(match_tree), + std::make_shared()), + false); return; } - addStreamEncoderFilterWorker(filter, nullptr, nullptr, false); + addStreamEncoderFilterWorker(filter, nullptr, false); } void addStreamFilter(StreamFilterSharedPtr filter) override { - addStreamDecoderFilterWorker(filter, nullptr, nullptr, true); - addStreamEncoderFilterWorker(filter, nullptr, nullptr, true); + addStreamDecoderFilterWorker(filter, nullptr, true); + addStreamEncoderFilterWorker(filter, nullptr, true); } void addStreamFilter(StreamFilterSharedPtr filter, Matcher::MatchTreeSharedPtr match_tree) override { @@ -710,14 +800,15 @@ class FilterManager : public ScopeTrackedObject, // TODO(snowp): The match tree might be fully evaluated twice, ideally we should expose // the result to both filters after the first match evaluation. if (match_tree) { - auto matching_data = std::make_shared(); - addStreamDecoderFilterWorker(filter, match_tree, matching_data, true); - addStreamEncoderFilterWorker(filter, std::move(match_tree), std::move(matching_data), true); + auto matching_state = std::make_shared( + std::move(match_tree), std::make_shared()); + addStreamDecoderFilterWorker(filter, matching_state, true); + addStreamEncoderFilterWorker(filter, std::move(matching_state), true); return; } - addStreamDecoderFilterWorker(filter, nullptr, nullptr, true); - addStreamEncoderFilterWorker(filter, nullptr, nullptr, true); + addStreamDecoderFilterWorker(filter, nullptr, true); + addStreamEncoderFilterWorker(filter, nullptr, true); } void addAccessLogHandler(AccessLog::InstanceSharedPtr handler) override; @@ -800,11 +891,9 @@ class FilterManager : public ScopeTrackedObject, // TODO(snowp): Make private as filter chain construction is moved into FM. void addStreamDecoderFilterWorker(StreamDecoderFilterSharedPtr filter, - Matcher::MatchTreeSharedPtr match_tree, - HttpMatchingDataImplSharedPtr matching_data, bool dual_filter); + FilterMatchStateSharedPtr match_state, bool dual_filter); void addStreamEncoderFilterWorker(StreamEncoderFilterSharedPtr filter, - Matcher::MatchTreeSharedPtr match_tree, - HttpMatchingDataImplSharedPtr matching_data, bool dual_filter); + FilterMatchStateSharedPtr match_state, bool dual_filter); void disarmRequestTimeout(); diff --git a/source/common/http/header_utility.cc b/source/common/http/header_utility.cc index 41c92645963d..24b817f170f4 100644 --- a/source/common/http/header_utility.cc +++ b/source/common/http/header_utility.cc @@ -309,5 +309,16 @@ Http::Status HeaderUtility::checkRequiredHeaders(const Http::RequestHeaderMap& h return Http::okStatus(); } +bool HeaderUtility::isRemovableHeader(absl::string_view header) { + return (header.empty() || header[0] != ':') && + !absl::EqualsIgnoreCase(header, Headers::get().HostLegacy.get()); +} + +bool HeaderUtility::isModifiableHeader(absl::string_view header) { + return (header.empty() || header[0] != ':') && + (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.treat_host_like_authority") || + !absl::EqualsIgnoreCase(header, Headers::get().HostLegacy.get())); +} + } // namespace Http } // namespace Envoy diff --git a/source/common/http/header_utility.h b/source/common/http/header_utility.h index 291200134552..bcd60eaaecc4 100644 --- a/source/common/http/header_utility.h +++ b/source/common/http/header_utility.h @@ -186,6 +186,20 @@ class HeaderUtility { * missing. */ static Http::Status checkRequiredHeaders(const Http::RequestHeaderMap& headers); + + /** + * Returns true if a header may be safely removed without causing additional + * problems. Effectively, header names beginning with ":" and the "host" header + * may not be removed. + */ + static bool isRemovableHeader(absl::string_view header); + + /** + * Returns true if a header may be safely modified without causing additional + * problems. Currently header names beginning with ":" and the "host" header + * may not be modified. + */ + static bool isModifiableHeader(absl::string_view header); }; } // namespace Http } // namespace Envoy diff --git a/source/common/http/http1/codec_impl.cc b/source/common/http/http1/codec_impl.cc index f0ce0cfb5f88..ea9ad820d43c 100644 --- a/source/common/http/http1/codec_impl.cc +++ b/source/common/http/http1/codec_impl.cc @@ -1096,10 +1096,6 @@ void ServerConnectionImpl::sendProtocolErrorOld(absl::string_view details) { } Status ServerConnectionImpl::sendProtocolError(absl::string_view details) { - if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.early_errors_via_hcm")) { - sendProtocolErrorOld(details); - return okStatus(); - } // We do this here because we may get a protocol error before we have a logical stream. if (!active_request_.has_value()) { RETURN_IF_ERROR(onMessageBeginBase()); @@ -1212,15 +1208,6 @@ Envoy::StatusOr ClientConnectionImpl::onHeadersComplete() { pending_response_.value().encoder_.connectRequest()) { ENVOY_CONN_LOG(trace, "codec entering upgrade mode for CONNECT response.", connection_); handling_upgrade_ = true; - - // For responses to connect requests, do not accept the chunked - // encoding header: https://tools.ietf.org/html/rfc7231#section-4.3.6 - if (headers->TransferEncoding() && - absl::EqualsIgnoreCase(headers->TransferEncoding()->value().getStringView(), - Headers::get().TransferEncodingValues.Chunked)) { - RETURN_IF_ERROR(sendProtocolError(Http1ResponseCodeDetails::get().InvalidTransferEncoding)); - return codecProtocolError("http/1.1 protocol error: unsupported transfer encoding"); - } } if (strict_1xx_and_204_headers_ && (parser_.status_code < 200 || parser_.status_code == 204)) { diff --git a/source/common/http/http1/conn_pool.cc b/source/common/http/http1/conn_pool.cc index 0a070c28395d..8a59c0100557 100644 --- a/source/common/http/http1/conn_pool.cc +++ b/source/common/http/http1/conn_pool.cc @@ -39,26 +39,10 @@ ActiveClient::StreamWrapper::~StreamWrapper() { void ActiveClient::StreamWrapper::onEncodeComplete() { encode_complete_ = true; } void ActiveClient::StreamWrapper::decodeHeaders(ResponseHeaderMapPtr&& headers, bool end_stream) { - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.fixed_connection_close")) { - close_connection_ = - HeaderUtility::shouldCloseConnection(parent_.codec_client_->protocol(), *headers); - if (close_connection_) { - parent_.parent().host()->cluster().stats().upstream_cx_close_notify_.inc(); - } - } else { - // If Connection: close OR - // Http/1.0 and not Connection: keep-alive OR - // Proxy-Connection: close - if ((absl::EqualsIgnoreCase(headers->getConnectionValue(), - Headers::get().ConnectionValues.Close)) || - (parent_.codec_client_->protocol() == Protocol::Http10 && - !absl::EqualsIgnoreCase(headers->getConnectionValue(), - Headers::get().ConnectionValues.KeepAlive)) || - (absl::EqualsIgnoreCase(headers->getProxyConnectionValue(), - Headers::get().ConnectionValues.Close))) { - parent_.parent().host()->cluster().stats().upstream_cx_close_notify_.inc(); - close_connection_ = true; - } + close_connection_ = + HeaderUtility::shouldCloseConnection(parent_.codec_client_->protocol(), *headers); + if (close_connection_) { + parent_.parent().host()->cluster().stats().upstream_cx_close_notify_.inc(); } ResponseDecoderWrapper::decodeHeaders(std::move(headers), end_stream); } diff --git a/source/common/http/http2/codec_impl.cc b/source/common/http/http2/codec_impl.cc index 74a565b27916..f53aff523b69 100644 --- a/source/common/http/http2/codec_impl.cc +++ b/source/common/http/http2/codec_impl.cc @@ -716,6 +716,15 @@ void ConnectionImpl::shutdownNotice() { } } +Status ConnectionImpl::protocolErrorForTest() { + int rc = nghttp2_submit_goaway(session_, NGHTTP2_FLAG_NONE, + nghttp2_session_get_last_proc_stream_id(session_), + NGHTTP2_PROTOCOL_ERROR, nullptr, 0); + ASSERT(rc == 0); + + return sendPendingFrames(); +} + Status ConnectionImpl::onBeforeFrameReceived(const nghttp2_frame_hd* hd) { ENVOY_CONN_LOG(trace, "about to recv frame type={}, flags={}", connection_, static_cast(hd->type), static_cast(hd->flags)); diff --git a/source/common/http/http2/codec_impl.h b/source/common/http/http2/codec_impl.h index 023c32c77e13..3e51d57eba5a 100644 --- a/source/common/http/http2/codec_impl.h +++ b/source/common/http/http2/codec_impl.h @@ -105,6 +105,7 @@ class ConnectionImpl : public virtual Connection, protected Logger::Loggable match_tree) + : delegated_callbacks_(delegated_callbacks), match_tree_(std::move(match_tree)) {} + + void addStreamDecoderFilter(Envoy::Http::StreamDecoderFilterSharedPtr filter) override { + delegated_callbacks_.addStreamDecoderFilter(std::move(filter), match_tree_); + } + void addStreamDecoderFilter( + Envoy::Http::StreamDecoderFilterSharedPtr filter, + Matcher::MatchTreeSharedPtr match_tree) override { + delegated_callbacks_.addStreamDecoderFilter(std::move(filter), std::move(match_tree)); + } + void addStreamEncoderFilter(Envoy::Http::StreamEncoderFilterSharedPtr filter) override { + delegated_callbacks_.addStreamEncoderFilter(std::move(filter), match_tree_); + } + void addStreamEncoderFilter( + Envoy::Http::StreamEncoderFilterSharedPtr filter, + Matcher::MatchTreeSharedPtr match_tree) override { + delegated_callbacks_.addStreamEncoderFilter(std::move(filter), std::move(match_tree)); + } + void addStreamFilter(Envoy::Http::StreamFilterSharedPtr filter) override { + delegated_callbacks_.addStreamFilter(std::move(filter), match_tree_); + } + void + addStreamFilter(Envoy::Http::StreamFilterSharedPtr filter, + Matcher::MatchTreeSharedPtr match_tree) override { + delegated_callbacks_.addStreamFilter(std::move(filter), std::move(match_tree)); + } + void addAccessLogHandler(AccessLog::InstanceSharedPtr handler) override { + delegated_callbacks_.addAccessLogHandler(std::move(handler)); + } + + Envoy::Http::FilterChainFactoryCallbacks& delegated_callbacks_; + Matcher::MatchTreeSharedPtr match_tree_; +}; +} // namespace + +Envoy::Http::FilterFactoryCb MatchWrapperConfig::createFilterFactoryFromProtoTyped( + const envoy::extensions::common::matching::v3::ExtensionWithMatcher& proto_config, + const std::string& prefix, Server::Configuration::FactoryContext& context) { + + ASSERT(proto_config.has_extension_config()); + auto& factory = + Config::Utility::getAndCheckFactory( + proto_config.extension_config()); + + auto message = Config::Utility::translateAnyToFactoryConfig( + proto_config.extension_config().typed_config(), context.messageValidationVisitor(), factory); + auto filter_factory = factory.createFilterFactoryFromProto(*message, prefix, context); + + auto match_tree = + Matcher::MatchTreeFactory(context.messageValidationVisitor()) + .create(proto_config.matcher()); + + return [filter_factory, match_tree](Envoy::Http::FilterChainFactoryCallbacks& callbacks) -> void { + DelegatingFactoryCallbacks delegated_callbacks(callbacks, match_tree); + + return filter_factory(delegated_callbacks); + }; +} + +/** + * Static registration for the match wrapper filter. @see RegisterFactory. + * Note that we register this as a filter in order to serve as a drop in wrapper for other HTTP + * filters. While not a real filter, by being registered as one all the code paths that look up HTTP + * filters will look up this filter factory instead, which does the work to create and associate a + * match tree with the underlying filter. + */ +REGISTER_FACTORY(MatchWrapperConfig, Server::Configuration::NamedHttpFilterConfigFactory); + +} // namespace MatchWrapper +} // namespace Http +} // namespace Common +} // namespace Envoy diff --git a/source/common/http/match_wrapper/config.h b/source/common/http/match_wrapper/config.h new file mode 100644 index 000000000000..c1417aaa6f24 --- /dev/null +++ b/source/common/http/match_wrapper/config.h @@ -0,0 +1,28 @@ +#pragma once + +#include "envoy/extensions/common/matching/v3/extension_matcher.pb.validate.h" +#include "envoy/server/filter_config.h" + +#include "extensions/filters/http/common/factory_base.h" +#include "extensions/filters/http/well_known_names.h" + +namespace Envoy { +namespace Common { +namespace Http { +namespace MatchWrapper { + +class MatchWrapperConfig : public Extensions::HttpFilters::Common::FactoryBase< + envoy::extensions::common::matching::v3::ExtensionWithMatcher> { +public: + MatchWrapperConfig() : FactoryBase("match-wrapper") {} + +private: + Envoy::Http::FilterFactoryCb createFilterFactoryFromProtoTyped( + const envoy::extensions::common::matching::v3::ExtensionWithMatcher& proto_config, + const std::string&, Server::Configuration::FactoryContext& context) override; +}; + +} // namespace MatchWrapper +} // namespace Http +} // namespace Common +} // namespace Envoy diff --git a/source/common/http/utility.cc b/source/common/http/utility.cc index ff2cae86e262..86660479c683 100644 --- a/source/common/http/utility.cc +++ b/source/common/http/utility.cc @@ -554,7 +554,7 @@ void Utility::sendLocalReply(const bool& is_reset, const EncodeFunctions& encode // TODO(dio): Probably it is worth to consider caching the encoded message based on gRPC // status. // JsonFormatter adds a '\n' at the end. For header value, it should be removed. - // https://github.com/envoyproxy/envoy/blob/master/source/common/formatter/substitution_formatter.cc#L129 + // https://github.com/envoyproxy/envoy/blob/main/source/common/formatter/substitution_formatter.cc#L129 if (body_text[body_text.length() - 1] == '\n') { body_text = body_text.substr(0, body_text.length() - 1); } diff --git a/source/common/init/target_impl.cc b/source/common/init/target_impl.cc index 8ee37eabfd14..4e3fda44b6a0 100644 --- a/source/common/init/target_impl.cc +++ b/source/common/init/target_impl.cc @@ -4,7 +4,7 @@ namespace Envoy { namespace Init { TargetHandleImpl::TargetHandleImpl(absl::string_view handle_name, absl::string_view name, - std::weak_ptr fn) + std::weak_ptr fn) : handle_name_(handle_name), name_(name), fn_(std::move(fn)) {} bool TargetHandleImpl::initialize(const Watcher& watcher) const { @@ -26,7 +26,7 @@ absl::string_view TargetHandleImpl::name() const { return name_; } TargetImpl::TargetImpl(absl::string_view name, InitializeFn fn) : name_(fmt::format("target {}", name)), - fn_(std::make_shared([this, fn](WatcherHandlePtr watcher_handle) { + fn_(std::make_shared([this, fn](WatcherHandlePtr watcher_handle) { watcher_handle_ = std::move(watcher_handle); fn(); })) {} @@ -38,7 +38,7 @@ absl::string_view TargetImpl::name() const { return name_; } TargetHandlePtr TargetImpl::createHandle(absl::string_view handle_name) const { // Note: can't use std::make_unique here because TargetHandleImpl ctor is private. return TargetHandlePtr( - new TargetHandleImpl(handle_name, name_, std::weak_ptr(fn_))); + new TargetHandleImpl(handle_name, name_, std::weak_ptr(fn_))); } bool TargetImpl::ready() { @@ -54,7 +54,7 @@ bool TargetImpl::ready() { SharedTargetImpl::SharedTargetImpl(absl::string_view name, InitializeFn fn) : name_(fmt::format("shared target {}", name)), - fn_(std::make_shared([this, fn](WatcherHandlePtr watcher_handle) { + fn_(std::make_shared([this, fn](WatcherHandlePtr watcher_handle) { if (initialized_) { watcher_handle->ready(); } else { @@ -70,7 +70,7 @@ absl::string_view SharedTargetImpl::name() const { return name_; } TargetHandlePtr SharedTargetImpl::createHandle(absl::string_view handle_name) const { // Note: can't use std::make_unique here because TargetHandleImpl ctor is private. return TargetHandlePtr( - new TargetHandleImpl(handle_name, name_, std::weak_ptr(fn_))); + new TargetHandleImpl(handle_name, name_, std::weak_ptr(fn_))); } bool SharedTargetImpl::ready() { diff --git a/source/common/init/target_impl.h b/source/common/init/target_impl.h index da7281c69b2f..9cc0bac76c11 100644 --- a/source/common/init/target_impl.h +++ b/source/common/init/target_impl.h @@ -20,7 +20,7 @@ using InitializeFn = std::function; * and resets it later in `ready`. Users needn't care about this implementation detail, they only * need to provide an `InitializeFn` above when constructing a target. */ -using InternalInitalizeFn = std::function; +using InternalInitializeFn = std::function; /** * A TargetHandleImpl functions as a weak reference to a TargetImpl. It is how a ManagerImpl safely @@ -32,7 +32,7 @@ class TargetHandleImpl : public TargetHandle, Logger::Loggable friend class SharedTargetImpl; TargetHandleImpl(absl::string_view handle_name, absl::string_view name, - std::weak_ptr fn); + std::weak_ptr fn); public: // Init::TargetHandle @@ -48,7 +48,7 @@ class TargetHandleImpl : public TargetHandle, Logger::Loggable const std::string name_; // The target's callback function, only called if the weak pointer can be "locked" - const std::weak_ptr fn_; + const std::weak_ptr fn_; }; /** @@ -86,7 +86,7 @@ class TargetImpl : public Target, Logger::Loggable { WatcherHandlePtr watcher_handle_; // The callback function, called via TargetHandleImpl by the manager - const std::shared_ptr fn_; + const std::shared_ptr fn_; }; /** @@ -124,7 +124,7 @@ class SharedTargetImpl : public Target, Logger::Loggable { std::vector watcher_handles_; // The callback function, called via TargetHandleImpl by the manager - const std::shared_ptr fn_; + const std::shared_ptr fn_; // The state so as to signal the manager when a ready target is added. bool initialized_{false}; diff --git a/source/common/local_info/BUILD b/source/common/local_info/BUILD index 07d7c3e5f646..47c3b542d5bb 100644 --- a/source/common/local_info/BUILD +++ b/source/common/local_info/BUILD @@ -13,6 +13,7 @@ envoy_cc_library( hdrs = ["local_info_impl.h"], deps = [ "//include/envoy/local_info:local_info_interface", + "//source/common/config:context_provider_lib", "//source/common/config:version_converter_lib", "//source/common/stats:symbol_table_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", diff --git a/source/common/local_info/local_info_impl.h b/source/common/local_info/local_info_impl.h index 3c4c7ed117d0..887958e44c56 100644 --- a/source/common/local_info/local_info_impl.h +++ b/source/common/local_info/local_info_impl.h @@ -5,30 +5,46 @@ #include "envoy/config/core/v3/base.pb.h" #include "envoy/local_info/local_info.h" +#include "common/config/context_provider_impl.h" #include "common/config/version_converter.h" #include "common/stats/symbol_table_impl.h" namespace Envoy { namespace LocalInfo { +namespace { + +const envoy::config::core::v3::Node buildLocalNode(const envoy::config::core::v3::Node& node, + absl::string_view zone_name, + absl::string_view cluster_name, + absl::string_view node_name) { + envoy::config::core::v3::Node local_node; + local_node.MergeFrom(node); + if (!zone_name.empty()) { + local_node.mutable_locality()->set_zone(std::string(zone_name)); + } + if (!cluster_name.empty()) { + local_node.set_cluster(std::string(cluster_name)); + } + if (!node_name.empty()) { + local_node.set_id(std::string(node_name)); + } + return local_node; +} + +} // namespace + class LocalInfoImpl : public LocalInfo { public: LocalInfoImpl(Stats::SymbolTable& symbol_table, const envoy::config::core::v3::Node& node, + const Protobuf::RepeatedPtrField& node_context_params, const Network::Address::InstanceConstSharedPtr& address, absl::string_view zone_name, absl::string_view cluster_name, absl::string_view node_name) - : node_(node), address_(address), zone_stat_name_storage_(zone_name, symbol_table), - zone_stat_name_(zone_stat_name_storage_.statName()) { - if (!zone_name.empty()) { - node_.mutable_locality()->set_zone(std::string(zone_name)); - } - if (!cluster_name.empty()) { - node_.set_cluster(std::string(cluster_name)); - } - if (!node_name.empty()) { - node_.set_id(std::string(node_name)); - } - } + : node_(buildLocalNode(node, zone_name, cluster_name, node_name)), address_(address), + context_provider_(node_, node_context_params), + zone_stat_name_storage_(zone_name, symbol_table), + zone_stat_name_(zone_stat_name_storage_.statName()) {} Network::Address::InstanceConstSharedPtr address() const override { return address_; } const std::string& zoneName() const override { return node_.locality().zone(); } @@ -36,10 +52,12 @@ class LocalInfoImpl : public LocalInfo { const std::string& clusterName() const override { return node_.cluster(); } const std::string& nodeName() const override { return node_.id(); } const envoy::config::core::v3::Node& node() const override { return node_; } + const Config::ContextProvider& contextProvider() const override { return context_provider_; } private: - envoy::config::core::v3::Node node_; - Network::Address::InstanceConstSharedPtr address_; + const envoy::config::core::v3::Node node_; + const Network::Address::InstanceConstSharedPtr address_; + const Config::ContextProviderImpl context_provider_; const Stats::StatNameManagedStorage zone_stat_name_storage_; const Stats::StatName zone_stat_name_; }; diff --git a/source/common/matcher/matcher.h b/source/common/matcher/matcher.h index 4e21e17ea463..44ae0a26e787 100644 --- a/source/common/matcher/matcher.h +++ b/source/common/matcher/matcher.h @@ -160,7 +160,7 @@ template class MatchTreeFactory { auto& factory = Config::Utility::getAndCheckFactory>(config); ProtobufTypes::MessagePtr message = Config::Utility::translateAnyToFactoryConfig( config.typed_config(), validation_visitor_, factory); - return factory.createDataInput(*message); + return factory.createDataInput(*message, validation_visitor_); } InputMatcherPtr createInputMatcher( diff --git a/source/common/network/connection_impl.cc b/source/common/network/connection_impl.cc index 4ce8ec63dd38..ac1b2edb5b25 100644 --- a/source/common/network/connection_impl.cc +++ b/source/common/network/connection_impl.cc @@ -20,6 +20,7 @@ #include "common/network/listen_socket_impl.h" #include "common/network/raw_buffer_socket.h" #include "common/network/utility.h" +#include "common/runtime/runtime_features.h" namespace Envoy { namespace Network { @@ -490,20 +491,14 @@ void ConnectionImpl::setBufferLimits(uint32_t limit) { // limits we err on the side of buffering more triggering watermark callbacks less often. // // Given the current implementation for straight up TCP proxying, the common case is reading - // |limit| bytes through the socket, passing |limit| bytes to the connection (triggering the high - // watermarks) and the immediately draining |limit| bytes to the socket (triggering the low - // watermarks). We avoid this by setting the high watermark to limit + 1 so a single read will - // not trigger watermarks if the socket is not blocked. - // - // If the connection class is changed to write to the buffer and flush to the socket in the same - // stack then instead of checking watermarks after the write and again after the flush it can - // check once after both operations complete. At that point it would be better to change the high - // watermark from |limit + 1| to |limit| as the common case (move |limit| bytes, flush |limit| - // bytes) would not trigger watermarks but a blocked socket (move |limit| bytes, flush 0 bytes) - // would result in respecting the exact buffer limit. + // |limit| bytes through the socket, passing |limit| bytes to the connection and the immediately + // draining |limit| bytes to the socket. Triggering the high watermarks and then immediately + // triggering the low watermarks would be expensive, but we narrowly avoid triggering high + // watermark when moving |limit| bytes through the connection because the high watermark + // computation checks if the size of the buffer exceeds the high watermark value. if (limit > 0) { - write_buffer_->setWatermarks(limit + 1); - read_buffer_->setWatermarks(limit + 1); + write_buffer_->setWatermarks(limit); + read_buffer_->setWatermarks(limit); } } @@ -781,7 +776,11 @@ ServerConnectionImpl::ServerConnectionImpl(Event::Dispatcher& dispatcher, TransportSocketPtr&& transport_socket, StreamInfo::StreamInfo& stream_info, bool connected) : ConnectionImpl(dispatcher, std::move(socket), std::move(transport_socket), stream_info, - connected) {} + connected) { + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) { + noDelay(true); + } +} void ServerConnectionImpl::setTransportSocketConnectTimeout(std::chrono::milliseconds timeout) { if (!transport_connect_pending_) { @@ -860,6 +859,9 @@ void ClientConnectionImpl::connect() { socket_->addressProvider().remoteAddress()->asString()); const Api::SysCallIntResult result = socket_->connect(socket_->addressProvider().remoteAddress()); if (result.rc_ == 0) { + if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) { + noDelay(true); + } // write will become ready. ASSERT(connecting_); } else { diff --git a/source/common/network/socket_interface.h b/source/common/network/socket_interface.h index 070dec2b3c69..89ae1d9770de 100644 --- a/source/common/network/socket_interface.h +++ b/source/common/network/socket_interface.h @@ -17,6 +17,8 @@ namespace Network { class SocketInterfaceExtension : public Server::BootstrapExtension { public: SocketInterfaceExtension(SocketInterface& sock_interface) : sock_interface_(sock_interface) {} + // Server::BootstrapExtension + void onServerInitialized() override {} protected: SocketInterface& sock_interface_; diff --git a/source/common/network/socket_interface_impl.h b/source/common/network/socket_interface_impl.h index d87ad6a95913..ecc14e2e551b 100644 --- a/source/common/network/socket_interface_impl.h +++ b/source/common/network/socket_interface_impl.h @@ -20,6 +20,7 @@ class SocketInterfaceImpl : public SocketInterfaceBase { Server::BootstrapExtensionPtr createBootstrapExtension(const Protobuf::Message& config, Server::Configuration::ServerFactoryContext& context) override; + ProtobufTypes::MessagePtr createEmptyConfigProto() override; std::string name() const override { return "envoy.extensions.network.socket_interface.default_socket_interface"; diff --git a/source/common/network/tcp_listener_impl.cc b/source/common/network/tcp_listener_impl.cc index 1d9b37b1a324..38c77db31e44 100644 --- a/source/common/network/tcp_listener_impl.cc +++ b/source/common/network/tcp_listener_impl.cc @@ -124,8 +124,7 @@ void TcpListenerImpl::enable() { socket_->ioHandle().enableFileEvents(Event::Fil void TcpListenerImpl::disable() { socket_->ioHandle().enableFileEvents(0); } -void TcpListenerImpl::setRejectFraction(const float reject_fraction) { - ASSERT(0 <= reject_fraction && reject_fraction <= 1); +void TcpListenerImpl::setRejectFraction(const UnitFloat reject_fraction) { reject_fraction_ = reject_fraction; } diff --git a/source/common/network/tcp_listener_impl.h b/source/common/network/tcp_listener_impl.h index d0859549ae9b..fd66ffc60dee 100644 --- a/source/common/network/tcp_listener_impl.h +++ b/source/common/network/tcp_listener_impl.h @@ -3,6 +3,8 @@ #include "envoy/common/random_generator.h" #include "envoy/runtime/runtime.h" +#include "common/common/interval_value.h" + #include "absl/strings/string_view.h" #include "base_listener_impl.h" @@ -20,7 +22,7 @@ class TcpListenerImpl : public BaseListenerImpl { ~TcpListenerImpl() override { socket_->ioHandle().resetFileEvents(); } void disable() override; void enable() override; - void setRejectFraction(float reject_fraction) override; + void setRejectFraction(UnitFloat reject_fraction) override; static const absl::string_view GlobalMaxCxRuntimeKey; @@ -38,7 +40,7 @@ class TcpListenerImpl : public BaseListenerImpl { static bool rejectCxOverGlobalLimit(); Random::RandomGenerator& random_; - float reject_fraction_; + UnitFloat reject_fraction_; }; } // namespace Network diff --git a/source/common/network/udp_listener_impl.h b/source/common/network/udp_listener_impl.h index a6b2852b3990..5e592c267276 100644 --- a/source/common/network/udp_listener_impl.h +++ b/source/common/network/udp_listener_impl.h @@ -30,7 +30,7 @@ class UdpListenerImpl : public BaseListenerImpl, // Network::Listener Interface void disable() override; void enable() override; - void setRejectFraction(float) override {} + void setRejectFraction(UnitFloat) override {} // Network::UdpListener Interface Event::Dispatcher& dispatcher() override; diff --git a/source/common/protobuf/utility.h b/source/common/protobuf/utility.h index a23fe7d216f3..e6480c2aee2f 100644 --- a/source/common/protobuf/utility.h +++ b/source/common/protobuf/utility.h @@ -166,17 +166,17 @@ class RepeatedPtrUtil { // Based on MessageUtil::hash() defined below. template static std::size_t hash(const Protobuf::RepeatedPtrField& source) { - // Use Protobuf::io::CodedOutputStream to force deterministic serialization, so that the same - // message doesn't hash to different values. std::string text; { - // For memory safety, the StringOutputStream needs to be destroyed before - // we read the string. - Protobuf::io::StringOutputStream string_stream(&text); - Protobuf::io::CodedOutputStream coded_stream(&string_stream); - coded_stream.SetSerializationDeterministic(true); + Protobuf::TextFormat::Printer printer; + printer.SetExpandAny(true); + printer.SetUseFieldNumber(true); + printer.SetSingleLineMode(true); + printer.SetHideUnknownFields(true); for (const auto& message : source) { - message.SerializeToCodedStream(&coded_stream); + std::string text_message; + printer.PrintToString(message, &text_message); + absl::StrAppend(&text, text_message); } } return HashUtil::xxHash64(text); diff --git a/source/common/router/BUILD b/source/common/router/BUILD index f4873e98aa27..96557a1af8b4 100644 --- a/source/common/router/BUILD +++ b/source/common/router/BUILD @@ -264,7 +264,6 @@ envoy_cc_library( "//source/common/http:header_utility_lib", "//source/common/http:headers_lib", "//source/common/http:utility_lib", - "//source/common/runtime:runtime_features_lib", "@envoy_api//envoy/config/route/v3:pkg_cc_proto", ], ) @@ -368,6 +367,7 @@ envoy_cc_library( deps = [ ":header_formatter_lib", "//include/envoy/http:header_map_interface", + "//source/common/http:header_utility_lib", "//source/common/http:headers_lib", "//source/common/protobuf:utility_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", diff --git a/source/common/router/header_parser.cc b/source/common/router/header_parser.cc index 1e4a9f4e3098..5bd40ba20796 100644 --- a/source/common/router/header_parser.cc +++ b/source/common/router/header_parser.cc @@ -7,6 +7,7 @@ #include "envoy/config/core/v3/base.pb.h" #include "common/common/assert.h" +#include "common/http/header_utility.h" #include "common/http/headers.h" #include "common/protobuf/utility.h" @@ -46,8 +47,10 @@ HeaderFormatterPtr parseInternal(const envoy::config::core::v3::HeaderValue& hea // will cause us to have to worry about interaction with other aspects of the // RouteAction, e.g. prefix rewriting. We also reject other :-prefixed // headers, since it seems dangerous and there doesn't appear a use case. - if (key[0] == ':') { - throw EnvoyException(":-prefixed headers may not be modified"); + // Host is disallowed as it created confusing and inconsistent behaviors for + // HTTP/1 and HTTP/2. It could arguably be allowed on the response path. + if (!Http::HeaderUtility::isModifiableHeader(key)) { + throw EnvoyException(":-prefixed or host headers may not be modified"); } absl::string_view format(header_value.value()); @@ -258,7 +261,7 @@ HeaderParserPtr HeaderParser::configure( // We reject :-prefix (e.g. :path) removal here. This is dangerous, since other aspects of // request finalization assume their existence and they are needed for well-formedness in most // cases. - if (header[0] == ':' || Http::LowerCaseString(header).get() == "host") { + if (!Http::HeaderUtility::isRemovableHeader(header)) { throw EnvoyException(":-prefixed or host headers may not be removed"); } header_parser->headers_to_remove_.emplace_back(header); diff --git a/source/common/router/rds_impl.cc b/source/common/router/rds_impl.cc index 3886e68e01f6..5a88fd01516d 100644 --- a/source/common/router/rds_impl.cc +++ b/source/common/router/rds_impl.cc @@ -87,7 +87,7 @@ RdsRouteConfigSubscription::RdsRouteConfigSubscription( subscription_ = factory_context.clusterManager().subscriptionFactory().subscriptionFromConfigSource( rds.config_source(), Grpc::Common::typeUrl(resource_name), *scope_, *this, - resource_decoder_); + resource_decoder_, false); local_init_manager_.add(local_init_target_); config_update_info_ = std::make_unique(factory_context.timeSource()); diff --git a/source/common/router/retry_state_impl.cc b/source/common/router/retry_state_impl.cc index f4303a55f461..1f62d8bc023d 100644 --- a/source/common/router/retry_state_impl.cc +++ b/source/common/router/retry_state_impl.cc @@ -13,7 +13,6 @@ #include "common/http/codes.h" #include "common/http/headers.h" #include "common/http/utility.h" -#include "common/runtime/runtime_features.h" namespace Envoy { namespace Router { @@ -52,12 +51,10 @@ RetryStatePtr RetryStateImpl::create(const RetryPolicy& route_policy, request_headers.removeEnvoyRetryOn(); request_headers.removeEnvoyRetryGrpcOn(); request_headers.removeEnvoyMaxRetries(); - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.consume_all_retry_headers")) { - request_headers.removeEnvoyHedgeOnPerTryTimeout(); - request_headers.removeEnvoyRetriableHeaderNames(); - request_headers.removeEnvoyRetriableStatusCodes(); - request_headers.removeEnvoyUpstreamRequestPerTryTimeoutMs(); - } + request_headers.removeEnvoyHedgeOnPerTryTimeout(); + request_headers.removeEnvoyRetriableHeaderNames(); + request_headers.removeEnvoyRetriableStatusCodes(); + request_headers.removeEnvoyUpstreamRequestPerTryTimeoutMs(); return ret; } diff --git a/source/common/router/router_ratelimit.cc b/source/common/router/router_ratelimit.cc index cfe882a60f42..6a194da1df9f 100644 --- a/source/common/router/router_ratelimit.cc +++ b/source/common/router/router_ratelimit.cc @@ -17,6 +17,27 @@ namespace Envoy { namespace Router { +namespace { +bool populateDescriptor(const std::vector& actions, + std::vector& descriptor_entries, + const std::string& local_service_cluster, + const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) { + bool result = true; + for (const RateLimit::DescriptorProducerPtr& action : actions) { + RateLimit::DescriptorEntry descriptor_entry; + result = result && + action->populateDescriptor(descriptor_entry, local_service_cluster, headers, info); + if (!result) { + break; + } + if (!descriptor_entry.key_.empty()) { + descriptor_entries.push_back(descriptor_entry); + } + } + return result; +} +} // namespace + const uint64_t RateLimitPolicyImpl::MAX_STAGE_NUMBER = 10UL; bool DynamicMetadataRateLimitOverride::populateOverride( @@ -44,22 +65,23 @@ bool DynamicMetadataRateLimitOverride::populateOverride( return false; } -bool SourceClusterAction::populateDescriptor(RateLimit::Descriptor& descriptor, +bool SourceClusterAction::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string& local_service_cluster, const Http::RequestHeaderMap&, const StreamInfo::StreamInfo&) const { - descriptor.entries_.push_back({"source_cluster", local_service_cluster}); + descriptor_entry = {"source_cluster", local_service_cluster}; return true; } -bool DestinationClusterAction::populateDescriptor(RateLimit::Descriptor& descriptor, +bool DestinationClusterAction::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string&, const Http::RequestHeaderMap&, const StreamInfo::StreamInfo& info) const { - descriptor.entries_.push_back({"destination_cluster", info.routeEntry()->clusterName()}); + descriptor_entry = {"destination_cluster", info.routeEntry()->clusterName()}; return true; } -bool RequestHeadersAction::populateDescriptor(RateLimit::Descriptor& descriptor, const std::string&, +bool RequestHeadersAction::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, + const std::string&, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo&) const { const auto header_value = headers.get(header_name_); @@ -71,13 +93,12 @@ bool RequestHeadersAction::populateDescriptor(RateLimit::Descriptor& descriptor, return skip_if_absent_; } // TODO(https://github.com/envoyproxy/envoy/issues/13454): Potentially populate all header values. - descriptor.entries_.push_back( - {descriptor_key_, std::string(header_value[0]->value().getStringView())}); + descriptor_entry = {descriptor_key_, std::string(header_value[0]->value().getStringView())}; return true; } -bool RemoteAddressAction::populateDescriptor(RateLimit::Descriptor& descriptor, const std::string&, - const Http::RequestHeaderMap&, +bool RemoteAddressAction::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, + const std::string&, const Http::RequestHeaderMap&, const StreamInfo::StreamInfo& info) const { const Network::Address::InstanceConstSharedPtr& remote_address = info.downstreamAddressProvider().remoteAddress(); @@ -85,14 +106,14 @@ bool RemoteAddressAction::populateDescriptor(RateLimit::Descriptor& descriptor, return false; } - descriptor.entries_.push_back({"remote_address", remote_address->ip()->addressAsString()}); + descriptor_entry = {"remote_address", remote_address->ip()->addressAsString()}; return true; } -bool GenericKeyAction::populateDescriptor(RateLimit::Descriptor& descriptor, const std::string&, - const Http::RequestHeaderMap&, +bool GenericKeyAction::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, + const std::string&, const Http::RequestHeaderMap&, const StreamInfo::StreamInfo&) const { - descriptor.entries_.push_back({descriptor_key_, descriptor_value_}); + descriptor_entry = {descriptor_key_, descriptor_value_}; return true; } @@ -106,8 +127,8 @@ MetaDataAction::MetaDataAction( default_value_(action.default_value()), source_(envoy::config::route::v3::RateLimit::Action::MetaData::DYNAMIC) {} -bool MetaDataAction::populateDescriptor(RateLimit::Descriptor& descriptor, const std::string&, - const Http::RequestHeaderMap&, +bool MetaDataAction::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, + const std::string&, const Http::RequestHeaderMap&, const StreamInfo::StreamInfo& info) const { const envoy::config::core::v3::Metadata* metadata_source; @@ -126,10 +147,10 @@ bool MetaDataAction::populateDescriptor(RateLimit::Descriptor& descriptor, const Envoy::Config::Metadata::metadataValue(metadata_source, metadata_key_).string_value(); if (!metadata_string_value.empty()) { - descriptor.entries_.push_back({descriptor_key_, metadata_string_value}); + descriptor_entry = {descriptor_key_, metadata_string_value}; return true; } else if (metadata_string_value.empty() && !default_value_.empty()) { - descriptor.entries_.push_back({descriptor_key_, default_value_}); + descriptor_entry = {descriptor_key_, default_value_}; return true; } @@ -142,12 +163,12 @@ HeaderValueMatchAction::HeaderValueMatchAction( expect_match_(PROTOBUF_GET_WRAPPED_OR_DEFAULT(action, expect_match, true)), action_headers_(Http::HeaderUtility::buildHeaderDataVector(action.headers())) {} -bool HeaderValueMatchAction::populateDescriptor(RateLimit::Descriptor& descriptor, +bool HeaderValueMatchAction::populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string&, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo&) const { if (expect_match_ == Http::HeaderUtility::matchHeaders(headers, action_headers_)) { - descriptor.entries_.push_back({"header_match", descriptor_value_}); + descriptor_entry = {"header_match", descriptor_value_}; return true; } else { return false; @@ -225,13 +246,8 @@ void RateLimitPolicyEntryImpl::populateDescriptors(std::vectorpopulateDescriptor(descriptor, local_service_cluster, headers, info); - if (!result) { - break; - } - } + bool result = + populateDescriptor(actions_, descriptor.entries_, local_service_cluster, headers, info); if (limit_override_) { limit_override_.value()->populateOverride(descriptor, &info.dynamicMetadata()); @@ -242,6 +258,18 @@ void RateLimitPolicyEntryImpl::populateDescriptors(std::vector& descriptors, + const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& info) const { + RateLimit::LocalDescriptor descriptor({}); + bool result = + populateDescriptor(actions_, descriptor.entries_, local_service_cluster, headers, info); + if (result) { + descriptors.emplace_back(descriptor); + } +} + RateLimitPolicyImpl::RateLimitPolicyImpl( const Protobuf::RepeatedPtrField& rate_limits, ProtobufMessage::ValidationVisitor& validator) diff --git a/source/common/router/router_ratelimit.h b/source/common/router/router_ratelimit.h index 7aa0d6ca0401..b7d592f32974 100644 --- a/source/common/router/router_ratelimit.h +++ b/source/common/router/router_ratelimit.h @@ -42,7 +42,7 @@ class DynamicMetadataRateLimitOverride : public RateLimitOverrideAction { class SourceClusterAction : public RateLimit::DescriptorProducer { public: // Ratelimit::DescriptorProducer - bool populateDescriptor(RateLimit::Descriptor& descriptor, + bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const override; @@ -54,7 +54,7 @@ class SourceClusterAction : public RateLimit::DescriptorProducer { class DestinationClusterAction : public RateLimit::DescriptorProducer { public: // Ratelimit::DescriptorProducer - bool populateDescriptor(RateLimit::Descriptor& descriptor, + bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const override; @@ -70,7 +70,7 @@ class RequestHeadersAction : public RateLimit::DescriptorProducer { skip_if_absent_(action.skip_if_absent()) {} // Ratelimit::DescriptorProducer - bool populateDescriptor(RateLimit::Descriptor& descriptor, + bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const override; @@ -87,7 +87,7 @@ class RequestHeadersAction : public RateLimit::DescriptorProducer { class RemoteAddressAction : public RateLimit::DescriptorProducer { public: // Ratelimit::DescriptorProducer - bool populateDescriptor(RateLimit::Descriptor& descriptor, + bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const override; @@ -104,7 +104,7 @@ class GenericKeyAction : public RateLimit::DescriptorProducer { : "generic_key") {} // Ratelimit::DescriptorProducer - bool populateDescriptor(RateLimit::Descriptor& descriptor, + bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const override; @@ -123,7 +123,7 @@ class MetaDataAction : public RateLimit::DescriptorProducer { // for maintaining backward compatibility with the deprecated DynamicMetaData action MetaDataAction(const envoy::config::route::v3::RateLimit::Action::DynamicMetaData& action); // Ratelimit::DescriptorProducer - bool populateDescriptor(RateLimit::Descriptor& descriptor, + bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const override; @@ -144,7 +144,7 @@ class HeaderValueMatchAction : public RateLimit::DescriptorProducer { const envoy::config::route::v3::RateLimit::Action::HeaderValueMatch& action); // Ratelimit::DescriptorProducer - bool populateDescriptor(RateLimit::Descriptor& descriptor, + bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const override; @@ -169,6 +169,10 @@ class RateLimitPolicyEntryImpl : public RateLimitPolicyEntry { void populateDescriptors(std::vector& descriptors, const std::string& local_service_cluster, const Http::RequestHeaderMap&, const StreamInfo::StreamInfo& info) const override; + void populateLocalDescriptors(std::vector& descriptors, + const std::string& local_service_cluster, + const Http::RequestHeaderMap&, + const StreamInfo::StreamInfo& info) const override; private: const std::string disable_key_; diff --git a/source/common/router/scoped_rds.cc b/source/common/router/scoped_rds.cc index 17613814efea..17a1d64b57ec 100644 --- a/source/common/router/scoped_rds.cc +++ b/source/common/router/scoped_rds.cc @@ -112,7 +112,7 @@ ScopedRdsConfigSubscription::ScopedRdsConfigSubscription( subscription_ = factory_context.clusterManager().subscriptionFactory().subscriptionFromConfigSource( scoped_rds.scoped_rds_config_source(), Grpc::Common::typeUrl(resource_name), *scope_, - *this, resource_decoder_); + *this, resource_decoder_, false); initialize([scope_key_builder]() -> Envoy::Config::ConfigProvider::ConfigConstSharedPtr { return std::make_shared( diff --git a/source/common/router/upstream_request.cc b/source/common/router/upstream_request.cc index 6a9243370d27..f0f42fb8c362 100644 --- a/source/common/router/upstream_request.cc +++ b/source/common/router/upstream_request.cc @@ -337,7 +337,12 @@ void UpstreamRequest::onPoolFailure(ConnectionPool::PoolFailureReason reason, reset_reason = Http::StreamResetReason::ConnectionFailure; break; case ConnectionPool::PoolFailureReason::Timeout: - reset_reason = Http::StreamResetReason::LocalReset; + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.treat_upstream_connect_timeout_as_connect_failure")) { + reset_reason = Http::StreamResetReason::ConnectionFailure; + } else { + reset_reason = Http::StreamResetReason::LocalReset; + } } // Mimic an upstream reset. diff --git a/source/common/router/vhds.cc b/source/common/router/vhds.cc index 6038b42684b9..db3e3b8389e0 100644 --- a/source/common/router/vhds.cc +++ b/source/common/router/vhds.cc @@ -32,9 +32,8 @@ VhdsSubscription::VhdsSubscription( scope_(factory_context.scope().createScope(stat_prefix + "vhds." + config_update_info_->routeConfigName() + ".")), stats_({ALL_VHDS_STATS(POOL_COUNTER(*scope_))}), - init_target_( - fmt::format("VhdsConfigSubscription {}", config_update_info_->routeConfigName()), - [this]() { subscription_->start({config_update_info_->routeConfigName()}, true); }), + init_target_(fmt::format("VhdsConfigSubscription {}", config_update_info_->routeConfigName()), + [this]() { subscription_->start({config_update_info_->routeConfigName()}); }), route_config_providers_(route_config_providers) { const auto& config_source = config_update_info_->routeConfiguration() .vhds() @@ -48,7 +47,7 @@ VhdsSubscription::VhdsSubscription( subscription_ = factory_context.clusterManager().subscriptionFactory().subscriptionFromConfigSource( config_update_info_->routeConfiguration().vhds().config_source(), - Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_); + Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_, true); } void VhdsSubscription::updateOnDemand(const std::string& with_route_config_name_prefix) { diff --git a/source/common/runtime/runtime_features.cc b/source/common/runtime/runtime_features.cc index b2d19539c375..116ed1b2fb2d 100644 --- a/source/common/runtime/runtime_features.cc +++ b/source/common/runtime/runtime_features.cc @@ -61,16 +61,11 @@ constexpr const char* runtime_features[] = { "envoy.reloadable_features.allow_500_after_100", "envoy.reloadable_features.allow_preconnect", "envoy.reloadable_features.allow_response_for_timeout", - "envoy.reloadable_features.consume_all_retry_headers", + "envoy.reloadable_features.always_nodelay", "envoy.reloadable_features.check_ocsp_policy", "envoy.reloadable_features.disable_tls_inspector_injection", - "envoy.reloadable_features.disallow_unbounded_access_logs", - "envoy.reloadable_features.early_errors_via_hcm", - "envoy.reloadable_features.enable_dns_cache_circuit_breakers", - "envoy.reloadable_features.fix_upgrade_response", - "envoy.reloadable_features.fix_wildcard_matching", - "envoy.reloadable_features.fixed_connection_close", "envoy.reloadable_features.hcm_stream_error_on_invalid_message", + "envoy.reloadable_features.health_check.graceful_goaway_handling", "envoy.reloadable_features.http_default_alpn", "envoy.reloadable_features.http_match_on_all_headers", "envoy.reloadable_features.http_set_copy_replace_all_headers", @@ -86,8 +81,12 @@ constexpr const char* runtime_features[] = { "envoy.reloadable_features.stop_faking_paths", "envoy.reloadable_features.strict_1xx_and_204_response_headers", "envoy.reloadable_features.tls_use_io_handle_bio", + "envoy.reloadable_features.treat_host_like_authority", + "envoy.reloadable_features.treat_upstream_connect_timeout_as_connect_failure", + "envoy.reloadable_features.upstream_host_weight_change_causes_rebuild", "envoy.reloadable_features.vhds_heartbeats", "envoy.reloadable_features.unify_grpc_handling", + "envoy.reloadable_features.upstream_http2_flood_checks", "envoy.restart_features.use_apple_api_for_dns_lookups", }; @@ -107,8 +106,6 @@ constexpr const char* disabled_runtime_features[] = { "envoy.reloadable_features.enable_type_url_downgrade_and_upgrade", // TODO(alyssawilk) flip true after the release. "envoy.reloadable_features.new_tcp_connection_pool", - // TODO(yanavlasov) flip true after all tests for upstream flood checks are implemented - "envoy.reloadable_features.upstream_http2_flood_checks", // Sentinel and test flag. "envoy.reloadable_features.test_feature_false", }; diff --git a/source/common/runtime/runtime_impl.cc b/source/common/runtime/runtime_impl.cc index 51fba626e709..eacc4b58ffa2 100644 --- a/source/common/runtime/runtime_impl.cc +++ b/source/common/runtime/runtime_impl.cc @@ -438,8 +438,8 @@ RtdsSubscription::RtdsSubscription( void RtdsSubscription::createSubscription() { const auto resource_name = getResourceName(); subscription_ = parent_.cm_->subscriptionFactory().subscriptionFromConfigSource( - config_source_, Grpc::Common::typeUrl(resource_name), *stats_scope_, *this, - resource_decoder_); + config_source_, Grpc::Common::typeUrl(resource_name), *stats_scope_, *this, resource_decoder_, + false); } void RtdsSubscription::onConfigUpdate(const std::vector& resources, diff --git a/source/common/secret/sds_api.cc b/source/common/secret/sds_api.cc index f239b725b1c8..83a660b40186 100644 --- a/source/common/secret/sds_api.cc +++ b/source/common/secret/sds_api.cc @@ -33,7 +33,7 @@ SdsApi::SdsApi(envoy::config::core::v3::ConfigSource sds_config, absl::string_vi const auto resource_name = getResourceName(); // This has to happen here (rather than in initialize()) as it can throw exceptions. subscription_ = subscription_factory_.subscriptionFromConfigSource( - sds_config_, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_); + sds_config_, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_, false); } void SdsApi::resolveDataSource(const FileContentMap& files, diff --git a/source/common/stats/symbol_table_impl.cc b/source/common/stats/symbol_table_impl.cc index f7799a7fc55f..22529eadab7e 100644 --- a/source/common/stats/symbol_table_impl.cc +++ b/source/common/stats/symbol_table_impl.cc @@ -246,12 +246,12 @@ void SymbolTableImpl::incRefCount(const StatName& stat_name) { ASSERT(decode_search != decode_map_.end(), "Please see " - "https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#" + "https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md#" "debugging-symbol-table-assertions"); auto encode_search = encode_map_.find(decode_search->second->toStringView()); ASSERT(encode_search != encode_map_.end(), "Please see " - "https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#" + "https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md#" "debugging-symbol-table-assertions"); ++encode_search->second.ref_count_; diff --git a/source/common/stats/tag_extractor_impl.cc b/source/common/stats/tag_extractor_impl.cc index 6aefbdf6cd25..5e735d4ab77f 100644 --- a/source/common/stats/tag_extractor_impl.cc +++ b/source/common/stats/tag_extractor_impl.cc @@ -26,7 +26,9 @@ bool regexStartsWithDot(absl::string_view regex) { TagExtractorImplBase::TagExtractorImplBase(absl::string_view name, absl::string_view regex, absl::string_view substr) - : name_(name), prefix_(std::string(extractRegexPrefix(regex))), substr_(substr) {} + : name_(name), prefix_(std::string(extractRegexPrefix(regex))), substr_(substr) { + PERF_TAG_INIT; +} std::string TagExtractorImplBase::extractRegexPrefix(absl::string_view regex) { std::string prefix; @@ -90,6 +92,7 @@ bool TagExtractorStdRegexImpl::extractTag(absl::string_view stat_name, std::vect if (substrMismatch(stat_name)) { PERF_RECORD(perf, "re-skip", name_); + PERF_TAG_INC(skipped_); return false; } @@ -113,9 +116,11 @@ bool TagExtractorStdRegexImpl::extractTag(absl::string_view stat_name, std::vect std::string::size_type end = remove_subexpr.second - stat_name.begin(); remove_characters.insert(start, end); PERF_RECORD(perf, "re-match", name_); + PERF_TAG_INC(matched_); return true; } PERF_RECORD(perf, "re-miss", name_); + PERF_TAG_INC(missed_); return false; } @@ -129,6 +134,7 @@ bool TagExtractorRe2Impl::extractTag(absl::string_view stat_name, std::vector #include +#ifdef ENVOY_PERF_ANNOTATION +#include +#endif + #include "envoy/stats/tag_extractor.h" #include "common/common/regex.h" @@ -14,6 +18,29 @@ namespace Envoy { namespace Stats { +// To check if a tag extractor is actually used you can run +// bazel test //test/... --test_output=streamed --define=perf_annotation=enabled +#ifdef ENVOY_PERF_ANNOTATION + +struct Counters { + uint32_t skipped_{}; + uint32_t matched_{}; + uint32_t missed_{}; +}; + +#define PERF_TAG_COUNTERS std::unique_ptr counters_ + +#define PERF_TAG_INIT counters_ = std::make_unique() +#define PERF_TAG_INC(member) ++(counters_->member) + +#else + +#define PERF_TAG_COUNTERS +#define PERF_TAG_INIT +#define PERF_TAG_INC(member) + +#endif + class TagExtractorImplBase : public TagExtractor { public: /** @@ -32,6 +59,13 @@ class TagExtractorImplBase : public TagExtractor { TagExtractorImplBase(absl::string_view name, absl::string_view regex, absl::string_view substr = ""); +#ifdef ENVOY_PERF_ANNOTATION + ~TagExtractorImplBase() override { + std::cout << fmt::format("TagStats for {} tag extractor: skipped {}, matched {}, missing {}", + name_, counters_->skipped_, counters_->matched_, counters_->missed_) + << std::endl; + } +#endif std::string name() const override { return name_; } absl::string_view prefixToken() const override { return prefix_; } @@ -62,6 +96,8 @@ class TagExtractorImplBase : public TagExtractor { const std::string name_; const std::string prefix_; const std::string substr_; + + PERF_TAG_COUNTERS; }; class TagExtractorStdRegexImpl : public TagExtractorImplBase { diff --git a/source/common/stats/thread_local_store.h b/source/common/stats/thread_local_store.h index dd66d137b491..946a84ed874c 100644 --- a/source/common/stats/thread_local_store.h +++ b/source/common/stats/thread_local_store.h @@ -141,7 +141,7 @@ using ParentHistogramImplSharedPtr = RefcountPtr; /** * Store implementation with thread local caching. For design details see - * https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md + * https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md */ class ThreadLocalStoreImpl : Logger::Loggable, public StoreRoot { public: diff --git a/source/common/stats/utility.h b/source/common/stats/utility.h index 7ca48a2a10ef..90c1aa54ff10 100644 --- a/source/common/stats/utility.h +++ b/source/common/stats/utility.h @@ -68,7 +68,7 @@ class Utility { * Creates a nested scope from a vector of tokens which are used to create the * name. The tokens can be specified as DynamicName or StatName. For * tokens specified as DynamicName, a dynamic StatName will be created. See - * https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#dynamic-stat-tokens + * https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md#dynamic-stat-tokens * for more detail on why symbolic StatNames are preferred when possible. * * See also scopeFromStatNames, which is slightly faster but does not allow @@ -97,7 +97,7 @@ class Utility { * Creates a counter from a vector of tokens which are used to create the * name. The tokens can be specified as DynamicName or StatName. For * tokens specified as DynamicName, a dynamic StatName will be created. See - * https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#dynamic-stat-tokens + * https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md#dynamic-stat-tokens * for more detail on why symbolic StatNames are preferred when possible. * * See also counterFromStatNames, which is slightly faster but does not allow @@ -130,7 +130,7 @@ class Utility { * Creates a gauge from a vector of tokens which are used to create the * name. The tokens can be specified as DynamicName or StatName. For * tokens specified as DynamicName, a dynamic StatName will be created. See - * https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#dynamic-stat-tokens + * https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md#dynamic-stat-tokens * for more detail on why symbolic StatNames are preferred when possible. * * See also gaugeFromStatNames, which is slightly faster but does not allow @@ -167,7 +167,7 @@ class Utility { * Creates a histogram from a vector of tokens which are used to create the * name. The tokens can be specified as DynamicName or StatName. For * tokens specified as DynamicName, a dynamic StatName will be created. See - * https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#dynamic-stat-tokens + * https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md#dynamic-stat-tokens * for more detail on why symbolic StatNames are preferred when possible. * * See also histogramFromStatNames, which is slightly faster but does not allow @@ -204,7 +204,7 @@ class Utility { * Creates a TextReadout from a vector of tokens which are used to create the * name. The tokens can be specified as DynamicName or StatName. For * tokens specified as DynamicName, a dynamic StatName will be created. See - * https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#dynamic-stat-tokens + * https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md#dynamic-stat-tokens * for more detail on why symbolic StatNames are preferred when possible. * * See also TextReadoutFromStatNames, which is slightly faster but does not allow diff --git a/source/common/tcp/conn_pool.cc b/source/common/tcp/conn_pool.cc index 90426c794c49..034f647d7f51 100644 --- a/source/common/tcp/conn_pool.cc +++ b/source/common/tcp/conn_pool.cc @@ -6,6 +6,7 @@ #include "envoy/event/timer.h" #include "envoy/upstream/upstream.h" +#include "common/runtime/runtime_features.h" #include "common/stats/timespan_impl.h" #include "common/upstream/upstream_impl.h" @@ -31,7 +32,10 @@ ActiveTcpClient::ActiveTcpClient(Envoy::ConnectionPool::ConnPoolImplBase& parent host->cluster().stats().upstream_cx_tx_bytes_total_, host->cluster().stats().upstream_cx_tx_bytes_buffered_, &host->cluster().stats().bind_errors_, nullptr}); - connection_->noDelay(true); + + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) { + connection_->noDelay(true); + } connection_->connect(); } diff --git a/source/common/tcp/original_conn_pool.cc b/source/common/tcp/original_conn_pool.cc index b34c31280f89..9aa1e21d90f4 100644 --- a/source/common/tcp/original_conn_pool.cc +++ b/source/common/tcp/original_conn_pool.cc @@ -6,6 +6,7 @@ #include "envoy/event/timer.h" #include "envoy/upstream/upstream.h" +#include "common/runtime/runtime_features.h" #include "common/stats/timespan_impl.h" #include "common/upstream/upstream_impl.h" @@ -400,7 +401,9 @@ OriginalConnPoolImpl::ActiveConn::ActiveConn(OriginalConnPoolImpl& parent) // We just universally set no delay on connections. Theoretically we might at some point want // to make this configurable. - conn_->noDelay(true); + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) { + conn_->noDelay(true); + } } OriginalConnPoolImpl::ActiveConn::~ActiveConn() { diff --git a/source/common/upstream/cds_api_impl.cc b/source/common/upstream/cds_api_impl.cc index 4568bf84b8f3..36d10a20b56f 100644 --- a/source/common/upstream/cds_api_impl.cc +++ b/source/common/upstream/cds_api_impl.cc @@ -33,7 +33,7 @@ CdsApiImpl::CdsApiImpl(const envoy::config::core::v3::ConfigSource& cds_config, cm_(cm), scope_(scope.createScope("cluster_manager.cds.")) { const auto resource_name = getResourceName(); subscription_ = cm_.subscriptionFactory().subscriptionFromConfigSource( - cds_config, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_); + cds_config, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_, false); } void CdsApiImpl::onConfigUpdate(const std::vector& resources, diff --git a/source/common/upstream/cluster_manager_impl.cc b/source/common/upstream/cluster_manager_impl.cc index ebf842ee95dd..35ac22b48195 100644 --- a/source/common/upstream/cluster_manager_impl.cc +++ b/source/common/upstream/cluster_manager_impl.cc @@ -846,25 +846,32 @@ ThreadLocalCluster* ClusterManagerImpl::getThreadLocalCluster(absl::string_view void ClusterManagerImpl::maybePreconnect( ThreadLocalClusterManagerImpl::ClusterEntry& cluster_entry, + const ClusterConnectivityState& state, std::function pick_preconnect_pool) { - // TODO(alyssawilk) As currently implemented, this will always just preconnect - // one connection ahead of actually needed connections. - // - // Instead we want to track the following metrics across the entire connection - // pool and use the same algorithm we do for per-upstream preconnect: - // ((pending_streams_ + num_active_streams_) * global_preconnect_ratio > - // (connecting_stream_capacity_ + num_active_streams_))) - // and allow multiple preconnects per pick. - // Also cap preconnects such that - // num_unused_preconnect < num hosts - // since if we have more preconnects than hosts, we should consider kicking into - // per-upstream preconnect. - // - // Once we do this, this should loop capped number of times while shouldPreconnect is true. - if (cluster_entry.cluster_info_->peekaheadRatio() > 1.0) { + auto peekahead_ratio = cluster_entry.cluster_info_->peekaheadRatio(); + if (peekahead_ratio <= 1.0) { + return; + } + + // 3 here is arbitrary. Just as in ConnPoolImplBase::tryCreateNewConnections + // we want to limit the work which can be done on any given preconnect attempt. + for (int i = 0; i < 3; ++i) { + // See if adding this one new connection + // would put the cluster over desired capacity. If so, stop preconnecting. + // + // We anticipate the incoming stream here, because maybePreconnect is called + // before a new stream is established. + if (!ConnectionPool::ConnPoolImplBase::shouldConnect( + state.pending_streams_, state.active_streams_, state.connecting_stream_capacity_, + peekahead_ratio, true)) { + return; + } ConnectionPool::Instance* preconnect_pool = pick_preconnect_pool(); - if (preconnect_pool) { - preconnect_pool->maybePreconnect(cluster_entry.cluster_info_->peekaheadRatio()); + if (!preconnect_pool || !preconnect_pool->maybePreconnect(peekahead_ratio)) { + // Given that the next preconnect pick may be entirely different, we could + // opt to try again even if the first preconnect fails. Err on the side of + // caution and wait for the next attempt. + return; } } } @@ -882,10 +889,9 @@ ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::httpConnPool( // performed here in anticipation of the new stream. // TODO(alyssawilk) refactor to have one function call and return a pair, so this invariant is // code-enforced. - maybePreconnect(*this, [this, &priority, &protocol, &context]() { + maybePreconnect(*this, parent_.cluster_manager_state_, [this, &priority, &protocol, &context]() { return connPool(priority, protocol, context, true); }); - return ret; } @@ -901,7 +907,7 @@ ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::tcpConnPool( // TODO(alyssawilk) refactor to have one function call and return a pair, so this invariant is // code-enforced. // Now see if another host should be preconnected. - maybePreconnect(*this, + maybePreconnect(*this, parent_.cluster_manager_state_, [this, &priority, &context]() { return tcpConnPool(priority, context, true); }); return ret; @@ -1246,7 +1252,7 @@ void ClusterManagerImpl::ThreadLocalClusterManagerImpl::onHostHealthFailure( if (host->cluster().features() & ClusterInfo::Features::CLOSE_CONNECTIONS_ON_HOST_HEALTH_FAILURE) { - // Close non connection pool TCP connections obtained from tcpConnForCluster() + // Close non connection pool TCP connections obtained from tcpConn() // // TODO(jono): The only remaining user of the non-pooled connections seems to be the statsd // TCP client. Perhaps it could be rewritten to use a connection pool, and this code deleted. @@ -1357,8 +1363,10 @@ ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::connPool( LoadBalancerContext* context, bool peek) { HostConstSharedPtr host = (peek ? lb_->peekAnotherHost(context) : lb_->chooseHost(context)); if (!host) { - ENVOY_LOG(debug, "no healthy host for HTTP connection pool"); - cluster_info_->stats().upstream_cx_none_healthy_.inc(); + if (!peek) { + ENVOY_LOG(debug, "no healthy host for HTTP connection pool"); + cluster_info_->stats().upstream_cx_none_healthy_.inc(); + } return nullptr; } @@ -1426,8 +1434,10 @@ ClusterManagerImpl::ThreadLocalClusterManagerImpl::ClusterEntry::tcpConnPool( ResourcePriority priority, LoadBalancerContext* context, bool peek) { HostConstSharedPtr host = (peek ? lb_->peekAnotherHost(context) : lb_->chooseHost(context)); if (!host) { - ENVOY_LOG(debug, "no healthy host for TCP connection pool"); - cluster_info_->stats().upstream_cx_none_healthy_.inc(); + if (!peek) { + ENVOY_LOG(debug, "no healthy host for TCP connection pool"); + cluster_info_->stats().upstream_cx_none_healthy_.inc(); + } return nullptr; } diff --git a/source/common/upstream/cluster_manager_impl.h b/source/common/upstream/cluster_manager_impl.h index 3cd527ec55c1..98d04ac2d734 100644 --- a/source/common/upstream/cluster_manager_impl.h +++ b/source/common/upstream/cluster_manager_impl.h @@ -563,6 +563,7 @@ class ClusterManagerImpl : public ClusterManager, Logger::Loggable preconnect_pool); ClusterManagerFactory& factory_; diff --git a/source/common/upstream/eds.cc b/source/common/upstream/eds.cc index 15d2409f4d09..b780c8c80c22 100644 --- a/source/common/upstream/eds.cc +++ b/source/common/upstream/eds.cc @@ -41,13 +41,14 @@ EdsClusterImpl::EdsClusterImpl( subscription_ = factory_context.clusterManager().subscriptionFactory().subscriptionFromConfigSource( eds_config, Grpc::Common::typeUrl(resource_name), info_->statsScope(), *this, - resource_decoder_); + resource_decoder_, false); } void EdsClusterImpl::startPreInit() { subscription_->start({cluster_name_}); } void EdsClusterImpl::BatchUpdateHelper::batchUpdate(PrioritySet::HostUpdateCb& host_update_cb) { - absl::node_hash_map updated_hosts; + absl::flat_hash_map updated_hosts; + absl::flat_hash_set all_new_hosts; PriorityStateManager priority_state_manager(parent_, parent_.local_info_, &host_update_cb); for (const auto& locality_lb_endpoint : cluster_load_assignment_.endpoints()) { parent_.validateEndpointsForZoneAwareRouting(locality_lb_endpoint); @@ -55,10 +56,11 @@ void EdsClusterImpl::BatchUpdateHelper::batchUpdate(PrioritySet::HostUpdateCb& h priority_state_manager.initializePriorityFor(locality_lb_endpoint); for (const auto& lb_endpoint : locality_lb_endpoint.lb_endpoints()) { - priority_state_manager.registerHostForPriority( - lb_endpoint.endpoint().hostname(), - parent_.resolveProtoAddress(lb_endpoint.endpoint().address()), locality_lb_endpoint, - lb_endpoint, parent_.time_source_); + auto address = parent_.resolveProtoAddress(lb_endpoint.endpoint().address()); + priority_state_manager.registerHostForPriority(lb_endpoint.endpoint().hostname(), address, + locality_lb_endpoint, lb_endpoint, + parent_.time_source_); + all_new_hosts.emplace(address->asString()); } } @@ -79,13 +81,13 @@ void EdsClusterImpl::BatchUpdateHelper::batchUpdate(PrioritySet::HostUpdateCb& h if (priority_state[i].first != nullptr) { cluster_rebuilt |= parent_.updateHostsPerLocality( i, overprovisioning_factor, *priority_state[i].first, parent_.locality_weights_map_[i], - priority_state[i].second, priority_state_manager, updated_hosts); + priority_state[i].second, priority_state_manager, updated_hosts, all_new_hosts); } else { // If the new update contains a priority with no hosts, call the update function with an empty // set of hosts. cluster_rebuilt |= parent_.updateHostsPerLocality( i, overprovisioning_factor, {}, parent_.locality_weights_map_[i], empty_locality_map, - priority_state_manager, updated_hosts); + priority_state_manager, updated_hosts, all_new_hosts); } } @@ -98,7 +100,7 @@ void EdsClusterImpl::BatchUpdateHelper::batchUpdate(PrioritySet::HostUpdateCb& h } cluster_rebuilt |= parent_.updateHostsPerLocality( i, overprovisioning_factor, {}, parent_.locality_weights_map_[i], empty_locality_map, - priority_state_manager, updated_hosts); + priority_state_manager, updated_hosts, all_new_hosts); } parent_.all_hosts_ = std::move(updated_hosts); @@ -236,7 +238,8 @@ bool EdsClusterImpl::updateHostsPerLocality( const uint32_t priority, const uint32_t overprovisioning_factor, const HostVector& new_hosts, LocalityWeightsMap& locality_weights_map, LocalityWeightsMap& new_locality_weights_map, PriorityStateManager& priority_state_manager, - absl::node_hash_map& updated_hosts) { + absl::flat_hash_map& updated_hosts, + const absl::flat_hash_set& all_new_hosts) { const auto& host_set = priority_set_.getOrCreateHostSet(priority, overprovisioning_factor); HostVectorSharedPtr current_hosts_copy(new HostVector(host_set.hosts())); @@ -247,14 +250,14 @@ bool EdsClusterImpl::updateHostsPerLocality( // is responsible for both determining whether there was a change and to perform the actual update // to current_hosts_copy, so it must be called even if we know that we need to update (e.g. if the // overprovisioning factor changes). - // TODO(htuch): We eagerly update all the host sets here on weight changes, which isn't great, - // since this has the knock on effect that we rebuild the load balancers and locality scheduler. - // We could make this happen lazily, as we do for host-level weight updates, where as things age - // out of the locality scheduler, we discover their new weights. We don't currently have a shared - // object for locality weights that we can update here, we should add something like this to - // improve performance and scalability of locality weight updates. - const bool hosts_updated = updateDynamicHostList(new_hosts, *current_hosts_copy, hosts_added, - hosts_removed, updated_hosts, all_hosts_); + // + // TODO(htuch): We eagerly update all the host sets here on weight changes, which may have + // performance implications, since this has the knock on effect that we rebuild the load balancers + // and locality scheduler. See the comment in BaseDynamicClusterImpl::updateDynamicHostList + // about this. In the future we may need to do better here. + const bool hosts_updated = + updateDynamicHostList(new_hosts, *current_hosts_copy, hosts_added, hosts_removed, + updated_hosts, all_hosts_, all_new_hosts); if (hosts_updated || host_set.overprovisioningFactor() != overprovisioning_factor || locality_weights_map != new_locality_weights_map) { ASSERT(std::all_of(current_hosts_copy->begin(), current_hosts_copy->end(), diff --git a/source/common/upstream/eds.h b/source/common/upstream/eds.h index 4ab24c38788a..224f0984e420 100644 --- a/source/common/upstream/eds.h +++ b/source/common/upstream/eds.h @@ -52,8 +52,8 @@ class EdsClusterImpl bool updateHostsPerLocality(const uint32_t priority, const uint32_t overprovisioning_factor, const HostVector& new_hosts, LocalityWeightsMap& locality_weights_map, LocalityWeightsMap& new_locality_weights_map, - PriorityStateManager& priority_state_manager, - absl::node_hash_map& updated_hosts); + PriorityStateManager& priority_state_manager, HostMap& updated_hosts, + const absl::flat_hash_set& all_new_hosts); bool validateUpdateSize(int num_resources); // ClusterImplBase diff --git a/source/common/upstream/health_checker_impl.cc b/source/common/upstream/health_checker_impl.cc index 65c97b032cb9..be41d9064d16 100644 --- a/source/common/upstream/health_checker_impl.cc +++ b/source/common/upstream/health_checker_impl.cc @@ -260,11 +260,14 @@ void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onInterval() { parent_.transportSocketMatchMetadata().get()); client_.reset(parent_.createCodecClient(conn)); client_->addConnectionCallbacks(connection_callback_impl_); + client_->setCodecConnectionCallbacks(http_connection_callback_impl_); expect_reset_ = false; + reuse_connection_ = parent_.reuse_connection_; } Http::RequestEncoder* request_encoder = &client_->newStream(*this); request_encoder->getStream().addCallbacks(*this); + request_in_flight_ = true; const auto request_headers = Http::createHeaderMap( {{Http::Headers::get().Method, "GET"}, @@ -284,15 +287,51 @@ void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onInterval() { void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onResetStream(Http::StreamResetReason, absl::string_view) { + request_in_flight_ = false; + ENVOY_CONN_LOG(debug, "connection/stream error health_flags={}", *client_, + HostUtility::healthFlagsToString(*host_)); if (expect_reset_) { return; } - ENVOY_CONN_LOG(debug, "connection/stream error health_flags={}", *client_, - HostUtility::healthFlagsToString(*host_)); + if (client_ && !reuse_connection_) { + client_->close(); + } + handleFailure(envoy::data::core::v3::NETWORK); } +void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onGoAway( + Http::GoAwayErrorCode error_code) { + ENVOY_CONN_LOG(debug, "connection going away goaway_code={}, health_flags={}", *client_, + error_code, HostUtility::healthFlagsToString(*host_)); + + // Runtime guard around graceful handling of NO_ERROR GOAWAY handling. The old behavior is to + // ignore GOAWAY completely. + if (!parent_.runtime_.snapshot().runtimeFeatureEnabled( + "envoy.reloadable_features.health_check.graceful_goaway_handling")) { + return; + } + + if (request_in_flight_ && error_code == Http::GoAwayErrorCode::NoError) { + // The server is starting a graceful shutdown. Allow the in flight request + // to finish without treating this as a health check error, and then + // reconnect. + reuse_connection_ = false; + return; + } + + if (request_in_flight_) { + // Record this as a failed health check. + handleFailure(envoy::data::core::v3::NETWORK); + } + + if (client_) { + expect_reset_ = true; + client_->close(); + } +} + HttpHealthCheckerImpl::HttpActiveHealthCheckSession::HealthCheckResult HttpHealthCheckerImpl::HttpActiveHealthCheckSession::healthCheckResult() { uint64_t response_code = Http::Utility::getResponseStatus(*response_headers_); @@ -323,6 +362,8 @@ HttpHealthCheckerImpl::HttpActiveHealthCheckSession::healthCheckResult() { } void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onResponseComplete() { + request_in_flight_ = false; + switch (healthCheckResult()) { case HealthCheckResult::Succeeded: handleSuccess(false); @@ -350,36 +391,15 @@ bool HttpHealthCheckerImpl::HttpActiveHealthCheckSession::shouldClose() const { return false; } - if (!parent_.reuse_connection_) { + if (!reuse_connection_) { return true; } - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.fixed_connection_close")) { - return Http::HeaderUtility::shouldCloseConnection(client_->protocol(), *response_headers_); - } - - if (response_headers_->Connection()) { - const bool close = - absl::EqualsIgnoreCase(response_headers_->Connection()->value().getStringView(), - Http::Headers::get().ConnectionValues.Close); - if (close) { - return true; - } - } - - if (response_headers_->ProxyConnection() && protocol_ < Http::Protocol::Http2) { - const bool close = - absl::EqualsIgnoreCase(response_headers_->ProxyConnection()->value().getStringView(), - Http::Headers::get().ConnectionValues.Close); - if (close) { - return true; - } - } - - return false; + return Http::HeaderUtility::shouldCloseConnection(client_->protocol(), *response_headers_); } void HttpHealthCheckerImpl::HttpActiveHealthCheckSession::onTimeout() { + request_in_flight_ = false; if (client_) { host_->setActiveHealthFailureType(Host::ActiveHealthFailureType::TIMEOUT); ENVOY_CONN_LOG(debug, "connection/stream timeout health_flags={}", *client_, @@ -526,7 +546,9 @@ void TcpHealthCheckerImpl::TcpActiveHealthCheckSession::onInterval() { expect_close_ = false; client_->connect(); - client_->noDelay(true); + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) { + client_->noDelay(true); + } } if (!parent_.send_bytes_.empty()) { diff --git a/source/common/upstream/health_checker_impl.h b/source/common/upstream/health_checker_impl.h index 8066cc9e9b8f..3230866c306e 100644 --- a/source/common/upstream/health_checker_impl.h +++ b/source/common/upstream/health_checker_impl.h @@ -106,6 +106,7 @@ class HttpHealthCheckerImpl : public HealthCheckerImplBase { void onBelowWriteBufferLowWatermark() override {} void onEvent(Network::ConnectionEvent event); + void onGoAway(Http::GoAwayErrorCode error_code); class ConnectionCallbackImpl : public Network::ConnectionCallbacks { public: @@ -119,7 +120,18 @@ class HttpHealthCheckerImpl : public HealthCheckerImplBase { HttpActiveHealthCheckSession& parent_; }; + class HttpConnectionCallbackImpl : public Http::ConnectionCallbacks { + public: + HttpConnectionCallbackImpl(HttpActiveHealthCheckSession& parent) : parent_(parent) {} + // Http::ConnectionCallbacks + void onGoAway(Http::GoAwayErrorCode error_code) override { parent_.onGoAway(error_code); } + + private: + HttpActiveHealthCheckSession& parent_; + }; + ConnectionCallbackImpl connection_callback_impl_{*this}; + HttpConnectionCallbackImpl http_connection_callback_impl_{*this}; HttpHealthCheckerImpl& parent_; Http::CodecClientPtr client_; Http::ResponseHeaderMapPtr response_headers_; @@ -127,6 +139,8 @@ class HttpHealthCheckerImpl : public HealthCheckerImplBase { const Http::Protocol protocol_; Network::SocketAddressProviderSharedPtr local_address_provider_; bool expect_reset_{}; + bool reuse_connection_ = false; + bool request_in_flight_ = false; }; using HttpActiveHealthCheckSessionPtr = std::unique_ptr; diff --git a/source/common/upstream/load_balancer_impl.cc b/source/common/upstream/load_balancer_impl.cc index 1297da36f163..9e87d2edbfa2 100644 --- a/source/common/upstream/load_balancer_impl.cc +++ b/source/common/upstream/load_balancer_impl.cc @@ -22,6 +22,12 @@ static const std::string RuntimeZoneEnabled = "upstream.zone_routing.enabled"; static const std::string RuntimeMinClusterSize = "upstream.zone_routing.min_cluster_size"; static const std::string RuntimePanicThreshold = "upstream.healthy_panic_threshold"; +bool tooManyPreconnects(size_t num_preconnect_picks, uint32_t healthy_hosts) { + // Currently we only allow the number of preconnected connections to equal the + // number of healthy hosts. + return num_preconnect_picks >= healthy_hosts; +} + // Distributes load between priorities based on the per priority availability and the normalized // total availability. Load is assigned to each priority according to how available each priority is // adjusted for the normalized total availability. @@ -108,16 +114,16 @@ LoadBalancerBase::LoadBalancerBase( priority_set_(priority_set) { for (auto& host_set : priority_set_.hostSetsPerPriority()) { recalculatePerPriorityState(host_set->priority(), priority_set_, per_priority_load_, - per_priority_health_, per_priority_degraded_); + per_priority_health_, per_priority_degraded_, total_healthy_hosts_); } // Recalculate panic mode for all levels. recalculatePerPriorityPanic(); - priority_set_.addPriorityUpdateCb( - [this](uint32_t priority, const HostVector&, const HostVector&) -> void { - recalculatePerPriorityState(priority, priority_set_, per_priority_load_, - per_priority_health_, per_priority_degraded_); - }); + priority_set_.addPriorityUpdateCb([this](uint32_t priority, const HostVector&, + const HostVector&) -> void { + recalculatePerPriorityState(priority, priority_set_, per_priority_load_, per_priority_health_, + per_priority_degraded_, total_healthy_hosts_); + }); priority_set_.addPriorityUpdateCb( [this](uint32_t priority, const HostVector&, const HostVector&) -> void { @@ -146,11 +152,13 @@ void LoadBalancerBase::recalculatePerPriorityState(uint32_t priority, const PrioritySet& priority_set, HealthyAndDegradedLoad& per_priority_load, HealthyAvailability& per_priority_health, - DegradedAvailability& per_priority_degraded) { + DegradedAvailability& per_priority_degraded, + uint32_t& total_healthy_hosts) { per_priority_load.healthy_priority_load_.get().resize(priority_set.hostSetsPerPriority().size()); per_priority_load.degraded_priority_load_.get().resize(priority_set.hostSetsPerPriority().size()); per_priority_health.get().resize(priority_set.hostSetsPerPriority().size()); per_priority_degraded.get().resize(priority_set.hostSetsPerPriority().size()); + total_healthy_hosts = 0; // Determine the health of the newly modified priority level. // Health ranges from 0-100, and is the ratio of healthy/degraded hosts to total hosts, modified @@ -232,6 +240,10 @@ void LoadBalancerBase::recalculatePerPriorityState(uint32_t priority, per_priority_load.healthy_priority_load_.get().end(), 0) + std::accumulate(per_priority_load.degraded_priority_load_.get().begin(), per_priority_load.degraded_priority_load_.get().end(), 0)); + + for (auto& host_set : priority_set.hostSetsPerPriority()) { + total_healthy_hosts += host_set->healthyHosts().size(); + } } // Method iterates through priority levels and turns on/off panic mode. @@ -774,6 +786,10 @@ void EdfLoadBalancerBase::refresh(uint32_t priority) { } HostConstSharedPtr EdfLoadBalancerBase::peekAnotherHost(LoadBalancerContext* context) { + if (tooManyPreconnects(stashed_random_.size(), total_healthy_hosts_)) { + return nullptr; + } + const absl::optional hosts_source = hostSourceToUse(context, random(true)); if (!hosts_source) { return nullptr; @@ -859,6 +875,9 @@ HostConstSharedPtr LeastRequestLoadBalancer::unweightedHostPick(const HostVector } HostConstSharedPtr RandomLoadBalancer::peekAnotherHost(LoadBalancerContext* context) { + if (tooManyPreconnects(stashed_random_.size(), total_healthy_hosts_)) { + return nullptr; + } return peekOrChoose(context, true); } diff --git a/source/common/upstream/load_balancer_impl.h b/source/common/upstream/load_balancer_impl.h index fab367067255..add54d9e9dac 100644 --- a/source/common/upstream/load_balancer_impl.h +++ b/source/common/upstream/load_balancer_impl.h @@ -121,7 +121,8 @@ class LoadBalancerBase : public LoadBalancer { void static recalculatePerPriorityState(uint32_t priority, const PrioritySet& priority_set, HealthyAndDegradedLoad& priority_load, HealthyAvailability& per_priority_health, - DegradedAvailability& per_priority_degraded); + DegradedAvailability& per_priority_degraded, + uint32_t& total_healthy_hosts); void recalculatePerPriorityPanic(); protected: @@ -154,6 +155,8 @@ class LoadBalancerBase : public LoadBalancer { DegradedAvailability per_priority_degraded_; // Levels which are in panic std::vector per_priority_panic_; + // The total count of healthy hosts across all priority levels. + uint32_t total_healthy_hosts_; }; class LoadBalancerContextBase : public LoadBalancerContext { diff --git a/source/common/upstream/load_stats_reporter.cc b/source/common/upstream/load_stats_reporter.cc index b7ff8e94b7c3..9edf4e318a27 100644 --- a/source/common/upstream/load_stats_reporter.cc +++ b/source/common/upstream/load_stats_reporter.cc @@ -152,36 +152,38 @@ void LoadStatsReporter::startLoadReportPeriod() { // problems due to referencing of temporaries in the below loop with Google's // internal string type. Consider this optimization when the string types // converge. + const ClusterManager::ClusterInfoMaps all_clusters = cm_.clusters(); absl::node_hash_map existing_clusters; if (message_->send_all_clusters()) { - auto cluster_info_map = cm_.clusters(); - for (const auto& p : cluster_info_map.active_clusters_) { + for (const auto& p : all_clusters.active_clusters_) { const std::string& cluster_name = p.first; - if (clusters_.count(cluster_name) > 0) { - existing_clusters.emplace(cluster_name, clusters_[cluster_name]); + auto it = clusters_.find(cluster_name); + if (it != clusters_.end()) { + existing_clusters.emplace(cluster_name, it->second); } } } else { for (const std::string& cluster_name : message_->clusters()) { - if (clusters_.count(cluster_name) > 0) { - existing_clusters.emplace(cluster_name, clusters_[cluster_name]); + auto it = clusters_.find(cluster_name); + if (it != clusters_.end()) { + existing_clusters.emplace(cluster_name, it->second); } } } clusters_.clear(); // Reset stats for all hosts in clusters we are tracking. - auto handle_cluster_func = [this, &existing_clusters](const std::string& cluster_name) { - clusters_.emplace(cluster_name, existing_clusters.count(cluster_name) > 0 - ? existing_clusters[cluster_name] + auto handle_cluster_func = [this, &existing_clusters, + &all_clusters](const std::string& cluster_name) { + auto existing_cluster_it = existing_clusters.find(cluster_name); + clusters_.emplace(cluster_name, existing_cluster_it != existing_clusters.end() + ? existing_cluster_it->second : time_source_.monotonicTime().time_since_epoch()); - // TODO(lambdai): Move the clusters() call out of this lambda. - auto cluster_info_map = cm_.clusters(); - auto it = cluster_info_map.active_clusters_.find(cluster_name); - if (it == cluster_info_map.active_clusters_.end()) { + auto it = all_clusters.active_clusters_.find(cluster_name); + if (it == all_clusters.active_clusters_.end()) { return; } // Don't reset stats for existing tracked clusters. - if (existing_clusters.count(cluster_name) > 0) { + if (existing_cluster_it != existing_clusters.end()) { return; } auto& cluster = it->second.get(); @@ -195,8 +197,7 @@ void LoadStatsReporter::startLoadReportPeriod() { cluster.info()->loadReportStats().upstream_rq_dropped_.latch(); }; if (message_->send_all_clusters()) { - auto cluster_info_map = cm_.clusters(); - for (const auto& p : cluster_info_map.active_clusters_) { + for (const auto& p : all_clusters.active_clusters_) { const std::string& cluster_name = p.first; handle_cluster_func(cluster_name); } diff --git a/source/common/upstream/strict_dns_cluster.cc b/source/common/upstream/strict_dns_cluster.cc index 70f1dd37e00b..a7f528b18f6b 100644 --- a/source/common/upstream/strict_dns_cluster.cc +++ b/source/common/upstream/strict_dns_cluster.cc @@ -118,9 +118,10 @@ void StrictDnsClusterImpl::ResolveTarget::startResolve() { if (status == Network::DnsResolver::ResolutionStatus::Success) { parent_.info_->stats().update_success_.inc(); - absl::node_hash_map updated_hosts; + HostMap updated_hosts; HostVector new_hosts; std::chrono::seconds ttl_refresh_rate = std::chrono::seconds::max(); + absl::flat_hash_set all_new_hosts; for (const auto& resp : response) { // TODO(mattklein123): Currently the DNS interface does not consider port. We need to // make a new address that has port in it. We need to both support IPv6 as well as @@ -135,14 +136,14 @@ void StrictDnsClusterImpl::ResolveTarget::startResolve() { lb_endpoint_.load_balancing_weight().value(), locality_lb_endpoint_.locality(), lb_endpoint_.endpoint().health_check_config(), locality_lb_endpoint_.priority(), lb_endpoint_.health_status(), parent_.time_source_)); - + all_new_hosts.emplace(new_hosts.back()->address()->asString()); ttl_refresh_rate = min(ttl_refresh_rate, resp.ttl_); } HostVector hosts_added; HostVector hosts_removed; if (parent_.updateDynamicHostList(new_hosts, hosts_, hosts_added, hosts_removed, - updated_hosts, all_hosts_)) { + updated_hosts, all_hosts_, all_new_hosts)) { ENVOY_LOG(debug, "DNS hosts have changed for {}", dns_address_); ASSERT(std::all_of(hosts_.begin(), hosts_.end(), [&](const auto& host) { return host->priority() == locality_lb_endpoint_.priority(); diff --git a/source/common/upstream/upstream_impl.cc b/source/common/upstream/upstream_impl.cc index 18025dfca211..c4badf0f937a 100644 --- a/source/common/upstream/upstream_impl.cc +++ b/source/common/upstream/upstream_impl.cc @@ -728,7 +728,7 @@ ClusterInfoImpl::ClusterInfoImpl( config, *stats_scope_, factory_context.clusterManager()) : nullptr), features_(ClusterInfoImpl::HttpProtocolOptionsConfigImpl::parseFeatures( - config, http_protocol_options_)), + config, *http_protocol_options_)), resource_managers_(config, runtime, name_, *stats_scope_, factory_context.clusterManager().clusterCircuitBreakersStatNames()), maintenance_mode_runtime_key_(absl::StrCat("upstream.maintenance_mode.", name_)), @@ -1384,12 +1384,11 @@ void PriorityStateManager::updateClusterPrioritySet( } } -bool BaseDynamicClusterImpl::updateDynamicHostList(const HostVector& new_hosts, - HostVector& current_priority_hosts, - HostVector& hosts_added_to_current_priority, - HostVector& hosts_removed_from_current_priority, - HostMap& updated_hosts, - const HostMap& all_hosts) { +bool BaseDynamicClusterImpl::updateDynamicHostList( + const HostVector& new_hosts, HostVector& current_priority_hosts, + HostVector& hosts_added_to_current_priority, HostVector& hosts_removed_from_current_priority, + HostMap& updated_hosts, const HostMap& all_hosts, + const absl::flat_hash_set& all_new_hosts) { uint64_t max_host_weight = 1; // Did hosts change? @@ -1414,8 +1413,10 @@ bool BaseDynamicClusterImpl::updateDynamicHostList(const HostVector& new_hosts, // do the same thing. // Keep track of hosts we see in new_hosts that we are able to match up with an existing host. - absl::node_hash_set existing_hosts_for_current_priority( + absl::flat_hash_set existing_hosts_for_current_priority( current_priority_hosts.size()); + // Keep track of hosts we're adding (or replacing) + absl::flat_hash_set new_hosts_for_current_priority(new_hosts.size()); HostVector final_hosts; for (const HostSharedPtr& host : new_hosts) { if (updated_hosts.count(host->address()->asString())) { @@ -1450,6 +1451,17 @@ bool BaseDynamicClusterImpl::updateDynamicHostList(const HostVector& new_hosts, if (host->weight() > max_host_weight) { max_host_weight = host->weight(); } + if (existing_host->second->weight() != host->weight()) { + existing_host->second->weight(host->weight()); + if (Runtime::runtimeFeatureEnabled( + "envoy.reloadable_features.upstream_host_weight_change_causes_rebuild")) { + // We do full host set rebuilds so that load balancers can do pre-computation of data + // structures based on host weight. This may become a performance problem in certain + // deployments so it is runtime feature guarded and may also need to be configurable + // and/or dynamic in the future. + hosts_changed = true; + } + } hosts_changed |= updateHealthFlag(*host, *existing_host->second, Host::HealthFlag::FAILED_EDS_HEALTH); @@ -1485,10 +1497,10 @@ bool BaseDynamicClusterImpl::updateDynamicHostList(const HostVector& new_hosts, hosts_added_to_current_priority.emplace_back(existing_host->second); } - existing_host->second->weight(host->weight()); final_hosts.push_back(existing_host->second); updated_hosts[existing_host->second->address()->asString()] = existing_host->second; } else { + new_hosts_for_current_priority.emplace(host->address()->asString()); if (host->weight() > max_host_weight) { max_host_weight = host->weight(); } @@ -1545,7 +1557,18 @@ bool BaseDynamicClusterImpl::updateDynamicHostList(const HostVector& new_hosts, if (!current_priority_hosts.empty() && dont_remove_healthy_hosts) { erase_from = std::remove_if(current_priority_hosts.begin(), current_priority_hosts.end(), - [&updated_hosts, &final_hosts, &max_host_weight](const HostSharedPtr& p) { + [&all_new_hosts, &new_hosts_for_current_priority, &updated_hosts, + &final_hosts, &max_host_weight](const HostSharedPtr& p) { + if (all_new_hosts.contains(p->address()->asString()) && + !new_hosts_for_current_priority.contains(p->address()->asString())) { + // If the address is being completely deleted from this priority, but is + // referenced from another priority, then we assume that the other + // priority will perform an in-place update to re-use the existing Host. + // We should therefore not mark it as PENDING_DYNAMIC_REMOVAL, but + // instead remove it immediately from this priority. + return false; + } + if (!(p->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC) || p->healthFlagGet(Host::HealthFlag::FAILED_EDS_HEALTH))) { if (p->weight() > max_host_weight) { diff --git a/source/common/upstream/upstream_impl.h b/source/common/upstream/upstream_impl.h index 3ff38c4b770d..1c9fb7fa9961 100644 --- a/source/common/upstream/upstream_impl.h +++ b/source/common/upstream/upstream_impl.h @@ -913,13 +913,15 @@ class BaseDynamicClusterImpl : public ClusterImplBase { * priority. * @param updated_hosts is used to aggregate the new state of all hosts across priority, and will * be updated with the hosts that remain in this priority after the update. - * @param all_hosts all known hosts prior to this host update. + * @param all_hosts all known hosts prior to this host update across all priorities. + * @param all_new_hosts addresses of all hosts in the new configuration across all priorities. * @return whether the hosts for the priority changed. */ bool updateDynamicHostList(const HostVector& new_hosts, HostVector& current_priority_hosts, HostVector& hosts_added_to_current_priority, HostVector& hosts_removed_from_current_priority, - HostMap& updated_hosts, const HostMap& all_hosts); + HostMap& updated_hosts, const HostMap& all_hosts, + const absl::flat_hash_set& all_new_hosts); }; /** diff --git a/source/common/version/version.cc b/source/common/version/version.cc index d2ddbae3c818..8b013dde01cb 100644 --- a/source/common/version/version.cc +++ b/source/common/version/version.cc @@ -36,6 +36,14 @@ const envoy::config::core::v3::BuildVersion& VersionInfo::buildVersion() { return *result; } +bool VersionInfo::sslFipsCompliant() { + bool fipsCompliant = false; +#ifdef BORINGSSL_FIPS + fipsCompliant = true; +#endif + return fipsCompliant; +} + const std::string& VersionInfo::buildType() { #ifdef NDEBUG static const std::string release_type = "RELEASE"; diff --git a/source/common/version/version.h b/source/common/version/version.h index a5720105ef85..345ef2714c06 100644 --- a/source/common/version/version.h +++ b/source/common/version/version.h @@ -22,6 +22,8 @@ class VersionInfo { static const std::string& revisionStatus(); // Repository information and build type. static const std::string& version(); + // FIPS Compliance of envoy build + static bool sslFipsCompliant(); static const envoy::config::core::v3::BuildVersion& buildVersion(); diff --git a/source/docs/aclick.png b/source/docs/aclick.png new file mode 100644 index 0000000000000000000000000000000000000000..04fab49d633e68a45347e450094e5748e09aab93 GIT binary patch literal 70109 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 z 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 literal 0 HcmV?d00001 diff --git a/source/docs/click.png b/source/docs/click.png new file mode 100644 index 0000000000000000000000000000000000000000..c4dd6d3d5f03f12b57d06e6ca1a045db46c424ac GIT binary patch literal 120611 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

6cV~v! z&Q-$FDHGvt@vhN*(&s!DO3JzF16lKWZbv!YYTz z^@y$9&K1gkNCiPdL=% zF3)a?C~jBWd|rK6r!gDDyW=K2BZ1EpX9QA@lM;V}^7>+bmhxJa1I1$1+yJ<-2PSDC zfed}x^@@TL0+g;N`&O=>5*8IUIUK3hA8H!bs^?3SYdq(5p20iA`O3aRZ*rfJw!NlX z{eG#eRwYgo)r624f^pMc7M%_jXGmB(vex-pVA<@`(h4;yD7MTO3J}a?jSL2>dhXU* z>($@Md6ama8QuuYZ5W{fv3SgaS4nz)h6{IB^&=A`sks)%ONuPdZCKnbf2_X&lW$Lx z^2c0n(_riPaJ2i^P(HK>&v1Ms6-)nvRsFtjdS zGCU@326*+QYV#DoHS#A^?FP(zueoW<3yY)*rw?B;Bs4(R?2{k1^UX}5#ZsJ6#C%NG z2X4P`Dyy#rKVmh;pGGH9sa@Ze>l|%Z+$1z4qB@s#1(mXyP3YfNx$xD>xi!3YS=~Gu zX7JFVpByss>l3}Um515_5Sjh{TpE*<%nluAW-tqky~AUt%KAJ}%IkIp|7BFaRz>^J zB-VqzFcxL@#tWS~n!ZGqkTrjn`&~gK4DuqcAnPP>6|qr}uUG@^e@;f^$CJ?*hZo`Z zkP6%prairpeiRqDE^H;ki5a=tMv(NSqe6$?biGWo=4M)jVvv%?+oiJesagc*sJPTwz?W$bO2T8G zKZ8}D$^Q*b{DPE6U9?bWog)2Ea|e;Z7G?Fq+g4H zHQzN5!5pXd_2LA;1ds{&i3_>0-q~7@7k;CtNm*bL&WT7rn1_t>LtTMi8GoG(c;5 z#m)H|(&@>}ar8sV+t0153!8t%3Tk+i%;SBs^4!zK^fummiSYZ#Y4(8A|3l_J28U9j~MIr68&OA2#bi^N!8yfx)rvIx>1MTh4Yl%&jRg4pJ2K6_rS>IjmYp?aaE#K&in@k(R>2BS4 zjhXw`yJ6zAPH7dQOTc#wwd;9SB?%UbxUauLC*mNBCK*#>eLj*r6%59?m+P{a3ecR5 zg>=dre)^_?-?xEc;91hdrs(z2in_qfXA7Ruq_Ea+c%A**q?C)0eSWd}HF221+rfOJ z(o>1#=H6;aLf~LZ!Qq!*tEttEOGcZSJKdXySA9)VN2AN|%Dk}>wv0OzG4i2x?kbWk z$DY+?kL=dNSG{KvZl>~P;ye$r@d(8p0|Ue1`f|y^*AFmG_{bVV5vq-)C6&DTQrW}_ z_qRcuVe6z>oW4PWN?D-^N*#MsORhIfZoc?AS^UlfBgds&aXdS4uB-bSu?TvM?}`r=$0EM7#S}ffm+TBd5hTS}RQ~ zy8^1n+Vsnt5p#`SL>z{id)k}cERz$7oWLJIUJt^QE zG3|XtAYvcx=GC8&POJ8OrIMa`6(V(%`%6wPPGEPy#iPlaV9VcgDlFfqvM6Vx$(bRv3~tzt=;aS zvy8hShyAjQ^x4~Fsh|$(4w<4p=RI?SV^8%ZTbsOCN5=KzIggw(&UU#o2p5Xa~R0aquLN7nI!hRo{BwRbM|m=-1(l zz8jR@N&d-hbs0AdrP}L?u8z_4Buzi^{gLKh@DH2+`gH)ntbb;XNdn3Vtzc&CjYGHb zpSG#j7YFg%t4h|9DFjL2f%BM=M5#1F&MXHq%jC4$<-y>3WzT3v#=^M z=ojW}vo|~h)Sym%1s9dFMnYa125D}=r*9NwtAlcwaGd0x5L8jV?{rQ$tX4~*;PpGk z^BYVj?AnW11TM1MNB0V89Lsl&7ug};>ewt_q{V;}YJ98j6mx`j;3^b*bINK~6c|#j z(y&*_$@MO44Ek7 zDrLy4Z?1cb)y^QNYHKQO?_NQ|+ePmW85NL7_J#cC?#ta4Ho_%;QmAgZ z-_3i;x>hjFL>@M~D9ZLu=X=}B=9fU?6ob?$vk4ve9aIeL1;BQa*QZ+=yffuy7Oslx z18EW(D=i*JID;(Z*eN=rVGG0{u>@?OFRM4Yxq3c4ke#JZ|^AFh^4l@Vn zMei}a6JCBl)GT?Q46P;^FQM5~!;T>nD2Icd9vk>x;cO^VYBdF|@hC7@t%U~HCdNfr zT2{~q)v6UJAsQKpXyjYlWQfndjo7+uUL9!pr!W-w^;zp7_sgFIJ#s*yXK|L`qqB#e zrTdO-iCx)GP807ls2SOu733FUJwbPoL%GRi^Y^2)Q{alD*61wc zgx9ZC-UhOMSX5Jq^P<-1xIP=s^-OSxrIB~t$?n$_Wl&i=GTUlofR#;A+Is^@HP{?} z+vN8lv{(}jsGuz8jIwC36OA5fx;@kQh6860twK}I7FJ_zAsGhqOx%OSnh=;w*UD`2 zGzc$tClGzT5ViEIeT~@nuO)_Ql zcUF5qy<>8ayhCF$Oz7K0vY4T|g;DXUMMpfUKT75r&ucjAtFk!k28o+GheGVz*=LVO zL)6a2^ZrWbAwvyUCZ6oCSo_S-VpGwv+o8Pj{Fhk*Txu_dwTk8381`Hs8=E}2Ua=6lO z9qmW69Q`K6;CwQ450uZy$62I{lNlumuT6H

O?=|v{6GqR&-TJoGa2tH)mbL)^+Y0$TbWZpO_Vr#d8TJyFkTq`H`OJ@A zXmsYyXf6=mtq+h0h7)O5xn-qMce6lSM@R90+NIkOGemAPi?XK3O!;}!K;G+*@%po@ zZQNPuupm`c)_o2B=N%JBj;4r%LZxdQ@ya+M~Miv6@adtYJPT47LWpXmN%C*9pnyyRg{|_ zJed{Z#P8YPGs}9)>}Z{str4b&Fmt+38h(KX${sw*ePRyEUONuRB*Q#a#IuTNsQ2X0 zzeXgjkWj`oI~f?(r{A`9?LJ%1q1h*4>3f(5;!ZfWJnmG=>9s$3U$oQ@(Lycs6?8s# zCQ}1OcW~jWjSO%|ZdE7NYVFMY8ak%-$)O`{Z&)>5=tWjbT2#6uM;pB^GdUH@t}{W1 zBzM&A;t22BU@Ju27Blj#_Q$F+D)%m7D{1vfVAS>gJPC}uKApY%YVz4TiPdhqBQWG- zfn(x9i|J3Nfi{8_SqV0~Ya{?bRNECg*=c`aMzr24h9&^V2Zqi&^<2>#lN-deyY>z8 z;GiF_R=rxIf7^uC=+Iam(QWiKkT3Bo(dURETC?l@&|>a6iR{tx{Fn?PxwV2hbz|{?KL% z-`OeyR-<)@%-q0b-0NuWG)Yxyx|dW~ila=pwzl6x2=%H&cO2){b*gH~zY_^~@^NeB z4V8GSb&q5oi#bts_@$-=9;mPo5zE zWPQzat$^{@ndo7dUH#6h* zBSZOh#Aj9dvjQ`oxc^=Ra90F=Y>XVaeVp4YKpYf0PiOx~-oI_lhpiAF@(yrT{dt%U zg!LYgC`-8DhDGrUbAoA94;t=%JO*~e|ERn(hN?gToQsY|UE}-Hlb|4%?AF$yZ+}1+ zmmAz1AT|1IEoK#0J@$N*i*ILE(|4r;G1_fd?9lUZzd^K6*V?UvWARN_6vjWl?b}{# zG%;!N9td@2hi?&u%Q|~a{WfqoX=odkeXA)&qVT!%xFX4{>xzZ(udnj7ryM&=VY#~P zWJ!LRi(E;__{Z57$L6rlH1=jsNp7%E1x^aM>B;LOg3B)Q`W;{6b_eYldN*bEvy~ZRw~k);g<8K=F|5+^ZWDR zU3zAPUp#8>mb44x&Y@Yz!O=Q)H84UdJ<>o`@fe=nZBjg9^P&UZ%b!N!4o%abPs$Hd zD@KT^VRTuB!r@L3qIr9;y#>nHU-xi@lOzB+JJU<6%GEF`?;U#e%O#pCE{nqZQg)Q~4Yv7N)!CoP^JDS` zvT9kI_GZ?0Dn44BjWb>H7{YD39<8wN_0g#c%cuUzNS~`ZLRI}~#uZY=*Gk*#j--*e z8EpeUZD2;;K^2vr!*|KJn_GTaL&bR+dn3LI#XbAe?wQN zU5|0&3+*QK`lB>CC7|fna$w$0EMgL0KnA8*?C1If)+@6l=1jkFBz5gZ&BmaB@=u;d zwIk(PgfeGrEd!?{dwB+s5;Hv>+qd3YTlEqd{`SXWmpfLk;E3#n#9IK@d)jL1{#zti zJ%GWHwh$v%22^+RZj@7kS;0jRR02j!p2F|%Ok-Ab(*OZ7@N-UBt z@ojDu9KS-MU|{F#gCXLeHy$+qSW%9S--QvAS)`eb0z-8BxXZ8BTdH z0CRnc1?C~lxn3|4=x?haI2y{p;^|mMuO5aK56$v0kx+CJCIW2vgss3{+ zd$oZF-Ro;~-kGJ2_Vr{eGg+zdFMYf*E3`3}Q2vijEu~LIYF`Gpilz;YD^`~IGvPvO z5fkDs+5upwhwhB})(e#allBuUN*Gd^Ya;PFGaOZy@aT5y#RYACVZH&SWASx1uL$9z2>Yf0@=nOEC7OivRONx zXMrHRA3qQNITb#cD|w|jY&L2Pvq+%T=Xpc)X1Vi~Lj)swn}v3MvpS%QK_!U95j1=# z>TR_FVf>EI!4_YMnokN~sIEn8@I1!)E8CYn_lj0qdzOAQ1P9s}&J-QL!EqT+xeR1} z>e?w3EGX%!TJzDJGReBpB_1;duaFlui_2fZ^IglG^!ASCpx1{$uWWDsSNSS@e+esi9Pqa>jS6Kl2Ljn1dr&F;O~q;FEwU{PprTN9v5CA0}C9rg+?X;kH; z40`7KcLa?J80Q>UG~1K%(diwC-tHkDK`7Xpwp1%?6hflgyN-*Cm_6rlmM z`T{?D;dP$133bK2cc{DWaKopy?|`c4mQ5M5pty}H&kPcBM1vx`)x}s3d3eF z;O1v$=ry=`9DDrjew6FekOno(gwmfW9^ORGrE6E(X8%?D^K+`bu6ZG8xA0l*%K95f zn)xJ6DL9p1AW0WwBv93PTU3B$AMT}yGu^&i0Xf&Ph>+ucHE_JG8HNt3S;dxMT1D`D zzNy)Gd|!-v5wh7fey_^|@(_&>&&1Sc4eKtMg@G2KR*aZ1fCWym*O(B!6X#0kJKXDh zXDItL)$o!d-J$jOS$Bx$erxHObo`2=%f9RJbPvEHIAvDXBVY{({%?~nM(Y-f<^%vK z6v%PZ!N~Z4tBSngviaz8`-hs4OIH^ZeW-7;313_^^?vP1(9n@0??Iu><_+oX5UZIj z>zK-(Lt`aw>)Y8?a5{?ax=Rka$FUXE;s#`=>0vxYeEG?Jn^0U*CnqRc0ASUcC(so( zi)>*axUca1KnFZUUz>exwtw*DAD3`Qs8ngvVoFLuPF6_-buh7zd+{BZ@>fw6l%P#k z2=Ymiv7dTV`&}r~mX7>Tp$%9$zFt?SLLlOmhP&cf5c?5T17I&@CYU$Y(8V<0u+j%_6iQ*$y z!QR0DESzZj&;Vgnla6Nnt;hi0k^;9Y>{Bzl?5k;J{F6Udj`Hez*`1*{^v)GrQFX+cqT5?oyKSgd9~{$EO^)w3(#e8$LQFp8mKV3j5BmQDX*uzv+}nMa}S2O_5Y3BL>4k1?J@&~M!C+py&h=~4IntdMH8_t?wYpTdwsFEn zv`mo}gBmPY>?RK>k#O_Ja|R(fb-bSM=r3TvhLf59H#rFGPx(zd9hFk z;#=bAutn86KA`%C^h%vBR?M4u3qyz_0QKp=;{?HX54{7r!9tsqIjdc$uFHcggfV;w z2b#<59WB4x>;e;p#ro(ejJ?o|WUdH=j%vr8?z-%N+{(FPPh>u?&)-z)D(!yrW2Ip~ zODK<pP7_J>FNV;~u!un4p`>A?8N!wASUko9{_n-}=m&Y!Qb=v4GrsajPFr1$QP^_pe4V zj>ZEuk~i%Qmi3r=5q zZ2x|G{uE&WRT;v6?fMRm2;P4sQ-6jVtO6WR(-?Ejg)n>>kSpz;u`Mr|Iel5*(<=!m zy^nfa8p`Lg^YtjM8#Jwf*=Ic%E$M2pLD0!)L8pCcIUGHHMaV4kbMlj!=aYh8`70pm zd!r_wrgPQFTtuxLzi`oDQD@YxU1;^Xu10N3v3wzc zLpG#(0Kl5rdj9w~lIC#4Qw9N;Gb-gC+sOp1n&sxUE@Q#*=89~~bE_z1+u_bGC4c83T(IL;4d!^PT(=W@=%=M>_ zxwo~o5-zPj;0(-WUCI&A=Dbw}l|3|Hd{$NyZ4PhIB*;;2cG8d1*M{egpzPtMe>4Hd zxSV#RZSFObrZqq67fQx-y`P2~Duz!yMf!?*pC&wiVM7#5ENj+7uMua$aEd@-d!|Fp zyuHl?`_Vp?<95EyZA$Tv+v+TukBZJW*U=0~Ia6St@#$OEygA1rRI8}qHI}@emRcS^ z;o?*2D#MJ1g%;MQ_W{UDHfl*r?W8l`Q9Svy^_>$F*&Ggzf8V@RieJM~Z}aTFcbsP$ zA6bVw9vmM#;gzKN*nHaF${Xm*KTsJ#h`rsXsZ~B#a`Np{Vrl06kSJE0e@W)hd}Ke7 zS$y@cGp=S!iEpCW&vWJ&{2#Y>yFReJ2gx1OdlR4o*C%jl*olyu1qR@v-~Qk60ZDQB z(F!AEhW9-q_IJ>Ljt|!83j|-*|A*wpWkgbf-)4WGnw@qjDS?%_Z>0(#VKG=$nTq&Z ziDF;uza&!IDIBgCmQ3$PU4CbTAk)VG>SS&2{+id(VT5LZ`UjvK6re{Qgy#ogu-6rA zwlp#c-HEj_nCK$Te}Kqac~Vk}kZ=W|$KfZ)sBHfrV@IdzoYwQr804)iWC(i2?5vvd z&AZUK%?VP6i-%7;JnO#@aL5avr|vzZB-Q`?$!=?o@x*nQnVv2KfkLDHVh1C?xa!J5 z@bQBfzi2n+3KzOe;fdC3nF&JohyC`)VC7GKcxQ~@nSXQi-+lZ)A;|M2D`|R(qu{_!#GV`ier%!${ z?wgpCSPElHKC}wL+n@Wo>{c^xt|@-?&yOv=lt-L+oD2#JybNsnyy0_S5N022lV*E; zpVNq@Ao?Zrntn{`acFqQQ~0u_H2D=F3Z`YTLY!xRR3hZF({QC(!Bz2iR?fqaUZjb} z?PDarzL1u=E1ulcjbAH=)Te@Z0<^cABT`&OndlAO#pc1ClE;J2)ym0KYd)=ku@u`I z(z9cXFBSS9v5J{|t?5}@qt*s>+XFAuC!d(GpeIo@4jg(&Ppy1AwEv<&OArnUHdS;!o`P9-_1u2oXYv^m&z2s zEAG16#D`d)jp~~u{J)(yY0TYRNokXCYmavj%5eGJ$`O(?Nb**NpKp57R#xzlM85Z% zd)89Xqv2c5?H)Eh{Hfxb{k}hrBIQn7lj-AwUTwpk29dC%`QXflFUGe#Ysm|2bi0FF zu@dv%m%?t|I$rxbg2sSlZ`#21%bZv$Y@cu-zi~y+-_b@s zenCxzYaG}A09w8q)l$<#UPqW76!udoO|3Oanz!?~nP*)|^Er}X8?@>9u}~XY^4pE+ zlEkx8$Znd3E0Ez>{()&?)HS*V(~g^+-SSgsMYvfn0xZW?R^(*8sfm?nVs}L}>wWqV z3PhCIL9u5v?HMZHFVl$y$jGaAl3uOhzEEJj4~mnQ(IkTzc^ee6gR!h-ko9LZl&=fi z=p75sK+`;uzLbTTt|+FbL%4@>V1O2bu541v`y!&Rs?ER78lXuIr=xS>od@ zBcOI#7`mX7-D=^*B1e@WM4R>s8iJ!hXDMT#Gb*5lMQK(@x&8Z^Fd+H5w^ZCZIqGWD z@)7d!NQX?F^7X!#a~F(E%OxMmmzMa$+Ud6<%}PHo^tN<)Cib0H-%s+Q!@AXfT{Gc#8`&o0lVT{lux$esgd+k%A+X3m6Dp&ZYGK!tEenfu1O#_ublhL=x;5 z?wZdz@ex;9JP<^%@}1#xe}B-1utgGi$P-hnr{=qGd}rGt`*modb+@aBBycBD?m=kQW^wDxB&7{~cSZ?Th7+H6=hoay8}B zu(IKnz##b0@tQaIs`n=)U+QK$JL*xn9QBLRcHnZhTmt0M>`i^7x-_|Ki6zEvF?G41 zhH2Z~38R;0J;P6i9X!^`#I^XKgpVxD_?2PKLNn&;X8g6{b4i#lYJW;`dx_(`3rOP@=uuwMPAUM*YY-ncIkFLrst@n2aFsKFx%SQm z*X+=}*j#*zCgM72+>RtSv#g%MP`UXnBu6}JA4c$E+g!fu-)?ISLum1SeBl_wwGhP_ z2Jj+~Q1ULMNddLOM&F{0=8q3&AKO9#w^)w(=oF%3Q_fBM7_2S!#u-z_jR19@I+SWF zY@aWyP-y|EQ`>5deejX)kCA8XPTohjSguT0um+_9H=86UBV1AzO61wa^;I=io`z}? zr=`79E*YmW8t2xBeP6o6&ispQ$3vIaQQh7IgAfuoqFSs-I?9g=iKzlziqU&yCMsq* zYcx28b8Nu1tz|CXp1lUyXL5XQ%D23*2MHkOuIyx1epEpxON+3Z&PXeMoy^{PXDr>1L z-CE(IwRL2L5y2*)v{Y<_^6%lH>xpxMz>J6KMCFWhOQ<4h)!JA@HS9g8>-YV^qaQ3| z)RusAa_VuD8OdR5U`cIOn0;s)W2`C-DsZc7>xZ}ikPwcXFP`q1g$fV}@A51e+3I0Y zZ9-vq_DS%z4smJq_Dy)1_A$#w0Wou)dWnb_5ruGf*(ruh>F}cb>&%Iv z^cA#>m1PV@?Pm%J%`m4!alM!^q>=UhTe1P^(eI?xF9D_FKz}kz4~n2TLInQDk10Df zA@!HoScN_Ew<@guuRS?VRxo=4RbLiT8=mkN^Glmzg`L)T_MG!+97S1^+Ri^atS4~g zyRD|bChGB6NbM^|=J-@qQabPGaQx|%Li0m*u8oe&xH6>xZj#sh@nXE@-#mW^<}X2~ zEnKBgS#wrnM7DHpkT>H&;+5(TwT$a6w<3jIP7Ha;O%#-D1#ZRj&A)AyWf!w)|LC)| zUU=!PuIS{qj+*In{jelZz%b*+9k~RMfKB@c@4nA-Qm$Dtn9l?Sjp9Iyl&0;3!>wr0 zpF4;K2f(@edmGlRFFyC(()Px}jb1im`s~J`51~wG-%e+<5HQr0p#oL`HQn&%23B03 z1PVP-tg}p*%007nxecz)&-_`UcbQ`k?FzbQIN9^FkRSGXjUA53vSz$ zX7h;C`<(fl87lDGc3_FmxcB92Yy2{|!m~@|1;O``r|)Y}gU{optd{L)tcO7AwPm6z zJvQooQQgm!pR$=9D(tL?5R+<0pK}d%QpuYyhRPx~h~{foqQ_6|;`(9R;Nal2YTXmw zb(sVr9;?_OG z2Mn4Dz9*@6>zyT`!ldv&x)xJTSGZ{O5+Pe8n0OCLTJr6rs;$W?qW>!vAUSO~vwLOZ zpl_ir*%d?#Fv^*_N3@cwzTJy-cFtkXx)GJ$e)Vh`FGeuyQ*Ad^$lA}l&tfwonTT`P zE4v1E;Lhx`{mps=T45)DtDYl?pz%i*XmOb1FwZR`Sf+BY2^lV_OM;NwBV z45dHUcHz$HCru8o?(cVhSx9pIYPl2!&SjJ5DsWIGO9OVKUR_JH{uo6C}jTv3c$0#N?4?pJ2+f;AykS!|2pcO|_@+ zP8*87aP5!wQw81a%Q4TrZyEdOZx{v6cQJ&M;MN>4_0nJ zZx<+#F5bU(7QCoGyqfHr65{ggjY?!ft~zpmoesCttVgq>BFh;8o!P;@kZhkX=zfOF zu}QnD`S|F5Y1O}$-htHh6Dv>C)lY=D4AMVmsq6p~-&{MsF4qe>S5y|=7uK=oL-fLy zwp3`xdw^=>Veuv`GVDwz>gzL3NS)8)#REMSP-n#zOmFiv(2V;?C8-(Xh@fuU7{jqz??82>TsyWwp8TiwP1eosa zE@?O&Na5gEjdi;$1`2YshiwSdUKLJUsU!=`G-5;NO5vzv}&OCEJXO(ZLD_Xjr-NMIu)_q=Ojg8Er+$rujy9tgQ+Aun!M5N zFEdXSch_O3JQV%r^X}+z0iFj-UC@WT3>#f=CJ&**uhq*V<<=5|Ey+>tPf@N7n>XD# zATH;{ES5ghwGdiF_gg8~@&3X&VVi1R4WrnD)m~ZMhuqEC*p4<~)XRH*H>MP*?8p=Z zTe)Ga6qakHf#(ku=@nws!u2IL(RCCGYyuL;8!Sg8_9R4EEr-|HQJHG3*4!a`m8#%Q z`gg$0o>2Kup*TJROnarNbob@{LVceLX9nbWF(N)5ddHu`JCMm;SS^;+6&qK!d6#{$ z*)43nmgv$s_%UHHaju8*#O#M)=$t_m?9!7QPL4XF#IPZV9kZzer)r@T3fw> z59Psu-Gl2K61DSK7u6Tw7uD5XXg061!zI((G|Dj}nN|nuM-0GPe67vtP0IE0r7|lW zFKqBCC-V7rawrIz(StM*n)|EcSKFdDdbA*va~Z;QY;7l$%dM>n3;JOIMNuHczi}t&xS9ZNAgnH==)b zv9KB`3f6GO!}dGxI$R~rZj1IcLis8cW}Uj;SqDVog291S#Oj)AZws5 z&}%n61O%`6-LvT2Tjcn(g*ExKZatEYU&KZ}J^9C6frd1lsFML7$Q69j=aqq9NX6S+XPF{B)BKR zg1ZF|0fM``yG!E`+(U48cWWFPcXxLQ?k@dy_C9Cd=RB|6dq3PeMtxZ|x~gleT5HWY zfAwE;W_ZabzwH0GFixAlEnkv%<{Q7(H4&UA)#2-~d|+jG%ofXhtyA^;SG_UXg8FIe z!8xRt^6}-SYmDtIna}E_1gycl4I2*t)U{Ngc}MJ@0&lgccX=)_Cw4cYPJPU{BLg%61oHdZ>0s%e2kPzfA@Guw$td+#T(|GmJ99vKR*L= z;g9{vr~emd`)>+DiMGGWX#I7!z}5; z_5de4ZoUr(RsWyDi2qG7DtPqQzx;*Vp!B7LG{e&4!5LaEaHt1^A=le2vqg>aN-EI- zGK#4Gsu?(wx&YpvYH10h_%TN(fzA6cOUU05wV!Ohx&l5up5mVqc#q8mDT|YMPv(ct zjO|tPD5Db>o8qs>LhJ&WxR(pR1W`iHHo09{>SB4Boql&$kkYNuFDBQo_kekJ-~o&~ zbZ&09b2l)EOhb?>NDc5*PD;yltsRU+>HkeE5rEJrEa2-44-Yp;s#^7F8qwTHagmMD z#z`b^%d1q2-?HHjOC#Q<^X)@?{m`|oZHNN5pA8|NlvhQE}VLTy# z?f7-jt0w*uZx?I#Q-}GH0h_gGTSI(<^_l7x8%_FFtvYs}3Kl!{hvS=yZ!EXQcU2jr z=Np1FjnUOy&)J32s4RVzKvL(C)c#{2lBb2kvdWT$n)k|E@LD6}TJ2n7l}7${%>qN6 zkYHogWG+Qz0U$ur7+Xz$oLwk4vdtM7@Y}6rCB3W~#;5oLHx)&FL*_dVje6mZcRG?R zB|oU%ens5*j@=t&MNmz-bC{)`2Y-HWX6Q$wpG-o ziI6GjjVX0B&Qp~3A{sz^A%Xm8_gI3Q(*AgFWp-DB6*7z20?AAd)x!%>5 zm6e{hxcpY`2aPnnnz}P{Q1hWYubzC@gb;VV;v@`92E)0YpfeTUneNMTc?5wNzYUFw zcRI$W-_Hd&CX~lTc~08pKi~Bn=(w)74xga3*xgcy}8)fPlk$az(8r4jR52>$25+aP#JxD-wz_hB@e+_^qs+W zbDvxbtCd3-$i1Ue73P;AK1cG4a$N1`!y#%1%69ErkC!{HNoIO6I8};j3wz>AdV3&d zn~;L5I~<5p`4>6`vNY^ZzZ1Sin7W6&4~(>9N>Q!}&sTf{%XS+L#JuB{KO zv0Xq~;gDGSUa~#4?UL}$}?by2jR9uI%qqqqAm2^7|#>dO4cQHwT{07&Wx&}fS9H|JDOzq9G zcML>koFo&HJWp1~qU{iUDU<5$&c@NMj}+c`0ebVz+%CVZYsg;veh9{K+>u@HNc{>* zs`ms3f1_z~Zbp6EgaB9Bp7#E2H>`SviIVA$gHn68cBHe*Z3Pq(ejSd?vV28WUAU^+ zYQYi5!dXgH2e_f}#fbY|RlgxCN(YjgbL2D@CSWg7Fg zm-R0ia^Sm}S#tj_QJFJo(k!-siX-7KlzPJ*YpzM}&cwS#fw65`vC+ z;2z-Qlcn|B5e5V0HtC}vg}~u8Ye?N=c%UCuQMU8ocW-o(7z6ssTKhWa!K$*ISHPyw znIuv7<%;CP*{(7>cJt%OXFg%Uvf1B~+iEl^)mh2LuFW?*Uw%XgWYUflyZ<2gs0}}< z!S|JoXe|yXBF!gTNw98{$I9is?5QPG}G`yYE2zu-z$g~;Zmy>OshnLt=@r90E zHGi*dtP?{NQ)IMMX~y#<$S-(Uz!!ST^i?5uGFp)SZ>r(6r>AEV{NinOx*p6Xnb4Q< zj^wXEayM3?Q_$-qO zuEB$iK75e=wM$M?TC>z3Ii{#BAn&E6M>65P%@44}p<8NPj2_BmAof`84hrb=VpMX$jhahn|9XD5(Mq@^Vy9XZQUm>yIb)|so26^yF%=w?RM z;b74GV?(mnV47}V9%~lX&cB?bayCEYvf8G_Pyw7q(mgjRbTW8p=%k8fFR~##GU+D|2~q+Dzc@zn<_#{#CB^QnGp#wM?!j$@ zgnn&Q_>3;#pyyRHzGh75GZ@|AO_g7k{FsKZCGEAk3oDFW^#T<~%Jh4mgs3n)e_kd*`+-~l?~ZpteF@#?P{74HKJ84xy^VIR>J z#Wyu(A1Y)&3~crSZ_k^-OCCYr53hY)vZ-hW^8r4&Eo(5>p7?<9!qEg=KM9yjEOWYP zbyR?mx#r%xQ2kdv%1oK{nnEQojHq~N6Ypa50Y2(wfCrHf<)PUV)RL0;VzpNqG<@*q z=U1$5lP~3~5_F9E;5CKaQ9nfYtFqPwjzk3&`kHB=h|nxsz^$aH7Ez<+kJXZN=JSmo z$++7Bc?l|%MhSt!oaXdK#se_p_<8t%Y2m}@u+tQm7nk}8)1)lH zra2}z3>tztE|0lkD-z-tQ4@xwmFXR>jaOQ12MaU=eO42g=3;P;#Wmleaowv7=K~t--xkLBw+E$+T1{Ya4 zl74unl5GQGLm)f^&T2ED^2pl>3=g6sDuCq%+zrtJEMPqx-O+rM${YcZ#40+9e8q|& zt43Q=4-B@cph>ZI#O_{xt}8DzJ)K8M*?y8?q?AkVS6F&JhlZA0W$wGcn3qKh=VyS8 zx{*Riblby+1T@u3W|PbKDH6mB8njkmp<-X9-(HdBav^`&nyKsQdWSb>xyFWlrRrO3 zeBJL4`a%2`IBv(X2nJ0rGZ>M@hWKZak~*|{amj&injd*P>lg)j0j4RjwZ-Bs6ub51 zq_r?hyXUtEGXV;~g&Jcw9&6$_j=B1hlkSYBanQ4@gdNCnjsp&12I~Ndb)xm9K9MtvS8xd(yng!5#X65*1wA zut>11anJF#2iA?Lv#&DQDqZgy-BYpH+Ms+pxfRqwM)3 z29~Qvb{HlMvND1aMH#=>@65wGnIPASQ&@L@nFBO;v_9IsNwzS%ioWRfy-umZl+dM>V^0t%aM#vN~8 zbFzo|s^=UmW~ZogLpm6D7#q|1-jJyM?r*s0Jy+N6zR+H{kZcHCZjY$(%Xq*r4i9U; z{;nonhSi;HcF7&o+%=szImkxWjtdxHwh1T_n;U9h)X}{L)mGkY1Qb{OAh{lN)76_+ za({0=Px5ej3!R!kX~XjJ1Fm^81t*c;>W$*Ua+dF7Fre>7D3pdVM^<78pW7sYx+IRZ zy?V>8ovGdcW+c=;4_jBDs9_BaH~9^sqw%7eeZ+Y1E&|>MpPM#$RDpJaD~^b``z43d zH&5{ln*xe*E5@ccySF!=B266zB0ANf`Ns>UTyUGyl*@NzuBX}J>SO9Ex3P6_sqeJj zlSrG!q3nirs(22*`h7Stz}o))VuXB7dk2Ui&Nf{jSYI?_r=cW zLvh*F@|X;B`ey?9h1k$X-NuD06q5BXlFlMk-)3zdZZDPC&4?b^RZ1IDGdFgUo7KH@<0MpbBWHIo^(L8I)GdmLjTH*WIK_MSymT21^tH zwV=t%>T(?`B9;Yb@fzBKYFGgYHQ}rl3$Sgki}Sfnh!Hg%py^s#ZDwawM^X6jzEu6L z>F(CLbWatYoA*1fvRn~@54Nl457kv>mwh#52Q5~d$T&^YoiY3Uhsx^PZ4E_bvNcP1 zsQV*9Ur6VX+Qi@I;;Tz=f9_AP!;WEm^_1OQ-Z@?yZ!B5Kqz90l%L^m9GEZ|+RidSm zb@s9Ta%lm(qt9)0_VxW=lPmm~u9oH1pu}j{w5(?9=V;j87(YT>AA=}Z_|L+wdCaEJ z;)Czo`)BEV-~rX4*|jvn*X=8W~k7lKX_+D)R&>2>q-cCwr%Gj2OCj}4Z>T2JO+ z?^J6606>KYiTi9B=Un16KDruEr!2U9%%i7E;L~=~l5>!F~0~by!L3CKRWRWaD$TyX zK|;=)d2Jd^mAey3HZAE(|C-zOmG3vzJFtW9M|4J1t{I%yFbfM#k=*K z6sID)hi5STnDHz|pv2Ys6puhD+V&IWfse8TQ>oP*ayA9P=ScZL%G!mt!bWcXhr~0e zPrR--mUTkcLDHXPSu4Y~89~A0e$f+j=H%L*2Wn=+A;YO=Ouaye?nSo-RXR zaEgchCg?zGZ{g}H{E!w!w9>K7a;!}OM>5gUpl;llc7}z@t2PGXa`@(oNL-bZ%kpkK z6h=V%U42PuD#FT>toyd(HWQ;J&yA?sFG0aSFSz8x8M0PvD~(t@UIVli4)+Rz_-`D- z-dF8tDZCmkY&9*W2~KYUoblIbQPAE3eCin8*L*)MT6J(F(81d#woY6xM_bezv}rc= zL|Iz|{qj63rh3pZ9DJg4Mg0oi3F8E;?;e>tp5HLw?G;F11+O2uNvx;I?qo z55N^oho7#u%h13wn_aItXxkkwK~EPBI%+%eR>!RENf>n>!r}c-Ez!L_k{un6`0M8E zUz$F8fmt3@*rCODCq>%FmygYiv%p+&5%x>C`g;j=mqNmT?6lk5TON*>oX(%hp;{N-#}(fCgF#^G<23GVk>{-$vb2~|x;6_! z-AI*7hg^+c&cm@0qBlPAa;qLMKPPC8DKN}NzQtzk61CLR6MR@-v4o2jV>OKK==uax zUcrd+C;;L8!MoIo%+@fbxx?inSLlG`NzUBzzkVH7UPy_ZCOR)a zJD+_Tht4(ZE8hJ^8iF;O8MEuG>xJ`=;^OP;bxu~@=6iF6a$6v_gNYnbAuWWjK^n}h z-U-_ZMgs+d+D3XJp;vbS0D#iyYRv#o#xsdpAOmElaWX08<$8hUM^;r^_&8DHL2K0;JLo41-J@YECvjh*DmN<~b6283o{FVH0>rrcZ`_KtJ36 zWpeEcZSqVDyX5>@z4%3`pDxkOlaNK0+Q-X$fwyo> zM<~RGe^x^{CdKcn(kAP{8QJd4oBrH*Vhlq?pHiE?`u*e7hcE;1hYLUkv8yseaA`MRZT|=7|9Tx6 z?Vqso?*sZD2vvV9Eu3IDiuyoY?8rWju#$#5spSjH(oQqV&Sh%)Y z3j$A&yqA(DlY+%l6I!@(RHqAfleq!|Xcu-&dI)W$1)ZNL@$%lY4Y``%Ws%pcaL35w zcoNpgusSKIZEwRQgu|K3^)KGl96#U4BlC-KGDpA&P>6*|4-6M8IE+xxA%!h}@m!DD z|1|X23*AL#WC44}`*`9-aQib+ylE}T874#JoAk+oH4n~vN~F+xbI?NA+SsXi;yQ6XLy^ zpJ^tBTk*V(ek6Ra=D+2%_S#-AX7mi1HM+9`Klj`y2{u8-Wb)mm_KZlB=aFfxJK*fJm1T^`cT$Hi59&>ZY5ED5;iy-llh}9SA4~I$R zR??7%qqxlbDs6y3Y zW|1PM!^xnlca`^j4>>gSdnX-l7`$B5sCH{x7&e}8B=51_@${%gz!B}Ws$BlUL;0;8 zj%nv&FDOKqMODWTs-+(BMvW~7Ab@3WM=l@%d>F;eOuL2x2`IN=Wy6*+;otUkjG;5gT54yi6r}^+fKg;TtI6h4`Dm+B*$NxjV+VxS{ zbE0z`7$d_fqrnAXfM%j5vyLZ^jfhw!KDx9-C4|nRiby5TZdn{X!v+H{!|6gfA+6At z+z`C`TS0i*dHXMhEoAO{?o?nbnRi2VF%IYU;Sr(hF}|Nfg=LaX%GUSrMt; z$II;VB}c)Ea;N1+HAAGN2{bf&jmZ{FzcMRncS!VPYIvKQ>ARXvWHj`Bu71t)SsSuitfruYyiWtEn#>wu*RU^M3b;F`oZ zlwBIxjyMs`le8?Fn=)4?xW_qzed+l;zWR4pID%k%{`1tAC6Ry7ZS2IUtqDWQ%b>`f z%|P>~TV&C;_>X9BK=x))g^{rI> zz!E~|lCQ=L1lBS?aZ&zZu}<|xs5Jit#=Z$Kn`ND_boYYjy;9E;G3rba_yfRSZvwO0 zR8$dh7Xzem7&~k;@ZKA~s@0Z>F@*s<_euM85#xURJQyZ?>@#x2ke>p3;tYh<0utrkhMc9i*n-w)!}(tdPofCRij)V#6P+h|H0dmadD1YZrHHeu0RGy80^v z;=X&QJwo$e>2Ar8LbRu8*caWo<}E~vUs*OLO5qu#({kM3%-lKVlD(^(r*P_8!Pkc) ziWJS1Tj4-Tru)}IZbPDl`CH8wqy767T-?;s5KF*JmL$hUR z35C{zl-~3WB2lwZhw2?~SL+4%f!b>KiyJ`iH%vMJ z&`|g9Lb0#d0HxjfBrXq1i^fv%a`~kBQq}$aj?U*)hl!rLM}8;#xg@#Kh2Q!eFEVMp zSzFRCd8aZ1#j$_9+~1a2Q~y;b#P7ZrN|{^VW)X%F_h4bpq%($n8vW zRkkZ_?Sr)+W@Aq}HP1Y4O2AA$+?32JLz>Yl^US}SAHnpElj0m8AX#^sI6|q(eIG({ zX7%=I&mnBBJ~egn>921c%f&R(%MiWAdw10UO5eHRw|f~iM)_V2 zZb`XYL6Kkh^5|!(KIKFa_9nAcaL-RaxfMiBdx5MS**9z6`rJ)gQq4u%(6Ta^1cimG z?u}vb$C_3HMw2+`Xf3()TQRIg(~CW;)oFftnsQ|NQZFTWk)+nA*3@COn>ic04+=)= z+79TLaSiUzh|%uiwk-ug5SeaE;G`Yv@~=t{|NqZgj&5JG2Cuj_0#a{IfbjM&6Tl zOxgn$LVH&V2U`k9U~gqI3;OoS_w5rMJ#LXTjLgz(aINoC?;3;(IdY$kV9o0QXr}-Y z-XpCZ270iYY2Kc6WT0rNIdn|f^d;n@n4S(HZVUf$=RIXH{0IxLE)H^!CM#cyj^qu~ zaskwH6k%=C8};WvV+paJPlurD)XzvwB2!c|)BvlUB&Oc2R;v;6IPfuVgOnbRt~FO_ zeAx}C&0oH4AdW`_hl4?*pE(~u;;@6G4RfAV zoRZ^6L|YXE)#X$zCSMLt`<3#HOvHy@IZK_+T#6-eqj!B!$PlDXIX1M}N_BLdSZxv5 z+eeY>b$tUUbmIA}+z#G)1;~UX%y@|?8f~fWCr$(E1XMUtGcE3#)*hzNPZ{%%7QG+@+|7xBni5ioiq$`dTly>>DFZ{*YY0A>asYwTKVW!K81XQ7FGpY7rybu z;{hPd(rea9+93kQ71iYg-o%gAv*rPCky}fzoZ}AX>|1xKEj$_+Qea>$y!Y@ZwBib$ zd=)n~tEY$jVZy6`Z_>s+1ea@8LvHfPv_^lP9eb_NSd1+jBD~l|N5RTcyj)t&8!62- zeOVQiIdiHk08|EG%mWS40X{5z<3(Hrg?IqNNkT4$@hMyZqpH%?RjSSbT*k^9r}U0u zu&n~J&&DRBU-YwjKK%Hn@}{o~!%ocO8i}KlylI}C2$_<8u=fpW3Vgbl>kb4@1F}3N zue}uz0F1rofl~xQP;VVKemDmk&YfT?FWV||D4~g%Ug^rQ+*EjgJ4a%AwIgFKMCS9$ zd<+AGZdX@6J~ApA0njDJZ2;Wu@rbE!pLiW1%5s_arP|<(j`D$1^_)t;)lR}1)Xw1X z?$BEQ$!#_IbC=N}e}7xvo#)9V*7)~PA}dy%E9+D8b4O{U>FYcJPu`MVddWfuDo%h; z^__0W;tB(!F5EE7w5Yd{L2E0U$0R4~i~ZfH@sJ8#!}J+*wYwEf3fbd0cHZerC1qqU zn1))#r5SB<_VS8TFN;Q|&%=vs`}l)Nz3^%I7ov0Qa-y|I|K4}{{m`$WwMWnt;x zIN3?(tX9z+pW^7`7H6pw?i!G3B|lkAeV&}S))YC8V42cq!}JeNK2eSTpLp_6l%P|< zLz-YQwr>0U(%h$y5^g3Z8jDZO*V3b_v;b4{JUoiw6KJxeL0crxdLZ@wZ>=?CyCo>!+%I58x zXt)n0zhdLnn~vxBHPoGtvxA+Nzi64D?*(VoSHq05aO|9q9=AGbAY6XnzpPyenp|?? zDcrkoQ>trn$jD`b6Q+MRiInsni4AoeDNnpZ-{H}7PI2{nMG9__4cnth*IQ2aH*Qv! z0oo^lV(*h)p^@sU`ElUB8CrX7$%oN1BIOg=zdR23J$_?`$_~gpSQMICkvg`-h6jnv zfP^_f&M$s?gL?M7p6~f4u?&<~IkktvR99&<(%;NQ5j5C2pM41PzGCYYUU$o-Eto~p zAE~`h>srw>mo(c+#@0mvn4Z*kPUjxNj{YyWG5|o`n13kBoecF=2J!&m2=JMQKs!ap zzNvW7Pr(e7KanmEi#oaAefF7BMm6BU&5;?k(%-}3b6Ck^OMlTQ!XBR>1wcTTd-ZjM zmAO$yTVIDp&i@%CUjy;cb?T=EXP%1EJrPhGh68A02MK)2ZPjxuqdGL_y|)|xg_g%t z8tTpttmXp|A~HW7e%UXv^bskWwOwvgecuT$8`IKC#7KC0Bd@ms3c*Ue`VpAc%bg4PNUigjSGto>}<2i1=JPtFypi0V?#~ZQ%o^d1*}8% z)ZBpDSS8HC<{M;_a1$IzylVdBt4xj4SS78KWU3W6z7p&<`j=LILr&!mX>|FYN{YoW znZnvSAL8p#wB6!oG;u}(49IcR?`t%h+O+1~FX*UgqHXG45dkmAai+2Pq*ru!{(~gb z=H!5*7%O%bkR{>xoNp0*}`um$~=?cGsH8D9~=S8UlPM`7GWnPgXqj zY8`oH85PEL;-;k;zwn!D9+WscSEsB>g5dG?_gyRu`oC#)S%~i9JU40Ni4SU+&H*d5 zqN3g>#ELhwE2$+|o6)YkdH|9t7ASQtDP{6so6C`p)Mpf0AY8o823Jqyk=ST%JnYo{ zra{WwFzxNJZ|}c99_=}HQ>in#ZBk+{XZ&Q)wa29ev3i)ZGBpM_70|V~ zNvwYnJF~BjBbE$>aBem2o;!W_wA zmfCrwvw1sus(eB#$w_u5GcM1^jU7<6(7yF?a37m5jP}Ril0&Rj57o2k(s|1U12C+Y zx3TXsc%)+-MbkW};8m_>%g+r#NHO@7x^eR0zr>cfIW%`t5>O!lmH6H$81OY+d3Gkj zAE2?1eX=c5(H(Q2ns*Nr?#yzVb7kk{-D*OH)rHb!788{RG%ngyLUxb&1p*sJqt#e5 z_nrwztm4ZQ}EE_S7pv2=f<`+g8)R3dwE(F`|I z2kU5PE}?+gTSD7!+~)oh8lZuT8(*vCS)#Y`U~$PR(0P@e)ZPg5Ux4|BhKKw2%EIW9 z!fIHbGCxfZsCGp7^&?rPx1$zND>uDmM$r4<4^{B5bAA}ZeTPtF851u>0tx%Q$^faM z9)Ut_#Bd_8>bP3KgL0k8^@9?7WG1jCQZgN;vb8|+X5V}DxYiyT0VY7R3)?SoPK`1= zwj-7X&y-sV?=+5>)u*jJdvMZQ$Xp3_Uc564Z?^F*&%;078KT|@jcB@k&X83aHt{HPe}w3f>s#5f+oZ6$V) zQFx8lqfg$uAgPY|eu4K#(gwzhYowaIq_de|PVztva&nulN@7BJNbZqO@E8lHSc-#0 zkNEt8`wJWEg+eQfIw(8*s>YUkdc}RNed%5MO_Oqj99*M#ia| z${qktUls5A_e4$>J#1DNUD7I8;-aV;%AhN%8A*lRp1uNns7+264AaM6e$sX&KrZ|( z0wZ$V`54(hjTl005IE#X&aZ_Vz^N9UWbv2sQ7}?^`=|Im=hGk(%U3$~yF{Km96{ps zDble6cxcn5Fzif(RIj}sgK3OhKYd6xLBHYso+#OJo+Vt#o*J{;E2lpp=NmzaKQ`{I zP=NnV_hHWp$9)j=3I93;J=?87y0!2R{e7+9A61$94`Kcgh@y58@n4%s8hnzdZOQb1 zF2B0$j++lj2rKP=VeQ|XXE3em=rc_`+rE4a*j3tAYkhlJoJHS35b?LF=^woOp9uTk z>=_pi3YEl2y6N}m!nS)X?>TSdvu`_kCvCR)fp;2Yk2W;Cn^WHg1r_8&zrJAW-POaY zHKCUvRRi5hBMccdPTqM-O9D@oaXeetCqM2t?UFAK$;yU(jTgqH-@nz_4pOQ09JBbo z-0I(|K{k~UO?yh94Z(nNWE`#!=dXiu4`tF$&*XDfF_kE7ON|$&U;7@`KQFGHHss&& zk7COk+tw(0Ros2Gzf0|Ho!0Z`01uG2I7VUzk-g+OOOQr@n}H#T-b%NvSvPmWAU?!L{w2NxjomM+kRq>kuRM0Iyy1naWHW`s zmhb8IhZ8m15-8k;*7x7@%Y#=`nU06JvRd|<+pHn{Q0vdFXl{LV1AL`6=4j>cFlSN$ zH}dnE*`=-J4RmQ#86JqP?cq;JaY7GjdhIzt--Adz3|<4-k?!wV?Hr6be1($)3}&N4 zWp=`7xbh;$5qZK?AMFq9C4*~5{Z04Ny9P`&U<`PsjU9cMK-ly0H``qTc);LxpzZ~ixv#OQScf$mYD?C#!1eAsh71M7^M+9l;@%CZQMJEGGC z%T6+Ccv2Ff380?XlKXyqk7jiEm->RpJ?$3J>l3w8A%{kzsfnAU8r)Xv!mZ)J2R6v# zxaX|y8?#W2p7P#v>}O|SZfO%Zd*1eX4%>n5%jl75s0-xdW%m6e^l1}tBfqQiMOO_& z+&>zbUo6jQhBXn+6ME?zcRhtdG zw$H($5%_HVNkU(f6MuP=D7O336$tf8R0G=*E;RH+%om5R!&E=_f4>(-i?lu82M632 zjs2-;8fpNI*akLw8## z)3VW7@Zn%mZe+GzEmQ7s4{KU;xjw$L>@|KFOc~n$et*~+9J3gcbj2=%a3&j_rFrS< zpnSY;A?cj!k@wj$aOH8gk-xgp)js1MJr_p_9)GK`74{n{T~WY9Dbo-ROCr@swuL&? zdUy~ekTs01H&uXx8!fg%MUw?ygpd0-19;3 zi6bw!Cx$cVI;F&KV}$4)TpqJ>)f?U%b3WAQC)+k;mJHcQm|Kf2b?j9Ff*lm5O#328 z>q}Zl)SJk6Qiq{Jw%XOS`q1u-7zr&>Fz(Z#l9bs;Hm-K(D3;pn+FaJV6QqpcW8gPn z8)?Vqwh>dnXXWAN8s-w`+WNjsbg?2X*YkJns%<~$01aL|b`b&&3zpX4hc4Fjl*}>_ z1zQOX*~iz|2%3O7;%;NU3pFnL=D|l-veom9J2&H_xzZCDY$ARu)4MmN8PdsgUSAPY zZiaW{Sk{O+%Qt@^tm8Y?6XQqhzQUIR2S)$6f8T{;#)+4e9CEL$WehhjmQfJ#GHdYvbw(o+`qnjaZ$ z57u;K+3CU8NJkkKPBMxhka6Zc`RAmb;CELK+$8tsV>q@&8T?04NxEIWQ ziqEZAw>mo_ruzSv_hxRj-PQYJfabQ`ab5E@K3mPSz zpTO%oE|z7p+|SEgcgS;_GNYb5ZfGJrW6!73{tufsNAAAwR+Lr~SQ+kh(^h(oU|P4Z z7V?BrES7Kjnm(FUT9EI|Vcont@k{*&>awoOWU{R3^&1T9C&b$f5yQC`9)}YvbF|an zp|1gN8!mWzOYesr?W}Uvnael=G_0c8+f8d}YdW4iLAfKgyZR@$8C7YIqKm_91mQB) zmR@}ocp`y%+>#z=j7By!-NxU5akolS)l|Qnb&Ov2(trHU**mFxxcnwW!P@Aj<1P_X z!n(+AD-r;2;aX{bznULYp8sb-^(f;pFuZy$ZIh7{ZRC1ECm#n6L);yxj1&ez~A%9%A^SZoEhtzJOby;a`T^9`bGhC?;J@<5gQ zO6|gEXq|5I zMAJJ~oxVJ9-5^hdr(>#lgirQ9}=K>0@>Ld3U<9;#E z4o)r-PC}kG1&VHXtHf{rUF6O5Yw21J@_lKT29D|VWg&dWYOiCFY0B0`UU`rNR_o#}w*gLb{hUtZ^MU>H;CJHKoqcz5 zhWGp|ckvjWgr#?wnJo?E_3G<5ZP3rrOkTV;VzC?5*Cd8(__(?enD_&YIT5>It4@bl z95j(HsfyI$E*JCme~XPjYMj0q-1H+i2ZGyuU^b%4J%Xt6++3%pmslOL4cV-U z+>70BRxG7ceV+xi7y4B6N-)#)B;~J)6!H~G7ERbuO!;1%v)b3;!aL?3xyYKcX!yQY z{I?c>;^uiGIu^l1w5UoL$vLoFr(B#QYFtTP-0bct$Z-# zl;?hW@=hJ=T6W3vi{C+UR9e}zI)L%!22{upzY0Trc{i%o984}IydN?}u2!fyo9&GX32hOY5JozMrvXaK<&p3^R-PD< z@x#?-OXMN3jC~JEX8=@71kH*aDF;Xf%70X##ykd;$$wUY?UIeit(~5v6Ywr2ZFZLX z#!a7U&9)vK=@CR@7RsE5z2z$v-yCVIj$5YIgO3;sDaPjad{2?n2;Ha9Sc3w?NQpn@ z2)9@ZUys=QU16E+stv)fj-)ttTUe__ab`Ixh7@imdptCa1r*Yi}+8=NEjAb&v~*m=lo;)jd*-^9&) zhbaFNH_Mm(uPHQ}Ro?$X+}yNp{U74yvM)Jm3I^gdbF}~ifPe!SE1yEkYI64FRY^r& z00^ByF#`3eP-PYt88)yeL~!aEQ9bd>RS=)0PrzO0%^4swPc4USBiUJI_t0P**!>~k zuYF@Cq9ulmI+$$-3fKZVKc(|w;-hTP;^o_6wqpk8f zW`{yQCNwjG>v8P9rASy~&KBzyfu~&Vn*LTYAMtXvTtQ{VWe3+f<*I8nrzUewMZYZ2 zh|(t?)vfdf=-L<+Gdb+6HfEM8QBfzE-sp(7b;Xs|<)8`x$q$Oft^62TR}<*E7GBR- zp&NPP2O&Uy_(jHgq7Pd2K3%oJ(AR}5i8LbqRO4A&FQ}MlOaLLtc6mp22*4-d2f!ab zcX(OLFpL;E{eX9fDF?x0fru*gnr=NF+0V{dx&_sdUTbse=zot|z4)I5&-EtLtQMtg zhaGV#WxOT|24$|r7toq~R%KT$r4~HHi34>C$wpjpL;&?c=vP^-#{ibznlNt(S_Lzh z@iO3GF+ETU-i9|r30VgRvO1JzTk4xeoJ(9%s0b?%*3u?GYtxab`X6#N+Ud+a!@ z0S=8Hk=*yu$mYl-1t<2^^(9WsRc}r!0R_+ z1H$;bLfWE~?1B)cod&qNPS1h(@~&lb?kQZx_Nv1C15WU1y&ruC+oqOC!w}e&sF=$HWTk=rONBj`(}&?QcZm=2U?oRByp)I6%~jc$9ziJRw)sI4#<`<9A3~x(r7ma7{C-XU6UU_zY-**Q0;y3Hh zGs&B~J>JTo#>Dt1hCZS|?-xtUr_J4q;a)L}9TBUai;UuTDx>t|^>>n2XO^A0B8geV3kRw!fn6k&HOH9qrX#P6+ru!UN%FSg{4#$-vDJ;(L! zP~0jt;gq<7&s|cB2YlG*Y(C}%94mv4?L9uWUQhEi&qZGa`M83)@E<=u|Gx#=41CW4 zKRP&pW2(Thmu9Ybxf0sPIJvut7G8i)A;TUFfWot$0KmhC3=iP6S~-RTXulL``(fEf z{VVG0Nvk8fmYIGC*ROiNWeF9ohAcn0ysB9^@~+66&C4$k!^fo9g3S*+34i^z#WYa; z-{NWNz5f(XheWOkle{q;K|*bK+Iz_3Fmo-M)~mbykOLQcTikKMJ~5$m@#((Jjd1HO z2HSTmmj8K?!ivCXuS5N1frQ-^jSsGszkH3vUIAs+<9U4;gf@@cO49S}g{7dASva6z z{irj%vb3UP&9wX2YUTa%(2+cplePvCdeRZy?GJxIQzN*;9>cPQTG(lPy4&%~VXoo3 zm@T*rHc#+DTd5W1+Kh`r_IyfKkKWc=?#DA0fBp9UP0VSlvEe>9gb^vus@9RJ)g zJA<;kTe?Nlko=}VP+8wwa2tk?_o7sLT;J~}6usK8G6$#qS8D!d=&$Ga>(u{6*!Az^ zG3lQaoNw-(s+n`EZq=#Ue{@%M?e6{dD{DRLx7OlpT_x23uNHq_3G7VVJu!}I z4Nnn@W|TD3!9hb;TnvhuZ>vW4_}#Qimefq@V%S^DxF2|xe*W0}0;Z!eJC!eLnp|E- zFyfo88$Hy^Xv1I=m>b>zT~R>BRhOAkY6=@{Wi!V|*gQhOCGEM5cQm)Bl!A6P>x?DiQMC%!==BtNo~0>J6yYKKqx}v(h87pysz^eLA5#SwouS?Xk}i4zdQ0V3=wtbMak3fQiziNUIkr z@_amD-#Gt=jBbyjv2g~ib#U7JP%C-*)VAL9eA7{_bd;0k&rCfG_Uuz~N!-tUy;ho| zG_Ony{B10b=d&B#N{_8JY}KBM@?UJnBdU!*8Lz*aXM)sEHhnyR^#|)m%HgD!g_hpP*(t1iE?vJ6jBVS$Gp6MHVjm7 z`C{J>elszfp#3%YH%PI$JCxNK!2QF5)%M8;{g4lfdm?o5I_-To{?p&qWyQnF5KFU4R&6PXR5S=x@9jE8 zcV1M(`N+-h!N1Ej^A%?M5ln!4bJw~6vhk#=bTGbtDLRU1C+H+Q3K#-|<`A>^HZBb`)a9D%*ulK1EFBB|K_;I(CdX)Cv{H_q z!U&yz#6v}rw0aqYC z!@SE1ws3l+isdMB@;9>`#^-wur5;$l?6YEXfe>p$IO>oK`Zgm?vJ8hldt}t4?~Q_$ zkVqIB{DeunmbRIZ^Qu*Ex>5nP{90sw`_cL4Cuqni(Gwu9P%XHd-uNIo+ai2ijF?iiveyuramndo6pxX%`nr^w7Y~R zI!ly@M{rPRrE-hr(P03S5(FKO>uzr1!5P%s{|PwB&XdCf0nC z)O43SrubXo_bgE@-|eAbdew(*?IndSe-TDk;Gjs2>~PfdnM!Fby_L-LSy=5C-ybg+ zM+-(Xm~Z4j7*0sIzXCroa5OZ{KRLdR_2L7pYHQb?Zit#&D98RzCm}avv8*VK3sMA1 zxV3&=h%9Etx_&k8oQbs_pBR%G?m?mPy?)D=!qki8VUAbeE5@33)CA|Rm)kD2QH?i{gYzf6^;t=rBUEOw#{jA$ z0TNKIyiUTbDFUT`YXWD|hp11pJJ}GN-3W9Z1f%F{YOn%l%FT?7TZ(Kz;;6^Q+n7aUgy1#H&Z$KcjGCW4 zcbIj(RqoEmo#`~2>~{-Y-D3JU@?}c<#8%*vdiW>h+poSN01*vBJ@W@CP(br=9eFIT zeOy5z^agfg=kTF?lVh3yo6hE_I%Zcv*JT7^2-X6DoU=iIZq`@r51CM|0-~iARr2h= zxmeg$nlv@MM8FSCU%Z=xN}gwP{Du55M9MBU)#lU|;MWQs#2%g$fCvBfXOmGB;N6%! z9PTwEK9u0}?}|98?iFAr&SL2WdJAR+h;S8=|5VXd83ZLr`%FeXx+n=JSWx_mv5QI6 zX|IX5!0^7735i$^d2%VQzmf=x#bMA5w8q zfdwC+zYamPBTy#n8YV$|4-L>`{Va+i6@!y-1g(l5)GKYjkj6At{Pupwvj4nwX^ z0kXrq`+-lMgH)Xtr_1~JyP@-?+jU079>P}+e`eIy9wI9$A}R%M-t-o>zpn4gI?;vr zdaM1;(Vl$nPRiGyg$ZW#;Skh|yIs+1pB!Jl7_2m{>c^g15L+~|uzDjnb%pSA>n5@q zH8+PM{5^*M!X*+!G&dsDiL>atC3Pw_%KRC|vRHxKRv@nNMS1J4B7)6PzX_yZL&H1! zM-Lmz!!gOVtg3A}*0-?YqNqpM0&}+e2B(19GvMUu#sKKFRtT(ZtIfhIq_r_e`^~ro z|TTGh|dB1<{{7P@?}esKt=R1KY4O(C#+d!}SPo5i4N;iF%uN`^1sKHcaDhZ69{8c@mKbj~Bs5Alt)9|(f z>leAtUuLh{KZIL5$rSgA-F%E$poDD?3suZPwfmhUw+nFW#WN_N0x{l5!*_C$Th~;H zWfV_xrDId~B${+PA%Fg`&PfraHlFowTSzVfujEp?4uu{7<5b(S-DVcUKDQb4SWH}MTheBLF% zeO5O7bZ}21_y`j+{U?nqtwcyzH2JB2l3Ztua~SQ^9;pM;tG)658q=-j zYpnzc7IZLqMdoW5O3rNvK#mR7#6m~i>`PUWV|eoOX9B;mp(P~Er@PJpyyvNNR$3Wk zU~WoVLvQY$JfRXRRe?+hoxKsPeDEIAZo8*y=`$?>nl3h`~XMQE) zZ$M%=ISoCGA7v=AKGOIMM~z<-JFOnA%1CQy+_HFauCSHmoW^nXdxuD^ddZyb2Q}Ha z_hz_2Sbd$*MX$qv{hQ9YS;C4cK>9XmiB?GCVbuK!t9M_cWOhVw@(By-uKqxt`V5-!l>ZnmJ)%e=Qp_{Uv_Do-#T|6-DVtI~U=~y7!QBi;;xgVEH z881C6cosT6SbB>-Wy(h2C)g69`DUbbo+=qb`a1qbj_V#tN``8DU{H>0fC=mLluRpa z0|`hjp|lXOR17Y~>{FurFaL5qt7 zScyg098D^=P8pWLIwfGSeJXCzJhjS|{G3T!-nu~{r-m%E`l2doExZ`T*Yq^9fh0PF zgB`rS*AqbsT)ceZiX+i8%`>X*LAi#bKcd#D`R4Ih#kdt||FHJOJO!HfOzj^w!_(Jx zE|=AL&j_=t*pv?YQ**(F24#3=NiI%cUIpi}$71AG*E zf>{DC7*Rq67c{)_y>{rWJGCWm=ZiZ%3M$bX$!{8a3Pb6&#c_VauvRJi7La>0>hf~8 zWUA*gUD!Uy?mS1DF<6DIW^FUY8J^vlsFA!C$;R0*rrv(T4BGfsCXmW0P(QpriM}wr zTpddA!{E5kGHJj3);2FavEh?fsZSi6xmAv#A_oUFural~%(LYDfv~X{?|l9T^&nT; zCL~kuaq{8nH2E9@jpdg)7=MfaCay`i^v<>j_HLU+B9G<7R^j26%mT+Rn?_1#z)%Zp!s4w9jr#(zr8j*N3w4NDA7b^cknG4x@yZL-5&K z@o3d-9-b6Ir|qhrBd$~63G#vkou9)iTQh=eoI@YB7*pcWmL}}ZPA5VNEWyVT@Z++2 z+y`|Hnbrq$;vCk?9sLF->>D3pf3wvyf2$=}W-Mp22mh}13d7%=qd8VKo+}y()=0V} zy`O*-h^Gd3S5$x6grD;)U%XhPnm^oh!1iAewzSRPt#T!Z(O?aH0@ps@?LvSUxodR1 zK<$t;a!zWz+*{0#O86?D{9Ge?j)aIAHj9*3niId+7X=wKdYS zWc-VcR18JpUy0bvZ}Rnje)7ZHZ!^D5hx-dJ!La6kJu6}7Wr)i338FIfp@gVR?|;6z z7>iPe`0*G|kba^I{{q%#e%JnsboGCKM<4M^T)-=1|Fyt_s(@Ke-R<(cfLr%*91bX@ zf#37_TX&`oidyw6nRCES98Oz(N1F1?H)j`Z$8K?wjI~Jp`JzKcvtz2Kg+q2%*bNg1_=}RoFLokX;8P zfXLqC!a_DcQOw#ZblJZT$m(c)6u$(r<-??HYME~!ez@+%2pu)TCU3isjlK`nwH#Rm z&F1ZvDB{?iovrkE3o$@O!j#5&ao}Ft)FT1fdjWhGeOv<{-Del-F}h>Z*-$z4N3S zRjcT`et~C@|9zITF5Savk-53;=(XpBX)}yrG0S}zLF>2lgp=2u9ZXtCh6mRMWHm3T z;Bso_YUDUu#2kHEpWpwd7NDhc-E>!v33W9KbapLQ=O>-i!FTyel6Td0IhW9NIv^d-Y-c1vkIL;ZqTG z&59UZ&+CJG+Ch?O0neHx7x?iK?o0=@fcGurFIT{dkXaRV5?(a4!n4HKBQT@Ec*w}wMJf#O7-V%G)jV|qd;Uk=B{s?GK~NPR>511_O74ul+mv@*=Z_p^&GSJc-%*b^f1H%ZlFI{d4X{HW_;;ILuJfKROPlmNkOoBwurMC zsgjm^kn+uG(&bLaTsnh~Bk~Sbf5-_ks{NX>^EAul4Lx`{tgKC4y|@+2OB!PmM*6qS3HK-7G@@48xaeJ0l&L< zSAb(ceRl|V!;ZigW?w{xmlw|SH@+}x5)1ymDKOU!=iV$my4gQQ_|0i~o-e z^dpb@Ey`;|!Fs|%vSYi)sZqeyEx-&oPPf0%=!iVDho^rVQuMyuZ{aS;a?#t9d#)Qd z617`KLR4h9e*+C9HebyIBzL|so`Z%`QcG{{IA}acVu#so=&Lbhn=&3BsdJsvKx9Fz zZVsp~;3pbj#WqTn;-TUCeXxrP@ZDN`Y6FFl7!NJSxbB4c9?(>YC)(=Ep2AuSmcH@3 z6slro8tR7;)Nx;;d<3OrucKwAgof4=wNlB1hhR)y=NL(woW|8n$s_8so6@4EQxU{^ zqZT12CRd;0ErEUwjlAQxo{O8g;S)0(9H$%1BVNDt?`UYcwS5Yma(DwjFX>0Dx{*;o z>AQY+4yogwfagqpSQGR)Zvo9uA+qw|9;cBRA_#AL(kWk`hCzGQVsjUulU_yN`|_oL zT|v;%XR=(#W~)y<5AEnGR1q!Cvmgjov5;TMrI|M)>*mSi2a98H zq6xk8?OuvEvEzK7Q7QZ0+4IrV#_$do%h)nxvLP==+ZQS}&>h)MVifwW)D=((W2cp7 zo-?pm=_na2Y~3{UuqATP+jWp`^FpIC!b6UBT-N*9p(a3Hh0iFsDI6A1D?YCY#Z~~+ z+mif~s-P1k-EU5_s2BjIM+2Bd)j4E8p18B&&_*m2z@&z_(RnLwo;F25dvR$&e{ay# zW00yaV&7+s-=mY?V-R4Yzub`_qaY+EO3z4>qts?^yI_30PJHJYJd$E9qPFP*p z;GwkR=d43S+rH$t{3sEw%JR9LYi`9z1UHDHThLG_jZf6yyu*JK`%}gyCKqd$(eY;s zB(N3A&D+Iwc(BR8mH%G&p8t915%H<>32XhWuK*$-Fp;oc+5`ps!5ab$Uq#6PGyJPM z4C6!szh*_57Ck{PKZ4H7h_@j-bvScOfY}ecWjfc z?Oz|g@BukOV$v+AMD`!-$~z9ewuq1pQMBZzaWc*Tljx_Ey|HaiBnVRpkC(yJC~a;h zYZ{FI(Fm1k{3@B$E@+JZc12m?BqYh(x3veOoyQ3T>#@Fj9m~%q_(UDBSCb6Q4#)$B z9CMcR$Xwq9%Io%3>Q~&iJ9K z5SR0&+I#iSC|F{9X!#|jVSv+*S@s#EtU_-Hbx3WAW=9eA2s-goc+K<#v|KyMTs0k}>m{jA@Uz8~`bYGz)Unn0zkuu(PvS4P$czTdzF+b^GoV86UJ|G^ zA4)&EcMfF%P#Q$hr@~lKMkga#d+AT1zJ$5~nKhM=J~3d~CSsdHj%onqtKpwUYFPTT zkJbO+W4)P_dH>GG1URhC7)yJX5SDU$y=MRNF;$`sn~DW2eMI&hwP5k4W$>B27iVG^ z@&uS$!oIhGuEAgq&$C$Wh4oU}mwX`|gbiG1p-06kxqE>RonTBSZhU;4{s^rWCEwIm zyIkyx1-8}?IR;7{^0GwyW33vR;1SDN`4$jX`LURndQ+dTiilDDUQ@hz6*0+utN*YI zA#1ptZp6HTp$zrs6?8eKQ1Yb2D8!?%6QdANQd#;QFq^D|S8BpSW75*-L$rgU9R`3% z&aD?7rK{UttZvVpHI6r;wv;+Ej2dnB?yFIp5x&m~7iXM&=jWb;0n+=CBp~I78*qxa zIkHD*F1$X6*$63mq<~T8l%_MNdLI)$P#6tj*lo|3m{>1f;UasWWS+yR$E@jNAOsT| zjQsOYz4>Sk%I!?BSFo6YLY-dtR}U^iTt@tZ5cD%T(OC<0VW&$TO$!Z+4ETD5pW z+`mDw`>KrG{Wcgbxc)SBt03veTx)bf{i@|Z|1=2=)LNMcZH>GUR8vc@FF1L?r1d%W z(x^u#-JT&5586|x1?}yLvsBGO{6nVzEzytRcxyzJcG9XSL^H zNINIY^r6=1e!cs1SmWlWpl7WQ;KO-qaWdUTbsE6f)bNUyH=-H_!~KT*5PF#Fu~hchq#TArzA{juspR({;Z4wT*Uh6>@BTzPpt4>@5Wl)|@41 z2%b~D?>8A^x41gC0zT7PGMb$(!;qO?NOg>tvM~edQjW&n8tbhl>WE{7=vhDUpP5N- zLJ%75x74lE@PN;Cr{px&`vZ0R?xM7%Kl+D|=&OwpJr(zHC}QSjO=i^rEk}EmEDe0( ziHviJ9cKQeZ9DrKs)*Q>g1vNuwmbf&ncu1IG$0wF{)_o-+%B#9G!GdOrhUglq6BpreG(MIN z-oFvYZ5UsL4LY^V=GKi?yhqyg^m%BTNc#H%|Ha`K>Q1}%gKD_2;=)EmyEIS_1~H_d z1*4D8;TA|0&Tr+MnR`M->)9L=M%dh>4(}ZUV*2iLuJm9+Qc` z`ZKdY9$4?H!Vp$@BZi=Tq19t>;&Suf@E-s3yU=uR!;QDpS`BX$%lVlzK6O;}f(=^z zTZHhJ3lmer!w-ZZenOtCiv86jDjbIrr`C(jnY}cE_U+8w+kJ#cJCh+Xj%{^~DHGc1 z28^fguR5`&&lN^35k6I)F{cU(P|M?ht%bU)+L329BsXG~Ukz50EiWHlE65L~Sg&<+ z%p{pcwWb6%R~YHovv|oU1vWo)%hxtUS~-7b1kB3Q3(?CYh@$H+<9;cOR9)#T4?EL6 z2z}8v`g&o##$aj_7`9j;7Pz(d-RL5v!TZJ4lU25YWBCE2h{fLDLH}XZS|OKhd;4zV zJwMELM+7a*=MQZ3RnPikY6y577Z3i!*kkYjr?HTmW4^`mo&%=nQ_#qA`;p6FfymMF zUwUbl{U!=wvx>~eFsXsXfj}$12^{Ja5WaWm4FipV1NcegjByoPa4eXG91~z98+ByX z-wmtX5wRbsPw;j=~mdh z{p?nI&`^MT@#b2MPF8t^VcYv53Y7OZGJ?yv6KXi`-M0UeLaqr!FmF?jq`a|}7{?1d z$H@s0xsg{qZV{h5Agj@FPndB~$vVB;>YquO_hJ)fWL@ z*{n&&yQ>csS+H8ZT(&+|2{}9BJD`Q=FPN3L`+&tYrvvr*Sy@JT(jz9{>Au|vwQdd~_r7MkiC0bCCO zq7c6sv!5_&c)*tjnzz8Mu3w<`G0c^Isx)~n9UXBbs>x{i^%by`zr7_9uBrIwW;nb@ zY0DpBY5n4&b@xlIk$R2b+PdMY9r^Js=KgqXa#Q4p?)^!qC-(3YcJ=GC zd^Vqwhbk(t7c{ahzV>vc2^09`i`@C+?a;0)4`b4`2Dpqa`OK7*LMwyaR0 zJEh8DH>iU#ZvD)BmTSzG*eaH|nM8NJ`ssNJOzr7nmOt|!2yk$jq4pM(g`-#5s@>u~ zNMt05M(qdLE+x=9s#c9fe?9rT+(7et{k1vDoGW>j7cHTfS%T-1lp$~v_WD1RW2dONE>zy{J+s( zu#aMIhe;maO-uyRr+?JvfCNA7d5ri8{wcA8xS06S@&8s`^snl!r|=`B6%^#fzg>{G zDlDX<79bPt{BC? zS1bMZ`Dvvzb!B%a+omddc&dSn~S zI9kxzsYJfX>)X#zbo9nImUAX7ZUothj1Pue*c9HS-a@suF_$&a?C&2?K0!~#r;dhU z`Q42jELsOIr=8gj*kFRfl=bN#*2#@@^WRt?lJ+7T&x<;>G2HLOa89~ zvS&=63MQnk1HS;}YYX1hl+Q$)if1q0XohL8Qr0&Vp>$uISWJrdN}1X7%Ai(SnsTcHt~Tg6~YK;Y%tjjx{6FId+XkMZ1wZ!xt3 zubbn^PwLdcpBb^A?`CRi8}>ka2du09%qmHqeJHxj<5DiZ4p+b4<9+`?cwp`3Q(JQ3 zH#*d$C+=Wx-=r7ZtS6*sbc@jTB5ZoH78ns(ZjUkbV-wCdga{;TsPnO7)h+1yGWfG8kfx2>Fl+CjMy_F7*}IL5q#$% zn@^ykct33)b9z?A^Hf3c!Xhy!>x$>sTfSmB%4nQ|zxBoe{)gxIuJd3#0?{e7KF_mw zS_sekK6tg|rvSEk1r|I4f=Scrx?2QD7jEZ!36O9ukGB{BZkxWWZP+3jm%&1+IH+=9e>spx2IjtS8ams#t znv`K{&OYW^+S(ZV{mLI(ZcK3sLA98=9U94*w%-mS0F6!V-mUDPNhv?f1O_DgJmWhO z9g%WDoK$Ig6WN3&I+;uer8BZ@P;DfVJPen@JH}*&Y^LNf5a{UrRg+)#Fyfs0bD6QT zrjO84=cl{oR3+-$YB&FO5o??CbnK_AEq0%~brmt44ExgR+%b!6vvWXnsN{8REMj!7 zz`7i91-(WLt~0t}KlD#!qK<+W@L9$Dk=eP2TyR4dbyCz6VlYkzJ{z;|B zR`?+xa=xf|sLm}f@K!F1+44FfE<^s%neMbKF(&rH>;sw~#NTN3((r3gRjOc5dc$8~ zgN(GBMGsENol1gVL%ulRQFv98$h(5Eg>&ni(|%rEQ%d#-2xpAyE7z-}=tR+%MD>aK zsP3iT77GVxY3$)Z3R8xOFfM5r>su;$XGG_D@d9FL37(l#VhzRzqTHO zsG{{QZu8||28(J<+SN;D2X`bJBBtX?&x;3iB$;WzjQlZa=a!b!)fDl`E#C~LzQ3MD zH&6Av%q`$Fk{7U@uJT>T=UQLg`|S=LR)^Ijo3K9c>mo4HDc{{P6a0U|bV1z%Ov#Th zNoov*cticPiW32^ri8dM-g}cnHO{I*TVvsf-qvvi3+??5yG|N=ixD2I4;WDn)^6Yb zjM^KI)GgP~eCSlw8`r=hH)pgdc1-~VbK(SD$-gWX&3H_b4y{<3NWbf)5b0U)Tu*vN zb96Mmj$K-6HR0C5<2N`BW<$V>F1k^!~AGl?%i*LEZeH}8&G&&jgPk=ti}S8 zwPCkAkOy?eIf(duSzXl^eV_-SadP*b?qF{#g@)UuHM8ivi%35~3s>QfvAeyhAAr) z296oHJ(jw9Rejni&3P^ZsH)xp9ovWZ06}mi&nO=N?$ek;-wjGi<=#lscq%fqF7(EN_vP5+WoofaB88dFn~U! z-o8#M5;VYh*F583-!{ZYz|V$(Vaeqpn~j}D^6PkHG;>m&+MJ{?wcl)NnTT7qmHZud zE{nd!P`#lzEc@*Uv*krKoSqJNpC0;|Hm|zaQMn3l^lB zxX^5;5}mY+O1UaF$z+BU{!D>oine&Fv!*>DqoBYL%jWQhyZ{t<|tN-Z-2oQjVa z75FEi2n*u;PZhr))ofh$yyHjlqeMlEngB+U|KQU&G<9&ozUAAFi@qDm{|qFOM$0(v zU$(*4DsPDm=l`Np)me{PF&Y+H0VY^{^Q84MSy0~C6jQnMN~0kH|1%5;u)||XYkJpo z!3~Wv3bIfoxO8e5ZRA>WMTDs;Wuy0$USPnS`Gl||KGmI-wrUP|keA-xWCB_9M}_8U zA?#_ugirGfRwF5!q!>)5v91CXnGS{DIqmk>Hqh^%d02Wh{$pkt8KST^Dj5GR@bj*T zP?HW8Rzj6wV!065Xrw6+L(HIge3srOn%}7^R6`!X?`|9{wI8K92D? z#Z80qFN!#w@o7HW)sdb@*Gk3JJl{Lc3?1^Gc9u z;4woz$I^_2zBOhW@UU?&?I50=rSUFa;Dj4PCfZ*m4h0`XG_@&D==ubPn^K7BL;R(` z<8)RpZGsK$17;M^qY>l*nEr|e)yQqZYwf|i7v46qcAW3iEjAgo@c*e7obftP&U)D= z4#Hrj6NCa}7NJLw=KZ)?5AgDBe{4-%j6nv%)*#*qP_3z&9?_a=<1E zp@3#L70&MLO+wD$VxpiPYrcfgwi_3D)r>7Dc^_r`GJT}&x*KT#{MJK$=B5s9Kgksk za0TSwU*}+AAIZ+<7R1#B2yl`bO|;s>r;7L4FWV8~1xTQk5Ab7VkYP_Ka{`HBs&l2Q z4KK>R-0_T6Nr3BIG3$il;Dq4kB1k;o1rwVJm=W|gBR*Xt;p8`-MbRt}Iq4$tf-Z>L z1hqdNg_p7#FUuuK^A1+)(bo#7Y`V3mm#E_P>mMA|-JyKZe44vSCdIeU!?!GA zzNL#VKY4x#Ck-68?L;ijLQ?Er-#x@aeKq z6Qjdm!kip>QaQ?Dc$(Rj_u`=ZH#|w%h??gYv^4&`5KQxTYc2d7j3=RSj z^u_31<^AR09gCrlOpoUo0#@C)^?}GF=u?t@ zbFo5A%*GX*QuDm_gsgwoi?BLE55i4oSN*?uyzjA)``q9wY zqGR5hr~aD+wWz+k37NG%vELFWG2b7U!5}MPG1-y`*v=UpE4;+ck(9mB7W<+;7Ww4l zJ$h!9`4oWZC-`$g=(1+q!rsFiseVGvPWVV6Y21TDoz-!H>DXA|IHraqytHVqej zoL19-o>x`ZEQ(%fp@zSMDXF%cqofadAvv@Jadhg!?=?`KH1cwic>JMish`t9Kk6Ih~9Y!!^Q zoH|VlOIGzz8%o-K!jXx_5ZcyHC@Qw~p1dVW&*BXrJbj4oc^}a7$2ThPBUy(IJiw9G zoR^tjkt2pr)YsuqTq@WN=+~B!v&Xj-BQG>3Z?j|j74a*1ic%rn+iOml8;>ku#Hsc} zZ(O(&h8#sEZl6Ab1S#Iz!c9x4=S~oqq=9fsD{CVVS0#i_jo*+B>@HkIR)(3Dm_~XC!+c ze#;^QSK@eEjL#l{&e6O#mcwiAFL`M&F}3WX@ifvX$$U2qnsM_#n8dq~-=~j6z0`p9 z*2Kz7XkV7Zi77do_&1W*$)*;F+a;D4-a?KHe&^zlm`E3<%B1GSBUi^rL&=Tef_iZ- zIoKbpN*vXA{X@|lT1oA33ph9D*}U7nAU$_^ee)?k3kW_E*>`s0Uj?XyUp9E1k(#j(D4@1Z&>E& zH&$?kblA6uxbr35a>FhhL})T$LWYmrZ~r&bRkZ#;X1aH?P5Ur@DJ&mbxs~dy9h-h- zdkgPb>p8cG$dKMz(@HH_u_=5X2)tN8&)Bj&Tx(|rJMgxrn z!BE7>;HoK{9+hHyB^RMA!}L$2g3JG;9EBFAIx1x;y&X!rf=N=vL~0koPvLU4(Q~I< zZp*bez(NSQw`yznpAQclU=Vg$)kN74M;Az4qu~;-q{jwWF`ELAK7<-@}P3 z>+qmR*3PgPa4O^Rw{=;Zse5+8SV5i6-4qSNDRek4ub(7c508$1lvrNT+KgcmYq@xs zc3a(dk2?Y%5(&;A-T0>LzK+H^H3bGv#gHjE$gCZ>xDAhwpC7E^cTcK0oyl@K8s^)g z5GaQ6sm;{fL`FoDe5`Z2%;a~5fwx1@IH{kR6zQ%CWUCLhor0)G8QR*~a&+MT_?IMK z`sgG^hfU+zAfcAt6aEl)v}~8+9r%?0V##%$rJN5D-$3HH(|^Q`?}D~ zaxv0QTGrAEz1yDp4`_VD!3&-^V*PBgyHY{c)Yv#v8*$^^0g;v-T(s#Gv?!@MuB6_{ zb@8cxTDCiWR`EUzFXIWP9B#fXPtELoo*~L^cdwjdHQEB(T+UR*)( zjkVI@ILRtbF{ORvHc~O22-cKKa-03mSPQdSpOL;0ctVH~L zNKyIk8c{=*=;?vxEqEI**L_)!4fBb zxi3W2Iq9(VN}99l8i=gFMuySJBmT`fa=;8!NP7LRcFje zbbswR|1w{xi0vScfPEfHHP*2;>g*b*HM^@<+_7-5OV+#S5>zmwogucUL=;S8!%sNr z=#Z04P|WArVi;*WSCYTu4AH!bot^CE5!lA!2H_m?*UA#TPF^gaPOahbwVc{LU7W_R zc38%Z+w9P2#@n!^6KHo15w1;QXPfOjvcdBCzSxP5x3$N>kGdO9r?U6-vW0r#i)NMC zlW4LxaQ{BNXr{n1pO!KuI#2HmKg|z1;fRXNfpcVpye{dp%jj!lgtx|X4G;eSVbHe! zM+};hROyS!mp)MlG841azSQb?9liHRf*|>`NI-512RsZ}7*(DcmTC^#{q2@*cDrtG zJ@AJ|z-bd@f4$^A7Ww=?iO*{J$9MhJX)9SztuS`ZjR}iwhx_9#ZX~%%l&-kZN(XuF ze?9_h}4GX;o@gw&1ys;(v6%GfMxY`8M;(PG~vE(&ZKD%=eYtA0MVdZ?2%nPw!N-)L5Nw&CFbB<1QdobzRUUh!L(PRAPS zVcEi`(@5Wk>r)TzbB+Dp@9XKw@pp5)9m@&c8^p@S7Bs9h1>|iwnEbf`+q#U9YJol8 z)5{mnW@-jEBSRq3UL9?18@+>%w8y;eWjOrPS7VadKk84`U$A_mxs&?OP;;K7Dm`5f z7+1b1mJ5_f@tZp~FAu2}Z8nd13lolxa->)2#gsVlrT;%Q@l zirV1@R{l)VuRD=F6+g{!1NvE%PnS}ccy4Q})RUXBBZdS@ zpM86qf)a`+j~fal=lrvYbKV$JRGzP%^I5L7S^~y|BVQZMA>-;(-|ah7)Tm_8YJAKG z8RlaCsr-6JUZ#s<-dB7AC_m|MB>Qi&6j)yhC(Cf22NlsRS{r%Rv9C z2#|o^-evh4Jlc*39WmX|E#JHtFIvBAZeA-F?y`_som@OP+F~Qxa4>>)V%Lx=XCXh* z{mM-WyysNB_UPu6^_v`~&VF?ri)*%<%xQ2nT0kvuB7{4?7!=>KnNAu@WpLDZ{KRKu zDJ;tArJXR=)a0*=f1>C-w*XIIdW3 zV{9XPi|=%UFu6dHgD?Th&i$Xr{mH?9N$&Lw|9>O*tN$00`$tkG3EI4TeD9jMA+4=eXhi3+R0&DkU~+kD#Kv<_?4keN z^9?l%JyWxYV3<2(o29r6YL41 z1hCZ|`24QgK`E(QE)JoWMZIE;vN}l&kYw7s;69oRdjXF@9bWpe0_*d!v5Pd`raDX4 zW(@&P&6Sh(G^On6Hr-^_IHcpIs7QxdkFkwr9=8LNC?~`vYkZgwC(HVl^7k%98Ys;r z<#z+$3zWTQ&|k7Z4)DA}pq6QJm|xSlJGE8LknG@j3*Vh((H+FhhD}XQrjstQvzgrv z%u|^p4>OHp9=Eu5kizpfXE`cRYM*-;4~D=aKNQtNLX z_`t&J!-_$MTInO4@9gw>$L&fvljYnXoX)!HUlDDw$@+vhNh$YbR|olzRh9Lee^b%E zA2)j0ZNlmem$JU2Jo|@=7XKB|cusHb`P5j#*<|uki=vlcJbm(O0`=Zfa40=29;}9D zMeGbaCBJ$zzS?v|z0;D^-AoC3%Vm|jkTuQXp;oQCKJbl}uIOApMM1T*d(wK=w2B(G6JbISI>+O97*9039`J+a$7qCYyAy)C=K(?6|Ha!|Mz!5`>%IXBEl{91#aoKIyR;N2?i!rp zuEC+DNYUah#ogVDyF+ky39cb<(r2xAop1nxtDUb)_i3+mdh@c%Pia(8gCb{M1L z0PF6IpS>g6-vY8)bb+4+aWy?c=>c}%&|A|m;|0O@;?wx6;oR8X#H6Q%Szo@jovRV@ z?Bj>_AR`hx+^Y6G5TmOj31|O{NQ?T~DP@*#F!)_tWWC9Hq0H!g7k`a^BLbsu$A0p~ z1NdgKc4#{z0e*1CA@n_lNdhLgwymj$*mwc1c`0rk{&xUg|1C0Xyg_T$QuqxX?fHw0 zbA1fLL{V|a=ljnu9nv&V>Sd9cN51#tir;YoGWdi(!<{9p(X0yxcAi%8D6hgbWfmDz z506?FQaF>qKA&@6hjjUaT8LWiBa`Zw-e#t!fh?;79RxcRe? zoPaj|?13{&Tjwa)QtJP7JU&Qk+g0${OEW^Ka{#hp)Hos0R;!BV+tLKlrN<}VCH~-w zV6$+*%K~jI5E0E|XrZ!j3rMYHr~9*SfpJ+?Zr{XlWbCLBY3_JKRq>^Tr$TbNZ=<3q3o|&%Xk|m3c2dJ1(C*lYEhqCuaPBc-}@f|JCf>`mGyZ>Lc?7sWAf7G%a z&ftI4vVSAYo|{BP1mtxVf`hu05z^>RUxg}v!6^&mzmfly^#IS;l8q{b%6IN)gyqkf zZC+7UheJ@5r(TCoUw-}g8ARCtKvPpv&iWqyrMF1Tr%g@LNSDfoK{a{~D5xWK{JQRN zeo=?j2M=m&U^Mrt>>-2&m>b%?^HEIc6FxHjD?;w?^wJ;RrFHMX0vxar^^2Vx{WLT+ zYsUGZ$E0q~7lIvapt-UQNYlKQ+egOPI$C?nQV#a}J(LVBFpZQTV1F^b*$tt8f0EcY zqHW%+NvUxVw|O_xAlnMe-Ed^cCAO)cSs2;o-_&v^a@gmOL45x@EO%k_NW?NkhAqDQ zt^RnzlFAMD&@%GWi*`mp(w%#x%2=TRXrT2fmbRVb$i=)u`D4rLXPk%+pObijN}eqe z<_aG35kvE5Z?csU!*P;`k{g`^niIO}x0qa{j!n%5eDO~kYX0-s2#0Q2dPP?1DK#$?MJ(#p#`=0}m4?jF zw`gjIk92-pO(O`nmZ)&cbW-@HN9>C|301tvNDBDmieyO1!E1(4c17hpp#|W2zzLdk z)9A{}nstqAG$X$9Fs?_=q&mC!X<)XmNyuPFT7exTbh_E8FWx0#M;`UrWYb*oJxb$}iqi~ChHl+N$Wk$>C zPcK&Sye;VOm|Turk~guFB?w9KsCzP>{zOwAjWB>!V(6s9FrZ^_9rEu(~T;V!OO(u#=)s<+le zSo>gzsA9({P?^c5F{L-R`;#yAp69U`@LlqHy_$dTgs925!JUv37!P<{O?=3tnkv#R?1L2`!b3 ziiyOQ5z>CfN9y07#gPnCtYjHjk7$i~P4i|u!J{2}o7>(;hI_%Zk@1=q2wBR?iL~lm z#$vihl)QR3RoTZh=Qg~sL=-93s={oc12fzSom5~-8U8&V$T#zi%y(P-{w@2s1%Z*{ zry>XO2n6et4(^uq#1}dGZBMt)s1P&Gkc zB_FxgwV(zMiD?*Ke)sW>-iNISd4DJ&IeC%YOP!cl*IR_xjF0&rT}SCfEmfV8%u{np z7}ZYXdY8Gq18RW5O9Bgh<+$N-V3()+Xzb{~zQpKH7Yzo@2em>wEA0A075E z_CC`mM>xpHoJ!!A&3)wz-KZX@Hbb%`Kad{!c;y)Gqi z(emDcTR|fRFN%%%_(`@_r&anqY4xCbW}TUP%GNmBx$r3LVB(gidb z04(HJXX!}n|E_DmGQS8-ga>_bBe6im!a(<>H0OwTkn#j^yHusq#*nfg6`wI1J(L)y z^WN`N*dwJ=Un%nh2#^eSS4iy@SuY^H5|T%1^*5Sd2*H#*IXMYf zLFw`bUTi#peqU~m_oaGLXqj=pQrxrC4;A_%W%t~M1$xD0CKiFLjcsXyW#cjGR0;g_ zrpJNzqYc+tTla#MT8qsFKrr0lhBng*L`9X8dW)hX_VxSxt82`TGI_;{a?3q}*=Y39 zYBQ>;7R#B0HQS3Lm~9J4>q)h((=hkyg;5jBo@SIpL^-rnwd=~9Ubx8J9GFvU7iV3$ zKF~f%9)%I#r-9j}PW-F-Y@^xsuqF{0N5iBk3dv_CFv9gL^DPK-YGKtgNpGlDd?O9Ub9L}zM+TEPxwuKkmZzJY#^4_3soGu&hPS%;H+l`xJc&S+OYmwJT_1kT|6qf$M z6GB+IHa4g4c>yleOKDAU`&KsCb<5<|ZZgH*+L}2w*E)8&x|*VlJ?JK7cI#D-y|B;> zQybS_AOI~~q_6s2txYU6&v2u7FuVvpjumf~$*8Nr4ha6SBR&H#D2o}3`;h()RRc+( zpBVvQF?9<|`*6iyIl{no82r&rm7-uPK`gJabY0GAyDB+o8p^3chKR{hZDD1{&iyV& z6W;vYO%W%dqqZ;nwKzKygEowpAmZoHaAkQ zMhzP_s(xUSh)^p!!%x6}>hu8vGJi4de?IIXU$p*>$_Bux?7!J+1_X^TQ)CA{W99y3 zHLSBHn#Qyltnli6+;?Fn&ypsLvA_94Bz(JD#vT95mwFgOeXH2Qil%-M>nmYzn{(7KDQ@4yO1HczfT2( zcr~g!+LY)Kitss=o$$985q@K7i@9^NiF4R-G8&l!aoW*CAzjOhq%F(t5^637{@g_z z;v%njpVkdiHbzrgoeez74aR_Pciw{Zhg=A((O304mg&zQ93VF7XoRb~7ujnJ#9s$8f%#Y*gI(=J$bbh!eg zOb=8$;;BSZxi1IU2`?v4?OwhW^}pm;g`hhgZFE%{m}cw>;>uAA><*L%omFZm9L@*6 z-!CuvPQd-{oxu9Pv;YG_Ql~;a@m$kVZo+3x%yBelJwe5j_&m}p@>`$VLX*?6U8@)r zj#;9EQDa+Wn;a~GlEr~oFU5wVL!inH3rF=oZ_a=*Ao^$PqVJcPRYq;rL>$|Rn}}FD ztKg2t=(~z^(?!K8@ejh!S5wYEy{w4>fg14U6v6jN52sjub#>p9@h7xDMHsYr_kM?nqNiuZrroXAHMt${ zUAl!3!W+DGzXcsXYk$&C61_avn9LlD(pbJb5`FM_x@qMifFqlc{gX@uSMN!4Pej%R z_LP}%)l1k~==I5|-Dmg+U(^m0{9Swk@Y(^lwCL!`OWOfwhbHzNH$fYz0{u5A=fK#t z7Ltrl1-Tr8ZSzCuhmA3A^A|BH{Y<5HXQ;}A1;vqS&ftaX;%x4FPSbYc8}xoN?_u0+ zI&T8uIK7$e1GxZC6j6cvS`?a^(d@4Iw6Cta+&U=kMxK7u|r9b`M&6kEi$VEY3=RQI1;C5?bu z18;n^Xk_o`Z$%2V!y2B5ILfb9w(F;$saV{q6;>K-phMN+E;6S|)$@ z=gkH-oTW%aN?4Qv@|k%OGu~Ur){(Adf3t_=q%K zLd{Px-8iW^O*)C7u(1-$;1Ftn!-+Py)7>?s@RPj`mJR^B@>Qj2Ea;#zA;7ufG@++^msLBP00ZStwF zBVQGYs_yYU?6jAC54}?@eubs1o!a_a(l|2ZD4%2K+YZOZf+2P7Q;MOUnZhfO4+UAI z6Lg0Aq|{%VJl$Zk*Owx`N0K;X5J8P_PZihv+P4#BeeCxs&UB-*Z@9KdNk<1F)l!PL zK7_d0OK0sHXO9wRcqXWQkvQ^njK9Q?7_T+tWF=spQ6XbNi5`g&TeR%2(0N1ojS{@wG9AW6E9~hUtzt);WEvo)z zwA3gc66rn~7#3OT)I7f+`L06Ef@`m8K*>AHqJIB4^_Pqv!;>DpXUt#9oA>T z>-B4%h~kK+4c+ZzWq&q~KJeZF4f@~k;4`ga4O>IsxGXt}m(_{lE94+L%ILr0t`Nr?U&Fmy5s--pi zzQj5aT)W>1@$s-b;83FxT!E^M8+o`N(B2*tyE+1ARDi2vZzaWK;eSvSv~WuN`XjWq z_qy_G`(&B=IC3gDtvpS!zd<;3Qwfm7;UR2@A?8c(c*I@ScKg`GUC?pjH$iPavdxX2 z@T`{vY08nAT~jFk;|F5(ettBE>>0hk5;zM+ozQ>WT-F<`RE$Sjrhjf*S#y0&k_V=s z_j72F8_qQL1lcc-v4$2y0z_fgXV3bpWO=u0I&OLgL=YpxFvEkPia8xrzL8#WSeijX ztjm4|tS`RiHI_eZP{KQCTqIGF#D4JQrU{MWp|kT>EAk71(ff<(1~>PE7?rOWWO<8+ zZP2_r%nYv%OyTouitM(GgN2-8Z~HF2)mGlvb`R@-rEbRC>+$u2(CqmDr`6WZT>DSY zr)WmnpT^FmL^5Xyr?#^f>|(DdxaGCawWZOHFsgpjwqt6A1DB#!EoV*_G_(WmOV*ZK zV57ZpOzdR{{&|T3vJp%MzbhC-HBUCJtx>bGEubCoEZ84u+qG+{UKL;wd@TZ07Tkv& zY+jU*@nTE*{(d3yWPCHt)2D$$r>0A1cQ{|fbQ?J(zuApkdy9FTn6Tguxfi(le5@AD zWp8h{_dslwH?`o^pWtyG-8B@ed07k4${615wa3!66SSI&5$&ZlBBSKc^*AZ099lm)d=UN13;_*63eXFK##z6bYo{dwNOQEIMrST zcjuY`v9p3fOs3#cXTR!Z#{~iE5hAdfpz0FR+`j4#l@T@zfMF#c1IskyQU0AIfYBnF zelMHDCQMRig`r~ZL_251BB;En?bNXvUEYaRHPg34u8vkpE!lGrY`}%K^#0X4z#_IN zOAf}(^wwzUbCDF&cbCOyod?Ri(@;loWNxE!+`xq59_L;TN!66tlzg}cteOsBg1|dT z&3$dzTB@+Z(EnrVvi5Tg%)Z5-!B;nX&U5F%2VNQSbh^tr+~9}Djxv?0kZG)cBo~jh z7dTW{Rc`WnbboG+@wae%D=T(;i|D+|dza91I!IYJa!`i-Oo5)NLMGR$lKGZ&=?f&E z9CN!9K~Mmv>yu20xwY_lOmwAp?r+i$oPhu>9y`PgU2SxL1s%(H&WGr*QYJb;KFjJH zEPn_Jc5i!N0O=O8MqwmGbR5Y!`|QyVC(y4fzi%5|@K4TJ@_y-BueRcf=Tmc4`-v2? zlq{+_o7Ogn`nYAy;_c6In`u7GSk#)Y*c#<@=IWFSIMXF{ue%5abW#xRmnyI-A^Gbj zl)pjoOhzoEiv#lu1Pi7jGLaa7lb4&1Q@H^3snc>YzQ2ESyNUJjEr)Duo#OvA` zDn^D}oO3whzeOk;Y#}FDO6g@GRZr1970@?hD0Yfa+PhADCdE{1mN#;Bw1Py7KSs*s z^2sGtr!dGDW*(}W>}X~hGtP5$zuHfoup4d^^LpWo@`?wkUTdY1^2yIAM@3^59a@BM zA?4g6BXRQRdz>$k|MA_f6b_SFQJtV-{gpvKB?(CtpL;XCYFd-Cw^}X~)H?+E{ELQ} z{`@8P?I`kaqDX@c5737DPye8f971szkhA!M?{C>A5YDCuPI(BM{{7_A-s>_XQE&E% zI*WLX4N*yEEdk4+@1msw*bgby`&YME564Aq85*`7oq^_AG5wSA8P89D=JWiLKJ`^# z;dXm|z%hE&%Q5||3F0e`KBDAmxUsl!G}F%zIU?zA2z_ zx<+z=6Iv^4OP5xKs~N&Xp=-EC74NRx-uY|A`C^0-fD#jIS_uun6$MOyOSZpLW+X6a z$Lk^u=j|#(a`@P^8-iN)j7=7h%kXwdaRDdkMJsOUm;kKhRQaU;A_Z z;WG3V_eD(yktn0=mLDFV%~lbzfe6>6R){J;Qe_%NbG3M^&|)Z~=1*~V8wQdf71y19 zo}uM+j!GjXnS=FdBV>l`SnK3AEb`&fY5ES2@lu`<&k9l$O{cd>BA*biCt8H};LaO& zx+Z=BmAI|=%3FpMb@mMOq?63lL7K_Te+wG3a*E%U@b%LR37b8xIyd#|K;kS{2ksd1 zUw>!U)jRl!#VW$)_!6<$fg{TwdEaZ#DD{A%FKaY^+tS zl$6uD30K?Sr;(^Q6B)jr;W{f7+5ujF-tA+`iue5x{`I$D<=3RKSxygKG#$OYEZh8# zqCKg$fA(IrdT%DaefSv_O44urV?$Y3fsu=l2py4k4Apm2k%*J`Cv-CAirD&#>okEj z=*K2OK3>@bx7&}Ni52_rflW_lhFh#vt2DzEmc!+wWR0c$MVZCypN_^Ku1Pe^A&HiC+XLoSql3T33tl!!v(e|MTff40a;dKjTqpN)!+G$Y97U&g|GYc zK^GdTL)zw}yUu*u9%5WPN-h$InnCJa9L6aNqD|wb$ z6_?r6v@hu!fub=U!$lt27hd-g!YSP0!wHz(JDj|iXy{?Nq#k#QTq+o<-7L}%qRGP* z$9>!LBvYBR&lM5fo7oEPgi}W}$c&rqR%HE#NUKv#k(a$yZG@7lMiGJ-c%XXwA=%Ol z_k^Zs=K#M9KrB0SiT~h~n7rj4*zqCG!U7(_jJr*XUN~RjgfR@o+VLQi)ivcU=tM_3?@f#S z{xGlMDD)(mT-H4TF>vu)>^t-JMvm7`jLFT(haTE5n%OB#r+z&@aK!*jEGjwE9_6D% zeLNpg*8Ke;`Y&EOCz0+WNd?@p3JIyKVAg)1J%S+7Kx{NS&Fg=nIy@V1EePzFbJMJ$ z$jyJ`ltd8Ia~@J$j3bqNDb|DP#EYn-a>JW&m^ET4KIPnqv7PIyYqHly?+^(%4&HH>c|%b&FapOss;_M^gZDKGrC)^Xk$vitq~>h;mE_|BdjRgdk4$p^X$ji3t^O(u8zl}}$EQReRW0|_u- zpWaCSDgs|*5l`PRSJbD}LP|~<35k4d*)(73*#JeLlIA}20LFcGqEn3FkVaf{Je6@* z6INFy^qY=tO0ZIJ%T~ZApYF*a9V)a08M&M0?K2MTfvq9mccD(|ZLK%WO;SvuO0iZ5 z7ft$`z9SgZ-9C9v>Z|eDZPxoT5T-Sr-PjQpe%~glef`(Y)Sc#k%p(B4kLL98p0t%| zZb~_q7X(W72Nn&hP_*)VG@4poeNB~kduJ3R z*@^(ri3M>F^`(+&4f}0`51*~dG=sh8V%4}btI8*`9XV#Z|H0&Bu?iTI)~YD|l-^UK{w8ZEYA>&v?3>dQ=R zbEQ${*PVXOe>RbIDX&TrCdya7rkXt8NMJm-VK~0jMVPqXyJ2N9%x8MqGi4l9%~XwV z;VxM1WwoNG0OU7D@;13cgEPl8XP51G>ND*GT{4Y?g#S$4JKy+3xAI{MB7P|3@3_CS z1^X)|+z_{UoO=XAXhaJ$`{d*+)IzPY;85a zA)^?>N0AyqjoeosLVx1pYP<{_&0fb9uCH40#@+I!SjjZqI)a*3R4bTyV~(ql?$2Fo zU)4q&%x}NBj1aHjsGr|pr4V?`(=1B0kCL?|yd0&vb*SI6uF7mTYy1sAMP8h5-{m{f zzaRWp&cjR54xaNk@S{%cUWjGd^q%^gkKp=k#=LoYA8zg7-qd*tdOKL^xC9eyuk*xD;F>FK-bVa@{!HTqSY4m({y~?~(;Kj0)Fu zb$(H>NJ8}YHQU?ug^Dw$KkA%p<=iWtZ&MHy>yfwqq)&qO0Vs1OM z4$T|dgf6nUVjk{aTB^0nW&XekGoaCAbbS*N3}qc@(+OW(!gxGGV&AtLgu+@rq|-|a zs9tc8aO$Q-0uccdxu;Nm>3wIzWOa)ftbpZw5jP*w{Y^^mM_>5ATha3SyoJBlOMp2e zhx5t7aw~Z6WuIpfCC?|H(}IT2_9l#j>JRdL8Z-^Qyz>!6jED=~d+hJs;h!huqj-Wh)xW>oO~6t{s>ql9`@3yIZq#m@ z_bHS-?}tT13A9O}yzdFm2BWR7NuuNDUlP9kckz3`e{WZ2@vj^V`xRXUxr+|=81x~# zTIx4swHK@7X~1*GY4t(=T|fF)VV=Qqnl>^LYTc$~0W)itF)}^BYb(=WOnX zY=&b2hdK&qYC9b|YL*P}T+4vHE~ za@-jcf&Gt{-LfzPaj}d`#SL)s#S;%_^>{_6n zK)aAR^2yUL8xfFI{=o103>?WH-oWMeFBh^r=Pd{W0>r30&QCys@%e&L3R~Ajn)fh} z;2P`Ut0;0j!Vkf8zi?|hbHMtu1)3iS5A&by{Tt7^XL@c=^oLQaP?S;}AG}WPR^H+A zv#yhMs1=)pD$QLae9NpPoEj`NgKoUgpYM{q&kH zS+~d24p9SF25waz=Fd|lp@$=^p=xa89l~bQI(8eG_5JDPUsgcecP-M#8D0n5Ak(bf zu_Rohegz?rxDE_Z8B}k0)WlG=mLI~-da=^s;BpX;drDg%Q#V7UOvhE=D_~&NaCZ^P zUk=cv4XvYq(Q`X*)xH6@yRz{rsJ~i$&HiSOu7s2KnelwdYhBcNhZP?_o#kh-5+R{5 z9x{yN0fgOVv|?j7-&E%#lNMBz8}4PSgORjPO(%~Onm-y5leoyS=2}x7t7h!~7=n0= z4Y&W?SDHZ%?DxK4xG|DfY}ll9Q4hG%^=t;;7nCh_2C# zIlDUM*MnB7LpN1Ad&WRTWKo=byj_9lg%=g-*NYJ*V}$-i3@v&(KC*L&?3w6dDUO?X zoW5HQRGriySh4xa62Y^d$F4? zzDAwzxc5j@<2^@JR8dg$X!<_gcEHF55Ij8F;TX!8l{L)~bbHc3fvBs$mzUFnhg^0u zn~KhM3|FVH%{q94#Av(_bSPRV=p9WO?LWE>ltI4UH8U=GhGv<@XH7_M}4KF5=m zw0@w1c$ID13QwuiA?P)aYLkyo(Qv}L61#u?%!;U)7xL-Z9)iU$0*HWyYQFJ8V?@ot zIjUnvF5T111$+s{RMsSz)LMtT5Y7;fjI1KBw*48$Y{wjfN)&$jF9s5#`iP|;`ypW6 zMgUh)Wfx-H&?OrWHRU5iy+A}!(V^tcq^BH7*iLiN_?15qhL8BFhEo1b%w%d%c$ z1B~kMIMd>Z<5bC9)qhOz&ZM{h8QJ&?b{HuTi={j3tWN``@ z`^7xn&>vf=+yG=TS!Xc0s)d?PpO={mnDXZ7+8OyOqBX|~IdP8zdOc71P6Km7*3A#hZWE{P zm#eF8kQWF`g7i>H@oBu)UO>8%@kXL!ZYg(peJV^u0(c>->037^(`m#)?m0JPz%aEC zly-iKJn!enVp-w!^#_@H3fynVzhiy;1_cB^W~$AR?_}B)R?;|qZ)d8SFQ$QEwdDxy zAJj#*;XpH~`W4JUzp!4&&BUVuLvB=jHG$s%4QQ?S)XF14aQC|g8DM+83Y$WK_q;4A zV=BzA{%(-DlU97uh6?hKU6s4onK3oBrL%KJ08HRZRK46`!Tu(_y6YmLeEgKQwaF{q zV%}89nmZ^_vzC@Se#mRuYffF)*w-`WxKUiQ$yraqE&9gN76o7%&mHn~Cod%v+bqb< zK04?e9SCRX!-E&zftTLz0It5}i@j@eIARi$H-3x-WvCBR$$F`&HTQev2R0-n<>WE~DFt?9r3ANoo3P6a)->j# zxuDQy6f^)+tDoNlP&7oieUH-Zqzwt4X;AgPDXp)bS>>-3w6C}u)iMB-39+u}UCLzN z&O&XHI~Z^G=8v0S_4kWv{|$_Sf}~4iXb{9KwJR_dp|1f+N@Tq3_5O&Rbah;{wV3|w zrVCYp*?a&oM(77N*Kh(rf6@vzp(7m+z@koOee$4ajuCtcbf}QY-;p^k%~iE)`j-}< z7caM85l+4Zv&QwO03_Xb+;g*rl?Ai=VhgD^?gqLdFXh&(U;V?ov54&HqK9LNS~o-_ zc327F+dJkGmEnD#wSiBq^{5n}GadhC&`vWzoELf$j0m1sK@fbtT3HiNakx>j&%J0ZbvH;|ZE79`c#)6g%rj+g}jYoA}PGcCZlqMOh}a)Ah4Z z08LDK?_OVVAS)@;RXDp~f_RX%c-qT3n_L%oEde&X1<7%bpHMNflyy7;t1+C+u9uSb zzIHNB%qiuxRvW#N z^~%1tx2c+aK|yQHj$FuMH(D5T7s#@+Djfp-()hm1eMibZlf-CPkffC^sJ!vUQ>PAw zm{{<@ao!V;MV4EGfhKu#4;E}#C281Qk$-NvkM(;Ql}Pq{sK|efa&z23Va0(f>LnPE zT<7lkuv;eDPC~~b`Z%|-N!b1i?;IFya~~pJHFEVlOBQLhE$|9{o259*<yL7DV|{ip?cMy%HGdQ-f$J=+w#L3588l4B`I}u) zEZWFW9D;-h@(_6B3ZS8z(_ia8#vUvSx{j4kHvuLldMZcx(@*0}EJHcCktz|CD+$fT z=h7vy&f}LoC*>N*od=*?yZ8IgXbxl=Wu8fKZv4C9lHK@YPg_S1e@={BDEnh(KFMcw z2(-DYX^N~lX4qd1#%w{yAu3g#uTOd9a^~1|dRLP;(~;rSdhuMG$p($htO^0mkfi49 zP}mZ#cqEE$OwbkkW|qIjBFp#|5a&OPk~oSm(X}s7&lZjenEx&U!p7`Ht`!KHLm(CF zp8$03pn0Co@Rt81yD%h4CmwUT9jv6Oqie&x)2WIf;E?R#+Ycc82;Y|mXnZcr=o;#Oc5sbWr^~gm2jeDKb9OlA}hdg zar1xS;-9b68aMmSiH+RVqNdSCk^8=PMmT@ZymK?#rEwJ1iP1N~D6(73j@*D${;yi` zGMoQhD=t(c#R3-7?3}W_5NH?#=tXDX#-0sx@fs+i0qpZD=-As@Qh7h))&HS5k!wf>Wwx zFaXVx3Dn?ctCEG!Ez(vVo)V1yeKGK3tbN&Zmrs9#6_uKp7%;5{RPkuql46;Ri`PIh z;C;>*SG=G5Glr!|%2+XhD3+7WqF8J}+Iaonh2VQXa}u@GlrvmvP~kEM=Fz!O?=(SC zs~<6jz~1va64QE2&KAGP6(y4lRG^Sfu7<9Rp;u}E^rZJre8s>Wpw=sTo$~#s5`_N)EB?Em4fLi$jO`KQ z@J&svez$tEt0E*Rq#gC3^o_e_1JunFsz__OXdCd@s!}<^&;b`0-&HlyI8G-4A0lz7 z#hg7Lx3gSofvBcB{-P@@XWvx-0D;qj=>m2SlYtlFlQV30+)1=%qTs+70{r1CR&L8g zyMxmFk;8P!LIL$}2ap&-)%%H$0!!;}(ECH*4Ktml_Sw1cJY;rL{ft!7pAT z>X+cAs)shjaEO)8mr|iDe!rEoJdEYFrimD?7p}6s})%w`S3TA zdG~YeF>lX}c1RTooXKlxl=89Z4w-cwah*Advwz3RO8bvp>U%UeKQ+J+l0^i#JT<|> z(39xHo;?<15DWN8R>mtD-2`{12g>^7oYCb-1Hf{J&(LNDkhMZvn8A#S2tdw}%--qz z21_)(qPS_r#zu6-?Ss$c-YBe_6wDg47TS#|YQ0mZnu=bz4%2P}SG0qKRRtY`r^AZ8 z>E6NDdn3#IehPF0B*_08ogjqXW3p)UtxVL!eVb$@)(ln=J&E`+Ou^@x*6vP*5Meve zfOP!3^-M|uq_#)aY-V*;WZstYbt5$e#E(#b^~t8S==PhbM!*@n=cUT_M*%13sZXPL z7CWp^IJNnv+4(V8VcbMb7*57$LLyMwL43f7>T+~H%agYdI0>*@wI(qPMm5gao;%ey zM3))&*-Z8(fs~w<6$<*mFYp*qmM`P3H*bmKOiyp``0nz16KvP!U?jH#c{g0J6_)b~ zCIV_#3|~r9rQVAKR#G0e`1iA%lJb zaeizjov2d)=kpV+O_yP;r)@AS1XdMKauLHq36B+DQNcg9_H-&jh53bvKJX1rd$;0L zNr!Mbh1CTIj>#DQp74az-IkerdIyzD(9xC@37j|jV2;ve;G+%)7b3LlQ2$>c=T*ea z>Ou`H#%u$4AFQh1CH+i%uyraicTX9D4Uz7;NSsTGHu}|lfo+ugStcMzx06v^*p?@+aA2L+tY`E&G`=OCV$09-IBGC(jJk<0ZO-jLDT;OMD23= z51jg2%Huj~I@DnmOc*bW=6cKGdv`50$C!9IXOG6OPkB@&4WBJ1^K=_SJ9F&79B0V> zLy0D%po@FS(#v5Fiqe~K!T2JM##oxuWzCGE#7hlHkG}Lo4!wSY))wC;)y->PMv8-N z%^UvS54#wxiesXI2IfQ1rN?#efd|DK5qF3@RQ`)O$#+RBbE=1raF%PncrhQbGcDrd z+r7}3K!8uhGq#-fkF@9_3$d@L@GKM}g_ zB&H1;yQroZv$4OB3$L8&D8JjO2!>nt>d?E0RGL3--F=U zo3uIGC=g8f7T_%|Q!&A{y0Q(1x)w*k!;xxH&?EOrQh_{YA6 zwG*`zh>$=8xIlmBcJ0KwqmW9-Q%Uz1X||Zc2x64JmwKjtv~FSQ!yeLR7nUmST7wre zZ4a@SW#7SR-4-ZVZoAbVBy>eD8=K&h2<*iocVHEGwu%q-%H3$GFXnql?a8dWaW?o2 z&3Fr6BI6+^HlI7-wFzmC{ZEoY6wP2fZS(A@*24mr?PX}uK zgg*JS69w$@3-r7s=ItKu3vcuAJenkGujpLT9(QgAT|R2K>#XT;!!gY+v;&So9)uR> zJ-Ijf_0A%rDvhxk4SEM>h5myu4eA5ocsO!?u0#c&I$dwRv6+UaoAq+XHDL7+bt({E&f}Ipozi{GQsm@49jbz zPU6MyWqZXvzNzo~S_eR)TP!WN{U;tsiV zFiDu;*BxQ1`WkE`Dy*?!Zf8Y5!Tx?2tKbW+GZ0mq@) z@uP$zPdTRsLgc8XBzbuSsinN`u4Z<1p))2Dw7EklLAF~w?*ulv30VYiS`UoPNdj9{ zeorPXmFt}B4EDmVE++%sUW&NYAM+_cj86u}AW;Ycv+Hkv#-tm>hxuC`-ikT^@GtlhUcd;LWFp8G5SNrb_;qkn#%`;t6$fSi(LqM3|Ub zuItAmj!!txNS@&iS*e^GepV26ZM`jbWeL4g zw@z7nQ3Gp#eV7tDp_AZNGv{%!OD-OBC=<$a%F3!*Wz#CmD>bi;_P&PC(@;+`8X=tL zMwI83wlmmeW>aSH&xxZ>3K}u5^Dt)+b%ogtj{o%Luz^;K(>vj(Br@6Iaz?Uluc&F1 z&h(45C751+rLre1u=Z@>*BH->h?y}-Ar$z*Ngp-=S2SAv%@W-%cFqWG+a_P;Ro2~X zk6WvL{YD`G1Wah0uCUskT`#mO4t=*`V7$B59xGy4OLa(W*dG00prEHQ7qrPk2!LN> z@Vt~DIM_+swhEefy^wK!7T=ow&lcB#Tul}?sj*kuu>8`y(p&;?(mX$luB(Q3-%ZZ$ z%lBoYfUL4=lu3$?gD;jUpaPnFeh-%F$}1-ae(j73hYZ#iS!3hsEq6;gaFOETll@LD z_eVHmvvzjopVj?aPN>Y_6t9Z94A=upoy?xns9WcMu^0Ml7i3pHF+_y8$3r2Mm zy%jy$^mBYm?2HCK`$MGUb<{af0W!m$wiB!7fX>gNweieZ05%zB4CXQ%#@C1IetzbDkzRszWBwU%WXHC6W=&1yDnee&_JskC|2Gfz1ZRMf7QJJ=;SupL7g zAzj%OEUd{};f&i)%`3p)bXbG$i^aRg@(Sw|2F@EuaZS%cKz->QZl6t`xPXdSDrEn? z&CZf|(HDTHMlo|&&u_$l?P`}mwuxapF^S);W0U;L3wVYLcge&G zc=x!JJ)TFQz0OV(d_ZAp`v7l0Le%5xt9Q2xxZnn2b6aC0u4 z;itjA3MBnUW7H4E_u-npS2s<-p?=Ma&`Ev8l6r%64x-Do3_Rs=uvR2!|!Aj1U3HB zrMo8ah?cr%hGTmBZ>&$?$0yuB|K?CcY}B~m)r5rdIMY1o-vmWiJ#Y<`C3qC;hqWKX z?tT|+if$6hb{pHxx*sJMs+fiflBv4SDT)y5nIEc?m92!f>$`3^`){-EqH=6+c~d56 z2zys%rjGyE1&AK}%-#ZM6z(bT@Q^&w4nW2E75RP1r3!!Hdzle{m}cVvuh}6`EZL*r zQXcjN52RKN{R{t+f+5V8U)o4YKg1&MggRCI{!B2L`|{(04)8&0-#av~Vnfj+3`ZMQ zuqUvEzkprWAB}3qorLuF*5!*1_E6dut+cTZ!g%hdYH)B6=(}o}xr!j}S?{~!-T|_; zwA>Lk0SU!S5ED`Kh3#3F{TK8<@tuF@e=csl|4RQ`YcAYULaNy*4Z+oA_tUT2L2~AH z@)N?{_{ts+0TXefCWYNyHP$56(%2^X9Axh4{gaw{R}8hpM*S?B0U`n8{`vlD9XLZI zwz$1XEf!A-S>*i<87Q?oXK$jU<_S<<3H9MtGz6f1Km=qg&#njFLO^8N)C=x0amgF_ zN9gEv7*pMsfRv^ch|@(VFzC?$V}90&)6Lg4k~k^7{nSyOQJd>guy5^T^ekcc|8CuV zB!aYCrokJ~4?Q%-dFL^vilZE7*UeqIfB#p+Cpu?W79p!n46%-I2my?n=fCWld`9~J zGqgFc`(K4NJNW+Ecl{4f`wOnJG761+kM<~3zdk>|Pa{~T{)vtA{@qJynrQ*K9Q@AS zoN;bOu^k1`e0frGs14URVL=7mBs|7|2%pE4q)}+MP zDj#nk&@z#DPSz!<+1&`wgvb4q>Sv|sC5 z%QoIKpJ%VUs&ws`TqXLk7O652K^H8j8l?Y=Xs-Cuij@r((t3(z6j5hh>M`}IS@W^} z+=o0tFT-qaw!OqQP<4&K_+6EyS|fM8Q^t>K2e&cX{c@;{{ScjZFXF_Fz zx$lZ;8xNY8ftP`zF-t5RuU!NUs#oe^Vw~QiimFa?N3Jn)J_htOV56xL0tEOR+#iee zvGX|+9`g5&?wyM|yTpC(Rx}ySiR!cVXkdUL8l!>w#i`Qd-sb+>wrxW4%R;nQzkY{$ zY(?B&5rn&r{gk|GE)d`9tQtVOgww& z`H(6#ik*KPL+(>8*(SJo=ru|n(pe=g7Ep*1Q3M3R5 zY|gILOmYlk$A^C|A~jgLYNcTjpkq@KRoF2cMj+v-m6tLs85vE5w$cVU^@$|;I?`jE zsd(4owaF=d`g{snZuLe~f)sg?XKg zde@?Z&pZP^#YO`j;#YrrjLEF??mgAt9}9S+(%lGyNBngdDq);5uQ*zL96k7l(LM^W ztbYivu$`Bnoa0g^4TZzOrZ5Q`{Jq=&ll$K}o7cu)r)mp19i{Km#kxP(idev|2-=!7 zQ8#Yj32SoF?q1PStdb}dGd$|*dKN9B)MXkx(3E??pdMs0v^YSY{P~nmaixTtLP6Jp z%628!{G+_cX>^yvKCfygFHfbz&hD44aoPvH$5JhGGV!}n?FVMJt&K?PyCsFBr%VfH zXuW-D?XkF(h6<{?V}`{;=kIds$bwW)pJ%L7MK8V$fdYx!>rSPf*HncNr>;ii!lJ zLNQ+b;=z!VKY#ytqGIgAXydi?5&v$eUa2|5IW3FF?ru;P0Z7Tb)R`Q-nhwH$on--I zDmVL+oVjoAk7ss!<*FQ+rF z=>7#evY)y|=bTSF!FCXN* z3%edViDnfQY;q?TSe3?EDo*>WjxrZc+}Kx_pxPCu^8-HijoDhK+V!8kOC3gXO3)}6 zoV6()(u!TcMRFcOyaK&uUVtFyPRO8ObvOBObnDeznOzabQCoWYi^iDE<9acoU%Ziq zAMeI0oLTgeoahKCJ&$=UEnHy!>md66FZKmug=31PmssflY%Ojl8PA;6a3 zW?m$D0+;G0{s?y~e6!j>4qGzmD6Mbpj;pLSz#bbi>GE3nRqP%QiI$aMTEH zIKy47;}~{2aG(2sv8jnRpGSHFg@NITkwS~1fLAKgQP_dZn&cubBV0TB69k7O7ZF=D zL3tF$xXl%99Vs=mw(7crhe+3mDOX_f()ubI?AGF*_qR-nn#)eAMpt}#vkdc&J3OgH zQst@lLJgLHn9I1us;7+{6m^+4rE}3jx}mfTT3PVZ7vegOi(@}0?<8Ctc0Q+_Yz~hT z4Xz@jb+YZ(*9Qghz+Oa!NDuhDIFouet%txzlWL6Vd=Z+yXY?6Muea4nLS}AzSl4X@ zJyGh7?FDO(4-ImDg^{=R+Is0Bg-HD4sh%;myn-k%3OYWF!GI`_-6V@c03SKOwPMed z&u%6S1#$EV+k#&1JoKH!<2So+BZF8mO=r$a7z@t7s;_!!SdlpeEf`CdFz;`&m3pAw%C3c8%NOIyLVeUCVm`- zo?{&4>^KQehdK4a6NC6l-t;HP_RI)2UUk8g@i&xoCF=nHomYf5_m7Zu1LNS8-tx`; z-mk_EDe}d&4_#>(4TItqGuR(nU?F)Y7NUxqtQB!9k{0$i=bq2m5<~dR+^TAE&ZyFt zNu|5P*_e2j)mVYQ)MxhhLck-%> zf%T6lLLRIv9x&bCXiujiU}Otl z5ze>8_>9uU6`fw(Y$egXu^K*yh3 zNqiUUi37>~Q0eq!r>Qo{+i#d%MCpu1~elbWw@|I?(rHYg&5uHzh6;_|L9s! zn!#N4#xuC+nX0YfJ$=m^8peS>aRpVPCYGn-oV)4$sN}MZsH6U*S;EQ6P0Fpz3eK($ zq^}Nijy?L=m+_QB$|0~2B-RVx1;#m#Z~ByanR?t#{Z#~8oS&>R^1iI#(7+FWmmPk6 zQbmfCQu|>=y?2Lp%WbXQ@Ll&(dHyu>;*xrE>>;45{%&f;|G!iezLwY&W$6$#T^#qkQNo9Q>Tvt)UA zcN&P~M4H|(#I7;>XzGkctEp-2OmTY(fog4tO^EY{hpWa8FDvitsJD-I{k=p_N7VDj zM;^^_Z^v(>p3_q{b<(f1ui>fSJf~TMzH;{w&AT)zYqCmOvrv~<_!!vLGc+~zg133X zl`J<|CR~6fvw_XPtPId-wQ-rT?k5BazNdQ%XCr9z2kuR`GO7T>)OR)-Mv7V$?C!^LGY*nZl-_*Ijsj5hJ48njcG%mcBc$t33aYODYt%tpWI zQd0`bb}DLe-M;Bt%8HDDIHlU;gI(m+(PCt+SpCULqYHYV$$g@JoGlyKj$-U+2jy&E zAY7o8#q-2rtI7gn@32E9-7Eo|6UIQXC(!K1J=Za zAFaL6n;S~w0s;?IsM6hN$H;^3_gVEeNkoVdF@FfbBgfP1=GB?nC!Trx000Lp3ko98 z^}T!RnMgbf4{>N{rfi zX44yNYz#JUMj;B08)+T_Z~EK%RB{(769YEB=XD*1j683Z8Dg%fVeX z6r-5`TufBKZg=i#^vFS^U}n|io&2~;53~#;9QB-AP%-v$5d(CJwH_z*=`W83f=qoI zM8QyZT=@}OD^S?>aRN6uZ$s(XK$4Y*WrepF;ZptCi=|Oi2_VH-zGOG9RBFv_XTFu_ z#1!0^Hh+k$p~qc6s;ez@!apjD&9?0@U7wqeZV4}b zif!}kHVEwT2?-63!0>#pRe$v@%SJGZSGgAg2Sm4+%3gJG!1*vO0KxvfeyvTs;9@rO zgW<#?oPL6vYT!3L70H0g8e^8O@6&gCp>05?FbM>Pqf?R{VUsWA&mit0jd zzw5noLak7r5f1li@a~Hrjm4^H+d~Wdx?q@FAK|d(0D--l36A%Z?19zjcO*IHPytK} zDz#Dij9?(ziy(8-b$|%>?Un=h!=+rn@=3q*(R3p8Sk{Sc@e6WnM*k9tasld(Vl7GA z-Ptzht7@}_W8|Pfxy>xsyGO#zD{{6+=ctMndQnqd$C^mx?@uVkJ@ki4#o7GabZ@CP zB~H%rwpcy|nHn^F4+^qLDZY)M{i3)4a zUd+wUo*;DzG_zive^t{mOj+K|_u2EpVp{OWjf*9YM}lG?lbvh*PFHD%-qBj^7Jkc0 z}#KJYPXy)amxb=rhm0T?82wh+dP6nc@j23P;M(%`A+vC_&$w4rI z!wLgiz0C!5;(dbfU5c9?G`kZ^b1?@%IlA~Z+Kd={dvIOnj#>TeYo}gm*9?o>`VcrY z2XKO+;8o**>ggo!rtmxlE>8&;<#^u<_F3ob83Zp=HWQ9Rn%cYyrc?WkYu4tha%}uE zz0zSf3={ZIi2$vYX^+m9Jmkv68p_K6#W(zBatE&3cZ^N!_+ZUtq#9kGLfbB`BGbiS zOM`NBQdgR8@=?6zv78*ZT4m;xP?)qOpEB*xz6-;Fzrp0JHkMi2qnzCMwD)lSUF_Mzp&_oYrJ1@YTjc2}-*tG5LVzput+7Juktcr4BG=2q!zHs}-3fpm< zx?H-`gk3w}iYCt=Cvhh%cI}3;DML2L7MKPN**KI$!3yQG(!G_v)uo4<+-9!novCv{ zxe1U^W*gAFm$4xaGX;j6paGZrsiS3uY4$2y&fcUvLQxZPhEqx&m)<{n%ZS9tsa&&5 zq)9-cmEa<0#I30}MZAC}m$if~#F98&HY11jKE+VH@7s^s>5nbUr@NC#fzk#8aKmnO zDZ5t6Ri>zITZWkzwToRbRyQ=dwpAsE=_(`*%DO$A5(B{SqKZ4qQC#iSw?DBajElKn z!}FJP1APvA20_yN|$wm>_& zOndV~-cL_kt{l)UNkZO@?iA);^{>E9Y#1X zt|!|zCud?2f4_QoH3|)AdrDwoZhd29BDjHTVv+#|Mw4HzdXae-+(mrsY>8Wr-CA%z4OoUL<-U5(& zr(4#uV+YNlykofZ2U{P}s`f}WFcj;5!?K`CZdofV<0@2}cyVkSdwWbfqp36>cD6*B z{3FG13xy6Yy#cnac1fZ^U+L|Usa2x>+(d_IHS+@#THvYS7wWn4TG;`Mx7AVs@qFEI z&M{m=3sg1dV@1a~`kSVvjgQ`?09Mf{GOU6^(HgdSsn|LQM@+SIOG-AA!ZoiZEVsW&I&g2WN z;Imf@?Qcem;g+})3e>91{0TIWMi(Ud%8i3C5T`XkkOFsbm!yqGBlR$|){IsSt**Hp z@ENlNDFYkn7rq~kB0a>!jM|pptD}EBwJX_Cn@TvD*`<4P>#c&2=rf;Xm%t=pT{<^K zX0UGZhti0)+_}M}-=CKENy;}cG_7wRN}zobcSX(jC(bV>g>4V5&op)qC2lIL7O#to zV_eJHL!1iFwbC<(u4oznU7sl@y@lOGqh05HpGfOk%I-mv%W4wYi=KGIO-c1x-3Qgh zgT-8Jd9&GA1IE(b)1UM&!O5al4)L6}LJSW#P85a=+)J7@r3F;z6=M_BqK)68JKIti~Y8qPKFO5CZ{t5$St2PL`;DA7erGpJ(A)OrQAChYH zhmD4OsJ5hkhd?^R{|!_WbdR5bCGgBKl?+SY3Lbt_dDbzwA{rOeGvW5y76`GkDEe|5;#BSDo@b zN{<4c;wA$bYvon|5wYV3kcfpD=B-XCNIYrTBDQT0{9USm4YLm`| z-Bg9+Z}o>&Fw@xPf1HA&~4PLqF^GTPs=5gVJ#Y^Zwpp!fOSxrE=2*Lp0e zP;{;E4pl`Es7({AIE0S8_KYpFM?ZY9vt}h`-0NX>U&$!}ckG((PLck>8+Ow9*d@@} zy6+cK<^S;dgS_R1l>5%r&0Lw)KM^5KIYO{4gciiV_O3;y=mdLgDvgq=9q++fc|uy$ zBJuhb3px$(O+!+pTzP^%S5(~+(4?g?WQUnv^Os`?e%OYucCimNo?3Gfms|%IF_w}ly}^cySe$gw)QZ_ z2L9~HdAK*GfkbCKLyt9Ma%$pF*ZlIa-aA-c0*RLQHkk`QQTl!+z;H-m8BV(Wj;wQY z^0i*#na(Gi*enz5yj(~t=vz1+8gF73IBt(^k{+K1&Td`D|WIX=T-A|dBfXJ2+xb%bWwFMdBj#yZP#$a*f6w$a? zjX$t(T?@v8bV7lUp={Hsu(o2M^&rR=rI3y+Y5Z~Y99fseSVz;Cl=NfU)^mM4aANJ) z!4&ufJEoeWRk?N|z4p#zoV&E{eY0-?*X;oOB`hLxrF`no-6&Z&je;-DFc7BAwLp)` zy3soRA@#EgTYGRJlAk57f#tv#$3T#{rAWZnkE`Tc%R{PSyu1$zQ)7%+PH!8ypT}9x zNiJZATrY}2wl0^RKMrbh!| zh+$BAJfNYqrgt_Q3fFrS_@?a)%_#}(;@7-+ri<06r&gUsVC@$4d%ItW$d!w6|XO}U*%8-z_*=O3@r>yYE4_5KZ zMRy*BiKW4g)%V*>WS(%JVFc*;*h@eRZ#b3BZU*;r6B}*|9eW%NO$RMcK4Ghgsh!6D zdeHkiGMNr6f0Fp9#olkHF|O#Rm^%|pc1G9fDEXTtm*zW%7cxva4sQ9Ed($qM9D0fC zXsg@ank5cPBIHx{1DeXft1Enqth)dkYPo1UB_~FJ_NfMzcO=bBJJU%ni<0H0l`_=) z6w%o2VdDBT;oO??(I>&LiS&At^hoH_QFEkH9&;a9JIR(@^;rE)Z=2lvq-c_ew{($n z%Ivl>3G3Ra;5m&|5(KAxDb_kQH?{9iG>NY5uiS?PvL4M<}uR{}DpL z@+aIgVVoYzsnFP$s7u6`YDSp5B7(IqWuR@)m-sLq+J~?FQFoI854=VBgSRNu555n& zZQ__iIIxVRmP}iHk!S#9^E6LDnB?P9?@t&Fnv>GQs{DHQE9kiDY7oZNhP8&y+7ZuK zLD|Wdq2363`Vlof5|B=cZU2fY(^X}Ou$iJ^!A*On?)p@d_t?&AwtPe-1`lX&f+cF| z->hrO)*ddNHwL4l0*yMbO`jfVfoGjaTovm$;0DbqQR(2~ZE>(U^r7sW96}tiu)=JO zgDj2``b9f{g$jv2^Jy}z4R9Dap`v55#YK3bOxQ6{?^L$zI0@DOPEwEOPPwBOov9`W za(fdq>aB4lF%q5CyA}$=XuOA8hz&rnA_W{L{EXbLv8j;A01A}s=Vaq@lFweP&uEi3 z^rfI0Jre(?n1q%*vFDlAaT-DGY8>s1?WJZ|mlD76a4ZHnIM!1bnPf2R(z)+23a4{D z?IMdzSke8`!;^|GQEF=qEJ`M7tNI}L0@#j|E2z!dAFEd9swx(Cne%x%_`iUN?jkOo zgz2vLg2<4an_G%@zpcrZ!ZV*8)Ijc};~fB9yup<&yi6C(dTAJU-o(o4aueJMm9~#* zZFeYwdk|+74{sTWYDt>6b!0!P382TDFG<)LHAQz!224dPqMGNcngaCFvA+y#eIJ!ChpYb%NeXb-MIx>jHfv@1xJ zf5@W%Bxk$~iYv@K{OO>N3_&qaBn6HgHXH2sB^4ZKJ`$V1@e=>OX&y``kNExvQ#P0F z!AgU(N|hPov^qQX$+b$cQl;`zfp8{5(Glyod{ne@4dus4hviTDye87x&Ph2XSa9#& zyHg$usYH~2fJ1z3txb`lI<-a^hXvT`Blqog)gOaxSLs0vYi4sYn2!Sw7Fv@nb&4$p zhoGSTQH3GUcSpWpxPN|{$HG~z}#%4B;>Jol&!)Bz41GZ z4Wljrpr?ZJE*^*U=1&52=)Vdv5QqOQh=Dix9oK~0T5KPjNT?LxYiZ#km8GQqj`s4{uHvXkT6N{5U%bAAS0^mhzzDt`O>cOA>q|`Os1>;Po9*Sz zo)T>pWO3nH1>wZ89WGyTzYm6y$q+6p+bI6O3nm7up7eeHN65sr+Q;tuW&`(y)KNOi z%z^jUuq-{|_ERDW&=FbnP&0k-o6F+87qyGQt6IOe@1epon7t9`Yc?oqhME)09K}ZU zILE7i+iF|{5<>$O4a!9XPrat42G2|j^|?-nj^A~rXZsU11=E^p)L<_W?zwQw+iW3q zU6xu*rq6WJ28({~zg+*&kwfcxT7w4sY?5mBwizfYx=AM3n$Gy1^ z47vX?Y7p61Qb9c4swZBrKWKKYE9n?ga^%#OVQS??-+sv3>ZGlSqgEzKmfCE44&M6w zh=_LrK=sA&kNT3zPRY@q9Q}CgITko>sv6Hx8C+saqK&q}HrQwHqY#3Ff9 zd3adUAziaJpWRU-wHKR3%R?jM>@=`{XA(?>8jOq_yc^kzv4}}l@T9@Q$I&py793!{ zYqpehM?7o!I!VGb7Bvg0Du^4!B7H8Hn7FCl*04*Ms0*k7i)CvOF-;EhDoJ@*Cs zda~BbRjQZy1J}E83tR9L{jLA=|8tW?Ev}{!0HE5RZ>fHSC*8DQ5FN(&1w@&NCoDRX zm5VFUTce)3-3QOL=WJJuj+2IEgu=~r;+#Q`stYZ` z)JN)Z%=%IpJ!C!z?BS7L)20xs?E#C@v{>ViEu}U7@c6fWYTUz(M_zJzzm!bMBcP*W zDSs18MnSX!x&jJBV1MKR&6n4kHQ;>9JHPa&?|OV-p~D0Zt)xQAB12*tucKezC*yy( z0PdPEP#EubtnXhZ1~N8(iyN$J=VMj|^IhJE=*)i*1hXvYy)fY0FO`#%X}{{$=ydf5MV42Vv;hv!c}l^Bxry>0?*`-~O$&rG_I0LXDobVdWqp6WB>Es1iP-iQZEC6@gi&qs zz}+&}>j8JzVcz`WYR2Sf9i~@qGEuG8w9vxwjr+yy?{Vu$ldE**#gL=weRU|O`Ee~4*%UIN%wM~q>n?)KuCcJpPX*ZX> zFS{Fj%WJ8FuR3#5=&{2t3-M#JR(88Mx~nuAs634CJu3N6r5rT%*4u< zmKSfn1(-;6dR$fI2X~1ef)kTFqr1rgqAEHp;=~|bB$lygvnfC9G&ll&a3{46lxMy_ z46=uO^%)McpVgFSj52k{6WY||S&LO2(Gk5!xb}+&*Y`YIN%SDfdfnMqf0i>AgLQJ$ z0XuWkFX3EC6I68=KX{bAzOn}V9;LFRIF5WgQqh3&8tm(`ABR4gQ)9vyc<^1$n9}}+ zF+p;JI_`+<$B0-j5 z^lw9akfwEtRRHE?d;TIS&XD)#@W<54yvMm=nx3tvp@Hm|e0a=wdQvX+a{KtHCI0#D zw*mJfcf5(t9ggH=q>0BJAAGh{M_-~=GUFnI-o;kKOMM+$-=$Er^k*MUD@;~Ld;V&j zvjl{`yELfkXxdlyPuH`@%G{z+R`ttlF%X1!&`v7ixj1k}a--%-N@6lTNb`}JBn~MW zsJ3*6@W>*>Gn?fw>aCm(T@5X7WVw;WI8$BSqaDar9fnrQyNMy{4aR@^W(QV-DfI0g z!cGq3nDfffp3V`lZr&uWE$>)KZOAJjY<2yrfoqNcu=YZK*Q|~HgzUKJ8s1iR`uUyE zMu)&K>IUPk^Wo@#!X=hLJ-ydM@9&h25MC79!wn_-De8}H10KDR>VpH5;*z&-m_>o+ zfuKmvti(ZSN1^BAgk4e!vvk$P@4E>>ktkH}07sX{A8yLIXO0U^h{*=T6UmFt^#dT`kKccdgfp4wg*`DK#gUDYKh zls~`ZQbUgJeM;X)cLeAs)ewvVfnch8+W-ond!uUo=gFa`V`=kZHjcyU{BvF+>{hhfO!$Qw_fc6IjXE{FuPJ!}k2 zzA1o4xZsh{!w$zqI_wwdALF_6LN-goiZCX`)oOf5OUPO0^ZqEC|$MszisS zDT6YK1Tbj&ZDoSA7fNC}uv5egtY3Upm)jiQyRKZ7qviSN0J&GXA2dcQke~Vai-QxX zIANzs7b;|Z@?>)RHFwfreE(*f1CjAG82NGddCqmRFOJ>qugf`P`2l8`tpVQ)r&A_W zu*Lbl!XR(6pN9~7_W>H2r`!-$;O1t0Ae!}3E!BFLp%XY;7? zEK_)T>+7oi;zLqH?tjNta;Ub6gj>G57P7%Gi-;EWpM?)F8GupzMzOcwHT3cEAa0Fv zRK8OUJEHmnvk5BjWzWn*^Lu5HYtv~IGVBrI$`dKpx|zjmbva9V^WLWtB9|IW2^3`R zM*F!Httt!l&^(v16Dz3pdIA$K^C%xdSnPiFgelL3&^9(jZyDS{!C4EhK_A_?~`D zK%FTzoRVvmJA*!;{~$yxUh_Hy8}lMImd_&9+Jg^$r{Coi{@r`>)c37Z{U}p^^w!-9 zkHV+OC&gj*)=O^+DustPC@O0@F#IxA+WLxRMMJ^3@_6F;<=5`X8RBvi!4b;$US(r0 zH4=`Fd%ff~frpd-HqCQgD!)<8&r593M=6@jplNUZdaUqe75Ik#)Cwue2m^sr?*# z{-T60t^B1dcDa2QTK5W7)e=3Y!=2Hz%)hl-AF``uaosS)H*zKKV4i2Zc>EJ|dB%LT zCW#d{G~LZTE%x=BJyV=P1`6aeVE~Dfxbm<$y`(4PE4rswWlcP?M68gz(E^UKlSMmi z+I;sLw;}mV!{nNcVztKGyW&ozSZ}JzGCZbt+zGKQl9IdoG*d7na5WgO zdE(35c^^lloJ8FB+Zf~L=t{V;ULs~R%fBR>MziUMZr6!xioBm?;(k~uI90rbS{o?{BtqUSAc`o}eCw-^3_~ze z9dowPiwJ=0oXEYR2u?EBZ2=e~9TczT4G@r2=Z|55R2k;``u+TX*BVqubv>{GqCL@w6)hv4+ zTt^KAcQZ+6^wY}lY4U=j&KJH;VY+9RY`T5cRg!^zb_)v7{auxB`q`O})2;EOcs1K# z+Wb90Aoa-hICo#oaE34bp=H5S5rG-b+3K@=xC=?45NZln)s1-JG=mG)kzVg%MXT<& z&c3Ne_ugTyV?hBqk~&v?#I+`OLlK(+BgOg72-kc*CDsMg!N6j72K8kZ_M`kWBYsT0 z+SMQP$yYx0oLw`fBXzVPLiHYfK21jpJuu69P}!XWF3S|(V*8rk>i4`B=FjbC$w#M5 z2XC%10%F24`7$<)hD~z>?coagudJ@o8DSb>#XTlmJAc`CA-J4d?!csw_#)B3=r@-u zZ*z;w!dk;v4%x2VRpNMRI=(ut(^m-pgeNKt|L+4(cZ05f8-TJCUCgAj-yHRyI&g4_ zkBp9InN7Q%n`~_%adoP6*)0}vi0kJpop2~3S!nDY7{?~i8OPjtLznf!!^_w)fH-Bv zU_u4=ta;0<-<8DxQZee~I<&9OaY)ssWI1?4JlYIgQ%g2$P23^zN)%oD7u6xmGE+KN zndWttbk#OwrKy$%4E3?R_6R~>uv>QJ{se6kaz?b}L z*eHT@B`HeIcn0b?ND9wGmcmqG70&{ydNa2E*0({YwR_*;{qE@G|3ePC{ju{-XpC)> zexZ>)e(UMzV#JyY=@-i;PYj0xB2WF&oWyg2@T70{C~o2HgKDd0J^^pk4m)!rttd&H z?Zon+dTf&ZHiuVK9;sI5Wc@Uu+zfpo&oN<|GXN2{FY}87H=j6J{-UgN=BPoUC`mVK z4G8)%+Y`9+`DgWYHTVo@8jo?Zcij z_@aZKjvjxDf_I%dQU062C}DD!RGo}}%R57=6b8lRLJ!lt{I@)C$a>gfXEi zv3?ZhD_dqpo#w8|R9MEu9A?ep&qjb3KN3rch>D)6IXIv)Im}j(C`8usbDiShZE1%v zT{oVf!)sIWM9^|6zyK+73!&NtI@RLi?(en0R0v;v{n48(p9nyqiLCm2(^j)!I|lIm z;rfQTne+$hJsbzzJD*Sej<7#a`(e>-j8(LNk6XX`0BEYQmGIN-SvLE%K+l&?e{In$ zLy}!qx=s-^@E%aLJsI%!nR7@tb!bwnpPa2=X910;sL33d?Ok2R)4{6B;5aSz3ZW#K z1@vV#A-T{r7FA|zo~HHa2;>TMU~X3WoAsxWl1x^HIV$-XUJ}AiQ z^$gsr=H&m$Ot55$->jtjB|l6#6DmXu%WdlGlmmKZn!zCs*zb#I2CIK0%;mtn46Cjo zhVx%F1yP6e)@HQAp9;+R+mA{L4m>O`%nLALb>77gj8?VG4?HaU*u-j!5AGh8Fac-4 z)JYCk^od2{S#6jK?vOGw+3$m|3k~Pjkv^&orK4fYp!znvS6N6~Yoof)rFieK5gZxl zQE=sg3k{4aB!vzm0RAD*J9QwFz9>KM)|I>^K=RI5@>#Hlgjamax0x@kt1|i_}ocG|7~!7T3nN4k5Y#PK^dqu zK0vIIhOTo)*C{x^)!A@)YirXgVY0#i>r-Pzu|8c_O1FXJ1P4XjvbB`Z@>m3QjkmEo zhqu0CtZ}=vqXz{(MPgImP0`0zqY|FzZ`xrcxYZ2^2@Y_lZH-wjk_=<(t9opcOcjQz zS9(#q|5;?4b1eAhYD#zjQ%SPg^_87%Kpay%;l>Li3oi+?1v_#il#)vCPw_@J=I z0+!j}VfLddV|K!|HkqQhSI3*k+`F6RP|y-Nfv1N5wU-#`EYkcjH-suaJJDYME;^+lS_wgWKRXMdq%Kr?_62`N^Tu zEAEOtSG9H;g~rm`^Q=JP!K25Tq0>`#=8SuY;v}O6#A*QF>(;&Lqic(q3$JllO%@v^ zEnW0h-IDox2lASXb&#{yCi z@ZhdIzBiLzQ%J+#<6{=7ADQk!EZ1Q8CrxuX$sY)=1)5z6;1iet;3`R`S085Zc4J8k zWHd++7VoIgDOlZ)A+DmH)DeL%i@-8Qb3s)ka4VskFHiO(xNnocuw95MX|cjz?f;+d z9bbprU}P>hD|?u~HZ!h#A^$5aEav%)3P^8~NiEo9)r%v9 zCeA%Q51MDJXew!`oiBev?k8_z*EAh(+ZoHPW8awSbzEy})w&$O=A)4X!SMNw3gtk} zmBLZK$zEiy1H&~lUpoW>=H%$p3SRz@?v1br-$6=MvqQrZ0WHZ zRcnLq-B0}7d3^$|c-nwk&G}&D?xPehKi2QbGshOU+1*}RKhOp zP|0@L@5vsCp;Yf}nb_yRKR%0q8Eoc5WTCvPT1r}}2-~4$shaD$bG5s3zZJnof;jLa z`g#<4J9P{J$>>_%E=5-4T);in?<~Ep`K*RjBU;DfK0)2D3soS|O&&lBKSF6Pl$21tyced3_|iSbMT12 z9rio52E3eZ_fJG%n1;U_N?Q`HA0V$qj;iGMKt1wLg|CPuFaB|se>&q!yFX9-UJ-FL z{8KJyHHFKr&}0ae8fQzIvw8WTu|Y~{?gCl*nPI_D;ugODeKziVeqI>OzQ4Wrx2~Vt zq>ASsdvNr76GOu*&*mA{cTBI}^AFqqDK%XnCub-cKTgQ@I#R`3AG9G+uK!@Be^?%b z8!CgU|5$3obBO;mlvW9aZ;2H^F|4Ra@#*Hu-|TtX^3zw} zb4T<@S6EO(4~=dp33dA;(*3tILLc0n5Rn&3AHSy;AXeRp*&c$9y-zqUTBtM66cIV9 z>WckP$0JJ-9^+0@4t2;vBTL2Fl%3c?PeI~Q2{FTG@fFzFymACSov11pi0EGon?|DQ zFnRDOyy$efv5-7EkE{0`121M8zne;ob5X-Cb2RC@5y?BjupAu;`Q0wN@H4>z7}=yA zo}Abif>VF5uaL?UVak09d?ZJfL~50hYq!e@T-7{V`yjjvQTc^D*%MgU*Lbr#G7guk zTi>Rr_=8M296=Lz2Olibf0{u+8&~1!9RG%;AmY=N{~0e27uQOF zA^K2V#;0v%LNTf6oXQq;5$ImM;bfW`Qw`T%e!=Uft5sKtWJMtm4=a}ag@*$P*y+Z+ z^9*cfZpuT(to)Un%zq~japVA>Jvc2#%;f5L8vDXG^>u$qD7MS!Cv&MTB03l0-t_V4 z`tZ5$*_aB;Lrt7~s}=BUYvx4d7?WZ!t1NMg);)hM#7-zKsUt74M~=BN6y8Un_9w7= zrtNd&W<#SU77z+jsTW=Hoo)`Z&t?(jVDcbp_)0Kaf2(#JqW*ovPka8hv7m1!T5M>0Y zL6Wn5SFW#bYGJPSdaYR-&Z6Z0ikTV%_OIue8oyzarO?!>%o(#oW1C4#3eSag7i<-Y zKf{tiN?CRY_uDUu)Q%`q!Y3D2W+iRN`WtOGg!&_Zy2T!=8@d{e`a27h@PAxL2tOFV z$eyjV-@ibvRBQRb6wxACHnR&wzODnY@ChDCRV*#aO=faRac4ARR30^!nl=b zj+%nI?vbpf`WdiVI5KSLXPXe9RE8arc!9dOI)pT=B7b_3>Xh5t-KkrTHfP&@;6J)f zu8WU*K;m)3-E{RZ+#urX{14pONUWIN%_SSBmSv*H$wnWpmHt-DVSC$j5Q}r>-8w5W zxV!OXI&Kmf&-@@Ksj^mdd5fWn$R|!JZL#*R#0j0L4XMlb_97dsd3KG%%r<@s)#21B zwDY#_XZjfo{V0)uS%wUfxfZ84BFfgcxq;9Ejfq!YN|~AsdRew;TI?!>K(t%`?6<)@ zIi;`Tun#XUFHxR)XKgn;lrH$M)^fMgi{M(fIn4aSW* zf{{tvVBg$yHw8Fs2`K(5ip7WK`m3u?7{e()(LR->nEyIJ6On}8e#NWq?16=3nRN}8 z>ZvD_(iXgrkvBYf;{6(%mq$unb&aB*ePb1WdO8`&_ZtBID1mkBVL8no7+C()Y!2SH zrOm9UCic+r>G#ns#eS?PQ)psk;AAKq;H2mM(rVlz#v5L|^qzXw2?yZG$rai6odfCHGweE#%ZQl0aPm`YOc(_@xuN7TvV zBkZ7!dnkS8?x=tm2BO9WpF%U0(zt1XY*8;{?Xa$|LEFr+sEk#b5Rkz~q+&4Xxu5F^ zP<1A9d>#*DoY$Vpiv_=YcrJzW zbN=n~sB_fuq6$dj77Gbg4djg(YYR9vPErVM#F1{rje z9Dm0O<^72P`x90UNA{&k#Iw!c(l1sQHQe0+D-`@9nEOWc$4`@BbW_3J_v@%Ov))6- z&z3f-K#5ks(QSBD;^v zc$=%WDUU)tpersM$233PjR_KH$PU(CsUCaDNgm8vA&*&*)}_7dOWxK?9LHu#^y0=f zm?1-6qZZs||M=(3x?Fi)^zP8;^--|~1a`@=N^GfxO*Q;eooL=()s=^tJYr5gPmioj zbJkhX$Y*~W&j8f+dwRK{{F5rS!seo>ee4Z7kWnAI9(1V*>8~ySXeS{?GxcieR)VV* zmU;F?EBcJYIf8BDj+TX5FZR#YPf}C|(dKib(Su4>xC*?(0bwJNkqZ(VZF9EUpa3hM zuiaOU2|)n3hsZ0O91wg-z`FAFgeAgoB~~gV*VOi+=Y^?TeDRfwKO=*|LyLc&gBb-e z#c}c&;ad>ki(x2Jyx$>8l)r%q+h|5d`68jX_>`RBPA_onB^yyf zC7SMVl+=jpttYEvxJ{hwBL>7paO^%GUWQS(Yb0gcp9X!qc0*#4!k@#SC3E}Qg%yF zB7r#~2GqwX*}8#L+-P2SQhssBPNSx)>;;pzSwCY7`c_5;Z27Sn&^Z{mSR4QEj?|=m{A>#Is){fRs#h^> zlj6fsf=*NOO1#9hD}AwH_q>Dy?j$#0p2{pXc~xAzp35_AEN7rt4X#sGQq6!#Di0>U zp}*!Tm+V1WQQI3RU~+Wpqg1nF-d#B)o2h(IFvnlJ9vGvDRdhjgNLmgu$t@E%og+2+ z!TjySf=81i= zVx5x+G9QO25(N+bIkpr++J4c9kr*k_#Q{+Gl{uRZm%BFxc0xs)AE;`!K8Lo9vfg>q z%Wr+~B?$OQiy_a1YsgUsYz&WYw%zqNbyU+=i3H)g2=4E#vL1=d-bS04I2nxp$nXA< zkH3!XZm|S)cdgNY5hZ%PU2j*Mkv=;h4xGwX`ZXt#nJ6fNYdQg*9s`lpUf|-7QIjYdz*(>*^$*14^o(JMwXisV*FN$r6nf-xso zlW7twqr(fIZ_w^*Y>Z3x-QRTRy}y zV!6Lj@`Z;;^ELtyaT5KG7d;(Rsz+yG@FnnvH||Mr5D{iEoJTJXHT1jo&YS1xhg5GR~>yUYz`cU$vjuwO$4qs^^dH+=zAVTlo}LE z#C@8LsuZi$h%35$S9ORrgQ;A3HeO4p*)lM=z&cIy(r#q_+fg-{kkn-2P(H`v(yjTM zLssdqAy%#4ZczYWy1U*h%|-MwuKAl*J!s9``pTol@gLJ!NFA}e+zCyPv77E$HMHam zW+PU{$SvK1{hVI{JQ^0-HD*sdEzbxKELEwrt(@L{^3Sh`$EqfG@@JzSxgFdam2xVo zuZC3z?0v+<+~OeTcByOnOp!ovy32F?l^d!jtCQY$ebs%pJmin7zv@}%a@vhN#cI<|ba??KxdmVKwr@zD`l@4#tt2wfnG z)%hJoIfe@D54cv_febFW*2wH|?Kl~Cam2caLOD-7{Y2fEvkq}gHfI^Ml%a|qmveww z9@}hUpE`30ygEg{JqFpX1A=9SUAc7%rE4EY< z$6i|;+5%pTdyV6LUs^9?Yrc*NBor`{-^(0cwc3zzlOyh8$_})FbajJnL{sXfUb{+A zjHEx=%F$S*i_nX>WIQQda`$%p#sDTq4hII&f~ck=U-eKQw*y^uEl38!s?!gA8T|({ zSMWBz|eU+FTxi2Em4cq^>l@=tE@R@e;D2sQSAY=U@}r4_z{ z=u*kxA9V7QRB@d@|AETsc3PD7^q|vHjWsCs??mJA`+w2+o<<|$xBh&gzcHq~uy(GR=TCb4BF z$FywVEyc}y#Qe*e0;>wf2Xi57^D1ixU;FXAkf@P(yKZUrXQt*9y((7b`ao5U8|%1o zw?#q9-PNt^&U)(d(}%70+bPFXEu6~rGo4=)sq<0i@#FD$^kFCOU7O4z=pDKaJu^dY z?>RhjOa8o%@|4s};6E6baPYoc>^e+~xDu1b)EQ%u(++$K7vJb?hOYsz?SJ;i&-?P-MKa;d`dhjeNN;s995#z~`#mlqg zKOz@57a)8$reavLVjGqx7E=T@xJ#?P!L!fS=t zu|}3J&GdR9uGgVQ&mSzf^eA2SPb~EKvBTuFZ#vR%94h!MF3{~fJl$@mMvS-w_UJHu zzW0A3?TD|8mQtZYG<-4c6Lm;w`pK=wIaK5`7Sq*7-hQxE`XI3kF2KkSnDnCOq+>TV z+$1ZtvZJ*fmyeg!el=>KpGITh6Fri&pt#f@BX9?mlrnXxu|0q7oc`Lh67s_AE5Cu+ zex4b!$f4z8vQq5*;$|e6#fi_3P@y(f2d{~p>G${e80X^bb4DFh_tm&TBGtCM@6XdV z7b65ABc95GekF5dCyKAUUe?m!9EcqnrqQ51J|zB%v{I6z^cl*u`~2hLr8-pVb6eg6 zdh#&p3i~3y6l32`zFPX^+LjF}IIEkzJ#@2M6h(}5D=Vp&Zged1J;J}8a-$V4R@r99 z(Y0NVef9|qwy=;?q(EHo+4DJfziukfPoKM`-h6yxejb~A0rC@*jRc|yKVW1F?oEb@ zm*mBV#+&2eTSQhN%I>zmnzU0(`=D@7F2j!KBb@?}fNouGl;r22F$-8})$ zTkvR7%Sk8KVWfq(5HLh3qD_`xIGQxuC{uA*F1{(PYSWJ0FR|;GrULJ_y7N4oH6t5u zj)GSyHHIz4rIw!Uo>Fv)Nw5x)j4ZjEN^Y?FAy&t`kW>1^!e%wa;;EgFScj#@=Qu0U zQ;7+$doE=(cyysWs(E_B8^`H&a~aM2hwZgJij#AWnG@XhIWh@_lx@&MuJ13=1GPUb#{{&H>%sm%~erJRwgHF{s5A2B!;} z&bJY&=G?@E{-4I*^nSLhEWv{diMBkz$)StEWr1X+v$tzz5CSUg`8>@@F_AEfY{HJP zuqWQ_aJhb3^SiZo?uaykety)t25n!@l#AG^=KGnQY5`iG=|(ls=rn9j^Y{K?tyx$x z?zo!V<#L3bl*MLA=Y0F-N%Y-S7#A0@1i5{aNSR`njjKX%?}|S8Gj&ChS{@D&c7Y<- zEAastLfSnJ=*Tw7BqHFc_p_y4NLX^-)2fX(DT^+$FUSozumCcJ=~vL`tKm>acO#cC z{vp#}=!g&dg7spNO{ZmPyyCt!;fZAUV0sm-6+cW2-W#tfmo!~BEeyG$7q26MyP9Ub znc2(Xa9cAblobVm*k4TIQ+g!|>HPrKc#TEsRC!I)Rgu^np_8jpb+IurExJJac^nG1=KUw&QC&Voa)D0r)S}ZEX+3=Yk5SuZY$2 z=VNR{)_EZPD|$^yaAEQk3%F*5VNjnD?-xDdv2G>{TeQX?`&-_zz0pSdqPIgJ~8w(oLa+p#luFm_ zjW|2XMY@=oy-gES)jH4V%laDFgoDSCRUQH)P@rq(uRuo(2L1yMeEtCrG&?jIKrF7FwOrr2?WR=$UPwGw=x!b}UR!h2u0&{u zka8u6j-^-ck&3%SbwjLQVr*rxomi-QbB$MW9lZ)S7QxmE(e9E*{-;IfZ=0PQybPeD zw*znO8m>Jgs{Rx=sLrcueR|c>|qfrp$|c&O{#!HKo)l_=*!si;limMu0Y?YVPzq+XGDnko>R6ByHOE(o-p3_ zL^M*eme{>v(pE}0y^`sUOg#q)z2p*)!(+SDQu4A;-%al2Dg;PC*gdcgBwm&4YjrAp z>OK6Hb|?ZRIGo@jgfY6F=f{n(7%RP?U_7i9&>xl%hF}TK{fwJzsv|as<^E0D28elm zGXhW*)qa?~w&@VraFx*+3Efi_2alEn)gXe@{?XZ>fdq#ZJ!T*y>o611JqE8rGk@eo zbWtCUO{mlX(s_eK_K26?i+y<1#&`V*&=!4q)3(lnBxtpw0&YWnLV^Jxd1qU7r_J0B zW)V73dW)FbbmBv-g*`%I~7_jSY+5@?7oJj+nZ6UhK3&=LkuEu*Ss4 z%?*c=JLxfO=I{-iZxR}c7>TsL^;9}@qy1T)IE68Zh#Nv5(0(jLNG`%K4Lt@quY>B} zIE^<&ezij38rFo_l+hOsV^R9gO7P@1cJxPmo%DBY;{@Ij+ws`QI)+| z(Kx$YiREKlY-RR&`pHKb1qFDve9+2kE{+5+= zsr9ZIL#2FI233pg8WGzx^KFfoS>&giSJtKxq&3z}ksZZVN}bNi3<=L7ciKuL8=(Y5Z@AZ#inl>4#~^?Y9dciyk=-uCp9 zK`a>n%O^gOn7BYGZEmy7Y+POVzGhck!Lh)TrDJ53=%8o-KT|?HbxUtkmgYvP=>+5c z1Ct<}Nv+r&pv40sz7lv*y#ssog;yr#W~uJFwlz2GnHU(hEJa+!9wXi@db4n@X;bLE zg4a^a`B>vej0!;mI~W>HZ86ASXUc6&zq?qnl35PL5FxpV#AYaemS7%Fx#iys!XraY&p>O6~`-S zg7(s-H;hbPbM8h7j_EvOq?$z``J>yAmPtpCg4DRmO`$f-pg4jorqPh))?gooI9U*@NhmL!KbMhI*-BuWejiAYq5SYwdQ3Tskd3ztMSlp>KM%>+(gBWC)i$g`6*EKKwkNELU3X z9i*YjsK&o=_IhVtBQ&-x){;L)nc`gQ*McOkYb9N5A{Sk+RklYgD;xW`(<@?P5F)XG znr(PoAoFng72tL9yGx9`#oC?CMC{^KnvcYwOFV&I&g|voA$%uRcN2D}cu`;3FwWOp zOW0TvwpFAuqS`z@bGt5{T04?*e+VF%eA;M=?B&mIkCbT_YS5Zt!sMD;Cm2~2Fe&vs zyfr+(zt=wdyvHm(h_BZFtx*r^O*igsw&im1iyIXchR&{3aZ-gKK~o%)i>*Cbj-hu; z1AiBIt7>gskL*oD67eR;bCWhgGV{bGQ(PDz~FV(EFq>#i}_rz$TJ=z z;)CLJg8FNg>^BE>*78AUEJlO#=Z%!z{Ifh|vsB~KdeHllF$q<{PVfHa&)ilA{TwgMnXs1XQfgeSUQghs7o*;A_YIdi{KNaDp@(9!r%8+*7w~Q@N zOMMz$Hfp4dNZ$ENz#v2)GNM2%Gk<-O{1+LcQ{Q;tpY4l?4;ELC*)A;`5P$lKTo~3V zoyIs9x)Z~y44xuC@=W1N2meg6;ayG;?bbwq(AE3t*yQhsxPWGg_N{P5QQQUD)XTeEi>l|WjGib1xs z5LeZj(o&^kk`ghyuIZ}p6h-x}iihig>im+4yZOVm0LRyyvv(r}m#ddkN~PJK=2s1@ zO8fin;3|3)M(obV3#E8!e2x}quk_Li8P2){lJ+pMB_YR+TYC53<-sbak`wXWZk0Tr zF#+RzuGE$r2H1GW>iCscCbRSGg-MPD!V%THnfE>*8aO~T!nlboYE7OQmcvS0F6nui6?Ck6SDwbKPd627Ao|Y#NPY?d9!ju% zMk-Ta5whZ|Ti17wQ-~X$X4J4kZ?eZ-@{mVHv=j~UW zT7rGI=(x|Zt&NJP9@MHly%^4F@I>stQfAovlN->px*7KL4LWn3xvtCf#hyy@JaC}I{>;gZ2}JXl`m^}-Wop|X#Y zlSH6Kz3%@;+A%mbfMA8`=J2q~eQi4`%x7B@ad_{>eMoL{%g1JDj(p%z4XIRdO~>jF%dY3aw1=@VdDU|kr#1U-#maf)H2i8m zOrTEZFnPV#c{Gmz`yK6Uqdtv;$pX`AX)0SM&#>Bf08%mB`GTw4bIo+zCbMY#y$Bq^ zJ1j=E(jlw!>jkD3jk2bs4Zo=dCvkL6E>b={MeZ!ELZqeKbag&&2 zMOpslph3Flh|0CW^A@ImGYpHE8pvaOdq1I293V@JrUk>_u|#th)@|Rkn`9Uw@%^$l zy2<|)CLC+^L_MnZJWwrf(vIIQidDuf%M35+$>73;Icn7+5I2Y<;0^ zx|uXT_q>Ko24#QwaVQ4fughq}y34HgE>!Ya(@CoJa`1tcBA>D!I}L()Q&b)hkUZxJ zBK%oTRE;+`kLvpF&|yrN=WF~=!uOmYTXq~>7? zzB>`9MQM;)$-LcV$zNU1>}E#_o9q_#|FdT>z^(DHN%*ro8l;0Y7Ma=whHu2@v^@7u z=g4D!?P(SR+h&jhMw9c4`ReYeV3T6%+rysv+!|ZuHH%gFRp0$>cS=fF4)zNGKn#Mk zj1YfDn6giyTNu3VxRu)r4R9#6HMyb@Hn{7)CDiz z(=Rpfi~r#QaNN2>U{iWO`2O?{gr_bly>a%uQ}<>jD{P2ee!msss&l|a!Pq~i<#nC1 zFofner49y@yXSC^jC)2H^~}Nf#ZDDnuHw^GBF3jJ!|1qN7Tg?3}g z2OX5ZTe^&P-uRQQ{r)@QVf+x_1rO#&J%w-rW!oup{n76L&agDUG&bO!kvVtxyC>v; zic~8|5}b$H#bJU)ko}sLOuS~*zjO12|4X}BqquptsZ z^xjmocpRszEdEEBKB7cWS3WWGSTO59h!;`~4lY=)zfR-eW>!B&Y$w^xxHKL5dddYL zWUGU|SlG+`4qM^NI4$p|_f}O(8gj~|lv^(@>xZYYRYcouQ?2HBb6F-1|7aIv^zL`X z&zWP+Ftop(Sh6)VT>X3TLIeZ#2-tgSb>E$$e!*)H4^JxEs=H7TUMgh=4 zol^ow%-*wtSDp;1rBzpH=P^vDoC$xPo77I?0uCCK3|Ggm>G$*vCsnuNe*pyMMl0Uw z2%jZ#ObX#_Ow^4yxb>D1VF47+j$W=DgQ{%&!zoA*E=^kYGAdd6Gr$EQZ23@-w1Qq>HNJ&k39J^J|>^%I>o1wn))B*=Y4d-YM-R2y+W`^v*J@Z>< zQP}*$F=pax1shH^2jB8vvLMM<{dv14t9-9Myis~TVUX32m zXV=g=wy0u&;(~-UOQHQqv}pqmt7>d7+#7T8`@UL8D{Qn zpK=_LfCC7ewzTSuc4I}-0ONmC}*`tr)B@zvjlwBP2I8aRa- zH#Jw8wbxfdH1uuS%ZqyX=m(DWN%aR_Q-Qs9yT|sT$`Hl*g2z5tNV61O^SU9)T#vh6 zcag|Myt14&_iqhd)I^*!Sn*tnQCCURkwtsH!S$aK5NAJCo$KY|Z%DX9`@LvH7uF)h zdN&=*=<~8C;irW%^s`MRDm_PUU5A4;=bV3>{B9enOQ>S8h$j;&e{;DA2w;&sen_b0@^!soPp3$)1_cG1)31eVGsMosRM2YlrNEEDP%7J* z%O2I>S7g!xK74=OBZ<5(?M0x&ZxmgQ8d&fw7Tn(jdqq+r0qvA0P#VTOR0e~p$=5#w> zQxRNoKjLK4bUR(em{yKE3X6DPOxL1Of-Pw~r1KQ=ERMS-m+#Fv z4F{j4O!9;c36xAY-wf`MKS$4g_5DQX^-VUqFh3f`j?33p)(ztbJzmH|cJqDnN%HK? zjprm)@c`$0ZDSe?N6$+Xerp5n-Q`0xL384(?-tA4NM566CK=PemrTC&B*?h9(R@nJ zLfR`k3+V{euBR$2lMxEj*L&OJAyB{PXV7~M%XnG5%ghQG zNM`-<`9#~DiofV2B=ySj85O_&6#3Aw@#u-c_#Z2TYu?!EuCsSC{S~E2k6*I5WCEAo zQq3~M`}4jf?;-v~MTnhD==-XYVv)&87qDE`^w@qSh>z zqU*B0L|@8E-|)oF=f}=*DA<3+Jn7QDh>I@o8W*^7m7o6l&T#m?loLA@5kvI{iNJqk zT$%p^j|BE1@~{7fOLXA2A2a2jlhpn_Z29|$H9}|B_=~zCbbJpxt#OL6QEQJo4&r?!vB%Ty4b(ZZ zv+I#?Tll=q*zd__j^*mQwpN5Fw92ImwVok20ngg8r=1R@hNg&yvIj>b+L#Xy?(v%V`c#;d!JcXDO$Dbtxeogj7P9`bcp1!NDp(Qs61 zk=PsM9gf+8`<+dy<;C(}mJYs09c&<1c@`MI^y&y=eAU*sA?YQZ+SV#7=Db_oy#kT? zhF{VaoP=et#wV5;9pD(Zi@_fb=?4vF`%jqHM(5yTlG=o|`L^d=G+AS5m5`gekrBUL^KU|`D|hqQLNyS#VfCV!{!mg$DSxC#@fDlqfpDbv)e zzgsc?+G@)dfNlPeQAfespswa*{GF)2T!Qyzqd-dC;b-r=J;-szkF%Z6?3f;?0mhd{`YI zZM4Nm+2{`WiorPTL45G(!*ICEu%3^hpLb^xL*@`#T@aa;-8pC=8GBp7NOZEp}oDI zx9t``o!Of2b|WLoYB>)OxH#N*?}y<+Z|^TJi_=GJJ*Je2{Hcm0C(BP&&glB&9A)a& z5Eu3S?Ejx2jAxDIEyGFfo0V}-KdCLSbuujEZT!ZmLh*>{`%&vGBx!Pnq*|9=;*Gxv zDM^S)kwe3SjJ243VX#2nJk&nU(qfZFMkb6T1~=dKbVT;{h~Ig4%f?-DNV3i>ovM6t z^A-7LPctv4z!QgAWm8_hTnT@5W_6F9ZPb9@5~rae5AwZ_MgC#0%sTbV2LAr1y#jxx z^ygWX9U*b!b76qHO?AnA5%g(hpN``Yj2@Bk67+`U8{Q}HsY1e7df8~XOw>moKugU2 zTlgyi&+D|+lZp@3Ybpw=NXo2FwO6S(WR4;f9s?G?Vw3o1FJG5G(va#Jg3l2nMn-oc z0hC4&xwi7`VY_1z(knb>OSxV&*j69@5>rGm0NP25Sf{8h>$SGI-$;fok$UM0())N- z=TZ-_F`fA!>6I#mg)H2AUPM$gV}*R{yavay%pU(;wBCroyww3uWX!7s$ZPi3#z)>? zV1}Zb*;nWka`nei)m-HJ1Kma2MHjbWDZ--E|1Hu>(|<0#Xf8UrI9z{U%c4v&3^zLD zXM1a#B!r;s_aZKFgOggt1xkDNuLCZoom|nZsrDo#zw6ynm9UfI_j8lVzlnJ0oJ2V| zbH;DsD=C<&LDYXT>D=2j7(ncvBSDYk1aHO7$bc$sN#6316Y;=`b;4rFz6VSJUDdWco;EAoxu5{A^rAqn^Whdzn=jpDr4|b~$i?>;#dDDA!7;@!L zbfCn9aM$UQWNsC=D#`J|L{+XC5s~KI=_`q^bkduhMy%}L1YAu7%`6HGqAuv5IxJNXNmzg|BdZ5c<&}Rt8ZK5&8A6;>?)_?p$`$04Q{@ zMlVun|BYb=)O144*h+aDBpDOr47~fGtA|y!SX_s3ibZN>nhXQutpzayE3<*fURwE< z%sQr}mpu#{GGrp;DOcDLEM<`{J*`~0*C#y8Rs&);+mw2>ujzmEn6bBB&XIQaP2&Ny zrdB>*aaw1O9Iq@GI$KKp?~E>aW%SH$`{_zdQ#x*>yZuL*UM`gOm;m@a1D-L4w`Iw zY~ulmfT@&m8MvUrcKsZ0zQ>u4yYMulM|#Du}&r$zFGb`1^u zH;1lT^4+*$hYK{qPc}?3^bj^Duaf%){F9%6HX>XbBdB;RCqk&c{8M9V6dgxq;Px~o zt92iOW0n8TI)0t`f3)%g7JO4~Bn01wWX-}5Ti2#fC<_5s&oIsIh-D0UCP%uYyMehA_2H>r zaJ$lDOG?<{OHEvp7MG6sL=IMAd}dpde#G|WKGaa@RDfstV+f-0i0j$S>@Sg02J7Vd zwV?=U-_~z*G|OkC55Vc)G@|Q6>nWd_chKa;e94tx7u-mPU-9{E5${USnNqwIl;iu zn>*;I6GBi1hjkHX1&k7o40!8|BL6h^fQ~(B!f7=wcngt`%zSG+n}n?RnfS~3;mOQ> z-;w`~DAB(&iVh{$`xf0c_~5-S_|u@DH)%07T=>X@olWu&1~0vNhA}Z`58W#Tgr8oP zO37h#MkrkdBIe0;j%rDmB@nHOdXj`Yzldvn>dVX@td<0#_+DR6I7%4e=u9Apv{F~{ z`LhrR+?=_`gIfEaUIiI?P4f3 z?om^XP)c+CrP_y7tQsFIqKJ>?SF?*&l-jXFi`K8*3@^-0OK2J|X;XZ@HINe_x}qf0 z$FyoPF8w8+(UND_OBw7|9Ad}Lx7e+9P+MuHEHy*bDK z_MbFQJ(*@+KgekaHYEjWq!Y7tZ>Oeg#z~E;epVFBffncxRS!rm9aItr?=D3pR}W-e zd}c6-&(pGjQ_Mx^a0J2Lzmk4kgJE;Gx&6JLmJe8v=YYolJjB#;^(Ou;jmHNfTd{x}NPP;N-#7K3Zd1%tgK}AfDQ=C706k36f^+Jow@x68zl2^;7 z?T9w&pIR4?S=!!*^pkqHZ%Wwi%B_Z|ETyH*4^>iir5%{tU-*JM6SdG}+tzMlh-mp$ zi1o{lN3SGS0c^K+Y*#!CmR8m9=6-ad^SwMjvnIOyVSe9N#oAP1GNzz6HW3&`i<;d~ zby2)Q3@;n0aOvIvG8W>iE}Em2F|oDr@nd!Rk+%3Vb)-yj-S{@TC0dj3U>9PsjAN>M zE}a_@vX{Mx8^KoxgK;_1RWdrcfox+IffD|2y0GrSt+oFxP_K@kqPKnJr{MXgW@ zLoA{4Pw#ui^#bSFl2@7ZAk)8=n_Uapw^YmNnB0=BO%q!*sjIQV;b<^2kJ{5zz0|VS zF}-r(i{83p1j4S{kdshsbR-v`TEk%QLk~EXj3_;+f;(a}G}PH>Qg*!akPFe%06iA| z0j~2AunM`W*P}WQ{Mn+N^gG)9C}n7&t842F3^27nvIVFQL1w9@CSW4R#tN zMB^ezCwKlu$O(rhr*m#D4C-!2r5!NFULHrc=tf;Cy5Ib*KGEsiP??s+PAM|o37T6k z4N7!f!t*UXdum!RY%blra}G6f=Kx#PB^9T)40RdrBy|KtN$*iHK%f!J4I7h=6r^JKRP;G_%Z2foN+dUB_HNbSoz$g5PcR+VpogOqdrnKH-e< zeYbe!@$vE`4}F#G#eKoeg6iT<+R}We=5)86Qqfk?l8O1fP0sMVk@Q2~?*B7gYLUFf z8(WreWciINl9!_Xwld(=JAx0<80F%$RUfN##!gR|iw0H)9lnXSSNckG~X(?=Ad zaS-Z)&fM@X6xZH_)+Oic$D!)*&{LrJCjgdsq{Q zXZw@C&BW{c6B4f%lIID(Ky>*1rBorlBmUo(s!qdaU7dC)5i~RA@=V!9NyZn&Oe*;8 zzs20V{g>Hz|5U9)vCOY>vTN6W)jA`fW=~TX646nJuNX zBpg8`V;8FXnX?RDZ~5-wIgCq8MR;u+kB{NeUi_-MAl4D6b!mDCRcrU|a6Pj`S~7ep zF2eA-zfLN)QXrk2*1hS3c=<|L&inES5nUK zey}s!T4b6@z=c&rzU{SqCyEU9MGlGkjq64?qYZeY)c+x`xP@nZv7K~h`Z74*HcNy< zV+fjSZbx$O04k$Q)NXZ!)~r@9_09cGIE*~kVD!}itw^f;veirM$ zibf~zK+mUiCvF?FC|`ny*i5@RhOOq7v?&>npIJ2ii-cnW&msT zWi%8&s$8jd{vO_xJ!nQ*4FU0NW7O(T^&;_yr5qK(j^5iy5hi}#lb29@GEUdb?k@IT z$?bBCip95W5E<(zSYp|RYoqN++TdpOBqR0-8>L6|4_mD`V#B7!56(FXntXRaeu`DCQLA8Ye^T7F%9h`Gwxhzt zrzLG-^i!Vq+_3umr3b1jx_j760mV@T5-llX57IXr)3J}p-!ep51H9gJHh10MKPEM*bF z%6O)U>v_;GOTM?&5&2+Od{Nc+gP)v3U_At~*M09iel8GEYh0ecQVhOIAd9{c&4|rK zA+hU$Tdz0(Z8$aBeq;$4tVmtvRE{T`FUzMs0Z)f>E7Hs{n~WGfAuXT=&ki3!$Da$d ziEq;CP(1C(T_!yhw3%+%lzc0%!LTl36w$(BHEBE8kf+^Zy&xG z6rUiyZ~Ja~^>Wh59x;4_bo}SK7^S_VZYjTqu0+_IFBwcFEbP1l&ua{oe0h4Bf=dcN z^M6s*uwa%YQBKqy;Ko)~w_ssIOICni2vNL2WGXp&YHt%aumoaf$f=(9edl(FPA$|; zp&PI7Ip~LP9J1ouTaaWLFBN)QT^}*bo5>Y0t83O=R$PRJH55^7_QYI^`mYxxkPx$p z41;xguc_&jU)xQZnAxNFv{@{=EGHsDmH3{kv-8^|54!mrIl}U1m`&E$6Q9rRh72EY zWfq&5u7{9zC|!y4?t>bf9xnR6sRk92Z~>!^{F^t;gM%B!_~BBn%lH1K3jkGtpHhkm7jp_n#z z!CuE7?CD{p-cA{Kk88g>su`BK2zJ#h42ixZbs_|wVm$%^K$8GntOS7j5`+cOv}_r8 zS*`LKS<-w+;E_|a6~)H5#j?)~*w|aMEvjUI=Q=0Z>y4nvn3*QBHAqGflM{5DZOcy( zS>+aK8#vR%*{*`yT7C2AjpQQVz3YhpZtWzwK=mul=S`|mpv}qskl#R0r)}wM7)znB z;q)PGBSA{hRo)UF&-4jngZ;$|^L!_2RA6*+^Z2G3{&R&@0Hmo&)_S-v*Y_=Oc;0j^zIsM2QX}%bjfK&pC1qM8G9}*nQzXFwGZ8>GHSx-E9tqMPS- zL*0VSTjY0h8^~SV$R;hUA?s!sDsBi1wJra2hW71c;Mee^rcs}Mh3VY1 zlE)D#`rPrcS>U#Wq|kRnFmT(@VFgUmI(emUA}+?`M4V;VvYNR@!x@pvK-wg%Y(<)0 zKMnn*F=cuE`c|p9yEi%<@?BPzY-ms?_f1WFze$@Y&ODPZqY|CZ+pK3Xqp#C*>&^u$ ze=##mrwx3n=ds*Nzcp7ZlTtLGY|mFOUJ5O<(fD3jZDTp1G6HfE%aW-w|596F_L-gd zCo&KSjAvb&v|_P>mZqX)WCS~;bEe)GxG^?vQmEfs%rpoN+O?{R8sE=rFvOqN->s23 zII-AWoY~J)t-jMCOMcOn_8v0>l~s`UB~lB@6iB0H9sFMByY#*=CYv0$`jfU{#aC-) zXJ>#{cW{BOI8pP&tT&Pl(}i8RDs64?=I^tRCYxv8Zdm&oKDe>tI@Ws-P$Ods|Y7>RsKhQt9s)An~VFGhy1QCw8u@clANuWUv=N zi`(l~Emv&7cm12eP<_r2kTJQj+xe^I94Hv5!h}_PO61eoBHHFko|bj;Lur$98qBTw zznFUqpt!njTXZ*#gg}tsK@)-mch}$qcZWc5r*UmU5(rMv;O_1&!QEYhySv`b_y6DV z_s+fb-g)(^c6Igc-SlpHtu@yiV~#nepvoAa6~&H6U4!v6Y0$umxUz=KBV2Nw@1c_? zwVuvhS=bkop1mHC)0EqowdYb^djBQCdMgMBjf{J68!wby(hYxi{F<9v7z2KEa3~XxE z?sXzruO1Y~(GbL+IfCfGD{X>MXn(VEaU}o5AZja!NW`m7`?*c!wIl=fqJ3gfDZa=O zIsOcnS&8r^*{OqZ`QD339)A7%()H&Db{olS9X|FzzBMc%4&ynIM`h=P;_k-d6A$ez<_d6zOXu z-42cVAvHQ%S0s&G@-lRS+ch#Jf_FfzYc5m#w$a`$Nm{jC(^CO@E8@*_Os2D6ObIrH zvQ4RUW5GN8G+mpqgubX`^Uj(c6vy;r@MCvOq_u9Lm0$N;9q7Zm(00`XF6P~E2dLUk z(V}||-AFge!Xox4kw*Y2D|I(41GsZt!dT#CD;5)oaMpuhvA%&3!~v%@Z#4bg7n-@z z4yXT1OcO$GK*36Cj#N3Sdq_MFVya~Ph<|zW+LHt!qj%&n(Arq;k4R=AJiU*kRFk~QgkMAmk0?}lWam$-JGfuGLJ8tSXd-RyTmSHtpYMYt&R;i zXrC`LXW1opPt&q94?lh!j9XE~^*K-rv1`WlHVlg z@cz0mR_)o@GJT7Vk|~sRxy$Ck_Sbb8mrF&AujyUvQBq(7MqSv!ep@kvvY}G?!libt z$IZlI?~{$?%ngQVc~9>d9ZNmmie+ogUgdgqU^_D|_sxO;(K%D9Nzg8=?5_s0=Zp0V zNeQH9?*JjAGSPN{ou;F5j{!N`7_&A$D8d5RLv3pOCTs=0jA^GT@CwJk4jig7NpU3bRc{ z68ZTdz(~3!wc4(hdsy<;%XB6jl6Q<&(F4krKCJb3(nJLXW5tzl))!*=?=y^`!4G5D zA4C=%B_bIM7Ac1g6U7I$n5xfTW%|XXSJk@*4JLhh<2^XJA2BqJ&49I6ytyl?R~+_y zEGA+Yl=!oxiK}EMM?Q4V-BIA}v(IGT7`^&p_b8jLQc6DQjQOc04fdEJ!Y}>^ z)p^ErFLQg{>@dMw!%IUITS=UMal8P8 z&@)TrrCH0!(|H4%c7%8mkVp{G0AO|Y-MES_O8;UURM#r6F~|3(YJ9_Tjy9cCjpA)} zCp`9it!Wv`HPxLy?#hb+&?E9SkWDiFE5WoU5>9$0Oqc2>t3MlHYk?6XS>>rT4cd0fokD2g`LT2kMyY?Iwe-VD{AiX(ca2?@-1?_t_b+i0bJSyJ~G z65CR-J{(_DOPY^NY#Ny9!sA7hxI=L9E||L;c9s#CS*e;Ss$j9<$Jcg88EaF=RCD(3 z&!!1XhfBxn?gPG8@G38EliLF>-?X(k*wuzSK7}`>6J?|uYYWgw$Iu6;q7HkU!&b1U zOZqW)^U$WUi{5Wr54hA1 za!1-~usYs%C1*i;x81(6Jn^i$*E09Z#^EJ-3q1N3dvUU2yhBS7!-1Nw;Q`xWsSNMN z%f(*SDyQkgu*8)kj?$LW3&n5x|Z-SmLtua}%kp~T9e`j^Y&Jl^z+isl5xqBK=? zf=NDQQ{%Mm8qW8Vr4)O2Lb;u%4hG08=uv<&Y_@nAVDmc8!WY6Hf;XUHovxd*t(2EX z57kKNq}5yuP#_gw(lF&0KhwAaFz-ttgj)KAc;Z0=(u1cx;502;Bk<$_U?#MFfKJa+ zY~I01wTcVDeF2tQ_gWc*I+9x(yM6RKY=bj5{Km{uT0dE4Ko(*d1g1{jUmEUqClzqn zkd!6x8x~b`l-zI4L+GW|ve49DQiAc=ALdjtI;6Miq7Cfj9X4mT3nE&vZ7fVaKZAKvX zf^4uLF@uBgu>70D?CFieSqf`LjFzYkt7Og!Ligte#!6J^+U2h$M*Y&;L)Z!q_$bY0 zG*~96aFj@=%+c0s-4`fU;L_Y9cl&UE?_-tDLE1l{usDj+HTaX{QgfYXXPhK_NbslI z^ChHNrMgd3yCwNQmIgo2Z-!`-)OAfkMv+iQ139br#+6zm@AiTAv&8ECb<$t-ByvkC z^YGteM@*`_b^7yg2Q}HIrfy%;6V@*8&4mbYq#|ts4TY06s?b~eP&0;Te+{-OBv;~@ zXRdHkVdE3FbYHyG7N()tT=#Kd+QZ&s-4#?IB{Jjyq|8>5OK*v8@+s|Jz`3MVq9^rygGUQHZg~TJr8TjCdips;+Gy+#?8r>~ffW*Of5H|K zyzn{Rj~FbGAbEqi*au?ppI81IqA3xNR{wbV=W*0lQcLcimtf})-(+sY|LtioJ!z_Y zifk+yY!vI=)@AlPN`eMDmhe7NUgn6(87xOYE~3=tnnssjqgrNS@&0`m*xdyk*)DwxWD0Luv9*dC6l@Tbhj*h|H0heGNFw+@ZoyHV9DrUKJn&v9tuiq*WE1tY?*(>?vY#v zySzLs@ST`6mya^!?a-L!Tb5AkdYJPXZJ34&wqY^+WUa${axGqPpYcIzxii}5M5t<| z?*G~qI5T{ewF^@jROOUkHvO^^B;}4qiOV00@%pKG0s!%qu}{nD4d<5)XoD+PX6jD| z>J3XYoH-IMw9@E|W52C*YUOK+icO2Tbxh(P{Y9l_Hw!0fy5zYVdJv#AjAzNCB&ev|=6QKMu2GRUjc6Q@{yfI2>{-(+QS2*7k7@DcRgkdAg8+6;W?C>3U;cLs`7# zqqFoSBJ$;Gwe3oDQ%rc2X3|Ax=SWGmJzWipxvhl|y{EbNgsa@{PJiE?3kVPYgG*v@ z^uQd+#eIe66*%tE&|-f$ZsZP+AHtoXRcSD)v1gfZ%1JtDmr|N|N*5kv4Sf}eM>xvs zu;!_NHGul+NbbS~J8UX0DI?O~741u+Clq1abTC`|+Y!TG5UwNK<+s7H@50Nqv&X|l zmX|zFTb5U#y6Hr2zJTmOO=?6f#toHGA?wh&XUCfZr_l}?8u1)D*;(1IPC!KDiY{zIC)t=l|?w!f;=3Cj`GIFe9!u*JE3%>e0tLjkm ziIa?%JiKM-k$(a=}^3-g7sm{&|VRS~--oSfZfwaB)uuO$w zLQ(9n4hYTPL>*nRutsL+a!vV}58WTD6wh>~-RNaRe48QXm?J_3c&>s5WO~mHbXC>( ztqt}~B>bD-iF~+-e(+bpt7LQiOn_oqjDi~`K7Z3PRBhG?z1(U0U3naBUliL?0?@RCB(~e)K z(sTb7eYL{FNG)*PdnftF(t!!Zq}JOS)WvTLTm}o9Ez4FzHyOYRFHIr#GF-xv!rc?Q z@gt7P*6>VfZ`Ogrt$q!=4MNi?`&H9N3rOO~-O>Pb?d>{9JmfGGIkI70FgeZV zlE!0iK)-3oAZmv5#3l0M{mI%mXI6WSyLpe)havk=zPt48px~uyHLaYVpu znAr2G-o>zyQz=C@SRSfl1s=s{nhB5~Gru9*z$kOG|58TdZK+y=^FP7XDKh*ETjyO- zTt!w#cHnEObMMJrQb)$k_&c18dCr&(;491%*{?=8jCyV9qGl1=J&=Va*NnB%T_EjP z&cqDe{E=?r7M_3w-97!%vS->ze3}=mtnYZa_>dKPA}suVD3}a6Ny*8+ZcXO70gSJC zL#Aaos-CaS1fv1>t_iCV>XnN+dvmZt1Ab&AauXNy6^}DRt{Nb%Y|9*Q10m~~eO+$C zJ}8(Oho?h}rjZn{32_pC7T{9VxEMa^#>&C+jNt*1Dl}W9oZkJ3?z2^N8mg`quED86 znLdh3Fy1!9Q@4I)ntd2A0s!0Qn73uL)wy2)D!Vqc!8HT`zSz$eX0A8Sfeg+urZGx@ z{2kTVtH4z_py)`km5o^(Sco8k=yhaq0S0WlzQygzy8kt=r_N{&_4_UA!_0oUWzV~| zDVg`7$;Opx%e3ifK}yY{QC6@k+kEr>cgb%)xstpo+aTCT4~PFX(n&M^uPo9XtMQ>p z1@AkiclSU*2##uJ6q|cq1sI`K?{LeoLd8bAKWh)qZS4)*4be;;qT3K|r9R*8`w+df zfJ-bp=;&c!>G#2}eM6Yi=Qyg_axV#briK%C%S-J1^poD;!9|gol@Yd-Z*E=wx`SYW z`4`>+o;RZ2O_wCVvu=d`c*utYkWfMI1CUCrIpKnF1dB)O5c>#(qa^M4`3q>q zah$BVPi`itlG>6%!R9p!Z$euosmiV#s1*GfQIK><=hYa*^Q&oGT?2^1?j@XhaV(Qj zEJBb3IAihC$G~b7<9fZJxFRx;!XXJJiwBfkpKG=tFRk|QL;&S8*)u~8Ly{(`l=D1o zT8ULY?$&m(_aIrp!=8fZp+JqchKL{Mp{}wMJyl2dkFYl48H5BcgK>_$l__6@c{`p= zxxz^IFLrfRy~E8?Md1>M$*tqAKY)oNA#4|~8Arr8Uwuk`(J!krFAR6Na2e*;y`dc3Ts0_8_{Vu04c+PhJJt@zwHTZ8gP|`^xK`|I0TL$4 zvsd4kL4d?WhCfd1Do7klBmrPBFf;>JPTU_Jk{>EMjWsr&L%y{Mwyx z2_@lMsj^HsE4d>>8y|?))9hsm0YnE4WrV06= z&M!d~tJ)?hkg^GPL=eFDYwm>=rnmjrN`|N1f}e~>_)&c9^rJ81@ASet%+en}!z#gZ z+YB|L2LS$DEEDSdyYIsp25ba#`ph9kn}S?u?~8jl$=sqdqQUC=0$Nm|!DU>98f35D zJzwGxtn3yfuMYn2h$_ebf~Zp5JyH8x3xEowDq1q-^i>WSq*6B@G}IH3bTY@!G&U8L z{rvr`PWSY(c>e0;TP@iGQvcCU2GkNByer%az#M6pk%46TA79+@^$!90w9wI|N${K0 zyz1{DSFDcr&${JSK!fg1STF2GGFu)1{F}x`i8kxc6=v9n={KRc;h&oqKf#mXgdH4Q zxhs!$yK!b!7^*#s&{y^FS^sjZ$ykK3*`n=TmpKG%C3n`p-Zc4kYZA%9>IyO0DlcrQ zPriJ(+d3-T{t+}rv7t_J%48l!NyDI5zi8pP<>z$d$Z*$Xc~&+_i0;jPpxOE0*wZ=1 z_-(6GJT9WrQ^PK9mwD~_W_|4keNYlHb$$IgT22NjuWc~x(b8AiqBQUQR5OdT-mJF>b_799SJIhs`c`S zdEo9J(%RGIN;j|^$`a%>ORAbV2LC8SF4uUqtLDZwo9dEKo_3apG4&Y)^=CKEUv*H0Sbmw+X&iMV+{5 z^)h>)m}hHu_`OskArM*+r>vi6iM$%d!LhD}e*#bb;t%>w3p!#(Z&j1i6CdZ3KhJ*IcA{Hq6ZvM5m_ zce@z5+lhFCUhWI|5RV@fu>|wihTSV>Eh|H(4NXs0-RpiY^nw<7XxFCvK*xzPU4xHY zXOWa#eSFo8n`85pZ}?)Jn3M0&lf2I0UnCoJt!j=2h7NLrP;dk@u_dg_GhClu5?_@F zH!gi%ZwRt~!&yEj`kkV{D7*h1;5?2ipK$V(<&u#`y-ZRd;;JBI_st)E5^<-n6eq_e z$D-mhXA;2$v<_WbfR-&02}&y9(|m6jQ}0+~$^8`xl}v|(N8&l)*t_G^FHLY{{N%Kx z`8@1KyJ@KK=0DS2ks?*%`wwBejni!QyD?1=8e#wZL>iQsH zC7G~6e)e<;y8vmUR{r}_>O9S!a6O}bA|l{gG5`FeBZO3D-*s{W^?Q!zZPvxwOW4NL zjyTL{Kkh#I+h{rea2sI_x3QLmIsKt+NN0F*`RV(dC!OF{Oa$%Ea>LQxMb^TlB2vPt zOD`hXhwBCQ7s*%296np5zlsD5V3iQJQU>@CjzV{{{sYZq(mq1EcZbim`#hxE^Xawn zI0fE3z%jwV0SAcRpY}7tIF*l`szrmt6bGWlbKm=xS>oE>u$prNt>Jf_d2IGVt2A2y zaYycp0&maSmGM5euY$QhHra5GT;J7pbPl+mJHv<|s(&Sd!t~wv)NT0pjSoKc2DDr2 zKbW+kD!~{aGSeONaI6^@82^(M?QDDQ@~r(;UJSZGL=X<_fRRH&S#*Ii&BAq3oOI5c zj+DFAjsiUgirTQB3Dv!c>OXNoUdvh-mwKWz6)4~SkUD>kRYgf3j(c^xz$AmyLpsMTDE9@RD3ZRq z9L{(ka`*e1rfw5=1LRhOw?aQUPaUBs>kk%3Ca}MF4Slu(Yc*|LLg)|ZUuonb>6r@F zQb0E~MQ<$0|LGwJ!@jRcSN?U8Dyx772BmlTgDu5lR%LC~paFKL_q_wtQQ4(63`S0T z{rX|;VLy3)roHXpvrku2ttnIdwJ#)6o!ePo_ogMn{~8&2eDQ7qpZhnwFdzNDay-kg z9dkgNa5cU#{+A441iS-49SzZF|tJW2^Y9+MRf=7>NzttUzui(Z?3_75RycWE6 zgc{E>!8Bw5HDV?73;p{?yi#IGxT!KJIZo-;jz?Y8d(7kN>YvZ?5jp@JGSU$sS%@bU zzC2}do-h{;Oi7j`YXt|q3;&cMoromq{#H?%3|$flrV3AnQiZ=L^mG;$WOpdL2>R_- zhWQ7JQpFLRuS?z7<}&k2P@$g?KI6Uto{R4j-QzD3aUBYf3^U-urjFqXYZlu}7eBr@1{z}q=)n0dzf$1sLSxTFkDF^BMYtGqX*y4+o(s-BR1 zgK)dbW7O`){L#`xtQIm*uw5U&P9EJkNeS+>kA7EOcxG2M$)OxsDn;SL$o3vCmEH2{ z>T7cvuirKBD%v=OC{LMlwvMK&jwQ)sp_oqoQ4S(J-Gv#r+4Sl<8-2OF2{((TV$wLM z#Ouy7gJDndhB>P_;&&|#Ww*FJ8JF3{H(-J|0)_bRE4%uc&z$%PWV1~S|c6C%Gr z31K0nzEZLJ$p#ZmA;W@jTbGF=Q*2cyouMBK?%#a61aupp?mo2@cBqCD0cN}@rw@82 zCJvP--D*s!kd~f-C>iuARpZHV#4a0W$L$1`2|OGMljXe8QMS(LJDKlZgYD7pjdq@z z6N)+ULl8H09ElKSZX~`ozwSjVPuiYcj@S8sQzfXju{C{Gr9oV2e@c| z^hFE&iM&V21HxkH6~7)@j(7#fAD`)LP_uuuq(h`v88UI&jB)60NNjf zS$d+F6xf0|A%u-#V14lKL`tQFdT<(J7g^6oX1HvD(?T4MI^20nSikZBdJ{Am7#d@2 zKQ<&EfUOouZc!^eqK}1v%UYAx2+A&DPfPdmoz@rK^45~m@U}7B0F&M_Z12IxFLi0E z08i#KJhV`8nTZmn)Nlx)S#*dl~p1aox0wbiYDw?TwnH)MbK@1qPEpp}oZ#D+EWb~&h;4HZfd zJ`)BfyVE3UK|R&<$gkf)Y_Ztv{eBCHfJ!XYy{_QeLAbne;%$1x(IKN#F#7YC4kqwY z+fn&J{lun9K*mGbOVH>`G%Ez7%hKk39%5FP7G5NoGi!2S1mmk{6UNHW7Teud3T%+lY-r*8mrx3qW25JYu4}?}n(S5a z;wis9XSQ}TJ3ZYTK$q7ootaQ$f|>aag)F2)Fz|?Vj3k5G?lsuS#-0lD1Sm?8p|I@= zejiR;Gr^~S0BfXlO_3MzcF6bxgwm3I_~S45Ml?xlN8~9$YhDT)ioXH?D?U&??|0|C z4rMRk+t*q7|!vA9m-zEN5HW9swNr`Z_65;J^*i?P^qz@4~} zVzaw-@h>K{28xWM>o!f6R+tn(9blSPtWqStS%&WR!0$=ZJd5JRaY)zaJT+6|sYEk~ z_oH(%x?rTbncKzVI+zKA(GU**mWJ?cA7{7v2MuxaU(*mgwS>>*7gmN7cq@$JU34Ms zU=gpe@4qgm(aohrSpfv#FB)RH=KqNXC&mveW!y>1XhXi2$|lzd^#zo56@vJly_3mFG|o$L;s{*D&Yc+gnUts_HtyeC%V;DG29o+!I^d02L$-au>2Rf-{BC zOm@FZl@a1A4JY0!Kp2CD2n}SAEV6Z&`BFe}d#aN@nh56Cl)6cf3W4{__-PuJBZ7*2 zUK9j{Fp-UD2eM*J0m4Rdab@r zXs`h?0P-10i?AfRG>FvbFhZ~qY?DZ7_AQSyaFj|!qU)R(6yi1>-+DmtmUxPpCv1hZ?i1$a2tp1IhY# zD@Q0)oR4NA@-4`L%I#Ny(e*J)8Cx(kF7s8Q4@H~e4Eq4b(nbHfk(26Nv|4J>Utk&1l2S>0Saz)rB!wtJ}v(GPt6EL^)LW%)t|m# zpW8K6zB(j8{X?{>*v~GQ;N3N@8X91)fZ+kTRxjtroc30)AS$XX@DA=xG-9pwL7lYP zPb5UfO6D(4aj3WK@~)I{fgb$kn-0ZcA*w!~wm3NVvYzu`ga;M1ptk>+Ba^4Xd19KC zCnLnKtiK@z-)0PCxXLx3Z`2-&WYa(U{{{xb2mg04IRAeO2A4V2XO4cT^0-Q(##xPKobtBNg#Aj%jTJu3hPfE=Neow71}eMi2;7g=3tE5V0qHnU4HVyuL_C`5_ za9R)&YKb37;+Kr{E)Y0-M(8f3dB>*Bi0=x4ekI0t@Ea z9_vOrduMwuy}uVETBG)Q#}p?9Ey+tz-CF{-06fgSJxPe8BN)AN@gKOU)4y-@sc=m$ zf(&~m?gU_x{|VFfBf^jvlVL{%S_Tzlc+yY^CgE__%%s(aCBwWRL#VE3(vY>%nv8ho z_2M4$*-<;A6V?jYRm&<5hF7AXRL4~zhPrqy_hXEvJJTD;xgK;nqwMx@On3oh`~h0S z0>}gK@W6@J{`6uARk)5-OOto=Ou(ba*eNJKHbu>xie_9+aP_ls6`Hbc_wzvZ$Oibga!X}cRq#wQ~wl!JO#LI;>>5oskKcUYYk8#@(v{c}wD4OQt;Po2B;TiSi~x3EBek322R zyG3<39!XlV<)MuG@m4#7~}Nuy>zm^vs+t&-??{!eXo7 z#edZ|kga9i1qRdmNM~xymq=1M{|hqw&nR@v;jfDUZN6%{k`G>I8vjmq^{6$J52QwsXzY2NF0&#!rcv?Bvs;&QFX2va?6dOFEvaZLr>Yf`yi-~(gMyQ350f=3cOIHt zJosCSA}ct&BF5KbE}eDOCc#K9)Eq^dB_WEQ$+rje@w&nVLs<+gK0?|JUd~_zW~N_r zK5hU&U-~i}&@o_8uxdz(=GWS;DvjZWAe=6f6UMZLG#Fc>|qD7~VJS)g{pcImV zfhg#+?dx+DVEXd=t~q+?DYM8NSQ5m5F=C|plCX%%ZpN^V#FZEW^u_==waiP@aD~jM zABojS^?oX!fRyeTmf-AT&C4ext%W%gra(hMZ|=A&jXMrgZ`a*lzp_reo#mbPUvHZW zqQL;Z>4#03!%o3u+8UBemuI@>!xRXl)b)#xs}8%I4HI2g5Im_m=;?a#iqZ5T7+-49 zwwT|^M86bm>*@SQCi-A#y6b{X(0?aNa9(cKU+B}g++c3nD(?`Ks+%Kq`Q&*Sc=Awz z8QIX`ytf{oF@0!cb>)>nW7s0YZ=6_KI!$giX-pG#^Qe*%)^8?o>p6{Lg)asw1FI4r@a!l8uyl85z7=o-i!!0XnJ2FT$^@ETUB zpco*=CF^Z#a&gH1#|q1WG2gguWOF=UqSXVcaNLtiN7KH-{K0K0V6LW8QEXi1*ccIqURe(STN7Kq$BHf zd5Z{sz-~9mZk=66YXYxyq(k{OM=p22vuBH0;_kU=3DINT`g&P#-BDpGO*5C*fq*ND zTJuxbveX4$q~FzgKBI}3$6nTpA>H^mthzlduEk_krfFx-FlksS6On5&)Z*YbPF`Io4ef4 zTc#i}Ii;~y>f>(cbQ7WTJX!smqTjz`58?yKBz$SGZHc(buu!Wl+AnIk0oZYGwn$b0 z3!hPQV4k+v?KHk*7B#Q^-3v=3K0%Q5m2L&ff&ReNO@yFO{L1 zv_|!{ouDr#&%b?c@L^WxH3A|fqCsq~9O=Tx#IwmGwi4p1nUa6xnh{3Qww(9*&Mk$u z!qjlDY78`MUKufVXPph}epZ*+vPRhwqr$>oLj* zss>X+QF3AKIO@u*Wt|6k>6r7(rT~K4aP(w&EkBEA|I>|(F3JkA?jFA{Uu(A^cfH#6 zci-=@u!G9l=%Q(blT1b^Y4pgfW&Q2-cY5|8lD^HgLo>$)GBfd-n1=`YRgSu{rxAUt z7LmTIwSCyyQ*O0$772M$GnqeH@Ls(Jjl@^o-MwEPNfP!1Li)<(LnrbQ_H}U9bm+yk z7XSZFyJ!oimzCc~ioE)hQx`VhQBU(=2;ubJBSo4avt_ID?T#JxU=|QX-BuEyp|LV~ zMam?2BoHC%>yY%aWvIPZoC$NzM|>;r%KP~9&6&kJdRNvYyp$E%qr1-cf953uv2Na= zek*oo7Qke2Ks*pvZ#_6ML|>wxZ-yWqdUlvq=v70;aJ1=VKXQ27t2SmeJR;~3-Q=`c zqG!q^y`OO1(986AeX^4<=Bq@51QaX0YF#oM$sMv1(53Zac984Vtamys3eL`zrG2PM zaheU;dRRslP9BD&)}IG}RiZb~vL72DSDn>i9u@1jYurI8KBpCfRQ(f06Yw|pi?)Sm zJ|uwY(1Os;mgZ4%Ql-bOhO+8rs6)oqLaVP9&5;SO9ce&~B;25n=Zg+-l~8Snbm4Qr z04@_6Loa&eIZo*7xMB$@~ zCE#uld+=h++H_w{wW zHauH>_SXh$LmOA`aeuYxLt|4Q=xrfVPw84Q0D_*}E_wBJwL+Wrl8AHFoOYi0dzlmR zFS8fc8TG?)d#tIcXOvDx5(&osb5`4TDdT`F z!Qzr~`Xm-92jP@3XvpTBM^DUxwI@;6Ua!w|UCTpX4hvfFAYRp`{q<_Dbx_7Y(hTR- ze#a6&K9c%Q@EQyhGm6%M%;?`)*`2?j{FC_9aV#RMw=Ha(Z>cP={zXELC8@t}6l5SH zt82O1RdD1TsS4tfO(A8doWxD?VIhA4Z3uE&rCsD1|6ixLrF!@_Ei(x|KFZ&*z@lbv zw-sC^D4x2OY(vdWo6ee$B$ll+N!X_+!DsDh#QK#BMMqd7{1+eIs@7;aCfaJ}dYUpqP z=N}EKNoK%}HW~OfY70hW?nkekxKQJe$iy_?Uefwn8N9n=$(JAyMC1a1>lD3=H;B(z z?)nu(WTS>~!KDz@fNL!;ejv3hJwNV;IHQwuDlpUkpko!&XKh*M`0&P7N0clD%sWjs zJp1`FNJ2Mvloor`8xDc;or6>2^cWmt}+IY8eRj}oOFFfe4obdbD8%hx`z_?+4|a>OK@K9=n-Wf20= z3^#&1; zx{2lFyq;zDca(E`1<$>PR|8LWCG-z(YvX;nn3%_gO(;JWaw&M@k>}js(c#VeuSq6S zA-UE{Qm5{86T4N7X=2j4aw`2;!Yujzs_rsrIfaA_@?CzB;L3b9CXQNf))HC&6B}sr za8K6v`)~R2Lswg|3Mbj-q*{c8}jdzn?T32F`5SB(_kYuLUnk zT851_>9kYs4nvQYpT(#Udv2s&=DP`?Fd>B}Yr^G~?# z=Ee0a7>?tW$J>PvFwk=W`BswSeW&s(ef8m*+3v&T9q5wtlQTdgbeH>hUp;`p0Db~z zYA=v#B4>hAz+(Gf+!nuG*~0hvTDOumI1f=FQvOHUuT7Z}@>k~FL*N$g^AP}WG{Q2< zRkR7d%kz3+)a|mjy<+n(nRgy1|1t9p>9c+ArRcOLdNLwc=@q)8%+TnBf1?oIqFK1x zZ31s(4GR${)+%cI!}syc8*D~V z(;rZ*ZH;^x1ia{w7~e9)7Ya6zSE~q`&fb2V%sV3DcWt$tt|swn^s$WUfiym|le)*s zSc<&A#++Sc%PFl=b)1o^4Nht2^6g!MjNig)_L>Q`UQ4xPxKZTf(B zTB)7Ak8B270yy9ZgsQ8`odd*{&5<$Sa(zkA*YLL%@FlWX~7bVO07|*CR^l3QR;z@Y59%U^Vuwj_~`Hb&7}6^F1F#yhM$A74LC4 zn-sbFH6#!ZsOPz|(&>|D#r9>LZ->dXNSXuzDEZlO&U5eF30XQ8S9(YCDj3!e_#Mb> z#_TNCF+cALK;ChLfLtBE-@Ml5L?gN$NQ@+1s1)d{i@0uY=kb*)fgQQK_iIm`<-~L}Zp~7Y&x&Ak#f;`=#5M2H6Jauo|CL|

g+`=vg zKp(A2QGdB{I=gmlESRP8$_$XP^fDfFOLCL_OJs#39HVdXzmfOnR`*}z{pn%-+q^$I zI&G9LTteZ{WSS(CMz_KVp^O9P(dgAp3I5@!Tw1l=R|EhIg%XR++Vj|qD`TgaDou}i z1t3DX967Fjoj_*m%i02c?=NIw?kk_jZ4~3QtoxCmes66iQV~x=#3<^s#&&)#pop*D z_H0-dpZjoGo#CvTc(RBvzrp1k6)-E1W@U)lT;9Fzt5epQhJV(yczZfxRe$}-gi>#& zzGrU0ZrkJHM!h>Kk^8#qcVXzu{2g~ODXjIi?!fg3s+z`>`N!qUmf;j8i}k1?!ev1& zm-i`zIUgAGqTOX91cvNEqeLb4xwp(2Yi?H4{ig}8kEiyo-ie7UYPsyhy#NMo~fUc4B+lm%L3#6l%`dB`)JF?}BHQ{*Ru z#5?2kF_~9&%|-AsP0k9OTL%zQ*0)#~<5lx5+zm11oG7PsYB&`IQ84pg+~++p<77xi z_J7V=KM8tV!RjPLAB8=I)QU((efj=AJO~q8sSSjK(u0}Hg;Uu5!5F*CG&6@KdhW!# zGPs&`Le1a8vgw?92+!-wnM_-L#&ND|JNQ;aLGc&$a~cc=Y2Y0)#p|~^-abO>F=RA- zox^)4=;hOWbCxA^k)D_J>sP-H1d}weQBYE@(CBc1%`@Df>dtIW_pe6puxFn6SIJIp zau*^O%@QOue*4I5LT(LTkGyqx7?C#1Or`uD-HZ*jze}x_+K~w8AxU;^MGxF(*@)jG z-OS*QY4ZBu9D=Kp{Bsa-_~b2Pnu-3q#>Dv=i;9#+N75>`4xG}Zcn1E$i#7M5@Lxo>S3;+bxSQW(D6f3jM$;_Woi*z2ilixnwoh6s50 z6l&!ms2eadT8(91Dr>+G5;vXA{0ukh(jh#lp0w5;Bs63lY`d+@bcmmiT3S$p^%7>m zKH0jfw{1>{@Y%84Q^`(KL^)ncX)zN(QZTlbrMCe_uGdMQ&SHHkH@3XWncZ77AP=j} zVw_dQL66@3((u3>6}Po-B}q!gPF zuZMGnMov#Xp?$paw-Y!3lKidU5xu-DY{mt@x-#F@5@iVSmnRIqiaru zuBnru=^q?hSRc<-$;IG03Dg3|Kr#3TpcDcdz=^^E6GBWQz$dVc5PUdd$1dCjVr$hI zwGAFJRE1LotyTFd$oMC8ehNSK<1@;cwJQ*>LGgWUC0$)NXXY#iB_A% zqxPC1E&T#Qz9-4){bVS70Pq=vNe0j7nD<^6oIK`($O47cxTg-WI}04W(qH!ey?`}L z!qrSzlfnPpc(2rsT+CcOs!OrzCs9-u4^=FHzP}iwc-@5PO(9>IX;?8FM+`Sq9 zoEMdjuozY2vB2-Xfpkh=Dvnovsk!Jvy1=MLvW@xM?^rP3o}ib_1J#2QTlL9J7{5SM z0V<$eP0?O`RgS+>GYKmX*C>Afu3!E>V%Co1Lpn6;fcL{D-TY%QJK&_Jru6_xDaugl ztpaNAw-~9$Gm~WNfoYNYG>5dS@^*yL1m!grZX;ZOI{onyl>VF5(kQ_&y%b0DjWDW- z#L?!+*ev+L)BVVkyB~>ruBNfRs>}RIv1{(GI}L|z@KHm7(&y};+w}4h<*Q{>OD8l9ltl14lo7+&Ae9S{M114aP>2LH zypWhK64?L|2U;x7c+!)3EAz%6YCWGrgmmnY21MHKOw@3X}Y`07IhtpdV!pPhtPi*6aF?l%Wv8q&rGlR*kn} zjEDfW@}*t?p6j8$1rGM(86^4a^F}PcD=MKSA)1AQtyQR^zt!1|jhBA%`y(me>~hFc z4RgC8ET2-B@R1cj=iHJ=fW6>&vXC?*wVV#q*!Vdc`iM?iW)Q*K}Uv+D=}5t~~L76+B?2U_)y6W2E_D6<=!zUZlZqr5D? z-Hl=g1{!ggpWI_7H*#->^!64B0?{O375B6-SkfR2#+#kn!$X_vd0eZ#!u~PC4n_cw zpFnCm8_U7{#${I2nN)v{Fikq4g5mak%{Q%k%>nsUR6i-@`s&w!=IwHlt<13Q7Oj=w z<@LO^&#Y4J!4F8`Q7l3~C0yS?kiE821xw0%v#t8KOzS1e;^_Mv`+h@+(dP!93rqVF zO2-`x70zD=t~Q(mM==hp0KiFVZWF6l;w-u6=V)@|mKW*jUYEJ7TdnuF{*{_b4-M-5 zQ#JkBgnB^p5F((!&xcRia&<_a7fAk6{>jqr_$95He3Fj7JS7xSRYd@={+wy2V& z6e`!ct=OZd`N4r(%B;W^hi;F@L_BvJ>oHC|n_)eC?!oJgAz4^y{uhwS^mh)7o5F4o zAN4fW*$}fQKLQ+U^uz84Tb1VB{wVG@U}2Q@sVjDQEv+o?!=KVWVyHglQ9aqWFj;yz zESNeUWe%4ihR4P`s<3-cCkhi%GOtb3X$!RQ+fC`n%LjPe4q5T4$xFGL6)(SYh9K_| zQJ$D0F|18j3&w0w*f!;z*b#uu-kN6I%d5EEe!Iu!)6$KvzBrC`3sHoP;Bq(Kj0;R^ z)Eh<;dP~=Efjcsun1Bq)k7zb-){fN?sj2|E#$WzM_dh*=b9rzhXt|x6>exnKjnn=; z{uRKMKgA}}Uk>kdt{+G&d-IUBeP9JuyFEZx)K)E?7kSgso+S|}=XviZUQ0i4)#=Vc zzy<4`G{BYd1YtI^xf~m=(Yl#zjGnxDy0a}8D2&o`;w-t}Iq$L7m#y|$G~N9=txRBi zTGKo2B@l4Va?r1Y{5n%!Q~ley3l3s^`KVykU0_L zJR|}mJz8dOiP&bUTEH1Xio4xMOzTX#rxts3fY3YV?3N7mK3`wi1eeW5nWSklCwkkb zb!$)`kKr?pa5ax>s`tC(%k8A-P-`GoTZmUHEs8c*Mt5iMVjuA;sddkq+Crcbmq6HO zG5!ZvYKe$-m@K{YiYM*KNscUEh_zY3j+dd=LV?)6Z#uLGNZcEsdtg~YbZ*G6lOGBOXAlqs=nr6oh@$whnN!0qIm5#Q7+~U8yz538hdH`db`W<-%0STUVIQ zH^6cvt;_{Ak^7b58QeddOA?gjN)r$|dq+Igx{#J0c7}xSq4f)%q3+4lO7$HIOSOME z#TIs^Brx)h7oYbSCX^&q(xnl@9i5ZcO!H|Qa2LaSLuz@S70AdWi7B=mfG>)2?<|pT z_e>--_h$m7*iX;m-Y*B2cz&@|iE&Cxx(QuJdckQC>JWNzr#ALk7!Xsbh~HeFSoub& z_3O(or&f|yA>0XH*y{X>=5z*N3zSj2XmdAzOxuXDr7vA&>dBF1pb54$^Ne#-+a}(E zZ_cF1YxtT?<}BAEC!AdAFItYw+c-4}UpM^or%>eTbgLz(lyN9Vi zJ@WGlzil9AsjVpJZ{Q2$R-AW=Jl9;vnf-{q&Wsr4-l7^2@3L`|&d924nf5*~w&*N( zZJe4ttw7{#mm;^Pzn;Y$fO#4r!M{|d`+FF}iMr(8Tnz`POH(=^J(QaX`qP~If->wHSwyVOHHooMYm1g?PAi{Q0v8X0w z4els5)e+_-@Z2ORU#cJFVfOT3(zH@@O!V*r;rSruaz3|AhxyOvwbo}>?99g6%?<4F zHL{k&&&g!I(;T(wZVWectY<{s5YavC)YqWP0j`C=A1m)3BSO$xtyVZnNbWy+|Rc3doh_zkD&YR66gyV6IzxM zKI%(7s5z#nN~KvwPS2pzC_RHyp78Sy3yYJi*q`({+NEx2uV)2Anf83*P@6Gy z6`~OZ`0e~wEO~hF_7qQQX5c$+?wktRi9g;0B-w*$)eMU7nWa(>mANM69r_sIZ0}I9xB82{zNez~4F*eXWWt|B2ODsOWq#0yN>86$6fSs<%lT`&TP#F&eR`@H0y{6$k`f$P z(>F*m$OxUV*=^)SQFTR31ccH)odoPy8>`<}{@A@XX3rcuhN)vYWH~^wLIEv<*YJlm+$x89^mH<+1mKP!{m9vLy198zyb z@!WOav@DZcxE2@}?2{_O3wGP@8O6=rB`bgQeU8M&(jW5LS#Wd;JFdlO#v=PWKm8HuClVPrBVv|QblL4`WFmmkEQB!p z(6JOrG%aV3+lYA0+`63ICfFgZ`&=_0m(?_0P?Wmq&M%2=@lPLE9YRe+z#dxF#qURW@?k~y9g1}`LC!)Kwe+1aU$x!c!-QM#3@Cu!9+=ZauP zywD5ca5!arHt{b$esbG8jq)75iG;)X3hUkWsH_ZB#TF!mt%1z?TCN)PEz;`1b6#$b z0Dbqp6I^29<{SByy4h=_!gFR)Z+?${sCJXw(Gf*q#KFpKOvQOxeegt9AAcg)@%#!} zZktMe*Nc%-l1?IclW+dRntHEYNUb4RRTk5l%K97Y z&Kadi8~pdnx6hgtUmh+7>UiQt9R=HHWBpCmT1lf5x(K4o?~8>;)j3GH(_GECR+k*!3yL(v!UU$KtTarmmqC?GGQaeS z5!L;~G6g(0LA}nl9gBo2cpRw$B@|FqLQ*Fw+x+r+T^;23T-%Q`C>-3ysygStk1kW= z;oMB|ShQ(icEEK6ixlrn25*G$IUJQqX5UXn-#x~Aok$4Xt9z!pZGJ@%ANFOhw zp)@0;{($Vn%(ICJsdHM-i z2R3Op4#6S0nwalQp>>et!aCH9w=jaAg|>IVxow3D9suT-tt=K!7gvu2gNIV;y_GKV z@+SmwscI{CL}XG?BazM4RzSX&BR!_Pev;tn0yyr>8B4&gbDvF;1nE z3ej6fN7v3!$aXtj{1-u)DT=Z&i{!+3zdDg4B zF2TO#L+;kCO=d%i-fX7pkDIyQqfapNaT8BN&L4S(Q8^zRX1Y<#ehtbsyw9D9F34xJ zPCLeQQW94Jmm-J*evQA$c1&&RjBLC<&#ZnaOW@XHBHC}O?ksFl>v^SggJoG;A}sBwH|HuH{l|BBA=lAUl1@VRSz{LI1tI9JWPfZ|4rPnIK<=)`D&dZoHE(L3 zSZkV3bjfWReIZ7xk9xg#ZNFg@2ne8JwQOAvaD~xr^dPKbJN$F%D4_1 zz_|f&wsW1)vffWr=jQ)|6$D&_VlfQ4SbLJhbpTi<8bMtC1`J!0klE*^H02-5Q?&rmv zO@f{3lNKhO1Zmgys>OOqVyDBI0cT4Yi0t9yx3A$UebAsqU;ZjOJUn42sX8Mtw9ABN z6DD(ef!eD`SN5YeIwt+f#ZHHLa zCLnrs(uLq7Z=8@eQU7EhoNP3|SZ6$I{%1RhZ`j_+ePsK{)g30PehYm4{6U#W?(B`_ z6{usd!Mm9IzF#YTWscOz_E%9TL-`3-%FgQ3xak2~C*_WO?x6PK)h~xf5L?%j$W#@h zYNfkQZHz&LQ%bXm0R$VP8XYJXG!xK^97fkM8?yJouH6>#;{E6ScX+sMJx+m|Zt2}E z@PjPOr{J5fcUh9l!U}RLX)XO&NG9Dyn1(OI?(vqV8kz6znHYZUE#FFwtV9*JR+=lt zQ)hzqbJ>$lDL<&h8Fm zJ&d&zO1ES&3vsJbN-|5w`t{$sV+9dY4;uqNWhdOURB!THN@WL6P5HKf|?xrQtc;j)hWy)rp8sGHFnm; z#Li|^myaIvI-LFT7SGK6hCdb`g_U;wS%*{#HhHM;waIMSuIHh(VOunH;cD6Qx}QgS zS^epH`ewgk$eQm#^7Sn&^W%jj!ID%<&4w2;k`;Qq*IiY}k?()?r7)vm0q$nj3A&Z;}pGL4->%F=Bj&3U!*zOaq>HSr&k5Bhs0?PJUm z4gML(hzDeIjOc>n6cO8pxcEoYd~FpG3A3&`k^A9m6%BbGeRs{EV!;du@y`cupN~V_ zRkMo0jWgr+gMj{g8}{wAfX*4jiV!Hj@O=(UuX+R9kAqM18AVPdOgrZpL*e6Vp^QmY zC7bX&Kl2BW{6_a3Gzog0pYu@GPd6o?c!Sj>w6wF+Vew$r`a9)V7w&y{ZLEE9mY|hE zO#IJwvJrz@;Tj~BUc;j40XB&xs-;MJ1<>9)k1v+gu&0lI=xonmP4zT~fCQ_m^b_?* z=(L$~2)A~4dba*&%+kf#ppw^Ed(nkh{KVoRr8jSSH(i$8-gVQ>eyyr~cJRR}MhMn5 zGZJ7FsIr}c5jXPZOu+8&@Rr!6Mu&8eiEe|D#Rt;;iClN;Od=wK^kTbo)j5soBz2_6 zL8#fHb0=_908^r$b;0SZl7pfueR$l>76#p-6r`l2d}salPUTHcoI^#@h<*95WmHYe zzx>oi2jue(TVAo8EVWJ1(>?6^lq!{xE}v%=zFelE3T$qE`7xVJSODPF93QB`KhJD# z2$_O?6qwg(SHd2kTz=X+C0X{JsQkJBtg8S!d(;W4BMO2Gkn7Xljcyou)Cu#KjCuX$ zJh1{7oMhE)iS^gZMUb}d9V|Be;+s{Tiq2%Kdn$6}O8QR%n=@-JO#Dwa`|96u)qmpv zeL<|LB`kW`oWczI+lg)`S3_FwFc(88#sbMbt^&dp8H_c(ul-u(TPqg~yzk_44+%Ipvx4}}>9-gXy6SYLZt~=k#^kteh=il#m#CYz zm_P&2L72wm^=LgM+l zmO0dKl{ zc^YhSZYBs^Yi+6M(ZFo6i6d3Nu{^=RyxnnItVm1knQs{m0u3ovk)!HUIZG)KAg|>` z;l7Hz@I#9Nd)}|{tCcb%@mVoFY_et7^yE<|#v;bP@>!MSVNk!qP^ zmjs=mR{J_}7RY$HC31{!3YLq!A39FE;F|%?6k{WcdZ5W1zaEK7-GEqfp^M9j2e#q> zRp=YhjX_sVRma3h*G((;QR)gJa_rg`*S9;s0P0_v$;i`iZwPdT;{@=f1 zWzbBib)|@>*#LwGX%PgU4Q};&2p==T2%V}Dd|Iam%67mdtmPK-u3OSvSTl*h z?`$<=lKO|oO;lLa&C2U%)Z{*-x9NNS^dUD}pS!DZn|T}%gB8q(^@APFsD!eKcLS2= zmB|G>^V+OVy|{Kq)C3TJAb*rzAqNpMvS7oK2>-P!isyj$vnttwhfSZ~C092^5dpOL zdU}{Qv0i3qes`X`Xq=*_w_dpIrwfmEj|Ch)N6DG8|%KA!IdV@8;qq=@kF`u_P`opWbDX#iw_o*L>KY3 zGCXS6)aE4hA5~roL3VkTDFa5iIgXRqU5b=2G$q?3nEz z9nj!KiJV{S`Z_{5qY7nX5|r&)#&vkYONwNLhZCIy&vAHGD;af^Xav(E$PBFHyjwSZKKQ=H9-(L@NX2*}7dfzH z7hC4xUB29lBRb!|#l^mRBX^|!7oIw5JmC!>pH*6N&BCy3K^k?k?IXu+dy8rAZ;kWw z?()e_;KLjqi(I6C!dI1Y_elg^Zd_bK&l?na4#}U=M^E?XuKAAWHRylyOVm!3vGyc3UplMk;W*d&o1(aKpOL^K55=7ikX@ugR7xJ) zZ(qmap9UTcXI#JMRmS4aq{iHpxpE?B($81PDQ<2zn1MiiN*TWA2?>1pzxCw~v5z;d zH(TaFYNggg7Pj7D{}U?ceJlB2sUR_P>HkwIXi%!0Fi>sDJV-wtcu!hx_CZT){K(eM ztmq@ie0LX*N|}0*lA4xm4*ZQXz44o^^y0g#Rp<0B+Y=K@9hAm3U!s{0`_FgvMkRjlui9oV=sx+28=G9YIL+5k&3wJ(XNR26I5> zFW37oN>(bsT&D)u;dP1uoS!5$GJlysjc$c4^x@19R@bYn!efsnk&~Qit!lHmP(&y{ ziJ$l~A`Yb^Y%xqrXazN@>_P{G9(t|?dl*KgZWwC@jGl^aAB91Y{EL3v)%?Wnn8{tj zQ>>exIbHzaHmSGTNzJfOBvvW9|3?R&W)cS=otCFTOhV67a;)1=I+FO7g&D1fTX=)e z)FzGgxk&l=kc4|U95KAyHJzvQswl@hC}!t_u3`;qmlTgrDV@-@-_i5EYpNSyw8JqQ zpQ81x&SuSfj+X`56A75LAc5637=V+W8-ba;r4mFSi8EJo4Qh*;$ttC40VUWlMHM=dkye@ ze-AH{RPxkK{04AM9h>V2M)Pk#?5uf}nWwt^4>kbY9Vlg~>5XZ8EPk1eIr9-oUVAZF zlkp5wv%{T)xaC8+;J2Hf805NMbDc=zpW?(FPw%m0mE8kNR+(q;8H$e6SO^7H3-5Q# zj<(!S$sgRl3iCZ=EYBEXF;30j%`Z;Pqljj2e|lw9Lab+lB>`gRdSSX#rdqC;8$_6} zdiBG~AXpL@o1&{8pzL2@dbX7sCvvjMXo2}@r~3@0?ekpD8y_Al42YX(Tt)+`PMWgWWcfgvqW+I5SD9TGQA(YVHvs{_((si<>YAb(&=2?Dt%mJDuR1EWfNArw zTAu;SElmJDqZ2-)8)>XSr7q9B#uCs!ZSr2LAjHg6wBoY#!zidB0@mzrbU0jc`kUQ0 z1bv!(M_#z!&475eTfn`FW1q;Ts#k1ZMSE)!ng05J1r1RAJ)B| zWvbvkTL`!Qd)SmRWn(*FUNr68jsnQ5O?57nF!|J+zSi)iYSCTrsxi-bp1{mM)+G$! z4_cD6QDP}B+Baz}P4-K$AcV!OcDSwPI01^_!$OiZgr9~%cx&OfC+);S@d#4`=eR_E zh(XQ2UlRxq6fRCIRj7Jl3yRr*&|iH}%94kB^-}zr6HE1f&6+%jN{BxeMUwrzyFrR) ze1@#MUOp+~0>hVwAs_SIDG>_Y3-5)0w9#HH+S7D7w?8Z6I@kJK)H-udz+4mCaEEC5 zrNr{esRN_`22d~m#6F@HiMTInP|>as2Z<0 z_ac+IZq37a(IKDEs9xhaa#+O@Foz5}hS}CU^-&*`rLqJiRLS+;=L9EhZ)-PAtDlwYTL%cbDqoimI2J>I0(vFK^r|Hipxu60UW3kezV9Jd69$C(76T#c zoHH-_Mq6X@zCVoAR9m)x+ptE)WA&*;K%kqLa55hIGQsh;ysD;$3G|`b(yX*5sg}aM zt<0c7?=>)0+Q`&CFDq54Yaw%B8SB={nPR0X!!#o<+rcaXn2+)JGI!4@N}0{w&9WR|_K8QBR8H%z~O zv!h^rK((yU<6K#`E1E_nkp}i{2Bk#+K)9x+=3Nu!=ctg%KTnP3>_#KqiL#4rQ$^$} z;^a#x0+}xtugKF=;pAbDx;qsON`%MLp?x)xsSe@9C!~pSSoVUvEoAeS{eyYj*tZ+5 znYYl51SM>;KPJdY-=DMI%j)m*xwZ{*WpBn{cCIn6)aye-%Y84U@ zdhBNN%ytS8@tM$Z1DjupkXkuS=#HY7A?T4`9nE9ozUX zA?Kn3km+7@AKfqWB>D*;xV|^>z~T9zk)9?tmz-= z87l{-BXiGAZ^9PX*TN1IFCdy-tsyuO$5&uzi5VATWj}yQqQ7Bcno=!;xy#<;{-`#TI4xGRVpS*qxjmCjPf#Y67-XdiF0Dl8#E&Ad!VILJT9$+ns^4XQw2UQ=T zu*~Hp!u;!Y41EN{wJ%Y82p&Uk;cn9ONCAaLe7ekD{2%Bf)wN!%N`EW&iD%RI=Py{U zAI=_qo02qEbCgfjj@?Ex&OeTVW$t!5j^@#*@(J6P8vZId4gMRqfYM*UFaE#)nA}}! zFk1SU1#UU@V~gMc`PAYEDSN6IFr{|m*2HJ<6Lp3Ed8h2o6#mZ|Vhx{uK{mgTHt{se ztB2&>-s43pg1s%XVg%J$v`@GB@F9cDv*NZT(S}2X`j2}4QyQ0V@F&gD5KKT*;KxQ? zxXD?y#a2qh2S3#I7v_K=9G^WZ@CweWcrYI6#U5oRv#}^JHZ$+nwwJyrh7*nQ0Hngt z!29^AyDnj$Ko*OMSR;Y;c1?YrXy5_A9qIe!wlYG@2oEEF3_O}2^K~etpSg}PnaK?z zB?2l;3`b~o;md3~G^~4ftIE)pTvvJb(io75872-AlKyzNOD* zTBtqFn#6}9zr3$_1IyTC!@hm+Ra@sG_N*+EWG8a-lxVQQ!3(P0YxT8q4N)caGA3z^ z-F*o}xMOMgt!E)K&B&S$>vz>1k?~kY9;&Fc_idsTJ7u5?)ck&Eiy?L)FVSlntyane z&+LFYjCX#V=;H}waUYe6qy8B8vNQvC8Fn?S)g3Jw8Fh%noh5xAvJ*#vb4w{F2YJV3 z#v!GIUKrbcm9c#_vb8S-KlW{Fkm#i+uce-z&As#sdAK1YSvYste1?~OKrQp8eqY>q8N16cMzlHtVo3$mYwt=TCBuc+8 z%QC1UqDVPJD1%CH6mKUIzJ1oK8uT}R`vzqemzg^{7M<=J%z}U;<-o{n^um=Xy=-Uc z)-oSB75&kVtZo8(nxj_?&x6BgST|mVyTJzx{-lc1>|uU=m~Md%Sb}q^5oy9#rQ=hi zAR^*j5Y^9LI~yeYUNX4Bg^iMtH3tqO(A!Q+HKzBgn+FHKao+hPV`_oM7=8;JKM^G@ zZ8RK!iRqEU16)x@y@#ilzwm6N02n&Gw9cHnwuHAbX0UfUwX(z#*K*5v*Vd(SHCCZ> zRWB95y@u2WNBH)|J3sifebe+h@feTI-I;(t067Yhn<(NKOhNcD6Rz(CJKP?UGgZii zygSWXWZ?t*t+@5m6DOnv3M_TBK!m0y+)^a?Z&8XR0HSp;{O3sT8X$L=wKB&FNV6 zaJz^Sw;sOr*>F_eObMVHLS4*HzeaZ1vCuFv;$Tdx1G|(@t+G#7teR?rr)M}Vp~w8* zk#I)mvRnC>@hl67f8|*<^q|)Q#6&~VRBfC#DT#Ll6lHj<xiglPrfEC+#74xXK$X zWB`ufls3_=_NmY|=G`C_bLTl)q`KWo$boXuSLInTIKWTRa<}oVVY76J|ynbN{CE7q_0^wbo%`{9j@%F7SXk zY84e$U+Txl&HN+_*zt8PIlfD3ijL=P^N``ree8nxC8VrNJJb%xE;Mz!tFYxwY~vAa z)uX`-6_D9bfWsi-yPB>*1C@*vfWih}H00j%=EuUS$fU3>Zc2Bw0Jh3%UdSi59A%un zFGgr}G6tFC*a_QDQcm=S^d@gmO?BZdX;`y?;HoPJ8&1%%UFuDibJ_bud?DJ|o5!S) z@zP2%1}4Mj^crsL&YpPBlb7T-C>libn?sSYV+SVv*DDbZ9m_EvWNKpA=GecFjqV25 z${Om)#m@bGag{f}=x)&D_3pr$VbWI>nc9IfHy?V>Rm*9?+?x9!Rj%qYK|j!PuKW;; z#|L0jxmbOku4Qh~&|P#kthk|)+9v_$E0hOt8G@atPMqrv>srFr41RjKis~!aKC?x{N2AsG|twBk9@Cn;0tPB z<8q(5i8;8QMBmkxGs_?L8mHq7^)4{z2{lClV8ryif!&=D$fU`C8~ZwlshZskNaUBP z;^tMhRbOt25lChGpsS4+ZsXO(F|@!h_oPy|G|y+9Oo&Ec02Q>;K2rJWSzY8FouJHw zemW76!5}#*q<{bXL`qB6o_IS{v%Q12Y(wlu&OI{tCmpWcnW&d3_7N3mW_yaPdCy}7 zCrTqzhm}JBOdTl3;sn%3FgXb3x3w|+_`i#MtzI=W9+E%*h>G4Ag;aVpXG&%mF~=PueI#)Gl~#4vhgc5Pu`-qLJC$6 z-LTr|06b}-W=6vi zsX=(3;(j;neY&ePKvbqB-lS(n=l$XY&TWmRAENq;#ws3NckNx8<}?Kpw$6r5qBC0% z+~KYVSTM=cYqcQ_b@QtKAejE8bHsNOim^6*bo9mM$@m+gy1yjoOwsne9H^r!sY(Ci zJzMMj54vl!jGsdie`w-R9pe5~3-EgO02hn+to?{{#MH1083%*uV20w0GX%-8^G3*) zym+K2qB(qCz&*Xj*@qZZxxaJDK|JH5flX#buOWouQoqm9ylyMl9t3K)!eLQ9Y&CT; zHr*vAvAk?(l5QWdwL+!`t?LymtD-%Qy(@lUspW^)@cXN+=@5AV!XK|?q(iV&oR}YR zG}4wcF&UKIZ6)`WSY4`+M55)}H;G^^b!LPOXNCg3Nm75G@r7nM(nv;t@^V{nmQRZX zP0cmkA0mz<40a}WlNJqDjOMZ z2sxt@Sqk)+uu6HA#UE!yM3)}rCDdNipU{_g)pxR_{rG;2fVD$o>rBa3vXJZD!Fx2V z{HD&OxN+rTh~H}yTcUUb<{Ep?w9;SJOYkz&ia(Rl(`Lma; ziT!C67XpPJLRX8i8JSd8wfc6A(RlE%oYNcc&zAX|ZwTA%qRGB6&*CGgy3_>|6^TB&wYstB5tJb3*RA}K2;13h} zTX?*bZds?l?34@ZI2~E-{>(o148>X|p}G2(+RS6CQ^xXZm`EkKBOhoLNu#+0>eRs> z&*ZCeF|>LaUYgujIIraXl49JxN7`e_4F$NzGVS72UI5Yi@FE%^0XiuDhRcd(HMVz+ zE2*RJfZN8^y?DS@l-2&l7sRbkbhS8edrP$*y9dSo(%vHPXHzi~e;Am(Ze6ORTP0C>Iiy^@+Q>Ybpetnf#dt+FPe@`u=p+ENr8Dr;^I z^mwHFgj42DI0L*9ZZh6_h z{3{KtwLqGCzkDd&{=yzX5Dq&No)@{m`94C3cwA`O;L+Xcls>GOW@ zz@@51-wDE)J8`Ykoo1f%cTp^+yaeL*<8p&^l0L6f3Rl=oG{4PMrtMe1R{|l)42au2 z)qU+{8?spU)M~Cr%J_eyY5I>T*<_QR1#(Ypb6~PJOy=fOt3rH8Yp3C1}0bj(Q~PWB&lIBBp1Ujh8<=Q<&@G`3C6%so$$#THC_6- z2&$GqWR8M9BiprPkc?Wu^e-sM0v@{+92C9b4cr?x#iSOrE--%4GM1>e0MAO588dEZK>}m+~ru6?2Re`xjL5B)A_d z*TUj|h#K84Hx{JPyR7Y@Tr9z4?MT z0kqoQ*HJgZ&DrZ?mCN&tFDurC)f1s05&2oSW1t}a_J@#4?mcC1lTrhU%kq5R+TzGC zj1N8zQ+z@RgarRt{x+Mqz@&8`_4t{j?bEv9a?E9vZt3qW40cNki+w2nDQ@b{7KG;X?Cj z)P#hDLNH+kMdypG*F`;-(d#wx#ouX_$q?^F8(EvrjV;_~%3iNdr8iE~@vz}}I#IQ) z)yX!hW`H;9Lbicq>X)#w=JXs=6eOdV&x)_usunf(%*>>oyKM~8+{!Ks*J=gbX^Tbf z@?;hHq)GZjDqV9Hoj&D`i#?7>dY5U$$YWQifixF}YV{EV&$5Ntj`+{pw$$2^F4yZA zzh^W_L#=T=#hfmf2BSQ%KU}Cj_!oRf5qLo~Z1!)M-G8SAs`0-M_zhU)D_ z1|l68Y^`r&VB>v3>?>_^N&laBg8uU&0iQ$ZDXX$uTqIfc>(}p#YuFC1ug*Q=pSzb_ws=BH>pKm%3 zl58Af3~|RSuRzMFnX5J_z?|H+#pf9b4$sX40{m=XrGUQ4k>ksi>eME~B;|Q4rCbaLEmq-+%X0WMIKgPGdnXBaPxm3rUEY?L9)f=m zT=Pf8ldPt_Yi^>X;U@loB`e}AR#o9x+)cgd?M(s%gO$)`$j*G|wrwGL&Gczo0j^mW z$I7saXW#J)cFkQv7t#opK$4Ok-HfgAE%kX9K=wrBTr_`fl}>)orM;YzRe*(6%5TC= zdA4TPg>kUF`tar?^w-uN%4>j=>?Ih*w@dRrqT5#2s6+PFm#NV7d38@8O{lw|>VTR# zZ(Jt^=imepBtbN1)mCv*WiBa1hozn=1Y$&4BXlo)MS^18rtrH}1>d21+>XX-jXUR1 zy!!c1_|ID8>&+(~dc8}N0g~6mepg>*8)|#a>VCzm3W*djf7MKn&P{*lee5W??03kR zZ;2J|B@)%OoKTp)(Q%@|Z0C;I?Pd{5RTx4$bUWP3DgXr;JvUkh5a%FAW!#)dzyx2g zws9HFhgqkIBC?D1T0)$jn`iM(+lqkg4Xm5tPov%84f9eWtCrhp=c=l!&(OC1yK+U3f3My`QigH5 zEg8#S%zIFJbOIx}t5jQK;|o*zGkDwSG><9x#b5mih;Yq$fpY1AsS~M^GvW z80F|BOrhJTxCbYq^M52R^N%Upo19)w`cn086ccNbR~sLf+>|K|mu9tT2xK{N*Kn8N z&~QiuB3I45tg&$AQWOo}Q4+AC#-kcR#M-cnI=E<50+sv-RIs82ST8?kn_rf_#FF?d z?RQrxxUq23XB>utPX+iei&rAn6lx6racW#0wfqMG4do{XQ8PrwRZ_BOiAw{1eo2hY zYGkji@XC{4kCIT>mIZnoiEwrxW=El~#mDue4;KKNP3Dj!KBF^cpXU!9ZDkokzxL`3 zULw2Qz0*TO@p`)570sVHL|4n%#f+wD+xm(?uqvkK1MR5ZF*+A_?azr9`t?0Y$Xm65 zl;L>8`)j{aJ5f=J77HF)3)M%7mKBJX!9vkF{^ktrP9H(uAd>c?sSIS27GCi z4I3A`d+)y}*W5fTtco0;enPS2*5OZZ&v6AXzn+nIjx>6nPBihc--}}9X?@Q+rzax| z7pdlJLd_$MvOKfRxM8Hw_-)v@;y{IfzsyTMHI#QZH}CqKA+R=;Dxi0S^OlUj;KZ-! zU(!4J@W5J|)or3i8p87?WJznIU!+v&?LTN)DaCY@9b@}%evRuTej|-CCPo_`lm9*9 zRnX69;~&IE-oC6y6{{tvhOv`%#@(4djH$0hP%+ME2lx$bdE8$7B5&BU?Ntxwb+(bN zPERE9+gW0wYTGYimdJG_Hn7^5^@$4D-ZhMK)nGb`D;BFfPuP=m+2SEF(9<4*WO+EY zkPryFtTa(_tK0fGnWn&9UcWi#1g5sTC&UxgWm0Wu9jzYCxRT5Aj6yF-@F5g6kmn@} zVa%70bHo=U%~gXw{Q zS#mz(?V)d8#8JxWfQjp1;Y$T!?M@@)=OoO=r43I2Cs!=!fk##5cwS6qu693#!;YxG zAI`?sqUQOt92JP~OG5+UJ@?FlPpfI?F0=O=c;^-55J$H6!n=?1Mo=_^Wa+wuNCHKx zh0=#A$vhPp_gxSOZ8_^IbXNJTo?cY0Iy=SZaXe8@;M$o_uA>F=YAz{PEX!4w)oq2S zy76MT*ydS6k{zLPR7RhHg({b{RverLrc|^{70v2%H?XwL@s_|$;pky*Pvyo|+n^m? zle*WR3)fH2IPFKCt=tywrcIlTnd!EIY3vX;heqY;f$~%@A?%GFPba{_itBoE==m+{#gQjqaZLrlA^op?(Si|PyH_Tw?Db_e# zq~F`KK4gu$xo{s%&IfPgKOGg4h zEgBwru9x`J@r%gHW$m^&$&EOc?PC-Z)MegN3WIdO>1KfC0(TB8T(R{k2RF}(v7qc} zW7!qmPZ>CUz; zK>>A>uIu6IiWjBGHN)N7LxRt)!FzQ6lRVmQ39hM4EZnfxOn_{B)|L0p{;iCsp_B6` zjvlwhtGu4tC$B>l?qWb=*qgW6zc_^2qM<>ORPSQpOpKvchBKapqps4bXXGKfLMI>& z^1KsVNgYYaNj`8`1=@U`1+NPmsnfMxK9t4XfPamj;m64g^*Y7yv~WALCr~Nf!?XlF zvwF=fOkXlQ6jv5nRMe_m+p0&H?4ier7t3}uFND-qvl|WV2E30`>lE9(J=358#1X*) z$9n@CT>+@aBwr8e*nG9<<8rt1h1lNfI#FmD@GQIjUXLvjV&P*^*{^42qw&73HeXuy z$Y~w6-|8&V9MZX+SpnOLDV=rBgMq_zax4HB6%lZKuqmexd zLQ($CpO-YyqB*V$U4u(5J=l@(BfLMYhq+IZ*V!NXNA8S>V*ni7!O4MTqiS|-4}I+R z7s2~IEQsDKRS60Fc&QHc-pi+3lxT59b!I}zwF9imw#i}kLb$>L4uj@bLGfRGxVS$= z=uKC~Dq6$O0b~Qy@+OZ~;|h!mh6$J1!;CG*t9Z4kB=rTvZ`;O7Q7Vb%($=y2G4{qv zpQ`SxEfRi*@->y2+&hatm&-W|(LcyKjmK0`jCAetE2eV@(S1`T7gnP*tKPoVn3n{? zb-@VcNgKj`m?kwOBrcwVAS($os4NqSTa|mKkOnPKQ}UrVZ>->E*EOW|;Zv(+yF=vQ z>|ya@)!7E$$^jZdA;+@60d%AbiW&T?vs z4KeRiPo=reAocZdemG;hcJ;a+R^#fCRaE#EBtp?)Zr1g7810cOdH6(IoC@ya-g8z$$Z)^vdC%Ad#pOb}M-k?OF_$Xud4Gun>q zy0DUaa1Tsct#*3{JFft&Tvr=xlcf}gH_gUf2RJ~#=f=`cot%&61+4pqo6IJ)F=w z@V0d$^Rtl%dZBSQmjtIH;VMk$P%E3tQ}ke7HOkun(y5V4%^RCrSfr5!m8Ifci;|nh zdURy1?Imuzke-HVj&Q@d1x89(a@9O09tGd3R+*mrNLPf8?bT-Nk=RFu;IoXu{j#R| zl$_q29U>l9S|=`B=6upbo-ryLZlrN_L_Q9QmHqZ}_dMP5ed}uzVti7*?w{}bmxY;q zl2);(i&p&K_BbnU74f|LFRh(rP#n$M?uTH(6I_BMNP-jGB?)dxaF^gN!CBljxVu|$ zmnFdBu(-R+1`Dod^F06et#i)%=~SJm{?b)5(>*mkclZ3d`?^-k1&Wt4l)>B4FID%7 zg}{00i%+l8(J)M;5$-?)eo==k65ZbUK+p|uanm*QfV`Y<8S|VVOnuY zuR5VZCw#C7?T))!*z$U2j=qWtFnxR`S+ZiL3kp$toPXzAkWxTtIsXNUAsGB0Od$wc`Mz8G7{W zSdlhfX^VwMZHn*_6M^Sx{Pi(W*Mb5dLFeVP{mvok8}^)y%5Te>qGz+T`RyW}-c~;f z;UIBz-?uv+4{2`E@*`p1{t@}U-h6J==B(dP#Nh4N#ijD;d@v;;c(xGa-EVvM78^V& z`#@Vok4;>tSU9pLfPk$Agjpjz?~`;p*B1D?ncK!kALb5URtzI3gC>|$=jwR3W}{7h zm^f2=rq&}7Q%JxTo1&GBsB@0T5j~7xhNVR;G3$tnjhxX@#29aGef;Qe+--{C_aT;u zZ0J3sZP7IPx6M0z#w|1^q}U6NH(4!Na|>!Pmwcw8nU`N5wr--9OPV4H#2GKFeYk)< zvPFY#4s8mxgHf1i&nkL0@M1S}!g$!Y^ETE~A)mFA<>lUR4-M}K+>g1!_ z@y&TdGh{0xu{(uME87fj8GkJV1gXWX)vj7>V8Tz5ebm{qx_^2qr&67K zyD<6aIb@#6>lQukPwkA5N^wEnH6`+Mpo6UP5dHTYS;V8C3LdCoF9&MX5UuAju|m)M z+pzVVP%XJYZQgzd%LxkJ*RMmLV~NnXKtt8Hq0MIp`m~HNiu1Rpr%7EE&#v|t=|5f^ z=ZrTljZ~Pc+S4(ezvsji0x)m#><;|MiH^g&H;B$+w7x7GtdkO4_I~g>>fKmWv#7UQ z!#g&fDR*6iMX&+L(wj! z_{)~EGlCgoSyL{j)m0{cqSiSyo}C%X$GnoUti`5@piPLil`yAc9n|{iCMrf3XChin z`2fh4pI3gTa@lOXNF24vr1wy;2}xQIp9Eo1sve2GX&nrk68J5urXzMko1qh;2Hj2m z{A#~kPPT*D(K&6QH6pcI->0{xG!2~;2+w`z19|T~O4Z(@RD3%{edf-S^dr;A>*OEL zvk+4{3R43#PF>fNZzI&%fA+qBsP*^z&U~BaAegS--xtW2qE7vm*1*)?!Rt3fG&|sR zlW<`NyU-|lW!NBn@?ttw!Y^h0Y76ZeR`LAC*iABUBcB!+U6fL)%nH&bvS>MB=@})o zN|p~d%2W0@vw{93eVG4R>r?fp5zASID5l!`mUOnRLU*O+?dq6#E-UEdTgiN#8!S`P z5M8!)&KE{x7+b@zy-2Vr{1Dy9VN)WSv}m!zEg8iu`m;U~`h;c~H8AnQrtM!AG z;y0|J{I@0{?GK=2t-e8zqX64?1J>DZ?zIuFujOC7Z;VP1p_p$;`Z5r*ECf-^6r*jW zyLcW(T;~vyL_xyr{kmQnLzU9JaJ=BRk&&Kpp+WrB{i-cGc^gj5ag7ZvTrAhUi&SX2 z9oNGS$};%+QfIt@x+qUcpXl+|!iqxkuwX22!OG|H898lm2DoF}*DEQDY|IuTjcU9zE~P_AC~v17nC z%JI0r=~O>kWr~h6W~QW79ej1A-YD~Gj7{FD(* z2u_Idh?cO0$15$Qovn@;{HmwpEM|m|9kdM`dhQ;s&B<|uY`D1>VH6q2UKc!YFbug5 zo)jkA>dW?*0PkK=ohlVTO=pA;qJ*?K9YlUb#|W8@aZI$Hq)4{ENT3S|d%mEqNhR=9 zuqj@e7l&|dD=sc+56G!=dE8g$H9Af{b)UYXB&{d4%fP>D#=*$0Okbn~MV>}K4mqJ# zUtbgln%YpR?ub{i))Xc8I-?N&q!bShM`cvfsp13A3ov=TrUdRmy&Ae5S63XnWtPWI z>P-sv+`kQsfzoOwA4+`FqXu0Fs=xs80Ui$Na&5?``AHheB6~SCtBlWn_ zo|N7`IHVD+G@g|`bMz6YXr1;KHhEx9v@2vscB1oDvvL!lQ};}@W!&MfVV5DBXIXxn z^M>NIi0JzYseEvt+^OSx*z7fCz6F8E?4@;Hx|2fs^slu`-P z2&k*6xj>U^|8hYS}K7&j97@FU_-(tnJwYnFvBivSEX`Pf<^CK>QoVG zAL*Vc-fy)IQLI3u$jT{^9Y=^+#&Gz}oZQCw5IwcPA^w#MmeI$W-fpyb7=%HU#WR~( z_?0Jvk-=)bI`F5XUi1nfffY^G)-|O%SS&u#xx7yEmt9-?gPJ(2hVUMZHUE{|j^fEv zhiS1ke#2I9SbiiC4v|AIONh>G4BDzXHt>SU)7zin;-q8B7W@6Htk7}OM`8l~Yg9wB z#(NIug>`cC)mIbtJ;GwBGfewWi?+k%5TFB^^livFt%f z&6JWc*qDtqY&id@PCc#mxJcQc-xq@Y{?watJvnosy8T#Pqk~Fs8c8gfMlojo#VR?_ z_NqU_61t<%t-xLR3uxyi1(>Gm=YMo9 zh~Zp=;0g5cI7b~zNg9;qBUcYx#MfTu@nM&*eEMt3O-wgH^Vf>oFdy#2?b?hKQ0OHv z@%}H+`6mf-v1dj8W6upg$B#+^^MCj5HthL9yZZOye-k|GF(f}VR<{4wp#76b_s<@M z`x-%U6QlWEgBVD)H{DlX#ptPN=qFV}N)01if&Y{NFDZgJHW9A&|DR%eLSCE04MX7h zfc-Daf_v4RiLG}vrBDe__ja^PrIU6?K&weNtj_XGxOIz1X;nIch(&Ls$=vaSSG!l$ zpM$O7j#b#*I_^<0BVru8!?Xm>1YzbKo!toT(b;up{-3Wqr;e5FV3W|kbuItQ`vUPH z+x(8Wrmsz&p>_|Z-n=fdT-P~a_u%I)y9_8+v$I=XZ65xcXS7Q=voamfgaa>#T^514 zRpAz`tyK?IfBQBoyN>wAZPsE-bma4=H0q)1 z!GGiEytrU%VSHX@%Qg&s&qY~&XChDhR@z&Tis6>`@B}Ur`o~n@**kEun;ehLEDQ~% zw@#484vC~0$kEy`ua+c4!!I!9E&}1nTSHS!EMe-mgChq3*f`fKP0L|*{-J%C)?qeS z7HM!OjU#qNSWYL}JMYPW+K-c4=`5Cq+we0Dvv-WW)zj6p!Jw-psEgFKp^e|!CHQH# zQ=vgmeX+hxB!jg5B3$|i>@2PLE8mag)9gt6q`=GcSG%3JP8f!PtZ8`I#)F04oDVI? zFOdvzU!@sJ@&^z}wCfy55mY4T%0EzX?S>UTVMC@PzR8Hv49wRJgE#coG6glfR3)YY z0$*8QHvgf`u*1?#0dq=aUfw+M>t)0wFlGsAEgj=?H4B7IR}nWlO$J5tihIwc#%rY1 z!#6~>7Nu*;Oo-{8`q!M<%Y_Uv(s)ToO_e9=s>9y0x9?P;C23zxz z3;1Gnnx>0hZTFsOg_*yUID56@EK#slG5-+zj$eS9%SG<{WAGOQd>^#Ocuj|+8G>c9 zFHmlVvvBIjk*T(x$Vg(+eO~XJ(`Vf}OC>Q;hOQN+wFxV=o^sS!+$H)bIX|TmYIaBO z>mA{y2+zBamalak-tC6j4C7yUk)UO0xmO~dT!x3NiL$*nEPP=GPS6oUoStVV?+ks8 zf%~QWN7ZNV?7Tu6m(F1oH)Cl&{e9e)vA`h;Jks3}gef(dvODtLol|7}Qsb*Z%cDVo z*Bg$26l;z7`8WUdR4DKfnOJ?{??I6=Bh7@Rri%8sIYz~FO*PuE?GkiLC;5!L zT^6ixdmba_@75}3r!`$N?TLnbZZ|)**4H`bno~+EEC<^~A|qZRm_ZkuZohusYOrJR zQ8gbQ-K=M?Z;m@we_MD(5@`$>@J?F&>|A~Aw|^Y;NWT{9CTBfyHtgKzB&HhF$8S7y zy79fFG{J!H#+|Mw@@fYdGXW(eP@g$`Whjl;WQMvy2D3%6NgYSu;y1s#?iuQ6%}&q4 z4eLP_KCO{d)A=S%;T z7}4^YRymPrqli4+mgCb1=gxin=Qp?g@(C={W{Z^Qg~1@9!_$iAIyEo>C*4YG*v)gBvNp#P zsT(Y0ENt7e;Z6eF9Il|^lwHQ}fYV3KFwQ?ws@!NJfpM}$>)|Bth^jx(NC1alnNWo94#u?2l zzRRqlWe2^&%{#5-)wDX0q`edMwUTc1!(r@7k9p6JUK2yzon8q{|NL#%cWKGdyFGgh zxU*{Ki@&CX*Sz>QEe+I`L$ieW>61KR#)zW-;BGEXqhlFVvQi!A-+EA`!hdnMU*p%0 z^YU(PhcXoIh`_(N`__LUe_@Y;8a8BTv;Ez z1Q@~_9~&*sN;UL6st?f5cqrOTl87lrP35tP#_0)oh~|*tzV1z*gMJw23V$1Y@cS2k zR|*NV!AgG43>MC;`y*w8V`p!V8L8F&h?Q|^hKUC*PzdU(@agSgi|TmfgfVOAb6AbA zC2%GAbvb((bU#}zhG{{$%#n9uqjfML?=J?VjVmU*pSWd178Vzsx8H`FR9qTksd7Ly z%)Ec|(FdkXSuQ$z@a~g7Ho#Yvqpkcx{*aCFCMiI}VC93V)^Cx<-wcAVduP9-8+~hk zc$e?$jHebakL2T^0VZ#eKmnLaj#wEJeV%m|f9L#ON8y8PyxJa|I|qoCpiCU1NDRKQ_x~H0o>&ox-xJtQk@;d}2EA@>xrC87Y%QOzc=~Jv zLhW}FRT4;1)Fhh&-J6tvrWKndeh4D47MK<$w}<<%q*i&W?rFtsdxET28Y!TV2$Tb zy{IrybuCDSKGR_nMSDI5> zfFc#}u~FezVAwy9f8vB03^$KPiwxE$DNH*SDPnkfiiCfioGPi1<#!r5xqdB=X4ZR> zGm?wBukF$yg~+5Xk5(@`nm9Y+9?4Q{dv=!6h2S&e90d7x)%LZaoxq3`MIXUAMhxcU zc43W>ONKn+tOUAS`Y23A+zC=J>k6%@doXn=9r1WuhC@QrS#y{)_BD6E0|$qY^I;%E zOFjQPCTQ8~Iu+qsu+_J|yiAdGFSSMP{(2(ns+T}AaQOOC=@hhfrHpHCDaC7I9Ne}6vX}=^BPgU)dGO;nh{ywhw zJv0?@jI@DG4*FMrz)zu)OVChrvGqxR{$$=S`^OffccOi8(Y(HE?phdv?)D7B*XSXl z-n zu8->M+hc}nt<25YTGwu5KD&2wf^5nGMmO>Ud+C}vM)WS%sU)AiiOpl>%w`Hx-!oxD zCib)>M`fy!1^g<0hSe_UKHov}9!C zk;7ad+BvzC0Ij};ff;B~%`9TOwRpBl@T2V^U7dBkJl{R^rG zSMdUDU?E;U@T>9S*tp{>)0+cUa@LM1meeCl;zDN}+<6t%{1tQ5mIL%b)_vHAHg}1t zzzr!=W6d+`l-k9q6#}?83LL&n1YbrCF{pb<#23T2$+I#xVOIdNHBOLG=+q#~h&32`{&_yyS|@;geE( zvg|y86K1xh)X0~Go0cZTvG8$Z&QT}FogHnvkRIj~^M{vf3Xw`qn_33f5dzvI6 zdE+g&1B%6mc$|E-%7iPcA44+v@rOQKb@Hh22)LzUW%Y@N9nP05;t0!yC*<;E7YF(~ z2Iacn9p8Xg@%ddT->e!h%h26iUj%Tuw|LNAu4U@Pe}uK>T1zTO7Im`RU!f8`M@VbN z=+RPgP~EbJiGO%I{;oT~=>HXPR@nXz!0EHf#yab1SRAsV7yfy*PqzLSXrd6Jc14pF zIpbs>=p1YOD|RKux{O}wcP!ZUENyd1fZaxr*o>flKr-LyfQ1m^jPYEKZED>S$^7kU zk*eO9g2sZ@UN#p;$c$eNqv}RRhvMjuUEZG|SA4ECPl`omcCK-|0nCR&ol+x$mO0Cc z=o%*r1^$8dl+M-ZAQvz86*Se@rXMZ%`@LKc&9H4kHuFi(*MO>MXN^{%0A4MUU>I$k zqYH(x`WurU4S!L?eQb|`BT?p}lX?uThT}?2+&-{PPDlbl=!{H#-+gD22wPw&GDOe7 z?9_Wf@)`xW9c+FWpCx|U18cjy=>FF04*nx6du)AcIB$Ycn1`2m6jMfr-7yo=qVMQc z>!N=DK^VBci>Ph$JU%uZRhxw`F}2IsxVI>WK+qlqEwt4aI$Vn96z`k1)h4-DwwUhO zx{6i}uo@kl@EY8Rg#z{0mq_lMUqvc(HJBStWv(VWLroVm- zc`;GGY#To}#J%W4s65xpjtO^KUHrUhOR~I+a1uLfK|(;;@qVs- zGVF|lY_gW!Vrs2*G@Y>4vf)XoJmP^&g$hn>jF89{p-~yGUNp=+Up{JU7GyHW3aMOb zq5R41m(v7*u^reQy$Z2|e)UJW`&CKaLbbW4&@-(UdR3U2pllj|V4K~)`mkL9A z=&bUEoL{Aog@wMViQq{ONfM^?JZ^+$Y?2A|x#Z`usZ99IZMhF>+>gIPRkh7kfHySE zIHSLK4C(z>DNTho?Pnj9z~uYH|4q>{NmHN_fB(+@P1Hg8+y9+GdT-btYx{p>+qdZs zAWy!Wa4xXH!v&Lp^{@^p83ma%#zb{(&8x;}-fCEt^&e~~hL-m{a>OLe{jK5OYW&yz bcb*VMGGQERKG|;o_#rK>@ag9VeZT(&dzk!V literal 0 HcmV?d00001 diff --git a/source/docs/results.png b/source/docs/results.png new file mode 100644 index 0000000000000000000000000000000000000000..2db903370cf881ec5dd318d6414a0eef2ad63c31 GIT binary patch literal 231768 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. The `shared_ptr` +allows for both non- and aggregated: if non-aggregated, you'll be the only holder of that +`shared_ptr`. diff --git a/source/extensions/access_loggers/common/BUILD b/source/extensions/access_loggers/common/BUILD index d7e449111640..e7851e9da725 100644 --- a/source/extensions/access_loggers/common/BUILD +++ b/source/extensions/access_loggers/common/BUILD @@ -34,7 +34,6 @@ envoy_cc_library( "//source/common/common:assert_lib", "//source/common/grpc:typed_async_client_lib", "//source/common/protobuf:utility_lib", - "//source/common/runtime:runtime_features_lib", "@com_google_absl//absl/types:optional", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], diff --git a/source/extensions/access_loggers/common/grpc_access_logger.h b/source/extensions/access_loggers/common/grpc_access_logger.h index 4778d3299b01..8598bfbaeb57 100644 --- a/source/extensions/access_loggers/common/grpc_access_logger.h +++ b/source/extensions/access_loggers/common/grpc_access_logger.h @@ -12,7 +12,6 @@ #include "common/common/assert.h" #include "common/grpc/typed_async_client.h" #include "common/protobuf/utility.h" -#include "common/runtime/runtime_features.h" #include "absl/container/flat_hash_map.h" #include "absl/types/optional.h" @@ -248,13 +247,8 @@ class GrpcAccessLogger : public Detail::GrpcAccessLogger( - config.config().name(), config.config().root_id(), config.config().vm_config().vm_id(), - config.config().vm_config().runtime(), - Common::Wasm::anyToBytes(config.config().configuration()), config.config().fail_open(), + config_.config().name(), config_.config().root_id(), config_.config().vm_config().vm_id(), + config_.config().vm_config().runtime(), + Common::Wasm::anyToBytes(config_.config().configuration()), config_.config().fail_open(), envoy::config::core::v3::TrafficDirection::UNSPECIFIED, context.localInfo(), nullptr); - bool singleton = config.singleton(); - auto callback = [&context, singleton, plugin, cb](Common::Wasm::WasmHandleSharedPtr base_wasm) { + auto callback = [this, &context, plugin](Common::Wasm::WasmHandleSharedPtr base_wasm) { if (!base_wasm) { if (plugin->fail_open_) { ENVOY_LOG(error, "Unable to create Wasm service {}", plugin->name_); @@ -35,10 +32,11 @@ void WasmFactory::createWasm(const envoy::extensions::wasm::v3::WasmService& con } return; } - if (singleton) { + if (config_.singleton()) { // Return a Wasm VM which will be stored as a singleton by the Server. - cb(std::make_unique(plugin, Common::Wasm::getOrCreateThreadLocalPlugin( - base_wasm, plugin, context.dispatcher()))); + wasm_service_ = std::make_unique( + plugin, + Common::Wasm::getOrCreateThreadLocalPlugin(base_wasm, plugin, context.dispatcher())); return; } // Per-thread WASM VM. @@ -48,11 +46,11 @@ void WasmFactory::createWasm(const envoy::extensions::wasm::v3::WasmService& con tls_slot->set([base_wasm, plugin](Event::Dispatcher& dispatcher) { return Common::Wasm::getOrCreateThreadLocalPlugin(base_wasm, plugin, dispatcher); }); - cb(std::make_unique(plugin, std::move(tls_slot))); + wasm_service_ = std::make_unique(plugin, std::move(tls_slot)); }; if (!Common::Wasm::createWasm( - config.config().vm_config(), plugin, context.scope().createScope(""), + config_.config().vm_config(), plugin, context.scope().createScope(""), context.clusterManager(), context.initManager(), context.dispatcher(), context.api(), context.lifecycleNotifier(), remote_data_provider_, std::move(callback))) { // NB: throw if we get a synchronous configuration failures as this is how such failures are @@ -69,12 +67,7 @@ WasmFactory::createBootstrapExtension(const Protobuf::Message& config, MessageUtil::downcastAndValidate( config, context.messageValidationContext().staticValidationVisitor()); - auto wasm_service_extension = std::make_unique(); - createWasm(typed_config, context, - [extension = wasm_service_extension.get()](WasmServicePtr wasm) { - extension->wasm_service_ = std::move(wasm); - }); - return wasm_service_extension; + return std::make_unique(typed_config, context); } // /** diff --git a/source/extensions/bootstrap/wasm/config.h b/source/extensions/bootstrap/wasm/config.h index b8f3850ef621..9d09a7905b2d 100644 --- a/source/extensions/bootstrap/wasm/config.h +++ b/source/extensions/bootstrap/wasm/config.h @@ -34,37 +34,37 @@ class WasmService { }; using WasmServicePtr = std::unique_ptr; -using CreateWasmServiceCallback = std::function; -class WasmFactory : public Server::Configuration::BootstrapExtensionFactory, - Logger::Loggable { +class WasmFactory : public Server::Configuration::BootstrapExtensionFactory { public: ~WasmFactory() override = default; std::string name() const override { return "envoy.bootstrap.wasm"; } - void createWasm(const envoy::extensions::wasm::v3::WasmService& config, - Server::Configuration::ServerFactoryContext& context, - CreateWasmServiceCallback&& cb); Server::BootstrapExtensionPtr createBootstrapExtension(const Protobuf::Message& config, Server::Configuration::ServerFactoryContext& context) override; ProtobufTypes::MessagePtr createEmptyConfigProto() override { return std::make_unique(); } - -private: - Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider_; }; -class WasmServiceExtension : public Server::BootstrapExtension { +class WasmServiceExtension : public Server::BootstrapExtension, Logger::Loggable { public: + WasmServiceExtension(const envoy::extensions::wasm::v3::WasmService& config, + Server::Configuration::ServerFactoryContext& context) + : config_(config), context_(context) {} WasmService& wasmService() { ASSERT(wasm_service_ != nullptr); return *wasm_service_; } + void onServerInitialized() override; private: + void createWasm(Server::Configuration::ServerFactoryContext& context); + + envoy::extensions::wasm::v3::WasmService config_; + Server::Configuration::ServerFactoryContext& context_; WasmServicePtr wasm_service_; - friend class WasmFactory; + Config::DataSource::RemoteAsyncDataProviderPtr remote_data_provider_; }; } // namespace Wasm diff --git a/source/extensions/clusters/redis/redis_cluster.cc b/source/extensions/clusters/redis/redis_cluster.cc index 673bffa67705..7565936db273 100644 --- a/source/extensions/clusters/redis/redis_cluster.cc +++ b/source/extensions/clusters/redis/redis_cluster.cc @@ -94,19 +94,22 @@ void RedisCluster::updateAllHosts(const Upstream::HostVector& hosts_added, void RedisCluster::onClusterSlotUpdate(ClusterSlotsPtr&& slots) { Upstream::HostVector new_hosts; + absl::flat_hash_set all_new_hosts; for (const ClusterSlot& slot : *slots) { new_hosts.emplace_back(new RedisHost(info(), "", slot.primary(), *this, true, time_source_)); + all_new_hosts.emplace(slot.primary()->asString()); for (auto const& replica : slot.replicas()) { new_hosts.emplace_back(new RedisHost(info(), "", replica, *this, false, time_source_)); + all_new_hosts.emplace(replica->asString()); } } - absl::node_hash_map updated_hosts; + Upstream::HostMap updated_hosts; Upstream::HostVector hosts_added; Upstream::HostVector hosts_removed; const bool host_updated = updateDynamicHostList(new_hosts, hosts_, hosts_added, hosts_removed, - updated_hosts, all_hosts_); + updated_hosts, all_hosts_, all_new_hosts); const bool slot_updated = lb_factory_ ? lb_factory_->onClusterSlotUpdate(std::move(slots), updated_hosts) : false; diff --git a/source/extensions/common/dynamic_forward_proxy/dns_cache.h b/source/extensions/common/dynamic_forward_proxy/dns_cache.h index 7425639531db..20cf354a3a19 100644 --- a/source/extensions/common/dynamic_forward_proxy/dns_cache.h +++ b/source/extensions/common/dynamic_forward_proxy/dns_cache.h @@ -191,12 +191,9 @@ class DnsCache { /** * Check if a DNS request is allowed given resource limits. - * @param pending_request optional pending request resource limit. If no resource limit is - * provided the internal DNS cache limit is used. * @return RAII handle for pending request circuit breaker if the request was allowed. */ - virtual Upstream::ResourceAutoIncDecPtr - canCreateDnsRequest(ResourceLimitOptRef pending_request) PURE; + virtual Upstream::ResourceAutoIncDecPtr canCreateDnsRequest() PURE; }; using DnsCacheSharedPtr = std::shared_ptr; diff --git a/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.cc b/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.cc index 094255c84924..4be9cf22786d 100644 --- a/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.cc +++ b/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.cc @@ -83,15 +83,10 @@ DnsCacheImpl::loadDnsCacheEntry(absl::string_view host, uint16_t default_port, } } -Upstream::ResourceAutoIncDecPtr -DnsCacheImpl::canCreateDnsRequest(ResourceLimitOptRef pending_requests) { - const auto has_pending_requests = pending_requests.has_value(); - auto& current_pending_requests = - has_pending_requests ? pending_requests->get() : resource_manager_.pendingRequests(); +Upstream::ResourceAutoIncDecPtr DnsCacheImpl::canCreateDnsRequest() { + auto& current_pending_requests = resource_manager_.pendingRequests(); if (!current_pending_requests.canCreate()) { - if (!has_pending_requests) { - stats_.dns_rq_pending_overflow_.inc(); - } + stats_.dns_rq_pending_overflow_.inc(); return nullptr; } return std::make_unique(current_pending_requests); diff --git a/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.h b/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.h index d918ddcb7a7a..2dcee3d0483c 100644 --- a/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.h +++ b/source/extensions/common/dynamic_forward_proxy/dns_cache_impl.h @@ -53,8 +53,7 @@ class DnsCacheImpl : public DnsCache, Logger::Loggable getHost(absl::string_view host_name) override; - Upstream::ResourceAutoIncDecPtr - canCreateDnsRequest(ResourceLimitOptRef pending_requests) override; + Upstream::ResourceAutoIncDecPtr canCreateDnsRequest() override; private: struct LoadDnsCacheEntryHandleImpl diff --git a/source/extensions/filters/common/local_ratelimit/BUILD b/source/extensions/filters/common/local_ratelimit/BUILD index 1a201025ca3f..0234c335c3e6 100644 --- a/source/extensions/filters/common/local_ratelimit/BUILD +++ b/source/extensions/filters/common/local_ratelimit/BUILD @@ -15,6 +15,9 @@ envoy_cc_library( deps = [ "//include/envoy/event:dispatcher_interface", "//include/envoy/event:timer_interface", + "//include/envoy/ratelimit:ratelimit_interface", "//source/common/common:thread_synchronizer_lib", + "//source/common/protobuf:utility_lib", + "@envoy_api//envoy/extensions/common/ratelimit/v3:pkg_cc_proto", ], ) diff --git a/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc b/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc index 2adee384673e..ab3100e50391 100644 --- a/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc +++ b/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.cc @@ -1,27 +1,62 @@ #include "extensions/filters/common/local_ratelimit/local_ratelimit_impl.h" +#include "common/protobuf/utility.h" + namespace Envoy { namespace Extensions { namespace Filters { namespace Common { namespace LocalRateLimit { -LocalRateLimiterImpl::LocalRateLimiterImpl(const std::chrono::milliseconds fill_interval, - const uint32_t max_tokens, - const uint32_t tokens_per_fill, - Event::Dispatcher& dispatcher) - : fill_interval_(fill_interval), max_tokens_(max_tokens), tokens_per_fill_(tokens_per_fill), - fill_timer_(fill_interval_ > std::chrono::milliseconds(0) +LocalRateLimiterImpl::LocalRateLimiterImpl( + const std::chrono::milliseconds fill_interval, const uint32_t max_tokens, + const uint32_t tokens_per_fill, Event::Dispatcher& dispatcher, + const Protobuf::RepeatedPtrField< + envoy::extensions::common::ratelimit::v3::LocalRateLimitDescriptor>& descriptors) + : fill_timer_(fill_interval > std::chrono::milliseconds(0) ? dispatcher.createTimer([this] { onFillTimer(); }) - : nullptr) { - if (fill_timer_ && fill_interval_ < std::chrono::milliseconds(50)) { + : nullptr), + time_source_(dispatcher.timeSource()) { + if (fill_timer_ && fill_interval < std::chrono::milliseconds(50)) { throw EnvoyException("local rate limit token bucket fill timer must be >= 50ms"); } - tokens_ = max_tokens; + token_bucket_.max_tokens_ = max_tokens; + token_bucket_.tokens_per_fill_ = tokens_per_fill; + token_bucket_.fill_interval_ = absl::FromChrono(fill_interval); + tokens_.tokens_ = max_tokens; if (fill_timer_) { - fill_timer_->enableTimer(fill_interval_); + fill_timer_->enableTimer(fill_interval); + } + + for (const auto& descriptor : descriptors) { + LocalDescriptorImpl new_descriptor; + for (const auto& entry : descriptor.entries()) { + new_descriptor.entries_.push_back({entry.key(), entry.value()}); + } + RateLimit::TokenBucket token_bucket; + token_bucket.fill_interval_ = + absl::Milliseconds(PROTOBUF_GET_MS_OR_DEFAULT(descriptor.token_bucket(), fill_interval, 0)); + if (token_bucket.fill_interval_ % token_bucket_.fill_interval_ != absl::ZeroDuration()) { + throw EnvoyException( + "local rate descriptor limit is not a multiple of token bucket fill timer"); + } + token_bucket.max_tokens_ = descriptor.token_bucket().max_tokens(); + token_bucket.tokens_per_fill_ = + PROTOBUF_GET_WRAPPED_OR_DEFAULT(descriptor.token_bucket(), tokens_per_fill, 1); + new_descriptor.token_bucket_ = token_bucket; + + auto token_state = std::make_unique(); + token_state->tokens_ = token_bucket.max_tokens_; + token_state->fill_time_ = time_source_.monotonicTime(); + new_descriptor.token_state_ = std::move(token_state); + + auto result = descriptors_.emplace(std::move(new_descriptor)); + if (!result.second) { + throw EnvoyException(absl::StrCat("duplicate descriptor in the local rate descriptor: ", + result.first->toString())); + } } } @@ -32,28 +67,45 @@ LocalRateLimiterImpl::~LocalRateLimiterImpl() { } void LocalRateLimiterImpl::onFillTimer() { + onFillTimerHelper(tokens_, token_bucket_); + onFillTimerDescriptorHelper(); + fill_timer_->enableTimer(absl::ToChronoMilliseconds(token_bucket_.fill_interval_)); +} + +void LocalRateLimiterImpl::onFillTimerHelper(const TokenState& tokens, + const RateLimit::TokenBucket& bucket) { // Relaxed consistency is used for all operations because we don't care about ordering, just the // final atomic correctness. - uint32_t expected_tokens = tokens_.load(std::memory_order_relaxed); + uint32_t expected_tokens = tokens.tokens_.load(std::memory_order_relaxed); uint32_t new_tokens_value; do { // expected_tokens is either initialized above or reloaded during the CAS failure below. - new_tokens_value = std::min(max_tokens_, expected_tokens + tokens_per_fill_); + new_tokens_value = std::min(bucket.max_tokens_, expected_tokens + bucket.tokens_per_fill_); // Testing hook. synchronizer_.syncPoint("on_fill_timer_pre_cas"); // Loop while the weak CAS fails trying to update the tokens value. - } while ( - !tokens_.compare_exchange_weak(expected_tokens, new_tokens_value, std::memory_order_relaxed)); + } while (!tokens.tokens_.compare_exchange_weak(expected_tokens, new_tokens_value, + std::memory_order_relaxed)); +} - fill_timer_->enableTimer(fill_interval_); +void LocalRateLimiterImpl::onFillTimerDescriptorHelper() { + auto current_time = time_source_.monotonicTime(); + for (const auto& descriptor : descriptors_) { + if (std::chrono::duration_cast( + current_time - descriptor.token_state_->fill_time_) >= + absl::ToChronoMilliseconds(descriptor.token_bucket_.fill_interval_)) { + onFillTimerHelper(*descriptor.token_state_, descriptor.token_bucket_); + descriptor.token_state_->fill_time_ = current_time; + } + } } -bool LocalRateLimiterImpl::requestAllowed() const { +bool LocalRateLimiterImpl::requestAllowedHelper(const TokenState& tokens) const { // Relaxed consistency is used for all operations because we don't care about ordering, just the // final atomic correctness. - uint32_t expected_tokens = tokens_.load(std::memory_order_relaxed); + uint32_t expected_tokens = tokens.tokens_.load(std::memory_order_relaxed); do { // expected_tokens is either initialized above or reloaded during the CAS failure below. if (expected_tokens == 0) { @@ -64,13 +116,26 @@ bool LocalRateLimiterImpl::requestAllowed() const { synchronizer_.syncPoint("allowed_pre_cas"); // Loop while the weak CAS fails trying to subtract 1 from expected. - } while (!tokens_.compare_exchange_weak(expected_tokens, expected_tokens - 1, - std::memory_order_relaxed)); + } while (!tokens.tokens_.compare_exchange_weak(expected_tokens, expected_tokens - 1, + std::memory_order_relaxed)); // We successfully decremented the counter by 1. return true; } +bool LocalRateLimiterImpl::requestAllowed( + absl::Span request_descriptors) const { + if (!descriptors_.empty() && !request_descriptors.empty()) { + for (const auto& request_descriptor : request_descriptors) { + auto it = descriptors_.find(request_descriptor); + if (it != descriptors_.end()) { + return requestAllowedHelper(*it->token_state_); + } + } + } + return requestAllowedHelper(tokens_); +} + } // namespace LocalRateLimit } // namespace Common } // namespace Filters diff --git a/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.h b/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.h index 2e35dc5b0ef4..953fb612daf8 100644 --- a/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.h +++ b/source/extensions/filters/common/local_ratelimit/local_ratelimit_impl.h @@ -4,8 +4,11 @@ #include "envoy/event/dispatcher.h" #include "envoy/event/timer.h" +#include "envoy/extensions/common/ratelimit/v3/ratelimit.pb.h" +#include "envoy/ratelimit/ratelimit.h" #include "common/common/thread_synchronizer.h" +#include "common/protobuf/protobuf.h" namespace Envoy { namespace Extensions { @@ -15,20 +18,56 @@ namespace LocalRateLimit { class LocalRateLimiterImpl { public: - LocalRateLimiterImpl(const std::chrono::milliseconds fill_interval, const uint32_t max_tokens, - const uint32_t tokens_per_fill, Event::Dispatcher& dispatcher); + LocalRateLimiterImpl( + const std::chrono::milliseconds fill_interval, const uint32_t max_tokens, + const uint32_t tokens_per_fill, Event::Dispatcher& dispatcher, + const Protobuf::RepeatedPtrField< + envoy::extensions::common::ratelimit::v3::LocalRateLimitDescriptor>& descriptors); ~LocalRateLimiterImpl(); - bool requestAllowed() const; + bool requestAllowed(absl::Span request_descriptors) const; private: + struct TokenState { + mutable std::atomic tokens_; + MonotonicTime fill_time_; + }; + struct LocalDescriptorImpl : public RateLimit::LocalDescriptor { + std::unique_ptr token_state_; + RateLimit::TokenBucket token_bucket_; + std::string toString() const { + std::vector entries; + entries.reserve(entries_.size()); + for (const auto& entry : entries_) { + entries.push_back(absl::StrCat(entry.key_, "=", entry.value_)); + } + return absl::StrJoin(entries, ", "); + } + }; + struct LocalDescriptorHash { + using is_transparent = void; // NOLINT(readability-identifier-naming)t + size_t operator()(const RateLimit::LocalDescriptor& d) const { + return absl::Hash>()(d.entries_); + } + }; + struct LocalDescriptorEqual { + using is_transparent = void; // NOLINT(readability-identifier-naming) + size_t operator()(const RateLimit::LocalDescriptor& a, + const RateLimit::LocalDescriptor& b) const { + return a.entries_ == b.entries_; + } + }; + void onFillTimer(); + void onFillTimerHelper(const TokenState& state, const RateLimit::TokenBucket& bucket); + void onFillTimerDescriptorHelper(); + bool requestAllowedHelper(const TokenState& tokens) const; - const std::chrono::milliseconds fill_interval_; - const uint32_t max_tokens_; - const uint32_t tokens_per_fill_; + RateLimit::TokenBucket token_bucket_; const Event::TimerPtr fill_timer_; - mutable std::atomic tokens_; + TimeSource& time_source_; + TokenState tokens_; + absl::flat_hash_set descriptors_; mutable Thread::ThreadSynchronizer synchronizer_; // Used for testing only. friend class LocalRateLimiterImplTest; diff --git a/source/extensions/filters/http/dynamic_forward_proxy/BUILD b/source/extensions/filters/http/dynamic_forward_proxy/BUILD index dc15f124ed78..528a26369494 100644 --- a/source/extensions/filters/http/dynamic_forward_proxy/BUILD +++ b/source/extensions/filters/http/dynamic_forward_proxy/BUILD @@ -15,7 +15,6 @@ envoy_cc_library( hdrs = ["proxy_filter.h"], deps = [ "//include/envoy/http:filter_interface", - "//source/common/runtime:runtime_features_lib", "//source/extensions/clusters:well_known_names", "//source/extensions/common/dynamic_forward_proxy:dns_cache_interface", "//source/extensions/filters/http:well_known_names", diff --git a/source/extensions/filters/http/dynamic_forward_proxy/proxy_filter.cc b/source/extensions/filters/http/dynamic_forward_proxy/proxy_filter.cc index 84f57a535903..3c76af250c34 100644 --- a/source/extensions/filters/http/dynamic_forward_proxy/proxy_filter.cc +++ b/source/extensions/filters/http/dynamic_forward_proxy/proxy_filter.cc @@ -4,8 +4,6 @@ #include "envoy/config/core/v3/base.pb.h" #include "envoy/extensions/filters/http/dynamic_forward_proxy/v3/dynamic_forward_proxy.pb.h" -#include "common/runtime/runtime_features.h" - #include "extensions/clusters/well_known_names.h" #include "extensions/common/dynamic_forward_proxy/dns_cache.h" #include "extensions/filters/http/well_known_names.h" @@ -71,19 +69,9 @@ Http::FilterHeadersStatus ProxyFilter::decodeHeaders(Http::RequestHeaderMap& hea return Http::FilterHeadersStatus::Continue; } - const bool should_use_dns_cache_circuit_breakers = - Runtime::runtimeFeatureEnabled("envoy.reloadable_features.enable_dns_cache_circuit_breakers"); - - circuit_breaker_ = config_->cache().canCreateDnsRequest( - !should_use_dns_cache_circuit_breakers - ? absl::make_optional(std::reference_wrapper( - cluster_info_->resourceManager(route_entry->priority()).pendingRequests())) - : absl::nullopt); + circuit_breaker_ = config_->cache().canCreateDnsRequest(); if (circuit_breaker_ == nullptr) { - if (!should_use_dns_cache_circuit_breakers) { - cluster_info_->stats().upstream_rq_pending_overflow_.inc(); - } ENVOY_STREAM_LOG(debug, "pending request overflow", *this->decoder_callbacks_); this->decoder_callbacks_->sendLocalReply( Http::Code::ServiceUnavailable, ResponseStrings::get().PendingRequestOverflow, nullptr, @@ -132,17 +120,15 @@ Http::FilterHeadersStatus ProxyFilter::decodeHeaders(Http::RequestHeaderMap& hea } switch (result.status_) { - case LoadDnsCacheEntryStatus::InCache: { + case LoadDnsCacheEntryStatus::InCache: ASSERT(cache_load_handle_ == nullptr); ENVOY_STREAM_LOG(debug, "DNS cache entry already loaded, continuing", *decoder_callbacks_); return Http::FilterHeadersStatus::Continue; - } - case LoadDnsCacheEntryStatus::Loading: { + case LoadDnsCacheEntryStatus::Loading: ASSERT(cache_load_handle_ != nullptr); ENVOY_STREAM_LOG(debug, "waiting to load DNS cache entry", *decoder_callbacks_); return Http::FilterHeadersStatus::StopAllIterationAndWatermark; - } - case LoadDnsCacheEntryStatus::Overflow: { + case LoadDnsCacheEntryStatus::Overflow: ASSERT(cache_load_handle_ == nullptr); ENVOY_STREAM_LOG(debug, "DNS cache overflow", *decoder_callbacks_); decoder_callbacks_->sendLocalReply(Http::Code::ServiceUnavailable, @@ -150,8 +136,6 @@ Http::FilterHeadersStatus ProxyFilter::decodeHeaders(Http::RequestHeaderMap& hea absl::nullopt, ResponseStrings::get().DnsCacheOverflow); return Http::FilterHeadersStatus::StopIteration; } - } - NOT_REACHED_GCOVR_EXCL_LINE; } diff --git a/source/extensions/filters/http/ext_authz/ext_authz.cc b/source/extensions/filters/http/ext_authz/ext_authz.cc index 18a5e1e0c885..37247272d879 100644 --- a/source/extensions/filters/http/ext_authz/ext_authz.cc +++ b/source/extensions/filters/http/ext_authz/ext_authz.cc @@ -212,8 +212,7 @@ void Filter::onComplete(Filters::Common::ExtAuthz::ResponsePtr&& response) { for (const auto& header : response->headers_to_remove) { // We don't allow removing any :-prefixed headers, nor Host, as removing // them would make the request malformed. - if (absl::StartsWithIgnoreCase(absl::string_view(header.get()), ":") || - header == Http::Headers::get().HostLegacy) { + if (!Http::HeaderUtility::isRemovableHeader(header.get())) { continue; } ENVOY_STREAM_LOG(trace, "'{}'", *callbacks_, header.get()); diff --git a/source/extensions/filters/http/ext_proc/BUILD b/source/extensions/filters/http/ext_proc/BUILD index 93b55a5586f8..933e17b54dc6 100644 --- a/source/extensions/filters/http/ext_proc/BUILD +++ b/source/extensions/filters/http/ext_proc/BUILD @@ -14,9 +14,15 @@ envoy_cc_library( srcs = ["ext_proc.cc"], hdrs = ["ext_proc.h"], deps = [ + ":client_interface", + ":mutation_utils_lib", "//include/envoy/http:filter_interface", + "//include/envoy/http:header_map_interface", + "//include/envoy/stats:stats_macros", "//source/extensions/filters/http/common:pass_through_filter_lib", + "@com_google_absl//absl/strings:str_format", "@envoy_api//envoy/extensions/filters/http/ext_proc/v3alpha:pkg_cc_proto", + "@envoy_api//envoy/service/ext_proc/v3alpha:pkg_cc_proto", ], ) @@ -27,6 +33,7 @@ envoy_cc_extension( security_posture = "unknown", status = "alpha", deps = [ + ":client_lib", ":ext_proc", "//source/extensions/filters/http:well_known_names", "//source/extensions/filters/http/common:factory_base_lib", @@ -43,6 +50,18 @@ envoy_cc_library( ], ) +envoy_cc_library( + name = "mutation_utils_lib", + srcs = ["mutation_utils.cc"], + hdrs = ["mutation_utils.h"], + deps = [ + "//include/envoy/http:header_map_interface", + "//source/common/http:header_utility_lib", + "//source/common/protobuf:utility_lib", + "@envoy_api//envoy/service/ext_proc/v3alpha:pkg_cc_proto", + ], +) + envoy_cc_library( name = "client_lib", srcs = ["client_impl.cc"], diff --git a/source/extensions/filters/http/ext_proc/config.cc b/source/extensions/filters/http/ext_proc/config.cc index 83d6ac7de14c..7db245641a54 100644 --- a/source/extensions/filters/http/ext_proc/config.cc +++ b/source/extensions/filters/http/ext_proc/config.cc @@ -2,6 +2,7 @@ #include +#include "extensions/filters/http/ext_proc/client_impl.h" #include "extensions/filters/http/ext_proc/ext_proc.h" namespace Envoy { @@ -11,11 +12,19 @@ namespace ExternalProcessing { Http::FilterFactoryCb ExternalProcessingFilterConfig::createFilterFactoryFromProtoTyped( const envoy::extensions::filters::http::ext_proc::v3alpha::ExternalProcessor& proto_config, - const std::string&, Server::Configuration::FactoryContext&) { - const auto filter_config = std::make_shared(proto_config); + const std::string& stats_prefix, Server::Configuration::FactoryContext& context) { + const uint32_t timeout_ms = + PROTOBUF_GET_MS_OR_DEFAULT(proto_config.grpc_service(), timeout, DefaultTimeout); + const auto filter_config = std::make_shared( + proto_config, std::chrono::milliseconds(timeout_ms), context.scope(), stats_prefix); - return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) { - callbacks.addStreamFilter(Http::StreamFilterSharedPtr{std::make_shared(filter_config)}); + return [filter_config, grpc_service = proto_config.grpc_service(), + &context](Http::FilterChainFactoryCallbacks& callbacks) { + auto client = std::make_unique( + context.clusterManager().grpcAsyncClientManager(), grpc_service, context.scope()); + + callbacks.addStreamFilter( + Http::StreamFilterSharedPtr{std::make_shared(filter_config, std::move(client))}); }; } diff --git a/source/extensions/filters/http/ext_proc/config.h b/source/extensions/filters/http/ext_proc/config.h index c6e961ffa9e3..d6b0df8ded2c 100644 --- a/source/extensions/filters/http/ext_proc/config.h +++ b/source/extensions/filters/http/ext_proc/config.h @@ -21,6 +21,8 @@ class ExternalProcessingFilterConfig ExternalProcessingFilterConfig() : FactoryBase(HttpFilterNames::get().ExternalProcessing) {} private: + static constexpr uint64_t DefaultTimeout = 200; + Http::FilterFactoryCb createFilterFactoryFromProtoTyped( const envoy::extensions::filters::http::ext_proc::v3alpha::ExternalProcessor& proto_config, const std::string& stats_prefix, Server::Configuration::FactoryContext& context) override; diff --git a/source/extensions/filters/http/ext_proc/ext_proc.cc b/source/extensions/filters/http/ext_proc/ext_proc.cc index 7beb30461201..9b0ce3da4144 100644 --- a/source/extensions/filters/http/ext_proc/ext_proc.cc +++ b/source/extensions/filters/http/ext_proc/ext_proc.cc @@ -1,11 +1,183 @@ #include "extensions/filters/http/ext_proc/ext_proc.h" +#include "extensions/filters/http/ext_proc/mutation_utils.h" + +#include "absl/strings/str_format.h" + namespace Envoy { namespace Extensions { namespace HttpFilters { namespace ExternalProcessing { -void Filter::onDestroy() {} +using envoy::service::ext_proc::v3alpha::HeadersResponse; +using envoy::service::ext_proc::v3alpha::ImmediateResponse; +using envoy::service::ext_proc::v3alpha::ProcessingRequest; +using envoy::service::ext_proc::v3alpha::ProcessingResponse; + +using Http::FilterHeadersStatus; +using Http::RequestHeaderMap; +using Http::ResponseHeaderMap; + +static const std::string kErrorPrefix = "ext_proc error"; + +void Filter::closeStream() { + if (!stream_closed_) { + if (stream_) { + ENVOY_LOG(debug, "Closing gRPC stream to processing server"); + stream_->close(); + stats_.streams_closed_.inc(); + } + stream_closed_ = true; + } +} + +void Filter::onDestroy() { closeStream(); } + +FilterHeadersStatus Filter::decodeHeaders(RequestHeaderMap& headers, bool end_of_stream) { + // We're at the start, so start the stream and send a headers message + request_headers_ = &headers; + stream_ = client_->start(*this, config_->grpcTimeout()); + stats_.streams_started_.inc(); + ProcessingRequest req; + auto* headers_req = req.mutable_request_headers(); + MutationUtils::buildHttpHeaders(headers, *headers_req->mutable_headers()); + headers_req->set_end_of_stream(end_of_stream); + request_state_ = FilterState::HEADERS; + stream_->send(std::move(req), false); + stats_.stream_msgs_sent_.inc(); + + // Wait until we have a gRPC response before allowing any more callbacks + return FilterHeadersStatus::StopAllIterationAndWatermark; +} + +FilterHeadersStatus Filter::encodeHeaders(ResponseHeaderMap& headers, bool end_of_stream) { + if (stream_closed_) { + return FilterHeadersStatus::Continue; + } + + response_headers_ = &headers; + ProcessingRequest req; + auto* headers_req = req.mutable_response_headers(); + MutationUtils::buildHttpHeaders(headers, *headers_req->mutable_headers()); + headers_req->set_end_of_stream(end_of_stream); + response_state_ = FilterState::HEADERS; + stream_->send(std::move(req), false); + stats_.stream_msgs_sent_.inc(); + return FilterHeadersStatus::StopAllIterationAndWatermark; +} + +void Filter::onReceiveMessage( + std::unique_ptr&& r) { + auto response = std::move(r); + bool message_handled = false; + ENVOY_LOG(debug, "Received gRPC message. State = {}", request_state_); + + if (response->has_request_headers()) { + message_handled = handleRequestHeadersResponse(response->request_headers()); + } else if (response->has_response_headers()) { + message_handled = handleResponseHeadersResponse(response->response_headers()); + } else if (response->has_immediate_response()) { + handleImmediateResponse(response->immediate_response()); + message_handled = true; + } + + if (message_handled) { + stats_.stream_msgs_received_.inc(); + } else { + stats_.spurious_msgs_received_.inc(); + // Ignore messages received out of order. However, close the stream to + // protect ourselves since the server is not following the protocol. + ENVOY_LOG(warn, "Spurious response message received on gRPC stream"); + cleanupState(); + closeStream(); + } +} + +bool Filter::handleRequestHeadersResponse(const HeadersResponse& response) { + if (request_state_ == FilterState::HEADERS) { + ENVOY_LOG(debug, "applying request_headers response"); + MutationUtils::applyCommonHeaderResponse(response, *request_headers_); + request_state_ = FilterState::IDLE; + decoder_callbacks_->continueDecoding(); + return true; + } + return false; +} + +bool Filter::handleResponseHeadersResponse(const HeadersResponse& response) { + if (response_state_ == FilterState::HEADERS) { + ENVOY_LOG(debug, "applying response_headers response"); + MutationUtils::applyCommonHeaderResponse(response, *response_headers_); + response_state_ = FilterState::IDLE; + encoder_callbacks_->continueEncoding(); + return true; + } + return false; +} + +void Filter::handleImmediateResponse(const ImmediateResponse& response) { + // We don't want to process any more stream messages after this. + // Close the stream before sending because "sendLocalResponse" triggers + // additional calls to this filter. + request_state_ = FilterState::IDLE; + response_state_ = FilterState::IDLE; + closeStream(); + sendImmediateResponse(response); +} + +void Filter::onGrpcError(Grpc::Status::GrpcStatus status) { + ENVOY_LOG(debug, "Received gRPC error on stream: {}", status); + stats_.streams_failed_.inc(); + + if (config_->failureModeAllow()) { + // Ignore this and treat as a successful close + onGrpcClose(); + stats_.failure_mode_allowed_.inc(); + + } else { + stream_closed_ = true; + ImmediateResponse errorResponse; + errorResponse.mutable_status()->set_code(envoy::type::v3::StatusCode::InternalServerError); + errorResponse.set_details(absl::StrFormat("%s: gRPC error %i", kErrorPrefix, status)); + handleImmediateResponse(errorResponse); + } +} + +void Filter::onGrpcClose() { + ENVOY_LOG(debug, "Received gRPC stream close"); + stream_closed_ = true; + stats_.streams_closed_.inc(); + // Successful close. We can ignore the stream for the rest of our request + // and response processing. + cleanupState(); +} + +void Filter::cleanupState() { + if (request_state_ != FilterState::IDLE) { + request_state_ = FilterState::IDLE; + decoder_callbacks_->continueDecoding(); + } + if (response_state_ != FilterState::IDLE) { + response_state_ = FilterState::IDLE; + encoder_callbacks_->continueEncoding(); + } +} + +void Filter::sendImmediateResponse(const ImmediateResponse& response) { + const auto status_code = response.has_status() ? response.status().code() : 200; + const auto grpc_status = + response.has_grpc_status() + ? absl::optional(response.grpc_status().status()) + : absl::nullopt; + const auto mutate_headers = [&response](Http::ResponseHeaderMap& headers) { + if (response.has_headers()) { + MutationUtils::applyHeaderMutations(response.headers(), headers); + } + }; + + encoder_callbacks_->sendLocalReply(static_cast(status_code), response.body(), + mutate_headers, grpc_status, response.details()); +} } // namespace ExternalProcessing } // namespace HttpFilters diff --git a/source/extensions/filters/http/ext_proc/ext_proc.h b/source/extensions/filters/http/ext_proc/ext_proc.h index 069cc3008c17..b8c8860d00a8 100644 --- a/source/extensions/filters/http/ext_proc/ext_proc.h +++ b/source/extensions/filters/http/ext_proc/ext_proc.h @@ -1,41 +1,131 @@ #pragma once +#include #include #include "envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.pb.h" #include "envoy/grpc/async_client.h" #include "envoy/http/filter.h" +#include "envoy/service/ext_proc/v3alpha/external_processor.pb.h" +#include "envoy/stats/scope.h" +#include "envoy/stats/stats_macros.h" #include "common/common/logger.h" #include "extensions/filters/http/common/pass_through_filter.h" +#include "extensions/filters/http/ext_proc/client.h" namespace Envoy { namespace Extensions { namespace HttpFilters { namespace ExternalProcessing { +#define ALL_EXT_PROC_FILTER_STATS(COUNTER) \ + COUNTER(streams_started) \ + COUNTER(stream_msgs_sent) \ + COUNTER(stream_msgs_received) \ + COUNTER(spurious_msgs_received) \ + COUNTER(streams_closed) \ + COUNTER(streams_failed) \ + COUNTER(failure_mode_allowed) + +struct ExtProcFilterStats { + ALL_EXT_PROC_FILTER_STATS(GENERATE_COUNTER_STRUCT) +}; + class FilterConfig { public: - FilterConfig(const envoy::extensions::filters::http::ext_proc::v3alpha::ExternalProcessor& config) - : failure_mode_allow_(config.failure_mode_allow()) {} + FilterConfig(const envoy::extensions::filters::http::ext_proc::v3alpha::ExternalProcessor& config, + const std::chrono::milliseconds grpc_timeout, Stats::Scope& scope, + const std::string& stats_prefix) + : failure_mode_allow_(config.failure_mode_allow()), grpc_timeout_(grpc_timeout), + stats_(generateStats(stats_prefix, config.stat_prefix(), scope)) {} bool failureModeAllow() const { return failure_mode_allow_; } + const std::chrono::milliseconds& grpcTimeout() const { return grpc_timeout_; } + + const ExtProcFilterStats& stats() const { return stats_; } + private: + ExtProcFilterStats generateStats(const std::string& prefix, + const std::string& filter_stats_prefix, Stats::Scope& scope) { + const std::string final_prefix = absl::StrCat(prefix, "ext_proc.", filter_stats_prefix); + return {ALL_EXT_PROC_FILTER_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))}; + } + const bool failure_mode_allow_; + const std::chrono::milliseconds grpc_timeout_; + + ExtProcFilterStats stats_; }; using FilterConfigSharedPtr = std::shared_ptr; -class Filter : public Logger::Loggable, public Http::PassThroughFilter { +class Filter : public Logger::Loggable, + public Http::PassThroughFilter, + public ExternalProcessorCallbacks { + // The state of filter execution -- this is used to determine + // how to handle gRPC callbacks. + enum class FilterState { + // The filter is not waiting for anything, so any response on the + // gRPC stream is spurious and will result in the filter closing + // the stream. + IDLE, + // The filter is waiting for a "request_headers" or a "response_headers" message. + // Any other response on the gRPC stream will be treated as spurious. + HEADERS, + }; + public: - Filter(const FilterConfigSharedPtr& config) : config_(config) {} + Filter(const FilterConfigSharedPtr& config, ExternalProcessorClientPtr&& client) + : config_(config), client_(std::move(client)), stats_(config->stats()) {} void onDestroy() override; + Http::FilterHeadersStatus decodeHeaders(Http::RequestHeaderMap& headers, + bool end_stream) override; + Http::FilterHeadersStatus encodeHeaders(Http::ResponseHeaderMap& headers, + bool end_stream) override; + + // ExternalProcessorCallbacks + + void onReceiveMessage( + std::unique_ptr&& response) override; + + void onGrpcError(Grpc::Status::GrpcStatus error) override; + + void onGrpcClose() override; + private: - FilterConfigSharedPtr config_; + void closeStream(); + void cleanupState(); + void sendImmediateResponse(const envoy::service::ext_proc::v3alpha::ImmediateResponse& response); + + bool + handleRequestHeadersResponse(const envoy::service::ext_proc::v3alpha::HeadersResponse& response); + bool + handleResponseHeadersResponse(const envoy::service::ext_proc::v3alpha::HeadersResponse& response); + void + handleImmediateResponse(const envoy::service::ext_proc::v3alpha::ImmediateResponse& response); + + const FilterConfigSharedPtr config_; + const ExternalProcessorClientPtr client_; + ExtProcFilterStats stats_; + + // The state of the request-processing, or "decoding" side of the filter. + // We maintain separate states for encoding and decoding since they may + // be interleaved. + FilterState request_state_ = FilterState::IDLE; + + // The state of the response-processing side + FilterState response_state_ = FilterState::IDLE; + + ExternalProcessorStreamPtr stream_; + bool stream_closed_ = false; + + Http::HeaderMap* request_headers_ = nullptr; + Http::HeaderMap* response_headers_ = nullptr; }; } // namespace ExternalProcessing diff --git a/source/extensions/filters/http/ext_proc/mutation_utils.cc b/source/extensions/filters/http/ext_proc/mutation_utils.cc new file mode 100644 index 000000000000..84df1e6a40d7 --- /dev/null +++ b/source/extensions/filters/http/ext_proc/mutation_utils.cc @@ -0,0 +1,80 @@ +#include "extensions/filters/http/ext_proc/mutation_utils.h" + +#include "envoy/http/header_map.h" + +#include "common/http/header_utility.h" +#include "common/http/headers.h" +#include "common/protobuf/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { + +using Http::Headers; +using Http::LowerCaseString; + +using envoy::service::ext_proc::v3alpha::HeaderMutation; +using envoy::service::ext_proc::v3alpha::HeadersResponse; + +void MutationUtils::buildHttpHeaders(const Http::HeaderMap& headers_in, + envoy::config::core::v3::HeaderMap& headers_out) { + headers_in.iterate([&headers_out](const Http::HeaderEntry& e) -> Http::HeaderMap::Iterate { + auto* new_header = headers_out.add_headers(); + new_header->set_key(std::string(e.key().getStringView())); + new_header->set_value(std::string(e.value().getStringView())); + return Http::HeaderMap::Iterate::Continue; + }); +} + +void MutationUtils::applyCommonHeaderResponse(const HeadersResponse& response, + Http::HeaderMap& headers) { + if (response.has_response()) { + const auto& common_response = response.response(); + if (common_response.has_header_mutation()) { + applyHeaderMutations(common_response.header_mutation(), headers); + } + } +} + +void MutationUtils::applyHeaderMutations(const HeaderMutation& mutation, Http::HeaderMap& headers) { + for (const auto& remove_header : mutation.remove_headers()) { + if (Http::HeaderUtility::isRemovableHeader(remove_header)) { + headers.remove(LowerCaseString(remove_header)); + } + } + + for (const auto& sh : mutation.set_headers()) { + if (!sh.has_header()) { + continue; + } + if (isSettableHeader(sh.header().key())) { + // Make "false" the default. This is logical and matches the ext_authz + // filter. However, the router handles this same protobuf and uses "true" + // as the default instead. + const bool append = PROTOBUF_GET_WRAPPED_OR_DEFAULT(sh, append, false); + if (append) { + headers.addCopy(LowerCaseString(sh.header().key()), sh.header().value()); + } else { + headers.setCopy(LowerCaseString(sh.header().key()), sh.header().value()); + } + } + } +} + +// Ignore attempts to set certain sensitive headers that can break later processing. +// We may re-enable some of these after further testing. This logic is specific +// to the ext_proc filter so it is not shared with HeaderUtils. +bool MutationUtils::isSettableHeader(absl::string_view key) { + const auto& headers = Headers::get(); + return !absl::EqualsIgnoreCase(key, headers.HostLegacy.get()) && + !absl::EqualsIgnoreCase(key, headers.Host.get()) && + !absl::EqualsIgnoreCase(key, headers.Method.get()) && + !absl::EqualsIgnoreCase(key, headers.Scheme.get()) && + !absl::StartsWithIgnoreCase(key, headers.prefix()); +} + +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/http/ext_proc/mutation_utils.h b/source/extensions/filters/http/ext_proc/mutation_utils.h new file mode 100644 index 000000000000..1fc888e77d8c --- /dev/null +++ b/source/extensions/filters/http/ext_proc/mutation_utils.h @@ -0,0 +1,34 @@ +#pragma once + +#include "envoy/http/header_map.h" +#include "envoy/service/ext_proc/v3alpha/external_processor.pb.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { + +class MutationUtils { +public: + // Convert a header map until a protobuf + static void buildHttpHeaders(const Http::HeaderMap& headers_in, + envoy::config::core::v3::HeaderMap& headers_out); + + // Apply mutations that are common to header responses. + static void + applyCommonHeaderResponse(const envoy::service::ext_proc::v3alpha::HeadersResponse& response, + Http::HeaderMap& headers); + + // Modify header map based on a set of mutations from a protobuf + static void + applyHeaderMutations(const envoy::service::ext_proc::v3alpha::HeaderMutation& mutation, + Http::HeaderMap& headers); + +private: + static bool isSettableHeader(absl::string_view key); +}; + +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/source/extensions/filters/http/local_ratelimit/BUILD b/source/extensions/filters/http/local_ratelimit/BUILD index 048d7d4ed4e0..91493ff13f66 100644 --- a/source/extensions/filters/http/local_ratelimit/BUILD +++ b/source/extensions/filters/http/local_ratelimit/BUILD @@ -26,6 +26,7 @@ envoy_cc_library( "//source/common/router:header_parser_lib", "//source/common/runtime:runtime_lib", "//source/extensions/filters/common/local_ratelimit:local_ratelimit_lib", + "//source/extensions/filters/common/ratelimit:ratelimit_lib", "//source/extensions/filters/http/common:pass_through_filter_lib", "@envoy_api//envoy/extensions/filters/http/local_ratelimit/v3:pkg_cc_proto", ], diff --git a/source/extensions/filters/http/local_ratelimit/config.cc b/source/extensions/filters/http/local_ratelimit/config.cc index 529fd0dd2977..a5e629d88055 100644 --- a/source/extensions/filters/http/local_ratelimit/config.cc +++ b/source/extensions/filters/http/local_ratelimit/config.cc @@ -17,7 +17,7 @@ Http::FilterFactoryCb LocalRateLimitFilterConfig::createFilterFactoryFromProtoTy const envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit& proto_config, const std::string&, Server::Configuration::FactoryContext& context) { FilterConfigSharedPtr filter_config = std::make_shared( - proto_config, context.dispatcher(), context.scope(), context.runtime()); + proto_config, context.localInfo(), context.dispatcher(), context.scope(), context.runtime()); return [filter_config](Http::FilterChainFactoryCallbacks& callbacks) -> void { callbacks.addStreamFilter(std::make_shared(filter_config)); }; @@ -27,7 +27,8 @@ Router::RouteSpecificFilterConfigConstSharedPtr LocalRateLimitFilterConfig::createRouteSpecificFilterConfigTyped( const envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit& proto_config, Server::Configuration::ServerFactoryContext& context, ProtobufMessage::ValidationVisitor&) { - return std::make_shared(proto_config, context.dispatcher(), context.scope(), + return std::make_shared(proto_config, context.localInfo(), + context.dispatcher(), context.scope(), context.runtime(), true); } diff --git a/source/extensions/filters/http/local_ratelimit/local_ratelimit.cc b/source/extensions/filters/http/local_ratelimit/local_ratelimit.cc index 3b13bfa374ac..263d77849dc2 100644 --- a/source/extensions/filters/http/local_ratelimit/local_ratelimit.cc +++ b/source/extensions/filters/http/local_ratelimit/local_ratelimit.cc @@ -6,6 +6,7 @@ #include "envoy/http/codes.h" #include "common/http/utility.h" +#include "common/router/config_impl.h" namespace Envoy { namespace Extensions { @@ -14,16 +15,17 @@ namespace LocalRateLimitFilter { FilterConfig::FilterConfig( const envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit& config, - Event::Dispatcher& dispatcher, Stats::Scope& scope, Runtime::Loader& runtime, - const bool per_route) + const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher, Stats::Scope& scope, + Runtime::Loader& runtime, const bool per_route) : status_(toErrorCode(config.status().code())), stats_(generateStats(config.stat_prefix(), scope)), rate_limiter_(Filters::Common::LocalRateLimit::LocalRateLimiterImpl( std::chrono::milliseconds( PROTOBUF_GET_MS_OR_DEFAULT(config.token_bucket(), fill_interval, 0)), config.token_bucket().max_tokens(), - PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.token_bucket(), tokens_per_fill, 1), dispatcher)), - runtime_(runtime), + PROTOBUF_GET_WRAPPED_OR_DEFAULT(config.token_bucket(), tokens_per_fill, 1), dispatcher, + config.descriptors())), + local_info_(local_info), runtime_(runtime), filter_enabled_( config.has_filter_enabled() ? absl::optional( @@ -35,7 +37,9 @@ FilterConfig::FilterConfig( Envoy::Runtime::FractionalPercent(config.filter_enforced(), runtime_)) : absl::nullopt), response_headers_parser_( - Envoy::Router::HeaderParser::configure(config.response_headers_to_add())) { + Envoy::Router::HeaderParser::configure(config.response_headers_to_add())), + stage_(static_cast(config.stage())), + has_descriptors_(!config.descriptors().empty()) { // Note: no token bucket is fine for the global config, which would be the case for enabling // the filter globally but disabled and then applying limits at the virtual host or // route level. At the virtual or route level, it makes no sense to have an no token @@ -46,7 +50,10 @@ FilterConfig::FilterConfig( } } -bool FilterConfig::requestAllowed() const { return rate_limiter_.requestAllowed(); } +bool FilterConfig::requestAllowed( + absl::Span request_descriptors) const { + return rate_limiter_.requestAllowed(request_descriptors); +} LocalRateLimitStats FilterConfig::generateStats(const std::string& prefix, Stats::Scope& scope) { const std::string final_prefix = prefix + ".http_local_rate_limit"; @@ -61,7 +68,7 @@ bool FilterConfig::enforced() const { return filter_enforced_.has_value() ? filter_enforced_->enabled() : false; } -Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap&, bool) { +Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap& headers, bool) { const auto* config = getConfig(); if (!config->enabled()) { @@ -70,7 +77,12 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap&, bool) { config->stats().enabled_.inc(); - if (config->requestAllowed()) { + std::vector descriptors; + if (config->hasDescriptors()) { + populateDescriptors(descriptors, headers); + } + + if (config->requestAllowed(descriptors)) { config->stats().ok_.inc(); return Http::FilterHeadersStatus::Continue; } @@ -94,6 +106,28 @@ Http::FilterHeadersStatus Filter::decodeHeaders(Http::RequestHeaderMap&, bool) { return Http::FilterHeadersStatus::StopIteration; } +void Filter::populateDescriptors(std::vector& descriptors, + Http::RequestHeaderMap& headers) { + Router::RouteConstSharedPtr route = decoder_callbacks_->route(); + if (!route || !route->routeEntry()) { + return; + } + + const Router::RouteEntry* route_entry = route->routeEntry(); + // Get all applicable rate limit policy entries for the route. + const auto* config = getConfig(); + for (const Router::RateLimitPolicyEntry& rate_limit : + route_entry->rateLimitPolicy().getApplicableRateLimit(config->stage())) { + const std::string& disable_key = rate_limit.disableKey(); + + if (!disable_key.empty()) { + continue; + } + rate_limit.populateLocalDescriptors(descriptors, config->localInfo().clusterName(), headers, + decoder_callbacks_->streamInfo()); + } +} + const FilterConfig* Filter::getConfig() const { const auto* config = Http::Utility::resolveMostSpecificPerFilterConfig( "envoy.filters.http.local_ratelimit", decoder_callbacks_->route()); diff --git a/source/extensions/filters/http/local_ratelimit/local_ratelimit.h b/source/extensions/filters/http/local_ratelimit/local_ratelimit.h index 6549094d07c3..cffbc399c05d 100644 --- a/source/extensions/filters/http/local_ratelimit/local_ratelimit.h +++ b/source/extensions/filters/http/local_ratelimit/local_ratelimit.h @@ -7,6 +7,7 @@ #include "envoy/extensions/filters/http/local_ratelimit/v3/local_rate_limit.pb.h" #include "envoy/http/filter.h" +#include "envoy/local_info/local_info.h" #include "envoy/runtime/runtime.h" #include "envoy/stats/scope.h" #include "envoy/stats/stats_macros.h" @@ -17,6 +18,7 @@ #include "common/runtime/runtime_protos.h" #include "extensions/filters/common/local_ratelimit/local_ratelimit_impl.h" +#include "extensions/filters/common/ratelimit/ratelimit.h" #include "extensions/filters/http/common/pass_through_filter.h" namespace Envoy { @@ -43,19 +45,22 @@ struct LocalRateLimitStats { /** * Global configuration for the HTTP local rate limit filter. */ -class FilterConfig : public ::Envoy::Router::RouteSpecificFilterConfig { +class FilterConfig : public Router::RouteSpecificFilterConfig { public: FilterConfig(const envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit& config, - Event::Dispatcher& dispatcher, Stats::Scope& scope, Runtime::Loader& runtime, - bool per_route = false); + const LocalInfo::LocalInfo& local_info, Event::Dispatcher& dispatcher, + Stats::Scope& scope, Runtime::Loader& runtime, bool per_route = false); ~FilterConfig() override = default; + const LocalInfo::LocalInfo& localInfo() const { return local_info_; } Runtime::Loader& runtime() { return runtime_; } - bool requestAllowed() const; + bool requestAllowed(absl::Span request_descriptors) const; bool enabled() const; bool enforced() const; LocalRateLimitStats& stats() const { return stats_; } const Router::HeaderParser& responseHeadersParser() const { return *response_headers_parser_; } Http::Code status() const { return status_; } + uint64_t stage() const { return stage_; } + bool hasDescriptors() const { return has_descriptors_; } private: friend class FilterTest; @@ -73,10 +78,13 @@ class FilterConfig : public ::Envoy::Router::RouteSpecificFilterConfig { const Http::Code status_; mutable LocalRateLimitStats stats_; Filters::Common::LocalRateLimit::LocalRateLimiterImpl rate_limiter_; + const LocalInfo::LocalInfo& local_info_; Runtime::Loader& runtime_; const absl::optional filter_enabled_; const absl::optional filter_enforced_; Router::HeaderParserPtr response_headers_parser_; + const uint64_t stage_; + const bool has_descriptors_; }; using FilterConfigSharedPtr = std::shared_ptr; @@ -96,8 +104,10 @@ class Filter : public Http::PassThroughFilter { private: friend class FilterTest; - const FilterConfig* getConfig() const; + void populateDescriptors(std::vector& descriptors, + Http::RequestHeaderMap& headers); + const FilterConfig* getConfig() const; FilterConfigSharedPtr config_; }; diff --git a/source/extensions/filters/http/oauth2/filter.cc b/source/extensions/filters/http/oauth2/filter.cc index aa1396ec9302..892ab8a9abcd 100644 --- a/source/extensions/filters/http/oauth2/filter.cc +++ b/source/extensions/filters/http/oauth2/filter.cc @@ -48,7 +48,7 @@ constexpr const char* CookieTailHttpOnlyFormatString = ";version=1;path=/;Max-Age={};secure;HttpOnly"; const char* AuthorizationEndpointFormat = - "{}?client_id={}&scope=user&response_type=code&redirect_uri={}&state={}"; + "{}?client_id={}&scope={}&response_type=code&redirect_uri={}&state={}"; constexpr absl::string_view UnauthorizedBodyMessage = "OAuth flow failed."; @@ -60,6 +60,7 @@ constexpr absl::string_view REDIRECT_RACE = "oauth.race_redirect"; constexpr absl::string_view REDIRECT_LOGGED_IN = "oauth.logged_in"; constexpr absl::string_view REDIRECT_FOR_CREDENTIALS = "oauth.missing_credentials"; constexpr absl::string_view SIGN_OUT = "oauth.sign_out"; +constexpr absl::string_view DEFAULT_AUTH_SCOPE = "user"; template std::vector headerMatchers(const T& matcher_protos) { @@ -73,6 +74,25 @@ std::vector headerMatchers(const T& matcher_pro return matchers; } +// Transforms the proto list of 'auth_scopes' into a vector of std::string, also +// handling the default value logic. +std::vector +authScopesList(const Protobuf::RepeatedPtrField& auth_scopes_protos) { + std::vector scopes; + + // If 'auth_scopes' is empty it must return a list with the default value. + if (auth_scopes_protos.empty()) { + scopes.emplace_back(DEFAULT_AUTH_SCOPE); + } else { + scopes.reserve(auth_scopes_protos.size()); + + for (const auto& scope : auth_scopes_protos) { + scopes.emplace_back(scope); + } + } + return scopes; +} + // Sets the auth token as the Bearer token in the authorization header. void setBearerToken(Http::RequestHeaderMap& headers, const std::string& token) { headers.setInline(authorization_handle.handle(), absl::StrCat("Bearer ", token)); @@ -90,6 +110,8 @@ FilterConfig::FilterConfig( redirect_matcher_(proto_config.redirect_path_matcher()), signout_path_(proto_config.signout_path()), secret_reader_(secret_reader), stats_(FilterConfig::generateStats(stats_prefix, scope)), + encoded_auth_scopes_(Http::Utility::PercentEncoding::encode( + absl::StrJoin(authScopesList(proto_config.auth_scopes()), " "), ":/=&? ")), forward_bearer_token_(proto_config.forward_bearer_token()), pass_through_header_matchers_(headerMatchers(proto_config.pass_through_matcher())) { if (!cluster_manager.clusters().hasCluster(oauth_token_endpoint_.cluster())) { @@ -275,9 +297,9 @@ Http::FilterHeadersStatus OAuth2Filter::decodeHeaders(Http::RequestHeaderMap& he const std::string escaped_redirect_uri = Http::Utility::PercentEncoding::encode(redirect_uri, ":/=&?"); - const std::string new_url = - fmt::format(AuthorizationEndpointFormat, config_->authorizationEndpoint(), - config_->clientId(), escaped_redirect_uri, escaped_state); + const std::string new_url = fmt::format( + AuthorizationEndpointFormat, config_->authorizationEndpoint(), config_->clientId(), + config_->encodedAuthScopes(), escaped_redirect_uri, escaped_state); response_headers->setLocation(new_url); decoder_callbacks_->encodeHeaders(std::move(response_headers), true, REDIRECT_FOR_CREDENTIALS); diff --git a/source/extensions/filters/http/oauth2/filter.h b/source/extensions/filters/http/oauth2/filter.h index eae64214ee09..aa2367ab815f 100644 --- a/source/extensions/filters/http/oauth2/filter.h +++ b/source/extensions/filters/http/oauth2/filter.h @@ -123,6 +123,7 @@ class FilterConfig { std::string clientSecret() const { return secret_reader_->clientSecret(); } std::string tokenSecret() const { return secret_reader_->tokenSecret(); } FilterStats& stats() { return stats_; } + const std::string& encodedAuthScopes() const { return encoded_auth_scopes_; } private: static FilterStats generateStats(const std::string& prefix, Stats::Scope& scope); @@ -135,6 +136,7 @@ class FilterConfig { const Matchers::PathMatcher signout_path_; std::shared_ptr secret_reader_; FilterStats stats_; + const std::string encoded_auth_scopes_; const bool forward_bearer_token_ : 1; const std::vector pass_through_header_matchers_; }; diff --git a/source/extensions/filters/http/oauth2/oauth_client.cc b/source/extensions/filters/http/oauth2/oauth_client.cc index 170d4a9a2c98..ec0793587448 100644 --- a/source/extensions/filters/http/oauth2/oauth_client.cc +++ b/source/extensions/filters/http/oauth2/oauth_client.cc @@ -70,6 +70,7 @@ void OAuth2ClientImpl::onSuccess(const Http::AsyncClient::Request&, const auto response_code = message->headers().Status()->value().getStringView(); if (response_code != "200") { ENVOY_LOG(debug, "Oauth response code: {}", response_code); + ENVOY_LOG(debug, "Oauth response body: {}", message->bodyAsString()); parent_->sendUnauthorizedResponse(); return; } diff --git a/source/extensions/filters/http/oauth2/oauth_client.h b/source/extensions/filters/http/oauth2/oauth_client.h index ebc9a97e3ab5..e270c327236e 100644 --- a/source/extensions/filters/http/oauth2/oauth_client.h +++ b/source/extensions/filters/http/oauth2/oauth_client.h @@ -90,7 +90,13 @@ class OAuth2ClientImpl : public OAuth2Client, Logger::Loggableheaders().setReferenceMethod(Http::Headers::get().MethodValues.Post); - request->headers().setContentType(Http::Headers::get().ContentTypeValues.FormUrlEncoded); + request->headers().setReferenceContentType( + Http::Headers::get().ContentTypeValues.FormUrlEncoded); + // Use the Accept header to ensure the Access Token Response is returned as JSON. + // Some authorization servers return other encodings (e.g. FormUrlEncoded) in the absence of the + // Accept header. RFC 6749 Section 5.1 defines the media type to be JSON, so this is safe. + request->headers().setReference(Http::CustomHeaders::get().Accept, + Http::Headers::get().ContentTypeValues.Json); return request; } }; diff --git a/source/extensions/filters/network/common/redis/client_impl.cc b/source/extensions/filters/network/common/redis/client_impl.cc index 6cdc7b8ad007..289de27c32c1 100644 --- a/source/extensions/filters/network/common/redis/client_impl.cc +++ b/source/extensions/filters/network/common/redis/client_impl.cc @@ -2,6 +2,8 @@ #include "envoy/extensions/filters/network/redis_proxy/v3/redis_proxy.pb.h" +#include "common/runtime/runtime_features.h" + namespace Envoy { namespace Extensions { namespace NetworkFilters { @@ -64,7 +66,9 @@ ClientPtr ClientImpl::create(Upstream::HostConstSharedPtr host, Event::Dispatche client->connection_->addConnectionCallbacks(*client); client->connection_->addReadFilter(Network::ReadFilterSharedPtr{new UpstreamReadFilter(*client)}); client->connection_->connect(); - client->connection_->noDelay(true); + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) { + client->connection_->noDelay(true); + } return client; } diff --git a/source/extensions/filters/network/local_ratelimit/local_ratelimit.cc b/source/extensions/filters/network/local_ratelimit/local_ratelimit.cc index 773daf175139..287679763927 100644 --- a/source/extensions/filters/network/local_ratelimit/local_ratelimit.cc +++ b/source/extensions/filters/network/local_ratelimit/local_ratelimit.cc @@ -18,7 +18,9 @@ Config::Config( PROTOBUF_GET_MS_REQUIRED(proto_config.token_bucket(), fill_interval)), proto_config.token_bucket().max_tokens(), PROTOBUF_GET_WRAPPED_OR_DEFAULT(proto_config.token_bucket(), tokens_per_fill, 1), - dispatcher)), + dispatcher, + Protobuf::RepeatedPtrField< + envoy::extensions::common::ratelimit::v3::LocalRateLimitDescriptor>())), enabled_(proto_config.runtime_enabled(), runtime), stats_(generateStats(proto_config.stat_prefix(), scope)) {} @@ -27,7 +29,7 @@ LocalRateLimitStats Config::generateStats(const std::string& prefix, Stats::Scop return {ALL_LOCAL_RATE_LIMIT_STATS(POOL_COUNTER_PREFIX(scope, final_prefix))}; } -bool Config::canCreateConnection() { return rate_limiter_.requestAllowed(); } +bool Config::canCreateConnection() { return rate_limiter_.requestAllowed(descriptors_); } Network::FilterStatus Filter::onNewConnection() { if (!config_->enabled()) { diff --git a/source/extensions/filters/network/local_ratelimit/local_ratelimit.h b/source/extensions/filters/network/local_ratelimit/local_ratelimit.h index e1cd52ac1bee..f8ac07272459 100644 --- a/source/extensions/filters/network/local_ratelimit/local_ratelimit.h +++ b/source/extensions/filters/network/local_ratelimit/local_ratelimit.h @@ -49,6 +49,7 @@ class Config : Logger::Loggable { Runtime::FeatureFlag enabled_; LocalRateLimitStats stats_; + std::vector descriptors_; friend class LocalRateLimitTestBase; }; diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec.h b/source/extensions/filters/network/mysql_proxy/mysql_codec.h index e9f9cd7148c6..be10f19e59da 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec.h +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec.h @@ -1,6 +1,8 @@ #pragma once +#include #include +#include "envoy/buffer/buffer.h" #include "envoy/common/platform.h" #include "common/buffer/buffer_impl.h" @@ -69,18 +71,21 @@ constexpr uint16_t MYSQL_SERVER_CAPAB = 0x0101; constexpr uint8_t MYSQL_SERVER_LANGUAGE = 0x21; constexpr uint16_t MYSQL_SERVER_STATUS = 0x0200; constexpr uint16_t MYSQL_SERVER_EXT_CAPAB = 0x0200; -constexpr uint8_t MYSQL_AUTHPLGIN = 0x00; -constexpr uint8_t MYSQL_UNSET = 0x00; -constexpr uint8_t MYSQL_UNSET_SIZE = 10; -constexpr uint16_t MYSQL_CLIENT_CONNECT_WITH_DB = 0x0008; -constexpr uint16_t MYSQL_CLIENT_CAPAB_41VS320 = 0x0200; -constexpr uint16_t MYSQL_CLIENT_CAPAB_SSL = 0x0800; constexpr uint16_t MYSQL_EXT_CLIENT_CAPAB = 0x0300; -constexpr uint16_t MYSQL_EXT_CL_PLG_AUTH_CL_DATA = 0x0020; -constexpr uint16_t MYSQL_EXT_CL_SECURE_CONNECTION = 0x8000; + +constexpr uint32_t CLIENT_PLUGIN_AUTH = (1 << 19); +constexpr uint32_t CLIENT_SECURE_CONNECTION = 0x8000; +constexpr uint32_t CLIENT_PROTOCOL_41 = 0x00000200; +constexpr uint32_t CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 0x00200000; +constexpr uint32_t CLIENT_CONNECT_WITH_DB = 0x00000008; +constexpr uint32_t CLIENT_CONNECT_ATTRS = 0x00100000; +constexpr uint32_t CLIENT_SSL = 0x00000800; +constexpr uint16_t MYSQL_EXT_CL_PLUGIN_AUTH = 0x8; constexpr uint32_t MYSQL_MAX_PACKET = 0x00000001; constexpr uint8_t MYSQL_CHARSET = 0x21; +constexpr uint8_t MYSQL_SQL_STATE_LEN = 5; + constexpr uint8_t LENENCODINT_1BYTE = 0xfb; constexpr uint8_t LENENCODINT_2BYTES = 0xfc; constexpr uint8_t LENENCODINT_3BYTES = 0xfd; @@ -90,6 +95,9 @@ constexpr int MYSQL_SUCCESS = 0; constexpr int MYSQL_FAILURE = -1; constexpr char MYSQL_STR_END = '\0'; +// error code +constexpr uint16_t MYSQL_CR_AUTH_PLUGIN_ERR = 2061; + class MySQLCodec : public Logger::Loggable { public: enum class PktType { @@ -104,7 +112,7 @@ class MySQLCodec : public Logger::Loggable { return parseMessage(data, len); } - virtual std::string encode() PURE; + virtual void encode(Buffer::Instance& out) PURE; protected: virtual int parseMessage(Buffer::Instance& data, uint32_t len) PURE; diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin.cc b/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin.cc index eee66db20d0a..afd8e762843a 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin.cc +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin.cc @@ -2,152 +2,216 @@ #include "extensions/filters/network/mysql_proxy/mysql_codec.h" #include "extensions/filters/network/mysql_proxy/mysql_utils.h" +#include "extensions/filters/network/mysql_proxy/mysql_utils.h" +#include namespace Envoy { namespace Extensions { namespace NetworkFilters { namespace MySQLProxy { -void ClientLogin::setClientCap(int client_cap) { client_cap_ = client_cap; } +void ClientLogin::setClientCap(uint32_t client_cap) { client_cap_ = client_cap; } + +void ClientLogin::setBaseClientCap(uint16_t base_cap) { base_cap_ = base_cap; } -void ClientLogin::setExtendedClientCap(int extended_client_cap) { - extended_client_cap_ = extended_client_cap; +void ClientLogin::setExtendedClientCap(uint16_t extended_client_cap) { + ext_cap_ = extended_client_cap; } -void ClientLogin::setMaxPacket(int max_packet) { max_packet_ = max_packet; } +void ClientLogin::setMaxPacket(uint32_t max_packet) { max_packet_ = max_packet; } -void ClientLogin::setCharset(int charset) { charset_ = charset; } +void ClientLogin::setCharset(uint8_t charset) { charset_ = charset; } -void ClientLogin::setUsername(std::string& username) { +void ClientLogin::setUsername(const std::string& username) { if (username.length() <= MYSQL_MAX_USER_LEN) { username_.assign(username); } } -void ClientLogin::setDb(std::string& db) { db_ = db; } +void ClientLogin::setDb(const std::string& db) { db_ = db; } + +void ClientLogin::setAuthResp(const std::string& auth_resp) { auth_resp_.assign(auth_resp); } -void ClientLogin::setAuthResp(std::string& auth_resp) { auth_resp_.assign(auth_resp); } +void ClientLogin::setAuthPluginName(const std::string& auth_plugin_name) { + auth_plugin_name_ = auth_plugin_name; +} -bool ClientLogin::isResponse41() const { return client_cap_ & MYSQL_CLIENT_CAPAB_41VS320; } +bool ClientLogin::isResponse41() const { return client_cap_ & CLIENT_PROTOCOL_41; } -bool ClientLogin::isResponse320() const { return !(client_cap_ & MYSQL_CLIENT_CAPAB_41VS320); } +bool ClientLogin::isResponse320() const { return !(client_cap_ & CLIENT_PROTOCOL_41); } -bool ClientLogin::isSSLRequest() const { return client_cap_ & MYSQL_CLIENT_CAPAB_SSL; } +bool ClientLogin::isSSLRequest() const { return client_cap_ & CLIENT_SSL; } -bool ClientLogin::isConnectWithDb() const { return client_cap_ & MYSQL_CLIENT_CONNECT_WITH_DB; } +bool ClientLogin::isConnectWithDb() const { return client_cap_ & CLIENT_CONNECT_WITH_DB; } bool ClientLogin::isClientAuthLenClData() const { - return extended_client_cap_ & MYSQL_EXT_CL_PLG_AUTH_CL_DATA; + return client_cap_ & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA; } bool ClientLogin::isClientSecureConnection() const { - return extended_client_cap_ & MYSQL_EXT_CL_SECURE_CONNECTION; + return client_cap_ & CLIENT_SECURE_CONNECTION; } int ClientLogin::parseMessage(Buffer::Instance& buffer, uint32_t) { - uint16_t client_cap = 0; - if (BufferHelper::readUint16(buffer, client_cap) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing client_cap in mysql ClientLogin msg"); - return MYSQL_FAILURE; - } - setClientCap(client_cap); - uint16_t extended_client_cap = 0; - if (BufferHelper::readUint16(buffer, extended_client_cap) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing extended_client_cap in mysql ClientLogin msg"); + /* 4.0 uses 2 byte, 4.1+ uses 4 bytes, but the proto-flag is in the lower 2 + * bytes */ + if (BufferHelper::peekUint16(buffer, base_cap_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when paring cap client login message"); return MYSQL_FAILURE; } - setExtendedClientCap(extended_client_cap); - uint32_t max_packet = 0; - if (BufferHelper::readUint32(buffer, max_packet) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing max_packet in mysql ClientLogin msg"); - return MYSQL_FAILURE; - } - setMaxPacket(max_packet); - if (isSSLRequest()) { - // Stop Parsing if CLIENT_SSL flag is set + if (client_cap_ & CLIENT_SSL) { + if (BufferHelper::readUint32(buffer, client_cap_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when paring cap client ssl message"); + return MYSQL_FAILURE; + } + if (BufferHelper::readUint32(buffer, max_packet_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when paring max packet client ssl message"); + return MYSQL_FAILURE; + } + if (BufferHelper::readUint8(buffer, charset_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when paring character client ssl message"); + return MYSQL_FAILURE; + } + if (BufferHelper::readBytes(buffer, UNSET_BYTES) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when paring reserved data of client ssl message"); + return MYSQL_FAILURE; + } return MYSQL_SUCCESS; } - uint8_t charset = 0; - if (BufferHelper::readUint8(buffer, charset) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing charset in mysql ClientLogin msg"); - return MYSQL_FAILURE; - } - setCharset(charset); - if (BufferHelper::readBytes(buffer, UNSET_BYTES) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error skipping unset bytes in mysql ClientLogin msg"); - return MYSQL_FAILURE; - } - std::string username; - if (BufferHelper::readString(buffer, username) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing username in mysql ClientLogin msg"); - return MYSQL_FAILURE; - } - setUsername(username); - std::string auth_resp; - if (isClientAuthLenClData()) { - uint64_t auth_resp_len = 0; - if (BufferHelper::readLengthEncodedInteger(buffer, auth_resp_len) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing LengthEncodedInteger in mysql ClientLogin msg"); + if (client_cap_ & CLIENT_PROTOCOL_41) { + if (BufferHelper::readUint32(buffer, client_cap_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when parsing client cap of client login message"); return MYSQL_FAILURE; } - if (BufferHelper::readStringBySize(buffer, auth_resp_len, auth_resp) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing auth_resp in mysql ClientLogin msg"); + if (BufferHelper::readUint32(buffer, max_packet_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when parsing max packet of client login message"); return MYSQL_FAILURE; } - } else if (isClientSecureConnection()) { - uint8_t auth_resp_len = 0; - if (BufferHelper::readUint8(buffer, auth_resp_len) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing auth_resp_len in mysql ClientLogin msg"); + if (BufferHelper::readUint8(buffer, charset_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when parsing charset of client login message"); return MYSQL_FAILURE; } - if (BufferHelper::readStringBySize(buffer, auth_resp_len, auth_resp) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing auth_resp in mysql ClientLogin msg"); + if (BufferHelper::readBytes(buffer, UNSET_BYTES) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when skiping bytes of client login message"); return MYSQL_FAILURE; } - } else { - if (BufferHelper::readString(buffer, auth_resp) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing auth_resp in mysql ClientLogin msg"); + if (BufferHelper::readString(buffer, username_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when parsing username of client login message"); return MYSQL_FAILURE; } - } - setAuthResp(auth_resp); - if (isConnectWithDb()) { - std::string db; - if (BufferHelper::readString(buffer, db) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing auth_resp in mysql ClientLogin msg"); + if (client_cap_ & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) { + uint64_t auth_len; + if (BufferHelper::readLengthEncodedInteger(buffer, auth_len) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when parsing username of client login message"); + return MYSQL_FAILURE; + } + if (BufferHelper::readStringBySize(buffer, auth_len, auth_resp_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when parsing auth resp of client login message"); + return MYSQL_FAILURE; + } + + } else if (client_cap_ & CLIENT_SECURE_CONNECTION) { + uint8_t auth_len; + if (BufferHelper::readUint8(buffer, auth_len) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when parsing auth resp length of client login message"); + return MYSQL_FAILURE; + } + if (BufferHelper::readStringBySize(buffer, auth_len, auth_resp_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when parsing auth resp data of client login message"); + return MYSQL_FAILURE; + } + } else { + if (BufferHelper::readString(buffer, auth_resp_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when parsing auth resp data of client login message"); + return MYSQL_FAILURE; + } + } + + if ((client_cap_ & CLIENT_CONNECT_WITH_DB) && + (BufferHelper::readString(buffer, db_) != MYSQL_SUCCESS)) { + ENVOY_LOG(info, "error when parsing db name client login message"); return MYSQL_FAILURE; } - setDb(db); + if ((client_cap_ & CLIENT_PLUGIN_AUTH) && + (BufferHelper::readString(buffer, auth_plugin_name_) != MYSQL_SUCCESS)) { + ENVOY_LOG(info, "error when parsing auth plugin name of client login message"); + return MYSQL_FAILURE; + } + } + + if (BufferHelper::readUint16(buffer, base_cap_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when parsing cap of client login message"); + return MYSQL_FAILURE; + } + if (BufferHelper::readUint24(buffer, max_packet_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when paring max packet of client login message"); + return MYSQL_FAILURE; + } + if (BufferHelper::readString(buffer, username_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error when paring username of client login message"); + return MYSQL_FAILURE; + } + // there are more + if (BufferHelper::readStringEof(buffer, auth_resp_)) { + ENVOY_LOG(info, "error when paring auth resp of client login message"); + return MYSQL_FAILURE; } return MYSQL_SUCCESS; } -std::string ClientLogin::encode() { +void ClientLogin::encode(Buffer::Instance& out) { uint8_t enc_end_string = 0; - Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); - BufferHelper::addUint16(*buffer, client_cap_); - BufferHelper::addUint16(*buffer, extended_client_cap_); - BufferHelper::addUint32(*buffer, max_packet_); - BufferHelper::addUint8(*buffer, charset_); - for (int idx = 0; idx < UNSET_BYTES; idx++) { - BufferHelper::addUint8(*buffer, 0); + if (client_cap_ & CLIENT_SSL) { + BufferHelper::addUint32(out, client_cap_); + BufferHelper::addUint32(out, max_packet_); + BufferHelper::addUint8(out, charset_); + for (int i = 0; i < UNSET_BYTES; i++) { + BufferHelper::addUint8(out, 0); + } + return; } - BufferHelper::addString(*buffer, username_); - BufferHelper::addUint8(*buffer, enc_end_string); - if ((extended_client_cap_ & MYSQL_EXT_CL_PLG_AUTH_CL_DATA) || - (extended_client_cap_ & MYSQL_EXT_CL_SECURE_CONNECTION)) { - BufferHelper::addUint8(*buffer, auth_resp_.length()); - BufferHelper::addString(*buffer, auth_resp_); + if (client_cap_ & CLIENT_PROTOCOL_41) { + BufferHelper::addUint32(out, client_cap_); + BufferHelper::addUint32(out, max_packet_); + BufferHelper::addUint8(out, charset_); + for (int i = 0; i < UNSET_BYTES; i++) { + BufferHelper::addUint8(out, 0); + } + BufferHelper::addString(out, username_); + BufferHelper::addUint8(out, enc_end_string); + if (client_cap_ & CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) { + BufferHelper::addLengthEncodedInteger(out, auth_resp_.length()); + BufferHelper::addString(out, auth_resp_); + } else if (client_cap_ & CLIENT_SECURE_CONNECTION) { + BufferHelper::addUint8(out, auth_resp_.length()); + BufferHelper::addString(out, auth_resp_); + } else { + BufferHelper::addString(out, auth_resp_); + BufferHelper::addUint8(out, enc_end_string); + } + if (client_cap_ & CLIENT_CONNECT_WITH_DB) { + BufferHelper::addString(out, db_); + BufferHelper::addUint8(out, enc_end_string); + } + if (client_cap_ & CLIENT_PLUGIN_AUTH) { + BufferHelper::addString(out, auth_plugin_name_); + BufferHelper::addUint8(out, enc_end_string); + } } else { - BufferHelper::addString(*buffer, auth_resp_); - BufferHelper::addUint8(*buffer, enc_end_string); - } - if (client_cap_ & MYSQL_CLIENT_CONNECT_WITH_DB) { - BufferHelper::addString(*buffer, db_); - BufferHelper::addUint8(*buffer, enc_end_string); + BufferHelper::addUint16(out, base_cap_); + BufferHelper::addUint24(out, max_packet_); + BufferHelper::addString(out, username_); + BufferHelper::addUint8(out, enc_end_string); + if (client_cap_ & CLIENT_CONNECT_WITH_DB) { + BufferHelper::addString(out, auth_resp_); + BufferHelper::addUint8(out, enc_end_string); + BufferHelper::addString(out, db_); + BufferHelper::addUint8(out, enc_end_string); + } else { + BufferHelper::addString(out, auth_resp_); + BufferHelper::addUint8(out, -1); + } } - - return buffer->toString(); } } // namespace MySQLProxy diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin.h b/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin.h index 0ce28edfb2d0..3ceae0ca71c8 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin.h +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin.h @@ -2,6 +2,7 @@ #include "common/buffer/buffer_impl.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" +#include namespace Envoy { namespace Extensions { @@ -13,37 +14,47 @@ class ClientLogin : public MySQLCodec { public: // MySQLCodec int parseMessage(Buffer::Instance& buffer, uint32_t len) override; - std::string encode() override; + void encode(Buffer::Instance&) override; - int getClientCap() const { return client_cap_; } - int getExtendedClientCap() const { return extended_client_cap_; } - int getMaxPacket() const { return max_packet_; } - int getCharset() const { return charset_; } - const std::string& getUsername() const { return username_; } - const std::string& getAuthResp() const { return auth_resp_; } - const std::string& getDb() const { return db_; } + uint32_t getClientCap() const { return client_cap_; } + uint16_t getBaseClientCap() const { return base_cap_; } + uint16_t getExtendedClientCap() const { return ext_cap_; } + uint32_t getMaxPacket() const { return max_packet_; } + uint8_t getCharset() const { return charset_; } + std::string getUsername() const { return username_; } + std::string getAuthResp() const { return auth_resp_; } + std::string getDb() const { return db_; } + std::string getAuthPluginName() const { return auth_plugin_name_; } bool isResponse41() const; bool isResponse320() const; bool isSSLRequest() const; bool isConnectWithDb() const; bool isClientAuthLenClData() const; bool isClientSecureConnection() const; - void setClientCap(int client_cap); - void setExtendedClientCap(int extended_client_cap); - void setMaxPacket(int max_packet); - void setCharset(int charset); - void setUsername(std::string& username); - void setAuthResp(std::string& auth_resp); - void setDb(std::string& db); + void setClientCap(uint32_t client_cap); + void setBaseClientCap(uint16_t base_cap); + void setExtendedClientCap(uint16_t ext_cap); + void setMaxPacket(uint32_t max_packet); + void setCharset(uint8_t charset); + void setUsername(const std::string& username); + void setAuthResp(const std::string& auth_resp); + void setDb(const std::string& db); + void setAuthPluginName(const std::string& auth_plugin_name); private: - int client_cap_; - int extended_client_cap_; - int max_packet_; - int charset_; + union { + uint32_t client_cap_; + struct { + uint16_t base_cap_; + uint16_t ext_cap_; + }; + }; + uint32_t max_packet_; + uint8_t charset_; std::string username_; std::string auth_resp_; std::string db_; + std::string auth_plugin_name_; }; } // namespace MySQLProxy diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.cc b/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.cc index dc7c1da36420..3084427fc402 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.cc +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.cc @@ -1,73 +1,456 @@ #include "extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h" +#include "common/common/assert.h" +#include "common/common/logger.h" +#include "envoy/buffer/buffer.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" #include "extensions/filters/network/mysql_proxy/mysql_utils.h" +#include +#include namespace Envoy { namespace Extensions { namespace NetworkFilters { namespace MySQLProxy { -void ClientLoginResponse::setRespCode(uint8_t resp_code) { resp_code_ = resp_code; } +ClientLoginResponse::AuthSwitchMessage::AuthSwitchMessage( + ClientLoginResponse::AuthSwitchMessage&& other) noexcept + : auth_plugin_data_(std::move(other.auth_plugin_data_)), + auth_plugin_name_(std::move(other.auth_plugin_name_)) {} -void ClientLoginResponse::setAffectedRows(uint8_t affected_rows) { affected_rows_ = affected_rows; } +ClientLoginResponse::AuthSwitchMessage& ClientLoginResponse::AuthSwitchMessage::operator=( + ClientLoginResponse::AuthSwitchMessage&& other) noexcept { + if (&other == this) { + return *this; + } + auth_plugin_data_ = std::move(other.auth_plugin_data_); + auth_plugin_name_ = std::move(other.auth_plugin_name_); + return *this; +} + +bool ClientLoginResponse::AuthSwitchMessage::operator==( + const ClientLoginResponse::AuthSwitchMessage& other) const { + return auth_plugin_data_ == other.auth_plugin_data_ && + auth_plugin_name_ == other.auth_plugin_name_; +} + +ClientLoginResponse::AuthMoreMessage::AuthMoreMessage( + ClientLoginResponse::AuthMoreMessage&& other) noexcept + : more_plugin_data_(std::move(other.more_plugin_data_)) {} + +ClientLoginResponse::AuthMoreMessage& ClientLoginResponse::AuthMoreMessage::operator=( + ClientLoginResponse::AuthMoreMessage&& other) noexcept { + if (&other == this) { + return *this; + } + more_plugin_data_ = std::move(other.more_plugin_data_); + return *this; +} + +bool ClientLoginResponse::AuthMoreMessage::operator==( + const ClientLoginResponse::AuthMoreMessage& other) const { + return more_plugin_data_ == other.more_plugin_data_; +} + +ClientLoginResponse::OkMessage::OkMessage(ClientLoginResponse::OkMessage&& other) noexcept + : affected_rows_(other.affected_rows_), last_insert_id_(other.last_insert_id_), + status_(other.status_), warnings_(other.warnings_), info_(std::move(other.info_)) {} + +ClientLoginResponse::OkMessage& +ClientLoginResponse::OkMessage::operator=(ClientLoginResponse::OkMessage&& other) noexcept { + if (&other == this) { + return *this; + } + affected_rows_ = other.affected_rows_; + last_insert_id_ = other.last_insert_id_; + status_ = other.status_; + warnings_ = other.warnings_; + info_ = std::move(other.info_); + return *this; +} + +bool ClientLoginResponse::OkMessage::operator==(const ClientLoginResponse::OkMessage& other) const { + return affected_rows_ == other.affected_rows_ && last_insert_id_ == other.last_insert_id_ && + status_ == other.status_ && warnings_ == other.warnings_ && info_ == other.info_; +} + +ClientLoginResponse::ErrMessage::ErrMessage(ClientLoginResponse::ErrMessage&& other) noexcept + : marker_(other.marker_), error_code_(other.error_code_), sql_state_(other.sql_state_), + error_message_(other.error_message_) {} + +ClientLoginResponse::ErrMessage& +ClientLoginResponse::ErrMessage::operator=(ErrMessage&& other) noexcept { + if (&other == this) { + return *this; + } + error_code_ = other.error_code_; + marker_ = other.marker_; + sql_state_ = std::move(other.sql_state_); + error_message_ = std::move(other.error_message_); + return *this; +} + +bool ClientLoginResponse::ErrMessage::operator==(const ErrMessage& other) const { + return error_code_ == other.error_code_ && marker_ == other.marker_ && + sql_state_ == other.sql_state_ && error_message_ == other.error_message_; +} + +void ClientLoginResponse::cleanup() { + // Need to manually delete because of the union. + switch (type_) { + case Ok: + ok_.~OkMessage(); + break; + case Err: + err_.~ErrMessage(); + break; + case AuthSwitch: + auth_switch_.~AuthSwitchMessage(); + break; + case AuthMoreData: + auth_more_.~AuthMoreMessage(); + default: + break; + } +} + +void ClientLoginResponse::type(ClientLoginResponseType type) { + cleanup(); + // Need to use placement new because of the union. + type_ = type; + switch (type_) { + case ClientLoginResponseType::Ok: { + new (&ok_) OkMessage(); + break; + } + case ClientLoginResponseType::Err: { + new (&err_) ErrMessage(); + break; + } + case ClientLoginResponseType::AuthSwitch: { + new (&auth_switch_) AuthSwitchMessage(); + break; + } + case ClientLoginResponseType::AuthMoreData: { + new (&auth_more_) AuthMoreMessage(); + break; + } + default: + break; + } +} + +ClientLoginResponse::ClientLoginResponse(const ClientLoginResponse& other) + : type_(ClientLoginResponseType::Null) { + type(other.type_); + switch (type_) { + case Null: + break; + case AuthSwitch: + asAuthSwitchMessage() = other.asAuthSwitchMessage(); + break; + case AuthMoreData: + asAuthMoreMessage() = other.asAuthMoreMessage(); + break; + case Ok: + asOkMessage() = other.asOkMessage(); + break; + case Err: + asErrMessage() = other.asErrMessage(); + break; + default: + break; + } +} +ClientLoginResponse::ClientLoginResponse(ClientLoginResponse&& other) noexcept { + type(other.type_); + switch (type_) { + case Null: + break; + case AuthSwitch: + new (&auth_switch_) AuthSwitchMessage(std::move(other.auth_switch_)); + break; + case AuthMoreData: + new (&auth_more_) AuthMoreMessage(std::move(other.auth_more_)); + break; + case Ok: + new (&ok_) OkMessage(std::move(other.ok_)); + break; + case Err: + new (&err_) ErrMessage(std::move(other.err_)); + break; + default: + break; + } +} + +ClientLoginResponse& ClientLoginResponse::operator=(const ClientLoginResponse& other) { + if (&other == this) { + return *this; + } + + type(other.type_); + switch (type_) { + case Null: + break; + case AuthSwitch: + asAuthSwitchMessage() = other.asAuthSwitchMessage(); + break; + case AuthMoreData: + asAuthMoreMessage() = other.asAuthMoreMessage(); + break; + case Ok: + asOkMessage() = other.asOkMessage(); + break; + case Err: + asErrMessage() = other.asErrMessage(); + break; + default: + break; + } + return *this; +} +ClientLoginResponse& ClientLoginResponse::operator=(ClientLoginResponse&& other) noexcept { + if (&other == this) { + return *this; + } + type(other.type_); + switch (type_) { + case Null: + break; + case AuthSwitch: + auth_switch_ = std::move(other.auth_switch_); + break; + case AuthMoreData: + auth_more_ = std::move(other.auth_more_); + break; + case Ok: + ok_ = std::move(other.ok_); + break; + case Err: + err_ = std::move(other.err_); + break; + default: + break; + } + return *this; +} + +bool ClientLoginResponse::operator==(const ClientLoginResponse& other) const { + if (&other == this) { + return true; + } + if (other.type_ != type_) { + return false; + } + switch (type_) { + case Ok: + return ok_ == other.ok_; + case Err: + return err_ == other.err_; + case AuthSwitch: + return auth_switch_ == other.auth_switch_; + case AuthMoreData: + return auth_more_ == other.auth_more_; + default: + return true; + } +} -void ClientLoginResponse::setLastInsertId(uint8_t last_insert_id) { - last_insert_id_ = last_insert_id; +ClientLoginResponse::AuthMoreMessage& ClientLoginResponse::asAuthMoreMessage() { + ASSERT(type_ == AuthMoreData); + return auth_more_; } -void ClientLoginResponse::setServerStatus(uint16_t status) { server_status_ = status; } +const ClientLoginResponse::AuthMoreMessage& ClientLoginResponse::asAuthMoreMessage() const { + ASSERT(type_ == AuthMoreData); + return auth_more_; +} -void ClientLoginResponse::setWarnings(uint16_t warnings) { warnings_ = warnings; } +ClientLoginResponse::AuthSwitchMessage& ClientLoginResponse::asAuthSwitchMessage() { + ASSERT(type_ == AuthSwitch); + return auth_switch_; +} -int ClientLoginResponse::parseMessage(Buffer::Instance& buffer, uint32_t) { - uint8_t resp_code = 0; +const ClientLoginResponse::AuthSwitchMessage& ClientLoginResponse::asAuthSwitchMessage() const { + ASSERT(type_ == AuthSwitch); + return auth_switch_; +} + +ClientLoginResponse::OkMessage& ClientLoginResponse::asOkMessage() { + ASSERT(type_ == Ok); + return ok_; +} +const ClientLoginResponse::OkMessage& ClientLoginResponse::asOkMessage() const { + ASSERT(type_ == Ok); + return ok_; +} + +ClientLoginResponse::ErrMessage& ClientLoginResponse::asErrMessage() { + ASSERT(type_ == Err); + return err_; +} +const ClientLoginResponse::ErrMessage& ClientLoginResponse::asErrMessage() const { + ASSERT(type_ == Err); + return err_; +} + +int ClientLoginResponse::parseMessage(Buffer::Instance& buffer, uint32_t len) { + uint8_t resp_code; if (BufferHelper::readUint8(buffer, resp_code) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing response code in mysql Login Ok msg"); + ENVOY_LOG(info, "error parsing response code in mysql Login response msg"); return MYSQL_FAILURE; } - setRespCode(resp_code); - if ((resp_code == MYSQL_RESP_AUTH_SWITCH) && BufferHelper::endOfBuffer(buffer)) { - // OldAuthSwitchRequest + switch (resp_code) { + case MYSQL_RESP_AUTH_SWITCH: + return parseAuthSwitch(buffer, len); + case MYSQL_RESP_OK: + return parseOk(buffer, len); + case MYSQL_RESP_ERR: + return parseErr(buffer, len); + case MYSQL_RESP_MORE: + return parseAuthMore(buffer, len); + } + ENVOY_LOG(info, "unknown mysql Login resp msg type"); + return MYSQL_FAILURE; +} + +// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest +int ClientLoginResponse::parseAuthSwitch(Buffer::Instance& buffer, uint32_t) { + // OldAuthSwitchRequest + type(AuthSwitch); + if (BufferHelper::endOfBuffer(buffer)) { + auth_switch_.setIsOldAuthSwitch(true); return MYSQL_SUCCESS; } - uint8_t affected_rows = 0; - if (BufferHelper::readUint8(buffer, affected_rows) != MYSQL_SUCCESS) { + if (BufferHelper::readString(buffer, auth_switch_.auth_plugin_name_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing auth plugin name mysql Login response msg"); + return MYSQL_FAILURE; + } + if (BufferHelper::readStringEof(buffer, auth_switch_.auth_plugin_data_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing auth plugin data code in mysql Login Ok msg"); + return MYSQL_FAILURE; + } + auth_switch_.setIsOldAuthSwitch(false); + return MYSQL_SUCCESS; +} + +// https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html +int ClientLoginResponse::parseOk(Buffer::Instance& buffer, uint32_t) { + type(Ok); + if (BufferHelper::readLengthEncodedInteger(buffer, ok_.affected_rows_) != MYSQL_SUCCESS) { ENVOY_LOG(info, "error parsing affected_rows in mysql Login Ok msg"); return MYSQL_FAILURE; } - setAffectedRows(affected_rows); - uint8_t last_insert_id = 0; - if (BufferHelper::readUint8(buffer, last_insert_id) != MYSQL_SUCCESS) { + if (BufferHelper::readLengthEncodedInteger(buffer, ok_.last_insert_id_) != MYSQL_SUCCESS) { ENVOY_LOG(info, "error parsing last_insert_id in mysql Login Ok msg"); return MYSQL_FAILURE; } - setLastInsertId(last_insert_id); - uint16_t server_status = 0; - if (BufferHelper::readUint16(buffer, server_status) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing server_status in mysql Login Ok msg"); + + if (BufferHelper::readUint16(buffer, ok_.status_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing status in mysql Login Ok msg"); return MYSQL_FAILURE; } - setServerStatus(server_status); - uint16_t warnings = 0; - if (BufferHelper::readUint16(buffer, warnings) != MYSQL_SUCCESS) { + // the exist of warning feild is determined by server cap flag, but a decoder can not know the + // cap flag, so just assume the CLIENT_PROTOCOL_41 is always set. ref + // https://github.com/mysql/mysql-connector-j/blob/release/8.0/src/main/protocol-impl/java/com/mysql/cj/protocol/a/result/OkPacket.java#L48 + if (BufferHelper::readUint16(buffer, ok_.warnings_) != MYSQL_SUCCESS) { ENVOY_LOG(info, "error parsing warnings in mysql Login Ok msg"); return MYSQL_FAILURE; } - setWarnings(warnings); + if (BufferHelper::readString(buffer, ok_.info_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing info in mysql Login Ok msg"); + return MYSQL_FAILURE; + } return MYSQL_SUCCESS; } -std::string ClientLoginResponse::encode() { - Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); - BufferHelper::addUint8(*buffer, resp_code_); - BufferHelper::addUint8(*buffer, affected_rows_); - BufferHelper::addUint8(*buffer, last_insert_id_); - BufferHelper::addUint16(*buffer, server_status_); - BufferHelper::addUint16(*buffer, warnings_); +// https://dev.mysql.com/doc/internals/en/packet-ERR_Packet.html +int ClientLoginResponse::parseErr(Buffer::Instance& buffer, uint32_t) { + type(Err); + if (BufferHelper::readUint16(buffer, err_.error_code_) != MYSQL_RESP_OK) { + ENVOY_LOG(info, "error parsing error code in mysql Login error msg"); + return MYSQL_FAILURE; + } + if (BufferHelper::readUint8(buffer, err_.marker_) != MYSQL_RESP_OK) { + ENVOY_LOG(info, "error parsing sql state marker in mysql Login error msg"); + return MYSQL_FAILURE; + } + if (BufferHelper::readStringBySize(buffer, MYSQL_SQL_STATE_LEN, err_.sql_state_) != + MYSQL_RESP_OK) { + ENVOY_LOG(info, "error parsing sql state in mysql Login error msg"); + return MYSQL_FAILURE; + } + if (BufferHelper::readStringEof(buffer, err_.error_message_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing error message in mysql Login error msg"); + return MYSQL_FAILURE; + } + return MYSQL_SUCCESS; +} + +// https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthMoreData +int ClientLoginResponse::parseAuthMore(Buffer::Instance& buffer, uint32_t) { + type(AuthMoreData); + if (BufferHelper::readStringEof(buffer, auth_more_.more_plugin_data_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing more plugin data in mysql Login auth more msg"); + return MYSQL_FAILURE; + } + return MYSQL_SUCCESS; +} + +void ClientLoginResponse::encode(Buffer::Instance& out) { + switch (type_) { + case AuthSwitch: + encodeAuthSwitch(out); + break; + case Ok: + encodeOk(out); + break; + case Err: + encodeErr(out); + break; + case AuthMoreData: + encodeAuthMore(out); + break; + default: + break; + } +} + +void ClientLoginResponse::encodeAuthSwitch(Buffer::Instance& out) { + BufferHelper::addUint8(out, MYSQL_RESP_AUTH_SWITCH); + if (auth_switch_.isOldAuthSwitch()) { + return; + } + BufferHelper::addString(out, auth_switch_.auth_plugin_name_); + BufferHelper::addUint8(out, 0); + BufferHelper::addString(out, auth_switch_.auth_plugin_data_); + BufferHelper::addUint8(out, EOF); +} + +void ClientLoginResponse::encodeOk(Buffer::Instance& out) { + BufferHelper::addUint8(out, MYSQL_RESP_OK); + BufferHelper::addLengthEncodedInteger(out, ok_.affected_rows_); + BufferHelper::addLengthEncodedInteger(out, ok_.last_insert_id_); + BufferHelper::addUint16(out, ok_.status_); + BufferHelper::addUint16(out, ok_.warnings_); + BufferHelper::addString(out, ok_.info_); + BufferHelper::addUint8(out, EOF); +} + +void ClientLoginResponse::encodeErr(Buffer::Instance& out) { + BufferHelper::addUint8(out, MYSQL_RESP_ERR); + BufferHelper::addUint16(out, err_.error_code_); + BufferHelper::addUint8(out, err_.marker_); + BufferHelper::addString(out, err_.sql_state_); + BufferHelper::addString(out, err_.error_message_); + BufferHelper::addUint8(out, EOF); +} - std::string e_string = buffer->toString(); - return e_string; +void ClientLoginResponse::encodeAuthMore(Buffer::Instance& out) { + BufferHelper::addUint8(out, MYSQL_RESP_MORE); + BufferHelper::addString(out, auth_more_.more_plugin_data_); + BufferHelper::addUint8(out, EOF); } } // namespace MySQLProxy diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h b/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h index cc7d5c25861d..94ba318f37d0 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h @@ -1,38 +1,169 @@ #pragma once +#include #include #include "common/buffer/buffer_impl.h" +#include "envoy/buffer/buffer.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_clogin.h" namespace Envoy { namespace Extensions { namespace NetworkFilters { namespace MySQLProxy { +enum ClientLoginResponseType { Null, Ok, Err, AuthSwitch, AuthMoreData }; + +// ClientLoginResponse colud be +// Protocol::OldAuthSwitchRequest, Protocol::AuthSwitchRequest when server want switch auth method +// or OK_Packet, ERR_Packet when server auth ok or error class ClientLoginResponse : public MySQLCodec { public: + ClientLoginResponse() : type_(Null) {} + ~ClientLoginResponse() override { cleanup(); } + + ClientLoginResponse(const ClientLoginResponse& other); // copy constructor + ClientLoginResponse(ClientLoginResponse&& other) noexcept; // move constructor + ClientLoginResponse& operator=(const ClientLoginResponse& other); // copy assignment + ClientLoginResponse& operator=(ClientLoginResponse&& other) noexcept; // move assignment + bool operator==(const ClientLoginResponse& other) const; // test for equality, unit tests + bool operator!=(const ClientLoginResponse& other) const { return !(*this == other); } + // MySQLCodec int parseMessage(Buffer::Instance& buffer, uint32_t len) override; - std::string encode() override; - - uint8_t getRespCode() const { return resp_code_; } - uint8_t getAffectedRows() const { return affected_rows_; } - uint8_t getLastInsertId() const { return last_insert_id_; } - uint16_t getServerStatus() const { return server_status_; } - uint16_t getWarnings() const { return warnings_; } - void setRespCode(uint8_t resp_code); - void setAffectedRows(uint8_t affected_rows); - void setLastInsertId(uint8_t last_insert_id); - void setServerStatus(uint16_t status); - void setWarnings(uint16_t warnings); + void encode(Buffer::Instance&) override; + + class AuthMoreMessage { + public: + AuthMoreMessage() = default; + ~AuthMoreMessage() = default; + AuthMoreMessage(const AuthMoreMessage&) = default; + AuthMoreMessage(AuthMoreMessage&&) noexcept; + AuthMoreMessage& operator=(const AuthMoreMessage&) = default; + AuthMoreMessage& operator=(AuthMoreMessage&&) noexcept; + bool operator==(const AuthMoreMessage&) const; + std::string getAuthMoreData() const { return more_plugin_data_; } + void setAuthMoreData(const std::string& data) { more_plugin_data_ = data; } + friend ClientLoginResponse; + + private: + std::string more_plugin_data_; + }; + + class AuthSwitchMessage { + public: + AuthSwitchMessage() = default; + ~AuthSwitchMessage() = default; + AuthSwitchMessage(const AuthSwitchMessage&); + AuthSwitchMessage(AuthSwitchMessage&&) noexcept; + AuthSwitchMessage& operator=(const AuthSwitchMessage&) = default; + AuthSwitchMessage& operator=(AuthSwitchMessage&&) noexcept; + bool operator==(const AuthSwitchMessage&) const; + bool isOldAuthSwitch() const { return is_old_auth_switch_; } + std::string getAuthPluginData() const { return auth_plugin_data_; } + std::string getAuthPluginName() const { return auth_plugin_name_; } + void setIsOldAuthSwitch(bool old) { is_old_auth_switch_ = old; } + void setAuthPluginData(const std::string& data) { auth_plugin_data_ = data; } + void setAuthPluginName(const std::string& name) { auth_plugin_name_ = name; } + friend ClientLoginResponse; + + private: + bool is_old_auth_switch_; + std::string auth_plugin_data_; + std::string auth_plugin_name_; + }; + + class OkMessage { + public: + OkMessage() = default; + ~OkMessage() = default; + OkMessage(const OkMessage&) = default; + OkMessage(OkMessage&&) noexcept; + OkMessage& operator=(const OkMessage&) = default; + OkMessage& operator=(OkMessage&&) noexcept; + bool operator==(const OkMessage&) const; + void setAffectedRows(uint64_t affected_rows) { affected_rows_ = affected_rows; } + void setLastInsertId(uint64_t last_insert_id) { last_insert_id_ = last_insert_id; } + void setServerStatus(uint16_t status) { status_ = status; } + void setWarnings(uint16_t warnings) { warnings_ = warnings; } + void setInfo(const std::string& info) { info_ = info; } + uint64_t getAffectedRows() const { return affected_rows_; } + uint64_t getLastInsertId() const { return last_insert_id_; } + uint16_t getServerStatus() const { return status_; } + uint16_t getWarnings() const { return warnings_; } + std::string getInfo() const { return info_; } + friend ClientLoginResponse; + + private: + uint64_t affected_rows_; + uint64_t last_insert_id_; + uint16_t status_; + uint16_t warnings_; + std::string info_; + }; + + class ErrMessage { + public: + ErrMessage() = default; + ~ErrMessage() = default; + ErrMessage(const ErrMessage&) = default; + ErrMessage(ErrMessage&&) noexcept; + ErrMessage& operator=(const ErrMessage&) = default; + ErrMessage& operator=(ErrMessage&&) noexcept; + bool operator==(const ErrMessage&) const; + void setErrorCode(uint16_t error_code) { error_code_ = error_code; } + void setSqlStateMarker(uint8_t marker) { marker_ = marker; } + void setSqlState(const std::string& state) { sql_state_ = state; } + void setErrorMessage(const std::string& msg) { error_message_ = msg; } + uint16_t getErrorCode() const { return error_code_; } + uint8_t getSqlStateMarker() const { return marker_; } + std::string getSqlState() const { return sql_state_; } + std::string getErrorMessage() const { return error_message_; } + friend ClientLoginResponse; + + private: + uint8_t marker_; + uint16_t error_code_; + std::string sql_state_; + std::string error_message_; + }; + const OkMessage& asOkMessage() const; + OkMessage& asOkMessage(); + const ErrMessage& asErrMessage() const; + ErrMessage& asErrMessage(); + const AuthSwitchMessage& asAuthSwitchMessage() const; + AuthSwitchMessage& asAuthSwitchMessage(); + const AuthMoreMessage& asAuthMoreMessage() const; + AuthMoreMessage& asAuthMoreMessage(); + + /** + * Get/set the type of the ClientLoginResponse. A ClientLoginResponse can only be a single type at + * a time. Each time type() is called the type is changed and then the type specific as* methods + * can be used. + */ + ClientLoginResponseType type() const { return type_; } + void type(ClientLoginResponseType); private: - uint8_t resp_code_; - uint8_t affected_rows_; - uint8_t last_insert_id_; - uint16_t server_status_; - uint16_t warnings_; + int parseAuthSwitch(Buffer::Instance& buffer, uint32_t len); + int parseOk(Buffer::Instance& buffer, uint32_t len); + int parseErr(Buffer::Instance& buffer, uint32_t len); + int parseAuthMore(Buffer::Instance& buffer, uint32_t len); + void encodeAuthSwitch(Buffer::Instance&); + void encodeOk(Buffer::Instance&); + void encodeErr(Buffer::Instance&); + void encodeAuthMore(Buffer::Instance&); + + void cleanup(); + + ClientLoginResponseType type_{}; + union { + AuthSwitchMessage auth_switch_; + AuthMoreMessage auth_more_; + ErrMessage err_; + OkMessage ok_; + }; }; } // namespace MySQLProxy diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec_command.cc b/source/extensions/filters/network/mysql_proxy/mysql_codec_command.cc index c5aa4c30b0fc..7bcd7a77d0e6 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec_command.cc +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec_command.cc @@ -1,5 +1,6 @@ #include "extensions/filters/network/mysql_proxy/mysql_codec_command.h" +#include "envoy/buffer/buffer.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" #include "extensions/filters/network/mysql_proxy/mysql_utils.h" @@ -52,15 +53,11 @@ int Command::parseMessage(Buffer::Instance& buffer, uint32_t len) { return MYSQL_SUCCESS; } -void Command::setData(std::string& data) { data_.assign(data); } +void Command::setData(const std::string& data) { data_.assign(data); } -std::string Command::encode() { - Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); - - BufferHelper::addUint8(*buffer, static_cast(cmd_)); - BufferHelper::addString(*buffer, data_); - std::string e_string = buffer->toString(); - return e_string; +void Command::encode(Buffer::Instance& out) { + BufferHelper::addUint8(out, static_cast(cmd_)); + BufferHelper::addString(out, data_); } } // namespace MySQLProxy diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec_command.h b/source/extensions/filters/network/mysql_proxy/mysql_codec_command.h index 6f4ae5239e1f..121bbaab1b1b 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec_command.h +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec_command.h @@ -1,6 +1,7 @@ #pragma once #include "common/buffer/buffer_impl.h" +#include "envoy/buffer/buffer.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" namespace Envoy { @@ -36,15 +37,15 @@ class Command : public MySQLCodec { // MySQLCodec int parseMessage(Buffer::Instance&, uint32_t len) override; - std::string encode() override; + void encode(Buffer::Instance&) override; Cmd parseCmd(Buffer::Instance& data); Cmd getCmd() const { return cmd_; } const std::string& getData() const { return data_; } std::string& getDb() { return db_; } void setCmd(Cmd cmd); - void setData(std::string& data); - void setDb(std::string db); + void setData(const std::string& data); + void setDb(const std::string db); bool isQuery() { return is_query_; } private: @@ -58,7 +59,7 @@ class CommandResponse : public MySQLCodec { public: // MySQLCodec int parseMessage(Buffer::Instance&, uint32_t) override { return MYSQL_SUCCESS; } - std::string encode() override { return ""; } + void encode(Buffer::Instance&) override {} void setServerStatus(uint16_t status); void setWarnings(uint16_t warnings); diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec_greeting.cc b/source/extensions/filters/network/mysql_proxy/mysql_codec_greeting.cc index cccc07377a34..0575dfb40e3e 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec_greeting.cc +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec_greeting.cc @@ -1,104 +1,192 @@ #include "extensions/filters/network/mysql_proxy/mysql_codec_greeting.h" +#include "envoy/buffer/buffer.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" #include "extensions/filters/network/mysql_proxy/mysql_utils.h" +#include namespace Envoy { namespace Extensions { namespace NetworkFilters { namespace MySQLProxy { -void ServerGreeting::setProtocol(int protocol) { protocol_ = protocol; } +void ServerGreeting::setProtocol(uint8_t protocol) { protocol_ = protocol; } -void ServerGreeting::setVersion(std::string& version) { version_.assign(version); } +void ServerGreeting::setVersion(const std::string& version) { version_.assign(version); } -void ServerGreeting::setThreadId(int thread_id) { thread_id_ = thread_id; } +void ServerGreeting::setThreadId(uint32_t thread_id) { thread_id_ = thread_id; } -void ServerGreeting::setSalt(std::string& salt) { salt_ = salt; } +void ServerGreeting::setAuthPluginData(const std::string& data) { + if (data.size() <= 8) { + auth_plugin_data1_ = data; + return; + } + auth_plugin_data1_ = data.substr(0, 8); + auth_plugin_data2_ = data.substr(8); +} + +void ServerGreeting::setAuthPluginData1(const std::string& data) { auth_plugin_data1_ = data; } -void ServerGreeting::setServerCap(int server_cap) { server_cap_ = server_cap; } +void ServerGreeting::setAuthPluginData2(const std::string& data) { auth_plugin_data2_ = data; } + +void ServerGreeting::setServerCap(uint32_t server_cap) { server_cap_ = server_cap; } + +void ServerGreeting::setBaseServerCap(uint16_t base_server_cap) { + base_server_cap_ = base_server_cap; +} -void ServerGreeting::setServerLanguage(int server_language) { server_language_ = server_language; } +void ServerGreeting::setExtServerCap(uint16_t ext_server_cap) { ext_server_cap_ = ext_server_cap; } -void ServerGreeting::setServerStatus(int server_status) { server_status_ = server_status; } +void ServerGreeting::setServerCharset(uint8_t server_charset) { server_charset_ = server_charset; } -void ServerGreeting::setExtServerCap(int ext_server_cap) { ext_server_cap_ = ext_server_cap; } +void ServerGreeting::setServerStatus(uint16_t server_status) { server_status_ = server_status; } + +void ServerGreeting::setAuthPluginName(const std::string& name) { auth_plugin_name_ = name; } int ServerGreeting::parseMessage(Buffer::Instance& buffer, uint32_t) { - uint8_t protocol = 0; - if (BufferHelper::readUint8(buffer, protocol) != MYSQL_SUCCESS) { + uint8_t auth_plugin_data_len = 0; + // parsing logic from + // https://github.com/mysql/mysql-proxy/blob/ca6ad61af9088147a568a079c44d0d322f5bee59/src/network-mysqld-packet.c#L1171 + if (BufferHelper::readUint8(buffer, protocol_) != MYSQL_SUCCESS) { ENVOY_LOG(info, "error parsing protocol in mysql Greeting msg"); return MYSQL_FAILURE; } - setProtocol(protocol); - std::string version; - if (BufferHelper::readString(buffer, version) != MYSQL_SUCCESS) { + if (BufferHelper::readString(buffer, version_) != MYSQL_SUCCESS) { ENVOY_LOG(info, "error parsing version in mysql Greeting msg"); return MYSQL_FAILURE; } - setVersion(version); - uint32_t thread_id = 0; - if (BufferHelper::readUint32(buffer, thread_id) != MYSQL_SUCCESS) { + if (BufferHelper::readUint32(buffer, thread_id_) != MYSQL_SUCCESS) { ENVOY_LOG(info, "error parsing thread_id in mysql Greeting msg"); return MYSQL_FAILURE; } - setThreadId(thread_id); - std::string salt; - if (BufferHelper::readString(buffer, salt) != MYSQL_SUCCESS) { - ENVOY_LOG(info, "error parsing salt in mysql Greeting msg"); + // read auth plugin data part 1, which is 8 byte. + if (BufferHelper::readStringBySize(buffer, 8, auth_plugin_data1_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing auth_plugin_data1 in mysql Greeting msg"); + return MYSQL_FAILURE; + } + if (BufferHelper::readBytes(buffer, 1) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error skiping bytes in mysql Greeting msg"); return MYSQL_FAILURE; } - setSalt(salt); if (protocol_ == MYSQL_PROTOCOL_9) { // End of HandshakeV9 greeting - return MYSQL_SUCCESS; + goto CHECK; } - uint16_t server_cap = 0; - if (BufferHelper::readUint16(buffer, server_cap) != MYSQL_SUCCESS) { + if (BufferHelper::readUint16(buffer, base_server_cap_) != MYSQL_SUCCESS) { ENVOY_LOG(info, "error parsing server_cap in mysql Greeting msg"); return MYSQL_FAILURE; } - setServerCap(server_cap); if (BufferHelper::endOfBuffer(buffer)) { // HandshakeV10 can terminate after Server Capabilities - return MYSQL_SUCCESS; + goto CHECK; } - uint8_t server_language = 0; - if (BufferHelper::readUint8(buffer, server_language) != MYSQL_SUCCESS) { + if (BufferHelper::readUint8(buffer, server_charset_) != MYSQL_SUCCESS) { ENVOY_LOG(info, "error parsing server_language in mysql Greeting msg"); return MYSQL_FAILURE; } - setServerLanguage(server_language); - uint16_t server_status = 0; - if (BufferHelper::readUint16(buffer, server_status) != MYSQL_SUCCESS) { + if (BufferHelper::readUint16(buffer, server_status_) != MYSQL_SUCCESS) { ENVOY_LOG(info, "error parsing server_status in mysql Greeting msg"); return MYSQL_FAILURE; } - setServerStatus(server_status); - uint16_t ext_server_cap = 0; - if (BufferHelper::readUint16(buffer, ext_server_cap) != MYSQL_SUCCESS) { + if (BufferHelper::readUint16(buffer, ext_server_cap_) != MYSQL_SUCCESS) { ENVOY_LOG(info, "error parsing ext_server_cap in mysql Greeting msg"); return MYSQL_FAILURE; } - setExtServerCap(ext_server_cap); + if (BufferHelper::readUint8(buffer, auth_plugin_data_len) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing auth_plugin_data_len in mysql Greeting msg"); + return MYSQL_FAILURE; + } + if (BufferHelper::readBytes(buffer, 10) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing reserved in mysql Greeting msg"); + return MYSQL_FAILURE; + } + if (server_cap_ & CLIENT_PLUGIN_AUTH) { + int auth_plugin_data_len2 = 0; + if (auth_plugin_data_len > 8) { + auth_plugin_data_len2 = auth_plugin_data_len - 8; + } + if (BufferHelper::readStringBySize(buffer, auth_plugin_data_len2, auth_plugin_data2_) != + MYSQL_SUCCESS) { + ENVOY_LOG(info, "error skiping auth_plugin_data2 in mysql Greeting msg"); + return MYSQL_FAILURE; + } + int skiped_bytes = 12 - (12 > auth_plugin_data_len2 ? auth_plugin_data_len2 : 12); + if (BufferHelper::readBytes(buffer, skiped_bytes) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error skiping in mysql Greeting msg"); + return MYSQL_FAILURE; + } + if (BufferHelper::readString(buffer, auth_plugin_name_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing auth_plugin_name in mysql Greeting msg"); + return MYSQL_FAILURE; + } + } else if (server_cap_ & CLIENT_SECURE_CONNECTION) { + if (BufferHelper::readStringBySize(buffer, 12, auth_plugin_data2_) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error parsing auth_plugin_data2 in mysql Greeting msg"); + return MYSQL_FAILURE; + } + if (BufferHelper::readBytes(buffer, 1) != MYSQL_SUCCESS) { + ENVOY_LOG(info, "error skiping in mysql Greeting msg"); + return MYSQL_FAILURE; + } + } +CHECK: + /* some final assertions */ + auto auth_plugin_len = auth_plugin_data1_.size() + auth_plugin_data2_.size(); + if (server_cap_ & CLIENT_PLUGIN_AUTH) { + if (auth_plugin_len != auth_plugin_data_len) { + ENVOY_LOG(info, "error parsing auth plugin data in mysql Greeting msg"); + return MYSQL_FAILURE; + } + } else if (server_cap_ & CLIENT_SECURE_CONNECTION) { + if (auth_plugin_len != 20 && auth_plugin_data_len != 0) { + ENVOY_LOG(info, "error parsing auth plugin data in mysql Greeting msg"); + return MYSQL_FAILURE; + } + } else { + /* old auth */ + if (auth_plugin_len != 8) { + ENVOY_LOG(info, "error parsing auth plugin data in mysql Greeting msg"); + return MYSQL_FAILURE; + } + } return MYSQL_SUCCESS; } -std::string ServerGreeting::encode() { +void ServerGreeting::encode(Buffer::Instance& out) { + // https://github.com/mysql/mysql-proxy/blob/ca6ad61af9088147a568a079c44d0d322f5bee59/src/network-mysqld-packet.c#L1339 uint8_t enc_end_string = 0; - Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); - BufferHelper::addUint8(*buffer, protocol_); - BufferHelper::addString(*buffer, version_); - BufferHelper::addUint8(*buffer, enc_end_string); - BufferHelper::addUint32(*buffer, thread_id_); - BufferHelper::addString(*buffer, salt_); - BufferHelper::addUint8(*buffer, enc_end_string); - BufferHelper::addUint16(*buffer, server_cap_); - BufferHelper::addUint8(*buffer, server_language_); - BufferHelper::addUint16(*buffer, server_status_); - BufferHelper::addUint16(*buffer, ext_server_cap_); - - return buffer->toString(); + BufferHelper::addUint8(out, protocol_); + BufferHelper::addString(out, version_); + BufferHelper::addUint8(out, enc_end_string); + BufferHelper::addUint32(out, thread_id_); + BufferHelper::addString(out, auth_plugin_data1_.substr(0, 8)); + BufferHelper::addUint8(out, enc_end_string); + if (protocol_ == MYSQL_PROTOCOL_9) { + return; + } + BufferHelper::addUint16(out, base_server_cap_); + BufferHelper::addUint8(out, server_charset_); + BufferHelper::addUint16(out, server_status_); + BufferHelper::addUint16(out, ext_server_cap_); + + if (server_cap_ & CLIENT_PLUGIN_AUTH) { + BufferHelper::addUint8(out, auth_plugin_data2_.size() + auth_plugin_data1_.size()); + } else { + BufferHelper::addUint8(out, 0); + } + // reserved + for (int i = 0; i < 10; i++) { + BufferHelper::addUint8(out, 0); + } + if (server_cap_ & CLIENT_PLUGIN_AUTH) { + BufferHelper::addString(out, auth_plugin_data2_); + BufferHelper::addString(out, auth_plugin_name_); + // TODO(qinggniq) version 5.5.7-9 and 5.6.0-1 will not add tail \0 + BufferHelper::addUint8(out, enc_end_string); + } else if (server_cap_ & CLIENT_SECURE_CONNECTION) { + BufferHelper::addString(out, auth_plugin_data2_); + BufferHelper::addUint8(out, enc_end_string); + } } } // namespace MySQLProxy diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec_greeting.h b/source/extensions/filters/network/mysql_proxy/mysql_codec_greeting.h index 2e3f63dd548c..89d78c51ad5b 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec_greeting.h +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec_greeting.h @@ -2,6 +2,7 @@ #include "common/buffer/buffer_impl.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" +#include namespace Envoy { namespace Extensions { @@ -12,34 +13,54 @@ class ServerGreeting : public MySQLCodec { public: // MySQLCodec int parseMessage(Buffer::Instance& buffer, uint32_t len) override; - std::string encode() override; + void encode(Buffer::Instance&) override; - int getProtocol() const { return protocol_; } + uint8_t getProtocol() const { return protocol_; } const std::string& getVersion() const { return version_; } - int getThreadId() const { return thread_id_; } - const std::string& getSalt() const { return salt_; }; - int getServerCap() const { return server_cap_; } - int getServerLanguage() const { return server_language_; } + uint32_t getThreadId() const { return thread_id_; } + uint8_t getAutPluginDataLen() const { + return auth_plugin_data1_.size() + auth_plugin_data2_.size(); + } + const std::string& getAuthPluginData1() const { return auth_plugin_data1_; } + const std::string& getAuthPluginData2() const { return auth_plugin_data2_; } + std::string getAuthPluginData() const { return auth_plugin_data1_ + auth_plugin_data2_; } + int getServerCharset() const { return server_charset_; } int getServerStatus() const { return server_status_; } - int getExtServerCap() const { return ext_server_cap_; } - void setProtocol(int protocol); - void setVersion(std::string& version); - void setThreadId(int thread_id); - void setSalt(std::string& salt); - void setServerCap(int server_cap); - void setServerLanguage(int server_language); - void setServerStatus(int server_status); - void setExtServerCap(int ext_server_cap); + uint32_t getServerCap() const { return server_cap_; } + uint16_t getBaseServerCap() const { return base_server_cap_; } + uint16_t getExtServerCap() const { return ext_server_cap_; } + const std::string& getAuthPluginName() const { return auth_plugin_name_; } + + void setProtocol(uint8_t protocol); + void setVersion(const std::string& version); + void setThreadId(uint32_t thread_id); + void setServerCap(uint32_t server_cap); + void setBaseServerCap(uint16_t base_server_cap); + void setExtServerCap(uint16_t ext_server_cap); + void setAuthPluginName(const std::string& name); + void setAuthPluginData1(const std::string& name); + void setAuthPluginData2(const std::string& name); + void setServerCharset(uint8_t server_language); + void setServerStatus(uint16_t server_status); + + void setAuthPluginData(const std::string& salt); private: - int protocol_; + uint8_t protocol_{0}; std::string version_; - int thread_id_; - std::string salt_; - int server_cap_; - int server_language_; - int server_status_; - int ext_server_cap_; + uint32_t thread_id_{0}; + std::string auth_plugin_data1_; + std::string auth_plugin_data2_; + union { + uint32_t server_cap_{0}; + struct { + uint16_t ext_server_cap_; + uint16_t base_server_cap_; + }; + }; + uint8_t server_charset_{0}; + uint16_t server_status_{0}; + std::string auth_plugin_name_; }; } // namespace MySQLProxy diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec_switch_resp.cc b/source/extensions/filters/network/mysql_proxy/mysql_codec_switch_resp.cc index 77f43d4d81c6..9fc905809b1f 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec_switch_resp.cc +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec_switch_resp.cc @@ -1,5 +1,6 @@ #include "extensions/filters/network/mysql_proxy/mysql_codec_switch_resp.h" +#include "envoy/buffer/buffer.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" #include "extensions/filters/network/mysql_proxy/mysql_utils.h" @@ -8,17 +9,14 @@ namespace Extensions { namespace NetworkFilters { namespace MySQLProxy { -void ClientSwitchResponse::setAuthPluginResp(std::string& auth_plugin_resp) { +void ClientSwitchResponse::setAuthPluginResp(const std::string& auth_plugin_resp) { auth_plugin_resp_.assign(auth_plugin_resp); } int ClientSwitchResponse::parseMessage(Buffer::Instance&, uint32_t) { return MYSQL_SUCCESS; } -std::string ClientSwitchResponse::encode() { - Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); - - BufferHelper::addString(*buffer, auth_plugin_resp_); - return buffer->toString(); +void ClientSwitchResponse::encode(Buffer::Instance& out) { + BufferHelper::addString(out, auth_plugin_resp_); } } // namespace MySQLProxy diff --git a/source/extensions/filters/network/mysql_proxy/mysql_codec_switch_resp.h b/source/extensions/filters/network/mysql_proxy/mysql_codec_switch_resp.h index 7b26ad7bcf27..8d105013065e 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_codec_switch_resp.h +++ b/source/extensions/filters/network/mysql_proxy/mysql_codec_switch_resp.h @@ -1,6 +1,7 @@ #pragma once #include "common/buffer/buffer_impl.h" +#include "envoy/buffer/buffer.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" namespace Envoy { @@ -12,9 +13,9 @@ class ClientSwitchResponse : public MySQLCodec { public: // MySQLCodec int parseMessage(Buffer::Instance& buffer, uint32_t len) override; - std::string encode() override; + void encode(Buffer::Instance&) override; - void setAuthPluginResp(std::string& auth_swith_resp); + void setAuthPluginResp(const std::string& auth_swith_resp); private: std::string auth_plugin_resp_; diff --git a/source/extensions/filters/network/mysql_proxy/mysql_decoder.cc b/source/extensions/filters/network/mysql_proxy/mysql_decoder.cc index 9b102316e81b..ee6ec08fb201 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_decoder.cc +++ b/source/extensions/filters/network/mysql_proxy/mysql_decoder.cc @@ -1,6 +1,8 @@ #include "extensions/filters/network/mysql_proxy/mysql_decoder.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h" #include "extensions/filters/network/mysql_proxy/mysql_utils.h" +#include namespace Envoy { namespace Extensions { @@ -48,18 +50,27 @@ void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t client_login_resp.decode(message, seq, len); callbacks_.onClientLoginResponse(client_login_resp); - if (client_login_resp.getRespCode() == MYSQL_RESP_OK) { + switch (client_login_resp.type()) { + case Ok: { session_.setState(MySQLSession::State::Req); // reset seq# when entering the REQ state session_.setExpectedSeq(MYSQL_REQUEST_PKT_NUM); - } else if (client_login_resp.getRespCode() == MYSQL_RESP_AUTH_SWITCH) { + break; + } + case AuthSwitch: { session_.setState(MySQLSession::State::AuthSwitchResp); - } else if (client_login_resp.getRespCode() == MYSQL_RESP_ERR) { + break; + } + case Err: { // client/server should close the connection: // https://dev.mysql.com/doc/internals/en/connection-phase.html session_.setState(MySQLSession::State::Error); - } else { + break; + } + case AuthMoreData: + default: session_.setState(MySQLSession::State::NotHandled); + break; } break; } @@ -78,16 +89,25 @@ void DecoderImpl::parseMessage(Buffer::Instance& message, uint8_t seq, uint32_t client_login_resp.decode(message, seq, len); callbacks_.onMoreClientLoginResponse(client_login_resp); - if (client_login_resp.getRespCode() == MYSQL_RESP_OK) { + switch (client_login_resp.type()) { + case Ok: { session_.setState(MySQLSession::State::Req); - } else if (client_login_resp.getRespCode() == MYSQL_RESP_MORE) { + break; + } + case AuthMoreData: { session_.setState(MySQLSession::State::AuthSwitchResp); - } else if (client_login_resp.getRespCode() == MYSQL_RESP_ERR) { + break; + } + case Err: { // stop parsing auth req/response, attempt to resync in command state session_.setState(MySQLSession::State::Resync); session_.setExpectedSeq(MYSQL_REQUEST_PKT_NUM); - } else { + break; + } + case AuthSwitch: + default: session_.setState(MySQLSession::State::NotHandled); + break; } break; } diff --git a/source/extensions/filters/network/mysql_proxy/mysql_filter.cc b/source/extensions/filters/network/mysql_proxy/mysql_filter.cc index d3fa62e55fc8..8b9292df245d 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_filter.cc +++ b/source/extensions/filters/network/mysql_proxy/mysql_filter.cc @@ -84,15 +84,15 @@ void MySQLFilter::onClientLogin(ClientLogin& client_login) { } void MySQLFilter::onClientLoginResponse(ClientLoginResponse& client_login_resp) { - if (client_login_resp.getRespCode() == MYSQL_RESP_AUTH_SWITCH) { + if (client_login_resp.type() == AuthSwitch) { config_->stats_.auth_switch_request_.inc(); - } else if (client_login_resp.getRespCode() == MYSQL_RESP_ERR) { + } else if (client_login_resp.type() == Err) { config_->stats_.login_failures_.inc(); } } void MySQLFilter::onMoreClientLoginResponse(ClientLoginResponse& client_login_resp) { - if (client_login_resp.getRespCode() == MYSQL_RESP_ERR) { + if (client_login_resp.type() == Err) { config_->stats_.login_failures_.inc(); } } diff --git a/source/extensions/filters/network/mysql_proxy/mysql_utils.cc b/source/extensions/filters/network/mysql_proxy/mysql_utils.cc index d86608ea64b6..b1df8835e3ee 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_utils.cc +++ b/source/extensions/filters/network/mysql_proxy/mysql_utils.cc @@ -1,4 +1,10 @@ #include "extensions/filters/network/mysql_proxy/mysql_utils.h" +#include "common/buffer/buffer_impl.h" +#include "envoy/common/exception.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec.h" + +#include +#include namespace Envoy { namespace Extensions { @@ -13,21 +19,43 @@ void BufferHelper::addUint16(Buffer::Instance& buffer, uint16_t val) { buffer.writeLEInt(val); } +void BufferHelper::addUint24(Buffer::Instance& buffer, uint32_t val) { + buffer.writeLEInt(val); +} + void BufferHelper::addUint32(Buffer::Instance& buffer, uint32_t val) { buffer.writeLEInt(val); } +// Implementation of MySQL lenenc encoder based on +// https://dev.mysql.com/doc/internals/en/integer.html#packet-Protocol::FixedLengthInteger +void BufferHelper::addLengthEncodedInteger(Buffer::Instance& buffer, uint64_t val) { + if (val < 251) { + buffer.writeLEInt(val); + } else if (val < (1 << 16)) { + buffer.writeLEInt(0xfc); + buffer.writeLEInt(val); + } else if (val < (1 << 24)) { + buffer.writeLEInt(0xfd); + buffer.writeLEInt(val); + } else { + buffer.writeLEInt(0xfe); + buffer.writeLEInt(val); + } +} + void BufferHelper::addString(Buffer::Instance& buffer, const std::string& str) { buffer.add(str); } -std::string BufferHelper::encodeHdr(const std::string& cmd_str, uint8_t seq) { +void BufferHelper::addStringBySize(Buffer::Instance& buffer, size_t len, const std::string& str) { + buffer.add(str.substr(0, len)); +} + +void BufferHelper::encodeHdr(Buffer::Instance& pkg, uint8_t seq) { + // the pkg buffer should only contain one package data + uint32_t header = (seq << 24) | (pkg.length() & MYSQL_HDR_PKT_SIZE_MASK); Buffer::OwnedImpl buffer; - // First byte contains sequence number, next 3 bytes contain cmd string size - uint32_t header = (seq << 24) | (cmd_str.length() & MYSQL_HDR_PKT_SIZE_MASK); addUint32(buffer, header); - - std::string e_string = buffer.toString(); - e_string.append(cmd_str); - return e_string; + pkg.prepend(buffer); } bool BufferHelper::endOfBuffer(Buffer::Instance& buffer) { return buffer.length() == 0; } @@ -54,6 +82,17 @@ int BufferHelper::readUint16(Buffer::Instance& buffer, uint16_t& val) { } } +int BufferHelper::readUint24(Buffer::Instance& buffer, uint32_t& val) { + try { + val = buffer.peekLEInt(0); + buffer.drain(sizeof(uint8_t) * 3); + return MYSQL_SUCCESS; + } catch (EnvoyException& e) { + // buffer underflow + return MYSQL_FAILURE; + } +} + int BufferHelper::readUint32(Buffer::Instance& buffer, uint32_t& val) { try { val = buffer.peekLEInt(0); @@ -98,6 +137,20 @@ int BufferHelper::readLengthEncodedInteger(Buffer::Instance& buffer, uint64_t& v return MYSQL_SUCCESS; } +int BufferHelper::readStringEof(Buffer::Instance& buffer, std::string& str) { + char end = EOF; + ssize_t index = buffer.search(&end, sizeof(end), 0); + if (index == -1) { + return MYSQL_FAILURE; + } + if (static_cast(buffer.length()) < (index + 1)) { + return MYSQL_FAILURE; + } + str.assign(std::string(static_cast(buffer.linearize(index)), index)); + buffer.drain(index + 1); + return MYSQL_SUCCESS; +} + int BufferHelper::readBytes(Buffer::Instance& buffer, size_t skip_bytes) { if (buffer.length() < skip_bytes) { return MYSQL_FAILURE; @@ -116,7 +169,6 @@ int BufferHelper::readString(Buffer::Instance& buffer, std::string& str) { return MYSQL_FAILURE; } str.assign(std::string(static_cast(buffer.linearize(index)), index)); - str = str.substr(0); buffer.drain(index + 1); return MYSQL_SUCCESS; } @@ -126,7 +178,6 @@ int BufferHelper::readStringBySize(Buffer::Instance& buffer, size_t len, std::st return MYSQL_FAILURE; } str.assign(std::string(static_cast(buffer.linearize(len)), len)); - str = str.substr(0); buffer.drain(len); return MYSQL_SUCCESS; } @@ -141,6 +192,25 @@ int BufferHelper::peekUint32(Buffer::Instance& buffer, uint32_t& val) { } } +int BufferHelper::peekUint16(Buffer::Instance& buffer, uint16_t& val) { + try { + val = buffer.peekLEInt(0); + return MYSQL_SUCCESS; + } catch (EnvoyException& e) { + // buffer undeflow + return MYSQL_FAILURE; + } +} + +int BufferHelper::peekUint8(Buffer::Instance& buffer, uint8_t& val) { + try { + val = buffer.peekLEInt(0); + return MYSQL_SUCCESS; + } catch (EnvoyException& e) { + // buffer undeflow + return MYSQL_FAILURE; + } +} void BufferHelper::consumeHdr(Buffer::Instance& buffer) { buffer.drain(sizeof(uint32_t)); } int BufferHelper::peekHdr(Buffer::Instance& buffer, uint32_t& len, uint8_t& seq) { diff --git a/source/extensions/filters/network/mysql_proxy/mysql_utils.h b/source/extensions/filters/network/mysql_proxy/mysql_utils.h index 75f1a46a11b0..189b1f1a1308 100644 --- a/source/extensions/filters/network/mysql_proxy/mysql_utils.h +++ b/source/extensions/filters/network/mysql_proxy/mysql_utils.h @@ -1,6 +1,9 @@ #pragma once +#include #include +#include +#include "envoy/buffer/buffer.h" #include "envoy/common/platform.h" #include "common/buffer/buffer_impl.h" @@ -27,18 +30,26 @@ class BufferHelper : public Logger::Loggable { public: static void addUint8(Buffer::Instance& buffer, uint8_t val); static void addUint16(Buffer::Instance& buffer, uint16_t val); + static void addUint24(Buffer::Instance& buffer, uint32_t val); static void addUint32(Buffer::Instance& buffer, uint32_t val); + static void addLengthEncodedInteger(Buffer::Instance& buffer, uint64_t val); static void addString(Buffer::Instance& buffer, const std::string& str); - static std::string encodeHdr(const std::string& cmd_str, uint8_t seq); + static void addStringBySize(Buffer::Instance& buffer, size_t len, const std::string& str); + static void encodeHdr(Buffer::Instance& pkg, uint8_t seq); static bool endOfBuffer(Buffer::Instance& buffer); static int readUint8(Buffer::Instance& buffer, uint8_t& val); static int readUint16(Buffer::Instance& buffer, uint16_t& val); + static int readUint24(Buffer::Instance& buffer, uint32_t& val); static int readUint32(Buffer::Instance& buffer, uint32_t& val); static int readLengthEncodedInteger(Buffer::Instance& buffer, uint64_t& val); static int readBytes(Buffer::Instance& buffer, size_t skip_bytes); static int readString(Buffer::Instance& buffer, std::string& str); static int readStringBySize(Buffer::Instance& buffer, size_t len, std::string& str); + static int readStringEof(Buffer::Instance& buffer, std::string& str); + static int readAll(Buffer::Instance& buffer, std::string& str); static int peekUint32(Buffer::Instance& buffer, uint32_t& val); + static int peekUint16(Buffer::Instance& buffer, uint16_t& val); + static int peekUint8(Buffer::Instance& buffer, uint8_t& val); static void consumeHdr(Buffer::Instance& buffer); static int peekHdr(Buffer::Instance& buffer, uint32_t& len, uint8_t& seq); }; diff --git a/source/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter.cc b/source/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter.cc index 9b3584de72c4..0e1136336c00 100644 --- a/source/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter.cc +++ b/source/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter.cc @@ -33,7 +33,7 @@ Network::FilterStatus ProxyFilter::onNewConnection() { return Network::FilterStatus::Continue; } - circuit_breaker_ = config_->cache().canCreateDnsRequest(absl::nullopt); + circuit_breaker_ = config_->cache().canCreateDnsRequest(); if (circuit_breaker_ == nullptr) { ENVOY_CONN_LOG(debug, "pending request overflow", read_callbacks_->connection()); diff --git a/source/extensions/filters/network/thrift_proxy/conn_manager.cc b/source/extensions/filters/network/thrift_proxy/conn_manager.cc index 7f7129715edf..d3e23b5d29cc 100644 --- a/source/extensions/filters/network/thrift_proxy/conn_manager.cc +++ b/source/extensions/filters/network/thrift_proxy/conn_manager.cc @@ -181,9 +181,15 @@ bool ConnectionManager::passthroughEnabled() const { return false; } - // This is called right after the metadata has been parsed, and the ActiveRpc being processed must - // be in the rpcs_ list. - ASSERT(!rpcs_.empty()); + // If the rpcs list is empty, a local response happened. + // + // TODO(rgs1): we actually could still enable passthrough for local + // responses as long as the transport is framed and the protocol is + // not Twitter. + if (rpcs_.empty()) { + return false; + } + return (*rpcs_.begin())->passthroughSupported(); } @@ -323,12 +329,26 @@ FilterStatus ConnectionManager::ActiveRpc::applyDecoderFilters(ActiveRpcDecoderF for (; entry != decoder_filters_.end(); entry++) { const FilterStatus status = filter_action_((*entry)->handle_.get()); if (local_response_sent_) { - // The filter called sendLocalReply: stop processing filters and return - // FilterStatus::Continue irrespective of the current result. + // The filter called sendLocalReply but _did not_ close the connection. + // We return FilterStatus::Continue irrespective of the current result, + // which is fine because subsequent calls to this method will skip + // filters anyway. + // + // Note: we need to return FilterStatus::Continue here, in order for decoding + // to proceed. This is important because as noted above, the connection remains + // open so we need to consume the remaining bytes. break; } if (status != FilterStatus::Continue) { + // If we got FilterStatus::StopIteration and a local reply happened but + // local_response_sent_ was not set, the connection was closed. + // + // In this case, either resetAllRpcs() gets called via onEvent(LocalClose) or + // dispatch() stops the processing. + // + // In other words, after a local reply closes the connection and StopIteration + // is returned we are done. return status; } } diff --git a/source/extensions/rate_limit_descriptors/expr/config.cc b/source/extensions/rate_limit_descriptors/expr/config.cc index aeebb5f96e23..57aeda76b92e 100644 --- a/source/extensions/rate_limit_descriptors/expr/config.cc +++ b/source/extensions/rate_limit_descriptors/expr/config.cc @@ -30,7 +30,7 @@ class ExpressionDescriptor : public RateLimit::DescriptorProducer { } // Ratelimit::DescriptorProducer - bool populateDescriptor(RateLimit::Descriptor& descriptor, const std::string&, + bool populateDescriptor(RateLimit::DescriptorEntry& descriptor_entry, const std::string&, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info) const override { ProtobufWkt::Arena arena; @@ -42,7 +42,7 @@ class ExpressionDescriptor : public RateLimit::DescriptorProducer { // service. return skip_if_error_; } - descriptor.entries_.push_back({descriptor_key_, Filters::Common::Expr::print(result.value())}); + descriptor_entry = {descriptor_key_, Filters::Common::Expr::print(result.value())}; return true; } diff --git a/source/extensions/retry/priority/previous_priorities/previous_priorities.h b/source/extensions/retry/priority/previous_priorities/previous_priorities.h index 05e4f3db37a2..f626f664a04c 100644 --- a/source/extensions/retry/priority/previous_priorities/previous_priorities.h +++ b/source/extensions/retry/priority/previous_priorities/previous_priorities.h @@ -29,7 +29,8 @@ class PreviousPrioritiesRetryPriority : public Upstream::RetryPriority { void recalculatePerPriorityState(uint32_t priority, const Upstream::PrioritySet& priority_set) { // Recalculate health and priority the same way the load balancer does it. Upstream::LoadBalancerBase::recalculatePerPriorityState( - priority, priority_set, per_priority_load_, per_priority_health_, per_priority_degraded_); + priority, priority_set, per_priority_load_, per_priority_health_, per_priority_degraded_, + total_healthy_hosts_); } uint32_t adjustedAvailability(std::vector& per_priority_health, @@ -47,6 +48,7 @@ class PreviousPrioritiesRetryPriority : public Upstream::RetryPriority { Upstream::HealthyAndDegradedLoad per_priority_load_; Upstream::HealthyAvailability per_priority_health_; Upstream::DegradedAvailability per_priority_degraded_; + uint32_t total_healthy_hosts_; }; } // namespace Priority diff --git a/source/extensions/transport_sockets/tls/BUILD b/source/extensions/transport_sockets/tls/BUILD index 860b56203c30..2bcb141d5a63 100644 --- a/source/extensions/transport_sockets/tls/BUILD +++ b/source/extensions/transport_sockets/tls/BUILD @@ -177,6 +177,7 @@ envoy_cc_library( ], deps = [ "//source/common/common:assert_lib", + "//source/common/common:empty_string", "//source/common/common:utility_lib", "//source/common/network:address_lib", ], diff --git a/source/extensions/transport_sockets/tls/context_impl.cc b/source/extensions/transport_sockets/tls/context_impl.cc index 2ba6a6ee89be..f718e79ac4d3 100644 --- a/source/extensions/transport_sockets/tls/context_impl.cc +++ b/source/extensions/transport_sockets/tls/context_impl.cc @@ -464,29 +464,51 @@ ContextImpl::ContextImpl(Stats::Scope& scope, const Envoy::Ssl::ContextConfig& c parsed_alpn_protocols_ = parseAlpnProtocols(config.alpnProtocols()); // Use the SSL library to iterate over the configured ciphers. + // + // Note that if a negotiated cipher suite is outside of this set, we'll issue an ENVOY_BUG. for (TlsContext& tls_context : tls_contexts_) { for (const SSL_CIPHER* cipher : SSL_CTX_get_ciphers(tls_context.ssl_ctx_.get())) { stat_name_set_->rememberBuiltin(SSL_CIPHER_get_name(cipher)); } } - // Add hardcoded cipher suites from the TLS 1.3 spec: - // https://tools.ietf.org/html/rfc8446 - stat_name_set_->rememberBuiltins( - {"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_AES_128_GCM_SHA256"}); - - // Curves from - // https://github.com/google/boringssl/blob/f4d8b969200f1ee2dd872ffb85802e6a0976afe7/ssl/ssl_key_share.cc#L384 + // Add supported cipher suites from the TLS 1.3 spec: + // https://tools.ietf.org/html/rfc8446#appendix-B.4 + // AES-CCM cipher suites are removed (no BoringSSL support). // - // Note that if a curve is configured outside this set, we'll issue an ENVOY_BUG so - // it will hopefully be caught. + // Note that if a negotiated cipher suite is outside of this set, we'll issue an ENVOY_BUG. stat_name_set_->rememberBuiltins( - {"P-224", "P-256", "P-384", "P-521", "X25519", "CECPQ2", "CECPQ2b"}); + {"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"}); - // Algorithms - stat_name_set_->rememberBuiltins({"ecdsa_secp256r1_sha256", "rsa_pss_rsae_sha256"}); + // All supported curves. Source: + // https://github.com/google/boringssl/blob/3743aafdacff2f7b083615a043a37101f740fa53/ssl/ssl_key_share.cc#L302-L309 + // + // Note that if a negotiated curve is outside of this set, we'll issue an ENVOY_BUG. + stat_name_set_->rememberBuiltins({"P-224", "P-256", "P-384", "P-521", "X25519", "CECPQ2"}); - // Versions + // All supported signature algorithms. Source: + // https://github.com/google/boringssl/blob/3743aafdacff2f7b083615a043a37101f740fa53/ssl/ssl_privkey.cc#L436-L453 + // + // Note that if a negotiated algorithm is outside of this set, we'll issue an ENVOY_BUG. + stat_name_set_->rememberBuiltins({ + "rsa_pkcs1_md5_sha1", + "rsa_pkcs1_sha1", + "rsa_pkcs1_sha256", + "rsa_pkcs1_sha384", + "rsa_pkcs1_sha512", + "ecdsa_sha1", + "ecdsa_secp256r1_sha256", + "ecdsa_secp384r1_sha384", + "ecdsa_secp521r1_sha512", + "rsa_pss_rsae_sha256", + "rsa_pss_rsae_sha384", + "rsa_pss_rsae_sha512", + "ed25519", + }); + + // All supported protocol versions. + // + // Note that if a negotiated version is outside of this set, we'll issue an ENVOY_BUG. stat_name_set_->rememberBuiltins({"TLSv1", "TLSv1.1", "TLSv1.2", "TLSv1.3"}); } @@ -735,12 +757,8 @@ bool ContextImpl::dnsNameMatch(const absl::string_view dns_name, const absl::str if (pattern_len > 1 && pattern[0] == '*' && pattern[1] == '.') { if (dns_name.length() > pattern_len - 1) { const size_t off = dns_name.length() - pattern_len + 1; - if (Runtime::runtimeFeatureEnabled("envoy.reloadable_features.fix_wildcard_matching")) { - return dns_name.substr(0, off).find('.') == std::string::npos && - dns_name.substr(off, pattern_len - 1) == pattern.substr(1, pattern_len - 1); - } else { - return dns_name.substr(off, pattern_len - 1) == pattern.substr(1, pattern_len - 1); - } + return dns_name.substr(0, off).find('.') == std::string::npos && + dns_name.substr(off, pattern_len - 1) == pattern.substr(1, pattern_len - 1); } } diff --git a/source/extensions/transport_sockets/tls/context_impl.h b/source/extensions/transport_sockets/tls/context_impl.h index c533e0cc9c4f..77c52874becb 100644 --- a/source/extensions/transport_sockets/tls/context_impl.h +++ b/source/extensions/transport_sockets/tls/context_impl.h @@ -250,6 +250,11 @@ class ServerContextImpl : public ContextImpl, public Envoy::Ssl::ServerContext { ServerContextImpl(Stats::Scope& scope, const Envoy::Ssl::ServerContextConfig& config, const std::vector& server_names, TimeSource& time_source); + // Select the TLS certificate context in SSL_CTX_set_select_certificate_cb() callback with + // ClientHello details. This is made public for use by custom TLS extensions who want to + // manually create and use this as a client hello callback. + enum ssl_select_cert_result_t selectTlsContext(const SSL_CLIENT_HELLO* ssl_client_hello); + private: using SessionContextID = std::array; @@ -259,9 +264,6 @@ class ServerContextImpl : public ContextImpl, public Envoy::Ssl::ServerContext { HMAC_CTX* hmac_ctx, int encrypt); bool isClientEcdsaCapable(const SSL_CLIENT_HELLO* ssl_client_hello); bool isClientOcspCapable(const SSL_CLIENT_HELLO* ssl_client_hello); - // Select the TLS certificate context in SSL_CTX_set_select_certificate_cb() callback with - // ClientHello details. - enum ssl_select_cert_result_t selectTlsContext(const SSL_CLIENT_HELLO* ssl_client_hello); OcspStapleAction ocspStapleAction(const ServerContextImpl::TlsContext& ctx, bool client_ocsp_capable); diff --git a/source/extensions/transport_sockets/tls/ssl_handshaker.cc b/source/extensions/transport_sockets/tls/ssl_handshaker.cc index 7d8def3ecfd1..766454bbd879 100644 --- a/source/extensions/transport_sockets/tls/ssl_handshaker.cc +++ b/source/extensions/transport_sockets/tls/ssl_handshaker.cc @@ -233,6 +233,8 @@ Network::PostIoAction SslHandshakerImpl::doHandshake() { : PostIoAction::Close; } else { int err = SSL_get_error(ssl(), rc); + ENVOY_CONN_LOG(trace, "ssl error occurred while read: {}", handshake_callbacks_->connection(), + Utility::getErrorDescription(err)); switch (err) { case SSL_ERROR_WANT_READ: case SSL_ERROR_WANT_WRITE: diff --git a/source/extensions/transport_sockets/tls/ssl_handshaker.h b/source/extensions/transport_sockets/tls/ssl_handshaker.h index 50090f6f43a7..cb9be1e3190f 100644 --- a/source/extensions/transport_sockets/tls/ssl_handshaker.h +++ b/source/extensions/transport_sockets/tls/ssl_handshaker.h @@ -37,7 +37,9 @@ class SslExtendedSocketInfoImpl : public Envoy::Ssl::SslExtendedSocketInfo { Envoy::Ssl::ClientValidationStatus::NotValidated}; }; -class SslHandshakerImpl : public Ssl::ConnectionInfo, public Ssl::Handshaker { +class SslHandshakerImpl : public Ssl::ConnectionInfo, + public Ssl::Handshaker, + protected Logger::Loggable { public: SslHandshakerImpl(bssl::UniquePtr ssl, int ssl_extended_socket_info_index, Ssl::HandshakeCallbacks* handshake_callbacks); diff --git a/source/extensions/transport_sockets/tls/ssl_socket.cc b/source/extensions/transport_sockets/tls/ssl_socket.cc index ba408c2d417f..7c005f47c700 100644 --- a/source/extensions/transport_sockets/tls/ssl_socket.cc +++ b/source/extensions/transport_sockets/tls/ssl_socket.cc @@ -137,6 +137,8 @@ Network::IoResult SslSocket::doRead(Buffer::Instance& read_buffer) { if (result.error_.has_value()) { keep_reading = false; int err = SSL_get_error(rawSsl(), result.error_.value()); + ENVOY_CONN_LOG(trace, "ssl error occurred while read: {}", callbacks_->connection(), + Utility::getErrorDescription(err)); switch (err) { case SSL_ERROR_WANT_READ: break; @@ -221,7 +223,9 @@ void SslSocket::drainErrorQueue() { ERR_func_error_string(err), ":", ERR_reason_error_string(err))); } - ENVOY_CONN_LOG(debug, "{}", callbacks_->connection(), failure_reason_); + if (!failure_reason_.empty()) { + ENVOY_CONN_LOG(debug, "{}", callbacks_->connection(), failure_reason_); + } if (saw_error && !saw_counted_error) { ctx_->stats().connection_error_.inc(); } @@ -263,6 +267,8 @@ Network::IoResult SslSocket::doWrite(Buffer::Instance& write_buffer, bool end_st bytes_to_write = std::min(write_buffer.length(), static_cast(16384)); } else { int err = SSL_get_error(rawSsl(), rc); + ENVOY_CONN_LOG(trace, "ssl error occurred while write: {}", callbacks_->connection(), + Utility::getErrorDescription(err)); switch (err) { case SSL_ERROR_WANT_WRITE: bytes_to_retry_ = bytes_to_write; diff --git a/source/extensions/transport_sockets/tls/utility.cc b/source/extensions/transport_sockets/tls/utility.cc index e5b8bc17085f..775fc763077b 100644 --- a/source/extensions/transport_sockets/tls/utility.cc +++ b/source/extensions/transport_sockets/tls/utility.cc @@ -1,6 +1,7 @@ #include "extensions/transport_sockets/tls/utility.h" #include "common/common/assert.h" +#include "common/common/empty_string.h" #include "common/network/address_impl.h" #include "absl/strings/str_join.h" @@ -214,6 +215,50 @@ absl::optional Utility::getLastCryptoError() { return absl::nullopt; } +absl::string_view Utility::getErrorDescription(int err) { + switch (err) { + case SSL_ERROR_NONE: + return SSL_ERROR_NONE_MESSAGE; + case SSL_ERROR_SSL: + return SSL_ERROR_SSL_MESSAGE; + case SSL_ERROR_WANT_READ: + return SSL_ERROR_WANT_READ_MESSAGE; + case SSL_ERROR_WANT_WRITE: + return SSL_ERROR_WANT_WRITE_MESSAGE; + case SSL_ERROR_WANT_X509_LOOKUP: + return SSL_ERROR_WANT_X509_LOOPUP_MESSAGE; + case SSL_ERROR_SYSCALL: + return SSL_ERROR_SYSCALL_MESSAGE; + case SSL_ERROR_ZERO_RETURN: + return SSL_ERROR_ZERO_RETURN_MESSAGE; + case SSL_ERROR_WANT_CONNECT: + return SSL_ERROR_WANT_CONNECT_MESSAGE; + case SSL_ERROR_WANT_ACCEPT: + return SSL_ERROR_WANT_ACCEPT_MESSAGE; + case SSL_ERROR_WANT_CHANNEL_ID_LOOKUP: + return SSL_ERROR_WANT_CHANNEL_ID_LOOKUP_MESSAGE; + case SSL_ERROR_PENDING_SESSION: + return SSL_ERROR_PENDING_SESSION_MESSAGE; + case SSL_ERROR_PENDING_CERTIFICATE: + return SSL_ERROR_PENDING_CERTIFICATE_MESSAGE; + case SSL_ERROR_WANT_PRIVATE_KEY_OPERATION: + return SSL_ERROR_WANT_PRIVATE_KEY_OPERATION_MESSAGE; + case SSL_ERROR_PENDING_TICKET: + return SSL_ERROR_PENDING_TICKET_MESSAGE; + case SSL_ERROR_EARLY_DATA_REJECTED: + return SSL_ERROR_EARLY_DATA_REJECTED_MESSAGE; + case SSL_ERROR_WANT_CERTIFICATE_VERIFY: + return SSL_ERROR_WANT_CERTIFICATE_VERIFY_MESSAGE; + case SSL_ERROR_HANDOFF: + return SSL_ERROR_HANDOFF_MESSAGE; + case SSL_ERROR_HANDBACK: + return SSL_ERROR_HANDBACK_MESSAGE; + default: + ENVOY_BUG(false, "Unknown BoringSSL error had occurred"); + return SSL_ERROR_UNKNOWN_ERROR_MESSAGE; + } +} + } // namespace Tls } // namespace TransportSockets } // namespace Extensions diff --git a/source/extensions/transport_sockets/tls/utility.h b/source/extensions/transport_sockets/tls/utility.h index f4f06eb4cb23..68c82de170fe 100644 --- a/source/extensions/transport_sockets/tls/utility.h +++ b/source/extensions/transport_sockets/tls/utility.h @@ -15,6 +15,29 @@ namespace TransportSockets { namespace Tls { namespace Utility { +static constexpr absl::string_view SSL_ERROR_NONE_MESSAGE = "NONE"; +static constexpr absl::string_view SSL_ERROR_SSL_MESSAGE = "SSL"; +static constexpr absl::string_view SSL_ERROR_WANT_READ_MESSAGE = "WANT_READ"; +static constexpr absl::string_view SSL_ERROR_WANT_WRITE_MESSAGE = "WANT_WRITE"; +static constexpr absl::string_view SSL_ERROR_WANT_X509_LOOPUP_MESSAGE = "WANT_X509_LOOKUP"; +static constexpr absl::string_view SSL_ERROR_SYSCALL_MESSAGE = "SYSCALL"; +static constexpr absl::string_view SSL_ERROR_ZERO_RETURN_MESSAGE = "ZERO_RETURN"; +static constexpr absl::string_view SSL_ERROR_WANT_CONNECT_MESSAGE = "WANT_CONNECT"; +static constexpr absl::string_view SSL_ERROR_WANT_ACCEPT_MESSAGE = "WANT_ACCEPT"; +static constexpr absl::string_view SSL_ERROR_WANT_CHANNEL_ID_LOOKUP_MESSAGE = + "WANT_CHANNEL_ID_LOOKUP"; +static constexpr absl::string_view SSL_ERROR_PENDING_SESSION_MESSAGE = "PENDING_SESSION"; +static constexpr absl::string_view SSL_ERROR_PENDING_CERTIFICATE_MESSAGE = "PENDING_CERTIFICATE"; +static constexpr absl::string_view SSL_ERROR_WANT_PRIVATE_KEY_OPERATION_MESSAGE = + "WANT_PRIVATE_KEY_OPERATION"; +static constexpr absl::string_view SSL_ERROR_PENDING_TICKET_MESSAGE = "PENDING_TICKET"; +static constexpr absl::string_view SSL_ERROR_EARLY_DATA_REJECTED_MESSAGE = "EARLY_DATA_REJECTED"; +static constexpr absl::string_view SSL_ERROR_WANT_CERTIFICATE_VERIFY_MESSAGE = + "WANT_CERTIFICATE_VERIFY"; +static constexpr absl::string_view SSL_ERROR_HANDOFF_MESSAGE = "HANDOFF"; +static constexpr absl::string_view SSL_ERROR_HANDBACK_MESSAGE = "HANDBACK"; +static constexpr absl::string_view SSL_ERROR_UNKNOWN_ERROR_MESSAGE = "UNKNOWN_ERROR"; + /** * Retrieves the serial number of a certificate. * @param cert the certificate @@ -89,6 +112,13 @@ SystemTime getExpirationTime(const X509& cert); */ absl::optional getLastCryptoError(); +/** + * Returns error string corresponding error code derived from OpenSSL. + * @param err error code + * @return string message corresponding error code. + */ +absl::string_view getErrorDescription(int err); + } // namespace Utility } // namespace Tls } // namespace TransportSockets diff --git a/source/extensions/upstreams/http/config.cc b/source/extensions/upstreams/http/config.cc index 6d8a8090c738..2d8cbd447c4f 100644 --- a/source/extensions/upstreams/http/config.cc +++ b/source/extensions/upstreams/http/config.cc @@ -43,33 +43,23 @@ getHttp2Options(const envoy::extensions::upstreams::http::v3::HttpProtocolOption } // namespace -uint64_t -ProtocolOptionsConfigImpl::parseFeatures(const envoy::config::cluster::v3::Cluster& config, - std::shared_ptr options) { +uint64_t ProtocolOptionsConfigImpl::parseFeatures(const envoy::config::cluster::v3::Cluster& config, + const ProtocolOptionsConfigImpl& options) { uint64_t features = 0; - if (options) { - if (options->use_http2_) { - features |= Upstream::ClusterInfo::Features::HTTP2; - } - if (options->use_downstream_protocol_) { - features |= Upstream::ClusterInfo::Features::USE_DOWNSTREAM_PROTOCOL; - } - } else { - if (config.has_http2_protocol_options()) { - features |= Upstream::ClusterInfo::Features::HTTP2; - } - if (config.protocol_selection() == - envoy::config::cluster::v3::Cluster::USE_DOWNSTREAM_PROTOCOL) { - features |= Upstream::ClusterInfo::Features::USE_DOWNSTREAM_PROTOCOL; - } + if (options.use_http2_) { + features |= Upstream::ClusterInfo::Features::HTTP2; } - if (config.close_connections_on_host_health_failure()) { - features |= Upstream::ClusterInfo::Features::CLOSE_CONNECTIONS_ON_HOST_HEALTH_FAILURE; + if (options.use_downstream_protocol_) { + features |= Upstream::ClusterInfo::Features::USE_DOWNSTREAM_PROTOCOL; } - if (options->use_alpn_) { + if (options.use_alpn_) { features |= Upstream::ClusterInfo::Features::USE_ALPN; } + + if (config.close_connections_on_host_health_failure()) { + features |= Upstream::ClusterInfo::Features::CLOSE_CONNECTIONS_ON_HOST_HEALTH_FAILURE; + } return features; } diff --git a/source/extensions/upstreams/http/config.h b/source/extensions/upstreams/http/config.h index cb4a1cb0dca9..6d84667b8caa 100644 --- a/source/extensions/upstreams/http/config.h +++ b/source/extensions/upstreams/http/config.h @@ -37,7 +37,7 @@ class ProtocolOptionsConfigImpl : public Upstream::ProtocolOptionsConfig { // Given the supplied cluster config, and protocol options configuration, // returns a unit64_t representing the enabled Upstream::ClusterInfo::Features. static uint64_t parseFeatures(const envoy::config::cluster::v3::Cluster& config, - std::shared_ptr options); + const ProtocolOptionsConfigImpl& options); const Envoy::Http::Http1Settings http1_settings_; const envoy::config::core::v3::Http2ProtocolOptions http2_options_; diff --git a/source/server/admin/admin.h b/source/server/admin/admin.h index c766978c9638..3ca7be09f1af 100644 --- a/source/server/admin/admin.h +++ b/source/server/admin/admin.h @@ -256,14 +256,6 @@ class AdminImpl : public Admin, struct NullThreadLocalOverloadState : public ThreadLocalOverloadState { NullThreadLocalOverloadState(Event::Dispatcher& dispatcher) : dispatcher_(dispatcher) {} const OverloadActionState& getState(const std::string&) override { return inactive_; } - Event::TimerPtr createScaledTimer(OverloadTimerType, Event::TimerCb callback) override { - return dispatcher_.createTimer(callback); - } - Event::TimerPtr createScaledTimer(Event::ScaledTimerMinimum, - Event::TimerCb callback) override { - return dispatcher_.createTimer(callback); - } - Event::Dispatcher& dispatcher_; const OverloadActionState inactive_ = OverloadActionState::inactive(); }; @@ -281,6 +273,8 @@ class AdminImpl : public Admin, return tls_->getTyped(); } + Event::ScaledRangeTimerManagerFactory scaledTimerFactory() override { return nullptr; } + bool registerForAction(const std::string&, Event::Dispatcher&, OverloadActionCb) override { // This method shouldn't be called by the admin listener NOT_REACHED_GCOVR_EXCL_LINE; diff --git a/source/server/config_validation/api.cc b/source/server/config_validation/api.cc index fb6152b9053d..bcf28c69172b 100644 --- a/source/server/config_validation/api.cc +++ b/source/server/config_validation/api.cc @@ -17,6 +17,12 @@ Event::DispatcherPtr ValidationImpl::allocateDispatcher(const std::string& name) return Event::DispatcherPtr{new Event::ValidationDispatcher(name, *this, time_system_)}; } +Event::DispatcherPtr +ValidationImpl::allocateDispatcher(const std::string&, + const Event::ScaledRangeTimerManagerFactory&) { + NOT_REACHED_GCOVR_EXCL_LINE; +} + Event::DispatcherPtr ValidationImpl::allocateDispatcher(const std::string&, Buffer::WatermarkFactoryPtr&&) { NOT_REACHED_GCOVR_EXCL_LINE; diff --git a/source/server/config_validation/api.h b/source/server/config_validation/api.h index 21c0dca05820..2d44022c191a 100644 --- a/source/server/config_validation/api.h +++ b/source/server/config_validation/api.h @@ -20,6 +20,8 @@ class ValidationImpl : public Impl { Random::RandomGenerator& random_generator); Event::DispatcherPtr allocateDispatcher(const std::string& name) override; + Event::DispatcherPtr allocateDispatcher(const std::string& name, + const Event::ScaledRangeTimerManagerFactory&) override; Event::DispatcherPtr allocateDispatcher(const std::string& name, Buffer::WatermarkFactoryPtr&& watermark_factory) override; diff --git a/source/server/config_validation/server.cc b/source/server/config_validation/server.cc index e4130e383ff2..946c0253b4f5 100644 --- a/source/server/config_validation/server.cc +++ b/source/server/config_validation/server.cc @@ -85,8 +85,8 @@ void ValidationInstance::initialize(const Options& options, bootstrap.mutable_node()->set_hidden_envoy_deprecated_build_version(VersionInfo::version()); local_info_ = std::make_unique( - stats().symbolTable(), bootstrap.node(), local_address, options.serviceZone(), - options.serviceClusterName(), options.serviceNodeName()); + stats().symbolTable(), bootstrap.node(), bootstrap.node_context_params(), local_address, + options.serviceZone(), options.serviceClusterName(), options.serviceNodeName()); Configuration::InitialImpl initial_config(bootstrap, options); overload_manager_ = std::make_unique( diff --git a/source/server/connection_handler_impl.cc b/source/server/connection_handler_impl.cc index 98191c58566a..6ce13186e84d 100644 --- a/source/server/connection_handler_impl.cc +++ b/source/server/connection_handler_impl.cc @@ -12,6 +12,7 @@ #include "common/event/deferred_task.h" #include "common/network/connection_impl.h" #include "common/network/utility.h" +#include "common/runtime/runtime_features.h" #include "common/stats/timespan_impl.h" #include "extensions/transport_sockets/well_known_names.h" @@ -153,7 +154,7 @@ void ConnectionHandlerImpl::enableListeners() { } } -void ConnectionHandlerImpl::setListenerRejectFraction(float reject_fraction) { +void ConnectionHandlerImpl::setListenerRejectFraction(UnitFloat reject_fraction) { listener_reject_fraction_ = reject_fraction; for (auto& listener : listeners_) { listener.second.listener_->listener()->setRejectFraction(reject_fraction); @@ -589,7 +590,9 @@ ConnectionHandlerImpl::ActiveTcpConnection::ActiveTcpConnection( active_connections_.listener_.stats_.downstream_cx_length_ms_, time_source)) { // We just universally set no delay on connections. Theoretically we might at some point want // to make this configurable. - connection_->noDelay(true); + if (!Runtime::runtimeFeatureEnabled("envoy.reloadable_features.always_nodelay")) { + connection_->noDelay(true); + } auto& listener = active_connections_.listener_; listener.stats_.downstream_cx_total_.inc(); listener.stats_.downstream_cx_active_.inc(); diff --git a/source/server/connection_handler_impl.h b/source/server/connection_handler_impl.h index e616294e53b8..d5524d97d74e 100644 --- a/source/server/connection_handler_impl.h +++ b/source/server/connection_handler_impl.h @@ -83,7 +83,7 @@ class ConnectionHandlerImpl : public Network::ConnectionHandler, void stopListeners() override; void disableListeners() override; void enableListeners() override; - void setListenerRejectFraction(float reject_fraction) override; + void setListenerRejectFraction(UnitFloat reject_fraction) override; const std::string& statPrefix() const override { return per_handler_stat_prefix_; } /** @@ -361,7 +361,7 @@ class ConnectionHandlerImpl : public Network::ConnectionHandler, std::list> listeners_; std::atomic num_handler_connections_{}; bool disable_listeners_; - float listener_reject_fraction_{0}; + UnitFloat listener_reject_fraction_{UnitFloat::min()}; }; class ActiveUdpListenerBase : public ConnectionHandlerImpl::ActiveListenerImplBase, diff --git a/source/server/lds_api.cc b/source/server/lds_api.cc index 23198c8e3b9c..4daa03077b13 100644 --- a/source/server/lds_api.cc +++ b/source/server/lds_api.cc @@ -31,11 +31,10 @@ LdsApiImpl::LdsApiImpl(const envoy::config::core::v3::ConfigSource& lds_config, const auto resource_name = getResourceName(); if (lds_resources_locator == nullptr) { subscription_ = cm.subscriptionFactory().subscriptionFromConfigSource( - lds_config, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_); + lds_config, Grpc::Common::typeUrl(resource_name), *scope_, *this, resource_decoder_, false); } else { subscription_ = cm.subscriptionFactory().collectionSubscriptionFromUrl( - *lds_resources_locator, lds_config, Grpc::Common::typeUrl(resource_name), *scope_, *this, - resource_decoder_); + *lds_resources_locator, lds_config, resource_name, *scope_, *this, resource_decoder_); } init_manager.add(init_target_); } diff --git a/source/server/listener_manager_impl.cc b/source/server/listener_manager_impl.cc index eb588456a9b9..a60451c6e67d 100644 --- a/source/server/listener_manager_impl.cc +++ b/source/server/listener_manager_impl.cc @@ -1024,7 +1024,7 @@ Network::DrainableFilterChainSharedPtr ListenerFilterChainFactoryBuilder::buildF std::vector server_names(filter_chain.filter_chain_match().server_names().begin(), filter_chain.filter_chain_match().server_names().end()); - auto filter_chain_res = std::make_unique( + auto filter_chain_res = std::make_shared( config_factory.createTransportSocketFactory(*message, factory_context_, std::move(server_names)), listener_component_factory_.createNetworkFilterFactoryList(filter_chain.filters(), diff --git a/source/server/overload_manager_impl.cc b/source/server/overload_manager_impl.cc index 07872b98442b..5c9b7266c453 100644 --- a/source/server/overload_manager_impl.cc +++ b/source/server/overload_manager_impl.cc @@ -26,14 +26,9 @@ namespace Server { */ class ThreadLocalOverloadStateImpl : public ThreadLocalOverloadState { public: - ThreadLocalOverloadStateImpl( - Event::ScaledRangeTimerManagerPtr scaled_timer_manager, - const NamedOverloadActionSymbolTable& action_symbol_table, - const absl::flat_hash_map& timer_minimums) - : action_symbol_table_(action_symbol_table), timer_minimums_(timer_minimums), - actions_(action_symbol_table.size(), OverloadActionState(UnitFloat::min())), - scaled_timer_action_(action_symbol_table.lookup(OverloadActionNames::get().ReduceTimeouts)), - scaled_timer_manager_(std::move(scaled_timer_manager)) {} + explicit ThreadLocalOverloadStateImpl(const NamedOverloadActionSymbolTable& action_symbol_table) + : action_symbol_table_(action_symbol_table), + actions_(action_symbol_table.size(), OverloadActionState(UnitFloat::min())) {} const OverloadActionState& getState(const std::string& action) override { if (const auto symbol = action_symbol_table_.lookup(action); symbol != absl::nullopt) { @@ -42,35 +37,14 @@ class ThreadLocalOverloadStateImpl : public ThreadLocalOverloadState { return always_inactive_; } - Event::TimerPtr createScaledTimer(OverloadTimerType timer_type, - Event::TimerCb callback) override { - auto minimum_it = timer_minimums_.find(timer_type); - const Event::ScaledTimerMinimum minimum = - minimum_it != timer_minimums_.end() - ? minimum_it->second - : Event::ScaledTimerMinimum(Event::ScaledMinimum(UnitFloat::max())); - return scaled_timer_manager_->createTimer(minimum, std::move(callback)); - } - - Event::TimerPtr createScaledTimer(Event::ScaledTimerMinimum minimum, - Event::TimerCb callback) override { - return scaled_timer_manager_->createTimer(minimum, std::move(callback)); - } - void setState(NamedOverloadActionSymbolTable::Symbol action, OverloadActionState state) { actions_[action.index()] = state; - if (scaled_timer_action_.has_value() && scaled_timer_action_.value() == action) { - scaled_timer_manager_->setScaleFactor(1 - state.value()); - } } private: static const OverloadActionState always_inactive_; const NamedOverloadActionSymbolTable& action_symbol_table_; - const absl::flat_hash_map& timer_minimums_; std::vector actions_; - absl::optional scaled_timer_action_; - const Event::ScaledRangeTimerManagerPtr scaled_timer_manager_; }; const OverloadActionState ThreadLocalOverloadStateImpl::always_inactive_{UnitFloat::min()}; @@ -86,6 +60,8 @@ class ThresholdTriggerImpl final : public OverloadAction::Trigger { const OverloadActionState state = actionState(); state_ = value >= threshold_ ? OverloadActionState::saturated() : OverloadActionState::inactive(); + // This is a floating point comparison, though state_ is always either + // saturated or inactive so there's no risk due to floating point precision. return state.value() != actionState().value(); } @@ -117,6 +93,9 @@ class ScaledTriggerImpl final : public OverloadAction::Trigger { state_ = OverloadActionState( UnitFloat((value - scaling_threshold_) / (saturated_threshold_ - scaling_threshold_))); } + // All values of state_ are produced via this same code path. Even if + // old_state and state_ should be approximately equal, there's no harm in + // signaling for a small change if they're not float::operator== equal. return state_.value() != old_state.value(); } @@ -141,31 +120,31 @@ Stats::Gauge& makeGauge(Stats::Scope& scope, absl::string_view a, absl::string_v return scope.gaugeFromStatName(stat_name.statName(), import_mode); } -OverloadTimerType parseTimerType( +Event::ScaledTimerType parseTimerType( envoy::config::overload::v3::ScaleTimersOverloadActionConfig::TimerType config_timer_type) { using Config = envoy::config::overload::v3::ScaleTimersOverloadActionConfig; switch (config_timer_type) { case Config::HTTP_DOWNSTREAM_CONNECTION_IDLE: - return OverloadTimerType::HttpDownstreamIdleConnectionTimeout; + return Event::ScaledTimerType::HttpDownstreamIdleConnectionTimeout; case Config::HTTP_DOWNSTREAM_STREAM_IDLE: - return OverloadTimerType::HttpDownstreamIdleStreamTimeout; + return Event::ScaledTimerType::HttpDownstreamIdleStreamTimeout; default: throw EnvoyException(fmt::format("Unknown timer type {}", config_timer_type)); } } -absl::flat_hash_map +Event::ScaledTimerTypeMap parseTimerMinimums(const ProtobufWkt::Any& typed_config, ProtobufMessage::ValidationVisitor& validation_visitor) { using Config = envoy::config::overload::v3::ScaleTimersOverloadActionConfig; const Config action_config = MessageUtil::anyConvertAndValidate(typed_config, validation_visitor); - absl::flat_hash_map timer_map; + Event::ScaledTimerTypeMap timer_map; for (const auto& scale_timer : action_config.timer_scale_factors()) { - const OverloadTimerType timer_type = parseTimerType(scale_timer.timer()); + const Event::ScaledTimerType timer_type = parseTimerType(scale_timer.timer()); const Event::ScaledTimerMinimum minimum = scale_timer.has_min_timeout() @@ -258,7 +237,7 @@ bool OverloadAction::updateResourcePressure(const std::string& name, double pres } const auto trigger_new_state = it->second->actionState(); active_gauge_.set(trigger_new_state.isSaturated() ? 1 : 0); - scale_percent_gauge_.set(trigger_new_state.value() * 100); + scale_percent_gauge_.set(trigger_new_state.value().value() * 100); { // Compute the new state as the maximum over all trigger states. @@ -315,7 +294,8 @@ OverloadManagerImpl::OverloadManagerImpl(Event::Dispatcher& dispatcher, Stats::S } if (name == OverloadActionNames::get().ReduceTimeouts) { - timer_minimums_ = parseTimerMinimums(action.typed_config(), validation_visitor); + timer_minimums_ = std::make_shared( + parseTimerMinimums(action.typed_config(), validation_visitor)); } else if (action.has_typed_config()) { throw EnvoyException(fmt::format( "Overload action \"{}\" has an unexpected value for the typed_config field", name)); @@ -338,9 +318,8 @@ void OverloadManagerImpl::start() { ASSERT(!started_); started_ = true; - tls_.set([this](Event::Dispatcher& dispatcher) { - return std::make_shared(createScaledRangeTimerManager(dispatcher), - action_symbol_table_, timer_minimums_); + tls_.set([this](Event::Dispatcher&) { + return std::make_shared(action_symbol_table_); }); if (resources_.empty()) { @@ -392,10 +371,25 @@ bool OverloadManagerImpl::registerForAction(const std::string& action, } ThreadLocalOverloadState& OverloadManagerImpl::getThreadLocalOverloadState() { return *tls_; } +Event::ScaledRangeTimerManagerFactory OverloadManagerImpl::scaledTimerFactory() { + return [this](Event::Dispatcher& dispatcher) { + auto manager = createScaledRangeTimerManager(dispatcher, timer_minimums_); + registerForAction(OverloadActionNames::get().ReduceTimeouts, dispatcher, + [manager = manager.get()](OverloadActionState scale_state) { + manager->setScaleFactor( + // The action state is 0 for no overload up to 1 for maximal overload, + // but the scale factor for timers is 1 for no scaling and 0 for maximal + // scaling, so invert the value to pass in (1-value). + scale_state.value().invert()); + }); + return manager; + }; +} -Event::ScaledRangeTimerManagerPtr -OverloadManagerImpl::createScaledRangeTimerManager(Event::Dispatcher& dispatcher) const { - return std::make_unique(dispatcher); +Event::ScaledRangeTimerManagerPtr OverloadManagerImpl::createScaledRangeTimerManager( + Event::Dispatcher& dispatcher, + const Event::ScaledTimerTypeMapConstSharedPtr& timer_minimums) const { + return std::make_unique(dispatcher, timer_minimums); } void OverloadManagerImpl::updateResourcePressure(const std::string& resource, double pressure, diff --git a/source/server/overload_manager_impl.h b/source/server/overload_manager_impl.h index b86162fb0224..6dcfff6d032b 100644 --- a/source/server/overload_manager_impl.h +++ b/source/server/overload_manager_impl.h @@ -15,6 +15,7 @@ #include "envoy/thread_local/thread_local.h" #include "common/common/logger.h" +#include "common/event/scaled_range_timer_manager_impl.h" #include "absl/container/node_hash_map.h" #include "absl/container/node_hash_set.h" @@ -114,6 +115,7 @@ class OverloadManagerImpl : Logger::Loggable, public OverloadM bool registerForAction(const std::string& action, Event::Dispatcher& dispatcher, OverloadActionCb callback) override; ThreadLocalOverloadState& getThreadLocalOverloadState() override; + Event::ScaledRangeTimerManagerFactory scaledTimerFactory() override; // Stop the overload manager timer and wait for any pending resource updates to complete. // After this returns, overload manager clients should not receive any more callbacks @@ -122,8 +124,9 @@ class OverloadManagerImpl : Logger::Loggable, public OverloadM protected: // Factory for timer managers. This allows test-only subclasses to inject a mock implementation. - virtual Event::ScaledRangeTimerManagerPtr - createScaledRangeTimerManager(Event::Dispatcher& dispatcher) const; + virtual Event::ScaledRangeTimerManagerPtr createScaledRangeTimerManager( + Event::Dispatcher& dispatcher, + const Event::ScaledTimerTypeMapConstSharedPtr& timer_minimums) const; private: using FlushEpochId = uint64_t; @@ -170,7 +173,7 @@ class OverloadManagerImpl : Logger::Loggable, public OverloadM absl::node_hash_map resources_; absl::node_hash_map actions_; - absl::flat_hash_map timer_minimums_; + Event::ScaledTimerTypeMapConstSharedPtr timer_minimums_; absl::flat_hash_map state_updates_to_flush_; diff --git a/source/server/server.cc b/source/server/server.cc index 26795a8b2aed..b7afdd253e25 100644 --- a/source/server/server.cc +++ b/source/server/server.cc @@ -141,7 +141,10 @@ InstanceImpl::~InstanceImpl() { ENVOY_LOG(debug, "destroyed listener manager"); } -Upstream::ClusterManager& InstanceImpl::clusterManager() { return *config_.clusterManager(); } +Upstream::ClusterManager& InstanceImpl::clusterManager() { + ASSERT(config_.clusterManager() != nullptr); + return *config_.clusterManager(); +} void InstanceImpl::drainListeners() { ENVOY_LOG(info, "closing and draining listeners"); @@ -353,10 +356,17 @@ void InstanceImpl::initialize(const Options& options, stats_store_.setHistogramSettings(Config::Utility::createHistogramSettings(bootstrap_)); const std::string server_stats_prefix = "server."; + const std::string server_compilation_settings_stats_prefix = "server.compilation_settings"; server_stats_ = std::make_unique( ServerStats{ALL_SERVER_STATS(POOL_COUNTER_PREFIX(stats_store_, server_stats_prefix), POOL_GAUGE_PREFIX(stats_store_, server_stats_prefix), POOL_HISTOGRAM_PREFIX(stats_store_, server_stats_prefix))}); + server_compilation_settings_stats_ = + std::make_unique( + CompilationSettings::ServerCompilationSettingsStats{ALL_SERVER_COMPILATION_SETTINGS_STATS( + POOL_COUNTER_PREFIX(stats_store_, server_compilation_settings_stats_prefix), + POOL_GAUGE_PREFIX(stats_store_, server_compilation_settings_stats_prefix), + POOL_HISTOGRAM_PREFIX(stats_store_, server_compilation_settings_stats_prefix))}); validation_context_.staticWarningValidationVisitor().setUnknownCounter( server_stats_->static_unknown_fields_); validation_context_.dynamicWarningValidationVisitor().setUnknownCounter( @@ -385,6 +395,9 @@ void InstanceImpl::initialize(const Options& options, } } server_stats_->version_.set(version_int); + if (VersionInfo::sslFipsCompliant()) { + server_compilation_settings_stats_->fips_mode_.set(1); + } bootstrap_.mutable_node()->set_hidden_envoy_deprecated_build_version(VersionInfo::version()); bootstrap_.mutable_node()->set_user_agent_name("envoy"); @@ -403,8 +416,8 @@ void InstanceImpl::initialize(const Options& options, } local_info_ = std::make_unique( - stats().symbolTable(), bootstrap_.node(), local_address, options.serviceZone(), - options.serviceClusterName(), options.serviceNodeName()); + stats().symbolTable(), bootstrap_.node(), bootstrap_.node_context_params(), local_address, + options.serviceZone(), options.serviceClusterName(), options.serviceNodeName()); Configuration::InitialImpl initial_config(bootstrap_, options); @@ -568,6 +581,11 @@ void InstanceImpl::initialize(const Options& options, stat_flush_timer_->enableTimer(stats_config.flushInterval()); } + // Now that we are initialized, notify the bootstrap extensions. + for (auto&& bootstrap_extension : bootstrap_extensions_) { + bootstrap_extension->onServerInitialized(); + } + // GuardDog (deadlock detection) object and thread setup before workers are // started and before our own run() loop runs. main_thread_guard_dog_ = std::make_unique( diff --git a/source/server/server.h b/source/server/server.h index cf4d24eadd56..cea2e6dff812 100644 --- a/source/server/server.h +++ b/source/server/server.h @@ -50,6 +50,18 @@ namespace Envoy { namespace Server { +namespace CompilationSettings { +/** + * All server compilation settings stats. @see stats_macros.h + */ +#define ALL_SERVER_COMPILATION_SETTINGS_STATS(COUNTER, GAUGE, HISTOGRAM) \ + GAUGE(fips_mode, NeverImport) + +struct ServerCompilationSettingsStats { + ALL_SERVER_COMPILATION_SETTINGS_STATS(GENERATE_COUNTER_STRUCT, GENERATE_GAUGE_STRUCT, + GENERATE_HISTOGRAM_STRUCT) +}; +} // namespace CompilationSettings /** * All server wide stats. @see stats_macros.h @@ -322,6 +334,8 @@ class InstanceImpl final : Logger::Loggable, time_t original_start_time_; Stats::StoreRoot& stats_store_; std::unique_ptr server_stats_; + std::unique_ptr + server_compilation_settings_stats_; Assert::ActionRegistrationPtr assert_action_registration_; Assert::ActionRegistrationPtr envoy_bug_action_registration_; ThreadLocal::Instance& thread_local_; diff --git a/source/server/worker_impl.cc b/source/server/worker_impl.cc index 760b7ca630bc..95c24175321c 100644 --- a/source/server/worker_impl.cc +++ b/source/server/worker_impl.cc @@ -16,7 +16,8 @@ namespace Server { WorkerPtr ProdWorkerFactory::createWorker(uint32_t index, OverloadManager& overload_manager, const std::string& worker_name) { - Event::DispatcherPtr dispatcher(api_.allocateDispatcher(worker_name)); + Event::DispatcherPtr dispatcher( + api_.allocateDispatcher(worker_name, overload_manager.scaledTimerFactory())); return std::make_unique(tls_, hooks_, std::move(dispatcher), std::make_unique(*dispatcher, index), overload_manager, api_); @@ -152,7 +153,7 @@ void WorkerImpl::stopAcceptingConnectionsCb(OverloadActionState state) { } void WorkerImpl::rejectIncomingConnectionsCb(OverloadActionState state) { - handler_->setListenerRejectFraction(static_cast(state.value())); + handler_->setListenerRejectFraction(state.value()); } } // namespace Server diff --git a/test/README.md b/test/README.md index 2746efe98c8d..85f12625902f 100644 --- a/test/README.md +++ b/test/README.md @@ -10,7 +10,7 @@ various classes, macros, and matchers that Envoy uses from those frameworks. Envoy contains an integration testing framework, for testing downstream-Envoy-upstream communication. -[See the framework's README for more information.](https://github.com/envoyproxy/envoy/blob/master/test/integration/README.md) +[See the framework's README for more information.](https://github.com/envoyproxy/envoy/blob/main/test/integration/README.md) ## Custom matchers @@ -93,7 +93,7 @@ EXPECT_THAT(response->headers(), IsSupersetOfHeaders(required_headers)); ## Controlling time in tests In Envoy production code, time and timers are managed via -[`Event::TimeSystem`](https://github.com/envoyproxy/envoy/blob/master/include/envoy/event/timer.h), +[`Event::TimeSystem`](https://github.com/envoyproxy/envoy/blob/main/include/envoy/event/timer.h), which provides a mechanism for querying the time and setting up time-based callbacks. Bypassing this abstraction in Envoy code is flagged as a format violation in CI. @@ -127,7 +127,7 @@ Envoy uses [Google Benchmark](https://github.com/google/benchmark/) for microbenchmarks. There are custom bazel rules, `envoy_cc_benchmark_binary` and `envoy_benchmark_test`, to execute them locally and in CI environments respectively. `envoy_benchmark_test` rules call the benchmark binary from a -[script](https://github.com/envoyproxy/envoy/blob/master/bazel/test_for_benchmark_wrapper.sh) +[script](https://github.com/envoyproxy/envoy/blob/main/bazel/test_for_benchmark_wrapper.sh) which runs the benchmark with a minimal number of iterations and skipping expensive benchmarks to quickly verify that the binary is able to run to completion. In order to collect meaningful bechmarks, `bazel run -c opt` the @@ -135,4 +135,4 @@ benchmark binary target on a quiescent machine. If you would like to detect when your benchmark test is running under the wrapper, call -[`Envoy::benchmark::skipExpensiveBechmarks()`](https://github.com/envoyproxy/envoy/blob/master/test/benchmark/main.h). +[`Envoy::benchmark::skipExpensiveBechmarks()`](https://github.com/envoyproxy/envoy/blob/main/test/benchmark/main.h). diff --git a/test/common/access_log/access_log_impl_test.cc b/test/common/access_log/access_log_impl_test.cc index 1e6eb05c0cea..ce4d3909af73 100644 --- a/test/common/access_log/access_log_impl_test.cc +++ b/test/common/access_log/access_log_impl_test.cc @@ -141,6 +141,40 @@ name: accesslog output_); } +TEST_F(AccessLogImplTest, HeadersBytes) { + const std::string yaml = R"EOF( +name: accesslog +typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + path: /dev/null + log_format: + text_format_source: + inline_string: "%REQUEST_HEADERS_BYTES% %RESPONSE_HEADERS_BYTES% %RESPONSE_TRAILERS_BYTES%" + )EOF"; + + InstanceSharedPtr log = AccessLogFactory::fromProto(parseAccessLogFromV3Yaml(yaml), context_); + + EXPECT_CALL(*file_, write(_)); + request_headers_.addCopy("request_header_key", "request_header_val"); + response_headers_.addCopy("response_header_key", "response_header_val"); + response_trailers_.addCopy("response_trailer_key", "response_trailer_val"); + + // request headers: + // :method: GET + // :path: / + // request_header_key: request_header_val + // + // response headers: + // response_header_key: response_header_val + // + // response trailers: + // response_trailer_key: response_trailer_val + + log->log(&request_headers_, &response_headers_, &response_trailers_, stream_info_); + + EXPECT_EQ(output_, "52 38 40"); +} + TEST_F(AccessLogImplTest, EnvoyUpstreamServiceTime) { const std::string yaml = R"EOF( name: accesslog diff --git a/test/common/common/BUILD b/test/common/common/BUILD index f0e0b68986ca..1b52c8229856 100644 --- a/test/common/common/BUILD +++ b/test/common/common/BUILD @@ -196,6 +196,7 @@ envoy_cc_test( name = "random_generator_test", srcs = ["random_generator_test.cc"], deps = [ + "//source/common/common:interval_value", "//source/common/common:random_generator_lib", "//test/mocks/runtime:runtime_mocks", "//test/test_common:environment_lib", @@ -361,3 +362,15 @@ envoy_cc_test( srcs = ["interval_value_test.cc"], deps = ["//source/common/common:interval_value"], ) + +envoy_cc_test( + name = "scope_tracker_test", + srcs = ["scope_tracker_test.cc"], + deps = [ + "//source/common/api:api_lib", + "//source/common/common:scope_tracker", + "//source/common/event:dispatcher_lib", + "//test/mocks:common_lib", + "//test/test_common:utility_lib", + ], +) diff --git a/test/common/common/random_generator_test.cc b/test/common/common/random_generator_test.cc index 71bf25624740..7658080b116b 100644 --- a/test/common/common/random_generator_test.cc +++ b/test/common/common/random_generator_test.cc @@ -1,5 +1,6 @@ #include +#include "common/common/interval_value.h" #include "common/common/random_generator.h" #include "gmock/gmock.h" @@ -70,15 +71,13 @@ TEST(UUID, SanityCheckOfUniqueness) { TEST(Random, Bernoilli) { Random::RandomGeneratorImpl random; - EXPECT_FALSE(random.bernoulli(0)); - EXPECT_FALSE(random.bernoulli(-1)); - EXPECT_TRUE(random.bernoulli(1)); - EXPECT_TRUE(random.bernoulli(2)); + EXPECT_FALSE(random.bernoulli(UnitFloat(0.0f))); + EXPECT_TRUE(random.bernoulli(UnitFloat(1.0f))); int true_count = 0; static const auto num_rolls = 100000; for (size_t i = 0; i < num_rolls; ++i) { - if (random.bernoulli(0.4)) { + if (random.bernoulli(UnitFloat(0.4f))) { ++true_count; } } diff --git a/test/common/common/scope_tracker_test.cc b/test/common/common/scope_tracker_test.cc new file mode 100644 index 000000000000..1c5451660f04 --- /dev/null +++ b/test/common/common/scope_tracker_test.cc @@ -0,0 +1,37 @@ +#include + +#include "common/api/api_impl.h" +#include "common/common/scope_tracker.h" +#include "common/event/dispatcher_impl.h" + +#include "test/mocks/common.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace { + +using testing::_; + +TEST(ScopeTrackerScopeStateTest, ShouldManageTrackedObjectOnDispatcherStack) { + Api::ApiPtr api(Api::createApiForTest()); + Event::DispatcherPtr dispatcher(api->allocateDispatcher("test_thread")); + MockScopedTrackedObject tracked_object; + { + ScopeTrackerScopeState scope(&tracked_object, *dispatcher); + // Check that the tracked_object is on the tracked object stack + dispatcher->popTrackedObject(&tracked_object); + + // Restore it to the top, it should be removed in the dtor of scope. + dispatcher->pushTrackedObject(&tracked_object); + } + + // Check nothing is tracked now. + EXPECT_CALL(tracked_object, dumpState(_, _)).Times(0); + static_cast(dispatcher.get())->onFatalError(std::cerr); +} + +} // namespace +} // namespace Envoy diff --git a/test/common/common/utility_test.cc b/test/common/common/utility_test.cc index 6f4f8a2a628b..48b82909bfce 100644 --- a/test/common/common/utility_test.cc +++ b/test/common/common/utility_test.cc @@ -20,6 +20,8 @@ #include "gtest/gtest.h" using testing::ContainerEq; +using testing::ElementsAre; +using testing::WhenSorted; #ifdef WIN32 using testing::HasSubstr; using testing::Not; @@ -977,4 +979,27 @@ TEST(ErrorDetailsTest, WindowsFormatMessage) { } #endif +TEST(SetUtil, All) { + { + absl::flat_hash_set result; + SetUtil::setDifference({1, 2, 3}, {1, 3}, result); + EXPECT_THAT(result, WhenSorted(ElementsAre(2))); + } + { + absl::flat_hash_set result; + SetUtil::setDifference({1, 2, 3}, {4, 5}, result); + EXPECT_THAT(result, WhenSorted(ElementsAre(1, 2, 3))); + } + { + absl::flat_hash_set result; + SetUtil::setDifference({}, {4, 5}, result); + EXPECT_THAT(result, WhenSorted(ElementsAre())); + } + { + absl::flat_hash_set result; + SetUtil::setDifference({1, 2, 3}, {}, result); + EXPECT_THAT(result, WhenSorted(ElementsAre(1, 2, 3))); + } +} + } // namespace Envoy diff --git a/test/common/config/BUILD b/test/common/config/BUILD index 80a2bfa5b6d5..56b8e4b5ac98 100644 --- a/test/common/config/BUILD +++ b/test/common/config/BUILD @@ -229,6 +229,7 @@ envoy_cc_test_library( hdrs = ["delta_subscription_test_harness.h"], deps = [ ":subscription_test_harness", + "//source/common/common:utility_lib", "//source/common/config:new_grpc_mux_lib", "//source/common/config:version_converter_lib", "//source/common/grpc:common_lib", @@ -438,6 +439,16 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "context_provider_impl_test", + srcs = ["context_provider_impl_test.cc"], + deps = [ + ":xds_test_utility_lib", + "//source/common/config:context_provider_lib", + "//test/test_common:utility_lib", + ], +) + envoy_cc_test( name = "datasource_test", srcs = ["datasource_test.cc"], diff --git a/test/common/config/context_provider_impl_test.cc b/test/common/config/context_provider_impl_test.cc new file mode 100644 index 000000000000..2947f91765aa --- /dev/null +++ b/test/common/config/context_provider_impl_test.cc @@ -0,0 +1,42 @@ +#include "common/config/context_provider_impl.h" + +#include "test/common/config/xds_test_utility.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +using ::testing::Pair; + +namespace Envoy { +namespace Config { +namespace { + +TEST(ContextProviderTest, NodeContext) { + envoy::config::core::v3::Node node; + TestUtility::loadFromYaml(R"EOF( + id: some_id + cluster: some_cluster + user_agent_name: xds_client + user_agent_version: 1.2.3 + locality: + region: some_region + metadata: + foo: true + )EOF", + node); + const std::vector params_vec{ + "id", + "cluster", + "user_agent_name", + }; + Protobuf::RepeatedPtrField node_context_params{params_vec.cbegin(), + params_vec.cend()}; + ContextProviderImpl context_provider(node, node_context_params); + EXPECT_CONTEXT_PARAMS(context_provider.nodeContext(), Pair("xds.node.cluster", "some_cluster"), + Pair("xds.node.id", "some_id"), + Pair("xds.node.user_agent_name", "xds_client")); +} + +} // namespace +} // namespace Config +} // namespace Envoy diff --git a/test/common/config/delta_subscription_impl_test.cc b/test/common/config/delta_subscription_impl_test.cc index 9633eb08d34d..f0256bfaaca5 100644 --- a/test/common/config/delta_subscription_impl_test.cc +++ b/test/common/config/delta_subscription_impl_test.cc @@ -149,7 +149,7 @@ TEST(DeltaSubscriptionImplFixturelessTest, NoGrpcStream) { GrpcSubscriptionImplPtr subscription = std::make_unique( xds_context, callbacks, resource_decoder, stats, Config::TypeUrl::get().ClusterLoadAssignment, - dispatcher, std::chrono::milliseconds(12345), false); + dispatcher, std::chrono::milliseconds(12345), false, false); EXPECT_CALL(*async_client, startRaw(_, _, _, _)).WillOnce(Return(nullptr)); diff --git a/test/common/config/delta_subscription_test_harness.h b/test/common/config/delta_subscription_test_harness.h index b05d43ae87db..2e9766c1c196 100644 --- a/test/common/config/delta_subscription_test_harness.h +++ b/test/common/config/delta_subscription_test_harness.h @@ -45,9 +45,10 @@ class DeltaSubscriptionTestHarness : public SubscriptionTestHarness { std::unique_ptr(async_client_), dispatcher_, *method_descriptor_, envoy::config::core::v3::ApiVersion::AUTO, random_, stats_store_, rate_limit_settings_, local_info_); - subscription_ = std::make_unique( - xds_context_, callbacks_, resource_decoder_, stats_, - Config::TypeUrl::get().ClusterLoadAssignment, dispatcher_, init_fetch_timeout, false); + subscription_ = + std::make_unique(xds_context_, callbacks_, resource_decoder_, stats_, + Config::TypeUrl::get().ClusterLoadAssignment, + dispatcher_, init_fetch_timeout, false, false); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); } @@ -77,7 +78,7 @@ class DeltaSubscriptionTestHarness : public SubscriptionTestHarness { subscription_started_ = true; last_cluster_names_ = cluster_names; expectSendMessage(last_cluster_names_, ""); - subscription_->start(cluster_names); + subscription_->start(flattenResources(cluster_names)); } void expectSendMessage(const std::set& cluster_names, const std::string& version, @@ -172,7 +173,7 @@ class DeltaSubscriptionTestHarness : public SubscriptionTestHarness { std::inserter(unsub, unsub.begin())); expectSendMessage(sub, unsub, Grpc::Status::WellKnownGrpcStatus::Ok, "", {}); - subscription_->updateResourceInterest(cluster_names); + subscription_->updateResourceInterest(flattenResources(cluster_names)); last_cluster_names_ = cluster_names; } diff --git a/test/common/config/filesystem_subscription_test_harness.h b/test/common/config/filesystem_subscription_test_harness.h index d19da3bbed6f..9c372725207e 100644 --- a/test/common/config/filesystem_subscription_test_harness.h +++ b/test/common/config/filesystem_subscription_test_harness.h @@ -54,11 +54,11 @@ class FilesystemSubscriptionTestHarness : public SubscriptionTestHarness { void startSubscription(const std::set& cluster_names) override { std::ifstream config_file(path_); file_at_start_ = config_file.good(); - subscription_.start(cluster_names); + subscription_.start(flattenResources(cluster_names)); } void updateResourceInterest(const std::set& cluster_names) override { - subscription_.updateResourceInterest(cluster_names); + subscription_.updateResourceInterest(flattenResources(cluster_names)); } void updateFile(const std::string& json, bool run_dispatcher = true) { diff --git a/test/common/config/grpc_subscription_test_harness.h b/test/common/config/grpc_subscription_test_harness.h index 929f91619a10..b500c8aaf1c1 100644 --- a/test/common/config/grpc_subscription_test_harness.h +++ b/test/common/config/grpc_subscription_test_harness.h @@ -55,7 +55,7 @@ class GrpcSubscriptionTestHarness : public SubscriptionTestHarness { rate_limit_settings_, true); subscription_ = std::make_unique( mux_, callbacks_, resource_decoder_, stats_, Config::TypeUrl::get().ClusterLoadAssignment, - dispatcher_, init_fetch_timeout, false); + dispatcher_, init_fetch_timeout, false, false); } ~GrpcSubscriptionTestHarness() override { @@ -98,7 +98,7 @@ class GrpcSubscriptionTestHarness : public SubscriptionTestHarness { EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); last_cluster_names_ = cluster_names; expectSendMessage(last_cluster_names_, "", true); - subscription_->start(cluster_names); + subscription_->start(flattenResources(cluster_names)); } void deliverConfigUpdate(const std::vector& cluster_names, @@ -154,7 +154,7 @@ class GrpcSubscriptionTestHarness : public SubscriptionTestHarness { } expectSendMessage(both, version_); expectSendMessage(cluster_names, version_); - subscription_->updateResourceInterest(cluster_names); + subscription_->updateResourceInterest(flattenResources(cluster_names)); last_cluster_names_ = cluster_names; } diff --git a/test/common/config/http_subscription_test_harness.h b/test/common/config/http_subscription_test_harness.h index 91ab6905a8b8..cdb0266c5a92 100644 --- a/test/common/config/http_subscription_test_harness.h +++ b/test/common/config/http_subscription_test_harness.h @@ -111,13 +111,13 @@ class HttpSubscriptionTestHarness : public SubscriptionTestHarness { version_ = ""; cluster_names_ = cluster_names; expectSendMessage(cluster_names, ""); - subscription_->start(cluster_names); + subscription_->start(flattenResources(cluster_names)); } void updateResourceInterest(const std::set& cluster_names) override { cluster_names_ = cluster_names; expectSendMessage(cluster_names, version_); - subscription_->updateResourceInterest(cluster_names); + subscription_->updateResourceInterest(flattenResources(cluster_names)); timer_cb_(); } diff --git a/test/common/config/new_grpc_mux_impl_test.cc b/test/common/config/new_grpc_mux_impl_test.cc index f143530f5ae9..91dd3a0be8d0 100644 --- a/test/common/config/new_grpc_mux_impl_test.cc +++ b/test/common/config/new_grpc_mux_impl_test.cc @@ -32,6 +32,7 @@ using testing::_; using testing::Invoke; using testing::NiceMock; using testing::Return; +using testing::ReturnRef; namespace Envoy { namespace Config { @@ -267,6 +268,44 @@ TEST_F(NewGrpcMuxImplTest, V2ResourceResponseV3ResourceWatch) { } } +// Validate basic gRPC mux subscriptions to xdstp:// glob collections. +TEST_F(NewGrpcMuxImplTest, XdsTpGlobCollection) { + setup(); + + const std::string& type_url = Config::TypeUrl::get().ClusterLoadAssignment; + MockContextProvider context_provider; + EXPECT_CALL(local_info_, contextProvider()).WillOnce(ReturnRef(context_provider)); + xds::core::v3::ContextParams context_params; + EXPECT_CALL(context_provider, nodeContext()).WillOnce(ReturnRef(context_params)); + // We verify that the gRPC mux normalizes the context parameter order below. + auto watch = grpc_mux_->addWatch( + type_url, + {"xdstp://foo/envoy.config.endpoint.v3.ClusterLoadAssignment/bar/*?thing=some&some=thing"}, + callbacks_, resource_decoder_, true); + + EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(Return(&async_stream_)); + grpc_mux_->start(); + + auto response = std::make_unique(); + response->set_type_url(type_url); + response->set_system_version_info("1"); + + envoy::config::endpoint::v3::ClusterLoadAssignment load_assignment; + load_assignment.set_cluster_name("ignore"); + auto* resource = response->add_resources(); + resource->set_name( + "xdstp://foo/envoy.config.endpoint.v3.ClusterLoadAssignment/bar/a?some=thing&thing=some"); + resource->mutable_resource()->PackFrom(load_assignment); + EXPECT_CALL(callbacks_, onConfigUpdate(_, _, "1")) + .WillOnce(Invoke([&load_assignment](const std::vector& added_resources, + const Protobuf::RepeatedPtrField&, + const std::string&) { + EXPECT_EQ(1, added_resources.size()); + EXPECT_TRUE(TestUtility::protoEqual(added_resources[0].get().resource(), load_assignment)); + })); + grpc_mux_->onDiscoveryResponse(std::move(response), control_plane_stats_); +} + } // namespace } // namespace Config } // namespace Envoy diff --git a/test/common/config/subscription_factory_impl_test.cc b/test/common/config/subscription_factory_impl_test.cc index efa98bfafb12..0687e8aa50ae 100644 --- a/test/common/config/subscription_factory_impl_test.cc +++ b/test/common/config/subscription_factory_impl_test.cc @@ -46,13 +46,15 @@ class SubscriptionFactoryTest : public testing::Test { subscriptionFromConfigSource(const envoy::config::core::v3::ConfigSource& config) { return subscription_factory_.subscriptionFromConfigSource( config, Config::TypeUrl::get().ClusterLoadAssignment, stats_store_, callbacks_, - resource_decoder_); + resource_decoder_, false); } - SubscriptionPtr collectionSubscriptionFromUrl(const std::string& xds_url) { + SubscriptionPtr + collectionSubscriptionFromUrl(const std::string& xds_url, + const envoy::config::core::v3::ConfigSource& config) { const auto resource_locator = XdsResourceIdentifier::decodeUrl(xds_url); return subscription_factory_.collectionSubscriptionFromUrl( - resource_locator, {}, Config::TypeUrl::get().ClusterLoadAssignment, stats_store_, + resource_locator, config, "envoy.config.endpoint.v3.ClusterLoadAssignment", stats_store_, callbacks_, resource_decoder_); } @@ -215,14 +217,15 @@ TEST_F(SubscriptionFactoryTest, FilesystemCollectionSubscription) { EXPECT_CALL(callbacks_, onConfigUpdateFailed(_, _)); // Unix paths start with /, Windows with c:/. const std::string file_path = test_path[0] == '/' ? test_path.substr(1) : test_path; - collectionSubscriptionFromUrl(fmt::format("file:///{}", file_path))->start({}); + collectionSubscriptionFromUrl(fmt::format("file:///{}", file_path), {})->start({}); } -TEST_F(SubscriptionFactoryTest, FilesystemCollectionSubscriptionNonExistentFile){ - EXPECT_THROW_WITH_MESSAGE(collectionSubscriptionFromUrl("file:///blahblah")->start({}), - EnvoyException, - "envoy::api::v2::Path must refer to an existing path in the system: " - "'/blahblah' does not exist")} +TEST_F(SubscriptionFactoryTest, FilesystemCollectionSubscriptionNonExistentFile) { + EXPECT_THROW_WITH_MESSAGE(collectionSubscriptionFromUrl("file:///blahblah", {})->start({}), + EnvoyException, + "envoy::api::v2::Path must refer to an existing path in the system: " + "'/blahblah' does not exist"); +} TEST_F(SubscriptionFactoryTest, LegacySubscription) { envoy::config::core::v3::ConfigSource config; @@ -328,6 +331,48 @@ TEST_F(SubscriptionFactoryTest, GrpcSubscription) { subscriptionFromConfigSource(config)->start({"static_cluster"}); } +TEST_F(SubscriptionFactoryTest, GrpcCollectionSubscriptionBadType) { + EXPECT_THROW_WITH_MESSAGE(collectionSubscriptionFromUrl("xdstp:///foo", {})->start({}), + EnvoyException, + "xdstp:// type does not match " + "envoy.config.endpoint.v3.ClusterLoadAssignment in xdstp:///foo"); +} + +TEST_F(SubscriptionFactoryTest, GrpcCollectionSubscriptionUnsupportedApiType) { + envoy::config::core::v3::ConfigSource config; + auto* api_config_source = config.mutable_api_config_source(); + api_config_source->set_api_type(envoy::config::core::v3::ApiConfigSource::DELTA_GRPC); + api_config_source->set_transport_api_version(envoy::config::core::v3::V3); + api_config_source->add_grpc_services()->mutable_envoy_grpc()->set_cluster_name("static_cluster"); + Upstream::ClusterManager::ClusterSet primary_clusters; + primary_clusters.insert("static_cluster"); + EXPECT_CALL(cm_, primaryClusters()).WillOnce(ReturnRef(primary_clusters)); + EXPECT_THROW_WITH_REGEX( + collectionSubscriptionFromUrl( + "xdstp://foo/envoy.config.endpoint.v3.ClusterLoadAssignment/bar", config) + ->start({}), + EnvoyException, "Unknown xdstp:// transport API type in api_type: DELTA_GRPC"); +} + +TEST_F(SubscriptionFactoryTest, GrpcCollectionSubscription) { + envoy::config::core::v3::ConfigSource config; + auto* api_config_source = config.mutable_api_config_source(); + api_config_source->set_api_type(envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC); + api_config_source->set_transport_api_version(envoy::config::core::v3::V3); + api_config_source->add_grpc_services()->mutable_envoy_grpc()->set_cluster_name("static_cluster"); + Upstream::ClusterManager::ClusterSet primary_clusters; + primary_clusters.insert("static_cluster"); + EXPECT_CALL(cm_, primaryClusters()).WillOnce(ReturnRef(primary_clusters)); + GrpcMuxSharedPtr ads_mux = std::make_shared>(); + EXPECT_CALL(cm_, adsMux()).WillOnce(Return(ads_mux)); + EXPECT_CALL(dispatcher_, createTimer_(_)); + // onConfigUpdateFailed() should not be called for gRPC stream connection failure + EXPECT_CALL(callbacks_, onConfigUpdateFailed(_, _)).Times(0); + collectionSubscriptionFromUrl("xdstp://foo/envoy.config.endpoint.v3.ClusterLoadAssignment/bar", + config) + ->start({}); +} + // Use of the V2 transport fails by default. TEST_F(SubscriptionFactoryTest, LogWarningOnDeprecatedV2Transport) { envoy::config::core::v3::ConfigSource config; @@ -345,7 +390,7 @@ TEST_F(SubscriptionFactoryTest, LogWarningOnDeprecatedV2Transport) { EXPECT_THROW_WITH_REGEX(subscription_factory_.subscriptionFromConfigSource( config, Config::TypeUrl::get().ClusterLoadAssignment, stats_store_, - callbacks_, resource_decoder_), + callbacks_, resource_decoder_, false), EnvoyException, "V2 .and AUTO. xDS transport protocol versions are deprecated in"); } @@ -367,7 +412,7 @@ TEST_F(SubscriptionFactoryTest, LogWarningOnDeprecatedAutoTransport) { EXPECT_THROW_WITH_REGEX(subscription_factory_.subscriptionFromConfigSource( config, Config::TypeUrl::get().ClusterLoadAssignment, stats_store_, - callbacks_, resource_decoder_), + callbacks_, resource_decoder_, false), EnvoyException, "V2 .and AUTO. xDS transport protocol versions are deprecated in"); } diff --git a/test/common/config/subscription_impl_test.cc b/test/common/config/subscription_impl_test.cc index 84bc88f6cea6..321f81ecdca9 100644 --- a/test/common/config/subscription_impl_test.cc +++ b/test/common/config/subscription_impl_test.cc @@ -20,6 +20,24 @@ enum class SubscriptionType { Filesystem, }; +// NOLINTNEXTLINE(readability-identifier-naming) +void PrintTo(const SubscriptionType sub, std::ostream* os) { + (*os) << ([sub]() -> absl::string_view { + switch (sub) { + case SubscriptionType::Grpc: + return "Grpc"; + case SubscriptionType::DeltaGrpc: + return "DeltaGrpc"; + case SubscriptionType::Http: + return "Http"; + case SubscriptionType::Filesystem: + return "Filesystem"; + default: + return "unknown"; + } + })(); +} + class SubscriptionImplTest : public testing::TestWithParam { public: SubscriptionImplTest() : SubscriptionImplTest(std::chrono::milliseconds(0)) {} diff --git a/test/common/config/subscription_test_harness.h b/test/common/config/subscription_test_harness.h index 57342a11af92..5b0fe24a49d2 100644 --- a/test/common/config/subscription_test_harness.h +++ b/test/common/config/subscription_test_harness.h @@ -109,6 +109,14 @@ class SubscriptionTestHarness : public Event::TestUsingSimulatedTime { virtual void doSubscriptionTearDown() {} + // Helper util to convert to absl::flat_hash_set when calling Subscription interface methods. + absl::flat_hash_set flattenResources(const std::set& resources) { + absl::flat_hash_set flat_resources; + std::copy(resources.begin(), resources.end(), + std::inserter(flat_resources, flat_resources.begin())); + return flat_resources; + } + Stats::TestUtil::TestStore stats_store_; SubscriptionStats stats_; ControlPlaneStats control_plane_stats_; diff --git a/test/common/config/watch_map_test.cc b/test/common/config/watch_map_test.cc index 5a0f3c5fe0b6..34ae11c16e3c 100644 --- a/test/common/config/watch_map_test.cc +++ b/test/common/config/watch_map_test.cc @@ -136,7 +136,7 @@ TEST(WatchMapTest, Basic) { } { // The watch is interested in Alice and Bob... - std::set update_to({"alice", "bob"}); + absl::flat_hash_set update_to({"alice", "bob"}); AddedRemoved added_removed = watch_map.updateWatchInterest(watch, update_to); EXPECT_EQ(update_to, added_removed.added_); EXPECT_TRUE(added_removed.removed_.empty()); @@ -159,10 +159,10 @@ TEST(WatchMapTest, Basic) { } { // The watch is now interested in Bob, Carol, Dave, Eve... - std::set update_to({"bob", "carol", "dave", "eve"}); + absl::flat_hash_set update_to({"bob", "carol", "dave", "eve"}); AddedRemoved added_removed = watch_map.updateWatchInterest(watch, update_to); - EXPECT_EQ(std::set({"carol", "dave", "eve"}), added_removed.added_); - EXPECT_EQ(std::set({"alice"}), added_removed.removed_); + EXPECT_EQ(absl::flat_hash_set({"carol", "dave", "eve"}), added_removed.added_); + EXPECT_EQ(absl::flat_hash_set({"alice"}), added_removed.removed_); // ...the update is going to contain Alice, Carol, Dave... Protobuf::RepeatedPtrField updated_resources; @@ -209,7 +209,7 @@ TEST(WatchMapTest, Overlap) { // First watch becomes interested. { - std::set update_to({"alice", "dummy"}); + absl::flat_hash_set update_to({"alice", "dummy"}); AddedRemoved added_removed = watch_map.updateWatchInterest(watch1, update_to); EXPECT_EQ(update_to, added_removed.added_); // add to subscription EXPECT_TRUE(added_removed.removed_.empty()); @@ -222,7 +222,7 @@ TEST(WatchMapTest, Overlap) { } // Second watch becomes interested. { - std::set update_to({"alice", "dummy"}); + absl::flat_hash_set update_to({"alice", "dummy"}); AddedRemoved added_removed = watch_map.updateWatchInterest(watch2, update_to); EXPECT_TRUE(added_removed.added_.empty()); // nothing happens EXPECT_TRUE(added_removed.removed_.empty()); @@ -251,7 +251,8 @@ TEST(WatchMapTest, Overlap) { { AddedRemoved added_removed = watch_map.updateWatchInterest(watch2, {"dummy"}); EXPECT_TRUE(added_removed.added_.empty()); - EXPECT_EQ(std::set({"alice"}), added_removed.removed_); // remove from subscription + EXPECT_EQ(absl::flat_hash_set({"alice"}), + added_removed.removed_); // remove from subscription } } @@ -348,7 +349,7 @@ TEST(WatchMapTest, AddRemoveAdd) { // First watch becomes interested. { - std::set update_to({"alice", "dummy"}); + absl::flat_hash_set update_to({"alice", "dummy"}); AddedRemoved added_removed = watch_map.updateWatchInterest(watch1, update_to); EXPECT_EQ(update_to, added_removed.added_); // add to subscription EXPECT_TRUE(added_removed.removed_.empty()); @@ -363,7 +364,7 @@ TEST(WatchMapTest, AddRemoveAdd) { { AddedRemoved added_removed = watch_map.updateWatchInterest(watch1, {"dummy"}); EXPECT_TRUE(added_removed.added_.empty()); - EXPECT_EQ(std::set({"alice"}), + EXPECT_EQ(absl::flat_hash_set({"alice"}), added_removed.removed_); // remove from subscription // (The xDS client should have responded to updateWatchInterest()'s return value by removing @@ -371,9 +372,10 @@ TEST(WatchMapTest, AddRemoveAdd) { } // Second watch becomes interested. { - std::set update_to({"alice", "dummy"}); + absl::flat_hash_set update_to({"alice", "dummy"}); AddedRemoved added_removed = watch_map.updateWatchInterest(watch2, update_to); - EXPECT_EQ(std::set({"alice"}), added_removed.added_); // add to subscription + EXPECT_EQ(absl::flat_hash_set({"alice"}), + added_removed.added_); // add to subscription EXPECT_TRUE(added_removed.removed_.empty()); // Both watches receive the update. For watch2, this is obviously desired. @@ -521,6 +523,50 @@ TEST(WatchMapTest, OnConfigUpdateFailed) { watch_map.onConfigUpdateFailed(ConfigUpdateFailureReason::UpdateRejected, nullptr); } +// Validate watch behavior when subscribed to xdstp:// glob collections. +TEST(WatchMapTest, OnConfigUpdateXdsTpGlobCollections) { + MockSubscriptionCallbacks callbacks; + TestUtility::TestOpaqueResourceDecoderImpl + resource_decoder("cluster_name"); + WatchMap watch_map(true); + Watch* watch = watch_map.addWatch(callbacks, resource_decoder); + watch_map.updateWatchInterest(watch, {"xdstp://foo/bar/baz/*?some=thing&thing=some"}); + + // verify update + { + // Verify that we pay attention to all matching resources, no matter the order of context + // params. + Protobuf::RepeatedPtrField update; + envoy::config::endpoint::v3::ClusterLoadAssignment resource1; + resource1.set_cluster_name("xdstp://foo/bar/baz/a?some=thing&thing=some"); + update.Add()->PackFrom(resource1); + envoy::config::endpoint::v3::ClusterLoadAssignment resource2; + resource2.set_cluster_name("xdstp://foo/bar/baz/b?thing=some&some=thing"); + update.Add()->PackFrom(resource2); + // Ignore non-matching resources. + envoy::config::endpoint::v3::ClusterLoadAssignment ignored_resource; + ignored_resource.set_cluster_name("xdstp://foo/bar/baz/c?thing=some"); + update.Add()->PackFrom(ignored_resource); + ignored_resource.set_cluster_name("xdstp://foo/bar/baz/d"); + update.Add()->PackFrom(ignored_resource); + ignored_resource.set_cluster_name("xdstp://blah/bar/baz/e"); + update.Add()->PackFrom(ignored_resource); + ignored_resource.set_cluster_name("whatevs"); + update.Add()->PackFrom(ignored_resource); + expectDeltaUpdate(callbacks, {resource1, resource2}, {}, "version0"); + doDeltaUpdate(watch_map, update, {}, "version0"); + } + // verify removal + { + Protobuf::RepeatedPtrField update; + expectDeltaUpdate(callbacks, {}, {"xdstp://foo/bar/baz/a?thing=some&some=thing"}, "version1"); + doDeltaUpdate( + watch_map, update, + {"xdstp://foo/bar/baz/*", "xdstp://foo/bar/baz/a?thing=some&some=thing", "whatevs"}, + "version1"); + } +} + TEST(WatchMapTest, OnConfigUpdateUsingNamespaces) { MockSubscriptionCallbacks callbacks1; MockSubscriptionCallbacks callbacks2; diff --git a/test/common/config/xds_context_params_test.cc b/test/common/config/xds_context_params_test.cc index dbcf29a2c70b..50d96431f501 100644 --- a/test/common/config/xds_context_params_test.cc +++ b/test/common/config/xds_context_params_test.cc @@ -32,11 +32,19 @@ TEST(XdsContextParamsTest, NodeAll) { baz: 42 )EOF", node); - const auto context_params = XdsContextParams::encode( - node, - {"id", "cluster", "user_agent_name", "user_agent_version", "locality.region", "locality.zone", - "locality.sub_zone", "metadata"}, - {}, {}, {}); + const std::vector params_vec{"id", + "cluster", + "user_agent_name", + "user_agent_version", + "locality.region", + "locality.zone", + "locality.sub_zone", + "metadata"}; + Protobuf::RepeatedPtrField node_context_params{params_vec.cbegin(), + params_vec.cend()}; + + const auto context_params = XdsContextParams::encodeResource( + XdsContextParams::encodeNodeContext(node, node_context_params), {}, {}, {}); EXPECT_CONTEXT_PARAMS( context_params, Pair("xds.node.cluster", "some_cluster"), Pair("xds.node.id", "some_id"), Pair("xds.node.locality.sub_zone", "some_sub_zone"), @@ -64,8 +72,12 @@ TEST(XdsContextParamsTest, NodeParameterSelection) { baz: 42 )EOF", node); - const auto context_params = XdsContextParams::encode( - node, {"cluster", "user_agent_version", "locality.region", "locality.sub_zone"}, {}, {}, {}); + const std::vector params_vec{"cluster", "user_agent_version", "locality.region", + "locality.sub_zone"}; + Protobuf::RepeatedPtrField node_context_params{params_vec.cbegin(), + params_vec.cend()}; + const auto context_params = XdsContextParams::encodeResource( + XdsContextParams::encodeNodeContext(node, node_context_params), {}, {}, {}); EXPECT_CONTEXT_PARAMS(context_params, Pair("xds.node.cluster", "some_cluster"), Pair("xds.node.locality.sub_zone", "some_sub_zone"), Pair("xds.node.locality.region", "some_region"), @@ -87,8 +99,12 @@ TEST(XdsContextParamsTest, NodeUserAgentBuildVersion) { baz: 42 )EOF", node); - const auto context_params = XdsContextParams::encode( - node, {"user_agent_build_version.version", "user_agent_build_version.metadata"}, {}, {}, {}); + const std::vector params_vec{"user_agent_build_version.version", + "user_agent_build_version.metadata"}; + Protobuf::RepeatedPtrField node_context_params{params_vec.cbegin(), + params_vec.cend()}; + const auto context_params = XdsContextParams::encodeResource( + XdsContextParams::encodeNodeContext(node, node_context_params), {}, {}, {}); EXPECT_CONTEXT_PARAMS(context_params, Pair("xds.node.user_agent_build_version.metadata.bar", "\"a\""), Pair("xds.node.user_agent_build_version.metadata.baz", "42"), @@ -106,7 +122,7 @@ TEST(XdsContextParamsTest, ResoureContextParams) { baz: "true" )EOF", resource_context_params); - const auto context_params = XdsContextParams::encode({}, {}, resource_context_params, {}, {}); + const auto context_params = XdsContextParams::encodeResource({}, resource_context_params, {}, {}); EXPECT_CONTEXT_PARAMS(context_params, Pair("bar", "123"), Pair("baz", "true"), Pair("foo", "\"some_string\"")); } @@ -114,7 +130,7 @@ TEST(XdsContextParamsTest, ResoureContextParams) { // Validate client feature capabilities context parameter transform. TEST(XdsContextParamsTest, ClientFeatureCapabilities) { const auto context_params = - XdsContextParams::encode({}, {}, {}, {"some.feature", "another.feature"}, {}); + XdsContextParams::encodeResource({}, {}, {"some.feature", "another.feature"}, {}); EXPECT_CONTEXT_PARAMS(context_params, Pair("xds.client_feature.another.feature", "true"), Pair("xds.client_feature.some.feature", "true")); } @@ -122,7 +138,7 @@ TEST(XdsContextParamsTest, ClientFeatureCapabilities) { // Validate per-resource well-known attributes transform. TEST(XdsContextParamsTest, ResourceWktAttribs) { const auto context_params = - XdsContextParams::encode({}, {}, {}, {}, {{"foo", "1"}, {"bar", "2"}}); + XdsContextParams::encodeResource({}, {}, {}, {{"foo", "1"}, {"bar", "2"}}); EXPECT_CONTEXT_PARAMS(context_params, Pair("xds.resource.foo", "1"), Pair("xds.resource.bar", "2")); } @@ -142,8 +158,12 @@ TEST(XdsContextParamsTest, Layering) { xds.node.cluster: another_cluster )EOF", resource_context_params); - const auto context_params = XdsContextParams::encode( - node, {"id", "cluster"}, resource_context_params, {"id"}, {{"cluster", "huh"}}); + const std::vector params_vec{"id", "cluster"}; + Protobuf::RepeatedPtrField node_context_params{params_vec.cbegin(), + params_vec.cend()}; + const auto context_params = XdsContextParams::encodeResource( + XdsContextParams::encodeNodeContext(node, node_context_params), resource_context_params, + {"id"}, {{"cluster", "huh"}}); EXPECT_CONTEXT_PARAMS(context_params, Pair("id", "another_id"), Pair("xds.client_feature.id", "true"), Pair("xds.node.cluster", "another_cluster"), Pair("xds.node.id", "some_id"), diff --git a/test/common/conn_pool/conn_pool_base_test.cc b/test/common/conn_pool/conn_pool_base_test.cc index d9981c7ad7ba..7e4693cacf25 100644 --- a/test/common/conn_pool/conn_pool_base_test.cc +++ b/test/common/conn_pool/conn_pool_base_test.cc @@ -12,6 +12,7 @@ namespace Envoy { namespace ConnectionPool { using testing::AnyNumber; +using testing::HasSubstr; using testing::Invoke; using testing::InvokeWithoutArgs; using testing::Return; @@ -90,6 +91,15 @@ class ConnPoolImplBaseTest : public testing::Test { std::vector clients_; }; +TEST_F(ConnPoolImplBaseTest, DumpState) { + std::stringstream out; + pool_.dumpState(out, 0); + std::string state = out.str(); + EXPECT_THAT(state, HasSubstr("ready_clients_.size(): 0, busy_clients_.size(): 0, " + "connecting_clients_.size(): 0, connecting_stream_capacity_: 0, " + "num_active_streams_: 0")); +} + TEST_F(ConnPoolImplBaseTest, BasicPreconnect) { // Create more than one connection per new stream. ON_CALL(*cluster_, perUpstreamPreconnectRatio).WillByDefault(Return(1.5)); diff --git a/test/common/event/dispatcher_impl_test.cc b/test/common/event/dispatcher_impl_test.cc index 8c612144db50..198a0ef4fd8b 100644 --- a/test/common/event/dispatcher_impl_test.cc +++ b/test/common/event/dispatcher_impl_test.cc @@ -1,10 +1,13 @@ #include +#include "envoy/common/scope_tracker.h" #include "envoy/thread/thread.h" #include "common/api/api_impl.h" #include "common/api/os_sys_calls_impl.h" #include "common/common/lock_guard.h" +#include "common/common/scope_tracker.h" +#include "common/common/utility.h" #include "common/event/deferred_task.h" #include "common/event/dispatcher_impl.h" #include "common/event/timer_impl.h" @@ -21,8 +24,11 @@ #include "gtest/gtest.h" using testing::_; +using testing::ByMove; using testing::InSequence; +using testing::MockFunction; using testing::NiceMock; +using testing::Return; namespace Envoy { namespace Event { @@ -533,9 +539,71 @@ TEST_F(DispatcherImplTest, IsThreadSafe) { EXPECT_FALSE(dispatcher_->isThreadSafe()); } +TEST_F(DispatcherImplTest, ShouldDumpNothingIfNoTrackedObjects) { + std::array buffer; + OutputBufferStream ostream{buffer.data(), buffer.size()}; + + // Call on FatalError to trigger dumps of tracked objects. + dispatcher_->post([this, &ostream]() { + Thread::LockGuard lock(mu_); + static_cast(dispatcher_.get())->onFatalError(ostream); + work_finished_ = true; + cv_.notifyOne(); + }); + + Thread::LockGuard lock(mu_); + while (!work_finished_) { + cv_.wait(mu_); + } + + // Check ostream still empty. + EXPECT_EQ(ostream.contents(), ""); +} + +class MessageTrackedObject : public ScopeTrackedObject { +public: + MessageTrackedObject(absl::string_view sv) : sv_(sv) {} + void dumpState(std::ostream& os, int /*indent_level*/) const override { os << sv_; } + +private: + absl::string_view sv_; +}; + +TEST_F(DispatcherImplTest, ShouldDumpTrackedObjectsInFILO) { + std::array buffer; + OutputBufferStream ostream{buffer.data(), buffer.size()}; + + // Call on FatalError to trigger dumps of tracked objects. + dispatcher_->post([this, &ostream]() { + Thread::LockGuard lock(mu_); + + // Add several tracked objects to the dispatcher + MessageTrackedObject first{"first"}; + ScopeTrackerScopeState first_state{&first, *dispatcher_}; + MessageTrackedObject second{"second"}; + ScopeTrackerScopeState second_state{&second, *dispatcher_}; + MessageTrackedObject third{"third"}; + ScopeTrackerScopeState third_state{&third, *dispatcher_}; + + static_cast(dispatcher_.get())->onFatalError(ostream); + work_finished_ = true; + cv_.notifyOne(); + }); + + Thread::LockGuard lock(mu_); + while (!work_finished_) { + cv_.wait(mu_); + } + + // Check the dump includes and registered objects in a FILO order. + EXPECT_EQ(ostream.contents(), "thirdsecondfirst"); +} + class TestFatalAction : public Server::Configuration::FatalAction { public: - void run(const ScopeTrackedObject* /*current_object*/) override { ++times_ran_; } + void run(absl::Span /*tracked_objects*/) override { + ++times_ran_; + } bool isAsyncSignalSafe() const override { return true; } int getNumTimesRan() { return times_ran_; } @@ -1270,6 +1338,48 @@ TEST_F(TimerUtilsTest, TimerValueConversion) { checkConversion(std::chrono::milliseconds(600014), 600, 14000); } +TEST(DispatcherWithScaledTimerFactoryTest, CreatesScaledTimerManager) { + Api::ApiPtr api = Api::createApiForTest(); + MockFunction scaled_timer_manager_factory; + + MockScaledRangeTimerManager* manager = new MockScaledRangeTimerManager(); + EXPECT_CALL(scaled_timer_manager_factory, Call) + .WillOnce(Return(ByMove(ScaledRangeTimerManagerPtr(manager)))); + + DispatcherPtr dispatcher = + api->allocateDispatcher("test_thread", scaled_timer_manager_factory.AsStdFunction()); +} + +TEST(DispatcherWithScaledTimerFactoryTest, CreateScaledTimerWithMinimum) { + Api::ApiPtr api = Api::createApiForTest(); + MockFunction scaled_timer_manager_factory; + + MockScaledRangeTimerManager* manager = new MockScaledRangeTimerManager(); + EXPECT_CALL(scaled_timer_manager_factory, Call) + .WillOnce(Return(ByMove(ScaledRangeTimerManagerPtr(manager)))); + + DispatcherPtr dispatcher = + api->allocateDispatcher("test_thread", scaled_timer_manager_factory.AsStdFunction()); + + EXPECT_CALL(*manager, createTimer_(ScaledTimerMinimum(ScaledMinimum(UnitFloat(0.8f))), _)); + dispatcher->createScaledTimer(ScaledTimerMinimum(ScaledMinimum(UnitFloat(0.8f))), []() {}); +} + +TEST(DispatcherWithScaledTimerFactoryTest, CreateScaledTimerWithTimerType) { + Api::ApiPtr api = Api::createApiForTest(); + MockFunction scaled_timer_manager_factory; + + MockScaledRangeTimerManager* manager = new MockScaledRangeTimerManager(); + EXPECT_CALL(scaled_timer_manager_factory, Call) + .WillOnce(Return(ByMove(ScaledRangeTimerManagerPtr(manager)))); + + DispatcherPtr dispatcher = + api->allocateDispatcher("test_thread", scaled_timer_manager_factory.AsStdFunction()); + + EXPECT_CALL(*manager, createTypedTimer_(ScaledTimerType::UnscaledRealTimerForTest, _)); + dispatcher->createScaledTimer(ScaledTimerType::UnscaledRealTimerForTest, []() {}); +} + class DispatcherWithWatchdogTest : public testing::Test { protected: DispatcherWithWatchdogTest() diff --git a/test/common/event/scaled_range_timer_manager_impl_test.cc b/test/common/event/scaled_range_timer_manager_impl_test.cc index 824a285ee4f6..14d240ecc704 100644 --- a/test/common/event/scaled_range_timer_manager_impl_test.cc +++ b/test/common/event/scaled_range_timer_manager_impl_test.cc @@ -1,5 +1,6 @@ #include +#include "envoy/common/scope_tracker.h" #include "envoy/event/timer.h" #include "common/event/dispatcher_impl.h" @@ -24,9 +25,14 @@ class ScopeTrackingDispatcher : public WrappedDispatcher { ScopeTrackingDispatcher(DispatcherPtr dispatcher) : WrappedDispatcher(*dispatcher), dispatcher_(std::move(dispatcher)) {} - const ScopeTrackedObject* setTrackedObject(const ScopeTrackedObject* object) override { + void pushTrackedObject(const ScopeTrackedObject* object) override { scope_ = object; - return impl_.setTrackedObject(object); + return impl_.pushTrackedObject(object); + } + + void popTrackedObject(const ScopeTrackedObject* expected_object) override { + scope_ = nullptr; + return impl_.popTrackedObject(expected_object); } const ScopeTrackedObject* scope_{nullptr}; @@ -280,7 +286,7 @@ TEST_P(ScaledRangeTimerManagerTestWithScope, ScheduleWithScalingFactorZero) { MockFunction callback; auto timer = manager.createTimer(AbsoluteMinimum(std::chrono::seconds(0)), callback.AsStdFunction()); - manager.setScaleFactor(0); + manager.setScaleFactor(UnitFloat(0)); EXPECT_CALL(callback, Call).WillOnce([&] { EXPECT_EQ(dispatcher_.scope_, getScope()); }); @@ -394,7 +400,7 @@ TEST_F(ScaledRangeTimerManagerTest, MultipleTimersWithScaling) { timers[1].timer->enableTimer(std::chrono::seconds(6)); timers[2].timer->enableTimer(std::chrono::seconds(10)); - manager.setScaleFactor(0.5); + manager.setScaleFactor(UnitFloat(0.5)); // Advance time to start = 1 second, so timers[0] hits its min. simTime().advanceTimeAndRun(std::chrono::seconds(1), dispatcher_, Dispatcher::RunType::Block); @@ -403,7 +409,7 @@ TEST_F(ScaledRangeTimerManagerTest, MultipleTimersWithScaling) { simTime().advanceTimeAndRun(std::chrono::seconds(1), dispatcher_, Dispatcher::RunType::Block); // At 4x speed, timers[1] will fire in only 1 second. - manager.setScaleFactor(0.25); + manager.setScaleFactor(UnitFloat(0.25)); // Advance time to start = 3, which should make timers[1] hit its scaled max. simTime().advanceTimeAndRun(std::chrono::seconds(1), dispatcher_, Dispatcher::RunType::Block); @@ -411,7 +417,7 @@ TEST_F(ScaledRangeTimerManagerTest, MultipleTimersWithScaling) { // Advance time to start = 6, which is the minimum required for timers[2] to fire. simTime().advanceTimeAndRun(std::chrono::seconds(3), dispatcher_, Dispatcher::RunType::Block); - manager.setScaleFactor(0); + manager.setScaleFactor(UnitFloat(0)); // With a scale factor of 0, timers[2] should be ready to be fired immediately. dispatcher_.run(Dispatcher::RunType::Block); @@ -464,7 +470,7 @@ TEST_F(ScaledRangeTimerManagerTest, MultipleTimersSameTimesFastClock) { TEST_F(ScaledRangeTimerManagerTest, ScheduledWithScalingFactorZero) { ScaledRangeTimerManagerImpl manager(dispatcher_); - manager.setScaleFactor(0); + manager.setScaleFactor(UnitFloat(0)); TrackedRangeTimer timer(AbsoluteMinimum(std::chrono::seconds(4)), manager, simTime()); @@ -566,7 +572,7 @@ TEST_F(ScaledRangeTimerManagerTest, MultipleTimersWithChangeInScalingFactor) { timers[0].timer->enableTimer(std::chrono::seconds(15)); timers[1].timer->enableTimer(std::chrono::seconds(14)); - manager.setScaleFactor(0.1); + manager.setScaleFactor(UnitFloat(0.1)); timers[2].timer->enableTimer(std::chrono::seconds(21)); timers[3].timer->enableTimer(std::chrono::seconds(16)); @@ -574,7 +580,7 @@ TEST_F(ScaledRangeTimerManagerTest, MultipleTimersWithChangeInScalingFactor) { // Advance to timer 0's min. simTime().advanceTimeAndRun(std::chrono::seconds(5), dispatcher_, Dispatcher::RunType::Block); - manager.setScaleFactor(0.5); + manager.setScaleFactor(UnitFloat(0.5)); // Now that the scale factor is 0.5, fire times are 0: start+10, 1: start+13, 2: start+14, 3: // start+13. Advance to timer 2's min. @@ -583,7 +589,7 @@ TEST_F(ScaledRangeTimerManagerTest, MultipleTimersWithChangeInScalingFactor) { // Advance to time start+9. simTime().advanceTimeAndRun(std::chrono::seconds(2), dispatcher_, Dispatcher::RunType::Block); - manager.setScaleFactor(0.1); + manager.setScaleFactor(UnitFloat(0.1)); // Now that the scale factor is reduced, fire times are 0: start+6, 1: start+12.2, // 2: start+8.4, 3: start+10.6. Timers 0 and 2 should fire immediately since their // trigger times are in the past. @@ -598,7 +604,7 @@ TEST_F(ScaledRangeTimerManagerTest, MultipleTimersWithChangeInScalingFactor) { timers[0].timer->enableTimer(std::chrono::seconds(13)); // Fire times are now 0: start+19, 1: start+13, 2: none, 3: start+13. - manager.setScaleFactor(0.5); + manager.setScaleFactor(UnitFloat(0.5)); // Advance to timer 1's min. simTime().advanceTimeAndRun(std::chrono::seconds(2), dispatcher_, Dispatcher::RunType::Block); @@ -611,12 +617,40 @@ TEST_F(ScaledRangeTimerManagerTest, MultipleTimersWithChangeInScalingFactor) { simTime().advanceTimeAndRun(std::chrono::seconds(3), dispatcher_, Dispatcher::RunType::Block); // The time is now start+16. Setting the scale factor to 0 should make timer 0 fire immediately. - manager.setScaleFactor(0); + manager.setScaleFactor(UnitFloat(0)); dispatcher_.run(Dispatcher::RunType::Block); EXPECT_THAT(*timers[0].trigger_times, ElementsAre(start + std::chrono::seconds(9), start + std::chrono::seconds(16))); } +TEST_F(ScaledRangeTimerManagerTest, LooksUpConfiguredMinimums) { + // Test-only class that overrides one of the createScaledTimer overloads to show that the other + // one calls into this one after looking up the minimum. + class TestScaledRangeTimerManager : public ScaledRangeTimerManagerImpl { + public: + using ScaledRangeTimerManagerImpl::createTimer; + using ScaledRangeTimerManagerImpl::ScaledRangeTimerManagerImpl; + TimerPtr createTimer(ScaledTimerMinimum minimum, TimerCb callback) override { + return createScaledTimer(minimum, callback); + } + MOCK_METHOD(TimerPtr, createScaledTimer, (ScaledTimerMinimum, TimerCb)); + }; + + const ScaledTimerTypeMap timer_types{ + {ScaledTimerType::UnscaledRealTimerForTest, ScaledMinimum(UnitFloat::max())}, + {ScaledTimerType::HttpDownstreamIdleConnectionTimeout, ScaledMinimum(UnitFloat(0.3))}, + {ScaledTimerType::HttpDownstreamIdleStreamTimeout, ScaledMinimum(UnitFloat(0.6))}, + }; + + TestScaledRangeTimerManager manager(dispatcher_, + std::make_unique(timer_types)); + for (const auto& [timer_type, minimum] : timer_types) { + SCOPED_TRACE(static_cast(timer_type)); + EXPECT_CALL(manager, createScaledTimer(minimum, _)); + manager.createTimer(timer_type, []() {}); + } +} + } // namespace } // namespace Event } // namespace Envoy diff --git a/test/common/filter/http/filter_config_discovery_impl_test.cc b/test/common/filter/http/filter_config_discovery_impl_test.cc index 89c668e42357..2d7d7d0e00e6 100644 --- a/test/common/filter/http/filter_config_discovery_impl_test.cc +++ b/test/common/filter/http/filter_config_discovery_impl_test.cc @@ -88,8 +88,7 @@ class FilterConfigDiscoveryImplTest : public FilterConfigDiscoveryTestBase { void setup(bool warm = true) { provider_ = createProvider("foo", warm); callbacks_ = factory_context_.cluster_manager_.subscription_factory_.callbacks_; - EXPECT_CALL(*factory_context_.cluster_manager_.subscription_factory_.subscription_, - start(_, _)); + EXPECT_CALL(*factory_context_.cluster_manager_.subscription_factory_.subscription_, start(_)); if (!warm) { EXPECT_CALL(init_watcher_, ready()); } diff --git a/test/common/formatter/BUILD b/test/common/formatter/BUILD index 9001f4820bdc..b50c253bb24c 100644 --- a/test/common/formatter/BUILD +++ b/test/common/formatter/BUILD @@ -4,6 +4,7 @@ load( "envoy_cc_benchmark_binary", "envoy_cc_fuzz_test", "envoy_cc_test", + "envoy_cc_test_library", "envoy_package", "envoy_proto_library", ) @@ -33,10 +34,21 @@ envoy_cc_fuzz_test( ], ) +envoy_cc_test_library( + name = "command_extension_lib", + srcs = ["command_extension.cc"], + hdrs = ["command_extension.h"], + deps = [ + "//source/common/formatter:substitution_formatter_lib", + "//source/common/protobuf:utility_lib", + ], +) + envoy_cc_test( name = "substitution_formatter_test", srcs = ["substitution_formatter_test.cc"], deps = [ + ":command_extension_lib", "//source/common/common:utility_lib", "//source/common/formatter:substitution_formatter_lib", "//source/common/http:header_map_lib", @@ -57,10 +69,12 @@ envoy_cc_test( name = "substitution_format_string_test", srcs = ["substitution_format_string_test.cc"], deps = [ + ":command_extension_lib", "//source/common/formatter:substitution_format_string_lib", "//test/mocks/http:http_mocks", "//test/mocks/server:factory_context_mocks", "//test/mocks/stream_info:stream_info_mocks", + "//test/test_common:registry_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], diff --git a/test/common/formatter/command_extension.cc b/test/common/formatter/command_extension.cc new file mode 100644 index 000000000000..4dd2faa34625 --- /dev/null +++ b/test/common/formatter/command_extension.cc @@ -0,0 +1,45 @@ +#include "test/common/formatter/command_extension.h" + +#include "common/protobuf/utility.h" + +namespace Envoy { +namespace Formatter { + +absl::optional TestFormatter::format(const Http::RequestHeaderMap&, + const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, + const StreamInfo::StreamInfo&, + absl::string_view) const { + return "TestFormatter"; +} + +ProtobufWkt::Value TestFormatter::formatValue(const Http::RequestHeaderMap&, + const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, + const StreamInfo::StreamInfo&, + absl::string_view) const { + return ValueUtil::stringValue(""); +} + +FormatterProviderPtr TestCommandParser::parse(const std::string& token, size_t, size_t) const { + if (absl::StartsWith(token, "COMMAND_EXTENSION")) { + return std::make_unique(); + } + + return nullptr; +} + +CommandParserPtr TestCommandFactory::createCommandParserFromProto(const Protobuf::Message&) { + return std::make_unique(); +} + +std::string TestCommandFactory::configType() { return "google.protobuf.StringValue"; } + +ProtobufTypes::MessagePtr TestCommandFactory::createEmptyConfigProto() { + return std::make_unique(); +} + +std::string TestCommandFactory::name() const { return "envoy.formatter.TestFormatter"; } + +} // namespace Formatter +} // namespace Envoy diff --git a/test/common/formatter/command_extension.h b/test/common/formatter/command_extension.h new file mode 100644 index 000000000000..64e875a90114 --- /dev/null +++ b/test/common/formatter/command_extension.h @@ -0,0 +1,37 @@ +#include + +#include "envoy/config/typed_config.h" +#include "envoy/registry/registry.h" + +#include "common/formatter/substitution_formatter.h" + +namespace Envoy { +namespace Formatter { + +class TestFormatter : public FormatterProvider { +public: + // FormatterProvider + absl::optional format(const Http::RequestHeaderMap&, const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, const StreamInfo::StreamInfo&, + absl::string_view) const override; + ProtobufWkt::Value formatValue(const Http::RequestHeaderMap&, const Http::ResponseHeaderMap&, + const Http::ResponseTrailerMap&, const StreamInfo::StreamInfo&, + absl::string_view) const override; +}; + +class TestCommandParser : public CommandParser { +public: + TestCommandParser() = default; + FormatterProviderPtr parse(const std::string& token, size_t, size_t) const override; +}; + +class TestCommandFactory : public CommandParserFactory { +public: + CommandParserPtr createCommandParserFromProto(const Protobuf::Message&) override; + std::string configType() override; + ProtobufTypes::MessagePtr createEmptyConfigProto() override; + std::string name() const override; +}; + +} // namespace Formatter +} // namespace Envoy diff --git a/test/common/formatter/substitution_format_string_test.cc b/test/common/formatter/substitution_format_string_test.cc index 105c18f0038b..37e83fe3caa9 100644 --- a/test/common/formatter/substitution_format_string_test.cc +++ b/test/common/formatter/substitution_format_string_test.cc @@ -2,9 +2,11 @@ #include "common/formatter/substitution_format_string.h" +#include "test/common/formatter/command_extension.h" #include "test/mocks/http/mocks.h" #include "test/mocks/server/factory_context.h" #include "test/mocks/stream_info/mocks.h" +#include "test/test_common/registry.h" #include "test/test_common/utility.h" #include "gmock/gmock.h" @@ -93,9 +95,45 @@ TEST_F(SubstitutionFormatStringUtilsTest, TestInvalidConfigs) { TestUtility::loadFromYaml(yaml, config_); EXPECT_THROW_WITH_MESSAGE( SubstitutionFormatStringUtils::fromProtoConfig(config_, context_.api()), EnvoyException, - "Only string values or nested structs are supported in structured access log format."); + "Only string values, nested structs and list values are supported in structured access log " + "format."); } } +TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigFormatterExtension) { + TestCommandFactory factory; + Registry::InjectFactory command_register(factory); + + const std::string yaml = R"EOF( + text_format_source: + inline_string: "plain text %COMMAND_EXTENSION()%" + formatters: + - name: envoy.formatter.TestFormatter + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + auto formatter = SubstitutionFormatStringUtils::fromProtoConfig(config_, context_.api()); + EXPECT_EQ("plain text TestFormatter", formatter->format(request_headers_, response_headers_, + response_trailers_, stream_info_, body_)); +} + +TEST_F(SubstitutionFormatStringUtilsTest, TestFromProtoConfigFormatterExtensionUnknown) { + const std::string yaml = R"EOF( + text_format_source: + inline_string: "plain text" + formatters: + - name: envoy.formatter.TestFormatterUnknown + typed_config: + "@type": type.googleapis.com/google.protobuf.Any +)EOF"; + TestUtility::loadFromYaml(yaml, config_); + + EXPECT_THROW_WITH_MESSAGE(SubstitutionFormatStringUtils::fromProtoConfig(config_, context_.api()), + EnvoyException, + "Formatter not found: envoy.formatter.TestFormatterUnknown"); +} + } // namespace Formatter } // namespace Envoy diff --git a/test/common/formatter/substitution_formatter_test.cc b/test/common/formatter/substitution_formatter_test.cc index 0dae4dfdc0c4..73b7ce9ecd99 100644 --- a/test/common/formatter/substitution_formatter_test.cc +++ b/test/common/formatter/substitution_formatter_test.cc @@ -14,6 +14,7 @@ #include "common/protobuf/utility.h" #include "common/router/string_accessor_impl.h" +#include "test/common/formatter/command_extension.h" #include "test/mocks/api/mocks.h" #include "test/mocks/http/mocks.h" #include "test/mocks/ssl/mocks.h" @@ -1049,6 +1050,42 @@ TEST(SubstitutionFormatterTest, requestHeaderFormatter) { } } +TEST(SubstitutionFormatterTest, headersByteSizeFormatter) { + StreamInfo::MockStreamInfo stream_info; + Http::TestRequestHeaderMapImpl request_header{{":method", "GET"}, {":path", "/"}}; + Http::TestResponseHeaderMapImpl response_header{{":method", "PUT"}}; + Http::TestResponseTrailerMapImpl response_trailer{{":method", "POST"}, {"test-2", "test-2"}}; + std::string body; + + { + HeadersByteSizeFormatter formatter(HeadersByteSizeFormatter::HeaderType::RequestHeaders); + EXPECT_EQ( + formatter.format(request_header, response_header, response_trailer, stream_info, body), + "16"); + EXPECT_THAT( + formatter.formatValue(request_header, response_header, response_trailer, stream_info, body), + ProtoEq(ValueUtil::numberValue(16))); + } + { + HeadersByteSizeFormatter formatter(HeadersByteSizeFormatter::HeaderType::ResponseHeaders); + EXPECT_EQ( + formatter.format(request_header, response_header, response_trailer, stream_info, body), + "10"); + EXPECT_THAT( + formatter.formatValue(request_header, response_header, response_trailer, stream_info, body), + ProtoEq(ValueUtil::numberValue(10))); + } + { + HeadersByteSizeFormatter formatter(HeadersByteSizeFormatter::HeaderType::ResponseTrailers); + EXPECT_EQ( + formatter.format(request_header, response_header, response_trailer, stream_info, body), + "23"); + EXPECT_THAT( + formatter.formatValue(request_header, response_header, response_trailer, stream_info, body), + ProtoEq(ValueUtil::numberValue(23))); + } +} + TEST(SubstitutionFormatterTest, responseHeaderFormatter) { StreamInfo::MockStreamInfo stream_info; Http::TestRequestHeaderMapImpl request_header{{":method", "GET"}, {":path", "/"}}; @@ -1623,7 +1660,7 @@ TEST(SubstitutionFormatterTest, StructFormatterPlainStringTest) { expected_json_map); } -TEST(SubstitutionFormatterTest, StructFormatterNestedObject) { +TEST(SubstitutionFormatterTest, StructFormatterTypesTest) { StreamInfo::MockStreamInfo stream_info; Http::TestRequestHeaderMapImpl request_header; Http::TestResponseHeaderMapImpl response_header; @@ -1637,24 +1674,151 @@ TEST(SubstitutionFormatterTest, StructFormatterNestedObject) { ProtobufWkt::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( - level_one: - level_two: - level_three: - plain_string: plain_string_value - protocol: '%PROTOCOL%' + string_type: plain_string_value + struct_type: + plain_string: plain_string_value + protocol: '%PROTOCOL%' + list_type: + - plain_string_value + - '%PROTOCOL%' )EOF", key_mapping); StructFormatter formatter(key_mapping, false, false); const ProtobufWkt::Struct expected = TestUtility::jsonToStruct(R"EOF({ - "level_one": { - "level_two": { - "level_three": { - "plain_string": "plain_string_value", - "protocol": "HTTP/1.1" - } - } - } + "string_type": "plain_string_value", + "struct_type": { + "plain_string": "plain_string_value", + "protocol": "HTTP/1.1" + }, + "list_type": [ + "plain_string_value", + "HTTP/1.1" + ] + })EOF"); + const ProtobufWkt::Struct out_struct = + formatter.format(request_header, response_header, response_trailer, stream_info, body); + EXPECT_TRUE(TestUtility::protoEqual(out_struct, expected)); +} + +// Test that nested values are formatted properly, including inter-type nesting. +TEST(SubstitutionFormatterTest, StructFormatterNestedObjectsTest) { + StreamInfo::MockStreamInfo stream_info; + Http::TestRequestHeaderMapImpl request_header; + Http::TestResponseHeaderMapImpl response_header; + Http::TestResponseTrailerMapImpl response_trailer; + std::string body; + + envoy::config::core::v3::Metadata metadata; + populateMetadataTestData(metadata); + absl::optional protocol = Http::Protocol::Http11; + EXPECT_CALL(stream_info, protocol()).WillRepeatedly(Return(protocol)); + + ProtobufWkt::Struct key_mapping; + // For both struct and list, we test 3 nesting levels of all types (string, struct and list). + TestUtility::loadFromYaml(R"EOF( + struct: + struct_string: plain_string_value + struct_protocol: '%PROTOCOL%' + struct_struct: + struct_struct_string: plain_string_value + struct_struct_protocol: '%PROTOCOL%' + struct_struct_struct: + struct_struct_struct_string: plain_string_value + struct_struct_struct_protocol: '%PROTOCOL%' + struct_struct_list: + - struct_struct_list_string + - '%PROTOCOL%' + struct_list: + - struct_list_string + - '%PROTOCOL%' + # struct_list_struct + - struct_list_struct_string: plain_string_value + struct_list_struct_protocol: '%PROTOCOL%' + # struct_list_list + - - struct_list_list_string + - '%PROTOCOL%' + list: + - list_string + - '%PROTOCOL%' + # list_struct + - list_struct_string: plain_string_value + list_struct_protocol: '%PROTOCOL%' + list_struct_struct: + list_struct_struct_string: plain_string_value + list_struct_struct_protocol: '%PROTOCOL%' + list_struct_list: + - list_struct_list_string + - '%PROTOCOL%' + # list_list + - - list_list_string + - '%PROTOCOL%' + # list_list_struct + - list_list_struct_string: plain_string_value + list_list_struct_protocol: '%PROTOCOL%' + # list_list_list + - - list_list_list_string + - '%PROTOCOL%' + )EOF", + key_mapping); + StructFormatter formatter(key_mapping, false, false); + const ProtobufWkt::Struct expected = TestUtility::jsonToStruct(R"EOF({ + "struct": { + "struct_string": "plain_string_value", + "struct_protocol": "HTTP/1.1", + "struct_struct": { + "struct_struct_string": "plain_string_value", + "struct_struct_protocol": "HTTP/1.1", + "struct_struct_struct": { + "struct_struct_struct_string": "plain_string_value", + "struct_struct_struct_protocol": "HTTP/1.1", + }, + "struct_struct_list": [ + "struct_struct_list_string", + "HTTP/1.1", + ], + }, + "struct_list": [ + "struct_list_string", + "HTTP/1.1", + { + "struct_list_struct_string": "plain_string_value", + "struct_list_struct_protocol": "HTTP/1.1", + }, + [ + "struct_list_list_string", + "HTTP/1.1", + ], + ], + }, + "list": [ + "list_string", + "HTTP/1.1", + { + "list_struct_string": "plain_string_value", + "list_struct_protocol": "HTTP/1.1", + "list_struct_struct": { + "list_struct_struct_string": "plain_string_value", + "list_struct_struct_protocol": "HTTP/1.1", + }, + "list_struct_list": [ + "list_struct_list_string", + "HTTP/1.1", + ] + }, + [ + "list_list_string", + "HTTP/1.1", + { + "list_list_struct_string": "plain_string_value", + "list_list_struct_protocol": "HTTP/1.1", + }, + [ + "list_list_list_string", + "HTTP/1.1", + ], + ], + ], })EOF"); const ProtobufWkt::Struct out_struct = formatter.format(request_header, response_header, response_trailer, stream_info, body); @@ -1735,10 +1899,14 @@ TEST(SubstitutionFormatterTest, StructFormatterAlternateHeaderTest) { ProtobufWkt::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( - request_present_header_or_request_absent_header: '%REQ(request_present_header?request_absent_header)%' - request_absent_header_or_request_present_header: '%REQ(request_absent_header?request_present_header)%' - response_absent_header_or_response_absent_header: '%RESP(response_absent_header?response_present_header)%' - response_present_header_or_response_absent_header: '%RESP(response_present_header?response_absent_header)%' + request_present_header_or_request_absent_header: + '%REQ(request_present_header?request_absent_header)%' + request_absent_header_or_request_present_header: + '%REQ(request_absent_header?request_present_header)%' + response_absent_header_or_response_absent_header: + '%RESP(response_absent_header?response_present_header)%' + response_present_header_or_response_absent_header: + '%RESP(response_present_header?response_absent_header)%' )EOF", key_mapping); StructFormatter formatter(key_mapping, false, false); @@ -2029,7 +2197,8 @@ TEST(SubstitutionFormatterTest, StructFormatterMultiTokenTest) { ProtobufWkt::Struct key_mapping; TestUtility::loadFromYaml(R"EOF( - multi_token_field: '%PROTOCOL% plainstring %REQ(some_request_header)% %RESP(some_response_header)%' + multi_token_field: '%PROTOCOL% plainstring %REQ(some_request_header)% + %RESP(some_response_header)%' )EOF", key_mapping); for (const bool preserve_types : {false, true}) { @@ -2449,6 +2618,23 @@ TEST(SubstitutionFormatterTest, ParserSuccesses) { } } +TEST(SubstitutionFormatterTest, FormatterExtension) { + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, {":path", "/"}}; + Http::TestResponseHeaderMapImpl response_headers; + Http::TestResponseTrailerMapImpl response_trailers; + StreamInfo::MockStreamInfo stream_info; + std::string body; + + std::vector commands; + commands.push_back(std::make_unique()); + + auto providers = SubstitutionFormatParser::parse("foo %COMMAND_EXTENSION(x)%", commands); + + EXPECT_EQ(providers.size(), 2); + EXPECT_EQ("TestFormatter", providers[1]->format(request_headers, response_headers, + response_trailers, stream_info, body)); +} + } // namespace } // namespace Formatter } // namespace Envoy diff --git a/test/common/grpc/grpc_client_integration.h b/test/common/grpc/grpc_client_integration.h index 13683282d37e..80321fc83592 100644 --- a/test/common/grpc/grpc_client_integration.h +++ b/test/common/grpc/grpc_client_integration.h @@ -69,9 +69,7 @@ class VersionedGrpcClientIntegrationParamTest return fmt::format("{}_{}_{}", std::get<0>(p.param) == Network::Address::IpVersion::v4 ? "IPv4" : "IPv6", std::get<1>(p.param) == ClientType::GoogleGrpc ? "GoogleGrpc" : "EnvoyGrpc", - std::get<2>(p.param) == envoy::config::core::v3::ApiVersion::V3 - ? "V3" - : envoy::config::core::v3::ApiVersion::V2 ? "V2" : "AUTO"); + ApiVersion_Name(std::get<2>(p.param))); } Network::Address::IpVersion ipVersion() const override { return std::get<0>(GetParam()); } ClientType clientType() const override { return std::get<1>(GetParam()); } diff --git a/test/common/http/conn_manager_impl_test.cc b/test/common/http/conn_manager_impl_test.cc index cd9944e0d7cc..bda9295769cc 100644 --- a/test/common/http/conn_manager_impl_test.cc +++ b/test/common/http/conn_manager_impl_test.cc @@ -2656,7 +2656,8 @@ TEST_F(HttpConnectionManagerImplTest, RequestTimeoutCallbackDisarmsAndReturns408 EXPECT_CALL(response_encoder_, encodeData(_, true)).WillOnce(AddBufferToString(&response_body)); conn_manager_->newStream(response_encoder_); - EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, popTrackedObject(_)); request_timer->invokeCallback(); return Http::okStatus(); })); @@ -2886,7 +2887,8 @@ TEST_F(HttpConnectionManagerImplTest, RequestHeaderTimeoutCallbackDisarmsAndRetu EXPECT_CALL(*request_header_timer, enableTimer(request_headers_timeout_, _)); conn_manager_->newStream(response_encoder_); - EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, popTrackedObject(_)); return Http::okStatus(); })); @@ -2978,77 +2980,6 @@ TEST_F(HttpConnectionManagerImplTest, Http10Rejected) { conn_manager_->onData(fake_input, false); } -TEST_F(HttpConnectionManagerImplTest, Http10ConnCloseLegacy) { - http1_settings_.accept_http_10_ = true; - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.fixed_connection_close", "false"}}); - setup(false, ""); - EXPECT_CALL(*codec_, protocol()).Times(AnyNumber()).WillRepeatedly(Return(Protocol::Http10)); - EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance& data) -> Http::Status { - decoder_ = &conn_manager_->newStream(response_encoder_); - RequestHeaderMapPtr headers{ - new TestRequestHeaderMapImpl{{":authority", "host:80"}, {":method", "CONNECT"}}}; - decoder_->decodeHeaders(std::move(headers), true); - data.drain(4); - return Http::okStatus(); - })); - - EXPECT_CALL(response_encoder_, encodeHeaders(_, true)) - .WillOnce(Invoke([](const ResponseHeaderMap& headers, bool) -> void { - EXPECT_EQ("close", headers.getConnectionValue()); - })); - - Buffer::OwnedImpl fake_input("1234"); - conn_manager_->onData(fake_input, false); -} - -TEST_F(HttpConnectionManagerImplTest, ProxyConnectLegacyClose) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.fixed_connection_close", "false"}}); - setup(false, ""); - EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance& data) -> Http::Status { - decoder_ = &conn_manager_->newStream(response_encoder_); - RequestHeaderMapPtr headers{new TestRequestHeaderMapImpl{ - {":authority", "host:80"}, {":method", "CONNECT"}, {"proxy-connection", "close"}}}; - decoder_->decodeHeaders(std::move(headers), true); - data.drain(4); - return Http::okStatus(); - })); - - EXPECT_CALL(response_encoder_, encodeHeaders(_, true)) - .WillOnce(Invoke([](const ResponseHeaderMap& headers, bool) -> void { - EXPECT_EQ("close", headers.getConnectionValue()); - })); - - Buffer::OwnedImpl fake_input("1234"); - conn_manager_->onData(fake_input, false); -} - -TEST_F(HttpConnectionManagerImplTest, ConnectLegacyClose) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.fixed_connection_close", "false"}}); - setup(false, ""); - EXPECT_CALL(*codec_, dispatch(_)).WillOnce(Invoke([&](Buffer::Instance& data) -> Http::Status { - decoder_ = &conn_manager_->newStream(response_encoder_); - RequestHeaderMapPtr headers{new TestRequestHeaderMapImpl{ - {":authority", "host"}, {":method", "CONNECT"}, {"connection", "close"}}}; - decoder_->decodeHeaders(std::move(headers), true); - data.drain(4); - return Http::okStatus(); - })); - - EXPECT_CALL(response_encoder_, encodeHeaders(_, true)) - .WillOnce(Invoke([](const ResponseHeaderMap& headers, bool) -> void { - EXPECT_EQ("close", headers.getConnectionValue()); - })); - - Buffer::OwnedImpl fake_input("1234"); - conn_manager_->onData(fake_input, false); -} - TEST_F(HttpConnectionManagerImplTest, MaxStreamDurationCallbackNotCalledIfResetStreamValidly) { max_stream_duration_ = std::chrono::milliseconds(5000); setup(false, ""); diff --git a/test/common/http/conn_manager_impl_test_2.cc b/test/common/http/conn_manager_impl_test_2.cc index 76495244f508..b7dfd0298ab9 100644 --- a/test/common/http/conn_manager_impl_test_2.cc +++ b/test/common/http/conn_manager_impl_test_2.cc @@ -2348,9 +2348,10 @@ TEST_F(HttpConnectionManagerImplTest, TestSessionTrace) { { RequestHeaderMapPtr headers{ new TestRequestHeaderMapImpl{{":authority", "host"}, {":path", "/"}, {":method", "POST"}}}; - EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, setTrackedObject(_)) - .Times(2) - .WillOnce(Invoke([](const ScopeTrackedObject* object) -> const ScopeTrackedObject* { + + EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, pushTrackedObject(_)) + .Times(1) + .WillOnce(Invoke([](const ScopeTrackedObject* object) -> void { ASSERT(object != nullptr); // On the first call, this should be the active stream. std::stringstream out; object->dumpState(out); @@ -2358,9 +2359,8 @@ TEST_F(HttpConnectionManagerImplTest, TestSessionTrace) { EXPECT_THAT(state, testing::HasSubstr("filter_manager_callbacks_.requestHeaders(): null")); EXPECT_THAT(state, testing::HasSubstr("protocol_: 1")); - return nullptr; - })) - .WillRepeatedly(Return(nullptr)); + })); + EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, popTrackedObject(_)); EXPECT_CALL(*decoder_filters_[0], decodeHeaders(_, false)) .WillOnce(Invoke([](HeaderMap&, bool) -> FilterHeadersStatus { return FilterHeadersStatus::StopIteration; @@ -2371,9 +2371,9 @@ TEST_F(HttpConnectionManagerImplTest, TestSessionTrace) { // Send trailers to that stream, and verify by this point headers are in logged state. { RequestTrailerMapPtr trailers{new TestRequestTrailerMapImpl{{"foo", "bar"}}}; - EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, setTrackedObject(_)) - .Times(2) - .WillOnce(Invoke([](const ScopeTrackedObject* object) -> const ScopeTrackedObject* { + EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, pushTrackedObject(_)) + .Times(1) + .WillOnce(Invoke([](const ScopeTrackedObject* object) -> void { ASSERT(object != nullptr); // On the first call, this should be the active stream. std::stringstream out; object->dumpState(out); @@ -2381,9 +2381,8 @@ TEST_F(HttpConnectionManagerImplTest, TestSessionTrace) { EXPECT_THAT(state, testing::HasSubstr("filter_manager_callbacks_.requestHeaders(): \n")); EXPECT_THAT(state, testing::HasSubstr("':authority', 'host'\n")); EXPECT_THAT(state, testing::HasSubstr("protocol_: 1")); - return nullptr; - })) - .WillRepeatedly(Return(nullptr)); + })); + EXPECT_CALL(filter_callbacks_.connection_.dispatcher_, popTrackedObject(_)); EXPECT_CALL(*decoder_filters_[0], decodeComplete()); EXPECT_CALL(*decoder_filters_[0], decodeTrailers(_)) .WillOnce(Return(FilterTrailersStatus::StopIteration)); diff --git a/test/common/http/conn_manager_impl_test_base.cc b/test/common/http/conn_manager_impl_test_base.cc index 973e67bf5949..3407d3c6faed 100644 --- a/test/common/http/conn_manager_impl_test_base.cc +++ b/test/common/http/conn_manager_impl_test_base.cc @@ -53,7 +53,7 @@ void HttpConnectionManagerImplTest::setup(bool ssl, const std::string& server_na server_name_ = server_name; ON_CALL(filter_callbacks_.connection_, ssl()).WillByDefault(Return(ssl_connection_)); ON_CALL(Const(filter_callbacks_.connection_), ssl()).WillByDefault(Return(ssl_connection_)); - ON_CALL(overload_manager_.overload_state_, createScaledTypedTimer_) + ON_CALL(filter_callbacks_.connection_.dispatcher_, createScaledTypedTimer_) .WillByDefault([&](auto, auto callback) { return filter_callbacks_.connection_.dispatcher_.createTimer(callback).release(); }); diff --git a/test/common/http/conn_manager_utility_test.cc b/test/common/http/conn_manager_utility_test.cc index d97ded1d529f..f21e49898cdf 100644 --- a/test/common/http/conn_manager_utility_test.cc +++ b/test/common/http/conn_manager_utility_test.cc @@ -801,42 +801,6 @@ TEST_F(ConnectionManagerUtilityTest, ClearUpgradeHeadersForNonUpgradeRequests) { } } -TEST_F(ConnectionManagerUtilityTest, ClearUpgradeHeadersForNonUpgradeRequestsLegacy) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.fix_upgrade_response", "false"}}); - - // Test with the request headers not valid upgrade headers - { - TestRequestHeaderMapImpl request_headers{{"upgrade", "foo"}}; - TestResponseHeaderMapImpl response_headers{{"connection", "upgrade"}, - {"transfer-encoding", "eep"}, - {"upgrade", "foo"}, - {"custom_header", "custom_value"}}; - EXPECT_FALSE(Utility::isUpgrade(request_headers)); - EXPECT_TRUE(Utility::isUpgrade(response_headers)); - ConnectionManagerUtility::mutateResponseHeaders(response_headers, &request_headers, config_, - ""); - - EXPECT_EQ(2UL, response_headers.size()) << response_headers; - EXPECT_EQ("custom_value", response_headers.get_("custom_header")); - EXPECT_EQ("foo", response_headers.get_("upgrade")); - } - - // Test with the response headers not valid upgrade headers - { - TestRequestHeaderMapImpl request_headers{{"connection", "UpGrAdE"}, {"upgrade", "foo"}}; - TestResponseHeaderMapImpl response_headers{{"transfer-encoding", "foo"}, {"upgrade", "bar"}}; - EXPECT_TRUE(Utility::isUpgrade(request_headers)); - EXPECT_FALSE(Utility::isUpgrade(response_headers)); - ConnectionManagerUtility::mutateResponseHeaders(response_headers, &request_headers, config_, - ""); - - EXPECT_EQ(1UL, response_headers.size()) << response_headers; - EXPECT_EQ("bar", response_headers.get_("upgrade")); - } -} - // Test that we correctly return x-request-id if we were requested to force a trace. TEST_F(ConnectionManagerUtilityTest, MutateResponseHeadersReturnXRequestId) { TestResponseHeaderMapImpl response_headers; diff --git a/test/common/http/filter_manager_test.cc b/test/common/http/filter_manager_test.cc index 85d755e864cb..2b18706e6733 100644 --- a/test/common/http/filter_manager_test.cc +++ b/test/common/http/filter_manager_test.cc @@ -6,6 +6,7 @@ #include "common/http/filter_manager.h" #include "common/matcher/exact_map_matcher.h" +#include "common/matcher/matcher.h" #include "common/stream_info/filter_state_impl.h" #include "common/stream_info/stream_info_impl.h" @@ -155,6 +156,28 @@ Matcher::MatchTreeSharedPtr createRequestMatchingTree() { return tree; } +struct TestAction : Matcher::ActionBase {}; + +Matcher::MatchTreeSharedPtr createRequestMatchingTreeCustomAction() { + auto tree = std::make_shared>( + std::make_unique("match-header"), absl::nullopt); + + tree->addChild("match", Matcher::OnMatch{ + []() { return std::make_unique(); }, nullptr}); + + return tree; +} + +Matcher::MatchTreeSharedPtr createResponseMatchingTreeCustomAction() { + auto tree = std::make_shared>( + std::make_unique("match-header"), absl::nullopt); + + tree->addChild("match", Matcher::OnMatch{ + []() { return std::make_unique(); }, nullptr}); + + return tree; +} + Matcher::MatchTreeSharedPtr createRequestAndResponseMatchingTree() { auto tree = std::make_shared>( std::make_unique("match-header"), absl::nullopt); @@ -198,7 +221,8 @@ TEST_F(FilterManagerTest, MatchTreeSkipActionDecodingHeaders) { TEST_F(FilterManagerTest, MatchTreeSkipActionRequestAndResponseHeaders) { initialize(); - EXPECT_CALL(dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(dispatcher_, popTrackedObject(_)); // This stream filter will skip further callbacks once it sees both the request and response // header. As such, it should see the decoding callbacks but none of the encoding callbacks. @@ -249,6 +273,78 @@ TEST_F(FilterManagerTest, MatchTreeSkipActionRequestAndResponseHeaders) { filter_manager_->decodeData(data, true); filter_manager_->destroyFilters(); } + +// Verify that we propagate custom match actions to a decoding filter. +TEST_F(FilterManagerTest, MatchTreeFilterActionDecodingHeaders) { + initialize(); + + std::shared_ptr decoder_filter(new MockStreamDecoderFilter()); + EXPECT_CALL(*decoder_filter, setDecoderFilterCallbacks(_)); + EXPECT_CALL(*decoder_filter, onMatchCallback(_)); + EXPECT_CALL(*decoder_filter, decodeHeaders(_, _)); + EXPECT_CALL(*decoder_filter, decodeComplete()); + EXPECT_CALL(*decoder_filter, onDestroy()); + + EXPECT_CALL(filter_factory_, createFilterChain(_)) + .WillRepeatedly(Invoke([&](FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamDecoderFilter(decoder_filter, createRequestMatchingTreeCustomAction()); + })); + + RequestHeaderMapPtr grpc_headers{ + new TestRequestHeaderMapImpl{{":authority", "host"}, + {":path", "/"}, + {":method", "GET"}, + {"match-header", "match"}, + {"content-type", "application/grpc"}}}; + + ON_CALL(filter_manager_callbacks_, requestHeaders()) + .WillByDefault(Return(makeOptRef(*grpc_headers))); + filter_manager_->createFilterChain(); + + filter_manager_->requestHeadersInitialized(); + filter_manager_->decodeHeaders(*grpc_headers, true); + filter_manager_->destroyFilters(); +} + +// Verify that we propagate custom match actions exactly once to a dual filter. +TEST_F(FilterManagerTest, MatchTreeFilterActionDualFilter) { + initialize(); + + std::shared_ptr filter(new MockStreamFilter()); + EXPECT_CALL(*filter, setDecoderFilterCallbacks(_)); + EXPECT_CALL(*filter, setEncoderFilterCallbacks(_)); + EXPECT_CALL(*filter, decodeHeaders(_, true)) + .WillOnce(Invoke([&](auto&, bool) -> FilterHeadersStatus { + ResponseHeaderMapPtr headers{new TestResponseHeaderMapImpl{ + {":status", "200"}, {"match-header", "match"}, {"content-type", "application/grpc"}}}; + filter->decoder_callbacks_->encodeHeaders(std::move(headers), true, "details"); + + return FilterHeadersStatus::StopIteration; + })); + EXPECT_CALL(*filter, onDestroy()); + + EXPECT_CALL(filter_factory_, createFilterChain(_)) + .WillRepeatedly(Invoke([&](FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamFilter(filter, createResponseMatchingTreeCustomAction()); + })); + + RequestHeaderMapPtr grpc_headers{ + new TestRequestHeaderMapImpl{{":authority", "host"}, + {":path", "/"}, + {":method", "GET"}, + {"match-header", "match"}, + {"content-type", "application/grpc"}}}; + + ON_CALL(filter_manager_callbacks_, requestHeaders()) + .WillByDefault(Return(makeOptRef(*grpc_headers))); + filter_manager_->createFilterChain(); + + filter_manager_->requestHeadersInitialized(); + EXPECT_CALL(*filter, encodeHeaders(_, true)); + EXPECT_CALL(*filter, onMatchCallback(_)); + filter_manager_->decodeHeaders(*grpc_headers, true); + filter_manager_->destroyFilters(); +} } // namespace } // namespace Http -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/test/common/http/header_utility_test.cc b/test/common/http/header_utility_test.cc index 1d6548a29211..ad7bb071dd40 100644 --- a/test/common/http/header_utility_test.cc +++ b/test/common/http/header_utility_test.cc @@ -692,5 +692,23 @@ TEST(PercentEncoding, ShouldCloseConnection) { Protocol::Http11, TestRequestHeaderMapImpl{{"proxy-connection", "foo,close"}})); } +TEST(RequiredHeaders, IsRemovableHeader) { + EXPECT_FALSE(HeaderUtility::isRemovableHeader(":path")); + EXPECT_FALSE(HeaderUtility::isRemovableHeader("host")); + EXPECT_FALSE(HeaderUtility::isRemovableHeader("Host")); + EXPECT_TRUE(HeaderUtility::isRemovableHeader("")); + EXPECT_TRUE(HeaderUtility::isRemovableHeader("hostname")); + EXPECT_TRUE(HeaderUtility::isRemovableHeader("Content-Type")); +} + +TEST(RequiredHeaders, IsModifiableHeader) { + EXPECT_FALSE(HeaderUtility::isRemovableHeader(":path")); + EXPECT_FALSE(HeaderUtility::isRemovableHeader("host")); + EXPECT_FALSE(HeaderUtility::isRemovableHeader("Host")); + EXPECT_TRUE(HeaderUtility::isRemovableHeader("")); + EXPECT_TRUE(HeaderUtility::isRemovableHeader("hostname")); + EXPECT_TRUE(HeaderUtility::isRemovableHeader("Content-Type")); +} + } // namespace Http } // namespace Envoy diff --git a/test/common/http/http1/codec_impl_test.cc b/test/common/http/http1/codec_impl_test.cc index c2eb068cf3f5..e20a89f2d7b0 100644 --- a/test/common/http/http1/codec_impl_test.cc +++ b/test/common/http/http1/codec_impl_test.cc @@ -930,24 +930,6 @@ TEST_F(Http1ServerConnectionImplTest, SimpleGet) { EXPECT_EQ(0U, buffer.length()); } -TEST_F(Http1ServerConnectionImplTest, BadRequestNoStreamLegacy) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.early_errors_via_hcm", "false"}}); - initialize(); - - std::string output; - ON_CALL(connection_, write(_, _)).WillByDefault(AddBufferToString(&output)); - - MockRequestDecoder decoder; - EXPECT_CALL(callbacks_, newStream(_, _)).Times(0); - EXPECT_CALL(decoder, sendLocalReply(_, _, _, _, _, _)).Times(0); - - Buffer::OwnedImpl buffer("bad"); - auto status = codec_->dispatch(buffer); - EXPECT_TRUE(isCodecProtocolError(status)); -} - // Test that if the stream is not created at the time an error is detected, it // is created as part of sending the protocol error. TEST_F(Http1ServerConnectionImplTest, BadRequestNoStream) { diff --git a/test/common/http/http1/conn_pool_test.cc b/test/common/http/http1/conn_pool_test.cc index cd31d3ca441c..2f9814b1b5e0 100644 --- a/test/common/http/http1/conn_pool_test.cc +++ b/test/common/http/http1/conn_pool_test.cc @@ -822,46 +822,6 @@ TEST_F(Http1ConnPoolImplTest, ProxyConnectionCloseHeader) { EXPECT_EQ(0U, cluster_->stats_.upstream_cx_destroy_with_active_rq_.value()); } -/** - * Test legacy behavior when upstream sends us 'proxy-connection: close' - */ -TEST_F(Http1ConnPoolImplTest, ProxyConnectionCloseHeaderLegacy) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.fixed_connection_close", "false"}}); - InSequence s; - - // Request 1 should kick off a new connection. - NiceMock outer_decoder; - ConnPoolCallbacks callbacks; - conn_pool_->expectClientCreate(); - Http::ConnectionPool::Cancellable* handle = conn_pool_->newStream(outer_decoder, callbacks); - - EXPECT_NE(nullptr, handle); - - NiceMock request_encoder; - ResponseDecoder* inner_decoder; - EXPECT_CALL(*conn_pool_->test_clients_[0].codec_, newStream(_)) - .WillOnce(DoAll(SaveArgAddress(&inner_decoder), ReturnRef(request_encoder))); - EXPECT_CALL(callbacks.pool_ready_, ready()); - - conn_pool_->test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::Connected); - EXPECT_TRUE( - callbacks.outer_encoder_ - ->encodeHeaders(TestRequestHeaderMapImpl{{":path", "/"}, {":method", "GET"}}, true) - .ok()); - - // Response with 'proxy-connection: close' which should cause the connection to go away, even if - // there are other tokens in that header. - EXPECT_CALL(*conn_pool_, onClientDestroy()); - ResponseHeaderMapPtr response_headers( - new TestResponseHeaderMapImpl{{":status", "200"}, {"Proxy-Connection", "Close"}}); - inner_decoder->decodeHeaders(std::move(response_headers), true); - dispatcher_.clearDeferredDeleteList(); - - EXPECT_EQ(0U, cluster_->stats_.upstream_cx_destroy_with_active_rq_.value()); -} - /** * Test when upstream is HTTP/1.0 and does not send 'connection: keep-alive' */ @@ -898,45 +858,6 @@ TEST_F(Http1ConnPoolImplTest, Http10NoConnectionKeepAlive) { EXPECT_EQ(0U, cluster_->stats_.upstream_cx_destroy_with_active_rq_.value()); } -/** - * Test legacy behavior when upstream is HTTP/1.0 and does not send 'connection: keep-alive' - */ -TEST_F(Http1ConnPoolImplTest, Http10NoConnectionKeepAliveLegacy) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.fixed_connection_close", "false"}}); - InSequence s; - - // Request 1 should kick off a new connection. - NiceMock outer_decoder; - ConnPoolCallbacks callbacks; - conn_pool_->expectClientCreate(Protocol::Http10); - Http::ConnectionPool::Cancellable* handle = conn_pool_->newStream(outer_decoder, callbacks); - - EXPECT_NE(nullptr, handle); - - NiceMock request_encoder; - ResponseDecoder* inner_decoder; - EXPECT_CALL(*conn_pool_->test_clients_[0].codec_, newStream(_)) - .WillOnce(DoAll(SaveArgAddress(&inner_decoder), ReturnRef(request_encoder))); - EXPECT_CALL(callbacks.pool_ready_, ready()); - - conn_pool_->test_clients_[0].connection_->raiseEvent(Network::ConnectionEvent::Connected); - EXPECT_TRUE( - callbacks.outer_encoder_ - ->encodeHeaders(TestRequestHeaderMapImpl{{":path", "/"}, {":method", "GET"}}, true) - .ok()); - - // Response without 'connection: keep-alive' which should cause the connection to go away. - EXPECT_CALL(*conn_pool_, onClientDestroy()); - ResponseHeaderMapPtr response_headers( - new TestResponseHeaderMapImpl{{":protocol", "HTTP/1.0"}, {":status", "200"}}); - inner_decoder->decodeHeaders(std::move(response_headers), true); - dispatcher_.clearDeferredDeleteList(); - - EXPECT_EQ(0U, cluster_->stats_.upstream_cx_destroy_with_active_rq_.value()); -} - /** * Test when we reach max requests per connection. */ diff --git a/test/common/http/http2/codec_impl_test.cc b/test/common/http/http2/codec_impl_test.cc index a8ab0eaf146d..3daec9a7d43f 100644 --- a/test/common/http/http2/codec_impl_test.cc +++ b/test/common/http/http2/codec_impl_test.cc @@ -349,6 +349,24 @@ TEST_P(Http2CodecImplTest, ShutdownNotice) { response_encoder_->encodeHeaders(response_headers, true); } +TEST_P(Http2CodecImplTest, ProtocolErrorForTest) { + initialize(); + EXPECT_EQ(absl::nullopt, request_encoder_->http1StreamEncoderOptions()); + + TestRequestHeaderMapImpl request_headers; + HttpTestUtility::addDefaultHeaders(request_headers); + EXPECT_CALL(request_decoder_, decodeHeaders_(_, true)); + EXPECT_TRUE(request_encoder_->encodeHeaders(request_headers, true).ok()); + + EXPECT_CALL(client_callbacks_, onGoAway(Http::GoAwayErrorCode::Other)); + + // We have to dynamic cast because protocolErrorForTest() is intentionally not on the + // Connection API. + ServerConnectionImpl* raw_server = dynamic_cast(server_.get()); + ASSERT(raw_server != nullptr); + EXPECT_EQ(StatusCode::CodecProtocolError, getStatusCode(raw_server->protocolErrorForTest())); +} + // 100 response followed by 200 results in a [decode100ContinueHeaders, decodeHeaders] sequence. TEST_P(Http2CodecImplTest, ContinueHeaders) { initialize(); diff --git a/test/common/http/http2/hpack_corpus/use_after_free b/test/common/http/http2/hpack_corpus/use_after_free new file mode 100644 index 000000000000..0d0ddd84ec63 --- /dev/null +++ b/test/common/http/http2/hpack_corpus/use_after_free @@ -0,0 +1 @@ +headers { headers { key: ":path" value: " " } headers { key: " " e: " " } } \ No newline at end of file diff --git a/test/common/http/http2/hpack_corpus/whitespace b/test/common/http/http2/hpack_corpus/whitespace new file mode 100644 index 000000000000..64b25dbdeae3 --- /dev/null +++ b/test/common/http/http2/hpack_corpus/whitespace @@ -0,0 +1,26 @@ +headers { + headers { + key: ":path" + value: ":path" + } + headers { + key: "GET" + value: "\364\214\214\214\364\214\214\214\364\214\214\214\364\214\214\214\364\214\214\214tqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq\361\214\214\214\364\214\214\214\354\214\214" + } + headers { + key: "YYYYYYYYYYYYYYY" + value: "htvp" + } + headers { + key: "x-envoy" + value: " " + } + headers { + key: ":status" + value: "a " + } + headers { + key: "header" + value: " a" + } +} \ No newline at end of file diff --git a/test/common/http/http2/hpack_fuzz_test.cc b/test/common/http/http2/hpack_fuzz_test.cc index 6cab23df21f4..49fa83db3dbc 100644 --- a/test/common/http/http2/hpack_fuzz_test.cc +++ b/test/common/http/http2/hpack_fuzz_test.cc @@ -8,6 +8,7 @@ #include "test/test_common/utility.h" #include "absl/container/fixed_array.h" +#include "absl/strings/escaping.h" #include "nghttp2/nghttp2.h" namespace Envoy { @@ -25,6 +26,8 @@ std::vector createNameValueArray(const test::fuzz::Headers& input) { for (const auto& header : input.headers()) { // TODO(asraa): Consider adding flags in fuzzed input. const uint8_t flags = 0; + ENVOY_LOG_MISC(trace, "encoding: {} {}", absl::CEscape(header.key()), + absl::CEscape(header.value())); nva[i++] = {const_cast(reinterpret_cast(header.key().data())), const_cast(reinterpret_cast(header.value().data())), header.key().size(), header.value().size(), flags}; @@ -56,18 +59,18 @@ Buffer::OwnedImpl encodeHeaders(nghttp2_hd_deflater* deflater, return payload; } -std::vector decodeHeaders(nghttp2_hd_inflater* inflater, - const Buffer::OwnedImpl& payload, bool end_headers) { +std::vector> +decodeHeaders(nghttp2_hd_inflater* inflater, const Buffer::OwnedImpl& payload, bool end_headers) { // Decode using nghttp2 Buffer::RawSliceVector slices = payload.getRawSlices(); const int num_slices = slices.size(); ASSERT(num_slices == 1, absl::StrCat("number of slices ", num_slices)); - std::vector decoded_headers; + std::vector> decoded_headers; int inflate_flags = 0; - nghttp2_nv decoded_nv; + nghttp2_nv nv; while (slices[0].len_ > 0) { - ssize_t result = nghttp2_hd_inflate_hd2(inflater, &decoded_nv, &inflate_flags, + ssize_t result = nghttp2_hd_inflate_hd2(inflater, &nv, &inflate_flags, reinterpret_cast(slices[0].mem_), slices[0].len_, end_headers); // Decoding should not fail and data should not be left in slice. @@ -78,7 +81,10 @@ std::vector decodeHeaders(nghttp2_hd_inflater* inflater, if (inflate_flags & NGHTTP2_HD_INFLATE_EMIT) { // One header key value pair has been successfully decoded. - decoded_headers.push_back(decoded_nv); + decoded_headers.push_back({std::string(reinterpret_cast(nv.name), nv.namelen), + std::string(reinterpret_cast(nv.value), nv.valuelen)}); + ENVOY_LOG_MISC(trace, "decoded: {}, {}", absl::CEscape(decoded_headers.back().first), + absl::CEscape(decoded_headers.back().second)); } } @@ -90,10 +96,15 @@ std::vector decodeHeaders(nghttp2_hd_inflater* inflater, } struct NvComparator { - inline bool operator()(const nghttp2_nv& a, const nghttp2_nv& b) { - absl::string_view a_str(reinterpret_cast(a.name), a.namelen); - absl::string_view b_str(reinterpret_cast(b.name), b.namelen); - return a_str.compare(b_str); + inline bool operator()(const nghttp2_nv& a, const nghttp2_nv& b) const { + absl::string_view a_name(reinterpret_cast(a.name), a.namelen); + absl::string_view b_name(reinterpret_cast(b.name), b.namelen); + if (a_name != b_name) { + return a_name < b_name; + } + absl::string_view a_val(reinterpret_cast(a.value), a.valuelen); + absl::string_view b_val(reinterpret_cast(b.value), b.valuelen); + return a_val < b_val; } }; @@ -108,8 +119,8 @@ DEFINE_PROTO_FUZZER(const test::common::http::http2::HpackTestCase& input) { // Create name value pairs from headers. std::vector input_nv = createNameValueArray(input.headers()); - // Skip encoding empty headers. nghttp2 will throw a nullptr error on runtime if it receives a - // nullptr input. + // Skip encoding an empty header map. nghttp2 will throw a nullptr error on runtime if it receives + // a nullptr input. if (!input_nv.data()) { return; } @@ -127,18 +138,19 @@ DEFINE_PROTO_FUZZER(const test::common::http::http2::HpackTestCase& input) { ASSERT(!payload.getRawSlices().empty()); // Decode headers with nghttp2 - std::vector output_nv = decodeHeaders(inflater, payload, input.end_headers()); + std::vector> output_nv = + decodeHeaders(inflater, payload, input.end_headers()); // Verify that decoded == encoded. ASSERT(input_nv.size() == output_nv.size()); std::sort(input_nv.begin(), input_nv.end(), NvComparator()); - std::sort(output_nv.begin(), output_nv.end(), NvComparator()); + std::sort(output_nv.begin(), output_nv.end()); + for (size_t i = 0; i < input_nv.size(); i++) { - absl::string_view in_name = {reinterpret_cast(input_nv[i].name), input_nv[i].namelen}; - absl::string_view out_name = {reinterpret_cast(output_nv[i].name), output_nv[i].namelen}; - absl::string_view in_val = {reinterpret_cast(input_nv[i].value), input_nv[i].valuelen}; - absl::string_view out_val = {reinterpret_cast(output_nv[i].value), - output_nv[i].valuelen}; + std::string in_name = {reinterpret_cast(input_nv[i].name), input_nv[i].namelen}; + std::string out_name = output_nv[i].first; + std::string in_val = {reinterpret_cast(input_nv[i].value), input_nv[i].valuelen}; + std::string out_val = output_nv[i].second; ASSERT(in_name == out_name); ASSERT(in_val == out_val); } diff --git a/test/common/http/match_wrapper/BUILD b/test/common/http/match_wrapper/BUILD new file mode 100644 index 000000000000..52425ab72baa --- /dev/null +++ b/test/common/http/match_wrapper/BUILD @@ -0,0 +1,19 @@ +load( + "//bazel:envoy_build_system.bzl", + "envoy_cc_test", + "envoy_package", +) + +licenses(["notice"]) # Apache 2 + +envoy_package() + +envoy_cc_test( + name = "config_test", + srcs = ["config_test.cc"], + deps = [ + "//source/common/http/match_wrapper:config", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:registry_lib", + ], +) diff --git a/test/common/http/match_wrapper/config_test.cc b/test/common/http/match_wrapper/config_test.cc new file mode 100644 index 000000000000..d29207ff3c6e --- /dev/null +++ b/test/common/http/match_wrapper/config_test.cc @@ -0,0 +1,91 @@ +#include "envoy/http/filter.h" +#include "envoy/server/factory_context.h" +#include "envoy/server/filter_config.h" + +#include "common/http/match_wrapper/config.h" + +#include "test/mocks/server/factory_context.h" +#include "test/test_common/registry.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Common { +namespace Http { +namespace MatchWrapper { +namespace { + +struct TestFactory : public Envoy::Server::Configuration::NamedHttpFilterConfigFactory { + std::string name() const override { return "test"; } + ProtobufTypes::MessagePtr createEmptyConfigProto() override { + return std::make_unique(); + } + Envoy::Http::FilterFactoryCb + createFilterFactoryFromProto(const Protobuf::Message&, const std::string&, + Server::Configuration::FactoryContext&) override { + return [](auto& callbacks) { + callbacks.addStreamDecoderFilter(nullptr); + callbacks.addStreamEncoderFilter(nullptr); + callbacks.addStreamFilter(nullptr); + + callbacks.addStreamDecoderFilter(nullptr, nullptr); + callbacks.addStreamEncoderFilter(nullptr, nullptr); + callbacks.addStreamFilter(nullptr, nullptr); + + callbacks.addAccessLogHandler(nullptr); + }; + } +}; + +TEST(MatchWrapper, WithMatcher) { + TestFactory test_factory; + Envoy::Registry::InjectFactory + inject_factory(test_factory); + + NiceMock factory_context; + + const auto config = + TestUtility::parseYaml(R"EOF( +extension_config: + name: test + typed_config: + "@type": type.googleapis.com/google.protobuf.StringValue +matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: default-matcher-header + exact_match_map: + map: + match: + action: + name: skip + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.common.matcher.action.v3.SkipFilter +)EOF"); + + MatchWrapperConfig match_wrapper_config; + auto cb = match_wrapper_config.createFilterFactoryFromProto(config, "", factory_context); + + Envoy::Http::MockFilterChainFactoryCallbacks factory_callbacks; + testing::InSequence s; + + // This matches the sequence of calls in the filter factory above: the ones that call the overload + // without a match tree has a match tree added, the other one does not. + EXPECT_CALL(factory_callbacks, addStreamDecoderFilter(_, testing::NotNull())); + EXPECT_CALL(factory_callbacks, addStreamEncoderFilter(_, testing::NotNull())); + EXPECT_CALL(factory_callbacks, addStreamFilter(_, testing::NotNull())); + EXPECT_CALL(factory_callbacks, addStreamDecoderFilter(_, testing::IsNull())); + EXPECT_CALL(factory_callbacks, addStreamEncoderFilter(_, testing::IsNull())); + EXPECT_CALL(factory_callbacks, addStreamFilter(_, testing::IsNull())); + EXPECT_CALL(factory_callbacks, addAccessLogHandler(_)); + cb(factory_callbacks); +} + +} // namespace +} // namespace MatchWrapper +} // namespace Http +} // namespace Common +} // namespace Envoy \ No newline at end of file diff --git a/test/common/matcher/test_utility.h b/test/common/matcher/test_utility.h index 623d8158c55f..1348d9cd80d4 100644 --- a/test/common/matcher/test_utility.h +++ b/test/common/matcher/test_utility.h @@ -1,6 +1,7 @@ #pragma once #include "envoy/matcher/matcher.h" +#include "envoy/protobuf/message_validator.h" #include "common/matcher/matcher.h" @@ -29,7 +30,8 @@ class TestDataInputFactory : public DataInputFactory { TestDataInputFactory(absl::string_view factory_name, absl::string_view data) : factory_name_(std::string(factory_name)), value_(std::string(data)), injection_(*this) {} - DataInputPtr createDataInput(const Protobuf::Message&) override { + DataInputPtr createDataInput(const Protobuf::Message&, + ProtobufMessage::ValidationVisitor&) override { return std::make_unique( DataInputGetResult{DataInputGetResult::DataAvailability::AllDataAvailable, value_}); } diff --git a/test/common/network/BUILD b/test/common/network/BUILD index 789915d03cc1..539ba78aff39 100644 --- a/test/common/network/BUILD +++ b/test/common/network/BUILD @@ -86,6 +86,7 @@ envoy_cc_test( "//test/mocks/api:api_mocks", "//test/mocks/buffer:buffer_mocks", "//test/mocks/event:event_mocks", + "//test/mocks/network:io_handle_mocks", "//test/mocks/network:network_mocks", "//test/mocks/stats:stats_mocks", "//test/test_common:environment_lib", diff --git a/test/common/network/connection_impl_test.cc b/test/common/network/connection_impl_test.cc index 63361228acbe..c1c927a26ed4 100644 --- a/test/common/network/connection_impl_test.cc +++ b/test/common/network/connection_impl_test.cc @@ -122,6 +122,7 @@ class TestClientConnectionImpl : public Network::ClientConnectionImpl { public: using ClientConnectionImpl::ClientConnectionImpl; Buffer::Instance& readBuffer() { return *read_buffer_; } + ConnectionSocketPtr& socket() { return socket_; } }; class ConnectionImplTest : public testing::TestWithParam { @@ -399,11 +400,13 @@ TEST_P(ConnectionImplTest, SetServerTransportSocketTimeout) { ConnectionMocks mocks = createConnectionMocks(false); MockTransportSocket* transport_socket = mocks.transport_socket_.get(); IoHandlePtr io_handle = std::make_unique(0); + // Avoid setting noDelay on the fake fd of 0. + auto local_addr = std::make_shared("/pipe/path"); auto* mock_timer = new NiceMock(mocks.dispatcher_.get()); auto server_connection = std::make_unique( *mocks.dispatcher_, - std::make_unique(std::move(io_handle), nullptr, nullptr), + std::make_unique(std::move(io_handle), local_addr, nullptr), std::move(mocks.transport_socket_), stream_info_, true); EXPECT_CALL(*mock_timer, enableTimer(std::chrono::milliseconds(3 * 1000), _)); @@ -418,10 +421,11 @@ TEST_P(ConnectionImplTest, SetServerTransportSocketTimeoutAfterConnect) { ConnectionMocks mocks = createConnectionMocks(false); MockTransportSocket* transport_socket = mocks.transport_socket_.get(); IoHandlePtr io_handle = std::make_unique(0); + auto local_addr = std::make_shared("/pipe/path"); auto server_connection = std::make_unique( *mocks.dispatcher_, - std::make_unique(std::move(io_handle), nullptr, nullptr), + std::make_unique(std::move(io_handle), local_addr, nullptr), std::move(mocks.transport_socket_), stream_info_, true); transport_socket->callbacks_->raiseEvent(ConnectionEvent::Connected); @@ -436,11 +440,12 @@ TEST_P(ConnectionImplTest, ServerTransportSocketTimeoutDisabledOnConnect) { ConnectionMocks mocks = createConnectionMocks(false); MockTransportSocket* transport_socket = mocks.transport_socket_.get(); IoHandlePtr io_handle = std::make_unique(0); + auto local_addr = std::make_shared("/pipe/path"); auto* mock_timer = new NiceMock(mocks.dispatcher_.get()); auto server_connection = std::make_unique( *mocks.dispatcher_, - std::make_unique(std::move(io_handle), nullptr, nullptr), + std::make_unique(std::move(io_handle), local_addr, nullptr), std::move(mocks.transport_socket_), stream_info_, true); bool timer_destroyed = false; @@ -598,7 +603,24 @@ TEST_P(ConnectionImplTest, ConnectionStats) { MockConnectionStats client_connection_stats; client_connection_->setConnectionStats(client_connection_stats.toBufferStats()); EXPECT_TRUE(client_connection_->connecting()); + + // Make sure that NO_DELAY starts out false, so that the check below verifies that it transitions + // to true actually tests something. + int initial_value = 0; + socklen_t size = sizeof(int); + Api::SysCallIntResult result = testClientConnection()->socket()->getSocketOption( + IPPROTO_TCP, TCP_NODELAY, &initial_value, &size); + ASSERT_EQ(0, result.rc_); + ASSERT_EQ(0, initial_value); + client_connection_->connect(); + + int new_value = 0; + result = testClientConnection()->socket()->getSocketOption(IPPROTO_TCP, TCP_NODELAY, + &initial_value, &size); + ASSERT_EQ(0, result.rc_); + ASSERT_EQ(0, new_value); + // The Network::Connection class oddly uses onWrite as its indicator of if // it's done connection, rather than the Connected event. EXPECT_TRUE(client_connection_->connecting()); @@ -984,38 +1006,55 @@ TEST_P(ConnectionImplTest, ReadWatermarks) { }; EXPECT_FALSE(testClientConnection()->readBuffer().highWatermarkTriggered()); + EXPECT_FALSE(testClientConnection()->shouldDrainReadBuffer()); EXPECT_TRUE(client_connection_->readEnabled()); - // Add 4 bytes to the buffer and verify the connection becomes read disabled. + // Add 2 bytes to the buffer so that it sits at exactly the read limit. Verify that + // shouldDrainReadBuffer is true, but the connection remains read enabled. { - Buffer::OwnedImpl buffer("data"); + Buffer::OwnedImpl buffer("12"); + server_connection_->write(buffer, false); + EXPECT_CALL(*client_read_filter, onData(_, false)).WillOnce(Invoke(on_filter_data_exit)); + dispatcher_->run(Event::Dispatcher::RunType::Block); + + EXPECT_TRUE(testClientConnection()->shouldDrainReadBuffer()); + EXPECT_FALSE(testClientConnection()->readBuffer().highWatermarkTriggered()); + EXPECT_TRUE(client_connection_->readEnabled()); + } + // Add 1 bytes to the buffer to go over the high watermark. Verify the connection becomes read + // disabled. + { + Buffer::OwnedImpl buffer("3"); server_connection_->write(buffer, false); EXPECT_CALL(*client_read_filter, onData(_, false)).WillOnce(Invoke(on_filter_data_exit)); dispatcher_->run(Event::Dispatcher::RunType::Block); EXPECT_TRUE(testClientConnection()->readBuffer().highWatermarkTriggered()); + EXPECT_TRUE(testClientConnection()->shouldDrainReadBuffer()); EXPECT_FALSE(client_connection_->readEnabled()); } - // Drain 3 bytes from the buffer. This bring sit below the low watermark, and + // Drain 2 bytes from the buffer. This bring sit below the low watermark, and // read enables, as well as triggering a kick for the remaining byte. { - testClientConnection()->readBuffer().drain(3); + testClientConnection()->readBuffer().drain(2); EXPECT_FALSE(testClientConnection()->readBuffer().highWatermarkTriggered()); + EXPECT_FALSE(testClientConnection()->shouldDrainReadBuffer()); EXPECT_TRUE(client_connection_->readEnabled()); EXPECT_CALL(*client_read_filter, onData(_, false)); dispatcher_->run(Event::Dispatcher::RunType::NonBlock); } - // Add 3 bytes to the buffer and verify the connection becomes read disabled + // Add 2 bytes to the buffer and verify the connection becomes read disabled // again. { - Buffer::OwnedImpl buffer("bye"); + Buffer::OwnedImpl buffer("45"); server_connection_->write(buffer, false); EXPECT_CALL(*client_read_filter, onData(_, false)).WillOnce(Invoke(on_filter_data_exit)); dispatcher_->run(Event::Dispatcher::RunType::Block); EXPECT_TRUE(testClientConnection()->readBuffer().highWatermarkTriggered()); + EXPECT_TRUE(testClientConnection()->shouldDrainReadBuffer()); EXPECT_FALSE(client_connection_->readEnabled()); } @@ -1024,8 +1063,9 @@ TEST_P(ConnectionImplTest, ReadWatermarks) { // does not want to read. { client_connection_->readDisable(true); - testClientConnection()->readBuffer().drain(3); + testClientConnection()->readBuffer().drain(2); EXPECT_FALSE(testClientConnection()->readBuffer().highWatermarkTriggered()); + EXPECT_FALSE(testClientConnection()->shouldDrainReadBuffer()); EXPECT_FALSE(client_connection_->readEnabled()); EXPECT_CALL(*client_read_filter, onData(_, false)).Times(0); @@ -1061,6 +1101,7 @@ TEST_P(ConnectionImplTest, ReadWatermarks) { })); dispatcher_->run(Event::Dispatcher::RunType::Block); EXPECT_TRUE(testClientConnection()->readBuffer().highWatermarkTriggered()); + EXPECT_TRUE(testClientConnection()->shouldDrainReadBuffer()); EXPECT_FALSE(client_connection_->readEnabled()); // Read disable and read enable, to set dispatch_buffered_data_ true. @@ -1220,7 +1261,7 @@ TEST_P(ConnectionImplTest, WatermarkFuzzing) { // If the current bytes buffered plus the bytes we write this loop go over // the watermark and we're not currently above, we will get a callback for // going above. - if (bytes_to_write + bytes_buffered > 11 && is_below) { + if (bytes_to_write + bytes_buffered > 10 && is_below) { ENVOY_LOG_MISC(trace, "Expect onAboveWriteBufferHighWatermark"); EXPECT_CALL(client_callbacks_, onAboveWriteBufferHighWatermark()); is_below = false; @@ -1858,22 +1899,19 @@ TEST_P(ConnectionImplTest, DelayedCloseTimeoutNullStats) { } // Test DumpState methods. -TEST_P(ConnectionImplTest, NetworkSocketDumpsWithoutAllocatingMemory) { +TEST_P(ConnectionImplTest, NetworkAndPipeSocketDumpsWithoutAllocatingMemory) { std::array buffer; OutputBufferStream ostream{buffer.data(), buffer.size()}; IoHandlePtr io_handle = std::make_unique(0); + // Avoid setting noDelay on the fake fd of 0. + auto local_addr = std::make_shared("/pipe/path"); Address::InstanceConstSharedPtr server_addr; - Address::InstanceConstSharedPtr local_addr; if (GetParam() == Network::Address::IpVersion::v4) { server_addr = Network::Address::InstanceConstSharedPtr{ new Network::Address::Ipv4Instance("1.1.1.1", 80, nullptr)}; - local_addr = Network::Address::InstanceConstSharedPtr{ - new Network::Address::Ipv4Instance("1.2.3.4", 56789, nullptr)}; } else { server_addr = Network::Address::InstanceConstSharedPtr{ new Network::Address::Ipv6Instance("::1", 80, nullptr)}; - local_addr = Network::Address::InstanceConstSharedPtr{ - new Network::Address::Ipv6Instance("::1:2:3:4", 56789, nullptr)}; } auto connection_socket = @@ -1895,12 +1933,12 @@ TEST_P(ConnectionImplTest, NetworkSocketDumpsWithoutAllocatingMemory) { contents, HasSubstr( "remote_address_: 1.1.1.1:80, direct_remote_address_: 1.1.1.1:80, local_address_: " - "1.2.3.4:56789")); + "/pipe/path")); } else { EXPECT_THAT( contents, HasSubstr("remote_address_: [::1]:80, direct_remote_address_: [::1]:80, local_address_: " - "[::1:2:3:4]:56789")); + "/pipe/path")); } } @@ -1909,10 +1947,11 @@ TEST_P(ConnectionImplTest, NetworkConnectionDumpsWithoutAllocatingMemory) { OutputBufferStream ostream{buffer.data(), buffer.size()}; ConnectionMocks mocks = createConnectionMocks(false); IoHandlePtr io_handle = std::make_unique(0); + auto local_addr = std::make_shared("/pipe/path"); auto server_connection = std::make_unique( *mocks.dispatcher_, - std::make_unique(std::move(io_handle), nullptr, nullptr), + std::make_unique(std::move(io_handle), local_addr, nullptr), std::move(mocks.transport_socket_), stream_info_, true); // Start measuring memory and dump state. @@ -1978,7 +2017,8 @@ class MockTransportConnectionImplTest : public testing::Test { TransportSocketPtr(transport_socket_), stream_info_, true); connection_->addConnectionCallbacks(callbacks_); // File events will trigger setTrackedObject on the dispatcher. - EXPECT_CALL(dispatcher_, setTrackedObject(_)).WillRepeatedly(Return(nullptr)); + EXPECT_CALL(dispatcher_, pushTrackedObject(_)).Times(AnyNumber()); + EXPECT_CALL(dispatcher_, popTrackedObject(_)).Times(AnyNumber()); } ~MockTransportConnectionImplTest() override { connection_->close(ConnectionCloseType::NoFlush); } @@ -2083,17 +2123,41 @@ TEST_F(MockTransportConnectionImplTest, ReadBufferResumeAfterReadDisable) { connection_->enableHalfClose(true); connection_->addReadFilter(read_filter); - // Add some data to the read buffer to trigger read activate calls when re-enabling read. EXPECT_CALL(*transport_socket_, doRead(_)) .WillOnce(Invoke([](Buffer::Instance& buffer) -> IoResult { - buffer.add("0123456789"); - return {PostIoAction::KeepOpen, 10, false}; + buffer.add("0123"); + return {PostIoAction::KeepOpen, 4, false}; })); - // Expect a change to the event mask when hitting the read buffer high-watermark. - EXPECT_CALL(*file_event_, setEnabled(Event::FileReadyType::Write)); + // Buffer is under the read limit, expect no changes to the file event registration. + EXPECT_CALL(*file_event_, setEnabled(_)).Times(0); EXPECT_CALL(*read_filter, onNewConnection()).WillOnce(Return(FilterStatus::Continue)); EXPECT_CALL(*read_filter, onData(_, false)).WillOnce(Return(FilterStatus::Continue)); file_ready_cb_(Event::FileReadyType::Read); + EXPECT_FALSE(connection_->shouldDrainReadBuffer()); + + // Do a second read to hit the read limit. + EXPECT_CALL(*transport_socket_, doRead(_)) + .WillOnce(Invoke([](Buffer::Instance& buffer) -> IoResult { + buffer.add("4"); + return {PostIoAction::KeepOpen, 1, false}; + })); + // Buffer is exactly at the read limit, expect no changes to the file event registration. + EXPECT_CALL(*file_event_, setEnabled(_)).Times(0); + EXPECT_CALL(*read_filter, onData(_, false)).WillOnce(Return(FilterStatus::Continue)); + file_ready_cb_(Event::FileReadyType::Read); + EXPECT_TRUE(connection_->shouldDrainReadBuffer()); + + // Do a third read to trigger the high watermark. + EXPECT_CALL(*transport_socket_, doRead(_)) + .WillOnce(Invoke([](Buffer::Instance& buffer) -> IoResult { + buffer.add("5"); + return {PostIoAction::KeepOpen, 1, false}; + })); + // Expect a change to the event mask when going over the read limit. + EXPECT_CALL(*file_event_, setEnabled(Event::FileReadyType::Write)); + EXPECT_CALL(*read_filter, onData(_, false)).WillOnce(Return(FilterStatus::Continue)); + file_ready_cb_(Event::FileReadyType::Read); + EXPECT_TRUE(connection_->shouldDrainReadBuffer()); // Already read disabled, expect no changes to enabled events mask. EXPECT_CALL(*file_event_, setEnabled(_)).Times(0); @@ -2111,7 +2175,7 @@ TEST_F(MockTransportConnectionImplTest, ReadBufferResumeAfterReadDisable) { EXPECT_CALL(*transport_socket_, doRead(_)).Times(0); EXPECT_CALL(*read_filter, onData(_, _)) .WillRepeatedly(Invoke([&](Buffer::Instance& data, bool) -> FilterStatus { - EXPECT_EQ(10, data.length()); + EXPECT_EQ(6, data.length()); data.drain(data.length() - 1); return FilterStatus::Continue; })); @@ -2120,6 +2184,7 @@ TEST_F(MockTransportConnectionImplTest, ReadBufferResumeAfterReadDisable) { EXPECT_CALL(*file_event_, setEnabled(Event::FileReadyType::Read | Event::FileReadyType::Write)); EXPECT_CALL(*file_event_, activate(Event::FileReadyType::Read)); file_ready_cb_(Event::FileReadyType::Read); + EXPECT_FALSE(connection_->shouldDrainReadBuffer()); // Drain the rest of the buffer and verify there are no spurious read activate calls. EXPECT_CALL(*transport_socket_, doRead(_)) @@ -2131,6 +2196,7 @@ TEST_F(MockTransportConnectionImplTest, ReadBufferResumeAfterReadDisable) { return FilterStatus::Continue; })); file_ready_cb_(Event::FileReadyType::Read); + EXPECT_FALSE(connection_->shouldDrainReadBuffer()); EXPECT_CALL(*file_event_, setEnabled(_)); connection_->readDisable(true); diff --git a/test/common/network/listener_impl_test.cc b/test/common/network/listener_impl_test.cc index 38dc232a4b7f..e6a882a35ddd 100644 --- a/test/common/network/listener_impl_test.cc +++ b/test/common/network/listener_impl_test.cc @@ -358,7 +358,7 @@ TEST_P(TcpListenerImplTest, SetListenerRejectFractionZero) { TestTcpListenerImpl listener(dispatcherImpl(), random_generator, socket, listener_callbacks, true); - listener.setRejectFraction(0); + listener.setRejectFraction(UnitFloat(0)); // This connection will be accepted and not rejected. { @@ -389,7 +389,7 @@ TEST_P(TcpListenerImplTest, SetListenerRejectFractionIntermediate) { TestTcpListenerImpl listener(dispatcherImpl(), random_generator, socket, listener_callbacks, true); - listener.setRejectFraction(0.5f); + listener.setRejectFraction(UnitFloat(0.5f)); // The first connection will be rejected because the random value is too small. { @@ -452,7 +452,7 @@ TEST_P(TcpListenerImplTest, SetListenerRejectFractionAll) { TestTcpListenerImpl listener(dispatcherImpl(), random_generator, socket, listener_callbacks, true); - listener.setRejectFraction(1); + listener.setRejectFraction(UnitFloat(1)); { testing::InSequence s1; diff --git a/test/common/router/config_impl_test.cc b/test/common/router/config_impl_test.cc index 9e4747e06047..29789154e5c9 100644 --- a/test/common/router/config_impl_test.cc +++ b/test/common/router/config_impl_test.cc @@ -1721,10 +1721,10 @@ most_specific_header_mutations_wins: true } } -// Validate that we can't add :-prefixed request headers. -TEST_F(RouteMatcherTest, TestRequestHeadersToAddNoPseudoHeader) { +// Validate that we can't add :-prefixed or Host request headers. +TEST_F(RouteMatcherTest, TestRequestHeadersToAddNoHostOrPseudoHeader) { for (const std::string& header : - {":path", ":authority", ":method", ":scheme", ":status", ":protocol"}) { + {":path", ":authority", ":method", ":scheme", ":status", ":protocol", "host"}) { const std::string yaml = fmt::format(R"EOF( virtual_hosts: - name: www2 @@ -1743,10 +1743,33 @@ TEST_F(RouteMatcherTest, TestRequestHeadersToAddNoPseudoHeader) { parseRouteConfigurationFromYaml(yaml); EXPECT_THROW_WITH_MESSAGE(TestConfigImpl config(route_config, factory_context_, true), - EnvoyException, ":-prefixed headers may not be modified"); + EnvoyException, ":-prefixed or host headers may not be modified"); } } +TEST_F(RouteMatcherTest, TestRequestHeadersToAddLegacyHostHeader) { + TestScopedRuntime scoped_runtime; + Runtime::LoaderSingleton::getExisting()->mergeValues( + {{"envoy.reloadable_features.treat_host_like_authority", "false"}}); + + const std::string yaml = R"EOF( +virtual_hosts: + - name: www2 + domains: ["*"] + request_headers_to_add: + - header: + key: "host" + value: vhost-www2 + append: false +)EOF"; + + NiceMock stream_info; + + envoy::config::route::v3::RouteConfiguration route_config = parseRouteConfigurationFromYaml(yaml); + + EXPECT_NO_THROW(TestConfigImpl config(route_config, factory_context_, true)); +} + // Validate that we can't remove :-prefixed request headers. TEST_F(RouteMatcherTest, TestRequestHeadersToRemoveNoPseudoHeader) { for (const std::string& header : diff --git a/test/common/router/rds_impl_test.cc b/test/common/router/rds_impl_test.cc index 77fb99a23125..ac3a5c046fee 100644 --- a/test/common/router/rds_impl_test.cc +++ b/test/common/router/rds_impl_test.cc @@ -109,7 +109,7 @@ stat_prefix: foo validation_visitor_, outer_init_manager_, "foo.", *route_config_provider_manager_); rds_callbacks_ = server_factory_context_.cluster_manager_.subscription_factory_.callbacks_; EXPECT_CALL(*server_factory_context_.cluster_manager_.subscription_factory_.subscription_, - start(_, _)); + start(_)); outer_init_manager_.initialize(init_watcher_); } @@ -524,7 +524,7 @@ name: foo // Static + dynamic. setup(); EXPECT_CALL(*server_factory_context_.cluster_manager_.subscription_factory_.subscription_, - start(_, _)); + start(_)); outer_init_manager_.initialize(init_watcher_); const std::string response1_json = R"EOF( @@ -690,7 +690,7 @@ name: foo_route_config TEST_F(RouteConfigProviderManagerImplTest, OnConfigUpdateEmpty) { setup(); EXPECT_CALL(*server_factory_context_.cluster_manager_.subscription_factory_.subscription_, - start(_, _)); + start(_)); outer_init_manager_.initialize(init_watcher_); EXPECT_CALL(init_watcher_, ready()); server_factory_context_.cluster_manager_.subscription_factory_.callbacks_->onConfigUpdate({}, ""); @@ -699,7 +699,7 @@ TEST_F(RouteConfigProviderManagerImplTest, OnConfigUpdateEmpty) { TEST_F(RouteConfigProviderManagerImplTest, OnConfigUpdateWrongSize) { setup(); EXPECT_CALL(*server_factory_context_.cluster_manager_.subscription_factory_.subscription_, - start(_, _)); + start(_)); outer_init_manager_.initialize(init_watcher_); envoy::config::route::v3::RouteConfiguration route_config; const auto decoded_resources = TestUtility::decodeResources({route_config, route_config}); @@ -731,7 +731,7 @@ TEST_F(RouteConfigProviderManagerImplTest, ConfigDumpAfterConfigRejected) { // dynamic. setup(); EXPECT_CALL(*server_factory_context_.cluster_manager_.subscription_factory_.subscription_, - start(_, _)); + start(_)); outer_init_manager_.initialize(init_watcher_); const std::string response1_yaml = R"EOF( diff --git a/test/common/router/retry_state_impl_test.cc b/test/common/router/retry_state_impl_test.cc index 02ca75b9ea40..05221252b26e 100644 --- a/test/common/router/retry_state_impl_test.cc +++ b/test/common/router/retry_state_impl_test.cc @@ -1319,58 +1319,6 @@ TEST_F(RouterRetryStateImplTest, RemoveAllRetryHeaders) { EXPECT_FALSE(request_headers.has("x-envoy-hedge-on-per-try-timeout")); EXPECT_FALSE(request_headers.has("x-envoy-upstream-rq-per-try-timeout-ms")); } - - // Repeat policy is enabled case with runtime flag disabled. - { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.consume_all_retry_headers", "false"}}); - - Http::TestRequestHeaderMapImpl request_headers{ - {"x-envoy-retry-on", "5xx,retriable-header-names,retriable-status-codes"}, - {"x-envoy-retry-grpc-on", "resource-exhausted"}, - {"x-envoy-retriable-header-names", "X-Upstream-Pushback"}, - {"x-envoy-retriable-status-codes", "418,420"}, - {"x-envoy-max-retries", "7"}, - {"x-envoy-hedge-on-per-try-timeout", "true"}, - {"x-envoy-upstream-rq-per-try-timeout-ms", "2"}, - }; - setup(request_headers); - EXPECT_TRUE(state_->enabled()); - - EXPECT_FALSE(request_headers.has("x-envoy-retry-on")); - EXPECT_FALSE(request_headers.has("x-envoy-retry-grpc-on")); - EXPECT_FALSE(request_headers.has("x-envoy-max-retries")); - EXPECT_TRUE(request_headers.has("x-envoy-retriable-header-names")); - EXPECT_TRUE(request_headers.has("x-envoy-retriable-status-codes")); - EXPECT_TRUE(request_headers.has("x-envoy-hedge-on-per-try-timeout")); - EXPECT_TRUE(request_headers.has("x-envoy-upstream-rq-per-try-timeout-ms")); - } - - // Repeat policy is disabled case with runtime flag disabled. - { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.consume_all_retry_headers", "false"}}); - - Http::TestRequestHeaderMapImpl request_headers{ - {"x-envoy-retriable-header-names", "X-Upstream-Pushback"}, - {"x-envoy-retriable-status-codes", "418,420"}, - {"x-envoy-max-retries", "7"}, - {"x-envoy-hedge-on-per-try-timeout", "true"}, - {"x-envoy-upstream-rq-per-try-timeout-ms", "2"}, - }; - setup(request_headers); - EXPECT_EQ(nullptr, state_); - - EXPECT_FALSE(request_headers.has("x-envoy-retry-on")); - EXPECT_FALSE(request_headers.has("x-envoy-retry-grpc-on")); - EXPECT_FALSE(request_headers.has("x-envoy-max-retries")); - EXPECT_TRUE(request_headers.has("x-envoy-retriable-header-names")); - EXPECT_TRUE(request_headers.has("x-envoy-retriable-status-codes")); - EXPECT_TRUE(request_headers.has("x-envoy-hedge-on-per-try-timeout")); - EXPECT_TRUE(request_headers.has("x-envoy-upstream-rq-per-try-timeout-ms")); - } } } // namespace diff --git a/test/common/router/router_ratelimit_test.cc b/test/common/router/router_ratelimit_test.cc index 4fab81660417..43a61fdab55f 100644 --- a/test/common/router/router_ratelimit_test.cc +++ b/test/common/router/router_ratelimit_test.cc @@ -209,11 +209,16 @@ TEST_F(RateLimitConfiguration, TestVirtualHost) { EXPECT_EQ(1U, rate_limits.size()); std::vector descriptors; + std::vector local_descriptors; for (const RateLimitPolicyEntry& rate_limit : rate_limits) { rate_limit.populateDescriptors(descriptors, "service_cluster", header_, stream_info_); + rate_limit.populateLocalDescriptors(local_descriptors, "service_cluster", header_, + stream_info_); } EXPECT_THAT(std::vector({{{{"destination_cluster", "www2test"}}}}), testing::ContainerEq(descriptors)); + EXPECT_THAT(std::vector({{"destination_cluster", "www2test"}}), + testing::ContainerEq(local_descriptors.at(0).entries_)); } TEST_F(RateLimitConfiguration, Stages) { @@ -249,24 +254,35 @@ TEST_F(RateLimitConfiguration, Stages) { EXPECT_EQ(2U, rate_limits.size()); std::vector descriptors; + std::vector local_descriptors; for (const RateLimitPolicyEntry& rate_limit : rate_limits) { rate_limit.populateDescriptors(descriptors, "service_cluster", header_, stream_info_); + rate_limit.populateLocalDescriptors(local_descriptors, "service_cluster", header_, + stream_info_); } EXPECT_THAT(std::vector( {{{{"destination_cluster", "www2test"}}}, {{{"destination_cluster", "www2test"}, {"source_cluster", "service_cluster"}}}}), testing::ContainerEq(descriptors)); + EXPECT_THAT(std::vector( + {{{{"destination_cluster", "www2test"}}}, + {{{"destination_cluster", "www2test"}, {"source_cluster", "service_cluster"}}}}), + testing::ContainerEq(local_descriptors)); descriptors.clear(); + local_descriptors.clear(); rate_limits = route->rateLimitPolicy().getApplicableRateLimit(1UL); EXPECT_EQ(1U, rate_limits.size()); for (const RateLimitPolicyEntry& rate_limit : rate_limits) { rate_limit.populateDescriptors(descriptors, "service_cluster", header_, stream_info_); + rate_limit.populateLocalDescriptors(local_descriptors, "service_cluster", header_, + stream_info_); } EXPECT_THAT(std::vector({{{{"remote_address", "10.0.0.1"}}}}), testing::ContainerEq(descriptors)); - + EXPECT_THAT(std::vector({{{{"remote_address", "10.0.0.1"}}}}), + testing::ContainerEq(local_descriptors)); rate_limits = route->rateLimitPolicy().getApplicableRateLimit(10UL); EXPECT_TRUE(rate_limits.empty()); } @@ -277,6 +293,7 @@ class RateLimitPolicyEntryTest : public testing::Test { rate_limit_entry_ = std::make_unique( parseRateLimitFromV3Yaml(yaml), ProtobufMessage::getStrictValidationVisitor()); descriptors_.clear(); + local_descriptors_.clear(); stream_info_.downstream_address_provider_->setRemoteAddress(default_remote_address_); ON_CALL(Const(stream_info_), routeEntry()).WillByDefault(testing::Return(&route_)); } @@ -285,6 +302,7 @@ class RateLimitPolicyEntryTest : public testing::Test { Http::TestRequestHeaderMapImpl header_; NiceMock route_; std::vector descriptors_; + std::vector local_descriptors_; Network::Address::InstanceConstSharedPtr default_remote_address_{ new Network::Address::Ipv4Instance("10.0.0.1")}; NiceMock stream_info_; @@ -313,8 +331,11 @@ TEST_F(RateLimitPolicyEntryTest, RemoteAddress) { setupTest(yaml); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_THAT(std::vector({{{{"remote_address", "10.0.0.1"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"remote_address", "10.0.0.1"}}}}), + testing::ContainerEq(local_descriptors_)); } // Verify no descriptor is emitted if remote is a pipe. @@ -329,7 +350,9 @@ TEST_F(RateLimitPolicyEntryTest, PipeAddress) { stream_info_.downstream_address_provider_->setRemoteAddress( std::make_shared("/hello")); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_TRUE(descriptors_.empty()); + EXPECT_TRUE(local_descriptors_.empty()); } TEST_F(RateLimitPolicyEntryTest, SourceService) { @@ -341,9 +364,14 @@ TEST_F(RateLimitPolicyEntryTest, SourceService) { setupTest(yaml); rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "service_cluster", header_, + stream_info_); EXPECT_THAT( std::vector({{{{"source_cluster", "service_cluster"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT( + std::vector({{{{"source_cluster", "service_cluster"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, DestinationService) { @@ -355,9 +383,14 @@ TEST_F(RateLimitPolicyEntryTest, DestinationService) { setupTest(yaml); rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "service_cluster", header_, + stream_info_); EXPECT_THAT( std::vector({{{{"destination_cluster", "fake_cluster"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT( + std::vector({{{{"destination_cluster", "fake_cluster"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, RequestHeaders) { @@ -372,8 +405,13 @@ TEST_F(RateLimitPolicyEntryTest, RequestHeaders) { Http::TestRequestHeaderMapImpl header{{"x-header-name", "test_value"}}; rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "service_cluster", header, + stream_info_); EXPECT_THAT(std::vector({{{{"my_header_name", "test_value"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT( + std::vector({{{{"my_header_name", "test_value"}}}}), + testing::ContainerEq(local_descriptors_)); } // Validate that a descriptor is added if the missing request header @@ -395,8 +433,13 @@ TEST_F(RateLimitPolicyEntryTest, RequestHeadersWithSkipIfAbsent) { Http::TestRequestHeaderMapImpl header{{"x-header-name", "test_value"}}; rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "service_cluster", header, + stream_info_); EXPECT_THAT(std::vector({{{{"my_header_name", "test_value"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT( + std::vector({{{{"my_header_name", "test_value"}}}}), + testing::ContainerEq(local_descriptors_)); } // Tests if the descriptors are added if one of the headers is missing @@ -418,7 +461,10 @@ TEST_F(RateLimitPolicyEntryTest, RequestHeadersWithDefaultSkipIfAbsent) { Http::TestRequestHeaderMapImpl header{{"x-header-test", "test_value"}}; rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "service_cluster", header, + stream_info_); EXPECT_TRUE(descriptors_.empty()); + EXPECT_TRUE(local_descriptors_.empty()); } TEST_F(RateLimitPolicyEntryTest, RequestHeadersNoMatch) { @@ -433,7 +479,10 @@ TEST_F(RateLimitPolicyEntryTest, RequestHeadersNoMatch) { Http::TestRequestHeaderMapImpl header{{"x-header-name", "test_value"}}; rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "service_cluster", header, + stream_info_); EXPECT_TRUE(descriptors_.empty()); + EXPECT_TRUE(local_descriptors_.empty()); } TEST_F(RateLimitPolicyEntryTest, RateLimitKey) { @@ -446,8 +495,11 @@ TEST_F(RateLimitPolicyEntryTest, RateLimitKey) { setupTest(yaml); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_THAT(std::vector({{{{"generic_key", "fake_key"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"generic_key", "fake_key"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, GenericKeyWithSetDescriptorKey) { @@ -461,8 +513,11 @@ TEST_F(RateLimitPolicyEntryTest, GenericKeyWithSetDescriptorKey) { setupTest(yaml); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_THAT(std::vector({{{{"fake_key", "fake_value"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"fake_key", "fake_value"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, GenericKeyWithEmptyDescriptorKey) { @@ -476,8 +531,11 @@ TEST_F(RateLimitPolicyEntryTest, GenericKeyWithEmptyDescriptorKey) { setupTest(yaml); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_THAT(std::vector({{{{"generic_key", "fake_value"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"generic_key", "fake_value"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, DEPRECATED_FEATURE_TEST(DynamicMetaDataMatch)) { @@ -504,9 +562,12 @@ TEST_F(RateLimitPolicyEntryTest, DEPRECATED_FEATURE_TEST(DynamicMetaDataMatch)) TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, MetaDataMatchDynamicSourceByDefault) { @@ -533,9 +594,12 @@ TEST_F(RateLimitPolicyEntryTest, MetaDataMatchDynamicSourceByDefault) { TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, MetaDataMatchDynamicSource) { @@ -563,9 +627,12 @@ TEST_F(RateLimitPolicyEntryTest, MetaDataMatchDynamicSource) { TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, MetaDataMatchRouteEntrySource) { @@ -594,9 +661,12 @@ TEST_F(RateLimitPolicyEntryTest, MetaDataMatchRouteEntrySource) { TestUtility::loadFromYaml(metadata_yaml, route_.metadata_); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"fake_key", "foo"}}}}), + testing::ContainerEq(local_descriptors_)); } // Tests that the default_value is used in the descriptor when the metadata_key is empty. @@ -624,9 +694,12 @@ TEST_F(RateLimitPolicyEntryTest, MetaDataNoMatchWithDefaultValue) { TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_THAT(std::vector({{{{"fake_key", "fake_value"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"fake_key", "fake_value"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, MetaDataNoMatch) { @@ -652,8 +725,10 @@ TEST_F(RateLimitPolicyEntryTest, MetaDataNoMatch) { TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_TRUE(descriptors_.empty()); + EXPECT_TRUE(local_descriptors_.empty()); } TEST_F(RateLimitPolicyEntryTest, MetaDataEmptyValue) { @@ -679,8 +754,10 @@ TEST_F(RateLimitPolicyEntryTest, MetaDataEmptyValue) { TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_TRUE(descriptors_.empty()); + EXPECT_TRUE(local_descriptors_.empty()); } // Tests that no descriptor is generated when both the metadata_key and default_value are empty. TEST_F(RateLimitPolicyEntryTest, MetaDataAndDefaultValueEmpty) { @@ -707,8 +784,10 @@ TEST_F(RateLimitPolicyEntryTest, MetaDataAndDefaultValueEmpty) { TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_TRUE(descriptors_.empty()); + EXPECT_TRUE(local_descriptors_.empty()); } TEST_F(RateLimitPolicyEntryTest, MetaDataNonStringNoMatch) { @@ -735,8 +814,10 @@ TEST_F(RateLimitPolicyEntryTest, MetaDataNonStringNoMatch) { TestUtility::loadFromYaml(metadata_yaml, stream_info_.dynamicMetadata()); rate_limit_entry_->populateDescriptors(descriptors_, "", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header_, stream_info_); EXPECT_TRUE(descriptors_.empty()); + EXPECT_TRUE(local_descriptors_.empty()); } TEST_F(RateLimitPolicyEntryTest, HeaderValueMatch) { @@ -753,8 +834,11 @@ TEST_F(RateLimitPolicyEntryTest, HeaderValueMatch) { Http::TestRequestHeaderMapImpl header{{"x-header-name", "test_value"}}; rate_limit_entry_->populateDescriptors(descriptors_, "", header, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header, stream_info_); EXPECT_THAT(std::vector({{{{"header_match", "fake_value"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"header_match", "fake_value"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, HeaderValueMatchNoMatch) { @@ -771,7 +855,9 @@ TEST_F(RateLimitPolicyEntryTest, HeaderValueMatchNoMatch) { Http::TestRequestHeaderMapImpl header{{"x-header-name", "not_same_value"}}; rate_limit_entry_->populateDescriptors(descriptors_, "", header, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header, stream_info_); EXPECT_TRUE(descriptors_.empty()); + EXPECT_TRUE(local_descriptors_.empty()); } TEST_F(RateLimitPolicyEntryTest, HeaderValueMatchHeadersNotPresent) { @@ -789,8 +875,11 @@ TEST_F(RateLimitPolicyEntryTest, HeaderValueMatchHeadersNotPresent) { Http::TestRequestHeaderMapImpl header{{"x-header-name", "not_same_value"}}; rate_limit_entry_->populateDescriptors(descriptors_, "", header, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header, stream_info_); EXPECT_THAT(std::vector({{{{"header_match", "fake_value"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT(std::vector({{{{"header_match", "fake_value"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, HeaderValueMatchHeadersPresent) { @@ -808,7 +897,9 @@ TEST_F(RateLimitPolicyEntryTest, HeaderValueMatchHeadersPresent) { Http::TestRequestHeaderMapImpl header{{"x-header-name", "test_value"}}; rate_limit_entry_->populateDescriptors(descriptors_, "", header, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "", header, stream_info_); EXPECT_TRUE(descriptors_.empty()); + EXPECT_TRUE(local_descriptors_.empty()); } TEST_F(RateLimitPolicyEntryTest, CompoundActions) { @@ -821,10 +912,16 @@ TEST_F(RateLimitPolicyEntryTest, CompoundActions) { setupTest(yaml); rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "service_cluster", header_, + stream_info_); EXPECT_THAT( std::vector( {{{{"destination_cluster", "fake_cluster"}, {"source_cluster", "service_cluster"}}}}), testing::ContainerEq(descriptors_)); + EXPECT_THAT( + std::vector( + {{{{"destination_cluster", "fake_cluster"}, {"source_cluster", "service_cluster"}}}}), + testing::ContainerEq(local_descriptors_)); } TEST_F(RateLimitPolicyEntryTest, CompoundActionsNoDescriptor) { @@ -841,7 +938,10 @@ TEST_F(RateLimitPolicyEntryTest, CompoundActionsNoDescriptor) { setupTest(yaml); rate_limit_entry_->populateDescriptors(descriptors_, "service_cluster", header_, stream_info_); + rate_limit_entry_->populateLocalDescriptors(local_descriptors_, "service_cluster", header_, + stream_info_); EXPECT_TRUE(descriptors_.empty()); + EXPECT_TRUE(local_descriptors_.empty()); } TEST_F(RateLimitPolicyEntryTest, DynamicMetadataRateLimitOverride) { diff --git a/test/common/router/router_test.cc b/test/common/router/router_test.cc index f28662ff3100..26525ebf3536 100644 --- a/test/common/router/router_test.cc +++ b/test/common/router/router_test.cc @@ -116,8 +116,9 @@ class RouterTestBase : public testing::Test { // Make the "system time" non-zero, because 0 is considered invalid by DateUtil. test_time_.setMonotonicTime(std::chrono::milliseconds(50)); - // Allow any number of setTrackedObject calls for the dispatcher strict mock. - EXPECT_CALL(callbacks_.dispatcher_, setTrackedObject(_)).Times(AnyNumber()); + // Allow any number of (append|pop)TrackedObject calls for the dispatcher strict mock. + EXPECT_CALL(callbacks_.dispatcher_, pushTrackedObject(_)).Times(AnyNumber()); + EXPECT_CALL(callbacks_.dispatcher_, popTrackedObject(_)).Times(AnyNumber()); } void expectResponseTimerCreate() { @@ -294,7 +295,8 @@ class RouterTestBase : public testing::Test { [&](Http::ResponseDecoder& decoder, Http::ConnectionPool::Callbacks& callbacks) -> Http::ConnectionPool::Cancellable* { response_decoder_ = &decoder; - EXPECT_CALL(callbacks_.dispatcher_, setTrackedObject(_)).Times(testing::AtLeast(2)); + EXPECT_CALL(callbacks_.dispatcher_, pushTrackedObject(_)).Times(testing::AtLeast(1)); + EXPECT_CALL(callbacks_.dispatcher_, popTrackedObject(_)).Times(testing::AtLeast(1)); callbacks.onPoolReady(original_encoder_, cm_.thread_local_cluster_.conn_pool_.host_, upstream_stream_info_, Http::Protocol::Http10); return nullptr; @@ -555,6 +557,78 @@ TEST_F(RouterTest, PoolFailureWithPriority) { "upstream_reset_before_response_started{connection failure,tls version mismatch}"); } +TEST_F(RouterTest, PoolFailureDueToConnectTimeout) { + ON_CALL(callbacks_.route_->route_entry_, priority()) + .WillByDefault(Return(Upstream::ResourcePriority::High)); + EXPECT_CALL(cm_.thread_local_cluster_, + httpConnPool(Upstream::ResourcePriority::High, _, &router_)); + EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_, newStream(_, _)) + .WillOnce(Invoke([&](Http::StreamDecoder&, Http::ConnectionPool::Callbacks& callbacks) + -> Http::ConnectionPool::Cancellable* { + callbacks.onPoolFailure(ConnectionPool::PoolFailureReason::Timeout, "connect_timeout", + cm_.thread_local_cluster_.conn_pool_.host_); + return nullptr; + })); + + Http::TestResponseHeaderMapImpl response_headers{ + {":status", "503"}, {"content-length", "134"}, {"content-type", "text/plain"}}; + EXPECT_CALL(callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), false)); + EXPECT_CALL(callbacks_, encodeData(_, true)); + EXPECT_CALL(callbacks_.stream_info_, + setResponseFlag(StreamInfo::ResponseFlag::UpstreamConnectionFailure)); + EXPECT_CALL(callbacks_.stream_info_, onUpstreamHostSelected(_)) + .WillOnce(Invoke([&](const Upstream::HostDescriptionConstSharedPtr host) -> void { + EXPECT_EQ(host_address_, host->address()); + })); + + Http::TestRequestHeaderMapImpl headers; + HttpTestUtility::addDefaultHeaders(headers); + router_.decodeHeaders(headers, true); + EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); + // Pool failure, so upstream request was not initiated. + EXPECT_EQ(0U, + callbacks_.route_->route_entry_.virtual_cluster_.stats().upstream_rq_total_.value()); + EXPECT_EQ(callbacks_.details(), + "upstream_reset_before_response_started{connection failure,connect_timeout}"); +} + +TEST_F(RouterTest, PoolFailureDueToConnectTimeoutLegacy) { + TestScopedRuntime scoped_runtime; + Runtime::LoaderSingleton::getExisting()->mergeValues( + {{"envoy.reloadable_features.treat_upstream_connect_timeout_as_connect_failure", "false"}}); + ON_CALL(callbacks_.route_->route_entry_, priority()) + .WillByDefault(Return(Upstream::ResourcePriority::High)); + EXPECT_CALL(cm_.thread_local_cluster_, + httpConnPool(Upstream::ResourcePriority::High, _, &router_)); + EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_, newStream(_, _)) + .WillOnce(Invoke([&](Http::StreamDecoder&, Http::ConnectionPool::Callbacks& callbacks) + -> Http::ConnectionPool::Cancellable* { + callbacks.onPoolFailure(ConnectionPool::PoolFailureReason::Timeout, "connect_timeout", + cm_.thread_local_cluster_.conn_pool_.host_); + return nullptr; + })); + + Http::TestResponseHeaderMapImpl response_headers{ + {":status", "503"}, {"content-length", "127"}, {"content-type", "text/plain"}}; + EXPECT_CALL(callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), false)); + EXPECT_CALL(callbacks_, encodeData(_, true)); + EXPECT_CALL(callbacks_.stream_info_, setResponseFlag(StreamInfo::ResponseFlag::LocalReset)); + EXPECT_CALL(callbacks_.stream_info_, onUpstreamHostSelected(_)) + .WillOnce(Invoke([&](const Upstream::HostDescriptionConstSharedPtr host) -> void { + EXPECT_EQ(host_address_, host->address()); + })); + + Http::TestRequestHeaderMapImpl headers; + HttpTestUtility::addDefaultHeaders(headers); + router_.decodeHeaders(headers, true); + EXPECT_TRUE(verifyHostUpstreamStats(0, 1)); + // Pool failure, so upstream request was not initiated. + EXPECT_EQ(0U, + callbacks_.route_->route_entry_.virtual_cluster_.stats().upstream_rq_total_.value()); + EXPECT_EQ(callbacks_.details(), + "upstream_reset_before_response_started{local reset,connect_timeout}"); +} + TEST_F(RouterTest, Http1Upstream) { EXPECT_CALL(cm_.thread_local_cluster_, httpConnPool(_, absl::optional(), _)); EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_, newStream(_, _)) @@ -2190,7 +2264,8 @@ TEST_F(RouterTest, GrpcOk) { EXPECT_EQ(1U, callbacks_.route_->route_entry_.virtual_cluster_.stats().upstream_rq_total_.value()); - EXPECT_CALL(callbacks_.dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(callbacks_.dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(callbacks_.dispatcher_, popTrackedObject(_)); Http::ResponseHeaderMapPtr response_headers( new Http::TestResponseHeaderMapImpl{{":status", "200"}}); EXPECT_CALL(cm_.thread_local_cluster_.conn_pool_.host_->outlier_detector_, @@ -2198,7 +2273,8 @@ TEST_F(RouterTest, GrpcOk) { response_decoder->decodeHeaders(std::move(response_headers), false); EXPECT_TRUE(verifyHostUpstreamStats(0, 0)); - EXPECT_CALL(callbacks_.dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(callbacks_.dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(callbacks_.dispatcher_, popTrackedObject(_)); Http::ResponseTrailerMapPtr response_trailers( new Http::TestResponseTrailerMapImpl{{"grpc-status", "0"}}); response_decoder->decodeTrailers(std::move(response_trailers)); diff --git a/test/common/router/router_upstream_log_test.cc b/test/common/router/router_upstream_log_test.cc index 7821291c80a5..a16c05fd6ba2 100644 --- a/test/common/router/router_upstream_log_test.cc +++ b/test/common/router/router_upstream_log_test.cc @@ -99,7 +99,8 @@ class RouterUpstreamLogTest : public testing::Test { ShadowWriterPtr(new MockShadowWriter()), router_proto); router_ = std::make_shared(*config_); router_->setDecoderFilterCallbacks(callbacks_); - EXPECT_CALL(callbacks_.dispatcher_, setTrackedObject(_)).Times(testing::AnyNumber()); + EXPECT_CALL(callbacks_.dispatcher_, pushTrackedObject(_)).Times(testing::AnyNumber()); + EXPECT_CALL(callbacks_.dispatcher_, popTrackedObject(_)).Times(testing::AnyNumber()); upstream_locality_.set_zone("to_az"); context_.cluster_manager_.initializeThreadLocalClusters({"fake_cluster"}); @@ -326,5 +327,43 @@ name: accesslog EXPECT_LE(std::abs(std::difftime(log_time, now)), 300); } +// Test request headers/response headers/response trailers byte size. +TEST_F(RouterUpstreamLogTest, HeaderByteSize) { + const std::string yaml = R"EOF( +name: accesslog +typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + log_format: + text_format_source: + inline_string: "%REQUEST_HEADERS_BYTES% %RESPONSE_HEADERS_BYTES% %RESPONSE_TRAILERS_BYTES%" + path: "/dev/null" + )EOF"; + + envoy::config::accesslog::v3::AccessLog upstream_log; + TestUtility::loadFromYaml(yaml, upstream_log); + + init(absl::optional(upstream_log)); + run(200, {{"request-header-name", "request-header-val"}}, + {{"response-header-name", "response-header-val"}}, + {{"response-trailer-name", "response-trailer-val"}}); + + EXPECT_EQ(output_.size(), 1U); + // Request headers: + // scheme: http + // :method: GET + // :authority: host + // :path: / + // x-envoy-expected-rq-timeout-ms: 10 + // request-header-name: request-header-val + + // Response headers: + // :status: 200 + // response-header-name: response-header-val + + // Response trailers: + // response-trailer-name: response-trailer-val + EXPECT_EQ(output_.front(), "110 49 41"); +} + } // namespace Router } // namespace Envoy diff --git a/test/common/router/scoped_rds_test.cc b/test/common/router/scoped_rds_test.cc index 72bc3e60c3c5..7ef4868b25af 100644 --- a/test/common/router/scoped_rds_test.cc +++ b/test/common/router/scoped_rds_test.cc @@ -123,7 +123,7 @@ class ScopedRdsTest : public ScopedRoutesTestBase { // srds subscription EXPECT_CALL(server_factory_context_.cluster_manager_.subscription_factory_, - subscriptionFromConfigSource(_, _, _, _, _)) + subscriptionFromConfigSource(_, _, _, _, _, _)) .Times(AnyNumber()); // rds subscription EXPECT_CALL( @@ -132,17 +132,17 @@ class ScopedRdsTest : public ScopedRoutesTestBase { _, Eq(Grpc::Common::typeUrl( API_NO_BOOST(envoy::api::v2::RouteConfiguration)().GetDescriptor()->full_name())), - _, _, _)) + _, _, _, _)) .Times(AnyNumber()) .WillRepeatedly( Invoke([this](const envoy::config::core::v3::ConfigSource&, absl::string_view, Stats::Scope&, Envoy::Config::SubscriptionCallbacks& callbacks, - Envoy::Config::OpaqueResourceDecoder&) { + Envoy::Config::OpaqueResourceDecoder&, bool) { auto ret = std::make_unique>(); rds_subscription_by_config_subscription_[ret.get()] = &callbacks; - EXPECT_CALL(*ret, start(_, _)) + EXPECT_CALL(*ret, start(_)) .WillOnce(Invoke([this, config_sub_addr = ret.get()]( - const std::set& resource_names, const bool) { + const absl::flat_hash_set& resource_names) { EXPECT_EQ(resource_names.size(), 1); auto iter = rds_subscription_by_config_subscription_.find(config_sub_addr); EXPECT_NE(iter, rds_subscription_by_config_subscription_.end()); diff --git a/test/common/runtime/runtime_impl_test.cc b/test/common/runtime/runtime_impl_test.cc index c451f87d8119..1a4a2c6d8e7a 100644 --- a/test/common/runtime/runtime_impl_test.cc +++ b/test/common/runtime/runtime_impl_test.cc @@ -834,11 +834,11 @@ class RtdsLoaderImplTest : public LoaderImplTest { rtds_layer->mutable_rtds_config(); } EXPECT_CALL(cm_, subscriptionFactory()).Times(layers_.size()); - ON_CALL(cm_.subscription_factory_, subscriptionFromConfigSource(_, _, _, _, _)) - .WillByDefault( - testing::Invoke([this](const envoy::config::core::v3::ConfigSource&, absl::string_view, - Stats::Scope&, Config::SubscriptionCallbacks& callbacks, - Config::OpaqueResourceDecoder&) -> Config::SubscriptionPtr { + ON_CALL(cm_.subscription_factory_, subscriptionFromConfigSource(_, _, _, _, _, _)) + .WillByDefault(testing::Invoke( + [this](const envoy::config::core::v3::ConfigSource&, absl::string_view, Stats::Scope&, + Config::SubscriptionCallbacks& callbacks, Config::OpaqueResourceDecoder&, + bool) -> Config::SubscriptionPtr { auto ret = std::make_unique>(); rtds_subscriptions_.push_back(ret.get()); rtds_callbacks_.push_back(&callbacks); @@ -848,7 +848,7 @@ class RtdsLoaderImplTest : public LoaderImplTest { generator_, validation_visitor_, *api_); loader_->initialize(cm_); for (auto* sub : rtds_subscriptions_) { - EXPECT_CALL(*sub, start(_, _)); + EXPECT_CALL(*sub, start(_)); } loader_->startRtdsSubscriptions(rtds_init_callback_.AsStdFunction()); @@ -1168,7 +1168,7 @@ TEST_F(RtdsLoaderImplTest, MultipleRtdsLayers) { TEST_F(RtdsLoaderImplTest, BadConfigSource) { Upstream::MockClusterManager cm_; - EXPECT_CALL(cm_.subscription_factory_, subscriptionFromConfigSource(_, _, _, _, _)) + EXPECT_CALL(cm_.subscription_factory_, subscriptionFromConfigSource(_, _, _, _, _, _)) .WillOnce(InvokeWithoutArgs([]() -> Config::SubscriptionPtr { throw EnvoyException("bad config"); return nullptr; diff --git a/test/common/secret/sds_api_test.cc b/test/common/secret/sds_api_test.cc index 90ccdec8ebd8..72e9ce27abdc 100644 --- a/test/common/secret/sds_api_test.cc +++ b/test/common/secret/sds_api_test.cc @@ -105,11 +105,11 @@ TEST_F(SdsApiTest, InitManagerInitialised) { NiceMock validation_visitor; envoy::config::core::v3::ConfigSource config_source; - EXPECT_CALL(subscription_factory_, subscriptionFromConfigSource(_, _, _, _, _)) + EXPECT_CALL(subscription_factory_, subscriptionFromConfigSource(_, _, _, _, _, _)) .WillOnce(Invoke([this, &sds_config_path, &resource_decoder, &stats](const envoy::config::core::v3::ConfigSource&, absl::string_view, Stats::Scope&, Config::SubscriptionCallbacks& cbs, - Config::OpaqueResourceDecoder&) -> Config::SubscriptionPtr { + Config::OpaqueResourceDecoder&, bool) -> Config::SubscriptionPtr { return std::make_unique(*dispatcher_, sds_config_path, cbs, resource_decoder, stats, validation_visitor_, *api_); @@ -134,7 +134,7 @@ TEST_F(SdsApiTest, InitManagerInitialised) { TEST_F(SdsApiTest, BadConfigSource) { ::testing::InSequence s; envoy::config::core::v3::ConfigSource config_source; - EXPECT_CALL(subscription_factory_, subscriptionFromConfigSource(_, _, _, _, _)) + EXPECT_CALL(subscription_factory_, subscriptionFromConfigSource(_, _, _, _, _, _)) .WillOnce(InvokeWithoutArgs([]() -> Config::SubscriptionPtr { throw EnvoyException("bad config"); return nullptr; diff --git a/test/common/signal/fatal_action_test.cc b/test/common/signal/fatal_action_test.cc index 9a286c9f9fb1..1e276f016e4a 100644 --- a/test/common/signal/fatal_action_test.cc +++ b/test/common/signal/fatal_action_test.cc @@ -1,3 +1,6 @@ +#include + +#include "envoy/common/scope_tracker.h" #include "envoy/server/fatal_action_config.h" #include "common/signal/fatal_action.h" @@ -23,9 +26,10 @@ class TestFatalErrorHandler : public FatalErrorHandlerInterface { void onFatalError(std::ostream& /*os*/) const override {} void runFatalActionsOnTrackedObject(const FatalAction::FatalActionPtrList& actions) const override { - // Call the Fatal Actions with nullptr + // Call the Fatal Actions with a non-empty vector so it runs the action. + std::vector tracked_objects{nullptr}; for (const Server::Configuration::FatalActionPtr& action : actions) { - action->run(nullptr); + action->run(tracked_objects); } } }; @@ -33,7 +37,9 @@ class TestFatalErrorHandler : public FatalErrorHandlerInterface { class TestFatalAction : public Server::Configuration::FatalAction { public: TestFatalAction(bool is_safe, int* const counter) : is_safe_(is_safe), counter_(counter) {} - void run(const ScopeTrackedObject* /*current_object*/) override { ++(*counter_); } + void run(absl::Span /*tracked_objects*/) override { + ++(*counter_); + } bool isAsyncSignalSafe() const override { return is_safe_; } private: diff --git a/test/common/signal/signals_test.cc b/test/common/signal/signals_test.cc index 3ecf49f6695b..f2e5ddde8c8d 100644 --- a/test/common/signal/signals_test.cc +++ b/test/common/signal/signals_test.cc @@ -1,6 +1,9 @@ #include #include +#include + +#include "envoy/common/scope_tracker.h" #include "common/signal/fatal_error_handler.h" #include "common/signal/signal_action.h" @@ -28,21 +31,27 @@ extern void resetFatalActionStateForTest(); // Use this test handler instead of a mock, because fatal error handlers must be // signal-safe and a mock might allocate memory. class TestFatalErrorHandler : public FatalErrorHandlerInterface { +public: void onFatalError(std::ostream& os) const override { os << "HERE!"; } void runFatalActionsOnTrackedObject(const FatalAction::FatalActionPtrList& actions) const override { // Run the actions for (const auto& action : actions) { - action->run(nullptr); + action->run(tracked_objects_); } } + +private: + std::vector tracked_objects_{nullptr}; }; // Use this to test fatal actions get called, as well as the order they run. class EchoFatalAction : public Server::Configuration::FatalAction { public: EchoFatalAction(absl::string_view echo_msg) : echo_msg_(echo_msg) {} - void run(const ScopeTrackedObject* /*current_object*/) override { std::cerr << echo_msg_; } + void run(absl::Span /*tracked_objects*/) override { + std::cerr << echo_msg_; + } bool isAsyncSignalSafe() const override { return true; } private: @@ -52,7 +61,9 @@ class EchoFatalAction : public Server::Configuration::FatalAction { // Use this to test failing while in a signal handler. class SegfaultFatalAction : public Server::Configuration::FatalAction { public: - void run(const ScopeTrackedObject* /*current_object*/) override { raise(SIGSEGV); } + void run(absl::Span /*tracked_objects*/) override { + raise(SIGSEGV); + } bool isAsyncSignalSafe() const override { return false; } }; diff --git a/test/common/stats/BUILD b/test/common/stats/BUILD index ab9daa0d6643..53f7d41fc446 100644 --- a/test/common/stats/BUILD +++ b/test/common/stats/BUILD @@ -231,6 +231,25 @@ envoy_cc_test( ], ) +envoy_cc_benchmark_binary( + name = "tag_extractor_impl_benchmark", + srcs = [ + "tag_extractor_impl_speed_test.cc", + ], + external_deps = [ + "benchmark", + ], + deps = [ + "//source/common/stats:tag_producer_lib", + "@envoy_api//envoy/config/metrics/v3:pkg_cc_proto", + ], +) + +envoy_benchmark_test( + name = "tag_extractor_impl_benchmark_test", + benchmark_binary = "tag_extractor_impl_benchmark", +) + envoy_cc_test( name = "thread_local_store_test", srcs = ["thread_local_store_test.cc"], diff --git a/test/common/stats/tag_extractor_impl_speed_test.cc b/test/common/stats/tag_extractor_impl_speed_test.cc new file mode 100644 index 000000000000..e6a8603d73b2 --- /dev/null +++ b/test/common/stats/tag_extractor_impl_speed_test.cc @@ -0,0 +1,110 @@ +// Note: this should be run with --compilation_mode=opt +// Running ./bazel-out/k8-opt/bin/test/common/stats/tag_extractor_impl_benchmark +// Run on (24 X 4300 MHz CPU s) +// CPU Caches: +// L1 Data 32 KiB (x12) +// L1 Instruction 32 KiB (x12) +// L2 Unified 1024 KiB (x12) +// L3 Unified 16896 KiB (x1) +// Load Average: 0.94, 0.75, 0.88 +// ***WARNING*** CPU scaling is enabled, the benchmark real time +// measurements may be noisy and will incur extra overhead. +// ------------------------------------------------------------ +// Benchmark Time CPU Iterations +// ------------------------------------------------------------ +// BM_ExtractTags/0 1759 ns 1757 ns 397721 +// BM_ExtractTags/1 498 ns 497 ns 1386765 +// BM_ExtractTags/2 814 ns 813 ns 789388 +// BM_ExtractTags/3 621 ns 620 ns 1109055 +// BM_ExtractTags/4 1320 ns 1318 ns 536701 +// BM_ExtractTags/5 882 ns 880 ns 817115 +// BM_ExtractTags/6 327 ns 327 ns 2171259 +// BM_ExtractTags/7 572 ns 571 ns 1205250 +// BM_ExtractTags/8 1238 ns 1236 ns 558481 +// BM_ExtractTags/9 1669 ns 1667 ns 414483 +// BM_ExtractTags/10 310 ns 310 ns 2237065 +// BM_ExtractTags/11 476 ns 476 ns 1465925 +// BM_ExtractTags/12 1102 ns 1100 ns 631707 +// BM_ExtractTags/13 1307 ns 1305 ns 513760 +// BM_ExtractTags/14 1583 ns 1581 ns 447159 +// BM_ExtractTags/15 957 ns 956 ns 729726 +// BM_ExtractTags/16 822 ns 821 ns 869110 +// BM_ExtractTags/17 821 ns 820 ns 839293 +// BM_ExtractTags/18 783 ns 782 ns 898442 +// BM_ExtractTags/19 330 ns 329 ns 2098821 +// BM_ExtractTags/20 342 ns 342 ns 2044062 +// BM_ExtractTags/21 389 ns 389 ns 1785110 +// BM_ExtractTags/22 847 ns 846 ns 831652 +// BM_ExtractTags/23 2022 ns 2019 ns 353368 +// BM_ExtractTags/24 306 ns 305 ns 2226702 +// BM_ExtractTags/25 277 ns 277 ns 2516796 +// BM_ExtractTags/26 494 ns 494 ns 1363306 + +#include "envoy/config/metrics/v3/stats.pb.h" + +#include "common/common/assert.h" +#include "common/config/well_known_names.h" +#include "common/stats/tag_producer_impl.h" + +#include "benchmark/benchmark.h" + +namespace Envoy { +namespace Stats { +namespace { + +using Params = std::tuple; + +const std::vector params = { + {"listener.127.0.0.1_3012.http.http_prefix.downstream_rq_5xx", 3}, + {"cluster.ratelimit.upstream_rq_timeout", 1}, + {"listener.[__1]_0.ssl.cipher.AES256-SHA", 2}, + {"cluster.ratelimit.ssl.ciphers.ECDHE-RSA-AES128-GCM-SHA256", 2}, + {"listener.[2001_0db8_85a3_0000_0000_8a2e_0370_7334]_3543.ssl.cipher.AES256-SHA", 2}, + {"listener.127.0.0.1_0.ssl.cipher.AES256-SHA", 2}, + {"mongo.mongo_filter.op_reply", 1}, + {"mongo.mongo_filter.cmd.foo_cmd.reply_size", 2}, + {"mongo.mongo_filter.collection.bar_collection.query.multi_get", 2}, + {"mongo.mongo_filter.collection.bar_collection.callsite.baz_callsite.query.scatter_get", 3}, + {"ratelimit.foo_ratelimiter.over_limit", 1}, + {"http.egress_dynamodb_iad.downstream_cx_total", 1}, + {"http.egress_dynamodb_iad.dynamodb.operation.Query.upstream_rq_time", 2}, + {"http.egress_dynamodb_iad.dynamodb.table.bar_table.upstream_rq_time", 2}, + {"http.egress_dynamodb_iad.dynamodb.table.bar_table.capacity.Query.__partition_id=ABC1234", 4}, + {"cluster.grpc_cluster.grpc.grpc_service_1.grpc_method_1.success", 3}, + {"vhost.vhost_1.vcluster.vcluster_1.upstream_rq_2xx", 3}, + {"vhost.vhost_1.vcluster.vcluster_1.upstream_rq_200", 3}, + {"http.egress_dynamodb_iad.user_agent.ios.downstream_cx_total", 2}, + {"auth.clientssl.clientssl_prefix.auth_ip_allowlist", 1}, + {"tcp.tcp_prefix.downstream_flow_control_resumed_reading_total", 1}, + {"udp.udp_prefix-with-dashes.downstream_flow_control_resumed_reading_total", 1}, + {"http.fault_connection_manager.fault.fault_cluster.aborts_injected", 2}, + {"http.rds_connection_manager.rds.route_config.123.update_success", 2}, + {"listener_manager.worker_123.dispatcher.loop_duration_us", 1}, + {"mongo_mongo_mongo_mongo.this_is_rather_long_string_which " + "does_not_match_and_consumes_a_lot_in_case_of_backtracking_imposed_by_greedy_pattern", + 0}, + {"another_long_but_matching_string_which_may_consume_resources_if_missing_end_of_line_lock_rq_" + "2xx", + 1}, +}; + +// NOLINTNEXTLINE(readability-identifier-naming) +void BM_ExtractTags(benchmark::State& state) { + TagProducerImpl tag_extractors{envoy::config::metrics::v3::StatsConfig()}; + const auto idx = state.range(0); + const auto& p = params[idx]; + absl::string_view str = std::get<0>(p); + const uint32_t tags_size = std::get<1>(p); + + for (auto _ : state) { + UNREFERENCED_PARAMETER(_); + TagVector tags; + tag_extractors.produceTags(str, tags); + RELEASE_ASSERT(tags.size() == tags_size, ""); + } +} +BENCHMARK(BM_ExtractTags)->DenseRange(0, 26, 1); + +} // namespace +} // namespace Stats +} // namespace Envoy diff --git a/test/common/tcp/conn_pool_test.cc b/test/common/tcp/conn_pool_test.cc index 7fc3408c2cca..6eaf1aa815c1 100644 --- a/test/common/tcp/conn_pool_test.cc +++ b/test/common/tcp/conn_pool_test.cc @@ -319,7 +319,6 @@ class TcpConnPoolImplDestructorTest : public Event::TestUsingSimulatedTime, EXPECT_CALL(*connection_, addReadFilter(_)); EXPECT_CALL(*connection_, connect()); EXPECT_CALL(*connection_, setConnectionStats(_)); - EXPECT_CALL(*connection_, noDelay(true)); EXPECT_CALL(*connection_, streamInfo()).Times(2); EXPECT_CALL(*connection_, id()).Times(AnyNumber()); diff --git a/test/common/thread_local/thread_local_impl_test.cc b/test/common/thread_local/thread_local_impl_test.cc index de6c67318c6a..89a13a6263ef 100644 --- a/test/common/thread_local/thread_local_impl_test.cc +++ b/test/common/thread_local/thread_local_impl_test.cc @@ -16,23 +16,18 @@ namespace Envoy { namespace ThreadLocal { TEST(MainThreadVerificationTest, All) { - // Main thread singleton is initialized in the constructor of tls instance. Call to main thread - // verification will fail before that. - EXPECT_DEATH(Thread::MainThread::isMainThread(), - "InjectableSingleton used prior to initialization"); + // Before threading is on, assertion on main thread should be true. + EXPECT_TRUE(Thread::MainThread::isMainThread()); { - EXPECT_DEATH(Thread::MainThread::isMainThread(), - "InjectableSingleton used prior to initialization"); InstanceImpl tls; - // Call to main thread verification should succeed after tls instance has been initialized. - ASSERT(Thread::MainThread::isMainThread()); + // Tls instance has been initialized. + // Call to main thread verification should succeed in main thread. + EXPECT_TRUE(Thread::MainThread::isMainThread()); tls.shutdownGlobalThreading(); tls.shutdownThread(); } - // Main thread singleton is cleared in the destructor of tls instance. Call to main thread - // verification will fail after that. - EXPECT_DEATH(Thread::MainThread::isMainThread(), - "InjectableSingleton used prior to initialization"); + // After threading is off, assertion on main thread should be true. + EXPECT_TRUE(Thread::MainThread::isMainThread()); } class TestThreadLocalObject : public ThreadLocalObject { @@ -305,6 +300,8 @@ TEST(ThreadLocalInstanceImplDispatcherTest, Dispatcher) { thread_dispatcher->run(Event::Dispatcher::RunType::NonBlock); // Verify we have the expected dispatcher for the new thread thread. EXPECT_EQ(thread_dispatcher.get(), &tls.dispatcher()); + // Verify that it is inside the worker thread. + EXPECT_FALSE(Thread::MainThread::isMainThread()); }); thread->join(); diff --git a/test/common/upstream/BUILD b/test/common/upstream/BUILD index b0822e79a1ce..d0d323a12952 100644 --- a/test/common/upstream/BUILD +++ b/test/common/upstream/BUILD @@ -104,6 +104,7 @@ envoy_cc_test( "//test/mocks/ssl:ssl_mocks", "//test/mocks/upstream:cluster_manager_mocks", "//test/mocks/upstream:health_checker_mocks", + "//test/test_common:test_runtime_lib", "//test/test_common:utility_lib", "@envoy_api//envoy/config/cluster/v3:pkg_cc_proto", "@envoy_api//envoy/config/core/v3:pkg_cc_proto", diff --git a/test/common/upstream/cds_api_impl_test.cc b/test/common/upstream/cds_api_impl_test.cc index 5f6a65696c52..285228a97d0b 100644 --- a/test/common/upstream/cds_api_impl_test.cc +++ b/test/common/upstream/cds_api_impl_test.cc @@ -40,7 +40,7 @@ class CdsApiImplTest : public testing::Test { cds_ = CdsApiImpl::create(cds_config, cm_, store_, validation_visitor_); cds_->setInitializedCb([this]() -> void { initialized_.ready(); }); - EXPECT_CALL(*cm_.subscription_factory_.subscription_, start(_, _)); + EXPECT_CALL(*cm_.subscription_factory_.subscription_, start(_)); cds_->initialize(); cds_callbacks_ = cm_.subscription_factory_.callbacks_; } diff --git a/test/common/upstream/cluster_manager_impl_test.cc b/test/common/upstream/cluster_manager_impl_test.cc index df0cfd1aba77..83c403131fb8 100644 --- a/test/common/upstream/cluster_manager_impl_test.cc +++ b/test/common/upstream/cluster_manager_impl_test.cc @@ -28,6 +28,7 @@ using ::testing::DoAll; using ::testing::Eq; using ::testing::InSequence; using ::testing::Invoke; +using ::testing::InvokeWithoutArgs; using ::testing::Mock; using ::testing::NiceMock; using ::testing::Return; @@ -237,6 +238,10 @@ TEST_F(ClusterManagerImplTest, MultipleProtocolCluster) { protocol_selection: USE_DOWNSTREAM_PROTOCOL )EOF"; create(parseBootstrapFromV3Yaml(yaml)); + auto info = + cluster_manager_->clusters().active_clusters_.find("http12_cluster")->second.get().info(); + EXPECT_NE(0, info->features() & Upstream::ClusterInfo::Features::USE_DOWNSTREAM_PROTOCOL); + checkConfigDump(R"EOF( static_clusters: - cluster: @@ -4216,8 +4221,10 @@ class PreconnectTest : public ClusterManagerImplTest { // Set up the HostSet. host1_ = makeTestHost(cluster_->info(), "tcp://127.0.0.1:80", time_system_); host2_ = makeTestHost(cluster_->info(), "tcp://127.0.0.1:80", time_system_); + host3_ = makeTestHost(cluster_->info(), "tcp://127.0.0.1:80", time_system_); + host4_ = makeTestHost(cluster_->info(), "tcp://127.0.0.1:80", time_system_); - HostVector hosts{host1_, host2_}; + HostVector hosts{host1_, host2_, host3_, host4_}; auto hosts_ptr = std::make_shared(hosts); // Sending non-mergeable updates. @@ -4229,6 +4236,8 @@ class PreconnectTest : public ClusterManagerImplTest { Cluster* cluster_{}; HostSharedPtr host1_; HostSharedPtr host2_; + HostSharedPtr host3_; + HostSharedPtr host4_; }; TEST_F(PreconnectTest, PreconnectOff) { @@ -4266,6 +4275,96 @@ TEST_F(PreconnectTest, PreconnectOn) { ->tcpConnPool(ResourcePriority::Default, nullptr); } +TEST_F(PreconnectTest, PreconnectHighHttp) { + // With preconnect set to 3, the first request will kick off 3 preconnect attempts. + initialize(3); + int http_preconnect = 0; + EXPECT_CALL(factory_, allocateConnPool_(_, _, _, _)) + .Times(4) + .WillRepeatedly(InvokeWithoutArgs([&]() -> Http::ConnectionPool::Instance* { + auto* ret = new NiceMock(); + ON_CALL(*ret, maybePreconnect(_)).WillByDefault(InvokeWithoutArgs([&]() -> bool { + ++http_preconnect; + return true; + })); + return ret; + })); + cluster_manager_->getThreadLocalCluster("cluster_1") + ->httpConnPool(ResourcePriority::Default, Http::Protocol::Http11, nullptr); + // Expect preconnect to be called 3 times across the four hosts. + EXPECT_EQ(3, http_preconnect); +} + +TEST_F(PreconnectTest, PreconnectHighTcp) { + // With preconnect set to 3, the first request will kick off 3 preconnect attempts. + initialize(3); + int tcp_preconnect = 0; + EXPECT_CALL(factory_, allocateTcpConnPool_(_)) + .Times(4) + .WillRepeatedly(InvokeWithoutArgs([&]() -> Tcp::ConnectionPool::Instance* { + auto* ret = new NiceMock(); + ON_CALL(*ret, maybePreconnect(_)).WillByDefault(InvokeWithoutArgs([&]() -> bool { + ++tcp_preconnect; + return true; + })); + return ret; + })); + cluster_manager_->getThreadLocalCluster("cluster_1") + ->tcpConnPool(ResourcePriority::Default, nullptr); + // Expect preconnect to be called 3 times across the four hosts. + EXPECT_EQ(3, tcp_preconnect); +} + +TEST_F(PreconnectTest, PreconnectCappedAt3) { + // With preconnect set to 20, no more than 3 connections will be preconnected. + initialize(20); + int http_preconnect = 0; + EXPECT_CALL(factory_, allocateConnPool_(_, _, _, _)) + .Times(4) + .WillRepeatedly(InvokeWithoutArgs([&]() -> Http::ConnectionPool::Instance* { + auto* ret = new NiceMock(); + ON_CALL(*ret, maybePreconnect(_)).WillByDefault(InvokeWithoutArgs([&]() -> bool { + ++http_preconnect; + return true; + })); + return ret; + })); + cluster_manager_->getThreadLocalCluster("cluster_1") + ->httpConnPool(ResourcePriority::Default, Http::Protocol::Http11, nullptr); + // Expect preconnect to be called 3 times across the four hosts. + EXPECT_EQ(3, http_preconnect); + + // A subsequent call to get a connection will consume one of the preconnected + // connections, leaving two in queue, and kick off 2 more. This time we won't + // do the full 3 as the number of outstanding preconnects is limited by the + // number of healthy hosts. + http_preconnect = 0; + cluster_manager_->getThreadLocalCluster("cluster_1") + ->httpConnPool(ResourcePriority::Default, Http::Protocol::Http11, nullptr); + EXPECT_EQ(2, http_preconnect); +} + +TEST_F(PreconnectTest, PreconnectCappedByMaybePreconnect) { + // Set preconnect high, and verify preconnecting stops when maybePreconnect returns false. + initialize(20); + int http_preconnect_calls = 0; + EXPECT_CALL(factory_, allocateConnPool_(_, _, _, _)) + .Times(2) + .WillRepeatedly(InvokeWithoutArgs([&]() -> Http::ConnectionPool::Instance* { + auto* ret = new NiceMock(); + ON_CALL(*ret, maybePreconnect(_)).WillByDefault(InvokeWithoutArgs([&]() -> bool { + ++http_preconnect_calls; + // Force maybe preconnect to fail. + return false; + })); + return ret; + })); + cluster_manager_->getThreadLocalCluster("cluster_1") + ->httpConnPool(ResourcePriority::Default, Http::Protocol::Http11, nullptr); + // Expect preconnect to be called once and then preconnecting is stopped. + EXPECT_EQ(1, http_preconnect_calls); +} + } // namespace } // namespace Upstream } // namespace Envoy diff --git a/test/common/upstream/eds_speed_test.cc b/test/common/upstream/eds_speed_test.cc index 33bd77353ad4..59d6740f4aac 100644 --- a/test/common/upstream/eds_speed_test.cc +++ b/test/common/upstream/eds_speed_test.cc @@ -65,7 +65,7 @@ class EdsSpeedTest { )EOF", Envoy::Upstream::Cluster::InitializePhase::Secondary); - EXPECT_CALL(*cm_.subscription_factory_.subscription_, start(_, _)); + EXPECT_CALL(*cm_.subscription_factory_.subscription_, start(_)); cluster_->initialize([this] { initialized_ = true; }); EXPECT_CALL(*async_client_, startRaw(_, _, _, _)).WillOnce(testing::Return(&async_stream_)); subscription_->start({"fare"}); @@ -86,7 +86,7 @@ class EdsSpeedTest { eds_callbacks_ = cm_.subscription_factory_.callbacks_; subscription_ = std::make_unique( grpc_mux_, *eds_callbacks_, resource_decoder_, subscription_stats_, type_url_, dispatcher_, - std::chrono::milliseconds(), false); + std::chrono::milliseconds(), false, false); } // Set up an EDS config with multiple priorities, localities, weights and make sure diff --git a/test/common/upstream/eds_test.cc b/test/common/upstream/eds_test.cc index a487b2dcc123..2c8abf6159c5 100644 --- a/test/common/upstream/eds_test.cc +++ b/test/common/upstream/eds_test.cc @@ -23,6 +23,7 @@ #include "test/mocks/ssl/mocks.h" #include "test/mocks/upstream/cluster_manager.h" #include "test/mocks/upstream/health_checker.h" +#include "test/test_common/test_runtime.h" #include "test/test_common/utility.h" #include "gmock/gmock.h" @@ -35,7 +36,7 @@ namespace Upstream { namespace { class EdsTest : public testing::Test { -protected: +public: EdsTest() : api_(Api::createApiForTest(stats_)) { resetCluster(); } void resetCluster() { @@ -104,7 +105,7 @@ class EdsTest : public testing::Test { } void initialize() { - EXPECT_CALL(*cm_.subscription_factory_.subscription_, start(_, _)); + EXPECT_CALL(*cm_.subscription_factory_.subscription_, start(_)); cluster_->initialize([this] { initialized_ = true; }); } @@ -324,6 +325,56 @@ TEST_F(EdsTest, EdsClusterFromFileIsPrimaryCluster) { EXPECT_TRUE(initialized_); } +namespace { + +void endpointWeightChangeCausesRebuildTest(EdsTest& test, bool expect_rebuild) { + envoy::config::endpoint::v3::ClusterLoadAssignment cluster_load_assignment; + cluster_load_assignment.set_cluster_name("fare"); + auto* endpoints = cluster_load_assignment.add_endpoints(); + auto* endpoint = endpoints->add_lb_endpoints(); + endpoint->mutable_endpoint()->mutable_address()->mutable_socket_address()->set_address("1.2.3.4"); + endpoint->mutable_endpoint()->mutable_address()->mutable_socket_address()->set_port_value(80); + endpoint->mutable_load_balancing_weight()->set_value(30); + + test.initialize(); + test.doOnConfigUpdateVerifyNoThrow(cluster_load_assignment); + EXPECT_TRUE(test.initialized_); + EXPECT_EQ(0UL, test.stats_.counter("cluster.name.update_no_rebuild").value()); + EXPECT_EQ(30UL, + test.stats_.gauge("cluster.name.max_host_weight", Stats::Gauge::ImportMode::Accumulate) + .value()); + auto& hosts = test.cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_EQ(hosts.size(), 1); + EXPECT_EQ(hosts[0]->weight(), 30); + + endpoint->mutable_load_balancing_weight()->set_value(31); + test.doOnConfigUpdateVerifyNoThrow(cluster_load_assignment); + EXPECT_EQ(expect_rebuild ? 0UL : 1UL, + test.stats_.counter("cluster.name.update_no_rebuild").value()); + EXPECT_EQ(31UL, + test.stats_.gauge("cluster.name.max_host_weight", Stats::Gauge::ImportMode::Accumulate) + .value()); + auto& new_hosts = test.cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_EQ(new_hosts.size(), 1); + EXPECT_EQ(new_hosts[0]->weight(), 31); +} + +} // namespace + +// Verify that host weight changes cause a full rebuild. +TEST_F(EdsTest, EndpointWeightChangeCausesRebuild) { + endpointWeightChangeCausesRebuildTest(*this, true); +} + +// Verify that host weight changes do not cause a full rebuild when the feature flag is disabled. +TEST_F(EdsTest, EndpointWeightChangeCausesRebuildDisabled) { + TestScopedRuntime scoped_runtime; + Runtime::LoaderSingleton::getExisting()->mergeValues( + {{"envoy.reloadable_features.upstream_host_weight_change_causes_rebuild", "false"}}); + + endpointWeightChangeCausesRebuildTest(*this, false); +} + // Validate that onConfigUpdate() updates the endpoint metadata. TEST_F(EdsTest, EndpointMetadata) { envoy::config::endpoint::v3::ClusterLoadAssignment cluster_load_assignment; @@ -786,7 +837,7 @@ TEST_F(EdsTest, EndpointRemovalClusterDrainOnHostRemoval) { } // Verifies that if an endpoint is moved to a new priority, the active hc status is preserved. -TEST_F(EdsTest, EndpointMovedToNewPriority) { +TEST_F(EdsTest, EndpointMovedToNewPriorityWithDrain) { envoy::config::endpoint::v3::ClusterLoadAssignment cluster_load_assignment; cluster_load_assignment.set_cluster_name("fare"); resetClusterDrainOnHostRemoval(); @@ -848,6 +899,7 @@ TEST_F(EdsTest, EndpointMovedToNewPriority) { // The endpoint was healthy in the original priority, so moving it // around should preserve that. EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); } { @@ -860,6 +912,7 @@ TEST_F(EdsTest, EndpointMovedToNewPriority) { // The endpoint was healthy in the original priority, so moving it // around should preserve that. EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); } // Moves all the endpoints to priority 1. @@ -881,13 +934,15 @@ TEST_F(EdsTest, EndpointMovedToNewPriority) { // The endpoints were healthy, so moving them around should preserve that. EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); EXPECT_FALSE(hosts[1]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[1]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); } } // Verifies that if an endpoint is moved between priorities, the health check value // of the host is preserved -TEST_F(EdsTest, EndpointMoved) { +TEST_F(EdsTest, EndpointMovedWithDrain) { envoy::config::endpoint::v3::ClusterLoadAssignment cluster_load_assignment; cluster_load_assignment.set_cluster_name("fare"); resetClusterDrainOnHostRemoval(); @@ -957,6 +1012,7 @@ TEST_F(EdsTest, EndpointMoved) { // The endpoint was healthy in the original priority, so moving it // around should preserve that. EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); } { @@ -970,6 +1026,296 @@ TEST_F(EdsTest, EndpointMoved) { // The endpoint was healthy in the original priority, so moving it // around should preserve that. EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); + } +} + +// Verifies that if an endpoint is moved to a new priority, the active hc status is preserved. +TEST_F(EdsTest, EndpointMovedToNewPriority) { + envoy::config::endpoint::v3::ClusterLoadAssignment cluster_load_assignment; + cluster_load_assignment.set_cluster_name("fare"); + resetCluster(); + + auto health_checker = std::make_shared(); + EXPECT_CALL(*health_checker, start()); + EXPECT_CALL(*health_checker, addHostCheckCompleteCb(_)).Times(2); + cluster_->setHealthChecker(health_checker); + + auto add_endpoint = [&cluster_load_assignment](int port, int priority) { + auto* endpoints = cluster_load_assignment.add_endpoints(); + endpoints->set_priority(priority); + + auto* socket_address = endpoints->add_lb_endpoints() + ->mutable_endpoint() + ->mutable_address() + ->mutable_socket_address(); + socket_address->set_address("1.2.3.4"); + socket_address->set_port_value(port); + }; + + add_endpoint(80, 0); + add_endpoint(81, 0); + + doOnConfigUpdateVerifyNoThrow(cluster_load_assignment); + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_EQ(hosts.size(), 2); + + // Mark the hosts as healthy + for (auto& host : hosts) { + EXPECT_TRUE(host->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + host->healthFlagClear(Host::HealthFlag::FAILED_ACTIVE_HC); + host->healthFlagClear(Host::HealthFlag::PENDING_ACTIVE_HC); + } + } + + // Moves the endpoints between priorities + cluster_load_assignment.clear_endpoints(); + add_endpoint(81, 0); + add_endpoint(80, 1); + + // Verify that no hosts gets added or removed to/from the PrioritySet. + cluster_->prioritySet().addMemberUpdateCb([&](const auto& added, const auto& removed) { + EXPECT_TRUE(added.empty()); + EXPECT_TRUE(removed.empty()); + }); + + doOnConfigUpdateVerifyNoThrow(cluster_load_assignment); + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_EQ(hosts.size(), 1); + + // assert that it didn't move + EXPECT_EQ(hosts[0]->address()->asString(), "1.2.3.4:81"); + + // The endpoint was healthy in the original priority, so moving it + // around should preserve that. + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); + } + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[1]->hosts(); + EXPECT_EQ(hosts.size(), 1); + + // assert that it moved + EXPECT_EQ(hosts[0]->address()->asString(), "1.2.3.4:80"); + + // The endpoint was healthy in the original priority, so moving it + // around should preserve that. + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); + } + + // Moves all the endpoints to priority 1. + cluster_load_assignment.clear_endpoints(); + add_endpoint(80, 1); + add_endpoint(81, 1); + + doOnConfigUpdateVerifyNoThrow(cluster_load_assignment); + + { + // Priority 0 should now be empty. + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_EQ(hosts.size(), 0); + } + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[1]->hosts(); + EXPECT_EQ(hosts.size(), 2); + + // The endpoints were healthy, so moving them around should preserve that. + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); + EXPECT_FALSE(hosts[1]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[1]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); + } +} + +// Verifies that if an endpoint is moved between priorities, the health check value +// of the host is preserved +TEST_F(EdsTest, EndpointMoved) { + envoy::config::endpoint::v3::ClusterLoadAssignment cluster_load_assignment; + cluster_load_assignment.set_cluster_name("fare"); + resetCluster(); + + auto health_checker = std::make_shared(); + EXPECT_CALL(*health_checker, start()); + EXPECT_CALL(*health_checker, addHostCheckCompleteCb(_)).Times(2); + cluster_->setHealthChecker(health_checker); + + auto add_endpoint = [&cluster_load_assignment](int port, int priority) { + auto* endpoints = cluster_load_assignment.add_endpoints(); + endpoints->set_priority(priority); + + auto* socket_address = endpoints->add_lb_endpoints() + ->mutable_endpoint() + ->mutable_address() + ->mutable_socket_address(); + socket_address->set_address("1.2.3.4"); + socket_address->set_port_value(port); + }; + + add_endpoint(80, 0); + add_endpoint(81, 1); + doOnConfigUpdateVerifyNoThrow(cluster_load_assignment); + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_EQ(hosts.size(), 1); + + EXPECT_TRUE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_EQ(0, hosts[0]->priority()); + // Mark the host as healthy and remove the pending active hc flag. + hosts[0]->healthFlagClear(Host::HealthFlag::FAILED_ACTIVE_HC); + hosts[0]->healthFlagClear(Host::HealthFlag::PENDING_ACTIVE_HC); + } + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[1]->hosts(); + EXPECT_EQ(hosts.size(), 1); + + EXPECT_TRUE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_EQ(1, hosts[0]->priority()); + // Mark the host as healthy and remove the pending active hc flag. + hosts[0]->healthFlagClear(Host::HealthFlag::FAILED_ACTIVE_HC); + hosts[0]->healthFlagClear(Host::HealthFlag::PENDING_ACTIVE_HC); + } + + // Moves the endpoints between priorities + cluster_load_assignment.clear_endpoints(); + add_endpoint(81, 0); + add_endpoint(80, 1); + // Verify that no hosts gets added or removed to/from the PrioritySet. + cluster_->prioritySet().addMemberUpdateCb([&](const auto& added, const auto& removed) { + EXPECT_TRUE(added.empty()); + EXPECT_TRUE(removed.empty()); + }); + doOnConfigUpdateVerifyNoThrow(cluster_load_assignment); + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_EQ(hosts.size(), 1); + + // assert that it moved + EXPECT_EQ(hosts[0]->address()->asString(), "1.2.3.4:81"); + EXPECT_EQ(0, hosts[0]->priority()); + + // The endpoint was healthy in the original priority, so moving it + // around should preserve that. + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); + } + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[1]->hosts(); + EXPECT_EQ(hosts.size(), 1); + + // assert that it moved + EXPECT_EQ(hosts[0]->address()->asString(), "1.2.3.4:80"); + EXPECT_EQ(1, hosts[0]->priority()); + + // The endpoint was healthy in the original priority, so moving it + // around should preserve that. + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + EXPECT_FALSE(hosts[0]->healthFlagGet(Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); + } +} + +// Verifies that if an endpoint is moved to a new priority and has its health check address altered +// then nothing bad happens +TEST_F(EdsTest, EndpointMovedToNewPriorityWithHealthAddressChange) { + envoy::config::endpoint::v3::ClusterLoadAssignment cluster_load_assignment; + cluster_load_assignment.set_cluster_name("fare"); + resetCluster(); + + auto health_checker = std::make_shared(); + EXPECT_CALL(*health_checker, start()); + EXPECT_CALL(*health_checker, addHostCheckCompleteCb(_)).Times(2); + cluster_->setHealthChecker(health_checker); + + auto add_endpoint = [&cluster_load_assignment](int port, int priority, int health_port) { + auto* endpoints = cluster_load_assignment.add_endpoints(); + endpoints->set_priority(priority); + auto* endpoint = endpoints->add_lb_endpoints()->mutable_endpoint(); + + auto* socket_address = endpoint->mutable_address()->mutable_socket_address(); + socket_address->set_address("1.2.3.4"); + socket_address->set_port_value(port); + + endpoint->mutable_health_check_config()->set_port_value(health_port); + }; + + add_endpoint(80, 0, 80); + add_endpoint(81, 1, 81); + + doOnConfigUpdateVerifyNoThrow(cluster_load_assignment); + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_EQ(hosts.size(), 1); + + EXPECT_TRUE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + hosts[0]->healthFlagClear(Host::HealthFlag::FAILED_ACTIVE_HC); + hosts[0]->healthFlagClear(Host::HealthFlag::PENDING_ACTIVE_HC); + } + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[1]->hosts(); + EXPECT_EQ(hosts.size(), 1); + + EXPECT_TRUE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + hosts[0]->healthFlagClear(Host::HealthFlag::FAILED_ACTIVE_HC); + hosts[0]->healthFlagClear(Host::HealthFlag::PENDING_ACTIVE_HC); + } + + cluster_load_assignment.clear_endpoints(); + add_endpoint(80, 0, 80); + add_endpoint(81, 0, 82); + + // Changing a health check endpoint at the same time as priority is an add and immediate remove + cluster_->prioritySet().addMemberUpdateCb([&](const auto& added, const auto& removed) { + EXPECT_EQ(added.size(), 1); + EXPECT_EQ(removed.size(), 1); + }); + + doOnConfigUpdateVerifyNoThrow(cluster_load_assignment); + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_EQ(hosts.size(), 2); + + EXPECT_EQ(hosts[1]->address()->asString(), "1.2.3.4:81"); + EXPECT_TRUE(hosts[1]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); + hosts[1]->healthFlagClear(Host::HealthFlag::FAILED_ACTIVE_HC); + hosts[1]->healthFlagClear(Host::HealthFlag::PENDING_ACTIVE_HC); + } + + cluster_load_assignment.clear_endpoints(); + add_endpoint(80, 0, 80); + add_endpoint(81, 1, 83); + + // Changing a health check endpoint at the same time as priority is an add and immediate remove + cluster_->prioritySet().addMemberUpdateCb([&](const auto& added, const auto& removed) { + EXPECT_EQ(added.size(), 1); + EXPECT_EQ(removed.size(), 1); + }); + + doOnConfigUpdateVerifyNoThrow(cluster_load_assignment); + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_EQ(hosts.size(), 1); + } + + { + auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[1]->hosts(); + EXPECT_EQ(hosts.size(), 1); + + EXPECT_EQ(hosts[0]->address()->asString(), "1.2.3.4:81"); + EXPECT_TRUE(hosts[0]->healthFlagGet(Host::HealthFlag::FAILED_ACTIVE_HC)); } } diff --git a/test/common/upstream/health_check_fuzz.cc b/test/common/upstream/health_check_fuzz.cc index 1d109d74bda4..d2e37392b296 100644 --- a/test/common/upstream/health_check_fuzz.cc +++ b/test/common/upstream/health_check_fuzz.cc @@ -304,7 +304,7 @@ void TcpHealthCheckFuzz::raiseEvent(const Network::ConnectionEvent& event_type, } // In the specific case of: - // https://github.com/envoyproxy/envoy/blob/master/source/common/upstream/health_checker_impl.cc#L489 + // https://github.com/envoyproxy/envoy/blob/main/source/common/upstream/health_checker_impl.cc#L489 // This blows away client, should create a new one if (event_type == Network::ConnectionEvent::Connected && empty_response_) { ENVOY_LOG_MISC(trace, "Will create client from connected event and empty response."); diff --git a/test/common/upstream/health_check_fuzz_test_utils.cc b/test/common/upstream/health_check_fuzz_test_utils.cc index 78b5feac68fd..e67e5f7a6cf0 100644 --- a/test/common/upstream/health_check_fuzz_test_utils.cc +++ b/test/common/upstream/health_check_fuzz_test_utils.cc @@ -51,11 +51,12 @@ void HttpHealthCheckerImplTestBase::expectClientCreate( std::shared_ptr cluster{ new NiceMock()}; Event::MockDispatcher dispatcher_; - return new CodecClientForTest( + test_session.codec_client_ = new CodecClientForTest( Http::CodecClient::Type::HTTP1, std::move(conn_data.connection_), test_session.codec_, nullptr, Upstream::makeTestHost(cluster, "tcp://127.0.0.1:9000", dispatcher_.timeSource()), dispatcher_); + return test_session.codec_client_; })); } diff --git a/test/common/upstream/health_check_fuzz_test_utils.h b/test/common/upstream/health_check_fuzz_test_utils.h index 1074823314e7..a87eb51232f9 100644 --- a/test/common/upstream/health_check_fuzz_test_utils.h +++ b/test/common/upstream/health_check_fuzz_test_utils.h @@ -48,6 +48,7 @@ class HttpHealthCheckerImplTestBase : public HealthCheckerTestBase { Network::MockClientConnection* client_connection_{}; NiceMock request_encoder_; Http::ResponseDecoder* stream_response_callbacks_{}; + CodecClientForTest* codec_client_{}; }; using TestSessionPtr = std::unique_ptr; diff --git a/test/common/upstream/health_checker_impl_test.cc b/test/common/upstream/health_checker_impl_test.cc index 4c97ea1dd7dd..79673e3ae47c 100644 --- a/test/common/upstream/health_checker_impl_test.cc +++ b/test/common/upstream/health_checker_impl_test.cc @@ -1,5 +1,6 @@ #include #include +#include #include #include "envoy/config/core/v3/base.pb.h" @@ -137,6 +138,7 @@ class HttpHealthCheckerImplTest : public Event::TestUsingSimulatedTime, Network::MockClientConnection* client_connection_{}; NiceMock request_encoder_; Http::ResponseDecoder* stream_response_callbacks_{}; + CodecClientForTest* codec_client_{}; }; using TestSessionPtr = std::unique_ptr; @@ -176,6 +178,25 @@ class HttpHealthCheckerImplTest : public Event::TestUsingSimulatedTime, addCompletionCallback(); } + void setupHCHttp2() { + const std::string yaml = R"EOF( + timeout: 1s + interval: 1s + no_traffic_interval: 5s + interval_jitter: 1s + unhealthy_threshold: 1 + healthy_threshold: 1 + http_health_check: + service_name_matcher: + prefix: locations + path: /healthcheck + codec_client_type: Http2 + )EOF"; + + allocHealthChecker(yaml); + addCompletionCallback(); + } + void setupInitialJitter() { const std::string yaml = R"EOF( timeout: 1s @@ -561,10 +582,11 @@ class HttpHealthCheckerImplTest : public Event::TestUsingSimulatedTime, std::shared_ptr cluster{ new NiceMock()}; Event::MockDispatcher dispatcher_; - return new CodecClientForTest( + test_session.codec_client_ = new CodecClientForTest( Http::CodecClient::Type::HTTP1, std::move(conn_data.connection_), test_session.codec_, nullptr, Upstream::makeTestHost(cluster, "tcp://127.0.0.1:9000", simTime()), dispatcher_); + return test_session.codec_client_; })); } @@ -663,6 +685,35 @@ class HttpHealthCheckerImplTest : public Event::TestUsingSimulatedTime, MOCK_METHOD(void, onHostStatus, (HostSharedPtr host, HealthTransition changed_state)); + void expectUnhealthyTransition(size_t index, bool first_check) { + EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Changed)); + EXPECT_CALL(*test_sessions_[index]->timeout_timer_, disableTimer()); + EXPECT_CALL(*test_sessions_[index]->interval_timer_, enableTimer(_, _)); + if (first_check) { + EXPECT_CALL(event_logger_, logUnhealthy(_, _, _, _)); + } + EXPECT_CALL(event_logger_, logEjectUnhealthy(_, _, _)); + } + + void expectHealthyTransition(size_t index) { + EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Changed)); + EXPECT_CALL(*test_sessions_[index]->timeout_timer_, disableTimer()); + EXPECT_CALL(*test_sessions_[index]->interval_timer_, enableTimer(_, _)); + EXPECT_CALL(event_logger_, logAddHealthy(_, _, _)); + } + + void expectUnchanged(size_t index) { + EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); + EXPECT_CALL(*test_sessions_[index]->timeout_timer_, disableTimer()); + EXPECT_CALL(*test_sessions_[index]->interval_timer_, enableTimer(_, _)); + } + + void expectChangePending(size_t index) { + EXPECT_CALL(*this, onHostStatus(_, HealthTransition::ChangePending)); + EXPECT_CALL(*test_sessions_[index]->timeout_timer_, disableTimer()); + EXPECT_CALL(*test_sessions_[index]->interval_timer_, enableTimer(_, _)); + } + std::vector test_sessions_; std::shared_ptr health_checker_; std::list connection_index_{}; @@ -2052,56 +2103,6 @@ TEST_F(HttpHealthCheckerImplTest, ProxyConnectionClose) { test_sessions_[0]->interval_timer_->invokeCallback(); } -TEST_F(HttpHealthCheckerImplTest, ConnectionCloseLegacy) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.fixed_connection_close", "false"}}); - setupNoServiceValidationHC(); - EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); - - cluster_->prioritySet().getMockHostSet(0)->hosts_ = { - makeTestHost(cluster_->info_, "tcp://127.0.0.1:80", simTime())}; - expectSessionCreate(); - expectStreamCreate(0); - EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); - health_checker_->start(); - - EXPECT_CALL(*test_sessions_[0]->interval_timer_, enableTimer(_, _)); - EXPECT_CALL(*test_sessions_[0]->timeout_timer_, disableTimer()); - respond(0, "200", true); - EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); - - expectClientCreate(0); - expectStreamCreate(0); - EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); - test_sessions_[0]->interval_timer_->invokeCallback(); -} - -TEST_F(HttpHealthCheckerImplTest, ProxyConnectionCloseLegacy) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.fixed_connection_close", "false"}}); - setupNoServiceValidationHC(); - EXPECT_CALL(*this, onHostStatus(_, HealthTransition::Unchanged)); - - cluster_->prioritySet().getMockHostSet(0)->hosts_ = { - makeTestHost(cluster_->info_, "tcp://127.0.0.1:80", simTime())}; - expectSessionCreate(); - expectStreamCreate(0); - EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); - health_checker_->start(); - - EXPECT_CALL(*test_sessions_[0]->interval_timer_, enableTimer(_, _)); - EXPECT_CALL(*test_sessions_[0]->timeout_timer_, disableTimer()); - respond(0, "200", false, true); - EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); - - expectClientCreate(0); - expectStreamCreate(0); - EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); - test_sessions_[0]->interval_timer_->invokeCallback(); -} - TEST_F(HttpHealthCheckerImplTest, HealthCheckIntervals) { setupHealthCheckIntervalOverridesHC(); cluster_->prioritySet().getMockHostSet(0)->hosts_ = { @@ -2612,6 +2613,290 @@ TEST_F(HttpHealthCheckerImplTest, NoTransportSocketMatchCriteria) { health_checker_->start(); } +// Test receiving GOAWAY (error) is interpreted as connection close event. +TEST_F(HttpHealthCheckerImplTest, GoAwayErrorProbeInProgress) { + // FailureType::Network will be issued, it will render host unhealthy only if unhealthy_threshold + // is reached. + setupNoServiceValidationHCWithHttp2(); + EXPECT_CALL(runtime_.snapshot_, featureEnabled("health_check.verify_cluster", 100)) + .WillRepeatedly(Return(false)); + + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80", simTime())}; + cluster_->info_->stats().upstream_cx_total_.inc(); + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + health_checker_->start(); + + // We start off as healthy, and should continue to be healthy. + expectUnchanged(0); + respond(0, "200", false, false, true, false, {}, false); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + expectStreamCreate(0); + test_sessions_[0]->interval_timer_->invokeCallback(); + + // GOAWAY with non-NO_ERROR code will result in a healthcheck failure and the + // connection closing. Status is unchanged because unhealthy_threshold is 2. + expectChangePending(0); + EXPECT_CALL( + runtime_.snapshot_, + runtimeFeatureEnabled("envoy.reloadable_features.health_check.graceful_goaway_handling")) + .WillOnce(Return(true)); + test_sessions_[0]->codec_client_->raiseGoAway(Http::GoAwayErrorCode::Other); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + expectClientCreate(0); + expectStreamCreate(0); + + test_sessions_[0]->interval_timer_->invokeCallback(); + + // GOAWAY with non-NO_ERROR code will result in a healthcheck failure and the + // connection closing. This time it goes unhealthy. + expectUnhealthyTransition(0, false); + EXPECT_CALL( + runtime_.snapshot_, + runtimeFeatureEnabled("envoy.reloadable_features.health_check.graceful_goaway_handling")) + .WillOnce(Return(true)); + test_sessions_[0]->codec_client_->raiseGoAway(Http::GoAwayErrorCode::Other); + EXPECT_EQ(Host::Health::Unhealthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + EXPECT_TRUE(cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->healthFlagGet( + Host::HealthFlag::FAILED_ACTIVE_HC)); +} + +// Test receiving GOAWAY (no error) is handled gracefully while a check is in progress. +TEST_F(HttpHealthCheckerImplTest, GoAwayProbeInProgress) { + setupHCHttp2(); + EXPECT_CALL(runtime_.snapshot_, featureEnabled("health_check.verify_cluster", 100)) + .WillRepeatedly(Return(false)); + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80", simTime())}; + cluster_->info_->stats().upstream_cx_total_.inc(); + + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + health_checker_->start(); + + EXPECT_CALL( + runtime_.snapshot_, + runtimeFeatureEnabled("envoy.reloadable_features.health_check.graceful_goaway_handling")) + .WillOnce(Return(true)); + // GOAWAY with NO_ERROR code during check should be handled gracefully. + test_sessions_[0]->codec_client_->raiseGoAway(Http::GoAwayErrorCode::NoError); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + expectUnchanged(0); + respond(0, "200", false, false, true, false, {}, false); + + // GOAWAY should cause a new connection to be created. + expectClientCreate(0); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + test_sessions_[0]->interval_timer_->invokeCallback(); + + // Test host state hasn't changed. + expectUnchanged(0); + respond(0, "200", false, false, true, false, {}, false); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); +} + +// Test receiving GOAWAY (no error) closes connection after an in progress probe times outs. +TEST_F(HttpHealthCheckerImplTest, GoAwayProbeInProgressTimeout) { + setupHCHttp2(); + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80", simTime())}; + EXPECT_CALL(runtime_.snapshot_, featureEnabled("health_check.verify_cluster", 100)) + .WillRepeatedly(Return(false)); + + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + health_checker_->start(); + + EXPECT_CALL( + runtime_.snapshot_, + runtimeFeatureEnabled("envoy.reloadable_features.health_check.graceful_goaway_handling")) + .WillOnce(Return(true)); + test_sessions_[0]->codec_client_->raiseGoAway(Http::GoAwayErrorCode::NoError); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + // Unhealthy threshold is 1 so first timeout causes unhealthy + expectUnhealthyTransition(0, true); + test_sessions_[0]->timeout_timer_->invokeCallback(); + EXPECT_EQ(Host::Health::Unhealthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + // GOAWAY should cause a new connection to be created. + expectClientCreate(0); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + test_sessions_[0]->interval_timer_->invokeCallback(); + + // Host should go back to healthy after a successful check. + expectHealthyTransition(0); + respond(0, "200", false, false, true, false, {}, false); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); +} + +// Test receiving GOAWAY (no error) closes connection after a stream reset. +TEST_F(HttpHealthCheckerImplTest, GoAwayProbeInProgressStreamReset) { + setupHCHttp2(); + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80", simTime())}; + EXPECT_CALL(runtime_.snapshot_, featureEnabled("health_check.verify_cluster", 100)) + .WillRepeatedly(Return(false)); + + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + health_checker_->start(); + + EXPECT_CALL( + runtime_.snapshot_, + runtimeFeatureEnabled("envoy.reloadable_features.health_check.graceful_goaway_handling")) + .WillOnce(Return(true)); + test_sessions_[0]->codec_client_->raiseGoAway(Http::GoAwayErrorCode::NoError); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + // Unhealthy threshold is 1 so first timeout causes unhealthy + expectUnhealthyTransition(0, true); + test_sessions_[0]->request_encoder_.stream_.resetStream(Http::StreamResetReason::RemoteReset); + EXPECT_EQ(Host::Health::Unhealthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + // GOAWAY should cause a new connection to be created. + expectClientCreate(0); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + test_sessions_[0]->interval_timer_->invokeCallback(); + + // Host should go back to healthy after a successful check. + expectHealthyTransition(0); + respond(0, "200", false, false, true, false, {}, false); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); +} + +// Test receiving GOAWAY (no error) and a connection close. +TEST_F(HttpHealthCheckerImplTest, GoAwayProbeInProgressConnectionClose) { + setupHCHttp2(); + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80", simTime())}; + EXPECT_CALL(runtime_.snapshot_, featureEnabled("health_check.verify_cluster", 100)) + .WillRepeatedly(Return(false)); + + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + health_checker_->start(); + + EXPECT_CALL( + runtime_.snapshot_, + runtimeFeatureEnabled("envoy.reloadable_features.health_check.graceful_goaway_handling")) + .WillOnce(Return(true)); + test_sessions_[0]->codec_client_->raiseGoAway(Http::GoAwayErrorCode::NoError); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + // Unhealthy threshold is 1 so first timeout causes unhealthy + expectUnhealthyTransition(0, true); + test_sessions_[0]->client_connection_->raiseEvent(Network::ConnectionEvent::RemoteClose); + EXPECT_EQ(Host::Health::Unhealthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + // GOAWAY should cause a new connection to be created. + expectClientCreate(0); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + test_sessions_[0]->interval_timer_->invokeCallback(); + + // Host should go back to healthy after a successful check. + expectHealthyTransition(0); + respond(0, "200", false, false, true, false, {}, false); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); +} + +// Test receiving GOAWAY between checks affects nothing. +TEST_F(HttpHealthCheckerImplTest, GoAwayBetweenChecks) { + setupHCHttp2(); + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80", simTime())}; + EXPECT_CALL(runtime_.snapshot_, featureEnabled("health_check.verify_cluster", 100)) + .WillRepeatedly(Return(false)); + + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + health_checker_->start(); + + expectUnchanged(0); + respond(0, "200", false, false, true, false, {}, false); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + EXPECT_CALL( + runtime_.snapshot_, + runtimeFeatureEnabled("envoy.reloadable_features.health_check.graceful_goaway_handling")) + .WillOnce(Return(true)); + // GOAWAY should cause a new connection to be created but should not affect health status. + test_sessions_[0]->codec_client_->raiseGoAway(Http::GoAwayErrorCode::Other); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + expectClientCreate(0); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + test_sessions_[0]->interval_timer_->invokeCallback(); + + // Host should stay healthy. + expectUnchanged(0); + respond(0, "200", false, false, true, false, {}, false); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); +} + +// Smoke test that receiving GOAWAY (no error) is ignored when GOAWAY handling +// is disabled via runtime. This is based on the +// GoAwayProbeInProgressStreamReset test case. A single case gives us sufficient +// coverage due to the simplicity of the runtime check. +TEST_F(HttpHealthCheckerImplTest, GoAwayProbeInProgressStreamResetRuntimeDisabled) { + setupHCHttp2(); + cluster_->prioritySet().getMockHostSet(0)->hosts_ = { + makeTestHost(cluster_->info_, "tcp://127.0.0.1:80", simTime())}; + EXPECT_CALL(runtime_.snapshot_, featureEnabled("health_check.verify_cluster", 100)) + .WillRepeatedly(Return(false)); + + expectSessionCreate(); + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + health_checker_->start(); + + EXPECT_CALL( + runtime_.snapshot_, + runtimeFeatureEnabled("envoy.reloadable_features.health_check.graceful_goaway_handling")) + .WillOnce(Return(false)); + + test_sessions_[0]->codec_client_->raiseGoAway(Http::GoAwayErrorCode::NoError); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + // Unhealthy threshold is 1 so the first reset causes the host to go unhealthy. + expectUnhealthyTransition(0, true); + test_sessions_[0]->request_encoder_.stream_.resetStream(Http::StreamResetReason::RemoteReset); + EXPECT_EQ(Host::Health::Unhealthy, + cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); + + // The new stream should be created on the same connection (old behavior since + // the new behavior is disabled via runtime). + expectStreamCreate(0); + EXPECT_CALL(*test_sessions_[0]->timeout_timer_, enableTimer(_, _)); + test_sessions_[0]->interval_timer_->invokeCallback(); + + // Host should go back to healthy after a successful check. + expectHealthyTransition(0); + respond(0, "200", false, false, true, false, {}, false); + EXPECT_EQ(Host::Health::Healthy, cluster_->prioritySet().getMockHostSet(0)->hosts_[0]->health()); +} + class TestProdHttpHealthChecker : public ProdHttpHealthCheckerImpl { public: using ProdHttpHealthCheckerImpl::ProdHttpHealthCheckerImpl; @@ -3951,6 +4236,21 @@ class GrpcHealthCheckerImplTestBase : public Event::TestUsingSimulatedTime, std::list codec_index_{}; }; +// NOLINTNEXTLINE(readability-identifier-naming) +void PrintTo(const GrpcHealthCheckerImplTestBase::ResponseSpec& spec, std::ostream* os) { + (*os) << "(headers{" << absl::StrJoin(spec.response_headers, ",", absl::PairFormatter(":")) + << "},"; + (*os) << "body{" << absl::StrJoin(spec.body_chunks, ",", [](std::string* out, const auto& spec) { + absl::StrAppend(out, spec.valid ? "valid" : "invalid", ",{", + absl::StrJoin(spec.data, "-", + [](std::string* out, uint8_t byte) { + absl::StrAppend(out, absl::Hex(byte, absl::kZeroPad2)); + }), + "}"); + }) << "}"; + (*os) << "trailers{" << absl::StrJoin(spec.trailers, ",", absl::PairFormatter(":")) << "})"; +} + class GrpcHealthCheckerImplTest : public testing::Test, public GrpcHealthCheckerImplTestBase {}; // Test single host check success. diff --git a/test/common/upstream/load_balancer_impl_test.cc b/test/common/upstream/load_balancer_impl_test.cc index 63f981cd7ab6..6135f6f9534a 100644 --- a/test/common/upstream/load_balancer_impl_test.cc +++ b/test/common/upstream/load_balancer_impl_test.cc @@ -596,7 +596,8 @@ TEST_P(FailoverTest, BasicDegradedHosts) { host_set_.degraded_hosts_ = host_set_.hosts_; failover_host_set_.hosts_ = failover_host_set_.healthy_hosts_; init(false); - EXPECT_EQ(host_set_.degraded_hosts_[0], lb_->peekAnotherHost(nullptr)); + // We don't preconnect degraded hosts. + EXPECT_EQ(nullptr, lb_->peekAnotherHost(nullptr)); EXPECT_EQ(host_set_.degraded_hosts_[0], lb_->chooseHost(nullptr)); } @@ -802,7 +803,8 @@ TEST_P(RoundRobinLoadBalancerTest, Normal) { hostSet().healthy_hosts_.push_back(makeTestHost(info_, "tcp://127.0.0.1:82", simTime())); hostSet().hosts_.push_back(hostSet().healthy_hosts_.back()); hostSet().runCallbacks({hostSet().healthy_hosts_.back()}, {}); - peekThenPick({2, 0, 1, 2}); + peekThenPick({2, 0, 1}); + peekThenPick({2}); // Now peek a few extra to push the index forward, alter the host set, and // make sure the index is restored to 0. @@ -812,7 +814,8 @@ TEST_P(RoundRobinLoadBalancerTest, Normal) { hostSet().healthy_hosts_.push_back(makeTestHost(info_, "tcp://127.0.0.1:83", simTime())); hostSet().hosts_.push_back(hostSet().healthy_hosts_.back()); hostSet().runCallbacks({hostSet().healthy_hosts_.back()}, {hostSet().healthy_hosts_.front()}); - peekThenPick({1, 2, 3}); + EXPECT_EQ(hostSet().healthy_hosts_[1], lb_->chooseHost(nullptr)); + peekThenPick({2, 3}); } // Validate that the RNG seed influences pick order. diff --git a/test/config/utility.cc b/test/config/utility.cc index e0e6012e520c..928d9416738e 100644 --- a/test/config/utility.cc +++ b/test/config/utility.cc @@ -1025,7 +1025,9 @@ void ConfigHelper::addSslConfig(const ServerSslOptions& options) { filter_chain->mutable_transport_socket()->mutable_typed_config()->PackFrom(tls_context); } -bool ConfigHelper::setAccessLog(const std::string& filename, absl::string_view format) { +bool ConfigHelper::setAccessLog( + const std::string& filename, absl::string_view format, + std::vector formatters) { if (getFilterFromListener("http") == nullptr) { return false; } @@ -1035,8 +1037,15 @@ bool ConfigHelper::setAccessLog(const std::string& filename, absl::string_view f loadHttpConnectionManager(hcm_config); envoy::extensions::access_loggers::file::v3::FileAccessLog access_log_config; if (!format.empty()) { - access_log_config.mutable_log_format()->mutable_text_format_source()->set_inline_string( - absl::StrCat(format, "\n")); + auto* log_format = access_log_config.mutable_log_format(); + log_format->mutable_text_format_source()->set_inline_string(absl::StrCat(format, "\n")); + if (!formatters.empty()) { + for (const auto& formatter : formatters) { + auto* added_formatter = log_format->add_formatters(); + added_formatter->set_name(formatter.name()); + added_formatter->mutable_typed_config()->PackFrom(formatter.typed_config()); + } + } } access_log_config.set_path(filename); hcm_config.mutable_access_log(0)->mutable_typed_config()->PackFrom(access_log_config); @@ -1154,6 +1163,12 @@ void ConfigHelper::addListenerFilter(const std::string& filter_yaml) { } } +void ConfigHelper::addBootstrapExtension(const std::string& config) { + RELEASE_ASSERT(!finalized_, ""); + auto* extension = bootstrap_.add_bootstrap_extensions(); + TestUtility::loadFromYaml(config, *extension); +} + bool ConfigHelper::loadHttpConnectionManager( envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& hcm) { return loadFilter< diff --git a/test/config/utility.h b/test/config/utility.h index b51843eb3341..b6368fdf0e04 100644 --- a/test/config/utility.h +++ b/test/config/utility.h @@ -199,6 +199,9 @@ class ConfigHelper { // Add a listener filter prior to existing filters. void addListenerFilter(const std::string& filter_yaml); + // Add a new bootstrap extension. + void addBootstrapExtension(const std::string& config); + // Sets the client codec to the specified type. void setClientCodec(envoy::extensions::filters::network::http_connection_manager::v3:: HttpConnectionManager::CodecType type); @@ -209,7 +212,8 @@ class ConfigHelper { // Set the HTTP access log for the first HCM (if present) to a given file. The default is // the platform's null device. - bool setAccessLog(const std::string& filename, absl::string_view format = ""); + bool setAccessLog(const std::string& filename, absl::string_view format = "", + std::vector formatters = {}); // Set the listener access log for the first listener to a given file. bool setListenerAccessLog(const std::string& filename, absl::string_view format = ""); diff --git a/test/extensions/access_loggers/common/grpc_access_logger_test.cc b/test/extensions/access_loggers/common/grpc_access_logger_test.cc index 1c2ce6b36249..fc6a37e871df 100644 --- a/test/extensions/access_loggers/common/grpc_access_logger_test.cc +++ b/test/extensions/access_loggers/common/grpc_access_logger_test.cc @@ -230,42 +230,6 @@ TEST_F(GrpcAccessLogTest, WatermarksOverrun) { TestUtility::findCounter(stats_store_, "mock_access_log_prefix.logs_dropped")->value()); } -// Test legacy behavior of unbounded access logs. -TEST_F(GrpcAccessLogTest, WatermarksLegacy) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.disallow_unbounded_access_logs", "false"}}); - - initLogger(FlushInterval, 1); - - // Start a stream for the first log. - MockAccessLogStream stream; - AccessLogCallbacks* callbacks; - expectStreamStart(stream, &callbacks); - - EXPECT_CALL(stream, isAboveWriteBufferHighWatermark()) - .Times(AnyNumber()) - .WillRepeatedly(Return(true)); - - // Fail to flush, so the log stays buffered up. - EXPECT_CALL(stream, sendMessageRaw_(_, false)).Times(0); - logger_->log(mockHttpEntry()); - // Message should still be initialized. - EXPECT_EQ(1, logger_->numInits()); - EXPECT_EQ(1, - TestUtility::findCounter(stats_store_, "mock_access_log_prefix.logs_written")->value()); - EXPECT_EQ(0, - TestUtility::findCounter(stats_store_, "mock_access_log_prefix.logs_dropped")->value()); - - // As with the above test, try to log more. The log will not be dropped. - EXPECT_CALL(stream, sendMessageRaw_(_, _)).Times(0); - logger_->log(mockHttpEntry()); - EXPECT_EQ(2, - TestUtility::findCounter(stats_store_, "mock_access_log_prefix.logs_written")->value()); - EXPECT_EQ(0, - TestUtility::findCounter(stats_store_, "mock_access_log_prefix.logs_dropped")->value()); -} - // Test that stream failure is handled correctly. TEST_F(GrpcAccessLogTest, StreamFailure) { initLogger(FlushInterval, 0); diff --git a/test/extensions/access_loggers/grpc/http_grpc_access_log_integration_test.cc b/test/extensions/access_loggers/grpc/http_grpc_access_log_integration_test.cc index 590a3d280e51..f2b5c2559933 100644 --- a/test/extensions/access_loggers/grpc/http_grpc_access_log_integration_test.cc +++ b/test/extensions/access_loggers/grpc/http_grpc_access_log_integration_test.cc @@ -126,7 +126,8 @@ class AccessLogIntegrationTest : public Grpc::VersionedGrpcClientIntegrationPara }; INSTANTIATE_TEST_SUITE_P(IpVersionsCientType, AccessLogIntegrationTest, - VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::VersionedGrpcClientIntegrationParamTest::protocolTestParamsToString); // Test a basic full access logging flow. TEST_P(AccessLogIntegrationTest, BasicAccessLogFlow) { diff --git a/test/extensions/access_loggers/grpc/tcp_grpc_access_log_integration_test.cc b/test/extensions/access_loggers/grpc/tcp_grpc_access_log_integration_test.cc index e8516e0f6d3a..51fc54f02d78 100644 --- a/test/extensions/access_loggers/grpc/tcp_grpc_access_log_integration_test.cc +++ b/test/extensions/access_loggers/grpc/tcp_grpc_access_log_integration_test.cc @@ -126,7 +126,8 @@ class TcpGrpcAccessLogIntegrationTest : public Grpc::VersionedGrpcClientIntegrat }; INSTANTIATE_TEST_SUITE_P(IpVersionsCientType, TcpGrpcAccessLogIntegrationTest, - VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::VersionedGrpcClientIntegrationParamTest::protocolTestParamsToString); // Test a basic full access logging flow. TEST_P(TcpGrpcAccessLogIntegrationTest, BasicAccessLogFlow) { diff --git a/test/extensions/bootstrap/wasm/BUILD b/test/extensions/bootstrap/wasm/BUILD index b9c8282420c3..eb3d63864f82 100644 --- a/test/extensions/bootstrap/wasm/BUILD +++ b/test/extensions/bootstrap/wasm/BUILD @@ -45,6 +45,21 @@ envoy_extension_cc_test( ], ) +envoy_extension_cc_test( + name = "wasm_integration_test", + srcs = ["wasm_integration_test.cc"], + data = envoy_select_wasm([ + "//test/extensions/bootstrap/wasm/test_data:http_cpp.wasm", + ]), + extension_name = "envoy.bootstrap.wasm", + deps = [ + "//source/extensions/bootstrap/wasm:config", + "//source/extensions/common/wasm:wasm_lib", + "//test/extensions/common/wasm:wasm_runtime", + "//test/integration:http_protocol_integration_lib", + ], +) + envoy_extension_cc_test( name = "config_test", srcs = ["config_test.cc"], diff --git a/test/extensions/bootstrap/wasm/config_test.cc b/test/extensions/bootstrap/wasm/config_test.cc index a0b7274e53fd..560a60d9cd56 100644 --- a/test/extensions/bootstrap/wasm/config_test.cc +++ b/test/extensions/bootstrap/wasm/config_test.cc @@ -53,6 +53,7 @@ class WasmFactoryTest : public testing::TestWithParam { EXPECT_CALL(context_, lifecycleNotifier()) .WillRepeatedly(testing::ReturnRef(lifecycle_notifier_)); extension_ = factory->createBootstrapExtension(config, context_); + extension_->onServerInitialized(); static_cast(extension_.get())->wasmService(); EXPECT_CALL(init_watcher_, ready()); init_manager_.initialize(init_watcher_); diff --git a/test/extensions/bootstrap/wasm/test_data/BUILD b/test/extensions/bootstrap/wasm/test_data/BUILD index d26678e60ee0..a8f2ec270f1a 100644 --- a/test/extensions/bootstrap/wasm/test_data/BUILD +++ b/test/extensions/bootstrap/wasm/test_data/BUILD @@ -84,6 +84,11 @@ envoy_wasm_cc_binary( srcs = ["emscripten_cpp.cc"], ) +envoy_wasm_cc_binary( + name = "http_cpp.wasm", + srcs = ["http_cpp.cc"], +) + envoy_wasm_cc_binary( name = "logging_cpp.wasm", srcs = ["logging_cpp.cc"], diff --git a/test/extensions/bootstrap/wasm/test_data/http_cpp.cc b/test/extensions/bootstrap/wasm/test_data/http_cpp.cc new file mode 100644 index 000000000000..7b70f7b7d4e0 --- /dev/null +++ b/test/extensions/bootstrap/wasm/test_data/http_cpp.cc @@ -0,0 +1,46 @@ +// NOLINT(namespace-envoy) +#include + +#include "proxy_wasm_intrinsics.h" + +template std::unique_ptr wrap_unique(T* ptr) { return std::unique_ptr(ptr); } + +START_WASM_PLUGIN(WasmHttpCpp) + +// Required Proxy-Wasm ABI version. +WASM_EXPORT(void, proxy_abi_version_0_1_0, ()) {} + +WASM_EXPORT(uint32_t, proxy_on_configure, (uint32_t, uint32_t)) { + proxy_set_tick_period_milliseconds(100); + return 1; +} + +WASM_EXPORT(void, proxy_on_tick, (uint32_t)) { + HeaderStringPairs headers; + headers.push_back(std::make_pair(":method", "GET")); + headers.push_back(std::make_pair(":path", "/")); + headers.push_back(std::make_pair(":authority", "example.com")); + headers.push_back(std::make_pair("x-test", "test")); + HeaderStringPairs trailers; + uint32_t token; + WasmResult result = makeHttpCall("wasm_cluster", headers, "", trailers, 10000, &token); + // We have sent successfully, stop timer - we only want to send one request. + if (result == WasmResult::Ok) { + proxy_set_tick_period_milliseconds(0); + } +} + +WASM_EXPORT(void, proxy_on_http_call_response, (uint32_t, uint32_t, uint32_t headers, uint32_t, uint32_t)) { + if (headers != 0) { + auto status = getHeaderMapValue(WasmHeaderMapType::HttpCallResponseHeaders, "status"); + if ("200" == status->view()) { + proxy_set_tick_period_milliseconds(0); + return; + } + } + // Request failed - very possibly because of the integration test not being ready. + // Try again to prevent flakes. + proxy_set_tick_period_milliseconds(100); +} + +END_WASM_PLUGIN diff --git a/test/extensions/bootstrap/wasm/wasm_integration_test.cc b/test/extensions/bootstrap/wasm/wasm_integration_test.cc new file mode 100644 index 000000000000..6d022664a85b --- /dev/null +++ b/test/extensions/bootstrap/wasm/wasm_integration_test.cc @@ -0,0 +1,95 @@ +#include "extensions/common/wasm/wasm.h" + +#include "test/extensions/common/wasm/wasm_runtime.h" +#include "test/integration/http_protocol_integration.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace Wasm { +namespace { + +class WasmIntegrationTest : public HttpIntegrationTest, public testing::TestWithParam { +public: + WasmIntegrationTest() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, Network::Address::IpVersion::v4) {} + + void createUpstreams() override { + HttpIntegrationTest::createUpstreams(); + addFakeUpstream(FakeHttpConnection::Type::HTTP1); + } + + void cleanup() { + if (wasm_connection_ != nullptr) { + ASSERT_TRUE(wasm_connection_->close()); + ASSERT_TRUE(wasm_connection_->waitForDisconnect()); + } + cleanupUpstreamAndDownstream(); + } + void initialize() override { + auto httpwasm = TestEnvironment::substitute( + "{{ test_rundir }}/test/extensions/bootstrap/wasm/test_data/http_cpp.wasm"); + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* wasm = bootstrap.mutable_static_resources()->add_clusters(); + wasm->MergeFrom(bootstrap.static_resources().clusters()[0]); + wasm->set_name("wasm_cluster"); + }); + + config_helper_.addBootstrapExtension(fmt::format(R"EOF( +name: envoy.filters.http.wasm +typed_config: + '@type': type.googleapis.com/envoy.extensions.wasm.v3.WasmService + singleton: true + config: + name: "singleton" + root_id: "singleton" + configuration: + '@type': type.googleapis.com/google.protobuf.StringValue + value: "" + vm_config: + vm_id: "my_vm_id" + runtime: "envoy.wasm.runtime.{}" + code: + local: + filename: {} + )EOF", + GetParam(), httpwasm)); + HttpIntegrationTest::initialize(); + } + + FakeHttpConnectionPtr wasm_connection_; + FakeStreamPtr wasm_request_; + IntegrationStreamDecoderPtr response_; +}; + +INSTANTIATE_TEST_SUITE_P(Runtimes, WasmIntegrationTest, + Envoy::Extensions::Common::Wasm::sandbox_runtime_values, + Envoy::Extensions::Common::Wasm::wasmTestParamsToString); +GTEST_ALLOW_UNINSTANTIATED_PARAMETERIZED_TEST(WasmIntegrationTest); + +TEST_P(WasmIntegrationTest, FilterMakesCallInConfigureTime) { + initialize(); + ASSERT_TRUE(fake_upstreams_.back()->waitForHttpConnection(*dispatcher_, wasm_connection_)); + + // Expect the filter to send us an HTTP request + ASSERT_TRUE(wasm_connection_->waitForNewStream(*dispatcher_, wasm_request_)); + ASSERT_TRUE(wasm_request_->waitForEndStream(*dispatcher_)); + + EXPECT_EQ("test", wasm_request_->headers() + .get(Envoy::Http::LowerCaseString("x-test"))[0] + ->value() + .getStringView()); + + // Respond back to the filter. + Http::TestResponseHeaderMapImpl response_headers{ + {":status", "200"}, + }; + wasm_request_->encodeHeaders(response_headers, true); + cleanup(); +} + +} // namespace +} // namespace Wasm +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/clusters/redis/redis_cluster_test.cc b/test/extensions/clusters/redis/redis_cluster_test.cc index 80a84ac10b08..3330fde17bf0 100644 --- a/test/extensions/clusters/redis/redis_cluster_test.cc +++ b/test/extensions/clusters/redis/redis_cluster_test.cc @@ -977,34 +977,75 @@ TEST_F(RedisClusterTest, HostRemovalAfterHcFail) { cluster_->initialize([&]() -> void { initialized_.ready(); }); EXPECT_CALL(*cluster_callback_, onClusterSlotUpdate(_, _)); - expectClusterSlotResponse(singleSlotPrimaryReplica("127.0.0.1", "127.0.0.2", 22120)); + expectClusterSlotResponse(twoSlotsPrimariesWithReplica()); - // Verify that both hosts are initially marked with FAILED_ACTIVE_HC, then + // Verify that all hosts are initially marked with FAILED_ACTIVE_HC, then // clear the flag to simulate that these hosts have been successfully health // checked. { EXPECT_CALL(membership_updated_, ready()); const auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); - EXPECT_EQ(2UL, hosts.size()); + EXPECT_EQ(4UL, hosts.size()); - for (size_t i = 0; i < 2; ++i) { + for (size_t i = 0; i < 4; ++i) { EXPECT_TRUE(hosts[i]->healthFlagGet(Upstream::Host::HealthFlag::FAILED_ACTIVE_HC)); hosts[i]->healthFlagClear(Upstream::Host::HealthFlag::FAILED_ACTIVE_HC); hosts[i]->healthFlagClear(Upstream::Host::HealthFlag::PENDING_ACTIVE_HC); health_checker->runCallbacks(hosts[i], Upstream::HealthTransition::Changed); } - expectHealthyHosts(std::list({"127.0.0.1:22120", "127.0.0.2:22120"})); + expectHealthyHosts(std::list( + {"127.0.0.1:22120", "127.0.0.3:22120", "127.0.0.2:22120", "127.0.0.4:22120"})); } - // Failed HC - EXPECT_CALL(membership_updated_, ready()); - EXPECT_CALL(*cluster_callback_, onHostHealthUpdate()); - const auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); - hosts[1]->healthFlagSet(Upstream::Host::HealthFlag::FAILED_ACTIVE_HC); - health_checker->runCallbacks(hosts[1], Upstream::HealthTransition::Changed); + // Fail a HC for one of the hosts + { + EXPECT_CALL(membership_updated_, ready()); + EXPECT_CALL(*cluster_callback_, onHostHealthUpdate()); + const auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + hosts[2]->healthFlagSet(Upstream::Host::HealthFlag::FAILED_ACTIVE_HC); + health_checker->runCallbacks(hosts[2], Upstream::HealthTransition::Changed); + + EXPECT_THAT(cluster_->prioritySet().hostSetsPerPriority()[0]->hosts().size(), 4U); + EXPECT_THAT(cluster_->prioritySet().hostSetsPerPriority()[0]->healthyHosts().size(), 3U); + } - EXPECT_THAT(2U, cluster_->prioritySet().hostSetsPerPriority()[0]->hosts().size()); - EXPECT_THAT(1U, cluster_->prioritySet().hostSetsPerPriority()[0]->healthyHosts().size()); + // Remove 2nd shard. + { + expectRedisResolve(); + EXPECT_CALL(membership_updated_, ready()); + resolve_timer_->invokeCallback(); + EXPECT_CALL(*cluster_callback_, onClusterSlotUpdate(_, _)); + expectClusterSlotResponse(singleSlotPrimaryReplica("127.0.0.1", "127.0.0.3", 22120)); + + const auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + + // We expect the host that failed health checks to be instantly removed, + // but the other should remain until its health check fails too. + EXPECT_THAT(cluster_->prioritySet().hostSetsPerPriority()[0]->hosts().size(), 3U); + expectHealthyHosts( + std::list({"127.0.0.1:22120", "127.0.0.3:22120", "127.0.0.4:22120"})); + EXPECT_TRUE(hosts[2]->healthFlagGet(Upstream::Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); + } + + /* + // TODO(#14630) This part of the test doesn't pass, as removal of PENDING_DYNAMIC_REMOVAL hosts + // does not seem to be implemented for redis clusters at present. + + // Fail the HC for the remaining removed host + { + EXPECT_CALL(membership_updated_, ready()); + EXPECT_CALL(*cluster_callback_, onHostHealthUpdate()); + const auto& hosts = cluster_->prioritySet().hostSetsPerPriority()[0]->hosts(); + EXPECT_TRUE(hosts[2]->healthFlagGet(Upstream::Host::HealthFlag::PENDING_DYNAMIC_REMOVAL)); + hosts[2]->healthFlagSet(Upstream::Host::HealthFlag::FAILED_ACTIVE_HC); + health_checker->runCallbacks(hosts[2], Upstream::HealthTransition::Changed); + + // The pending removal host should also have been removed now + EXPECT_THAT(cluster_->prioritySet().hostSetsPerPriority()[0]->hosts().size(), 2U); + expectHealthyHosts(std::list( + {"127.0.0.1:22120", "127.0.0.3:22120"})); + } + */ } } // namespace Redis diff --git a/test/extensions/common/dynamic_forward_proxy/dns_cache_impl_test.cc b/test/extensions/common/dynamic_forward_proxy/dns_cache_impl_test.cc index 9afbe86d8895..5eb41286c467 100644 --- a/test/extensions/common/dynamic_forward_proxy/dns_cache_impl_test.cc +++ b/test/extensions/common/dynamic_forward_proxy/dns_cache_impl_test.cc @@ -662,7 +662,7 @@ TEST_F(DnsCacheImplTest, MaxHostOverflow) { TEST_F(DnsCacheImplTest, CircuitBreakersNotInvoked) { initialize(); - auto raii_ptr = dns_cache_->canCreateDnsRequest(absl::nullopt); + auto raii_ptr = dns_cache_->canCreateDnsRequest(); EXPECT_NE(raii_ptr.get(), nullptr); } @@ -670,21 +670,11 @@ TEST_F(DnsCacheImplTest, DnsCacheCircuitBreakersOverflow) { config_.mutable_dns_cache_circuit_breaker()->mutable_max_pending_requests()->set_value(0); initialize(); - auto raii_ptr = dns_cache_->canCreateDnsRequest(absl::nullopt); + auto raii_ptr = dns_cache_->canCreateDnsRequest(); EXPECT_EQ(raii_ptr.get(), nullptr); EXPECT_EQ(1, TestUtility::findCounter(store_, "dns_cache.foo.dns_rq_pending_overflow")->value()); } -TEST_F(DnsCacheImplTest, ClustersCircuitBreakersOverflow) { - initialize(); - NiceMock pending_requests_; - - EXPECT_CALL(pending_requests_, canCreate()).WillOnce(Return(false)); - auto raii_ptr = dns_cache_->canCreateDnsRequest(pending_requests_); - EXPECT_EQ(raii_ptr.get(), nullptr); - EXPECT_EQ(0, TestUtility::findCounter(store_, "dns_cache.foo.dns_rq_pending_overflow")->value()); -} - TEST(DnsCacheImplOptionsTest, UseTcpForDnsLookupsOptionSet) { NiceMock dispatcher; std::shared_ptr resolver{std::make_shared()}; diff --git a/test/extensions/common/dynamic_forward_proxy/dns_cache_resource_manager_test.cc b/test/extensions/common/dynamic_forward_proxy/dns_cache_resource_manager_test.cc index 04127f486fff..5ed57a7b4606 100644 --- a/test/extensions/common/dynamic_forward_proxy/dns_cache_resource_manager_test.cc +++ b/test/extensions/common/dynamic_forward_proxy/dns_cache_resource_manager_test.cc @@ -68,10 +68,13 @@ TEST_F(DnsCacheResourceManagerTest, CheckDnsResource) { EXPECT_EQ(2, pending_requests.count()); EXPECT_TRUE(pending_requests.canCreate()); + EXPECT_EQ(0, resource_manager_->stats().rq_pending_open_.value()); + EXPECT_EQ(0, resource_manager_->stats().rq_pending_remaining_.value()); + cleanup(); } } // namespace } // namespace DynamicForwardProxy } // namespace Common } // namespace Extensions -} // namespace Envoy \ No newline at end of file +} // namespace Envoy diff --git a/test/extensions/common/dynamic_forward_proxy/mocks.cc b/test/extensions/common/dynamic_forward_proxy/mocks.cc index ef27a4de5b00..6b99e00bf460 100644 --- a/test/extensions/common/dynamic_forward_proxy/mocks.cc +++ b/test/extensions/common/dynamic_forward_proxy/mocks.cc @@ -16,7 +16,7 @@ MockDnsCacheResourceManager::MockDnsCacheResourceManager() { MockDnsCacheResourceManager::~MockDnsCacheResourceManager() = default; MockDnsCache::MockDnsCache() { - ON_CALL(*this, canCreateDnsRequest_(_)).WillByDefault(Return(nullptr)); + ON_CALL(*this, canCreateDnsRequest_()).WillByDefault(Return(nullptr)); } MockDnsCache::~MockDnsCache() = default; diff --git a/test/extensions/common/dynamic_forward_proxy/mocks.h b/test/extensions/common/dynamic_forward_proxy/mocks.h index aee7140a0aed..e8f7f87e993d 100644 --- a/test/extensions/common/dynamic_forward_proxy/mocks.h +++ b/test/extensions/common/dynamic_forward_proxy/mocks.h @@ -41,9 +41,8 @@ class MockDnsCache : public DnsCache { MockLoadDnsCacheEntryResult result = loadDnsCacheEntry_(host, default_port, callbacks); return {result.status_, LoadDnsCacheEntryHandlePtr{result.handle_}}; } - Upstream::ResourceAutoIncDecPtr - canCreateDnsRequest(ResourceLimitOptRef pending_requests) override { - Upstream::ResourceAutoIncDec* raii_ptr = canCreateDnsRequest_(pending_requests); + Upstream::ResourceAutoIncDecPtr canCreateDnsRequest() override { + Upstream::ResourceAutoIncDec* raii_ptr = canCreateDnsRequest_(); return std::unique_ptr(raii_ptr); } MOCK_METHOD(MockLoadDnsCacheEntryResult, loadDnsCacheEntry_, @@ -58,7 +57,7 @@ class MockDnsCache : public DnsCache { MOCK_METHOD((void), iterateHostMap, (IterateHostMapCb)); MOCK_METHOD((absl::optional), getHost, (absl::string_view)); - MOCK_METHOD(Upstream::ResourceAutoIncDec*, canCreateDnsRequest_, (ResourceLimitOptRef)); + MOCK_METHOD(Upstream::ResourceAutoIncDec*, canCreateDnsRequest_, ()); }; class MockLoadDnsCacheEntryHandle : public DnsCache::LoadDnsCacheEntryHandle { diff --git a/test/extensions/common/wasm/wasm_runtime.cc b/test/extensions/common/wasm/wasm_runtime.cc index a8451c70df64..e39288e12d0b 100644 --- a/test/extensions/common/wasm/wasm_runtime.cc +++ b/test/extensions/common/wasm/wasm_runtime.cc @@ -35,6 +35,10 @@ std::vector> runtimesAndLanguages() { return values; } +std::string wasmTestParamsToString(const ::testing::TestParamInfo& p) { + return p.param; +} + } // namespace Wasm } // namespace Common } // namespace Extensions diff --git a/test/extensions/common/wasm/wasm_runtime.h b/test/extensions/common/wasm/wasm_runtime.h index ef248d85310b..5c1d73fbd08c 100644 --- a/test/extensions/common/wasm/wasm_runtime.h +++ b/test/extensions/common/wasm/wasm_runtime.h @@ -20,6 +20,8 @@ inline auto runtime_values = testing::ValuesIn(runtimes()); inline auto sandbox_runtime_values = testing::ValuesIn(sandboxRuntimes()); inline auto runtime_and_language_values = testing::ValuesIn(runtimesAndLanguages()); +std::string wasmTestParamsToString(const ::testing::TestParamInfo& p); + } // namespace Wasm } // namespace Common } // namespace Extensions diff --git a/test/extensions/filters/common/local_ratelimit/local_ratelimit_test.cc b/test/extensions/filters/common/local_ratelimit/local_ratelimit_test.cc index a6142dfb16aa..f35cf5b325b5 100644 --- a/test/extensions/filters/common/local_ratelimit/local_ratelimit_test.cc +++ b/test/extensions/filters/common/local_ratelimit/local_ratelimit_test.cc @@ -1,6 +1,7 @@ #include "extensions/filters/common/local_ratelimit/local_ratelimit_impl.h" #include "test/mocks/event/mocks.h" +#include "test/test_common/utility.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -16,19 +17,27 @@ namespace LocalRateLimit { class LocalRateLimiterImplTest : public testing::Test { public: - void initialize(const std::chrono::milliseconds fill_interval, const uint32_t max_tokens, - const uint32_t tokens_per_fill) { - + void initializeTimer() { fill_timer_ = new Event::MockTimer(&dispatcher_); EXPECT_CALL(*fill_timer_, enableTimer(_, nullptr)); EXPECT_CALL(*fill_timer_, disableTimer()); + } + + void initialize(const std::chrono::milliseconds fill_interval, const uint32_t max_tokens, + const uint32_t tokens_per_fill) { - rate_limiter_ = std::make_shared(fill_interval, max_tokens, - tokens_per_fill, dispatcher_); + initializeTimer(); + + rate_limiter_ = std::make_shared( + fill_interval, max_tokens, tokens_per_fill, dispatcher_, descriptors_); } Thread::ThreadSynchronizer& synchronizer() { return rate_limiter_->synchronizer_; } + Envoy::Protobuf::RepeatedPtrField< + envoy::extensions::common::ratelimit::v3::LocalRateLimitDescriptor> + descriptors_; + std::vector route_descriptors_; NiceMock dispatcher_; Event::MockTimer* fill_timer_{}; std::shared_ptr rate_limiter_; @@ -37,8 +46,8 @@ class LocalRateLimiterImplTest : public testing::Test { // Make sure we fail with a fill rate this is too fast. TEST_F(LocalRateLimiterImplTest, TooFastFillRate) { EXPECT_THROW_WITH_MESSAGE( - LocalRateLimiterImpl(std::chrono::milliseconds(49), 100, 1, dispatcher_), EnvoyException, - "local rate limit token bucket fill timer must be >= 50ms"); + LocalRateLimiterImpl(std::chrono::milliseconds(49), 100, 1, dispatcher_, descriptors_), + EnvoyException, "local rate limit token bucket fill timer must be >= 50ms"); } // Verify various token bucket CAS edge cases. @@ -59,15 +68,15 @@ TEST_F(LocalRateLimiterImplTest, CasEdgeCases) { synchronizer().barrierOn("on_fill_timer_pre_cas"); // This should succeed. - EXPECT_TRUE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); // Now signal the thread to continue which should cause a CAS failure and the loop to repeat. synchronizer().signal("on_fill_timer_pre_cas"); t1.join(); // 1 -> 0 tokens - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_FALSE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_)); } // This tests the case in which two allowed checks race. @@ -78,12 +87,12 @@ TEST_F(LocalRateLimiterImplTest, CasEdgeCases) { // Start a thread and see if we are under limit. This will wait pre-CAS. synchronizer().waitOn("allowed_pre_cas"); - std::thread t1([&] { EXPECT_FALSE(rate_limiter_->requestAllowed()); }); + std::thread t1([&] { EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_)); }); // Wait until the thread is actually waiting. synchronizer().barrierOn("allowed_pre_cas"); // Consume a token on this thread, which should cause the CAS to fail on the other thread. - EXPECT_TRUE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); synchronizer().signal("allowed_pre_cas"); t1.join(); } @@ -94,17 +103,17 @@ TEST_F(LocalRateLimiterImplTest, TokenBucket) { initialize(std::chrono::milliseconds(200), 1, 1); // 1 -> 0 tokens - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_FALSE(rate_limiter_->requestAllowed()); - EXPECT_FALSE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_)); // 0 -> 1 tokens EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); fill_timer_->invokeCallback(); // 1 -> 0 tokens - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_FALSE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_)); // 0 -> 1 tokens EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); @@ -115,8 +124,8 @@ TEST_F(LocalRateLimiterImplTest, TokenBucket) { fill_timer_->invokeCallback(); // 1 -> 0 tokens - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_FALSE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_)); } // Verify token bucket functionality with max tokens and tokens per fill > 1. @@ -124,25 +133,25 @@ TEST_F(LocalRateLimiterImplTest, TokenBucketMultipleTokensPerFill) { initialize(std::chrono::milliseconds(200), 2, 2); // 2 -> 0 tokens - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_FALSE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_)); // 0 -> 2 tokens EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); fill_timer_->invokeCallback(); // 2 -> 1 tokens - EXPECT_TRUE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); // 1 -> 2 tokens EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); fill_timer_->invokeCallback(); // 2 -> 0 tokens - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_FALSE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_)); } // Verify token bucket functionality with max tokens > tokens per fill. @@ -150,17 +159,239 @@ TEST_F(LocalRateLimiterImplTest, TokenBucketMaxTokensGreaterThanTokensPerFill) { initialize(std::chrono::milliseconds(200), 2, 1); // 2 -> 0 tokens - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_FALSE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_)); // 0 -> 1 tokens EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(200), nullptr)); fill_timer_->invokeCallback(); // 1 -> 0 tokens - EXPECT_TRUE(rate_limiter_->requestAllowed()); - EXPECT_FALSE(rate_limiter_->requestAllowed()); + EXPECT_TRUE(rate_limiter_->requestAllowed(route_descriptors_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(route_descriptors_)); +} + +class LocalRateLimiterDescriptorImplTest : public LocalRateLimiterImplTest { +public: + void initializeWithDescriptor(const std::chrono::milliseconds fill_interval, + const uint32_t max_tokens, const uint32_t tokens_per_fill) { + + initializeTimer(); + + rate_limiter_ = std::make_shared( + fill_interval, max_tokens, tokens_per_fill, dispatcher_, descriptors_); + } + const std::string single_descriptor_config_yaml = R"( + entries: + - key: foo2 + value: bar2 + token_bucket: + max_tokens: {} + tokens_per_fill: {} + fill_interval: {} + )"; + + const std::string multiple_descriptor_config_yaml = R"( + entries: + - key: hello + value: world + - key: foo + value: bar + token_bucket: + max_tokens: 1 + tokens_per_fill: 1 + fill_interval: 0.05s + )"; + + // Default token bucket + std::vector descriptor_{{{{"foo2", "bar2"}}}}; + std::vector descriptor2_{{{{"hello", "world"}, {"foo", "bar"}}}}; +}; + +// Verify descriptor rate limit time interval is multiple of token bucket fill interval. +TEST_F(LocalRateLimiterDescriptorImplTest, DescriptorRateLimitDivisibleByTokenFillInterval) { + TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 10, 10, "60s"), + *descriptors_.Add()); + + EXPECT_THROW_WITH_MESSAGE( + LocalRateLimiterImpl(std::chrono::milliseconds(59000), 2, 1, dispatcher_, descriptors_), + EnvoyException, "local rate descriptor limit is not a multiple of token bucket fill timer"); +} + +TEST_F(LocalRateLimiterDescriptorImplTest, DuplicateDescriptor) { + TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 1, 1, "0.1s"), + *descriptors_.Add()); + TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 1, 1, "0.1s"), + *descriptors_.Add()); + + EXPECT_THROW_WITH_MESSAGE( + LocalRateLimiterImpl(std::chrono::milliseconds(50), 1, 1, dispatcher_, descriptors_), + EnvoyException, "duplicate descriptor in the local rate descriptor: foo2=bar2"); +} + +// Verify no exception for per route config without descriptors. +TEST_F(LocalRateLimiterDescriptorImplTest, DescriptorRateLimitNoExceptionWithoutDescriptor) { + VERBOSE_EXPECT_NO_THROW( + LocalRateLimiterImpl(std::chrono::milliseconds(59000), 2, 1, dispatcher_, descriptors_)); +} + +// Verify various token bucket CAS edge cases for descriptors. +TEST_F(LocalRateLimiterDescriptorImplTest, CasEdgeCasesDescriptor) { + // This tests the case in which an allowed check races with the fill timer. + { + TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 1, 1, "0.1s"), + *descriptors_.Add()); + initializeWithDescriptor(std::chrono::milliseconds(50), 1, 1); + + synchronizer().enable(); + + // Start a thread and start the fill callback. This will wait pre-CAS. + dispatcher_.time_system_.advanceTimeAndRun(std::chrono::milliseconds(100), dispatcher_, + Envoy::Event::Dispatcher::RunType::NonBlock); + synchronizer().waitOn("on_fill_timer_pre_cas"); + std::thread t1([&] { + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(50), nullptr)); + fill_timer_->invokeCallback(); + }); + // Wait until the thread is actually waiting. + synchronizer().barrierOn("on_fill_timer_pre_cas"); + + // This should succeed. + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + + // Now signal the thread to continue which should cause a CAS failure and the loop to repeat. + synchronizer().signal("on_fill_timer_pre_cas"); + t1.join(); + + // 1 -> 0 tokens + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); + } + + // This tests the case in which two allowed checks race. + { + initializeWithDescriptor(std::chrono::milliseconds(50), 1, 1); + + synchronizer().enable(); + + // Start a thread and see if we are under limit. This will wait pre-CAS. + synchronizer().waitOn("allowed_pre_cas"); + std::thread t1([&] { EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); }); + // Wait until the thread is actually waiting. + synchronizer().barrierOn("allowed_pre_cas"); + + // Consume a token on this thread, which should cause the CAS to fail on the other thread. + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + synchronizer().signal("allowed_pre_cas"); + t1.join(); + } +} + +TEST_F(LocalRateLimiterDescriptorImplTest, TokenBucketDescriptor2) { + TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 1, 1, "0.1s"), + *descriptors_.Add()); + initializeWithDescriptor(std::chrono::milliseconds(50), 1, 1); + + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); + dispatcher_.time_system_.advanceTimeAndRun(std::chrono::milliseconds(100), dispatcher_, + Envoy::Event::Dispatcher::RunType::NonBlock); +} + +// Verify token bucket functionality with a single token. +TEST_F(LocalRateLimiterDescriptorImplTest, TokenBucketDescriptor) { + TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 1, 1, "0.1s"), + *descriptors_.Add()); + initializeWithDescriptor(std::chrono::milliseconds(50), 1, 1); + + // 1 -> 0 tokens + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); + + // 0 -> 1 tokens + dispatcher_.time_system_.advanceTimeAndRun(std::chrono::milliseconds(100), dispatcher_, + Envoy::Event::Dispatcher::RunType::NonBlock); + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(50), nullptr)); + fill_timer_->invokeCallback(); + + // 1 -> 0 tokens + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); + + // 0 -> 1 tokens + dispatcher_.time_system_.advanceTimeAndRun(std::chrono::milliseconds(100), dispatcher_, + Envoy::Event::Dispatcher::RunType::NonBlock); + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(50), nullptr)); + fill_timer_->invokeCallback(); + + // 1 -> 1 tokens + dispatcher_.time_system_.advanceTimeAndRun(std::chrono::milliseconds(100), dispatcher_, + Envoy::Event::Dispatcher::RunType::NonBlock); + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(50), nullptr)); + fill_timer_->invokeCallback(); + + // 1 -> 0 tokens + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); +} + +// Verify token bucket functionality with request per unit > 1. +TEST_F(LocalRateLimiterDescriptorImplTest, TokenBucketMultipleTokensPerFillDescriptor) { + TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 2, 2, "0.1s"), + *descriptors_.Add()); + initializeWithDescriptor(std::chrono::milliseconds(50), 2, 2); + + // 2 -> 0 tokens + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); + + // 0 -> 2 tokens + dispatcher_.time_system_.advanceTimeAndRun(std::chrono::milliseconds(100), dispatcher_, + Envoy::Event::Dispatcher::RunType::NonBlock); + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(50), nullptr)); + fill_timer_->invokeCallback(); + + // 2 -> 1 tokens + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + + // 1 -> 2 tokens + dispatcher_.time_system_.advanceTimeAndRun(std::chrono::milliseconds(100), dispatcher_, + Envoy::Event::Dispatcher::RunType::NonBlock); + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(50), nullptr)); + fill_timer_->invokeCallback(); + + // 2 -> 0 tokens + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); +} + +// Verify token bucket functionality with multiple descriptors. +TEST_F(LocalRateLimiterDescriptorImplTest, TokenBucketDifferentDescriptorDifferentRateLimits) { + TestUtility::loadFromYaml(multiple_descriptor_config_yaml, *descriptors_.Add()); + TestUtility::loadFromYaml(fmt::format(single_descriptor_config_yaml, 1, 1, "1000s"), + *descriptors_.Add()); + initializeWithDescriptor(std::chrono::milliseconds(50), 2, 1); + + // 1 -> 0 tokens for descriptor_ and descriptor2_ + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor2_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor2_)); + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); + + // 0 -> 1 tokens for descriptor2_ + dispatcher_.time_system_.advanceTimeAndRun(std::chrono::milliseconds(50), dispatcher_, + Envoy::Event::Dispatcher::RunType::NonBlock); + EXPECT_CALL(*fill_timer_, enableTimer(std::chrono::milliseconds(50), nullptr)); + fill_timer_->invokeCallback(); + + // 1 -> 0 tokens for descriptor2_ and 0 only for descriptor_ + EXPECT_TRUE(rate_limiter_->requestAllowed(descriptor2_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor2_)); + EXPECT_FALSE(rate_limiter_->requestAllowed(descriptor_)); } } // Namespace LocalRateLimit diff --git a/test/extensions/filters/http/dynamic_forward_proxy/BUILD b/test/extensions/filters/http/dynamic_forward_proxy/BUILD index 95a296e8ae03..bf2a85ad0146 100644 --- a/test/extensions/filters/http/dynamic_forward_proxy/BUILD +++ b/test/extensions/filters/http/dynamic_forward_proxy/BUILD @@ -51,9 +51,6 @@ envoy_extension_cc_test( "//test/config/integration/certs", ], extension_name = "envoy.filters.http.dynamic_forward_proxy", - # TODO(envoyproxy/windows-dev): Diagnose flake in msvc-cl pipeline, observed timeout in - # IpVersions/ProxyFilterIntegrationTest.ReloadClusterAndAttachToCache/IPv6 - tags = ["flaky_on_windows"], deps = [ "//source/extensions/clusters/dynamic_forward_proxy:cluster", "//source/extensions/filters/http/dynamic_forward_proxy:config", diff --git a/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc b/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc index 1332c9c11a60..7181173eb637 100644 --- a/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc +++ b/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_integration_test.cc @@ -102,11 +102,6 @@ name: envoy.clusters.dynamic_forward_proxy } } - void disableDnsCacheCircuitBreakers() { - config_helper_.addRuntimeOverride("envoy.reloadable_features.enable_dns_cache_circuit_breakers", - "false"); - } - bool upstream_tls_{}; std::string upstream_cert_name_{"upstreamlocalhost"}; CdsHelper cds_helper_; @@ -142,30 +137,6 @@ TEST_P(ProxyFilterIntegrationTest, RequestWithBody) { EXPECT_EQ(1, test_server_->counter("dns_cache.foo.host_added")->value()); } -TEST_P(ProxyFilterIntegrationTest, RequestWithBodyWithClusterCircuitBreaker) { - disableDnsCacheCircuitBreakers(); - setup(); - codec_client_ = makeHttpConnection(lookupPort("http")); - const Http::TestRequestHeaderMapImpl request_headers{ - {":method", "POST"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", - fmt::format("localhost:{}", fake_upstreams_[0]->localAddress()->ip()->port())}}; - - auto response = - sendRequestAndWaitForResponse(request_headers, 1024, default_response_headers_, 1024); - checkSimpleRequestSuccess(1024, 1024, response.get()); - EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); - EXPECT_EQ(1, test_server_->counter("dns_cache.foo.host_added")->value()); - - // Now send another request. This should hit the DNS cache. - response = sendRequestAndWaitForResponse(request_headers, 512, default_response_headers_, 512); - checkSimpleRequestSuccess(512, 512, response.get()); - EXPECT_EQ(1, test_server_->counter("dns_cache.foo.dns_query_attempt")->value()); - EXPECT_EQ(1, test_server_->counter("dns_cache.foo.host_added")->value()); -} - // Verify that after we populate the cache and reload the cluster we reattach to the cache with // its existing hosts. TEST_P(ProxyFilterIntegrationTest, ReloadClusterAndAttachToCache) { @@ -346,25 +317,5 @@ TEST_P(ProxyFilterIntegrationTest, DnsCacheCircuitBreakersInvoked) { EXPECT_EQ("503", response->headers().Status()->value().getStringView()); } -TEST_P(ProxyFilterIntegrationTest, ClusterCircuitBreakersInvoked) { - disableDnsCacheCircuitBreakers(); - setup(1024, 0); - - codec_client_ = makeHttpConnection(lookupPort("http")); - const Http::TestRequestHeaderMapImpl request_headers{ - {":method", "POST"}, - {":path", "/test/long/url"}, - {":scheme", "http"}, - {":authority", - fmt::format("localhost:{}", fake_upstreams_[0]->localAddress()->ip()->port())}}; - - auto response = codec_client_->makeRequestWithBody(request_headers, 1024); - response->waitForEndStream(); - EXPECT_EQ(1, test_server_->counter("cluster.cluster_0.upstream_rq_pending_overflow")->value()); - - EXPECT_TRUE(response->complete()); - EXPECT_EQ("503", response->headers().Status()->value().getStringView()); -} - } // namespace } // namespace Envoy diff --git a/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_test.cc b/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_test.cc index 6af89ae829ec..c4ee7cce19b4 100644 --- a/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_test.cc +++ b/test/extensions/filters/http/dynamic_forward_proxy/proxy_filter_test.cc @@ -90,7 +90,7 @@ TEST_F(ProxyFilterTest, HttpDefaultPort) { EXPECT_CALL(callbacks_, route()); EXPECT_CALL(cm_, getThreadLocalCluster(_)); - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)) + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) .WillOnce(Return(circuit_breakers_)); EXPECT_CALL(*transport_socket_factory_, implementsSecureTransport()).WillOnce(Return(false)); Extensions::Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle* handle = @@ -112,7 +112,7 @@ TEST_F(ProxyFilterTest, HttpsDefaultPort) { EXPECT_CALL(callbacks_, route()); EXPECT_CALL(cm_, getThreadLocalCluster(_)); - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)) + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) .WillOnce(Return(circuit_breakers_)); EXPECT_CALL(*transport_socket_factory_, implementsSecureTransport()).WillOnce(Return(true)); Extensions::Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle* handle = @@ -134,7 +134,7 @@ TEST_F(ProxyFilterTest, CacheOverflow) { EXPECT_CALL(callbacks_, route()); EXPECT_CALL(cm_, getThreadLocalCluster(_)); - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)) + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) .WillOnce(Return(circuit_breakers_)); EXPECT_CALL(*transport_socket_factory_, implementsSecureTransport()).WillOnce(Return(true)); EXPECT_CALL(*dns_cache_manager_->dns_cache_, loadDnsCacheEntry_(Eq("foo"), 443, _)) @@ -151,17 +151,13 @@ TEST_F(ProxyFilterTest, CacheOverflow) { // Circuit breaker overflow TEST_F(ProxyFilterTest, CircuitBreakerOverflow) { - // Disable dns cache circuit breakers because which we expect to be used cluster circuit breakers. - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.enable_dns_cache_circuit_breakers", "false"}}); Upstream::ResourceAutoIncDec* circuit_breakers_( new Upstream::ResourceAutoIncDec(pending_requests_)); InSequence s; EXPECT_CALL(callbacks_, route()); EXPECT_CALL(cm_, getThreadLocalCluster(_)); - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)) + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) .WillOnce(Return(circuit_breakers_)); EXPECT_CALL(*transport_socket_factory_, implementsSecureTransport()).WillOnce(Return(true)); Extensions::Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle* handle = @@ -176,7 +172,7 @@ TEST_F(ProxyFilterTest, CircuitBreakerOverflow) { filter2->setDecoderFilterCallbacks(callbacks_); EXPECT_CALL(callbacks_, route()); EXPECT_CALL(cm_, getThreadLocalCluster(_)); - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)); + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()); EXPECT_CALL(callbacks_, sendLocalReply(Http::Code::ServiceUnavailable, Eq("Dynamic forward proxy pending request overflow"), _, _, Eq("Dynamic forward proxy pending request overflow"))); @@ -185,8 +181,6 @@ TEST_F(ProxyFilterTest, CircuitBreakerOverflow) { EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter2->decodeHeaders(request_headers_, false)); - EXPECT_EQ(1, - cm_.thread_local_cluster_.cluster_.info_->stats_.upstream_rq_pending_overflow_.value()); filter2->onDestroy(); EXPECT_CALL(*handle, onDestroy()); filter_->onDestroy(); @@ -200,7 +194,7 @@ TEST_F(ProxyFilterTest, CircuitBreakerOverflowWithDnsCacheResourceManager) { EXPECT_CALL(callbacks_, route()); EXPECT_CALL(cm_, getThreadLocalCluster(_)); - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)) + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) .WillOnce(Return(circuit_breakers_)); EXPECT_CALL(*transport_socket_factory_, implementsSecureTransport()).WillOnce(Return(true)); Extensions::Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle* handle = @@ -215,7 +209,7 @@ TEST_F(ProxyFilterTest, CircuitBreakerOverflowWithDnsCacheResourceManager) { filter2->setDecoderFilterCallbacks(callbacks_); EXPECT_CALL(callbacks_, route()); EXPECT_CALL(cm_, getThreadLocalCluster(_)); - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)); + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()); EXPECT_CALL(callbacks_, sendLocalReply(Http::Code::ServiceUnavailable, Eq("Dynamic forward proxy pending request overflow"), _, _, Eq("Dynamic forward proxy pending request overflow"))); @@ -284,7 +278,7 @@ TEST_F(ProxyFilterTest, HostRewrite) { EXPECT_CALL(callbacks_, route()); EXPECT_CALL(cm_, getThreadLocalCluster(_)); - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)) + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) .WillOnce(Return(circuit_breakers_)); EXPECT_CALL(*transport_socket_factory_, implementsSecureTransport()).WillOnce(Return(false)); Extensions::Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle* handle = @@ -312,7 +306,7 @@ TEST_F(ProxyFilterTest, HostRewriteViaHeader) { EXPECT_CALL(callbacks_, route()); EXPECT_CALL(cm_, getThreadLocalCluster(_)); - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)) + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) .WillOnce(Return(circuit_breakers_)); EXPECT_CALL(*transport_socket_factory_, implementsSecureTransport()).WillOnce(Return(false)); Extensions::Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle* handle = diff --git a/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc b/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc index c3ec137d8360..13e8c40b378b 100644 --- a/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc +++ b/test/extensions/filters/http/ext_authz/ext_authz_integration_test.cc @@ -560,7 +560,8 @@ class ExtAuthzHttpIntegrationTest : public HttpIntegrationTest, }; INSTANTIATE_TEST_SUITE_P(IpVersionsCientType, ExtAuthzGrpcIntegrationTest, - VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::VersionedGrpcClientIntegrationParamTest::protocolTestParamsToString); // Verifies that the request body is included in the CheckRequest when the downstream protocol is // HTTP/1.1. diff --git a/test/extensions/filters/http/ext_proc/BUILD b/test/extensions/filters/http/ext_proc/BUILD index ff8393e41984..395d0e6020f9 100644 --- a/test/extensions/filters/http/ext_proc/BUILD +++ b/test/extensions/filters/http/ext_proc/BUILD @@ -5,6 +5,7 @@ load( load( "//test/extensions:extensions_build_system.bzl", "envoy_extension_cc_test", + "envoy_extension_cc_test_library", ) licenses(["notice"]) # Apache 2 @@ -27,7 +28,23 @@ envoy_extension_cc_test( srcs = ["filter_test.cc"], extension_name = "envoy.filters.http.ext_proc", deps = [ + ":mock_server_lib", + ":utils_lib", "//source/extensions/filters/http/ext_proc", + "//test/common/http:common_lib", + "//test/mocks/server:factory_context_mocks", + "//test/test_common:test_runtime_lib", + ], +) + +envoy_extension_cc_test( + name = "ordering_test", + srcs = ["ordering_test.cc"], + extension_name = "envoy.filters.http.ext_proc", + deps = [ + ":mock_server_lib", + "//source/extensions/filters/http/ext_proc", + "//test/common/http:common_lib", "//test/mocks/server:factory_context_mocks", "//test/test_common:test_runtime_lib", ], @@ -46,3 +63,52 @@ envoy_extension_cc_test( "@envoy_api//envoy/config/core/v3:pkg_cc_proto", ], ) + +envoy_extension_cc_test( + name = "mutation_utils_test", + srcs = ["mutation_utils_test.cc"], + extension_name = "envoy.filters.http.ext_proc", + deps = [ + ":utils_lib", + "//source/extensions/filters/http/ext_proc:mutation_utils_lib", + "//test/test_common:utility_lib", + ], +) + +envoy_extension_cc_test( + name = "ext_proc_integration_test", + srcs = ["ext_proc_integration_test.cc"], + extension_name = "envoy.filters.http.ext_proc", + deps = [ + ":utils_lib", + "//source/extensions/filters/http/ext_proc:config", + "//test/common/http:common_lib", + "//test/integration:http_integration_lib", + "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/filters/http/ext_proc/v3alpha:pkg_cc_proto", + "@envoy_api//envoy/service/ext_proc/v3alpha:pkg_cc_proto", + ], +) + +envoy_extension_cc_test_library( + name = "mock_server_lib", + srcs = ["mock_server.cc"], + hdrs = ["mock_server.h"], + extension_name = "envoy.filters.http.ext_proc", + deps = [ + "//source/extensions/filters/http/ext_proc:client_interface", + ], +) + +envoy_extension_cc_test_library( + name = "utils_lib", + srcs = ["utils.cc"], + hdrs = ["utils.h"], + extension_name = "envoy.filters.http.ext_proc", + deps = [ + "//include/envoy/http:header_map_interface", + "//test/test_common:utility_lib", + "@com_google_absl//absl/strings:str_format", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + ], +) diff --git a/test/extensions/filters/http/ext_proc/config_test.cc b/test/extensions/filters/http/ext_proc/config_test.cc index 59d3757457ab..34e38c8df433 100644 --- a/test/extensions/filters/http/ext_proc/config_test.cc +++ b/test/extensions/filters/http/ext_proc/config_test.cc @@ -36,7 +36,7 @@ TEST(HttpExtProcConfigTest, CorrectConfig) { ProtobufTypes::MessagePtr proto_config = factory.createEmptyConfigProto(); TestUtility::loadFromYaml(yaml, *proto_config); - testing::StrictMock context; + testing::NiceMock context; EXPECT_CALL(context, messageValidationVisitor()); Http::FilterFactoryCb cb = factory.createFilterFactoryFromProto(*proto_config, "stats", context); Http::MockFilterChainFactoryCallbacks filter_callback; diff --git a/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc b/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc new file mode 100644 index 000000000000..1e91e1762d1b --- /dev/null +++ b/test/extensions/filters/http/ext_proc/ext_proc_integration_test.cc @@ -0,0 +1,403 @@ +#include "envoy/extensions/filters/http/ext_proc/v3alpha/ext_proc.pb.h" +#include "envoy/network/address.h" +#include "envoy/service/ext_proc/v3alpha/external_processor.pb.h" + +#include "extensions/filters/http/ext_proc/config.h" + +#include "test/common/http/common.h" +#include "test/extensions/filters/http/ext_proc/utils.h" +#include "test/integration/http_integration.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { + +using envoy::service::ext_proc::v3alpha::ProcessingRequest; +using envoy::service::ext_proc::v3alpha::ProcessingResponse; +using Extensions::HttpFilters::ExternalProcessing::HasNoHeader; +using Extensions::HttpFilters::ExternalProcessing::HeaderProtosEqual; +using Extensions::HttpFilters::ExternalProcessing::SingleHeaderValueIs; + +using Http::LowerCaseString; + +// These tests exercise the ext_proc filter through Envoy's integration test +// environment by configuring an instance of the Envoy server and driving it +// through the mock network stack. + +class ExtProcIntegrationTest : public HttpIntegrationTest, + public Grpc::GrpcClientIntegrationParamTest { +protected: + ExtProcIntegrationTest() : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, ipVersion()) {} + + void createUpstreams() override { + // Need to create a separate "upstream" for the gRPC server + HttpIntegrationTest::createUpstreams(); + addFakeUpstream(FakeHttpConnection::Type::HTTP2); + } + + void TearDown() override { + if (processor_connection_) { + ASSERT_TRUE(processor_connection_->close()); + ASSERT_TRUE(processor_connection_->waitForDisconnect()); + } + cleanupUpstreamAndDownstream(); + } + + void initializeConfig() { + config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + // This is the cluster for our gRPC server, starting by copying an existing cluster + auto* server_cluster = bootstrap.mutable_static_resources()->add_clusters(); + server_cluster->MergeFrom(bootstrap.static_resources().clusters()[0]); + server_cluster->set_name("ext_proc_server"); + server_cluster->mutable_load_assignment()->set_cluster_name("ext_proc_server"); + ConfigHelper::setHttp2(*server_cluster); + + // Load configuration of the server from YAML and use a helper to add a grpc_service + // stanza pointing to the cluster that we just made + setGrpcService(*proto_config_.mutable_grpc_service(), "ext_proc_server", + fake_upstreams_.back()->localAddress()); + + // Construct a configuration proto for our filter and then re-write it + // to JSON so that we can add it to the overall config + envoy::config::listener::v3::Filter ext_proc_filter; + ext_proc_filter.set_name("envoy.filters.http.ext_proc"); + ext_proc_filter.mutable_typed_config()->PackFrom(proto_config_); + config_helper_.addFilter(MessageUtil::getJsonStringFromMessage(ext_proc_filter)); + }); + setDownstreamProtocol(Http::CodecClient::Type::HTTP2); + } + + IntegrationStreamDecoderPtr + sendDownstreamRequest(std::function modify_headers) { + auto conn = makeClientConnection(lookupPort("http")); + codec_client_ = makeHttpConnection(std::move(conn)); + Http::TestRequestHeaderMapImpl headers; + if (modify_headers != nullptr) { + modify_headers(headers); + } + HttpTestUtility::addDefaultHeaders(headers); + return codec_client_->makeHeaderOnlyRequest(headers); + } + + void verifyDownstreamResponse(IntegrationStreamDecoder& response, int status_code) { + response.waitForEndStream(); + EXPECT_TRUE(response.complete()); + EXPECT_EQ(std::to_string(status_code), response.headers().getStatusValue()); + } + + void handleUpstreamRequest() { + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + upstream_request_->encodeData(100, true); + } + + void waitForFirstMessage(ProcessingRequest& request) { + ASSERT_TRUE(fake_upstreams_.back()->waitForHttpConnection(*dispatcher_, processor_connection_)); + ASSERT_TRUE(processor_connection_->waitForNewStream(*dispatcher_, processor_stream_)); + ASSERT_TRUE(processor_stream_->waitForGrpcMessage(*dispatcher_, request)); + } + + envoy::extensions::filters::http::ext_proc::v3alpha::ExternalProcessor proto_config_{}; + FakeHttpConnectionPtr processor_connection_; + FakeStreamPtr processor_stream_; +}; + +INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, ExtProcIntegrationTest, + GRPC_CLIENT_INTEGRATION_PARAMS); + +// Test the filter using the default configuration by connecting to +// an ext_proc server that responds to the request_headers message +// by immediately closing the stream. +TEST_P(ExtProcIntegrationTest, GetAndCloseStream) { + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(nullptr); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(request_headers_msg); + // Just close the stream without doing anything + processor_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + processor_stream_->encodeTrailers(Http::TestResponseTrailerMapImpl{{"grpc-status", "0"}}); + + handleUpstreamRequest(); + verifyDownstreamResponse(*response, 200); +} + +// Test the filter using the default configuration by connecting to +// an ext_proc server that responds to the request_headers message +// by returning a failure before the first stream response can be sent. +TEST_P(ExtProcIntegrationTest, GetAndFailStream) { + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(nullptr); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(request_headers_msg); + // Fail the stream immediately + processor_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "500"}}, true); + verifyDownstreamResponse(*response, 500); +} + +// Test the filter using the default configuration by connecting to +// an ext_proc server that responds to the request_headers message +// successfully, but then sends a gRPC error. +TEST_P(ExtProcIntegrationTest, GetAndFailStreamOutOfLine) { + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(nullptr); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(request_headers_msg); + processor_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + ProcessingResponse resp1; + resp1.mutable_request_headers(); + processor_stream_->sendGrpcMessage(resp1); + + // Fail the stream in between messages + processor_stream_->encodeTrailers(Http::TestResponseTrailerMapImpl{{"grpc-status", "13"}}); + + verifyDownstreamResponse(*response, 500); +} + +// Test the filter using the default configuration by connecting to +// an ext_proc server that responds to the request_headers message +// successfully, but then sends a gRPC error. +TEST_P(ExtProcIntegrationTest, GetAndFailStreamOutOfLineLater) { + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(nullptr); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(request_headers_msg); + processor_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + ProcessingResponse resp1; + resp1.mutable_request_headers(); + processor_stream_->sendGrpcMessage(resp1); + + // Fail the stream in between messages + processor_stream_->encodeTrailers(Http::TestResponseTrailerMapImpl{{"grpc-status", "13"}}); + + verifyDownstreamResponse(*response, 500); +} + +// Test the filter using the default configuration by connecting to +// an ext_proc server that responds to the request_headers message +// successfully but closes the stream after response_headers. +TEST_P(ExtProcIntegrationTest, GetAndCloseStreamOnResponse) { + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(nullptr); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(request_headers_msg); + processor_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + ProcessingResponse resp1; + resp1.mutable_request_headers(); + processor_stream_->sendGrpcMessage(resp1); + + handleUpstreamRequest(); + + ProcessingRequest response_headers_msg; + ASSERT_TRUE(processor_stream_->waitForGrpcMessage(*dispatcher_, response_headers_msg)); + processor_stream_->encodeTrailers(Http::TestResponseTrailerMapImpl{{"grpc-status", "0"}}); + + verifyDownstreamResponse(*response, 200); +} + +// Test the filter using the default configuration by connecting to +// an ext_proc server that responds to the request_headers message +// successfully but then fails on the response_headers message. +TEST_P(ExtProcIntegrationTest, GetAndFailStreamOnResponse) { + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(nullptr); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(request_headers_msg); + processor_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + ProcessingResponse resp1; + resp1.mutable_request_headers(); + processor_stream_->sendGrpcMessage(resp1); + + handleUpstreamRequest(); + + ProcessingRequest response_headers_msg; + ASSERT_TRUE(processor_stream_->waitForGrpcMessage(*dispatcher_, response_headers_msg)); + processor_stream_->encodeTrailers(Http::TestResponseTrailerMapImpl{{"grpc-status", "13"}}); + + verifyDownstreamResponse(*response, 500); +} + +// Test the filter using the default configuration by connecting to +// an ext_proc server that responds to the request_headers message +// by requesting to modify the request headers. +TEST_P(ExtProcIntegrationTest, GetAndSetHeaders) { + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest( + [](Http::HeaderMap& headers) { headers.addCopy(LowerCaseString("x-remove-this"), "yes"); }); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(request_headers_msg); + + ASSERT_TRUE(request_headers_msg.has_request_headers()); + const auto request_headers = request_headers_msg.request_headers(); + Http::TestRequestHeaderMapImpl expected_request_headers{{":scheme", "http"}, + {":method", "GET"}, + {"host", "host"}, + {":path", "/"}, + {"x-remove-this", "yes"}}; + EXPECT_THAT(request_headers.headers(), HeaderProtosEqual(expected_request_headers)); + + processor_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + + // Ask to change the headers + ProcessingResponse response_msg; + auto response_header_mutation = + response_msg.mutable_request_headers()->mutable_response()->mutable_header_mutation(); + auto mut1 = response_header_mutation->add_set_headers(); + mut1->mutable_header()->set_key("x-new-header"); + mut1->mutable_header()->set_value("new"); + response_header_mutation->add_remove_headers("x-remove-this"); + processor_stream_->sendGrpcMessage(response_msg); + + ASSERT_TRUE(fake_upstreams_[0]->waitForHttpConnection(*dispatcher_, fake_upstream_connection_)); + ASSERT_TRUE(fake_upstream_connection_->waitForNewStream(*dispatcher_, upstream_request_)); + ASSERT_TRUE(upstream_request_->waitForEndStream(*dispatcher_)); + + EXPECT_THAT(upstream_request_->headers(), HasNoHeader("x-remove-this")); + EXPECT_THAT(upstream_request_->headers(), SingleHeaderValueIs("x-new-header", "new")); + + upstream_request_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + upstream_request_->encodeData(100, true); + + // Now expect a message for the response path + ProcessingRequest response_headers_msg; + ASSERT_TRUE(processor_stream_->waitForGrpcMessage(*dispatcher_, response_headers_msg)); + ASSERT_TRUE(response_headers_msg.has_response_headers()); + const auto response_headers = response_headers_msg.response_headers(); + Http::TestRequestHeaderMapImpl expected_response_headers{{":status", "200"}}; + EXPECT_THAT(response_headers.headers(), HeaderProtosEqual(expected_response_headers)); + + // Send back a response but don't do anything + ProcessingResponse response_2; + response_2.mutable_response_headers(); + processor_stream_->sendGrpcMessage(response_2); + + verifyDownstreamResponse(*response, 200); +} + +// Test the filter using the default configuration by connecting to +// an ext_proc server that responds to the response_headers message +// by requesting to modify the request headers. +TEST_P(ExtProcIntegrationTest, GetAndSetHeadersOnResponse) { + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(nullptr); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(request_headers_msg); + + ASSERT_TRUE(request_headers_msg.has_request_headers()); + processor_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + ProcessingResponse resp1; + resp1.mutable_request_headers(); + processor_stream_->sendGrpcMessage(resp1); + + handleUpstreamRequest(); + + ProcessingRequest response_headers_msg; + ASSERT_TRUE(processor_stream_->waitForGrpcMessage(*dispatcher_, response_headers_msg)); + ASSERT_TRUE(response_headers_msg.has_response_headers()); + ProcessingResponse resp2; + auto* headers2 = resp2.mutable_response_headers(); + auto* response_mutation = headers2->mutable_response()->mutable_header_mutation(); + auto* add1 = response_mutation->add_set_headers(); + add1->mutable_header()->set_key("x-response-processed"); + add1->mutable_header()->set_value("1"); + processor_stream_->sendGrpcMessage(resp2); + + verifyDownstreamResponse(*response, 200); + EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-response-processed", "1")); +} + +// Test the filter using the default configuration by connecting to +// an ext_proc server that responds to the request_headers message +// by sending back an immediate_response message, which should be +// returned directly to the downstream. +TEST_P(ExtProcIntegrationTest, GetAndRespondImmediately) { + // Logger::Registry::getLog(Logger::Id::filter).set_level(spdlog::level::trace); + + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(nullptr); + + ProcessingRequest request_headers_msg; + waitForFirstMessage(request_headers_msg); + + EXPECT_TRUE(request_headers_msg.has_request_headers()); + processor_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + + // Produce an immediate response + ProcessingResponse response_msg; + auto* immediate_response = response_msg.mutable_immediate_response(); + immediate_response->mutable_status()->set_code(envoy::type::v3::StatusCode::Unauthorized); + immediate_response->set_body("{\"reason\": \"Not authorized\"}"); + immediate_response->set_details("Failed because you are not authorized"); + auto* hdr1 = immediate_response->mutable_headers()->add_set_headers(); + hdr1->mutable_header()->set_key("x-failure-reason"); + hdr1->mutable_header()->set_value("testing"); + auto* hdr2 = immediate_response->mutable_headers()->add_set_headers(); + hdr2->mutable_header()->set_key("content-type"); + hdr2->mutable_header()->set_value("application/json"); + processor_stream_->sendGrpcMessage(response_msg); + + verifyDownstreamResponse(*response, 401); + EXPECT_THAT(response->headers(), SingleHeaderValueIs("x-failure-reason", "testing")); + EXPECT_THAT(response->headers(), SingleHeaderValueIs("content-type", "application/json")); + EXPECT_EQ("{\"reason\": \"Not authorized\"}", response->body()); +} + +// Test the filter using the default configuration by connecting to +// an ext_proc server that responds to the request_headers message +// by sending back an immediate_response message after the +// request_headers message +TEST_P(ExtProcIntegrationTest, GetAndRespondImmediatelyOnResponse) { + initializeConfig(); + HttpIntegrationTest::initialize(); + auto response = sendDownstreamRequest(nullptr); + + // request_headers message to processor + ProcessingRequest request_headers_msg; + waitForFirstMessage(request_headers_msg); + EXPECT_TRUE(request_headers_msg.has_request_headers()); + processor_stream_->encodeHeaders(Http::TestResponseHeaderMapImpl{{":status", "200"}}, false); + + // Response to request_headers + ProcessingResponse resp1; + resp1.mutable_request_headers(); + processor_stream_->sendGrpcMessage(resp1); + + handleUpstreamRequest(); + + // response_headers message to processor + ProcessingRequest response_headers_msg; + ASSERT_TRUE(processor_stream_->waitForGrpcMessage(*dispatcher_, response_headers_msg)); + ASSERT_TRUE(response_headers_msg.has_response_headers()); + + // Response to response_headers + ProcessingResponse resp2; + auto* immediate_response = resp2.mutable_immediate_response(); + immediate_response->mutable_status()->set_code(envoy::type::v3::StatusCode::Unauthorized); + immediate_response->set_body("{\"reason\": \"Not authorized\"}"); + immediate_response->set_details("Failed because you are not authorized"); + processor_stream_->sendGrpcMessage(resp2); + + verifyDownstreamResponse(*response, 401); + EXPECT_EQ("{\"reason\": \"Not authorized\"}", response->body()); +} + +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/http/ext_proc/filter_test.cc b/test/extensions/filters/http/ext_proc/filter_test.cc index 7f821c6b130e..0a08ff30e7ec 100644 --- a/test/extensions/filters/http/ext_proc/filter_test.cc +++ b/test/extensions/filters/http/ext_proc/filter_test.cc @@ -1,5 +1,8 @@ #include "extensions/filters/http/ext_proc/ext_proc.h" +#include "test/common/http/common.h" +#include "test/extensions/filters/http/ext_proc/mock_server.h" +#include "test/extensions/filters/http/ext_proc/utils.h" #include "test/mocks/http/mocks.h" #include "test/mocks/network/mocks.h" #include "test/mocks/router/mocks.h" @@ -18,26 +21,76 @@ namespace HttpFilters { namespace ExternalProcessing { namespace { -template class HttpFilterTestBase : public T { -public: - HttpFilterTestBase() = default; +using envoy::service::ext_proc::v3alpha::ProcessingRequest; +using envoy::service::ext_proc::v3alpha::ProcessingResponse; +using Http::FilterDataStatus; +using Http::FilterHeadersStatus; +using Http::FilterTrailersStatus; +using Http::LowerCaseString; + +using testing::Eq; +using testing::Invoke; +using testing::Unused; + +using namespace std::chrono_literals; + +// These tests are all unit tests that directly drive an instance of the +// ext_proc filter and verify the behavior using mocks. + +class HttpFilterTest : public testing::Test { +protected: void initialize(std::string&& yaml) { + client_ = std::make_unique(); + EXPECT_CALL(*client_, start(_, _)).WillOnce(Invoke(this, &HttpFilterTest::doStart)); + envoy::extensions::filters::http::ext_proc::v3alpha::ExternalProcessor proto_config{}; if (!yaml.empty()) { TestUtility::loadFromYaml(yaml, proto_config); } - config_.reset(new FilterConfig(proto_config)); - filter_ = std::make_unique(config_); + config_.reset(new FilterConfig(proto_config, 200ms, stats_store_, "")); + filter_ = std::make_unique(config_, std::move(client_)); filter_->setEncoderFilterCallbacks(encoder_callbacks_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); } + ExternalProcessorStreamPtr doStart(ExternalProcessorCallbacks& callbacks, + const std::chrono::milliseconds& timeout) { + stream_callbacks_ = &callbacks; + stream_timeout_ = timeout; + + auto stream = std::make_unique(); + EXPECT_CALL(*stream, send(_, _)).WillRepeatedly(Invoke(this, &HttpFilterTest::doSend)); + EXPECT_CALL(*stream, close()).WillRepeatedly(Invoke(this, &HttpFilterTest::doSendClose)); + return stream; + } + + void doSend(ProcessingRequest&& request, bool end_stream) { + ASSERT_FALSE(stream_close_sent_); + ASSERT_TRUE(last_request_processed_); + last_request_ = std::move(request); + last_request_processed_ = false; + if (end_stream) { + stream_close_sent_ = true; + } + } + + void doSendClose() { + ASSERT_FALSE(stream_close_sent_); + stream_close_sent_ = true; + } + + std::unique_ptr client_; + ExternalProcessorCallbacks* stream_callbacks_ = nullptr; + ProcessingRequest last_request_; + bool last_request_processed_ = true; + bool stream_close_sent_ = false; + std::chrono::milliseconds stream_timeout_; NiceMock stats_store_; FilterConfigSharedPtr config_; std::unique_ptr filter_; - NiceMock decoder_callbacks_; - NiceMock encoder_callbacks_; + Http::MockStreamDecoderFilterCallbacks decoder_callbacks_; + Http::MockStreamEncoderFilterCallbacks encoder_callbacks_; Http::TestRequestHeaderMapImpl request_headers_; Http::TestResponseHeaderMapImpl response_headers_; Http::TestRequestTrailerMapImpl request_trailers_; @@ -45,34 +98,648 @@ template class HttpFilterTestBase : public T { Buffer::OwnedImpl data_; }; -class HttpFilterTest : public HttpFilterTestBase { -public: - HttpFilterTest() = default; -}; - +// Using the default configuration, test the filter with a processor that +// replies to the request_headers message with an empty response TEST_F(HttpFilterTest, SimplestPost) { initialize(R"EOF( grpc_service: envoy_grpc: - cluster_name: "ext_authz_server" + cluster_name: "ext_proc_server" failure_mode_allow: true )EOF"); EXPECT_TRUE(config_->failureModeAllow()); - EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, true)); + // Create synthetic HTTP request + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + request_headers_.addCopy(LowerCaseString("content-type"), "text/plain"); + request_headers_.addCopy(LowerCaseString("content-length"), 10); + request_headers_.addCopy(LowerCaseString("x-some-other-header"), "yes"); + + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + + // Verify that call was received by mock gRPC server + EXPECT_FALSE(last_request_.async_mode()); + EXPECT_FALSE(stream_close_sent_); + ASSERT_TRUE(last_request_.has_request_headers()); + const auto request_headers = last_request_.request_headers(); + EXPECT_FALSE(request_headers.end_of_stream()); + + Http::TestRequestHeaderMapImpl expected{{":path", "/"}, + {":method", "POST"}, + {":scheme", "http"}, + {"host", "host"}, + {"content-type", "text/plain"}, + {"content-length", "10"}, + {"x-some-other-header", "yes"}}; + EXPECT_THAT(request_headers.headers(), HeaderProtosEqual(expected)); + last_request_processed_ = true; + + // Send back a response + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + std::unique_ptr resp1 = std::make_unique(); + resp1->mutable_request_headers(); + stream_callbacks_->onReceiveMessage(std::move(resp1)); + + data_.add("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + response_headers_.addCopy(LowerCaseString(":status"), "200"); + response_headers_.addCopy(LowerCaseString("content-type"), "text/plain"); + response_headers_.addCopy(LowerCaseString("content-length"), "3"); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->encodeHeaders(response_headers_, false)); + + // Expect another stream message + EXPECT_FALSE(last_request_.async_mode()); + EXPECT_FALSE(stream_close_sent_); + ASSERT_TRUE(last_request_.has_response_headers()); + const auto response_headers = last_request_.response_headers(); + EXPECT_FALSE(response_headers.end_of_stream()); + + Http::TestRequestHeaderMapImpl expected_response{ + {":status", "200"}, {"content-type", "text/plain"}, {"content-length", "3"}}; + EXPECT_THAT(response_headers.headers(), HeaderProtosEqual(expected_response)); + last_request_processed_ = true; + + // Send back a response + EXPECT_CALL(encoder_callbacks_, continueEncoding()); + std::unique_ptr resp2 = std::make_unique(); + resp2->mutable_response_headers(); + stream_callbacks_->onReceiveMessage(std::move(resp2)); + + data_.add("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + EXPECT_TRUE(stream_close_sent_); + + EXPECT_EQ(1, config_->stats().streams_started_.value()); + EXPECT_EQ(2, config_->stats().stream_msgs_sent_.value()); + EXPECT_EQ(2, config_->stats().stream_msgs_received_.value()); + EXPECT_EQ(1, config_->stats().streams_closed_.value()); +} + +// Using the default configuration, test the filter with a processor that +// replies to the request_headers message with a message that modifies the request +// headers. +TEST_F(HttpFilterTest, PostAndChangeHeaders) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + )EOF"); + + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + request_headers_.addCopy(LowerCaseString("x-some-other-header"), "yes"); + request_headers_.addCopy(LowerCaseString("x-do-we-want-this"), "no"); + + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + + EXPECT_FALSE(last_request_.async_mode()); + EXPECT_FALSE(stream_close_sent_); + ASSERT_TRUE(last_request_.has_request_headers()); + + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + std::unique_ptr resp1 = std::make_unique(); + auto req_headers_response = resp1->mutable_request_headers(); + auto headers_mut = req_headers_response->mutable_response()->mutable_header_mutation(); + auto add1 = headers_mut->add_set_headers(); + add1->mutable_header()->set_key("x-new-header"); + add1->mutable_header()->set_value("new"); + add1->mutable_append()->set_value(false); + auto add2 = headers_mut->add_set_headers(); + add2->mutable_header()->set_key("x-some-other-header"); + add2->mutable_header()->set_value("no"); + add2->mutable_append()->set_value(true); + *headers_mut->add_remove_headers() = "x-do-we-want-this"; + stream_callbacks_->onReceiveMessage(std::move(resp1)); + + // We should now have changed the original header a bit + Http::TestRequestHeaderMapImpl expected{{":path", "/"}, + {":method", "POST"}, + {":scheme", "http"}, + {"host", "host"}, + {"x-new-header", "new"}, + {"x-some-other-header", "yes"}, + {"x-some-other-header", "no"}}; + EXPECT_THAT(&request_headers_, HeaderMapEqualIgnoreOrder(&expected)); + last_request_processed_ = true; + + data_.add("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + response_headers_.addCopy(LowerCaseString(":status"), "200"); + response_headers_.addCopy(LowerCaseString("content-type"), "text/plain"); + response_headers_.addCopy(LowerCaseString("content-length"), "3"); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->encodeHeaders(response_headers_, false)); + + // Expect another stream message + EXPECT_FALSE(last_request_.async_mode()); + EXPECT_FALSE(stream_close_sent_); + ASSERT_TRUE(last_request_.has_response_headers()); + const auto response_headers = last_request_.response_headers(); + EXPECT_FALSE(response_headers.end_of_stream()); + + Http::TestRequestHeaderMapImpl expected_response{ + {":status", "200"}, {"content-type", "text/plain"}, {"content-length", "3"}}; + EXPECT_TRUE(ExtProcTestUtility::headerProtosEqualIgnoreOrder(expected_response, + response_headers.headers())); + last_request_processed_ = true; + + // Send back a response + EXPECT_CALL(encoder_callbacks_, continueEncoding()); + std::unique_ptr resp2 = std::make_unique(); + auto resp_headers = resp2->mutable_response_headers(); + auto resp_headers_mut = resp_headers->mutable_response()->mutable_header_mutation(); + auto resp_add1 = resp_headers_mut->add_set_headers(); + resp_add1->mutable_header()->set_key("x-new-header"); + resp_add1->mutable_header()->set_value("new"); + stream_callbacks_->onReceiveMessage(std::move(resp2)); + + // We should now have changed the original header a bit + Http::TestRequestHeaderMapImpl final_expected_response{{":status", "200"}, + {"content-type", "text/plain"}, + {"content-length", "3"}, + {"x-new-header", "new"}}; + EXPECT_THAT(&response_headers_, HeaderMapEqualIgnoreOrder(&final_expected_response)); + + data_.add("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + EXPECT_TRUE(stream_close_sent_); + + EXPECT_EQ(1, config_->stats().streams_started_.value()); + EXPECT_EQ(2, config_->stats().stream_msgs_sent_.value()); + EXPECT_EQ(2, config_->stats().stream_msgs_received_.value()); + EXPECT_EQ(1, config_->stats().streams_closed_.value()); +} + +// Using the default configuration, test the filter with a processor that +// replies to the request_headers message with an "immediate response" message +// that should result in a response being directly sent downstream with +// custom headers. +TEST_F(HttpFilterTest, PostAndRespondImmediately) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + )EOF"); + + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + + Http::TestResponseHeaderMapImpl immediate_response_headers; + EXPECT_CALL(encoder_callbacks_, sendLocalReply(Http::Code::BadRequest, "Bad request", _, + Eq(absl::nullopt), "Got a bad request")) + .WillOnce(Invoke([&immediate_response_headers]( + Unused, Unused, + std::function modify_headers, + Unused, Unused) { modify_headers(immediate_response_headers); })); + std::unique_ptr resp1 = std::make_unique(); + auto* immediate_response = resp1->mutable_immediate_response(); + immediate_response->mutable_status()->set_code(envoy::type::v3::StatusCode::BadRequest); + immediate_response->set_body("Bad request"); + immediate_response->set_details("Got a bad request"); + auto* immediate_headers = immediate_response->mutable_headers(); + auto* hdr1 = immediate_headers->add_set_headers(); + hdr1->mutable_header()->set_key("content-type"); + hdr1->mutable_header()->set_value("text/plain"); + auto* hdr2 = immediate_headers->add_set_headers(); + hdr2->mutable_append()->set_value(true); + hdr2->mutable_header()->set_key("x-another-thing"); + hdr2->mutable_header()->set_value("1"); + auto* hdr3 = immediate_headers->add_set_headers(); + hdr3->mutable_append()->set_value(true); + hdr3->mutable_header()->set_key("x-another-thing"); + hdr3->mutable_header()->set_value("2"); + stream_callbacks_->onReceiveMessage(std::move(resp1)); + + Http::TestResponseHeaderMapImpl expected_response_headers{ + {"content-type", "text/plain"}, {"x-another-thing", "1"}, {"x-another-thing", "2"}}; + EXPECT_THAT(&immediate_response_headers, HeaderMapEqualIgnoreOrder(&expected_response_headers)); + + data_.add("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, true)); + data_.add("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + EXPECT_TRUE(stream_close_sent_); + + EXPECT_EQ(1, config_->stats().streams_started_.value()); + EXPECT_EQ(1, config_->stats().stream_msgs_sent_.value()); + EXPECT_EQ(1, config_->stats().stream_msgs_received_.value()); + EXPECT_EQ(1, config_->stats().streams_closed_.value()); +} + +// Using the default configuration, test the filter with a processor that +// replies to the request_headers message with an "immediate response" message +// during response headers processing that should result in a response being +// directly sent downstream with custom headers. +TEST_F(HttpFilterTest, PostAndRespondImmediatelyOnResponse) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + )EOF"); + + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + + EXPECT_FALSE(last_request_.async_mode()); + EXPECT_FALSE(stream_close_sent_); + ASSERT_TRUE(last_request_.has_request_headers()); + last_request_processed_ = true; + + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + std::unique_ptr resp1 = std::make_unique(); + resp1->mutable_request_headers(); + stream_callbacks_->onReceiveMessage(std::move(resp1)); + + data_.add("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->encodeHeaders(response_headers_, false)); + + EXPECT_FALSE(last_request_.async_mode()); + EXPECT_FALSE(stream_close_sent_); + ASSERT_TRUE(last_request_.has_response_headers()); + last_request_processed_ = true; + + Http::TestResponseHeaderMapImpl immediate_response_headers; + EXPECT_CALL(encoder_callbacks_, sendLocalReply(Http::Code::BadRequest, "Bad request", _, + Eq(absl::nullopt), "Got a bad request")) + .WillOnce(Invoke([&immediate_response_headers]( + Unused, Unused, + std::function modify_headers, + Unused, Unused) { modify_headers(immediate_response_headers); })); + std::unique_ptr resp2 = std::make_unique(); + auto* immediate_response = resp2->mutable_immediate_response(); + immediate_response->mutable_status()->set_code(envoy::type::v3::StatusCode::BadRequest); + immediate_response->set_body("Bad request"); + immediate_response->set_details("Got a bad request"); + stream_callbacks_->onReceiveMessage(std::move(resp2)); + EXPECT_TRUE(immediate_response_headers.empty()); + + data_.add("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + EXPECT_TRUE(stream_close_sent_); + + EXPECT_EQ(1, config_->stats().streams_started_.value()); + EXPECT_EQ(2, config_->stats().stream_msgs_sent_.value()); + EXPECT_EQ(2, config_->stats().stream_msgs_received_.value()); + EXPECT_EQ(1, config_->stats().streams_closed_.value()); +} + +// Using the default configuration, test the filter with a processor that +// replies to the request_headers message with an empty immediate_response message +TEST_F(HttpFilterTest, RespondImmediatelyDefault) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + )EOF"); + + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + + Http::TestResponseHeaderMapImpl immediate_response_headers; + EXPECT_CALL(encoder_callbacks_, sendLocalReply(Http::Code::OK, "", _, Eq(absl::nullopt), "")) + .WillOnce(Invoke([&immediate_response_headers]( + Unused, Unused, + std::function modify_headers, + Unused, Unused) { modify_headers(immediate_response_headers); })); + std::unique_ptr resp1 = std::make_unique(); + resp1->mutable_immediate_response(); + stream_callbacks_->onReceiveMessage(std::move(resp1)); + EXPECT_TRUE(immediate_response_headers.empty()); + data_.add("foo"); - EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->decodeData(data_, true)); - EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); - EXPECT_EQ(Http::FilterHeadersStatus::Continue, - filter_->encode100ContinueHeaders(response_headers_)); - EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, true)); + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, true)); data_.add("bar"); - EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(data_, false)); - EXPECT_EQ(Http::FilterDataStatus::Continue, filter_->encodeData(data_, true)); - EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); filter_->onDestroy(); + EXPECT_TRUE(stream_close_sent_); +} + +// Using the default configuration, test the filter with a processor that +// replies to the request_headers message with an immediate response message +// that contains a non-default gRPC status. +TEST_F(HttpFilterTest, RespondImmediatelyGrpcError) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + )EOF"); + + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + + Http::TestResponseHeaderMapImpl immediate_response_headers; + EXPECT_CALL(encoder_callbacks_, sendLocalReply(Http::Code::Forbidden, "", _, Eq(999), "")) + .WillOnce(Invoke([&immediate_response_headers]( + Unused, Unused, + std::function modify_headers, + Unused, Unused) { modify_headers(immediate_response_headers); })); + std::unique_ptr resp1 = std::make_unique(); + auto* immediate_response = resp1->mutable_immediate_response(); + immediate_response->mutable_status()->set_code(envoy::type::v3::StatusCode::Forbidden); + immediate_response->mutable_grpc_status()->set_status(999); + stream_callbacks_->onReceiveMessage(std::move(resp1)); + EXPECT_TRUE(immediate_response_headers.empty()); + + data_.add("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, true)); + data_.add("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + EXPECT_TRUE(stream_close_sent_); +} + +// Using the default configuration, test the filter with a processor that +// returns an error from from the gRPC stream. +TEST_F(HttpFilterTest, PostAndFail) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + )EOF"); + + EXPECT_FALSE(config_->failureModeAllow()); + + // Create synthetic HTTP request + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + EXPECT_FALSE(stream_close_sent_); + + // Oh no! The remote server had a failure! + Http::TestResponseHeaderMapImpl immediate_response_headers; + EXPECT_CALL(encoder_callbacks_, + sendLocalReply(Http::Code::InternalServerError, "", _, Eq(absl::nullopt), + "ext_proc error: gRPC error 13")) + .WillOnce(Invoke([&immediate_response_headers]( + Unused, Unused, + std::function modify_headers, + Unused, Unused) { modify_headers(immediate_response_headers); })); + stream_callbacks_->onGrpcError(Grpc::Status::Internal); + + data_.add("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, true)); + data_.add("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + // The other side closed the stream + EXPECT_FALSE(stream_close_sent_); + EXPECT_TRUE(immediate_response_headers.empty()); + + EXPECT_EQ(1, config_->stats().streams_started_.value()); + EXPECT_EQ(1, config_->stats().stream_msgs_sent_.value()); + EXPECT_EQ(1, config_->stats().streams_failed_.value()); +} + +// Using the default configuration, test the filter with a processor that +// returns an error from from the gRPC stream during response header processing. +TEST_F(HttpFilterTest, PostAndFailOnResponse) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + )EOF"); + + EXPECT_FALSE(config_->failureModeAllow()); + + // Create synthetic HTTP request + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + + EXPECT_FALSE(last_request_.async_mode()); + EXPECT_FALSE(stream_close_sent_); + ASSERT_TRUE(last_request_.has_request_headers()); + last_request_processed_ = true; + + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + std::unique_ptr resp1 = std::make_unique(); + resp1->mutable_request_headers(); + stream_callbacks_->onReceiveMessage(std::move(resp1)); + + data_.add("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->encodeHeaders(response_headers_, false)); + + // Oh no! The remote server had a failure! + Http::TestResponseHeaderMapImpl immediate_response_headers; + EXPECT_CALL(encoder_callbacks_, + sendLocalReply(Http::Code::InternalServerError, "", _, Eq(absl::nullopt), + "ext_proc error: gRPC error 13")) + .WillOnce(Invoke([&immediate_response_headers]( + Unused, Unused, + std::function modify_headers, + Unused, Unused) { modify_headers(immediate_response_headers); })); + stream_callbacks_->onGrpcError(Grpc::Status::Internal); + + data_.add("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + // The other side closed the stream + EXPECT_FALSE(stream_close_sent_); + EXPECT_TRUE(immediate_response_headers.empty()); + + EXPECT_EQ(1, config_->stats().streams_started_.value()); + EXPECT_EQ(2, config_->stats().stream_msgs_sent_.value()); + EXPECT_EQ(1, config_->stats().stream_msgs_received_.value()); + EXPECT_EQ(1, config_->stats().streams_failed_.value()); +} + +// Using the default configuration, test the filter with a processor that +// returns an error from the gRPC stream that is ignored because the +// failure_mode_allow parameter is set. +TEST_F(HttpFilterTest, PostAndIgnoreFailure) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + failure_mode_allow: true + )EOF"); + + EXPECT_TRUE(config_->failureModeAllow()); + + // Create synthetic HTTP request + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + EXPECT_FALSE(stream_close_sent_); + + // Oh no! The remote server had a failure which we will ignore + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + stream_callbacks_->onGrpcError(Grpc::Status::Internal); + + data_.add("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, true)); + data_.add("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + // The other side closed the stream + EXPECT_FALSE(stream_close_sent_); + + EXPECT_EQ(1, config_->stats().streams_started_.value()); + EXPECT_EQ(1, config_->stats().stream_msgs_sent_.value()); + EXPECT_EQ(1, config_->stats().streams_closed_.value()); + EXPECT_EQ(1, config_->stats().failure_mode_allowed_.value()); +} + +// Using the default configuration, test the filter with a processor that +// replies to the request_headers message by closing the gRPC stream. +TEST_F(HttpFilterTest, PostAndClose) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + )EOF"); + + EXPECT_FALSE(config_->failureModeAllow()); + + // Create synthetic HTTP request + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + + EXPECT_FALSE(last_request_.async_mode()); + EXPECT_FALSE(stream_close_sent_); + ASSERT_TRUE(last_request_.has_request_headers()); + + // Close the stream, which should tell the filter to keep on going + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + stream_callbacks_->onGrpcClose(); + + data_.add("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, true)); + data_.add("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + + // The other side closed the stream + EXPECT_FALSE(stream_close_sent_); + + EXPECT_EQ(1, config_->stats().streams_started_.value()); + EXPECT_EQ(1, config_->stats().stream_msgs_sent_.value()); + EXPECT_EQ(1, config_->stats().streams_closed_.value()); +} + +// Using the default configuration, test the filter with a processor that +// replies to the request_headers message incorrectly by sending a +// request_body message, which should result in the stream being closed +// and ignored. +TEST_F(HttpFilterTest, OutOfOrder) { + initialize(R"EOF( + grpc_service: + envoy_grpc: + cluster_name: "ext_proc_server" + )EOF"); + + HttpTestUtility::addDefaultHeaders(request_headers_, "POST"); + EXPECT_EQ(FilterHeadersStatus::StopAllIterationAndWatermark, + filter_->decodeHeaders(request_headers_, false)); + + EXPECT_FALSE(last_request_.async_mode()); + EXPECT_FALSE(stream_close_sent_); + ASSERT_TRUE(last_request_.has_request_headers()); + + // Return an out-of-order message. The server should close the stream + // and continue as if nothing happened. + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + std::unique_ptr resp1 = std::make_unique(); + resp1->mutable_request_body(); + stream_callbacks_->onReceiveMessage(std::move(resp1)); + + data_.add("foo"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encode100ContinueHeaders(response_headers_)); + EXPECT_EQ(FilterHeadersStatus::Continue, filter_->encodeHeaders(response_headers_, true)); + data_.add("bar"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, false)); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data_, true)); + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + filter_->onDestroy(); + + // We closed the stream + EXPECT_TRUE(stream_close_sent_); + + EXPECT_EQ(1, config_->stats().streams_started_.value()); + EXPECT_EQ(1, config_->stats().stream_msgs_sent_.value()); + EXPECT_EQ(1, config_->stats().spurious_msgs_received_.value()); + EXPECT_EQ(1, config_->stats().streams_closed_.value()); } } // namespace diff --git a/test/extensions/filters/http/ext_proc/mock_server.cc b/test/extensions/filters/http/ext_proc/mock_server.cc new file mode 100644 index 000000000000..888144e38518 --- /dev/null +++ b/test/extensions/filters/http/ext_proc/mock_server.cc @@ -0,0 +1,17 @@ +#include "test/extensions/filters/http/ext_proc/mock_server.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { + +MockClient::MockClient() = default; +MockClient::~MockClient() = default; + +MockStream::MockStream() = default; +MockStream::~MockStream() = default; + +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/http/ext_proc/mock_server.h b/test/extensions/filters/http/ext_proc/mock_server.h new file mode 100644 index 000000000000..6e3654d5f9c8 --- /dev/null +++ b/test/extensions/filters/http/ext_proc/mock_server.h @@ -0,0 +1,31 @@ +#pragma once + +#include "extensions/filters/http/ext_proc/client.h" + +#include "gmock/gmock.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { + +class MockClient : public ExternalProcessorClient { +public: + MockClient(); + ~MockClient() override; + MOCK_METHOD(ExternalProcessorStreamPtr, start, + (ExternalProcessorCallbacks&, const std::chrono::milliseconds&)); +}; + +class MockStream : public ExternalProcessorStream { +public: + MockStream(); + ~MockStream() override; + MOCK_METHOD(void, send, (envoy::service::ext_proc::v3alpha::ProcessingRequest&&, bool)); + MOCK_METHOD(void, close, ()); +}; + +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/http/ext_proc/mutation_utils_test.cc b/test/extensions/filters/http/ext_proc/mutation_utils_test.cc new file mode 100644 index 000000000000..fd31976332e3 --- /dev/null +++ b/test/extensions/filters/http/ext_proc/mutation_utils_test.cc @@ -0,0 +1,125 @@ +#include "extensions/filters/http/ext_proc/mutation_utils.h" + +#include "test/extensions/filters/http/ext_proc/utils.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { +namespace { + +using Http::LowerCaseString; + +TEST(MutationUtils, TestBuildHeaders) { + Http::TestRequestHeaderMapImpl headers{ + {":method", "GET"}, + {":path", "/foo/the/bar?size=123"}, + {"content-type", "text/plain; encoding=UTF8"}, + {"x-something-else", "yes"}, + }; + LowerCaseString reference_key("x-reference"); + std::string reference_value("Foo"); + headers.addReference(reference_key, reference_value); + headers.addCopy(LowerCaseString("x-number"), 9999); + + envoy::config::core::v3::HeaderMap proto_headers; + MutationUtils::buildHttpHeaders(headers, proto_headers); + + Http::TestRequestHeaderMapImpl expected{{":method", "GET"}, + {":path", "/foo/the/bar?size=123"}, + {"content-type", "text/plain; encoding=UTF8"}, + {"x-something-else", "yes"}, + {"x-reference", "Foo"}, + {"x-number", "9999"}}; + EXPECT_TRUE(ExtProcTestUtility::headerProtosEqualIgnoreOrder(expected, proto_headers)); +} + +TEST(MutationUtils, TestApplyMutations) { + Http::TestRequestHeaderMapImpl headers{ + {":scheme", "https"}, + {":method", "GET"}, + {":path", "/foo/the/bar?size=123"}, + {"host", "localhost:1000"}, + {":authority", "localhost:1000"}, + {"content-type", "text/plain; encoding=UTF8"}, + {"x-append-this", "1"}, + {"x-replace-this", "Yes"}, + {"x-remove-this", "Yes"}, + {"x-envoy-strange-thing", "No"}, + }; + + envoy::service::ext_proc::v3alpha::HeaderMutation mutation; + auto* s = mutation.add_set_headers(); + s->mutable_append()->set_value(true); + s->mutable_header()->set_key("x-append-this"); + s->mutable_header()->set_value("2"); + s = mutation.add_set_headers(); + s->mutable_append()->set_value(true); + s->mutable_header()->set_key("x-append-this"); + s->mutable_header()->set_value("3"); + s = mutation.add_set_headers(); + s->mutable_append()->set_value(false); + s->mutable_header()->set_key("x-replace-this"); + s->mutable_header()->set_value("no"); + // Default of "append" is "false" and mutations + // are applied in order. + s = mutation.add_set_headers(); + s->mutable_header()->set_key("x-replace-this"); + s->mutable_header()->set_value("nope"); + // Incomplete structures should be ignored + mutation.add_set_headers(); + + mutation.add_remove_headers("x-remove-this"); + // Attempts to remove ":" and "host" headers should be ignored + mutation.add_remove_headers("host"); + mutation.add_remove_headers(":method"); + mutation.add_remove_headers(""); + + // Attempts to set method, host, authority, and x-envoy headers + // should be ignored until we explicitly allow them. + s = mutation.add_set_headers(); + s->mutable_header()->set_key("host"); + s->mutable_header()->set_value("invalid:123"); + s = mutation.add_set_headers(); + s->mutable_header()->set_key("Host"); + s->mutable_header()->set_value("invalid:456"); + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":authority"); + s->mutable_header()->set_value("invalid:789"); + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":method"); + s->mutable_header()->set_value("PATCH"); + s = mutation.add_set_headers(); + s->mutable_header()->set_key(":scheme"); + s->mutable_header()->set_value("http"); + s = mutation.add_set_headers(); + s->mutable_header()->set_key("X-Envoy-StrangeThing"); + s->mutable_header()->set_value("Yes"); + + MutationUtils::applyHeaderMutations(mutation, headers); + + Http::TestRequestHeaderMapImpl expected_headers{ + {":scheme", "https"}, + {":method", "GET"}, + {":path", "/foo/the/bar?size=123"}, + {"host", "localhost:1000"}, + {":authority", "localhost:1000"}, + {"content-type", "text/plain; encoding=UTF8"}, + {"x-append-this", "1"}, + {"x-append-this", "2"}, + {"x-append-this", "3"}, + {"x-replace-this", "nope"}, + {"x-envoy-strange-thing", "No"}, + }; + + EXPECT_TRUE(TestUtility::headerMapEqualIgnoreOrder(headers, expected_headers)); +} + +} // namespace +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/http/ext_proc/ordering_test.cc b/test/extensions/filters/http/ext_proc/ordering_test.cc new file mode 100644 index 000000000000..8849905ab20b --- /dev/null +++ b/test/extensions/filters/http/ext_proc/ordering_test.cc @@ -0,0 +1,348 @@ +#include "extensions/filters/http/ext_proc/ext_proc.h" + +#include "test/common/http/common.h" +#include "test/extensions/filters/http/ext_proc/mock_server.h" +#include "test/mocks/http/mocks.h" +#include "test/mocks/network/mocks.h" +#include "test/mocks/router/mocks.h" +#include "test/mocks/runtime/mocks.h" +#include "test/mocks/tracing/mocks.h" +#include "test/mocks/upstream/cluster_manager.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { +namespace { + +using envoy::extensions::filters::http::ext_proc::v3alpha::ExternalProcessor; +using envoy::service::ext_proc::v3alpha::ProcessingRequest; +using envoy::service::ext_proc::v3alpha::ProcessingResponse; + +using Http::FilterDataStatus; +using Http::FilterHeadersStatus; +using Http::FilterTrailersStatus; + +using testing::Invoke; +using testing::Unused; + +using namespace std::chrono_literals; + +// These tests directly drive the filter. They concentrate on testing out all the different +// ordering options for the protocol, which means that unlike other tests they do not verify +// every parameter sent to or from the filter. + +class OrderingTest : public testing::Test { +protected: + void initialize(std::function cb) { + client_ = std::make_unique(); + EXPECT_CALL(*client_, start(_, _)).WillOnce(Invoke(this, &OrderingTest::doStart)); + + ExternalProcessor proto_config; + proto_config.mutable_grpc_service()->mutable_envoy_grpc()->set_cluster_name("ext_proc_server"); + if (cb != nullptr) { + cb(proto_config); + } + config_.reset(new FilterConfig(proto_config, 200ms, stats_store_, "")); + filter_ = std::make_unique(config_, std::move(client_)); + filter_->setEncoderFilterCallbacks(encoder_callbacks_); + filter_->setDecoderFilterCallbacks(decoder_callbacks_); + } + + void TearDown() override { filter_->onDestroy(); } + + // Called by the "start" method on the stream by the filter + ExternalProcessorStreamPtr doStart(ExternalProcessorCallbacks& callbacks, + const std::chrono::milliseconds&) { + stream_callbacks_ = &callbacks; + auto stream = std::make_unique(); + EXPECT_CALL(*stream, send(_, _)).WillRepeatedly(Invoke(this, &OrderingTest::doSend)); + EXPECT_CALL(*stream, close()).WillRepeatedly(Invoke(this, &OrderingTest::doSendClose)); + return stream; + } + + // Called on the stream after it's been created. These delegate + // to "stream_delegate_" so that we can have expectations there. + + void doSend(ProcessingRequest&& request, bool end_stream) { + stream_delegate_.send(std::move(request), end_stream); + } + + void doSendClose() { stream_delegate_.close(); } + + // Send data through the filter as if we are the proxy + + void sendRequestHeadersGet(bool expect_callback) { + HttpTestUtility::addDefaultHeaders(request_headers_, "GET"); + EXPECT_EQ(expect_callback ? FilterHeadersStatus::StopAllIterationAndWatermark + : FilterHeadersStatus::Continue, + filter_->decodeHeaders(request_headers_, true)); + } + + void sendResponseHeaders(bool expect_callback) { + response_headers_.setStatus(200); + EXPECT_EQ(expect_callback ? FilterHeadersStatus::StopAllIterationAndWatermark + : FilterHeadersStatus::Continue, + filter_->encodeHeaders(response_headers_, false)); + } + + void sendRequestBody() { + Buffer::OwnedImpl data; + data.add("Dummy data"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->decodeData(data, true)); + } + + void sendResponseBody() { + Buffer::OwnedImpl data; + data.add("Dummy data"); + EXPECT_EQ(FilterDataStatus::Continue, filter_->encodeData(data, true)); + } + + void sendRequestTrailers() { + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); + } + + void sendResponseTrailers() { + EXPECT_EQ(FilterTrailersStatus::Continue, filter_->encodeTrailers(response_trailers_)); + } + + // Make it easier to send responses from the external processor + + void sendRequestHeadersReply() { + auto reply = std::make_unique(); + reply->mutable_request_headers(); + stream_callbacks_->onReceiveMessage(std::move(reply)); + } + + void sendResponseHeadersReply() { + auto reply = std::make_unique(); + reply->mutable_response_headers(); + stream_callbacks_->onReceiveMessage(std::move(reply)); + } + + void sendImmediateResponse500() { + auto reply = std::make_unique(); + auto* ir = reply->mutable_immediate_response(); + ir->mutable_status()->set_code(envoy::type::v3::StatusCode::InternalServerError); + stream_callbacks_->onReceiveMessage(std::move(reply)); + } + + void sendGrpcError() { stream_callbacks_->onGrpcError(Grpc::Status::Internal); } + + void closeGrpcStream() { stream_callbacks_->onGrpcClose(); } + + std::unique_ptr client_; + MockStream stream_delegate_; + ExternalProcessorCallbacks* stream_callbacks_ = nullptr; + NiceMock stats_store_; + FilterConfigSharedPtr config_; + std::unique_ptr filter_; + Http::MockStreamDecoderFilterCallbacks decoder_callbacks_; + Http::MockStreamEncoderFilterCallbacks encoder_callbacks_; + Http::TestRequestHeaderMapImpl request_headers_; + Http::TestResponseHeaderMapImpl response_headers_; + Http::TestRequestTrailerMapImpl request_trailers_; + Http::TestResponseTrailerMapImpl response_trailers_; +}; + +// *** Tests for the normal processing path *** + +// A normal call with the default configuration +TEST_F(OrderingTest, DefaultOrderingGet) { + initialize(nullptr); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + sendRequestHeadersReply(); + sendRequestTrailers(); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendResponseHeaders(true); + EXPECT_CALL(encoder_callbacks_, continueEncoding()); + sendResponseHeadersReply(); + sendResponseBody(); + sendResponseTrailers(); + + EXPECT_CALL(stream_delegate_, close()); +} + +// An immediate response on the request path +TEST_F(OrderingTest, ImmediateResponseOnRequest) { + initialize(nullptr); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(encoder_callbacks_, sendLocalReply(Http::Code::InternalServerError, _, _, _, _)); + EXPECT_CALL(stream_delegate_, close()); + sendImmediateResponse500(); + // The rest of the filter isn't necessarily called after this. +} + +// An immediate response on the response path +TEST_F(OrderingTest, ImmediateResponseOnResponse) { + initialize(nullptr); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + sendRequestHeadersReply(); + sendRequestTrailers(); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendResponseHeaders(true); + EXPECT_CALL(encoder_callbacks_, sendLocalReply(Http::Code::InternalServerError, _, _, _, _)); + EXPECT_CALL(stream_delegate_, close()); + sendImmediateResponse500(); + sendResponseBody(); + sendResponseTrailers(); +} + +// *** Tests of out-of-order messages *** +// In general, for these the server closes the stream and ignores the +// processor for the rest of the filter lifetime. + +// Receive a response headers reply in response to the request +// headers message -- should close stream and stop sending, but otherwise +// continue without error. +TEST_F(OrderingTest, IncorrectRequestHeadersReply) { + initialize(nullptr); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + EXPECT_CALL(stream_delegate_, close()); + sendResponseHeadersReply(); + sendRequestTrailers(); + + // Expect us to go on from here normally but send no more stream messages + sendResponseHeaders(false); + sendResponseBody(); + sendResponseTrailers(); +} + +// Receive a request headers reply in response to the response +// headers message -- should continue without error. +TEST_F(OrderingTest, IncorrectResponseHeadersReply) { + initialize(nullptr); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + sendRequestHeadersReply(); + sendRequestTrailers(); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendResponseHeaders(true); + EXPECT_CALL(encoder_callbacks_, continueEncoding()); + EXPECT_CALL(stream_delegate_, close()); + sendRequestHeadersReply(); + // Still should ignore the message and go on but send no more stream messages + sendResponseBody(); + sendResponseTrailers(); +} + +// Receive an extra message -- we should ignore it +// and not send anything else to the server +TEST_F(OrderingTest, ExtraReply) { + initialize(nullptr); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + sendRequestHeadersReply(); + + // Extra call + EXPECT_CALL(stream_delegate_, close()); + sendRequestHeadersReply(); + + // After this we are ignoring the processor + sendRequestTrailers(); + sendResponseHeaders(false); + sendResponseBody(); + sendResponseTrailers(); +} + +// Receive an extra message after the immediate response -- it should +// be ignored. +TEST_F(OrderingTest, ExtraAfterImmediateResponse) { + initialize(nullptr); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(encoder_callbacks_, sendLocalReply(Http::Code::InternalServerError, _, _, _, _)); + EXPECT_CALL(stream_delegate_, close()); + sendImmediateResponse500(); + // Extra messages sent after immediate response shouldn't affect anything + sendRequestHeadersReply(); +} + +// *** Tests of gRPC stream state *** + +// gRPC error in response to message calls results in an error +TEST_F(OrderingTest, GrpcErrorInline) { + initialize(nullptr); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(encoder_callbacks_, sendLocalReply(Http::Code::InternalServerError, _, _, _, _)); + sendGrpcError(); + // The rest of the filter isn't called after this. +} + +// gRPC error in response to message results in connection being dropped +// if failures are ignored +TEST_F(OrderingTest, GrpcErrorInlineIgnored) { + initialize([](ExternalProcessor& cfg) { cfg.set_failure_mode_allow(true); }); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + sendGrpcError(); + + // After that we ignore the processor + sendRequestTrailers(); + sendResponseHeaders(false); + sendResponseBody(); + sendResponseTrailers(); +} + +// gRPC error in between calls should still be delivered +TEST_F(OrderingTest, GrpcErrorOutOfLine) { + initialize(nullptr); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + sendRequestHeadersReply(); + sendRequestTrailers(); + + EXPECT_CALL(encoder_callbacks_, sendLocalReply(Http::Code::InternalServerError, _, _, _, _)); + sendGrpcError(); +} + +// gRPC close after a proper message means rest of stream is ignored +TEST_F(OrderingTest, GrpcCloseAfter) { + initialize(nullptr); + + EXPECT_CALL(stream_delegate_, send(_, false)); + sendRequestHeadersGet(true); + EXPECT_CALL(decoder_callbacks_, continueDecoding()); + sendRequestHeadersReply(); + closeGrpcStream(); + + // After that we ignore the processor + sendRequestTrailers(); + sendResponseHeaders(false); + sendResponseBody(); + sendResponseTrailers(); +} + +} // namespace +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/ext_proc/utils.cc b/test/extensions/filters/http/ext_proc/utils.cc new file mode 100644 index 000000000000..1bc4464b1de8 --- /dev/null +++ b/test/extensions/filters/http/ext_proc/utils.cc @@ -0,0 +1,24 @@ +#include "test/extensions/filters/http/ext_proc/utils.h" + +#include "test/test_common/utility.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { + +bool ExtProcTestUtility::headerProtosEqualIgnoreOrder( + const Http::HeaderMap& expected, const envoy::config::core::v3::HeaderMap& actual) { + // Comparing header maps is hard because they have duplicates in them. + // So we're going to turn them into a HeaderMap and let Envoy do the work. + Http::TestRequestHeaderMapImpl actual_headers; + for (const auto& header : actual.headers()) { + actual_headers.addCopy(header.key(), header.value()); + } + return TestUtility::headerMapEqualIgnoreOrder(expected, actual_headers); +} + +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy \ No newline at end of file diff --git a/test/extensions/filters/http/ext_proc/utils.h b/test/extensions/filters/http/ext_proc/utils.h new file mode 100644 index 000000000000..e8b931dacc7a --- /dev/null +++ b/test/extensions/filters/http/ext_proc/utils.h @@ -0,0 +1,41 @@ +#pragma once + +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/http/header_map.h" + +#include "absl/strings/str_format.h" +#include "gmock/gmock.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace ExternalProcessing { + +class ExtProcTestUtility { +public: + // Compare a reference header map to a proto + static bool headerProtosEqualIgnoreOrder(const Http::HeaderMap& expected, + const envoy::config::core::v3::HeaderMap& actual); +}; + +MATCHER_P(HeaderProtosEqual, expected, "HTTP header protos match") { + return ExtProcTestUtility::headerProtosEqualIgnoreOrder(expected, arg); +} + +MATCHER_P(HasNoHeader, key, absl::StrFormat("Headers have no value for \"%s\"", key)) { + return arg.get(Http::LowerCaseString(std::string(key))).empty(); +} + +MATCHER_P2(SingleHeaderValueIs, key, value, + absl::StrFormat("Header \"%s\" equals \"%s\"", key, value)) { + const auto hdr = arg.get(Http::LowerCaseString(std::string(key))); + if (hdr.size() != 1) { + return false; + } + return hdr[0]->value() == value; +} + +} // namespace ExternalProcessing +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/fault/fault_filter_test.cc b/test/extensions/filters/http/fault/fault_filter_test.cc index 63707e4d96e9..570ef954e94d 100644 --- a/test/extensions/filters/http/fault/fault_filter_test.cc +++ b/test/extensions/filters/http/fault/fault_filter_test.cc @@ -122,15 +122,16 @@ class FaultFilterTest : public testing::Test { const std::string v2_empty_fault_config_yaml = "{}"; - void SetUpTest(const envoy::extensions::filters::http::fault::v3::HTTPFault fault) { + void setUpTest(const envoy::extensions::filters::http::fault::v3::HTTPFault fault) { config_ = std::make_shared(fault, runtime_, "prefix.", stats_, time_system_); filter_ = std::make_unique(config_); filter_->setDecoderFilterCallbacks(decoder_filter_callbacks_); filter_->setEncoderFilterCallbacks(encoder_filter_callbacks_); - EXPECT_CALL(decoder_filter_callbacks_.dispatcher_, setTrackedObject(_)).Times(AnyNumber()); + EXPECT_CALL(decoder_filter_callbacks_.dispatcher_, pushTrackedObject(_)).Times(AnyNumber()); + EXPECT_CALL(decoder_filter_callbacks_.dispatcher_, popTrackedObject(_)).Times(AnyNumber()); } - void SetUpTest(const std::string& yaml) { SetUpTest(convertYamlStrToProtoConfig(yaml)); } + void setUpTest(const std::string& yaml) { setUpTest(convertYamlStrToProtoConfig(yaml)); } void expectDelayTimer(uint64_t duration_ms) { timer_ = new Event::MockTimer(&decoder_filter_callbacks_.dispatcher_); @@ -228,7 +229,7 @@ TEST_F(FaultFilterTest, AbortWithHttpStatus) { fault.mutable_abort()->mutable_percentage()->set_denominator( envoy::type::v3::FractionalPercent::HUNDRED); fault.mutable_abort()->set_http_status(429); - SetUpTest(fault); + setUpTest(fault); EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.max_active_faults", std::numeric_limits::max())) @@ -274,7 +275,7 @@ TEST_F(FaultFilterTest, AbortWithHttpStatus) { } TEST_F(FaultFilterTest, HeaderAbortWithHttpStatus) { - SetUpTest(header_abort_only_yaml); + setUpTest(header_abort_only_yaml); request_headers_.addCopy("x-envoy-fault-abort-request", "429"); @@ -329,7 +330,7 @@ TEST_F(FaultFilterTest, AbortWithGrpcStatus) { fault.mutable_abort()->mutable_percentage()->set_denominator( envoy::type::v3::FractionalPercent::HUNDRED); fault.mutable_abort()->set_grpc_status(5); - SetUpTest(fault); + setUpTest(fault); EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.max_active_faults", std::numeric_limits::max())) @@ -377,7 +378,7 @@ TEST_F(FaultFilterTest, AbortWithGrpcStatus) { TEST_F(FaultFilterTest, HeaderAbortWithGrpcStatus) { decoder_filter_callbacks_.is_grpc_request_ = true; - SetUpTest(header_abort_only_yaml); + setUpTest(header_abort_only_yaml); request_headers_.addCopy("x-envoy-fault-abort-grpc-request", "5"); @@ -427,7 +428,7 @@ TEST_F(FaultFilterTest, HeaderAbortWithGrpcStatus) { } TEST_F(FaultFilterTest, HeaderAbortWithHttpAndGrpcStatus) { - SetUpTest(header_abort_only_yaml); + setUpTest(header_abort_only_yaml); request_headers_.addCopy("x-envoy-fault-abort-request", "429"); request_headers_.addCopy("x-envoy-fault-abort-grpc-request", "5"); @@ -478,7 +479,7 @@ TEST_F(FaultFilterTest, HeaderAbortWithHttpAndGrpcStatus) { } TEST_F(FaultFilterTest, FixedDelayZeroDuration) { - SetUpTest(fixed_delay_only_yaml); + setUpTest(fixed_delay_only_yaml); // Delay related calls EXPECT_CALL( @@ -506,7 +507,7 @@ TEST_F(FaultFilterTest, FixedDelayZeroDuration) { } TEST_F(FaultFilterTest, Overflow) { - SetUpTest(fixed_delay_only_yaml); + setUpTest(fixed_delay_only_yaml); // Delay related calls EXPECT_CALL( @@ -532,7 +533,7 @@ TEST_F(FaultFilterTest, Overflow) { // Verifies that we don't increment the active_faults gauge when not applying a fault. TEST_F(FaultFilterTest, Passthrough) { envoy::extensions::filters::http::fault::v3::HTTPFault fault; - SetUpTest(fault); + setUpTest(fault); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, true)); @@ -545,7 +546,7 @@ TEST_F(FaultFilterTest, FixedDelayDeprecatedPercentAndNonZeroDuration) { fault.mutable_delay()->mutable_percentage()->set_denominator( envoy::type::v3::FractionalPercent::HUNDRED); fault.mutable_delay()->mutable_fixed_delay()->set_seconds(5); - SetUpTest(fault); + setUpTest(fault); EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.max_active_faults", std::numeric_limits::max())) @@ -588,7 +589,7 @@ TEST_F(FaultFilterTest, FixedDelayDeprecatedPercentAndNonZeroDuration) { } TEST_F(FaultFilterTest, DelayForDownstreamCluster) { - SetUpTest(fixed_delay_only_yaml); + setUpTest(fixed_delay_only_yaml); EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.max_active_faults", std::numeric_limits::max())) @@ -624,7 +625,8 @@ TEST_F(FaultFilterTest, DelayForDownstreamCluster) { EXPECT_CALL(decoder_filter_callbacks_, continueDecoding()); EXPECT_EQ(Http::FilterDataStatus::StopIterationAndWatermark, filter_->decodeData(data_, false)); - EXPECT_CALL(decoder_filter_callbacks_.dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(decoder_filter_callbacks_.dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(decoder_filter_callbacks_.dispatcher_, popTrackedObject(_)); timer_->invokeCallback(); EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_->decodeTrailers(request_trailers_)); @@ -636,7 +638,7 @@ TEST_F(FaultFilterTest, DelayForDownstreamCluster) { } TEST_F(FaultFilterTest, FixedDelayAndAbortDownstream) { - SetUpTest(fixed_delay_and_abort_yaml); + setUpTest(fixed_delay_and_abort_yaml); EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.max_active_faults", std::numeric_limits::max())) @@ -702,7 +704,7 @@ TEST_F(FaultFilterTest, FixedDelayAndAbortDownstream) { } TEST_F(FaultFilterTest, FixedDelayAndAbort) { - SetUpTest(fixed_delay_and_abort_yaml); + setUpTest(fixed_delay_and_abort_yaml); EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.max_active_faults", std::numeric_limits::max())) @@ -758,7 +760,7 @@ TEST_F(FaultFilterTest, FixedDelayAndAbort) { } TEST_F(FaultFilterTest, FixedDelayAndAbortDownstreamNodes) { - SetUpTest(fixed_delay_and_abort_nodes_yaml); + setUpTest(fixed_delay_and_abort_nodes_yaml); EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.max_active_faults", std::numeric_limits::max())) @@ -812,13 +814,13 @@ TEST_F(FaultFilterTest, FixedDelayAndAbortDownstreamNodes) { } TEST_F(FaultFilterTest, NoDownstreamMatch) { - SetUpTest(fixed_delay_and_abort_nodes_yaml); + setUpTest(fixed_delay_and_abort_nodes_yaml); EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(request_headers_, true)); } TEST_F(FaultFilterTest, FixedDelayAndAbortHeaderMatchSuccess) { - SetUpTest(fixed_delay_and_abort_match_headers_yaml); + setUpTest(fixed_delay_and_abort_match_headers_yaml); request_headers_.addCopy("x-foo1", "Bar"); request_headers_.addCopy("x-foo2", "RandomValue"); @@ -875,7 +877,7 @@ TEST_F(FaultFilterTest, FixedDelayAndAbortHeaderMatchSuccess) { } TEST_F(FaultFilterTest, FixedDelayAndAbortHeaderMatchFail) { - SetUpTest(fixed_delay_and_abort_match_headers_yaml); + setUpTest(fixed_delay_and_abort_match_headers_yaml); request_headers_.addCopy("x-foo1", "Bar"); request_headers_.addCopy("x-foo3", "Baz"); @@ -903,7 +905,7 @@ TEST_F(FaultFilterTest, FixedDelayAndAbortHeaderMatchFail) { } TEST_F(FaultFilterTest, TimerResetAfterStreamReset) { - SetUpTest(fixed_delay_only_yaml); + setUpTest(fixed_delay_only_yaml); EXPECT_CALL(runtime_.snapshot_, getInteger("fault.http.max_active_faults", std::numeric_limits::max())) @@ -954,7 +956,7 @@ TEST_F(FaultFilterTest, TimerResetAfterStreamReset) { } TEST_F(FaultFilterTest, FaultWithTargetClusterMatchSuccess) { - SetUpTest(delay_with_upstream_cluster_yaml); + setUpTest(delay_with_upstream_cluster_yaml); const std::string upstream_cluster("www1"); EXPECT_CALL(decoder_filter_callbacks_.route_->route_entry_, clusterName()) @@ -999,7 +1001,7 @@ TEST_F(FaultFilterTest, FaultWithTargetClusterMatchSuccess) { } TEST_F(FaultFilterTest, FaultWithTargetClusterMatchFail) { - SetUpTest(delay_with_upstream_cluster_yaml); + setUpTest(delay_with_upstream_cluster_yaml); const std::string upstream_cluster("mismatch"); EXPECT_CALL(decoder_filter_callbacks_.route_->route_entry_, clusterName()) @@ -1027,7 +1029,7 @@ TEST_F(FaultFilterTest, FaultWithTargetClusterMatchFail) { } TEST_F(FaultFilterTest, FaultWithTargetClusterNullRoute) { - SetUpTest(delay_with_upstream_cluster_yaml); + setUpTest(delay_with_upstream_cluster_yaml); const std::string upstream_cluster("www1"); EXPECT_CALL(*decoder_filter_callbacks_.route_, routeEntry()).WillRepeatedly(Return(nullptr)); @@ -1108,7 +1110,7 @@ TEST_F(FaultFilterTest, RouteFaultOverridesListenerFault) { // route-level fault overrides listener-level fault { - SetUpTest(v2_empty_fault_config_yaml); // This is a valid listener level fault + setUpTest(v2_empty_fault_config_yaml); // This is a valid listener level fault TestPerFilterConfigFault(&delay_fault, nullptr); } @@ -1116,7 +1118,7 @@ TEST_F(FaultFilterTest, RouteFaultOverridesListenerFault) { { config_->stats().aborts_injected_.reset(); config_->stats().delays_injected_.reset(); - SetUpTest(v2_empty_fault_config_yaml); + setUpTest(v2_empty_fault_config_yaml); TestPerFilterConfigFault(nullptr, &delay_fault); } @@ -1124,7 +1126,7 @@ TEST_F(FaultFilterTest, RouteFaultOverridesListenerFault) { { config_->stats().aborts_injected_.reset(); config_->stats().delays_injected_.reset(); - SetUpTest(v2_empty_fault_config_yaml); + setUpTest(v2_empty_fault_config_yaml); TestPerFilterConfigFault(&delay_fault, &abort_fault); } } @@ -1135,7 +1137,7 @@ class FaultFilterRateLimitTest : public FaultFilterTest { envoy::extensions::filters::http::fault::v3::HTTPFault fault; fault.mutable_response_rate_limit()->mutable_fixed_limit()->set_limit_kbps(1); fault.mutable_response_rate_limit()->mutable_percentage()->set_numerator(100); - SetUpTest(fault); + setUpTest(fault); EXPECT_CALL( runtime_.snapshot_, diff --git a/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc b/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc index 557d1b755f47..43e275a5df4d 100644 --- a/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc +++ b/test/extensions/filters/http/grpc_json_transcoder/json_transcoder_filter_test.cc @@ -733,6 +733,41 @@ TEST_F(GrpcJsonTranscoderFilterTest, TranscodingUnaryWithHttpBodyAsOutput) { EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_trailers)); } +TEST_F(GrpcJsonTranscoderFilterTest, TranscodingUnaryOnRootPath) { + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, {":path", "/"}}; + + EXPECT_CALL(decoder_callbacks_, clearRouteCache()); + + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_.decodeHeaders(request_headers, false)); + EXPECT_EQ("application/grpc", request_headers.get_("content-type")); + EXPECT_EQ("/", request_headers.get_("x-envoy-original-path")); + EXPECT_EQ("GET", request_headers.get_("x-envoy-original-method")); + EXPECT_EQ("/bookstore.Bookstore/GetRoot", request_headers.get_(":path")); + EXPECT_EQ("trailers", request_headers.get_("te")); + + Http::TestResponseHeaderMapImpl response_headers{{"content-type", "application/grpc"}, + {":status", "200"}}; + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_.encodeHeaders(response_headers, false)); + EXPECT_EQ("application/json", response_headers.get_("content-type")); + + google::api::HttpBody response; + response.set_content_type("text/html"); + response.set_data("

Hello, world!

"); + + auto response_data = Grpc::Common::serializeToGrpcFrame(response); + + EXPECT_EQ(Http::FilterDataStatus::StopIterationAndBuffer, + filter_.encodeData(*response_data, false)); + + EXPECT_EQ(response.content_type(), response_headers.get_("content-type")); + EXPECT_EQ(response.data(), response_data->toString()); + + Http::TestRequestTrailerMapImpl request_trailers; + EXPECT_EQ(Http::FilterTrailersStatus::Continue, filter_.decodeTrailers(request_trailers)); +} + TEST_F(GrpcJsonTranscoderFilterTest, TranscodingUnaryWithInvalidHttpBodyAsOutput) { Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, {":path", "/echoResponseBodyPath"}}; diff --git a/test/extensions/filters/http/local_ratelimit/BUILD b/test/extensions/filters/http/local_ratelimit/BUILD index 38cd85098ee7..0aeca92a857a 100644 --- a/test/extensions/filters/http/local_ratelimit/BUILD +++ b/test/extensions/filters/http/local_ratelimit/BUILD @@ -19,6 +19,7 @@ envoy_extension_cc_test( "//source/extensions/filters/http/local_ratelimit:local_ratelimit_lib", "//test/common/stream_info:test_util", "//test/mocks/http:http_mocks", + "//test/mocks/local_info:local_info_mocks", "@envoy_api//envoy/extensions/filters/http/local_ratelimit/v3:pkg_cc_proto", ], ) diff --git a/test/extensions/filters/http/local_ratelimit/config_test.cc b/test/extensions/filters/http/local_ratelimit/config_test.cc index 3f48a5830e21..55cc18229969 100644 --- a/test/extensions/filters/http/local_ratelimit/config_test.cc +++ b/test/extensions/filters/http/local_ratelimit/config_test.cc @@ -63,7 +63,7 @@ stat_prefix: test const auto route_config = factory.createRouteSpecificFilterConfig( *proto_config, context, ProtobufMessage::getNullValidationVisitor()); const auto* config = dynamic_cast(route_config.get()); - EXPECT_TRUE(config->requestAllowed()); + EXPECT_TRUE(config->requestAllowed({})); } TEST(Factory, EnabledEnforcedDisabledByDefault) { @@ -125,6 +125,158 @@ stat_prefix: test EnvoyException); } +TEST(Factory, RouteSpecificFilterConfigWithDescriptorsWithNoTokenBucket) { + const std::string config_yaml = R"( +stat_prefix: test +token_bucket: + max_tokens: 1 + tokens_per_fill: 1 + fill_interval: 1000s +filter_enabled: + runtime_key: test_enabled + default_value: + numerator: 100 + denominator: HUNDRED +filter_enforced: + runtime_key: test_enforced + default_value: + numerator: 100 + denominator: HUNDRED +response_headers_to_add: + - append: false + header: + key: x-test-rate-limit + value: 'true' +descriptors: +- entries: + - key: hello + value: world + - key: foo + value: bar +- entries: + - key: foo2 + value: bar2 + )"; + + LocalRateLimitFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(config_yaml, *proto_config); + + NiceMock context; + + EXPECT_CALL(context.dispatcher_, createTimer_(_)).Times(0); + EXPECT_THROW(factory.createRouteSpecificFilterConfig(*proto_config, context, + ProtobufMessage::getNullValidationVisitor()), + EnvoyException); +} + +TEST(Factory, RouteSpecificFilterConfigWithDescriptors) { + const std::string config_yaml = R"( +stat_prefix: test +token_bucket: + max_tokens: 1 + tokens_per_fill: 1 + fill_interval: 60s +filter_enabled: + runtime_key: test_enabled + default_value: + numerator: 100 + denominator: HUNDRED +filter_enforced: + runtime_key: test_enforced + default_value: + numerator: 100 + denominator: HUNDRED +response_headers_to_add: + - append: false + header: + key: x-test-rate-limit + value: 'true' +descriptors: +- entries: + - key: hello + value: world + - key: foo + value: bar + token_bucket: + max_tokens: 10 + tokens_per_fill: 10 + fill_interval: 60s +- entries: + - key: foo2 + value: bar2 + token_bucket: + max_tokens: 100 + tokens_per_fill: 100 + fill_interval: 3600s + )"; + + LocalRateLimitFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(config_yaml, *proto_config); + + NiceMock context; + + EXPECT_CALL(context.dispatcher_, createTimer_(_)); + const auto route_config = factory.createRouteSpecificFilterConfig( + *proto_config, context, ProtobufMessage::getNullValidationVisitor()); + const auto* config = dynamic_cast(route_config.get()); + EXPECT_TRUE(config->requestAllowed({})); +} + +TEST(Factory, RouteSpecificFilterConfigWithDescriptorsTimerNotDivisible) { + const std::string config_yaml = R"( +stat_prefix: test +token_bucket: + max_tokens: 1 + tokens_per_fill: 1 + fill_interval: 100s +filter_enabled: + runtime_key: test_enabled + default_value: + numerator: 100 + denominator: HUNDRED +filter_enforced: + runtime_key: test_enforced + default_value: + numerator: 100 + denominator: HUNDRED +response_headers_to_add: + - append: false + header: + key: x-test-rate-limit + value: 'true' +descriptors: +- entries: + - key: hello + value: world + - key: foo + value: bar + token_bucket: + max_tokens: 10 + tokens_per_fill: 10 + fill_interval: 1s +- entries: + - key: foo2 + value: bar2 + token_bucket: + max_tokens: 100 + tokens_per_fill: 100 + fill_interval: 86400s + )"; + + LocalRateLimitFilterConfig factory; + ProtobufTypes::MessagePtr proto_config = factory.createEmptyRouteConfigProto(); + TestUtility::loadFromYaml(config_yaml, *proto_config); + + NiceMock context; + + EXPECT_CALL(context.dispatcher_, createTimer_(_)); + EXPECT_THROW(factory.createRouteSpecificFilterConfig(*proto_config, context, + ProtobufMessage::getNullValidationVisitor()), + EnvoyException); +} + } // namespace LocalRateLimitFilter } // namespace HttpFilters } // namespace Extensions diff --git a/test/extensions/filters/http/local_ratelimit/filter_test.cc b/test/extensions/filters/http/local_ratelimit/filter_test.cc index 9662f9a783e1..0c9f2507e016 100644 --- a/test/extensions/filters/http/local_ratelimit/filter_test.cc +++ b/test/extensions/filters/http/local_ratelimit/filter_test.cc @@ -3,6 +3,7 @@ #include "extensions/filters/http/local_ratelimit/local_ratelimit.h" #include "test/mocks/http/mocks.h" +#include "test/mocks/local_info/mocks.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -39,7 +40,8 @@ class FilterTest : public testing::Test { public: FilterTest() = default; - void setup(const std::string& yaml, const bool enabled = true, const bool enforced = true) { + void setupPerRoute(const std::string& yaml, const bool enabled = true, const bool enforced = true, + const bool per_route = false) { EXPECT_CALL( runtime_.snapshot_, featureEnabled(absl::string_view("test_enabled"), @@ -53,10 +55,14 @@ class FilterTest : public testing::Test { envoy::extensions::filters::http::local_ratelimit::v3::LocalRateLimit config; TestUtility::loadFromYaml(yaml, config); - config_ = std::make_shared(config, dispatcher_, stats_, runtime_); + config_ = std::make_shared(config, local_info_, dispatcher_, stats_, runtime_, + per_route); filter_ = std::make_shared(config_); filter_->setDecoderFilterCallbacks(decoder_callbacks_); } + void setup(const std::string& yaml, const bool enabled = true, const bool enforced = true) { + setupPerRoute(yaml, enabled, enforced); + } uint64_t findCounter(const std::string& name) { const auto counter = TestUtility::findCounter(stats_, name); @@ -69,6 +75,7 @@ class FilterTest : public testing::Test { testing::NiceMock decoder_callbacks_; NiceMock dispatcher_; NiceMock runtime_; + NiceMock local_info_; std::shared_ptr config_; std::shared_ptr filter_; }; @@ -140,6 +147,188 @@ TEST_F(FilterTest, RequestRateLimitedButNotEnforced) { EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.rate_limited")); } +static const std::string descriptor_config_yaml = R"( +stat_prefix: test +token_bucket: + max_tokens: {} + tokens_per_fill: 1 + fill_interval: 60s +filter_enabled: + runtime_key: test_enabled + default_value: + numerator: 100 + denominator: HUNDRED +filter_enforced: + runtime_key: test_enforced + default_value: + numerator: 100 + denominator: HUNDRED +response_headers_to_add: + - append: false + header: + key: x-test-rate-limit + value: 'true' +descriptors: +- entries: + - key: hello + value: world + - key: foo + value: bar + token_bucket: + max_tokens: 10 + tokens_per_fill: 10 + fill_interval: 60s +- entries: + - key: foo2 + value: bar2 + token_bucket: + max_tokens: {} + tokens_per_fill: 1 + fill_interval: 60s +stage: {} + )"; + +class DescriptorFilterTest : public FilterTest { +public: + DescriptorFilterTest() = default; + + void setUpTest(const std::string& yaml) { + setupPerRoute(yaml, true, true, true); + decoder_callbacks_.route_->route_entry_.rate_limit_policy_.rate_limit_policy_entry_.clear(); + decoder_callbacks_.route_->route_entry_.rate_limit_policy_.rate_limit_policy_entry_ + .emplace_back(route_rate_limit_); + } + + std::vector descriptor_{{{{"foo2", "bar2"}}}}; + std::vector descriptor_first_match_{{ + {{ + {"hello", "world"}, + {"foo", "bar"}, + }}, + {{{"foo2", "bar2"}}}, + }}; + std::vector descriptor_not_found_{{{{"foo", "bar"}}}}; + NiceMock route_rate_limit_; +}; + +TEST_F(DescriptorFilterTest, NoRouteEntry) { + setupPerRoute(fmt::format(descriptor_config_yaml, "1", "1", "0"), true, true, true); + + auto headers = Http::TestRequestHeaderMapImpl(); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enabled")); + EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enforced")); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.ok")); +} + +TEST_F(DescriptorFilterTest, NoCluster) { + setUpTest(fmt::format(descriptor_config_yaml, "1", "1", "0")); + + EXPECT_CALL(decoder_callbacks_, clusterInfo()).WillRepeatedly(testing::Return(nullptr)); + + auto headers = Http::TestRequestHeaderMapImpl(); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enabled")); + EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enforced")); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.ok")); +} + +TEST_F(DescriptorFilterTest, DisabledInRoute) { + setUpTest(fmt::format(descriptor_config_yaml, "1", "1", "0")); + + EXPECT_CALL(decoder_callbacks_.route_->route_entry_.rate_limit_policy_, + getApplicableRateLimit(0)); + + route_rate_limit_.disable_key_ = "disabled"; + + auto headers = Http::TestRequestHeaderMapImpl(); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enabled")); + EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enforced")); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.ok")); +} + +TEST_F(DescriptorFilterTest, RouteDescriptorRequestOk) { + setUpTest(fmt::format(descriptor_config_yaml, "1", "1", "0")); + + EXPECT_CALL(decoder_callbacks_.route_->route_entry_.rate_limit_policy_, + getApplicableRateLimit(0)); + + EXPECT_CALL(route_rate_limit_, populateLocalDescriptors(_, _, _, _)) + .WillOnce(testing::SetArgReferee<0>(descriptor_)); + + auto headers = Http::TestRequestHeaderMapImpl(); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enabled")); + EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enforced")); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.ok")); +} + +TEST_F(DescriptorFilterTest, RouteDescriptorRequestRatelimited) { + setUpTest(fmt::format(descriptor_config_yaml, "0", "0", "0")); + + EXPECT_CALL(decoder_callbacks_.route_->route_entry_.rate_limit_policy_, + getApplicableRateLimit(0)); + + EXPECT_CALL(route_rate_limit_, populateLocalDescriptors(_, _, _, _)) + .WillOnce(testing::SetArgReferee<0>(descriptor_)); + + auto headers = Http::TestRequestHeaderMapImpl(); + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enabled")); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enforced")); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.rate_limited")); +} + +TEST_F(DescriptorFilterTest, RouteDescriptorNotFound) { + setUpTest(fmt::format(descriptor_config_yaml, "1", "1", "0")); + + EXPECT_CALL(decoder_callbacks_.route_->route_entry_.rate_limit_policy_, + getApplicableRateLimit(0)); + + EXPECT_CALL(route_rate_limit_, populateLocalDescriptors(_, _, _, _)) + .WillOnce(testing::SetArgReferee<0>(descriptor_not_found_)); + + auto headers = Http::TestRequestHeaderMapImpl(); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enabled")); + EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enforced")); + EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.rate_limited")); +} + +TEST_F(DescriptorFilterTest, RouteDescriptorFirstMatch) { + // Request should not be rate limited as it should match first descriptor with 10 req/min + setUpTest(fmt::format(descriptor_config_yaml, "0", "0", "0")); + + EXPECT_CALL(decoder_callbacks_.route_->route_entry_.rate_limit_policy_, + getApplicableRateLimit(0)); + + EXPECT_CALL(route_rate_limit_, populateLocalDescriptors(_, _, _, _)) + .WillOnce(testing::SetArgReferee<0>(descriptor_first_match_)); + + auto headers = Http::TestRequestHeaderMapImpl(); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enabled")); + EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enforced")); + EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.rate_limited")); +} + +TEST_F(DescriptorFilterTest, RouteDescriptorWithStageConfig) { + setUpTest(fmt::format(descriptor_config_yaml, "1", "1", "1")); + + EXPECT_CALL(decoder_callbacks_.route_->route_entry_.rate_limit_policy_, + getApplicableRateLimit(1)); + + EXPECT_CALL(route_rate_limit_, populateLocalDescriptors(_, _, _, _)) + .WillOnce(testing::SetArgReferee<0>(descriptor_)); + + auto headers = Http::TestRequestHeaderMapImpl(); + EXPECT_EQ(Http::FilterHeadersStatus::Continue, filter_->decodeHeaders(headers, false)); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.enabled")); + EXPECT_EQ(0U, findCounter("test.http_local_rate_limit.enforced")); + EXPECT_EQ(1U, findCounter("test.http_local_rate_limit.ok")); +} + } // namespace LocalRateLimitFilter } // namespace HttpFilters } // namespace Extensions diff --git a/test/extensions/filters/http/oauth2/config_test.cc b/test/extensions/filters/http/oauth2/config_test.cc index 869d917b6e4e..f9f7d24a7978 100644 --- a/test/extensions/filters/http/oauth2/config_test.cc +++ b/test/extensions/filters/http/oauth2/config_test.cc @@ -47,6 +47,10 @@ void expectInvalidSecretConfig(const std::string& failed_secret_name, signout_path: path: exact: /signout + auth_scopes: + - user + - openid + - email )EOF"; OAuth2Config factory; @@ -87,6 +91,10 @@ TEST(ConfigTest, CreateFilter) { signout_path: path: exact: /signout + auth_scopes: + - user + - openid + - email )EOF"; OAuth2Config factory; diff --git a/test/extensions/filters/http/oauth2/filter_test.cc b/test/extensions/filters/http/oauth2/filter_test.cc index dd5fafd64670..c4a04d24c37b 100644 --- a/test/extensions/filters/http/oauth2/filter_test.cc +++ b/test/extensions/filters/http/oauth2/filter_test.cc @@ -35,6 +35,8 @@ static const std::string TEST_CALLBACK = "/_oauth"; static const std::string TEST_CLIENT_ID = "1"; static const std::string TEST_CLIENT_SECRET_ID = "MyClientSecretKnoxID"; static const std::string TEST_TOKEN_SECRET_ID = "MyTokenSecretKnoxID"; +static const std::string TEST_DEFAULT_SCOPE = "user"; +static const std::string TEST_ENCODED_AUTH_SCOPES = "user%20openid%20email"; namespace { Http::RegisterCustomInlineHeader @@ -78,12 +80,22 @@ class OAuth2Test : public testing::Test { init(); } - void init() { - // Set up the OAuth client + void init() { init(getConfig()); } + + void init(FilterConfigSharedPtr config) { + // Set up the OAuth client. oauth_client_ = new MockOAuth2Client(); std::unique_ptr oauth_client_ptr{oauth_client_}; - // Set up proto fields + config_ = config; + filter_ = std::make_shared(config_, std::move(oauth_client_ptr), test_time_); + filter_->setDecoderFilterCallbacks(decoder_callbacks_); + validator_ = std::make_shared(); + filter_->validator_ = validator_; + } + + // Set up proto fields with standard config. + FilterConfigSharedPtr getConfig() { envoy::extensions::filters::http::oauth2::v3alpha::OAuth2Config p; auto* endpoint = p.mutable_token_endpoint(); endpoint->set_cluster("auth.example.com"); @@ -94,10 +106,12 @@ class OAuth2Test : public testing::Test { p.set_authorization_endpoint("https://auth.example.com/oauth/authorize/"); p.mutable_signout_path()->mutable_path()->set_exact("/_signout"); p.set_forward_bearer_token(true); + p.add_auth_scopes("user"); + p.add_auth_scopes("openid"); + p.add_auth_scopes("email"); auto* matcher = p.add_pass_through_matcher(); matcher->set_name(":method"); matcher->set_exact_match("OPTIONS"); - auto credentials = p.mutable_credentials(); credentials->set_client_id(TEST_CLIENT_ID); credentials->mutable_token_secret()->set_name("secret"); @@ -105,15 +119,12 @@ class OAuth2Test : public testing::Test { MessageUtil::validate(p, ProtobufMessage::getStrictValidationVisitor()); - // Create the OAuth config. + // Create filter config. auto secret_reader = std::make_shared(); - config_ = std::make_shared(p, factory_context_.cluster_manager_, secret_reader, - scope_, "test."); + FilterConfigSharedPtr c = std::make_shared(p, factory_context_.cluster_manager_, + secret_reader, scope_, "test."); - filter_ = std::make_shared(config_, std::move(oauth_client_ptr), test_time_); - filter_->setDecoderFilterCallbacks(decoder_callbacks_); - validator_ = std::make_shared(); - filter_->validator_ = validator_; + return c; } Http::AsyncClient::Callbacks* popPendingCallback() { @@ -151,6 +162,73 @@ TEST_F(OAuth2Test, InvalidCluster) { "specify which cluster to direct OAuth requests to."); } +// Verifies that the OAuth config is created with a default value for auth_scopes field when it is +// not set in proto/yaml. +TEST_F(OAuth2Test, DefaultAuthScope) { + + // Set up proto fields with no auth scope set. + envoy::extensions::filters::http::oauth2::v3alpha::OAuth2Config p; + auto* endpoint = p.mutable_token_endpoint(); + endpoint->set_cluster("auth.example.com"); + endpoint->set_uri("auth.example.com/_oauth"); + endpoint->mutable_timeout()->set_seconds(1); + p.set_redirect_uri("%REQ(x-forwarded-proto)%://%REQ(:authority)%" + TEST_CALLBACK); + p.mutable_redirect_path_matcher()->mutable_path()->set_exact(TEST_CALLBACK); + p.set_authorization_endpoint("https://auth.example.com/oauth/authorize/"); + p.mutable_signout_path()->mutable_path()->set_exact("/_signout"); + p.set_forward_bearer_token(true); + auto* matcher = p.add_pass_through_matcher(); + matcher->set_name(":method"); + matcher->set_exact_match("OPTIONS"); + + auto credentials = p.mutable_credentials(); + credentials->set_client_id(TEST_CLIENT_ID); + credentials->mutable_token_secret()->set_name("secret"); + credentials->mutable_hmac_secret()->set_name("hmac"); + + MessageUtil::validate(p, ProtobufMessage::getStrictValidationVisitor()); + + // Create the OAuth config. + auto secret_reader = std::make_shared(); + FilterConfigSharedPtr test_config_; + test_config_ = std::make_shared(p, factory_context_.cluster_manager_, secret_reader, + scope_, "test."); + + // Auth_scopes was not set, should return default value. + EXPECT_EQ(test_config_->encodedAuthScopes(), TEST_DEFAULT_SCOPE); + + // Recreate the filter with current config and test if the scope was added + // as a query parameter in response headers. + init(test_config_); + Http::TestRequestHeaderMapImpl request_headers{ + {Http::Headers::get().Path.get(), "/not/_oauth"}, + {Http::Headers::get().Host.get(), "traffic.example.com"}, + {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, + {Http::Headers::get().Scheme.get(), "http"}, + {Http::Headers::get().ForwardedProto.get(), "http"}, + }; + + Http::TestResponseHeaderMapImpl response_headers{ + {Http::Headers::get().Status.get(), "302"}, + {Http::Headers::get().Location.get(), + "https://auth.example.com/oauth/" + "authorize/?client_id=" + + TEST_CLIENT_ID + "&scope=" + TEST_DEFAULT_SCOPE + + "&response_type=code&" + "redirect_uri=http%3A%2F%2Ftraffic.example.com%2F" + "_oauth&state=http%3A%2F%2Ftraffic.example.com%2Fnot%2F_oauth"}, + }; + + // explicitly tell the validator to fail the validation. + EXPECT_CALL(*validator_, setParams(_, _)); + EXPECT_CALL(*validator_, isValid()).WillOnce(Return(false)); + + EXPECT_CALL(decoder_callbacks_, encodeHeaders_(HeaderMapEqualRef(&response_headers), true)); + + EXPECT_EQ(Http::FilterHeadersStatus::StopIteration, + filter_->decodeHeaders(request_headers, false)); +} + /** * Scenario: The OAuth filter receives a sign out request. * @@ -243,8 +321,8 @@ TEST_F(OAuth2Test, OAuthErrorNonOAuthHttpCallback) { {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + - "&scope=user&response_type=code&" + TEST_CLIENT_ID + "&scope=" + TEST_ENCODED_AUTH_SCOPES + + "&response_type=code&" "redirect_uri=http%3A%2F%2Ftraffic.example.com%2F" "_oauth&state=http%3A%2F%2Ftraffic.example.com%2Fnot%2F_oauth"}, }; @@ -397,8 +475,8 @@ TEST_F(OAuth2Test, OAuthTestInvalidUrlInStateQueryParam) { Http::TestRequestHeaderMapImpl request_headers{ {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, - {Http::Headers::get().Path.get(), "/_oauth?code=abcdefxyz123&scope=user&" - "state=blah"}, + {Http::Headers::get().Path.get(), + "/_oauth?code=abcdefxyz123&scope=" + TEST_ENCODED_AUTH_SCOPES + "&state=blah"}, {Http::Headers::get().Cookie.get(), "OauthExpires=123;version=test"}, {Http::Headers::get().Cookie.get(), "BearerToken=legit_token;version=test"}, {Http::Headers::get().Cookie.get(), @@ -431,8 +509,10 @@ TEST_F(OAuth2Test, OAuthTestCallbackUrlInStateQueryParam) { Http::TestRequestHeaderMapImpl request_headers{ {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, - {Http::Headers::get().Path.get(), "/_oauth?code=abcdefxyz123&scope=user&" - "state=https%3A%2F%2Ftraffic.example.com%2F_oauth"}, + {Http::Headers::get().Path.get(), + "/_oauth?code=abcdefxyz123&scope=" + TEST_ENCODED_AUTH_SCOPES + + "&state=https%3A%2F%2Ftraffic.example.com%2F_oauth"}, + {Http::Headers::get().Cookie.get(), "OauthExpires=123;version=test"}, {Http::Headers::get().Cookie.get(), "BearerToken=legit_token;version=test"}, {Http::Headers::get().Cookie.get(), @@ -462,8 +542,9 @@ TEST_F(OAuth2Test, OAuthTestCallbackUrlInStateQueryParam) { Http::TestRequestHeaderMapImpl final_request_headers{ {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, - {Http::Headers::get().Path.get(), "/_oauth?code=abcdefxyz123&scope=user&" - "state=https%3A%2F%2Ftraffic.example.com%2F_oauth"}, + {Http::Headers::get().Path.get(), + "/_oauth?code=abcdefxyz123&scope=" + TEST_ENCODED_AUTH_SCOPES + + "&state=https%3A%2F%2Ftraffic.example.com%2F_oauth"}, {Http::Headers::get().Cookie.get(), "OauthExpires=123;version=test"}, {Http::Headers::get().Cookie.get(), "BearerToken=legit_token;version=test"}, {Http::Headers::get().Cookie.get(), @@ -487,8 +568,9 @@ TEST_F(OAuth2Test, OAuthTestUpdatePathAfterSuccess) { Http::TestRequestHeaderMapImpl request_headers{ {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, - {Http::Headers::get().Path.get(), "/_oauth?code=abcdefxyz123&scope=user&" - "state=https%3A%2F%2Ftraffic.example.com%2Foriginal_path"}, + {Http::Headers::get().Path.get(), + "/_oauth?code=abcdefxyz123&scope=" + TEST_ENCODED_AUTH_SCOPES + + "&state=https%3A%2F%2Ftraffic.example.com%2Foriginal_path"}, {Http::Headers::get().Cookie.get(), "OauthExpires=123;version=test"}, {Http::Headers::get().Cookie.get(), "BearerToken=legit_token;version=test"}, {Http::Headers::get().Cookie.get(), @@ -516,8 +598,9 @@ TEST_F(OAuth2Test, OAuthTestUpdatePathAfterSuccess) { Http::TestRequestHeaderMapImpl final_request_headers{ {Http::Headers::get().Host.get(), "traffic.example.com"}, {Http::Headers::get().Method.get(), Http::Headers::get().MethodValues.Get}, - {Http::Headers::get().Path.get(), "/_oauth?code=abcdefxyz123&scope=user&" - "state=https%3A%2F%2Ftraffic.example.com%2Foriginal_path"}, + {Http::Headers::get().Path.get(), + "/_oauth?code=abcdefxyz123&scope=" + TEST_ENCODED_AUTH_SCOPES + + "&state=https%3A%2F%2Ftraffic.example.com%2Foriginal_path"}, {Http::Headers::get().Cookie.get(), "OauthExpires=123;version=test"}, {Http::Headers::get().Cookie.get(), "BearerToken=legit_token;version=test"}, {Http::Headers::get().Cookie.get(), @@ -550,8 +633,8 @@ TEST_F(OAuth2Test, OAuthTestFullFlowPostWithParameters) { {Http::Headers::get().Location.get(), "https://auth.example.com/oauth/" "authorize/?client_id=" + - TEST_CLIENT_ID + - "&scope=user&response_type=code&" + TEST_CLIENT_ID + "&scope=" + TEST_ENCODED_AUTH_SCOPES + + "&response_type=code&" "redirect_uri=https%3A%2F%2Ftraffic.example.com%2F" "_oauth&state=https%3A%2F%2Ftraffic.example.com%2Ftest%" "3Fname%3Dadmin%26level%3Dtrace"}, diff --git a/test/extensions/filters/http/oauth2/oauth_integration_test.cc b/test/extensions/filters/http/oauth2/oauth_integration_test.cc index 565f4349ba94..14ec8dca4bac 100644 --- a/test/extensions/filters/http/oauth2/oauth_integration_test.cc +++ b/test/extensions/filters/http/oauth2/oauth_integration_test.cc @@ -58,6 +58,10 @@ name: oauth name: token hmac_secret: name: hmac + auth_scopes: + - user + - openid + - email )EOF"); // Add the OAuth cluster. diff --git a/test/extensions/filters/http/oauth2/oauth_test.cc b/test/extensions/filters/http/oauth2/oauth_test.cc index c1de35521ab8..f0a72cdfd4b2 100644 --- a/test/extensions/filters/http/oauth2/oauth_test.cc +++ b/test/extensions/filters/http/oauth2/oauth_test.cc @@ -79,12 +79,20 @@ TEST_F(OAuth2ClientTest, RequestAccessTokenSuccess) { mock_response->body().add(json); EXPECT_CALL(cm_.thread_local_cluster_.async_client_, send_(_, _, _)) - .WillRepeatedly( - Invoke([&](Http::RequestMessagePtr&, Http::AsyncClient::Callbacks& cb, - const Http::AsyncClient::RequestOptions&) -> Http::AsyncClient::Request* { - callbacks_.push_back(&cb); - return &request_; - })); + .WillRepeatedly(Invoke([&](Http::RequestMessagePtr& message, Http::AsyncClient::Callbacks& cb, + const Http::AsyncClient::RequestOptions&) + -> Http::AsyncClient::Request* { + EXPECT_EQ(Http::Headers::get().MethodValues.Post, + message->headers().Method()->value().getStringView()); + EXPECT_EQ(Http::Headers::get().ContentTypeValues.FormUrlEncoded, + message->headers().ContentType()->value().getStringView()); + EXPECT_TRUE( + !message->headers().get(Http::CustomHeaders::get().Accept).empty() && + message->headers().get(Http::CustomHeaders::get().Accept)[0]->value().getStringView() == + Http::Headers::get().ContentTypeValues.Json); + callbacks_.push_back(&cb); + return &request_; + })); client_->setCallbacks(*mock_callbacks_); client_->asyncGetAccessToken("a", "b", "c", "d"); diff --git a/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc b/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc index b02137388102..1a8e83aa5740 100644 --- a/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc +++ b/test/extensions/filters/http/ratelimit/ratelimit_integration_test.cc @@ -231,14 +231,18 @@ class RatelimitFilterEnvoyRatelimitedHeaderDisabledIntegrationTest }; INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, RatelimitIntegrationTest, - VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::VersionedGrpcClientIntegrationParamTest::protocolTestParamsToString); INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, RatelimitFailureModeIntegrationTest, - VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::VersionedGrpcClientIntegrationParamTest::protocolTestParamsToString); INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, RatelimitFilterHeadersEnabledIntegrationTest, - VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::VersionedGrpcClientIntegrationParamTest::protocolTestParamsToString); INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, RatelimitFilterEnvoyRatelimitedHeaderDisabledIntegrationTest, - VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::VersionedGrpcClientIntegrationParamTest::protocolTestParamsToString); TEST_P(RatelimitIntegrationTest, Ok) { XDS_DEPRECATED_FEATURE_TEST_SKIP; diff --git a/test/extensions/filters/http/squash/squash_filter_test.cc b/test/extensions/filters/http/squash/squash_filter_test.cc index 5cb81564c4be..260ba53ca2a1 100644 --- a/test/extensions/filters/http/squash/squash_filter_test.cc +++ b/test/extensions/filters/http/squash/squash_filter_test.cc @@ -2,6 +2,7 @@ #include #include +#include "envoy/common/scope_tracker.h" #include "envoy/extensions/filters/http/squash/v3/squash.pb.h" #include "common/http/message_impl.h" @@ -328,7 +329,8 @@ TEST_F(SquashFilterTest, Timeout) { EXPECT_CALL(request_, cancel()); EXPECT_CALL(filter_callbacks_, continueDecoding()); - EXPECT_CALL(filter_callbacks_.dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(filter_callbacks_.dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(filter_callbacks_.dispatcher_, popTrackedObject(_)); attachmentTimeout_timer_->invokeCallback(); EXPECT_EQ(Envoy::Http::FilterDataStatus::Continue, filter_->decodeData(buffer, false)); @@ -357,7 +359,9 @@ TEST_F(SquashFilterTest, CheckRetryPollingAttachment) { // Expect the second get attachment request expectAsyncClientSend(); - EXPECT_CALL(filter_callbacks_.dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(filter_callbacks_.dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(filter_callbacks_.dispatcher_, popTrackedObject(_)); + retry_timer->invokeCallback(); EXPECT_CALL(filter_callbacks_, continueDecoding()); completeGetStatusRequest("attached"); @@ -377,7 +381,8 @@ TEST_F(SquashFilterTest, PollingAttachmentNoCluster) { // Expect the second get attachment request ON_CALL(factory_context_.cluster_manager_, getThreadLocalCluster("squash")) .WillByDefault(Return(nullptr)); - EXPECT_CALL(filter_callbacks_.dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(filter_callbacks_.dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(filter_callbacks_.dispatcher_, popTrackedObject(_)); EXPECT_CALL(*retry_timer, enableTimer(config_->attachmentPollPeriod(), _)); retry_timer->invokeCallback(); } @@ -395,7 +400,8 @@ TEST_F(SquashFilterTest, CheckRetryPollingAttachmentOnFailure) { // Expect the second get attachment request expectAsyncClientSend(); - EXPECT_CALL(filter_callbacks_.dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(filter_callbacks_.dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(filter_callbacks_.dispatcher_, popTrackedObject(_)); retry_timer->invokeCallback(); EXPECT_CALL(filter_callbacks_, continueDecoding()); @@ -466,7 +472,8 @@ TEST_F(SquashFilterTest, TimerExpiresInline) { attachmentTimeout_timer_->scope_ = scope; attachmentTimeout_timer_->enabled_ = true; // timer expires inline - EXPECT_CALL(filter_callbacks_.dispatcher_, setTrackedObject(_)).Times(2); + EXPECT_CALL(filter_callbacks_.dispatcher_, pushTrackedObject(_)); + EXPECT_CALL(filter_callbacks_.dispatcher_, popTrackedObject(_)); attachmentTimeout_timer_->invokeCallback(); })); diff --git a/test/extensions/filters/network/common/fuzz/README.md b/test/extensions/filters/network/common/fuzz/README.md index 21181c8402a4..d35fb5d92dbc 100644 --- a/test/extensions/filters/network/common/fuzz/README.md +++ b/test/extensions/filters/network/common/fuzz/README.md @@ -1,2 +1,2 @@ -Network filters need to be fuzzed. Filters come in two flavors, each with their own fuzzer. Read filters should be added into the [Generic ReadFilter Fuzzer](https://github.com/envoyproxy/envoy/blob/master/test/extensions/filters/network/common/fuzz/network_readfilter_fuzz_test.cc). Write Filters should added into the [Generic WriteFilter Fuzzer](https://github.com/envoyproxy/envoy/blob/master/test/extensions/filters/network/common/fuzz/network_writefilter_fuzz_test.cc). Some filters are both raed and write filters: They should be added into both fuzzers. -To add a new filter into generic network level filter fuzzers, see the [doc](https://github.com/envoyproxy/envoy/blob/master/source/docs/network_filter_fuzzing.md). \ No newline at end of file +Network filters need to be fuzzed. Filters come in two flavors, each with their own fuzzer. Read filters should be added into the [Generic ReadFilter Fuzzer](https://github.com/envoyproxy/envoy/blob/main/test/extensions/filters/network/common/fuzz/network_readfilter_fuzz_test.cc). Write Filters should added into the [Generic WriteFilter Fuzzer](https://github.com/envoyproxy/envoy/blob/main/test/extensions/filters/network/common/fuzz/network_writefilter_fuzz_test.cc). Some filters are both raed and write filters: They should be added into both fuzzers. +To add a new filter into generic network level filter fuzzers, see the [doc](https://github.com/envoyproxy/envoy/blob/main/source/docs/network_filter_fuzzing.md). \ No newline at end of file diff --git a/test/extensions/filters/network/common/redis/BUILD b/test/extensions/filters/network/common/redis/BUILD index 5bb47f0c7e49..0f27100d53fc 100644 --- a/test/extensions/filters/network/common/redis/BUILD +++ b/test/extensions/filters/network/common/redis/BUILD @@ -58,6 +58,7 @@ envoy_cc_test( "//test/mocks/thread_local:thread_local_mocks", "//test/mocks/upstream:host_mocks", "//test/test_common:simulated_time_system_lib", + "//test/test_common:test_runtime_lib", "@envoy_api//envoy/extensions/filters/network/redis_proxy/v3:pkg_cc_proto", ], ) diff --git a/test/extensions/filters/network/common/redis/client_impl_test.cc b/test/extensions/filters/network/common/redis/client_impl_test.cc index 953aa9374fbd..e68f1769c1f6 100644 --- a/test/extensions/filters/network/common/redis/client_impl_test.cc +++ b/test/extensions/filters/network/common/redis/client_impl_test.cc @@ -14,6 +14,7 @@ #include "test/mocks/network/mocks.h" #include "test/mocks/upstream/host.h" #include "test/test_common/simulated_time_system.h" +#include "test/test_common/test_runtime.h" #include "gmock/gmock.h" #include "gtest/gtest.h" @@ -75,8 +76,9 @@ class RedisClientImplTest : public testing::Test, EXPECT_CALL(*upstream_connection_, addReadFilter(_)) .WillOnce(SaveArg<0>(&upstream_read_filter_)); EXPECT_CALL(*upstream_connection_, connect()); - EXPECT_CALL(*upstream_connection_, noDelay(true)); - + if (legacy_nodelay_) { + EXPECT_CALL(*upstream_connection_, noDelay(true)); + } redis_command_stats_ = Common::Redis::RedisCommandStats::createRedisCommandStats(stats_.symbolTable()); @@ -145,6 +147,7 @@ class RedisClientImplTest : public testing::Test, Common::Redis::RedisCommandStatsSharedPtr redis_command_stats_; std::string auth_username_; std::string auth_password_; + bool legacy_nodelay_{}; }; TEST_F(RedisClientImplTest, BatchWithZeroBufferAndTimeout) { @@ -338,6 +341,62 @@ TEST_F(RedisClientImplTest, Basic) { client_->close(); } +TEST_F(RedisClientImplTest, BasicLegacyNodelay) { + legacy_nodelay_ = true; + TestScopedRuntime scoped_runtime; + Runtime::LoaderSingleton::getExisting()->mergeValues( + {{"envoy.reloadable_features.always_nodelay", "false"}}); + InSequence s; + + setup(); + + client_->initialize(auth_username_, auth_password_); + + Common::Redis::RespValue request1; + MockClientCallbacks callbacks1; + EXPECT_CALL(*encoder_, encode(Ref(request1), _)); + EXPECT_CALL(*flush_timer_, enabled()).WillOnce(Return(false)); + PoolRequest* handle1 = client_->makeRequest(request1, callbacks1); + EXPECT_NE(nullptr, handle1); + + onConnected(); + + Common::Redis::RespValue request2; + MockClientCallbacks callbacks2; + EXPECT_CALL(*encoder_, encode(Ref(request2), _)); + EXPECT_CALL(*flush_timer_, enabled()).WillOnce(Return(false)); + PoolRequest* handle2 = client_->makeRequest(request2, callbacks2); + EXPECT_NE(nullptr, handle2); + + EXPECT_EQ(2UL, host_->cluster_.stats_.upstream_rq_total_.value()); + EXPECT_EQ(2UL, host_->cluster_.stats_.upstream_rq_active_.value()); + EXPECT_EQ(2UL, host_->stats_.rq_total_.value()); + EXPECT_EQ(2UL, host_->stats_.rq_active_.value()); + + Buffer::OwnedImpl fake_data; + EXPECT_CALL(*decoder_, decode(Ref(fake_data))).WillOnce(Invoke([&](Buffer::Instance&) -> void { + InSequence s; + Common::Redis::RespValuePtr response1(new Common::Redis::RespValue()); + EXPECT_CALL(callbacks1, onResponse_(Ref(response1))); + EXPECT_CALL(*connect_or_op_timer_, enableTimer(_, _)); + EXPECT_CALL(host_->outlier_detector_, + putResult(Upstream::Outlier::Result::ExtOriginRequestSuccess, _)); + callbacks_->onRespValue(std::move(response1)); + + Common::Redis::RespValuePtr response2(new Common::Redis::RespValue()); + EXPECT_CALL(callbacks2, onResponse_(Ref(response2))); + EXPECT_CALL(*connect_or_op_timer_, disableTimer()); + EXPECT_CALL(host_->outlier_detector_, + putResult(Upstream::Outlier::Result::ExtOriginRequestSuccess, _)); + callbacks_->onRespValue(std::move(response2)); + })); + upstream_read_filter_->onData(fake_data, false); + + EXPECT_CALL(*upstream_connection_, close(Network::ConnectionCloseType::NoFlush)); + EXPECT_CALL(*connect_or_op_timer_, disableTimer()); + client_->close(); +} + class ConfigEnableCommandStats : public Config { bool disableOutlierEvents() const override { return false; } std::chrono::milliseconds opTimeout() const override { return std::chrono::milliseconds(25); } diff --git a/test/extensions/filters/network/mysql_proxy/BUILD b/test/extensions/filters/network/mysql_proxy/BUILD index 92dbbdbf6bc2..a9ed2de9ed0d 100644 --- a/test/extensions/filters/network/mysql_proxy/BUILD +++ b/test/extensions/filters/network/mysql_proxy/BUILD @@ -34,6 +34,43 @@ envoy_extension_cc_test( ], ) +envoy_extension_cc_test( + name = "mysql_greet_tests", + srcs = [ + "mysql_greet_test.cc", + ], + extension_name = "envoy.filters.network.mysql_proxy", + deps = [ + ":mysql_test_utils_lib", + "//source/extensions/filters/network/mysql_proxy:proxy_lib", + ], +) + +envoy_extension_cc_test( + name = "mysql_clogin_tests", + srcs = [ + "mysql_clogin_test.cc", + ], + extension_name = "envoy.filters.network.mysql_proxy", + deps = [ + ":mysql_test_utils_lib", + "//source/extensions/filters/network/mysql_proxy:proxy_lib", + ], +) + +envoy_extension_cc_test( + name = "mysql_clogin_resp_tests", + srcs = [ + "mysql_clogin_resp_test.cc", + ], + extension_name = "envoy.filters.network.mysql_proxy", + deps = [ + ":mysql_test_utils_lib", + "//source/extensions/filters/network/mysql_proxy:proxy_lib", + ], +) + + envoy_extension_cc_test( name = "mysql_filter_tests", srcs = [ diff --git a/test/extensions/filters/network/mysql_proxy/mysql_clogin_resp_test.cc b/test/extensions/filters/network/mysql_proxy/mysql_clogin_resp_test.cc new file mode 100644 index 000000000000..ff13fa310d96 --- /dev/null +++ b/test/extensions/filters/network/mysql_proxy/mysql_clogin_resp_test.cc @@ -0,0 +1,456 @@ +#include "common/buffer/buffer_impl.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_clogin.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_command.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_greeting.h" +#include "extensions/filters/network/mysql_proxy/mysql_utils.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include "mysql_test_utils.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace MySQLProxy { + +constexpr int MYSQL_UT_LAST_ID = 0; +constexpr int MYSQL_UT_SERVER_OK = 0; +constexpr int MYSQL_UT_SERVER_WARNINGS = 0x0001; + +class MySQLCLoginRespTest : public testing::Test {}; + +/* + * Test the MYSQL Server Login Response OK message parser: + * - message is encoded using the ClientLoginResponse class + * - message is decoded using the ClientLoginResponse class + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginOkEncDec) { + ClientLoginResponse mysql_loginok_encode{}; + mysql_loginok_encode.type(Ok); + mysql_loginok_encode.asOkMessage().setAffectedRows(1); + mysql_loginok_encode.asOkMessage().setLastInsertId(MYSQL_UT_LAST_ID); + mysql_loginok_encode.asOkMessage().setServerStatus(MYSQL_UT_SERVER_OK); + mysql_loginok_encode.asOkMessage().setWarnings(MYSQL_UT_SERVER_WARNINGS); + Buffer::OwnedImpl decode_data; + mysql_loginok_encode.encode(decode_data); + + ClientLoginResponse mysql_loginok_decode{}; + mysql_loginok_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginok_decode, mysql_loginok_encode); +} + +/* + * Test the MYSQL Server Login Response Err message parser: + * - message is encoded using the ClientLoginResponse class + * - message is decoded using the ClientLoginResponse class + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginErrEncDec) { + ClientLoginResponse mysql_loginerr_encode{}; + mysql_loginerr_encode.type(Err); + mysql_loginerr_encode.asErrMessage().setErrorCode(MYSQL_ERROR_CODE); + mysql_loginerr_encode.asErrMessage().setSqlStateMarker('#'); + mysql_loginerr_encode.asErrMessage().setSqlState(MySQLTestUtils::getSqlState()); + mysql_loginerr_encode.asErrMessage().setErrorMessage(MySQLTestUtils::getErrorMessage()); + Buffer::OwnedImpl decode_data; + mysql_loginerr_encode.encode(decode_data); + + ClientLoginResponse mysql_loginok_decode{}; + mysql_loginok_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginok_decode, mysql_loginerr_encode); +} + +/* + * Test the MYSQL Server Login Old Auth Switch message parser: + * - message is encoded using the ClientLoginResponse class + * - message is decoded using the ClientLoginResponse class + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginOldAuthSwitch) { + ClientLoginResponse mysql_old_auth_switch_encode{}; + mysql_old_auth_switch_encode.type(AuthSwitch); + Buffer::OwnedImpl decode_data; + mysql_old_auth_switch_encode.encode(decode_data); + + ClientLoginResponse mysql_old_auth_switch_decode{}; + mysql_old_auth_switch_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_old_auth_switch_decode, mysql_old_auth_switch_encode); +} + +/* + * Test the MYSQL Server Login Auth Switch message parser: + * - message is encoded using the ClientLoginResponse class + * - message is decoded using the ClientLoginResponse class + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginAuthSwitch) { + ClientLoginResponse mysql_auth_switch_encode{}; + mysql_auth_switch_encode.type(AuthSwitch); + mysql_auth_switch_encode.asAuthSwitchMessage().setAuthPluginName( + MySQLTestUtils::getAuthPluginName()); + mysql_auth_switch_encode.asAuthSwitchMessage().setAuthPluginData( + MySQLTestUtils::getAuthPluginData20()); + Buffer::OwnedImpl decode_data; + mysql_auth_switch_encode.encode(decode_data); + + ClientLoginResponse mysql_auth_switch_decode{}; + mysql_auth_switch_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_auth_switch_decode, mysql_auth_switch_encode); +} + +/* + * Test the MYSQL Server Login Auth More message parser: + * - message is encoded using the ClientLoginResponse class + * - message is decoded using the ClientLoginResponse class + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginAuthMore) { + ClientLoginResponse mysql_auth_more_encode{}; + mysql_auth_more_encode.type(AuthMoreData); + mysql_auth_more_encode.asAuthMoreMessage().setAuthMoreData(MySQLTestUtils::getAuthPluginData20()); + Buffer::OwnedImpl decode_data; + mysql_auth_more_encode.encode(decode_data); + + ClientLoginResponse mysql_auth_more_decode{}; + mysql_auth_more_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_auth_more_decode, mysql_auth_more_encode); +} + +/* + * Negative Test the MYSQL Server Login OK message parser: + * - incomplete response code + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginOkIncompleteRespCode) { + ClientLoginResponse mysql_loginok_encode{}; + mysql_loginok_encode.type(Ok); + Buffer::OwnedImpl decode_data; + + ClientLoginResponse mysql_loginok_decode{}; + mysql_loginok_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginok_decode.type(), Null); +} + +/* + * Negative Test the MYSQL Server Login OK message parser: + * - incomplete affected rows + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginOkIncompleteAffectedRows) { + ClientLoginResponse mysql_loginok_encode{}; + mysql_loginok_encode.type(Ok); + mysql_loginok_encode.asOkMessage().setAffectedRows(1); + Buffer::OwnedImpl buffer; + mysql_loginok_encode.encode(buffer); + + int incomplete_len = sizeof(uint8_t); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_loginok_decode{}; + mysql_loginok_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginok_decode.type(), mysql_loginok_encode.type()); +} + +/* + * Negative Test the MYSQL Server Login OK message parser: + * - incomplete Client Login OK last insert id + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginOkIncompleteLastInsertId) { + ClientLoginResponse mysql_loginok_encode{}; + mysql_loginok_encode.type(Ok); + mysql_loginok_encode.asOkMessage().setAffectedRows(1); + mysql_loginok_encode.asOkMessage().setLastInsertId(MYSQL_UT_LAST_ID); + Buffer::OwnedImpl buffer; + mysql_loginok_encode.encode(buffer); + + int incomplete_len = sizeof(uint8_t) + MySQLTestUtils::sizeOfLengthEncodeInteger( + mysql_loginok_encode.asOkMessage().getAffectedRows()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_loginok_decode{}; + mysql_loginok_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginok_decode.type(), mysql_loginok_encode.type()); + EXPECT_EQ(mysql_loginok_decode.asOkMessage().getAffectedRows(), + mysql_loginok_encode.asOkMessage().getAffectedRows()); +} + +/* + * Negative Test the MYSQL Server Login OK message parser: + * - incomplete server status + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginOkIncompleteServerStatus) { + ClientLoginResponse mysql_loginok_encode{}; + mysql_loginok_encode.type(Ok); + mysql_loginok_encode.asOkMessage().setAffectedRows(1); + mysql_loginok_encode.asOkMessage().setLastInsertId(MYSQL_UT_LAST_ID); + mysql_loginok_encode.asOkMessage().setServerStatus(MYSQL_UT_SERVER_OK); + Buffer::OwnedImpl buffer; + mysql_loginok_encode.encode(buffer); + + int incomplete_len = sizeof(uint8_t) + + MySQLTestUtils::sizeOfLengthEncodeInteger( + mysql_loginok_encode.asOkMessage().getAffectedRows()) + + MySQLTestUtils::sizeOfLengthEncodeInteger( + mysql_loginok_encode.asOkMessage().getLastInsertId()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_loginok_decode{}; + mysql_loginok_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginok_decode.type(), mysql_loginok_encode.type()); + EXPECT_EQ(mysql_loginok_decode.asOkMessage().getAffectedRows(), + mysql_loginok_encode.asOkMessage().getAffectedRows()); + EXPECT_EQ(mysql_loginok_decode.asOkMessage().getLastInsertId(), + mysql_loginok_encode.asOkMessage().getLastInsertId()); + EXPECT_EQ(mysql_loginok_decode.asOkMessage().getServerStatus(), 0); +} + +/* + * Negative Test the MYSQL Server Login OK message parser: + * - incomplete warnings + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginOkIncompleteWarnings) { + ClientLoginResponse mysql_loginok_encode{}; + mysql_loginok_encode.type(Ok); + mysql_loginok_encode.asOkMessage().setAffectedRows(1); + mysql_loginok_encode.asOkMessage().setLastInsertId(MYSQL_UT_LAST_ID); + mysql_loginok_encode.asOkMessage().setServerStatus(MYSQL_UT_SERVER_OK); + mysql_loginok_encode.asOkMessage().setWarnings(MYSQL_UT_SERVER_WARNINGS); + Buffer::OwnedImpl buffer; + mysql_loginok_encode.encode(buffer); + + int incomplete_len = sizeof(uint8_t) + + MySQLTestUtils::sizeOfLengthEncodeInteger( + mysql_loginok_encode.asOkMessage().getAffectedRows()) + + MySQLTestUtils::sizeOfLengthEncodeInteger( + mysql_loginok_encode.asOkMessage().getLastInsertId()) + + sizeof(mysql_loginok_encode.asOkMessage().getServerStatus()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_loginok_decode{}; + mysql_loginok_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginok_decode.type(), mysql_loginok_encode.type()); + EXPECT_EQ(mysql_loginok_decode.asOkMessage().getAffectedRows(), + mysql_loginok_encode.asOkMessage().getAffectedRows()); + EXPECT_EQ(mysql_loginok_decode.asOkMessage().getLastInsertId(), + mysql_loginok_encode.asOkMessage().getLastInsertId()); + EXPECT_EQ(mysql_loginok_decode.asOkMessage().getServerStatus(), + mysql_loginok_encode.asOkMessage().getServerStatus()); + EXPECT_EQ(mysql_loginok_decode.asOkMessage().getWarnings(), 0); +} + +/* + * Negative Test the MYSQL Server Login Err message parser: + * - incomplete response code + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginErrIncompleteRespCode) { + ClientLoginResponse mysql_loginerr_encode{}; + mysql_loginerr_encode.type(Err); + Buffer::OwnedImpl decode_data; + + ClientLoginResponse mysql_loginerr_decode{}; + mysql_loginerr_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginerr_decode.type(), Null); +} + +/* + * Negative Test the MYSQL Server Login ERR message parser: + * - incomplete error code + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginErrIncompleteErrorcode) { + ClientLoginResponse mysql_loginerr_encode{}; + mysql_loginerr_encode.type(Err); + mysql_loginerr_encode.asErrMessage().setErrorCode(MYSQL_ERROR_CODE); + Buffer::OwnedImpl buffer; + mysql_loginerr_encode.encode(buffer); + + int incomplete_len = sizeof(uint8_t); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_loginerr_decode{}; + mysql_loginerr_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginerr_decode.type(), mysql_loginerr_encode.type()); + EXPECT_EQ(mysql_loginerr_decode.asErrMessage().getErrorCode(), 0); +} + +/* + * Negative Test the MYSQL Server Login ERR message parser: + * - incomplete sql state marker + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginErrIncompleteStateMarker) { + ClientLoginResponse mysql_loginerr_encode{}; + mysql_loginerr_encode.type(Err); + mysql_loginerr_encode.asErrMessage().setErrorCode(MYSQL_ERROR_CODE); + mysql_loginerr_encode.asErrMessage().setSqlStateMarker('#'); + Buffer::OwnedImpl buffer; + mysql_loginerr_encode.encode(buffer); + + int incomplete_len = + sizeof(uint8_t) + sizeof(mysql_loginerr_encode.asErrMessage().getErrorCode()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_loginerr_decode{}; + mysql_loginerr_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginerr_decode.type(), mysql_loginerr_encode.type()); + EXPECT_EQ(mysql_loginerr_decode.asErrMessage().getErrorCode(), + mysql_loginerr_encode.asErrMessage().getErrorCode()); + EXPECT_EQ(mysql_loginerr_decode.asErrMessage().getSqlStateMarker(), 0); +} + +/* + * Negative Test the MYSQL Server Login ERR message parser: + * - incomplete sql state + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginErrIncompleteSqlState) { + ClientLoginResponse mysql_loginerr_encode{}; + mysql_loginerr_encode.type(Err); + mysql_loginerr_encode.asErrMessage().setErrorCode(MYSQL_ERROR_CODE); + mysql_loginerr_encode.asErrMessage().setSqlStateMarker('#'); + mysql_loginerr_encode.asErrMessage().setSqlState(MySQLTestUtils::getSqlState()); + Buffer::OwnedImpl buffer; + mysql_loginerr_encode.encode(buffer); + + int incomplete_len = sizeof(uint8_t) + + sizeof(mysql_loginerr_encode.asErrMessage().getErrorCode()) + + sizeof(mysql_loginerr_encode.asErrMessage().getSqlStateMarker()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_loginerr_decode{}; + mysql_loginerr_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginerr_decode.type(), mysql_loginerr_encode.type()); + EXPECT_EQ(mysql_loginerr_decode.asErrMessage().getErrorCode(), + mysql_loginerr_encode.asErrMessage().getErrorCode()); + EXPECT_EQ(mysql_loginerr_decode.asErrMessage().getSqlStateMarker(), + mysql_loginerr_encode.asErrMessage().getSqlStateMarker()); + EXPECT_EQ(mysql_loginerr_decode.asErrMessage().getSqlState(), ""); +} + +/* + * Negative Test the MYSQL Server Login ERR message parser: + * - incomplete error message + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginErrIncompleteErrorMessage) { + ClientLoginResponse mysql_loginerr_encode{}; + mysql_loginerr_encode.type(Err); + mysql_loginerr_encode.asErrMessage().setErrorCode(MYSQL_ERROR_CODE); + mysql_loginerr_encode.asErrMessage().setSqlStateMarker('#'); + mysql_loginerr_encode.asErrMessage().setSqlState(MySQLTestUtils::getSqlState()); + Buffer::OwnedImpl buffer; + mysql_loginerr_encode.asErrMessage().setErrorMessage(MySQLTestUtils::getErrorMessage()); + mysql_loginerr_encode.encode(buffer); + + int incomplete_len = sizeof(uint8_t) + + sizeof(mysql_loginerr_encode.asErrMessage().getErrorCode()) + + sizeof(mysql_loginerr_encode.asErrMessage().getSqlStateMarker()) + + mysql_loginerr_encode.asErrMessage().getSqlState().size(); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_loginerr_decode{}; + mysql_loginerr_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_loginerr_decode.type(), mysql_loginerr_encode.type()); + EXPECT_EQ(mysql_loginerr_decode.asErrMessage().getErrorCode(), + mysql_loginerr_encode.asErrMessage().getErrorCode()); + EXPECT_EQ(mysql_loginerr_decode.asErrMessage().getSqlStateMarker(), + mysql_loginerr_encode.asErrMessage().getSqlStateMarker()); + EXPECT_EQ(mysql_loginerr_decode.asErrMessage().getSqlState(), + mysql_loginerr_encode.asErrMessage().getSqlState()); + EXPECT_EQ(mysql_loginerr_decode.asErrMessage().getErrorMessage(), ""); +} + +/* + * Negative Test the MYSQL Server Login Auth Switch message parser: + * - incomplete response code + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginAuthSwitchIncompleteRespCode) { + ClientLoginResponse mysql_login_auth_switch_encode{}; + mysql_login_auth_switch_encode.type(AuthSwitch); + Buffer::OwnedImpl decode_data; + + ClientLoginResponse mysql_login_auth_switch_decode{}; + mysql_login_auth_switch_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_login_auth_switch_decode.type(), Null); +} + +/* + * Negative Test the MYSQL Server Login ERR message parser: + * - incomplete auth plugin name + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginAuthSwitchIncompletePluginName) { + ClientLoginResponse mysql_login_auth_switch_encode{}; + mysql_login_auth_switch_encode.type(AuthSwitch); + mysql_login_auth_switch_encode.asAuthSwitchMessage().setAuthPluginName( + MySQLTestUtils::getAuthPluginName()); + Buffer::OwnedImpl buffer; + mysql_login_auth_switch_encode.encode(buffer); + + int incomplete_len = sizeof(uint8_t); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_login_auth_switch_decode{}; + mysql_login_auth_switch_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_login_auth_switch_decode.type(), mysql_login_auth_switch_encode.type()); + EXPECT_EQ(mysql_login_auth_switch_decode.asAuthSwitchMessage().getAuthPluginName(), ""); +} + +/* + * Negative Test the MYSQL Server Login Auth Switch message parser: + * - incomplete auth plugin data + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginAuthSwitchIncompletePluginData) { + ClientLoginResponse mysql_login_auth_switch_encode{}; + mysql_login_auth_switch_encode.type(AuthSwitch); + mysql_login_auth_switch_encode.asAuthSwitchMessage().setAuthPluginName( + MySQLTestUtils::getAuthPluginName()); + mysql_login_auth_switch_encode.asAuthSwitchMessage().setAuthPluginData( + MySQLTestUtils::getAuthPluginData20()); + Buffer::OwnedImpl buffer; + mysql_login_auth_switch_encode.encode(buffer); + + int incomplete_len = + sizeof(uint8_t) + + mysql_login_auth_switch_encode.asAuthSwitchMessage().getAuthPluginName().size() + 1; + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_login_auth_switch_decode{}; + mysql_login_auth_switch_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_login_auth_switch_decode.type(), mysql_login_auth_switch_encode.type()); + EXPECT_EQ(mysql_login_auth_switch_decode.asAuthSwitchMessage().getAuthPluginName(), + mysql_login_auth_switch_encode.asAuthSwitchMessage().getAuthPluginName()); + EXPECT_EQ(mysql_login_auth_switch_decode.asAuthSwitchMessage().getAuthPluginData(), ""); +} + +/* + * Negative Test the MYSQL Server Auth More message parser: + * - incomplete response code + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginAuthMoreIncompleteRespCode) { + ClientLoginResponse mysql_login_auth_more_encode{}; + mysql_login_auth_more_encode.type(AuthMoreData); + Buffer::OwnedImpl decode_data; + + ClientLoginResponse mysql_login_auth_more_decode{}; + mysql_login_auth_more_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_login_auth_more_decode.type(), Null); +} + +/* + * Negative Test the MYSQL Server Auth More message parser: + * - incomplete auth plugin name + */ +TEST_F(MySQLCLoginRespTest, MySQLLoginAuthMoreIncompletePluginData) { + ClientLoginResponse mysql_login_auth_more_encode{}; + mysql_login_auth_more_encode.type(AuthMoreData); + mysql_login_auth_more_encode.asAuthMoreMessage().setAuthMoreData( + MySQLTestUtils::getAuthPluginData20()); + Buffer::OwnedImpl buffer; + mysql_login_auth_more_encode.encode(buffer); + + int incomplete_len = sizeof(uint8_t); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLoginResponse mysql_login_auth_more_decode{}; + mysql_login_auth_more_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_login_auth_more_decode.type(), mysql_login_auth_more_encode.type()); + EXPECT_EQ(mysql_login_auth_more_decode.asAuthMoreMessage().getAuthMoreData(), ""); +} + +} // namespace MySQLProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/mysql_proxy/mysql_clogin_test.cc b/test/extensions/filters/network/mysql_proxy/mysql_clogin_test.cc new file mode 100644 index 000000000000..ff5ebe779b20 --- /dev/null +++ b/test/extensions/filters/network/mysql_proxy/mysql_clogin_test.cc @@ -0,0 +1,584 @@ +#include "common/buffer/buffer_impl.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_clogin.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_command.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_greeting.h" +#include "extensions/filters/network/mysql_proxy/mysql_utils.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include "mysql_test_utils.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace MySQLProxy { + +class MySQLCLoginTest : public testing::Test {}; + +/* + * Test the MYSQL Client Login 41 message parser: + * - message is encoded using the ClientLogin class + * - CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA set + * - message is decoded using the ClientLogin class + */ +TEST_F(MySQLCLoginTest, MySQLClLoginV41PluginAuthEncDec) { + ClientLogin mysql_clogin_encode{}; + uint32_t client_capab = 0; + client_capab |= (CLIENT_CONNECT_WITH_DB | CLIENT_PROTOCOL_41 | CLIENT_PLUGIN_AUTH); + mysql_clogin_encode.setClientCap(client_capab); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + std::string user("user1"); + mysql_clogin_encode.setUsername(user); + mysql_clogin_encode.setAuthResp(MySQLTestUtils::getAuthResp8()); + std::string db = "mysql_db"; + mysql_clogin_encode.setDb(db); + mysql_clogin_encode.setAuthPluginName(MySQLTestUtils::getAuthPluginName()); + + Buffer::OwnedImpl decode_data; + mysql_clogin_encode.encode(decode_data); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.isResponse41(), true); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getBaseClientCap(), mysql_clogin_encode.getBaseClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); + EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); + EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); + EXPECT_EQ(mysql_clogin_decode.getDb(), mysql_clogin_encode.getDb()); + EXPECT_EQ(mysql_clogin_decode.getAuthPluginName(), mysql_clogin_encode.getAuthPluginName()); +} + +/* + * Test the MYSQL Client Login 41 message parser: + * - message is encoded using the ClientLogin class + * - CLIENT_SECURE_CONNECTION set + * - message is decoded using the ClientLogin class + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41SecureConnEncDec) { + ClientLogin mysql_clogin_encode{}; + uint32_t client_capab = 0; + client_capab |= (CLIENT_CONNECT_WITH_DB | CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION); + mysql_clogin_encode.setClientCap(client_capab); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + std::string user("user1"); + mysql_clogin_encode.setUsername(user); + mysql_clogin_encode.setAuthResp(MySQLTestUtils::getAuthResp8()); + std::string db = "mysql_db"; + mysql_clogin_encode.setDb(db); + mysql_clogin_encode.setAuthPluginName(MySQLTestUtils::getAuthPluginName()); + Buffer::OwnedImpl decode_data; + mysql_clogin_encode.encode(decode_data); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.isResponse41(), true); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getBaseClientCap(), mysql_clogin_encode.getBaseClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); + EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); + EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); + EXPECT_EQ(mysql_clogin_decode.getDb(), mysql_clogin_encode.getDb()); + EXPECT_EQ(mysql_clogin_decode.getAuthPluginName(), ""); +} + +/* + * Test the MYSQL Client Login 41 message parser without CLIENT_CONNECT_WITH_DB: + * - message is encoded using the ClientLogin class + * - message is decoded using the ClientLogin class + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41EncDec) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + mysql_clogin_encode.setUsername("user"); + mysql_clogin_encode.setDb("mysql.db"); + mysql_clogin_encode.setAuthResp(MySQLTestUtils::getAuthResp8()); + mysql_clogin_encode.setAuthPluginName(MySQLTestUtils::getAuthPluginName()); + + Buffer::OwnedImpl decode_data; + mysql_clogin_encode.encode(decode_data); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.isResponse41(), true); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getBaseClientCap(), mysql_clogin_encode.getBaseClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); + EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); + EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); + + EXPECT_TRUE(mysql_clogin_decode.getAuthPluginName().empty()); + EXPECT_TRUE(mysql_clogin_decode.getDb().empty()); +} + +/* + * Test the MYSQL Client Login 320 message parser: + * - message is encoded using the ClientLogin class + * - message is decoded using the ClientLogin class + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin320EncDec) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(0); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + mysql_clogin_encode.setUsername("user"); + mysql_clogin_encode.setDb("mysql.db"); + mysql_clogin_encode.setAuthResp(MySQLTestUtils::getAuthResp8()); + mysql_clogin_encode.setAuthPluginName(MySQLTestUtils::getAuthPluginName()); + + Buffer::OwnedImpl decode_data; + mysql_clogin_encode.encode(decode_data); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.isResponse320(), true); + EXPECT_EQ(mysql_clogin_decode.getBaseClientCap(), mysql_clogin_encode.getBaseClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); + EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); + + EXPECT_EQ(mysql_clogin_decode.getAuthPluginName(), ""); + EXPECT_EQ(mysql_clogin_decode.getDb(), ""); + EXPECT_EQ(mysql_clogin_decode.getCharset(), 0); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), 0); +} + +TEST_F(MySQLCLoginTest, MySQLParseLengthEncodedInteger) { + { + // encode 2 byte value + Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); + uint64_t input_val = 5; + uint64_t output_val = 0; + BufferHelper::addUint8(*buffer, LENENCODINT_2BYTES); + BufferHelper::addUint16(*buffer, input_val); + EXPECT_EQ(BufferHelper::readLengthEncodedInteger(*buffer, output_val), MYSQL_SUCCESS); + EXPECT_EQ(input_val, output_val); + } + + { + // encode 3 byte value + Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); + uint64_t input_val = 5; + uint64_t output_val = 0; + BufferHelper::addUint8(*buffer, LENENCODINT_3BYTES); + BufferHelper::addUint16(*buffer, input_val); + BufferHelper::addUint8(*buffer, 0); + EXPECT_EQ(BufferHelper::readLengthEncodedInteger(*buffer, output_val), MYSQL_SUCCESS); + EXPECT_EQ(input_val, output_val); + } + + { + // encode 8 byte value + Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); + uint64_t input_val = 5; + uint64_t output_val = 0; + BufferHelper::addUint8(*buffer, LENENCODINT_8BYTES); + BufferHelper::addUint32(*buffer, input_val); + BufferHelper::addUint32(*buffer, 0); + EXPECT_EQ(BufferHelper::readLengthEncodedInteger(*buffer, output_val), MYSQL_SUCCESS); + EXPECT_EQ(input_val, output_val); + } + + { + // encode invalid length header + Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); + uint64_t input_val = 5; + uint64_t output_val = 0; + BufferHelper::addUint8(*buffer, 0xff); + BufferHelper::addUint32(*buffer, input_val); + EXPECT_EQ(BufferHelper::readLengthEncodedInteger(*buffer, output_val), MYSQL_FAILURE); + } + { + // encode and decode length header + uint64_t input_vals[4] = { + 5, + 251 + 5, + (1 << 16) + 5, + (1 << 24) + 5, + }; + for (uint64_t& input_val : input_vals) { + Buffer::OwnedImpl buffer; + uint64_t output_val = 0; + BufferHelper::addLengthEncodedInteger(buffer, input_val); + BufferHelper::readLengthEncodedInteger(buffer, output_val); + EXPECT_EQ(input_val, output_val); + } + } + { + // encode decode uint24 + Buffer::OwnedImpl buffer; + uint32_t val = 0xfffefd; + BufferHelper::addUint32(buffer, val); + uint32_t res = 0; + BufferHelper::readUint24(buffer, res); + EXPECT_EQ(val, res); + } +} + +/* + * Negative Test the MYSQL Client Login 41 message parser: + * Incomplete header at Client Capability + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41IncompleteClientCap) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41); + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int client_cap_len = sizeof(uint8_t); + Buffer::OwnedImpl decode_data(buffer.toString().data(), client_cap_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), 0); +} + +/* + * Negative Test the MYSQL Client Login 41 message parser: + * Incomplete header at Extended Client Capability + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41IncompleteExtClientCap) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41); + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = sizeof(uint16_t); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), 0); +} + +/* + * Negative Test the MYSQL Client Login 41 message parser: + * Incomplete header at Max Packet + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41IncompleteMaxPacket) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41 | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA); + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = sizeof(uint16_t) + sizeof(CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getBaseClientCap(), mysql_clogin_encode.getBaseClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), 0); +} + +/* + * Negative Test the MYSQL Client Login 41 message parser: + * Incomplete header at Charset + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41IncompleteCharset) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41 | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = + sizeof(uint16_t) + sizeof(CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) + sizeof(MYSQL_MAX_PACKET); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getCharset(), 0); +} + +/* + * Negative Test the MYSQL Client Login 41 message parser: + * Incomplete header at Unset bytes + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41IncompleteUnsetBytes) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41 | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + mysql_clogin_encode.setUsername("user1"); + + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = sizeof(uint16_t) + sizeof(CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) + + sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); +} + +/* + * Negative Test the MYSQL Client Login 41 message parser: + * Incomplete header at username + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41IncompleteUser) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41 | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + mysql_clogin_encode.setUsername("user1"); + + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = sizeof(uint16_t) + sizeof(CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) + + sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET) + UNSET_BYTES; + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); + EXPECT_EQ(mysql_clogin_decode.getUsername(), ""); +} + +/* + * Negative Test the MYSQL Client Login 41 message parser: + * Incomplete header at authlen + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41IncompleteAuthLen) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41 | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + std::string user("user1"); + mysql_clogin_encode.setUsername(user); + mysql_clogin_encode.setAuthResp(MySQLTestUtils::getAuthResp8()); + + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = sizeof(uint16_t) + sizeof(CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) + + sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET) + UNSET_BYTES + + user.length() + 1; + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); + EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); + EXPECT_EQ(mysql_clogin_decode.getAuthResp(), ""); +} + +/* + * Negative Test the MYSQL Client Login 41 message parser: + * Incomplete header at "authpasswd" + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41IncompleteAuthPasswd) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41 | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + std::string user("user1"); + mysql_clogin_encode.setUsername(user); + std::string passwd = MySQLTestUtils::getAuthPluginData8(); + mysql_clogin_encode.setAuthResp(passwd); + + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = sizeof(uint16_t) + sizeof(CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA) + + sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET) + UNSET_BYTES + + user.length() + 3; + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + ; + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); + EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); + EXPECT_EQ(mysql_clogin_decode.getAuthResp(), ""); +} + +/* + * Negative Test the MYSQL Client Login 41 message parser: + * Incomplete header at "db name" + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41IncompleteDbName) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41 | CLIENT_CONNECT_WITH_DB); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + std::string user("user1"); + mysql_clogin_encode.setUsername(user); + std::string passwd = MySQLTestUtils::getAuthPluginData8(); + mysql_clogin_encode.setAuthResp(passwd); + std::string db = "mysql.db"; + mysql_clogin_encode.setDb(db); + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = sizeof(uint16_t) + sizeof(CLIENT_PROTOCOL_41) + sizeof(MYSQL_MAX_PACKET) + + sizeof(MYSQL_CHARSET) + UNSET_BYTES + user.length() + 1 + passwd.size() + 1; + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); + EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); + EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); + EXPECT_EQ(mysql_clogin_decode.getDb(), ""); +} + +/* + * Negative Test the MYSQL Client Login 41 message parser: + * Incomplete header at "auth plugin name" + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin41IncompleteAuthPluginName) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_PROTOCOL_41 | CLIENT_CONNECT_WITH_DB | + CLIENT_PLUGIN_AUTH); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + std::string user("user1"); + mysql_clogin_encode.setUsername(user); + std::string passwd = MySQLTestUtils::getAuthPluginData8(); + mysql_clogin_encode.setAuthResp(passwd); + std::string db = "mysql.db"; + mysql_clogin_encode.setDb(db); + mysql_clogin_encode.setAuthPluginName(MySQLTestUtils::getAuthPluginName()); + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = sizeof(uint16_t) + sizeof(CLIENT_PROTOCOL_41) + sizeof(MYSQL_MAX_PACKET) + + sizeof(MYSQL_CHARSET) + UNSET_BYTES + user.length() + 1 + passwd.size() + 1 + + db.size() + 1; + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); + EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); + EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); + EXPECT_EQ(mysql_clogin_decode.getDb(), mysql_clogin_encode.getDb()); + EXPECT_EQ(mysql_clogin_decode.getAuthPluginName(), ""); +} + +/* + * Negative Test the MYSQL Client 320 login message parser: + * Incomplete header at cap + */ +TEST_F(MySQLCLoginTest, MySQLClient320LoginIncompleteClientCap) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_CONNECT_WITH_DB); + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = 0; + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), 0); +} + +/* + * Negative Test the MYSQL Client 320 login message parser: + * Incomplete auth len + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin320IncompleteMaxPacketSize) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(0); + mysql_clogin_encode.setExtendedClientCap(0); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = sizeof(uint16_t); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), 0); +} + +/* + * Negative Test the MYSQL Client login 320 message parser: + * Incomplete username + */ +TEST_F(MySQLCLoginTest, MySQLClientLogin320IncompleteUsername) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(0); + mysql_clogin_encode.setExtendedClientCap(0); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setUsername("user"); + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + + int incomplete_len = sizeof(uint16_t) + sizeof(uint16_t) + sizeof(uint8_t); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); + EXPECT_EQ(mysql_clogin_decode.getUsername(), ""); +} + +/* + * Test the MYSQL Client Login SSL message parser: + * - message is encoded using the ClientLogin class + * - message is decoded using the ClientLogin class + */ +TEST_F(MySQLCLoginTest, MySQLClientLoginSSLEncDec) { + ClientLogin mysql_clogin_encode{}; + mysql_clogin_encode.setClientCap(CLIENT_SSL | CLIENT_PROTOCOL_41 | CLIENT_PLUGIN_AUTH); + mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); + mysql_clogin_encode.setCharset(MYSQL_CHARSET); + Buffer::OwnedImpl decode_data; + mysql_clogin_encode.encode(decode_data); + + ClientLogin mysql_clogin_decode{}; + mysql_clogin_decode.decode(decode_data, CHALLENGE_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_clogin_decode.isSSLRequest(), true); + EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); + EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); +} + +} // namespace MySQLProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/mysql_proxy/mysql_codec_test.cc b/test/extensions/filters/network/mysql_proxy/mysql_codec_test.cc index 57105953727e..30589d4b211c 100644 --- a/test/extensions/filters/network/mysql_proxy/mysql_codec_test.cc +++ b/test/extensions/filters/network/mysql_proxy/mysql_codec_test.cc @@ -1,3 +1,4 @@ +#include "common/buffer/buffer_impl.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" #include "extensions/filters/network/mysql_proxy/mysql_codec_clogin.h" #include "extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h" @@ -7,6 +8,7 @@ #include "gmock/gmock.h" #include "gtest/gtest.h" +#include #include "mysql_test_utils.h" namespace Envoy { @@ -14,926 +16,10 @@ namespace Extensions { namespace NetworkFilters { namespace MySQLProxy { -constexpr int MYSQL_UT_RESP_OK = 0; -constexpr int MYSQL_UT_LAST_ID = 0; -constexpr int MYSQL_UT_SERVER_OK = 0; -constexpr int MYSQL_UT_SERVER_WARNINGS = 0x0001; - class MySQLCodecTest : public testing::Test {}; -TEST_F(MySQLCodecTest, MySQLServerChallengeV9EncDec) { - ServerGreeting mysql_greet_encode{}; - mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_9); - std::string ver(MySQLTestUtils::getVersion()); - mysql_greet_encode.setVersion(ver); - mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); - std::string salt(MySQLTestUtils::getSalt()); - mysql_greet_encode.setSalt(salt); - std::string data = mysql_greet_encode.encode(); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ServerGreeting mysql_greet_decode{}; - mysql_greet_decode.decode(*decode_data, GREETING_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_greet_decode.getSalt(), mysql_greet_encode.getSalt()); - EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); - EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); - EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); - EXPECT_EQ(mysql_greet_decode.getServerLanguage(), 0); - EXPECT_EQ(mysql_greet_decode.getServerStatus(), 0); - EXPECT_EQ(mysql_greet_decode.getExtServerCap(), 0); - EXPECT_EQ(mysql_greet_decode.getServerCap(), 0); -} - -/* - * Test the MYSQL Greeting message V10 parser: - * - message is encoded using the ServerGreeting class - * - message is decoded using the ServerGreeting class - */ -TEST_F(MySQLCodecTest, MySQLServerChallengeV10EncDec) { - ServerGreeting mysql_greet_encode{}; - mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); - std::string ver(MySQLTestUtils::getVersion()); - mysql_greet_encode.setVersion(ver); - mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); - std::string salt(MySQLTestUtils::getSalt()); - mysql_greet_encode.setSalt(salt); - mysql_greet_encode.setServerCap(MYSQL_SERVER_CAPAB); - mysql_greet_encode.setServerLanguage(MYSQL_SERVER_LANGUAGE); - mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); - mysql_greet_encode.setExtServerCap(MYSQL_SERVER_EXT_CAPAB); - std::string data = mysql_greet_encode.encode(); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ServerGreeting mysql_greet_decode{}; - mysql_greet_decode.decode(*decode_data, GREETING_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_greet_decode.getSalt(), mysql_greet_encode.getSalt()); - EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); - EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); - EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); - EXPECT_EQ(mysql_greet_decode.getServerLanguage(), mysql_greet_encode.getServerLanguage()); - EXPECT_EQ(mysql_greet_decode.getServerStatus(), mysql_greet_encode.getServerStatus()); - EXPECT_EQ(mysql_greet_decode.getExtServerCap(), mysql_greet_encode.getExtServerCap()); - EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); -} - -/* - * Negative Testing: Server Greetings Incomplete - * - incomplete protocol - */ -TEST_F(MySQLCodecTest, MySQLServerChallengeIncompleteProtocol) { - ServerGreeting mysql_greet_encode{}; - std::string data = ""; - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ServerGreeting mysql_greet_decode{}; - mysql_greet_decode.decode(*decode_data, GREETING_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_greet_decode.getProtocol(), 0); -} - -/* - * Negative Testing: Server Greetings Incomplete - * - incomplete version - */ -TEST_F(MySQLCodecTest, MySQLServerChallengeIncompleteVersion) { - ServerGreeting mysql_greet_encode{}; - mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_9); - std::string data = mysql_greet_encode.encode(); - int incomplete_size = sizeof(MYSQL_PROTOCOL_9); - data = data.substr(0, incomplete_size); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ServerGreeting mysql_greet_decode{}; - mysql_greet_decode.decode(*decode_data, GREETING_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_greet_decode.getVersion(), ""); - EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); -} - -/* - * Negative Testing: Server Greetings Incomplete - * - incomplete thread_id - */ -TEST_F(MySQLCodecTest, MySQLServerChallengeIncompleteThreadId) { - ServerGreeting mysql_greet_encode{}; - mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_9); - std::string ver(MySQLTestUtils::getVersion()); - mysql_greet_encode.setVersion(ver); - std::string data = mysql_greet_encode.encode(); - int incomplete_size = sizeof(MYSQL_PROTOCOL_9) + ver.length() + 1; - data = data.substr(0, incomplete_size); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ServerGreeting mysql_greet_decode{}; - mysql_greet_decode.decode(*decode_data, GREETING_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); - EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); - EXPECT_EQ(mysql_greet_decode.getThreadId(), 0); -} - -/* - * Negative Testing: Server Greetings Incomplete - * - incomplete salt - */ -TEST_F(MySQLCodecTest, MySQLServerChallengeIncompleteSalt) { - ServerGreeting mysql_greet_encode{}; - mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_9); - std::string ver(MySQLTestUtils::getVersion()); - mysql_greet_encode.setVersion(ver); - mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); - std::string data = mysql_greet_encode.encode(); - int incomplete_size = sizeof(MYSQL_PROTOCOL_9) + ver.length() + 1 + sizeof(MYSQL_THREAD_ID); - data = data.substr(0, incomplete_size); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ServerGreeting mysql_greet_decode{}; - mysql_greet_decode.decode(*decode_data, GREETING_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_greet_decode.getSalt(), ""); - EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); - EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); - EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); -} - -/* - * Negative Testing: Server Greetings Incomplete - * - incomplete Server Capabilities - */ -TEST_F(MySQLCodecTest, MySQLServerChallengeIncompleteServerCap) { - ServerGreeting mysql_greet_encode{}; - mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); - std::string ver(MySQLTestUtils::getVersion()); - mysql_greet_encode.setVersion(ver); - mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); - std::string salt(MySQLTestUtils::getSalt()); - mysql_greet_encode.setSalt(salt); - std::string data = mysql_greet_encode.encode(); - int incomplete_size = - sizeof(MYSQL_PROTOCOL_9) + ver.length() + 1 + sizeof(MYSQL_THREAD_ID) + salt.length() + 1; - data = data.substr(0, incomplete_size); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ServerGreeting mysql_greet_decode{}; - mysql_greet_decode.decode(*decode_data, GREETING_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_greet_decode.getSalt(), mysql_greet_encode.getSalt()); - EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); - EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); - EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); - EXPECT_EQ(mysql_greet_decode.getExtServerCap(), 0); -} - -/* - * Negative Testing: Server Greetings Incomplete - * - incomplete Server Status - */ -TEST_F(MySQLCodecTest, MySQLServerChallengeIncompleteServerStatus) { - ServerGreeting mysql_greet_encode{}; - mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); - std::string ver(MySQLTestUtils::getVersion()); - mysql_greet_encode.setVersion(ver); - mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); - std::string salt(MySQLTestUtils::getSalt()); - mysql_greet_encode.setSalt(salt); - mysql_greet_encode.setServerCap(MYSQL_SERVER_CAPAB); - mysql_greet_encode.setServerLanguage(MYSQL_SERVER_LANGUAGE); - std::string data = mysql_greet_encode.encode(); - int incomplete_size = sizeof(MYSQL_PROTOCOL_9) + ver.length() + 1 + sizeof(MYSQL_THREAD_ID) + - salt.length() + 1 + sizeof(MYSQL_SERVER_CAPAB) + - sizeof(MYSQL_SERVER_LANGUAGE); - data = data.substr(0, incomplete_size); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ServerGreeting mysql_greet_decode{}; - mysql_greet_decode.decode(*decode_data, GREETING_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_greet_decode.getSalt(), mysql_greet_encode.getSalt()); - EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); - EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); - EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); - EXPECT_EQ(mysql_greet_decode.getServerLanguage(), mysql_greet_encode.getServerLanguage()); - EXPECT_EQ(mysql_greet_decode.getServerStatus(), 0); - EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); -} - -/* - * Negative Testing: Server Greetings Incomplete - * - incomplete extended Server Capabilities - */ -TEST_F(MySQLCodecTest, MySQLServerChallengeIncompleteExtServerCap) { - ServerGreeting mysql_greet_encode{}; - mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); - std::string ver(MySQLTestUtils::getVersion()); - mysql_greet_encode.setVersion(ver); - mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); - std::string salt(MySQLTestUtils::getSalt()); - mysql_greet_encode.setSalt(salt); - mysql_greet_encode.setServerCap(MYSQL_SERVER_CAPAB); - mysql_greet_encode.setServerLanguage(MYSQL_SERVER_LANGUAGE); - mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); - std::string data = mysql_greet_encode.encode(); - int incomplete_size = sizeof(MYSQL_PROTOCOL_9) + ver.length() + 1 + sizeof(MYSQL_THREAD_ID) + - salt.length() + 1 + sizeof(MYSQL_SERVER_CAPAB) + - sizeof(MYSQL_SERVER_LANGUAGE) + sizeof(MYSQL_SERVER_STATUS); - data = data.substr(0, incomplete_size); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ServerGreeting mysql_greet_decode{}; - mysql_greet_decode.decode(*decode_data, GREETING_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_greet_decode.getSalt(), mysql_greet_encode.getSalt()); - EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); - EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); - EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); - EXPECT_EQ(mysql_greet_decode.getServerLanguage(), mysql_greet_encode.getServerLanguage()); - EXPECT_EQ(mysql_greet_decode.getServerStatus(), mysql_greet_encode.getServerStatus()); - EXPECT_EQ(mysql_greet_decode.getExtServerCap(), 0); - EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); -} - -/* - * Testing: Server Greetings Protocol 10 Server Capabilities only - */ -TEST_F(MySQLCodecTest, MySQLServerChallengeP10ServerCapOnly) { - ServerGreeting mysql_greet_encode{}; - mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); - std::string ver(MySQLTestUtils::getVersion()); - mysql_greet_encode.setVersion(ver); - mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); - std::string salt(MySQLTestUtils::getSalt()); - mysql_greet_encode.setSalt(salt); - mysql_greet_encode.setServerCap(MYSQL_SERVER_CAPAB); - std::string data = mysql_greet_encode.encode(); - int incomplete_size = sizeof(MYSQL_PROTOCOL_9) + ver.length() + 1 + sizeof(MYSQL_THREAD_ID) + - salt.length() + 1 + sizeof(MYSQL_SERVER_CAPAB); - data = data.substr(0, incomplete_size); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ServerGreeting mysql_greet_decode{}; - mysql_greet_decode.decode(*decode_data, GREETING_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_greet_decode.getSalt(), mysql_greet_encode.getSalt()); - EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); - EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); - EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); - EXPECT_EQ(mysql_greet_decode.getExtServerCap(), mysql_greet_encode.getExtServerCap()); -} - -/* - * Test the MYSQL Client Login 41 message parser: - * - message is encoded using the ClientLogin class - * - CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA set - * - message is decoded using the ClientLogin class - */ -TEST_F(MySQLCodecTest, MySQLClLoginV41PluginAuthEncDec) { - ClientLogin mysql_clogin_encode{}; - uint16_t client_capab = 0; - client_capab |= (MYSQL_CLIENT_CONNECT_WITH_DB | MYSQL_CLIENT_CAPAB_41VS320); - mysql_clogin_encode.setClientCap(client_capab); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_PLG_AUTH_CL_DATA); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - std::string db = "mysql_db"; - mysql_clogin_encode.setDb(db); - std::string data = mysql_clogin_encode.encode(); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.isResponse41(), true); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); - EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); - EXPECT_EQ(mysql_clogin_decode.getDb(), mysql_clogin_encode.getDb()); -} - -/* - * Test the MYSQL Client Login 41 message parser: - * - message is encoded using the ClientLogin class - * - CLIENT_SECURE_CONNECTION set - * - message is decoded using the ClientLogin class - */ -TEST_F(MySQLCodecTest, MySQLClientLogin41SecureConnEncDec) { - ClientLogin mysql_clogin_encode{}; - uint16_t client_capab = 0; - client_capab |= (MYSQL_CLIENT_CONNECT_WITH_DB | MYSQL_CLIENT_CAPAB_41VS320); - mysql_clogin_encode.setClientCap(client_capab); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_SECURE_CONNECTION); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - std::string db = "mysql_db"; - mysql_clogin_encode.setDb(db); - std::string data = mysql_clogin_encode.encode(); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.isResponse41(), true); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); - EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); - EXPECT_EQ(mysql_clogin_decode.getDb(), mysql_clogin_encode.getDb()); -} - -/* - * Test the MYSQL Client Login 41 message parser: - * - message is encoded using the ClientLogin class - * - message is decoded using the ClientLogin class - */ -TEST_F(MySQLCodecTest, MySQLClientLogin41EncDec) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(MYSQL_CLIENT_CAPAB_41VS320); - mysql_clogin_encode.setExtendedClientCap(0); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - std::string data = mysql_clogin_encode.encode(); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.isResponse41(), true); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); - EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); -} - -/* - * Test the MYSQL Client Login 320 message parser: - * - message is encoded using the ClientLogin class - * - message is decoded using the ClientLogin class - */ -TEST_F(MySQLCodecTest, MySQLClientLogin320EncDec) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_PLG_AUTH_CL_DATA); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - - std::string data = mysql_clogin_encode.encode(); - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.isResponse320(), true); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); - EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); -} - -TEST_F(MySQLCodecTest, MySQLParseLengthEncodedInteger) { - { - // encode 2 byte value - Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); - uint64_t input_val = 5; - uint64_t output_val = 0; - BufferHelper::addUint8(*buffer, LENENCODINT_2BYTES); - BufferHelper::addUint16(*buffer, input_val); - EXPECT_EQ(BufferHelper::readLengthEncodedInteger(*buffer, output_val), MYSQL_SUCCESS); - EXPECT_EQ(input_val, output_val); - } - - { - // encode 3 byte value - Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); - uint64_t input_val = 5; - uint64_t output_val = 0; - BufferHelper::addUint8(*buffer, LENENCODINT_3BYTES); - BufferHelper::addUint16(*buffer, input_val); - BufferHelper::addUint8(*buffer, 0); - EXPECT_EQ(BufferHelper::readLengthEncodedInteger(*buffer, output_val), MYSQL_SUCCESS); - EXPECT_EQ(input_val, output_val); - } - - { - // encode 8 byte value - Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); - uint64_t input_val = 5; - uint64_t output_val = 0; - BufferHelper::addUint8(*buffer, LENENCODINT_8BYTES); - BufferHelper::addUint32(*buffer, input_val); - BufferHelper::addUint32(*buffer, 0); - EXPECT_EQ(BufferHelper::readLengthEncodedInteger(*buffer, output_val), MYSQL_SUCCESS); - EXPECT_EQ(input_val, output_val); - } - - { - // encode invalid length header - Buffer::InstancePtr buffer(new Buffer::OwnedImpl()); - uint64_t input_val = 5; - uint64_t output_val = 0; - BufferHelper::addUint8(*buffer, 0xff); - BufferHelper::addUint32(*buffer, input_val); - EXPECT_EQ(BufferHelper::readLengthEncodedInteger(*buffer, output_val), MYSQL_FAILURE); - } -} - -/* - * Negative Test the MYSQL Client Login 320 message parser: - * Incomplete header at Client Capability - */ -TEST_F(MySQLCodecTest, MySQLClientLogin320IncompleteClientCap) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - std::string data = mysql_clogin_encode.encode(); - int client_cap_len = sizeof(uint8_t); - data = data.substr(0, client_cap_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), 0); -} - -/* - * Negative Test the MYSQL Client Login 320 message parser: - * Incomplete header at Extended Client Capability - */ -TEST_F(MySQLCodecTest, MySQLClientLogin320IncompleteExtClientCap) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = sizeof(uint16_t); - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), 0); -} - -/* - * Negative Test the MYSQL Client Login 320 message parser: - * Incomplete header at Max Packet - */ -TEST_F(MySQLCodecTest, MySQLClientLogin320IncompleteMaxPacket) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_PLG_AUTH_CL_DATA); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = sizeof(uint16_t) + sizeof(MYSQL_EXT_CL_PLG_AUTH_CL_DATA); - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), 0); -} - -/* - * Negative Test the MYSQL Client Login 320 message parser: - * Incomplete header at Charset - */ -TEST_F(MySQLCodecTest, MySQLClientLogin320IncompleteCharset) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_PLG_AUTH_CL_DATA); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = - sizeof(uint16_t) + sizeof(MYSQL_EXT_CL_PLG_AUTH_CL_DATA) + sizeof(MYSQL_MAX_PACKET); - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), 0); -} - -/* - * Negative Test the MYSQL Client Login 320 message parser: - * Incomplete header at Unset bytes - */ -TEST_F(MySQLCodecTest, MySQLClientLogin320IncompleteUnsetBytes) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_PLG_AUTH_CL_DATA); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = sizeof(uint16_t) + sizeof(MYSQL_EXT_CL_PLG_AUTH_CL_DATA) + - sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET); - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); -} - -/* - * Negative Test the MYSQL Client Login 320 message parser: - * Incomplete header at username - */ -TEST_F(MySQLCodecTest, MySQLClientLogin320IncompleteUser) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_PLG_AUTH_CL_DATA); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = sizeof(uint16_t) + sizeof(MYSQL_EXT_CL_PLG_AUTH_CL_DATA) + - sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET) + UNSET_BYTES; - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), ""); -} - -/* - * Negative Test the MYSQL Client Login 320 message parser: - * Incomplete header at authlen - */ -TEST_F(MySQLCodecTest, MySQLClientLogin320IncompleteAuthLen) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_PLG_AUTH_CL_DATA); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = sizeof(uint16_t) + sizeof(MYSQL_EXT_CL_PLG_AUTH_CL_DATA) + - sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET) + UNSET_BYTES + - user.length() + 1; - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); - EXPECT_EQ(mysql_clogin_decode.getAuthResp(), ""); -} - -/* - * Negative Test the MYSQL Client Login 320 message parser: - * Incomplete header at "authpasswd" - */ -TEST_F(MySQLCodecTest, MySQLClientLogin320IncompleteAuthPasswd) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_PLG_AUTH_CL_DATA); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = sizeof(uint16_t) + sizeof(MYSQL_EXT_CL_PLG_AUTH_CL_DATA) + - sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET) + UNSET_BYTES + - user.length() + 3; - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); - EXPECT_EQ(mysql_clogin_decode.getAuthResp(), ""); -} - -/* - * Negative Test the MYSQL Client SSL login message parser: - * Incomplete header at authlen - */ -TEST_F(MySQLCodecTest, MySQLClientSSLLoginIncompleteAuthLen) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_SECURE_CONNECTION); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = sizeof(uint16_t) + sizeof(MYSQL_EXT_CL_PLG_AUTH_CL_DATA) + - sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET) + UNSET_BYTES + - user.length() + 1; - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); - EXPECT_EQ(mysql_clogin_decode.getAuthResp(), ""); -} - -/* - * Negative Test the MYSQL Client SSL login message parser: - * Incomplete header at username - */ -TEST_F(MySQLCodecTest, MySQLClientSSLLoginIncompleteAuthPasswd) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_SECURE_CONNECTION); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = sizeof(uint16_t) + sizeof(MYSQL_EXT_CL_PLG_AUTH_CL_DATA) + - sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET) + UNSET_BYTES + - user.length() + 3; - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); - EXPECT_EQ(mysql_clogin_decode.getAuthResp(), ""); -} - -/* - * Negative Test the MYSQL Client login message parser: - * Incomplete auth len - */ -TEST_F(MySQLCodecTest, MySQLClientLoginIncompleteAuthPasswd) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(0); - mysql_clogin_encode.setExtendedClientCap(0); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = sizeof(uint16_t) + sizeof(MYSQL_EXT_CL_PLG_AUTH_CL_DATA) + - sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET) + UNSET_BYTES + - user.length() + 3; - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); - EXPECT_EQ(mysql_clogin_decode.getAuthResp(), ""); -} - -/* - * Negative Test the MYSQL Client login message parser: - * Incomplete auth len - */ -TEST_F(MySQLCodecTest, MySQLClientLoginIncompleteConnectDb) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(MYSQL_CLIENT_CONNECT_WITH_DB); - mysql_clogin_encode.setExtendedClientCap(0); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - std::string data = mysql_clogin_encode.encode(); - int incomplete_len = sizeof(uint16_t) + sizeof(MYSQL_EXT_CL_PLG_AUTH_CL_DATA) + - sizeof(MYSQL_MAX_PACKET) + sizeof(MYSQL_CHARSET) + UNSET_BYTES + - user.length() + 3 + user.length() + 2; - data = data.substr(0, incomplete_len); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); - EXPECT_EQ(mysql_clogin_decode.getCharset(), mysql_clogin_encode.getCharset()); - EXPECT_EQ(mysql_clogin_decode.getUsername(), mysql_clogin_encode.getUsername()); - EXPECT_EQ(mysql_clogin_decode.getAuthResp(), mysql_clogin_encode.getAuthResp()); -} - -/* - * Test the MYSQL Client Login SSL message parser: - * - message is encoded using the ClientLogin class - * - message is decoded using the ClientLogin class - */ -TEST_F(MySQLCodecTest, MySQLClientLoginSSLEncDec) { - ClientLogin mysql_clogin_encode{}; - mysql_clogin_encode.setClientCap(MYSQL_CLIENT_CAPAB_SSL | MYSQL_CLIENT_CAPAB_41VS320); - mysql_clogin_encode.setExtendedClientCap(MYSQL_EXT_CL_PLG_AUTH_CL_DATA); - mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); - mysql_clogin_encode.setCharset(MYSQL_CHARSET); - std::string user("user1"); - mysql_clogin_encode.setUsername(user); - std::string passwd = MySQLTestUtils::getAuthResp(); - mysql_clogin_encode.setAuthResp(passwd); - std::string data = mysql_clogin_encode.encode(); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLogin mysql_clogin_decode{}; - mysql_clogin_decode.decode(*decode_data, CHALLENGE_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_clogin_decode.isSSLRequest(), true); - EXPECT_EQ(mysql_clogin_decode.getClientCap(), mysql_clogin_encode.getClientCap()); - EXPECT_EQ(mysql_clogin_decode.getExtendedClientCap(), mysql_clogin_encode.getExtendedClientCap()); - EXPECT_EQ(mysql_clogin_decode.getMaxPacket(), mysql_clogin_encode.getMaxPacket()); -} - -/* - * Test the MYSQL Server Login OK message parser: - * - message is encoded using the ClientLoginResponse class - * - message is decoded using the ClientLoginResponse class - */ -TEST_F(MySQLCodecTest, MySQLLoginOkEncDec) { - ClientLoginResponse mysql_loginok_encode{}; - mysql_loginok_encode.setRespCode(MYSQL_UT_RESP_OK); - mysql_loginok_encode.setAffectedRows(1); - mysql_loginok_encode.setLastInsertId(MYSQL_UT_LAST_ID); - mysql_loginok_encode.setServerStatus(MYSQL_UT_SERVER_OK); - mysql_loginok_encode.setWarnings(MYSQL_UT_SERVER_WARNINGS); - std::string data = mysql_loginok_encode.encode(); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLoginResponse mysql_loginok_decode{}; - mysql_loginok_decode.decode(*decode_data, CHALLENGE_RESP_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_loginok_decode.getRespCode(), mysql_loginok_encode.getRespCode()); - EXPECT_EQ(mysql_loginok_decode.getAffectedRows(), mysql_loginok_encode.getAffectedRows()); - EXPECT_EQ(mysql_loginok_decode.getLastInsertId(), mysql_loginok_encode.getLastInsertId()); - EXPECT_EQ(mysql_loginok_decode.getServerStatus(), mysql_loginok_encode.getServerStatus()); - EXPECT_EQ(mysql_loginok_decode.getWarnings(), mysql_loginok_encode.getWarnings()); -} - -/* - * Test the MYSQL Server Login Old Auth Switch message parser: - * - message is encoded using the ClientLoginResponse class - * - message is decoded using the ClientLoginResponse class - */ -TEST_F(MySQLCodecTest, MySQLLoginOldAuthSwitch) { - ClientLoginResponse mysql_loginok_encode{}; - mysql_loginok_encode.setRespCode(MYSQL_RESP_AUTH_SWITCH); - std::string data = mysql_loginok_encode.encode(); - data = data.substr(0, 1); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLoginResponse mysql_loginok_decode{}; - mysql_loginok_decode.decode(*decode_data, CHALLENGE_RESP_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_loginok_decode.getRespCode(), mysql_loginok_encode.getRespCode()); -} - -/* - * Negative Test the MYSQL Server Login OK message parser: - * - incomplete Client Login OK response - */ -TEST_F(MySQLCodecTest, MySQLLoginOkIncompleteRespCode) { - ClientLoginResponse mysql_loginok_encode{}; - mysql_loginok_encode.setRespCode(MYSQL_UT_RESP_OK); - std::string data; - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLoginResponse mysql_loginok_decode{}; - mysql_loginok_decode.decode(*decode_data, CHALLENGE_RESP_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_loginok_decode.getRespCode(), 0); -} - -/* - * Negative Test the MYSQL Server Login OK message parser: - * - incomplete Client Login OK affected rows - */ -TEST_F(MySQLCodecTest, MySQLLoginOkIncompleteAffectedRows) { - ClientLoginResponse mysql_loginok_encode{}; - mysql_loginok_encode.setRespCode(MYSQL_UT_RESP_OK); - mysql_loginok_encode.setAffectedRows(1); - std::string data = mysql_loginok_encode.encode(); - data = data.substr(0, 1); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLoginResponse mysql_loginok_decode{}; - mysql_loginok_decode.decode(*decode_data, CHALLENGE_RESP_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_loginok_decode.getRespCode(), mysql_loginok_encode.getRespCode()); -} - -/* - * Negative Test the MYSQL Server Login OK message parser: - * - incomplete Client Login OK last insert id - */ -TEST_F(MySQLCodecTest, MySQLLoginOkIncompleteLastInsertId) { - ClientLoginResponse mysql_loginok_encode{}; - mysql_loginok_encode.setRespCode(MYSQL_UT_RESP_OK); - mysql_loginok_encode.setAffectedRows(1); - mysql_loginok_encode.setLastInsertId(MYSQL_UT_LAST_ID); - std::string data = mysql_loginok_encode.encode(); - data = data.substr(0, 2); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLoginResponse mysql_loginok_decode{}; - mysql_loginok_decode.decode(*decode_data, CHALLENGE_RESP_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_loginok_decode.getRespCode(), mysql_loginok_encode.getRespCode()); - EXPECT_EQ(mysql_loginok_decode.getAffectedRows(), mysql_loginok_encode.getAffectedRows()); -} - -/* - * Negative Test the MYSQL Server Login OK message parser: - * - incomplete Client Login OK server status - */ -TEST_F(MySQLCodecTest, MySQLLoginOkIncompleteServerStatus) { - ClientLoginResponse mysql_loginok_encode{}; - mysql_loginok_encode.setRespCode(MYSQL_UT_RESP_OK); - mysql_loginok_encode.setAffectedRows(1); - mysql_loginok_encode.setLastInsertId(MYSQL_UT_LAST_ID); - mysql_loginok_encode.setServerStatus(MYSQL_UT_SERVER_OK); - std::string data = mysql_loginok_encode.encode(); - data = data.substr(0, 3); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLoginResponse mysql_loginok_decode{}; - mysql_loginok_decode.decode(*decode_data, CHALLENGE_RESP_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_loginok_decode.getRespCode(), mysql_loginok_encode.getRespCode()); - EXPECT_EQ(mysql_loginok_decode.getAffectedRows(), mysql_loginok_encode.getAffectedRows()); - EXPECT_EQ(mysql_loginok_decode.getLastInsertId(), mysql_loginok_encode.getLastInsertId()); - EXPECT_EQ(mysql_loginok_decode.getServerStatus(), 0); -} - -/* - * Negative Test the MYSQL Server Login OK message parser: - * - incomplete Client Login OK warnings - */ -TEST_F(MySQLCodecTest, MySQLLoginOkIncompleteWarnings) { - ClientLoginResponse mysql_loginok_encode{}; - mysql_loginok_encode.setRespCode(MYSQL_UT_RESP_OK); - mysql_loginok_encode.setAffectedRows(1); - mysql_loginok_encode.setLastInsertId(MYSQL_UT_LAST_ID); - mysql_loginok_encode.setServerStatus(MYSQL_UT_SERVER_OK); - mysql_loginok_encode.setWarnings(MYSQL_UT_SERVER_WARNINGS); - std::string data = mysql_loginok_encode.encode(); - data = data.substr(0, 5); - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); - ClientLoginResponse mysql_loginok_decode{}; - mysql_loginok_decode.decode(*decode_data, CHALLENGE_RESP_SEQ_NUM, decode_data->length()); - EXPECT_EQ(mysql_loginok_decode.getRespCode(), mysql_loginok_encode.getRespCode()); - EXPECT_EQ(mysql_loginok_decode.getAffectedRows(), mysql_loginok_encode.getAffectedRows()); - EXPECT_EQ(mysql_loginok_decode.getLastInsertId(), mysql_loginok_encode.getLastInsertId()); - EXPECT_EQ(mysql_loginok_decode.getServerStatus(), mysql_loginok_encode.getServerStatus()); - EXPECT_EQ(mysql_loginok_decode.getWarnings(), 0); -} - TEST_F(MySQLCodecTest, MySQLCommandError) { - Command mysql_cmd_encode{}; - std::string data = mysql_cmd_encode.encode(); - data = ""; - - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(data)); + Buffer::InstancePtr decode_data(new Buffer::OwnedImpl("")); Command mysql_cmd_decode{}; decode_data->drain(4); mysql_cmd_decode.decode(*decode_data, 0, 0); @@ -945,14 +31,14 @@ TEST_F(MySQLCodecTest, MySQLCommandInitDb) { mysql_cmd_encode.setCmd(Command::Cmd::InitDb); std::string db = "mysqlDB"; mysql_cmd_encode.setData(db); - std::string data = mysql_cmd_encode.encode(); + Buffer::OwnedImpl decode_data; + mysql_cmd_encode.encode(decode_data); - std::string mysql_msg = BufferHelper::encodeHdr(data, 0); + BufferHelper::encodeHdr(decode_data, 0); - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(mysql_msg)); Command mysql_cmd_decode{}; - decode_data->drain(4); - mysql_cmd_decode.decode(*decode_data, 0, db.length() + 1); + decode_data.drain(4); + mysql_cmd_decode.decode(decode_data, 0, db.length() + 1); EXPECT_EQ(mysql_cmd_decode.getDb(), db); } @@ -961,14 +47,14 @@ TEST_F(MySQLCodecTest, MySQLCommandCreateDb) { mysql_cmd_encode.setCmd(Command::Cmd::CreateDb); std::string db = "mysqlDB"; mysql_cmd_encode.setData(db); - std::string data = mysql_cmd_encode.encode(); + Buffer::OwnedImpl decode_data; + mysql_cmd_encode.encode(decode_data); - std::string mysql_msg = BufferHelper::encodeHdr(data, 0); + BufferHelper::encodeHdr(decode_data, 0); - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(mysql_msg)); Command mysql_cmd_decode{}; - decode_data->drain(4); - mysql_cmd_decode.decode(*decode_data, 0, db.length() + 1); + decode_data.drain(4); + mysql_cmd_decode.decode(decode_data, 0, db.length() + 1); EXPECT_EQ(mysql_cmd_decode.getDb(), db); } @@ -977,28 +63,28 @@ TEST_F(MySQLCodecTest, MySQLCommandDropDb) { mysql_cmd_encode.setCmd(Command::Cmd::DropDb); std::string db = "mysqlDB"; mysql_cmd_encode.setData(db); - std::string data = mysql_cmd_encode.encode(); + Buffer::OwnedImpl decode_data; + mysql_cmd_encode.encode(decode_data); - std::string mysql_msg = BufferHelper::encodeHdr(data, 0); + BufferHelper::encodeHdr(decode_data, 0); - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(mysql_msg)); Command mysql_cmd_decode{}; - decode_data->drain(4); - mysql_cmd_decode.decode(*decode_data, 0, db.length() + 1); + decode_data.drain(4); + mysql_cmd_decode.decode(decode_data, 0, db.length() + 1); EXPECT_EQ(mysql_cmd_decode.getDb(), db); } TEST_F(MySQLCodecTest, MySQLCommandOther) { Command mysql_cmd_encode{}; mysql_cmd_encode.setCmd(Command::Cmd::FieldList); - std::string data = mysql_cmd_encode.encode(); + Buffer::OwnedImpl decode_data; + mysql_cmd_encode.encode(decode_data); - std::string mysql_msg = BufferHelper::encodeHdr(data, 0); + BufferHelper::encodeHdr(decode_data, 0); - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(mysql_msg)); Command mysql_cmd_decode{}; - decode_data->drain(4); - mysql_cmd_decode.decode(*decode_data, 0, 0); + decode_data.drain(4); + mysql_cmd_decode.decode(decode_data, 0, 0); EXPECT_EQ(mysql_cmd_decode.getCmd(), Command::Cmd::FieldList); } diff --git a/test/extensions/filters/network/mysql_proxy/mysql_command_test.cc b/test/extensions/filters/network/mysql_proxy/mysql_command_test.cc index 55cacdc2ab53..11a633688b67 100644 --- a/test/extensions/filters/network/mysql_proxy/mysql_command_test.cc +++ b/test/extensions/filters/network/mysql_proxy/mysql_command_test.cc @@ -1,5 +1,6 @@ #include +#include "common/buffer/buffer_impl.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" #include "extensions/filters/network/mysql_proxy/mysql_codec_clogin.h" #include "extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h" @@ -26,15 +27,16 @@ class MySQLCommandTest : public testing::Test, public MySQLTestUtils { uint32_t len = 0u; mysql_cmd_encode.setCmd(Command::Cmd::Query); mysql_cmd_encode.setData(query); - std::string data = mysql_cmd_encode.encode(); - std::string mysql_msg = BufferHelper::encodeHdr(data, 0); - Buffer::InstancePtr decode_data(new Buffer::OwnedImpl(mysql_msg)); - if (BufferHelper::peekHdr(*decode_data, len, seq) != MYSQL_SUCCESS) { + Buffer::OwnedImpl decode_data; + mysql_cmd_encode.encode(decode_data); + BufferHelper::encodeHdr(decode_data, 0); + + if (BufferHelper::peekHdr(decode_data, len, seq) != MYSQL_SUCCESS) { return MYSQL_FAILURE; } - BufferHelper::consumeHdr(*decode_data); - if (mysql_cmd_decode.decode(*decode_data, seq, len) != MYSQL_SUCCESS) { + BufferHelper::consumeHdr(decode_data); + if (mysql_cmd_decode.decode(decode_data, seq, len) != MYSQL_SUCCESS) { return MYSQL_FAILURE; } hsql::SQLParser::parse(mysql_cmd_decode.getData(), &result); diff --git a/test/extensions/filters/network/mysql_proxy/mysql_filter_test.cc b/test/extensions/filters/network/mysql_proxy/mysql_filter_test.cc index 968db699f72c..8b739733cd3a 100644 --- a/test/extensions/filters/network/mysql_proxy/mysql_filter_test.cc +++ b/test/extensions/filters/network/mysql_proxy/mysql_filter_test.cc @@ -1,3 +1,4 @@ +#include "common/buffer/buffer_impl.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" #include "extensions/filters/network/mysql_proxy/mysql_filter.h" #include "extensions/filters/network/mysql_proxy/mysql_utils.h" @@ -75,8 +76,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41OkTest) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); - std::string clogin_data = - encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); @@ -110,8 +110,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41PartialMessagesTest) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data_part_2, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); - std::string clogin_data = - encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data_part_1( new Buffer::OwnedImpl(clogin_data.substr(0, clogin_data.length() / 2))); @@ -166,8 +165,7 @@ TEST_F(MySQLFilterTest, MySqlFallbackPartialMessagesTest) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onWrite(*greet_data_part_2, false)); EXPECT_EQ(MySQLSession::State::Init, filter_->getSession().getState()); - std::string clogin_data = - encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data_part_1( new Buffer::OwnedImpl(clogin_data.substr(0, clogin_data.length() / 2))); @@ -214,8 +212,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41ErrTest) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); - std::string clogin_data = - encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); @@ -303,18 +300,17 @@ TEST_F(MySQLFilterTest, MySqlHandshakeSSLTest) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); - std::string clogin_data = encodeClientLogin(MYSQL_CLIENT_CAPAB_SSL | MYSQL_CLIENT_CAPAB_41VS320, - "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data = + encodeClientLogin(CLIENT_SSL | CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); EXPECT_EQ(1UL, config_->stats().upgraded_to_ssl_.value()); EXPECT_EQ(MySQLSession::State::SslPt, filter_->getSession().getState()); - std::string encr_data = "!@#$encr$#@!"; - std::string mysql_ssl_msg = BufferHelper::encodeHdr(encr_data, 2); - Buffer::InstancePtr query_create_index(new Buffer::OwnedImpl(mysql_ssl_msg)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*query_create_index, false)); + Buffer::OwnedImpl query_create_index("!@#$encr$#@!"); + BufferHelper::encodeHdr(query_create_index, 2); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(query_create_index, false)); EXPECT_EQ(MySQLSession::State::SslPt, filter_->getSession().getState()); } @@ -399,10 +395,11 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchErrTest) { mysql_cmd_encode.setCmd(Command::Cmd::Query); std::string query = "CREATE DATABASE mysqldb"; mysql_cmd_encode.setData(query); - std::string query_data = mysql_cmd_encode.encode(); - std::string mysql_msg = BufferHelper::encodeHdr(query_data, 0); - Buffer::InstancePtr client_query_data(new Buffer::OwnedImpl(mysql_msg)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_query_data, false)); + Buffer::OwnedImpl client_query_data; + + mysql_cmd_encode.encode(client_query_data); + BufferHelper::encodeHdr(client_query_data, 0); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(client_query_data, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); EXPECT_EQ(1UL, config_->stats().queries_parsed_.value()); } @@ -449,10 +446,10 @@ TEST_F(MySQLFilterTest, MySqlHandshake320AuthSwitchErrFailResync) { mysql_cmd_encode.setCmd(Command::Cmd::Query); std::string query = "CREATE DATABASE mysqldb"; mysql_cmd_encode.setData(query); - std::string query_data = mysql_cmd_encode.encode(); - std::string mysql_msg = BufferHelper::encodeHdr(query_data, 5); - Buffer::InstancePtr client_query_data(new Buffer::OwnedImpl(mysql_msg)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_query_data, false)); + Buffer::OwnedImpl client_query_data; + mysql_cmd_encode.encode(client_query_data); + BufferHelper::encodeHdr(client_query_data, 5); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(client_query_data, false)); EXPECT_EQ(MySQLSession::State::Resync, filter_->getSession().getState()); } @@ -557,8 +554,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41Ok2GreetTest) { EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); EXPECT_EQ(1UL, config_->stats().protocol_errors_.value()); - std::string clogin_data = - encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); EXPECT_EQ(2UL, config_->stats().login_attempts_.value()); @@ -588,15 +584,13 @@ TEST_F(MySQLFilterTest, MySqlHandshake41Ok2CloginTest) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); - std::string clogin_data = - encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); EXPECT_EQ(MySQLSession::State::ChallengeResp41, filter_->getSession().getState()); - std::string clogin_data2 = - encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data2 = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data2(new Buffer::OwnedImpl(clogin_data2)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data2, false)); EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); @@ -622,8 +616,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41OkOOOLoginTest) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); EXPECT_EQ(1UL, config_->stats().sessions_.value()); - std::string clogin_data = - encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); EXPECT_EQ(MySQLSession::State::Init, filter_->getSession().getState()); @@ -649,8 +642,7 @@ TEST_F(MySQLFilterTest, MySqlHandshake41OkOOOFullLoginTest) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onNewConnection()); EXPECT_EQ(1UL, config_->stats().sessions_.value()); - std::string clogin_data = - encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); EXPECT_EQ(MySQLSession::State::Init, filter_->getSession().getState()); @@ -786,10 +778,9 @@ TEST_F(MySQLFilterTest, MySqlHandshake320WrongServerRespCode) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); - std::string msg_data; - std::string mysql_msg = BufferHelper::encodeHdr(msg_data, 3); - Buffer::InstancePtr client_query_data(new Buffer::OwnedImpl(mysql_msg)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_query_data, false)); + Buffer::OwnedImpl client_query_data; + BufferHelper::encodeHdr(client_query_data, 3); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(client_query_data, false)); EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); } @@ -821,11 +812,8 @@ TEST_F(MySQLFilterTest, MySqlWrongHdrPkt) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*server_resp_ok_data, false)); EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); - Command mysql_cmd_encode{}; - std::string query_data = mysql_cmd_encode.encode(); - std::string mysql_msg = "123"; - Buffer::InstancePtr client_query_data(new Buffer::OwnedImpl(mysql_msg)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_query_data, false)); + Buffer::OwnedImpl client_query_data("123"); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(client_query_data, false)); EXPECT_EQ(MySQLSession::State::NotHandled, filter_->getSession().getState()); } @@ -847,8 +835,7 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*greet_data, false)); EXPECT_EQ(MySQLSession::State::ChallengeReq, filter_->getSession().getState()); - std::string clogin_data = - encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, "user1", CHALLENGE_SEQ_NUM); + std::string clogin_data = encodeClientLogin(CLIENT_PROTOCOL_41, "user1", CHALLENGE_SEQ_NUM); Buffer::InstancePtr client_login_data(new Buffer::OwnedImpl(clogin_data)); EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_login_data, false)); EXPECT_EQ(1UL, config_->stats().login_attempts_.value()); @@ -863,10 +850,11 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { mysql_cmd_encode.setCmd(Command::Cmd::Query); std::string query = "CREATE DATABASE mysqldb"; mysql_cmd_encode.setData(query); - std::string query_data = mysql_cmd_encode.encode(); - std::string mysql_msg = BufferHelper::encodeHdr(query_data, 0); - Buffer::InstancePtr client_query_data(new Buffer::OwnedImpl(mysql_msg)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*client_query_data, false)); + + Buffer::OwnedImpl client_query_data; + mysql_cmd_encode.encode(client_query_data); + BufferHelper::encodeHdr(client_query_data, 0); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(client_query_data, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); EXPECT_EQ(1UL, config_->stats().queries_parsed_.value()); @@ -878,10 +866,11 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { mysql_cmd_encode.setCmd(Command::Cmd::Query); query = "show databases"; mysql_cmd_encode.setData(query); - query_data = mysql_cmd_encode.encode(); - mysql_msg = BufferHelper::encodeHdr(query_data, 0); - Buffer::InstancePtr query_show(new Buffer::OwnedImpl(mysql_msg)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*query_show, false)); + + Buffer::OwnedImpl query_show; + mysql_cmd_encode.encode(query_show); + BufferHelper::encodeHdr(query_show, 0); + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(query_show, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); EXPECT_EQ(2UL, config_->stats().queries_parsed_.value()); @@ -893,10 +882,12 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { mysql_cmd_encode.setCmd(Command::Cmd::Query); query = "CREATE TABLE students (name TEXT, student_number INTEGER, city TEXT)"; mysql_cmd_encode.setData(query); - query_data = mysql_cmd_encode.encode(); - mysql_msg = BufferHelper::encodeHdr(query_data, 0); - Buffer::InstancePtr query_create(new Buffer::OwnedImpl(mysql_msg)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*query_create, false)); + + Buffer::OwnedImpl query_create; + mysql_cmd_encode.encode(query_create); + BufferHelper::encodeHdr(query_create, 0); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(query_create, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); EXPECT_EQ(3UL, config_->stats().queries_parsed_.value()); @@ -908,10 +899,12 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { mysql_cmd_encode.setCmd(Command::Cmd::Query); query = "CREATE index index1"; mysql_cmd_encode.setData(query); - query_data = mysql_cmd_encode.encode(); - mysql_msg = BufferHelper::encodeHdr(query_data, 0); - Buffer::InstancePtr query_create_index(new Buffer::OwnedImpl(mysql_msg)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*query_create_index, false)); + + Buffer::OwnedImpl query_create_index; + mysql_cmd_encode.encode(query_create_index); + BufferHelper::encodeHdr(query_create_index, 0); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(query_create_index, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); EXPECT_EQ(3UL, config_->stats().queries_parsed_.value()); @@ -924,10 +917,12 @@ TEST_F(MySQLFilterTest, MySqlLoginAndQueryTest) { mysql_cmd_encode.setCmd(Command::Cmd::FieldList); query = ""; mysql_cmd_encode.setData(query); - query_data = mysql_cmd_encode.encode(); - mysql_msg = BufferHelper::encodeHdr(query_data, 0); - Buffer::InstancePtr cmd_field_list(new Buffer::OwnedImpl(mysql_msg)); - EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(*cmd_field_list, false)); + + Buffer::OwnedImpl cmd_field_list; + mysql_cmd_encode.encode(cmd_field_list); + BufferHelper::encodeHdr(cmd_field_list, 0); + + EXPECT_EQ(Envoy::Network::FilterStatus::Continue, filter_->onData(cmd_field_list, false)); EXPECT_EQ(MySQLSession::State::ReqResp, filter_->getSession().getState()); EXPECT_EQ(3UL, config_->stats().queries_parsed_.value()); diff --git a/test/extensions/filters/network/mysql_proxy/mysql_greet_test.cc b/test/extensions/filters/network/mysql_proxy/mysql_greet_test.cc new file mode 100644 index 000000000000..1afc5fa9dd50 --- /dev/null +++ b/test/extensions/filters/network/mysql_proxy/mysql_greet_test.cc @@ -0,0 +1,469 @@ +#include "common/buffer/buffer_impl.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_clogin.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_command.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec_greeting.h" +#include "extensions/filters/network/mysql_proxy/mysql_utils.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" +#include +#include "mysql_test_utils.h" + +namespace Envoy { +namespace Extensions { +namespace NetworkFilters { +namespace MySQLProxy { + +class MySQLGreetTest : public testing::Test {}; + +TEST_F(MySQLGreetTest, MySQLServerChallengeV9EncDec) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_9); + std::string ver(MySQLTestUtils::getVersion()); + mysql_greet_encode.setVersion(ver); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + std::string auth_plugin_data(MySQLTestUtils::getAuthPluginData8()); + mysql_greet_encode.setAuthPluginData(auth_plugin_data); + Buffer::OwnedImpl data; + mysql_greet_encode.encode(data); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(data, GREETING_SEQ_NUM, data.length()); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), mysql_greet_encode.getBaseServerCap()); + EXPECT_EQ(mysql_greet_decode.getExtServerCap(), mysql_greet_encode.getExtServerCap()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData(), mysql_greet_encode.getAuthPluginData()); +} + +/* + * Test the MYSQL Greeting message V10: + * - message is encoded using the ServerGreeting class + * - message is decoded using the ServerGreeting class + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeV10EncDec) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + std::string ver(MySQLTestUtils::getVersion()); + mysql_greet_encode.setVersion(ver); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + mysql_greet_encode.setAuthPluginData(MySQLTestUtils::getAuthPluginData8()); + mysql_greet_encode.setServerCap(0); + mysql_greet_encode.setServerCharset(MYSQL_SERVER_LANGUAGE); + mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); + Buffer::OwnedImpl decode_data; + mysql_greet_encode.encode(decode_data); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData(), mysql_greet_encode.getAuthPluginData()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData1(), mysql_greet_encode.getAuthPluginData1()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData2(), ""); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getServerStatus(), mysql_greet_encode.getServerStatus()); + EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), mysql_greet_encode.getBaseServerCap()); + EXPECT_EQ(mysql_greet_decode.getExtServerCap(), mysql_greet_encode.getExtServerCap()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginName(), ""); +} + +/* + * Negative Testing: Server Greetings v10 Incomplete + * - incomplete protocol + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeIncompleteProtocol) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + + int incomplete_len = 0; + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), 0); +} + +/* + * Negative Testing: Server Greetings v10 Incomplete + * - incomplete version + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeIncompleteVersion) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + mysql_greet_encode.setVersion(MySQLTestUtils::getVersion()); + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + + int incomplete_len = sizeof(mysql_greet_encode.getProtocol()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getVersion(), ""); +} + +/* + * Negative Testing: Server Greetings v10 Incomplete + * - incomplete thread_id + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeIncompleteThreadId) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + std::string ver(MySQLTestUtils::getVersion()); + mysql_greet_encode.setVersion(ver); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + + int incomplete_len = sizeof(mysql_greet_encode.getProtocol()) + ver.size() + 1; + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), 0); +} + +/* + * Negative Testing: Server Greetings v10 Incomplete + * - incomplete auth_plugin_data + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeIncompleteSalt) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + std::string ver(MySQLTestUtils::getVersion()); + mysql_greet_encode.setVersion(ver); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + mysql_greet_encode.setAuthPluginData(MySQLTestUtils::getAuthPluginData8()); + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + + int incomplete_len = sizeof(mysql_greet_encode.getProtocol()) + ver.size() + 1 + + sizeof(mysql_greet_encode.getThreadId()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData(), ""); +} + +/* + * Negative Testing: Server Greetings v10 Incomplete + * - incomplete Server Capabilities + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeIncompleteServerCap) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + std::string ver(MySQLTestUtils::getVersion()); + mysql_greet_encode.setVersion(ver); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + mysql_greet_encode.setAuthPluginData1(MySQLTestUtils::getAuthPluginData8()); + mysql_greet_encode.setBaseServerCap(MYSQL_SERVER_CAPAB); + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + + int incomplete_len = sizeof(mysql_greet_encode.getProtocol()) + ver.size() + 1 + + sizeof(mysql_greet_encode.getThreadId()) + + mysql_greet_encode.getAuthPluginData1().size() + 1; + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData(), mysql_greet_encode.getAuthPluginData()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), 0); +} + +/* + * Negative Testing: Server Greetings Incomplete + * - incomplete Server Status + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeIncompleteServerStatus) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + std::string ver(MySQLTestUtils::getVersion()); + mysql_greet_encode.setVersion(ver); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + mysql_greet_encode.setAuthPluginData1(MySQLTestUtils::getAuthPluginData8()); + mysql_greet_encode.setBaseServerCap(MYSQL_SERVER_CAPAB); + mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + + int incomplete_len = sizeof(mysql_greet_encode.getProtocol()) + ver.size() + 1 + + sizeof(mysql_greet_encode.getThreadId()) + + mysql_greet_encode.getAuthPluginData1().size() + 1 + + sizeof(mysql_greet_encode.getBaseServerCap()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData(), mysql_greet_encode.getAuthPluginData()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), mysql_greet_encode.getBaseServerCap()); + EXPECT_EQ(mysql_greet_decode.getServerStatus(), 0); +} + +/* + * Negative Testing: Server Greetings v10 Incomplete + * - incomplete extended Server Capabilities + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeIncompleteExtServerCap) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + std::string ver(MySQLTestUtils::getVersion()); + mysql_greet_encode.setVersion(ver); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + mysql_greet_encode.setAuthPluginData1(MySQLTestUtils::getAuthPluginData8()); + mysql_greet_encode.setBaseServerCap(MYSQL_SERVER_CAPAB); + mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); + mysql_greet_encode.setExtServerCap(MYSQL_SERVER_EXT_CAPAB); + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + + int incomplete_len = sizeof(mysql_greet_encode.getProtocol()) + ver.size() + 1 + + sizeof(mysql_greet_encode.getThreadId()) + + mysql_greet_encode.getAuthPluginData1().size() + 1 + + sizeof(mysql_greet_encode.getBaseServerCap()) + + sizeof(mysql_greet_encode.getServerStatus()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData(), mysql_greet_encode.getAuthPluginData()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), mysql_greet_encode.getBaseServerCap()); + EXPECT_EQ(mysql_greet_decode.getServerStatus(), mysql_greet_encode.getServerStatus()); + EXPECT_EQ(mysql_greet_decode.getExtServerCap(), 0); +} + +/* + * Negative Testing: Server Greetings v10 Incomplete + * - incomplete extended Server Capabilities + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeP10ServerCapOnly) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + mysql_greet_encode.setVersion(MySQLTestUtils::getVersion()); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + std::string auth_plugin_data(MySQLTestUtils::getAuthPluginData8()); + mysql_greet_encode.setAuthPluginData(auth_plugin_data); + mysql_greet_encode.setServerCap(MYSQL_SERVER_CAPAB); + mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); + + Buffer::OwnedImpl decode_data; + mysql_greet_encode.encode(decode_data); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData(), mysql_greet_encode.getAuthPluginData()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData1(), mysql_greet_encode.getAuthPluginData1()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData2(), mysql_greet_encode.getAuthPluginData2()); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getServerStatus(), mysql_greet_encode.getServerStatus()); + EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), mysql_greet_encode.getBaseServerCap()); + EXPECT_EQ(mysql_greet_decode.getExtServerCap(), mysql_greet_encode.getExtServerCap()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginName(), mysql_greet_encode.getAuthPluginName()); +} + +/* + * Testing: Server Greetings Protocol 10 Server Capabilities with auth plugin data flag + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeP10ServerCapAuthPlugin) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + std::string ver(MySQLTestUtils::getVersion()); + mysql_greet_encode.setVersion(ver); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + std::string auth_plugin_data(MySQLTestUtils::getAuthPluginData20()); + mysql_greet_encode.setAuthPluginData(auth_plugin_data); + mysql_greet_encode.setServerCap(MYSQL_SERVER_CAP_AUTH_PLUGIN); + mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); + mysql_greet_encode.setAuthPluginName(MySQLTestUtils::getAuthPluginName()); + + Buffer::OwnedImpl decode_data; + mysql_greet_encode.encode(decode_data); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData(), mysql_greet_encode.getAuthPluginData()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData1(), mysql_greet_encode.getAuthPluginData1()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData2(), mysql_greet_encode.getAuthPluginData2()); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getServerStatus(), mysql_greet_encode.getServerStatus()); + EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), mysql_greet_encode.getBaseServerCap()); + EXPECT_EQ(mysql_greet_decode.getExtServerCap(), mysql_greet_encode.getExtServerCap()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginName(), mysql_greet_encode.getAuthPluginName()); +} + +/* + * Testing: Server Greetings Protocol 10 Server Capabilities with auth plugin data flag incomplete + * - incomplete of auth-plugin-data2 + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeP10ServerAuthPluginInCompleteAuthData2) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + mysql_greet_encode.setVersion(MySQLTestUtils::getVersion()); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + mysql_greet_encode.setAuthPluginData(MySQLTestUtils::getAuthPluginData20()); + mysql_greet_encode.setServerCap(MYSQL_SERVER_CAP_AUTH_PLUGIN); + mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + + int incomplete_len = + sizeof(mysql_greet_encode.getProtocol()) + mysql_greet_encode.getVersion().size() + 1 + + sizeof(mysql_greet_encode.getThreadId()) + mysql_greet_encode.getAuthPluginData1().size() + + 1 + sizeof(mysql_greet_encode.getBaseServerCap()) + + sizeof(mysql_greet_encode.getServerStatus()) + sizeof(mysql_greet_encode.getExtServerCap()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData1(), mysql_greet_encode.getAuthPluginData1()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData2(), ""); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getServerStatus(), mysql_greet_encode.getServerStatus()); + EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), mysql_greet_encode.getBaseServerCap()); + EXPECT_EQ(mysql_greet_decode.getExtServerCap(), mysql_greet_encode.getExtServerCap()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginName(), ""); +} + +/* + * Testing: Server Greetings Protocol 10 Server Capabilities with auth plugin data flag incomplete + * - incomplete of auth plugin name + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeP10ServerAuthPluginInCompleteAuthPluginName) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + mysql_greet_encode.setVersion(MySQLTestUtils::getVersion()); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + mysql_greet_encode.setAuthPluginData(MySQLTestUtils::getAuthPluginData20()); + mysql_greet_encode.setServerCap(MYSQL_SERVER_CAP_AUTH_PLUGIN); + mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); + mysql_greet_encode.setAuthPluginName(MySQLTestUtils::getAuthPluginName()); + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + + int incomplete_len = + sizeof(mysql_greet_encode.getProtocol()) + mysql_greet_encode.getVersion().size() + 1 + + sizeof(mysql_greet_encode.getThreadId()) + mysql_greet_encode.getAuthPluginData1().size() + + +sizeof(mysql_greet_encode.getServerStatus()) + sizeof(mysql_greet_encode.getExtServerCap()) + + 1 + sizeof(mysql_greet_encode.getBaseServerCap()) + 1 + 10 + + mysql_greet_encode.getAuthPluginData2().size(); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData1(), mysql_greet_encode.getAuthPluginData1()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData2(), mysql_greet_encode.getAuthPluginData2()); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getServerStatus(), mysql_greet_encode.getServerStatus()); + EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), mysql_greet_encode.getBaseServerCap()); + EXPECT_EQ(mysql_greet_decode.getExtServerCap(), mysql_greet_encode.getExtServerCap()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginName(), ""); +} + +/* + * Testing: Server Greetings Protocol 10 Server Capabilities with security connection flag + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeP10ServerCapSecurityConnection) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + std::string ver(MySQLTestUtils::getVersion()); + mysql_greet_encode.setVersion(ver); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + std::string auth_plugin_data(MySQLTestUtils::getAuthPluginData20()); + mysql_greet_encode.setAuthPluginData(auth_plugin_data); + mysql_greet_encode.setServerCap(MYSQL_SERVER_SECURE_CONNECTION); + mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); + + Buffer::OwnedImpl decode_data; + mysql_greet_encode.encode(decode_data); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData(), mysql_greet_encode.getAuthPluginData()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData1(), mysql_greet_encode.getAuthPluginData1()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData2(), mysql_greet_encode.getAuthPluginData2()); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getServerStatus(), mysql_greet_encode.getServerStatus()); + EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), mysql_greet_encode.getBaseServerCap()); + EXPECT_EQ(mysql_greet_decode.getExtServerCap(), mysql_greet_encode.getExtServerCap()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginName(), mysql_greet_encode.getAuthPluginName()); +} + +/* + * Testing: Server Greetings Protocol 10 Server Capabilities with security connection flag + * - incomplete of auth-plugin-data2 + */ +TEST_F(MySQLGreetTest, MySQLServerChallengeP10ServerSecurityConnectionInCompleteData2) { + ServerGreeting mysql_greet_encode{}; + mysql_greet_encode.setProtocol(MYSQL_PROTOCOL_10); + std::string ver(MySQLTestUtils::getVersion()); + mysql_greet_encode.setVersion(ver); + mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); + mysql_greet_encode.setAuthPluginData(MySQLTestUtils::getAuthPluginData20()); + mysql_greet_encode.setServerCap(MYSQL_SERVER_SECURE_CONNECTION); + mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + + int incomplete_len = + sizeof(mysql_greet_encode.getProtocol()) + ver.size() + 1 + + sizeof(mysql_greet_encode.getThreadId()) + mysql_greet_encode.getAuthPluginData1().size() + + 1 + sizeof(mysql_greet_encode.getBaseServerCap()) + + sizeof(mysql_greet_encode.getServerStatus()) + sizeof(mysql_greet_encode.getExtServerCap()); + Buffer::OwnedImpl decode_data(buffer.toString().data(), incomplete_len); + + ServerGreeting mysql_greet_decode{}; + mysql_greet_decode.decode(decode_data, GREETING_SEQ_NUM, decode_data.length()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData1(), mysql_greet_encode.getAuthPluginData1()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginData2(), ""); + EXPECT_EQ(mysql_greet_decode.getVersion(), mysql_greet_encode.getVersion()); + EXPECT_EQ(mysql_greet_decode.getProtocol(), mysql_greet_encode.getProtocol()); + EXPECT_EQ(mysql_greet_decode.getThreadId(), mysql_greet_encode.getThreadId()); + EXPECT_EQ(mysql_greet_decode.getServerStatus(), mysql_greet_encode.getServerStatus()); + EXPECT_EQ(mysql_greet_decode.getServerCap(), mysql_greet_encode.getServerCap()); + EXPECT_EQ(mysql_greet_decode.getBaseServerCap(), mysql_greet_encode.getBaseServerCap()); + EXPECT_EQ(mysql_greet_decode.getExtServerCap(), mysql_greet_encode.getExtServerCap()); + EXPECT_EQ(mysql_greet_decode.getAuthPluginName(), ""); +} +} // namespace MySQLProxy +} // namespace NetworkFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/network/mysql_proxy/mysql_integration_test.cc b/test/extensions/filters/network/mysql_proxy/mysql_integration_test.cc index 39fd97df02b8..b2ada0f6dbd5 100644 --- a/test/extensions/filters/network/mysql_proxy/mysql_integration_test.cc +++ b/test/extensions/filters/network/mysql_proxy/mysql_integration_test.cc @@ -83,7 +83,7 @@ TEST_P(MySQLIntegrationTest, MySQLLoginTest) { tcp_client->waitForData(str, true); // Client username/password and capabilities - std::string login = encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, user, CHALLENGE_SEQ_NUM); + std::string login = encodeClientLogin(CLIENT_PROTOCOL_41, user, CHALLENGE_SEQ_NUM); ASSERT_TRUE(tcp_client->write(login)); ASSERT_TRUE(fake_upstream_connection->waitForData(login.length(), &rcvd_data)); EXPECT_EQ(login, rcvd_data); @@ -130,7 +130,7 @@ TEST_P(MySQLIntegrationTest, MySQLUnitTestMultiClientsLoop) { tcp_client->waitForData(str, true); // Client username/password and capabilities - std::string login = encodeClientLogin(MYSQL_CLIENT_CAPAB_41VS320, user, CHALLENGE_SEQ_NUM); + std::string login = encodeClientLogin(CLIENT_PROTOCOL_41, user, CHALLENGE_SEQ_NUM); ASSERT_TRUE(tcp_client->write(login)); ASSERT_TRUE(fake_upstream_connection->waitForData(login.length(), &rcvd_data)); EXPECT_EQ(login, rcvd_data); diff --git a/test/extensions/filters/network/mysql_proxy/mysql_test_utils.cc b/test/extensions/filters/network/mysql_proxy/mysql_test_utils.cc index a72e503009b9..2d18322c2da2 100644 --- a/test/extensions/filters/network/mysql_proxy/mysql_test_utils.cc +++ b/test/extensions/filters/network/mysql_proxy/mysql_test_utils.cc @@ -1,11 +1,13 @@ #include "mysql_test_utils.h" +#include "common/buffer/buffer_impl.h" #include "extensions/filters/network/mysql_proxy/mysql_codec.h" #include "extensions/filters/network/mysql_proxy/mysql_codec_clogin.h" #include "extensions/filters/network/mysql_proxy/mysql_codec_clogin_resp.h" #include "extensions/filters/network/mysql_proxy/mysql_codec_greeting.h" #include "extensions/filters/network/mysql_proxy/mysql_codec_switch_resp.h" #include "extensions/filters/network/mysql_proxy/mysql_utils.h" +#include namespace Envoy { namespace Extensions { @@ -18,15 +20,15 @@ std::string MySQLTestUtils::encodeServerGreeting(int protocol) { std::string ver(MySQLTestUtils::getVersion()); mysql_greet_encode.setVersion(ver); mysql_greet_encode.setThreadId(MYSQL_THREAD_ID); - std::string salt(getSalt()); - mysql_greet_encode.setSalt(salt); + mysql_greet_encode.setAuthPluginData(getAuthPluginData8()); mysql_greet_encode.setServerCap(MYSQL_SERVER_CAPAB); - mysql_greet_encode.setServerLanguage(MYSQL_SERVER_LANGUAGE); + mysql_greet_encode.setServerCharset(MYSQL_SERVER_LANGUAGE); mysql_greet_encode.setServerStatus(MYSQL_SERVER_STATUS); mysql_greet_encode.setExtServerCap(MYSQL_SERVER_EXT_CAPAB); - std::string data = mysql_greet_encode.encode(); - std::string mysql_msg = BufferHelper::encodeHdr(data, GREETING_SEQ_NUM); - return mysql_msg; + Buffer::OwnedImpl buffer; + mysql_greet_encode.encode(buffer); + BufferHelper::encodeHdr(buffer, GREETING_SEQ_NUM); + return buffer.toString(); } std::string MySQLTestUtils::encodeClientLogin(uint16_t client_cap, std::string user, uint8_t seq) { @@ -36,36 +38,74 @@ std::string MySQLTestUtils::encodeClientLogin(uint16_t client_cap, std::string u mysql_clogin_encode.setMaxPacket(MYSQL_MAX_PACKET); mysql_clogin_encode.setCharset(MYSQL_CHARSET); mysql_clogin_encode.setUsername(user); - std::string auth_resp(getAuthResp()); - mysql_clogin_encode.setAuthResp(auth_resp); - std::string data = mysql_clogin_encode.encode(); - std::string mysql_msg = BufferHelper::encodeHdr(data, seq); - return mysql_msg; + mysql_clogin_encode.setAuthResp(getAuthPluginData8()); + Buffer::OwnedImpl buffer; + mysql_clogin_encode.encode(buffer); + BufferHelper::encodeHdr(buffer, seq); + return buffer.toString(); } std::string MySQLTestUtils::encodeClientLoginResp(uint8_t srv_resp, uint8_t it, uint8_t seq_force) { - ClientLoginResponse mysql_loginok_encode{}; - mysql_loginok_encode.setRespCode(srv_resp); - mysql_loginok_encode.setAffectedRows(MYSQL_SM_AFFECTED_ROWS); - mysql_loginok_encode.setLastInsertId(MYSQL_SM_LAST_ID); - mysql_loginok_encode.setServerStatus(MYSQL_SM_SERVER_OK); - mysql_loginok_encode.setWarnings(MYSQL_SM_SERVER_WARNINGS); - std::string data = mysql_loginok_encode.encode(); + ClientLoginResponse mysql_login_resp_encode{}; + switch (srv_resp) { + case MYSQL_RESP_OK: + mysql_login_resp_encode.type(Ok); + mysql_login_resp_encode.asOkMessage().setAffectedRows(MYSQL_SM_AFFECTED_ROWS); + mysql_login_resp_encode.asOkMessage().setLastInsertId(MYSQL_SM_LAST_ID); + mysql_login_resp_encode.asOkMessage().setServerStatus(MYSQL_SM_SERVER_OK); + mysql_login_resp_encode.asOkMessage().setWarnings(MYSQL_SM_SERVER_WARNINGS); + break; + case MYSQL_RESP_ERR: + mysql_login_resp_encode.type(Err); + mysql_login_resp_encode.asErrMessage().setErrorCode(MYSQL_ERROR_CODE); + mysql_login_resp_encode.asErrMessage().setSqlStateMarker('#'); + mysql_login_resp_encode.asErrMessage().setSqlState(MySQLTestUtils::getSqlState()); + mysql_login_resp_encode.asErrMessage().setErrorMessage(MySQLTestUtils::getErrorMessage()); + break; + case MYSQL_RESP_AUTH_SWITCH: + mysql_login_resp_encode.type(AuthSwitch); + mysql_login_resp_encode.asAuthSwitchMessage().setIsOldAuthSwitch(false); + mysql_login_resp_encode.asAuthSwitchMessage().setAuthPluginData( + MySQLTestUtils::getAuthPluginData20()); + mysql_login_resp_encode.asAuthSwitchMessage().setAuthPluginName( + MySQLTestUtils::getAuthPluginName()); + break; + case MYSQL_RESP_MORE: + mysql_login_resp_encode.type(AuthMoreData); + mysql_login_resp_encode.asAuthMoreMessage().setAuthMoreData( + MySQLTestUtils::getAuthPluginData20()); + } + uint8_t seq = CHALLENGE_RESP_SEQ_NUM + 2 * it; if (seq_force > 0) { seq = seq_force; } - std::string mysql_msg = BufferHelper::encodeHdr(data, seq); - return mysql_msg; + Buffer::OwnedImpl buffer; + mysql_login_resp_encode.encode(buffer); + BufferHelper::encodeHdr(buffer, seq); + return buffer.toString(); } std::string MySQLTestUtils::encodeAuthSwitchResp() { ClientSwitchResponse mysql_switch_resp_encode{}; std::string resp_opaque_data("mysql_opaque"); mysql_switch_resp_encode.setAuthPluginResp(resp_opaque_data); - std::string data = mysql_switch_resp_encode.encode(); - std::string mysql_msg = BufferHelper::encodeHdr(data, AUTH_SWITH_RESP_SEQ); - return mysql_msg; + Buffer::OwnedImpl buffer; + mysql_switch_resp_encode.encode(buffer); + BufferHelper::encodeHdr(buffer, AUTH_SWITH_RESP_SEQ); + return buffer.toString(); +} + +int MySQLTestUtils::sizeOfLengthEncodeInteger(uint64_t val) { + if (val < 251) { + return sizeof(uint8_t); + } else if (val < (1 << 16)) { + return sizeof(uint8_t) + sizeof(uint16_t); + } else if (val < (1 << 24)) { + return sizeof(uint8_t) + sizeof(uint8_t) * 3; + } else { + return sizeof(uint8_t) + sizeof(uint64_t); + } } } // namespace MySQLProxy diff --git a/test/extensions/filters/network/mysql_proxy/mysql_test_utils.h b/test/extensions/filters/network/mysql_proxy/mysql_test_utils.h index e39a353ddf44..a35a06a0acab 100644 --- a/test/extensions/filters/network/mysql_proxy/mysql_test_utils.h +++ b/test/extensions/filters/network/mysql_proxy/mysql_test_utils.h @@ -1,6 +1,9 @@ #pragma once #include "fmt/format.h" +#include "extensions/filters/network/mysql_proxy/mysql_codec.h" +#include + namespace Envoy { namespace Extensions { namespace NetworkFilters { @@ -15,15 +18,24 @@ constexpr int MYSQL_SM_SERVER_WARNINGS = 0x0001; constexpr int MYSQL_SM_AFFECTED_ROWS = 1; constexpr int CLIENT_NUM = 10; constexpr int PARALLEL_SESSIONS = 4; +constexpr uint32_t MYSQL_SERVER_CAP_AUTH_PLUGIN = 0x00080000; +constexpr uint32_t MYSQL_SERVER_SECURE_CONNECTION = 0x00008000; +constexpr uint16_t MYSQL_ERROR_CODE = MYSQL_CR_AUTH_PLUGIN_ERR; class MySQLTestUtils { public: - static std::string getSalt() { return "!@salt#$"; } - static std::string getAuthResp() { return "p4$$w0r6"; } + static std::string getAuthPluginData8() { return "!@salt#$"; } + static std::string getAuthPluginData20() { return "!@salt#$!@salt#$xxXX"; } + static std::string getAuthResp8() { return "p4$$w0r6"; } + static std::string getAuthResp20() { return "p4$$w0r6p4$$w0r61111"; } static std::string getVersion() { return fmt::format("{0}.{1}.{2}", MYSQL_VER_MAJOR, MYSQL_VER_MINOR, MYSQL_VER_VAR); } + static std::string getSqlState() { return "HY000"; } + static std::string getErrorMessage() { return "auth failed"; } + static std::string getAuthPluginName() { return "mysql_native_password"; } + static int sizeOfLengthEncodeInteger(uint64_t val); std::string encodeServerGreeting(int protocol); std::string encodeClientLogin(uint16_t client_cap, std::string user, uint8_t seq); diff --git a/test/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter_test.cc b/test/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter_test.cc index 93f8b17cfdcc..01073b5e6c96 100644 --- a/test/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter_test.cc +++ b/test/extensions/filters/network/sni_dynamic_forward_proxy/proxy_filter_test.cc @@ -73,7 +73,7 @@ TEST_F(SniDynamicProxyFilterTest, LoadDnsCache) { EXPECT_CALL(connection_, requestedServerName()).WillRepeatedly(Return("foo")); Upstream::ResourceAutoIncDec* circuit_breakers_{ new Upstream::ResourceAutoIncDec(pending_requests_)}; - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)) + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) .WillOnce(Return(circuit_breakers_)); Extensions::Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle* handle = new Extensions::Common::DynamicForwardProxy::MockLoadDnsCacheEntryHandle(); @@ -91,7 +91,7 @@ TEST_F(SniDynamicProxyFilterTest, LoadDnsInCache) { EXPECT_CALL(connection_, requestedServerName()).WillRepeatedly(Return("foo")); Upstream::ResourceAutoIncDec* circuit_breakers_{ new Upstream::ResourceAutoIncDec(pending_requests_)}; - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)) + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) .WillOnce(Return(circuit_breakers_)); EXPECT_CALL(*dns_cache_manager_->dns_cache_, loadDnsCacheEntry_(Eq("foo"), 443, _)) .WillOnce(Return(MockLoadDnsCacheEntryResult{LoadDnsCacheEntryStatus::InCache, nullptr})); @@ -104,7 +104,7 @@ TEST_F(SniDynamicProxyFilterTest, CacheOverflow) { EXPECT_CALL(connection_, requestedServerName()).WillRepeatedly(Return("foo")); Upstream::ResourceAutoIncDec* circuit_breakers_{ new Upstream::ResourceAutoIncDec(pending_requests_)}; - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)) + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()) .WillOnce(Return(circuit_breakers_)); EXPECT_CALL(*dns_cache_manager_->dns_cache_, loadDnsCacheEntry_(Eq("foo"), 443, _)) .WillOnce(Return(MockLoadDnsCacheEntryResult{LoadDnsCacheEntryStatus::Overflow, nullptr})); @@ -114,7 +114,7 @@ TEST_F(SniDynamicProxyFilterTest, CacheOverflow) { TEST_F(SniDynamicProxyFilterTest, CircuitBreakerInvoked) { EXPECT_CALL(connection_, requestedServerName()).WillRepeatedly(Return("foo")); - EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_(_)).WillOnce(Return(nullptr)); + EXPECT_CALL(*dns_cache_manager_->dns_cache_, canCreateDnsRequest_()).WillOnce(Return(nullptr)); EXPECT_CALL(connection_, close(Network::ConnectionCloseType::NoFlush)); EXPECT_EQ(Network::FilterStatus::StopIteration, filter_->onNewConnection()); } diff --git a/test/extensions/filters/network/thrift_proxy/conn_manager_test.cc b/test/extensions/filters/network/thrift_proxy/conn_manager_test.cc index f58948b262fc..3764bbd44573 100644 --- a/test/extensions/filters/network/thrift_proxy/conn_manager_test.cc +++ b/test/extensions/filters/network/thrift_proxy/conn_manager_test.cc @@ -1641,6 +1641,55 @@ stat_prefix: test filter_callbacks_.connection_.dispatcher_.clearDeferredDeleteList(); } +// When a local reply was sent, payload passthrough is disabled because there's no +// active RPC left. +TEST_F(ThriftConnectionManagerTest, NoPayloadPassthroughOnLocalReply) { + const std::string yaml = R"EOF( +transport: FRAMED +protocol: BINARY +payload_passthrough: true +stat_prefix: test +route_config: + name: "routes" + routes: + - match: + method_name: not_handled + route: + cluster: cluster +)EOF"; + + initializeFilter(yaml); + writeFramedBinaryMessage(buffer_, MessageType::Oneway, 0x0F); + + EXPECT_CALL(*decoder_filter_, passthroughSupported()).WillRepeatedly(Return(true)); + EXPECT_CALL(*decoder_filter_, passthroughData(_)).Times(0); + + ThriftFilters::DecoderFilterCallbacks* callbacks{}; + EXPECT_CALL(*decoder_filter_, setDecoderFilterCallbacks(_)) + .WillOnce( + Invoke([&](ThriftFilters::DecoderFilterCallbacks& cb) -> void { callbacks = &cb; })); + + NiceMock direct_response; + EXPECT_CALL(direct_response, encode(_, _, _)) + .WillOnce(Invoke([&](MessageMetadata&, Protocol&, + Buffer::Instance& buffer) -> DirectResponse::ResponseType { + buffer.add("response"); + return DirectResponse::ResponseType::ErrorReply; + })); + + EXPECT_CALL(*decoder_filter_, messageBegin(_)) + .WillOnce(Invoke([&](MessageMetadataSharedPtr) -> FilterStatus { + callbacks->sendLocalReply(direct_response, false); + return FilterStatus::StopIteration; + })); + + EXPECT_EQ(filter_->onData(buffer_, false), Network::FilterStatus::StopIteration); + EXPECT_EQ(1U, store_.counter("test.request").value()); + + Router::RouteConstSharedPtr route = callbacks->route(); + EXPECT_EQ(nullptr, route); +} + } // namespace ThriftProxy } // namespace NetworkFilters } // namespace Extensions diff --git a/test/extensions/quic_listeners/quiche/integration/quic_http_integration_test.cc b/test/extensions/quic_listeners/quiche/integration/quic_http_integration_test.cc index 4c6309620f3d..ce6f3cc1b24f 100644 --- a/test/extensions/quic_listeners/quiche/integration/quic_http_integration_test.cc +++ b/test/extensions/quic_listeners/quiche/integration/quic_http_integration_test.cc @@ -310,6 +310,9 @@ TEST_P(QuicHttpIntegrationTest, GetRequestAndEmptyResponse) { } TEST_P(QuicHttpIntegrationTest, GetRequestAndResponseWithBody) { + // Use the old nodelay in a random test for coverage. nodelay is a no-op for QUIC. + config_helper_.addRuntimeOverride("envoy.reloadable_features.always_nodelay", "false"); + initialize(); sendRequestAndVerifyResponse(default_request_headers_, /*request_size=*/0, default_response_headers_, /*response_size=*/1024, diff --git a/test/extensions/stats_sinks/metrics_service/metrics_service_integration_test.cc b/test/extensions/stats_sinks/metrics_service/metrics_service_integration_test.cc index 0bf4f9cada60..e325e04d5037 100644 --- a/test/extensions/stats_sinks/metrics_service/metrics_service_integration_test.cc +++ b/test/extensions/stats_sinks/metrics_service/metrics_service_integration_test.cc @@ -150,7 +150,8 @@ class MetricsServiceIntegrationTest : public Grpc::VersionedGrpcClientIntegratio }; INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, MetricsServiceIntegrationTest, - VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::VersionedGrpcClientIntegrationParamTest::protocolTestParamsToString); // Test a basic metric service flow. TEST_P(MetricsServiceIntegrationTest, BasicFlow) { diff --git a/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_integration_test.cc b/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_integration_test.cc index 5747ec73d125..7e0f40d136c0 100644 --- a/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_integration_test.cc +++ b/test/extensions/transport_sockets/proxy_protocol/proxy_protocol_integration_test.cc @@ -16,6 +16,7 @@ class ProxyProtocolIntegrationTest : public testing::TestWithParammergeValues( - {{"envoy.reloadable_features.fix_wildcard_matching", "false"}}); - EXPECT_TRUE(ContextImpl::dnsNameMatch("lyft.com", "lyft.com")); - EXPECT_TRUE(ContextImpl::dnsNameMatch("a.lyft.com", "*.lyft.com")); - // Legacy behavior - EXPECT_TRUE(ContextImpl::dnsNameMatch("a.b.lyft.com", "*.lyft.com")); - EXPECT_FALSE(ContextImpl::dnsNameMatch("foo.test.com", "*.lyft.com")); - EXPECT_FALSE(ContextImpl::dnsNameMatch("lyft.com", "*.lyft.com")); - EXPECT_FALSE(ContextImpl::dnsNameMatch("alyft.com", "*.lyft.com")); - EXPECT_FALSE(ContextImpl::dnsNameMatch("alyft.com", "*lyft.com")); - EXPECT_FALSE(ContextImpl::dnsNameMatch("lyft.com", "*lyft.com")); - EXPECT_FALSE(ContextImpl::dnsNameMatch("", "*lyft.com")); - EXPECT_FALSE(ContextImpl::dnsNameMatch("lyft.com", "")); -} - TEST_F(SslContextImplTest, TestVerifySubjectAltNameDNSMatched) { bssl::UniquePtr cert = readCertFromFile(TestEnvironment::substitute( "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/san_dns_cert.pem")); @@ -172,20 +155,6 @@ TEST_F(SslContextImplTest, TestMultiLevelMatch) { EXPECT_FALSE(ContextImpl::matchSubjectAltName(cert.get(), subject_alt_name_matchers)); } -TEST_F(SslContextImplTest, TestMultiLevelMatchLegacy) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.fix_wildcard_matching", "false"}}); - bssl::UniquePtr cert = readCertFromFile(TestEnvironment::substitute( - "{{ test_rundir " - "}}/test/extensions/transport_sockets/tls/test_data/san_multiple_dns_cert.pem")); - envoy::type::matcher::v3::StringMatcher matcher; - matcher.set_exact("foo.api.example.com"); - std::vector subject_alt_name_matchers; - subject_alt_name_matchers.push_back(Matchers::StringMatcherImpl(matcher)); - EXPECT_TRUE(ContextImpl::matchSubjectAltName(cert.get(), subject_alt_name_matchers)); -} - TEST_F(SslContextImplTest, TestVerifySubjectAltNameURIMatched) { bssl::UniquePtr cert = readCertFromFile(TestEnvironment::substitute( "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/san_uri_cert.pem")); @@ -202,17 +171,6 @@ TEST_F(SslContextImplTest, TestVerifySubjectAltMultiDomain) { EXPECT_FALSE(ContextImpl::verifySubjectAltName(cert.get(), verify_subject_alt_name_list)); } -TEST_F(SslContextImplTest, TestVerifySubjectAltMultiDomainLegacy) { - TestScopedRuntime scoped_runtime; - Runtime::LoaderSingleton::getExisting()->mergeValues( - {{"envoy.reloadable_features.fix_wildcard_matching", "false"}}); - bssl::UniquePtr cert = readCertFromFile(TestEnvironment::substitute( - "{{ test_rundir " - "}}/test/extensions/transport_sockets/tls/test_data/san_multiple_dns_cert.pem")); - std::vector verify_subject_alt_name_list = {"https://a.www.example.com"}; - EXPECT_TRUE(ContextImpl::verifySubjectAltName(cert.get(), verify_subject_alt_name_list)); -} - TEST_F(SslContextImplTest, TestMatchSubjectAltNameURIMatched) { bssl::UniquePtr cert = readCertFromFile(TestEnvironment::substitute( "{{ test_rundir }}/test/extensions/transport_sockets/tls/test_data/san_uri_cert.pem")); @@ -1977,11 +1935,16 @@ class SslContextStatsTest : public SslContextImplTest { TEST_F(SslContextStatsTest, IncOnlyKnownCounters) { // Incrementing a value for a cipher that is part of the configuration works, and // we'll be able to find the value in the stats store. - context_->incCounter("ssl.ciphers", "ECDHE-ECDSA-AES256-GCM-SHA384"); - Stats::CounterOptConstRef cipher = - store_.findCounterByString("ssl.ciphers.ECDHE-ECDSA-AES256-GCM-SHA384"); - ASSERT_TRUE(cipher.has_value()); - EXPECT_EQ(1, cipher->get().value()); + for (const auto& cipher : + {"TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384", "TLS_CHACHA20_POLY1305_SHA256"}) { + // Test supported built-in TLS v1.3 cipher suites + // https://tools.ietf.org/html/rfc8446#appendix-B.4. + context_->incCounter("ssl.ciphers", cipher); + Stats::CounterOptConstRef stat = + store_.findCounterByString(absl::StrCat("ssl.ciphers.", cipher)); + ASSERT_TRUE(stat.has_value()); + EXPECT_EQ(1, stat->get().value()); + } // Incrementing a stat for a random unknown cipher does not work. A // rate-limited error log message will also be generated but that is hard to @@ -1995,9 +1958,9 @@ TEST_F(SslContextStatsTest, IncOnlyKnownCounters) { // fallback registration does not occur. So we test for the fallback only in // release builds. #ifdef NDEBUG - cipher = store_.findCounterByString("ssl.ciphers.fallback"); - ASSERT_TRUE(cipher.has_value()); - EXPECT_EQ(1, cipher->get().value()); + Stats::CounterOptConstRef stat = store_.findCounterByString("ssl.ciphers.fallback"); + ASSERT_TRUE(stat.has_value()); + EXPECT_EQ(1, stat->get().value()); #endif } diff --git a/test/extensions/transport_sockets/tls/handshaker_test.cc b/test/extensions/transport_sockets/tls/handshaker_test.cc index 96fdba339cd2..82484f56dda0 100644 --- a/test/extensions/transport_sockets/tls/handshaker_test.cc +++ b/test/extensions/transport_sockets/tls/handshaker_test.cc @@ -153,7 +153,10 @@ TEST_F(HandshakerTest, ErrorCbOnAbnormalOperation) { BIO* bio = BIO_new(BIO_s_socket()); SSL_set_bio(client_ssl_.get(), bio, bio); - StrictMock handshake_callbacks; + NiceMock handshake_callbacks; + NiceMock mock_connection; + + ON_CALL(handshake_callbacks, connection).WillByDefault(ReturnRef(mock_connection)); EXPECT_CALL(handshake_callbacks, onFailure); SslHandshakerImpl handshaker(std::move(server_ssl_), 0, &handshake_callbacks); diff --git a/test/extensions/transport_sockets/tls/utility_test.cc b/test/extensions/transport_sockets/tls/utility_test.cc index ca7f5cab7396..d1823952ac67 100644 --- a/test/extensions/transport_sockets/tls/utility_test.cc +++ b/test/extensions/transport_sockets/tls/utility_test.cc @@ -138,6 +138,39 @@ TEST(UtilityTest, TestGetCertificationExtensionValue) { EXPECT_EQ("", Utility::getCertificateExtensionValue(*cert, "foo")); } +TEST(UtilityTest, SslErrorDescriptionTest) { + const std::vector> test_set = { + {0, "NONE"}, + {1, "SSL"}, + {2, "WANT_READ"}, + {3, "WANT_WRITE"}, + {4, "WANT_X509_LOOKUP"}, + {5, "SYSCALL"}, + {6, "ZERO_RETURN"}, + {7, "WANT_CONNECT"}, + {8, "WANT_ACCEPT"}, + {9, "WANT_CHANNEL_ID_LOOKUP"}, + {11, "PENDING_SESSION"}, + {12, "PENDING_CERTIFICATE"}, + {13, "WANT_PRIVATE_KEY_OPERATION"}, + {14, "PENDING_TICKET"}, + {15, "EARLY_DATA_REJECTED"}, + {16, "WANT_CERTIFICATE_VERIFY"}, + {17, "HANDOFF"}, + {18, "HANDBACK"}, + }; + + for (const auto& test_data : test_set) { + EXPECT_EQ(test_data.second, Utility::getErrorDescription(test_data.first)); + } + +#if defined(NDEBUG) + EXPECT_EQ(Utility::getErrorDescription(19), "UNKNOWN_ERROR"); +#else + EXPECT_DEATH(Utility::getErrorDescription(19), "Unknown BoringSSL error had occurred"); +#endif +} + } // namespace } // namespace Tls } // namespace TransportSockets diff --git a/test/integration/BUILD b/test/integration/BUILD index dc921e3a137e..e3e1549221ca 100644 --- a/test/integration/BUILD +++ b/test/integration/BUILD @@ -877,6 +877,8 @@ envoy_cc_test( "//test/integration/filters:encoder_decoder_buffer_filter_lib", "//test/integration/filters:invalid_header_filter_lib", "//test/integration/filters:process_context_lib", + "//test/integration/filters:set_response_code_filter_config_proto_cc_proto", + "//test/integration/filters:set_response_code_filter_lib", "//test/integration/filters:stop_iteration_and_continue", "//test/mocks/http:http_mocks", "//test/test_common:utility_lib", @@ -1076,6 +1078,7 @@ envoy_cc_test( "//test/integration/filters:set_response_code_filter_config_proto_cc_proto", "//test/integration/filters:set_response_code_filter_lib", "//test/test_common:utility_lib", + "@envoy_api//envoy/extensions/common/matching/v3:pkg_cc_proto", "@envoy_api//envoy/extensions/filters/network/http_connection_manager/v3:pkg_cc_proto", "@envoy_api//envoy/service/extension/v3:pkg_cc_proto", ], @@ -1582,6 +1585,18 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "command_formatter_extension_integration_test", + srcs = [ + "command_formatter_extension_integration_test.cc", + ], + deps = [ + ":http_integration_lib", + "//test/common/formatter:command_extension_lib", + "//test/test_common:utility_lib", + ], +) + envoy_cc_test( name = "health_check_integration_test", srcs = ["health_check_integration_test.cc"], diff --git a/test/integration/ads_integration_test.cc b/test/integration/ads_integration_test.cc index 0a0a61a56034..87a3e87cce38 100644 --- a/test/integration/ads_integration_test.cc +++ b/test/integration/ads_integration_test.cc @@ -1467,6 +1467,124 @@ TEST_P(AdsIntegrationTestWithRtdsAndSecondaryClusters, Basic) { testBasicFlow(); } +class XdsTpAdsIntegrationTest : public AdsIntegrationTest { +public: + void initialize() override { + config_helper_.addConfigModifier([](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + bootstrap.add_node_context_params("id"); + bootstrap.add_node_context_params("cluster"); + bootstrap.mutable_dynamic_resources()->set_lds_resources_locator( + "xdstp://test/envoy.config.listener.v3.Listener/foo/*"); + auto* lds_config = bootstrap.mutable_dynamic_resources()->mutable_lds_config(); + lds_config->set_resource_api_version(envoy::config::core::v3::ApiVersion::V3); + lds_config->mutable_api_config_source()->set_api_type( + envoy::config::core::v3::ApiConfigSource::AGGREGATED_DELTA_GRPC); + lds_config->mutable_api_config_source()->set_transport_api_version( + envoy::config::core::v3::V3); + }); + AdsIntegrationTest::initialize(); + } +}; + +INSTANTIATE_TEST_SUITE_P( + IpVersionsClientTypeDelta, XdsTpAdsIntegrationTest, + testing::Combine(testing::ValuesIn(TestEnvironment::getIpVersionsForTest()), + // There should be no variation across clients. + testing::Values(Grpc::ClientType::EnvoyGrpc), + // Only delta xDS is supported for XdsTp + testing::Values(Grpc::SotwOrDelta::Delta))); + +TEST_P(XdsTpAdsIntegrationTest, Basic) { + initialize(); + // Basic CDS/EDS xDS initialization (not xdstp:// yet). + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "", {}, {}, {}, true)); + sendDiscoveryResponse(Config::TypeUrl::get().Cluster, {}, + {buildCluster("cluster_0")}, {}, "1"); + + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "", {}, + {"cluster_0"}, {})); + sendDiscoveryResponse( + Config::TypeUrl::get().ClusterLoadAssignment, {}, {buildClusterLoadAssignment("cluster_0")}, + {}, "1"); + + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Cluster, "1", {}, {}, {})); + + // LDS/RDS xDS initialization (LDS via xdstp:// glob collection) + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "", {}, + {"xdstp://test/envoy.config.listener.v3.Listener/foo/" + "*?xds.node.cluster=cluster_name&xds.node.id=node_name"}, + {})); + sendDiscoveryResponse( + Config::TypeUrl::get().Listener, {}, + { + buildListener("xdstp://test/envoy.config.listener.v3.Listener/foo/" + "bar?xds.node.cluster=cluster_name&xds.node.id=node_name", + "route_config_0"), + // Ignore resource in impostor namespace. + buildListener("xdstp://test/envoy.config.listener.v3.Listener/impostor/" + "bar?xds.node.cluster=cluster_name&xds.node.id=node_name", + "route_config_0"), + // Ignore non-matching context params. + buildListener("xdstp://test/envoy.config.listener.v3.Listener/foo/" + "baz?xds.node.cluster=cluster_name&xds.node.id=other_name", + "route_config_0"), + }, + {}, "1"); + + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().ClusterLoadAssignment, "1", + {"cluster_0"}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "", {}, + {"route_config_0"}, {})); + sendDiscoveryResponse( + Config::TypeUrl::get().RouteConfiguration, {}, + {buildRouteConfig("route_config_0", "cluster_0")}, {}, "1"); + + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "1", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", {}, {}, {})); + + test_server_->waitForCounterEq("listener_manager.listener_create_success", 1); + makeSingleRequest(); + + // Add a second listener in the foo namespace. + sendDiscoveryResponse( + Config::TypeUrl::get().Listener, {}, + {buildListener("xdstp://test/envoy.config.listener.v3.Listener/foo/" + "baz?xds.node.cluster=cluster_name&xds.node.id=node_name", + "route_config_1")}, + {}, "2"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "1", {}, + {"route_config_1"}, {})); + sendDiscoveryResponse( + Config::TypeUrl::get().RouteConfiguration, {}, + {buildRouteConfig("route_config_1", "cluster_0")}, {}, "2"); + + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "2", {}, {}, {})); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().RouteConfiguration, "2", {}, {}, {})); + test_server_->waitForCounterEq("listener_manager.listener_create_success", 2); + makeSingleRequest(); + + // Update bar listener in the foo namespace. + sendDiscoveryResponse( + Config::TypeUrl::get().Listener, {}, + {buildListener("xdstp://test/envoy.config.listener.v3.Listener/foo/" + "bar?xds.node.cluster=cluster_name&xds.node.id=node_name", + "route_config_1")}, + {}, "3"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "3", {}, {}, {})); + test_server_->waitForCounterEq("listener_manager.listener_in_place_updated", 1); + makeSingleRequest(); + + // Remove bar listener from the foo namespace. + sendDiscoveryResponse( + Config::TypeUrl::get().Listener, {}, {}, + {"xdstp://test/envoy.config.listener.v3.Listener/foo/" + "bar?xds.node.cluster=cluster_name&xds.node.id=node_name"}, + "3"); + EXPECT_TRUE(compareDiscoveryRequest(Config::TypeUrl::get().Listener, "4", {}, {}, {})); + test_server_->waitForCounterEq("listener_manager.listener_removed", 1); + makeSingleRequest(); +} + // Some v2 ADS integration tests, these validate basic v2 support but are not complete, they reflect // tests that have historically been worth validating on both v2 and v3. They will be removed in Q1. class AdsClusterV2Test : public AdsIntegrationTest { diff --git a/test/integration/autonomous_upstream.cc b/test/integration/autonomous_upstream.cc index 16d49cb50e60..762da275b31d 100644 --- a/test/integration/autonomous_upstream.cc +++ b/test/integration/autonomous_upstream.cc @@ -119,8 +119,7 @@ bool AutonomousUpstream::createNetworkFilterChain(Network::Connection& connectio shared_connections_.emplace_back(new SharedConnectionWrapper(connection)); AutonomousHttpConnectionPtr http_connection( new AutonomousHttpConnection(*this, *shared_connections_.back(), http_type_, *this)); - testing::AssertionResult result = http_connection->initialize(); - RELEASE_ASSERT(result, result.message()); + http_connection->initialize(); http_connections_.push_back(std::move(http_connection)); return true; } diff --git a/test/integration/command_formatter_extension_integration_test.cc b/test/integration/command_formatter_extension_integration_test.cc new file mode 100644 index 000000000000..3cc1a21527dc --- /dev/null +++ b/test/integration/command_formatter_extension_integration_test.cc @@ -0,0 +1,40 @@ +#include "test/common/formatter/command_extension.h" +#include "test/integration/http_integration.h" +#include "test/test_common/registry.h" +#include "test/test_common/utility.h" + +#include "gtest/gtest.h" + +using testing::HasSubstr; + +namespace Envoy { +namespace Formatter { + +class CommandFormatterExtensionIntegrationTest : public testing::Test, public HttpIntegrationTest { +public: + CommandFormatterExtensionIntegrationTest() + : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, Network::Address::IpVersion::v4) {} +}; + +TEST_F(CommandFormatterExtensionIntegrationTest, BasicExtension) { + TestCommandFactory factory; + Registry::InjectFactory command_register(factory); + std::vector formatters; + envoy::config::core::v3::TypedExtensionConfig typed_config; + ProtobufWkt::StringValue config; + + typed_config.set_name("envoy.formatter.TestFormatter"); + typed_config.mutable_typed_config()->PackFrom(config); + formatters.push_back(typed_config); + + useAccessLog("%COMMAND_EXTENSION()%", formatters); + initialize(); + std::string response; + sendRawHttpAndWaitForResponse(lookupPort("http"), "GET / HTTP/1.1\r\nHost: host\r\n\r\n", + &response, true); + std::string log = waitForAccessLog(access_log_name_); + EXPECT_THAT(log, HasSubstr("TestFormatter")); +} + +} // namespace Formatter +} // namespace Envoy diff --git a/test/integration/extension_discovery_integration_test.cc b/test/integration/extension_discovery_integration_test.cc index b671e6f94bd9..28f49421dacd 100644 --- a/test/integration/extension_discovery_integration_test.cc +++ b/test/integration/extension_discovery_integration_test.cc @@ -1,3 +1,4 @@ +#include "envoy/extensions/common/matching/v3/extension_matcher.pb.h" #include "envoy/extensions/filters/network/http_connection_manager/v3/http_connection_manager.pb.h" #include "envoy/service/extension/v3/config_discovery.pb.h" @@ -18,6 +19,32 @@ std::string denyPrivateConfig() { )EOF"; } +std::string denyPrivateConfigWithMatcher() { + return R"EOF( + "@type": type.googleapis.com/envoy.extensions.common.matching.v3.ExtensionWithMatcher + extension_config: + name: response-filter-config + typed_config: + "@type": type.googleapis.com/test.integration.filters.SetResponseCodeFilterConfig + prefix: "/private" + code: 403 + matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: some-header + exact_match_map: + map: + match: + action: + name: skip + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.common.matcher.action.v3.SkipFilter + )EOF"; +} + std::string allowAllConfig() { return "code: 200"; } std::string invalidConfig() { return "code: 90"; } @@ -29,9 +56,10 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara : HttpIntegrationTest(Http::CodecClient::Type::HTTP1, ipVersion()) {} void addDynamicFilter(const std::string& name, bool apply_without_warming, - bool set_default_config = true, bool rate_limit = false) { + bool set_default_config = true, bool rate_limit = false, + bool use_default_matcher = false) { config_helper_.addConfigModifier( - [this, name, apply_without_warming, set_default_config, rate_limit]( + [this, name, apply_without_warming, set_default_config, rate_limit, use_default_matcher]( envoy::extensions::filters::network::http_connection_manager::v3::HttpConnectionManager& http_connection_manager) { auto* filter = http_connection_manager.mutable_http_filters()->Add(); @@ -39,11 +67,41 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara auto* discovery = filter->mutable_config_discovery(); discovery->add_type_urls( "type.googleapis.com/test.integration.filters.SetResponseCodeFilterConfig"); + discovery->add_type_urls( + "type.googleapis.com/envoy.extensions.common.matching.v3.ExtensionWithMatcher"); if (set_default_config) { - const auto default_configuration = - TestUtility::parseYaml( - "code: 403"); - discovery->mutable_default_config()->PackFrom(default_configuration); + if (use_default_matcher) { + const auto default_configuration = TestUtility::parseYaml< + envoy::extensions::common::matching::v3::ExtensionWithMatcher>( + R"EOF( + extension_config: + name: set-response-code + typed_config: + "@type": type.googleapis.com/test.integration.filters.SetResponseCodeFilterConfig + code: 403 + matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: default-matcher-header + exact_match_map: + map: + match: + action: + name: skip + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.common.matcher.action.v3.SkipFilter + )EOF"); + + discovery->mutable_default_config()->PackFrom(default_configuration); + } else { + const auto default_configuration = + TestUtility::parseYaml( + "code: 403"); + discovery->mutable_default_config()->PackFrom(default_configuration); + } } discovery->set_apply_default_config_without_warming(apply_without_warming); discovery->mutable_config_source()->set_resource_api_version( @@ -125,6 +183,19 @@ class ExtensionDiscoveryIntegrationTest : public Grpc::GrpcClientIntegrationPara ecds_stream_->sendGrpcMessage(response); } + void sendXdsResponseWithFullYaml(const std::string& name, const std::string& version, + const std::string& full_yaml) { + envoy::service::discovery::v3::DiscoveryResponse response; + response.set_version_info(version); + response.set_type_url("type.googleapis.com/envoy.config.core.v3.TypedExtensionConfig"); + const auto configuration = TestUtility::parseYaml(full_yaml); + envoy::config::core::v3::TypedExtensionConfig typed_config; + typed_config.set_name(name); + typed_config.mutable_typed_config()->MergeFrom(configuration); + response.add_resources()->PackFrom(typed_config); + ecds_stream_->sendGrpcMessage(response); + } + FakeUpstream& getEcdsFakeUpstream() const { return *fake_upstreams_[1]; } FakeHttpConnectionPtr ecds_connection_{nullptr}; @@ -176,6 +247,81 @@ TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccess) { } } +TEST_P(ExtensionDiscoveryIntegrationTest, BasicSuccessWithMatcher) { + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("foo", false); + initialize(); + test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); + registerTestServerPorts({"http"}); + sendXdsResponseWithFullYaml("foo", "1", denyPrivateConfigWithMatcher()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.foo.config_reload", + 1); + test_server_->waitUntilListenersReady(); + test_server_->waitForGaugeGe("listener_manager.workers_started", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initialized); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + } + Http::TestRequestHeaderMapImpl banned_request_headers{ + {":method", "GET"}, {":path", "/private/key"}, {":scheme", "http"}, {":authority", "host"}}; + { + auto response = codec_client_->makeHeaderOnlyRequest(banned_request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("403", response->headers().getStatusValue()); + } + Http::TestRequestHeaderMapImpl banned_request_headers_skipped{{":method", "GET"}, + {":path", "/private/key"}, + {"some-header", "match"}, + {":scheme", "http"}, + {":authority", "host"}}; + { + auto response = codec_client_->makeHeaderOnlyRequest(banned_request_headers_skipped); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); + } +} + +TEST_P(ExtensionDiscoveryIntegrationTest, BasicDefaultMatcher) { + on_server_init_function_ = [&]() { waitXdsStream(); }; + addDynamicFilter("foo", false, true, false, true); + initialize(); + test_server_->waitForCounterGe("listener_manager.lds.update_success", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initializing); + registerTestServerPorts({"http"}); + sendXdsResponse("foo", "1", invalidConfig()); + test_server_->waitForCounterGe("http.config_test.extension_config_discovery.foo.config_fail", 1); + test_server_->waitUntilListenersReady(); + test_server_->waitForGaugeGe("listener_manager.workers_started", 1); + EXPECT_EQ(test_server_->server().initManager().state(), Init::Manager::State::Initialized); + codec_client_ = makeHttpConnection(makeClientConnection((lookupPort("http")))); + { + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "GET"}, {":path", "/"}, {":scheme", "http"}, {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("403", response->headers().getStatusValue()); + } + Http::TestRequestHeaderMapImpl request_headers{{":method", "GET"}, + {"default-matcher-header", "match"}, + {":path", "/"}, + {":scheme", "http"}, + {":authority", "host"}}; + auto response = codec_client_->makeHeaderOnlyRequest(request_headers); + response->waitForEndStream(); + ASSERT_TRUE(response->complete()); + EXPECT_EQ("200", response->headers().getStatusValue()); +} + TEST_P(ExtensionDiscoveryIntegrationTest, BasicFailWithDefault) { on_server_init_function_ = [&]() { waitXdsStream(); }; addDynamicFilter("foo", false); diff --git a/test/integration/fake_upstream.cc b/test/integration/fake_upstream.cc index 0b18b9e8c9b6..d5545b128445 100644 --- a/test/integration/fake_upstream.cc +++ b/test/integration/fake_upstream.cc @@ -20,6 +20,7 @@ #include "test/test_common/utility.h" #include "absl/strings/str_cat.h" +#include "absl/synchronization/notification.h" using namespace std::chrono_literals; @@ -358,12 +359,6 @@ AssertionResult FakeConnectionBase::readDisable(bool disable, std::chrono::milli [disable](Network::Connection& connection) { connection.readDisable(disable); }, timeout); } -AssertionResult FakeConnectionBase::enableHalfClose(bool enable, - std::chrono::milliseconds timeout) { - return shared_connection_.executeOnDispatcher( - [enable](Network::Connection& connection) { connection.enableHalfClose(enable); }, timeout); -} - Http::RequestDecoder& FakeHttpConnection::newStream(Http::ResponseEncoder& encoder, bool) { absl::MutexLock lock(&lock_); new_streams_.emplace_back(new FakeStream(*this, encoder, time_system_)); @@ -377,6 +372,24 @@ void FakeHttpConnection::onGoAway(Http::GoAwayErrorCode code) { ENVOY_LOG(info, "FakeHttpConnection receives GOAWAY: ", code); } +void FakeHttpConnection::encodeGoAway() { + ASSERT(type_ == Type::HTTP2); + + shared_connection_.connection().dispatcher().post([this]() { codec_->goAway(); }); +} + +void FakeHttpConnection::encodeProtocolError() { + ASSERT(type_ == Type::HTTP2); + + Http::Http2::ServerConnectionImpl* codec = + dynamic_cast(codec_.get()); + ASSERT(codec != nullptr); + shared_connection_.connection().dispatcher().post([codec]() { + Http::Status status = codec->protocolErrorForTest(); + ASSERT(Http::getStatusCode(status) == Http::StatusCode::CodecProtocolError); + }); +} + AssertionResult FakeConnectionBase::waitForDisconnect(milliseconds timeout) { ENVOY_LOG(trace, "FakeConnectionBase waiting for disconnect"); absl::MutexLock lock(&lock_); @@ -513,6 +526,9 @@ bool FakeUpstream::createNetworkFilterChain(Network::Connection& connection, const std::vector&) { absl::MutexLock lock(&lock_); if (read_disable_on_new_connection_) { + // Disable early close detection to avoid closing the network connection before full + // initialization is complete. + connection.detectEarlyCloseWhenReadDisabled(false); connection.readDisable(true); } auto connection_wrapper = std::make_unique(connection); @@ -552,16 +568,16 @@ AssertionResult FakeUpstream::waitForHttpConnection( client_dispatcher, timeout)) { return AssertionFailure() << "Timed out waiting for new connection."; } + } + return runOnDispatcherThreadAndWait([&]() { + absl::MutexLock lock(&lock_); connection = std::make_unique( *this, consumeConnection(), http_type_, time_system_, max_request_headers_kb, max_request_headers_count, headers_with_underscores_action); - } - VERIFY_ASSERTION(connection->initialize()); - if (read_disable_on_new_connection_) { - VERIFY_ASSERTION(connection->readDisable(false)); - } - return AssertionSuccess(); + connection->initialize(); + return AssertionSuccess(); + }); } AssertionResult @@ -585,14 +601,17 @@ FakeUpstream::waitForHttpConnection(Event::Dispatcher& client_dispatcher, client_dispatcher, 5ms)) { continue; } + } + + return upstream.runOnDispatcherThreadAndWait([&]() { + absl::MutexLock lock(&upstream.lock_); connection = std::make_unique( upstream, upstream.consumeConnection(), upstream.http_type_, upstream.timeSystem(), Http::DEFAULT_MAX_REQUEST_HEADERS_KB, Http::DEFAULT_MAX_HEADERS_COUNT, envoy::config::core::v3::HttpProtocolOptions::ALLOW); - } - VERIFY_ASSERTION(connection->initialize()); - VERIFY_ASSERTION(connection->readDisable(false)); - return AssertionSuccess(); + connection->initialize(); + return AssertionSuccess(); + }); } } return AssertionFailure() << "Timed out waiting for HTTP connection."; @@ -610,19 +629,35 @@ AssertionResult FakeUpstream::waitForRawConnection(FakeRawConnectionPtr& connect if (!time_system_.waitFor(lock_, absl::Condition(&reached), timeout)) { return AssertionFailure() << "Timed out waiting for raw connection"; } - connection = std::make_unique(consumeConnection(), timeSystem()); } - VERIFY_ASSERTION(connection->initialize()); - VERIFY_ASSERTION(connection->readDisable(false)); - VERIFY_ASSERTION(connection->enableHalfClose(enable_half_close_)); - return AssertionSuccess(); + + return runOnDispatcherThreadAndWait([&]() { + absl::MutexLock lock(&lock_); + connection = std::make_unique(consumeConnection(), timeSystem()); + connection->initialize(); + // Skip enableHalfClose if the connection is already disconnected. + if (connection->connected()) { + connection->connection().enableHalfClose(enable_half_close_); + } + return AssertionSuccess(); + }); } SharedConnectionWrapper& FakeUpstream::consumeConnection() { ASSERT(!new_connections_.empty()); auto* const connection_wrapper = new_connections_.front().get(); + // Skip the thread safety check if the network connection has already been freed since there's no + // alternate way to get access to the dispatcher. + ASSERT(!connection_wrapper->connected() || + connection_wrapper->connection().dispatcher().isThreadSafe()); connection_wrapper->setParented(); connection_wrapper->moveBetweenLists(new_connections_, consumed_connections_); + if (read_disable_on_new_connection_ && connection_wrapper->connected()) { + // Re-enable read and early close detection. + auto& connection = connection_wrapper->connection(); + connection.detectEarlyCloseWhenReadDisabled(true); + connection.readDisable(false); + } return *connection_wrapper; } @@ -647,6 +682,20 @@ void FakeUpstream::onRecvDatagram(Network::UdpRecvData& data) { received_datagrams_.emplace_back(std::move(data)); } +AssertionResult FakeUpstream::runOnDispatcherThreadAndWait(std::function cb, + std::chrono::milliseconds timeout) { + auto result = std::make_shared(AssertionSuccess()); + auto done = std::make_shared(); + ASSERT(!dispatcher_->isThreadSafe()); + dispatcher_->post([&]() { + *result = cb(); + done->Notify(); + }); + RELEASE_ASSERT(done->WaitForNotificationWithTimeout(absl::FromChrono(timeout)), + "Timed out waiting for cb to run on dispatcher"); + return *result; +} + void FakeUpstream::sendUdpDatagram(const std::string& buffer, const Network::Address::InstanceConstSharedPtr& peer) { dispatcher_->post([this, buffer, peer] { @@ -682,17 +731,16 @@ FakeRawConnection::~FakeRawConnection() { } } -testing::AssertionResult FakeRawConnection::initialize() { - auto filter = Network::ReadFilterSharedPtr{new ReadFilter(*this)}; +void FakeRawConnection::initialize() { + FakeConnectionBase::initialize(); + Network::ReadFilterSharedPtr filter{new ReadFilter(*this)}; read_filter_ = filter; - testing::AssertionResult result = shared_connection_.executeOnDispatcher( - [filter = std::move(filter)](Network::Connection& connection) { - connection.addReadFilter(filter); - }); - if (!result) { - return result; + if (!shared_connection_.connected()) { + ENVOY_LOG(warn, "FakeRawConnection::initialize: network connection is already disconnected"); + return; } - return FakeConnectionBase::initialize(); + ASSERT(shared_connection_.connection().dispatcher().isThreadSafe()); + shared_connection_.connection().addReadFilter(filter); } AssertionResult FakeRawConnection::waitForData(uint64_t num_bytes, std::string* data, diff --git a/test/integration/fake_upstream.h b/test/integration/fake_upstream.h index d68f86677764..1eb98c10c7f1 100644 --- a/test/integration/fake_upstream.h +++ b/test/integration/fake_upstream.h @@ -257,17 +257,6 @@ class SharedConnectionWrapper : public Network::ConnectionCallbacks, connection_.addConnectionCallbacks(*this); } - Common::CallbackHandle* addDisconnectCallback(DisconnectCallback callback) { - absl::MutexLock lock(&lock_); - return disconnect_callback_manager_.add(callback); - } - - // Avoid directly removing by caller, since CallbackManager is not thread safe. - void removeDisconnectCallback(Common::CallbackHandle* handle) { - absl::MutexLock lock(&lock_); - handle->remove(); - } - // Network::ConnectionCallbacks void onEvent(Network::ConnectionEvent event) override { // Throughout this entire function, we know that the connection_ cannot disappear, since this @@ -278,7 +267,6 @@ class SharedConnectionWrapper : public Network::ConnectionCallbacks, if (event == Network::ConnectionEvent::RemoteClose || event == Network::ConnectionEvent::LocalClose) { disconnected_ = true; - disconnect_callback_manager_.runCallbacks(); } } @@ -315,6 +303,10 @@ class SharedConnectionWrapper : public Network::ConnectionCallbacks, if (disconnected_) { return testing::AssertionSuccess(); } + // Sanity check: detect if the post and wait is attempted from the dispatcher thread; fail + // immediately instead of deadlocking. + ASSERT(!connection_.dispatcher().isThreadSafe(), + "deadlock: executeOnDispatcher called from dispatcher thread."); bool callback_ready_event = false; bool unexpected_disconnect = false; connection_.dispatcher().post( @@ -345,13 +337,13 @@ class SharedConnectionWrapper : public Network::ConnectionCallbacks, void setParented() { absl::MutexLock lock(&lock_); + ASSERT(!parented_); parented_ = true; } private: Network::Connection& connection_; absl::Mutex lock_; - Common::CallbackManager<> disconnect_callback_manager_ ABSL_GUARDED_BY(lock_); bool parented_ ABSL_GUARDED_BY(lock_){}; bool disconnected_ ABSL_GUARDED_BY(lock_){}; }; @@ -363,7 +355,10 @@ using SharedConnectionWrapperPtr = std::unique_ptr; */ class FakeConnectionBase : public Logger::Loggable { public: - virtual ~FakeConnectionBase() { ASSERT(initialized_); } + virtual ~FakeConnectionBase() { + absl::MutexLock lock(&lock_); + ASSERT(initialized_); + } ABSL_MUST_USE_RESULT testing::AssertionResult close(std::chrono::milliseconds timeout = TestUtility::DefaultTimeout); @@ -380,14 +375,10 @@ class FakeConnectionBase : public Logger::Loggable { testing::AssertionResult waitForHalfClose(std::chrono::milliseconds timeout = TestUtility::DefaultTimeout); - ABSL_MUST_USE_RESULT - virtual testing::AssertionResult initialize() { + virtual void initialize() { + absl::MutexLock lock(&lock_); initialized_ = true; - return testing::AssertionSuccess(); } - ABSL_MUST_USE_RESULT - testing::AssertionResult - enableHalfClose(bool enabled, std::chrono::milliseconds timeout = TestUtility::DefaultTimeout); // The same caveats apply here as in SharedConnectionWrapper::connection(). Network::Connection& connection() const { return shared_connection_.connection(); } bool connected() const { return shared_connection_.connected(); } @@ -398,9 +389,9 @@ class FakeConnectionBase : public Logger::Loggable { time_system_(time_system) {} SharedConnectionWrapper& shared_connection_; - bool initialized_{}; absl::Mutex& lock_; // TODO(mattklein123): Use the shared connection lock and figure out better // guarded by annotations. + bool initialized_ ABSL_GUARDED_BY(lock_){}; bool half_closed_ ABSL_GUARDED_BY(lock_){}; Event::TestTimeSystem& time_system_; }; @@ -428,6 +419,12 @@ class FakeHttpConnection : public Http::ServerConnectionCallbacks, public FakeCo // Should only be called for HTTP2 void onGoAway(Http::GoAwayErrorCode code) override; + // Should only be called for HTTP2, sends a GOAWAY frame with NO_ERROR. + void encodeGoAway(); + + // Should only be called for HTTP2, sends a GOAWAY frame with ENHANCE_YOUR_CALM. + void encodeProtocolError(); + private: struct ReadFilter : public Network::ReadFilterBaseImpl { ReadFilter(FakeHttpConnection& parent) : parent_(parent) {} @@ -493,8 +490,7 @@ class FakeRawConnection : public FakeConnectionBase { testing::AssertionResult write(const std::string& data, bool end_stream = false, std::chrono::milliseconds timeout = TestUtility::DefaultTimeout); - ABSL_MUST_USE_RESULT - testing::AssertionResult initialize() override; + void initialize() override; // Creates a ValidatorFunction which returns true when data_to_wait_for is // contained in the incoming data string. Unlike many of Envoy waitFor functions, @@ -736,6 +732,9 @@ class FakeUpstream : Logger::Loggable, void threadRoutine(); SharedConnectionWrapper& consumeConnection() ABSL_EXCLUSIVE_LOCKS_REQUIRED(lock_); void onRecvDatagram(Network::UdpRecvData& data); + AssertionResult + runOnDispatcherThreadAndWait(std::function cb, + std::chrono::milliseconds timeout = TestUtility::DefaultTimeout); Network::SocketSharedPtr socket_; Network::ListenSocketFactorySharedPtr socket_factory_; diff --git a/test/integration/hds_integration_test.cc b/test/integration/hds_integration_test.cc index c473226d73f1..85ebbc9b8d31 100644 --- a/test/integration/hds_integration_test.cc +++ b/test/integration/hds_integration_test.cc @@ -374,7 +374,8 @@ class HdsIntegrationTest : public Grpc::VersionedGrpcClientIntegrationParamTest, }; INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, HdsIntegrationTest, - VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::VersionedGrpcClientIntegrationParamTest::protocolTestParamsToString); // Tests Envoy HTTP health checking a single healthy endpoint and reporting that it is // indeed healthy to the server. diff --git a/test/integration/health_check_integration_test.cc b/test/integration/health_check_integration_test.cc index a129e4947543..f6a403805351 100644 --- a/test/integration/health_check_integration_test.cc +++ b/test/integration/health_check_integration_test.cc @@ -15,8 +15,7 @@ namespace { // Integration tests for active health checking. // The tests fetch the cluster configuration using CDS in order to actively start health // checking after Envoy and the hosts are initialized. -class HealthCheckIntegrationTestBase : public Event::TestUsingSimulatedTime, - public HttpIntegrationTest { +class HealthCheckIntegrationTestBase : public HttpIntegrationTest { public: HealthCheckIntegrationTestBase( Network::Address::IpVersion ip_version, @@ -135,11 +134,11 @@ struct HttpHealthCheckIntegrationTestParams { FakeHttpConnection::Type upstream_protocol; }; -class HttpHealthCheckIntegrationTest +class HttpHealthCheckIntegrationTestBase : public testing::TestWithParam, public HealthCheckIntegrationTestBase { public: - HttpHealthCheckIntegrationTest() + HttpHealthCheckIntegrationTestBase() : HealthCheckIntegrationTestBase(GetParam().ip_version, GetParam().upstream_protocol) {} // Returns the 4 combinations for testing: @@ -201,11 +200,21 @@ class HttpHealthCheckIntegrationTest } }; +class HttpHealthCheckIntegrationTest : public Event::TestUsingSimulatedTime, + public HttpHealthCheckIntegrationTestBase {}; + INSTANTIATE_TEST_SUITE_P( IpHttpVersions, HttpHealthCheckIntegrationTest, testing::ValuesIn(HttpHealthCheckIntegrationTest::getHttpHealthCheckIntegrationTestParams()), HttpHealthCheckIntegrationTest::httpHealthCheckTestParamsToString); +class RealTimeHttpHealthCheckIntegrationTest : public HttpHealthCheckIntegrationTestBase {}; + +INSTANTIATE_TEST_SUITE_P( + IpHttpVersions, RealTimeHttpHealthCheckIntegrationTest, + testing::ValuesIn(HttpHealthCheckIntegrationTest::getHttpHealthCheckIntegrationTestParams()), + HttpHealthCheckIntegrationTest::httpHealthCheckTestParamsToString); + // Tests that a healthy endpoint returns a valid HTTP health check response. TEST_P(HttpHealthCheckIntegrationTest, SingleEndpointHealthyHttp) { const uint32_t cluster_idx = 0; @@ -254,6 +263,111 @@ TEST_P(HttpHealthCheckIntegrationTest, SingleEndpointTimeoutHttp) { EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.failure")->value()); } +// Tests that health checking gracefully handles a NO_ERROR GOAWAY from the upstream. +TEST_P(HttpHealthCheckIntegrationTest, SingleEndpointGoAway) { + initialize(); + + // GOAWAY doesn't exist in HTTP1. + if (upstream_protocol_ == FakeHttpConnection::Type::HTTP1) { + return; + } + + const uint32_t cluster_idx = 0; + initHttpHealthCheck(cluster_idx); + + // Send a GOAWAY with NO_ERROR and then a 200. The health checker should allow the request + // to finish despite the GOAWAY. + clusters_[cluster_idx].host_fake_connection_->encodeGoAway(); + clusters_[cluster_idx].host_stream_->encodeHeaders( + Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + + test_server_->waitForCounterGe("cluster.cluster_1.health_check.success", 1); + EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.success")->value()); + EXPECT_EQ(0, test_server_->counter("cluster.cluster_1.health_check.failure")->value()); + ASSERT_TRUE(clusters_[cluster_idx].host_fake_connection_->waitForDisconnect()); + + // Advance time to cause another health check. + timeSystem().advanceTimeWait(std::chrono::milliseconds(500)); + + ASSERT_TRUE(clusters_[cluster_idx].host_upstream_->waitForHttpConnection( + *dispatcher_, clusters_[cluster_idx].host_fake_connection_)); + ASSERT_TRUE(clusters_[cluster_idx].host_fake_connection_->waitForNewStream( + *dispatcher_, clusters_[cluster_idx].host_stream_)); + ASSERT_TRUE(clusters_[cluster_idx].host_stream_->waitForEndStream(*dispatcher_)); + + EXPECT_EQ(clusters_[cluster_idx].host_stream_->headers().getPathValue(), "/healthcheck"); + EXPECT_EQ(clusters_[cluster_idx].host_stream_->headers().getMethodValue(), "GET"); + EXPECT_EQ(clusters_[cluster_idx].host_stream_->headers().getHostValue(), + clusters_[cluster_idx].name_); + + clusters_[cluster_idx].host_stream_->encodeHeaders( + Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + + test_server_->waitForCounterGe("cluster.cluster_1.health_check.success", 2); + EXPECT_EQ(2, test_server_->counter("cluster.cluster_1.health_check.success")->value()); + EXPECT_EQ(0, test_server_->counter("cluster.cluster_1.health_check.failure")->value()); +} + +// Tests that health checking properly handles a GOAWAY with an error, followed +// by a reset. This test uses the real time system because it flakes with +// simulated time. +// The test goes through this sequence: +// 1) send a GOAWAY with a PROTOCOL_ERROR code from the upstream +// 2) waitForDisconnect on the health check connection +// 3) advance time to trigger another health check +// 4) wait for a new health check on a new connection. +// +// The flake was caused by the GOAWAY simultaneously causing the downstream +// health checker to close the connection (because of the GOAWAY) and the fake +// upstream to also close the connection (because of special handling for +// protocol errors). This meant that waitForDisconnect() could finish waiting +// before the health checker saw the GOAWAY and enabled the health check +// interval timer. This would cause simulated time to advance too early, and no +// followup health check would happen. Using real time solves this because then +// the ordering of advancing the time system and enabling the health check timer +// is inconsequential. +TEST_P(RealTimeHttpHealthCheckIntegrationTest, SingleEndpointGoAwayErroSingleEndpointGoAwayErrorr) { + initialize(); + + // GOAWAY doesn't exist in HTTP1. + if (upstream_protocol_ == FakeHttpConnection::Type::HTTP1) { + return; + } + + const uint32_t cluster_idx = 0; + initHttpHealthCheck(cluster_idx); + + // Send a GOAWAY with an error. The health checker should treat this as an + // error and cancel the request. + clusters_[cluster_idx].host_fake_connection_->encodeProtocolError(); + + ASSERT_TRUE(clusters_[cluster_idx].host_fake_connection_->waitForDisconnect()); + test_server_->waitForCounterGe("cluster.cluster_1.health_check.failure", 1); + EXPECT_EQ(0, test_server_->counter("cluster.cluster_1.health_check.success")->value()); + EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.failure")->value()); + + // Advance time to cause another health check. + timeSystem().advanceTimeWait(std::chrono::milliseconds(500)); + + ASSERT_TRUE(clusters_[cluster_idx].host_upstream_->waitForHttpConnection( + *dispatcher_, clusters_[cluster_idx].host_fake_connection_)); + ASSERT_TRUE(clusters_[cluster_idx].host_fake_connection_->waitForNewStream( + *dispatcher_, clusters_[cluster_idx].host_stream_)); + ASSERT_TRUE(clusters_[cluster_idx].host_stream_->waitForEndStream(*dispatcher_)); + + EXPECT_EQ(clusters_[cluster_idx].host_stream_->headers().getPathValue(), "/healthcheck"); + EXPECT_EQ(clusters_[cluster_idx].host_stream_->headers().getMethodValue(), "GET"); + EXPECT_EQ(clusters_[cluster_idx].host_stream_->headers().getHostValue(), + clusters_[cluster_idx].name_); + + clusters_[cluster_idx].host_stream_->encodeHeaders( + Http::TestResponseHeaderMapImpl{{":status", "200"}}, true); + + test_server_->waitForCounterGe("cluster.cluster_1.health_check.success", 1); + EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.success")->value()); + EXPECT_EQ(1, test_server_->counter("cluster.cluster_1.health_check.failure")->value()); +} + class TcpHealthCheckIntegrationTest : public testing::TestWithParam, public HealthCheckIntegrationTestBase { public: diff --git a/test/integration/http2_flood_integration_test.cc b/test/integration/http2_flood_integration_test.cc index ede1f86253be..96267012d99c 100644 --- a/test/integration/http2_flood_integration_test.cc +++ b/test/integration/http2_flood_integration_test.cc @@ -36,24 +36,33 @@ class SocketInterfaceSwap { // Object of this class hold the state determining the IoHandle which // should return EAGAIN from the `writev` call. struct IoHandleMatcher { - bool shouldReturnEgain(uint32_t port) const { + bool shouldReturnEgain(uint32_t src_port, uint32_t dst_port) const { absl::ReaderMutexLock lock(&mutex_); - return port == port_ && writev_returns_egain_; + return writev_returns_egain_ && (src_port == src_port_ || dst_port == dst_port_); } + // Source port to match. The port specified should be associated with a listener. void setSourcePort(uint32_t port) { absl::WriterMutexLock lock(&mutex_); - port_ = port; + src_port_ = port; + } + + // Destination port to match. The port specified should be associated with a listener. + void setDestinationPort(uint32_t port) { + absl::WriterMutexLock lock(&mutex_); + dst_port_ = port; } void setWritevReturnsEgain() { absl::WriterMutexLock lock(&mutex_); + ASSERT(src_port_ != 0 || dst_port_ != 0); writev_returns_egain_ = true; } private: mutable absl::Mutex mutex_; - uint32_t port_ ABSL_GUARDED_BY(mutex_) = 0; + uint32_t src_port_ ABSL_GUARDED_BY(mutex_) = 0; + uint32_t dst_port_ ABSL_GUARDED_BY(mutex_) = 0; bool writev_returns_egain_ ABSL_GUARDED_BY(mutex_) = false; }; @@ -64,7 +73,8 @@ class SocketInterfaceSwap { [writev_matcher = writev_matcher_]( Envoy::Network::TestIoSocketHandle* io_handle, const Buffer::RawSlice*, uint64_t) -> absl::optional { - if (writev_matcher->shouldReturnEgain(io_handle->localAddress()->ip()->port())) { + if (writev_matcher->shouldReturnEgain(io_handle->localAddress()->ip()->port(), + io_handle->peerAddress()->ip()->port())) { return Api::IoCallUint64Result( 0, Api::IoErrorPtr(Network::IoSocketError::getIoSocketEagainInstance(), Network::IoSocketError::deleteIoError)); @@ -121,8 +131,6 @@ INSTANTIATE_TEST_SUITE_P(IpVersions, Http2FloodMitigationTest, TestUtility::ipTestParamsToString); bool Http2FloodMitigationTest::initializeUpstreamFloodTest() { - config_helper_.addRuntimeOverride("envoy.reloadable_features.upstream_http2_flood_checks", - "true"); setDownstreamProtocol(Http::CodecClient::Type::HTTP2); setUpstreamProtocol(FakeHttpConnection::Type::HTTP2); // set lower upstream outbound frame limits to make tests run faster @@ -168,8 +176,6 @@ void Http2FloodMitigationTest::beginSession() { std::vector Http2FloodMitigationTest::serializeFrames(const Http2Frame& frame, uint32_t num_frames) { - // make sure all frames can fit into 16k buffer - ASSERT(num_frames <= ((16u * 1024u) / frame.size())); std::vector buf(num_frames * frame.size()); for (auto pos = buf.begin(); pos != buf.end();) { pos = std::copy(frame.begin(), frame.end(), pos); @@ -200,8 +206,7 @@ void Http2FloodMitigationTest::floodClient(const Http2Frame& frame, uint32_t num waitForNextUpstreamRequest(); // Make Envoy's writes into the upstream connection to return EAGAIN - writev_matcher_->setSourcePort( - fake_upstream_connection_->connection().addressProvider().remoteAddress()->ip()->port()); + writev_matcher_->setDestinationPort(fake_upstreams_[0]->localAddress()->ip()->port()); auto buf = serializeFrames(frame, num_frames); @@ -303,8 +308,7 @@ Http2FloodMitigationTest::prefillOutboundUpstreamQueue(uint32_t frame_count) { EXPECT_TRUE(upstream_request_->waitForData(*dispatcher_, 1)); // Make Envoy's writes into the upstream connection to return EAGAIN - writev_matcher_->setSourcePort( - fake_upstream_connection_->connection().addressProvider().remoteAddress()->ip()->port()); + writev_matcher_->setDestinationPort(fake_upstreams_[0]->localAddress()->ip()->port()); auto buf = serializeFrames(Http2Frame::makePingFrame(), frame_count); @@ -407,7 +411,7 @@ TEST_P(Http2FloodMitigationTest, Data) { EXPECT_GE(22000, buffer_factory->sumMaxBufferSizes()); // Verify that all buffers have watermarks set. EXPECT_THAT(buffer_factory->highWatermarkRange(), - testing::Pair(1024 * 1024 * 1024 + 1, 1024 * 1024 * 1024 + 1)); + testing::Pair(1024 * 1024 * 1024, 1024 * 1024 * 1024)); } // Verify that the server can detect flood triggered by a DATA frame from a decoder filter call @@ -1526,8 +1530,7 @@ TEST_P(Http2FloodMitigationTest, RequestMetadata) { // Make Envoy's writes into the upstream connection to return EAGAIN, preventing proxying of the // METADATA frames - writev_matcher_->setSourcePort( - fake_upstream_connection_->connection().addressProvider().remoteAddress()->ip()->port()); + writev_matcher_->setDestinationPort(fake_upstreams_[0]->localAddress()->ip()->port()); writev_matcher_->setWritevReturnsEgain(); @@ -1550,4 +1553,15 @@ TEST_P(Http2FloodMitigationTest, RequestMetadata) { EXPECT_EQ(1, test_server_->counter("cluster.cluster_0.http2.outbound_flood")->value()); } +// Validate that the default configuration has flood protection enabled. +TEST_P(Http2FloodMitigationTest, UpstreamFloodDetectionIsOnByDefault) { + setDownstreamProtocol(Http::CodecClient::Type::HTTP2); + setUpstreamProtocol(FakeHttpConnection::Type::HTTP2); + initialize(); + + floodClient(Http2Frame::makePingFrame(), + Http2::Utility::OptionsLimits::DEFAULT_MAX_OUTBOUND_CONTROL_FRAMES + 1, + "cluster.cluster_0.http2.outbound_control_flood"); +} + } // namespace Envoy diff --git a/test/integration/http2_integration_test.cc b/test/integration/http2_integration_test.cc index a0e73bdbde68..95ed212e7e2c 100644 --- a/test/integration/http2_integration_test.cc +++ b/test/integration/http2_integration_test.cc @@ -111,7 +111,9 @@ TEST_P(Http2IntegrationTest, CodecStreamIdleTimeout) { hcm.mutable_stream_idle_timeout()->set_nanos(IdleTimeoutMs * 1000 * 1000); }); initialize(); - envoy::config::core::v3::Http2ProtocolOptions http2_options; + envoy::config::core::v3::Http2ProtocolOptions http2_options = + ::Envoy::Http2::Utility::initializeAndValidateOptions( + envoy::config::core::v3::Http2ProtocolOptions()); http2_options.mutable_initial_stream_window_size()->set_value(65535); codec_client_ = makeRawHttpConnection(makeClientConnection(lookupPort("http")), http2_options); auto response = codec_client_->makeHeaderOnlyRequest(default_request_headers_); diff --git a/test/integration/http_integration.cc b/test/integration/http_integration.cc index 3e77ffef8e24..f3044030a16b 100644 --- a/test/integration/http_integration.cc +++ b/test/integration/http_integration.cc @@ -263,9 +263,11 @@ HttpIntegrationTest::HttpIntegrationTest(Http::CodecClient::Type downstream_prot config_helper_.setClientCodec(typeToCodecType(downstream_protocol_)); } -void HttpIntegrationTest::useAccessLog(absl::string_view format) { +void HttpIntegrationTest::useAccessLog( + absl::string_view format, + std::vector formatters) { access_log_name_ = TestEnvironment::temporaryPath(TestUtility::uniqueFilename()); - ASSERT_TRUE(config_helper_.setAccessLog(access_log_name_, format)); + ASSERT_TRUE(config_helper_.setAccessLog(access_log_name_, format, formatters)); } HttpIntegrationTest::~HttpIntegrationTest() { cleanupUpstreamAndDownstream(); } @@ -367,31 +369,39 @@ void HttpIntegrationTest::verifyResponse(IntegrationStreamDecoderPtr response, EXPECT_EQ(response->body(), expected_body); } +absl::optional HttpIntegrationTest::waitForNextUpstreamConnection( + const std::vector& upstream_indices, + std::chrono::milliseconds connection_wait_timeout, + FakeHttpConnectionPtr& fake_upstream_connection) { + AssertionResult result = AssertionFailure(); + int upstream_index = 0; + Event::TestTimeSystem::RealTimeBound bound(connection_wait_timeout); + // Loop over the upstreams until the call times out or an upstream request is received. + while (!result) { + upstream_index = upstream_index % upstream_indices.size(); + result = fake_upstreams_[upstream_indices[upstream_index]]->waitForHttpConnection( + *dispatcher_, fake_upstream_connection, std::chrono::milliseconds(5), + max_request_headers_kb_, max_request_headers_count_); + if (result) { + return upstream_index; + } else if (!bound.withinBound()) { + RELEASE_ASSERT(0, "Timed out waiting for new connection."); + break; + } + ++upstream_index; + } + RELEASE_ASSERT(result, result.message()); + return {}; +} + absl::optional HttpIntegrationTest::waitForNextUpstreamRequest(const std::vector& upstream_indices, std::chrono::milliseconds connection_wait_timeout) { absl::optional upstream_with_request; // If there is no upstream connection, wait for it to be established. if (!fake_upstream_connection_) { - AssertionResult result = AssertionFailure(); - int upstream_index = 0; - Event::TestTimeSystem::RealTimeBound bound(connection_wait_timeout); - // Loop over the upstreams until the call times out or an upstream request is received. - while (!result) { - upstream_index = upstream_index % upstream_indices.size(); - result = fake_upstreams_[upstream_indices[upstream_index]]->waitForHttpConnection( - *dispatcher_, fake_upstream_connection_, std::chrono::milliseconds(5), - max_request_headers_kb_, max_request_headers_count_); - if (result) { - upstream_with_request = upstream_index; - break; - } else if (!bound.withinBound()) { - result = (AssertionFailure() << "Timed out waiting for new connection."); - break; - } - ++upstream_index; - } - RELEASE_ASSERT(result, result.message()); + upstream_with_request = waitForNextUpstreamConnection(upstream_indices, connection_wait_timeout, + fake_upstream_connection_); } // Wait for the next stream on the upstream connection. AssertionResult result = diff --git a/test/integration/http_integration.h b/test/integration/http_integration.h index 457d81e666fe..5ee7de657997 100644 --- a/test/integration/http_integration.h +++ b/test/integration/http_integration.h @@ -106,7 +106,8 @@ class HttpIntegrationTest : public BaseIntegrationTest { ~HttpIntegrationTest() override; protected: - void useAccessLog(absl::string_view format = ""); + void useAccessLog(absl::string_view format = "", + std::vector formatters = {}); IntegrationCodecClientPtr makeHttpConnection(uint32_t port); // Makes a http connection object without checking its connected state. @@ -148,6 +149,11 @@ class HttpIntegrationTest : public BaseIntegrationTest { uint64_t upstream_index = 0, std::chrono::milliseconds connection_wait_timeout = TestUtility::DefaultTimeout); + absl::optional + waitForNextUpstreamConnection(const std::vector& upstream_indices, + std::chrono::milliseconds connection_wait_timeout, + FakeHttpConnectionPtr& fake_upstream_connection); + // Close |codec_client_| and |fake_upstream_connection_| cleanly. void cleanupUpstreamAndDownstream(); diff --git a/test/integration/integration_test.cc b/test/integration/integration_test.cc index f662744274f9..deeefd948b8b 100644 --- a/test/integration/integration_test.cc +++ b/test/integration/integration_test.cc @@ -364,6 +364,58 @@ TEST_P(IntegrationTest, EnvoyProxying100ContinueWithDecodeDataPause) { testEnvoyProxying1xx(true); } +// Verifies that we can construct a match tree with a filter, and that we are able to skip +// filter invocation through the match tree. +TEST_P(IntegrationTest, MatchingHttpFilterConstruction) { + config_helper_.addFilter(R"EOF( +name: matcher +typed_config: + "@type": type.googleapis.com/envoy.extensions.common.matching.v3.ExtensionWithMatcher + extension_config: + name: set-response-code + typed_config: + "@type": type.googleapis.com/test.integration.filters.SetResponseCodeFilterConfig + code: 403 + matcher: + matcher_tree: + input: + name: request-headers + typed_config: + "@type": type.googleapis.com/envoy.type.matcher.v3.HttpRequestHeaderMatchInput + header_name: match-header + exact_match_map: + map: + match: + action: + name: skip + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.common.matcher.action.v3.SkipFilter +)EOF"); + + initialize(); + + codec_client_ = makeHttpConnection(lookupPort("http")); + + { + auto response = codec_client_->makeRequestWithBody(default_request_headers_, 1024); + response->waitForEndStream(); + EXPECT_THAT(response->headers(), HttpStatusIs("403")); + } + + codec_client_ = makeHttpConnection(lookupPort("http")); + Http::TestRequestHeaderMapImpl request_headers{ + {":method", "POST"}, {":path", "/test/long/url"}, {":scheme", "http"}, + {":authority", "host"}, {"match-header", "match"}, {"content-type", "application/grpc"}}; + auto response = codec_client_->makeRequestWithBody(request_headers, 1024); + waitForNextUpstreamRequest(); + upstream_request_->encodeHeaders(default_response_headers_, true); + + response->waitForEndStream(); + EXPECT_THAT(response->headers(), HttpStatusIs("200")); + + codec_client_->close(); +} + // This is a regression for https://github.com/envoyproxy/envoy/issues/2715 and validates that a // pending request is not sent on a connection that has been half-closed. TEST_P(IntegrationTest, UpstreamDisconnectWithTwoRequests) { @@ -484,6 +536,17 @@ TEST_P(IntegrationTest, TestSmuggling) { sendRawHttpAndWaitForResponse(lookupPort("http"), request.c_str(), &response, false); EXPECT_THAT(response, HasSubstr("HTTP/1.1 400 Bad Request\r\n")); } + { + // Verify that sending `Transfer-Encoding: chunked` as a second header is detected and triggers + // the "no Transfer-Encoding + Content-Length" check. + std::string response; + const std::string request = + "GET / HTTP/1.1\r\nHost: host\r\ntransfer-encoding: " + "identity\r\ncontent-length: 36\r\ntransfer-encoding: chunked \r\n\r\n" + + smuggled_request; + sendRawHttpAndWaitForResponse(lookupPort("http"), request.c_str(), &response, false); + EXPECT_THAT(response, HasSubstr("HTTP/1.1 400 Bad Request\r\n")); + } } TEST_P(IntegrationTest, TestPipelinedResponses) { @@ -1209,6 +1272,14 @@ TEST_P(IntegrationTest, ViaAppendWith100Continue) { testEnvoyHandling100Continue(false, "foo"); } +// Pick a random test and use the old nodelay for coverage. This test can be +// removed when the code path is removed. +TEST_P(IntegrationTest, ViaAppendWith100ContinueWithOldNodelay) { + config_helper_.addRuntimeOverride("envoy.reloadable_features.always_nodelay", "false"); + config_helper_.addConfigModifier(setVia("foo")); + testEnvoyHandling100Continue(false, "foo"); +} + // Test delayed close semantics for downstream HTTP/1.1 connections. When an early response is // sent by Envoy, it will wait for response acknowledgment (via FIN/RST) from the client before // closing the socket (with a timeout for ensuring cleanup). @@ -1645,10 +1716,13 @@ TEST_P(IntegrationTest, ConnectWithChunkedBody) { EXPECT_FALSE(absl::StrContains(data, "onnection")) << data; ASSERT_TRUE(fake_upstream_connection->write( "HTTP/1.1 200 OK\r\ntransfer-encoding: chunked\r\n\r\nb\r\nHello World\r\n0\r\n\r\n")); - // The response will be rejected because chunked headers are not allowed with CONNECT upgrades. - // Envoy will send a local reply due to the invalid upstream response. - tcp_client->waitForDisconnect(false); - EXPECT_TRUE(absl::StartsWith(tcp_client->data(), "HTTP/1.1 503 Service Unavailable\r\n")); + tcp_client->waitForData("\r\n\r\n", false); + EXPECT_TRUE(absl::StartsWith(tcp_client->data(), "HTTP/1.1 200 OK\r\n")) << tcp_client->data(); + // Make sure the following payload is proxied without chunks or any other modifications. + ASSERT_TRUE(fake_upstream_connection->waitForData( + FakeRawConnection::waitForInexactMatch("\r\n\r\npayload"), &data)); + + tcp_client->close(); ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); } @@ -1745,4 +1819,117 @@ TEST_P(IntegrationTest, ConnectionIsTerminatedIfHCMStreamErrorIsFalseAndOverride EXPECT_EQ("400", response->headers().getStatusValue()); } +TEST_P(IntegrationTest, Preconnect) { + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + bootstrap.mutable_static_resources() + ->mutable_clusters(0) + ->mutable_preconnect_policy() + ->mutable_predictive_preconnect_ratio() + ->set_value(1.5); + }); + config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); + auto* load_assignment = cluster->mutable_load_assignment(); + load_assignment->clear_endpoints(); + for (int i = 0; i < 5; ++i) { + auto locality = load_assignment->add_endpoints(); + locality->add_lb_endpoints()->mutable_endpoint()->MergeFrom( + ConfigHelper::buildEndpoint(Network::Test::getLoopbackAddressString(version_))); + } + }); + + setUpstreamCount(5); + initialize(); + + std::list clients; + std::list encoders; + std::list responses; + std::vector fake_connections{15}; + + int upstream_index = 0; + for (uint32_t i = 0; i < 10; ++i) { + // Start a new request. + clients.push_back(makeHttpConnection(lookupPort("http"))); + auto encoder_decoder = clients.back()->startRequest(default_request_headers_); + encoders.push_back(&encoder_decoder.first); + responses.push_back(std::move(encoder_decoder.second)); + + // For each HTTP request, a new connection will be established, as none of + // the streams are closed so no connections can be reused. + waitForNextUpstreamConnection(std::vector({0, 1, 2, 3, 4}), + TestUtility::DefaultTimeout, fake_connections[upstream_index]); + ++upstream_index; + + // For every other connection, an extra connection should be preconnected. + if (i % 2 == 0) { + waitForNextUpstreamConnection(std::vector({0, 1, 2, 3, 4}), + TestUtility::DefaultTimeout, fake_connections[upstream_index]); + ++upstream_index; + } + } + + // Clean up. + while (!clients.empty()) { + clients.front()->close(); + clients.pop_front(); + } +} + +TEST_P(IntegrationTest, RandomPreconnect) { + config_helper_.addConfigModifier([&](envoy::config::bootstrap::v3::Bootstrap& bootstrap) -> void { + bootstrap.mutable_static_resources() + ->mutable_clusters(0) + ->mutable_preconnect_policy() + ->mutable_predictive_preconnect_ratio() + ->set_value(1.5); + }); + config_helper_.addConfigModifier([this](envoy::config::bootstrap::v3::Bootstrap& bootstrap) { + auto* cluster = bootstrap.mutable_static_resources()->mutable_clusters(0); + auto* load_assignment = cluster->mutable_load_assignment(); + load_assignment->clear_endpoints(); + for (int i = 0; i < 5; ++i) { + auto locality = load_assignment->add_endpoints(); + locality->add_lb_endpoints()->mutable_endpoint()->MergeFrom( + ConfigHelper::buildEndpoint(Network::Test::getLoopbackAddressString(version_))); + } + }); + + setUpstreamCount(5); + TestRandomGenerator rand; + autonomous_upstream_ = true; + initialize(); + + std::list clients; + std::list encoders; + std::list responses; + const uint32_t num_requests = 50; + + for (uint32_t i = 0; i < num_requests; ++i) { + if (rand.random() % 5 <= 3) { // Bias slightly towards more connections + // Start a new request. + clients.push_back(makeHttpConnection(lookupPort("http"))); + auto encoder_decoder = clients.back()->startRequest(default_request_headers_); + encoders.push_back(&encoder_decoder.first); + responses.push_back(std::move(encoder_decoder.second)); + } else if (!clients.empty()) { + // Finish up a request. + clients.front()->sendData(*encoders.front(), 0, true); + encoders.pop_front(); + responses.front()->waitForEndStream(); + responses.pop_front(); + clients.front()->close(); + clients.pop_front(); + } + } + // Clean up. + while (!clients.empty()) { + clients.front()->sendData(*encoders.front(), 0, true); + encoders.pop_front(); + responses.front()->waitForEndStream(); + responses.pop_front(); + clients.front()->close(); + clients.pop_front(); + } +} + } // namespace Envoy diff --git a/test/integration/load_stats_integration_test.cc b/test/integration/load_stats_integration_test.cc index d47682397192..bd2ad88ed00d 100644 --- a/test/integration/load_stats_integration_test.cc +++ b/test/integration/load_stats_integration_test.cc @@ -390,7 +390,8 @@ class LoadStatsIntegrationTest : public Grpc::VersionedGrpcClientIntegrationPara }; INSTANTIATE_TEST_SUITE_P(IpVersionsClientType, LoadStatsIntegrationTest, - VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS); + VERSIONED_GRPC_CLIENT_INTEGRATION_PARAMS, + Grpc::VersionedGrpcClientIntegrationParamTest::protocolTestParamsToString); // Validate the load reports for successful requests as cluster membership // changes. diff --git a/test/integration/stats_integration_test.cc b/test/integration/stats_integration_test.cc index f2b7bee41230..7a6981a5d9a3 100644 --- a/test/integration/stats_integration_test.cc +++ b/test/integration/stats_integration_test.cc @@ -279,7 +279,7 @@ TEST_P(ClusterMemoryTestRunner, MemoryLargeClusterSize) { // vary. // // If you encounter a failure here, please see - // https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#stats-memory-tests + // https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md#stats-memory-tests // for details on how to fix. // // We only run the exact test for ipv6 because ipv4 in some cases may allocate a @@ -325,7 +325,7 @@ TEST_P(ClusterMemoryTestRunner, MemoryLargeHostSizeWithStats) { // at the logs. // // If you encounter a failure here, please see - // https://github.com/envoyproxy/envoy/blob/master/source/docs/stats.md#stats-memory-tests + // https://github.com/envoyproxy/envoy/blob/main/source/docs/stats.md#stats-memory-tests // for details on how to fix. // // We only run the exact test for ipv6 because ipv4 in some cases may allocate a diff --git a/test/integration/tcp_proxy_integration_test.cc b/test/integration/tcp_proxy_integration_test.cc index e63c9d7d9d3f..8c9665cd3b15 100644 --- a/test/integration/tcp_proxy_integration_test.cc +++ b/test/integration/tcp_proxy_integration_test.cc @@ -99,6 +99,8 @@ TEST_P(TcpProxyIntegrationTest, TcpProxyUpstreamWritesFirst) { // Test TLS upstream. TEST_P(TcpProxyIntegrationTest, TcpProxyUpstreamTls) { + // Make sure old style nodelay is covered in at least one integration test. + config_helper_.addRuntimeOverride("envoy.reloadable_features.always_nodelay", "false"); upstream_tls_ = true; setUpstreamProtocol(FakeHttpConnection::Type::HTTP1); config_helper_.configureUpstreamTls(); diff --git a/test/integration/tcp_tunneling_integration_test.cc b/test/integration/tcp_tunneling_integration_test.cc index aef3d5f66fef..1369dfa79c81 100644 --- a/test/integration/tcp_tunneling_integration_test.cc +++ b/test/integration/tcp_tunneling_integration_test.cc @@ -792,9 +792,7 @@ TEST_P(TcpTunnelingIntegrationTest, ContentLengthHeaderIgnoredHttp1) { ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); } -// TODO(irozzo): temporarily disabled as a protocol error is thrown when -// transfer-encoding header is received in CONNECT responses. -TEST_P(TcpTunnelingIntegrationTest, DISABLED_TransferEncodingHeaderIgnoredHttp1) { +TEST_P(TcpTunnelingIntegrationTest, TransferEncodingHeaderIgnoredHttp1) { if (upstreamProtocol() == FakeHttpConnection::Type::HTTP2) { return; } @@ -812,13 +810,17 @@ TEST_P(TcpTunnelingIntegrationTest, DISABLED_TransferEncodingHeaderIgnoredHttp1) // Send upgrade headers downstream, fully establishing the connection. ASSERT_TRUE( - fake_upstream_connection->write("HTTP/1.1 299 OK\r\nTransfer-encoding: chunked\r\n\r\n")); + fake_upstream_connection->write("HTTP/1.1 200 OK\r\nTransfer-encoding: chunked\r\n\r\n")); // Now send some data and close the TCP client. - ASSERT_TRUE(tcp_client->write("hello", false)); + ASSERT_TRUE(tcp_client->write("hello")); + ASSERT_TRUE( + fake_upstream_connection->waitForData(FakeRawConnection::waitForInexactMatch("hello"))); + + // Close connections. + ASSERT_TRUE(fake_upstream_connection->close()); + ASSERT_TRUE(fake_upstream_connection->waitForDisconnect()); tcp_client->close(); - ASSERT_TRUE(upstream_request_->waitForData(*dispatcher_, 5)); - ASSERT_TRUE(fake_upstream_connection_->waitForDisconnect()); } TEST_P(TcpTunnelingIntegrationTest, DeferTransmitDataUntilSuccessConnectResponseIsReceived) { diff --git a/test/mocks/api/mocks.cc b/test/mocks/api/mocks.cc index 6678bf4b15ba..c402005a6dde 100644 --- a/test/mocks/api/mocks.cc +++ b/test/mocks/api/mocks.cc @@ -23,10 +23,16 @@ MockApi::~MockApi() = default; Event::DispatcherPtr MockApi::allocateDispatcher(const std::string& name) { return Event::DispatcherPtr{allocateDispatcher_(name, time_system_)}; } + +Event::DispatcherPtr +MockApi::allocateDispatcher(const std::string& name, + const Event::ScaledRangeTimerManagerFactory& scaled_timer_factory) { + return Event::DispatcherPtr{allocateDispatcher_(name, scaled_timer_factory, {}, time_system_)}; +} Event::DispatcherPtr MockApi::allocateDispatcher(const std::string& name, Buffer::WatermarkFactoryPtr&& watermark_factory) { return Event::DispatcherPtr{ - allocateDispatcher_(name, std::move(watermark_factory), time_system_)}; + allocateDispatcher_(name, {}, std::move(watermark_factory), time_system_)}; } MockOsSysCalls::MockOsSysCalls() { diff --git a/test/mocks/api/mocks.h b/test/mocks/api/mocks.h index c6a8222c4298..3658717afce1 100644 --- a/test/mocks/api/mocks.h +++ b/test/mocks/api/mocks.h @@ -31,14 +31,18 @@ class MockApi : public Api { // Api::Api Event::DispatcherPtr allocateDispatcher(const std::string& name) override; + Event::DispatcherPtr + allocateDispatcher(const std::string& name, + const Event::ScaledRangeTimerManagerFactory& scaled_timer_factory) override; Event::DispatcherPtr allocateDispatcher(const std::string& name, Buffer::WatermarkFactoryPtr&& watermark_factory) override; TimeSource& timeSource() override { return time_system_; } MOCK_METHOD(Event::Dispatcher*, allocateDispatcher_, (const std::string&, Event::TimeSystem&)); MOCK_METHOD(Event::Dispatcher*, allocateDispatcher_, - (const std::string&, Buffer::WatermarkFactoryPtr&& watermark_factory, - Event::TimeSystem&)); + (const std::string&, + const Event::ScaledRangeTimerManagerFactory& scaled_timer_factory, + Buffer::WatermarkFactoryPtr&& watermark_factory, Event::TimeSystem&)); MOCK_METHOD(Filesystem::Instance&, fileSystem, ()); MOCK_METHOD(Thread::ThreadFactory&, threadFactory, ()); MOCK_METHOD(Stats::Scope&, rootScope, ()); diff --git a/test/mocks/config/mocks.cc b/test/mocks/config/mocks.cc index f578384ab7d2..fff4a52dbb83 100644 --- a/test/mocks/config/mocks.cc +++ b/test/mocks/config/mocks.cc @@ -8,10 +8,11 @@ namespace Envoy { namespace Config { MockSubscriptionFactory::MockSubscriptionFactory() { - ON_CALL(*this, subscriptionFromConfigSource(_, _, _, _, _)) - .WillByDefault(testing::Invoke( - [this](const envoy::config::core::v3::ConfigSource&, absl::string_view, Stats::Scope&, - SubscriptionCallbacks& callbacks, OpaqueResourceDecoder&) -> SubscriptionPtr { + ON_CALL(*this, subscriptionFromConfigSource(_, _, _, _, _, _)) + .WillByDefault( + testing::Invoke([this](const envoy::config::core::v3::ConfigSource&, absl::string_view, + Stats::Scope&, SubscriptionCallbacks& callbacks, + OpaqueResourceDecoder&, bool) -> SubscriptionPtr { auto ret = std::make_unique>(); subscription_ = ret.get(); callbacks_ = &callbacks; @@ -42,5 +43,9 @@ MockUntypedConfigUpdateCallbacks::MockUntypedConfigUpdateCallbacks() = default; MockUntypedConfigUpdateCallbacks::~MockUntypedConfigUpdateCallbacks() = default; MockTypedFactory::~MockTypedFactory() = default; + +MockContextProvider::MockContextProvider() = default; +MockContextProvider::~MockContextProvider() = default; + } // namespace Config } // namespace Envoy diff --git a/test/mocks/config/mocks.h b/test/mocks/config/mocks.h index 8c2cc7ac2177..80e2eed8b505 100644 --- a/test/mocks/config/mocks.h +++ b/test/mocks/config/mocks.h @@ -62,10 +62,11 @@ class MockUntypedConfigUpdateCallbacks : public UntypedConfigUpdateCallbacks { class MockSubscription : public Subscription { public: - MOCK_METHOD(void, start, - (const std::set& resources, const bool use_prefix_matching)); - MOCK_METHOD(void, updateResourceInterest, (const std::set& update_to_these_names)); - MOCK_METHOD(void, requestOnDemandUpdate, (const std::set& add_these_names)); + MOCK_METHOD(void, start, (const absl::flat_hash_set& resources)); + MOCK_METHOD(void, updateResourceInterest, + (const absl::flat_hash_set& update_to_these_names)); + MOCK_METHOD(void, requestOnDemandUpdate, + (const absl::flat_hash_set& add_these_names)); }; class MockSubscriptionFactory : public SubscriptionFactory { @@ -76,7 +77,7 @@ class MockSubscriptionFactory : public SubscriptionFactory { MOCK_METHOD(SubscriptionPtr, subscriptionFromConfigSource, (const envoy::config::core::v3::ConfigSource& config, absl::string_view type_url, Stats::Scope& scope, SubscriptionCallbacks& callbacks, - OpaqueResourceDecoder& resource_decoder)); + OpaqueResourceDecoder& resource_decoder, bool use_namespace_matching)); MOCK_METHOD(SubscriptionPtr, collectionSubscriptionFromUrl, (const xds::core::v3::ResourceLocator& collection_locator, const envoy::config::core::v3::ConfigSource& config, absl::string_view type_url, @@ -106,19 +107,20 @@ class MockGrpcMux : public GrpcMux { MOCK_METHOD(ScopedResume, pause, (const std::vector type_urls), (override)); MOCK_METHOD(void, addSubscription, - (const std::set& resources, const std::string& type_url, + (const absl::flat_hash_set& resources, const std::string& type_url, SubscriptionCallbacks& callbacks, SubscriptionStats& stats, std::chrono::milliseconds init_fetch_timeout)); MOCK_METHOD(void, updateResourceInterest, - (const std::set& resources, const std::string& type_url)); + (const absl::flat_hash_set& resources, const std::string& type_url)); MOCK_METHOD(GrpcMuxWatchPtr, addWatch, - (const std::string& type_url, const std::set& resources, + (const std::string& type_url, const absl::flat_hash_set& resources, SubscriptionCallbacks& callbacks, OpaqueResourceDecoder& resource_decoder, const bool use_prefix_matching)); MOCK_METHOD(void, requestOnDemandUpdate, - (const std::string& type_url, const std::set& add_these_names)); + (const std::string& type_url, + const absl::flat_hash_set& add_these_names)); }; class MockGrpcStreamCallbacks @@ -165,5 +167,13 @@ class MockTypedFactory : public TypedFactory { MOCK_METHOD(std::string, category, (), (const)); }; +class MockContextProvider : public ContextProvider { +public: + MockContextProvider(); + ~MockContextProvider() override; + + MOCK_METHOD(const xds::core::v3::ContextParams&, nodeContext, (), (const)); +}; + } // namespace Config } // namespace Envoy diff --git a/test/mocks/event/mocks.cc b/test/mocks/event/mocks.cc index a8db4995abb3..75b60c1cbccb 100644 --- a/test/mocks/event/mocks.cc +++ b/test/mocks/event/mocks.cc @@ -24,6 +24,9 @@ MockDispatcher::MockDispatcher(const std::string& name) : name_(name) { to_delete_.clear(); })); ON_CALL(*this, createTimer_(_)).WillByDefault(ReturnNew>()); + ON_CALL(*this, createScaledTimer_(_, _)).WillByDefault(ReturnNew>()); + ON_CALL(*this, createScaledTypedTimer_(_, _)) + .WillByDefault(ReturnNew>()); ON_CALL(*this, post(_)).WillByDefault(Invoke([](PostCb cb) -> void { cb(); })); ON_CALL(buffer_factory_, create_(_, _, _)) diff --git a/test/mocks/event/mocks.h b/test/mocks/event/mocks.h index 8e29e84c3b32..066eb68d6747 100644 --- a/test/mocks/event/mocks.h +++ b/test/mocks/event/mocks.h @@ -5,6 +5,7 @@ #include #include +#include "envoy/common/scope_tracker.h" #include "envoy/common/time.h" #include "envoy/event/deferred_deletable.h" #include "envoy/event/dispatcher.h" @@ -83,6 +84,20 @@ class MockDispatcher : public Dispatcher { return timer; } + Event::TimerPtr createScaledTimer(ScaledTimerMinimum minimum, Event::TimerCb cb) override { + auto timer = Event::TimerPtr{createScaledTimer_(minimum, cb)}; + // Assert that the timer is not null to avoid confusing test failures down the line. + ASSERT(timer != nullptr); + return timer; + } + + Event::TimerPtr createScaledTimer(ScaledTimerType timer_type, Event::TimerCb cb) override { + auto timer = Event::TimerPtr{createScaledTypedTimer_(timer_type, cb)}; + // Assert that the timer is not null to avoid confusing test failures down the line. + ASSERT(timer != nullptr); + return timer; + } + Event::SchedulableCallbackPtr createSchedulableCallback(std::function cb) override { auto schedulable_cb = Event::SchedulableCallbackPtr{createSchedulableCallback_(cb)}; // Assert that schedulable_cb is not null to avoid confusing test failures down the line. @@ -124,13 +139,16 @@ class MockDispatcher : public Dispatcher { MOCK_METHOD(Network::UdpListener*, createUdpListener_, (Network::SocketSharedPtr socket, Network::UdpListenerCallbacks& cb)); MOCK_METHOD(Timer*, createTimer_, (Event::TimerCb cb)); + MOCK_METHOD(Timer*, createScaledTimer_, (ScaledTimerMinimum minimum, Event::TimerCb cb)); + MOCK_METHOD(Timer*, createScaledTypedTimer_, (ScaledTimerType timer_type, Event::TimerCb cb)); MOCK_METHOD(SchedulableCallback*, createSchedulableCallback_, (std::function cb)); MOCK_METHOD(void, deferredDelete_, (DeferredDeletable * to_delete)); MOCK_METHOD(void, exit, ()); MOCK_METHOD(SignalEvent*, listenForSignal_, (signal_t signal_num, SignalCb cb)); MOCK_METHOD(void, post, (std::function callback)); MOCK_METHOD(void, run, (RunType type)); - MOCK_METHOD(const ScopeTrackedObject*, setTrackedObject, (const ScopeTrackedObject* object)); + MOCK_METHOD(void, pushTrackedObject, (const ScopeTrackedObject* object)); + MOCK_METHOD(void, popTrackedObject, (const ScopeTrackedObject* expected_object)); MOCK_METHOD(bool, isThreadSafe, (), (const)); Buffer::WatermarkFactory& getWatermarkFactory() override { return buffer_factory_; } MOCK_METHOD(Thread::ThreadId, getCurrentThreadId, ()); @@ -184,8 +202,12 @@ class MockScaledRangeTimerManager : public ScaledRangeTimerManager { TimerPtr createTimer(ScaledTimerMinimum minimum, TimerCb callback) override { return TimerPtr{createTimer_(minimum, std::move(callback))}; } + TimerPtr createTimer(ScaledTimerType timer_type, TimerCb callback) override { + return TimerPtr{createTypedTimer_(timer_type, std::move(callback))}; + } MOCK_METHOD(Timer*, createTimer_, (ScaledTimerMinimum, TimerCb)); - MOCK_METHOD(void, setScaleFactor, (double), (override)); + MOCK_METHOD(Timer*, createTypedTimer_, (ScaledTimerType, TimerCb)); + MOCK_METHOD(void, setScaleFactor, (UnitFloat), (override)); }; class MockSchedulableCallback : public SchedulableCallback { diff --git a/test/mocks/event/wrapped_dispatcher.h b/test/mocks/event/wrapped_dispatcher.h index 974d61b39be2..a6dc7be22716 100644 --- a/test/mocks/event/wrapped_dispatcher.h +++ b/test/mocks/event/wrapped_dispatcher.h @@ -77,6 +77,13 @@ class WrappedDispatcher : public Dispatcher { } TimerPtr createTimer(TimerCb cb) override { return impl_.createTimer(std::move(cb)); } + TimerPtr createScaledTimer(ScaledTimerMinimum minimum, TimerCb cb) override { + return impl_.createScaledTimer(minimum, std::move(cb)); + } + + TimerPtr createScaledTimer(ScaledTimerType timer_type, TimerCb cb) override { + return impl_.createScaledTimer(timer_type, std::move(cb)); + } Event::SchedulableCallbackPtr createSchedulableCallback(std::function cb) override { return impl_.createSchedulableCallback(std::move(cb)); @@ -97,8 +104,12 @@ class WrappedDispatcher : public Dispatcher { void run(RunType type) override { impl_.run(type); } Buffer::WatermarkFactory& getWatermarkFactory() override { return impl_.getWatermarkFactory(); } - const ScopeTrackedObject* setTrackedObject(const ScopeTrackedObject* object) override { - return impl_.setTrackedObject(object); + void pushTrackedObject(const ScopeTrackedObject* object) override { + return impl_.pushTrackedObject(object); + } + + void popTrackedObject(const ScopeTrackedObject* expected_object) override { + return impl_.popTrackedObject(expected_object); } MonotonicTime approximateMonotonicTime() const override { diff --git a/test/mocks/http/mocks.h b/test/mocks/http/mocks.h index fe98c1f27831..378aa1c61a2b 100644 --- a/test/mocks/http/mocks.h +++ b/test/mocks/http/mocks.h @@ -219,12 +219,15 @@ class MockStreamDecoderFilterCallbacks : public StreamDecoderFilterCallbacks, void encode100ContinueHeaders(ResponseHeaderMapPtr&& headers) override { encode100ContinueHeaders_(*headers); } + MOCK_METHOD(ResponseHeaderMapOptRef, continueHeaders, (), (const)); void encodeHeaders(ResponseHeaderMapPtr&& headers, bool end_stream, absl::string_view details) override { stream_info_.setResponseCodeDetails(details); encodeHeaders_(*headers, end_stream); } + MOCK_METHOD(ResponseHeaderMapOptRef, responseHeaders, (), (const)); void encodeTrailers(ResponseTrailerMapPtr&& trailers) override { encodeTrailers_(*trailers); } + MOCK_METHOD(ResponseTrailerMapOptRef, responseTrailers, (), (const)); void encodeMetadata(MetadataMapPtr&& metadata_map) override { encodeMetadata_(std::move(metadata_map)); } @@ -318,6 +321,7 @@ class MockStreamDecoderFilter : public StreamDecoderFilter { // Http::StreamFilterBase MOCK_METHOD(void, onStreamComplete, ()); MOCK_METHOD(void, onDestroy, ()); + MOCK_METHOD(void, onMatchCallback, (const Matcher::Action&)); // Http::StreamDecoderFilter MOCK_METHOD(FilterHeadersStatus, decodeHeaders, (RequestHeaderMap & headers, bool end_stream)); @@ -343,6 +347,7 @@ class MockStreamEncoderFilter : public StreamEncoderFilter { // Http::StreamFilterBase MOCK_METHOD(void, onStreamComplete, ()); MOCK_METHOD(void, onDestroy, ()); + MOCK_METHOD(void, onMatchCallback, (const Matcher::Action&)); // Http::MockStreamEncoderFilter MOCK_METHOD(FilterHeadersStatus, encode100ContinueHeaders, (ResponseHeaderMap & headers)); @@ -364,6 +369,7 @@ class MockStreamFilter : public StreamFilter { // Http::StreamFilterBase MOCK_METHOD(void, onStreamComplete, ()); MOCK_METHOD(void, onDestroy, ()); + MOCK_METHOD(void, onMatchCallback, (const Matcher::Action&)); // Http::StreamDecoderFilter MOCK_METHOD(FilterHeadersStatus, decodeHeaders, (RequestHeaderMap & headers, bool end_stream)); @@ -374,9 +380,12 @@ class MockStreamFilter : public StreamFilter { // Http::MockStreamEncoderFilter MOCK_METHOD(FilterHeadersStatus, encode100ContinueHeaders, (ResponseHeaderMap & headers)); + MOCK_METHOD(ResponseHeaderMapOptRef, continueHeaders, (), (const)); MOCK_METHOD(FilterHeadersStatus, encodeHeaders, (ResponseHeaderMap & headers, bool end_stream)); + MOCK_METHOD(ResponseHeaderMapOptRef, responseHeaders, (), (const)); MOCK_METHOD(FilterDataStatus, encodeData, (Buffer::Instance & data, bool end_stream)); MOCK_METHOD(FilterTrailersStatus, encodeTrailers, (ResponseTrailerMap & trailers)); + MOCK_METHOD(ResponseTrailerMapOptRef, responseTrailers, (), (const)); MOCK_METHOD(FilterMetadataStatus, encodeMetadata, (MetadataMap & metadata_map)); MOCK_METHOD(void, setEncoderFilterCallbacks, (StreamEncoderFilterCallbacks & callbacks)); diff --git a/test/mocks/local_info/mocks.h b/test/mocks/local_info/mocks.h index 1f984967a14e..9a1f2426f1d9 100644 --- a/test/mocks/local_info/mocks.h +++ b/test/mocks/local_info/mocks.h @@ -23,6 +23,7 @@ class MockLocalInfo : public LocalInfo { MOCK_METHOD(const std::string&, nodeName, (), (const)); MOCK_METHOD(const Stats::StatName&, zoneStatName, (), (const)); MOCK_METHOD(envoy::config::core::v3::Node&, node, (), (const)); + MOCK_METHOD(const Config::ContextProvider&, contextProvider, (), (const)); const Stats::StatName& makeZoneStatName() const; diff --git a/test/mocks/network/mocks.h b/test/mocks/network/mocks.h index c30f97d8a1a3..d2f862c7501d 100644 --- a/test/mocks/network/mocks.h +++ b/test/mocks/network/mocks.h @@ -397,7 +397,7 @@ class MockListener : public Listener { MOCK_METHOD(void, onDestroy, ()); MOCK_METHOD(void, enable, ()); MOCK_METHOD(void, disable, ()); - MOCK_METHOD(void, setRejectFraction, (float)); + MOCK_METHOD(void, setRejectFraction, (UnitFloat)); }; class MockConnectionHandler : public ConnectionHandler { @@ -419,7 +419,7 @@ class MockConnectionHandler : public ConnectionHandler { MOCK_METHOD(void, stopListeners, ()); MOCK_METHOD(void, disableListeners, ()); MOCK_METHOD(void, enableListeners, ()); - MOCK_METHOD(void, setListenerRejectFraction, (float), (override)); + MOCK_METHOD(void, setListenerRejectFraction, (UnitFloat), (override)); MOCK_METHOD(const std::string&, statPrefix, (), (const)); }; @@ -509,7 +509,7 @@ class MockUdpListener : public UdpListener { MOCK_METHOD(void, onDestroy, ()); MOCK_METHOD(void, enable, ()); MOCK_METHOD(void, disable, ()); - MOCK_METHOD(void, setRejectFraction, (float), (override)); + MOCK_METHOD(void, setRejectFraction, (UnitFloat), (override)); MOCK_METHOD(Event::Dispatcher&, dispatcher, ()); MOCK_METHOD(Address::InstanceConstSharedPtr&, localAddress, (), (const)); MOCK_METHOD(Api::IoCallUint64Result, send, (const UdpSendData&)); diff --git a/test/mocks/ratelimit/mocks.h b/test/mocks/ratelimit/mocks.h index 7f983beabbca..2cb8fbef7471 100644 --- a/test/mocks/ratelimit/mocks.h +++ b/test/mocks/ratelimit/mocks.h @@ -14,10 +14,6 @@ inline bool operator==(const RateLimitOverride& lhs, const RateLimitOverride& rh return lhs.requests_per_unit_ == rhs.requests_per_unit_ && lhs.unit_ == rhs.unit_; } -inline bool operator==(const DescriptorEntry& lhs, const DescriptorEntry& rhs) { - return lhs.key_ == rhs.key_ && lhs.value_ == rhs.value_; -} - inline bool operator==(const Descriptor& lhs, const Descriptor& rhs) { return lhs.entries_ == rhs.entries_ && lhs.limit_ == rhs.limit_; } diff --git a/test/mocks/router/mocks.h b/test/mocks/router/mocks.h index 6d665cd26252..ea320f794828 100644 --- a/test/mocks/router/mocks.h +++ b/test/mocks/router/mocks.h @@ -194,6 +194,11 @@ class MockRateLimitPolicyEntry : public RateLimitPolicyEntry { const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, const StreamInfo::StreamInfo& info), (const)); + MOCK_METHOD(void, populateLocalDescriptors, + (std::vector & descriptors, + const std::string& local_service_cluster, const Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& info), + (const)); uint64_t stage_{}; std::string disable_key_; diff --git a/test/mocks/router/router_filter_interface.cc b/test/mocks/router/router_filter_interface.cc index f1fb2ab0518c..9d175ee6cb7a 100644 --- a/test/mocks/router/router_filter_interface.cc +++ b/test/mocks/router/router_filter_interface.cc @@ -18,7 +18,8 @@ MockRouterFilterInterface::MockRouterFilterInterface() ON_CALL(*this, config()).WillByDefault(ReturnRef(config_)); ON_CALL(*this, cluster()).WillByDefault(Return(cluster_info_)); ON_CALL(*this, upstreamRequests()).WillByDefault(ReturnRef(requests_)); - EXPECT_CALL(callbacks_.dispatcher_, setTrackedObject(_)).Times(AnyNumber()); + EXPECT_CALL(callbacks_.dispatcher_, pushTrackedObject(_)).Times(AnyNumber()); + EXPECT_CALL(callbacks_.dispatcher_, popTrackedObject(_)).Times(AnyNumber()); ON_CALL(*this, routeEntry()).WillByDefault(Return(&route_entry_)); ON_CALL(callbacks_, connection()).WillByDefault(Return(&client_connection_)); route_entry_.connect_config_.emplace(RouteEntry::ConnectConfig()); diff --git a/test/mocks/server/bootstrap_extension_factory.cc b/test/mocks/server/bootstrap_extension_factory.cc index 80984ea4093d..1866e4ffb730 100644 --- a/test/mocks/server/bootstrap_extension_factory.cc +++ b/test/mocks/server/bootstrap_extension_factory.cc @@ -2,6 +2,11 @@ namespace Envoy { namespace Server { + +MockBootstrapExtension::MockBootstrapExtension() = default; + +MockBootstrapExtension::~MockBootstrapExtension() = default; + namespace Configuration { MockBootstrapExtensionFactory::MockBootstrapExtensionFactory() = default; diff --git a/test/mocks/server/bootstrap_extension_factory.h b/test/mocks/server/bootstrap_extension_factory.h index f6421f788788..9ae9940cec8c 100644 --- a/test/mocks/server/bootstrap_extension_factory.h +++ b/test/mocks/server/bootstrap_extension_factory.h @@ -6,6 +6,15 @@ namespace Envoy { namespace Server { + +class MockBootstrapExtension : public BootstrapExtension { +public: + MockBootstrapExtension(); + ~MockBootstrapExtension() override; + + MOCK_METHOD(void, onServerInitialized, (), (override)); +}; + namespace Configuration { class MockBootstrapExtensionFactory : public BootstrapExtensionFactory { public: diff --git a/test/mocks/server/overload_manager.cc b/test/mocks/server/overload_manager.cc index 461b0b27daa3..3cb951ea7f8e 100644 --- a/test/mocks/server/overload_manager.cc +++ b/test/mocks/server/overload_manager.cc @@ -17,18 +17,6 @@ using ::testing::ReturnRef; MockThreadLocalOverloadState::MockThreadLocalOverloadState() : disabled_state_(OverloadActionState::inactive()) { ON_CALL(*this, getState).WillByDefault(ReturnRef(disabled_state_)); - ON_CALL(*this, createScaledTypedTimer_).WillByDefault(ReturnNew>()); - ON_CALL(*this, createScaledMinimumTimer_).WillByDefault(ReturnNew>()); -} - -Event::TimerPtr MockThreadLocalOverloadState::createScaledTimer(OverloadTimerType timer_type, - Event::TimerCb callback) { - return Event::TimerPtr{createScaledTypedTimer_(timer_type, std::move(callback))}; -} - -Event::TimerPtr MockThreadLocalOverloadState::createScaledTimer(Event::ScaledTimerMinimum minimum, - Event::TimerCb callback) { - return Event::TimerPtr{createScaledMinimumTimer_(minimum, std::move(callback))}; } MockOverloadManager::MockOverloadManager() { diff --git a/test/mocks/server/overload_manager.h b/test/mocks/server/overload_manager.h index c694ab7b1254..e5ab03992836 100644 --- a/test/mocks/server/overload_manager.h +++ b/test/mocks/server/overload_manager.h @@ -14,12 +14,6 @@ class MockThreadLocalOverloadState : public ThreadLocalOverloadState { public: MockThreadLocalOverloadState(); MOCK_METHOD(const OverloadActionState&, getState, (const std::string&), (override)); - Event::TimerPtr createScaledTimer(OverloadTimerType timer_type, Event::TimerCb callback) override; - Event::TimerPtr createScaledTimer(Event::ScaledTimerMinimum minimum, - Event::TimerCb callback) override; - MOCK_METHOD(Event::Timer*, createScaledTypedTimer_, (OverloadTimerType, Event::TimerCb)); - MOCK_METHOD(Event::Timer*, createScaledMinimumTimer_, - (Event::ScaledTimerMinimum, Event::TimerCb)); private: const OverloadActionState disabled_state_; @@ -35,6 +29,7 @@ class MockOverloadManager : public OverloadManager { MOCK_METHOD(bool, registerForAction, (const std::string& action, Event::Dispatcher& dispatcher, OverloadActionCb callback)); + MOCK_METHOD(Event::ScaledRangeTimerManagerFactory, scaledTimerFactory, (), (override)); MOCK_METHOD(ThreadLocalOverloadState&, getThreadLocalOverloadState, ()); testing::NiceMock overload_state_; diff --git a/test/mocks/stream_info/mocks.cc b/test/mocks/stream_info/mocks.cc index 45abe90b33e8..a35582ea6b1e 100644 --- a/test/mocks/stream_info/mocks.cc +++ b/test/mocks/stream_info/mocks.cc @@ -103,7 +103,9 @@ MockStreamInfo::MockStreamInfo() ON_CALL(*this, dynamicMetadata()).WillByDefault(ReturnRef(metadata_)); ON_CALL(Const(*this), dynamicMetadata()).WillByDefault(ReturnRef(metadata_)); ON_CALL(*this, filterState()).WillByDefault(ReturnRef(filter_state_)); - ON_CALL(Const(*this), filterState()).WillByDefault(ReturnRef(*filter_state_)); + ON_CALL(Const(*this), filterState()).WillByDefault(Invoke([this]() -> const FilterState& { + return *filter_state_; + })); ON_CALL(*this, upstreamFilterState()).WillByDefault(ReturnRef(upstream_filter_state_)); ON_CALL(*this, setUpstreamFilterState(_)) .WillByDefault(Invoke([this](const FilterStateSharedPtr& filter_state) { diff --git a/test/per_file_coverage.sh b/test/per_file_coverage.sh index 174c0cd14991..077ba276f79d 100755 --- a/test/per_file_coverage.sh +++ b/test/per_file_coverage.sh @@ -35,7 +35,6 @@ declare -a KNOWN_LOW_COVERAGE=( "source/extensions/filters/common/rbac:87.5" "source/extensions/filters/http/cache:92.4" "source/extensions/filters/http/cache/simple_http_cache:95.2" -"source/extensions/filters/http/dynamic_forward_proxy:95.0" "source/extensions/filters/http/grpc_json_transcoder:94.8" "source/extensions/filters/http/ip_tagging:91.2" "source/extensions/filters/http/kill_request:95.0" # Death tests don't report LCOV @@ -66,7 +65,7 @@ declare -a KNOWN_LOW_COVERAGE=( "source/extensions/watchdog/profile_action:85.7" "source/server:94.5" "source/server/admin:95.1" -"source/server/config_validation:76.6" +"source/server/config_validation:75.6" ) [[ -z "${SRCDIR}" ]] && SRCDIR="${PWD}" diff --git a/test/proto/bookstore.proto b/test/proto/bookstore.proto index b3865ffe9d95..6855e6444369 100644 --- a/test/proto/bookstore.proto +++ b/test/proto/bookstore.proto @@ -96,6 +96,11 @@ service Bookstore { get: "/indexStream" }; } + rpc GetRoot(google.protobuf.Empty) returns (google.api.HttpBody) { + option (google.api.http) = { + get: "/" + }; + } rpc PostBody(EchoBodyRequest) returns (google.protobuf.Empty) { option (google.api.http) = { post: "/postBody" diff --git a/test/server/BUILD b/test/server/BUILD index 82766be214b2..5b2fb73273c7 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -100,6 +100,16 @@ envoy_cc_test( ], ) +envoy_cc_test( + name = "filter_config_test", + srcs = ["filter_config_test.cc"], + deps = [ + "//include/envoy/server:filter_config_interface", + "//source/extensions/filters/http/common:pass_through_filter_lib", + "//test/mocks/server:server_mocks", + ], +) + envoy_cc_test( name = "hot_restart_impl_test", srcs = envoy_select_hot_restart(["hot_restart_impl_test.cc"]), diff --git a/test/server/config_validation/dispatcher_test.cc b/test/server/config_validation/dispatcher_test.cc index a4eb71df8d8a..b8a60c1da7a1 100644 --- a/test/server/config_validation/dispatcher_test.cc +++ b/test/server/config_validation/dispatcher_test.cc @@ -49,6 +49,15 @@ TEST_P(ConfigValidation, CreateConnection) { SUCCEED(); } +TEST_P(ConfigValidation, CreateScaledTimer) { + EXPECT_NE(dispatcher_->createScaledTimer(Event::ScaledTimerType::UnscaledRealTimerForTest, [] {}), + nullptr); + EXPECT_NE(dispatcher_->createScaledTimer( + Event::ScaledTimerMinimum(Event::ScaledMinimum(UnitFloat(0.5f))), [] {}), + nullptr); + SUCCEED(); +} + // Make sure that creating DnsResolver does not cause crash and each call to create // DNS resolver returns the same shared_ptr. TEST_F(ConfigValidation, SharedDnsResolver) { diff --git a/test/server/connection_handler_test.cc b/test/server/connection_handler_test.cc index 17fe5e69682b..dd55fea07a17 100644 --- a/test/server/connection_handler_test.cc +++ b/test/server/connection_handler_test.cc @@ -182,7 +182,7 @@ class ConnectionHandlerTest : public testing::Test, protected Logger::LoggableaddListener(absl::nullopt, *test_listener); - EXPECT_CALL(*listener, setRejectFraction(0.1234f)); + EXPECT_CALL(*listener, setRejectFraction(UnitFloat(0.1234f))); EXPECT_CALL(*listener, onDestroy()); - handler_->setListenerRejectFraction(0.1234f); + handler_->setListenerRejectFraction(UnitFloat(0.1234f)); } TEST_F(ConnectionHandlerTest, AddListenerSetRejectFraction) { @@ -481,11 +481,11 @@ TEST_F(ConnectionHandlerTest, AddListenerSetRejectFraction) { auto listener = new NiceMock(); TestListener* test_listener = addListener(1, false, false, "test_listener", listener, &listener_callbacks); - EXPECT_CALL(*listener, setRejectFraction(0.12345f)); + EXPECT_CALL(*listener, setRejectFraction(UnitFloat(0.12345f))); EXPECT_CALL(*socket_factory_, localAddress()).WillOnce(ReturnRef(local_address_)); EXPECT_CALL(*listener, onDestroy()); - handler_->setListenerRejectFraction(0.12345f); + handler_->setListenerRejectFraction(UnitFloat(0.12345f)); handler_->addListener(absl::nullopt, *test_listener); } diff --git a/test/server/filter_config_test.cc b/test/server/filter_config_test.cc new file mode 100644 index 000000000000..b8ddc9e063bf --- /dev/null +++ b/test/server/filter_config_test.cc @@ -0,0 +1,42 @@ +#include "envoy/server/filter_config.h" + +#include "extensions/filters/http/common/pass_through_filter.h" + +#include "test/mocks/server/factory_context.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace { + +class TestHttpFilterConfigFactory : public Server::Configuration::NamedHttpFilterConfigFactory { +public: + TestHttpFilterConfigFactory() = default; + + Http::FilterFactoryCb + createFilterFactoryFromProto(const Protobuf::Message&, const std::string&, + Server::Configuration::FactoryContext&) override { + return [](Http::FilterChainFactoryCallbacks& callbacks) -> void { + callbacks.addStreamDecoderFilter(std::make_shared()); + }; + } + + ProtobufTypes::MessagePtr createEmptyConfigProto() override { return nullptr; } + ProtobufTypes::MessagePtr createEmptyProtocolOptionsProto() override { return nullptr; } + + std::string name() const override { return "envoy.test.http_filter"; } + std::string configType() override { return ""; }; +}; + +TEST(NamedHttpFilterConfigFactoryTest, CreateFilterFactory) { + TestHttpFilterConfigFactory factory; + const std::string stats_prefix = "foo"; + Server::Configuration::MockFactoryContext context; + ProtobufTypes::MessagePtr message{new Envoy::ProtobufWkt::Struct()}; + + factory.createFilterFactoryFromProto(*message, stats_prefix, context); +} + +} // namespace +} // namespace Envoy diff --git a/test/server/lds_api_test.cc b/test/server/lds_api_test.cc index 4f59dbf6723e..1bc639f3e042 100644 --- a/test/server/lds_api_test.cc +++ b/test/server/lds_api_test.cc @@ -42,7 +42,7 @@ class LdsApiTest : public testing::Test { EXPECT_CALL(init_manager_, add(_)); lds_ = std::make_unique(lds_config, nullptr, cluster_manager_, init_manager_, store_, listener_manager_, validation_visitor_); - EXPECT_CALL(*cluster_manager_.subscription_factory_.subscription_, start(_, _)); + EXPECT_CALL(*cluster_manager_.subscription_factory_.subscription_, start(_)); init_target_handle_->initialize(init_watcher_); lds_callbacks_ = cluster_manager_.subscription_factory_.callbacks_; } diff --git a/test/server/overload_manager_impl_test.cc b/test/server/overload_manager_impl_test.cc index d160b42ceae8..cc0e8b29e287 100644 --- a/test/server/overload_manager_impl_test.cc +++ b/test/server/overload_manager_impl_test.cc @@ -1,6 +1,8 @@ #include +#include #include "envoy/config/overload/v3/overload.pb.h" +#include "envoy/event/scaled_range_timer_manager.h" #include "envoy/server/overload/overload_manager.h" #include "envoy/server/resource_monitor.h" #include "envoy/server/resource_monitor_config.h" @@ -25,17 +27,23 @@ using testing::_; using testing::AllOf; using testing::AnyNumber; using testing::ByMove; -using testing::DoubleNear; +using testing::DoAll; +using testing::FloatNear; using testing::Invoke; -using testing::MockFunction; +using testing::InvokeArgument; using testing::NiceMock; +using testing::Pointee; using testing::Property; using testing::Return; +using testing::SaveArg; +using testing::UnorderedElementsAreArray; namespace Envoy { namespace Server { namespace { +using TimerType = Event::ScaledTimerType; + class FakeResourceMonitor : public ResourceMonitor { public: FakeResourceMonitor(Event::Dispatcher& dispatcher) : dispatcher_(dispatcher), response_(0.0) {} @@ -120,12 +128,14 @@ class TestOverloadManager : public OverloadManagerImpl { } MOCK_METHOD(Event::ScaledRangeTimerManagerPtr, createScaledRangeTimerManager, - (Event::Dispatcher&), (const, override)); + (Event::Dispatcher&, const Event::ScaledTimerTypeMapConstSharedPtr&), + (const, override)); private: - Event::ScaledRangeTimerManagerPtr - createDefaultScaledRangeTimerManager(Event::Dispatcher& dispatcher) const { - return OverloadManagerImpl::createScaledRangeTimerManager(dispatcher); + Event::ScaledRangeTimerManagerPtr createDefaultScaledRangeTimerManager( + Event::Dispatcher& dispatcher, + const Event::ScaledTimerTypeMapConstSharedPtr& timer_minimums) const { + return OverloadManagerImpl::createScaledRangeTimerManager(dispatcher, timer_minimums); } }; @@ -236,7 +246,7 @@ TEST_F(OverloadManagerImplTest, CallbackOnlyFiresWhenStateChanges) { timer_cb_(); EXPECT_FALSE(is_active); EXPECT_THAT(action_state, AllOf(Property(&OverloadActionState::isSaturated, false), - Property(&OverloadActionState::value, 0))); + Property(&OverloadActionState::value, UnitFloat::min()))); EXPECT_EQ(0, cb_count); EXPECT_EQ(0, active_gauge.value()); EXPECT_EQ(0, scale_percent_gauge.value()); @@ -282,7 +292,7 @@ TEST_F(OverloadManagerImplTest, CallbackOnlyFiresWhenStateChanges) { timer_cb_(); EXPECT_FALSE(is_active); EXPECT_THAT(action_state, AllOf(Property(&OverloadActionState::isSaturated, false), - Property(&OverloadActionState::value, 0))); + Property(&OverloadActionState::value, UnitFloat::min()))); EXPECT_EQ(2, cb_count); EXPECT_EQ(30, pressure_gauge2.value()); @@ -302,7 +312,7 @@ TEST_F(OverloadManagerImplTest, CallbackOnlyFiresWhenStateChanges) { timer_cb_(); EXPECT_FALSE(is_active); EXPECT_THAT(action_state, AllOf(Property(&OverloadActionState::isSaturated, false), - Property(&OverloadActionState::value, 0))); + Property(&OverloadActionState::value, UnitFloat::min()))); EXPECT_EQ(4, cb_count); EXPECT_EQ(41, pressure_gauge1.value()); EXPECT_EQ(42, pressure_gauge2.value()); @@ -327,7 +337,7 @@ TEST_F(OverloadManagerImplTest, ScaledTrigger) { timer_cb_(); EXPECT_THAT(action_state, AllOf(Property(&OverloadActionState::isSaturated, false), - Property(&OverloadActionState::value, 0))); + Property(&OverloadActionState::value, UnitFloat::min()))); EXPECT_EQ(0, active_gauge.value()); EXPECT_EQ(0, scale_percent_gauge.value()); @@ -336,7 +346,7 @@ TEST_F(OverloadManagerImplTest, ScaledTrigger) { factory3_.monitor_->setPressure(0.65); timer_cb_(); - EXPECT_EQ(action_state.value(), 0.5 /* = 0.65 / (0.8 - 0.5) */); + EXPECT_EQ(action_state.value(), UnitFloat(0.5) /* = 0.65 / (0.8 - 0.5) */); EXPECT_EQ(0, active_gauge.value()); EXPECT_EQ(50, scale_percent_gauge.value()); @@ -408,13 +418,13 @@ TEST_F(OverloadManagerImplTest, DelayedUpdatesAreCoalesced) { // When monitor 3 publishes its update, the action won't be visible to the thread-local state factory3_.monitor_->setPressure(0.6); factory3_.monitor_->publishUpdate(); - EXPECT_EQ(action_state.value(), 0.0); + EXPECT_EQ(action_state.value(), UnitFloat::min()); // Now when monitor 4 publishes a larger value, the update from monitor 3 is skipped. EXPECT_FALSE(action_state.isSaturated()); factory4_.monitor_->setPressure(0.65); factory4_.monitor_->publishUpdate(); - EXPECT_EQ(action_state.value(), 0.5 /* = (0.65 - 0.5) / (0.8 - 0.5) */); + EXPECT_EQ(action_state.value(), UnitFloat(0.5) /* = (0.65 - 0.5) / (0.8 - 0.5) */); } TEST_F(OverloadManagerImplTest, FlushesUpdatesEvenWithOneUnresponsive) { @@ -492,96 +502,51 @@ constexpr char kReducedTimeoutsConfig[] = R"YAML( saturation_threshold: 1.0 )YAML"; -TEST_F(OverloadManagerImplTest, AdjustScaleFactor) { - setDispatcherExpectation(); +// These are the timer types according to the reduced timeouts config above. +constexpr std::pair kReducedTimeoutsMinimums[]{ + {TimerType::HttpDownstreamIdleConnectionTimeout, + Event::AbsoluteMinimum(std::chrono::seconds(2))}, + {TimerType::HttpDownstreamIdleStreamTimeout, Event::ScaledMinimum(UnitFloat(0.1))}, +}; +TEST_F(OverloadManagerImplTest, CreateScaledTimerManager) { auto manager(createOverloadManager(kReducedTimeoutsConfig)); - auto* scaled_timer_manager = new Event::MockScaledRangeTimerManager(); + auto* mock_scaled_timer_manager = new Event::MockScaledRangeTimerManager(); + + Event::ScaledTimerTypeMapConstSharedPtr timer_minimums; EXPECT_CALL(*manager, createScaledRangeTimerManager) - .WillOnce(Return(ByMove(Event::ScaledRangeTimerManagerPtr{scaled_timer_manager}))); + .WillOnce( + DoAll(SaveArg<1>(&timer_minimums), + Return(ByMove(Event::ScaledRangeTimerManagerPtr{mock_scaled_timer_manager})))); - manager->start(); + Event::MockDispatcher mock_dispatcher; + auto scaled_timer_manager = manager->scaledTimerFactory()(mock_dispatcher); - // The scaled trigger has range [0.5, 1.0] so 0.6 should map to a scale value of 0.2, which means - // a timer scale factor of 0.8 (1 - 0.2). - EXPECT_CALL(*scaled_timer_manager, setScaleFactor(DoubleNear(0.8, 0.00001))); - factory1_.monitor_->setPressure(0.6); - - timer_cb_(); + EXPECT_EQ(scaled_timer_manager.get(), mock_scaled_timer_manager); + EXPECT_THAT(timer_minimums, Pointee(UnorderedElementsAreArray(kReducedTimeoutsMinimums))); } -TEST_F(OverloadManagerImplTest, CreateUnscaledScaledTimer) { +TEST_F(OverloadManagerImplTest, AdjustScaleFactor) { setDispatcherExpectation(); auto manager(createOverloadManager(kReducedTimeoutsConfig)); - auto* scaled_timer_manager = new Event::MockScaledRangeTimerManager(); + auto* mock_scaled_timer_manager = new Event::MockScaledRangeTimerManager(); EXPECT_CALL(*manager, createScaledRangeTimerManager) - .WillOnce(Return(ByMove(Event::ScaledRangeTimerManagerPtr{scaled_timer_manager}))); - manager->start(); + .WillOnce(Return(ByMove(Event::ScaledRangeTimerManagerPtr{mock_scaled_timer_manager}))); - auto* mock_scaled_timer = new Event::MockTimer(); - MockFunction mock_callback; - EXPECT_CALL(*scaled_timer_manager, createTimer_) - .WillOnce([&](Event::ScaledTimerMinimum minimum, auto) { - // Since this timer was created with the timer type "unscaled", it should use the same value - // for the min and max. Test that by checking an arbitrary value. - EXPECT_EQ(minimum.computeMinimum(std::chrono::seconds(55)), std::chrono::seconds(55)); - return mock_scaled_timer; - }); - - auto timer = manager->getThreadLocalOverloadState().createScaledTimer( - OverloadTimerType::UnscaledRealTimerForTest, mock_callback.AsStdFunction()); - - EXPECT_CALL(*mock_scaled_timer, enableTimer(std::chrono::milliseconds(5 * 1000), _)); - timer->enableTimer(std::chrono::seconds(5)); -} + Event::MockDispatcher mock_dispatcher; + auto scaled_timer_manager = manager->scaledTimerFactory()(mock_dispatcher); -TEST_F(OverloadManagerImplTest, CreateScaledTimerWithAbsoluteMinimum) { - setDispatcherExpectation(); - auto manager(createOverloadManager(kReducedTimeoutsConfig)); - - auto* scaled_timer_manager = new Event::MockScaledRangeTimerManager(); - EXPECT_CALL(*manager, createScaledRangeTimerManager) - .WillOnce(Return(ByMove(Event::ScaledRangeTimerManagerPtr{scaled_timer_manager}))); manager->start(); - auto* mock_scaled_timer = new Event::MockTimer(); - MockFunction mock_callback; - EXPECT_CALL(*scaled_timer_manager, createTimer_) - .WillOnce([&](Event::ScaledTimerMinimum minimum, auto) { - // This timer was created with an absolute minimum. Test that by checking an arbitrary - // value. - EXPECT_EQ(minimum.computeMinimum(std::chrono::seconds(55)), std::chrono::seconds(2)); - return mock_scaled_timer; - }); - - auto timer = manager->getThreadLocalOverloadState().createScaledTimer( - OverloadTimerType::HttpDownstreamIdleConnectionTimeout, mock_callback.AsStdFunction()); - EXPECT_EQ(timer.get(), mock_scaled_timer); -} - -TEST_F(OverloadManagerImplTest, CreateScaledTimerWithProvidedMinimum) { - setDispatcherExpectation(); - auto manager(createOverloadManager(kReducedTimeoutsConfig)); - - auto* scaled_timer_manager = new Event::MockScaledRangeTimerManager(); - EXPECT_CALL(*manager, createScaledRangeTimerManager) - .WillOnce(Return(ByMove(Event::ScaledRangeTimerManagerPtr{scaled_timer_manager}))); - manager->start(); + EXPECT_CALL(mock_dispatcher, post).WillOnce(InvokeArgument<0>()); + // The scaled trigger has range [0.5, 1.0] so 0.6 should map to a scale value of 0.2, which means + // a timer scale factor of 0.8 (1 - 0.2). + EXPECT_CALL(*mock_scaled_timer_manager, + setScaleFactor(Property(&UnitFloat::value, FloatNear(0.8, 0.00001)))); + factory1_.monitor_->setPressure(0.6); - auto* mock_scaled_timer = new Event::MockTimer(); - MockFunction mock_callback; - EXPECT_CALL(*scaled_timer_manager, createTimer_) - .WillOnce([&](Event::ScaledTimerMinimum minimum, auto) { - // This timer was created with an absolute minimum. Test that by checking an arbitrary - // value. - EXPECT_EQ(minimum.computeMinimum(std::chrono::seconds(55)), std::chrono::seconds(3)); - return mock_scaled_timer; - }); - - auto timer = manager->getThreadLocalOverloadState().createScaledTimer( - Event::AbsoluteMinimum(std::chrono::seconds(3)), mock_callback.AsStdFunction()); - EXPECT_EQ(timer.get(), mock_scaled_timer); + timer_cb_(); } TEST_F(OverloadManagerImplTest, DuplicateResourceMonitor) { diff --git a/test/server/server_test.cc b/test/server/server_test.cc index 10cc3a019291..8695c0d0255d 100644 --- a/test/server/server_test.cc +++ b/test/server/server_test.cc @@ -362,6 +362,23 @@ TEST_P(ServerInstanceImplTest, ProxyVersionOveridesFromBootstrap) { server_thread->join(); } +// Validates that the "server.fips_mode" stat indicates the FIPS compliance from the Envoy Build +TEST_P(ServerInstanceImplTest, ValidateFIPSModeStat) { + auto server_thread = + startTestServer("test/server/test_data/server/proxy_version_bootstrap.yaml", true); + + if (VersionInfo::sslFipsCompliant()) { + EXPECT_EQ( + 1L, TestUtility::findGauge(stats_store_, "server.compilation_settings.fips_mode")->value()); + } else { + EXPECT_EQ( + 0L, TestUtility::findGauge(stats_store_, "server.compilation_settings.fips_mode")->value()); + } + + server_->dispatcher().post([&] { server_->shutdown(); }); + server_thread->join(); +} + TEST_P(ServerInstanceImplTest, EmptyShutdownLifecycleNotifications) { auto server_thread = startTestServer("test/server/test_data/server/node_bootstrap.yaml", false); server_->dispatcher().post([&] { server_->shutdown(); }); @@ -1320,13 +1337,20 @@ TEST_P(ServerInstanceImplTest, WithBootstrapExtensions) { return std::make_unique(); })); EXPECT_CALL(mock_factory, name()).WillRepeatedly(Return("envoy_test.bootstrap.foo")); + EXPECT_CALL(mock_factory, createBootstrapExtension(_, _)) - .WillOnce(Invoke([](const Protobuf::Message& config, Configuration::ServerFactoryContext&) { - const auto* proto = dynamic_cast(&config); - EXPECT_NE(nullptr, proto); - EXPECT_EQ(proto->a(), "foo"); - return std::make_unique(); - })); + .WillOnce( + Invoke([](const Protobuf::Message& config, Configuration::ServerFactoryContext& ctx) { + const auto* proto = dynamic_cast(&config); + EXPECT_NE(nullptr, proto); + EXPECT_EQ(proto->a(), "foo"); + auto mock_extension = std::make_unique(); + EXPECT_CALL(*mock_extension, onServerInitialized()).WillOnce(Invoke([&ctx]() { + // call to cluster manager, to make sure it is not nullptr. + ctx.clusterManager().clusters(); + })); + return mock_extension; + })); Registry::InjectFactory registered_factory( mock_factory); @@ -1363,7 +1387,7 @@ TEST_P(ServerInstanceImplTest, WithUnknownBootstrapExtensions) { #ifndef WIN32 class SafeFatalAction : public Configuration::FatalAction { public: - void run(const ScopeTrackedObject* /*current_object*/) override { + void run(absl::Span /*tracked_objects*/) override { std::cerr << "Called SafeFatalAction" << std::endl; } @@ -1372,7 +1396,7 @@ class SafeFatalAction : public Configuration::FatalAction { class UnsafeFatalAction : public Configuration::FatalAction { public: - void run(const ScopeTrackedObject* /*current_object*/) override { + void run(absl::Span /*tracked_objects*/) override { std::cerr << "Called UnsafeFatalAction" << std::endl; } diff --git a/tools/api/generate_go_protobuf.py b/tools/api/generate_go_protobuf.py index 5b25de2dbb0a..23ef195c84cd 100755 --- a/tools/api/generate_go_protobuf.py +++ b/tools/api/generate_go_protobuf.py @@ -16,7 +16,7 @@ IMPORT_BASE = 'github.com/envoyproxy/go-control-plane' OUTPUT_BASE = 'build_go' REPO_BASE = 'go-control-plane' -BRANCH = 'master' +BRANCH = 'main' MIRROR_MSG = 'Mirrored from envoyproxy/envoy @ ' USER_NAME = 'go-control-plane(Azure Pipelines)' USER_EMAIL = 'go-control-plane@users.noreply.github.com' diff --git a/tools/code_format/check_format.py b/tools/code_format/check_format.py index f6ff2c9d7365..71645f4cbfd0 100755 --- a/tools/code_format/check_format.py +++ b/tools/code_format/check_format.py @@ -546,6 +546,7 @@ def reportError(message): # If we hit the end of this release note block block, check the prior line. if not endsWithPeriod(prior_line): reportError("The following release note does not end with a '.'\n %s" % prior_line) + prior_line = '' elif prior_line: prior_line += line diff --git a/tools/git/last_github_commit.sh b/tools/git/last_github_commit.sh index 9746d259ac3b..2ca2fb0fda29 100755 --- a/tools/git/last_github_commit.sh +++ b/tools/git/last_github_commit.sh @@ -1,8 +1,8 @@ #!/bin/bash -# Looking back from HEAD, find the first commit that was merged onto master by GitHub. This is +# Looking back from HEAD, find the first commit that was merged onto main by GitHub. This is # likely the last non-local change on a given branch. There may be some exceptions for this -# heuristic, e.g. when patches are manually merged for security fixes on master, but this is very +# heuristic, e.g. when patches are manually merged for security fixes on main, but this is very # rare. git rev-list --no-merges --committer="GitHub " --max-count=1 HEAD diff --git a/tools/proto_format/proto_format.sh b/tools/proto_format/proto_format.sh index def0d2a6195b..116f546bb102 100755 --- a/tools/proto_format/proto_format.sh +++ b/tools/proto_format/proto_format.sh @@ -75,3 +75,4 @@ cp -f bazel-bin/tools/type_whisperer/BUILD.api_build_file api/BUILD # Misc. manual copies to keep generated_api_shadow/ in sync with api/. cp -f ./api/bazel/*.bzl ./api/bazel/BUILD ./generated_api_shadow/bazel +cp -f ./api/BUILD ./generated_api_shadow/ diff --git a/tools/proto_format/proto_sync.py b/tools/proto_format/proto_sync.py index f6fc70a13a66..adbaa011257d 100755 --- a/tools/proto_format/proto_sync.py +++ b/tools/proto_format/proto_sync.py @@ -433,7 +433,7 @@ def Sync(api_root, mode, labels, shadow): if deleted_files: print('The following files will be deleted: %s' % sorted(deleted_files)) print( - 'If this is not intended, please see https://github.com/envoyproxy/envoy/blob/master/api/STYLE.md#adding-an-extension-configuration-to-the-api.' + 'If this is not intended, please see https://github.com/envoyproxy/envoy/blob/main/api/STYLE.md#adding-an-extension-configuration-to-the-api.' ) if input('Delete files? [yN] ').strip().lower() == 'y': subprocess.run(['patch', '-p1'], input=diff, cwd=str(api_root_path.resolve())) diff --git a/tools/spelling/spelling_dictionary.txt b/tools/spelling/spelling_dictionary.txt index 8d87a26db725..ecf2e82a8c2a 100644 --- a/tools/spelling/spelling_dictionary.txt +++ b/tools/spelling/spelling_dictionary.txt @@ -35,6 +35,7 @@ STATNAME SkyWalking TIDs ceil +CCM CHACHA CHLO CHMOD @@ -369,6 +370,7 @@ absl accesslog accessor accessors +ackless acks acls addr @@ -700,6 +702,7 @@ inlined inlining inobservability inotify +inserter instantiation instantiations interpretable @@ -823,6 +826,7 @@ noncopyable nonresponsive noop nop +npos nthreads ntohl ntop @@ -870,6 +874,7 @@ passphrase passthrough pathname pausable +pausedness pcall pcap pclose @@ -999,6 +1004,7 @@ resolvers responder restarter resync +ret retransmitting retriable retriggers

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ʉhb>O|{);p4dk^wn|Uw=4pS?5Ki3et$gW4(2FB0RjKQAuyQqA*r{MZsIl=0vE;8^V2jNY1bK_-8D^FRqjB&*Y&Tsgujg zdx!SI0zo^Jk3AQo%~k|j7H3yB^e=fg&n}OYc1rm1Zkpz`kA9u2W@db{?{m01e@l}8 z=baIb?TMm;tD(;;$irjsj_PH&W`*dzw0g5kTcxViFi6E;rZ=XxQn=@M&Ag1*YL;$a zJF~w))JO9X?iD7#)!U^5db_IpKjXr;Pd+6OLWz1w6OfL3u|O;BP%_@2SIHR;#9qjGs36Zhrd!1dPpnfB}}OEK~e zum(k6bJQYPM7{8Mc$Im==b5nes4>b$X(|pgM1#W-h2Dik$n5zeMR0SKuQSZLOzP6K z13xS#Rj5#Zrb(#F4M zRLzUJJAdVkx6PDzzTQthK+j{Fs-T;{eU^u$P0KiwdwAq~(S+>oDagPZ2jYNKltj4~ z%T!QX2Xq{(J@4E-+Jmph2`}%B-sKVpaqa&pb-4pp-rw(V64(|=y0pc2Of8U8;~!bF zD#nri=*p^jN$Ba)v$UZz9j!?w!oVI5SBNcgN4~!8hQE~ZimkC5w z3CxC9oeBAi$e{qH9K;AOX)&FRC+Ga(q0LU`l%thfTl82q@!|Pk$yAE~B41A4{;>OM%bbMdw9js93P2LwsxEM{x1-U*^qSI%@=VbvIu z)HQmBO+i}mJg;A!+0zm-p~9cI-lsW!k}r!lB3^NR!dp)py+8MXKu8*vJnq&meK=J_ z+*NpgGVA5%X_O+_HkOpPs&s;%>t#B@N4;{LV1oxr-QK*t^1jCd_QqY>jbYCm*B)EF zE^#+2d4&Z9zW%aiwtly@qPV%T06hV^mujzM(Vti3Xd65!2oi@obPGGZx&2@>!qkf5 z;5QWG?$-bT93!)49Gp+_bl~_IAc_MUI|?+azY_lRO1M4rCB`y7IxzDX2K}8U@V)aV zZjJJkv;8sQ{t^7AelsvHPuT6Tp#a~!sFWjY9%bl<|IG!kDV5P_EENFNveEc@{yOp% zgGkZY$+*ak9#dX~lq(!P6P6c<*!PlpOJ3SwdXTi5R9Q98V-_#;I*u2FdEMs~Yho=P z5JZ1FMdGYp={=U8fhf}i{Ww+NgQ3u;g=}C=zvh(?A6XeI#9uhbUn#HYQ@1f^WSbKn zIo{a)Np^Z@mLxD|w&{)D00V<)XZJ#e2^|I<4F zweC5|=xO6=rA5=x#?!AxC20;muV8)bjUNDyE;m@k2iF^I(tMhseMwFVy>gU3b%Mtm z%d6<1b-oJ&>Wu2viQ^9chL9BTEwKKs)Z8kBZBuhEl}CXG^3?lQieYHjsl%rGj`^l2 z_cVpyv95ljTJY71v6%tuJuT)x+JLIiU_?H3YPq98ng8jPH1vnkJYsSPLTGy^){m3A z>pE=Ru`?A7-sqv)KYtW`zs>I=4Kv8x(G)PVJNwQJz1lVfzs5qM67uKSOEQG`3|kkApY~e_eGEb zOhQL9K>!+ENe?u7Zpl-l@&AM6woa{kQi0vGr}AmcpN-SUPs`D_upH7c9OtPJIOm$NN?Y2X;DRD`Y>i_vX#t!7^~Nn zMg?mh$)iFVsHH&H|VfYn+kL}XV%g2)Tw2ISs9j~E^=2jVukh|s8hz<%EYH$ z6(C1zpV<}nuUSHs%rBJ;VBxTQ!UvJRDrhU*PT2{bCMnsjo*3l@d5w{|CY@%KeFHKDA6pWn;HcWpc-(AbDq|*qMG(|4FYh#Yi?n3Q23!h4uKf@BlIJpyZ7%_xylG zDIS|$HHAeOeFxLJQ&-U&dX1RrPqs2NbLwYfk7c>9<=SI#X8!#AYMMpj`LcJvKwjDJ zd5d#~T8^qj2g2FmnT1>s&2A4eA$)oovHmcW2Od%zoQW%qXUSO6M~z)12vs>M2u`ln znL}UAWjnH^ve#o{(^GQ%>?%ZGN9j>Lly}WphZE06Kd7yiybrllm7yqX_Bhb{MF1AE zIb{i~Uq+&lkD2`xg%9#R6c4kfQQsYVF@-&i(Y%jwKgioY^1au8NxTsPC$`i~gtb9? zKihq}FxcQJtAD77+S&5w+SYe$v3p@yuX=VlA?bF4qpNAg%pMZ^8Y{ECD$j1ntz{Y zANxOZY}i~a#aL{;E=Se4dz7Eyt1t}Jn)tGJ$I7asEw1(ZLc_*jKT5KS|1FvNXS^t| z)(NV;Sucj?BE6*sNCUCSYaVk>cHvI3YRkiseNNF;a5@HQ@idG>40Ew!gnl!_`!F!5 z>70#(P1aYj|2yABR7(tqt@g*?s|%*=5VNX>qULe8J{F?DE%2U#fN`W&tB72H`7$} zI=#g5l37V*Tv>0A>XR6noT+AS#LqZ08eLreY`#><>V=Zz$kGw;&B~!$*1%^|!jJxs1z%aY@yMw?{YyL;r&}Uir#U z$9DRqpKtkW`|6>;7u%Pvf*&~M5|3fwugvN&6INtu1_8sje(zQu=ozI^MPb0o<|x_R zhLXWDmje;K+rn5&?%t-i;m-B45ePFS8Zx?UX3L(54{B z{t0r}pfl-quOxXamEC{!8Fbb5?QksrD{Q9ahT@bV9ezg7^`V%B&#DETB(cdtCV8dX zwOX1K|8RpC=SgX;nplJ!}-%&hnC$0d~q-pfpH?UkTOE%4Vi zWKFi8N)Li2ErLcYgkp@5&Gm}4=aea$@3?GGu0};xaW={_rMhV8^yX!~pWJyETPiea z()ve-MV>Tv;D})x{{>6^DIj+8TGlDzkVD-rh3-mXaJ^2)VfHcKP_Wp{TRf#gm-K}3 z{zPNBFHKMTRpLRjmtUsSCyTte5{^B)r}cFU#H!!A3&9rxsrS#OXn$hlEo1-g9 zl%r9?Rj!X~Yz!?GQ0mt+UMl|Xyi$i~lUvHXi=C+C7LyEK(D=!oz#_z9(;p@|QE9dm zf%9d}(q8>YD&HW3N=))ZkkjvhqpZd>-RyHRW@fq59txw}HWs>lr=LHE%SJ9%_4I7q zOR8_Gx(lWg*R`x{b9lwDl5YFL_&-BSD~()BFn(WO&LI;n2P=P6Kr80s9gr$4X+c<> zl(;UK{cLs@Q|*dCu$y=s4!JrgwnUUI>+z_gDi}$L!Qx6d zDK1J2;r!t>kOVj|x)hz&(%0*49N0^CNp+$NDhWJ| zZp=ra5XmCFj=sb0HHX4r;ay1L*U5)>dY;5KIj<8(t-H4yHDKX}1At8({zZO33pJqK z50EFTE3~iiPUmGkaD>G8it6fYkh|~1w?>WVrG|+7r8M022X!^2;B_X)DlRv)+g5yNTfQYM~DR(y0(WmYs zgcX~}mXKJfn7A1yJK6_?JpE#(nv*Fl$enG9U%X&F_t}sdc{JTPpflN}(bQ@F_G=y! zA@x`|sT|`8nG6(Zw^EsF!NB$^|93*Io}sSw&v~~hF6o(6i+v43rO4}n-P{{G2oPL# zbvHs5w8h-O($-$w1-N&yM88pK6C}4af+e?)IN#{Z>G0b*GcsI2N(SkBwvFhcs2l7} z_qh{&5LNjj7npM@E7$dl_&wEpWps9&Shsbx!1*;E4cBofB`^{1chRsduK(226GF8xoJs%~0i*vme+*Qi`;#=ol$`R=KEMvwA_*U|GIX;VHU4c*TT&%`LYYn==9kZCo7 z)?~a$_@RvC!&W>D|o<_waw@N)P-`bU%j_-8Nl~;V9|2~STt1s*(IYDFjx zKXzOdte>#P>x(aWbr7Qd!F$jb4Va<*ceF*WZw&2QzNPE>*ZW+oXqQ2@Bk|?+pSP`# zc@WnwM?|4%P=T9J8QIdP+)fd6=j!TMekqoh?fjGbzUj+&G4vFM!)4xR& zsS6rfma(!qxM@w~;2!_z1b@^seEayqBcy&itk!;A|M!GZ-L@6s5fi@74<329=3iqe zL{bd%O{K)w_9U#^S1Ol0_HNB{Xv=%YJ~nR6?@*P|g9S%a=?aYK$R?tlx&#s8H*`o> zqDyOdz;Wp-pT~Z%&AC+7V^?1&(?mHga^vihGGf}^sAZlq#h<8F9?*auXF0!+A!)`B*)oebyx{=->=PhQ%%XG}3oEBik8#72wmHcq zTp_#EG56C~)8%bBR+wYsrs12{{`R+NVS&GWf6tnCszh=?E6ch&smN)!X-mCM*4LRme@SQ^|!C#=hz;9Z8 zpO-~oafZ-!f0(JC11j`lHl~mnaa}{(5vGxqcB6b-aQwb41b_VDF|($Kp~Ao?xT71eo^B^Y%)BFf9RBH@-AiIczysPv!B z;F0W*y=~<*FopQJt6TcVFWYPm+id)svnRRPAa*z zZen*XYG->|0!|fF--~5p?l3#O<6(JT84pY+m^)PXtu!5xmVIzrPG2|2R>29aAbh^oIl5K>&itD zqXnUHVaR;d24aKh)wICz?&WSQMP4A*!dLrUlf>Rx^eHp9ebN20bB2#rVm|)#Y{(l_ zy-Zbg*5tHVoCk8WTgZCe&J{;_frnu#9|Jwl=laQYMzvIO8E-Xu7~WTq@!GO}o{fh^ zp#Uu@2#0!L6upP{ZYWW9r`R(!Wt3Yj1Cn^TCAM zq8tOq%N27nZ8Cc_2sreCpZODx%COXRL@KD{IymI$qm!GllCv9ek^!AeTmnzF7Qn_DBta)C0fB68F$eWPUC1Y7<_pv+K2 z7ha)qx!Voz2}=)=AHXbwTKL7{7Lh+&lC%fI4mvCyile$35X+Pd|K6H9pxt0jMi}rj z;qTIlXQ@l-F=LR>+>;FTq-oTk%MLG@i(8Rkxr3dgd&w`3RQKoelJG;F%vH%bYXXp`O6--hesb$^@l!i1FK8OM+jL4E*ZO0H^|zB5z1izpxZ(6NYmJZS z`&ClHW-~d6GuN~;MCystpVeodHh-(@ns;TqiavrJj8BAT=c-~jwUQmaTr=O9Ufqoh zu3wkm)LDrt8GK;qjI{S;yuVh7(R51gbxNq0Jl@2}*3#~acJ6FBA%6mEB5Lgp^FaKf zSiW#Ub~eJ(nkK_@!b{%w(XD1+&KT3Y>)fvLP?T#oLLzz#U*W81Uy>p;hmLLjTXCkY z>iQf0mMnS6zD6P>q6-Je)|t71;S2HzFnx%lx8o-91P@Mdx>k)blan5(l@d;>w$O0D z_=-7#$2zr&%W&Y%)mEEVi5bjuKTt{H&d??Np7iwzt3um6sXbmxtVK7&-T7GOch0;u zF|OuTtmQy)S)93L*iQ~}<(Kj7oQ!{7FFS&rfU`@r?|N6RuVZhEsc@n>2--NGj%dGBk0Ai5D8+rJ-9 zk*1!&7=yR22J>#Qe|)tkWW)>cGp=@qdDm>_1ie`QmkC-F2GrP&yzDLG-*|1W^OB1n zbcnY?r-#bhaHy#pnNr(qIp?k_MYMjJdDvaNI`ZnhO1u!F-FR$cW0HBPRCHHCd)+ zTcU8G^%Lkf1gp|FR$(WKiF#0_A#cc7s=(qZY*!L@P7`#J<{npGZyMJ_ zxN3Nj(!mY7$|mv)peD8MiOU zPxR%4Mbq2wykEu4dY6ye19L9sgt3Khez2+s?V78Xml#L`31_gLTd`MLj1)7Uj7Y1h zUe9g21QQ=9#uO};RHx_rO+)Q@I_nbDg0b9WvR`btdo<3IcETB19Yb<~V&|z^Y8mA+ zdcSo6BGPmNr=2n2E>9Z%yh6!<9G@EH7mDOGy&9RUoFHN`-N`DxWAZ5A)O0Cd>r9-~ zu6txE(?YgkxOod)Omij|>~s--)CsAVbErk|as-A2C}_(r z*FV!kZO(YzUZ>?=tWu^R7!QOX&S~lFGV|7);4rnEVA7r006{L*?e*UM&K}B%C84C- z{Y#wYoc9Y38DG8YWWE3EYW2z0z{#0ynedrx&bw1bJ1qy_ZFq%?=X%n2QPwB+o)*LhqHwVY&qzL6YG13h;0zJ=-395sneAQ}31!?xw z%+S#&bU+#aG%%_|<|ox(P$3M!FFbV!CWjVo9G2Tta>eBmk!QG+JHfK zhhKGej8?iLh09yTO{ILLILvGgNL7}NKJ_dAe7|ZldNSjGI=MK`wP~Isq#_|ku<}5f zp~0+grXsl+O;PyB74hss$~lt2zWsAzKHtQAwUq6!k>LGzrMuCQ?xS%HZGkKF$YQa`vn%Uij-qRk^UwUq@< z^zh?b1DC7J8V*_G&hq*XhYE}wgFlrngB;S5Uz^q?CgCx$`?4dp={4cK{91bSg6sUx zWX-gueyt&WtDG#yGzo9=&;LM;MJK4Q=c!!$o1quhhvI`M=#m1|P}lDbwG=i`i)5bz zIZpjjkdiglNd2zRAj+ma;9bpC{ch_?j3=@=>3q*&7#6y1Dyw%M?x{yae1&@-REFm4 zQEh^Y6#wPtN|1uImY2GFBIKaMkG=68=@R$21wNXzXV>O3#k%YL|B`hbN3h*I%s!IS zR>!}d#Au0Z>ePLhLnSH&JAA6h)Th(d6$g%g3usFgoHGz+G{@DcEoaZ)eg%fQRj&4u z1eSTxM}ovtwf>}=7EE3jI((>Q`NbdWctMBjF!$RUzxQnN8ZkHPiGAq{9}yeS3z z;n7FGDwoTzd=Eb-R7L0e>b;Cko)f?f>cyPI)B8JSwm4j0UdOdrshi`LKc_8%Sm!*E zB&;0t3U4mZn2lFYHa}eahi$d1V**~Pj`I> zO8?-c%-g3t9hjYZXhyO}oW^heWdiE|Dud#KF2c5{(wfl9MDu{bO^$<> zi2@)P2o%bK#9weZXKGsJ_fN&iGsCKM&&#t*ezk0UMg3Xcvb@pWLIgAz;_Mc1kF87H0E9;y)x7 z+HFz*ILkp-@b<6Skh8o6j4H&B<|lwUh63+Ad;5Py+jpQka&cH#BK}vbP?{*5w~d#9 z=1~I_l}cC#NCT+sKYRT2DuND*+j5(*{B4Jy0W@3umah>&yFWq0gxUYf0Y61Efx2?+ zsK$Wy|90v6N5&EAFI}}16i@PEHzD7rn|D;QxuBmCrVL^?`-z7P2@fUN6rL)F@5@rD<*#Mot&Y=F(pBM$ZWt(B~=k;>Tq zVyhL+sK@fkgNN=6M4P*(iR5@at9~7E$2oO06jicgzm#A24sZ2|i?#b_ssR0{1-`)uI^i4@*=*<+_%lKfyt{mt{mOZp87wne)G2F$3P@i}HI+@+;MoZ9KY zYWd|e#11B`kRDO~i+#{+sC&elsZYLX9VK9p-tExa-Og)|96$YFE-r0Yzl{X7I*p*w%9FwU? zS(G^ioh@6is~ZSzTVTuzb`doxs&ix(lpC6ueo&3`N^-3y>P3~?Edw3;w!uNHHZcdP zF%;1{3if@+y#1>54D4z?Je$d_*WRmnJyipFkaN61;o^Dy+&3#(!>R&Qe?`QC0ri`R z&6JKVq3nVsDVvqHGU z`Mn)VD~r{SB?dzk9Lyi8>Lt{t?NW0N8M-y=mqT4K`ok9(g6hXeUNQ#M0%x1V(<9e-W3PKT&fty~L$o@OMED$1!{Wj+?O zS3X?UU+1{{QXwrTb?!IouAvlLqO`mlp?4v;-S-`aF=3JtSnu_9PbXeo4^9K~i} z30x02m__q(N${j!J?v=U_V#K@(Gk=Y_R(9lnbyrvrL%j!kv;~FR^Z>H_~*59w4gDp zrLBqw>fql=K;usCQfwo1!b%!oW@;YGTSkaA!ZPmZzO+TA3&0xQ`IwCJaNkSQ#STG!CeM`=P}SBQSv4TLI4~c{d9u20NN%mqRPVS zjn*R6P=(Is9zcI6tl2O47(K7KeeN=_8>}y|2}YNkOmKCG#%qQ`k@i zxNrZ3g?0pJpXrrtinPYx;93X`h)zc38i{upWB>#O%(|)RJ;6ImA?vDyW7KK}OB_2Dt z8_SPyzBCFm($orS!YIKRN{ym^?nCVm*)qF1Tn!dkxKtB^*tQ#x4?n1eC3e5!eXd3xwG zSax6A!_ay3geT^f7M=AR0A&-1KfEUvqzdIyX4rR_VAeDF9Ff;*(Q7@m2N~BYJj0TL`v!I5T#*g7-9eg zrD2qYfdNEvVCWnM?iqda_r2@>ao3ISuJ!tdi*wHGcw#@j_tv_&TL51>w6W|K-Pz)M z`W61!*bjSts6hcq3*^ld`dRaJW6~Z!%R*!Zw<^yb7agD4W3^Ao5kpEZAuUBaTViec zxm$ZEa)!mSV9r@1SW`o#2JRNcLviApCAQ~oo?P7}VmzFZOF8ltnr@EM?zT{KsZ$*jFpS6W~rJ-G#W-VcQ-BQZ<#67GzWMy$S z_@fSv1}StLfz&P=+MOA`BWYxM^I*5s-Hz`K`>GqS&16lixaR^4NASL3kEqe_<3q!! zq0+>&3aTMKkB{%9G382^Wl^p0xovf0AlGrQ+gE6frS;Tc z+&`tvrc>D~RxPQ+1t|$(o7)Dzd2#F?i;g2n*9hYku_O?e?9FMN_hza#RUZ9wx z^F46XN>Mev^*m2OB-gBNNAHMkE~-aXD}ma8b0Y_~W*Q0+D- zI_wFXEjcQ3Lm*G~zy~dSc6Z@WpaxaC`=fvhBRC;oAGjA3kM+BGcm=7B1mk@nevg1U?H!a}I?ZR9rUi4JDIJdPm{hcUp4!6ULybI5s1$UfviZ!=WqF)#H1JS%(b_CRmo2WH)c?-BLmq7;uT(GFl6>N&WDNZeISV9SN0 z&|x~Kmj-%cp0#lvQK}$SCNm4ojqWZj6>wj&M;g7nq5-ihO+KxbrcPIA$wf!7*r)c` z5R4lukNdgE>T;pxZ~+qB7^R4jbkO@t$HK4*ArDTIwWC^M6p5{fPmcc_&)mkon|YWC z@05A7UTvmLZr~;Xp;z<|)PoX7X~Ev~wbn=?_6!F%E`Mxl%3*3MNdv831?#so5N{K# zqUK$?uI@rZx4~&C%=)JM)UfI4xv4P*>#&ep@LeMj|4{fv$Dx@uu$)4m_^05O0Vr1y ztLrH1UXt58y2BHla$1gkFbC98Eu~-*k$mYVv7t0c|KJdZ zC27emd1ib>DRG_1;5U+i=?gu+=^x&1_HF5 zLgQICHBLF>BDNEZIgie$iRy5P+mRiD8BB&}FUFG4$tMwZj9bX_0X?bD_ z3K?aVH#;3AFTeT3y?BSbUp0!|WZ!DYwws@MtnP#c^OdLSmlf-KPa$ef{zRJH0XNq> z67=1AOBffb*{IrWk`vo&jR7^AMXlt_bLZpPh8{1Vm0vR2`jsQ))^`yL<%Qs=X7%viaD z%`WkH@#P9s|EZzzle{nzE0P4MVji2R=km27w+OiIv^jW@oSen1>b#8dlH~~#=6aaK z=XDM#uWzg{VgQM}hz4@ukp?r7swJYSGr=xK9Q&(acJ85!tixo7qc{bvAKB!6t7u}? zf>yD~C}%%br8O4zIf-uz4A;2VKG46q`nX-wVRA6Whii(c+>#Zy56YSFT51&o=f2!k z24$AA^a*x!PhITuzj~b-8NC%x`EW1eM|0V2AS;Xg9-l}M!5yakoKYeuzW(}9Mxlv^ zIpsqHHkMyTz}=lzTeftYK&_RX!tC24>nDQs&Dtqbk||T!l<#K1=caAWG7`EzR(T2B zrIOn+~@_fS7^6AdcBJwsB5E5h;04R1A7u8&uN;T6J-~B{_6` zf5P*nz>1~MP+EGuvY5b>L{i-qvk$=eT(L6E>N}(8M?HFV$b_835uK!AinKxu**(pU zZlcLtR_@7wJH{fu5581B0$0YdnI`0}59-sI$Sd!lUgq!KlW+@>;mc^X%si5 zf~p@VuBaNYh*V`15J27=UL=NC$b9l*opkDrr6-O{Nl~A`xoiY}V!#Ov2q#@Zut*tO z_1u~VZ_Cq#IezYh`V*$Tf%_!@YHsW4AO8NE{}Ma-$U_Qge2tWy_DCj;Dxs;&R$CL} z`Vhy{l^N)K0V2zJEDYY&%(LjR@2yj6*s9^ZhJLX=vR&e|ne(EHK*15HAv2NtbkhR< zEjClFw;!43=uDBrmRKom&nXwsC|0#Ht-71qy39Ia2H|=c6m;v&kWeLe={fX#b57r@ zh6RN%F@?#NwGR6Gz=+{3*!ce?{(I2FJGf#b6tbnX2(HA z<)_}HnAyRnX<&ar`uJ-#11tP7DSI*n@QC71KJ|RdCr8}|8FRH{YOzhFg5Xk92OBD_ zp8U8%k|Jc+q`_yctim6Bl^D%0V)j5*XJ900=TZ*`ytCJKtwb?ZAvrs>_Qzw^<*_Qe zCnUEV1Vy;_SqJYTVwqVF7}Fk{+|<^q>aTj&`xc=s<`R;a?-WWTGW+1uO>sxH(D4n$_^(v#fBJ+YySNN5(3|wTOqXo~r48F$&yR z&cP0%-wEJ4B{?A$QGK1)ZL(>!v zG&TEWXrD@flz`|)L?MUip9ScztnlHZn^>l*UooLq`qxn?OZ#G*RNh`}+{NBL&(&Gb z{j~-7jm3_JRDL?Ql(?o(ZT33K|1Yb~a{C@yOR_eNwP|oRoPT`^wz93Xe}w38VM50TqTX$HJ|Xj-k~wq) zh99;fuWM_>>;(smroJ)62^O&`bV>$pXc6}z3W(g5m2q@#Qc=z~$_*c_8>?lc`<4a1 zl362=oc{o6Vk*<;1&n;2Y2YUD-(qpVBE4aV568uIzxw6a3g59;Z$oxWN4}^<+OkMr z9R+)~5!)sBJ110mXsy;SsG|Dp%V8g%7g|n%`kPCi#TU2!A1vxRQ}=(hsH^q}me9SN ztPmfUGC+FD^eooR|CMy@z>1Bk>ubV?GW{=*T_~;I0rd=#euqV!oPcVjx=+U5?Qo@5 zeN*dhonILI=^Wg|I|uDjo!q}g;vDDKj@tCcHut$R5Biy>&SLBX^g)BpgfBqO(L(~) zc#1%}DulDMr34xD+;G92dP$J-<=r2>du(!}r?oYwwZxdNI?5+x?a zw`5Q=Z*OxzGiKyF{W8+9s~B?l^;656GdF( zE)5}qfLC-%cF24uBWM`4%TI+=x000VINxBf4nf+267=F{2p%7|rlr)G|BssIgn?Ve>b13XiV5nH>Py7aS3SKZjjF&6 z&Do&UY6r8PtE`N;w^;L9JPigY@p2 z8xB-lTViiQciGx|Hu4V?mJYEOIak+2Q-9bIsbPV{cN=W--#VXvs`CLOC3ZaDr_#Pv zY3?i(ZxU(f41Ha46(ee*?E3naH*YcfE`dbt*H89m^w}?T6p04XKlcyj1le+Vkr$X? zSXvu0L5B$p^kTEdGz>&sVSi@%MJ(e{O*K@avJdXL8XW1f$cX;oI@E) zG?J%qqvbihC#9!DTvVApl8+i*9b6^bhSu4aq?~J~bj~NS$|h@grSA4syKF}*}70v~Z(4Ld@lju;bRvWNuPY23p?2>x~%||IwLc&)Yb!9!jh`jgI+PkUb zoG_rfu&UxXy1%Ds)O`Zds;Vl;5F;UZz3yt-^Zu>7^DcZXe@kmQrdUi})+RAkW}d!! z;?8J8B`5}(9rS?kf}93$%5MvUuL45fXCB%d611Yj;om6sHZ;SNHF>njX>Jy3cj~HH zzLyhxiI$}Gy4YN%w(OVN(rVSz#GRHFy8F1xwpXm_L~f#iB>5VpSm%`Hqo=b3WNhwB z)%O_(y> z14?GE^Ugvyxj`&{F5|mY0I8Kh>}CwVBXM^9tM(g1ZXCsK5jU^C7M(K`hny&PXhVD-xG3nk%09o+@bm8XMBO8 z&etCT6w#j)rhKQ;yL3rU#HBLW-LNJ{0Ciqjy%lMf5erv))GrppXd<3-QkqwX_6MSIKm?9S!q+=oP8Cw_tlow)`?X@Fv2nl{fO~AqaBOq zt}MU)k$#aV*p#qvi{ZRNUc+Mmb{7jXqd&Ec{TyDb)nyp`+86u60B@tAIX(b3TKGNS z?~bwH9>-ss`S4*Ze2&%hWf~|3-^1P8cyEmY$r1--9PVo35&PezbN`qtsG{MOd$nAZ z27eO&M&_%4fPnFhC(*^ff%H)X0RLvk^C!So(;FXvzazgzB!Pa*#{(#^27r>=-Ru62 zr^t)P_`LS}T17Ve##L+J*Qpbco!);T)&cO2Z2H-gU)JnhJU_HM#6QPlt^kiY=Q+^$ zAGrUZ2jl^sH#Jp?;AeQ_Iezqlwb(9v9w7CjB@v{ms*0GH_@1C%O4Oe=*CanMB?wK^ ze;EFr-5BWNHjZ-b?a?hj~`KHv zACA9c*#Cb%PEIf^G(!WG+Dggd^b144|9549j+Gs>%~XD}ln_3$oQCBs#;34jKRyM* zqZjK2wnv_l2VVZ8tl=&vp7E%Vcfx)_hfj4Cj6Kl>QUbuWdN-D5ZM9zk*%+wj>mSX3 zpgZplkXGlwg!cX{9&gXX)5B>RGz|RU$<-UAv(T73W;cF^1fYi?o*t>t_#nJe`1pnz zNHctfWOD<2Fr$SJ7IE#)qyFgpw!ITDr6$o*{-R&z!7d5#6P&qY5dP5_c;F%r`1$3h z`5Z2Vr@(Y(cj!;P$^rTlS94QY0Lm|4T*dR%XQ57(KPd4(t-YAh3WQPa!8tbmoD0)A z;pZQbJ#DLOWPCEe*Oo-%5zC+8{R}U!N~sRmTJ;_N+_-y|;=ueuZ*MOP9B1A&Z}+P& z9N?UBMFr!heOb9Tzx?-FfWMUu{0yzWc#og-QhR6hPk1il;khVmmyD6J64|D ztF8H@-hOQo0pGpf-fNZcQpw*>C*)sZ5eC8^o9{$g6SIwly|rmfe&E;Y)*f)j;LjNX z;)>~90RCNa^lzJ2^m-Or2!ZylkKnlqjGM>&NlAMFJXg^G2cZ4p zstBUw$L@_s0lVH>N1`D~>oi9 zj}dqCF^_4gPAuCa%~W3}V9!87vlF^Z zK&!~mFr836Lo71TI&r|?&%I-+I^&MJ7RtFXJ=s3-qcE@rjbwA@tDfD7xflrxZ=0S@ z;R&vHPCK+7_pD5~+D=Kg`Y_h(d_^~I=&n;OiTLew=k_Tm0S0Wo0Ji2`va|e>_!yE{ zzk_NYdF^NpNsQ8*H3Q?CLZhTx46uUP`lt(%?C17KzJ926J=F8Z6V z8s6)@bTfSE=Rr88yZL*}+6(r-CuVJd{NEO{20vD!QTWpYQF;srZOUk zaLNsG|77=;Vhdw8P&a$oK~e92F@CDQg2@+i^cR<^Sg?FeJK~W$Syh%%{>;sH!L)r@ zl~Xq+MCyK38M{RGCA7Wt050KfOwP}TQSJ|)h294$*yNHhH7$Oz=RZ{`}J^6GUN zv3J!4Z@MuG(YGEGo{4PQZR)=o_l*!AxoZ ztJhsBNy5eoonJ{6Ck5Yan|LWP!lO_F<`Hn2EoyeGwGdo24FYy`?j>6|Nruf8ZDI~R zps65-grja{KEYd?f^{LP%(kT$Mx39i4Jj1&JT{*o&)yO$y|@oVWwB^3CznX1*i-qv zhLMu`$S)O|Iy_k_dAtR7>`qP;`aXHFEu9V8ROpYkloL<&jR&~6^EYk_z-p>^tvF49 zGY?}a6LL%u4KzG~pND74CzF)gBorg#XZ&o=9fA$4`os{g`Prq6mxt)}GW$7h&jg;o z0AB5s=P)lWW51wq1`4n4%UK75%*zF|2;(&^PVC=&(LpM_15_z)1`3-vb9g zxyC1{>3UJL=Eq@F%L4j$nD-0d8Imf(Sv6hrt8TTQHcNtOC{Q`rH1f4^?cuBG24TB4 ziYgg`%p7){v)Y=Xj$k?1A2ee55CgZ2F@}21g*4c zxVx@;^R6(uA|&Zsb=vmJbVjswxBVnYW$z;SYv-&b7B8geOWQnq&*jGjg{&eK{^bOa zL|ud1BF&K<@j{n)ty@hw9m02*m5C=Vm%T38o>X7>tj(2Suc3FwtaT623YU6PYQPeW zaS}v#xI0X}D0hRw3zC?&Zy(tneWxW>%v5mK27#RDZ#mTTzTGv27F~|wb|SOe_{zAG zTP^zZzx}q|@s`r;1TFG?-vNKB8TUry8O`E=Kp0PH!R<9t!ZJaM#v`zXVtlk05pZ5b zYSwd#jnvd5QH(dk^4%AWfvA=oF;Scm@!60GV$W{5Y0rm7FayBY*KfYS|I_06W%t}| z0L+Ca>DXr$9x!5XEaEwVqbhYr#QW@>Lo(ZZ_6$?py@)S zf-IWXY0o9-y>$us)Ve``O5cIk&p*epo0H(ZT!TYen_w$ee$Q&p%%#q_m-3o#^G=IKvbQaht zfuID>wtsq-+hKzk6`3@=-@xSZZo~CqwxD>TPNj>!BxY91tbeU|#bsv0wc-|hcp5jc z;d+mu;I_>!gezh2Uc!}SxrSLr0);K!D;phJdKqh7DjQ6eoisM$Foj~wxj^mh3C z0_IFfI9aS`@O#-B?w(~PiOGa_RRKY;U@2IB$=mGA^l{XWrK9QEJ>Wc-sb@9CbP6>e z-hkF>2{No&B?qEh8oGz1j2bux^x(`sja(kPJd9#vu}iY$SNZo#;F@|=pEXUXFp3KX zZP}(1Y-$2SBHYdvDarzcjIixz1s`T-%E%n~LvK14Os>iFZ>+8T20yO>>f6UY+Uj1kJ>Ox5wl=7E@y6)+rvZ`=kqL zx5PMEv}8G$a&C>SHtZm?n5mDev6BR$*HBt5$03vZJ%^3Z-AwU3Z#(e*Cjz1?Ob=e2 z@!#WQg4>p5)bO4oAgY%1N4PB7zBQH8(Z6Ib-5a8u6t}ic17kF?6%(#z+t+sPGGtbK zXuiA!b@3?CM=Tfzm7M@fz}f6$ZXBw=N|aw+v4g!joqnR4%W*b@#syTCM^aXlWspT% zghJF2;~br)6OF&{1!eBrbWQYUY!3~gcpk0*)tm$K`c2}Xkpj%2C#ui3YPw%BAhJh* zxg|E9mm5rv%DdKH+D}!+kW%=;YheJzXtw-%BHY+TgCX79n{&|HWMbl$5C4ZsnaDyf z;W*z4-m*+hu`(EZSpxM9+!6~M#c2KlS36npLWTkPHT!UNdaW-hzsXrGYC*9IwerHc#plQqp3*)+F3zo) zA5!9c++6e?(YE>muH9)K%d5>E)(4rh%NC@Nmh_1t>BaC1j0;#(Pmso9yF{(M4@PI# zgx6}qLto6xL%Ds_4Ix0;khdQS<3FG#rExHAIkx;L7v(W#`@Yt;?;s1Fkkr4Dmf!57 zQAAzRC7k5e`5{ifburb=v198;Y*>_4{j0SA4L&b%)g1M9QmgUY$+75=>%*)T`<-v- zt0wn+&8bSi%g~^By)Z%CddFr$b`$WyZ2eK*lp}KCwPUYk!EN8&vB`hTYu8bUuAxFdvM->w zJ}gf6q#otCokrH%nyrUh6CZ;+-?0UF*I%B!i0IWEBq!XWD0Bx z=e%SX)6%$FAaSqp)(6)>Slr40WQoGV)k9{)=2T5Zx>(MN1q&2#KdY@&+k4k>G!6f0 zKzN#cYw3&T0&bxe0CFO9E4;2w-%}fkn1J{R4&I#ZuCIbwtoAR~&;^vw zCQArrTU=bz)6_u!?Aa^Ie*$!7sJCQk%Nd+nH zDd?i*KWH~hvR-p}?nQhXdGcMl?b1ycs5O4VOzBh!n?s+MH9OZ7^pTpw{!SYb4arOo z<2`1qKPclHL&v%iz!o!oo?Ofnd1pmfy0I6QPKICu#s(O5qUhB|E!0(I3Xpu3e&<5C z;kJy<5rw6t2t@>>Gbe8eB&>gg3mAXLl5 z3Jq(kUB5Ne=bywX(fhV`l$1%8@p|6j%McN3Q#}+3PDt@V#x45|3exQCG^I4p>mM#B zV8w3WoW6UTI^<0rHpUy?KVg={rDq<*MEUIcXU!WYLZ%MaK_L2p!#Z`x&4eVgr)6up zLSVhbEXjLw{l`PXLbaali5h)y0j^+`-kN+UYT4%@Or}N*aSKn0wucYgy+oYHZvvG0 zDnt+n!w%=$*4qxy9*#P94uPW(W2$1p8tzC1*e!v)GR4y4u<-RN+NEs}H^Rx!=f3ByAOg$#KST4;8j)dn2(? zTLmB4QSaIqYkHJ<{=OV0^Y~?ZmpYya-(?UHL6GvyJOKbltUY+(jwDO9Bqkv8b~q^>1?I5G9>&Yd*?G#cFXe1op5aHxKY zobT@AOk-HnvdffeZLI)^F8vcr*mEO>M%R-N-YaJsU*8x%N};fW;~U2IV;A`$?Bp@DWWJh3iMD@ zc}0PIbx4}p*jGN7&Ny>|b$=mKyCG-0eR$k{66S~=##LE=f>drK3`8QZ?~K1NxIht= zA%1aR%50TQV?*cqdpfxgJ+DHKxYGogdpM8SX@&DEe8D%P7*p>}i?jReT{cJiWf>nI zbnP_TJ$5`zAJRBsn(OD5v`|c(mgWl^t&ym70FRVTXI3@)fjNzll4d9>^Zm}s6als2 zY`GO9FYyenvi?8;q4kh0!pA?DyDrG5{9`OxZEH|T6sn;YTfEtD_9G&P2=#sKoi3%6X0|O z_8#-T?k$1R{+dN+WskW$ZtQ%CMuvotFoSUd2T9|?QCe``wv&?p$iy~M%0h$9c!afQ zvA1*EA{&YFP>#COYi+Q4|iP_H4dd<8YRzi?Q;S)@KF8c}G- z&ptSs4wJJ?AL51sFi1$ljBqv1rGAnmnAEs=cB70evh_;DuJVmUEg%#U@HOcKSr9H- zr9N=rdHon(t#~*RE((Io+VY=vMCE>m*mc69R9B3ACALR1Qtwe;eu`eLDJ*r75txPa zEA4Ob=x%;Y?}fJ9jVr@sC7CPtFjFz}9~P-QIitE?`=t(U!Kd`$v&-WtTRZY8?t@?B z`Raw$jgIvOv)oRDLX>rdEL5($HLwViOD{8v2fC3%^5toYekq~%e;oSkwA^Qh zwn$w?3E3&^wgKBQ2fr%8G1VoL9yX6S;68Vdyv@9G{pi%8(_2-3MhPdQhH(%1FvVH8 z?PH@zo7QCVdte`Ezy_Lsu_BT1KNA*>P-whK;Ke4;!0qMX>v_ z$MNy?9A`8PGj8chP>(kK?xFaO5#lxgW7p@OT zXYuWWJl^F=lMeM-3;jcm`r2y?Yu+1q+$V>nS`w>N z#Z97^&yQGaA}rRlv`2XQ<5SWE;MM9bFoBnf;tu<+=Bq~%TdD$8TkuWgY_DyyrToGD zhE1Arh@6tav_{V{kBrmYn1K4kJ^f2y{j9!53-?Cvr#<7<5iO@TvecGZ(8Y(n$l&+{ z!Bc$?-4JKAn;viT@%zh|k%p7u6kJ=rK*6;M;XXOjL_|vBbOygrzO=9tBaSXLwJ8_| zb$uheyW6a!?`Gobc{mc4f+W+uZ))T_I>sKLBFd4U<*IivYs>{!b8R6c;t}4*nu)$d z1R>{ug;3xnMF*gOA_xIFSYt~p^P5? zpiS9yYP!U2_0-4`%ibbcr>(Hjqs0j{r@*_QT<7D*Ryc89Ev~iJ6Iq-azl;0wSfEv* zUBitM39Czx{rM`a@He9<<*~z9;9S6>%MBnUMR(-!bYjLmfJTyFMx4zUItY|&N|^^0 zmzHj-Jbc--u(5Z1yt5URhFtjQ-j;gW8=9n@cl0HxYVSu(iiG#R;Wxq!=2rcm#z6!? zD5T|FyfR=Rx!wWX;t=H;7JyP24}kI`{r-Bx`1I-~wv3k1W3Cf%nR38;_sX$e=#?0E zd29#Y%5k>y=8Eb~j!UpG9<3g%+KeJM?`{*6FWVMyShdpiIL$~o)f?DUz&h+NuFVHN7ThAIE%A!pvulIJX@+?|( z5BrTf+q7;~>AQY3hloSa>XM87y?m!CJ>M?z%pQST`g3$4`CEx#{-uQal;&#FC&v3z zQ~l0RucKxqPYhU@`mGBeJ@Y*$mBdroF4L$Dq1>;Y(>Msu5N15PW=o!1>S$BSI3mEI z+kfn%!sw@bQOZYq4~6idPb-hwsI3Nj8oTU!iK#QFJc;0=uZbQ?h|F+5>{X)iGB4gv zA)i7}ssbYPjQ@9FF@+71B;Z$>J@pk71BUVij(~_R`eQa3L{+T9?_y>h`pT;&s?!y4 zaiz&zo)Rvk1&OF>8r}i#t7>@{i*|xN_DcmIs@4q$R-ejFB!`u$6BBZ=UDW178|rDs z$b*pHli6dNg5!tk$@&ciHs8x)V@~Vlw7=^oZ7;i`P@WGv@<@6Xv;~d#G*N{Rz$%Kp zEZO`XaQsKapA6PX^-!O%k(`=>3#;SzCpzZUBl@TVcsy;v-8~IC5ExWFr|I^uoz}de7`=+F4m|9k{T4 zb~I)GU}Y#FAyYP;RDRiRIZ+6YJ5IuU&g;tTe#-(c}tR-$X-=(h^Tfa`yv)8a)TTSVftRfrO6Rb*%H`g)Fs|>Y;k#nm@BmhiCsUJ^fjS?h1=GH1@* z=#_u`Py_EZg(gYnABIT3xP1(8mASu2k))`qdcm9NP(X$zOw)z*d|$!E)@<; z2hae36L!WOr2vdO3#GyT0C-DOEe*s|u;@Z6KwrFr;zg46;*j#I2e*J(1z85%{?!#= zYH9+_pNab1z&E0M^=f19%mzFG=%Q6$;J2>uel7s!s5;kAi-QV4_Hch;+8O^xz``N< zc2V}{tngC-eB-@$jz@R#XcYmbvvzjIgA4c&2s8f;D@rH@0j3$wm$<*;QYsJEudo}NZ(HYfkFR97h3=jq?UNT z-8q6wx0v8Ok7=(x{B2N?0-$+7_i`H^=$SLX1PEU(LV=F2pi=Q*VxpHq1T(1kEz@RuGw3rvUn3<59snc#xwe{}^!L)ihi{3L5` z15K(m0Yd1nr8f{?xf2M`_loT^g5L(6d572OocZL;c&eTUro(W?D9sZ*m1iJk<;0sV z!Ze4abDu}_`tc+U?<%d;WW?P!U#{FAfIRKkK`zP(o3&TFbQQFWp|?l5jp9BHXQ)YL zBRjm?*+-ka46}}pa2~R4Tcd*(voVKjLnu+0ofB_HHAfp8S!W;NCnvv^jfs~4XNL`w z5C1{JV1R@#?i^jl11oX^kGJfx>r!(}A+Q=Qp@kD$xy6Ml@f;Hm{i9gdbgmoYq?$Ps;!Ux0s&1aP=kSD*L< z09%L+z?CT}ws)QZhylCM=Wf&@TDKPH3%$6?H>Z@6D|zJ%#hIj68uzJyp7#WVTWOJL z03MrMhCF{~;$0R^?^! zCa=5*2{!g5_QF#6ELl*BSou9SK5%DhJKrMOVz;y8TSt!Z+EF?-4q9KdyLpyYDA%!j zK||$6bQ-ofYlRifZ?ZWe(a5^&<1O1xx*oTAzNgZ|Ls`XRMll5&H>a-asS+DZ1p#eL znzav^x}6~VW$$0ea}hl}u9Rpjxm6>7M7o-21;mo=JyP9DczJvJrqE9N-Q-)lJu006 zGO=>hC4Es@kWQ`$rtsX z8&Quv3@+)SC@fixD{O+9JzDl~>FNq-u)rjjDz}SAv5WZWtDGb?9M>=V%gvW^;P`8? z7+aeHU*>%7JK%{F@w$0w!6BRez(d-tXMKdCd616qwW(-EsHFK~1#wMTDI)NKi^unf z{Wv?h`Pgow6rt+dhN-Ai-=bm#Nl#&f-SOR~C}}ZR1SamePt=&E_OeKb?fOFPGGrmU zU$V;GAZe$E&;v9M1n8^hd>TE|h880^AUU4T@=FYjlC+~R>GhOe7CMSqX`W4Grj(Jq z<91!kZBe~dx%GvLN2E*fR;Hf$mD>#K;KI}F#?yGl-Q~2E*?C@_`LelryIu<m;q4OF9f#0#yYb8wbv%4 zwzf{YmQJN4U?;<*Sp&^dZj(lF9#f4L?O$@8v;cxmx_DG0Dm=`UI-CJMOTw-Q98988 z#4DXV%chdK@*qz`X3uZUM6abdV9~nvt}POp9%h5v)=9^r7RRMBnW>Xp^hHJpVQM?X zo7tvq+=+2FSBriUZP5T-j*Bn*2evZRkmtsIC7*Kd?gnsY;_}1!&~Yo@yhO>~T|=oD zWLZS5#|8=Sb=S|6rnsmoW0ofK3;W4(>5H8dT=sg0s(KtX)AWFqXJ(&4w&IDZD!yqriFte}Bu2XOu)mM%tGz0edONqiW>ZBAK=`*Z8 zc$(>W_+XN8u8Cx0ZL72$3)*c_Dn--1(W1NZr>6Y-6AXu*bK*3(I9ME4ODZ7(CCM@c zFJZ$`C(Og*u=j(a8vT5tbDd7(zaghXeJ4YzQ9eQ|)x%Y5<`pM~-+lBRJ3 z7Y@E8uQ@nfR?E!&5AI0xqeiz8UM4bP3yqq4T#B1~4HR+#!Faf)N{(Sn&cmH#M z;3XNm#`=%V9=L-q{w_g((_Ft;{@DXSWt~lX4|pyA@cl0e_%A~NEiVDi4$lkq_}?-* ze}~|&w#nbVJU9y|g9|pHvHvjC-?Zv)%kj4{@4N$az$@kFSO5O{H}w3);P^XYE&z7e z^>Pk{e?&82mtFqb-2N~$UI8NDoRF;iKjGuAR>bcOeogY)Gr)HEn0M#x-(UYW#9s`o zzY+CcqWvcg{w3PqnfTw8_8<5l`gf)Mhlu%CJpPAX_*bX>hcbWnuTJ~B#qqBp@^^fk z`PUHn*AV$-$o^~c|A%`0OW^SDU;Z^j{*I3;|Gyg|{%r~`%0!_Xd*!KT!+@N?e}D%B zp&y*f_1;sASp4CaWId7TT__>C+hztjbgEZ&*MFzN5%gbL071{TC-36>d`0EQLyT41 zGK*0sCYe`J3_?{evj54$y!I6Mbt3EX#_Xs%%))TpCz*5B*1sY5+JC6@pOYh_^&coj zZm}49;KG$kPnqO|e3O|QlIDw%Qk_}NdKrbl`IhF_x#Rz7TIYyYFGJ#-IfClsBbByH z2AsnmWo*^G&SNthKQQ}=r44De50 MR^@T&qZhCL2Mx5a8~^|S literal 0 HcmV?d00001 diff --git a/source/docs/file.png b/source/docs/file.png new file mode 100644 index 0000000000000000000000000000000000000000..3e535e3a3f7b772bc1edd564eb85808ab0983859 GIT binary patch literal 190636 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 z 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 supported_filter_names = { ... @@ -22,7 +22,7 @@ NetworkFilterNames::get().ExtAuthorization, NetworkFilterNames::get().TheNewFilt # Add a new WriteFilter into Generic Writefilter Fuzzer ## Step 1. Make sure the filter can be linked into the fuzzer -For WriteFilter, the config of the filter must be added into the `deps` field of `network_writefilter_fuzz_test` module in the file [BUILD](https://github.com/envoyproxy/envoy/blob/master/test/extensions/filters/network/common/fuzz/BUILD). +For WriteFilter, the config of the filter must be added into the `deps` field of `network_writefilter_fuzz_test` module in the file [BUILD](https://github.com/envoyproxy/envoy/blob/main/test/extensions/filters/network/common/fuzz/BUILD). ``` envoy_cc_fuzz_test( name = "network_writefilter_fuzz_test", @@ -43,7 +43,7 @@ envoy_cc_fuzz_test( ) ``` ## Step 2. Add the filter name into supported_filter_names -In [uber_per_writefilter.cc](https://github.com/envoyproxy/envoy/blob/master/test/extensions/filters/network/common/fuzz/uber_per_writefilter.cc), add the filter name into the vector `supported_filter_names` in method `UberWriteFilterFuzzer::filterNames()`. +In [uber_per_writefilter.cc](https://github.com/envoyproxy/envoy/blob/main/test/extensions/filters/network/common/fuzz/uber_per_writefilter.cc), add the filter name into the vector `supported_filter_names` in method `UberWriteFilterFuzzer::filterNames()`. ``` const std::vector supported_filter_names = { ... @@ -54,8 +54,8 @@ const std::vector supported_filter_names = { # Add test cases into corpus Good test cases can provide good examples for fuzzers to find more paths in the code, increase the coverage and help find bugs more efficiently. -Each test case is a file under the folder [network_readfilter_corpus](https://github.com/envoyproxy/envoy/blob/master/test/extensions/filters/network/common/fuzz/network_readfilter_corpus) or [network_writefilter_corpus](https://github.com/envoyproxy/envoy/blob/master/test/extensions/filters/network/common/fuzz/network_writefilter_corpus). It consists of two parts: `config` and `actions`. -`config` is the protobuf to instantiate a filter, and `actions` are sequences of actions to take in order to test the filter. +Each test case is a file under the folder [network_readfilter_corpus](https://github.com/envoyproxy/envoy/blob/main/test/extensions/filters/network/common/fuzz/network_readfilter_corpus) or [network_writefilter_corpus](https://github.com/envoyproxy/envoy/blob/main/test/extensions/filters/network/common/fuzz/network_writefilter_corpus). It consists of two parts: `config` and `actions`. +`config` is the protobuf to instantiate a filter, and `actions` are sequences of actions to take in order to test the filter. An example for testing MongoProxy filter: ``` config { @@ -80,14 +80,14 @@ actions { } } ``` -* `config.name` is the name of the filter. -* `config.typed_config.type_url` is the type url of the filter config API. +* `config.name` is the name of the filter. +* `config.typed_config.type_url` is the type url of the filter config API. * `config.typed_config.value` is the serialized string of the config protobuf, and in C++ we can call`config.SerializeAsString()` to obtain this. This string may contain special characters. Recommend using octal or hexadecimal sequence for the string. * `actions.on_data.data` (or `actions.on_write.data`) is the buffer parameter `data`(in string format) for testing ReadFilter's method onData() (or for testing WriteFilter's method onWrite()). This string may contain special characters. Recommend using octal or hexadecimal sequence for the string. * `actions.on_data.end_stream` (or `actions.on_write.end_stream`) is the bool parameter `end_stream` for testing ReadFilter's method onData() (or for testing WriteFilter's method onWrite()). * `actions.on_new_connection` is an action to call `onNewConnection` method of a ReadFilter. * `actions.advance_time.milliseconds` is the duration in milliseconds for the simulatedSystemTime to advance by. -For more details, see the APIs for [ReadFilter Fuzz Testcase](https://github.com/envoyproxy/envoy/blob/master/test/extensions/filters/network/common/fuzz/network_readfilter_fuzz.proto) and [WriteFilter Fuzz Testcase](https://github.com/envoyproxy/envoy/blob/master/test/extensions/filters/network/common/fuzz/network_writefilter_fuzz.proto). +For more details, see the APIs for [ReadFilter Fuzz Testcase](https://github.com/envoyproxy/envoy/blob/main/test/extensions/filters/network/common/fuzz/network_readfilter_fuzz.proto) and [WriteFilter Fuzz Testcase](https://github.com/envoyproxy/envoy/blob/main/test/extensions/filters/network/common/fuzz/network_writefilter_fuzz.proto). ## Convert a unit test case to a fuzz test case manually This section explains an approach to generate a corpus from unit tests. It is an optional step for users who want to generate the highest possible coverage. @@ -105,12 +105,12 @@ static std::string toOct(const std::string& source, const std::string& info) { } ``` In the unit test code, we temporarily add a function(finally we will remove it) like the above one. -Then we can fill in `config.typed_config.value` with the value returned or printed by -```toOct(config.SerializeAsString(), "config serialized string: ")``` +Then we can fill in `config.typed_config.value` with the value returned or printed by +```toOct(config.SerializeAsString(), "config serialized string: ")``` where `config` is the config protobuf in a unit test case. -We can also fill in `actions.on_data.data` or `actions.on_write.data` with the value returned or printed by -```toOct(buffer.toString(), "buffer:")``` +We can also fill in `actions.on_data.data` or `actions.on_write.data` with the value returned or printed by +```toOct(buffer.toString(), "buffer:")``` where `buffer` is the buffer to pass to `onData()` or `onWrite()` in a unit test case. Please note that the two fuzzers use the "real input" for fuzzers. If you are using a mock decoder and pass an empty buffer to onData(), that test case won't help cover much code in the fuzzers(but the config protobuf is still helpful). diff --git a/source/docs/pr_view.png b/source/docs/pr_view.png new file mode 100644 index 0000000000000000000000000000000000000000..0e3fb8e338aae8112a70678e71b72b1d2193d394 GIT binary patch literal 77503 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